[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/mod/quiz/report/statistics/tests/ -> stats_from_steps_walkthrough_test.php (source)

   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   * Quiz attempt walk through using data from csv file.
  19   *
  20   * The quiz stats below and the question stats found in qstats00.csv were calculated independently in a spreadsheet which is
  21   * available in open document or excel format here :
  22   * https://github.com/jamiepratt/moodle-quiz-tools/tree/master/statsspreadsheet
  23   *
  24   * Similarly the question variant's stats in qstats00.csv are calculated in stats_for_variant_1.xls and stats_for_variant_8.xls
  25   * The calculations in the spreadsheets are the same as for the other question stats but applied just to the attempts where the
  26   * variants appeared.
  27   *
  28   * @package    quiz_statistics
  29   * @category   phpunit
  30   * @copyright  2013 The Open University
  31   * @author     Jamie Pratt <me@jamiep.org>
  32   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  33   */
  34  
  35  defined('MOODLE_INTERNAL') || die();
  36  
  37  global $CFG;
  38  require_once($CFG->dirroot . '/mod/quiz/tests/attempt_walkthrough_from_csv_test.php');
  39  require_once($CFG->dirroot . '/mod/quiz/report/default.php');
  40  require_once($CFG->dirroot . '/mod/quiz/report/statistics/report.php');
  41  require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
  42  
  43  /**
  44   * Quiz attempt walk through using data from csv file.
  45   *
  46   * @package    quiz_statistics
  47   * @category   phpunit
  48   * @copyright  2013 The Open University
  49   * @author     Jamie Pratt <me@jamiep.org>
  50   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  51   */
  52  class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkthrough_from_csv_testcase {
  53  
  54      /**
  55       * @var quiz_statistics_report object to do stats calculations.
  56       */
  57      protected $report;
  58  
  59      protected function get_full_path_of_csv_file($setname, $test) {
  60          // Overridden here so that __DIR__ points to the path of this file.
  61          return  __DIR__."/fixtures/{$setname}{$test}.csv";
  62      }
  63  
  64      protected $files = array('questions', 'steps', 'results', 'qstats', 'responsecounts');
  65  
  66      /**
  67       * Create a quiz add questions to it, walk through quiz attempts and then check results.
  68       *
  69       * @param PHPUnit_Extensions_Database_DataSet_ITable[] of data read from csv file "questionsXX.csv",
  70       *                                                                                  "stepsXX.csv" and "resultsXX.csv".
  71       * @dataProvider get_data_for_walkthrough
  72       */
  73      public function test_walkthrough_from_csv($quizsettings, $csvdata) {
  74  
  75          $this->create_quiz_simulate_attempts_and_check_results($quizsettings, $csvdata);
  76  
  77          $whichattempts = QUIZ_GRADEAVERAGE; // All attempts.
  78          $whichtries = question_attempt::ALL_TRIES;
  79          $groupstudents = array();
  80          list($questions, $quizstats, $questionstats, $qubaids) =
  81                      $this->check_stats_calculations_and_response_analysis($csvdata, $whichattempts, $whichtries, $groupstudents);
  82          if ($quizsettings['testnumber'] === '00') {
  83              $this->check_variants_count_for_quiz_00($questions, $questionstats, $whichtries, $qubaids);
  84              $this->check_quiz_stats_for_quiz_00($quizstats);
  85          }
  86      }
  87  
  88      /**
  89       * Check actual question stats are the same as that found in csv file.
  90       *
  91       * @param $qstats         PHPUnit_Extensions_Database_DataSet_ITable data from csv file.
  92       * @param $questionstats  \core_question\statistics\questions\all_calculated_for_qubaid_condition Calculated stats.
  93       */
  94      protected function check_question_stats($qstats, $questionstats) {
  95          for ($rowno = 0; $rowno < $qstats->getRowCount(); $rowno++) {
  96              $slotqstats = $qstats->getRow($rowno);
  97              foreach ($slotqstats as $statname => $slotqstat) {
  98                  if (!in_array($statname, array('slot', 'subqname'))  && $slotqstat !== '') {
  99                      $this->assert_stat_equals($slotqstat,
 100                                                $questionstats,
 101                                                $slotqstats['slot'],
 102                                                $slotqstats['subqname'],
 103                                                $slotqstats['variant'],
 104                                                $statname);
 105                  }
 106              }
 107              // Check that sub-question boolean field is correctly set.
 108              $this->assert_stat_equals(!empty($slotqstats['subqname']),
 109                                        $questionstats,
 110                                        $slotqstats['slot'],
 111                                        $slotqstats['subqname'],
 112                                        $slotqstats['variant'],
 113                                        'subquestion');
 114          }
 115      }
 116  
 117      /**
 118       * Check that the stat is as expected within a reasonable tolerance.
 119       *
 120       * @param float|string|bool $expected expected value of stat.
 121       * @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats
 122       * @param int $slot
 123       * @param string $subqname if empty string then not an item stat.
 124       * @param int|string $variant if empty string then not a variantstat.
 125       * @param string $statname
 126       */
 127      protected function assert_stat_equals($expected, $questionstats, $slot, $subqname, $variant, $statname) {
 128  
 129          if ($variant === '' && $subqname === '') {
 130              $actual = $questionstats->for_slot($slot)->{$statname};
 131          } else if ($subqname !== '') {
 132              $actual = $questionstats->for_subq($this->randqids[$slot][$subqname])->{$statname};
 133          } else {
 134              $actual = $questionstats->for_slot($slot, $variant)->{$statname};
 135          }
 136          $message = "$statname for slot $slot";
 137          if ($expected === '**NULL**') {
 138              $this->assertEquals(null, $actual, $message);
 139          } else if (is_bool($expected)) {
 140              $this->assertEquals($expected, $actual, $message);
 141          } else if (is_numeric($expected)) {
 142              switch ($statname) {
 143                  case 'covariance' :
 144                  case 'discriminationindex' :
 145                  case 'discriminativeefficiency' :
 146                  case 'effectiveweight' :
 147                      $precision = 1e-5;
 148                      break;
 149                  default :
 150                      $precision = 1e-6;
 151              }
 152              $delta = abs($expected) * $precision;
 153              $this->assertEquals((float)$expected, $actual, $message, $delta);
 154          } else {
 155              $this->assertEquals($expected, $actual, $message);
 156          }
 157      }
 158  
 159      protected function assert_response_count_equals($question, $qubaids, $expected, $whichtries) {
 160          $responesstats = new \core_question\statistics\responses\analyser($question);
 161          $analysis = $responesstats->load_cached($qubaids, $whichtries);
 162          if (!isset($expected['subpart'])) {
 163              $subpart = 1;
 164          } else {
 165              $subpart = $expected['subpart'];
 166          }
 167          list($subpartid, $responseclassid) = $this->get_response_subpart_and_class_id($question,
 168                                                                                        $subpart,
 169                                                                                        $expected['modelresponse']);
 170  
 171          $subpartanalysis = $analysis->get_analysis_for_subpart($expected['variant'], $subpartid);
 172          $responseclassanalysis = $subpartanalysis->get_response_class($responseclassid);
 173          $actualresponsecounts = $responseclassanalysis->data_for_question_response_table('', '');
 174  
 175          foreach ($actualresponsecounts as $actualresponsecount) {
 176              if ($actualresponsecount->response == $expected['actualresponse'] || count($actualresponsecounts) == 1) {
 177                  $i = 1;
 178                  $partofanalysis = " slot {$expected['slot']}, rand q '{$expected['randq']}', variant {$expected['variant']}, ".
 179                                      "for expected model response {$expected['modelresponse']}, ".
 180                                      "actual response {$expected['actualresponse']}";
 181                  while (isset($expected['count'.$i])) {
 182                      if ($expected['count'.$i] != 0) {
 183                          $this->assertTrue(isset($actualresponsecount->trycount[$i]),
 184                              "There is no count at all for try $i on ".$partofanalysis);
 185                          $this->assertEquals($expected['count'.$i], $actualresponsecount->trycount[$i],
 186                                              "Count for try $i on ".$partofanalysis);
 187                      }
 188                      $i++;
 189                  }
 190                  if (isset($expected['totalcount'])) {
 191                      $this->assertEquals($expected['totalcount'], $actualresponsecount->totalcount,
 192                                          "Total count on ".$partofanalysis);
 193                  }
 194                  return;
 195              }
 196          }
 197          throw new coding_exception("Expected response '{$expected['actualresponse']}' not found.");
 198      }
 199  
 200      protected function get_response_subpart_and_class_id($question, $subpart, $modelresponse) {
 201          $qtypeobj = question_bank::get_qtype($question->qtype, false);
 202          $possibleresponses = $qtypeobj->get_possible_responses($question);
 203          $possibleresponsesubpartids = array_keys($possibleresponses);
 204          if (!isset($possibleresponsesubpartids[$subpart - 1])) {
 205              throw new coding_exception("Subpart '{$subpart}' not found.");
 206          }
 207          $subpartid = $possibleresponsesubpartids[$subpart - 1];
 208  
 209          if ($modelresponse == '[NO RESPONSE]') {
 210              return array($subpartid, null);
 211  
 212          } else if ($modelresponse == '[NO MATCH]') {
 213              return array($subpartid, 0);
 214          }
 215  
 216          $modelresponses = array();
 217          foreach ($possibleresponses[$subpartid] as $responseclassid => $subpartpossibleresponse) {
 218              $modelresponses[$responseclassid] = $subpartpossibleresponse->responseclass;
 219          }
 220          $this->assertContains($modelresponse, $modelresponses);
 221          $responseclassid = array_search($modelresponse, $modelresponses);
 222          return array($subpartid, $responseclassid);
 223      }
 224  
 225      /**
 226       * @param $responsecounts
 227       * @param $qubaids
 228       * @param $questions
 229       * @param $whichtries
 230       */
 231      protected function check_response_counts($responsecounts, $qubaids, $questions, $whichtries) {
 232          for ($rowno = 0; $rowno < $responsecounts->getRowCount(); $rowno++) {
 233              $expected = $responsecounts->getRow($rowno);
 234              $defaultsforexpected = array('randq' => '', 'variant' => '1', 'subpart' => '1');
 235              foreach ($defaultsforexpected as $key => $expecteddefault) {
 236                  if (!isset($expected[$key])) {
 237                      $expected[$key] = $expecteddefault;
 238                  }
 239              }
 240              if ($expected['randq'] == '') {
 241                  $question = $questions[$expected['slot']];
 242              } else {
 243                  $qid = $this->randqids[$expected['slot']][$expected['randq']];
 244                  $question = question_finder::get_instance()->load_question_data($qid);
 245              }
 246              $this->assert_response_count_equals($question, $qubaids, $expected, $whichtries);
 247          }
 248      }
 249  
 250      /**
 251       * @param $questions
 252       * @param $questionstats
 253       * @param $whichtries
 254       * @param $qubaids
 255       */
 256      protected function check_variants_count_for_quiz_00($questions, $questionstats, $whichtries, $qubaids) {
 257          $expectedvariantcounts = array(2 => array(1  => 6,
 258                                                    4  => 4,
 259                                                    5  => 3,
 260                                                    6  => 4,
 261                                                    7  => 2,
 262                                                    8  => 5,
 263                                                    10 => 1));
 264  
 265          foreach ($questions as $slot => $question) {
 266              if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
 267                  continue;
 268              }
 269              $responesstats = new \core_question\statistics\responses\analyser($question);
 270              $this->assertTimeCurrent($responesstats->get_last_analysed_time($qubaids, $whichtries));
 271              $analysis = $responesstats->load_cached($qubaids, $whichtries);
 272              $variantsnos = $analysis->get_variant_nos();
 273              if (isset($expectedvariantcounts[$slot])) {
 274                  // Compare contents, ignore ordering of array, using canonicalize parameter of assertEquals.
 275                  $this->assertEquals(array_keys($expectedvariantcounts[$slot]), $variantsnos, '', 0, 10, true);
 276              } else {
 277                  $this->assertEquals(array(1), $variantsnos);
 278              }
 279              $totalspervariantno = array();
 280              foreach ($variantsnos as $variantno) {
 281  
 282                  $subpartids = $analysis->get_subpart_ids($variantno);
 283                  foreach ($subpartids as $subpartid) {
 284                      if (!isset($totalspervariantno[$subpartid])) {
 285                          $totalspervariantno[$subpartid] = array();
 286                      }
 287                      $totalspervariantno[$subpartid][$variantno] = 0;
 288  
 289                      $subpartanalysis = $analysis->get_analysis_for_subpart($variantno, $subpartid);
 290                      $classids = $subpartanalysis->get_response_class_ids();
 291                      foreach ($classids as $classid) {
 292                          $classanalysis = $subpartanalysis->get_response_class($classid);
 293                          $actualresponsecounts = $classanalysis->data_for_question_response_table('', '');
 294                          foreach ($actualresponsecounts as $actualresponsecount) {
 295                              $totalspervariantno[$subpartid][$variantno] += $actualresponsecount->totalcount;
 296                          }
 297                      }
 298                  }
 299              }
 300              // Count all counted responses for each part of question and confirm that counted responses, for most question types
 301              // are the number of attempts at the question for each question part.
 302              if ($slot != 5) {
 303                  // Slot 5 holds a multi-choice multiple question.
 304                  // Multi-choice multiple is slightly strange. Actual answer counts given for each sub part do not add up to the
 305                  // total attempt count.
 306                  // This is because each option is counted as a sub part and each option can be off or on in each attempt. Off is
 307                  // not counted in response analysis for this question type.
 308                  foreach ($totalspervariantno as $totalpervariantno) {
 309                      if (isset($expectedvariantcounts[$slot])) {
 310                          // If we know how many attempts there are at each variant we can check
 311                          // that we have counted the correct amount of responses for each variant.
 312                          $this->assertEquals($expectedvariantcounts[$slot],
 313                                              $totalpervariantno,
 314                                              "Totals responses do not add up in response analysis for slot {$slot}.",
 315                                              0,
 316                                              10,
 317                                              true);
 318                      } else {
 319                          $this->assertEquals(25,
 320                                              array_sum($totalpervariantno),
 321                                              "Totals responses do not add up in response analysis for slot {$slot}.");
 322                      }
 323                  }
 324              }
 325          }
 326  
 327          foreach ($expectedvariantcounts as $slot => $expectedvariantcount) {
 328              foreach ($expectedvariantcount as $variantno => $s) {
 329                  $this->assertEquals($s, $questionstats->for_slot($slot, $variantno)->s);
 330              }
 331          }
 332      }
 333  
 334      /**
 335       * @param $quizstats
 336       */
 337      protected function check_quiz_stats_for_quiz_00($quizstats) {
 338          $quizstatsexpected = array(
 339              'median'             => 4.5,
 340              'firstattemptsavg'   => 4.617333332,
 341              'allattemptsavg'     => 4.617333332,
 342              'firstattemptscount' => 25,
 343              'allattemptscount'   => 25,
 344              'standarddeviation'  => 0.8117265554,
 345              'skewness'           => -0.092502502,
 346              'kurtosis'           => -0.7073968557,
 347              'cic'                => -87.2230935542,
 348              'errorratio'         => 136.8294900795,
 349              'standarderror'      => 1.1106813066
 350          );
 351  
 352          foreach ($quizstatsexpected as $statname => $statvalue) {
 353              $this->assertEquals($statvalue, $quizstats->$statname, $quizstats->$statname, abs($statvalue) * 1.5e-5);
 354          }
 355      }
 356  
 357      /**
 358       * Check the question stats and the response counts used in the statistics report. If the appropriate files exist in fixtures/.
 359       *
 360       * @param PHPUnit_Extensions_Database_DataSet_ITable[] $csvdata Data loaded from csv files for this test.
 361       * @param string $whichattempts
 362       * @param string $whichtries
 363       * @param int[] $groupstudents
 364       * @return array with contents 0 => $questions, 1 => $quizstats, 2=> $questionstats, 3=> $qubaids Might be needed for further
 365       *               testing.
 366       */
 367      protected function check_stats_calculations_and_response_analysis($csvdata, $whichattempts, $whichtries, $groupstudents) {
 368          $this->report = new quiz_statistics_report();
 369          $questions = $this->report->load_and_initialise_questions_for_calculations($this->quiz);
 370          list($quizstats, $questionstats) = $this->report->get_all_stats_and_analysis($this->quiz,
 371                                                                                       $whichattempts,
 372                                                                                       $whichtries,
 373                                                                                       $groupstudents,
 374                                                                                       $questions);
 375  
 376          $qubaids = quiz_statistics_qubaids_condition($this->quiz->id, $groupstudents, $whichattempts);
 377  
 378          // We will create some quiz and question stat calculator instances and some response analyser instances, just in order
 379          // to check the last analysed time then returned.
 380          $quizcalc = new \quiz_statistics\calculator();
 381          // Should not be a delay of more than one second between the calculation of stats above and here.
 382          $this->assertTimeCurrent($quizcalc->get_last_calculated_time($qubaids));
 383  
 384          $qcalc = new \core_question\statistics\questions\calculator($questions);
 385          $this->assertTimeCurrent($qcalc->get_last_calculated_time($qubaids));
 386  
 387          if (isset($csvdata['responsecounts'])) {
 388              $this->check_response_counts($csvdata['responsecounts'], $qubaids, $questions, $whichtries);
 389          }
 390          if (isset($csvdata['qstats'])) {
 391              $this->check_question_stats($csvdata['qstats'], $questionstats);
 392              return array($questions, $quizstats, $questionstats, $qubaids);
 393          }
 394          return array($questions, $quizstats, $questionstats, $qubaids);
 395      }
 396  
 397  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1