MDL-41752 question statistics class moved and improved
authorJamie Pratt <me@jamiep.org>
Tue, 24 Sep 2013 09:33:07 +0000 (16:33 +0700)
committerJamie Pratt <me@jamiep.org>
Fri, 27 Sep 2013 07:10:53 +0000 (14:10 +0700)
quiz_question_statistics_stats renamed to question_statistics_calculator
separate class question_statistics used to store calculated stats
and api changed, also code generally cleaned up.

mod/quiz/report/statistics/classes/calculator.php
mod/quiz/report/statistics/report.php
mod/quiz/report/statistics/statistics_question_table.php
mod/quiz/report/statistics/statistics_table.php
mod/quiz/report/statistics/tests/statistics_test.php
mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php
question/classes/statistics/questions/calculated.php [new file with mode: 0644]
question/classes/statistics/questions/calculated_for_subquestion.php [new file with mode: 0644]
question/classes/statistics/questions/calculator.php [moved from question/engine/statistics.php with 51% similarity]
question/classes/statistics/responses/analyser.php [moved from question/engine/responseanalysis.php with 91% similarity]

index 6a0d6b8..997ee73 100644 (file)
@@ -37,9 +37,7 @@ class quiz_statistics_calculator {
      * @param array $groupstudents     students in this group.
      * @param int   $p                 number of positions (slots).
      * @param float $sumofmarkvariance sum of mark variance, calculated as part of question statistics
-     * @return array with two elements:
-     *      - integer $s Number of attempts included in the stats.
-     *      - object $quizstats The statistics for overall attempt scores.
+     * @return quiz_statistics_calculated $quizstats The statistics for overall attempt scores.
      */
     public function calculate($quizid, $currentgroup, $useallattempts, $groupstudents, $p, $sumofmarkvariance) {
 
index 21d88ac..aab3d00 100644 (file)
@@ -28,8 +28,6 @@ defined('MOODLE_INTERNAL') || die();
 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_form.php');
 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php');
 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_question_table.php');
-require_once($CFG->dirroot . '/question/engine/statistics.php');
-require_once($CFG->dirroot . '/question/engine/responseanalysis.php');
 require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
 /**
  * The quiz statistics report provides summary information about each question in
@@ -46,7 +44,7 @@ class quiz_statistics_report extends quiz_default_report {
      */
     protected $context;
 
-    /** @var object instance of table class used for main questions stats table. */
+    /** @var quiz_statistics_table instance of table class used for main questions stats table. */
     protected $table;
 
     /**
@@ -131,14 +129,13 @@ class quiz_statistics_report extends quiz_default_report {
 
         if (!$nostudentsingroup) {
             // Get the data to be displayed.
-            list($quizstats, $questions, $subquestions) =
+            list($quizstats, $questionstats, $subquestionstats) =
                 $this->get_quiz_and_questions_stats($quiz, $currentgroup, $useallattempts, $groupstudents, $questions);
         } else {
             // Or create empty stats containers.
             $quizstats = new quiz_statistics_calculated($useallattempts);
-            $qstats = new question_statistics($questions);
-            $questions = $qstats->questions;
-            $subquestions = $qstats->subquestions;
+            $questionstats = array();
+            $subquestionstats = array();
         }
         $quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
 
@@ -174,23 +171,23 @@ class quiz_statistics_report extends quiz_default_report {
             $this->download_quiz_info_table($quizinfo);
 
             if ($quizstats->s()) {
-                $this->output_quiz_structure_analysis_table($quizstats->s(), $questions, $subquestions);
+                $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats);
 
                 if ($this->table->is_downloading() == 'xhtml' && $quizstats->s() != 0) {
                     $this->output_statistics_graph($quiz->id, $currentgroup, $useallattempts);
                 }
 
-                foreach ($questions as $question) {
+                foreach ($questions as $slot => $question) {
                     if (question_bank::get_qtype(
                             $question->qtype, false)->can_analyse_responses()) {
                         $this->output_individual_question_response_analysis(
-                                $question, $reporturl, $qubaids);
+                                $question, $questionstats[$slot]->s, $reporturl, $qubaids);
 
-                    } else if (!empty($question->_stats->subquestions)) {
-                        $subitemstodisplay = explode(',', $question->_stats->subquestions);
+                    } else if (!empty($questionstats[$slot]->subquestions)) {
+                        $subitemstodisplay = explode(',', $questionstats[$slot]->subquestions);
                         foreach ($subitemstodisplay as $subitemid) {
                             $this->output_individual_question_response_analysis(
-                                    $subquestions[$subitemid], $reporturl, $qubaids);
+                                $subquestionstats[$subitemid]->question, $subquestionstats[$subitemid]->s, $reporturl, $qubaids);
                         }
                     }
                 }
@@ -204,9 +201,8 @@ class quiz_statistics_report extends quiz_default_report {
                 print_error('questiondoesnotexist', 'question');
             }
 
-            $this->output_individual_question_data($quiz, $questions[$slot]);
-            $this->output_individual_question_response_analysis(
-                    $questions[$slot], $reporturl, $qubaids);
+            $this->output_individual_question_data($quiz, $questionstats[$slot]);
+            $this->output_individual_question_response_analysis($questions[$slot], $questionstats[$slot]->s, $reporturl, $qubaids);
 
             // Back to overview link.
             echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
@@ -219,9 +215,9 @@ class quiz_statistics_report extends quiz_default_report {
                 print_error('questiondoesnotexist', 'question');
             }
 
-            $this->output_individual_question_data($quiz, $subquestions[$qid]);
-            $this->output_individual_question_response_analysis(
-                    $subquestions[$qid], $reporturl, $qubaids);
+            $this->output_individual_question_data($quiz, $subquestionstats[$qid]);
+            $this->output_individual_question_response_analysis($subquestionstats[$qid]->question,
+                                                                $subquestionstats[$qid]->s, $reporturl, $qubaids);
 
             // Back to overview link.
             echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
@@ -231,7 +227,7 @@ class quiz_statistics_report extends quiz_default_report {
         } else if ($this->table->is_downloading()) {
             // Downloading overview report.
             $this->download_quiz_info_table($quizinfo);
-            $this->output_quiz_structure_analysis_table($quizstats->s(), $questions, $subquestions);
+            $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats);
             $this->table->finish_output();
 
         } else {
@@ -243,7 +239,7 @@ class quiz_statistics_report extends quiz_default_report {
             echo $this->output_quiz_info_table($quizinfo);
             if ($quizstats->s()) {
                 echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'));
-                $this->output_quiz_structure_analysis_table($quizstats->s(), $questions, $subquestions);
+                $this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats);
                 $this->output_statistics_graph($quiz->id, $currentgroup, $useallattempts);
             }
         }
@@ -255,16 +251,16 @@ class quiz_statistics_report extends quiz_default_report {
      * Display the statistical and introductory information about a question.
      * Only called when not downloading.
      * @param object $quiz the quiz settings.
-     * @param object $question the question to report on.
+     * @param \core_question\statistics\questions\calculated $questionstat the question to report on.
      * @param moodle_url $reporturl the URL to resisplay this report.
      * @param object $quizstats Holds the quiz statistics.
      */
-    protected function output_individual_question_data($quiz, $question) {
+    protected function output_individual_question_data($quiz, $questionstat) {
         global $OUTPUT;
 
         // On-screen display. Show a summary of the question's place in the quiz,
         // and the question statistics.
-        $datumfromtable = $this->table->format_row($question);
+        $datumfromtable = $this->table->format_row($questionstat);
 
         // Set up the question info table.
         $questioninfotable = new html_table();
@@ -275,13 +271,13 @@ class quiz_statistics_report extends quiz_default_report {
         $questioninfotable->data = array();
         $questioninfotable->data[] = array(get_string('modulename', 'quiz'), $quiz->name);
         $questioninfotable->data[] = array(get_string('questionname', 'quiz_statistics'),
-                $question->name.'&nbsp;'.$datumfromtable['actions']);
+                $questionstat->question->name.'&nbsp;'.$datumfromtable['actions']);
         $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'),
                 $datumfromtable['icon'] . '&nbsp;' .
-                question_bank::get_qtype($question->qtype, false)->menu_name() . '&nbsp;' .
+                question_bank::get_qtype($questionstat->question->qtype, false)->menu_name() . '&nbsp;' .
                 $datumfromtable['icon']);
         $questioninfotable->data[] = array(get_string('positions', 'quiz_statistics'),
-                $question->_stats->positions);
+                $questionstat->positions);
 
         // Set up the question statistics table.
         $questionstatstable = new html_table();
@@ -312,7 +308,7 @@ class quiz_statistics_report extends quiz_default_report {
         // Display the various bits.
         echo $OUTPUT->heading(get_string('questioninformation', 'quiz_statistics'));
         echo html_writer::table($questioninfotable);
-        echo $this->render_question_text($question);
+        echo $this->render_question_text($questionstat->question);
         echo $OUTPUT->heading(get_string('questionstatistics', 'quiz_statistics'));
         echo html_writer::table($questionstatstable);
     }
@@ -339,8 +335,7 @@ class quiz_statistics_report extends quiz_default_report {
      * @param moodle_url $reporturl the URL to resisplay this report.
      * @param qubaid_condition $qubaids
      */
-    protected function output_individual_question_response_analysis($question,
-            $reporturl, $qubaids) {
+    protected function output_individual_question_response_analysis($question, $s, $reporturl, $qubaids) {
         global $OUTPUT;
 
         if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
@@ -373,10 +368,10 @@ class quiz_statistics_report extends quiz_default_report {
             }
         }
 
-        $responesstats = new question_response_analyser($question);
+        $responesstats = new \core_question\statistics\responses\analyser($question);
         $responesstats->load_cached($qubaids);
 
-        $qtable->question_setup($reporturl, $question, $responesstats);
+        $qtable->question_setup($reporturl, $question, $s, $responesstats);
         if ($this->table->is_downloading()) {
             $exportclass->output_headers($qtable->headers);
         }
@@ -415,27 +410,27 @@ class quiz_statistics_report extends quiz_default_report {
     /**
      * Output the table that lists all the questions in the quiz with their statistics.
      * @param int $s number of attempts.
-     * @param array $questions the questions in the quiz.
-     * @param array $subquestions the subquestions of any random questions.
+     * @param \core_question\statistics\questions\calculated[] $questionstats the stats for the main questions in the quiz.
+     * @param \core_question\statistics\questions\calculated_for_subquestion[] $subquestionstats the stats of any random questions.
      */
-    protected function output_quiz_structure_analysis_table($s, $questions, $subquestions) {
+    protected function output_quiz_structure_analysis_table($s, $questionstats, $subquestionstats) {
         if (!$s) {
             return;
         }
 
-        foreach ($questions as $question) {
-            // Output the data for this questions.
-            $this->table->add_data_keyed($this->table->format_row($question));
+        foreach ($questionstats as $questionstat) {
+            // Output the data for these question statistics.
+            $this->table->add_data_keyed($this->table->format_row($questionstat));
 
-            if (empty($question->_stats->subquestions)) {
+            if (empty($questionstat->subquestions)) {
                 continue;
             }
 
             // And its subquestions, if it has any.
-            $subitemstodisplay = explode(',', $question->_stats->subquestions);
+            $subitemstodisplay = explode(',', $questionstat->subquestions);
             foreach ($subitemstodisplay as $subitemid) {
-                $subquestions[$subitemid]->maxmark = $question->maxmark;
-                $this->table->add_data_keyed($this->table->format_row($subquestions[$subitemid]));
+                $subquestionstats[$subitemid]->maxmark = $questionstat->maxmark;
+                $this->table->add_data_keyed($this->table->format_row($subquestionstats[$subitemid]));
             }
         }
 
@@ -519,43 +514,37 @@ class quiz_statistics_report extends quiz_default_report {
      * @param array $questions question definitions.
      * @return array with 4 elements:
      *     - $quizstats The statistics for overall attempt scores.
-     *     - $questions The questions, with an additional _stats field.
-     *     - $subquestions The subquestions, if any, with an additional _stats field.
-     *     - $s Number of attempts included in the stats.
+     *     - $questionstats array of \core_question\statistics\questions\calculated objects keyed by slot.
+     *     - $subquestionstats array of \core_question\statistics\questions\calculated_for_subquestion objects keyed by question id.
      */
     protected function get_quiz_and_questions_stats($quiz, $currentgroup, $useallattempts, $groupstudents, $questions) {
 
         $qubaids = quiz_statistics_qubaids_condition($quiz->id, $currentgroup, $groupstudents, $useallattempts);
 
-
-        $qstats = new question_statistics($questions);
+        $qcalc = new \core_question\statistics\questions\calculator($questions);
 
         $quizcalc = new quiz_statistics_calculator();
 
         if ($quizcalc->get_last_calculated_time($qubaids) === false) {
             // Recalculate now.
-            $qstats->calculate($qubaids);
+            list($questionstats, $subquestionstats) = $qcalc->calculate($qubaids);
 
             $quizstats = $quizcalc->calculate($quiz->id, $currentgroup, $useallattempts,
-                                               $groupstudents, count($questions), $qstats->get_sum_of_mark_variance());
+                                               $groupstudents, count($questions), $qcalc->get_sum_of_mark_variance());
 
-            $questions = $qstats->questions;
-            $subquestions = $qstats->subquestions;
 
             if ($quizstats->s()) {
-                $this->calculate_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestions);
+                $this->calculate_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats);
             }
         } else {
             $quizstats = $quizcalc->get_cached($qubaids);
-            $qstats->get_cached($qubaids);
-            $questions = $qstats->questions;
-            $subquestions = $qstats->subquestions;
+            list($questionstats, $subquestionstats) = $qcalc->get_cached($qubaids);
         }
 
-        return array($quizstats, $questions, $subquestions);
+        return array($quizstats, $questionstats, $subquestionstats);
     }
 
-    protected function calculate_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestions) {
+    protected function calculate_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats) {
 
         $done = array();
         foreach ($questions as $question) {
@@ -564,18 +553,18 @@ class quiz_statistics_report extends quiz_default_report {
             }
             $done[$question->id] = 1;
 
-            $responesstats = new question_response_analyser($question);
+            $responesstats = new \core_question\statistics\responses\analyser($question);
             $responesstats->calculate($qubaids);
         }
 
-        foreach ($subquestions as $question) {
-            if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses() ||
-                    isset($done[$question->id])) {
+        foreach ($subquestionstats as $subquestionstat) {
+            if (!question_bank::get_qtype($subquestionstat->question->qtype, false)->can_analyse_responses() ||
+                    isset($done[$subquestionstat->question->id])) {
                 continue;
             }
-            $done[$question->id] = 1;
+            $done[$subquestionstat->question->id] = 1;
 
-            $responesstats = new question_response_analyser($question);
+            $responesstats = new \core_question\statistics\responses\analyser($subquestionstat->question);
             $responesstats->calculate($qubaids);
         }
     }
index 508775b..9265181 100644 (file)
@@ -40,12 +40,12 @@ require_once($CFG->libdir . '/tablelib.php');
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 class quiz_statistics_question_table extends flexible_table {
-    /** @var object this question with a _stats field. */
+    /** @var object this question. */
     protected $questiondata;
 
     /**
      * Constructor.
-     * @param $qid the id of the particular question whose statistics are being
+     * @param int $qid the id of the particular question whose statistics are being
      * displayed.
      */
     public function __construct($qid) {
@@ -53,16 +53,14 @@ class quiz_statistics_question_table extends flexible_table {
     }
 
     /**
-     * Set up the columns and headers and other properties of the table and then
-     * call flexible_table::setup() method.
-     *
-     * @param moodle_url $reporturl the URL to redisplay this report.
-     * @param object $question a question with a _stats field
-     * @param bool $hassubqs
+     * @param moodle_url                                   $reporturl
+     * @param object                                       $questiondata
+     * @param integer                                      $s               number of attempts on this question.
+     * @param \core_question\statistics\responses\analyser $responesstats
      */
-    public function question_setup($reporturl, $questiondata,
-            question_response_analyser $responesstats) {
+    public function question_setup($reporturl, $questiondata, $s, \core_question\statistics\responses\analyser $responesstats) {
         $this->questiondata = $questiondata;
+        $this->s = $s;
 
         $this->define_baseurl($reporturl->out());
         $this->collapsible(false);
@@ -137,10 +135,10 @@ class quiz_statistics_question_table extends flexible_table {
      * @return string contents of this table cell.
      */
     protected function col_frequency($response) {
-        if (!$this->questiondata->_stats->s) {
+        if (!$this->s) {
             return '';
         }
 
-        return $this->format_percentage($response->count / $this->questiondata->_stats->s);
+        return $this->format_percentage($response->count / $this->s);
     }
 }
index 393cede..d186bee 100644 (file)
@@ -134,61 +134,63 @@ class quiz_statistics_table extends flexible_table {
 
     /**
      * The question number.
-     * @param object $question containst the data to display.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_number($question) {
-        if ($question->_stats->subquestion) {
+    protected function col_number($questionstat) {
+        if ($questionstat->subquestion) {
             return '';
         }
 
-        return $question->number;
+        return $questionstat->question->number;
     }
 
     /**
      * The question type icon.
-     * @param object $question containst the data to display.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_icon($question) {
-        return print_question_icon($question, true);
+    protected function col_icon($questionstat) {
+        return print_question_icon($questionstat->question, true);
     }
 
     /**
      * Actions that can be performed on the question by this user (e.g. edit or preview).
-     * @param object $question containst the data to display.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_actions($question) {
-        return quiz_question_action_icons($this->quiz, $this->cmid, $question, $this->baseurl);
+    protected function col_actions($questionstat) {
+        return quiz_question_action_icons($this->quiz, $this->cmid, $questionstat->question, $this->baseurl);
     }
 
     /**
      * The question type name.
-     * @param object $question containst the data to display.
+     *
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_qtype($question) {
-        return question_bank::get_qtype_name($question->qtype);
+    protected function col_qtype($questionstat) {
+        return question_bank::get_qtype_name($questionstat->question->qtype);
     }
 
     /**
      * The question name.
-     * @param object $question containst the data to display.
+     *
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_name($question) {
-        $name = $question->name;
+    protected function col_name($questionstat) {
+        $name = $questionstat->question->name;
 
         if ($this->is_downloading()) {
             return $name;
         }
 
         $url = null;
-        if ($question->_stats->subquestion) {
-            $url = new moodle_url($this->baseurl, array('qid' => $question->id));
-        } else if ($question->_stats->slot && $question->qtype != 'random') {
-            $url = new moodle_url($this->baseurl, array('slot' => $question->_stats->slot));
+        if ($questionstat->subquestion) {
+            $url = new moodle_url($this->baseurl, array('qid' => $questionstat->questionid));
+        } else if ($questionstat->slot && $questionstat->question->qtype != 'random') {
+            $url = new moodle_url($this->baseurl, array('slot' => $questionstat->slot));
         }
 
         if ($url) {
@@ -196,7 +198,7 @@ class quiz_statistics_table extends flexible_table {
                     array('title' => get_string('detailedanalysis', 'quiz_statistics')));
         }
 
-        if ($this->is_dubious_question($question)) {
+        if ($this->is_dubious_question($questionstat)) {
             $name = html_writer::tag('div', $name, array('class' => 'dubious'));
         }
 
@@ -205,82 +207,82 @@ class quiz_statistics_table extends flexible_table {
 
     /**
      * The number of attempts at this question.
-     * @param object $question containst the data to display.
+     *
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_s($question) {
-        if (!isset($question->_stats->s)) {
+    protected function col_s($questionstat) {
+        if (!isset($questionstat->s)) {
             return 0;
         }
 
-        return $question->_stats->s;
+        return $questionstat->s;
     }
 
     /**
      * The facility index (average fraction).
-     * @param object $question containst the data to display.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_facility($question) {
-        if (is_null($question->_stats->facility)) {
+    protected function col_facility($questionstat) {
+        if (is_null($questionstat->facility)) {
             return '';
         }
 
-        return number_format($question->_stats->facility*100, 2) . '%';
+        return number_format($questionstat->facility*100, 2) . '%';
     }
 
     /**
      * The standard deviation of the fractions.
-     * @param object $question containst the data to display.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_sd($question) {
-        if (is_null($question->_stats->sd) || $question->_stats->maxmark == 0) {
+    protected function col_sd($questionstat) {
+        if (is_null($questionstat->sd) || $questionstat->maxmark == 0) {
             return '';
         }
 
-        return number_format($question->_stats->sd*100 / $question->_stats->maxmark, 2) . '%';
+        return number_format($questionstat->sd*100 / $questionstat->maxmark, 2) . '%';
     }
 
     /**
      * An estimate of the fraction a student would get by guessing randomly.
-     * @param object $question containst the data to display.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_random_guess_score($question) {
-        if (is_null($question->_stats->randomguessscore)) {
+    protected function col_random_guess_score($questionstat) {
+        if (is_null($questionstat->randomguessscore)) {
             return '';
         }
 
-        return number_format($question->_stats->randomguessscore * 100, 2).'%';
+        return number_format($questionstat->randomguessscore * 100, 2).'%';
     }
 
     /**
      * The intended question weight. Maximum mark for the question as a percentage
      * of maximum mark for the quiz. That is, the indended influence this question
      * on the student's overall mark.
-     * @param object $question containst the data to display.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_intended_weight($question) {
-        return quiz_report_scale_summarks_as_percentage(
-                $question->_stats->maxmark, $this->quiz);
+    protected function col_intended_weight($questionstat) {
+        return quiz_report_scale_summarks_as_percentage($questionstat->maxmark, $this->quiz);
     }
 
     /**
      * The effective question weight. That is, an estimate of the actual
      * influence this question has on the student's overall mark.
-     * @param object $question containst the data to display.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_effective_weight($question) {
+    protected function col_effective_weight($questionstat) {
         global $OUTPUT;
 
-        if ($question->_stats->subquestion) {
+        if ($questionstat->subquestion) {
             return '';
         }
 
-        if ($question->_stats->negcovar) {
+        if ($questionstat->negcovar) {
             $negcovar = get_string('negcovar', 'quiz_statistics');
 
             if (!$this->is_downloading()) {
@@ -292,49 +294,49 @@ class quiz_statistics_table extends flexible_table {
             return $negcovar;
         }
 
-        return number_format($question->_stats->effectiveweight, 2) . '%';
+        return number_format($questionstat->effectiveweight, 2) . '%';
     }
 
     /**
      * Discrimination index. This is the product moment correlation coefficient
-     * between the fraction for this qestion, and the average fraction for the
+     * between the fraction for this question, and the average fraction for the
      * other questions in this quiz.
-     * @param object $question containst the data to display.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_discrimination_index($question) {
-        if (!is_numeric($question->_stats->discriminationindex)) {
-            return $question->_stats->discriminationindex;
+    protected function col_discrimination_index($questionstat) {
+        if (!is_numeric($questionstat->discriminationindex)) {
+            return $questionstat->discriminationindex;
         }
 
-        return number_format($question->_stats->discriminationindex, 2) . '%';
+        return number_format($questionstat->discriminationindex, 2) . '%';
     }
 
     /**
      * Discrimination efficiency, similar to, but different from, the Discrimination index.
-     * @param object $question containst the data to display.
+     *
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return string contents of this table cell.
      */
-    protected function col_discriminative_efficiency($question) {
-        if (!is_numeric($question->_stats->discriminativeefficiency)) {
+    protected function col_discriminative_efficiency($questionstat) {
+        if (!is_numeric($questionstat->discriminativeefficiency)) {
             return '';
         }
 
-        return number_format($question->_stats->discriminativeefficiency, 2) . '%';
+        return number_format($questionstat->discriminativeefficiency, 2) . '%';
     }
 
     /**
      * This method encapsulates the test for wheter a question should be considered dubious.
-     * @param object question the question object with a property _stats which
-     * includes all the stats for the question.
+     * @param \core_question\statistics\questions\calculated $questionstat stats for the question.
      * @return bool is this question possibly not pulling it's weight?
      */
-    protected function is_dubious_question($question) {
-        if (!is_numeric($question->_stats->discriminativeefficiency)) {
+    protected function is_dubious_question($questionstat) {
+        if (!is_numeric($questionstat->discriminativeefficiency)) {
             return false;
         }
 
-        return $question->_stats->discriminativeefficiency < 15;
+        return $questionstat->discriminativeefficiency < 15;
     }
 
     public function  wrap_html_start() {
index db191e4..258062c 100644 (file)
@@ -28,7 +28,6 @@ defined('MOODLE_INTERNAL') || die();
 
 global $CFG;
 require_once($CFG->libdir . '/questionlib.php');
-require_once($CFG->dirroot . '/question/engine/statistics.php');
 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
 require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
 
@@ -39,7 +38,13 @@ require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
  * @copyright 2010 The Open University
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class testable_question_statistics extends question_statistics {
+class testable_question_statistics extends \core_question\statistics\questions\calculator {
+
+    /**
+     * @var object[]
+     */
+    protected $lateststeps;
+
     public function set_step_data($states) {
         $this->lateststeps = $states;
     }
@@ -95,9 +100,9 @@ class quiz_statistics_question_stats_testcase extends basic_testcase {
         // Data is taken from questions mostly generated by
         // contrib/tools/generators/generator.php.
         $questions = $this->get_records_from_csv(__DIR__.'/fixtures/mdl_question.csv');
-        $this->qstats = new testable_question_statistics($questions, 22, 10045.45455);
-        $this->qstats->set_step_data($steps);
-        $this->qstats->calculate(null);
+        $calculator = new testable_question_statistics($questions);
+        $calculator->set_step_data($steps);
+        list($this->qstats, ) = $calculator->calculate(null);
 
         // Values expected are taken from contrib/tools/quiz_tools/stats.xls.
         $facility = array(0, 0, 0, 0, null, null, null, 41.19318182, 81.36363636,
@@ -125,13 +130,13 @@ class quiz_statistics_question_stats_testcase extends basic_testcase {
     }
 
     public function qstats_q_fields($fieldname, $values, $multiplier=1) {
-        foreach ($this->qstats->questions as $question) {
+        foreach ($this->qstats as $qstat) {
             $value = array_shift($values);
             if ($value !== null) {
-                $this->assertEquals($question->_stats->{$fieldname} * $multiplier,
+                $this->assertEquals($qstat->{$fieldname} * $multiplier,
                     $value, '', 1E-6);
             } else {
-                $this->assertEquals($question->_stats->{$fieldname} * $multiplier, $value);
+                $this->assertEquals($qstat->{$fieldname} * $multiplier, $value);
             }
         }
     }
index 32cb585..94c3a8b 100644 (file)
@@ -94,7 +94,7 @@ class quiz_report_statistics_from_steps extends mod_quiz_attempt_walkthrough_fro
         $this->check_attempts_results($csvdata['results'], $attemptids);
 
         $this->report = new testable_quiz_statistics_report();
-        list($quizstats, $questions, $subquestions) = $this->report->get_stats($this->quiz);
+        list($quizstats, $questionstats, $subquestionstats) = $this->report->get_stats($this->quiz);
 
         // These quiz stats and the question stats found in qstats00.csv were calculated independently in spreadsheet which is
         // available in open document or excel format here :
@@ -133,7 +133,7 @@ class quiz_report_statistics_from_steps extends mod_quiz_attempt_walkthrough_fro
                     }
                     $slot = $slotqstats['slot'];
                     $delta = abs($slotqstat) * $precision;
-                    $actual = $questions[$slot]->_stats->{$statname};
+                    $actual = $questionstats[$slot]->{$statname};
                     $this->assertEquals(floatval($slotqstat), $actual, "$statname for slot $slot", $delta);
                 }
             }
diff --git a/question/classes/statistics/questions/calculated.php b/question/classes/statistics/questions/calculated.php
new file mode 100644 (file)
index 0000000..05add67
--- /dev/null
@@ -0,0 +1,179 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Question statistics calculations class. Used in the quiz statistics report but also available for use elsewhere.
+ *
+ * @package    core
+ * @subpackage questionbank
+ * @copyright  2013 Open University
+ * @author     Jamie Pratt <me@jamiep.org>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_question\statistics\questions;
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * This class is used to return the stats as calculated by {@link \core_question\statistics\questions\calculator}
+ *
+ * @copyright 2013 Open University
+ * @author    Jamie Pratt <me@jamiep.org>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class calculated {
+
+    public $questionid;
+
+
+    // These first fields are the final fields cached in the db and shown in reports.
+
+    // See : http://docs.moodle.org/dev/Quiz_statistics_calculations#Position_statistics .
+
+    public $slot = null;
+
+    /**
+     * @var bool is this a sub question.
+     */
+    public $subquestion = false;
+
+    /**
+     * @var int total attempts at this question.
+     */
+    public $s = 0;
+
+    /**
+     * @var float effective weight of this question.
+     */
+    public $effectiveweight;
+
+    /**
+     * @var bool is covariance of this questions mark with other question marks negative?
+     */
+    public $negcovar;
+
+    /**
+     * @var float
+     */
+    public $discriminationindex;
+
+    /**
+     * @var float
+     */
+    public $discriminativeefficiency;
+
+    /**
+     * @var float standard deviation
+     */
+    public $sd;
+
+    /**
+     * @var float
+     */
+    public $facility;
+
+    /**
+     * @var float max mark achievable for this question.
+     */
+    public $maxmark;
+
+    /**
+     * @var string comma separated list of the positions in which this question appears.
+     */
+    public $positions;
+
+    /**
+     * @var null|float The average score that students would have got by guessing randomly. Or null if not calculable.
+     */
+    public $randomguessscore = null;
+
+    // End of fields in db.
+
+    protected $fieldsindb = array('questionid', 'slot', 'subquestion', 's', 'effectiveweight', 'negcovar', 'discriminationindex',
+        'discriminativeefficiency', 'sd', 'facility', 'subquestions', 'maxmark', 'positions', 'randomguessscore');
+
+    // Fields used for intermediate calculations.
+
+    public $totalmarks = 0;
+
+    public $totalothermarks = 0;
+
+    public $markvariancesum = 0;
+
+    public $othermarkvariancesum = 0;
+
+    public $covariancesum = 0;
+
+    public $covariancemaxsum = 0;
+
+    public $subquestions = '';
+
+    public $covariancewithoverallmarksum = 0;
+
+    public $markarray = array();
+
+    public $othermarksarray = array();
+
+    public $markaverage;
+
+    public $othermarkaverage;
+
+    public $markvariance;
+    public $othermarkvariance;
+    public $covariance;
+    public $covariancemax;
+    public $covariancewithoverallmark;
+
+    /**
+     * @var object full question data
+     */
+    public $question;
+
+    /**
+     * Set if this record has been retrieved from cache. This is the time that the statistics were calculated.
+     *
+     * @var integer
+     */
+    public $timemodified;
+
+    /**
+     * Cache calculated stats stored in this object in 'question_statistics' table.
+     *
+     * @param \qubaid_condition $qubaids
+     */
+    public function cache($qubaids) {
+        global $DB;
+        $toinsert = new \stdClass();
+        $toinsert->hashcode = $qubaids->get_hash_code();
+        $toinsert->timemodified = time();
+        foreach ($this->fieldsindb as $field) {
+            $toinsert->{$field} = $this->{$field};
+        }
+        $DB->insert_record('question_statistics', $toinsert, false);
+    }
+
+    /**
+     * @param object $record Given a record from 'question_statistics' copy stats from record to properties.
+     */
+    public function populate_from_record($record) {
+        foreach ($this->fieldsindb as $field) {
+            $this->$field = $record->$field;
+        }
+        $this->timemodified = $record->timemodified;
+    }
+
+
+}
diff --git a/question/classes/statistics/questions/calculated_for_subquestion.php b/question/classes/statistics/questions/calculated_for_subquestion.php
new file mode 100644 (file)
index 0000000..061cc96
--- /dev/null
@@ -0,0 +1,37 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Class for storing calculated sub question statistics and intermediate calculation values.
+ *
+ * @package    core_question
+ * @copyright  2013 The Open University
+ * @author     James Pratt me@jamiep.org
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_question\statistics\questions;
+defined('MOODLE_INTERNAL') || die();
+
+class calculated_for_subquestion extends calculated {
+    public $subquestion = true;
+
+    public $usedin = array();
+
+    public $differentweights = false;
+
+    public $negcovar = 0;
+}
similarity index 51%
rename from question/engine/statistics.php
rename to question/classes/statistics/questions/calculator.php
index e5f4d12..db8b574 100644 (file)
@@ -15,7 +15,7 @@
 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
 
 /**
- * Question statistics calculations class. Used in the quiz statistics report but also available for use elsewhere.
+ * Question statistics calculator class. Used in the quiz statistics report but also available for use elsewhere.
  *
  * @package    core
  * @subpackage questionbank
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-
+namespace core_question\statistics\questions;
 defined('MOODLE_INTERNAL') || die();
 
-
 /**
  * This class has methods to compute the question statistics from the raw data.
  *
@@ -35,88 +34,46 @@ defined('MOODLE_INTERNAL') || die();
  * @author    Jamie Pratt <me@jamiep.org>
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class question_statistics {
-    public $questions;
-    public $subquestions = array();
-
-    protected $summarksavg;
-
-    protected $sumofmarkvariance = 0;
-    protected $randomselectors = array();
+class calculator {
 
     /**
-     * Constructor.
-     *
-     * @param $questions array the main questions indexed by slot.
+     * @var calculated[]
      */
-    public function __construct($questions) {
-        foreach ($questions as $slot => $question) {
-            $question->_stats = $this->make_blank_question_stats();
-            $question->_stats->questionid = $question->id;
-            $question->_stats->slot = $slot;
-        }
-
-        $this->questions = $questions;
-    }
+    public $questionstats = array();
 
     /**
-     * @return object ready to hold all the question statistics.
+     * @var calculated_for_subquestion[]
      */
-    protected function make_blank_question_stats() {
-        $stats = new stdClass();
-        $stats->slot = null;
-        $stats->s = 0;
-        $stats->totalmarks = 0;
-        $stats->totalothermarks = 0;
-        $stats->markvariancesum = 0;
-        $stats->othermarkvariancesum = 0;
-        $stats->covariancesum = 0;
-        $stats->covariancemaxsum = 0;
-        $stats->subquestion = false;
-        $stats->subquestions = '';
-        $stats->covariancewithoverallmarksum = 0;
-        $stats->randomguessscore = null;
-        $stats->markarray = array();
-        $stats->othermarksarray = array();
-        return $stats;
-    }
+    public $subquestionstats = array();
 
     /**
-     * @param $qubaids qubaid_condition
-     * @return array with three items
-     *              - $lateststeps array of latest step data for the question usages
-     *              - $summarks    array of total marks for each usage, indexed by usage id
-     *              - $summarksavg the average of the total marks over all the usages
+     * @var float
      */
-    protected function get_latest_steps($qubaids) {
-        $dm = new question_engine_data_mapper();
+    protected $sumofmarkvariance = 0;
 
-        $fields = "    qas.id,
-    qa.questionusageid,
-    qa.questionid,
-    qa.slot,
-    qa.maxmark,
-    qas.fraction * qa.maxmark as mark";
+    protected $randomselectors = array();
 
-        $lateststeps = $dm->load_questions_usages_latest_steps($qubaids, array_keys($this->questions), $fields);
-        $summarks = array();
-        if ($lateststeps) {
-            foreach ($lateststeps as $step) {
-                if (!isset($summarks[$step->questionusageid])) {
-                    $summarks[$step->questionusageid] = 0;
-                }
-                $summarks[$step->questionusageid] += $step->mark;
-            }
-            $summarksavg = array_sum($summarks) / count($summarks);
-        } else {
-            $summarksavg = null;
+    /**
+     * Constructor.
+     *
+     * @param object[] questions to analyze, keyed by slot, also analyses sub questions for random questions.
+     *                              we expect some extra fields - slot, maxmark and number on the full question data objects.
+     */
+    public function __construct($questions) {
+        foreach ($questions as $slot => $question) {
+            $this->questionstats[$slot] = new calculated();
+            $this->questionstats[$slot]->questionid = $question->id;
+            $this->questionstats[$slot]->question = $question;
+            $this->questionstats[$slot]->slot = $slot;
+            $this->questionstats[$slot]->positions = $question->number;
+            $this->questionstats[$slot]->maxmark = $question->maxmark;
+            $this->questionstats[$slot]->randomguessscore = $this->get_random_guess_score($question);
         }
-
-        return array($lateststeps, $summarks, $summarksavg);
     }
 
     /**
-     * @param $qubaids qubaid_condition
+     * @param $qubaids \qubaid_condition
+     * @return array containing two arrays calculated[] and calculated_for_subquestion[].
      */
     public function calculate($qubaids) {
         set_time_limit(0);
@@ -124,38 +81,33 @@ class question_statistics {
         list($lateststeps, $summarks, $summarksavg) = $this->get_latest_steps($qubaids);
 
         if ($lateststeps) {
-            $subquestionstats = array();
 
             // Compute the statistics of position, and for random questions, work
             // out which questions appear in which positions.
             foreach ($lateststeps as $step) {
-                $this->initial_steps_walker($step, $this->questions[$step->slot]->_stats, $summarks);
+                $this->initial_steps_walker($step, $this->questionstats[$step->slot], $summarks);
 
                 // If this is a random question what is the real item being used?
-                if ($step->questionid != $this->questions[$step->slot]->id) {
-                    if (!isset($subquestionstats[$step->questionid])) {
-                        $subquestionstats[$step->questionid] = $this->make_blank_question_stats();
-                        $subquestionstats[$step->questionid]->questionid = $step->questionid;
-                        $subquestionstats[$step->questionid]->usedin = array();
-                        $subquestionstats[$step->questionid]->subquestion = true;
-                        $subquestionstats[$step->questionid]->differentweights = false;
-                        $subquestionstats[$step->questionid]->maxmark = $step->maxmark;
-                    } else if ($subquestionstats[$step->questionid]->maxmark != $step->maxmark) {
-                        $subquestionstats[$step->questionid]->differentweights = true;
+                if ($step->questionid != $this->questionstats[$step->slot]->questionid) {
+                    if (!isset($this->subquestionstats[$step->questionid])) {
+                        $this->subquestionstats[$step->questionid] = new calculated_for_subquestion();
+                        $this->subquestionstats[$step->questionid]->questionid = $step->questionid;
+                        $this->subquestionstats[$step->questionid]->maxmark = $step->maxmark;
+                    } else if ($this->subquestionstats[$step->questionid]->maxmark != $step->maxmark) {
+                        $this->subquestionstats[$step->questionid]->differentweights = true;
                     }
 
-                    $this->initial_steps_walker($step, $subquestionstats[$step->questionid], $summarks, false);
+                    $this->initial_steps_walker($step, $this->subquestionstats[$step->questionid], $summarks, false);
 
-                    $number = $this->questions[$step->slot]->number;
-                    $subquestionstats[$step->questionid]->usedin[$number] = $number;
+                    $number = $this->questionstats[$step->slot]->question->number;
+                    $this->subquestionstats[$step->questionid]->usedin[$number] = $number;
 
-                    $randomselectorstring = $this->questions[$step->slot]->category .
-                        '/' . $this->questions[$step->slot]->questiontext;
+                    $randomselectorstring = $this->questionstats[$step->slot]->question->category. '/'
+                                                                    .$this->questionstats[$step->slot]->question->questiontext;
                     if (!isset($this->randomselectors[$randomselectorstring])) {
                         $this->randomselectors[$randomselectorstring] = array();
                     }
-                    $this->randomselectors[$randomselectorstring][$step->questionid] =
-                        $step->questionid;
+                    $this->randomselectors[$randomselectorstring][$step->questionid] = $step->questionid;
                 }
             }
 
@@ -164,27 +116,27 @@ class question_statistics {
             }
 
             // Compute the statistics of question id, if we need any.
-            $this->subquestions = question_load_questions(array_keys($subquestionstats));
-            foreach ($this->subquestions as $qid => $subquestion) {
-                $subquestion->_stats = $subquestionstats[$qid];
-                $subquestion->maxmark = $subquestion->_stats->maxmark;
-                $subquestion->_stats->randomguessscore = $this->get_random_guess_score($subquestion);
+            $subquestions = question_load_questions(array_keys($this->subquestionstats));
+            foreach ($subquestions as $qid => $subquestion) {
+                $this->subquestionstats[$qid]->question = $subquestion;
+                $this->subquestionstats[$qid]->question->maxmark = $this->subquestionstats[$qid]->maxmark;
+                $this->subquestionstats[$qid]->randomguessscore = $this->get_random_guess_score($subquestion);
 
-                $this->initial_question_walker($subquestion->_stats);
+                $this->initial_question_walker($this->subquestionstats[$qid]);
 
-                if ($subquestionstats[$qid]->differentweights) {
+                if ($this->subquestionstats[$qid]->differentweights) {
                     // TODO output here really sucks, but throwing is too severe.
                     global $OUTPUT;
-                    echo $OUTPUT->notification(
-                        get_string('erroritemappearsmorethanoncewithdifferentweight',
-                                   'quiz_statistics', $this->subquestions[$qid]->name));
+                    $name = $this->subquestionstats[$qid]->question->name;
+                    echo $OUTPUT->notification( get_string('erroritemappearsmorethanoncewithdifferentweight',
+                                                            'quiz_statistics', $name));
                 }
 
-                if ($subquestion->_stats->usedin) {
-                    sort($subquestion->_stats->usedin, SORT_NUMERIC);
-                    $subquestion->_stats->positions = implode(',', $subquestion->_stats->usedin);
+                if ($this->subquestionstats[$qid]->usedin) {
+                    sort($this->subquestionstats[$qid]->usedin, SORT_NUMERIC);
+                    $this->subquestionstats[$qid]->positions = implode(',', $this->subquestionstats[$qid]->usedin);
                 } else {
-                    $subquestion->_stats->positions = '';
+                    $this->subquestionstats[$qid]->positions = '';
                 }
             }
 
@@ -194,106 +146,168 @@ class question_statistics {
             // This cannot be a foreach loop because we need to have both
             // $question and $nextquestion available, but apart from that it is
             // foreach ($this->questions as $qid => $question).
-            reset($this->questions);
-            while (list($slot, $question) = each($this->questions)) {
-                $nextquestion = current($this->questions);
-                $question->_stats->positions = $question->number;
-                $question->_stats->maxmark = $question->maxmark;
-                $question->_stats->randomguessscore = $this->get_random_guess_score($question);
-
-                $this->initial_question_walker($question->_stats);
-
-                if ($question->qtype == 'random') {
-                    $randomselectorstring = $question->category.'/'.$question->questiontext;
-                    if ($nextquestion && $nextquestion->qtype == 'random') {
-                        $nextrandomselectorstring = $nextquestion->category . '/' .
-                            $nextquestion->questiontext;
+            reset($this->questionstats);
+            while (list($slot, $questionstat) = each($this->questionstats)) {
+                $nextquestionstats = current($this->questionstats);
+
+                $this->initial_question_walker($questionstat);
+
+                if ($questionstat->question->qtype == 'random') {
+                    $randomselectorstring = $questionstat->question->category .'/'. $questionstat->question->questiontext;
+                    if ($nextquestionstats && $nextquestionstats->question->qtype == 'random') {
+                        $nextrandomselectorstring  =
+                            $nextquestionstats->question->category .'/'. $nextquestionstats->question->questiontext;
                         if ($randomselectorstring == $nextrandomselectorstring) {
                             continue; // Next loop iteration.
                         }
                     }
                     if (isset($this->randomselectors[$randomselectorstring])) {
-                        $question->_stats->subquestions = implode(',',
-                                                                  $this->randomselectors[$randomselectorstring]);
+                        $questionstat->subquestions = implode(',', $this->randomselectors[$randomselectorstring]);
                     }
                 }
             }
 
             // Go through the records one more time.
             foreach ($lateststeps as $step) {
-                $this->secondary_steps_walker($step, $this->questions[$step->slot]->_stats, $summarks, $summarksavg);
+                $this->secondary_steps_walker($step, $this->questionstats[$step->slot], $summarks, $summarksavg);
 
-                if ($this->questions[$step->slot]->qtype == 'random') {
-                    $this->secondary_steps_walker($step, $this->subquestions[$step->questionid]->_stats, $summarks, $summarksavg);
+                if ($this->questionstats[$step->slot]->subquestion) {
+                    $this->secondary_steps_walker($step, $this->subquestionstats[$step->questionid], $summarks, $summarksavg);
                 }
             }
 
             $sumofcovariancewithoverallmark = 0;
-            foreach ($this->questions as $slot => $question) {
-                $this->secondary_question_walker($question->_stats);
+            foreach ($this->questionstats as $questionstat) {
+                $this->secondary_question_walker($questionstat);
 
-                $this->sumofmarkvariance += $question->_stats->markvariance;
+                $this->sumofmarkvariance += $questionstat->markvariance;
 
-                if ($question->_stats->covariancewithoverallmark >= 0) {
-                    $sumofcovariancewithoverallmark +=
-                        sqrt($question->_stats->covariancewithoverallmark);
-                    $question->_stats->negcovar = 0;
-                } else {
-                    $question->_stats->negcovar = 1;
+                if ($questionstat->covariancewithoverallmark >= 0) {
+                    $sumofcovariancewithoverallmark += sqrt($questionstat->covariancewithoverallmark);
                 }
             }
 
-            foreach ($this->subquestions as $subquestion) {
-                $this->secondary_question_walker($subquestion->_stats);
+            foreach ($this->subquestionstats as $subquestionstat) {
+                $this->secondary_question_walker($subquestionstat);
             }
 
-            foreach ($this->questions as $question) {
+            foreach ($this->questionstats as $questionstat) {
                 if ($sumofcovariancewithoverallmark) {
-                    if ($question->_stats->negcovar) {
-                        $question->_stats->effectiveweight = null;
+                    if ($questionstat->negcovar) {
+                        $questionstat->effectiveweight = null;
                     } else {
-                        $question->_stats->effectiveweight = 100 *
-                            sqrt($question->_stats->covariancewithoverallmark) /
+                        $questionstat->effectiveweight = 100 * sqrt($questionstat->covariancewithoverallmark) /
                             $sumofcovariancewithoverallmark;
                     }
                 } else {
-                    $question->_stats->effectiveweight = null;
+                    $questionstat->effectiveweight = null;
                 }
             }
             $this->cache_stats($qubaids);
         }
-
-
+        return array($this->questionstats, $this->subquestionstats);
     }
 
     /**
-     * @param $qubaids qubaid_condition
+     * Load cached statistics from the database.
+     *
+     * @param $qubaids \qubaid_condition
+     * @return array containing two arrays calculated[] and calculated_for_subquestion[].
      */
-    protected function cache_stats($qubaids) {
+    public function get_cached($qubaids) {
         global $DB;
-        $cachetime = time();
-        foreach ($this->questions as $question) {
-            $question->_stats->hashcode = $qubaids->get_hash_code();
-            $question->_stats->timemodified = $cachetime;
-            $DB->insert_record('question_statistics', $question->_stats, false);
+        $timemodified = time() - self::TIME_TO_CACHE;
+        $questionstatrecs = $DB->get_record_select('question_statistics', 'hashcode = ? AND timemodified > ?',
+                                         array($qubaids->get_hash_code(), $timemodified));
+
+        $questionids = array();
+        foreach ($questionstatrecs as $fromdb) {
+            if (!$fromdb->slot) {
+                $questionids[] = $fromdb->questionid;
+            }
         }
+        $subquestions = question_load_questions($questionids);
+        foreach ($questionstatrecs as $fromdb) {
+            if ($fromdb->slot) {
+                $this->questionstats[$fromdb->slot]->populate_from_record($fromdb);
+                // Array created in constructor and populated from question.
+            } else {
+                $this->subquestionstats[$fromdb->questionid] = new calculated_for_subquestion();
+                $this->subquestionstats[$fromdb->questionid]->populate_from_record($fromdb);
+                $this->subquestionstats[$fromdb->questionid]->question = $subquestions[$fromdb->questionid];
+            }
+        }
+        return array($this->questionstats, $this->subquestionstats);
+    }
 
-        foreach ($this->subquestions as $subquestion) {
-            $subquestion->_stats->hashcode = $qubaids->get_hash_code();
-            $subquestion->_stats->timemodified = $cachetime;
-            $DB->insert_record('question_statistics', $subquestion->_stats, false);
+    /**
+     * Find time of non-expired statistics in the database.
+     *
+     * @param $qubaids \qubaid_condition
+     * @return integer|boolean Time of cached record that matches this qubaid_condition or false is non found.
+     */
+    public function get_last_calculated_time($qubaids) {
+        global $DB;
+
+        $timemodified = time() - self::TIME_TO_CACHE;
+        return $DB->get_field_select('question_statistics', 'timemodified', 'hashcode = ? AND timemodified > ?',
+                                     array($qubaids->get_hash_code(), $timemodified));
+    }
+
+    /** @var integer Time after which statistics are automatically recomputed. */
+    const TIME_TO_CACHE = 900; // 15 minutes.
+
+    /**
+     * Used when computing Coefficient of Internal Consistency by quiz statistics.
+     *
+     * @return float
+     */
+    public function get_sum_of_mark_variance() {
+        return $this->sumofmarkvariance;
+    }
+
+    /**
+     * @param $qubaids \qubaid_condition
+     * @return array with three items
+     *              - $lateststeps array of latest step data for the question usages
+     *              - $summarks    array of total marks for each usage, indexed by usage id
+     *              - $summarksavg the average of the total marks over all the usages
+     */
+    protected function get_latest_steps($qubaids) {
+        $dm = new \question_engine_data_mapper();
+
+        $fields = "    qas.id,
+    qa.questionusageid,
+    qa.questionid,
+    qa.slot,
+    qa.maxmark,
+    qas.fraction * qa.maxmark as mark";
+
+        $lateststeps = $dm->load_questions_usages_latest_steps($qubaids, array_keys($this->questionstats), $fields);
+        $summarks = array();
+        if ($lateststeps) {
+            foreach ($lateststeps as $step) {
+                if (!isset($summarks[$step->questionusageid])) {
+                    $summarks[$step->questionusageid] = 0;
+                }
+                $summarks[$step->questionusageid] += $step->mark;
+            }
+            $summarksavg = array_sum($summarks) / count($summarks);
+        } else {
+            $summarksavg = null;
         }
 
+        return array($lateststeps, $summarks, $summarksavg);
     }
 
     /**
      * Update $stats->totalmarks, $stats->markarray, $stats->totalothermarks
      * and $stats->othermarksarray to include another state.
      *
-     * @param object $step the state to add to the statistics.
-     * @param object $stats the question statistics we are accumulating.
-     * @param array  $summarks of the sum of marks for each question usage, indexed by question usage id
-     * @param bool $positionstat whether this is a statistic of position of question.
+     * @param object $step         the state to add to the statistics.
+     * @param calculated $stats        the question statistics we are accumulating.
+     * @param array  $summarks     of the sum of marks for each question usage, indexed by question usage id
+     * @param bool   $positionstat whether this is a statistic of position of question.
      */
     protected function initial_steps_walker($step, $stats, $summarks, $positionstat = true) {
         $stats->s++;
@@ -314,7 +328,7 @@ class question_statistics {
      * Perform some computations on the per-question statistics calculations after
      * we have been through all the states.
      *
-     * @param object $stats quetsion stats to update.
+     * @param calculated $stats question stats to update.
      */
     protected function initial_question_walker($stats) {
         $stats->markaverage = $stats->totalmarks / $stats->s;
@@ -335,9 +349,9 @@ class question_statistics {
      * Now we know the averages, accumulate the date needed to compute the higher
      * moments of the question scores.
      *
-     * @param object $step     the state to add to the statistics.
-     * @param object $stats    the question statistics we are accumulating.
-     * @param array  $summarks of the sum of marks for each question usage, indexed by question usage id
+     * @param object $step        the state to add to the statistics.
+     * @param calculated $stats       the question statistics we are accumulating.
+     * @param array  $summarks    of the sum of marks for each question usage, indexed by question usage id
      * @param float  $summarksavg the average sum of marks for all question usages
      */
     protected function secondary_steps_walker($step, $stats, $summarks, $summarksavg) {
@@ -345,14 +359,12 @@ class question_statistics {
         if ($stats->subquestion) {
             $othermarkdifference = $summarks[$step->questionusageid] - $stats->othermarkaverage;
         } else {
-            $othermarkdifference = $summarks[$step->questionusageid] - $step->mark -
-                    $stats->othermarkaverage;
+            $othermarkdifference = $summarks[$step->questionusageid] - $step->mark - $stats->othermarkaverage;
         }
         $overallmarkdifference = $summarks[$step->questionusageid] - $summarksavg;
 
         $sortedmarkdifference = array_shift($stats->markarray) - $stats->markaverage;
-        $sortedothermarkdifference = array_shift($stats->othermarksarray) -
-                $stats->othermarkaverage;
+        $sortedothermarkdifference = array_shift($stats->othermarksarray) - $stats->othermarkaverage;
 
         $stats->markvariancesum += pow($markdifference, 2);
         $stats->othermarkvariancesum += pow($othermarkdifference, 2);
@@ -364,18 +376,24 @@ class question_statistics {
     /**
      * Perform more per-question statistics calculations.
      *
-     * @param object $stats quetsion stats to update.
+     * @param calculated $stats question stats to update.
      */
     protected function secondary_question_walker($stats) {
+
         if ($stats->s > 1) {
             $stats->markvariance = $stats->markvariancesum / ($stats->s - 1);
             $stats->othermarkvariance = $stats->othermarkvariancesum / ($stats->s - 1);
             $stats->covariance = $stats->covariancesum / ($stats->s - 1);
             $stats->covariancemax = $stats->covariancemaxsum / ($stats->s - 1);
             $stats->covariancewithoverallmark = $stats->covariancewithoverallmarksum /
-                    ($stats->s - 1);
+                ($stats->s - 1);
             $stats->sd = sqrt($stats->markvariancesum / ($stats->s - 1));
 
+            if ($stats->covariancewithoverallmark >= 0) {
+                $stats->negcovar = 0;
+            } else {
+                $stats->negcovar = 1;
+            }
         } else {
             $stats->markvariance = null;
             $stats->othermarkvariance = null;
@@ -383,18 +401,21 @@ class question_statistics {
             $stats->covariancemax = null;
             $stats->covariancewithoverallmark = null;
             $stats->sd = null;
+            $stats->negcovar = 0;
         }
 
+
+
         if ($stats->markvariance * $stats->othermarkvariance) {
             $stats->discriminationindex = 100 * $stats->covariance /
-                    sqrt($stats->markvariance * $stats->othermarkvariance);
+                sqrt($stats->markvariance * $stats->othermarkvariance);
         } else {
             $stats->discriminationindex = null;
         }
 
         if ($stats->covariancemax) {
             $stats->discriminativeefficiency = 100 * $stats->covariance /
-                    $stats->covariancemax;
+                $stats->covariancemax;
         } else {
             $stats->discriminativeefficiency = null;
         }
@@ -405,42 +426,20 @@ class question_statistics {
      * @return number the random guess score for this question.
      */
     protected function get_random_guess_score($questiondata) {
-        return question_bank::get_qtype(
-                $questiondata->qtype, false)->get_random_guess_score($questiondata);
-    }
-
-    /**
-     * Used when computing CIC.
-     * @return number
-     */
-    public function get_sum_of_mark_variance() {
-        return $this->sumofmarkvariance;
+        return \question_bank::get_qtype(
+            $questiondata->qtype, false)->get_random_guess_score($questiondata);
     }
 
     /**
-     * @param qubaid_condition $qubaids
+     * @param $qubaids \qubaid_condition
      */
-    public function get_cached($qubaids) {
-        global $DB;
-        $questionstats = $DB->get_records('question_statistics',
-                                          array('hashcode' => $qubaids->get_hash_code()));
-
-        $subquestionstats = array();
-        foreach ($questionstats as $stat) {
-            if ($stat->slot) {
-                $this->questions[$stat->slot]->_stats = $stat;
-            } else {
-                $subquestionstats[$stat->questionid] = $stat;
-            }
+    protected function cache_stats($qubaids) {
+        foreach ($this->questionstats as $questionstat) {
+            $questionstat->cache($qubaids);
         }
 
-        if (!empty($subquestionstats)) {
-            $subqstofetch = array_keys($subquestionstats);
-            $this->subquestions = question_load_questions($subqstofetch);
-            foreach ($this->subquestions as $subqid => $subq) {
-                $this->subquestions[$subqid]->_stats = $subquestionstats[$subqid];
-                $this->subquestions[$subqid]->maxmark = $subq->defaultmark;
-            }
+        foreach ($this->subquestionstats as $subquestionstat) {
+            $subquestionstat->cache($qubaids);
         }
     }
 
similarity index 91%
rename from question/engine/responseanalysis.php
rename to question/classes/statistics/responses/analyser.php
index 0118dfd..ae3d206 100644 (file)
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
-
+namespace core_question\statistics\responses;
 defined('MOODLE_INTERNAL') || die();
 
-
 /**
  * This class can store and compute the analysis of the responses to a particular
  * question.
@@ -37,7 +36,7 @@ defined('MOODLE_INTERNAL') || die();
  * @author    Jamie Pratt <me@jamiep.org>
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class question_response_analyser {
+class analyser {
     /** @var object the data from the database that defines the question. */
     protected $questiondata;
 
@@ -67,9 +66,7 @@ class question_response_analyser {
     public function __construct($questiondata) {
         $this->questiondata = $questiondata;
 
-        $this->responseclasses =
-                question_bank::get_qtype($questiondata->qtype)->get_possible_responses(
-                        $questiondata);
+        $this->responseclasses = \question_bank::get_qtype($questiondata->qtype)->get_possible_responses($questiondata);
         foreach ($this->responseclasses as $subpartid => $responseclasses) {
             foreach ($responseclasses as $responseclassid => $notused) {
                 $this->responses[$subpartid][$responseclassid] = array();
@@ -121,11 +118,11 @@ class question_response_analyser {
     /**
      * Analyse all the response data for for all the specified attempts at
      * this question.
-     * @param qubaid_condition $qubaids which attempts to consider.
+     * @param \qubaid_condition $qubaids which attempts to consider.
      */
     public function calculate($qubaids) {
         // Load data.
-        $dm = new question_engine_data_mapper();
+        $dm = new \question_engine_data_mapper();
         $questionattempts = $dm->load_attempts_at_question($this->questiondata->id, $qubaids);
 
         // Analyse it.
@@ -138,16 +135,16 @@ class question_response_analyser {
 
     /**
      * Analyse the data from one question attempt.
-     * @param question_attempt $qa the data to analyse.
+     * @param \question_attempt $qa the data to analyse.
      */
-    protected function add_data_from_one_attempt(question_attempt $qa) {
-        $blankresponse = question_classified_response::no_response();
+    protected function add_data_from_one_attempt(\question_attempt $qa) {
+        $blankresponse = \question_classified_response::no_response();
 
         $partresponses = $qa->classify_response();
         foreach ($partresponses as $subpartid => $partresponse) {
             if (!isset($this->responses[$subpartid][$partresponse->responseclassid]
                     [$partresponse->response])) {
-                $resp = new stdClass();
+                $resp = new \stdClass();
                 $resp->count = 0;
                 if (!is_null($partresponse->fraction)) {
                     $resp->fraction = $partresponse->fraction;
@@ -167,7 +164,7 @@ class question_response_analyser {
 
     /**
      * Store the computed response analysis in the question_response_analysis table.
-     * @param qubaid_condition $qubaids
+     * @param \qubaid_condition $qubaids
      * data corresponding to.
      * @return bool true if cached data was found in the database and loaded, otherwise false, to mean no data was loaded.
      */
@@ -181,7 +178,7 @@ class question_response_analyser {
         }
 
         foreach ($rows as $row) {
-            $this->responses[$row->subqid][$row->aid][$row->response] = new stdClass();
+            $this->responses[$row->subqid][$row->aid][$row->response] = new \stdClass();
             $this->responses[$row->subqid][$row->aid][$row->response]->count = $row->rcount;
             $this->responses[$row->subqid][$row->aid][$row->response]->fraction = $row->credit;
         }
@@ -190,7 +187,7 @@ class question_response_analyser {
 
     /**
      * Store the computed response analysis in the question_response_analysis table.
-     * @param qubaid_condition $qubaids
+     * @param \qubaid_condition $qubaids
      */
     public function store_cached($qubaids) {
         global $DB;
@@ -199,7 +196,7 @@ class question_response_analyser {
         foreach ($this->responses as $subpartid => $partdata) {
             foreach ($partdata as $responseclassid => $classdata) {
                 foreach ($classdata as $response => $data) {
-                    $row = new stdClass();
+                    $row = new \stdClass();
                     $row->hashcode = $qubaids->get_hash_code();
                     $row->questionid = $this->questiondata->id;
                     $row->subqid = $subpartid;