diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index a4a39ffc1cd..54099770963 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -864,11 +864,9 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2012062000.01); } - // Moodle v2.3.0 release upgrade line // Put any upgrade step following this - if ($oldversion < 2012062500.02) { // Drop some old backup tables, not used anymore @@ -1497,7 +1495,6 @@ function xmldb_main_upgrade($oldversion) { // Moodle v2.4.0 release upgrade line // Put any upgrade step following this - if ($oldversion < 2012120300.01) { // Make sure site-course has format='site' //MDL-36840 diff --git a/mod/quiz/report/statistics/classes/calculated.php b/mod/quiz/report/statistics/classes/calculated.php index 40a62107e72..8103b7c752d 100644 --- a/mod/quiz/report/statistics/classes/calculated.php +++ b/mod/quiz/report/statistics/classes/calculated.php @@ -166,7 +166,6 @@ class quiz_statistics_calculated { } } - /** * @var array of names of properties of this class that are cached in db record. */ diff --git a/mod/quiz/report/statistics/classes/calculator.php b/mod/quiz/report/statistics/classes/calculator.php index 6a0d6b8ac25..85c8cb78f56 100644 --- a/mod/quiz/report/statistics/classes/calculator.php +++ b/mod/quiz/report/statistics/classes/calculator.php @@ -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) { @@ -92,7 +90,6 @@ class quiz_statistics_calculator { } } - $quizstats->cache(quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents, $useallattempts)); return $quizstats; diff --git a/mod/quiz/report/statistics/db/install.php b/mod/quiz/report/statistics/db/install.php index 4a15c7d6c03..e148f9ed4cc 100644 --- a/mod/quiz/report/statistics/db/install.php +++ b/mod/quiz/report/statistics/db/install.php @@ -21,10 +21,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - defined('MOODLE_INTERNAL') || die(); - /** * Post-install script */ diff --git a/mod/quiz/report/statistics/db/upgrade.php b/mod/quiz/report/statistics/db/upgrade.php index 4beb458c895..3581dfe1837 100644 --- a/mod/quiz/report/statistics/db/upgrade.php +++ b/mod/quiz/report/statistics/db/upgrade.php @@ -22,10 +22,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - defined('MOODLE_INTERNAL') || die(); - /** * Quiz statistics report upgrade code. */ diff --git a/mod/quiz/report/statistics/lib.php b/mod/quiz/report/statistics/lib.php index 2dc07516897..416981be39f 100644 --- a/mod/quiz/report/statistics/lib.php +++ b/mod/quiz/report/statistics/lib.php @@ -22,10 +22,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - defined('MOODLE_INTERNAL') || die(); - /** * Serve questiontext files in the question text when they are displayed in this report. * diff --git a/mod/quiz/report/statistics/report.php b/mod/quiz/report/statistics/report.php index 21d88acfc19..5179ee88cd5 100644 --- a/mod/quiz/report/statistics/report.php +++ b/mod/quiz/report/statistics/report.php @@ -22,14 +22,11 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - 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 +43,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; /** @@ -108,7 +105,6 @@ class quiz_statistics_report extends quiz_default_report { $qubaids = quiz_statistics_qubaids_condition($quiz->id, $currentgroup, $groupstudents, $useallattempts); - // If recalculate was requested, handle that. if ($recalculate && confirm_sesskey()) { $this->clear_cached_data($qubaids); @@ -131,14 +127,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 +169,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 +199,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('' . @@ -219,9 +213,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('' . @@ -231,7 +225,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 +237,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 +249,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 +269,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.' '.$datumfromtable['actions']); + $questionstat->question->name.' '.$datumfromtable['actions']); $questioninfotable->data[] = array(get_string('questiontype', 'quiz_statistics'), $datumfromtable['icon'] . ' ' . - question_bank::get_qtype($question->qtype, false)->menu_name() . ' ' . + question_bank::get_qtype($questionstat->question->qtype, false)->menu_name() . ' ' . $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 +306,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 +333,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 +366,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 +408,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 +512,36 @@ 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()); - - $questions = $qstats->questions; - $subquestions = $qstats->subquestions; + $groupstudents, count($questions), $qcalc->get_sum_of_mark_variance()); 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 +550,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); } } diff --git a/mod/quiz/report/statistics/statistics_form.php b/mod/quiz/report/statistics/statistics_form.php index a583ea915b1..02fb2f44baa 100644 --- a/mod/quiz/report/statistics/statistics_form.php +++ b/mod/quiz/report/statistics/statistics_form.php @@ -22,12 +22,10 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/formslib.php'); - /** * This is the settings form for the quiz statistics report. * diff --git a/mod/quiz/report/statistics/statistics_graph.php b/mod/quiz/report/statistics/statistics_graph.php index 6f762b3513e..bb21b0f70c1 100644 --- a/mod/quiz/report/statistics/statistics_graph.php +++ b/mod/quiz/report/statistics/statistics_graph.php @@ -28,7 +28,6 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - require_once(dirname(__FILE__) . '/../../../../config.php'); require_once($CFG->libdir . '/graphlib.php'); require_once($CFG->dirroot . '/mod/quiz/locallib.php'); diff --git a/mod/quiz/report/statistics/statistics_question_table.php b/mod/quiz/report/statistics/statistics_question_table.php index 508775b79d3..4bfc4115ab7 100644 --- a/mod/quiz/report/statistics/statistics_question_table.php +++ b/mod/quiz/report/statistics/statistics_question_table.php @@ -22,12 +22,10 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir . '/tablelib.php'); - /** * This table shows statistics about a particular question. * @@ -40,12 +38,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 +51,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 +133,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); } } diff --git a/mod/quiz/report/statistics/statistics_table.php b/mod/quiz/report/statistics/statistics_table.php index 393cedec9e4..73217a5909b 100644 --- a/mod/quiz/report/statistics/statistics_table.php +++ b/mod/quiz/report/statistics/statistics_table.php @@ -22,12 +22,10 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - defined('MOODLE_INTERNAL') || die(); require_once($CFG->libdir.'/tablelib.php'); - /** * This table has one row for each question in the quiz, with sub-rows when * random questions appear. There are columns for the various statistics. @@ -134,61 +132,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 +196,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 +205,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 +292,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() { diff --git a/mod/quiz/report/statistics/tests/statistics_test.php b/mod/quiz/report/statistics/tests/statistics_test.php index db191e439f2..cc2988807fb 100644 --- a/mod/quiz/report/statistics/tests/statistics_test.php +++ b/mod/quiz/report/statistics/tests/statistics_test.php @@ -23,23 +23,26 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - 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'); - /** * Test helper subclass of question_statistics * * @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 +98,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 +128,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); } } } diff --git a/mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php b/mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php index 32cb585349d..94c3a8b7d1d 100644 --- a/mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php +++ b/mod/quiz/report/statistics/tests/stats_from_steps_walkthrough_test.php @@ -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 index 00000000000..e8e3404b2f9 --- /dev/null +++ b/question/classes/statistics/questions/calculated.php @@ -0,0 +1,177 @@ +. + +/** + * 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 + * @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 + * @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 index 00000000000..061cc969387 --- /dev/null +++ b/question/classes/statistics/questions/calculated_for_subquestion.php @@ -0,0 +1,37 @@ +. + +/** + * 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; +} diff --git a/question/engine/statistics.php b/question/classes/statistics/questions/calculator.php similarity index 50% rename from question/engine/statistics.php rename to question/classes/statistics/questions/calculator.php index e5f4d12a1f6..55bb8031e3b 100644 --- a/question/engine/statistics.php +++ b/question/classes/statistics/questions/calculator.php @@ -15,7 +15,7 @@ // along with Moodle. If not, see . /** - * 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 @@ -24,10 +24,9 @@ * @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,61 +34,248 @@ defined('MOODLE_INTERNAL') || die(); * @author Jamie Pratt * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class question_statistics { - public $questions; - public $subquestions = array(); +class calculator { - protected $summarksavg; + /** + * @var calculated[] + */ + public $questionstats = array(); + /** + * @var calculated_for_subquestion[] + */ + public $subquestionstats = array(); + + /** + * @var float + */ protected $sumofmarkvariance = 0; + protected $randomselectors = array(); /** * Constructor. * - * @param $questions array the main questions indexed by slot. + * @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) { - $question->_stats = $this->make_blank_question_stats(); - $question->_stats->questionid = $question->id; - $question->_stats->slot = $slot; + $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); } - - $this->questions = $questions; } /** - * @return object ready to hold all the question statistics. + * @param $qubaids \qubaid_condition + * @return array containing two arrays calculated[] and 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 function calculate($qubaids) { + set_time_limit(0); + + list($lateststeps, $summarks, $summarksavg) = $this->get_latest_steps($qubaids); + + if ($lateststeps) { + + // 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->questionstats[$step->slot], $summarks); + + // If this is a random question what is the real item being used? + 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, $this->subquestionstats[$step->questionid], $summarks, false); + + $number = $this->questionstats[$step->slot]->question->number; + $this->subquestionstats[$step->questionid]->usedin[$number] = $number; + + $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; + } + } + + foreach ($this->randomselectors as $key => $notused) { + ksort($this->randomselectors[$key]); + } + + // Compute the statistics of question id, if we need any. + $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($this->subquestionstats[$qid]); + + if ($this->subquestionstats[$qid]->differentweights) { + // TODO output here really sucks, but throwing is too severe. + global $OUTPUT; + $name = $this->subquestionstats[$qid]->question->name; + echo $OUTPUT->notification( get_string('erroritemappearsmorethanoncewithdifferentweight', + 'quiz_statistics', $name)); + } + + if ($this->subquestionstats[$qid]->usedin) { + sort($this->subquestionstats[$qid]->usedin, SORT_NUMERIC); + $this->subquestionstats[$qid]->positions = implode(',', $this->subquestionstats[$qid]->usedin); + } else { + $this->subquestionstats[$qid]->positions = ''; + } + } + + // Finish computing the averages, and put the subquestion data into the + // corresponding questions. + + // 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->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])) { + $questionstat->subquestions = implode(',', $this->randomselectors[$randomselectorstring]); + } + } + } + + // Go through the records one more time. + foreach ($lateststeps as $step) { + $this->secondary_steps_walker($step, $this->questionstats[$step->slot], $summarks, $summarksavg); + + if ($this->questionstats[$step->slot]->subquestions) { + $this->secondary_steps_walker($step, $this->subquestionstats[$step->questionid], $summarks, $summarksavg); + } + } + + $sumofcovariancewithoverallmark = 0; + foreach ($this->questionstats as $questionstat) { + $this->secondary_question_walker($questionstat); + + $this->sumofmarkvariance += $questionstat->markvariance; + + if ($questionstat->covariancewithoverallmark >= 0) { + $sumofcovariancewithoverallmark += sqrt($questionstat->covariancewithoverallmark); + } + } + + foreach ($this->subquestionstats as $subquestionstat) { + $this->secondary_question_walker($subquestionstat); + } + + foreach ($this->questionstats as $questionstat) { + if ($sumofcovariancewithoverallmark) { + if ($questionstat->negcovar) { + $questionstat->effectiveweight = null; + } else { + $questionstat->effectiveweight = 100 * sqrt($questionstat->covariancewithoverallmark) / + $sumofcovariancewithoverallmark; + } + } else { + $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[]. + */ + public function get_cached($qubaids) { + global $DB; + $timemodified = time() - self::TIME_TO_CACHE; + $questionstatrecs = $DB->get_records_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); + } + + /** + * 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 > ? '. + 'ORDER BY timemodified DESC LIMIT 1', + 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(); + $dm = new \question_engine_data_mapper(); $fields = " qas.id, qa.questionusageid, @@ -98,7 +284,7 @@ class question_statistics { qa.maxmark, qas.fraction * qa.maxmark as mark"; - $lateststeps = $dm->load_questions_usages_latest_steps($qubaids, array_keys($this->questions), $fields); + $lateststeps = $dm->load_questions_usages_latest_steps($qubaids, array_keys($this->questionstats), $fields); $summarks = array(); if ($lateststeps) { foreach ($lateststeps as $step) { @@ -115,185 +301,14 @@ class question_statistics { return array($lateststeps, $summarks, $summarksavg); } - /** - * @param $qubaids qubaid_condition - */ - public function calculate($qubaids) { - set_time_limit(0); - - 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); - - // 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; - } - - $this->initial_steps_walker($step, $subquestionstats[$step->questionid], $summarks, false); - - $number = $this->questions[$step->slot]->number; - $subquestionstats[$step->questionid]->usedin[$number] = $number; - - $randomselectorstring = $this->questions[$step->slot]->category . - '/' . $this->questions[$step->slot]->questiontext; - if (!isset($this->randomselectors[$randomselectorstring])) { - $this->randomselectors[$randomselectorstring] = array(); - } - $this->randomselectors[$randomselectorstring][$step->questionid] = - $step->questionid; - } - } - - foreach ($this->randomselectors as $key => $notused) { - ksort($this->randomselectors[$key]); - } - - // 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); - - $this->initial_question_walker($subquestion->_stats); - - if ($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)); - } - - if ($subquestion->_stats->usedin) { - sort($subquestion->_stats->usedin, SORT_NUMERIC); - $subquestion->_stats->positions = implode(',', $subquestion->_stats->usedin); - } else { - $subquestion->_stats->positions = ''; - } - } - - // Finish computing the averages, and put the subquestion data into the - // corresponding questions. - - // 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; - if ($randomselectorstring == $nextrandomselectorstring) { - continue; // Next loop iteration. - } - } - if (isset($this->randomselectors[$randomselectorstring])) { - $question->_stats->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); - - if ($this->questions[$step->slot]->qtype == 'random') { - $this->secondary_steps_walker($step, $this->subquestions[$step->questionid]->_stats, $summarks, $summarksavg); - } - } - - $sumofcovariancewithoverallmark = 0; - foreach ($this->questions as $slot => $question) { - $this->secondary_question_walker($question->_stats); - - $this->sumofmarkvariance += $question->_stats->markvariance; - - if ($question->_stats->covariancewithoverallmark >= 0) { - $sumofcovariancewithoverallmark += - sqrt($question->_stats->covariancewithoverallmark); - $question->_stats->negcovar = 0; - } else { - $question->_stats->negcovar = 1; - } - } - - foreach ($this->subquestions as $subquestion) { - $this->secondary_question_walker($subquestion->_stats); - } - - foreach ($this->questions as $question) { - if ($sumofcovariancewithoverallmark) { - if ($question->_stats->negcovar) { - $question->_stats->effectiveweight = null; - } else { - $question->_stats->effectiveweight = 100 * - sqrt($question->_stats->covariancewithoverallmark) / - $sumofcovariancewithoverallmark; - } - } else { - $question->_stats->effectiveweight = null; - } - } - $this->cache_stats($qubaids); - } - - - } - - /** - * @param $qubaids qubaid_condition - */ - protected function cache_stats($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); - } - - foreach ($this->subquestions as $subquestion) { - $subquestion->_stats->hashcode = $qubaids->get_hash_code(); - $subquestion->_stats->timemodified = $cachetime; - $DB->insert_record('question_statistics', $subquestion->_stats, false); - } - - } - /** * 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 +329,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 +350,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 +360,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 +377,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 +402,19 @@ 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 +425,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); + return \question_bank::get_qtype( + $questiondata->qtype, false)->get_random_guess_score($questiondata); } /** - * Used when computing CIC. - * @return number + * @param $qubaids \qubaid_condition */ - public function get_sum_of_mark_variance() { - return $this->sumofmarkvariance; - } - - /** - * @param qubaid_condition $qubaids - */ - 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); } } diff --git a/question/engine/responseanalysis.php b/question/classes/statistics/responses/analyser.php similarity index 91% rename from question/engine/responseanalysis.php rename to question/classes/statistics/responses/analyser.php index 0118dfd9584..ae3d206242b 100644 --- a/question/engine/responseanalysis.php +++ b/question/classes/statistics/responses/analyser.php @@ -25,10 +25,9 @@ * @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 * @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; diff --git a/question/engine/statisticslib.php b/question/engine/statisticslib.php index ab99fc05a23..363c1024a2e 100644 --- a/question/engine/statisticslib.php +++ b/question/engine/statisticslib.php @@ -24,10 +24,8 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - defined('MOODLE_INTERNAL') || die(); - /** * Question statistics cron code. Deletes cached stats more than a certain age. */