Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 402] [Versions 310 and 403]

   1  <?php
   2  
   3  // This file is part of Moodle - http://moodle.org/
   4  //
   5  // Moodle is free software: you can redistribute it and/or modify
   6  // it under the terms of the GNU General Public License as published by
   7  // the Free Software Foundation, either version 3 of the License, or
   8  // (at your option) any later version.
   9  //
  10  // Moodle is distributed in the hope that it will be useful,
  11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13  // GNU General Public License for more details.
  14  //
  15  // You should have received a copy of the GNU General Public License
  16  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  17  
  18  /**
  19   * Contains logic class and interface for the grading evaluation plugin "Comparison
  20   * with the best assessment".
  21   *
  22   * @package    workshopeval
  23   * @subpackage best
  24   * @copyright  2009 David Mudrak <david.mudrak@gmail.com>
  25   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  26   */
  27  
  28  defined('MOODLE_INTERNAL') || die();
  29  
  30  require_once (__DIR__ . '/../lib.php');  // interface definition
  31  require_once($CFG->libdir . '/gradelib.php');
  32  
  33  /**
  34   * Defines the computation login of the grading evaluation subplugin
  35   */
  36  class workshop_best_evaluation extends workshop_evaluation {
  37  
  38      /** @var workshop the parent workshop instance */
  39      protected $workshop;
  40  
  41      /** @var the recently used settings in this workshop */
  42      protected $settings;
  43  
  44      /**
  45       * Constructor
  46       *
  47       * @param workshop $workshop The workshop api instance
  48       * @return void
  49       */
  50      public function __construct(workshop $workshop) {
  51          global $DB;
  52          $this->workshop = $workshop;
  53          $this->settings = $DB->get_record('workshopeval_best_settings', array('workshopid' => $this->workshop->id));
  54      }
  55  
  56      /**
  57       * Calculates the grades for assessment and updates 'gradinggrade' fields in 'workshop_assessments' table
  58       *
  59       * This function relies on the grading strategy subplugin providing get_assessments_recordset() method.
  60       * {@see self::process_assessments()} for the required structure of the recordset.
  61       *
  62       * @param stdClass $settings       The settings for this round of evaluation
  63       * @param null|int|array $restrict If null, update all reviewers, otherwise update just grades for the given reviewers(s)
  64       *
  65       * @return void
  66       */
  67      public function update_grading_grades(stdclass $settings, $restrict=null) {
  68          global $DB;
  69  
  70          // Remember the recently used settings for this workshop.
  71          if (empty($this->settings)) {
  72              $record = new stdclass();
  73              $record->workshopid = $this->workshop->id;
  74              $record->comparison = $settings->comparison;
  75              $DB->insert_record('workshopeval_best_settings', $record);
  76          } elseif ($this->settings->comparison != $settings->comparison) {
  77              $DB->set_field('workshopeval_best_settings', 'comparison', $settings->comparison,
  78                      array('workshopid' => $this->workshop->id));
  79          }
  80  
  81          // Get the grading strategy instance.
  82          $grader = $this->workshop->grading_strategy_instance();
  83  
  84          // get the information about the assessment dimensions
  85          $diminfo = $grader->get_dimensions_info();
  86  
  87          // fetch a recordset with all assessments to process
  88          $rs         = $grader->get_assessments_recordset($restrict);
  89          $batch      = array();    // will contain a set of all assessments of a single submission
  90          $previous   = null;       // a previous record in the recordset
  91          foreach ($rs as $current) {
  92              if (is_null($previous)) {
  93                  // we are processing the very first record in the recordset
  94                  $previous = $current;
  95              }
  96              if ($current->submissionid == $previous->submissionid) {
  97                  $batch[] = $current;
  98              } else {
  99                  // process all the assessments of a single submission
 100                  $this->process_assessments($batch, $diminfo, $settings);
 101                  // start with a new batch to be processed
 102                  $batch = array($current);
 103                  $previous = $current;
 104              }
 105          }
 106          // do not forget to process the last batch!
 107          $this->process_assessments($batch, $diminfo, $settings);
 108          $rs->close();
 109      }
 110  
 111      /**
 112       * Returns an instance of the form to provide evaluation settings.
 113       *
 114       * @return workshop_best_evaluation_settings_form
 115       */
 116      public function get_settings_form(moodle_url $actionurl=null) {
 117  
 118          $customdata['workshop'] = $this->workshop;
 119          $customdata['current'] = $this->settings;
 120          $attributes = array('class' => 'evalsettingsform best');
 121  
 122          return new workshop_best_evaluation_settings_form($actionurl, $customdata, 'post', '', $attributes);
 123      }
 124  
 125      /**
 126       * Delete all data related to a given workshop module instance
 127       *
 128       * @see workshop_delete_instance()
 129       * @param int $workshopid id of the workshop module instance being deleted
 130       * @return void
 131       */
 132      public static function delete_instance($workshopid) {
 133          global $DB;
 134  
 135          $DB->delete_records('workshopeval_best_settings', array('workshopid' => $workshopid));
 136      }
 137  
 138      ////////////////////////////////////////////////////////////////////////////////
 139      // Internal methods                                                           //
 140      ////////////////////////////////////////////////////////////////////////////////
 141  
 142      /**
 143       * Given a list of all assessments of a single submission, updates the grading grades in database
 144       *
 145       * @param array $assessments of stdclass (->assessmentid ->assessmentweight ->reviewerid ->gradinggrade ->submissionid ->dimensionid ->grade)
 146       * @param array $diminfo of stdclass (->id ->weight ->max ->min)
 147       * @param stdClass grading evaluation settings
 148       * @return void
 149       */
 150      protected function process_assessments(array $assessments, array $diminfo, stdclass $settings) {
 151          global $DB;
 152  
 153          if (empty($assessments)) {
 154              return;
 155          }
 156  
 157          // reindex the passed flat structure to be indexed by assessmentid
 158          $assessments = $this->prepare_data_from_recordset($assessments);
 159  
 160          // normalize the dimension grades to the interval 0 - 100
 161          $assessments = $this->normalize_grades($assessments, $diminfo);
 162  
 163          // get a hypothetical average assessment
 164          $average = $this->average_assessment($assessments);
 165  
 166          // if unable to calculate the average assessment, set the grading grades to null
 167          if (is_null($average)) {
 168              foreach ($assessments as $asid => $assessment) {
 169                  if (!is_null($assessment->gradinggrade)) {
 170                      $DB->set_field('workshop_assessments', 'gradinggrade', null, array('id' => $asid));
 171                  }
 172              }
 173              return;
 174          }
 175  
 176          // calculate variance of dimension grades
 177          $variances = $this->weighted_variance($assessments);
 178          foreach ($variances as $dimid => $variance) {
 179              $diminfo[$dimid]->variance = $variance;
 180          }
 181  
 182          // for every assessment, calculate its distance from the average one
 183          $distances = array();
 184          foreach ($assessments as $asid => $assessment) {
 185              $distances[$asid] = $this->assessments_distance($assessment, $average, $diminfo, $settings);
 186          }
 187  
 188          // identify the best assessments - that is those with the shortest distance from the best assessment
 189          $bestids = array_keys($distances, min($distances));
 190  
 191          // for every assessment, calculate its distance from the nearest best assessment
 192          $distances = array();
 193          foreach ($bestids as $bestid) {
 194              $best = $assessments[$bestid];
 195              foreach ($assessments as $asid => $assessment) {
 196                  $d = $this->assessments_distance($assessment, $best, $diminfo, $settings);
 197                  if (!is_null($d) and (!isset($distances[$asid]) or $d < $distances[$asid])) {
 198                      $distances[$asid] = $d;
 199                  }
 200              }
 201          }
 202  
 203          // calculate the grading grade
 204          foreach ($distances as $asid => $distance) {
 205              $gradinggrade = (100 - $distance);
 206              if ($gradinggrade < 0) {
 207                  $gradinggrade = 0;
 208              }
 209              if ($gradinggrade > 100) {
 210                  $gradinggrade = 100;
 211              }
 212              $grades[$asid] = grade_floatval($gradinggrade);
 213          }
 214  
 215          // if the new grading grade differs from the one stored in database, update it
 216          // we do not use set_field() here because we want to pass $bulk param
 217          foreach ($grades as $assessmentid => $grade) {
 218              if (grade_floats_different($grade, $assessments[$assessmentid]->gradinggrade)) {
 219                  // the value has changed
 220                  $record = new stdclass();
 221                  $record->id = $assessmentid;
 222                  $record->gradinggrade = grade_floatval($grade);
 223                  // do not set timemodified here, it contains the timestamp of when the form was
 224                  // saved by the peer reviewer, not when it was aggregated
 225                  $DB->update_record('workshop_assessments', $record, true);  // bulk operations expected
 226              }
 227          }
 228  
 229          // done. easy, heh? ;-)
 230      }
 231  
 232      /**
 233       * Prepares a structure of assessments and given grades
 234       *
 235       * @param array $assessments batch of recordset items as returned by the grading strategy
 236       * @return array
 237       */
 238      protected function prepare_data_from_recordset($assessments) {
 239          $data = array();    // to be returned
 240          foreach ($assessments as $a) {
 241              $id = $a->assessmentid; // just an abbreviation
 242              if (!isset($data[$id])) {
 243                  $data[$id] = new stdclass();
 244                  $data[$id]->assessmentid = $a->assessmentid;
 245                  $data[$id]->weight       = $a->assessmentweight;
 246                  $data[$id]->reviewerid   = $a->reviewerid;
 247                  $data[$id]->gradinggrade = $a->gradinggrade;
 248                  $data[$id]->submissionid = $a->submissionid;
 249                  $data[$id]->dimgrades    = array();
 250              }
 251              $data[$id]->dimgrades[$a->dimensionid] = $a->grade;
 252          }
 253          return $data;
 254      }
 255  
 256      /**
 257       * Normalizes the dimension grades to the interval 0.00000 - 100.00000
 258       *
 259       * Note: this heavily relies on PHP5 way of handling references in array of stdclasses. Hopefully
 260       * it will not change again soon.
 261       *
 262       * @param array $assessments of stdclass as returned by {@see self::prepare_data_from_recordset()}
 263       * @param array $diminfo of stdclass
 264       * @return array of stdclass with the same structure as $assessments
 265       */
 266      protected function normalize_grades(array $assessments, array $diminfo) {
 267          foreach ($assessments as $asid => $assessment) {
 268              foreach ($assessment->dimgrades as $dimid => $dimgrade) {
 269                  $dimmin = $diminfo[$dimid]->min;
 270                  $dimmax = $diminfo[$dimid]->max;
 271                  if ($dimmin == $dimmax) {
 272                      $assessment->dimgrades[$dimid] = grade_floatval($dimmax);
 273                  } else {
 274                      $assessment->dimgrades[$dimid] = grade_floatval(($dimgrade - $dimmin) / ($dimmax - $dimmin) * 100);
 275                  }
 276              }
 277          }
 278          return $assessments;
 279      }
 280  
 281      /**
 282       * Given a set of a submission's assessments, returns a hypothetical average assessment
 283       *
 284       * The passed structure must be array of assessments objects with ->weight and ->dimgrades properties.
 285       * Returns null if all passed assessments have zero weight as there is nothing to choose
 286       * from then.
 287       *
 288       * @param array $assessments as prepared by {@link self::prepare_data_from_recordset()}
 289       * @return null|stdClass
 290       */
 291      protected function average_assessment(array $assessments) {
 292          $sumdimgrades = array();
 293          foreach ($assessments as $a) {
 294              foreach ($a->dimgrades as $dimid => $dimgrade) {
 295                  if (!isset($sumdimgrades[$dimid])) {
 296                      $sumdimgrades[$dimid] = 0;
 297                  }
 298                  $sumdimgrades[$dimid] += $dimgrade * $a->weight;
 299              }
 300          }
 301  
 302          $sumweights = 0;
 303          foreach ($assessments as $a) {
 304              $sumweights += $a->weight;
 305          }
 306          if ($sumweights == 0) {
 307              // unable to calculate average assessment
 308              return null;
 309          }
 310  
 311          $average = new stdclass();
 312          $average->dimgrades = array();
 313          foreach ($sumdimgrades as $dimid => $sumdimgrade) {
 314              $average->dimgrades[$dimid] = grade_floatval($sumdimgrade / $sumweights);
 315          }
 316          return $average;
 317      }
 318  
 319      /**
 320       * Given a set of a submission's assessments, returns standard deviations of all their dimensions
 321       *
 322       * The passed structure must be array of assessments objects with at least ->weight
 323       * and ->dimgrades properties. This implementation uses weighted incremental algorithm as
 324       * suggested in "D. H. D. West (1979). Communications of the ACM, 22, 9, 532-535:
 325       * Updating Mean and Variance Estimates: An Improved Method"
 326       * {@link http://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Weighted_incremental_algorithm}
 327       *
 328       * @param array $assessments as prepared by {@link self::prepare_data_from_recordset()}
 329       * @return null|array indexed by dimension id
 330       */
 331      protected function weighted_variance(array $assessments) {
 332          $first = reset($assessments);
 333          if (empty($first)) {
 334              return null;
 335          }
 336          $dimids = array_keys($first->dimgrades);
 337          $asids  = array_keys($assessments);
 338          $vars   = array();  // to be returned
 339          foreach ($dimids as $dimid) {
 340              $n = 0;
 341              $s = 0;
 342              $sumweight = 0;
 343              foreach ($asids as $asid) {
 344                  $x = $assessments[$asid]->dimgrades[$dimid];    // value (data point)
 345                  $weight = $assessments[$asid]->weight;          // the values' weight
 346                  if ($weight == 0) {
 347                      continue;
 348                  }
 349                  if ($n == 0) {
 350                      $n = 1;
 351                      $mean = $x;
 352                      $s = 0;
 353                      $sumweight = $weight;
 354                  } else {
 355                      $n++;
 356                      $temp = $weight + $sumweight;
 357                      $q = $x - $mean;
 358                      $r = $q * $weight / $temp;
 359                      $s = $s + $sumweight * $q * $r;
 360                      $mean = $mean + $r;
 361                      $sumweight = $temp;
 362                  }
 363              }
 364              if ($sumweight > 0 and $n > 1) {
 365                  // for the sample: $vars[$dimid] = ($s * $n) / (($n - 1) * $sumweight);
 366                  // for the population:
 367                  $vars[$dimid] = $s / $sumweight;
 368              } else {
 369                  $vars[$dimid] = null;
 370              }
 371          }
 372          return $vars;
 373      }
 374  
 375      /**
 376       * Measures the distance of the assessment from a referential one
 377       *
 378       * The passed data structures must contain ->dimgrades property. The referential
 379       * assessment is supposed to be close to the average assessment. All dimension grades are supposed to be
 380       * normalized to the interval 0 - 100.
 381       * Returned value is rounded to 4 valid decimals to prevent some rounding issues - see the unit test
 382       * for an example.
 383       *
 384       * @param stdClass $assessment the assessment being measured
 385       * @param stdClass $referential assessment
 386       * @param array $diminfo of stdclass(->weight ->min ->max ->variance) indexed by dimension id
 387       * @param stdClass $settings
 388       * @return float|null rounded to 4 valid decimals
 389       */
 390      protected function assessments_distance(stdclass $assessment, stdclass $referential, array $diminfo, stdclass $settings) {
 391          $distance = 0;
 392          $n = 0;
 393          foreach (array_keys($assessment->dimgrades) as $dimid) {
 394              $agrade = $assessment->dimgrades[$dimid];
 395              $rgrade = $referential->dimgrades[$dimid];
 396              $var    = $diminfo[$dimid]->variance;
 397              $weight = $diminfo[$dimid]->weight;
 398              $n     += $weight;
 399  
 400              // variations very close to zero are too sensitive to a small change of data values
 401              $var = max($var, 0.01);
 402  
 403              if ($agrade != $rgrade) {
 404                  $absdelta   = abs($agrade - $rgrade);
 405                  $reldelta   = pow($agrade - $rgrade, 2) / ($settings->comparison * $var);
 406                  $distance  += $absdelta * $reldelta * $weight;
 407              }
 408          }
 409          if ($n > 0) {
 410              // average distance across all dimensions
 411              return round($distance / $n, 4);
 412          } else {
 413              return null;
 414          }
 415      }
 416  }
 417  
 418  
 419  /**
 420   * Represents the settings form for this plugin.
 421   */
 422  class workshop_best_evaluation_settings_form extends workshop_evaluation_settings_form {
 423  
 424      /**
 425       * Defines specific fields for this evaluation method.
 426       */
 427      protected function definition_sub() {
 428          $mform = $this->_form;
 429  
 430          $plugindefaults = get_config('workshopeval_best');
 431          $current = $this->_customdata['current'];
 432  
 433          $options = array();
 434          for ($i = 9; $i >= 1; $i = $i-2) {
 435              $options[$i] = get_string('comparisonlevel' . $i, 'workshopeval_best');
 436          }
 437          $mform->addElement('select', 'comparison', get_string('comparison', 'workshopeval_best'), $options);
 438          $mform->addHelpButton('comparison', 'comparison', 'workshopeval_best');
 439          $mform->setDefault('comparison', $plugindefaults->comparison);
 440  
 441          $this->set_data($current);
 442      }
 443  }