MDL-41756 (2) quiz statistics : break down question stats by variant

This commit is contained in:
James Pratt 2013-11-30 18:50:15 +07:00
parent aa05ae5da3
commit 1239d28776
11 changed files with 649 additions and 483 deletions

View file

@ -44,6 +44,12 @@ class calculated {
public $slot = null;
/**
* @var null|integer if this property is not null then this is the stats for a variant of a question or when inherited by
* calculated_for_subquestion and not null then this is the stats for a variant of a sub question.
*/
public $variant = null;
/**
* @var bool is this a sub question.
*/
@ -103,7 +109,7 @@ class calculated {
// End of fields in db.
protected $fieldsindb = array('questionid', 'slot', 'subquestion', 's', 'effectiveweight', 'negcovar', 'discriminationindex',
'discriminativeefficiency', 'sd', 'facility', 'subquestions', 'maxmark', 'positions', 'randomguessscore');
'discriminativeefficiency', 'sd', 'facility', 'subquestions', 'maxmark', 'positions', 'randomguessscore', 'variant');
// Fields used for intermediate calculations.
@ -152,6 +158,14 @@ class calculated {
*/
public $question;
/**
* An array of calculated stats for each variant of the question. Even when there is just one variant we still calculate this
* data as there is no way to know if there are variants before we have finished going through the attempt data one time.
*
* @var calculated[] $variants
*/
public $variantstats = array();
/**
* Set if this record has been retrieved from cache. This is the time that the statistics were calculated.
*
@ -173,6 +187,12 @@ class calculated {
$toinsert->{$field} = $this->{$field};
}
$DB->insert_record('question_statistics', $toinsert, false);
if (count($this->variantstats) > 1) {
foreach ($this->variantstats as $variantstat) {
$variantstat->cache($qubaids);
}
}
}
/**

View file

@ -34,4 +34,9 @@ class calculated_for_subquestion extends calculated {
public $differentweights = false;
public $negcovar = 0;
/**
* @var int only set immediately before display in the table. The order of display in the table.
*/
public $subqdisplayorder;
}

View file

@ -73,16 +73,41 @@ class calculator {
$this->progress = $progress;
foreach ($questions as $slot => $question) {
$this->questionstats[$slot] = new calculated();
$this->questionstats[$slot]->questionid = $question->id;
$this->questionstats[$slot]->question = $question;
$this->questionstats[$slot]->slot = $slot;
$this->questionstats[$slot]->positions = $question->number;
$this->questionstats[$slot]->maxmark = $question->maxmark;
$this->questionstats[$slot]->randomguessscore = $this->get_random_guess_score($question);
$this->questionstats[$slot] = $this->new_slot_stats($question, $slot);
}
}
/**
* 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[].
@ -98,21 +123,39 @@ class calculator {
// Compute the statistics of position, and for random questions, work
// out which questions appear in which positions.
foreach ($lateststeps as $step) {
$this->progress->increment_progress();
$this->initial_steps_walker($step, $this->questionstats[$step->slot], $summarks);
// If this is a random question what is the real item being used?
if ($step->questionid != $this->questionstats[$step->slot]->questionid) {
$this->progress->increment_progress();
$israndomquestion = ($step->questionid != $this->questionstats[$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;
}
// Step data walker for main question.
$this->initial_steps_walker($step, $this->questionstats[$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] = new calculated_for_subquestion();
$this->subquestionstats[$step->questionid]->questionid = $step->questionid;
$this->subquestionstats[$step->questionid]->maxmark = $step->maxmark;
$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 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;
}
$this->initial_steps_walker($step, $this->subquestionstats[$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;
@ -137,10 +180,15 @@ class calculator {
$this->progress->start_progress('', count($subquestions), 1);
foreach ($subquestions as $qid => $subquestion) {
$this->progress->increment_progress();
$subquestion->maxmark = $this->subquestionstats[$qid]->maxmark;
$this->subquestionstats[$qid]->question = $subquestion;
$this->subquestionstats[$qid]->question->maxmark = $this->subquestionstats[$qid]->maxmark;
$this->subquestionstats[$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->initial_question_walker($this->subquestionstats[$qid]);
if ($this->subquestionstats[$qid]->differentweights) {
@ -168,12 +216,13 @@ class calculator {
// foreach ($this->questions as $qid => $question).
reset($this->questionstats);
$this->progress->start_progress('', count($this->questionstats), 1);
while (list($slot, $questionstat) = each($this->questionstats)) {
while (list(, $questionstat) = each($this->questionstats)) {
$this->progress->increment_progress();
$nextquestionstats = current($this->questionstats);
$this->initial_question_walker($questionstat);
// 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') {
@ -194,7 +243,8 @@ class calculator {
$this->progress->start_progress('', count($lateststeps), 1);
foreach ($lateststeps as $step) {
$this->progress->increment_progress();
$this->secondary_steps_walker($step, $this->questionstats[$step->slot], $summarks);
$israndomquestion = ($this->questionstats[$step->slot]->question->qtype == 'random');
$this->secondary_steps_walker($step, $this->questionstats[$step->slot], $summarks, !$israndomquestion);
if ($this->questionstats[$step->slot]->subquestions) {
$this->secondary_steps_walker($step, $this->subquestionstats[$step->questionid], $summarks);
@ -257,19 +307,36 @@ class calculator {
$questionids = array();
foreach ($questionstatrecs as $fromdb) {
if (!$fromdb->slot) {
if (is_null($fromdb->variant) && !$fromdb->slot) {
$questionids[] = $fromdb->questionid;
}
}
$subquestions = question_load_questions($questionids);
foreach ($questionstatrecs as $fromdb) {
if ($fromdb->slot) {
$this->questionstats[$fromdb->slot]->populate_from_record($fromdb);
// Array created in constructor and populated from question.
} else {
$this->subquestionstats[$fromdb->questionid] = new calculated_for_subquestion();
$this->subquestionstats[$fromdb->questionid]->populate_from_record($fromdb);
$this->subquestionstats[$fromdb->questionid]->question = $subquestions[$fromdb->questionid];
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);
@ -313,6 +380,7 @@ class calculator {
$fields = " qas.id,
qa.questionusageid,
qa.questionid,
qa.variant,
qa.slot,
qa.maxmark,
qas.fraction * qa.maxmark as mark";
@ -335,12 +403,13 @@ class calculator {
* Update $stats->totalmarks, $stats->markarray, $stats->totalothermarks
* and $stats->othermarksarray to include another state.
*
* @param object $step the state to add to the statistics.
* @param object $step the state to add to the statistics.
* @param calculated $stats the question statistics we are accumulating.
* @param array $summarks of the sum of marks for each question usage, indexed by question usage id
* @param bool $positionstat whether this is a statistic of position of question.
* @param array $summarks of the sum of marks for each question usage, indexed by question usage id
* @param bool $positionstat whether this is a statistic of position of question.
* @param bool $dovariantalso do we also want to do the same calculations for this variant?
*/
protected function initial_steps_walker($step, $stats, $summarks, $positionstat = true) {
protected function initial_steps_walker($step, $stats, $summarks, $positionstat = true, $dovariantalso = true) {
$stats->s++;
$stats->totalmarks += $step->mark;
$stats->markarray[] = $step->mark;
@ -353,15 +422,20 @@ class calculator {
$stats->totalothermarks += $summarks[$step->questionusageid];
$stats->othermarksarray[] = $summarks[$step->questionusageid];
}
if ($dovariantalso) {
$this->initial_steps_walker($step, $stats->variantstats[$step->variant], $summarks, $positionstat, false);
}
}
/**
* Perform some computations on the per-question statistics calculations after
* we have been through all the states.
* we have been through all the step data.
*
* @param calculated $stats question stats to update.
* @param bool $dovariantsalso do we also want to do the same calculations for the variants?
*/
protected function initial_question_walker($stats) {
protected function initial_question_walker($stats, $dovariantsalso = true) {
$stats->markaverage = $stats->totalmarks / $stats->s;
if ($stats->maxmark != 0) {
@ -376,6 +450,12 @@ class calculator {
sort($stats->markarray, SORT_NUMERIC);
sort($stats->othermarksarray, SORT_NUMERIC);
if ($dovariantsalso) {
foreach ($stats->variantstats as $variantstat) {
$this->initial_question_walker($variantstat, false);
}
}
}
/**
@ -385,8 +465,9 @@ class calculator {
* @param object $step the state to add to the statistics.
* @param calculated $stats the question statistics we are accumulating.
* @param array $summarks of the sum of marks for each question usage, indexed by question usage id
* @param bool $dovariantalso do we also want to do the same calculations for the variant?
*/
protected function secondary_steps_walker($step, $stats, $summarks) {
protected function secondary_steps_walker($step, $stats, $summarks, $dovariantalso = true) {
$markdifference = $step->mark - $stats->markaverage;
if ($stats->subquestion) {
$othermarkdifference = $summarks[$step->questionusageid] - $stats->othermarkaverage;
@ -403,14 +484,19 @@ class calculator {
$stats->covariancesum += $markdifference * $othermarkdifference;
$stats->covariancemaxsum += $sortedmarkdifference * $sortedothermarkdifference;
$stats->covariancewithoverallmarksum += $markdifference * $overallmarkdifference;
if ($dovariantalso) {
$this->secondary_steps_walker($step, $stats->variantstats[$step->variant], $summarks, false);
}
}
/**
* Perform more per-question statistics calculations.
*
* @param calculated $stats question stats to update.
* @param bool $dovariantsalso do we also want to do the same calculations for the variants?
*/
protected function secondary_question_walker($stats) {
protected function secondary_question_walker($stats, $dovariantsalso = true) {
if ($stats->s > 1) {
$stats->markvariance = $stats->markvariancesum / ($stats->s - 1);
@ -449,6 +535,13 @@ class calculator {
} else {
$stats->discriminativeefficiency = null;
}
if ($dovariantsalso) {
foreach ($stats->variantstats as $variantstat) {
$this->secondary_question_walker($variantstat, false);
}
}
}
/**
@ -464,9 +557,9 @@ class calculator {
* @param $qubaids \qubaid_condition
*/
protected function cache_stats($qubaids) {
foreach ($this->questionstats as $questionstat) {
foreach ($this->questionstats as $questionstat) {
$questionstat->cache($qubaids);
}
}
foreach ($this->subquestionstats as $subquestionstat) {
$subquestionstat->cache($qubaids);