* Constructs the progress reporter. This will not output HTML just yet,
* until the required delay time expires.
*
- * @param string $heading Text to display above bar (if it appears); '' for none
+ * @param string $heading Text to display above bar (if it appears); '' for none (default)
* @param int $delay Delay time (default 5 seconds)
*/
- public function __construct($heading, $delay = self::DEFAULT_DISPLAY_DELAY) {
+ public function __construct($heading = '', $delay = self::DEFAULT_DISPLAY_DELAY) {
// Set start time based on delay.
$this->starttime = time() + $delay;
$this->heading = $heading;
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+namespace quiz_statistics;
+
+defined('MOODLE_INTERNAL') || die();
+
/**
* The statistics calculator returns an instance of this class which contains the calculated statistics.
*
* @author James Pratt me@jamiep.org
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class quiz_statistics_calculated {
+class calculated {
/**
* @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
* @return int|float
*/
protected function get_field($field) {
- $fieldname = quiz_statistics_calculator::using_attempts_string_id($this->whichattempts).$field;
+ $fieldname = calculator::using_attempts_string_id($this->whichattempts).$field;
return $this->{$fieldname};
}
}
$quizinfo[get_string($property, 'quiz_statistics',
- quiz_statistics_calculator::using_attempts_lang_string($this->whichattempts))] = $formattedvalue;
+ calculator::using_attempts_lang_string($this->whichattempts))] = $formattedvalue;
}
return $quizinfo;
/**
* Cache the stats contained in this class.
*
- * @param $qubaids qubaid_condition
+ * @param $qubaids \qubaid_condition
*/
public function cache($qubaids) {
global $DB;
- $toinsert = new stdClass();
+ $toinsert = new \stdClass();
foreach ($this->fieldsindb as $field) {
$toinsert->{$field} = $this->{$field};
/**
* Given a record from 'quiz_statistics' table load the data into the properties of this class.
*
- * @param $record from db.
+ * @param $record \stdClass from db.
*/
public function populate_from_record($record) {
foreach ($this->fieldsindb as $field) {
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+namespace quiz_statistics;
+defined('MOODLE_INTERNAL') || die();
+
/**
* Class to calculate and also manage caching of quiz statistics.
*
* @author James Pratt me@jamiep.org
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class quiz_statistics_calculator {
+class calculator {
+
+ /**
+ * @var \core\progress\base
+ */
+ protected $progress;
+
+ public function __construct(\core\progress\base $progress = null) {
+ if ($progress === null) {
+ $progress = new \core\progress\null();
+ }
+ $this->progress = $progress;
+ }
/**
* Compute the quiz statistics.
* @param array $groupstudents students in this group.
* @param int $p number of positions (slots).
* @param float $sumofmarkvariance sum of mark variance, calculated as part of question statistics
- * @return quiz_statistics_calculated $quizstats The statistics for overall attempt scores.
+ * @return calculated $quizstats The statistics for overall attempt scores.
*/
public function calculate($quizid, $whichattempts, $groupstudents, $p, $sumofmarkvariance) {
+ $this->progress->start_progress('', 3);
-
- $quizstats = new quiz_statistics_calculated($whichattempts);
+ $quizstats = new calculated($whichattempts);
$countsandaverages = $this->attempt_counts_and_averages($quizid, $groupstudents);
+ $this->progress->progress(1);
foreach ($countsandaverages as $propertyname => $value) {
$quizstats->{$propertyname} = $value;
}
$s = $quizstats->s();
-
- if ($s == 0) {
- return $quizstats;
- }
-
- // Recalculate sql again this time possibly including test for first attempt.
- list($fromqa, $whereqa, $qaparams) =
- quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts);
-
- $quizstats->median = $this->median($s, $fromqa, $whereqa, $qaparams);
-
- if ($s > 1) {
-
- $powers = $this->sum_of_powers_of_difference_to_mean($quizstats->avg(), $fromqa, $whereqa, $qaparams);
-
- $quizstats->standarddeviation = sqrt($powers->power2 / ($s - 1));
-
- // Skewness.
- if ($s > 2) {
- // See http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis.
- $m2 = $powers->power2 / $s;
- $m3 = $powers->power3 / $s;
- $m4 = $powers->power4 / $s;
-
- $k2 = $s * $m2 / ($s - 1);
- $k3 = $s * $s * $m3 / (($s - 1) * ($s - 2));
- if ($k2 != 0) {
- $quizstats->skewness = $k3 / (pow($k2, 3 / 2));
-
- // Kurtosis.
- if ($s > 3) {
- $k4 = $s * $s * ((($s + 1) * $m4) - (3 * ($s - 1) * $m2 * $m2)) / (($s - 1) * ($s - 2) * ($s - 3));
- $quizstats->kurtosis = $k4 / ($k2 * $k2);
+ if ($s != 0) {
+
+ // Recalculate sql again this time possibly including test for first attempt.
+ list($fromqa, $whereqa, $qaparams) =
+ quiz_statistics_attempts_sql($quizid, $groupstudents, $whichattempts);
+
+ $quizstats->median = $this->median($s, $fromqa, $whereqa, $qaparams);
+ $this->progress->progress(2);
+
+ if ($s > 1) {
+
+ $powers = $this->sum_of_powers_of_difference_to_mean($quizstats->avg(), $fromqa, $whereqa, $qaparams);
+ $this->progress->progress(3);
+
+ $quizstats->standarddeviation = sqrt($powers->power2 / ($s - 1));
+
+ // Skewness.
+ if ($s > 2) {
+ // See http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis.
+ $m2 = $powers->power2 / $s;
+ $m3 = $powers->power3 / $s;
+ $m4 = $powers->power4 / $s;
+
+ $k2 = $s * $m2 / ($s - 1);
+ $k3 = $s * $s * $m3 / (($s - 1) * ($s - 2));
+ if ($k2 != 0) {
+ $quizstats->skewness = $k3 / (pow($k2, 3 / 2));
+
+ // Kurtosis.
+ if ($s > 3) {
+ $k4 = $s * $s * ((($s + 1) * $m4) - (3 * ($s - 1) * $m2 * $m2)) / (($s - 1) * ($s - 2) * ($s - 3));
+ $quizstats->kurtosis = $k4 / ($k2 * $k2);
+ }
+
+ if ($p > 1) {
+ $quizstats->cic = (100 * $p / ($p - 1)) * (1 - ($sumofmarkvariance / $k2));
+ $quizstats->errorratio = 100 * sqrt(1 - ($quizstats->cic / 100));
+ $quizstats->standarderror = $quizstats->errorratio *
+ $quizstats->standarddeviation / 100;
+ }
}
- if ($p > 1) {
- $quizstats->cic = (100 * $p / ($p - 1)) * (1 - ($sumofmarkvariance / $k2));
- $quizstats->errorratio = 100 * sqrt(1 - ($quizstats->cic / 100));
- $quizstats->standarderror = $quizstats->errorratio *
- $quizstats->standarddeviation / 100;
- }
}
-
}
- }
-
- $quizstats->cache(quiz_statistics_qubaids_condition($quizid, $groupstudents, $whichattempts));
+ $quizstats->cache(quiz_statistics_qubaids_condition($quizid, $groupstudents, $whichattempts));
+ }
+ $this->progress->end_progress();
return $quizstats;
}
/**
* Load cached statistics from the database.
*
- * @param $qubaids qubaid_condition
- * @return quiz_statistics_calculated The statistics for overall attempt scores or false if not cached.
+ * @param $qubaids \qubaid_condition
+ * @return calculated The statistics for overall attempt scores or false if not cached.
*/
public function get_cached($qubaids) {
global $DB;
$timemodified = time() - self::TIME_TO_CACHE;
$fromdb = $DB->get_record_select('quiz_statistics', 'hashcode = ? AND timemodified > ?',
array($qubaids->get_hash_code(), $timemodified));
- $stats = new quiz_statistics_calculated();
+ $stats = new calculated();
$stats->populate_from_record($fromdb);
return $stats;
}
/**
* Find time of non-expired statistics in the database.
*
- * @param $qubaids qubaid_condition
+ * @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) {
* #Calculating_MEAN_of_grades_for_all_attempts_by_students
* @param int $quizid
* @param array $groupstudents
- * @return stdClass with properties with count and avg with prefixes firstattempts, highestattempts, etc.
+ * @return \stdClass with properties with count and avg with prefixes firstattempts, highestattempts, etc.
*/
protected function attempt_counts_and_averages($quizid, $groupstudents) {
global $DB;
- $attempttotals = new stdClass();
+ $attempttotals = new \stdClass();
foreach (array_keys(quiz_get_grading_options()) as $which) {
list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudents, $which);
$string['attemptsfirst'] = 'first attempt';
$string['backtoquizreport'] = 'Back to main statistics report page.';
$string['calculatefrom'] = 'Calculate statistics from';
+$string['calculatingallstats'] = 'Calculating statistics for quiz, questions and analysing repsonse data';
$string['cic'] = 'Coefficient of internal consistency (for {$a})';
$string['completestatsfilename'] = 'completestats';
$string['count'] = 'Count';
/** @var quiz_statistics_table instance of table class used for main questions stats table. */
protected $table;
+ /** @var \core\progress\base|null $progress Handles progress reporting or not. */
+ protected $progress = null;
+
/**
* Display the report.
*/
public function display($quiz, $cm, $course) {
- global $CFG, $DB, $OUTPUT, $PAGE;
+ global $OUTPUT;
+
+ raise_memory_limit(MEMORY_HUGE);
$this->context = context_module::instance($cm->id);
get_string('quizstructureanalysis', 'quiz_statistics'));
$questions = $this->load_and_initialise_questions_for_calculations($quiz);
+ // Print the page header stuff (if not downloading.
+ if (!$this->table->is_downloading()) {
+ $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
+ }
+
if (!$nostudentsingroup) {
// Get the data to be displayed.
+ $progress = $this->get_progress_trace_instance();
list($quizstats, $questionstats, $subquestionstats) =
- $this->get_quiz_and_questions_stats($quiz, $whichattempts, $groupstudents, $questions);
+ $this->get_all_stats_and_analysis($quiz, $whichattempts, $groupstudents, $questions, $progress);
} else {
// Or create empty stats containers.
- $quizstats = new quiz_statistics_calculated($whichattempts);
+ $quizstats = new \quiz_statistics\calculated($whichattempts);
$questionstats = array();
$subquestionstats = array();
}
$this->table->statistics_setup($quiz, $cm->id, $reporturl, $quizstats->s());
}
- // Print the page header stuff (if not downloading.
+ // Print the rest of the page header stuff (if not downloading.
if (!$this->table->is_downloading()) {
- $this->print_header_and_tabs($cm, $course, $quiz, 'statistics');
if (groups_get_activity_groupmode($cm)) {
groups_print_activity_menu($cm, $reporturl->out());
* Get the quiz and question statistics, either by loading the cached results,
* or by recomputing them.
*
- * @param object $quiz the quiz settings.
- * @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
+ * @param object $quiz the quiz settings.
+ * @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
* $quiz->grademethod ie.
* QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
* 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.
+ * @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.
*/
- public function get_quiz_and_questions_stats($quiz, $whichattempts, $groupstudents, $questions) {
+ public function get_all_stats_and_analysis($quiz, $whichattempts, $groupstudents, $questions, $progress = null) {
+
+ if ($progress === null) {
+ $progress = new \core\progress\null();
+ }
$qubaids = quiz_statistics_qubaids_condition($quiz->id, $groupstudents, $whichattempts);
- $qcalc = new \core_question\statistics\questions\calculator($questions);
+ $qcalc = new \core_question\statistics\questions\calculator($questions, $progress);
- $quizcalc = new quiz_statistics_calculator();
+ $quizcalc = new \quiz_statistics\calculator($progress);
if ($quizcalc->get_last_calculated_time($qubaids) === false) {
+
+ $progress->start_progress('', 3);
+
// Recalculate now.
list($questionstats, $subquestionstats) = $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);
+ $this->analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats, $progress);
}
+ $progress->progress(3);
+ $progress->end_progress();
} else {
$quizstats = $quizcalc->get_cached($qubaids);
list($questionstats, $subquestionstats) = $qcalc->get_cached($qubaids);
return array($quizstats, $questionstats, $subquestionstats);
}
- protected function analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats) {
+ /**
+ * Appropriate instance depending if we want html output for the user or not.
+ *
+ * @return \core\progress\base child of \core\progress\base to handle the display (or not) of task progress.
+ */
+ protected function get_progress_trace_instance() {
+ if ($this->progress === null) {
+ if (!$this->table->is_downloading()) {
+ $this->progress = new \core\progress\display_if_slow(get_string('calculatingallstats', 'quiz_statistics'));
+ $this->progress->set_display_names();
+ } else {
+ $this->progress = new \core\progress\null();
+ }
+ }
+ return $this->progress;
+ }
+
+ protected function analyse_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestionstats,
+ $progress = null) {
+
+ if ($progress === null) {
+ $progress = new \core\progress\null();
+ }
+
+ // Starting response analysis tasks.
+ $progress->start_progress('', count($questions) + count($subquestionstats));
+
+ // Starting response analysis of main questions.
+ $progress->start_progress('', count($questions), count($questions));
$done = array();
+ $donecount = 1;
foreach ($questions as $question) {
+ $progress->progress($donecount);
+ $donecount++;
if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
continue;
}
$responesstats = new \core_question\statistics\responses\analyser($question);
$responesstats->calculate($qubaids);
}
+ $progress->end_progress();
+ // Starting response analysis of sub-questions.
+ $countsubquestions = count($subquestionstats);
+ $progress->start_progress('', $countsubquestions, $countsubquestions);
+ $donecount = 1;
foreach ($subquestionstats as $subquestionstat) {
+ $progress->progress($donecount);
+ $donecount++;
if (!question_bank::get_qtype($subquestionstat->question->qtype, false)->can_analyse_responses() ||
isset($done[$subquestionstat->question->id])) {
continue;
$responesstats = new \core_question\statistics\responses\analyser($subquestionstat->question);
$responesstats->calculate($qubaids);
}
+ // Finished sub-question tasks.
+ $progress->end_progress();
+
+ // Finished all response analysis tasks.
+ $progress->end_progress();
}
/**
$options = array();
foreach (array_keys(quiz_get_grading_options()) as $which) {
- $options[$which] = \quiz_statistics_calculator::using_attempts_lang_string($which);
+ $options[$which] = \quiz_statistics\calculator::using_attempts_lang_string($which);
}
$mform->addElement('select', 'whichattempts', get_string('calculatefrom', 'quiz_statistics'), $options);
$groupstudents = array();
$questions = $this->report->load_and_initialise_questions_for_calculations($this->quiz);
list($quizstats, $questionstats, $subquestionstats) =
- $this->report->get_quiz_and_questions_stats($this->quiz, $whichattempts, $groupstudents, $questions);
+ $this->report->get_all_stats_and_analysis($this->quiz, $whichattempts, $groupstudents, $questions);
$qubaids = quiz_statistics_qubaids_condition($this->quiz->id, $groupstudents, $whichattempts);
// We will create some quiz and question stat calculator instances and some response analyser instances, just in order
// to check the time of the
- $quizcalc = new quiz_statistics_calculator();
+ $quizcalc = new \quiz_statistics\calculator();
// Should not be a delay of more than one second between the calculation of stats above and here.
$this->assertTimeCurrent($quizcalc->get_last_calculated_time($qubaids));
protected $randomselectors = array();
+ /**
+ * @var \progress_trace
+ */
+ protected $progress;
+
/**
* Constructor.
*
* @param object[] questions to analyze, keyed by slot, also analyses sub questions for random questions.
* we expect some extra fields - slot, maxmark and number on the full question data objects.
+ * @param \core\progress\base|null $progress the element to send progress messages to, default is {@link \core\progress\null}.
*/
- public function __construct($questions) {
+ public function __construct($questions, $progress = null) {
+
+ if ($progress === null) {
+ $progress = new \core\progress\null();
+ }
+ $this->progress = $progress;
+
foreach ($questions as $slot => $question) {
$this->questionstats[$slot] = new calculated();
$this->questionstats[$slot]->questionid = $question->id;
* @return array containing two arrays calculated[] and calculated_for_subquestion[].
*/
public function calculate($qubaids) {
- \core_php_time_limit::raise();
+
+ $this->progress->start_progress('', 6);
list($lateststeps, $summarks) = $this->get_latest_steps($qubaids);
if ($lateststeps) {
-
+ $this->progress->start_progress('', count($lateststeps), 1);
// Compute the statistics of position, and for random questions, work
// out which questions appear in which positions.
+ $countdone = 1;
foreach ($lateststeps as $step) {
+ $this->progress->progress($countdone);
+ $countdone++;
$this->initial_steps_walker($step, $this->questionstats[$step->slot], $summarks);
// If this is a random question what is the real item being used?
$this->randomselectors[$randomselectorstring][$step->questionid] = $step->questionid;
}
}
+ $this->progress->end_progress();
foreach ($this->randomselectors as $key => $notused) {
ksort($this->randomselectors[$key]);
// Compute the statistics of question id, if we need any.
$subquestions = question_load_questions(array_keys($this->subquestionstats));
+ $this->progress->start_progress('', count($subquestions), 1);
+ $countdone = 1;
foreach ($subquestions as $qid => $subquestion) {
+ $this->progress->progress($countdone);
+ $countdone++;
$this->subquestionstats[$qid]->question = $subquestion;
$this->subquestionstats[$qid]->question->maxmark = $this->subquestionstats[$qid]->maxmark;
$this->subquestionstats[$qid]->randomguessscore = $this->get_random_guess_score($subquestion);
$this->subquestionstats[$qid]->positions = '';
}
}
+ $this->progress->end_progress();
// Finish computing the averages, and put the subquestion data into the
// corresponding questions.
// $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);
+ $countdone = 1;
while (list($slot, $questionstat) = each($this->questionstats)) {
+ $this->progress->progress($countdone);
+ $countdone++;
$nextquestionstats = current($this->questionstats);
$this->initial_question_walker($questionstat);
}
}
}
+ $this->progress->end_progress();
// Go through the records one more time.
+ $this->progress->start_progress('', count($lateststeps), 1);
+ $countdone = 1;
foreach ($lateststeps as $step) {
+ $this->progress->progress($countdone);
+ $countdone++;
$this->secondary_steps_walker($step, $this->questionstats[$step->slot], $summarks);
if ($this->questionstats[$step->slot]->subquestions) {
$this->secondary_steps_walker($step, $this->subquestionstats[$step->questionid], $summarks);
}
}
+ $this->progress->end_progress();
+ $this->progress->start_progress('', count($this->questionstats), 1);
$sumofcovariancewithoverallmark = 0;
+ $countdone = 1;
foreach ($this->questionstats as $questionstat) {
+ $this->progress->progress($countdone);
+ $countdone++;
$this->secondary_question_walker($questionstat);
$this->sumofmarkvariance += $questionstat->markvariance;
$sumofcovariancewithoverallmark += sqrt($questionstat->covariancewithoverallmark);
}
}
+ $this->progress->end_progress();
+ $this->progress->start_progress('', count($this->subquestionstats), 1);
+ $countdone = 1;
foreach ($this->subquestionstats as $subquestionstat) {
+ $this->progress->progress($countdone);
+ $countdone++;
$this->secondary_question_walker($subquestionstat);
}
+ $this->progress->end_progress();
foreach ($this->questionstats as $questionstat) {
if ($sumofcovariancewithoverallmark) {
}
}
$this->cache_stats($qubaids);
+
+ // All finished.
+ $this->progress->end_progress();
}
return array($this->questionstats, $this->subquestionstats);
}