MDL-43338 quiz statistics : refactoring question statistics code

to improve readability and maintainability.
This commit is contained in:
James Pratt 2013-12-11 16:59:27 +07:00
parent 974c2cdc03
commit c3e2e754dd
8 changed files with 697 additions and 339 deletions

View File

@ -38,8 +38,6 @@ require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
*/
class quiz_statistics_report extends quiz_default_report {
const SUBQ_AND_VARIANT_ROW_LIMIT = 10;
/**
* @var context_module
*/
@ -145,13 +143,12 @@ class quiz_statistics_report extends quiz_default_report {
if (!$nostudentsingroup) {
// Get the data to be displayed.
$progress = $this->get_progress_trace_instance();
list($quizstats, $questionstats, $subquestionstats) =
list($quizstats, $questionstats) =
$this->get_all_stats_and_analysis($quiz, $whichattempts, $groupstudents, $questions, $progress);
} else {
// Or create empty stats containers.
$quizstats = new \quiz_statistics\calculated($whichattempts);
$questionstats = array();
$subquestionstats = array();
$questionstats = new \core_question\statistics\questions\all_calculated_for_qubaid_condition();
}
// Set up the table, if there is data.
@ -173,6 +170,10 @@ class quiz_statistics_report extends quiz_default_report {
echo $OUTPUT->notification(get_string('noattempts', 'quiz'));
}
foreach($questionstats->any_error_messages() as $errormessage) {
echo $OUTPUT->notification($errormessage);
}
// Print display options form.
$mform->display();
}
@ -183,7 +184,7 @@ 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(), $questionstats, $subquestionstats);
$this->output_quiz_structure_analysis_table($questionstats);
if ($this->table->is_downloading() == 'xhtml' && $quizstats->s() != 0) {
$this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts);
@ -193,13 +194,14 @@ class quiz_statistics_report extends quiz_default_report {
if (question_bank::get_qtype(
$question->qtype, false)->can_analyse_responses()) {
$this->output_individual_question_response_analysis(
$question, $questionstats[$slot]->s, $reporturl, $qubaids);
$question, $questionstats->for_slot($slot)->s, $reporturl, $qubaids);
} else if (!empty($questionstats[$slot]->subquestions)) {
$subitemstodisplay = explode(',', $questionstats[$slot]->subquestions);
foreach ($subitemstodisplay as $subitemid) {
$this->output_individual_question_response_analysis(
$subquestionstats[$subitemid]->question, $subquestionstats[$subitemid]->s, $reporturl, $qubaids);
} else if ($subqids = $questionstats->for_slot($slot)->get_sub_question_ids()) {
foreach ($subqids as $subqid) {
$this->output_individual_question_response_analysis($questionstats->for_subq($subqid)->question,
$questionstats->for_subq($subqid)->s,
$reporturl,
$qubaids);
}
}
}
@ -213,8 +215,11 @@ class quiz_statistics_report extends quiz_default_report {
print_error('questiondoesnotexist', 'question');
}
$this->output_individual_question_data($quiz, $questionstats[$slot]);
$this->output_individual_question_response_analysis($questions[$slot], $questionstats[$slot]->s, $reporturl, $qubaids);
$this->output_individual_question_data($quiz, $questionstats->for_slot($slot));
$this->output_individual_question_response_analysis($questions[$slot],
$questionstats->for_slot($slot)->s,
$reporturl,
$qubaids);
// Back to overview link.
echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
@ -223,13 +228,15 @@ class quiz_statistics_report extends quiz_default_report {
} else if ($qid) {
// Report on an individual sub-question indexed questionid.
if (!isset($subquestionstats[$qid])) {
if (is_null($questionstats->for_subq($qid))) {
print_error('questiondoesnotexist', 'question');
}
$this->output_individual_question_data($quiz, $subquestionstats[$qid]);
$this->output_individual_question_response_analysis($subquestionstats[$qid]->question,
$subquestionstats[$qid]->s, $reporturl, $qubaids);
$this->output_individual_question_data($quiz, $questionstats->for_subq($qid));
$this->output_individual_question_response_analysis($questionstats->for_subq($qid)->question,
$questionstats->for_subq($qid)->s,
$reporturl,
$qubaids);
// Back to overview link.
echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
@ -240,7 +247,9 @@ class quiz_statistics_report extends quiz_default_report {
// Downloading overview report.
$quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
$this->download_quiz_info_table($quizinfo);
$this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats);
if ($quizstats->s()) {
$this->output_quiz_structure_analysis_table($questionstats);
}
$this->table->finish_output();
} else {
@ -252,7 +261,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'), 3);
$this->output_quiz_structure_analysis_table($quizstats->s(), $questionstats, $subquestionstats);
$this->output_quiz_structure_analysis_table($questionstats);
$this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts);
}
}
@ -402,100 +411,25 @@ 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 \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.
* @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats the stats for all questions in
* the quiz including subqs and
* variants.
*/
protected function output_quiz_structure_analysis_table($s, $questionstats, $subquestionstats) {
if (!$s) {
return;
}
foreach ($questionstats as $questionstat) {
protected function output_quiz_structure_analysis_table($questionstats) {
foreach ($questionstats->get_all_slots() as $slot) {
// Output the data for these question statistics.
$this->table->add_data_keyed($this->table->format_row($questionstat));
if (count($questionstat->variantstats) > 1) {
if (count($questionstat->variantstats) > static::SUBQ_AND_VARIANT_ROW_LIMIT) {
$statstoadd = $this->find_min_median_and_max_facility_stats_objects($questionstat->variantstats);
} else {
ksort($questionstat->variantstats);
$statstoadd = $questionstat->variantstats;
}
$this->add_array_of_rows_to_table($statstoadd);
}
$this->table->add_data_keyed($this->table->format_row($questionstats->for_slot($slot)));
if (empty($questionstat->subquestions)) {
continue;
}
// And its sub-questions, if it has any.
$subitemstodisplay = explode(',', $questionstat->subquestions);
// We need to get all variants out of sub-questions to count them and possibly find min, median and max.
$displayorder = 1;
$subqvariants = array();
foreach ($subitemstodisplay as $subitemid) {
if (count($subquestionstats[$subitemid]->variantstats) > 1) {
ksort($subquestionstats[$subitemid]->variantstats);
foreach ($subquestionstats[$subitemid]->variantstats as $variantstat) {
$variantstat->subqdisplayorder = $displayorder;
$variantstat->question->number = $questionstat->question->number;
$subqvariants[] = $variantstat;
}
}
$displayorder++;
}
if (count($subqvariants) > static::SUBQ_AND_VARIANT_ROW_LIMIT) {
// Too many variants from randomly selected questions.
$toadd = $this->find_min_median_and_max_facility_stats_objects($subqvariants);
$this->add_array_of_rows_to_table($toadd);
} else if (count($subitemstodisplay) > static::SUBQ_AND_VARIANT_ROW_LIMIT) {
// Too many randomly selected questions.
$toadd = $this->find_min_median_and_max_facility_stats_objects($subitemstodisplay);
$this->add_array_of_rows_to_table($toadd);
} else {
foreach ($subitemstodisplay as $subitemid) {
$subquestionstats[$subitemid]->maxmark = $questionstat->maxmark;
$subquestionstats[$subitemid]->subqdisplayorder = $displayorder;
$subquestionstats[$subitemid]->question->number = $questionstat->question->number;
$this->table->add_data_keyed($this->table->format_row($subquestionstats[$subitemid]));
if (count($subquestionstats[$subitemid]->variantstats) > 1) {
ksort($subquestionstats[$subitemid]->variantstats);
foreach ($subquestionstats[$subitemid]->variantstats as $variantstat) {
$this->table->add_data_keyed($this->table->format_row($variantstat));
}
}
}
}
$limitvariants = !$this->table->is_downloading();
$this->add_array_of_rows_to_table($questionstats->all_subq_and_variant_stats_for_slot($slot, $limitvariants));
}
$this->table->finish_output(!$this->table->is_downloading());
}
protected function find_min_median_and_max_facility_stats_objects($questionstats) {
$facilities = array();
foreach ($questionstats as $key => $questionstat) {
$facilities[$key] = (float)$questionstat->facility;
}
asort($facilities);
$facilitykeys = array_keys($facilities);
$keyformin = $facilitykeys[0];
$keyformedian = $facilitykeys[(int)(round(count($facilitykeys) / 2)-1)];
$keyformax = $facilitykeys[count($facilitykeys) - 1];
$toreturn = array();
foreach (array($keyformin => 'minimumfacility',
$keyformedian => 'medianfacility',
$keyformax => 'maximumfacility') as $key => $stringid) {
$questionstats[$key]->minmedianmaxnotice = get_string($stringid, 'quiz_statistics');
$toreturn[] = $questionstats[$key];
}
return $toreturn;
}
/**
* @param \core_question\statistics\questions\calculator $statstoadd
* @param \core_question\statistics\questions\calculated[] $statstoadd
*/
protected function add_array_of_rows_to_table($statstoadd) {
foreach ($statstoadd as $stattoadd) {
@ -580,10 +514,9 @@ class quiz_statistics_report extends quiz_default_report {
* we calculate stats based on which attempts would affect the grade for each student.
* @param array $groupstudents students in this group.
* @param array $questions full question data.
* @return array with 4 elements:
* - $quizstats The statistics for overall attempt scores.
* - $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.
* @param \core\progress\base|null $progress
* @return array with 2 elements: - $quizstats The statistics for overall attempt scores.
* - $questionstats \core_question\statistics\questions\all_calculated_for_qubaid_condition
*/
public function get_all_stats_and_analysis($quiz, $whichattempts, $groupstudents, $questions, $progress = null) {
@ -602,23 +535,24 @@ class quiz_statistics_report extends quiz_default_report {
$progress->start_progress('', 3);
// Recalculate now.
list($questionstats, $subquestionstats) = $qcalc->calculate($qubaids);
$questionstats = $qcalc->calculate($qubaids);
$progress->progress(1);
$quizstats = $quizcalc->calculate($quiz->id, $whichattempts, $groupstudents, count($questions),
$qcalc->get_sum_of_mark_variance());
$progress->progress(2);
if ($quizstats->s()) {
$this->analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats, $progress);
$subquestions = $questionstats->get_sub_questions();
$this->analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestions, $progress);
}
$progress->progress(3);
$progress->end_progress();
} else {
$quizstats = $quizcalc->get_cached($qubaids);
list($questionstats, $subquestionstats) = $qcalc->get_cached($qubaids);
$questionstats = $qcalc->get_cached($qubaids);
}
return array($quizstats, $questionstats, $subquestionstats);
return array($quizstats, $questionstats);
}
/**
@ -638,7 +572,7 @@ class quiz_statistics_report extends quiz_default_report {
return $this->progress;
}
protected function analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats,
protected function analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestions,
$progress = null) {
if ($progress === null) {
@ -646,7 +580,7 @@ class quiz_statistics_report extends quiz_default_report {
}
// Starting response analysis tasks.
$progress->start_progress('', count($questions) + count($subquestionstats));
$progress->start_progress('', count($questions) + count($subquestions));
// Starting response analysis of main questions.
$progress->start_progress('', count($questions), count($questions));
@ -667,19 +601,19 @@ class quiz_statistics_report extends quiz_default_report {
$progress->end_progress();
// Starting response analysis of sub-questions.
$countsubquestions = count($subquestionstats);
$countsubquestions = count($subquestions);
$progress->start_progress('', $countsubquestions, $countsubquestions);
$donecount = 1;
foreach ($subquestionstats as $subquestionstat) {
foreach ($subquestions as $subquestion) {
$progress->progress($donecount);
$donecount++;
if (!question_bank::get_qtype($subquestionstat->question->qtype, false)->can_analyse_responses() ||
isset($done[$subquestionstat->question->id])) {
if (!question_bank::get_qtype($subquestion->qtype, false)->can_analyse_responses() ||
isset($done[$subquestion->id])) {
continue;
}
$done[$subquestionstat->question->id] = 1;
$done[$subquestion->id] = 1;
$responesstats = new \core_question\statistics\responses\analyser($subquestionstat->question);
$responesstats = new \core_question\statistics\responses\analyser($subquestion);
$responesstats->calculate($qubaids);
}
// Finished sub-question tasks.

View File

@ -28,7 +28,9 @@ 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.
* random questions and variants appear.
*
* There are columns for the various item and position statistics.
*
* @copyright 2008 Jamie Pratt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
@ -136,6 +138,9 @@ class quiz_statistics_table extends flexible_table {
* @return string contents of this table cell.
*/
protected function col_number($questionstat) {
if (!isset($questionstat->question->number)) {
return '';
}
$number = $questionstat->question->number;
if (isset($questionstat->subqdisplayorder)) {
@ -214,7 +219,7 @@ class quiz_statistics_table extends flexible_table {
}
if (!empty($questionstat->minmedianmaxnotice)) {
$name = $questionstat->minmedianmaxnotice . '<br />' . $name;
$name = get_string($questionstat->minmedianmaxnotice, 'quiz_statistics') . '<br />' . $name;
}
return $name;

View File

@ -30,6 +30,18 @@ require_once($CFG->libdir . '/questionlib.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
class testable_all_calculated_for_qubaid_condition extends \core_question\statistics\questions\all_calculated_for_qubaid_condition {
/**
* Disabling caching in tests so we are always sure to force the calculation of stats right then and there.
*
* @param qubaid_condition $qubaids
*/
public function cache($qubaids) {
}
}
/**
* Test helper subclass of question_statistics
*
@ -43,6 +55,8 @@ class testable_question_statistics extends \core_question\statistics\questions\c
*/
protected $lateststeps;
protected $statscollectionclassname = 'testable_all_calculated_for_qubaid_condition';
public function set_step_data($states) {
$this->lateststeps = $states;
}
@ -86,7 +100,7 @@ class testable_question_statistics extends \core_question\statistics\questions\c
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_statistics_question_stats_testcase extends basic_testcase {
/** @var qstats object created to test class. */
/** @var testable_all_calculated_for_qubaid_condition object created to test class. */
protected $qstats;
public function test_qstats() {
@ -99,7 +113,7 @@ class quiz_statistics_question_stats_testcase extends basic_testcase {
$questions = $this->get_records_from_csv(__DIR__.'/fixtures/mdl_question.csv');
$calculator = new testable_question_statistics($questions);
$calculator->set_step_data($steps);
list($this->qstats, ) = $calculator->calculate(null);
$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,
@ -127,13 +141,12 @@ class quiz_statistics_question_stats_testcase extends basic_testcase {
}
public function qstats_q_fields($fieldname, $values, $multiplier=1) {
foreach ($this->qstats as $qstat) {
foreach ($this->qstats->get_all_slots() as $slot) {
$value = array_shift($values);
if ($value !== null) {
$this->assertEquals($value, $qstat->{$fieldname} * $multiplier,
'', 1E-6);
$this->assertEquals($value, $this->qstats->for_slot($slot)->{$fieldname} * $multiplier, '', 1E-6);
} else {
$this->assertEquals($value, $qstat->{$fieldname} * $multiplier);
$this->assertEquals($value, $this->qstats->for_slot($slot)->{$fieldname} * $multiplier);
}
}
}

View File

@ -80,7 +80,7 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
$whichattempts = QUIZ_GRADEAVERAGE;
$groupstudents = array();
$questions = $this->report->load_and_initialise_questions_for_calculations($this->quiz);
list($quizstats, $questionstats, $subquestionstats) =
list($quizstats, $questionstats) =
$this->report->get_all_stats_and_analysis($this->quiz, $whichattempts, $groupstudents, $questions);
$qubaids = quiz_statistics_qubaids_condition($this->quiz->id, $groupstudents, $whichattempts);
@ -102,11 +102,9 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
$this->assertTimeCurrent($responesstats->get_last_analysed_time($qubaids));
}
// These quiz stats and the question stats found in qstats00.csv were calculated independently in spreadsheets which are
// 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 :
// https://github.com/jamiepratt/moodle-quiz-tools/tree/master/statsspreadsheet
// These quiz stats and the position stats here are calculated in stats.xls and stats.ods available, see above github URL.
$quizstatsexpected = array(
'median' => 4.5,
'firstattemptsavg' => 4.617333332,
@ -129,8 +127,7 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
$slotqstats = $csvdata['qstats']->getRow($rowno);
foreach ($slotqstats as $statname => $slotqstat) {
if ($statname !== 'slot') {
$this->assert_stat_equals($questionstats, $subquestionstats, $slotqstats['slot'],
null, null, $statname, (float)$slotqstat);
$this->assert_stat_equals($questionstats, $slotqstats['slot'], null, null, $statname, (float)$slotqstat);
}
}
}
@ -146,7 +143,7 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
'slot' => null,
'subquestion' => true);
foreach ($itemstats as $statname => $expected) {
$this->assert_stat_equals($questionstats, $subquestionstats, 1, null, 'numerical', $statname, $expected);
$this->assert_stat_equals($questionstats, 1, null, 'numerical', $statname, $expected);
}
@ -176,7 +173,7 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
'subquestion' => false));
foreach ($statsforslot2variants as $variant => $stats) {
foreach ($stats as $statname => $expected) {
$this->assert_stat_equals($questionstats, $subquestionstats, 2, $variant, null, $statname, $expected);
$this->assert_stat_equals($questionstats, 2, $variant, null, $statname, $expected);
}
}
}
@ -184,22 +181,21 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
/**
* Check that the stat is as expected within a reasonable tolerance.
*
* @param \core_question\statistics\questions\calculated[] $questionstats
* @param \core_question\statistics\questions\calculated_for_subquestion[] $subquestionstats
* @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats
* @param int $slot
* @param int|null $variant if null then not a variant stat.
* @param string|null $subqname if null then not an item stat.
* @param string $statname
* @param float $expected
*/
protected function assert_stat_equals($questionstats, $subquestionstats, $slot, $variant, $subqname, $statname, $expected) {
protected function assert_stat_equals($questionstats, $slot, $variant, $subqname, $statname, $expected) {
if ($variant === null && $subqname === null) {
$actual = $questionstats[$slot]->{$statname};
$actual = $questionstats->for_slot($slot)->{$statname};
} else if ($subqname !== null) {
$actual = $subquestionstats[$this->randqids[$slot][$subqname]]->{$statname};
$actual = $questionstats->for_subq($this->randqids[$slot][$subqname])->{$statname};
} else {
$actual = $questionstats[$slot]->variantstats[$variant]->{$statname};
$actual = $questionstats->for_slot($slot, $variant)->{$statname};
}
if (is_bool($expected) || is_string($expected)) {
$this->assertEquals($expected, $actual, "$statname for slot $slot");

View File

@ -0,0 +1,445 @@
<?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/>.
/**
* A collection of all the question statistics calculated for an activity instance ie. the stats calculated for slots and
* sub-questions and variants of those questions.
*
* @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;
/**
* A collection of all the question statistics calculated for an activity instance.
*
* @package core_question
*/
class all_calculated_for_qubaid_condition {
/**
* The limit of rows of sub-question and variants rows to display on main page of report before switching to showing min,
* median and max variants.
*/
const SUBQ_AND_VARIANT_ROW_LIMIT = 10;
/**
* @var object[]
*/
public $subquestions;
/**
* Holds slot (position) stats and stats for variants of questions in slots.
*
* @var calculated[]
*/
public $questionstats = array();
/**
* Holds sub-question stats and stats for variants of subqs.
*
* @var calculated_for_subquestion[]
*/
public $subquestionstats = array();
/**
* Set up a calculated_for_subquestion instance ready to store a randomly selected question's stats.
*
* @param object $step
* @param int|null $variant Is this to keep track of a variant's stats? If so what is the variant, if not null.
*/
public function initialise_for_subq($step, $variant = null) {
$newsubqstat = new calculated_for_subquestion($step, $variant);
if ($variant === null) {
$this->subquestionstats[$step->questionid] = $newsubqstat;
} else {
$this->subquestionstats[$step->questionid]->variantstats[$variant] = $newsubqstat;
}
}
/**
* Set up a calculated instance ready to store a slot question's stats.
*
* @param int $slot
* @param object $question
* @param int|null $variant Is this to keep track of a variant's stats? If so what is the variant, if not null.
*/
public function initialise_for_slot($slot, $question, $variant = null) {
$newqstat = new calculated($question, $slot, $variant);
if ($variant === null) {
$this->questionstats[$slot] = $newqstat;
} else {
$this->questionstats[$slot]->variantstats[$variant] = $newqstat;
}
}
/**
* Reference for a item stats instance for a questionid and optional variant no.
*
* @param $questionid
* @param int|null $variant if not null then we want the object to store a variant of a sub-question's stats.
* @return calculated_for_subquestion|null null if the stats object does not yet exist.
*/
public function for_subq($questionid, $variant = null) {
if ($variant === null) {
if (!isset($this->subquestionstats[$questionid])) {
return null;
} else {
return $this->subquestionstats[$questionid];
}
} else {
if (!isset($this->subquestionstats[$questionid]->variantstats[$variant])) {
return null;
} else {
return $this->subquestionstats[$questionid]->variantstats[$variant];
}
}
}
/**
* ids of all randomly selected question for all slots.
*
* @return int[] An array of all sub-question ids.
*/
public function get_all_subq_ids() {
return array_keys($this->subquestionstats);
}
/**
* All slots nos that stats have been calculated for.
*
* @return int[] An array of all slot nos.
*/
public function get_all_slots() {
return array_keys($this->questionstats);
}
/**
* Array of variants of one randomly selected question that have appeared in the attempt data.
*
* @param $questionid
* @return int[]
*/
public function get_variants_for_subq($questionid) {
if (count($this->subquestionstats[$questionid]->variantstats) > 1) {
return array_keys($this->subquestionstats[$questionid]->variantstats);
} else {
return false;
}
}
/**
* Array of variants that have appeared in the attempt data for a question in one slot.
*
* @param $slot
* @return int[]
*/
public function get_variants_for_slot($slot) {
if (count($this->questionstats[$slot]->variantstats) > 1) {
return array_keys($this->questionstats[$slot]->variantstats);
} else {
return false;
}
}
/**
* Reference to position stats instance for a slot and optional variant no.
*
* @param $slot
* @param null $variant if provided then we want the object which stores a variant of a position's stats.
* @return calculated|null
*/
public function for_slot($slot, $variant = null) {
if ($variant === null) {
if (!isset($this->questionstats[$slot])) {
return null;
} else {
return $this->questionstats[$slot];
}
} else {
if (!isset($this->questionstats[$slot]->variantstats[$variant])) {
return null;
} else {
return $this->questionstats[$slot]->variantstats[$variant];
}
}
}
/**
* Load cached statistics from the database.
*
* @param $qubaids \qubaid_condition
*/
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 (is_null($fromdb->variant) && !$fromdb->slot) {
$questionids[] = $fromdb->questionid;
}
}
$this->subquestions = question_load_questions($questionids);
foreach ($questionstatrecs as $fromdb) {
if (is_null($fromdb->variant)) {
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 = $this->subquestions[$fromdb->questionid];
}
}
}
// Add cached variant stats to data structure.
foreach ($questionstatrecs as $fromdb) {
if (!is_null($fromdb->variant)) {
if ($fromdb->slot) {
$newcalcinstance = new calculated();
$this->questionstats[$fromdb->slot]->variantstats[$fromdb->variant] = $newcalcinstance;
$newcalcinstance->question = $this->questionstats[$fromdb->slot]->question;
} else {
$newcalcinstance = new calculated_for_subquestion();
$this->subquestionstats[$fromdb->questionid]->variantstats[$fromdb->variant] = $newcalcinstance;
$newcalcinstance->question = $this->subquestions[$fromdb->questionid];
}
$newcalcinstance->populate_from_record($fromdb);
}
}
}
/**
* Find time of non-expired statistics in the database.
*
* @param $qubaids \qubaid_condition
* @return int|bool 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), IGNORE_MULTIPLE);
}
/** @var integer Time after which statistics are automatically recomputed. */
const TIME_TO_CACHE = 900; // 15 minutes.
/**
* Save stats to db.
*
* @param $qubaids \qubaid_condition
*/
public function cache($qubaids) {
foreach ($this->get_all_slots() as $slot) {
$this->for_slot($slot)->cache($qubaids);
}
foreach ($this->get_all_subq_ids() as $subqid) {
$this->for_subq($subqid)->cache($qubaids);
}
}
/**
* Return all sub-questions used.
*
* @return \object[] array of questions.
*/
public function get_sub_questions() {
return $this->subquestions;
}
/**
* Are there too many rows of sub-questions and / or variant rows.
*
* @param array $rows the rows we intend to add.
* @return bool
*/
protected function too_many_subq_and_or_variant_rows($rows) {
return (count($rows) > static::SUBQ_AND_VARIANT_ROW_LIMIT);
}
/**
* From a number of calculated instances find the three instances with min, median and maximum facility index values.
*
* @param calculated[] $questionstats
* @return calculated[] 3 stat objects with minimum, median and maximum facility index.
*/
protected function find_min_median_and_max_facility_stats_objects($questionstats) {
$facilities = array();
foreach ($questionstats as $key => $questionstat) {
$facilities[$key] = (float)$questionstat->facility;
}
asort($facilities);
$facilitykeys = array_keys($facilities);
$keyformin = $facilitykeys[0];
$keyformedian = $facilitykeys[(int)(round(count($facilitykeys) / 2)-1)];
$keyformax = $facilitykeys[count($facilitykeys) - 1];
$toreturn = array();
foreach (array($keyformin => 'minimumfacility',
$keyformedian => 'medianfacility',
$keyformax => 'maximumfacility') as $key => $stringid) {
$questionstats[$key]->minmedianmaxnotice = $stringid;
$toreturn[] = $questionstats[$key];
}
return $toreturn;
}
/**
* Return all stats for variants of question in slot $slot.
*
* @param int $slot
* @return calculated[]
*/
protected function all_variant_stats_for_one_slot($slot) {
$toreturn = array();
if ($variants = $this->get_variants_for_slot($slot)){
foreach ($variants as $variant) {
$toreturn[] = $this->for_slot($slot, $variant);
}
}
return $toreturn;
}
/**
* Return all stats for variants of randomly selected questions for one slot $slot.
*
* @param int $slot
* @return calculated[]
*/
protected function all_subq_variants_for_one_slot($slot) {
$toreturn = array();
$displayorder = 1;
foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) {
if ($variants = $this->get_variants_for_subq($subqid)) {
foreach ($variants as $variant) {
$toreturn[] = $this->make_new_subq_stat_for($displayorder, $slot, $subqid, $variant);
}
}
$displayorder++;
}
return $toreturn;
}
/**
* Return all stats for randomly selected questions for one slot $slot.
*
* @param int $slot
* @return calculated[]
*/
protected function all_subqs_for_one_slot($slot) {
$displayorder = 1;
$toreturn = array();
foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) {
$toreturn[] = $this->make_new_subq_stat_for($displayorder, $slot, $subqid);
$displayorder++;
}
return $toreturn;
}
/**
* Return all variant or 'sub-question' stats one slot, either :
* - variants of question
* - variants of randomly selected questions
* - randomly selected questions
*
* @param int $slot
* @param bool $limited
* @return calculated[]
*/
public function all_subq_and_variant_stats_for_slot($slot, $limited) {
// Random question in this slot?
if ($this->for_slot($slot)->get_sub_question_ids()) {
if ($limited) {
$subqvariantstats = $this->all_subq_variants_for_one_slot($slot);
if ($this->too_many_subq_and_or_variant_rows($subqvariantstats)) {
// Too many variants from randomly selected questions.
return $this->find_min_median_and_max_facility_stats_objects($subqvariantstats);
}
$subqstats = $this->all_subqs_for_one_slot($slot);
if ($this->too_many_subq_and_or_variant_rows($subqstats)) {
// Too many randomly selected questions.
return $this->find_min_median_and_max_facility_stats_objects($subqstats);
}
}
$toreturn = array();
$displaynumber = 1;
foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) {
$toreturn[] = $this->make_new_subq_stat_for($displaynumber, $slot, $subqid);
if ($variants = $this->get_variants_for_subq($subqid)) {
foreach ($variants as $variant) {
$toreturn[] = $this->make_new_subq_stat_for($displaynumber, $slot, $subqid, $variant);
}
}
$displaynumber++;
}
return $toreturn;
} else {
$variantstats = $this->all_variant_stats_for_one_slot($slot);
if ($limited && $this->too_many_subq_and_or_variant_rows($variantstats)) {
return $this->find_min_median_and_max_facility_stats_objects($variantstats);
} else {
return $variantstats;
}
}
}
/**
* We need a new object for display. Sub-question stats can appear more than once in different slots.
* So we create a clone of the object and then we can set properties on the object that are per slot.
*
* @param $displaynumber
* @param $slot
* @param $subqid
* @param null $variant
* @return calculated_for_subquestion|null
*/
protected function make_new_subq_stat_for($displaynumber, $slot, $subqid, $variant = null) {
$slotstat = fullclone($this->for_subq($subqid, $variant));
$slotstat->question->number = $this->for_slot($slot)->question->number;
$slotstat->subqdisplayorder = $displaynumber;
return $slotstat;
}
/**
* Call after calculations to output any error messages.
*
* @return string[] Array of strings describing error messages found during stats calculation.
*/
public function any_error_messages() {
$errors = array();
foreach ($this->get_all_slots() as $slot) {
foreach ($this->for_slot($slot)->get_sub_question_ids() as $subqid) {
if ($this->for_subq($subqid)->differentweights) {
$name = $this->for_subq($subqid)->question->name;
$errors[] = get_string('erroritemappearsmorethanoncewithdifferentweight', 'quiz_statistics', $name);
}
}
}
return $errors;
}
}

View File

@ -57,7 +57,7 @@ class calculated {
/**
* @var string if this stat has been picked as a min, median or maximum facility value then this string says which stat this
* is.
* is. Prepended to question name for display.
*/
public $minmedianmaxnotice = '';
@ -111,7 +111,6 @@ class calculated {
*/
public $randomguessscore = null;
// End of fields in db.
protected $fieldsindb = array('questionid', 'slot', 'subquestion', 's', 'effectiveweight', 'negcovar', 'discriminationindex',
@ -179,6 +178,40 @@ class calculated {
*/
public $timemodified;
/**
* Set up a calculated instance ready to store a question's (or a variant of a slot's question's)
* stats for one slot in the quiz.
*
* @param null|object $question
* @param null|int $slot
* @param null|int $variant
*/
public function __construct($question = null, $slot = null, $variant = null) {
if ($question !== null) {
$this->questionid = $question->id;
$this->maxmark = $question->maxmark;
$this->positions = $question->number;
$this->question = $question;
}
if ($slot !== null) {
$this->slot = $slot;
}
if ($variant !== null) {
$this->variant = $variant;
}
}
/**
* @return null|string a string that represents the pool of questions from this question draws if it random or null if not.
*/
public function random_selector_string() {
if ($this->question->qtype == 'random') {
return $this->question->category .'/'. $this->question->questiontext;
} else {
return null;
}
}
/**
* Cache calculated stats stored in this object in 'question_statistics' table.
*
@ -211,4 +244,18 @@ class calculated {
$this->timemodified = $record->timemodified;
}
public function sort_variants() {
ksort($this->variantstats);
}
/**
* @return int[] array of sub-question ids or empty array if there are none.
*/
public function get_sub_question_ids() {
if ($this->subquestions !== '') {
return explode(',', $this->subquestions);
} else {
return array();
}
}
}

View File

@ -39,4 +39,17 @@ class calculated_for_subquestion extends calculated {
* @var int only set immediately before display in the table. The order of display in the table.
*/
public $subqdisplayorder;
/**
* @param object|null $step the step data for the step that this sub-question was first encountered in.
* @param int|null $variant the variant no
*/
public function __construct($step = null, $variant = null) {
if ($step !== null) {
$this->questionid = $step->questionid;
$this->maxmark = $step->maxmark;
}
$this->variant = $variant;
}
}

View File

@ -37,20 +37,20 @@ defined('MOODLE_INTERNAL') || die();
class calculator {
/**
* @var calculated[]
* @var all_calculated_for_qubaid_condition all the stats calculated for slots and sub-questions and variants of those
* questions.
*/
public $questionstats = array();
/**
* @var calculated_for_subquestion[]
*/
public $subquestionstats = array();
protected $stats;
/**
* @var float
*/
protected $sumofmarkvariance = 0;
/**
* @var array[] keyed by a string representing the pool of questions that this random question draws from.
* string as returned from {@link \core_question\statistics\questions\calculated::random_selector_string}
*/
protected $randomselectors = array();
/**
@ -58,6 +58,8 @@ class calculator {
*/
protected $progress;
protected $statscollectionclassname = '\core_question\statistics\questions\all_calculated_for_qubaid_condition';
/**
* Constructor.
*
@ -71,46 +73,16 @@ class calculator {
$progress = new \core\progress\null();
}
$this->progress = $progress;
$this->stats = new $this->statscollectionclassname();
foreach ($questions as $slot => $question) {
$this->questionstats[$slot] = $this->new_slot_stats($question, $slot);
$this->stats->initialise_for_slot($slot, $question);
$this->stats->for_slot($slot)->randomguessscore = $this->get_random_guess_score($question);
}
}
/**
* Set up a calculated instance ready to store a questions stats.
*
* @param $question
* @param $slot
* @return calculated
*/
protected function new_slot_stats($question, $slot) {
$toreturn = new calculated();
$toreturn->questionid = $question->id;
$toreturn->maxmark = $question->maxmark;
$toreturn->question = $question;
$toreturn->slot = $slot;
$toreturn->positions = $question->number;
$toreturn->randomguessscore = $this->get_random_guess_score($question);
return $toreturn;
}
/**
* Set up a calculated instance ready to store a randomly selected question's stats.
*
* @param $step
* @return calculated_for_subquestion
*/
protected function new_subq_stats($step) {
$toreturn = new calculated_for_subquestion();
$toreturn->questionid = $step->questionid;
$toreturn->maxmark = $step->maxmark;
return $toreturn;
}
/**
* @param $qubaids \qubaid_condition
* @return array containing two arrays calculated[] and calculated_for_subquestion[].
* @return all_calculated_for_qubaid_condition
*/
public function calculate($qubaids) {
@ -126,43 +98,40 @@ class calculator {
$this->progress->increment_progress();
$israndomquestion = ($step->questionid != $this->questionstats[$step->slot]->questionid);
$israndomquestion = ($step->questionid != $this->stats->for_slot($step->slot)->questionid);
// If this is a variant we have not seen before create a place to store stats calculations for this variant.
if (!$israndomquestion && !isset($this->questionstats[$step->slot]->variantstats[$step->variant])) {
$this->questionstats[$step->slot]->variantstats[$step->variant] =
$this->new_slot_stats($this->questionstats[$step->slot]->question, $step->slot);
$this->questionstats[$step->slot]->variantstats[$step->variant]->variant = $step->variant;
if (!$israndomquestion && is_null($this->stats->for_slot($step->slot , $step->variant))) {
$this->stats->initialise_for_slot($step->slot, $this->stats->for_slot($step->slot)->question, $step->variant);
$this->stats->for_slot($step->slot, $step->variant)->randomguessscore =
$this->get_random_guess_score($this->stats->for_slot($step->slot)->question);
}
// Step data walker for main question.
$this->initial_steps_walker($step, $this->questionstats[$step->slot], $summarks, true, !$israndomquestion);
$this->initial_steps_walker($step, $this->stats->for_slot($step->slot), $summarks, true, !$israndomquestion);
// If this is a random question do the calculations for sub question stats.
if ($israndomquestion) {
if (!isset($this->subquestionstats[$step->questionid])) {
$this->subquestionstats[$step->questionid] = $this->new_subq_stats($step);
} else if ($this->subquestionstats[$step->questionid]->maxmark != $step->maxmark) {
$this->subquestionstats[$step->questionid]->differentweights = true;
if (is_null($this->stats->for_subq($step->questionid))) {
$this->stats->initialise_for_subq($step);
} else if ($this->stats->for_subq($step->questionid)->maxmark != $step->maxmark) {
$this->stats->for_subq($step->questionid)->differentweights = true;
}
// If this is a variant of this subq we have not seen before create a place to store stats calculations for it.
if (!isset($this->subquestionstats[$step->questionid]->variantstats[$step->variant])) {
$this->subquestionstats[$step->questionid]->variantstats[$step->variant] = $this->new_subq_stats($step);
$this->subquestionstats[$step->questionid]->variantstats[$step->variant]->variant = $step->variant;
if (is_null($this->stats->for_subq($step->questionid, $step->variant))) {
$this->stats->initialise_for_subq($step, $step->variant);
}
$this->initial_steps_walker($step, $this->subquestionstats[$step->questionid], $summarks, false);
$this->initial_steps_walker($step, $this->stats->for_subq($step->questionid), $summarks, false);
// Extra stuff we need to do in this loop for subqs to keep track of where they need to be displayed later.
$number = $this->questionstats[$step->slot]->question->number;
$this->subquestionstats[$step->questionid]->usedin[$number] = $number;
$number = $this->stats->for_slot($step->slot)->question->number;
$this->stats->for_subq($step->questionid)->usedin[$number] = $number;
// Keep track of which random questions are actually selected from each pool of questions that random
// questions are pulled from.
$randomselectorstring = $this->questionstats[$step->slot]->question->category. '/'
.$this->questionstats[$step->slot]->question->questiontext;
$randomselectorstring = $this->stats->for_slot($step->slot)->random_selector_string();
if (!isset($this->randomselectors[$randomselectorstring])) {
$this->randomselectors[$randomselectorstring] = array();
}
@ -173,67 +142,60 @@ class calculator {
foreach ($this->randomselectors as $key => $notused) {
ksort($this->randomselectors[$key]);
$this->randomselectors[$key] = implode(',', $this->randomselectors[$key]);
}
$subquestions = question_load_questions(array_keys($this->subquestionstats));
$this->stats->subquestions = question_load_questions($this->stats->get_all_subq_ids());
// Compute the statistics for sub questions, if there are any.
$this->progress->start_progress('', count($subquestions), 1);
foreach ($subquestions as $qid => $subquestion) {
$this->progress->start_progress('', count($this->stats->subquestions), 1);
foreach ($this->stats->subquestions as $qid => $subquestion) {
$this->progress->increment_progress();
$subquestion->maxmark = $this->subquestionstats[$qid]->maxmark;
$this->subquestionstats[$qid]->question = $subquestion;
$this->subquestionstats[$qid]->randomguessscore = $this->get_random_guess_score($subquestion);
$subquestion->maxmark = $this->stats->for_subq($qid)->maxmark;
$this->stats->for_subq($qid)->question = $subquestion;
$this->stats->for_subq($qid)->randomguessscore = $this->get_random_guess_score($subquestion);
foreach ($this->subquestionstats[$qid]->variantstats as $variantstat) {
$variantstat->question = $subquestion;
$variantstat->randomguessscore = $this->get_random_guess_score($subquestion);
$this->stats->for_subq($qid)->sort_variants();
if ($variants = $this->stats->get_variants_for_subq($qid)) {
foreach ($variants as $variant) {
$this->stats->for_subq($qid, $variant)->question = $subquestion;
$this->stats->for_subq($qid, $variant)->randomguessscore = $this->get_random_guess_score($subquestion);
}
}
$this->initial_question_walker($this->subquestionstats[$qid]);
$this->initial_question_walker($this->stats->for_subq($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);
if ($this->stats->for_subq($qid)->usedin) {
sort($this->stats->for_subq($qid)->usedin, SORT_NUMERIC);
$this->stats->for_subq($qid)->positions = implode(',', $this->stats->for_subq($qid)->usedin);
} else {
$this->subquestionstats[$qid]->positions = '';
$this->stats->for_subq($qid)->positions = '';
}
}
$this->progress->end_progress();
// Finish computing the averages, and put the subquestion data into the
// Finish computing the averages, and put the sub-question 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);
$this->progress->start_progress('', count($this->questionstats), 1);
while (list(, $questionstat) = each($this->questionstats)) {
$slots = $this->stats->get_all_slots();
$this->progress->start_progress('', count($slots), 1);
while (list(, $slot) = each($slots)) {
$this->stats->for_slot($slot)->sort_variants();
$this->progress->increment_progress();
$nextquestionstats = current($this->questionstats);
$nextslot = current($slots);
$this->initial_question_walker($questionstat);
$this->initial_question_walker($this->stats->for_slot($slot));
// The rest of this loop is again to work out where randomly selected question stats should be displayed.
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.
}
// The rest of this loop is to finish working out where randomly selected question stats should be displayed.
if ($this->stats->for_slot($slot)->question->qtype == 'random') {
$randomselectorstring = $this->stats->for_slot($slot)->random_selector_string();
if ($nextslot && ($randomselectorstring == $this->stats->for_slot($nextslot)->random_selector_string())) {
continue; // Next loop iteration.
}
if (isset($this->randomselectors[$randomselectorstring])) {
$questionstat->subquestions = implode(',', $this->randomselectors[$randomselectorstring]);
$this->stats->for_slot($slot)->subquestions = $this->randomselectors[$randomselectorstring];
}
}
}
@ -243,122 +205,59 @@ class calculator {
$this->progress->start_progress('', count($lateststeps), 1);
foreach ($lateststeps as $step) {
$this->progress->increment_progress();
$israndomquestion = ($this->questionstats[$step->slot]->question->qtype == 'random');
$this->secondary_steps_walker($step, $this->questionstats[$step->slot], $summarks, !$israndomquestion);
$israndomquestion = ($this->stats->for_slot($step->slot)->question->qtype == 'random');
$this->secondary_steps_walker($step, $this->stats->for_slot($step->slot), $summarks, !$israndomquestion);
if ($this->questionstats[$step->slot]->subquestions) {
$this->secondary_steps_walker($step, $this->subquestionstats[$step->questionid], $summarks);
if ($this->stats->for_slot($step->slot)->subquestions) {
$this->secondary_steps_walker($step, $this->stats->for_subq($step->questionid), $summarks);
}
}
$this->progress->end_progress();
$this->progress->start_progress('', count($this->questionstats), 1);
$slots = $this->stats->get_all_slots();
$this->progress->start_progress('', count($slots), 1);
$sumofcovariancewithoverallmark = 0;
foreach ($this->questionstats as $questionstat) {
foreach ($this->stats->get_all_slots() as $slot) {
$this->progress->increment_progress();
$this->secondary_question_walker($questionstat);
$this->secondary_question_walker($this->stats->for_slot($slot));
$this->sumofmarkvariance += $questionstat->markvariance;
$this->sumofmarkvariance += $this->stats->for_slot($slot)->markvariance;
if ($questionstat->covariancewithoverallmark >= 0) {
$sumofcovariancewithoverallmark += sqrt($questionstat->covariancewithoverallmark);
if ($this->stats->for_slot($slot)->covariancewithoverallmark >= 0) {
$sumofcovariancewithoverallmark += sqrt($this->stats->for_slot($slot)->covariancewithoverallmark);
}
}
$this->progress->end_progress();
$this->progress->start_progress('', count($this->subquestionstats), 1);
foreach ($this->subquestionstats as $subquestionstat) {
$subqids = $this->stats->get_all_subq_ids();
$this->progress->start_progress('', count($subqids), 1);
foreach ($subqids as $subqid) {
$this->progress->increment_progress();
$this->secondary_question_walker($subquestionstat);
$this->secondary_question_walker($this->stats->for_subq($subqid));
}
$this->progress->end_progress();
foreach ($this->questionstats as $questionstat) {
foreach ($this->stats->get_all_slots() as $slot) {
if ($sumofcovariancewithoverallmark) {
if ($questionstat->negcovar) {
$questionstat->effectiveweight = null;
if ($this->stats->for_slot($slot)->negcovar) {
$this->stats->for_slot($slot)->effectiveweight = null;
} else {
$questionstat->effectiveweight = 100 * sqrt($questionstat->covariancewithoverallmark) /
$sumofcovariancewithoverallmark;
$this->stats->for_slot($slot)->effectiveweight =
100 * sqrt($this->stats->for_slot($slot)->covariancewithoverallmark) /
$sumofcovariancewithoverallmark;
}
} else {
$questionstat->effectiveweight = null;
$this->stats->for_slot($slot)->effectiveweight = null;
}
}
$this->cache_stats($qubaids);
$this->stats->cache($qubaids);
// All finished.
$this->progress->end_progress();
}
return array($this->questionstats, $this->subquestionstats);
return $this->stats;
}
/**
* 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 (is_null($fromdb->variant) && !$fromdb->slot) {
$questionids[] = $fromdb->questionid;
}
}
$subquestions = question_load_questions($questionids);
foreach ($questionstatrecs as $fromdb) {
if (is_null($fromdb->variant)) {
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];
}
}
}
// Add cached variant stats to data structure.
foreach ($questionstatrecs as $fromdb) {
if (!is_null($fromdb->variant)) {
if ($fromdb->slot) {
$newcalcinstance = new calculated();
$this->questionstats[$fromdb->slot]->variantstats[$fromdb->variant] = $newcalcinstance;
$newcalcinstance->question = $this->questionstats[$fromdb->slot]->question;
} else {
$newcalcinstance = new calculated_for_subquestion();
$this->subquestionstats[$fromdb->questionid]->variantstats[$fromdb->variant] = $newcalcinstance;
$newcalcinstance->question = $subquestions[$fromdb->questionid];
}
$newcalcinstance->populate_from_record($fromdb);
}
}
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 > ?',
array($qubaids->get_hash_code(), $timemodified), IGNORE_MULTIPLE);
}
/** @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.
*
@ -385,7 +284,7 @@ class calculator {
qa.maxmark,
qas.fraction * qa.maxmark as mark";
$lateststeps = $dm->load_questions_usages_latest_steps($qubaids, array_keys($this->questionstats), $fields);
$lateststeps = $dm->load_questions_usages_latest_steps($qubaids, $this->stats->get_all_slots(), $fields);
$summarks = array();
if ($lateststeps) {
foreach ($lateststeps as $step) {
@ -536,7 +435,6 @@ class calculator {
$stats->discriminativeefficiency = null;
}
if ($dovariantsalso) {
foreach ($stats->variantstats as $variantstat) {
$this->secondary_question_walker($variantstat, false);
@ -554,16 +452,23 @@ class calculator {
}
/**
* 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.
*/
protected function cache_stats($qubaids) {
foreach ($this->questionstats as $questionstat) {
$questionstat->cache($qubaids);
}
foreach ($this->subquestionstats as $subquestionstat) {
$subquestionstat->cache($qubaids);
}
public function get_last_calculated_time($qubaids) {
return $this->stats->get_last_calculated_time($qubaids);
}
/**
* Load cached statistics from the database.
*
* @param $qubaids \qubaid_condition
* @return all_calculated_for_qubaid_condition
*/
public function get_cached($qubaids) {
$this->stats->get_cached($qubaids);
return $this->stats;
}
}