Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.
   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Question behaviour type for deferred feedback with CBM behaviour.
  19   *
  20   * @package    qbehaviour_deferredcbm
  21   * @copyright  2012 The Open University
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  require_once (__DIR__ . '/../deferredfeedback/behaviourtype.php');
  29  
  30  
  31  /**
  32   * Question behaviour type information for deferred feedback with CBM behaviour.
  33   *
  34   * @copyright  2012 The Open University
  35   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   */
  37  class qbehaviour_deferredcbm_type extends qbehaviour_deferredfeedback_type {
  38      public function adjust_random_guess_score($fraction) {
  39          return question_cbm::adjust_fraction($fraction, question_cbm::default_certainty());
  40      }
  41  
  42      public function summarise_usage(question_usage_by_activity $quba, question_display_options $options) {
  43          global $OUTPUT;
  44          $summarydata = parent::summarise_usage($quba, $options);
  45  
  46          if ($options->marks < question_display_options::MARK_AND_MAX) {
  47              return $summarydata;
  48          }
  49  
  50          // Prepare accumulators to hold the data we are about to collect.
  51          $notansweredcount  = 0;
  52          $notansweredweight = 0;
  53          $attemptcount = array(
  54              question_cbm::HIGH => 0,
  55              question_cbm::MED  => 0,
  56              question_cbm::LOW  => 0,
  57          );
  58          $totalweight = array(
  59              question_cbm::HIGH => 0,
  60              question_cbm::MED  => 0,
  61              question_cbm::LOW  => 0,
  62          );
  63          $totalrawscore = array(
  64              question_cbm::HIGH => 0,
  65              question_cbm::MED  => 0,
  66              question_cbm::LOW  => 0,
  67          );
  68          $totalcbmscore = array(
  69              question_cbm::HIGH => 0,
  70              question_cbm::MED  => 0,
  71              question_cbm::LOW  => 0,
  72          );
  73  
  74          // Loop through the data, and add it to the accumulators.
  75          foreach ($quba->get_attempt_iterator() as $qa) {
  76              if (strpos($qa->get_behaviour_name(), 'cbm') === false || $qa->get_max_mark() < 0.0000005) {
  77                  continue;
  78              }
  79  
  80              $gradedstep = $qa->get_last_step_with_behaviour_var('_rawfraction');
  81  
  82              if (!$gradedstep->has_behaviour_var('_rawfraction')) {
  83                  $notansweredcount  += 1;
  84                  $notansweredweight += $qa->get_max_mark();
  85                  continue;
  86              }
  87  
  88              $certainty = $qa->get_last_behaviour_var('certainty');
  89              if (is_null($certainty) || $certainty == -1) {
  90                  // Certainty -1 has never been used in standard Moodle, but is
  91                  // used in Tony-Gardiner Medwin's patches to mean 'No idea' which
  92                  // we intend to implement: MDL-42077. In the mean time, avoid
  93                  // errors for people who have used TGM's patches.
  94                  $certainty = question_cbm::default_certainty();
  95              }
  96  
  97              $attemptcount[$certainty]  += 1;
  98              $totalweight[$certainty]   += $qa->get_max_mark();
  99              $totalrawscore[$certainty] += $qa->get_max_mark() * $gradedstep->get_behaviour_var('_rawfraction');
 100              $totalcbmscore[$certainty] += $qa->get_mark();
 101          }
 102  
 103          // Hence compute some statistics.
 104          $totalquestions   = $notansweredcount + array_sum($attemptcount);
 105          $grandtotalweight = $notansweredweight + array_sum($totalweight);
 106          $accuracy         = array_sum($totalrawscore) / $grandtotalweight;
 107          $averagecbm       = array_sum($totalcbmscore) / $grandtotalweight;
 108          $cbmbonus         = $this->calculate_bonus($averagecbm, $accuracy);
 109          $accuracyandbonus = $accuracy + $cbmbonus;
 110  
 111          // Add a note to explain the max mark.
 112          $summarydata['qbehaviour_cbm_grade_explanation'] = array(
 113              'title' => '',
 114              'content' => html_writer::tag('i', get_string('cbmgradeexplanation', 'qbehaviour_deferredcbm')) .
 115                      $OUTPUT->help_icon('cbmgrades', 'qbehaviour_deferredcbm'),
 116          );
 117  
 118          // Now we can start generating some of the summary: overall values.
 119          $summarydata['qbehaviour_cbm_entire_quiz_heading'] = array(
 120              'title' => '',
 121              'content' => html_writer::tag('h3',
 122                      get_string('forentirequiz', 'qbehaviour_deferredcbm', $totalquestions),
 123                      array('class' => 'qbehaviour_deferredcbm_summary_heading')),
 124          );
 125          $summarydata['qbehaviour_cbm_entire_quiz_cbm_average'] = array(
 126              'title' => get_string('averagecbmmark', 'qbehaviour_deferredcbm'),
 127              'content' => format_float($averagecbm, $options->markdp),
 128          );
 129          $summarydata['qbehaviour_cbm_entire_quiz_accuracy'] = array(
 130              'title' => get_string('accuracy', 'qbehaviour_deferredcbm'),
 131              'content' => $this->format_probability($accuracy, 1),
 132          );
 133          $summarydata['qbehaviour_cbm_entire_quiz_cbm_bonus'] = array(
 134              'title' => get_string('cbmbonus', 'qbehaviour_deferredcbm'),
 135              'content' => $this->format_probability($cbmbonus, 1),
 136          );
 137          $summarydata['qbehaviour_cbm_entire_quiz_accuracy_and_bonus'] = array(
 138              'title' => get_string('accuracyandbonus', 'qbehaviour_deferredcbm'),
 139              'content' => $this->format_probability($accuracyandbonus, 1),
 140          );
 141  
 142          if ($notansweredcount && array_sum($attemptcount) > 0) {
 143              $totalquestions   = array_sum($attemptcount);
 144              $grandtotalweight = array_sum($totalweight);
 145              $accuracy         = array_sum($totalrawscore) / $grandtotalweight;
 146              $averagecbm       = array_sum($totalcbmscore) / $grandtotalweight;
 147              $cbmbonus         = $this->calculate_bonus($averagecbm, $accuracy);
 148              $accuracyandbonus = $accuracy + $cbmbonus;
 149  
 150              $summarydata['qbehaviour_cbm_answered_quiz_heading'] = array(
 151                  'title' => '',
 152                  'content' => html_writer::tag('h3',
 153                          get_string('foransweredquestions', 'qbehaviour_deferredcbm', $totalquestions),
 154                          array('class' => 'qbehaviour_deferredcbm_summary_heading')),
 155              );
 156              $summarydata['qbehaviour_cbm_answered_quiz_cbm_average'] = array(
 157                  'title' => get_string('averagecbmmark', 'qbehaviour_deferredcbm'),
 158                  'content' => format_float($averagecbm, $options->markdp),
 159              );
 160              $summarydata['qbehaviour_cbm_answered_quiz_accuracy'] = array(
 161                  'title' => get_string('accuracy', 'qbehaviour_deferredcbm'),
 162                  'content' => $this->format_probability($accuracy, 1),
 163              );
 164              $summarydata['qbehaviour_cbm_answered_quiz_cbm_bonus'] = array(
 165                  'title' => get_string('cbmbonus', 'qbehaviour_deferredcbm'),
 166                  'content' => $this->format_probability($cbmbonus, 1),
 167              );
 168              $summarydata['qbehaviour_cbm_answered_quiz_accuracy_and_bonus'] = array(
 169                  'title' => get_string('accuracyandbonus', 'qbehaviour_deferredcbm'),
 170                  'content' => $this->format_probability($accuracyandbonus, 1),
 171              );
 172          }
 173  
 174          // Now per-certainty level values.
 175          $summarydata['qbehaviour_cbm_judgement_heading'] = array(
 176              'title' => '',
 177              'content' => html_writer::tag('h3', get_string('breakdownbycertainty', 'qbehaviour_deferredcbm'),
 178                      array('class' => 'qbehaviour_deferredcbm_summary_heading')),
 179          );
 180  
 181          foreach ($attemptcount as $certainty => $count) {
 182              $key   = 'qbehaviour_cbm_judgement' . $certainty;
 183              $title = question_cbm::get_short_string($certainty);
 184  
 185              if ($count == 0) {
 186                  $summarydata[$key] = array(
 187                      'title' => $title,
 188                      'content' => get_string('noquestions', 'qbehaviour_deferredcbm'),
 189                  );
 190                  continue;
 191              }
 192  
 193              $lowerlimit = question_cbm::optimal_probablility_low($certainty);
 194              $upperlimit = question_cbm::optimal_probablility_high($certainty);
 195              $fraction = $totalrawscore[$certainty] / $totalweight[$certainty];
 196  
 197              $a = new stdClass();
 198              $a->responses = $count;
 199              $a->idealrangelow  = $this->format_probability($lowerlimit);
 200              $a->idealrangehigh = $this->format_probability($upperlimit);
 201              $a->fraction       = html_writer::tag('span', $this->format_probability($fraction),
 202                      array('class' => 'qbehaviour_deferredcbm_actual_percentage'));
 203  
 204              if ($fraction < $lowerlimit - 0.0000005) {
 205                  if ((pow($fraction - $lowerlimit, 2) * $count) > 0.5) { // Rough indicator of significance: t > 1.5 or 1.8.
 206                      $judgement = 'overconfident';
 207                  } else {
 208                      $judgement = 'slightlyoverconfident';
 209                  }
 210              } else if ($fraction > $upperlimit + 0.0000005) {
 211                  if ((pow($fraction - $upperlimit, 2) * $count) > 0.5) {
 212                      $judgement = 'underconfident';
 213                  } else {
 214                      $judgement = 'slightlyunderconfident';
 215                  }
 216              } else {
 217                  $judgement = 'judgementok';
 218              }
 219              $a->judgement = html_writer::tag('span', get_string($judgement, 'qbehaviour_deferredcbm'),
 220                      array('class' => 'qbehaviour_deferredcbm_' . $judgement));
 221  
 222              $summarydata[$key] = array(
 223                  'title' => $title,
 224                  'content' => get_string('judgementsummary', 'qbehaviour_deferredcbm', $a),
 225              );
 226          }
 227  
 228          return $summarydata;
 229      }
 230  
 231      protected function format_probability($probability, $dp = 0) {
 232          return format_float($probability * 100, $dp) . '%';
 233      }
 234  
 235      public function calculate_bonus($total, $accuracy) {
 236          $expectedforaccuracy = max(
 237              $accuracy * question_cbm::adjust_fraction(1, question_cbm::LOW) +
 238                  (1 - $accuracy) * question_cbm::adjust_fraction(0, question_cbm::LOW),
 239              $accuracy * question_cbm::adjust_fraction(1, question_cbm::MED) +
 240                  (1 - $accuracy) * question_cbm::adjust_fraction(0, question_cbm::MED),
 241              $accuracy * question_cbm::adjust_fraction(1, question_cbm::HIGH) +
 242                  (1 - $accuracy) * question_cbm::adjust_fraction(0, question_cbm::HIGH)
 243          );
 244          // The constant 0.1 here is determinted empirically from looking at lots
 245          // for CBM quiz results. See www.ucl.ac.uk/~ucgbarg/tea/IUPS_2013a.pdf.
 246          // It approximately maximises the reliability of accuracy + bonus.
 247          return 0.1 * ($total - $expectedforaccuracy);
 248      }
 249  }