Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]

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