mirror of
https://github.com/moodle/moodle.git
synced 2025-03-14 04:30:15 +01:00
MDL-43338 quiz statistics : refactoring question statistics code
to improve readability and maintainability.
This commit is contained in:
parent
974c2cdc03
commit
c3e2e754dd
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user