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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body