Skip to content
Snippets Groups Projects
Commit 6d719008 authored by Chris Sangwin's avatar Chris Sangwin
Browse files

Improve the basic question usage report.

parent b385cd5a
No related branches found
No related tags found
No related merge requests found
...@@ -11,6 +11,7 @@ How to report bugs and make suggestions is described on the [community](../About ...@@ -11,6 +11,7 @@ How to report bugs and make suggestions is described on the [community](../About
2. Support [variant matching](../Authoring/Variant_matching.md). 2. Support [variant matching](../Authoring/Variant_matching.md).
3. Add in the option `arccos(x)/arcosh(x)` for display of trig. This notation exists becase `arcsin` gives the arc length on the unit circle for a given y-coordinate. `arsinh` gives an area enclosed by a hyperbola and two rays from the origin for a given y-coordinate. 3. Add in the option `arccos(x)/arcosh(x)` for display of trig. This notation exists becase `arcsin` gives the arc length on the unit circle for a given y-coordinate. `arsinh` gives an area enclosed by a hyperbola and two rays from the origin for a given y-coordinate.
4. Allow students to type `arccos` etc. and treat these as synonyms of the trig functions. 4. Allow students to type `arccos` etc. and treat these as synonyms of the trig functions.
5. Substantially improve the basic question usage report.
## Version 4.4 ## Version 4.4
......
...@@ -552,10 +552,10 @@ $string['variant'] = 'Variant'; ...@@ -552,10 +552,10 @@ $string['variant'] = 'Variant';
$string['basicquestionreport'] = 'Basic question use report'; $string['basicquestionreport'] = 'Basic question use report';
$string['basicquestionreport_help'] = 'Generates a very basic report on attempts at this question on the server. Useful for deciding which PRT test can be added to improve feedback in the light of what the student actually does. (Most questions are only used in one place)'; $string['basicquestionreport_help'] = 'Generates a very basic report on attempts at this question on the server. Useful for deciding which PRT test can be added to improve feedback in the light of what the student actually does. (Most questions are only used in one place)';
$string['basicreportraw'] = 'Raw data'; $string['basicreportraw'] = 'Raw data';
$string['basicreportnotes'] = 'Frequency of answer notes, for each PRT, regardless of which variant was used.'; $string['basicreportnotes'] = 'Frequency of answer notes, for each PRT, regardless of which variant was used';
$string['basicreportnotessplit'] = 'Frequency of answer notes, for each PRT, split by |, regardless of which variant was used.'; $string['basicreportnotessplit'] = 'Frequency of answer notes, for each PRT, split by |, regardless of which variant was used';
$string['basicreportvariants'] = 'Raw inputs and PRT answer notes by variant.'; $string['basicreportvariants'] = 'Raw inputs and PRT answer notes by variant';
$string['basicreportinputsummary'] = 'Raw inputs, regardless of which variant was used.'; $string['basicreportinputsummary'] = 'Raw inputs, regardless of which variant was used';
// Equiv input specific string. // Equiv input specific string.
$string['equivnocomments'] = 'You are not permitted to use comments in this input type. Please just work line by line.'; $string['equivnocomments'] = 'You are not permitted to use comments in this input type. Please just work line by line.';
......
...@@ -60,6 +60,9 @@ echo $OUTPUT->header(); ...@@ -60,6 +60,9 @@ echo $OUTPUT->header();
$renderer = $PAGE->get_renderer('qtype_stack'); $renderer = $PAGE->get_renderer('qtype_stack');
echo $OUTPUT->heading($question->name, 2); echo $OUTPUT->heading($question->name, 2);
// Link back to question tests.
$out = html_writer::link($testquestionlink, stack_string('runquestiontests'), array('target' => '_blank')); $out = html_writer::link($testquestionlink, stack_string('runquestiontests'), array('target' => '_blank'));
// If question has no random variants. // If question has no random variants.
...@@ -73,8 +76,53 @@ echo html_writer::tag('p', $out . ' ' . ...@@ -73,8 +76,53 @@ echo html_writer::tag('p', $out . ' ' .
$OUTPUT->action_icon(question_preview_url($questionid, null, null, null, null, $context), $OUTPUT->action_icon(question_preview_url($questionid, null, null, null, null, $context),
new pix_icon('t/preview', get_string('preview')))); new pix_icon('t/preview', get_string('preview'))));
// Display a representation of the question, variables and PRTs for easy reference.
echo $OUTPUT->heading(stack_string('questiontext'), 3);
echo html_writer::tag('pre', $question->questiontext, array('class' => 'questiontext'));
$vars = $question->questionvariables;
if ($vars != '') {
$vars = trim($vars) . "\n\n";
}
$inputdisplay = array($vars);
foreach ($question->inputs as $inputname => $input) {
$vars .= $inputname . ':' . $input->get_teacher_answer() . ";\n";
}
$maxima = html_writer::start_tag('div', array('class' => 'questionvariables'));
$maxima .= html_writer::tag('pre', s(trim($vars)));
$maxima .= html_writer::end_tag('div');
echo $maxima;
$offlinemaxima = array();
$nodesummary1 = array();
$nodesummary2 = array();
$graphrepresentation = array();
foreach ($question->prts as $prtname => $prt) {
$nodes = $prt->get_nodes_summary();
$nodesummary1[$prtname] = '';
$nodesummary2[$prtname] = '';
$nodesummary3[$prtname] = '';
$offlinemaxima[$prtname] = $prt->get_maxima_representation();
foreach ($nodes as $key => $node) {
$nodesummary1[$prtname] .= ($key + 1). ': ' . $node->test . "\n";
$nodesummary2[$prtname] .= $node->truenote . "\n";
$nodesummary3[$prtname] .= $node->falsenote . "\n";
}
$graph = $prt->get_prt_graph();
$graphrepresentation[$prtname] = stack_abstract_graph_svg_renderer::render($graph, $prtname . 'graphsvg');
}
flush(); flush();
// Later we only display inputs relevant to a particular PTR, so we sort out prt input requirements here.
$allinputs = array_keys($question->inputs);
$inputsbyprt = array();
foreach ($question->prts as $prtname => $prt) {
$inputsbyprt[$prtname] = $prt->get_required_variables($allinputs);
}
$query = 'SELECT qa.*, qas_last.* $query = 'SELECT qa.*, qas_last.*
FROM {question_attempts} qa FROM {question_attempts} qa
LEFT JOIN {question_attempt_steps} qas_last ON qas_last.questionattemptid = qa.id LEFT JOIN {question_attempt_steps} qas_last ON qas_last.questionattemptid = qa.id
...@@ -132,6 +180,7 @@ foreach ($qprts as $key => $notused) { ...@@ -132,6 +180,7 @@ foreach ($qprts as $key => $notused) {
$qprts[$key] = array(); $qprts[$key] = array();
} }
$prtreport = array(); $prtreport = array();
$prtreportinputs = array();
// Create a summary of the data without different variants. // Create a summary of the data without different variants.
$prtreportsummary = array(); $prtreportsummary = array();
...@@ -139,13 +188,16 @@ $prtreportsummary = array(); ...@@ -139,13 +188,16 @@ $prtreportsummary = array();
foreach ($summary as $variant => $vdata) { foreach ($summary as $variant => $vdata) {
$inputreport[$variant] = $qinputs; $inputreport[$variant] = $qinputs;
$prtreport[$variant] = $qprts; $prtreport[$variant] = $qprts;
$prtreportinputs[$variant] = $qprts;
foreach ($vdata as $attemptsummary => $num) { foreach ($vdata as $attemptsummary => $num) {
$inputvals = array();
$rawdat = explode(';', $attemptsummary); $rawdat = explode(';', $attemptsummary);
foreach ($rawdat as $data) { foreach ($rawdat as $data) {
$data = trim($data); $data = trim($data);
foreach ($qinputs as $input => $notused) { foreach ($qinputs as $input => $notused) {
if (substr($data, 0, strlen($input . ':')) === $input . ':') { if (substr($data, 0, strlen($input . ':')) === $input . ':') {
// Tidy up inputs by (i) trimming status and whitespace, and (2) removing input name.
$datas = trim(substr($data, strlen($input . ':'))); $datas = trim(substr($data, strlen($input . ':')));
$status = 'other'; $status = 'other';
if (strpos($datas, '[score]') !== false) { if (strpos($datas, '[score]') !== false) {
...@@ -158,6 +210,9 @@ foreach ($summary as $variant => $vdata) { ...@@ -158,6 +210,9 @@ foreach ($summary as $variant => $vdata) {
$status = 'invalid'; $status = 'invalid';
$datas = trim(substr($datas, 0, -9)); $datas = trim(substr($datas, 0, -9));
} }
// Reconstruct input string but whitespace is trimmed.
$inputvals[$input] = $input . ':' . $datas;
// Add data.
if (array_key_exists($datas, $inputreport[$variant][$input][$status])) { if (array_key_exists($datas, $inputreport[$variant][$input][$status])) {
$inputreport[$variant][$input][$status][$datas] += (int) $num; $inputreport[$variant][$input][$status][$datas] += (int) $num;
} else { } else {
...@@ -177,12 +232,25 @@ foreach ($summary as $variant => $vdata) { ...@@ -177,12 +232,25 @@ foreach ($summary as $variant => $vdata) {
} }
} }
foreach ($qprts as $prt => $notused) { foreach ($qprts as $prt => $notused) {
// Only create an input summary of the inputs required for this PRT.
$inputsummary = '';
foreach ($inputsbyprt[$prt] as $input) {
if (array_key_exists($input, $inputvals)) {
$inputsummary .= $inputvals[$input] . '; ';
}
}
if (substr($data, 0, strlen($prt . ':')) === $prt . ':') { if (substr($data, 0, strlen($prt . ':')) === $prt . ':') {
$datas = trim(substr($data, strlen($prt . ':'))); $datas = trim(substr($data, strlen($prt . ':')));
if (array_key_exists($datas, $prtreport[$variant][$prt])) { if (array_key_exists($datas, $prtreport[$variant][$prt])) {
$prtreport[$variant][$prt][$datas] += (int) $num; $prtreport[$variant][$prt][$datas] += (int) $num;
if (array_key_exists($inputsummary, $prtreportinputs[$variant][$prt][$datas])) {
$prtreportinputs[$variant][$prt][$datas][$inputsummary] += (int) $num;
} else {
$prtreportinputs[$variant][$prt][$datas][$inputsummary] = (int) $num;
}
} else { } else {
$prtreport[$variant][$prt][$datas] = $num; $prtreport[$variant][$prt][$datas] = $num;
$prtreportinputs[$variant][$prt][$datas] = array($inputsummary => (int) $num);
} }
if (!array_key_exists($prt, $prtreportsummary)) { if (!array_key_exists($prt, $prtreportsummary)) {
$prtreportsummary[$prt] = array(); $prtreportsummary[$prt] = array();
...@@ -240,6 +308,11 @@ foreach ($prtreportsummary as $prt => $tdata) { ...@@ -240,6 +308,11 @@ foreach ($prtreportsummary as $prt => $tdata) {
} }
} }
} }
foreach ($prtreportinputs[$variant][$prt] as $note => $ipts) {
arsort($ipts);
$prtreportinputs[$variant][$prt][$note] = $ipts;
}
} }
foreach ($notesummary as $prt => $tdata) { foreach ($notesummary as $prt => $tdata) {
...@@ -247,7 +320,10 @@ foreach ($notesummary as $prt => $tdata) { ...@@ -247,7 +320,10 @@ foreach ($notesummary as $prt => $tdata) {
$notesummary[$prt] = $tdata; $notesummary[$prt] = $tdata;
} }
$sumout = ''; // Frequency of answer notes, for each PRT, split by |, regardless of which variant was used.
echo html_writer::tag('h3', stack_string('basicreportnotes'));
$sumout = array();
foreach ($prtreportsummary as $prt => $data) { foreach ($prtreportsummary as $prt => $data) {
$sumouti = ''; $sumouti = '';
$tot = 0; $tot = 0;
...@@ -263,71 +339,140 @@ foreach ($prtreportsummary as $prt => $data) { ...@@ -263,71 +339,140 @@ foreach ($prtreportsummary as $prt => $data) {
} }
} }
if (trim($sumouti) !== '') { if (trim($sumouti) !== '') {
$sumout .= '## ' . $prt . ' ('. $tot . ")\n" . $sumouti . "\n";; $sumout[$prt] = '## ' . $prt . ' ('. $tot . ")\n" . $sumouti . "\n";;
} }
} }
if (trim($sumout) !== '') {
echo html_writer::tag('h3', stack_string('basicreportnotes')); foreach ($question->prts as $prtname => $prt) {
echo html_writer::tag('pre', trim($sumout)); echo html_writer::start_tag('table');
echo html_writer::start_tag('tr');
echo html_writer::tag('td', $graphrepresentation[$prtname]);
$node = html_writer::start_tag('div', array('class' => 'questionvariables'));
$node .= html_writer::tag('pre', s($nodesummary1[$prtname]));
$node .= html_writer::end_tag('div');
echo html_writer::tag('td', $node);
$node = html_writer::start_tag('div', array('class' => 'questionvariables'));
$node .= html_writer::tag('pre', s($nodesummary2[$prtname]));
$node .= html_writer::end_tag('div');
echo html_writer::tag('td', $node);
$node = html_writer::start_tag('div', array('class' => 'questionvariables'));
$node .= html_writer::tag('pre', s($nodesummary3[$prtname]));
$node .= html_writer::end_tag('div');
echo html_writer::tag('td', $node);
$maxima = html_writer::start_tag('div', array('class' => 'questionvariables'));
$maxima .= html_writer::tag('pre', s($offlinemaxima[$prtname]));
$maxima .= html_writer::end_tag('div');
echo html_writer::tag('td', $maxima);
echo html_writer::end_tag('tr');
echo html_writer::end_tag('table');
if (array_key_exists($prtname, $sumout)) {
echo html_writer::tag('pre', trim($sumout[$prtname]));
}
} }
$sumout = ''; $sumout = array();
$prtlabels = array();
foreach ($notesummary as $prt => $data) { foreach ($notesummary as $prt => $data) {
$sumouti = ''; $sumouti = '';
if ($data !== array()) { if ($data !== array()) {
foreach ($data as $dat => $num) { foreach ($data as $dat => $num) {
// Use the old $tot, to give meaningful percentages of which individual notes occur overall. // Use the old $tot, to give meaningful percentages of which individual notes occur overall.
$prtlabels[$prt][$dat] = $num;
$sumouti .= str_pad($num, strlen((string) $pad) + 1) . '(' . $sumouti .= str_pad($num, strlen((string) $pad) + 1) . '(' .
str_pad(number_format((float) 100 * $num / $tot, 2, '.', ''), 6, ' ', STR_PAD_LEFT) . str_pad(number_format((float) 100 * $num / $tot, 2, '.', ''), 6, ' ', STR_PAD_LEFT) . '%); '.
'%); ' . $dat . "\n"; $dat . "\n";
} }
} }
if (trim($sumouti) !== '') { if (trim($sumouti) !== '') {
$sumout .= '## ' . $prt . ' ('. $tot . ")\n" . $sumouti . "\n";; $sumout[$prt] = '## ' . $prt . ' ('. $tot . ")\n" . $sumouti . "\n";;
} }
} }
if (trim($sumout) !== '') { if (trim(implode($sumout)) !== '') {
echo html_writer::tag('h3', stack_string('basicreportnotessplit')); echo html_writer::tag('h3', stack_string('basicreportnotessplit'));
echo html_writer::tag('pre', trim($sumout));
} }
echo html_writer::start_tag('table');
foreach ($question->prts as $prtname => $prt) {
if (array_key_exists($prtname, $prtlabels)) {
echo html_writer::start_tag('tr');
echo html_writer::tag('td', $prtname);
echo html_writer::tag('td', $graphrepresentation[$prtname]);
$graph = $prt->get_prt_graph($prtlabels[$prtname]);
echo html_writer::tag('td', stack_abstract_graph_svg_renderer::render($graph, $prtname . 'graphsvg'));
$node = html_writer::start_tag('div', array('class' => 'questionvariables'));
$node .= html_writer::tag('pre', s($nodesummary1[$prtname]));
$node .= html_writer::end_tag('div');
echo html_writer::tag('td', $node);
$node = html_writer::start_tag('div', array('class' => 'questionvariables'));
$node .= html_writer::tag('pre', s($nodesummary2[$prtname]));
$node .= html_writer::end_tag('div');
echo html_writer::tag('td', $node);
$node = html_writer::start_tag('div', array('class' => 'questionvariables'));
$node .= html_writer::tag('pre', s($nodesummary3[$prtname]));
$node .= html_writer::end_tag('div');
echo html_writer::tag('td', $node);
$maxima = html_writer::start_tag('div', array('class' => 'questionvariables'));
$maxima .= html_writer::tag('pre', s($sumout[$prtname]));
$maxima .= html_writer::end_tag('div');
echo html_writer::tag('td', $maxima);
echo html_writer::end_tag('tr');
}
}
echo html_writer::end_tag('table');
// Raw inputs and PRT answer notes by variant.
if (array_keys($summary) !== array()) {
echo html_writer::tag('h3', stack_string('basicreportvariants'));
}
foreach (array_keys($summary) as $variant) {
$sumout = ''; $sumout = '';
foreach ($inputreportsummary as $input => $idata) { foreach ($prtreport[$variant] as $prt => $idata) {
$sumouti = ''; $pad = 0;
$tot = 0; $tot = 0;
foreach ($idata as $key => $data) { foreach ($idata as $dat => $num) {
foreach ($data as $dat => $num) {
$tot += $num; $tot += $num;
} }
if ($idata !== array()) {
$sumout .= '## ' . $prt . ' ('. $tot . ")\n";
$pad = max($idata);
} }
foreach ($idata as $key => $data) { foreach ($idata as $dat => $num) {
if ($data !== array()) { $sumout .= str_pad($num, strlen((string) $pad) + 1) . '(' .
$sumouti .= '### ' . $key . "\n";
$pad = max($data);
foreach ($data as $dat => $num) {
$sumouti .= str_pad($num, strlen((string) $pad) + 1) . '(' .
str_pad(number_format((float) 100 * $num / $tot, 2, '.', ''), 6, ' ', STR_PAD_LEFT) . str_pad(number_format((float) 100 * $num / $tot, 2, '.', ''), 6, ' ', STR_PAD_LEFT) .
'%); ' . $dat . "\n"; '%); ' . $dat . "\n";
foreach ($prtreportinputs[$variant][$prt][$dat] as $inputsummary => $inum) {
$sumout .= str_pad($inum, strlen((string) $pad) + 1) . '(' .
str_pad(number_format((float) 100 * $inum / $tot, 2, '.', ''), 6, ' ', STR_PAD_LEFT) .
'%); ' . $inputsummary . "\n";
} }
$sumouti .= "\n"; $sumout .= "\n";
}
}
if (trim($sumouti) !== '') {
$sumout .= '## ' . $input . ' ('. $tot . ")\n" . $sumouti;
} }
$sumout .= "\n";
} }
if (trim($sumout) !== '') { if (trim($sumout) !== '') {
echo html_writer::tag('h3', stack_string('basicreportinputsummary')); echo html_writer::tag('h3', $variant);
echo html_writer::tag('pre', $sumout); echo html_writer::tag('pre', $sumout);
} }
// Output a report.
if (array_keys($summary) !== array()) {
echo html_writer::tag('h2', stack_string('basicreportvariants'));
} }
foreach (array_keys($summary) as $variant) { foreach (array_keys($summary) as $variant) {
// TODO: how do we go from a variant to a seed (if there is one....)? // TODO: how do we go from a variant to a seed (if there is one....)?
echo html_writer::tag('h3', $variant);
$sumout = ''; $sumout = '';
foreach ($inputreport[$variant] as $input => $idata) { foreach ($inputreport[$variant] as $input => $idata) {
$sumouti = ''; $sumouti = '';
...@@ -354,33 +499,44 @@ foreach (array_keys($summary) as $variant) { ...@@ -354,33 +499,44 @@ foreach (array_keys($summary) as $variant) {
} }
} }
if (trim($sumout) !== '') { if (trim($sumout) !== '') {
echo html_writer::tag('h3', $variant);
echo html_writer::tag('pre', $sumout); echo html_writer::tag('pre', $sumout);
} }
}
// Summary of just the inputs.
$sumout = ''; $sumout = '';
foreach ($prtreport[$variant] as $prt => $idata) { foreach ($inputreportsummary as $input => $idata) {
$pad = 0; $sumouti = '';
$tot = 0; $tot = 0;
foreach ($idata as $dat => $num) { foreach ($idata as $key => $data) {
foreach ($data as $dat => $num) {
$tot += $num; $tot += $num;
} }
if ($idata !== array()) {
$sumout .= '## ' . $prt . ' ('. $tot . ")\n";
$pad = max($idata);
} }
foreach ($idata as $dat => $num) { foreach ($idata as $key => $data) {
$sumout .= str_pad($num, strlen((string) $pad) + 1) . '(' . if ($data !== array()) {
$sumouti .= '### ' . $key . "\n";
$pad = max($data);
foreach ($data as $dat => $num) {
$sumouti .= str_pad($num, strlen((string) $pad) + 1) . '(' .
str_pad(number_format((float) 100 * $num / $tot, 2, '.', ''), 6, ' ', STR_PAD_LEFT) . str_pad(number_format((float) 100 * $num / $tot, 2, '.', ''), 6, ' ', STR_PAD_LEFT) .
'%); ' . $dat . "\n"; '%); ' . $dat . "\n";
} }
$sumout .= "\n"; $sumouti .= "\n";
}
}
if (trim($sumouti) !== '') {
$sumout .= '## ' . $input . ' ('. $tot . ")\n" . $sumouti;
}
} }
if (trim($sumout) !== '') { if (trim($sumout) !== '') {
echo html_writer::tag('h3', stack_string('basicreportinputsummary'));
echo html_writer::tag('pre', $sumout); echo html_writer::tag('pre', $sumout);
} }
}
echo html_writer::tag('h2', stack_string('basicreportraw')); // Output the raw data.
echo html_writer::tag('h3', stack_string('basicreportraw'));
$sumout = ''; $sumout = '';
foreach ($summary as $variant => $vdata) { foreach ($summary as $variant => $vdata) {
if ($vdata !== array()) { if ($vdata !== array()) {
......
...@@ -371,6 +371,7 @@ class stack_potentialresponse_node { ...@@ -371,6 +371,7 @@ class stack_potentialresponse_node {
$summary->truenote = $this->branches[true]['answernote']; $summary->truenote = $this->branches[true]['answernote'];
$summary->truescore = $this->branches[true]['score']; $summary->truescore = $this->branches[true]['score'];
$summary->truescoremode = $this->branches[true]['scoremodification']; $summary->truescoremode = $this->branches[true]['scoremodification'];
$summary->test = $this->get_maxima_representation();
return $summary; return $summary;
} }
......
...@@ -473,4 +473,39 @@ class stack_potentialresponse_tree { ...@@ -473,4 +473,39 @@ class stack_potentialresponse_tree {
'3' => get_string('feedbackstyle3', 'qtype_stack'), '3' => get_string('feedbackstyle3', 'qtype_stack'),
); );
} }
/*
* @param array $labels an array of labels for the branches.
*/
public function get_prt_graph($labels = false) {
$graph = new stack_abstract_graph();
foreach ($this->nodes as $key => $node) {
$summary = $node->summarise_branches();
if ($summary->truenextnode == -1) {
$left = null;
} else {
$left = $summary->truenextnode + 1;
}
if ($summary->falsenextnode == -1) {
$right = null;
} else {
$right = $summary->falsenextnode + 1;
}
$llabel = $summary->truescoremode . round($summary->truescore, 2);
if ($labels && array_key_exists($summary->truenote, $labels)) {
$llabel = $labels[$summary->truenote];
}
$rlabel = $summary->falsescoremode . round($summary->falsescore, 2);
if ($labels && array_key_exists($summary->falsenote, $labels)) {
$rlabel = $labels[$summary->falsenote];
}
$graph->add_node($key + 1, $left, $right, $llabel, $rlabel,
'#fgroup_id_' . $this->name . 'node_' . $key);
}
$graph->layout();
return $graph;
}
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment