mirror of
https://github.com/moodle/moodle.git
synced 2025-04-22 17:02:03 +02:00
MDL-41725 move db tables from quiz stats report
to question bank - move cron to clean up old cache records move code and rename classes - further review of the quiz reports statistics code - starting to separate calculations of quiz stats, question stats and response analysis - introduce hashcode db field for cached stats convert - code to use qubaids hashcode for caches We just drop the old tables (including previous upgrade steps) and re-create the new ones, because these tables just cache calculated statistics. No important data is stored in them.
This commit is contained in:
parent
7f3836d15a
commit
e68e4ccfdc
@ -1484,6 +1484,46 @@
|
||||
<INDEX NAME="attemptid-questionid" UNIQUE="true" FIELDS="attemptid, questionid"/>
|
||||
</INDEXES>
|
||||
</TABLE>
|
||||
<TABLE NAME="question_statistics" COMMENT="Statistics for individual questions used in an activity.">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="hashcode" TYPE="char" LENGTH="40" NOTNULL="true" SEQUENCE="false" COMMENT="sha1 hash of serialized qubaids_condition class. Unique for every combination of class name and property."/>
|
||||
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="slot" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The position in the quiz where this question appears"/>
|
||||
<FIELD NAME="subquestion" TYPE="int" LENGTH="4" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="s" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="effectiveweight" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
|
||||
<FIELD NAME="negcovar" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="discriminationindex" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
|
||||
<FIELD NAME="discriminativeefficiency" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
|
||||
<FIELD NAME="sd" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="10"/>
|
||||
<FIELD NAME="facility" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="10"/>
|
||||
<FIELD NAME="subquestions" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="maxmark" TYPE="number" LENGTH="12" NOTNULL="false" SEQUENCE="false" DECIMALS="7"/>
|
||||
<FIELD NAME="positions" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="positions in which this item appears. Only used for random questions."/>
|
||||
<FIELD NAME="randomguessscore" TYPE="number" LENGTH="12" NOTNULL="false" SEQUENCE="false" DECIMALS="7" COMMENT="An estimate of the score a student would get by guessing randomly."/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
<TABLE NAME="question_response_analysis" COMMENT="Analysis of student responses given to questions.">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="hashcode" TYPE="char" LENGTH="40" NOTNULL="true" SEQUENCE="false" COMMENT="sha1 hash of serialized qubaids_condition class. Unique for every combination of class name and property."/>
|
||||
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="subqid" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="aid" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="response" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="rcount" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="credit" TYPE="number" LENGTH="15" NOTNULL="true" SEQUENCE="false" DECIMALS="5"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
<TABLE NAME="mnet_application" COMMENT="Information about applications on remote hosts">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
@ -3013,4 +3053,4 @@
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
</TABLES>
|
||||
</XMLDB>
|
||||
</XMLDB>
|
||||
|
@ -2450,5 +2450,63 @@ function xmldb_main_upgrade($oldversion) {
|
||||
upgrade_main_savepoint(true, 2013091300.01);
|
||||
}
|
||||
|
||||
if ($oldversion < 2013092000.01) {
|
||||
|
||||
// Define table question_statistics to be created.
|
||||
$table = new xmldb_table('question_statistics');
|
||||
|
||||
// Adding fields to table question_statistics.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('hashcode', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('questionid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('slot', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
|
||||
$table->add_field('subquestion', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('s', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0');
|
||||
$table->add_field('effectiveweight', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null);
|
||||
$table->add_field('negcovar', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0');
|
||||
$table->add_field('discriminationindex', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null);
|
||||
$table->add_field('discriminativeefficiency', XMLDB_TYPE_NUMBER, '15, 5', null, null, null, null);
|
||||
$table->add_field('sd', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null);
|
||||
$table->add_field('facility', XMLDB_TYPE_NUMBER, '15, 10', null, null, null, null);
|
||||
$table->add_field('subquestions', XMLDB_TYPE_TEXT, null, null, null, null, null);
|
||||
$table->add_field('maxmark', XMLDB_TYPE_NUMBER, '12, 7', null, null, null, null);
|
||||
$table->add_field('positions', XMLDB_TYPE_TEXT, null, null, null, null, null);
|
||||
$table->add_field('randomguessscore', XMLDB_TYPE_NUMBER, '12, 7', null, null, null, null);
|
||||
|
||||
// Adding keys to table question_statistics.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
|
||||
|
||||
// Conditionally launch create table for question_statistics.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Define table question_response_analysis to be created.
|
||||
$table = new xmldb_table('question_response_analysis');
|
||||
|
||||
// Adding fields to table question_response_analysis.
|
||||
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
|
||||
$table->add_field('hashcode', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('questionid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('subqid', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null);
|
||||
$table->add_field('aid', XMLDB_TYPE_CHAR, '100', null, null, null, null);
|
||||
$table->add_field('response', XMLDB_TYPE_TEXT, null, null, null, null, null);
|
||||
$table->add_field('rcount', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
|
||||
$table->add_field('credit', XMLDB_TYPE_NUMBER, '15, 5', null, XMLDB_NOTNULL, null, null);
|
||||
|
||||
// Adding keys to table question_response_analysis.
|
||||
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
|
||||
|
||||
// Conditionally launch create table for question_response_analysis.
|
||||
if (!$dbman->table_exists($table)) {
|
||||
$dbman->create_table($table);
|
||||
}
|
||||
|
||||
// Main savepoint reached.
|
||||
upgrade_main_savepoint(true, 2013092000.01);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<XMLDB PATH="mod/quiz/report/statistics/db" VERSION="20120122" COMMENT="XMLDB file for Moodle mod/quiz/report/statistics"
|
||||
<XMLDB PATH="mod/quiz/report/statistics/db" VERSION="20130920" COMMENT="XMLDB file for Moodle mod/quiz/report/statistics"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="../../../../../lib/xmldb/xmldb.xsd"
|
||||
>
|
||||
@ -7,8 +7,7 @@
|
||||
<TABLE NAME="quiz_statistics" COMMENT="table to cache results from analysis done in statistics report for quizzes.">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="quizid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="groupid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="hashcode" TYPE="char" LENGTH="40" NOTNULL="true" SEQUENCE="false" COMMENT="sha1 hash of serialized qubaids_condition class. Unique for every combination of class name and property."/>
|
||||
<FIELD NAME="allattempts" TYPE="int" LENGTH="4" NOTNULL="true" SEQUENCE="false" COMMENT="bool used to indicate whether these stats are for all attempts or just for the first."/>
|
||||
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="firstattemptscount" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
@ -27,43 +26,5 @@
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
<TABLE NAME="quiz_question_statistics" COMMENT="Default comment for the table, please edit me">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="quizstatisticsid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="slot" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The position in the quiz where this question appears"/>
|
||||
<FIELD NAME="subquestion" TYPE="int" LENGTH="4" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="s" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="effectiveweight" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
|
||||
<FIELD NAME="negcovar" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
|
||||
<FIELD NAME="discriminationindex" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
|
||||
<FIELD NAME="discriminativeefficiency" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="5"/>
|
||||
<FIELD NAME="sd" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="10"/>
|
||||
<FIELD NAME="facility" TYPE="number" LENGTH="15" NOTNULL="false" SEQUENCE="false" DECIMALS="10"/>
|
||||
<FIELD NAME="subquestions" TYPE="text" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="maxmark" TYPE="number" LENGTH="12" NOTNULL="false" SEQUENCE="false" DECIMALS="7"/>
|
||||
<FIELD NAME="positions" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="positions in which this item appears. Only used for random questions."/>
|
||||
<FIELD NAME="randomguessscore" TYPE="number" LENGTH="12" NOTNULL="false" SEQUENCE="false" DECIMALS="7" COMMENT="An estimate of the score a student would get by guessing randomly."/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
<TABLE NAME="quiz_question_response_stats" COMMENT="Quiz question responses.">
|
||||
<FIELDS>
|
||||
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
|
||||
<FIELD NAME="quizstatisticsid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="subqid" TYPE="char" LENGTH="100" NOTNULL="true" SEQUENCE="false"/>
|
||||
<FIELD NAME="aid" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="response" TYPE="text" LENGTH="big" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="rcount" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false"/>
|
||||
<FIELD NAME="credit" TYPE="number" LENGTH="15" NOTNULL="true" SEQUENCE="false" DECIMALS="5"/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
</KEYS>
|
||||
</TABLE>
|
||||
</TABLES>
|
||||
</XMLDB>
|
||||
|
@ -37,43 +37,55 @@ function xmldb_quiz_statistics_upgrade($oldversion) {
|
||||
// Moodle v2.2.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
if ($oldversion < 2012061800) {
|
||||
|
||||
// Changing type of field subqid on table quiz_question_response_stats to char.
|
||||
$table = new xmldb_table('quiz_question_response_stats');
|
||||
$field = new xmldb_field('subqid', XMLDB_TYPE_CHAR, '100', null, XMLDB_NOTNULL, null, null, 'questionid');
|
||||
|
||||
// Launch change of type for field subqid.
|
||||
$dbman->change_field_type($table, $field);
|
||||
|
||||
// Statistics savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2012061800, 'quiz', 'statistics');
|
||||
}
|
||||
|
||||
if ($oldversion < 2012061801) {
|
||||
|
||||
// Changing type of field aid on table quiz_question_response_stats to char.
|
||||
$table = new xmldb_table('quiz_question_response_stats');
|
||||
$field = new xmldb_field('aid', XMLDB_TYPE_CHAR, '100', null, null, null, null, 'subqid');
|
||||
|
||||
// Launch change of type for field aid.
|
||||
$dbman->change_field_type($table, $field);
|
||||
|
||||
// Statistics savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2012061801, 'quiz', 'statistics');
|
||||
}
|
||||
|
||||
// Moodle v2.3.0 release upgrade line
|
||||
// Put any upgrade step following this
|
||||
|
||||
|
||||
// Moodle v2.4.0 release upgrade line
|
||||
// Put any upgrade step following this
|
||||
|
||||
|
||||
// Moodle v2.5.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
if ($oldversion < 2013092000) {
|
||||
|
||||
// Define table question_statistics to be dropped.
|
||||
$table = new xmldb_table('quiz_question_statistics');
|
||||
|
||||
// Conditionally launch drop table for question_statistics.
|
||||
if ($dbman->table_exists($table)) {
|
||||
$dbman->drop_table($table);
|
||||
}
|
||||
|
||||
// Define table question_response_analysis to be dropped.
|
||||
$table = new xmldb_table('quiz_question_response_stats');
|
||||
|
||||
// Conditionally launch drop table for question_response_analysis.
|
||||
if ($dbman->table_exists($table)) {
|
||||
$dbman->drop_table($table);
|
||||
}
|
||||
|
||||
$table = new xmldb_table('quiz_statistics');
|
||||
$field = new xmldb_field('quizid');
|
||||
|
||||
if ($dbman->field_exists($table, $field)) {
|
||||
$dbman->drop_field($table, $field);
|
||||
}
|
||||
|
||||
$field = new xmldb_field('groupid');
|
||||
|
||||
if ($dbman->field_exists($table, $field)) {
|
||||
$dbman->drop_field($table, $field);
|
||||
}
|
||||
|
||||
$field = new xmldb_field('hashcode', XMLDB_TYPE_CHAR, '40', null, XMLDB_NOTNULL, null, null, 'id');
|
||||
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
|
||||
// Main savepoint reached.
|
||||
upgrade_plugin_savepoint(true, 2013092000, 'quiz', 'statistics');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -68,24 +68,10 @@ function quiz_statistics_question_preview_pluginfile($previewcontext, $questioni
|
||||
function quiz_statistics_cron() {
|
||||
global $DB;
|
||||
|
||||
mtrace("\n Cleaning up old quiz statistics cache records...", '');
|
||||
|
||||
$expiretime = time() - 5*HOURSECS;
|
||||
$todelete = $DB->get_records_select_menu('quiz_statistics',
|
||||
'timemodified < ?', array($expiretime), '', 'id, 1');
|
||||
|
||||
if (!$todelete) {
|
||||
return true;
|
||||
}
|
||||
|
||||
list($todeletesql, $todeleteparams) = $DB->get_in_or_equal(array_keys($todelete));
|
||||
|
||||
$DB->delete_records_select('quiz_question_statistics',
|
||||
'quizstatisticsid ' . $todeletesql, $todeleteparams);
|
||||
|
||||
$DB->delete_records_select('quiz_question_response_stats',
|
||||
'quizstatisticsid ' . $todeletesql, $todeleteparams);
|
||||
|
||||
$DB->delete_records_select('quiz_statistics',
|
||||
'id ' . $todeletesql, $todeleteparams);
|
||||
$DB->delete_records_select('quiz_statistics', 'timemodified < ?', array($expiretime));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -1,405 +0,0 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Quiz statistics report calculations class.
|
||||
*
|
||||
* @package quiz_statistics
|
||||
* @copyright 2008 Jamie Pratt
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* This class has methods to compute the question statistics from the raw data.
|
||||
*
|
||||
* @copyright 2008 Jamie Pratt
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class quiz_statistics_question_stats {
|
||||
public $questions;
|
||||
public $subquestions = array();
|
||||
|
||||
protected $s;
|
||||
protected $summarksavg;
|
||||
protected $allattempts;
|
||||
|
||||
/** @var mixed states from which to calculate stats - iteratable. */
|
||||
protected $lateststeps;
|
||||
|
||||
protected $sumofmarkvariance = 0;
|
||||
protected $randomselectors = array();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
* @param $questions the questions.
|
||||
* @param $s the number of attempts included in the stats.
|
||||
* @param $summarksavg the average attempt summarks.
|
||||
*/
|
||||
public function __construct($questions, $s, $summarksavg) {
|
||||
$this->s = $s;
|
||||
$this->summarksavg = $summarksavg;
|
||||
|
||||
foreach ($questions as $slot => $question) {
|
||||
$question->_stats = $this->make_blank_question_stats();
|
||||
$question->_stats->questionid = $question->id;
|
||||
$question->_stats->slot = $slot;
|
||||
}
|
||||
|
||||
$this->questions = $questions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return object ready to hold all the question statistics.
|
||||
*/
|
||||
protected function make_blank_question_stats() {
|
||||
$stats = new stdClass();
|
||||
$stats->slot = null;
|
||||
$stats->s = 0;
|
||||
$stats->totalmarks = 0;
|
||||
$stats->totalothermarks = 0;
|
||||
$stats->markvariancesum = 0;
|
||||
$stats->othermarkvariancesum = 0;
|
||||
$stats->covariancesum = 0;
|
||||
$stats->covariancemaxsum = 0;
|
||||
$stats->subquestion = false;
|
||||
$stats->subquestions = '';
|
||||
$stats->covariancewithoverallmarksum = 0;
|
||||
$stats->randomguessscore = null;
|
||||
$stats->markarray = array();
|
||||
$stats->othermarksarray = array();
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the data that will be needed to perform the calculations.
|
||||
*
|
||||
* @param int $quizid the quiz id.
|
||||
* @param int $currentgroup the current group. 0 for none.
|
||||
* @param array $groupstudents students in this group.
|
||||
* @param bool $allattempts use all attempts, or just first attempts.
|
||||
*/
|
||||
public function load_step_data($quizid, $currentgroup, $groupstudents, $allattempts) {
|
||||
global $DB;
|
||||
|
||||
$this->allattempts = $allattempts;
|
||||
|
||||
list($qsql, $qparams) = $DB->get_in_or_equal(array_keys($this->questions),
|
||||
SQL_PARAMS_NAMED, 'q');
|
||||
list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
|
||||
$quizid, $currentgroup, $groupstudents, $allattempts, false);
|
||||
|
||||
$this->lateststeps = $DB->get_records_sql("
|
||||
SELECT
|
||||
qas.id,
|
||||
quiza.sumgrades,
|
||||
qa.questionid,
|
||||
qa.slot,
|
||||
qa.maxmark,
|
||||
qas.fraction * qa.maxmark as mark
|
||||
|
||||
FROM $fromqa
|
||||
JOIN {question_attempts} qa ON qa.questionusageid = quiza.uniqueid
|
||||
JOIN (
|
||||
SELECT questionattemptid, MAX(id) AS latestid
|
||||
FROM {question_attempt_steps}
|
||||
GROUP BY questionattemptid
|
||||
) lateststepid ON lateststepid.questionattemptid = qa.id
|
||||
JOIN {question_attempt_steps} qas ON qas.id = lateststepid.latestid
|
||||
|
||||
WHERE
|
||||
qa.slot $qsql AND
|
||||
$whereqa", $qparams + $qaparams);
|
||||
}
|
||||
|
||||
public function compute_statistics() {
|
||||
set_time_limit(0);
|
||||
|
||||
$subquestionstats = array();
|
||||
|
||||
// Compute the statistics of position, and for random questions, work
|
||||
// out which questions appear in which positions.
|
||||
foreach ($this->lateststeps as $step) {
|
||||
$this->initial_steps_walker($step, $this->questions[$step->slot]->_stats);
|
||||
|
||||
// If this is a random question what is the real item being used?
|
||||
if ($step->questionid != $this->questions[$step->slot]->id) {
|
||||
if (!isset($subquestionstats[$step->questionid])) {
|
||||
$subquestionstats[$step->questionid] = $this->make_blank_question_stats();
|
||||
$subquestionstats[$step->questionid]->questionid = $step->questionid;
|
||||
$subquestionstats[$step->questionid]->allattempts = $this->allattempts;
|
||||
$subquestionstats[$step->questionid]->usedin = array();
|
||||
$subquestionstats[$step->questionid]->subquestion = true;
|
||||
$subquestionstats[$step->questionid]->differentweights = false;
|
||||
$subquestionstats[$step->questionid]->maxmark = $step->maxmark;
|
||||
} else if ($subquestionstats[$step->questionid]->maxmark != $step->maxmark) {
|
||||
$subquestionstats[$step->questionid]->differentweights = true;
|
||||
}
|
||||
|
||||
$this->initial_steps_walker($step,
|
||||
$subquestionstats[$step->questionid], false);
|
||||
|
||||
$number = $this->questions[$step->slot]->number;
|
||||
$subquestionstats[$step->questionid]->usedin[$number] = $number;
|
||||
|
||||
$randomselectorstring = $this->questions[$step->slot]->category .
|
||||
'/' . $this->questions[$step->slot]->questiontext;
|
||||
if (!isset($this->randomselectors[$randomselectorstring])) {
|
||||
$this->randomselectors[$randomselectorstring] = array();
|
||||
}
|
||||
$this->randomselectors[$randomselectorstring][$step->questionid] =
|
||||
$step->questionid;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->randomselectors as $key => $notused) {
|
||||
ksort($this->randomselectors[$key]);
|
||||
}
|
||||
|
||||
// Compute the statistics of question id, if we need any.
|
||||
$this->subquestions = question_load_questions(array_keys($subquestionstats));
|
||||
foreach ($this->subquestions as $qid => $subquestion) {
|
||||
$subquestion->_stats = $subquestionstats[$qid];
|
||||
$subquestion->maxmark = $subquestion->_stats->maxmark;
|
||||
$subquestion->_stats->randomguessscore = $this->get_random_guess_score($subquestion);
|
||||
|
||||
$this->initial_question_walker($subquestion->_stats);
|
||||
|
||||
if ($subquestionstats[$qid]->differentweights) {
|
||||
// TODO output here really sucks, but throwing is too severe.
|
||||
global $OUTPUT;
|
||||
echo $OUTPUT->notification(
|
||||
get_string('erroritemappearsmorethanoncewithdifferentweight',
|
||||
'quiz_statistics', $this->subquestions[$qid]->name));
|
||||
}
|
||||
|
||||
if ($subquestion->_stats->usedin) {
|
||||
sort($subquestion->_stats->usedin, SORT_NUMERIC);
|
||||
$subquestion->_stats->positions = implode(',', $subquestion->_stats->usedin);
|
||||
} else {
|
||||
$subquestion->_stats->positions = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Finish computing the averages, and put the subquestion 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->questions);
|
||||
while (list($slot, $question) = each($this->questions)) {
|
||||
$nextquestion = current($this->questions);
|
||||
$question->_stats->allattempts = $this->allattempts;
|
||||
$question->_stats->positions = $question->number;
|
||||
$question->_stats->maxmark = $question->maxmark;
|
||||
$question->_stats->randomguessscore = $this->get_random_guess_score($question);
|
||||
|
||||
$this->initial_question_walker($question->_stats);
|
||||
|
||||
if ($question->qtype == 'random') {
|
||||
$randomselectorstring = $question->category.'/'.$question->questiontext;
|
||||
if ($nextquestion && $nextquestion->qtype == 'random') {
|
||||
$nextrandomselectorstring = $nextquestion->category . '/' .
|
||||
$nextquestion->questiontext;
|
||||
if ($randomselectorstring == $nextrandomselectorstring) {
|
||||
continue; // Next loop iteration.
|
||||
}
|
||||
}
|
||||
if (isset($this->randomselectors[$randomselectorstring])) {
|
||||
$question->_stats->subquestions = implode(',',
|
||||
$this->randomselectors[$randomselectorstring]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Go through the records one more time.
|
||||
foreach ($this->lateststeps as $step) {
|
||||
$this->secondary_steps_walker($step,
|
||||
$this->questions[$step->slot]->_stats);
|
||||
|
||||
if ($this->questions[$step->slot]->qtype == 'random') {
|
||||
$this->secondary_steps_walker($step,
|
||||
$this->subquestions[$step->questionid]->_stats);
|
||||
}
|
||||
}
|
||||
|
||||
$sumofcovariancewithoverallmark = 0;
|
||||
foreach ($this->questions as $slot => $question) {
|
||||
$this->secondary_question_walker($question->_stats);
|
||||
|
||||
$this->sumofmarkvariance += $question->_stats->markvariance;
|
||||
|
||||
if ($question->_stats->covariancewithoverallmark >= 0) {
|
||||
$sumofcovariancewithoverallmark +=
|
||||
sqrt($question->_stats->covariancewithoverallmark);
|
||||
$question->_stats->negcovar = 0;
|
||||
} else {
|
||||
$question->_stats->negcovar = 1;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->subquestions as $subquestion) {
|
||||
$this->secondary_question_walker($subquestion->_stats);
|
||||
}
|
||||
|
||||
foreach ($this->questions as $question) {
|
||||
if ($sumofcovariancewithoverallmark) {
|
||||
if ($question->_stats->negcovar) {
|
||||
$question->_stats->effectiveweight = null;
|
||||
} else {
|
||||
$question->_stats->effectiveweight = 100 *
|
||||
sqrt($question->_stats->covariancewithoverallmark) /
|
||||
$sumofcovariancewithoverallmark;
|
||||
}
|
||||
} else {
|
||||
$question->_stats->effectiveweight = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $stats the question statistics we are accumulating.
|
||||
* @param bool $positionstat whether this is a statistic of position of question.
|
||||
*/
|
||||
protected function initial_steps_walker($step, $stats, $positionstat = true) {
|
||||
$stats->s++;
|
||||
$stats->totalmarks += $step->mark;
|
||||
$stats->markarray[] = $step->mark;
|
||||
|
||||
if ($positionstat) {
|
||||
$stats->totalothermarks += $step->sumgrades - $step->mark;
|
||||
$stats->othermarksarray[] = $step->sumgrades - $step->mark;
|
||||
|
||||
} else {
|
||||
$stats->totalothermarks += $step->sumgrades;
|
||||
$stats->othermarksarray[] = $step->sumgrades;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform some computations on the per-question statistics calculations after
|
||||
* we have been through all the states.
|
||||
*
|
||||
* @param object $stats quetsion stats to update.
|
||||
*/
|
||||
protected function initial_question_walker($stats) {
|
||||
$stats->markaverage = $stats->totalmarks / $stats->s;
|
||||
|
||||
if ($stats->maxmark != 0) {
|
||||
$stats->facility = $stats->markaverage / $stats->maxmark;
|
||||
} else {
|
||||
$stats->facility = null;
|
||||
}
|
||||
|
||||
$stats->othermarkaverage = $stats->totalothermarks / $stats->s;
|
||||
|
||||
sort($stats->markarray, SORT_NUMERIC);
|
||||
sort($stats->othermarksarray, SORT_NUMERIC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Now we know the averages, accumulate the date needed to compute the higher
|
||||
* moments of the question scores.
|
||||
*
|
||||
* @param object $step the state to add to the statistics.
|
||||
* @param object $stats the question statistics we are accumulating.
|
||||
* @param bool $positionstat whether this is a statistic of position of question.
|
||||
*/
|
||||
protected function secondary_steps_walker($step, $stats) {
|
||||
$markdifference = $step->mark - $stats->markaverage;
|
||||
if ($stats->subquestion) {
|
||||
$othermarkdifference = $step->sumgrades - $stats->othermarkaverage;
|
||||
} else {
|
||||
$othermarkdifference = $step->sumgrades - $step->mark -
|
||||
$stats->othermarkaverage;
|
||||
}
|
||||
$overallmarkdifference = $step->sumgrades - $this->summarksavg;
|
||||
|
||||
$sortedmarkdifference = array_shift($stats->markarray) - $stats->markaverage;
|
||||
$sortedothermarkdifference = array_shift($stats->othermarksarray) -
|
||||
$stats->othermarkaverage;
|
||||
|
||||
$stats->markvariancesum += pow($markdifference, 2);
|
||||
$stats->othermarkvariancesum += pow($othermarkdifference, 2);
|
||||
$stats->covariancesum += $markdifference * $othermarkdifference;
|
||||
$stats->covariancemaxsum += $sortedmarkdifference * $sortedothermarkdifference;
|
||||
$stats->covariancewithoverallmarksum += $markdifference * $overallmarkdifference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform more per-question statistics calculations.
|
||||
*
|
||||
* @param object $stats quetsion stats to update.
|
||||
*/
|
||||
protected function secondary_question_walker($stats) {
|
||||
if ($stats->s > 1) {
|
||||
$stats->markvariance = $stats->markvariancesum / ($stats->s - 1);
|
||||
$stats->othermarkvariance = $stats->othermarkvariancesum / ($stats->s - 1);
|
||||
$stats->covariance = $stats->covariancesum / ($stats->s - 1);
|
||||
$stats->covariancemax = $stats->covariancemaxsum / ($stats->s - 1);
|
||||
$stats->covariancewithoverallmark = $stats->covariancewithoverallmarksum /
|
||||
($stats->s - 1);
|
||||
$stats->sd = sqrt($stats->markvariancesum / ($stats->s - 1));
|
||||
|
||||
} else {
|
||||
$stats->markvariance = null;
|
||||
$stats->othermarkvariance = null;
|
||||
$stats->covariance = null;
|
||||
$stats->covariancemax = null;
|
||||
$stats->covariancewithoverallmark = null;
|
||||
$stats->sd = null;
|
||||
}
|
||||
|
||||
if ($stats->markvariance * $stats->othermarkvariance) {
|
||||
$stats->discriminationindex = 100 * $stats->covariance /
|
||||
sqrt($stats->markvariance * $stats->othermarkvariance);
|
||||
} else {
|
||||
$stats->discriminationindex = null;
|
||||
}
|
||||
|
||||
if ($stats->covariancemax) {
|
||||
$stats->discriminativeefficiency = 100 * $stats->covariance /
|
||||
$stats->covariancemax;
|
||||
} else {
|
||||
$stats->discriminativeefficiency = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $questiondata
|
||||
* @return number the random guess score for this question.
|
||||
*/
|
||||
protected function get_random_guess_score($questiondata) {
|
||||
return question_bank::get_qtype(
|
||||
$questiondata->qtype, false)->get_random_guess_score($questiondata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used when computing CIC.
|
||||
* @return number
|
||||
*/
|
||||
public function get_sum_of_mark_variance() {
|
||||
return $this->sumofmarkvariance;
|
||||
}
|
||||
}
|
@ -28,9 +28,9 @@ defined('MOODLE_INTERNAL') || die();
|
||||
require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_form.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_table.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/report/statistics/statistics_question_table.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/report/statistics/qstats.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/report/statistics/responseanalysis.php');
|
||||
|
||||
require_once($CFG->dirroot . '/question/engine/statistics.php');
|
||||
require_once($CFG->dirroot . '/question/engine/responseanalysis.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
|
||||
|
||||
/**
|
||||
* The quiz statistics report provides summary information about each question in
|
||||
@ -104,9 +104,12 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
}
|
||||
}
|
||||
|
||||
$qubaids = quiz_statistics_qubaids_condition($quiz->id, $currentgroup, $groupstudents, $useallattempts);
|
||||
|
||||
|
||||
// If recalculate was requested, handle that.
|
||||
if ($recalculate && confirm_sesskey()) {
|
||||
$this->clear_cached_data($quiz->id, $currentgroup, $useallattempts);
|
||||
$this->clear_cached_data($qubaids);
|
||||
redirect($reporturl);
|
||||
}
|
||||
|
||||
@ -164,21 +167,21 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
if ($s) {
|
||||
$this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
|
||||
|
||||
if ($this->table->is_downloading() == 'xhtml') {
|
||||
$this->output_statistics_graph($quizstats->id, $s);
|
||||
if ($this->table->is_downloading() == 'xhtml' && $s != 0) {
|
||||
$this->output_statistics_graph($quiz->id, $currentgroup, $useallattempts);
|
||||
}
|
||||
|
||||
foreach ($questions as $question) {
|
||||
if (question_bank::get_qtype(
|
||||
$question->qtype, false)->can_analyse_responses()) {
|
||||
$this->output_individual_question_response_analysis(
|
||||
$question, $reporturl, $quizstats);
|
||||
$question, $reporturl, $qubaids);
|
||||
|
||||
} else if (!empty($question->_stats->subquestions)) {
|
||||
$subitemstodisplay = explode(',', $question->_stats->subquestions);
|
||||
foreach ($subitemstodisplay as $subitemid) {
|
||||
$this->output_individual_question_response_analysis(
|
||||
$subquestions[$subitemid], $reporturl, $quizstats);
|
||||
$subquestions[$subitemid], $reporturl, $qubaids);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -194,7 +197,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
|
||||
$this->output_individual_question_data($quiz, $questions[$slot]);
|
||||
$this->output_individual_question_response_analysis(
|
||||
$questions[$slot], $reporturl, $quizstats);
|
||||
$questions[$slot], $reporturl, $qubaids);
|
||||
|
||||
// Back to overview link.
|
||||
echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
|
||||
@ -209,7 +212,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
|
||||
$this->output_individual_question_data($quiz, $subquestions[$qid]);
|
||||
$this->output_individual_question_response_analysis(
|
||||
$subquestions[$qid], $reporturl, $quizstats);
|
||||
$subquestions[$qid], $reporturl, $qubaids);
|
||||
|
||||
// Back to overview link.
|
||||
echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
|
||||
@ -232,7 +235,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
if ($s) {
|
||||
echo $OUTPUT->heading(get_string('quizstructureanalysis', 'quiz_statistics'));
|
||||
$this->output_quiz_structure_analysis_table($s, $questions, $subquestions);
|
||||
$this->output_statistics_graph($quizstats->id, $s);
|
||||
$this->output_statistics_graph($quiz->id, $currentgroup, $useallattempts);
|
||||
}
|
||||
}
|
||||
|
||||
@ -323,12 +326,12 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
|
||||
/**
|
||||
* Display the response analysis for a question.
|
||||
* @param object $question the question to report on.
|
||||
* @param object $question the question to report on.
|
||||
* @param moodle_url $reporturl the URL to resisplay this report.
|
||||
* @param object $quizstats Holds the quiz statistics.
|
||||
* @param qubaid_condition $qubaids
|
||||
*/
|
||||
protected function output_individual_question_response_analysis($question,
|
||||
$reporturl, $quizstats) {
|
||||
$reporturl, $qubaids) {
|
||||
global $OUTPUT;
|
||||
|
||||
if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
|
||||
@ -361,8 +364,8 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
}
|
||||
}
|
||||
|
||||
$responesstats = new quiz_statistics_response_analyser($question);
|
||||
$responesstats->load_cached($quizstats->id);
|
||||
$responesstats = new question_response_analyser($question);
|
||||
$responesstats->load_cached($qubaids);
|
||||
|
||||
$qtable->question_setup($reporturl, $question, $responesstats);
|
||||
if ($this->table->is_downloading()) {
|
||||
@ -549,18 +552,16 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
|
||||
/**
|
||||
* Output the HTML needed to show the statistics graph.
|
||||
* @param int $quizstatsid the id of the statistics to show in the graph.
|
||||
* @param $quizid
|
||||
* @param $currentgroup
|
||||
* @param $useallattempts
|
||||
*/
|
||||
protected function output_statistics_graph($quizstatsid, $s) {
|
||||
protected function output_statistics_graph($quizid, $currentgroup, $useallattempts) {
|
||||
global $PAGE;
|
||||
|
||||
if ($s == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$output = $PAGE->get_renderer('mod_quiz');
|
||||
$imageurl = new moodle_url('/mod/quiz/report/statistics/statistics_graph.php',
|
||||
array('id' => $quizstatsid));
|
||||
compact('quizid', 'currentgroup', 'useallattempts'));
|
||||
$graphname = get_string('statisticsreportgraph', 'quiz_statistics');
|
||||
echo $output->graph($imageurl, $graphname);
|
||||
}
|
||||
@ -568,53 +569,39 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
/**
|
||||
* Return the stats data for when there are no stats to show.
|
||||
*
|
||||
* @param array $questions question definitions.
|
||||
* @param int $firstattemptscount number of first attempts (optional).
|
||||
* @param int $firstattemptscount total number of attempts (optional).
|
||||
* @return array with three elements:
|
||||
* @param int $allattemptscount total number of attempts (optional).
|
||||
* @return array with two elements:
|
||||
* - integer $s Number of attempts included in the stats (0).
|
||||
* - array $quizstats The statistics for overall attempt scores.
|
||||
* - array $qstats The statistics for each question.
|
||||
* - object $quizstats The statistics for overall attempt scores.
|
||||
*/
|
||||
protected function get_emtpy_stats($questions, $firstattemptscount = 0,
|
||||
$allattemptscount = 0) {
|
||||
protected function get_empty_stats($firstattemptscount = 0, $allattemptscount = 0) {
|
||||
$quizstats = new stdClass();
|
||||
$quizstats->firstattemptscount = $firstattemptscount;
|
||||
$quizstats->allattemptscount = $allattemptscount;
|
||||
|
||||
$qstats = new stdClass();
|
||||
$qstats->questions = $questions;
|
||||
$qstats->subquestions = array();
|
||||
$qstats->responses = array();
|
||||
|
||||
return array(0, $quizstats, false);
|
||||
return array(0, $quizstats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the quiz statistics.
|
||||
*
|
||||
* @param int $quizid the quiz id.
|
||||
* @param int $currentgroup the current group. 0 for none.
|
||||
* @param bool $nostudentsingroup true if there a no students.
|
||||
* @param bool $useallattempts use all attempts, or just first attempts.
|
||||
* @param array $groupstudents students in this group.
|
||||
* @param array $questions question definitions.
|
||||
* @return array with three elements:
|
||||
* @param int $quizid the quiz id.
|
||||
* @param int $currentgroup the current group. 0 for none.
|
||||
* @param bool $useallattempts use all attempts, or just first attempts.
|
||||
* @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 array with two elements:
|
||||
* - integer $s Number of attempts included in the stats.
|
||||
* - array $quizstats The statistics for overall attempt scores.
|
||||
* - array $qstats The statistics for each question.
|
||||
* - object $quizstats The statistics for overall attempt scores.
|
||||
*/
|
||||
protected function compute_stats($quizid, $currentgroup, $nostudentsingroup,
|
||||
$useallattempts, $groupstudents, $questions) {
|
||||
protected function calculate_quiz_stats($quizid, $currentgroup, $useallattempts, $groupstudents, $p, $sumofmarkvariance) {
|
||||
global $DB;
|
||||
|
||||
// Calculating MEAN of marks for all attempts by students
|
||||
// http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
|
||||
// #Calculating_MEAN_of_grades_for_all_attempts_by_students.
|
||||
if ($nostudentsingroup) {
|
||||
return $this->get_emtpy_stats($questions);
|
||||
}
|
||||
|
||||
list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql(
|
||||
$quizid, $currentgroup, $groupstudents, true);
|
||||
|
||||
@ -628,7 +615,7 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
GROUP BY CASE WHEN attempt = 1 THEN 1 ELSE 0 END", $qaparams);
|
||||
|
||||
if (!$attempttotals) {
|
||||
return $this->get_emtpy_stats($questions);
|
||||
return $this->get_empty_stats();
|
||||
}
|
||||
|
||||
if (isset($attempttotals[1])) {
|
||||
@ -660,10 +647,8 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
|
||||
$s = $usingattempts->countrecs;
|
||||
if ($s == 0) {
|
||||
return $this->get_emtpy_stats($questions, $firstattempts->countrecs,
|
||||
$allattempts->countrecs);
|
||||
return $this->get_empty_stats($firstattempts->countrecs, $allattempts->countrecs);
|
||||
}
|
||||
$summarksavg = $usingattempts->total / $usingattempts->countrecs;
|
||||
|
||||
$quizstats = new stdClass();
|
||||
$quizstats->allattempts = $useallattempts;
|
||||
@ -726,113 +711,55 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
if ($k2) {
|
||||
$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));
|
||||
if ($k2) {
|
||||
$quizstats->kurtosis = $k4 / ($k2*$k2);
|
||||
// Kurtosis.
|
||||
if ($s > 3) {
|
||||
$k4= $s*$s*((($s+1)*$m4)-(3*($s-1)*$m2*$m2))/(($s-1)*($s-2)*($s-3));
|
||||
if ($k2) {
|
||||
$quizstats->kurtosis = $k4 / ($k2*$k2);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$qstats = new quiz_statistics_question_stats($questions, $s, $summarksavg);
|
||||
$qstats->load_step_data($quizid, $currentgroup, $groupstudents, $useallattempts);
|
||||
$qstats->compute_statistics();
|
||||
|
||||
if ($s > 1) {
|
||||
$p = count($qstats->questions); // Number of positions.
|
||||
if ($p > 1 && isset($k2)) {
|
||||
$quizstats->cic = (100 * $p / ($p -1)) *
|
||||
(1 - ($qstats->get_sum_of_mark_variance()) / $k2);
|
||||
(1 - ($sumofmarkvariance / $k2));
|
||||
$quizstats->errorratio = 100 * sqrt(1 - ($quizstats->cic / 100));
|
||||
$quizstats->standarderror = $quizstats->errorratio *
|
||||
$quizstats->standarddeviation / 100;
|
||||
}
|
||||
}
|
||||
|
||||
return array($s, $quizstats, $qstats);
|
||||
$this->cache_stats(quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents, $useallattempts), $quizstats);
|
||||
|
||||
return array($s, $quizstats);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the cached statistics from the database.
|
||||
*
|
||||
* @param object $quiz the quiz settings
|
||||
* @param int $currentgroup the current group. 0 for none.
|
||||
* @param bool $nostudentsingroup true if there a no students.
|
||||
* @param bool $useallattempts use all attempts, or just first attempts.
|
||||
* @param array $groupstudents students in this group.
|
||||
* @param array $questions question definitions.
|
||||
* @return array with 4 elements:
|
||||
* - $quizstats The statistics for overall attempt scores.
|
||||
* - $questions The questions, with an additional _stats field.
|
||||
* - $subquestions The subquestions, if any, with an additional _stats field.
|
||||
* - $s Number of attempts included in the stats.
|
||||
* If there is no cached data in the database, returns an array of four nulls.
|
||||
* @param $qubaids qubaid_condition
|
||||
* @return The statistics for overall attempt scores or false if not cached.
|
||||
*/
|
||||
protected function try_loading_cached_stats($quiz, $currentgroup,
|
||||
$nostudentsingroup, $useallattempts, $groupstudents, $questions) {
|
||||
protected function get_cached_quiz_stats($qubaids) {
|
||||
global $DB;
|
||||
|
||||
$timemodified = time() - self::TIME_TO_CACHE_STATS;
|
||||
$quizstats = $DB->get_record_select('quiz_statistics',
|
||||
'quizid = ? AND groupid = ? AND allattempts = ? AND timemodified > ?',
|
||||
array($quiz->id, $currentgroup, $useallattempts, $timemodified));
|
||||
|
||||
if (!$quizstats) {
|
||||
// No cached data found.
|
||||
return array(null, $questions, null, null);
|
||||
}
|
||||
|
||||
if ($useallattempts) {
|
||||
$s = $quizstats->allattemptscount;
|
||||
} else {
|
||||
$s = $quizstats->firstattemptscount;
|
||||
}
|
||||
|
||||
$subquestions = array();
|
||||
$questionstats = $DB->get_records('quiz_question_statistics',
|
||||
array('quizstatisticsid' => $quizstats->id));
|
||||
|
||||
$subquestionstats = array();
|
||||
foreach ($questionstats as $stat) {
|
||||
if ($stat->slot) {
|
||||
$questions[$stat->slot]->_stats = $stat;
|
||||
} else {
|
||||
$subquestionstats[$stat->questionid] = $stat;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($subquestionstats)) {
|
||||
$subqstofetch = array_keys($subquestionstats);
|
||||
$subquestions = question_load_questions($subqstofetch);
|
||||
foreach ($subquestions as $subqid => $subq) {
|
||||
$subquestions[$subqid]->_stats = $subquestionstats[$subqid];
|
||||
$subquestions[$subqid]->maxmark = $subq->defaultmark;
|
||||
}
|
||||
}
|
||||
|
||||
return array($quizstats, $questions, $subquestions, $s);
|
||||
return $DB->get_record_select('quiz_statistics', 'hashcode = ? AND timemodified > ?',
|
||||
array($qubaids->get_hash_code(), $timemodified));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the statistics in the cache tables in the database.
|
||||
*
|
||||
* @param object $quizid the quiz id.
|
||||
* @param int $currentgroup the current group. 0 for none.
|
||||
* @param bool $useallattempts use all attempts, or just first attempts.
|
||||
* @param object $quizstats The statistics for overall attempt scores.
|
||||
* @param array $questions The questions, with an additional _stats field.
|
||||
* @param array $subquestions The subquestions, if any, with an additional _stats field.
|
||||
* @param $qubaids qubaid_condition
|
||||
* @param $quizstats object the quiz stats to cache
|
||||
*/
|
||||
protected function cache_stats($quizid, $currentgroup,
|
||||
$quizstats, $questions, $subquestions) {
|
||||
protected function cache_stats($qubaids, $quizstats) {
|
||||
global $DB;
|
||||
|
||||
$toinsert = clone($quizstats);
|
||||
$toinsert->quizid = $quizid;
|
||||
$toinsert->groupid = $currentgroup;
|
||||
$toinsert->hashcode = $qubaids->get_hash_code();
|
||||
$toinsert->timemodified = time();
|
||||
|
||||
// Fix up some dodgy data.
|
||||
@ -844,19 +771,8 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
}
|
||||
|
||||
// Store the data.
|
||||
$quizstats->id = $DB->insert_record('quiz_statistics', $toinsert);
|
||||
$DB->insert_record('quiz_statistics', $toinsert);
|
||||
|
||||
foreach ($questions as $question) {
|
||||
$question->_stats->quizstatisticsid = $quizstats->id;
|
||||
$DB->insert_record('quiz_question_statistics', $question->_stats, false);
|
||||
}
|
||||
|
||||
foreach ($subquestions as $subquestion) {
|
||||
$subquestion->_stats->quizstatisticsid = $quizstats->id;
|
||||
$DB->insert_record('quiz_question_statistics', $subquestion->_stats, false);
|
||||
}
|
||||
|
||||
return $quizstats->id;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -878,35 +794,45 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
protected function get_quiz_and_questions_stats($quiz, $currentgroup,
|
||||
$nostudentsingroup, $useallattempts, $groupstudents, $questions) {
|
||||
|
||||
list($quizstats, $questions, $subquestions, $s) =
|
||||
$this->try_loading_cached_stats($quiz, $currentgroup, $nostudentsingroup,
|
||||
$useallattempts, $groupstudents, $questions);
|
||||
$qubaids = quiz_statistics_qubaids_condition($quiz->id, $currentgroup, $groupstudents, $useallattempts);
|
||||
|
||||
if (is_null($quizstats)) {
|
||||
list($s, $quizstats, $qstats) = $this->compute_stats($quiz->id,
|
||||
$currentgroup, $nostudentsingroup, $useallattempts, $groupstudents, $questions);
|
||||
$quizstats = $this->get_cached_quiz_stats($qubaids);
|
||||
|
||||
$qstats = new question_statistics($questions);
|
||||
|
||||
if (empty($quizstats)) {
|
||||
// Recalculate now.
|
||||
$qstats->calculate($qubaids);
|
||||
|
||||
if ($nostudentsingroup) {
|
||||
list($s, $quizstats) = $this->get_empty_stats();
|
||||
} else {
|
||||
list($s, $quizstats) = $this->calculate_quiz_stats($quiz->id, $currentgroup, $useallattempts,
|
||||
$groupstudents, count($questions), $qstats->get_sum_of_mark_variance());
|
||||
}
|
||||
|
||||
$questions = $qstats->questions;
|
||||
$subquestions = $qstats->subquestions;
|
||||
|
||||
if ($s) {
|
||||
$questions = $qstats->questions;
|
||||
$subquestions = $qstats->subquestions;
|
||||
|
||||
$quizstatisticsid = $this->cache_stats($quiz->id, $currentgroup,
|
||||
$quizstats, $questions, $subquestions);
|
||||
|
||||
$this->analyse_responses($quizstatisticsid, $quiz->id, $currentgroup,
|
||||
$nostudentsingroup, $useallattempts, $groupstudents,
|
||||
$questions, $subquestions);
|
||||
$this->calculate_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestions);
|
||||
}
|
||||
} else {
|
||||
if ($useallattempts) {
|
||||
$s = $quizstats->allattemptscount;
|
||||
} else {
|
||||
$s = $quizstats->firstattemptscount;
|
||||
}
|
||||
$qstats->get_cached($qubaids);
|
||||
$questions = $qstats->questions;
|
||||
$subquestions = $qstats->subquestions;
|
||||
|
||||
}
|
||||
|
||||
return array($quizstats, $questions, $subquestions, $s);
|
||||
}
|
||||
|
||||
protected function analyse_responses($quizstatisticsid, $quizid, $currentgroup,
|
||||
$nostudentsingroup, $useallattempts, $groupstudents, $questions, $subquestions) {
|
||||
|
||||
$qubaids = quiz_statistics_qubaids_condition(
|
||||
$quizid, $currentgroup, $groupstudents, $useallattempts);
|
||||
protected function calculate_responses_for_all_questions_and_subquestions($qubaids, $questions, $subquestions) {
|
||||
|
||||
$done = array();
|
||||
foreach ($questions as $question) {
|
||||
@ -915,9 +841,8 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
}
|
||||
$done[$question->id] = 1;
|
||||
|
||||
$responesstats = new quiz_statistics_response_analyser($question);
|
||||
$responesstats->analyse($qubaids);
|
||||
$responesstats->store_cached($quizstatisticsid);
|
||||
$responesstats = new question_response_analyser($question);
|
||||
$responesstats->calculate($qubaids);
|
||||
}
|
||||
|
||||
foreach ($subquestions as $question) {
|
||||
@ -927,9 +852,8 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
}
|
||||
$done[$question->id] = 1;
|
||||
|
||||
$responesstats = new quiz_statistics_response_analyser($question);
|
||||
$responesstats->analyse($qubaids);
|
||||
$responesstats->store_cached($quizstatisticsid);
|
||||
$responesstats = new question_response_analyser($question);
|
||||
$responesstats->calculate($qubaids);
|
||||
}
|
||||
}
|
||||
|
||||
@ -957,12 +881,13 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
/**
|
||||
* Generate the snipped of HTML that says when the stats were last caculated,
|
||||
* with a recalcuate now button.
|
||||
* @param object $quizstats the overall quiz statistics.
|
||||
* @param int $quizid the quiz id.
|
||||
* @param int $currentgroup the id of the currently selected group, or 0.
|
||||
* @param array $groupstudents ids of students in the group.
|
||||
* @param bool $useallattempts whether to use all attempts, instead of just
|
||||
* first attempts.
|
||||
* @param object $quizstats the overall quiz statistics.
|
||||
* @param int $quizid the quiz id.
|
||||
* @param int $currentgroup the id of the currently selected group, or 0.
|
||||
* @param array $groupstudents ids of students in the group.
|
||||
* @param bool $useallattempts whether to use all attempts, instead of just
|
||||
* first attempts.
|
||||
* @param moodle_url $reporturl url for this report
|
||||
* @return string a HTML snipped saying when the stats were last computed,
|
||||
* or blank if that is not appropriate.
|
||||
*/
|
||||
@ -1008,28 +933,13 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
/**
|
||||
* Clear the cached data for a particular report configuration. This will
|
||||
* trigger a re-computation the next time the report is displayed.
|
||||
* @param int $quizid the quiz id.
|
||||
* @param int $currentgroup a group id, or 0.
|
||||
* @param bool $useallattempts whether all attempts, or just first attempts are included.
|
||||
* @param $qubaids qubaid_condition
|
||||
*/
|
||||
protected function clear_cached_data($quizid, $currentgroup, $useallattempts) {
|
||||
protected function clear_cached_data($qubaids) {
|
||||
global $DB;
|
||||
|
||||
$todelete = $DB->get_records_menu('quiz_statistics', array('quizid' => $quizid,
|
||||
'groupid' => $currentgroup, 'allattempts' => $useallattempts), '', 'id, 1');
|
||||
|
||||
if (!$todelete) {
|
||||
return;
|
||||
}
|
||||
|
||||
list($todeletesql, $todeleteparams) = $DB->get_in_or_equal(array_keys($todelete));
|
||||
|
||||
$DB->delete_records_select('quiz_question_statistics',
|
||||
'quizstatisticsid ' . $todeletesql, $todeleteparams);
|
||||
$DB->delete_records_select('quiz_question_response_stats',
|
||||
'quizstatisticsid ' . $todeletesql, $todeleteparams);
|
||||
$DB->delete_records_select('quiz_statistics',
|
||||
'id ' . $todeletesql, $todeleteparams);
|
||||
$DB->delete_records('quiz_statistics', array('hashcode' => $qubaids->get_hash_code()));
|
||||
$DB->delete_records('question_statistics', array('hashcode' => $qubaids->get_hash_code()));
|
||||
$DB->delete_records('question_response_analysis', array('hashcode' => $qubaids->get_hash_code()));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1067,42 +977,3 @@ class quiz_statistics_report extends quiz_default_report {
|
||||
}
|
||||
}
|
||||
|
||||
function quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents,
|
||||
$allattempts = true, $includeungraded = false) {
|
||||
global $DB;
|
||||
|
||||
$fromqa = '{quiz_attempts} quiza ';
|
||||
|
||||
$whereqa = 'quiza.quiz = :quizid AND quiza.preview = 0 AND quiza.state = :quizstatefinished';
|
||||
$qaparams = array('quizid' => $quizid, 'quizstatefinished' => quiz_attempt::FINISHED);
|
||||
|
||||
if (!empty($currentgroup) && $groupstudents) {
|
||||
list($grpsql, $grpparams) = $DB->get_in_or_equal(array_keys($groupstudents),
|
||||
SQL_PARAMS_NAMED, 'u');
|
||||
$whereqa .= " AND quiza.userid $grpsql";
|
||||
$qaparams += $grpparams;
|
||||
}
|
||||
|
||||
if (!$allattempts) {
|
||||
$whereqa .= ' AND quiza.attempt = 1';
|
||||
}
|
||||
|
||||
if (!$includeungraded) {
|
||||
$whereqa .= ' AND quiza.sumgrades IS NOT NULL';
|
||||
}
|
||||
|
||||
return array($fromqa, $whereqa, $qaparams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link qubaid_condition} from the values returned by
|
||||
* {@link quiz_statistics_attempts_sql}
|
||||
* @param string $fromqa from quiz_statistics_attempts_sql.
|
||||
* @param string $whereqa from quiz_statistics_attempts_sql.
|
||||
*/
|
||||
function quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents,
|
||||
$allattempts = true, $includeungraded = false) {
|
||||
list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $currentgroup,
|
||||
$groupstudents, $allattempts, $includeungraded);
|
||||
return new qubaid_join($fromqa, 'quiza.uniqueid', $whereqa, $qaparams);
|
||||
}
|
||||
|
@ -33,30 +33,14 @@ require_once(dirname(__FILE__) . '/../../../../config.php');
|
||||
require_once($CFG->libdir . '/graphlib.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
|
||||
|
||||
|
||||
/**
|
||||
* This helper function returns a sequence of colours each time it is called.
|
||||
* Used for chooseing colours for graph data series.
|
||||
* @return string colour name.
|
||||
*/
|
||||
function graph_get_new_colour() {
|
||||
static $colourindex = -1;
|
||||
$colours = array('red', 'green', 'yellow', 'orange', 'purple', 'black',
|
||||
'maroon', 'blue', 'ltgreen', 'navy', 'ltred', 'ltltgreen', 'ltltorange',
|
||||
'olive', 'gray', 'ltltred', 'ltorange', 'lime', 'ltblue', 'ltltblue');
|
||||
|
||||
$colourindex = ($colourindex + 1) % count($colours);
|
||||
|
||||
return $colours[$colourindex];
|
||||
}
|
||||
require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
|
||||
|
||||
// Get the parameters.
|
||||
$quizstatisticsid = required_param('id', PARAM_INT);
|
||||
$quizid = required_param('quizid', PARAM_INT);
|
||||
$currentgroup = required_param('currentgroup', PARAM_INT);
|
||||
$useallattempts = required_param('useallattempts', PARAM_INT);
|
||||
|
||||
// Load enough data to check permissions.
|
||||
$quizstatistics = $DB->get_record('quiz_statistics', array('id' => $quizstatisticsid));
|
||||
$quiz = $DB->get_record('quiz', array('id' => $quizstatistics->quizid), '*', MUST_EXIST);
|
||||
$quiz = $DB->get_record('quiz', array('id' => $quizid), '*', MUST_EXIST);
|
||||
$cm = get_coursemodule_from_instance('quiz', $quiz->id);
|
||||
|
||||
// Check access.
|
||||
@ -69,14 +53,21 @@ if (groups_get_activity_groupmode($cm)) {
|
||||
} else {
|
||||
$groups = array();
|
||||
}
|
||||
if ($quizstatistics->groupid && !in_array($quizstatistics->groupid, array_keys($groups))) {
|
||||
if ($currentgroup && !in_array($currentgroup, array_keys($groups))) {
|
||||
print_error('groupnotamember', 'group');
|
||||
}
|
||||
$groupstudents = get_users_by_capability($modcontext, array('mod/quiz:reviewmyattempts', 'mod/quiz:attempt'),
|
||||
'', '', '', '', $currentgroup, '', false);
|
||||
|
||||
$qubaids = quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents, $useallattempts);
|
||||
|
||||
// Load the rest of the required data.
|
||||
$questions = quiz_report_get_significant_questions($quiz);
|
||||
$questionstatistics = $DB->get_records_select('quiz_question_statistics',
|
||||
'quizstatisticsid = ? AND slot IS NOT NULL', array($quizstatistics->id));
|
||||
|
||||
// Load enough data to check permissions.
|
||||
$quizstatistics = $DB->get_record('quiz_statistics', array('hashcode' => $qubaids->get_hash_code()));
|
||||
$questionstatistics = $DB->get_records_select('question_statistics', 'hashcode = ? AND slot IS NOT NULL',
|
||||
array($qubaids->get_hash_code()));
|
||||
|
||||
// Create the graph, and set the basic options.
|
||||
$graph = new graph(800, 600);
|
||||
@ -108,7 +99,7 @@ $xdata = array();
|
||||
foreach (array_keys($fieldstoplot) as $fieldtoplot) {
|
||||
$ydata[$fieldtoplot] = array();
|
||||
$graph->y_format[$fieldtoplot] = array(
|
||||
'colour' => graph_get_new_colour(),
|
||||
'colour' => quiz_statistics_graph_get_new_colour(),
|
||||
'bar' => 'fill',
|
||||
'shadow_offset' => 1,
|
||||
'legend' => $fieldstoplot[$fieldtoplot]
|
||||
|
@ -61,7 +61,7 @@ class quiz_statistics_question_table extends flexible_table {
|
||||
* @param bool $hassubqs
|
||||
*/
|
||||
public function question_setup($reporturl, $questiondata,
|
||||
quiz_statistics_response_analyser $responesstats) {
|
||||
question_response_analyser $responesstats) {
|
||||
$this->questiondata = $questiondata;
|
||||
|
||||
$this->define_baseurl($reporturl->out());
|
||||
|
84
mod/quiz/report/statistics/statisticslib.php
Normal file
84
mod/quiz/report/statistics/statisticslib.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Common functions for the quiz statistics report.
|
||||
*
|
||||
* @package quiz_statistics
|
||||
* @copyright 2013 The Open University
|
||||
* @author James Pratt me@jamiep.org
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
function quiz_statistics_attempts_sql($quizid, $currentgroup, $groupstudents,
|
||||
$allattempts = true, $includeungraded = false) {
|
||||
global $DB;
|
||||
|
||||
$fromqa = '{quiz_attempts} quiza ';
|
||||
|
||||
$whereqa = 'quiza.quiz = :quizid AND quiza.preview = 0 AND quiza.state = :quizstatefinished';
|
||||
$qaparams = array('quizid' => (int)$quizid, 'quizstatefinished' => quiz_attempt::FINISHED);
|
||||
|
||||
if (!empty($currentgroup) && $groupstudents) {
|
||||
list($grpsql, $grpparams) = $DB->get_in_or_equal(array_keys($groupstudents),
|
||||
SQL_PARAMS_NAMED, 'u');
|
||||
$whereqa .= " AND quiza.userid $grpsql";
|
||||
$qaparams += $grpparams;
|
||||
}
|
||||
|
||||
if (!$allattempts) {
|
||||
$whereqa .= ' AND quiza.attempt = 1';
|
||||
}
|
||||
|
||||
if (!$includeungraded) {
|
||||
$whereqa .= ' AND quiza.sumgrades IS NOT NULL';
|
||||
}
|
||||
|
||||
return array($fromqa, $whereqa, $qaparams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link qubaid_condition} from the values returned by {@link quiz_statistics_attempts_sql}.
|
||||
*
|
||||
* @param int $quizid
|
||||
* @param int $currentgroup
|
||||
* @param array $groupstudents
|
||||
* @param bool $allattempts
|
||||
* @param bool $includeungraded
|
||||
* @return \qubaid_join
|
||||
*/
|
||||
function quiz_statistics_qubaids_condition($quizid, $currentgroup, $groupstudents,
|
||||
$allattempts = true, $includeungraded = false) {
|
||||
list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $currentgroup,
|
||||
$groupstudents, $allattempts, $includeungraded);
|
||||
return new qubaid_join($fromqa, 'quiza.uniqueid', $whereqa, $qaparams);
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper function returns a sequence of colours each time it is called.
|
||||
* Used for choosing colours for graph data series.
|
||||
* @return string colour name.
|
||||
*/
|
||||
function quiz_statistics_graph_get_new_colour() {
|
||||
static $colourindex = -1;
|
||||
$colours = array('red', 'green', 'yellow', 'orange', 'purple', 'black',
|
||||
'maroon', 'blue', 'ltgreen', 'navy', 'ltred', 'ltltgreen', 'ltltorange',
|
||||
'olive', 'gray', 'ltltred', 'ltorange', 'lime', 'ltblue', 'ltltblue');
|
||||
|
||||
$colourindex = ($colourindex + 1) % count($colours);
|
||||
|
||||
return $colours[$colourindex];
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit tests for (some of) mod/quiz/report/statistics/qstats.php.
|
||||
* Unit tests for (some of) /question/engine/statistics.php
|
||||
*
|
||||
* @package quiz_statistics
|
||||
* @category phpunit
|
||||
@ -28,18 +28,18 @@ defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/questionlib.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/report/statistics/qstats.php');
|
||||
require_once($CFG->dirroot . '/question/engine/statistics.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
|
||||
require_once($CFG->dirroot . '/mod/quiz/report/reportlib.php');
|
||||
|
||||
|
||||
/**
|
||||
* Test helper subclass of quiz_statistics_question_stats
|
||||
* Test helper subclass of question_statistics
|
||||
*
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class testable_quiz_statistics_question_stats extends quiz_statistics_question_stats {
|
||||
class testable_question_statistics extends question_statistics {
|
||||
public function set_step_data($states) {
|
||||
$this->lateststeps = $states;
|
||||
}
|
||||
@ -47,11 +47,38 @@ class testable_quiz_statistics_question_stats extends quiz_statistics_question_s
|
||||
protected function get_random_guess_score($questiondata) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $qubaids qubaid_condition is ignored in this test
|
||||
* @return array with three items
|
||||
* - $lateststeps array of latest step data for the question usages
|
||||
* - $summarks array of total marks for each usage, indexed by usage id
|
||||
* - $summarksavg the average of the total marks over all the usages */
|
||||
protected function get_latest_steps($qubaids) {
|
||||
$summarks = array();
|
||||
$fakeusageid = 0;
|
||||
foreach ($this->lateststeps as $step) {
|
||||
// The same 'sumgrades' field is available in step data for every slot, we will ignore all slots but slot 1.
|
||||
// The step for slot 1 is always the first one in the csv file for each usage, we will use that to separate steps from
|
||||
// each usage.
|
||||
if ($step->slot == 1) {
|
||||
$fakeusageid++;
|
||||
$summarks[$fakeusageid] = $step->sumgrades;
|
||||
}
|
||||
unset($step->sumgrades);
|
||||
$step->questionusageid = $fakeusageid;
|
||||
}
|
||||
|
||||
$summarksavg = array_sum($summarks) / count($summarks);
|
||||
return array($this->lateststeps, $summarks, $summarksavg);
|
||||
}
|
||||
|
||||
protected function cache_stats($qubaids) {
|
||||
// No caching wanted for tests.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for (some of) quiz_statistics_question_stats.
|
||||
* Unit tests for (some of) question_statistics.
|
||||
*
|
||||
* @copyright 2008 Jamie Pratt
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
@ -68,9 +95,9 @@ class quiz_statistics_question_stats_testcase extends basic_testcase {
|
||||
// Data is taken from questions mostly generated by
|
||||
// contrib/tools/generators/generator.php.
|
||||
$questions = $this->get_records_from_csv(__DIR__.'/fixtures/mdl_question.csv');
|
||||
$this->qstats = new testable_quiz_statistics_question_stats($questions, 22, 10045.45455);
|
||||
$this->qstats = new testable_question_statistics($questions, 22, 10045.45455);
|
||||
$this->qstats->set_step_data($steps);
|
||||
$this->qstats->compute_statistics();
|
||||
$this->qstats->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,
|
||||
|
@ -17,7 +17,7 @@
|
||||
/**
|
||||
* Quiz attempt walk through using data from csv file.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @package quiz_statistics
|
||||
* @category phpunit
|
||||
* @copyright 2013 The Open University
|
||||
* @author Jamie Pratt <me@jamiep.org>
|
||||
@ -42,7 +42,8 @@ class testable_quiz_statistics_report extends quiz_statistics_report {
|
||||
|
||||
public function get_stats($quiz, $useallattempts = true,
|
||||
$currentgroup = 0, $groupstudents = array(), $nostudentsingroup = false) {
|
||||
$this->clear_cached_data($quiz->id, $currentgroup, $useallattempts);
|
||||
$qubaids = quiz_statistics_qubaids_condition($quiz->id, $currentgroup, $groupstudents, $useallattempts);
|
||||
$this->clear_cached_data($qubaids);
|
||||
$questions = $this->load_and_initialise_questions_for_calculations($quiz);
|
||||
return $this->get_quiz_and_questions_stats($quiz, $currentgroup, $nostudentsingroup,
|
||||
$useallattempts, $groupstudents, $questions);
|
||||
@ -52,7 +53,7 @@ class testable_quiz_statistics_report extends quiz_statistics_report {
|
||||
/**
|
||||
* Quiz attempt walk through using data from csv file.
|
||||
*
|
||||
* @package mod_quiz
|
||||
* @package quiz_statistics
|
||||
* @category phpunit
|
||||
* @copyright 2013 The Open University
|
||||
* @author Jamie Pratt <me@jamiep.org>
|
||||
|
@ -24,7 +24,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->version = 2013050100;
|
||||
$plugin->requires = 2013050100;
|
||||
$plugin->version = 2013092000;
|
||||
$plugin->requires = 2013092000;
|
||||
$plugin->cron = 18000;
|
||||
$plugin->component = 'quiz_statistics';
|
||||
|
@ -421,6 +421,10 @@ abstract class question_bank {
|
||||
// Delete any old question preview that got left in the database.
|
||||
require_once($CFG->dirroot . '/question/previewlib.php');
|
||||
question_preview_cron();
|
||||
|
||||
// Clear older calculated stats from cache.
|
||||
require_once($CFG->dirroot . '/question/engine/statisticslib.php');
|
||||
question_usage_statistics_cron();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -359,16 +359,16 @@ ORDER BY
|
||||
* Load information about the latest state of each question from the database.
|
||||
*
|
||||
* @param qubaid_condition $qubaids used to restrict which usages are included
|
||||
* in the query. See {@link qubaid_condition}.
|
||||
* @param array $slots A list of slots for the questions you want to konw about.
|
||||
* in the query. See {@link qubaid_condition}.
|
||||
* @param array $slots A list of slots for the questions you want to konw about.
|
||||
* @param string|null $fields
|
||||
* @return array of records. See the SQL in this function to see the fields available.
|
||||
*/
|
||||
public function load_questions_usages_latest_steps(qubaid_condition $qubaids, $slots) {
|
||||
public function load_questions_usages_latest_steps(qubaid_condition $qubaids, $slots, $fields = null) {
|
||||
list($slottest, $params) = $this->db->get_in_or_equal($slots, SQL_PARAMS_NAMED, 'slot');
|
||||
|
||||
$records = $this->db->get_records_sql("
|
||||
SELECT
|
||||
qas.id,
|
||||
if ($fields === null) {
|
||||
$fields = "qas.id,
|
||||
qa.id AS questionattemptid,
|
||||
qa.questionusageid,
|
||||
qa.slot,
|
||||
@ -387,7 +387,13 @@ SELECT
|
||||
qas.state,
|
||||
qas.fraction,
|
||||
qas.timecreated,
|
||||
qas.userid
|
||||
qas.userid";
|
||||
|
||||
}
|
||||
|
||||
$records = $this->db->get_records_sql("
|
||||
SELECT
|
||||
{$fields}
|
||||
|
||||
FROM {$qubaids->from_question_attempts('qa')}
|
||||
JOIN {question_attempt_steps} qas ON
|
||||
@ -1458,6 +1464,14 @@ abstract class qubaid_condition {
|
||||
* @return the params needed by a query that uses {@link usage_id_in()}.
|
||||
*/
|
||||
public abstract function usage_id_in_params();
|
||||
|
||||
/**
|
||||
* @return string 40-character hash code that uniquely identifies the combination of properties and class name of this qubaid
|
||||
* condition.
|
||||
*/
|
||||
public function get_hash_code() {
|
||||
return sha1(serialize($this));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -18,9 +18,11 @@
|
||||
* This file contains the code to analyse all the responses to a particular
|
||||
* question.
|
||||
*
|
||||
* @package quiz_statistics
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @package core
|
||||
* @subpackage questionbank
|
||||
* @copyright 2013 Open University
|
||||
* @author Jamie Pratt <me@jamiep.org>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
@ -31,13 +33,13 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* This class can store and compute the analysis of the responses to a particular
|
||||
* question.
|
||||
*
|
||||
* @copyright 2010 The Open University
|
||||
* @copyright 2013 Open University
|
||||
* @author Jamie Pratt <me@jamiep.org>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class quiz_statistics_response_analyser {
|
||||
class question_response_analyser {
|
||||
/** @var object the data from the database that defines the question. */
|
||||
protected $questiondata;
|
||||
protected $loaded = false;
|
||||
|
||||
/**
|
||||
* @var array This is a multi-dimensional array that stores the results of
|
||||
@ -119,9 +121,9 @@ class quiz_statistics_response_analyser {
|
||||
/**
|
||||
* Analyse all the response data for for all the specified attempts at
|
||||
* this question.
|
||||
* @param $qubaids which attempts to consider.
|
||||
* @param qubaid_condition $qubaids which attempts to consider.
|
||||
*/
|
||||
public function analyse($qubaids) {
|
||||
public function calculate($qubaids) {
|
||||
// Load data.
|
||||
$dm = new question_engine_data_mapper();
|
||||
$questionattempts = $dm->load_attempts_at_question($this->questiondata->id, $qubaids);
|
||||
@ -130,8 +132,8 @@ class quiz_statistics_response_analyser {
|
||||
foreach ($questionattempts as $qa) {
|
||||
$this->add_data_from_one_attempt($qa);
|
||||
}
|
||||
$this->store_cached($qubaids);
|
||||
|
||||
$this->loaded = true;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -164,19 +166,16 @@ class quiz_statistics_response_analyser {
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the computed response analysis in the quiz_question_response_stats
|
||||
* table.
|
||||
* @param int $quizstatisticsid the cached quiz statistics to load the
|
||||
* Store the computed response analysis in the question_response_analysis table.
|
||||
* @param qubaid_condition $qubaids
|
||||
* data corresponding to.
|
||||
* @return bool true if cached data was found in the database and loaded,
|
||||
* otherwise false, to mean no data was loaded.
|
||||
* @return bool true if cached data was found in the database and loaded, otherwise false, to mean no data was loaded.
|
||||
*/
|
||||
public function load_cached($quizstatisticsid) {
|
||||
public function load_cached($qubaids) {
|
||||
global $DB;
|
||||
|
||||
$rows = $DB->get_records('quiz_question_response_stats',
|
||||
array('quizstatisticsid' => $quizstatisticsid,
|
||||
'questionid' => $this->questiondata->id));
|
||||
$rows = $DB->get_records('question_response_analysis',
|
||||
array('hashcode' => $qubaids->get_hash_code(), 'questionid' => $this->questiondata->id));
|
||||
if (!$rows) {
|
||||
return false;
|
||||
}
|
||||
@ -186,28 +185,22 @@ class quiz_statistics_response_analyser {
|
||||
$this->responses[$row->subqid][$row->aid][$row->response]->count = $row->rcount;
|
||||
$this->responses[$row->subqid][$row->aid][$row->response]->fraction = $row->credit;
|
||||
}
|
||||
$this->loaded = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the computed response analysis in the quiz_question_response_stats
|
||||
* table.
|
||||
* @param int $quizstatisticsid the cached quiz statistics this correspons to.
|
||||
* Store the computed response analysis in the question_response_analysis table.
|
||||
* @param qubaid_condition $qubaids
|
||||
*/
|
||||
public function store_cached($quizstatisticsid) {
|
||||
public function store_cached($qubaids) {
|
||||
global $DB;
|
||||
|
||||
if (!$this->loaded) {
|
||||
throw new coding_exception(
|
||||
'Question responses have not been analyised. Cannot store in the database.');
|
||||
}
|
||||
|
||||
$cachetime = time();
|
||||
foreach ($this->responses as $subpartid => $partdata) {
|
||||
foreach ($partdata as $responseclassid => $classdata) {
|
||||
foreach ($classdata as $response => $data) {
|
||||
$row = new stdClass();
|
||||
$row->quizstatisticsid = $quizstatisticsid;
|
||||
$row->hashcode = $qubaids->get_hash_code();
|
||||
$row->questionid = $this->questiondata->id;
|
||||
$row->subqid = $subpartid;
|
||||
if ($responseclassid === '') {
|
||||
@ -218,7 +211,8 @@ class quiz_statistics_response_analyser {
|
||||
$row->response = $response;
|
||||
$row->rcount = $data->count;
|
||||
$row->credit = $data->fraction;
|
||||
$DB->insert_record('quiz_question_response_stats', $row, false);
|
||||
$row->timemodified = $cachetime;
|
||||
$DB->insert_record('question_response_analysis', $row, false);
|
||||
}
|
||||
}
|
||||
}
|
447
question/engine/statistics.php
Normal file
447
question/engine/statistics.php
Normal file
@ -0,0 +1,447 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Question statistics calculations class. Used in the quiz statistics report but also available for use elsewhere.
|
||||
*
|
||||
* @package core
|
||||
* @subpackage questionbank
|
||||
* @copyright 2013 Open University
|
||||
* @author Jamie Pratt <me@jamiep.org>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* This class has methods to compute the question statistics from the raw data.
|
||||
*
|
||||
* @copyright 2013 Open University
|
||||
* @author Jamie Pratt <me@jamiep.org>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class question_statistics {
|
||||
public $questions;
|
||||
public $subquestions = array();
|
||||
|
||||
protected $summarksavg;
|
||||
|
||||
protected $sumofmarkvariance = 0;
|
||||
protected $randomselectors = array();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param $questions array the main questions indexed by slot.
|
||||
*/
|
||||
public function __construct($questions) {
|
||||
foreach ($questions as $slot => $question) {
|
||||
$question->_stats = $this->make_blank_question_stats();
|
||||
$question->_stats->questionid = $question->id;
|
||||
$question->_stats->slot = $slot;
|
||||
}
|
||||
|
||||
$this->questions = $questions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return object ready to hold all the question statistics.
|
||||
*/
|
||||
protected function make_blank_question_stats() {
|
||||
$stats = new stdClass();
|
||||
$stats->slot = null;
|
||||
$stats->s = 0;
|
||||
$stats->totalmarks = 0;
|
||||
$stats->totalothermarks = 0;
|
||||
$stats->markvariancesum = 0;
|
||||
$stats->othermarkvariancesum = 0;
|
||||
$stats->covariancesum = 0;
|
||||
$stats->covariancemaxsum = 0;
|
||||
$stats->subquestion = false;
|
||||
$stats->subquestions = '';
|
||||
$stats->covariancewithoverallmarksum = 0;
|
||||
$stats->randomguessscore = null;
|
||||
$stats->markarray = array();
|
||||
$stats->othermarksarray = array();
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $qubaids qubaid_condition
|
||||
* @return array with three items
|
||||
* - $lateststeps array of latest step data for the question usages
|
||||
* - $summarks array of total marks for each usage, indexed by usage id
|
||||
* - $summarksavg the average of the total marks over all the usages
|
||||
*/
|
||||
protected function get_latest_steps($qubaids) {
|
||||
$dm = new question_engine_data_mapper();
|
||||
|
||||
$fields = " qas.id,
|
||||
qa.questionusageid,
|
||||
qa.questionid,
|
||||
qa.slot,
|
||||
qa.maxmark,
|
||||
qas.fraction * qa.maxmark as mark";
|
||||
|
||||
$lateststeps = $dm->load_questions_usages_latest_steps($qubaids, array_keys($this->questions), $fields);
|
||||
$summarks = array();
|
||||
if ($lateststeps) {
|
||||
foreach ($lateststeps as $step) {
|
||||
if (!isset($summarks[$step->questionusageid])) {
|
||||
$summarks[$step->questionusageid] = 0;
|
||||
}
|
||||
$summarks[$step->questionusageid] += $step->mark;
|
||||
}
|
||||
$summarksavg = array_sum($summarks) / count($summarks);
|
||||
} else {
|
||||
$summarksavg = null;
|
||||
}
|
||||
|
||||
return array($lateststeps, $summarks, $summarksavg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $qubaids qubaid_condition
|
||||
*/
|
||||
public function calculate($qubaids) {
|
||||
set_time_limit(0);
|
||||
|
||||
list($lateststeps, $summarks, $summarksavg) = $this->get_latest_steps($qubaids);
|
||||
|
||||
if ($lateststeps) {
|
||||
$subquestionstats = array();
|
||||
|
||||
// Compute the statistics of position, and for random questions, work
|
||||
// out which questions appear in which positions.
|
||||
foreach ($lateststeps as $step) {
|
||||
$this->initial_steps_walker($step, $this->questions[$step->slot]->_stats, $summarks);
|
||||
|
||||
// If this is a random question what is the real item being used?
|
||||
if ($step->questionid != $this->questions[$step->slot]->id) {
|
||||
if (!isset($subquestionstats[$step->questionid])) {
|
||||
$subquestionstats[$step->questionid] = $this->make_blank_question_stats();
|
||||
$subquestionstats[$step->questionid]->questionid = $step->questionid;
|
||||
$subquestionstats[$step->questionid]->usedin = array();
|
||||
$subquestionstats[$step->questionid]->subquestion = true;
|
||||
$subquestionstats[$step->questionid]->differentweights = false;
|
||||
$subquestionstats[$step->questionid]->maxmark = $step->maxmark;
|
||||
} else if ($subquestionstats[$step->questionid]->maxmark != $step->maxmark) {
|
||||
$subquestionstats[$step->questionid]->differentweights = true;
|
||||
}
|
||||
|
||||
$this->initial_steps_walker($step, $subquestionstats[$step->questionid], $summarks, false);
|
||||
|
||||
$number = $this->questions[$step->slot]->number;
|
||||
$subquestionstats[$step->questionid]->usedin[$number] = $number;
|
||||
|
||||
$randomselectorstring = $this->questions[$step->slot]->category .
|
||||
'/' . $this->questions[$step->slot]->questiontext;
|
||||
if (!isset($this->randomselectors[$randomselectorstring])) {
|
||||
$this->randomselectors[$randomselectorstring] = array();
|
||||
}
|
||||
$this->randomselectors[$randomselectorstring][$step->questionid] =
|
||||
$step->questionid;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->randomselectors as $key => $notused) {
|
||||
ksort($this->randomselectors[$key]);
|
||||
}
|
||||
|
||||
// Compute the statistics of question id, if we need any.
|
||||
$this->subquestions = question_load_questions(array_keys($subquestionstats));
|
||||
foreach ($this->subquestions as $qid => $subquestion) {
|
||||
$subquestion->_stats = $subquestionstats[$qid];
|
||||
$subquestion->maxmark = $subquestion->_stats->maxmark;
|
||||
$subquestion->_stats->randomguessscore = $this->get_random_guess_score($subquestion);
|
||||
|
||||
$this->initial_question_walker($subquestion->_stats);
|
||||
|
||||
if ($subquestionstats[$qid]->differentweights) {
|
||||
// TODO output here really sucks, but throwing is too severe.
|
||||
global $OUTPUT;
|
||||
echo $OUTPUT->notification(
|
||||
get_string('erroritemappearsmorethanoncewithdifferentweight',
|
||||
'quiz_statistics', $this->subquestions[$qid]->name));
|
||||
}
|
||||
|
||||
if ($subquestion->_stats->usedin) {
|
||||
sort($subquestion->_stats->usedin, SORT_NUMERIC);
|
||||
$subquestion->_stats->positions = implode(',', $subquestion->_stats->usedin);
|
||||
} else {
|
||||
$subquestion->_stats->positions = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Finish computing the averages, and put the subquestion 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->questions);
|
||||
while (list($slot, $question) = each($this->questions)) {
|
||||
$nextquestion = current($this->questions);
|
||||
$question->_stats->positions = $question->number;
|
||||
$question->_stats->maxmark = $question->maxmark;
|
||||
$question->_stats->randomguessscore = $this->get_random_guess_score($question);
|
||||
|
||||
$this->initial_question_walker($question->_stats);
|
||||
|
||||
if ($question->qtype == 'random') {
|
||||
$randomselectorstring = $question->category.'/'.$question->questiontext;
|
||||
if ($nextquestion && $nextquestion->qtype == 'random') {
|
||||
$nextrandomselectorstring = $nextquestion->category . '/' .
|
||||
$nextquestion->questiontext;
|
||||
if ($randomselectorstring == $nextrandomselectorstring) {
|
||||
continue; // Next loop iteration.
|
||||
}
|
||||
}
|
||||
if (isset($this->randomselectors[$randomselectorstring])) {
|
||||
$question->_stats->subquestions = implode(',',
|
||||
$this->randomselectors[$randomselectorstring]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Go through the records one more time.
|
||||
foreach ($lateststeps as $step) {
|
||||
$this->secondary_steps_walker($step, $this->questions[$step->slot]->_stats, $summarks, $summarksavg);
|
||||
|
||||
if ($this->questions[$step->slot]->qtype == 'random') {
|
||||
$this->secondary_steps_walker($step, $this->subquestions[$step->questionid]->_stats, $summarks, $summarksavg);
|
||||
}
|
||||
}
|
||||
|
||||
$sumofcovariancewithoverallmark = 0;
|
||||
foreach ($this->questions as $slot => $question) {
|
||||
$this->secondary_question_walker($question->_stats);
|
||||
|
||||
$this->sumofmarkvariance += $question->_stats->markvariance;
|
||||
|
||||
if ($question->_stats->covariancewithoverallmark >= 0) {
|
||||
$sumofcovariancewithoverallmark +=
|
||||
sqrt($question->_stats->covariancewithoverallmark);
|
||||
$question->_stats->negcovar = 0;
|
||||
} else {
|
||||
$question->_stats->negcovar = 1;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->subquestions as $subquestion) {
|
||||
$this->secondary_question_walker($subquestion->_stats);
|
||||
}
|
||||
|
||||
foreach ($this->questions as $question) {
|
||||
if ($sumofcovariancewithoverallmark) {
|
||||
if ($question->_stats->negcovar) {
|
||||
$question->_stats->effectiveweight = null;
|
||||
} else {
|
||||
$question->_stats->effectiveweight = 100 *
|
||||
sqrt($question->_stats->covariancewithoverallmark) /
|
||||
$sumofcovariancewithoverallmark;
|
||||
}
|
||||
} else {
|
||||
$question->_stats->effectiveweight = null;
|
||||
}
|
||||
}
|
||||
$this->cache_stats($qubaids);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $qubaids qubaid_condition
|
||||
*/
|
||||
protected function cache_stats($qubaids) {
|
||||
global $DB;
|
||||
$cachetime = time();
|
||||
foreach ($this->questions as $question) {
|
||||
$question->_stats->hashcode = $qubaids->get_hash_code();
|
||||
$question->_stats->timemodified = $cachetime;
|
||||
$DB->insert_record('question_statistics', $question->_stats, false);
|
||||
}
|
||||
|
||||
foreach ($this->subquestions as $subquestion) {
|
||||
$subquestion->_stats->hashcode = $qubaids->get_hash_code();
|
||||
$subquestion->_stats->timemodified = $cachetime;
|
||||
$DB->insert_record('question_statistics', $subquestion->_stats, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 $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.
|
||||
*/
|
||||
protected function initial_steps_walker($step, $stats, $summarks, $positionstat = true) {
|
||||
$stats->s++;
|
||||
$stats->totalmarks += $step->mark;
|
||||
$stats->markarray[] = $step->mark;
|
||||
|
||||
if ($positionstat) {
|
||||
$stats->totalothermarks += $summarks[$step->questionusageid] - $step->mark;
|
||||
$stats->othermarksarray[] = $summarks[$step->questionusageid] - $step->mark;
|
||||
|
||||
} else {
|
||||
$stats->totalothermarks += $summarks[$step->questionusageid];
|
||||
$stats->othermarksarray[] = $summarks[$step->questionusageid];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform some computations on the per-question statistics calculations after
|
||||
* we have been through all the states.
|
||||
*
|
||||
* @param object $stats quetsion stats to update.
|
||||
*/
|
||||
protected function initial_question_walker($stats) {
|
||||
$stats->markaverage = $stats->totalmarks / $stats->s;
|
||||
|
||||
if ($stats->maxmark != 0) {
|
||||
$stats->facility = $stats->markaverage / $stats->maxmark;
|
||||
} else {
|
||||
$stats->facility = null;
|
||||
}
|
||||
|
||||
$stats->othermarkaverage = $stats->totalothermarks / $stats->s;
|
||||
|
||||
sort($stats->markarray, SORT_NUMERIC);
|
||||
sort($stats->othermarksarray, SORT_NUMERIC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Now we know the averages, accumulate the date needed to compute the higher
|
||||
* moments of the question scores.
|
||||
*
|
||||
* @param object $step the state to add to the statistics.
|
||||
* @param object $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 float $summarksavg the average sum of marks for all question usages
|
||||
*/
|
||||
protected function secondary_steps_walker($step, $stats, $summarks, $summarksavg) {
|
||||
$markdifference = $step->mark - $stats->markaverage;
|
||||
if ($stats->subquestion) {
|
||||
$othermarkdifference = $summarks[$step->questionusageid] - $stats->othermarkaverage;
|
||||
} else {
|
||||
$othermarkdifference = $summarks[$step->questionusageid] - $step->mark -
|
||||
$stats->othermarkaverage;
|
||||
}
|
||||
$overallmarkdifference = $summarks[$step->questionusageid] - $summarksavg;
|
||||
|
||||
$sortedmarkdifference = array_shift($stats->markarray) - $stats->markaverage;
|
||||
$sortedothermarkdifference = array_shift($stats->othermarksarray) -
|
||||
$stats->othermarkaverage;
|
||||
|
||||
$stats->markvariancesum += pow($markdifference, 2);
|
||||
$stats->othermarkvariancesum += pow($othermarkdifference, 2);
|
||||
$stats->covariancesum += $markdifference * $othermarkdifference;
|
||||
$stats->covariancemaxsum += $sortedmarkdifference * $sortedothermarkdifference;
|
||||
$stats->covariancewithoverallmarksum += $markdifference * $overallmarkdifference;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform more per-question statistics calculations.
|
||||
*
|
||||
* @param object $stats quetsion stats to update.
|
||||
*/
|
||||
protected function secondary_question_walker($stats) {
|
||||
if ($stats->s > 1) {
|
||||
$stats->markvariance = $stats->markvariancesum / ($stats->s - 1);
|
||||
$stats->othermarkvariance = $stats->othermarkvariancesum / ($stats->s - 1);
|
||||
$stats->covariance = $stats->covariancesum / ($stats->s - 1);
|
||||
$stats->covariancemax = $stats->covariancemaxsum / ($stats->s - 1);
|
||||
$stats->covariancewithoverallmark = $stats->covariancewithoverallmarksum /
|
||||
($stats->s - 1);
|
||||
$stats->sd = sqrt($stats->markvariancesum / ($stats->s - 1));
|
||||
|
||||
} else {
|
||||
$stats->markvariance = null;
|
||||
$stats->othermarkvariance = null;
|
||||
$stats->covariance = null;
|
||||
$stats->covariancemax = null;
|
||||
$stats->covariancewithoverallmark = null;
|
||||
$stats->sd = null;
|
||||
}
|
||||
|
||||
if ($stats->markvariance * $stats->othermarkvariance) {
|
||||
$stats->discriminationindex = 100 * $stats->covariance /
|
||||
sqrt($stats->markvariance * $stats->othermarkvariance);
|
||||
} else {
|
||||
$stats->discriminationindex = null;
|
||||
}
|
||||
|
||||
if ($stats->covariancemax) {
|
||||
$stats->discriminativeefficiency = 100 * $stats->covariance /
|
||||
$stats->covariancemax;
|
||||
} else {
|
||||
$stats->discriminativeefficiency = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $questiondata
|
||||
* @return number the random guess score for this question.
|
||||
*/
|
||||
protected function get_random_guess_score($questiondata) {
|
||||
return question_bank::get_qtype(
|
||||
$questiondata->qtype, false)->get_random_guess_score($questiondata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Used when computing CIC.
|
||||
* @return number
|
||||
*/
|
||||
public function get_sum_of_mark_variance() {
|
||||
return $this->sumofmarkvariance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param qubaid_condition $qubaids
|
||||
*/
|
||||
public function get_cached($qubaids) {
|
||||
global $DB;
|
||||
$questionstats = $DB->get_records('question_statistics',
|
||||
array('hashcode' => $qubaids->get_hash_code()));
|
||||
|
||||
$subquestionstats = array();
|
||||
foreach ($questionstats as $stat) {
|
||||
if ($stat->slot) {
|
||||
$this->questions[$stat->slot]->_stats = $stat;
|
||||
} else {
|
||||
$subquestionstats[$stat->questionid] = $stat;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($subquestionstats)) {
|
||||
$subqstofetch = array_keys($subquestionstats);
|
||||
$this->subquestions = question_load_questions($subqstofetch);
|
||||
foreach ($this->subquestions as $subqid => $subq) {
|
||||
$this->subquestions[$subqid]->_stats = $subquestionstats[$subqid];
|
||||
$this->subquestions[$subqid]->maxmark = $subq->defaultmark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
46
question/engine/statisticslib.php
Normal file
46
question/engine/statisticslib.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Functions common to the question usage statistics code.
|
||||
*
|
||||
* @package moodlecore
|
||||
* @subpackage questionbank
|
||||
* @copyright 2013 The Open University
|
||||
* @author Jamie Pratt <me@jamiep.org>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* Question statistics cron code. Deletes cached stats more than a certain age.
|
||||
*/
|
||||
function question_usage_statistics_cron() {
|
||||
global $DB;
|
||||
|
||||
$expiretime = time() - 5*HOURSECS;
|
||||
|
||||
mtrace("\n Cleaning up old question statistics cache records...", '');
|
||||
|
||||
$DB->delete_records_select('question_statistics', 'timemodified < ?', array($expiretime));
|
||||
$DB->delete_records_select('question_response_analysis', 'timemodified < ?', array($expiretime));
|
||||
|
||||
mtrace('done.');
|
||||
return true;
|
||||
}
|
@ -29,7 +29,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2013092000.00; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
$version = 2013092000.01; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user