Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 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 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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body