417
392
* Output the table that lists all the questions in the quiz with their statistics.
418
393
* @param int $s number of attempts.
419
* @param array $questions the questions in the quiz.
420
* @param array $subquestions the subquestions of any random questions.
394
* @param \core_question\statistics\questions\calculated[] $questionstats the stats for the main questions in the quiz.
395
* @param \core_question\statistics\questions\calculated_for_subquestion[] $subquestionstats the stats of any random questions.
422
protected function output_quiz_structure_analysis_table($s, $questions, $subquestions) {
397
protected function output_quiz_structure_analysis_table($s, $questionstats, $subquestionstats) {
427
foreach ($questions as $question) {
428
// Output the data for this questions.
429
$this->table->add_data_keyed($this->table->format_row($question));
402
foreach ($questionstats as $questionstat) {
403
// Output the data for these question statistics.
404
$this->table->add_data_keyed($this->table->format_row($questionstat));
431
if (empty($question->_stats->subquestions)) {
406
if (empty($questionstat->subquestions)) {
435
410
// And its subquestions, if it has any.
436
$subitemstodisplay = explode(',', $question->_stats->subquestions);
411
$subitemstodisplay = explode(',', $questionstat->subquestions);
437
412
foreach ($subitemstodisplay as $subitemid) {
438
$subquestions[$subitemid]->maxmark = $question->maxmark;
439
$this->table->add_data_keyed($this->table->format_row($subquestions[$subitemid]));
413
$subquestionstats[$subitemid]->maxmark = $questionstat->maxmark;
414
$this->table->add_data_keyed($this->table->format_row($subquestionstats[$subitemid]));
443
418
$this->table->finish_output(!$this->table->is_downloading());
446
protected function get_formatted_quiz_info_data($course, $cm, $quiz, $quizstats) {
448
// You can edit this array to control which statistics are displayed.
449
$todisplay = array('firstattemptscount' => 'number',
450
'allattemptscount' => 'number',
451
'firstattemptsavg' => 'summarks_as_percentage',
452
'allattemptsavg' => 'summarks_as_percentage',
453
'median' => 'summarks_as_percentage',
454
'standarddeviation' => 'summarks_as_percentage',
455
'skewness' => 'number_format',
456
'kurtosis' => 'number_format',
457
'cic' => 'number_format_percent',
458
'errorratio' => 'number_format_percent',
459
'standarderror' => 'summarks_as_percentage');
461
// General information about the quiz.
463
$quizinfo[get_string('quizname', 'quiz_statistics')] = format_string($quiz->name);
464
$quizinfo[get_string('coursename', 'quiz_statistics')] = format_string($course->fullname);
466
$quizinfo[get_string('idnumbermod')] = $cm->idnumber;
468
if ($quiz->timeopen) {
469
$quizinfo[get_string('quizopen', 'quiz')] = userdate($quiz->timeopen);
471
if ($quiz->timeclose) {
472
$quizinfo[get_string('quizclose', 'quiz')] = userdate($quiz->timeclose);
474
if ($quiz->timeopen && $quiz->timeclose) {
475
$quizinfo[get_string('duration', 'quiz_statistics')] =
476
format_time($quiz->timeclose - $quiz->timeopen);
480
foreach ($todisplay as $property => $format) {
481
if (!isset($quizstats->$property) || !$format) {
484
$value = $quizstats->$property;
487
case 'summarks_as_percentage':
488
$formattedvalue = quiz_report_scale_summarks_as_percentage($value, $quiz);
490
case 'number_format_percent':
491
$formattedvalue = quiz_format_grade($quiz, $value) . '%';
493
case 'number_format':
494
// 2 extra decimal places, since not a percentage,
495
// and we want the same number of sig figs.
496
$formattedvalue = format_float($value, $quiz->decimalpoints + 2);
499
$formattedvalue = $value + 0;
502
$formattedvalue = $value;
505
$quizinfo[get_string($property, 'quiz_statistics',
506
$this->using_attempts_string(!empty($quizstats->allattempts)))] =
514
422
* Output the table of overall quiz statistics.
515
423
* @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
564
472
* Output the HTML needed to show the statistics graph.
565
* @param int $quizstatsid the id of the statistics to show in the graph.
474
* @param $currentgroup
475
* @param $whichattempts
567
protected function output_statistics_graph($quizstatsid, $s) {
477
protected function output_statistics_graph($quizid, $currentgroup, $whichattempts) {
574
480
$output = $PAGE->get_renderer('mod_quiz');
575
481
$imageurl = new moodle_url('/mod/quiz/report/statistics/statistics_graph.php',
576
array('id' => $quizstatsid));
482
compact('quizid', 'currentgroup', 'whichattempts'));
577
483
$graphname = get_string('statisticsreportgraph', 'quiz_statistics');
578
484
echo $output->graph($imageurl, $graphname);
582
* Return the stats data for when there are no stats to show.
584
* @param array $questions question definitions.
585
* @param int $firstattemptscount number of first attempts (optional).
586
* @param int $firstattemptscount total number of attempts (optional).
587
* @return array with three elements:
588
* - integer $s Number of attempts included in the stats (0).
589
* - array $quizstats The statistics for overall attempt scores.
590
* - array $qstats The statistics for each question.
592
protected function get_emtpy_stats($questions, $firstattemptscount = 0,
593
$allattemptscount = 0) {
594
$quizstats = new stdClass();
595
$quizstats->firstattemptscount = $firstattemptscount;
596
$quizstats->allattemptscount = $allattemptscount;
598
$qstats = new stdClass();
599
$qstats->questions = $questions;
600
$qstats->subquestions = array();
601
$qstats->responses = array();
603
return array(0, $quizstats, false);
607
* Compute the quiz statistics.
609
* @param object $quizid the quiz id.
610
* @param int $currentgroup the current group. 0 for none.
611
* @param bool $nostudentsingroup true if there a no students.
612
* @param bool $useallattempts use all attempts, or just first attempts.
613
* @param array $groupstudents students in this group.
614
* @param array $questions question definitions.
615
* @return array with three elements:
616
* - integer $s Number of attempts included in the stats.
617
* - array $quizstats The statistics for overall attempt scores.
618
* - array $qstats The statistics for each question.
620
protected function compute_stats($quizid, $currentgroup, $nostudentsingroup,
621
$useallattempts, $groupstudents, $questions) {
624
// Calculating MEAN of marks for all attempts by students
625
// http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
626
// #Calculating_MEAN_of_grades_for_all_attempts_by_students.
627
if ($nostudentsingroup) {
628
return $this->get_emtpy_stats($questions);
631
list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
632
$quizid, $currentgroup, $groupstudents, true);
634
$attempttotals = $DB->get_records_sql("
636
CASE WHEN attempt = 1 THEN 1 ELSE 0 END AS isfirst,
637
COUNT(1) AS countrecs,
638
SUM(sumgrades) AS total
641
GROUP BY CASE WHEN attempt = 1 THEN 1 ELSE 0 END", $qaparams);
643
if (!$attempttotals) {
644
return $this->get_emtpy_stats($questions);
647
if (isset($attempttotals[1])) {
648
$firstattempts = $attempttotals[1];
649
$firstattempts->average = $firstattempts->total / $firstattempts->countrecs;
651
$firstattempts = new stdClass();
652
$firstattempts->countrecs = 0;
653
$firstattempts->total = 0;
654
$firstattempts->average = null;
657
$allattempts = new stdClass();
658
if (isset($attempttotals[0])) {
659
$allattempts->countrecs = $firstattempts->countrecs + $attempttotals[0]->countrecs;
660
$allattempts->total = $firstattempts->total + $attempttotals[0]->total;
662
$allattempts->countrecs = $firstattempts->countrecs;
663
$allattempts->total = $firstattempts->total;
666
if ($useallattempts) {
667
$usingattempts = $allattempts;
668
$usingattempts->sql = '';
670
$usingattempts = $firstattempts;
671
$usingattempts->sql = 'AND quiza.attempt = 1 ';
674
$s = $usingattempts->countrecs;
676
return $this->get_emtpy_stats($questions, $firstattempts->countrecs,
677
$allattempts->countrecs);
679
$summarksavg = $usingattempts->total / $usingattempts->countrecs;
681
$quizstats = new stdClass();
682
$quizstats->allattempts = $useallattempts;
683
$quizstats->firstattemptscount = $firstattempts->countrecs;
684
$quizstats->allattemptscount = $allattempts->countrecs;
685
$quizstats->firstattemptsavg = $firstattempts->average;
686
$quizstats->allattemptsavg = $allattempts->total / $allattempts->countrecs;
688
// Recalculate sql again this time possibly including test for first attempt.
689
list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
690
$quizid, $currentgroup, $groupstudents, $useallattempts);
694
// An even number of attempts.
695
$limitoffset = $s/2 - 1;
698
$limitoffset = floor($s/2);
701
$sql = "SELECT id, sumgrades
706
$medianmarks = $DB->get_records_sql_menu($sql, $qaparams, $limitoffset, $limit);
708
$quizstats->median = array_sum($medianmarks) / count($medianmarks);
710
// Fetch the sum of squared, cubed and power 4d
711
// differences between marks and mean mark.
712
$mean = $usingattempts->total / $s;
714
SUM(POWER((quiza.sumgrades - $mean), 2)) AS power2,
715
SUM(POWER((quiza.sumgrades - $mean), 3)) AS power3,
716
SUM(POWER((quiza.sumgrades - $mean), 4)) AS power4
719
$params = array('mean1' => $mean, 'mean2' => $mean, 'mean3' => $mean)+$qaparams;
721
$powers = $DB->get_record_sql($sql, $params, MUST_EXIST);
723
// Standard_Deviation:
724
// see http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
725
// #Standard_Deviation.
727
$quizstats->standarddeviation = sqrt($powers->power2 / ($s - 1));
731
// See http://docs.moodle.org/dev/
732
// Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis.
733
$m2= $powers->power2 / $s;
734
$m3= $powers->power3 / $s;
735
$m4= $powers->power4 / $s;
738
$k3= $s*$s*$m3/(($s-1)*($s-2));
740
$quizstats->skewness = $k3 / (pow($k2, 3/2));
746
$k4= $s*$s*((($s+1)*$m4)-(3*($s-1)*$m2*$m2))/(($s-1)*($s-2)*($s-3));
748
$quizstats->kurtosis = $k4 / ($k2*$k2);
753
$qstats = new quiz_statistics_question_stats($questions, $s, $summarksavg);
754
$qstats->load_step_data($quizid, $currentgroup, $groupstudents, $useallattempts);
755
$qstats->compute_statistics();
758
$p = count($qstats->questions); // Number of positions.
759
if ($p > 1 && isset($k2)) {
760
$quizstats->cic = (100 * $p / ($p -1)) *
761
(1 - ($qstats->get_sum_of_mark_variance()) / $k2);
762
$quizstats->errorratio = 100 * sqrt(1 - ($quizstats->cic / 100));
763
$quizstats->standarderror = $quizstats->errorratio *
764
$quizstats->standarddeviation / 100;
768
return array($s, $quizstats, $qstats);
772
* Load the cached statistics from the database.
774
* @param object $quiz the quiz settings
775
* @param int $currentgroup the current group. 0 for none.
776
* @param bool $nostudentsingroup true if there a no students.
777
* @param bool $useallattempts use all attempts, or just first attempts.
778
* @param array $groupstudents students in this group.
779
* @param array $questions question definitions.
780
* @return array with 4 elements:
781
* - $quizstats The statistics for overall attempt scores.
782
* - $questions The questions, with an additional _stats field.
783
* - $subquestions The subquestions, if any, with an additional _stats field.
784
* - $s Number of attempts included in the stats.
785
* If there is no cached data in the database, returns an array of four nulls.
787
protected function try_loading_cached_stats($quiz, $currentgroup,
788
$nostudentsingroup, $useallattempts, $groupstudents, $questions) {
791
$timemodified = time() - self::TIME_TO_CACHE_STATS;
792
$quizstats = $DB->get_record_select('quiz_statistics',
793
'quizid = ? AND groupid = ? AND allattempts = ? AND timemodified > ?',
794
array($quiz->id, $currentgroup, $useallattempts, $timemodified));
797
// No cached data found.
798
return array(null, $questions, null, null);
801
if ($useallattempts) {
802
$s = $quizstats->allattemptscount;
804
$s = $quizstats->firstattemptscount;
807
$subquestions = array();
808
$questionstats = $DB->get_records('quiz_question_statistics',
809
array('quizstatisticsid' => $quizstats->id));
811
$subquestionstats = array();
812
foreach ($questionstats as $stat) {
814
$questions[$stat->slot]->_stats = $stat;
816
$subquestionstats[$stat->questionid] = $stat;
820
if (!empty($subquestionstats)) {
821
$subqstofetch = array_keys($subquestionstats);
822
$subquestions = question_load_questions($subqstofetch);
823
foreach ($subquestions as $subqid => $subq) {
824
$subquestions[$subqid]->_stats = $subquestionstats[$subqid];
825
$subquestions[$subqid]->maxmark = $subq->defaultmark;
829
return array($quizstats, $questions, $subquestions, $s);
833
* Store the statistics in the cache tables in the database.
835
* @param object $quizid the quiz id.
836
* @param int $currentgroup the current group. 0 for none.
837
* @param bool $useallattempts use all attempts, or just first attempts.
838
* @param object $quizstats The statistics for overall attempt scores.
839
* @param array $questions The questions, with an additional _stats field.
840
* @param array $subquestions The subquestions, if any, with an additional _stats field.
842
protected function cache_stats($quizid, $currentgroup,
843
$quizstats, $questions, $subquestions) {
846
$toinsert = clone($quizstats);
847
$toinsert->quizid = $quizid;
848
$toinsert->groupid = $currentgroup;
849
$toinsert->timemodified = time();
851
// Fix up some dodgy data.
852
if (isset($toinsert->errorratio) && is_nan($toinsert->errorratio)) {
853
$toinsert->errorratio = null;
855
if (isset($toinsert->standarderror) && is_nan($toinsert->standarderror)) {
856
$toinsert->standarderror = null;
860
$quizstats->id = $DB->insert_record('quiz_statistics', $toinsert);
862
foreach ($questions as $question) {
863
$question->_stats->quizstatisticsid = $quizstats->id;
864
$DB->insert_record('quiz_question_statistics', $question->_stats, false);
867
foreach ($subquestions as $subquestion) {
868
$subquestion->_stats->quizstatisticsid = $quizstats->id;
869
$DB->insert_record('quiz_question_statistics', $subquestion->_stats, false);
872
return $quizstats->id;
876
488
* Get the quiz and question statistics, either by loading the cached results,
877
489
* or by recomputing them.
879
491
* @param object $quiz the quiz settings.
880
* @param int $currentgroup the current group. 0 for none.
881
* @param bool $nostudentsingroup true if there a no students.
882
* @param bool $useallattempts use all attempts, or just first attempts.
492
* @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
493
* $quiz->grademethod ie.
494
* QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
495
* we calculate stats based on which attempts would affect the grade for each student.
883
496
* @param array $groupstudents students in this group.
884
* @param array $questions question definitions.
497
* @param array $questions full question data.
885
498
* @return array with 4 elements:
886
499
* - $quizstats The statistics for overall attempt scores.
887
* - $questions The questions, with an additional _stats field.
888
* - $subquestions The subquestions, if any, with an additional _stats field.
889
* - $s Number of attempts included in the stats.
500
* - $questionstats array of \core_question\statistics\questions\calculated objects keyed by slot.
501
* - $subquestionstats array of \core_question\statistics\questions\calculated_for_subquestion objects keyed by question id.
891
protected function get_quiz_and_questions_stats($quiz, $currentgroup,
892
$nostudentsingroup, $useallattempts, $groupstudents, $questions) {
894
list($quizstats, $questions, $subquestions, $s) =
895
$this->try_loading_cached_stats($quiz, $currentgroup, $nostudentsingroup,
896
$useallattempts, $groupstudents, $questions);
898
if (is_null($quizstats)) {
899
list($s, $quizstats, $qstats) = $this->compute_stats($quiz->id,
900
$currentgroup, $nostudentsingroup, $useallattempts, $groupstudents, $questions);
903
$questions = $qstats->questions;
904
$subquestions = $qstats->subquestions;
906
$quizstatisticsid = $this->cache_stats($quiz->id, $currentgroup,
907
$quizstats, $questions, $subquestions);
909
$this->analyse_responses($quizstatisticsid, $quiz->id, $currentgroup,
910
$nostudentsingroup, $useallattempts, $groupstudents,
911
$questions, $subquestions);
503
public function get_quiz_and_questions_stats($quiz, $whichattempts, $groupstudents, $questions) {
505
$qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts);
507
$qcalc = new \core_question\statistics\questions\calculator($questions);
509
$quizcalc = new quiz_statistics_calculator();
511
if ($quizcalc->get_last_calculated_time($qubaids) === false) {
513
list($questionstats, $subquestionstats) = $qcalc->calculate($qubaids);
515
$quizstats = $quizcalc->calculate($quiz->id, $whichattempts, $groupstudents, count($questions),
516
$qcalc->get_sum_of_mark_variance());
518
if ($quizstats->s()) {
519
$this->analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats);
522
$quizstats = $quizcalc->get_cached($qubaids);
523
list($questionstats, $subquestionstats) = $qcalc->get_cached($qubaids);
915
return array($quizstats, $questions, $subquestions, $s);
526
return array($quizstats, $questionstats, $subquestionstats);
918
protected function analyse_responses($quizstatisticsid, $quizid, $currentgroup,
919
$nostudentsingroup, $useallattempts, $groupstudents, $questions, $subquestions) {
921
$qubaids = quiz_statistics_qubaids_condition(
922
$quizid, $currentgroup, $groupstudents, $useallattempts);
529
protected function analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats) {
925
532
foreach ($questions as $question) {
1022
627
* Clear the cached data for a particular report configuration. This will
1023
628
* trigger a re-computation the next time the report is displayed.
1024
* @param int $quizid the quiz id.
1025
* @param int $currentgroup a group id, or 0.
1026
* @param bool $useallattempts whether all attempts, or just first attempts are included.
629
* @param $qubaids qubaid_condition
1028
protected function clear_cached_data($quizid, $currentgroup, $useallattempts) {
631
protected function clear_cached_data($qubaids) {
1031
$todelete = $DB->get_records_menu('quiz_statistics', array('quizid' => $quizid,
1032
'groupid' => $currentgroup, 'allattempts' => $useallattempts), '', 'id, 1');
1038
list($todeletesql, $todeleteparams) = $DB->get_in_or_equal(array_keys($todelete));
1040
$DB->delete_records_select('quiz_question_statistics',
1041
'quizstatisticsid ' . $todeletesql, $todeleteparams);
1042
$DB->delete_records_select('quiz_question_response_stats',
1043
'quizstatisticsid ' . $todeletesql, $todeleteparams);
1044
$DB->delete_records_select('quiz_statistics',
1045
'id ' . $todeletesql, $todeleteparams);
633
$DB->delete_records('quiz_statistics', array('hashcode' => $qubaids->get_hash_code()));
634
$DB->delete_records('question_statistics', array('hashcode' => $qubaids->get_hash_code()));
635
$DB->delete_records('question_response_analysis', array('hashcode' => $qubaids->get_hash_code()));
1049
* @param bool $useallattempts whether we are using all attempts.
1050
* @return the appropriate lang string to describe this option.
639
* @param object $quiz the quiz.
640
* @return array of questions for this quiz.
1052
protected function using_attempts_string($useallattempts) {
1053
if ($useallattempts) {
1054
return get_string('allattempts', 'quiz_statistics');
1056
return get_string('firstattempts', 'quiz_statistics');
1061
function quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents,
1062
$allattempts = true, $includeungraded = false) {
1065
$fromqa = '{quiz_attempts} quiza ';
1067
$whereqa = 'quiza.quiz = :quizid AND quiza.preview = 0 AND quiza.state = :quizstatefinished';
1068
$qaparams = array('quizid' => $quizid, 'quizstatefinished' => quiz_attempt::FINISHED);
1070
if (!empty($currentgroup) && $groupstudents) {
1071
list($grpsql, $grpparams) = $DB->get_in_or_equal(array_keys($groupstudents),
1072
SQL_PARAMS_NAMED, 'u');
1073
$whereqa .= " AND quiza.userid $grpsql";
1074
$qaparams += $grpparams;
1077
if (!$allattempts) {
1078
$whereqa .= ' AND quiza.attempt = 1';
1081
if (!$includeungraded) {
1082
$whereqa .= ' AND quiza.sumgrades IS NOT NULL';
1085
return array($fromqa, $whereqa, $qaparams);
1089
* Return a {@link qubaid_condition} from the values returned by
1090
* {@link quiz_statistics_attempts_sql}
1091
* @param string $fromqa from quiz_statistics_attempts_sql.
1092
* @param string $whereqa from quiz_statistics_attempts_sql.
1094
function quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents,
1095
$allattempts = true, $includeungraded = false) {
1096
list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $currentgroup,
1097
$groupstudents, $allattempts, $includeungraded);
1098
return new qubaid_join($fromqa, 'quiza.uniqueid', $whereqa, $qaparams);
642
public function load_and_initialise_questions_for_calculations($quiz) {
643
// Load the questions.
644
$questions = quiz_report_get_significant_questions($quiz);
645
$questionids = array();
646
foreach ($questions as $question) {
647
$questionids[] = $question->id;
649
$fullquestions = question_load_questions($questionids);
650
foreach ($questions as $qno => $question) {
651
$q = $fullquestions[$question->id];
652
$q->maxmark = $question->maxmark;
654
$q->number = $question->number;
655
$questions[$qno] = $q;