MDL-41760 quiz_statistics : response analysis for first/last/all tries

This commit is contained in:
James Pratt 2014-03-11 15:06:35 +07:00
parent c0e88129d1
commit 038014c415
29 changed files with 1232 additions and 401 deletions

View File

@ -1455,19 +1455,31 @@
<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="whichtries" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="variant" TYPE="int" LENGTH="10" NOTNULL="false" 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" 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"/>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="question_response_count" COMMENT="Count for each responses for each try at a question.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="analysisid" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="try" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="rcount" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="analysisid" TYPE="foreign" FIELDS="analysisid" REFTABLE="question_response_analysis" REFFIELDS="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"/>

View File

@ -3124,5 +3124,64 @@ function xmldb_main_upgrade($oldversion) {
upgrade_main_savepoint(true, 2014022600.00);
}
if ($oldversion < 2014031400.01) {
// Delete any cached stats to force recalculation later, then we can be sure that cached records will have the correct
// field.
$DB->delete_records('question_response_analysis');
$DB->delete_records('question_statistics');
$DB->delete_records('quiz_statistics');
// Define field response to be dropped from question_response_analysis.
$table = new xmldb_table('question_response_analysis');
$field = new xmldb_field('rcount');
// Conditionally launch drop field response.
if ($dbman->field_exists($table, $field)) {
$dbman->drop_field($table, $field);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2014031400.01);
}
if ($oldversion < 2014031400.02) {
// Define table question_response_count to be created.
$table = new xmldb_table('question_response_count');
// Adding fields to table question_response_count.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('analysisid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('try', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('rcount', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
// Adding keys to table question_response_count.
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
$table->add_key('analysisid', XMLDB_KEY_FOREIGN, array('analysisid'), 'question_response_analysis', array('id'));
// Conditionally launch create table for question_response_count.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2014031400.02);
}
if ($oldversion < 2014031400.03) {
// Define field whichtries to be added to question_response_analysis.
$table = new xmldb_table('question_response_analysis');
$field = new xmldb_field('whichtries', XMLDB_TYPE_CHAR, '255', null, XMLDB_NOTNULL, null, null, 'hashcode');
// Conditionally launch add field whichtries.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2014031400.03);
}
return true;
}

View File

@ -37,6 +37,7 @@ $string['calculatingallstats'] = 'Calculating statistics for quiz, questions and
$string['cic'] = 'Coefficient of internal consistency (for {$a})';
$string['completestatsfilename'] = 'completestats';
$string['count'] = 'Count';
$string['counttryno'] = 'Count Try {$a}';
$string['coursename'] = 'Course name';
$string['detailedanalysis'] = 'More detailed analysis of the responses to this question';
$string['detailedanalysisforvariant'] = 'More detailed analysis of the responses to variant {$a} of this question';
@ -110,4 +111,5 @@ $string['statistics:view'] = 'View statistics report';
$string['statsfor'] = 'Quiz statistics (for {$a})';
$string['variant'] = 'Variant';
$string['variantno'] = 'Variant {$a}';
$string['whichtries'] = 'Analyze responses for';

View File

@ -18,7 +18,8 @@
* Quiz statistics report class.
*
* @package quiz_statistics
* @copyright 2008 Jamie Pratt
* @copyright 2014 Open University
* @author James Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@ -38,9 +39,7 @@ require_once($CFG->dirroot . '/mod/quiz/report/statistics/statisticslib.php');
*/
class quiz_statistics_report extends quiz_default_report {
/**
* @var context_module
*/
/** @var context_module context of this quiz.*/
protected $context;
/** @var quiz_statistics_table instance of table class used for main questions stats table. */
@ -74,6 +73,7 @@ class quiz_statistics_report extends quiz_default_report {
$slot = optional_param('slot', 0, PARAM_INT);
$variantno = optional_param('variant', null, PARAM_INT);
$whichattempts = optional_param('whichattempts', $quiz->grademethod, PARAM_INT);
$whichtries = optional_param('whichtries', question_attempt::LAST_TRY, PARAM_ALPHA);
$pageoptions = array();
$pageoptions['id'] = $cm->id;
@ -81,18 +81,18 @@ class quiz_statistics_report extends quiz_default_report {
$reporturl = new moodle_url('/mod/quiz/report.php', $pageoptions);
$mform = new quiz_statistics_settings_form($reporturl);
$mform = new quiz_statistics_settings_form($reporturl, compact('quiz'));
$mform->set_data(array('whichattempts' => $whichattempts));
if ($fromform = $mform->get_data()) {
$whichattempts = $fromform->whichattempts;
}
$mform->set_data(array('whichattempts' => $whichattempts, 'whichtries' => $whichtries));
if ($whichattempts != $quiz->grademethod) {
$reporturl->param('whichattempts', $whichattempts);
}
if ($whichtries != question_attempt::LAST_TRY) {
$reporturl->param('whichtries', $whichtries);
}
// Find out current groups mode.
$currentgroup = $this->get_current_group($cm, $course, $this->context);
$nostudentsingroup = false; // True if a group is selected and there is no one in it.
@ -145,7 +145,7 @@ class quiz_statistics_report extends quiz_default_report {
// Get the data to be displayed.
$progress = $this->get_progress_trace_instance();
list($quizstats, $questionstats) =
$this->get_all_stats_and_analysis($quiz, $whichattempts, $groupstudents, $questions, $progress);
$this->get_all_stats_and_analysis($quiz, $whichattempts, $whichtries, $groupstudents, $questions, $progress);
} else {
// Or create empty stats containers.
$quizstats = new \quiz_statistics\calculated($whichattempts);
@ -191,7 +191,7 @@ class quiz_statistics_report extends quiz_default_report {
$this->output_statistics_graph($quiz->id, $currentgroup, $whichattempts);
}
$this->output_all_question_response_analysis($qubaids, $questions, $questionstats, $reporturl);
$this->output_all_question_response_analysis($qubaids, $questions, $questionstats, $reporturl, $whichtries);
}
$this->table->export_class_instance()->finish_document();
@ -207,7 +207,8 @@ class quiz_statistics_report extends quiz_default_report {
$variantno,
$questionstats->for_subq($qid, $variantno)->s,
$reporturl,
$qubaids);
$qubaids,
$whichtries);
// Back to overview link.
echo $OUTPUT->box('<a href="' . $reporturl->out() . '">' .
get_string('backtoquizreport', 'quiz_statistics') . '</a>',
@ -233,7 +234,8 @@ class quiz_statistics_report extends quiz_default_report {
$variantno,
$questionstats->for_slot($slot, $variantno)->s,
$reporturl,
$qubaids);
$qubaids,
$whichtries);
}
if (!$this->table->is_downloading()) {
// Back to overview link.
@ -256,7 +258,7 @@ class quiz_statistics_report extends quiz_default_report {
} else {
// On-screen display of overview report.
echo $OUTPUT->heading(get_string('quizinformation', 'quiz_statistics'), 3);
echo $this->output_caching_info($quizstats, $quiz->id, $groupstudents, $whichattempts, $reporturl);
echo $this->output_caching_info($quizstats->timemodified, $quiz->id, $groupstudents, $whichattempts, $reporturl);
echo $this->everything_download_options();
$quizinfo = $quizstats->get_formatted_quiz_info_data($course, $cm, $quiz);
echo $this->output_quiz_info_table($quizinfo);
@ -273,6 +275,7 @@ class quiz_statistics_report extends quiz_default_report {
/**
* Display the statistical and introductory information about a question.
* Only called when not downloading.
*
* @param object $quiz the quiz settings.
* @param \core_question\statistics\questions\calculated $questionstat the question to report on.
*/
@ -340,6 +343,8 @@ class quiz_statistics_report extends quiz_default_report {
}
/**
* Output question text in a box with urls appropriate for a preview of the question.
*
* @param object $question question data.
* @return string HTML of question text, ready for display.
*/
@ -363,8 +368,10 @@ class quiz_statistics_report extends quiz_default_report {
* @param int $s
* @param moodle_url $reporturl the URL to redisplay this report.
* @param qubaid_condition $qubaids
* @param string $whichtries
*/
protected function output_individual_question_response_analysis($question, $variantno, $s, $reporturl, $qubaids) {
protected function output_individual_question_response_analysis($question, $variantno, $s, $reporturl, $qubaids,
$whichtries = question_attempt::LAST_TRY) {
global $OUTPUT;
if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
@ -399,8 +406,8 @@ class quiz_statistics_report extends quiz_default_report {
}
}
$responesanalyser = new \core_question\statistics\responses\analyser($question);
$responseanalysis = $responesanalyser->load_cached($qubaids);
$responesanalyser = new \core_question\statistics\responses\analyser($question, $whichtries);
$responseanalysis = $responesanalyser->load_cached($qubaids, $whichtries);
$qtable->question_setup($reporturl, $question, $s, $responseanalysis);
if ($this->table->is_downloading()) {
@ -427,6 +434,7 @@ class quiz_statistics_report extends quiz_default_report {
/**
* Output the table that lists all the questions in the quiz with their statistics.
*
* @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats the stats for all questions in
* the quiz including subqs and
* variants.
@ -442,7 +450,8 @@ class quiz_statistics_report extends quiz_default_report {
}
/**
* Output the table of overall quiz statistics.
* Return HTML for table of overall quiz statistics.
*
* @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
* @return string the HTML.
*/
@ -463,6 +472,7 @@ class quiz_statistics_report extends quiz_default_report {
/**
* Download the table of overall quiz statistics.
*
* @param array $quizinfo as returned by {@link get_formatted_quiz_info_data()}.
*/
protected function download_quiz_info_table($quizinfo) {
@ -493,6 +503,7 @@ class quiz_statistics_report extends quiz_default_report {
/**
* Output the HTML needed to show the statistics graph.
*
* @param $quizid
* @param $currentgroup
* @param $whichattempts
@ -516,13 +527,15 @@ class quiz_statistics_report extends quiz_default_report {
* $quiz->grademethod ie.
* QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
* we calculate stats based on which attempts would affect the grade for each student.
* @param string $whichtries which tries to analyse for response analysis. Will be one of
* question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
* @param array $groupstudents students in this group.
* @param array $questions full question data.
* @param \core\progress\base|null $progress
* @return array with 2 elements: - $quizstats The statistics for overall attempt scores.
* - $questionstats \core_question\statistics\questions\all_calculated_for_qubaid_condition
*/
public function get_all_stats_and_analysis($quiz, $whichattempts, $groupstudents, $questions, $progress = null) {
public function get_all_stats_and_analysis($quiz, $whichattempts, $whichtries, $groupstudents, $questions, $progress = null) {
if ($progress === null) {
$progress = new \core\progress\null();
@ -534,10 +547,9 @@ class quiz_statistics_report extends quiz_default_report {
$quizcalc = new \quiz_statistics\calculator($progress);
$progress->start_progress('', 3);
if ($quizcalc->get_last_calculated_time($qubaids) === false) {
$progress->start_progress('', 3);
// Recalculate now.
$questionstats = $qcalc->calculate($qubaids);
$progress->progress(1);
@ -545,17 +557,24 @@ class quiz_statistics_report extends quiz_default_report {
$quizstats = $quizcalc->calculate($quiz->id, $whichattempts, $groupstudents, count($questions),
$qcalc->get_sum_of_mark_variance());
$progress->progress(2);
if ($quizstats->s()) {
$subquestions = $questionstats->get_sub_questions();
$this->analyse_responses_for_all_questions_and_subquestions($questions, $subquestions, $qubaids, $progress);
}
$progress->progress(3);
$progress->end_progress();
} else {
$quizstats = $quizcalc->get_cached($qubaids);
$progress->progress(1);
$questionstats = $qcalc->get_cached($qubaids);
$progress->progress(2);
}
if ($quizstats->s()) {
$subquestions = $questionstats->get_sub_questions();
$this->analyse_responses_for_all_questions_and_subquestions($questions,
$subquestions,
$qubaids,
$whichtries,
$progress);
}
$progress->progress(3);
$progress->end_progress();
return array($quizstats, $questionstats);
}
@ -576,7 +595,17 @@ class quiz_statistics_report extends quiz_default_report {
return $this->progress;
}
protected function analyse_responses_for_all_questions_and_subquestions($questions, $subquestions, $qubaids, $progress = null) {
/**
* Analyse responses for all questions and sub questions in this quiz.
*
* @param object[] $questions as returned by self::load_and_initialise_questions_for_calculations
* @param object[] $subquestions full question objects.
* @param qubaid_condition $qubaids the question usages whose responses to analyse.
* @param string $whichtries which tries to analyse \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
* @param null|\core\progress\base $progress Used to indicate progress of task.
*/
protected function analyse_responses_for_all_questions_and_subquestions($questions, $subquestions, $qubaids,
$whichtries, $progress = null) {
if ($progress === null) {
$progress = new \core\progress\null();
}
@ -584,15 +613,25 @@ class quiz_statistics_report extends quiz_default_report {
// Starting response analysis tasks.
$progress->start_progress('', count($questions) + count($subquestions));
$done = $this->analyse_responses_for_questions($questions, $qubaids, $progress);
$done = $this->analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress);
$this->analyse_responses_for_questions($subquestions, $qubaids, $progress, $done);
$this->analyse_responses_for_questions($subquestions, $qubaids, $whichtries, $progress, $done);
// Finished all response analysis tasks.
$progress->end_progress();
}
protected function analyse_responses_for_questions($questions, $qubaids, $progress = null, $done = array()) {
/**
* Analyse responses for an array of questions or sub questions.
*
* @param object[] $questions as returned by self::load_and_initialise_questions_for_calculations.
* @param qubaid_condition $qubaids the question usages whose responses to analyse.
* @param string $whichtries which tries to analyse \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
* @param null|\core\progress\base $progress Used to indicate progress of task.
* @param int[] $done array keys are ids of questions that have been analysed before calling method.
* @return array array keys are ids of questions that were analysed after this method call.
*/
protected function analyse_responses_for_questions($questions, $qubaids, $whichtries, $progress = null, $done = array()) {
$countquestions = count($questions);
if (!$countquestions) {
return array();
@ -604,8 +643,10 @@ class quiz_statistics_report extends quiz_default_report {
foreach ($questions as $question) {
$progress->increment_progress();
if (question_bank::get_qtype($question->qtype, false)->can_analyse_responses() && !isset($done[$question->id])) {
$responesstats = new \core_question\statistics\responses\analyser($question);
$responesstats->calculate($qubaids);
$responesstats = new \core_question\statistics\responses\analyser($question, $whichtries);
if ($responesstats->get_last_analysed_time($qubaids, $whichtries) === false) {
$responesstats->calculate($qubaids, $whichtries);
}
}
$done[$question->id] = 1;
}
@ -614,7 +655,10 @@ class quiz_statistics_report extends quiz_default_report {
}
/**
* @return string HTML snipped for the Download full report as UI.
* Return a little form for the user to request to download the full report, including quiz stats and response analysis for
* all questions and sub-questions.
*
* @return string HTML.
*/
protected function everything_download_options() {
$downloadoptions = $this->table->get_download_menu();
@ -635,9 +679,9 @@ 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.
* Return HTML for a message that says when the stats were last calculated and a 'recalculate now' button.
*
* @param int $lastcachetime the time the stats were last cached.
* @param int $quizid the quiz id.
* @param array $groupstudents ids of students in the group or empty array if groups not used.
* @param string $whichattempts which attempts to use, represented internally as one of the constants as used in
@ -645,13 +689,12 @@ class quiz_statistics_report extends quiz_default_report {
* QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
* we calculate stats based on which attempts would affect the grade for each student.
* @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.
* @return string HTML.
*/
protected function output_caching_info($quizstats, $quizid, $groupstudents, $whichattempts, $reporturl) {
protected function output_caching_info($lastcachetime, $quizid, $groupstudents, $whichattempts, $reporturl) {
global $DB, $OUTPUT;
if (empty($quizstats->timemodified)) {
if (empty($lastcachetime)) {
return '';
}
@ -661,7 +704,7 @@ class quiz_statistics_report extends quiz_default_report {
SELECT COUNT(1)
FROM $fromqa
WHERE $whereqa
AND quiza.timefinish > {$quizstats->timemodified}", $qaparams);
AND quiza.timefinish > {$lastcachetime}", $qaparams);
if (!$count) {
$count = 0;
@ -669,7 +712,7 @@ class quiz_statistics_report extends quiz_default_report {
// Generate the output.
$a = new stdClass();
$a->lastcalculated = format_time(time() - $quizstats->timemodified);
$a->lastcalculated = format_time(time() - $lastcachetime);
$a->count = $count;
$recalcualteurl = new moodle_url($reporturl,
@ -686,8 +729,9 @@ 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.
* Clear the cached data for a particular report configuration. This will trigger a re-computation the next time the report
* is displayed.
*
* @param $qubaids qubaid_condition
*/
protected function clear_cached_data($qubaids) {
@ -698,6 +742,8 @@ class quiz_statistics_report extends quiz_default_report {
}
/**
* Load the questions in this quiz and add some properties to the objects needed in the reports.
*
* @param object $quiz the quiz.
* @return array of questions for this quiz.
*/
@ -726,8 +772,13 @@ class quiz_statistics_report extends quiz_default_report {
* @param $questions
* @param $questionstats
* @param $reporturl
* @param $whichtries string
*/
protected function output_all_question_response_analysis($qubaids, $questions, $questionstats, $reporturl) {
protected function output_all_question_response_analysis($qubaids,
$questions,
$questionstats,
$reporturl,
$whichtries = question_attempt::LAST_TRY) {
foreach ($questions as $slot => $question) {
if (question_bank::get_qtype(
$question->qtype, false)->can_analyse_responses()
@ -738,14 +789,16 @@ class quiz_statistics_report extends quiz_default_report {
$variantno,
$questionstats->for_slot($slot, $variantno)->s,
$reporturl,
$qubaids);
$qubaids,
$whichtries);
}
} else {
$this->output_individual_question_response_analysis($question,
null,
$questionstats->for_slot($slot)->s,
$reporturl,
$qubaids);
$qubaids,
$whichtries);
}
} else if ($subqids = $questionstats->for_slot($slot)->get_sub_question_ids()) {
foreach ($subqids as $subqid) {
@ -756,7 +809,8 @@ class quiz_statistics_report extends quiz_default_report {
$variantno,
$questionstats->for_subq($subqid, $variantno)->s,
$reporturl,
$qubaids);
$qubaids,
$whichtries);
}
} else {
$this->output_individual_question_response_analysis(
@ -764,7 +818,8 @@ class quiz_statistics_report extends quiz_default_report {
null,
$questionstats->for_subq($subqid)->s,
$reporturl,
$qubaids);
$qubaids,
$whichtries);
}
}

View File

@ -18,7 +18,8 @@
* Quiz statistics settings form definition.
*
* @package quiz_statistics
* @copyright 2008 Jamie Pratt
* @copyright 2014 Open University
* @author James Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@ -29,7 +30,9 @@ require_once($CFG->libdir . '/formslib.php');
/**
* This is the settings form for the quiz statistics report.
*
* @copyright 2008 Jamie Pratt
* @package quiz_statistics
* @copyright 2014 Open University
* @author James Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_statistics_settings_form extends moodleform {
@ -45,6 +48,15 @@ class quiz_statistics_settings_form extends moodleform {
$mform->addElement('select', 'whichattempts', get_string('calculatefrom', 'quiz_statistics'), $options);
if (quiz_allows_multiple_tries($this->_customdata['quiz'])) {
$mform->addElement('select', 'whichtries', get_string('whichtries', 'quiz_statistics'), array(
question_attempt::FIRST_TRY => get_string('firsttry', 'question'),
question_attempt::LAST_TRY => get_string('lasttry', 'question'),
question_attempt::ALL_TRIES => get_string('alltries', 'question'))
);
$mform->setDefault('whichtries', question_attempt::LAST_TRY);
}
$mform->addElement('submit', 'submitbutton', get_string('preferencessave', 'quiz_overview'));
}
}

View File

@ -15,10 +15,11 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Quiz statistics report, table for showing statistics about a particular question.
* Quiz statistics report, table for showing response analysis for a particular question (or sub question).
*
* @package quiz_statistics
* @copyright 2008 Jamie Pratt
* @copyright 2014 Open University
* @author James Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@ -31,10 +32,11 @@ require_once($CFG->libdir . '/tablelib.php');
*
* Lists the responses that students made to this question, with frequency counts.
*
* The responses may be grouped, either by subpart of the question, or by the
* The responses may be grouped, either by sub-part of the question, or by the
* answer they match.
*
* @copyright 2008 Jamie Pratt
* @copyright 2014 Open University
* @author James Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class quiz_statistics_question_table extends flexible_table {
@ -46,6 +48,7 @@ class quiz_statistics_question_table extends flexible_table {
/**
* Constructor.
*
* @param int $qid the id of the particular question whose statistics are being
* displayed.
*/
@ -54,6 +57,8 @@ class quiz_statistics_question_table extends flexible_table {
}
/**
* Set up columns and column names and other table settings.
*
* @param moodle_url $reporturl
* @param object $questiondata
* @param integer $s number of attempts on this question.
@ -93,8 +98,16 @@ class quiz_statistics_question_table extends flexible_table {
$columns[] = 'fraction';
$headers[] = get_string('optiongrade', 'quiz_statistics');
$columns[] = 'count';
$headers[] = get_string('count', 'quiz_statistics');
if (!$responseanalysis->has_multiple_tries_data()) {
$columns[] = 'totalcount';
$headers[] = get_string('count', 'quiz_statistics');
} else {
$countcolumns = range(1, $responseanalysis->get_maximum_tries());
foreach ($countcolumns as $countcolumn) {
$columns[] = 'trycount'.$countcolumn;
$headers[] = get_string('counttryno', 'quiz_statistics', $countcolumn);
}
}
$columns[] = 'frequency';
$headers[] = get_string('frequency', 'quiz_statistics');
@ -113,6 +126,12 @@ class quiz_statistics_question_table extends flexible_table {
parent::setup();
}
/**
* Take a float where 1 represents 100% and return a string representing the percentage.
*
* @param float $fraction The fraction.
* @return string The fraction as a percentage.
*/
protected function format_percentage($fraction) {
return format_float($fraction * 100, 2) . '%';
}
@ -139,7 +158,27 @@ class quiz_statistics_question_table extends flexible_table {
if (!$this->s) {
return '';
}
return $this->format_percentage($response->totalcount / $this->s);
}
return $this->format_percentage($response->count / $this->s);
/**
* If there is not a col_{column name} method then we call this method. If it returns null
* that means just output the property as in the table raw data. If this returns none null
* then this is the output for this cell of the table.
*
* @param string $colname The name of this column.
* @param object $response The raw data for this row.
* @return string|null The value for this cell of the table or null means use raw data.
*/
public function other_cols($colname, $response) {
if (preg_match('/^trycount(\d+)$/', $colname, $matches)) {
if (isset($response->trycount[$matches[1]])) {
return $response->trycount[$matches[1]];
} else {
return 0;
}
} else {
return null;
}
}
}

View File

@ -1,8 +1,11 @@
slot,facility,sd,effectiveweight,covariance,markvariance,othermarkvariance,discriminationindex,covariancemax,discriminativeefficiency
1,0.704,0.4513682901,21.2922742344,-0.022555556,0.2037333333,0.5002777794,-7.0650767526,0.2385555565,-9.4550536967
2,0.48,0.5099019514,18.8979800309,-0.1172777785,0.26,0.6334555578,-28.8982125772,0.318833334,-36.7834118938
3,0.973333332,0.13333334,4.443012573,-0.0098888894,0.0177777796,0.6609,-9.1230674268,0.045666669,-21.6545012165
4,0.68,0.4760952286,18.9347251357,-0.0833888893,0.2266666667,0.5990111128,-22.6306444113,0.2652222232,-31.4411395613
5,0.52,0.3055050463,11.1450138688,-0.0436944444,0.0933333333,0.6529555563,-17.6997047674,0.2063055556,-21.1794802584
6,0.64,0.4898979486,9.8081339177,-0.2015555547,0.24,0.8220111101,-45.3785178421,0.3539999995,-56.9365974439
7,0.62,0.331662479,15.4788602394,-0.0142499998,0.11,0.5774000005,-5.6543166602,0.2190833335,-6.5043742058
slot,subqname,variant,s,facility,sd,effectiveweight,covariance,markvariance,othermarkvariance,discriminationindex,covariancemax,discriminativeefficiency,maxmark
1,,,25,0.704,0.4513682901,21.2922742344,-0.022555556,0.2037333333,0.5002777794,-7.0650767526,0.2385555565,-9.4550536967,1
1,numerical,,12,0.583333333,0.514928651,**NULL**,,,,35.803933,,39.39393939,1
2,,,25,0.48,0.5099019514,18.8979800309,-0.1172777785,0.26,0.6334555578,-28.8982125772,0.318833334,-36.7834118938,1
2,,1,6,0.50,0.5477225575,**NULL**,,,,-10.5999788,,-14.28571429,1
2,,8,5,0.40,0.547722558,**NULL**,,,,-57.77466679,,-71.05263241,1
3,,,25,0.973333332,0.13333334,4.443012573,-0.0098888894,0.0177777796,0.6609,-9.1230674268,0.045666669,-21.6545012165,1
4,,,25,0.68,0.4760952286,18.9347251357,-0.0833888893,0.2266666667,0.5990111128,-22.6306444113,0.2652222232,-31.4411395613,1
5,,,25,0.52,0.3055050463,11.1450138688,-0.0436944444,0.0933333333,0.6529555563,-17.6997047674,0.2063055556,-21.1794802584,1
6,,,25,0.64,0.4898979486,9.8081339177,-0.2015555547,0.24,0.8220111101,-45.3785178421,0.3539999995,-56.9365974439,1
7,,,25,0.62,0.331662479,15.4788602394,-0.0142499998,0.11,0.5774000005,-5.6543166602,0.2190833335,-6.5043742058,1

1 slot subqname variant s facility sd effectiveweight covariance markvariance othermarkvariance discriminationindex covariancemax discriminativeefficiency maxmark
2 1 25 0.704 0.4513682901 21.2922742344 -0.022555556 0.2037333333 0.5002777794 -7.0650767526 0.2385555565 -9.4550536967 1
3 2 1 numerical 12 0.48 0.583333333 0.5099019514 0.514928651 18.8979800309 **NULL** -0.1172777785 0.26 0.6334555578 -28.8982125772 35.803933 0.318833334 -36.7834118938 39.39393939 1
4 3 2 25 0.973333332 0.48 0.13333334 0.5099019514 4.443012573 18.8979800309 -0.0098888894 -0.1172777785 0.0177777796 0.26 0.6609 0.6334555578 -9.1230674268 -28.8982125772 0.045666669 0.318833334 -21.6545012165 -36.7834118938 1
5 4 2 1 6 0.68 0.50 0.4760952286 0.5477225575 18.9347251357 **NULL** -0.0833888893 0.2266666667 0.5990111128 -22.6306444113 -10.5999788 0.2652222232 -31.4411395613 -14.28571429 1
6 5 2 8 5 0.52 0.40 0.3055050463 0.547722558 11.1450138688 **NULL** -0.0436944444 0.0933333333 0.6529555563 -17.6997047674 -57.77466679 0.2063055556 -21.1794802584 -71.05263241 1
7 6 3 25 0.64 0.973333332 0.4898979486 0.13333334 9.8081339177 4.443012573 -0.2015555547 -0.0098888894 0.24 0.0177777796 0.8220111101 0.6609 -45.3785178421 -9.1230674268 0.3539999995 0.045666669 -56.9365974439 -21.6545012165 1
8 7 4 25 0.62 0.68 0.331662479 0.4760952286 15.4788602394 18.9347251357 -0.0142499998 -0.0833888893 0.11 0.2266666667 0.5774000005 0.5990111128 -5.6543166602 -22.6306444113 0.2190833335 0.2652222232 -6.5043742058 -31.4411395613 1
9 5 25 0.52 0.3055050463 11.1450138688 -0.0436944444 0.0933333333 0.6529555563 -17.6997047674 0.2063055556 -21.1794802584 1
10 6 25 0.64 0.4898979486 9.8081339177 -0.2015555547 0.24 0.8220111101 -45.3785178421 0.3539999995 -56.9365974439 1
11 7 25 0.62 0.331662479 15.4788602394 -0.0142499998 0.11 0.5774000005 -5.6543166602 0.2190833335 -6.5043742058 1

View File

@ -0,0 +1,7 @@
slot,type,which,cat,mark,overrides.hint.0.text,overrides.hint.0.format,overrides.hint.1.text,overrides.hint.1.format,overrides.hint.2.text,overrides.hint.2.format,overrides.hint.3.text,overrides.hint.3.format,overrides.shuffleanswers
1,random,,rand,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
,shortanswer,,rand,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
,numerical,,rand,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
2,calculatedsimple,sumwithvariants,maincat,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
3,match,,maincat,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
4,truefalse,,maincat,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,0
1 slot type which cat mark overrides.hint.0.text overrides.hint.0.format overrides.hint.1.text overrides.hint.1.format overrides.hint.2.text overrides.hint.2.format overrides.hint.3.text overrides.hint.3.format overrides.shuffleanswers
2 1 random rand 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 0
3 shortanswer rand 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 0
4 numerical rand 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 0
5 2 calculatedsimple sumwithvariants maincat 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 0
6 3 match maincat 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 0
7 4 truefalse maincat 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 0

View File

@ -0,0 +1,2 @@
slot,type,which,cat,mark,overrides.hint.0.text,overrides.hint.0.format,overrides.hint.1.text,overrides.hint.1.format,overrides.hint.2.text,overrides.hint.2.format,overrides.hint.3.text,overrides.hint.3.format,overrides.hint.4.text,overrides.hint.4.format,overrides.shuffleanswers
1,match,,maincat,1,"Hint 1",0,"Hint 2",0,"Hint 3",0,"Hint 4",0,"Hint 5",0,0
1 slot type which cat mark overrides.hint.0.text overrides.hint.0.format overrides.hint.1.text overrides.hint.1.format overrides.hint.2.text overrides.hint.2.format overrides.hint.3.text overrides.hint.3.format overrides.hint.4.text overrides.hint.4.format overrides.shuffleanswers
2 1 match maincat 1 Hint 1 0 Hint 2 0 Hint 3 0 Hint 4 0 Hint 5 0 0

View File

@ -1,2 +1,4 @@
testnumber,preferredbehaviour
00,deferredfeedback
01,interactive
02,interactive

1 testnumber preferredbehaviour
2 00 deferredfeedback
3 01 interactive
4 02 interactive

View File

@ -1,4 +1,4 @@
slot,randq,variant,subpart,modelresponse,actualresponse,count
slot,randq,variant,subpart,modelresponse,actualresponse,totalcount
1,numerical,1,1,"3.142 (3.142..3.142)",3.142,1
1,numerical,1,1,"3.14 (3.14..3.14)",3.14,7
1,numerical,1,1,"3.1 (3.1..3.1)",3.1,4
@ -10,7 +10,7 @@ slot,randq,variant,subpart,modelresponse,actualresponse,count
2,,1,1,[NO MATCH],-0.2,1
2,,1,1,[NO MATCH],-1,1
2,,4,1,{a} + {b} (±0.01 Relative),19.4,2
2,,4,1,[NO RESPONSE],,1
2,,4,1,"[NO RESPONSE]",,1
2,,4,1,[NO MATCH],-0.4,1
3,,1,1,frog: amphibian,amphibian,25
3,,1,2,cat: mammal,mammal,24

1 slot randq variant subpart modelresponse actualresponse count totalcount
2 1 numerical 1 1 3.142 (3.142..3.142) 3.142 1
3 1 numerical 1 1 3.14 (3.14..3.14) 3.14 7
4 1 numerical 1 1 3.1 (3.1..3.1) 3.1 4
10 2 1 1 [NO MATCH] -0.2 1
11 2 1 1 [NO MATCH] -1 1
12 2 4 1 {a} + {b} (±0.01 Relative) 19.4 2
13 2 4 1 [NO RESPONSE] 1
14 2 4 1 [NO MATCH] -0.4 1
15 3 1 1 frog: amphibian amphibian 25
16 3 1 2 cat: mammal mammal 24

View File

@ -0,0 +1,73 @@
slot,randq,variant,subpart,modelresponse,actualresponse,count1,count2,count3,count4,count5,totalcount
1,shortanswer,1,1,frog,,0,0,0,0,0,0
1,shortanswer,1,1,toad,toad,1,0,1,0,0,2
1,shortanswer,1,1,*,butterfly,1,0,0,0,0,1
1,shortanswer,1,1,*,dog,1,1,0,0,0,2
1,shortanswer,1,1,*,chicken,0,0,1,0,0,1
1,shortanswer,1,1,*,Tod,1,0,0,0,0,1
1,shortanswer,1,1,*,Tony,0,1,0,0,0,1
1,shortanswer,1,1,*,Sharon,0,0,1,0,0,1
1,shortanswer,1,1,*,snake,0,1,0,0,0,1
1,shortanswer,1,1,*,snakes,0,0,1,0,0,1
1,shortanswer,1,1,*,Snakes,0,0,0,1,0,1
1,shortanswer,1,1,*,SnakeS,0,0,0,0,1,1
1,shortanswer,1,1,*,goat,1,0,0,0,0,1
1,shortanswer,1,1,*,"Mexican burrowing caecilian",0,0,1,0,0,1
1,shortanswer,1,1,*,newt,0,0,0,1,0,1
1,shortanswer,1,1,*,human,0,0,0,0,1,1
1,shortanswer,1,1,*,eggs,1,0,0,0,0,1
1,shortanswer,1,1,"[No response]",,0,0,0,0,0,0
1,numerical,1,1,3.14 (3.14..3.14),3.14,2,0,0,0,0,2
1,numerical,1,1,3.142 (3.142..3.142),,0,0,0,0,0,0
1,numerical,1,1,3.1 (3.1..3.1),3.1,1,0,0,0,0,1
1,numerical,1,1,3 (3..3),,0,0,0,0,0,0
1,numerical,1,1,*,2,1,0,0,0,0,1
1,numerical,1,1,*,20,0,1,0,0,0,1
1,numerical,1,1,*,34,0,0,1,0,0,1
1,numerical,1,1,[No response],,0,0,0,0,0,0
2,,1,1,{a} + {b} (±0.01 Relative),9.9,0,0,0,0,1,1
2,,1,1,[Did not match any answer],23,1,0,0,0,0,1
2,,1,1,[Did not match any answer],22,0,1,0,0,0,1
2,,1,1,[Did not match any answer],21,0,0,1,0,0,1
2,,1,1,[Did not match any answer],9,0,0,0,1,0,1
2,,1,1,[No response],,0,0,0,0,0,0
2,,2,1,{a} + {b} (±0.01 Relative),8.5,1,0,0,0,1,2
2,,2,1,"[Did not match any answer]",19.4,0,1,0,0,0,1
2,,2,1,[Did not match any answer],4.5,1,0,0,0,0,1
2,,2,1,"[Did not match any answer]",8,0,0,0,1,0,1
2,,2,1,[No response],,0,0,0,0,0,0
2,,3,1,{a} + {b} (±0.01 Relative),3.3,0,1,0,0,0,1
2,,3,1,[Did not match any answer],19.4,1,0,0,0,0,1
2,,3,1,[No response],,0,0,0,0,0,0
2,,4,1,{a} + {b} (±0.01 Relative),19.4,2,0,0,0,0,2
2,,4,1,{a} + {b} (±0.01 Relative),19.3,1,0,0,0,0,1
2,,4,1,[Did not match any answer],,0,0,0,0,0,0
2,,4,1,[No response],,0,0,0,0,0,0
2,,6,1,"{a} + {b} (±0.01 Relative)",9.4,1,0,0,0,0,1
2,,6,1,"[Did not match any answer]",,0,0,0,0,0,0
2,,6,1,"[No response]",,0,0,0,0,0,0
2,,9,1,"{a} + {b} (±0.01 Relative)",,0,0,0,0,0,0
2,,9,1,"[Did not match any answer]",7,1,0,0,0,0,1
2,,9,1,"[No response]",,0,0,0,0,0,0
2,,10,1,"{a} + {b} (±0.01 Relative)",,0,0,0,0,0,0
2,,10,1,"[Did not match any answer]",555,1,0,0,0,0,1
2,,10,1,"[Did not match any answer]",44,0,1,0,0,0,1
2,,10,1,"[Did not match any answer]",22,0,0,1,0,0,1
2,,10,1,"[Did not match any answer]",11,0,0,0,1,0,1
2,,10,1,"[Did not match any answer]",12,0,0,0,0,1,1
2,,10,1,"[No response]",,0,0,0,0,0,0
3,,1,1,"frog: amphibian",amphibian,8,0,1,0,0,9
3,,1,1,frog: mammal,mammal,0,0,0,1,1,2
3,,1,1,frog: insect,insect,4,3,0,0,0,7
3,,1,1,[No response],,0,0,0,0,0,0
3,,1,2,"cat: amphibian",amphibian,8,0,1,1,0,10
3,,1,2,cat: mammal,mammal,0,1,1,2,0,4
3,,1,2,"cat: insect",insect,4,1,1,1,0,7
3,,1,2,[No response],,0,0,0,0,0,0
3,,1,3,"newt: amphibian",amphibian,6,2,1,0,0,9
3,,1,3,"newt: mammal",mammal,3,0,0,1,0,5
3,,1,3,newt: insect,insect,3,2,0,0,0,5
3,,1,3,[No response],,0,0,0,0,0,0
4,,1,1,False,,3,0,0,0,0,3
4,,1,1,True,,9,0,0,0,0,9
4,,1,1,[No response],,0,0,0,0,0,0
1 slot randq variant subpart modelresponse actualresponse count1 count2 count3 count4 count5 totalcount
2 1 shortanswer 1 1 frog 0 0 0 0 0 0
3 1 shortanswer 1 1 toad toad 1 0 1 0 0 2
4 1 shortanswer 1 1 * butterfly 1 0 0 0 0 1
5 1 shortanswer 1 1 * dog 1 1 0 0 0 2
6 1 shortanswer 1 1 * chicken 0 0 1 0 0 1
7 1 shortanswer 1 1 * Tod 1 0 0 0 0 1
8 1 shortanswer 1 1 * Tony 0 1 0 0 0 1
9 1 shortanswer 1 1 * Sharon 0 0 1 0 0 1
10 1 shortanswer 1 1 * snake 0 1 0 0 0 1
11 1 shortanswer 1 1 * snakes 0 0 1 0 0 1
12 1 shortanswer 1 1 * Snakes 0 0 0 1 0 1
13 1 shortanswer 1 1 * SnakeS 0 0 0 0 1 1
14 1 shortanswer 1 1 * goat 1 0 0 0 0 1
15 1 shortanswer 1 1 * Mexican burrowing caecilian 0 0 1 0 0 1
16 1 shortanswer 1 1 * newt 0 0 0 1 0 1
17 1 shortanswer 1 1 * human 0 0 0 0 1 1
18 1 shortanswer 1 1 * eggs 1 0 0 0 0 1
19 1 shortanswer 1 1 [No response] 0 0 0 0 0 0
20 1 numerical 1 1 3.14 (3.14..3.14) 3.14 2 0 0 0 0 2
21 1 numerical 1 1 3.142 (3.142..3.142) 0 0 0 0 0 0
22 1 numerical 1 1 3.1 (3.1..3.1) 3.1 1 0 0 0 0 1
23 1 numerical 1 1 3 (3..3) 0 0 0 0 0 0
24 1 numerical 1 1 * 2 1 0 0 0 0 1
25 1 numerical 1 1 * 20 0 1 0 0 0 1
26 1 numerical 1 1 * 34 0 0 1 0 0 1
27 1 numerical 1 1 [No response] 0 0 0 0 0 0
28 2 1 1 {a} + {b} (±0.01 Relative) 9.9 0 0 0 0 1 1
29 2 1 1 [Did not match any answer] 23 1 0 0 0 0 1
30 2 1 1 [Did not match any answer] 22 0 1 0 0 0 1
31 2 1 1 [Did not match any answer] 21 0 0 1 0 0 1
32 2 1 1 [Did not match any answer] 9 0 0 0 1 0 1
33 2 1 1 [No response] 0 0 0 0 0 0
34 2 2 1 {a} + {b} (±0.01 Relative) 8.5 1 0 0 0 1 2
35 2 2 1 [Did not match any answer] 19.4 0 1 0 0 0 1
36 2 2 1 [Did not match any answer] 4.5 1 0 0 0 0 1
37 2 2 1 [Did not match any answer] 8 0 0 0 1 0 1
38 2 2 1 [No response] 0 0 0 0 0 0
39 2 3 1 {a} + {b} (±0.01 Relative) 3.3 0 1 0 0 0 1
40 2 3 1 [Did not match any answer] 19.4 1 0 0 0 0 1
41 2 3 1 [No response] 0 0 0 0 0 0
42 2 4 1 {a} + {b} (±0.01 Relative) 19.4 2 0 0 0 0 2
43 2 4 1 {a} + {b} (±0.01 Relative) 19.3 1 0 0 0 0 1
44 2 4 1 [Did not match any answer] 0 0 0 0 0 0
45 2 4 1 [No response] 0 0 0 0 0 0
46 2 6 1 {a} + {b} (±0.01 Relative) 9.4 1 0 0 0 0 1
47 2 6 1 [Did not match any answer] 0 0 0 0 0 0
48 2 6 1 [No response] 0 0 0 0 0 0
49 2 9 1 {a} + {b} (±0.01 Relative) 0 0 0 0 0 0
50 2 9 1 [Did not match any answer] 7 1 0 0 0 0 1
51 2 9 1 [No response] 0 0 0 0 0 0
52 2 10 1 {a} + {b} (±0.01 Relative) 0 0 0 0 0 0
53 2 10 1 [Did not match any answer] 555 1 0 0 0 0 1
54 2 10 1 [Did not match any answer] 44 0 1 0 0 0 1
55 2 10 1 [Did not match any answer] 22 0 0 1 0 0 1
56 2 10 1 [Did not match any answer] 11 0 0 0 1 0 1
57 2 10 1 [Did not match any answer] 12 0 0 0 0 1 1
58 2 10 1 [No response] 0 0 0 0 0 0
59 3 1 1 frog: amphibian amphibian 8 0 1 0 0 9
60 3 1 1 frog: mammal mammal 0 0 0 1 1 2
61 3 1 1 frog: insect insect 4 3 0 0 0 7
62 3 1 1 [No response] 0 0 0 0 0 0
63 3 1 2 cat: amphibian amphibian 8 0 1 1 0 10
64 3 1 2 cat: mammal mammal 0 1 1 2 0 4
65 3 1 2 cat: insect insect 4 1 1 1 0 7
66 3 1 2 [No response] 0 0 0 0 0 0
67 3 1 3 newt: amphibian amphibian 6 2 1 0 0 9
68 3 1 3 newt: mammal mammal 3 0 0 1 0 5
69 3 1 3 newt: insect insect 3 2 0 0 0 5
70 3 1 3 [No response] 0 0 0 0 0 0
71 4 1 1 False 3 0 0 0 0 3
72 4 1 1 True 9 0 0 0 0 9
73 4 1 1 [No response] 0 0 0 0 0 0

View File

@ -0,0 +1,6 @@
slot,subpart,modelresponse,actualresponse,totalcount,count1,count2,count3,count4,count5
1,1,frog: insect,insect,2,2,0,0,0,0
1,1,"frog: mammal",mammal,1,0,0,1,0,0
1,2,"cat: insect",insect,2,1,1,0,0,0
1,2,cat: amphibian,amphibian,1,1,0,0,0,0
1,3,newt: insect,insect,2,2,0,0,0,0
1 slot subpart modelresponse actualresponse totalcount count1 count2 count3 count4 count5
2 1 1 frog: insect insect 2 2 0 0 0 0
3 1 1 frog: mammal mammal 1 0 0 1 0 0
4 1 2 cat: insect insect 2 1 1 0 0 0
5 1 2 cat: amphibian amphibian 1 1 0 0 0 0
6 1 3 newt: insect insect 2 2 0 0 0 0

View File

@ -0,0 +1,83 @@
quizattempt,firstname,lastname,finished,randqs.1,responses.1.-submit,responses.1.-tryagain,responses.1.answer,responses.2.-submit,responses.2.-tryagain,responses.2.answer,variants.2,responses.3.-submit,responses.3.-tryagain,responses.3.0,responses.3.1,responses.3.2,responses.4.-submit,responses.4.answer
1,John,Jones,0,shortanswer,1,0,butterfly,0,0,19.4,4,0,0,,amphibian,,0,1
1,John,Jones,0,shortanswer,0,1,butterfly,0,0,19.4,4,1,0,insect,insect,insect,0,1
1,John,Jones,0,shortanswer,1,0,dog,0,0,19.4,4,0,1,insect,insect,insect,0,1
1,John,Jones,0,shortanswer,0,1,dog,0,0,19.4,4,1,0,insect,insect,amphibian,0,1
1,John,Jones,0,shortanswer,1,0,chicken,0,0,19.4,4,0,1,insect,insect,amphibian,0,1
1,John,Jones,0,shortanswer,0,0,chicken,0,0,19.4,4,1,0,insect,mammal,amphibian,0,1
1,John,Jones,0,shortanswer,0,0,chicken,0,0,19.4,4,0,1,insect,mammal,amphibian,0,1
1,John,Jones,0,shortanswer,0,0,chicken,0,0,19.4,4,1,0,insect,amphibian,amphibian,0,1
1,John,Jones,0,shortanswer,0,0,chicken,0,0,19.4,4,0,1,insect,amphibian,amphibian,0,1
1,John,Jones,1,shortanswer,0,0,chicken,0,0,19.4,4,1,0,mammal,amphibian,amphibian,0,1
2,Han,Solo,0,shortanswer,1,0,Tod,1,0,19.4,2,1,0,amphibian,amphibian,amphibian,1,0
2,Han,Solo,0,shortanswer,0,1,Tod,0,1,19.4,2,0,1,amphibian,amphibian,amphibian,0,0
2,Han,Solo,0,shortanswer,1,0,Tony,0,0,19.4,2,1,0,insect,mammal,insect,0,0
2,Han,Solo,0,shortanswer,0,1,Tony,0,0,19.4,2,0,1,insect,mammal,insect,0,0
2,Han,Solo,0,shortanswer,1,0,Sharon,0,0,19.4,2,1,0,amphibian,insect,mammal,0,0
2,Han,Solo,0,shortanswer,0,0,Sharon,0,0,19.4,2,0,1,amphibian,insect,mammal,0,0
2,Han,Solo,0,shortanswer,0,0,Sharon,0,0,19.4,2,1,0,amphibian,insect,mammal,0,0
2,Han,Solo,1,shortanswer,0,0,Sharon,0,0,19.4,2,0,0,amphibian,insect,mammal,0,0
3,Yoda,Wise He Is,0,shortanswer,1,0,snake,1,0,7,9,1,0,amphibian,insect,amphibian,0,1
3,Yoda,Wise He Is,0,shortanswer,0,1,snake,0,0,7,9,0,1,amphibian,insect,amphibian,0,1
3,Yoda,Wise He Is,0,shortanswer,1,0,snake,0,0,7,9,1,0,amphibian,insect,amphibian,0,1
3,Yoda,Wise He Is,0,shortanswer,0,1,snake,0,0,7,9,0,1,amphibian,insect,amphibian,0,1
3,Yoda,Wise He Is,0,shortanswer,1,0,snakes,0,0,7,9,1,0,amphibian,insect,amphibian,0,1
3,Yoda,Wise He Is,0,shortanswer,0,1,snakes,0,0,7,9,0,1,amphibian,insect,amphibian,0,1
3,Yoda,Wise He Is,0,shortanswer,1,0,Snakes,0,0,7,9,1,0,amphibian,mammal,amphibian,0,1
3,Yoda,Wise He Is,0,shortanswer,0,1,Snakes,0,0,7,9,0,0,amphibian,mammal,amphibian,0,1
3,Yoda,Wise He Is,1,shortanswer,1,0,SnakeS,0,0,7,9,0,0,amphibian,mammal,amphibian,0,1
4,Herbert,Garrison,0,shortanswer,0,0,dog,0,0,9.4,6,1,0,amphibian,amphibian,amphibian,0,1
4,Herbert,Garrison,0,shortanswer,0,0,dog,0,0,9.4,6,0,1,amphibian,amphibian,amphibian,0,1
4,Herbert,Garrison,0,shortanswer,0,0,dog,0,0,9.4,6,1,0,insect,insect,insect,0,1
4,Herbert,Garrison,0,shortanswer,0,0,dog,0,0,9.4,6,0,1,insect,insect,insect,0,1
4,Herbert,Garrison,0,shortanswer,0,0,dog,0,0,9.4,6,1,0,insect,amphibian,amphibian,0,1
4,Herbert,Garrison,0,shortanswer,0,0,dog,0,0,9.4,6,0,1,insect,amphibian,amphibian,0,1
4,Herbert,Garrison,1,shortanswer,0,0,dog,0,0,9.4,6,1,0,mammal,insect,mammal,0,1
5,Agent,Smith,0,numerical,1,0,3.1,1,0,x+y,2,0,0,amphibian,amphibian,mammal,0,1
5,Agent,Smith,0,numerical,0,0,3.1,1,0,4.5,2,0,0,amphibian,amphibian,mammal,0,1
5,Agent,Smith,1,numerical,0,0,3.1,0,0,4.5,2,0,0,amphibian,amphibian,mammal,0,1
6,Agent,Smith,0,numerical,0,0,3.142,0,0,19.4,3,0,0,amphibian,insect,mammal,1,0
6,Agent,Smith,0,numerical,1,0,3.14,1,0,19.4,3,1,0,amphibian,insect,mammal,0,0
6,Agent,Smith,0,numerical,0,0,3.14,0,1,19.4,3,0,1,amphibian,insect,mammal,0,0
6,Agent,Smith,1,numerical,0,0,3.14,1,0,3.3,3,1,0,insect,insect,amphibian,0,0
7,Agent,Smith,0,numerical,1,0,3.14,1,0,19.3,4,0,0,insect,amphibian,insect,0,1
7,Agent,Smith,1,numerical,0,0,3.14,0,0,19.3,4,0,0,insect,amphibian,insect,0,1
9,Bebe,Stevens,0,shortanswer,1,0,goat,1,0,8.5,2,0,0,amphibian,amphibian,insect,0,1
9,Bebe,Stevens,0,shortanswer,0,1,goat,0,0,8.5,2,0,0,amphibian,amphibian,insect,0,1
9,Bebe,Stevens,0,shortanswer,1,0,Mexican burrowing caecilian,0,0,8.5,2,0,0,amphibian,amphibian,insect,0,1
9,Bebe,Stevens,0,shortanswer,0,1,Mexican burrowing caecilian,0,0,8.5,2,0,0,amphibian,amphibian,insect,0,1
9,Bebe,Stevens,0,shortanswer,1,0,Mexican burrowing caecilian,0,0,8.5,2,0,0,amphibian,amphibian,insect,0,1
9,Bebe,Stevens,0,shortanswer,0,1,Mexican burrowing caecilian,0,0,8.5,2,0,0,amphibian,amphibian,insect,0,1
9,Bebe,Stevens,0,shortanswer,1,0,newt,0,0,8.5,2,0,0,amphibian,amphibian,insect,0,1
9,Bebe,Stevens,0,shortanswer,0,1,newt,0,0,8.5,2,0,0,amphibian,amphibian,insect,0,1
9,Bebe,Stevens,1,shortanswer,1,0,human,0,0,8.5,2,0,0,amphibian,amphibian,insect,0,1
10,Luke,Skywalker,0,numerical,1,0,2,1,0,555,10,1,0,amphibian,amphibian,amphibian,1,0
10,Luke,Skywalker,0,numerical,0,1,2,0,1,555,10,0,1,amphibian,amphibian,amphibian,0,0
10,Luke,Skywalker,0,numerical,1,0,20,1,0,44,10,1,0,amphibian,amphibian,amphibian,0,0
10,Luke,Skywalker,0,numerical,0,1,20,0,1,44,10,0,1,amphibian,amphibian,amphibian,0,0
10,Luke,Skywalker,0,numerical,1,0,34,1,0,22,10,1,0,amphibian,amphibian,amphibian,0,0
10,Luke,Skywalker,0,numerical,0,0,34,0,1,22,10,0,1,amphibian,amphibian,amphibian,0,0
10,Luke,Skywalker,0,numerical,0,0,34,1,0,11,10,1,0,amphibian,mammal,amphibian,0,0
10,Luke,Skywalker,0,numerical,0,0,34,0,1,11,10,0,0,amphibian,mammal,amphibian,0,0
10,Luke,Skywalker,1,numerical,0,0,34,1,0,12,10,0,0,amphibian,mammal,amphibian,0,0
11,Luke,Skywalker,0,shortanswer,1,0,toad,1,0,23,1,1,0,insect,amphibian,amphibian,0,1
11,Luke,Skywalker,0,shortanswer,0,0,toad,0,1,23,1,0,0,insect,amphibian,amphibian,0,1
11,Luke,Skywalker,0,shortanswer,0,0,toad,1,0,22,1,0,0,insect,amphibian,amphibian,0,1
11,Luke,Skywalker,0,shortanswer,0,0,toad,0,1,22,1,0,0,insect,amphibian,amphibian,0,1
11,Luke,Skywalker,0,shortanswer,0,0,toad,1,0,21,1,0,0,insect,amphibian,amphibian,0,1
11,Luke,Skywalker,0,shortanswer,0,0,toad,0,1,21,1,0,0,insect,amphibian,amphibian,0,1
11,Luke,Skywalker,0,shortanswer,0,0,toad,1,0,9,1,0,0,insect,amphibian,amphibian,0,1
11,Luke,Skywalker,0,shortanswer,0,0,toad,0,1,9,1,0,0,insect,amphibian,amphibian,0,1
11,Luke,Skywalker,1,shortanswer,0,0,toad,1,0,9.9,1,0,0,insect,amphibian,amphibian,0,1
12,Luke,Skywalker,0,shortanswer,1,0,toad,1,0,8,2,1,0,insect,insect,amphibian,0,1
12,Luke,Skywalker,0,shortanswer,0,1,toad,0,1,8,2,0,0,insect,insect,amphibian,0,1
12,Luke,Skywalker,0,shortanswer,1,0,toad,1,0,8,2,0,0,insect,insect,amphibian,0,1
12,Luke,Skywalker,0,shortanswer,0,1,toad,0,1,8,2,0,0,insect,insect,amphibian,0,1
12,Luke,Skywalker,0,shortanswer,1,0,toad,1,0,8,2,0,0,insect,insect,amphibian,0,1
12,Luke,Skywalker,0,shortanswer,0,0,toad,0,1,8,2,0,0,insect,insect,amphibian,0,1
12,Luke,Skywalker,0,shortanswer,0,0,toad,1,0,8,2,0,0,insect,insect,amphibian,0,1
12,Luke,Skywalker,0,shortanswer,0,0,toad,0,1,8,2,0,0,insect,insect,amphibian,0,1
12,Luke,Skywalker,1,shortanswer,0,0,toad,1,0,8.5,2,0,0,insect,insect,amphibian,0,1
13,Leia,The Princess,0,shortanswer,0,0,eggs,0,0,19.4,4,1,0,amphibian,amphibian,,1,1
13,Leia,The Princess,0,shortanswer,0,0,eggs,0,0,19.4,4,1,0,amphibian,amphibian,mammal,0,1
13,Leia,The Princess,1,shortanswer,0,0,eggs,0,0,19.4,4,0,0,amphibian,amphibian,mammal,0,1
1 quizattempt firstname lastname finished randqs.1 responses.1.-submit responses.1.-tryagain responses.1.answer responses.2.-submit responses.2.-tryagain responses.2.answer variants.2 responses.3.-submit responses.3.-tryagain responses.3.0 responses.3.1 responses.3.2 responses.4.-submit responses.4.answer
2 1 John Jones 0 shortanswer 1 0 butterfly 0 0 19.4 4 0 0 amphibian 0 1
3 1 John Jones 0 shortanswer 0 1 butterfly 0 0 19.4 4 1 0 insect insect insect 0 1
4 1 John Jones 0 shortanswer 1 0 dog 0 0 19.4 4 0 1 insect insect insect 0 1
5 1 John Jones 0 shortanswer 0 1 dog 0 0 19.4 4 1 0 insect insect amphibian 0 1
6 1 John Jones 0 shortanswer 1 0 chicken 0 0 19.4 4 0 1 insect insect amphibian 0 1
7 1 John Jones 0 shortanswer 0 0 chicken 0 0 19.4 4 1 0 insect mammal amphibian 0 1
8 1 John Jones 0 shortanswer 0 0 chicken 0 0 19.4 4 0 1 insect mammal amphibian 0 1
9 1 John Jones 0 shortanswer 0 0 chicken 0 0 19.4 4 1 0 insect amphibian amphibian 0 1
10 1 John Jones 0 shortanswer 0 0 chicken 0 0 19.4 4 0 1 insect amphibian amphibian 0 1
11 1 John Jones 1 shortanswer 0 0 chicken 0 0 19.4 4 1 0 mammal amphibian amphibian 0 1
12 2 Han Solo 0 shortanswer 1 0 Tod 1 0 19.4 2 1 0 amphibian amphibian amphibian 1 0
13 2 Han Solo 0 shortanswer 0 1 Tod 0 1 19.4 2 0 1 amphibian amphibian amphibian 0 0
14 2 Han Solo 0 shortanswer 1 0 Tony 0 0 19.4 2 1 0 insect mammal insect 0 0
15 2 Han Solo 0 shortanswer 0 1 Tony 0 0 19.4 2 0 1 insect mammal insect 0 0
16 2 Han Solo 0 shortanswer 1 0 Sharon 0 0 19.4 2 1 0 amphibian insect mammal 0 0
17 2 Han Solo 0 shortanswer 0 0 Sharon 0 0 19.4 2 0 1 amphibian insect mammal 0 0
18 2 Han Solo 0 shortanswer 0 0 Sharon 0 0 19.4 2 1 0 amphibian insect mammal 0 0
19 2 Han Solo 1 shortanswer 0 0 Sharon 0 0 19.4 2 0 0 amphibian insect mammal 0 0
20 3 Yoda Wise He Is 0 shortanswer 1 0 snake 1 0 7 9 1 0 amphibian insect amphibian 0 1
21 3 Yoda Wise He Is 0 shortanswer 0 1 snake 0 0 7 9 0 1 amphibian insect amphibian 0 1
22 3 Yoda Wise He Is 0 shortanswer 1 0 snake 0 0 7 9 1 0 amphibian insect amphibian 0 1
23 3 Yoda Wise He Is 0 shortanswer 0 1 snake 0 0 7 9 0 1 amphibian insect amphibian 0 1
24 3 Yoda Wise He Is 0 shortanswer 1 0 snakes 0 0 7 9 1 0 amphibian insect amphibian 0 1
25 3 Yoda Wise He Is 0 shortanswer 0 1 snakes 0 0 7 9 0 1 amphibian insect amphibian 0 1
26 3 Yoda Wise He Is 0 shortanswer 1 0 Snakes 0 0 7 9 1 0 amphibian mammal amphibian 0 1
27 3 Yoda Wise He Is 0 shortanswer 0 1 Snakes 0 0 7 9 0 0 amphibian mammal amphibian 0 1
28 3 Yoda Wise He Is 1 shortanswer 1 0 SnakeS 0 0 7 9 0 0 amphibian mammal amphibian 0 1
29 4 Herbert Garrison 0 shortanswer 0 0 dog 0 0 9.4 6 1 0 amphibian amphibian amphibian 0 1
30 4 Herbert Garrison 0 shortanswer 0 0 dog 0 0 9.4 6 0 1 amphibian amphibian amphibian 0 1
31 4 Herbert Garrison 0 shortanswer 0 0 dog 0 0 9.4 6 1 0 insect insect insect 0 1
32 4 Herbert Garrison 0 shortanswer 0 0 dog 0 0 9.4 6 0 1 insect insect insect 0 1
33 4 Herbert Garrison 0 shortanswer 0 0 dog 0 0 9.4 6 1 0 insect amphibian amphibian 0 1
34 4 Herbert Garrison 0 shortanswer 0 0 dog 0 0 9.4 6 0 1 insect amphibian amphibian 0 1
35 4 Herbert Garrison 1 shortanswer 0 0 dog 0 0 9.4 6 1 0 mammal insect mammal 0 1
36 5 Agent Smith 0 numerical 1 0 3.1 1 0 x+y 2 0 0 amphibian amphibian mammal 0 1
37 5 Agent Smith 0 numerical 0 0 3.1 1 0 4.5 2 0 0 amphibian amphibian mammal 0 1
38 5 Agent Smith 1 numerical 0 0 3.1 0 0 4.5 2 0 0 amphibian amphibian mammal 0 1
39 6 Agent Smith 0 numerical 0 0 3.142 0 0 19.4 3 0 0 amphibian insect mammal 1 0
40 6 Agent Smith 0 numerical 1 0 3.14 1 0 19.4 3 1 0 amphibian insect mammal 0 0
41 6 Agent Smith 0 numerical 0 0 3.14 0 1 19.4 3 0 1 amphibian insect mammal 0 0
42 6 Agent Smith 1 numerical 0 0 3.14 1 0 3.3 3 1 0 insect insect amphibian 0 0
43 7 Agent Smith 0 numerical 1 0 3.14 1 0 19.3 4 0 0 insect amphibian insect 0 1
44 7 Agent Smith 1 numerical 0 0 3.14 0 0 19.3 4 0 0 insect amphibian insect 0 1
45 9 Bebe Stevens 0 shortanswer 1 0 goat 1 0 8.5 2 0 0 amphibian amphibian insect 0 1
46 9 Bebe Stevens 0 shortanswer 0 1 goat 0 0 8.5 2 0 0 amphibian amphibian insect 0 1
47 9 Bebe Stevens 0 shortanswer 1 0 Mexican burrowing caecilian 0 0 8.5 2 0 0 amphibian amphibian insect 0 1
48 9 Bebe Stevens 0 shortanswer 0 1 Mexican burrowing caecilian 0 0 8.5 2 0 0 amphibian amphibian insect 0 1
49 9 Bebe Stevens 0 shortanswer 1 0 Mexican burrowing caecilian 0 0 8.5 2 0 0 amphibian amphibian insect 0 1
50 9 Bebe Stevens 0 shortanswer 0 1 Mexican burrowing caecilian 0 0 8.5 2 0 0 amphibian amphibian insect 0 1
51 9 Bebe Stevens 0 shortanswer 1 0 newt 0 0 8.5 2 0 0 amphibian amphibian insect 0 1
52 9 Bebe Stevens 0 shortanswer 0 1 newt 0 0 8.5 2 0 0 amphibian amphibian insect 0 1
53 9 Bebe Stevens 1 shortanswer 1 0 human 0 0 8.5 2 0 0 amphibian amphibian insect 0 1
54 10 Luke Skywalker 0 numerical 1 0 2 1 0 555 10 1 0 amphibian amphibian amphibian 1 0
55 10 Luke Skywalker 0 numerical 0 1 2 0 1 555 10 0 1 amphibian amphibian amphibian 0 0
56 10 Luke Skywalker 0 numerical 1 0 20 1 0 44 10 1 0 amphibian amphibian amphibian 0 0
57 10 Luke Skywalker 0 numerical 0 1 20 0 1 44 10 0 1 amphibian amphibian amphibian 0 0
58 10 Luke Skywalker 0 numerical 1 0 34 1 0 22 10 1 0 amphibian amphibian amphibian 0 0
59 10 Luke Skywalker 0 numerical 0 0 34 0 1 22 10 0 1 amphibian amphibian amphibian 0 0
60 10 Luke Skywalker 0 numerical 0 0 34 1 0 11 10 1 0 amphibian mammal amphibian 0 0
61 10 Luke Skywalker 0 numerical 0 0 34 0 1 11 10 0 0 amphibian mammal amphibian 0 0
62 10 Luke Skywalker 1 numerical 0 0 34 1 0 12 10 0 0 amphibian mammal amphibian 0 0
63 11 Luke Skywalker 0 shortanswer 1 0 toad 1 0 23 1 1 0 insect amphibian amphibian 0 1
64 11 Luke Skywalker 0 shortanswer 0 0 toad 0 1 23 1 0 0 insect amphibian amphibian 0 1
65 11 Luke Skywalker 0 shortanswer 0 0 toad 1 0 22 1 0 0 insect amphibian amphibian 0 1
66 11 Luke Skywalker 0 shortanswer 0 0 toad 0 1 22 1 0 0 insect amphibian amphibian 0 1
67 11 Luke Skywalker 0 shortanswer 0 0 toad 1 0 21 1 0 0 insect amphibian amphibian 0 1
68 11 Luke Skywalker 0 shortanswer 0 0 toad 0 1 21 1 0 0 insect amphibian amphibian 0 1
69 11 Luke Skywalker 0 shortanswer 0 0 toad 1 0 9 1 0 0 insect amphibian amphibian 0 1
70 11 Luke Skywalker 0 shortanswer 0 0 toad 0 1 9 1 0 0 insect amphibian amphibian 0 1
71 11 Luke Skywalker 1 shortanswer 0 0 toad 1 0 9.9 1 0 0 insect amphibian amphibian 0 1
72 12 Luke Skywalker 0 shortanswer 1 0 toad 1 0 8 2 1 0 insect insect amphibian 0 1
73 12 Luke Skywalker 0 shortanswer 0 1 toad 0 1 8 2 0 0 insect insect amphibian 0 1
74 12 Luke Skywalker 0 shortanswer 1 0 toad 1 0 8 2 0 0 insect insect amphibian 0 1
75 12 Luke Skywalker 0 shortanswer 0 1 toad 0 1 8 2 0 0 insect insect amphibian 0 1
76 12 Luke Skywalker 0 shortanswer 1 0 toad 1 0 8 2 0 0 insect insect amphibian 0 1
77 12 Luke Skywalker 0 shortanswer 0 0 toad 0 1 8 2 0 0 insect insect amphibian 0 1
78 12 Luke Skywalker 0 shortanswer 0 0 toad 1 0 8 2 0 0 insect insect amphibian 0 1
79 12 Luke Skywalker 0 shortanswer 0 0 toad 0 1 8 2 0 0 insect insect amphibian 0 1
80 12 Luke Skywalker 1 shortanswer 0 0 toad 1 0 8.5 2 0 0 insect insect amphibian 0 1
81 13 Leia The Princess 0 shortanswer 0 0 eggs 0 0 19.4 4 1 0 amphibian amphibian 1 1
82 13 Leia The Princess 0 shortanswer 0 0 eggs 0 0 19.4 4 1 0 amphibian amphibian mammal 0 1
83 13 Leia The Princess 1 shortanswer 0 0 eggs 0 0 19.4 4 0 0 amphibian amphibian mammal 0 1

View File

@ -0,0 +1,19 @@
quizattempt,firstname,lastname,responses.1.0,responses.1.1,responses.1.2,responses.1.-submit,responses.1.-tryagain,finished
1,John,Jones,insect,insect,insect,1,0,0
1,John,Jones,insect,insect,insect,0,1,0
1,John,Jones,insect,insect,insect,1,0,0
1,John,Jones,insect,insect,insect,0,1,0
1,John,Jones,insect,insect,insect,1,0,0
1,John,Jones,insect,insect,insect,0,1,0
1,John,Jones,insect,insect,insect,1,0,0
1,John,Jones,insect,insect,insect,0,1,0
1,John,Jones,insect,insect,insect,1,0,1
2,Mark,Jones,insect,amphibian,insect,1,0,0
2,Mark,Jones,insect,amphibian,insect,0,1,0
2,Mark,Jones,insect,insect,insect,1,0,0
2,Mark,Jones,insect,insect,insect,0,1,0
2,Mark,Jones,mammal,insect,insect,1,0,0
2,Mark,Jones,mammal,insect,insect,0,1,0
2,Mark,Jones,mammal,insect,insect,1,0,0
2,Mark,Jones,mammal,insect,insect,0,1,0
2,Mark,Jones,mammal,insect,insect,1,0,1
1 quizattempt firstname lastname responses.1.0 responses.1.1 responses.1.2 responses.1.-submit responses.1.-tryagain finished
2 1 John Jones insect insect insect 1 0 0
3 1 John Jones insect insect insect 0 1 0
4 1 John Jones insect insect insect 1 0 0
5 1 John Jones insect insect insect 0 1 0
6 1 John Jones insect insect insect 1 0 0
7 1 John Jones insect insect insect 0 1 0
8 1 John Jones insect insect insect 1 0 0
9 1 John Jones insect insect insect 0 1 0
10 1 John Jones insect insect insect 1 0 1
11 2 Mark Jones insect amphibian insect 1 0 0
12 2 Mark Jones insect amphibian insect 0 1 0
13 2 Mark Jones insect insect insect 1 0 0
14 2 Mark Jones insect insect insect 0 1 0
15 2 Mark Jones mammal insect insect 1 0 0
16 2 Mark Jones mammal insect insect 0 1 0
17 2 Mark Jones mammal insect insect 1 0 0
18 2 Mark Jones mammal insect insect 0 1 0
19 2 Mark Jones mammal insect insect 1 0 1

View File

@ -17,6 +17,14 @@
/**
* Quiz attempt walk through using data from csv file.
*
* The quiz stats below and the question stats found in qstats00.csv were calculated independently in a spreadsheet which is
* available in open document or excel format here :
* https://github.com/jamiepratt/moodle-quiz-tools/tree/master/statsspreadsheet
*
* Similarly the question variant's stats in qstats00.csv are calculated in stats_for_variant_1.xls and stats_for_variant_8.xls
* The calculations in the spreadsheets are the same as for the other question stats but applied just to the attempts where the
* variants appeared.
*
* @package quiz_statistics
* @category phpunit
* @copyright 2013 The Open University
@ -74,14 +82,20 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
$attemptids = $this->walkthrough_attempts($csvdata['steps']);
$this->check_attempts_results($csvdata['results'], $attemptids);
if (isset($csvdata['results'])) {
$this->check_attempts_results($csvdata['results'], $attemptids);
}
$this->report = new quiz_statistics_report();
$whichattempts = QUIZ_GRADEAVERAGE;
$whichattempts = QUIZ_GRADEAVERAGE; // All attempts.
$whichtries = question_attempt::ALL_TRIES;
$groupstudents = array();
$questions = $this->report->load_and_initialise_questions_for_calculations($this->quiz);
list($quizstats, $questionstats) =
$this->report->get_all_stats_and_analysis($this->quiz, $whichattempts, $groupstudents, $questions);
list($quizstats, $questionstats) = $this->report->get_all_stats_and_analysis($this->quiz,
$whichattempts,
$whichtries,
$groupstudents,
$questions);
$qubaids = quiz_statistics_qubaids_condition($this->quiz->id, $groupstudents, $whichattempts);
@ -94,21 +108,203 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
$qcalc = new \core_question\statistics\questions\calculator($questions);
$this->assertTimeCurrent($qcalc->get_last_calculated_time($qubaids));
$expectedvariantcounts = array(2 => array(1 => 6,
4 => 4,
5 => 3,
6 => 4,
7 => 2,
8 => 5,
10 => 1));
if (isset($csvdata['responsecounts'])) {
$this->check_response_counts($csvdata['responsecounts'], $qubaids, $questions, $whichtries);
}
if (isset($csvdata['qstats'])) {
$this->check_question_stats($csvdata['qstats'], $questionstats);
}
if ($quizsettings['testnumber'] === '00') {
$this->check_variants_count_for_quiz_00($questions, $questionstats, $whichtries, $qubaids);
$this->check_quiz_stats_for_quiz_00($quizstats);
}
}
/**
* Check actual question stats are the same as that found in csv file.
*
* @param $qstats PHPUnit_Extensions_Database_DataSet_ITable data from csv file.
* @param $questionstats \core_question\statistics\questions\all_calculated_for_qubaid_condition Calculated stats.
*/
protected function check_question_stats($qstats, $questionstats) {
for ($rowno = 0; $rowno < $qstats->getRowCount(); $rowno++) {
$slotqstats = $qstats->getRow($rowno);
foreach ($slotqstats as $statname => $slotqstat) {
if (!in_array($statname, array('slot', 'subqname')) && $slotqstat !== '') {
$this->assert_stat_equals($slotqstat,
$questionstats,
$slotqstats['slot'],
$slotqstats['subqname'],
$slotqstats['variant'],
$statname);
}
}
// Check that sub-question boolean field is correctly set.
$this->assert_stat_equals(!empty($slotqstats['subqname']),
$questionstats,
$slotqstats['slot'],
$slotqstats['subqname'],
$slotqstats['variant'],
'subquestion');
}
}
/**
* Check that the stat is as expected within a reasonable tolerance.
*
* @param float|string|bool $expected expected value of stat.
* @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats
* @param int $slot
* @param string $subqname if empty string then not an item stat.
* @param int|string $variant if empty string then not a variantstat.
* @param string $statname
*/
protected function assert_stat_equals($expected, $questionstats, $slot, $subqname, $variant, $statname) {
if ($variant === '' && $subqname === '') {
$actual = $questionstats->for_slot($slot)->{$statname};
} else if ($subqname !== '') {
$actual = $questionstats->for_subq($this->randqids[$slot][$subqname])->{$statname};
} else {
$actual = $questionstats->for_slot($slot, $variant)->{$statname};
}
$message = "$statname for slot $slot";
if ($expected === '**NULL**') {
$this->assertEquals(null, $actual, $message);
} else if (is_bool($expected)) {
$this->assertEquals($expected, $actual, $message);
} else if (is_numeric($expected)) {
switch ($statname) {
case 'covariance' :
case 'discriminationindex' :
case 'discriminativeefficiency' :
case 'effectiveweight' :
$precision = 1e-5;
break;
default :
$precision = 1e-6;
}
$delta = abs($expected) * $precision;
$this->assertEquals((float)$expected, $actual, $message, $delta);
} else {
$this->assertEquals($expected, $actual, $message);
}
}
protected function assert_response_count_equals($question, $qubaids, $expected, $whichtries) {
$responesstats = new \core_question\statistics\responses\analyser($question);
$analysis = $responesstats->load_cached($qubaids, $whichtries);
if (!isset($expected['subpart'])) {
$subpart = 1;
} else {
$subpart = $expected['subpart'];
}
list($subpartid, $responseclassid) = $this->get_response_subpart_and_class_id($question,
$subpart,
$expected['modelresponse']);
$subpartanalysis = $analysis->get_analysis_for_subpart($expected['variant'], $subpartid);
$responseclassanalysis = $subpartanalysis->get_response_class($responseclassid);
$actualresponsecounts = $responseclassanalysis->data_for_question_response_table('', '');
foreach ($actualresponsecounts as $actualresponsecount) {
if ($actualresponsecount->response == $expected['actualresponse'] ||
count($actualresponsecounts) == 1) {
$i = 1;
$partofanalysis = " slot {$expected['slot']}, rand q '{$expected['randq']}', variant {$expected['variant']}, ".
"for expected model response {$expected['modelresponse']}, ".
"actual response {$expected['actualresponse']}";
while (isset($expected['count'.$i])) {
if ($expected['count'.$i] != 0) {
$this->assertTrue(isset($actualresponsecount->trycount[$i]),
"There is no count at all for try $i on ".$partofanalysis);
$this->assertEquals($expected['count'.$i], $actualresponsecount->trycount[$i],
"Count for try $i on ".$partofanalysis);
}
$i++;
}
if (isset($expected['totalcount'])) {
$this->assertEquals($expected['totalcount'], $actualresponsecount->totalcount,
"Total count on ".$partofanalysis);
}
return;
}
}
throw new coding_exception("Expected response '{$expected['actualresponse']}' not found.");
}
protected function get_response_subpart_and_class_id($question, $subpart, $modelresponse) {
$qtypeobj = question_bank::get_qtype($question->qtype, false);
$possibleresponses = $qtypeobj->get_possible_responses($question);
$possibleresponsesubpartids = array_keys($possibleresponses);
if (!isset($possibleresponsesubpartids[$subpart - 1])) {
throw new coding_exception("Subpart '{$subpart}' not found.");
}
$subpartid = $possibleresponsesubpartids[$subpart - 1];
if ($modelresponse == '[NO RESPONSE]') {
return array($subpartid, null);
} else if ($modelresponse == '[NO MATCH]') {
return array($subpartid, 0);
}
$modelresponses = array();
foreach ($possibleresponses[$subpartid] as $responseclassid => $subpartpossibleresponse) {
$modelresponses[$responseclassid] = $subpartpossibleresponse->responseclass;
}
$this->assertContains($modelresponse, $modelresponses);
$responseclassid = array_search($modelresponse, $modelresponses);
return array($subpartid, $responseclassid);
}
/**
* @param $responsecounts
* @param $qubaids
* @param $questions
* @param $whichtries
*/
protected function check_response_counts($responsecounts, $qubaids, $questions, $whichtries) {
for ($rowno = 0; $rowno < $responsecounts->getRowCount(); $rowno++) {
$expected = $responsecounts->getRow($rowno);
$defaultsforexpected = array('randq' => '', 'variant' => '1', 'subpart' => '1');
foreach ($defaultsforexpected as $key => $expecteddefault) {
if (!isset($expected[$key])) {
$expected[$key] = $expecteddefault;
}
}
if ($expected['randq'] == '') {
$question = $questions[$expected['slot']];
} else {
$qid = $this->randqids[$expected['slot']][$expected['randq']];
$question = question_finder::get_instance()->load_question_data($qid);
}
$this->assert_response_count_equals($question, $qubaids, $expected, $whichtries);
}
}
/**
* @param $questions
* @param $questionstats
* @param $whichtries
* @param $qubaids
*/
protected function check_variants_count_for_quiz_00($questions, $questionstats, $whichtries, $qubaids) {
$expectedvariantcounts = array(2 => array(1 => 6,
4 => 4,
5 => 3,
6 => 4,
7 => 2,
8 => 5,
10 => 1));
foreach ($questions as $slot => $question) {
if (!question_bank::get_qtype($question->qtype, false)->can_analyse_responses()) {
continue;
}
$responesstats = new \core_question\statistics\responses\analyser($question);
$this->assertTimeCurrent($responesstats->get_last_analysed_time($qubaids));
$analysis = $responesstats->load_cached($qubaids);
$this->assertTimeCurrent($responesstats->get_last_analysed_time($qubaids, $whichtries));
$analysis = $responesstats->load_cached($qubaids, $whichtries);
$variantsnos = $analysis->get_variant_nos();
if (isset($expectedvariantcounts[$slot])) {
// Compare contents, ignore ordering of array, using canonicalize parameter of assertEquals.
@ -132,7 +328,7 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
$classanalysis = $subpartanalysis->get_response_class($classid);
$actualresponsecounts = $classanalysis->data_for_question_response_table('', '');
foreach ($actualresponsecounts as $actualresponsecount) {
$totalspervariantno[$subpartid][$variantno] += $actualresponsecount->count;
$totalspervariantno[$subpartid][$variantno] += $actualresponsecount->totalcount;
}
}
}
@ -163,89 +359,7 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
}
}
}
for ($rowno = 0; $rowno < $csvdata['responsecounts']->getRowCount(); $rowno++) {
$responsecount = $csvdata['responsecounts']->getRow($rowno);
if ($responsecount['randq'] == '') {
$question = $questions[$responsecount['slot']];
} else {
$qid = $this->randqids[$responsecount['slot']][$responsecount['randq']];
$question = question_finder::get_instance()->load_question_data($qid);
}
$this->assert_response_count_equals($question, $qubaids, $responsecount);
}
// These quiz stats and the question stats found in qstats00.csv were calculated independently in spreadsheet which is
// available in open document or excel format here :
// https://github.com/jamiepratt/moodle-quiz-tools/tree/master/statsspreadsheet
$quizstatsexpected = array(
'median' => 4.5,
'firstattemptsavg' => 4.617333332,
'allattemptsavg' => 4.617333332,
'firstattemptscount' => 25,
'allattemptscount' => 25,
'standarddeviation' => 0.8117265554,
'skewness' => -0.092502502,
'kurtosis' => -0.7073968557,
'cic' => -87.2230935542,
'errorratio' => 136.8294900795,
'standarderror' => 1.1106813066
);
foreach ($quizstatsexpected as $statname => $statvalue) {
$this->assertEquals($statvalue, $quizstats->$statname, $quizstats->$statname, abs($statvalue) * 1.5e-5);
}
for ($rowno = 0; $rowno < $csvdata['qstats']->getRowCount(); $rowno++) {
$slotqstats = $csvdata['qstats']->getRow($rowno);
foreach ($slotqstats as $statname => $slotqstat) {
if ($statname !== 'slot') {
$this->assert_stat_equals($questionstats, $slotqstats['slot'], null, null, $statname, (float)$slotqstat);
}
}
}
$itemstats = array('s' => 12,
'effectiveweight' => null,
'discriminationindex' => 35.803933,
'discriminativeefficiency' => 39.39393939,
'sd' => 0.514928651,
'facility' => 0.583333333,
'maxmark' => 1,
'positions' => '1',
'slot' => null,
'subquestion' => true);
foreach ($itemstats as $statname => $expected) {
$this->assert_stat_equals($questionstats, 1, null, 'numerical', $statname, $expected);
}
// These variant's stats are calculated in stats_for_variant_1.xls and stats_for_variant_8.xls
// The calculations in the spreadsheets are the same but applied just to the attempts where the variants appeared.
$statsforslot2variants = array(1 => array('s' => 6,
'effectiveweight' => null,
'discriminationindex' => -10.5999788,
'discriminativeefficiency' => -14.28571429,
'sd' => 0.5477225575,
'facility' => 0.50,
'maxmark' => 1,
'variant' => 1,
'slot' => 2,
'subquestion' => false),
8 => array('s' => 5,
'effectiveweight' => null,
'discriminationindex' => -57.77466679,
'discriminativeefficiency' => -71.05263241,
'sd' => 0.547722558,
'facility' => 0.40,
'maxmark' => 1,
'variant' => 8,
'slot' => 2,
'subquestion' => false));
foreach ($statsforslot2variants as $variant => $stats) {
foreach ($stats as $statname => $expected) {
$this->assert_stat_equals($questionstats, 2, $variant, null, $statname, $expected);
}
}
foreach ($expectedvariantcounts as $slot => $expectedvariantcount) {
foreach ($expectedvariantcount as $variantno => $s) {
$this->assertEquals($s, $questionstats->for_slot($slot, $variantno)->s);
@ -254,94 +368,26 @@ class quiz_report_statistics_from_steps_testcase extends mod_quiz_attempt_walkth
}
/**
* Check that the stat is as expected within a reasonable tolerance.
*
* @param \core_question\statistics\questions\all_calculated_for_qubaid_condition $questionstats
* @param int $slot
* @param int|null $variant if null then not a variant stat.
* @param string|null $subqname if null then not an item stat.
* @param string $statname
* @param float $expected
* @param $quizstats
*/
protected function assert_stat_equals($questionstats, $slot, $variant, $subqname, $statname, $expected) {
protected function check_quiz_stats_for_quiz_00($quizstats) {
$quizstatsexpected = array(
'median' => 4.5,
'firstattemptsavg' => 4.617333332,
'allattemptsavg' => 4.617333332,
'firstattemptscount' => 25,
'allattemptscount' => 25,
'standarddeviation' => 0.8117265554,
'skewness' => -0.092502502,
'kurtosis' => -0.7073968557,
'cic' => -87.2230935542,
'errorratio' => 136.8294900795,
'standarderror' => 1.1106813066
);
if ($variant === null && $subqname === null) {
$actual = $questionstats->for_slot($slot)->{$statname};
} else if ($subqname !== null) {
$actual = $questionstats->for_subq($this->randqids[$slot][$subqname])->{$statname};
} else {
$actual = $questionstats->for_slot($slot, $variant)->{$statname};
foreach ($quizstatsexpected as $statname => $statvalue) {
$this->assertEquals($statvalue, $quizstats->$statname, $quizstats->$statname, abs($statvalue) * 1.5e-5);
}
if (is_bool($expected) || is_string($expected)) {
$this->assertEquals($expected, $actual, "$statname for slot $slot");
} else {
switch ($statname) {
case 'covariance' :
case 'discriminationindex' :
case 'discriminativeefficiency' :
case 'effectiveweight' :
$precision = 1e-5;
break;
default :
$precision = 1e-6;
}
$delta = abs($expected) * $precision;
$this->assertEquals(floatval($expected), $actual, "$statname for slot $slot", $delta);
}
}
protected function assert_response_count_equals($question, $qubaids, $responsecount) {
$responesstats = new \core_question\statistics\responses\analyser($question);
$analysis = $responesstats->load_cached($qubaids);
if (!isset($responsecount['subpart'])) {
$subpart = 1;
} else {
$subpart = $responsecount['subpart'];
}
list($subpartid, $responseclassid) = $this->get_response_subpart_and_class_id($question,
$subpart,
$responsecount['modelresponse']);
$subpartanalysis = $analysis->get_analysis_for_subpart($responsecount['variant'], $subpartid);
$responseclassanalysis = $subpartanalysis->get_response_class($responseclassid);
$actualresponsecounts = $responseclassanalysis->data_for_question_response_table('', '');
if ($responsecount['modelresponse'] !== '[NO RESPONSE]') {
foreach ($actualresponsecounts as $actualresponsecount) {
if ($actualresponsecount->response == $responsecount['actualresponse']) {
$this->assertEquals($responsecount['count'], $actualresponsecount->count);
return;
}
}
throw new coding_exception("Actual response '{$responsecount['actualresponse']}' not found.");
} else {
$actualresponsecount = array_pop($actualresponsecounts);
$this->assertEquals($responsecount['count'], $actualresponsecount->count);
}
}
protected function get_response_subpart_and_class_id($question, $subpart, $modelresponse) {
$qtypeobj = question_bank::get_qtype($question->qtype, false);
$possibleresponses = $qtypeobj->get_possible_responses($question);
$possibleresponsesubpartids = array_keys($possibleresponses);
if (!isset($possibleresponsesubpartids[$subpart - 1])) {
throw new coding_exception("Subpart '{$subpart}' not found.");
}
$subpartid = $possibleresponsesubpartids[$subpart - 1];
if ($modelresponse == '[NO RESPONSE]') {
return array($subpartid, null);
} else if ($modelresponse == '[NO MATCH]') {
return array($subpartid, 0);
}
$modelresponses = array();
foreach ($possibleresponses[$subpartid] as $responseclassid => $subpartpossibleresponse) {
$modelresponses[$responseclassid] = $subpartpossibleresponse->responseclass;
}
$this->assertContains($modelresponse, $modelresponses);
$responseclassid = array_search($modelresponse, $modelresponses);
return array($subpartid, $responseclassid);
}
}

View File

@ -183,7 +183,9 @@ class mod_quiz_attempt_walkthrough_from_csv_testcase extends advanced_testcase {
$quizsettings = $quizzes->getRow($rowno);
$dataset = array();
foreach ($this->files as $file) {
$dataset[$file] = $this->load_csv_data_file($file, $quizsettings['testnumber']);
if (file_exists($this->get_full_path_of_csv_file($file, $quizsettings['testnumber']))) {
$dataset[$file] = $this->load_csv_data_file($file, $quizsettings['testnumber']);
}
}
$datasets[] = array($quizsettings, $dataset);
}

View File

@ -281,10 +281,70 @@ abstract class question_behaviour {
}
/**
* @return question_possible_response[] where keys are subpartid or an empty array if no classification is possible.
* Classify responses for this question into a number of sub parts and response classes as defined by
* {@link \question_type::get_possible_responses} for this question type.
*
* @param string $whichtries which tries to analyse for response analysis. Will be one of
* question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
* Defaults to question_attempt::LAST_TRY.
* @return (question_classified_response|array)[] If $whichtries is question_attempt::FIRST_TRY or LAST_TRY index is subpartid
* and values are question_classified_response instances.
* If $whichtries is question_attempt::ALL_TRIES then first key is submitted response no
* and the second key is subpartid.
*/
public function classify_response() {
return $this->question->classify_response($this->qa->get_last_qt_data());
public function classify_response($whichtries = question_attempt::LAST_TRY) {
if ($whichtries == question_attempt::LAST_TRY) {
return $this->question->classify_response($this->qa->get_last_qt_data());
} else {
$stepswithsubmit = $this->qa->get_steps_with_submitted_response_iterator();
if ($whichtries == question_attempt::FIRST_TRY) {
return $this->question->classify_response($stepswithsubmit[1]->get_qt_data());
} else {
$classifiedresponses = array();
foreach ($stepswithsubmit as $submittedresponseno => $step) {
$classifiedresponses[$submittedresponseno] = $this->question->classify_response($step->get_qt_data());
}
return $this->remove_repeated_submitted_responses($classifiedresponses);
}
}
}
/**
* Filter classified responses for multiple tries to remove identical responses that are not significant.
*
* In base class we compare last response to preceding responses and remove all identical responses until a different one is
* found then for that different response compare to preceding and remove all identical until ...
*
* @param array[] $classifiedresponses first index is submitted response no and second is sub-part id. Value is of type
* question_classified_response. Return value from self::classify_response for ALL_TRIES.
* @return array[] return non repeated responses.
*/
protected function remove_repeated_submitted_responses($classifiedresponses) {
$submittedresponsenos = array_keys($classifiedresponses);
$submittedresponsenos = array_reverse($submittedresponsenos);
$lastsubmittedresponseno = array_shift($submittedresponsenos);
$nooflastnewresponse = $lastsubmittedresponseno;
while (count($submittedresponsenos)) {
$precedingresponseno = array_shift($submittedresponsenos);
$responsesrepeated = true;
if (count($classifiedresponses[$precedingresponseno]) !== count($classifiedresponses[$nooflastnewresponse])) {
$responsesrepeated = false;
} else {
foreach (array_keys($classifiedresponses[$precedingresponseno]) as $subpartid) {
if ($classifiedresponses[$precedingresponseno][$subpartid] !=
$classifiedresponses[$nooflastnewresponse][$subpartid]) {
$responsesrepeated = false;
break;
}
}
}
if ($responsesrepeated) {
unset($classifiedresponses[$precedingresponseno]);
} else {
$nooflastnewresponse = $precedingresponseno;
}
}
return $classifiedresponses;
}
/**

View File

@ -90,4 +90,31 @@ class qbehaviour_interactivecountback extends qbehaviour_interactive {
return $this->question->compute_final_grade($responses, $totaltries);
}
/**
* Filter classified responses for multiple tries to remove identical responses that are not significant.
*
* For this behaviour the significant repeated response part are the first of any repeated responses, for any part of the
* question. These are the responses that are graded.
*
* @param array[] $classifiedresponses first index is submitted response no and second is sub-part id. Value is of type
* question_classified_response. Return value from self::classify_response for ALL_TRIES.
* @return array[] return non repeated responses.
*/
protected function remove_repeated_submitted_responses($classifiedresponses) {
$subpartids = array_keys($classifiedresponses[1]);
foreach ($subpartids as $subpartid) {
$lastdifferentresponsepart = 1;
$tryno = 2;
while (isset($classifiedresponses[$tryno])) {
if ($classifiedresponses[$tryno][$subpartid] != $classifiedresponses[$lastdifferentresponsepart][$subpartid]) {
$lastdifferentresponsepart = $tryno;
} else {
unset($classifiedresponses[$tryno][$subpartid]);
}
$tryno++;
}
}
return $classifiedresponses;
}
}

View File

@ -15,11 +15,10 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* This file contains the code to analyse all the responses to a particular
* question.
* This file contains the code to analyse all the responses to a particular question.
*
* @package core_question
* @copyright 2013 Open University
* @copyright 2014 Open University
* @author Jamie Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@ -28,19 +27,22 @@ namespace core_question\statistics\responses;
defined('MOODLE_INTERNAL') || die();
/**
* This class can store and compute the analysis of the responses to a particular
* question.
* This class can compute, store and cache the analysis of the responses to a particular question.
*
* @copyright 2013 Open University
* @author Jamie Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @package core_question
* @copyright 2014 The Open University
* @author James Pratt me@jamiep.org
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class analyser {
/** @var int Time after which responses are automatically reanalysed. */
const TIME_TO_CACHE = 900; // 15 minutes.
/** @var object full question data from db. */
protected $questiondata;
/**
* @var analysis_for_question
* @var analysis_for_question|analysis_for_question_all_tries
*/
public $analysis;
@ -63,15 +65,23 @@ class analyser {
* for a particular question.
*
* @param object $questiondata the full question data from the database defining this question.
* @param string $whichtries which tries to analyse.
*/
public function __construct($questiondata) {
public function __construct($questiondata, $whichtries = \question_attempt::LAST_TRY) {
$this->questiondata = $questiondata;
$qtypeobj = \question_bank::get_qtype($this->questiondata->qtype);
$this->analysis = new analysis_for_question($qtypeobj->get_possible_responses($this->questiondata));
if ($whichtries != \question_attempt::ALL_TRIES) {
$this->analysis = new analysis_for_question($qtypeobj->get_possible_responses($this->questiondata));
} else {
$this->analysis = new analysis_for_question_all_tries($qtypeobj->get_possible_responses($this->questiondata));
}
$this->breakdownbyvariant = $qtypeobj->break_down_stats_and_response_analysis_by_variant($this->questiondata);
}
/**
* Does the computed analysis have sub parts?
*
* @return bool whether this analysis has more than one subpart.
*/
public function has_subparts() {
@ -79,6 +89,8 @@ class analyser {
}
/**
* Does the computed analysis's sub parts have classes?
*
* @return bool whether this analysis has (a subpart with) more than one response class.
*/
public function has_response_classes() {
@ -91,40 +103,21 @@ class analyser {
}
/**
* @return bool whether this analysis has a response class more than one
* different acutal response, or if the actual response is different from
* the model response.
*/
public function has_actual_responses() {
foreach ($this->responseclasses as $subpartid => $partclasses) {
foreach ($partclasses as $responseclassid => $modelresponse) {
$numresponses = count($this->responses[$subpartid][$responseclassid]);
if ($numresponses > 1) {
return true;
}
$actualresponse = key($this->responses[$subpartid][$responseclassid]);
if ($numresponses == 1 && $actualresponse != $modelresponse->responseclass) {
return true;
}
}
}
return false;
}
/**
* Analyse all the response data for for all the specified attempts at
* this question.
* Analyse all the response data for for all the specified attempts at this question.
*
* @param \qubaid_condition $qubaids which attempts to consider.
* @param string $whichtries which tries to analyse. Will be one of
* \question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
* @return analysis_for_question
*/
public function calculate($qubaids) {
public function calculate($qubaids, $whichtries = \question_attempt::LAST_TRY) {
// Load data.
$dm = new \question_engine_data_mapper();
$questionattempts = $dm->load_attempts_at_question($this->questiondata->id, $qubaids);
// Analyse it.
foreach ($questionattempts as $qa) {
$responseparts = $qa->classify_response();
$responseparts = $qa->classify_response($whichtries);
if ($this->breakdownbyvariant) {
$this->analysis->count_response_parts($qa->get_variant(), $responseparts);
} else {
@ -132,33 +125,44 @@ class analyser {
}
}
$this->analysis->cache($qubaids, $this->questiondata->id);
$this->analysis->cache($qubaids, $whichtries, $this->questiondata->id);
return $this->analysis;
}
/** @var integer Time after which responses are automatically reanalysed. */
const TIME_TO_CACHE = 900; // 15 minutes.
/**
* Retrieve the computed response analysis from the question_response_analysis table.
*
* @param \qubaid_condition $qubaids which attempts to get cached response analysis for.
* @param \qubaid_condition $qubaids load the analysis of which question usages?
* @param string $whichtries load the analysis of which tries?
* @return analysis_for_question|boolean analysis or false if no cached analysis found.
*/
public function load_cached($qubaids) {
public function load_cached($qubaids, $whichtries) {
global $DB;
$timemodified = time() - self::TIME_TO_CACHE;
$rows = $DB->get_records_select('question_response_analysis', 'hashcode = ? AND questionid = ? AND timemodified > ?',
array($qubaids->get_hash_code(), $this->questiondata->id, $timemodified));
if (!$rows) {
// Variable name 'analyses' is the plural of 'analysis'.
$responseanalyses = $DB->get_records_select('question_response_analysis',
'hashcode = ? AND whichtries = ? AND questionid = ? AND timemodified > ?',
array($qubaids->get_hash_code(), $whichtries, $this->questiondata->id, $timemodified));
if (!$responseanalyses) {
return false;
}
foreach ($rows as $row) {
$class = $this->analysis->get_analysis_for_subpart($row->variant, $row->subqid)->get_response_class($row->aid);
$class->add_response_and_count($row->response, $row->credit, $row->rcount);
$analysisids = array();
foreach ($responseanalyses as $responseanalysis) {
$analysisforsubpart = $this->analysis->get_analysis_for_subpart($responseanalysis->variant, $responseanalysis->subqid);
$class = $analysisforsubpart->get_response_class($responseanalysis->aid);
$class->add_response($responseanalysis->response, $responseanalysis->credit);
$analysisids[] = $responseanalysis->id;
}
list($sql, $params) = $DB->get_in_or_equal($analysisids);
$counts = $DB->get_records_select('question_response_count', "analysisid {$sql}", $params);
foreach ($counts as $count) {
$responseanalysis = $responseanalyses[$count->analysisid];
$analysisforsubpart = $this->analysis->get_analysis_for_subpart($responseanalysis->variant, $responseanalysis->subqid);
$class = $analysisforsubpart->get_response_class($responseanalysis->aid);
$class->set_response_count($responseanalysis->response, $count->try, $count->rcount);
}
return $this->analysis;
}
@ -167,15 +171,17 @@ class analyser {
/**
* Find time of non-expired analysis in the database.
*
* @param $qubaids \qubaid_condition
* @param \qubaid_condition $qubaids check for the analysis of which question usages?
* @param string $whichtries check for the analysis of which tries?
* @return integer|boolean Time of cached record that matches this qubaid_condition or false if none found.
*/
public function get_last_analysed_time($qubaids) {
public function get_last_analysed_time($qubaids, $whichtries) {
global $DB;
$timemodified = time() - self::TIME_TO_CACHE;
return $DB->get_field_select('question_response_analysis', 'timemodified',
'hashcode = ? AND questionid = ? AND timemodified > ?',
array($qubaids->get_hash_code(), $this->questiondata->id, $timemodified), IGNORE_MULTIPLE);
'hashcode = ? AND whichtries = ? AND questionid = ? AND timemodified > ?',
array($qubaids->get_hash_code(), $whichtries, $this->questiondata->id, $timemodified),
IGNORE_MULTIPLE);
}
}

View File

@ -23,12 +23,35 @@
namespace core_question\statistics\responses;
/**
* The leafs of the analysis data structure.
*
* - There is a separate data structure for each question or sub question's analysis
* {@link \core_question\statistics\responses\analysis_for_question}
* or {@link \core_question\statistics\responses\analysis_for_question_all_tries}.
* - There are separate analysis for each variant in this top level instance.
* - Then there are class instances representing the analysis of each of the sub parts of each variant of the question.
* {@link \core_question\statistics\responses\analysis_for_subpart}.
* - Then within the sub part analysis there are response class analysis
* {@link \core_question\statistics\responses\analysis_for_class}.
* - Then within each class analysis there are analysis for each actual response
* {@link \core_question\statistics\responses\analysis_for_actual_response}.
*
* @package core_question
* @copyright 2014 The Open University
* @author James Pratt me@jamiep.org
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class analysis_for_actual_response {
/**
* @var int count of this response
* @var int[] count per try for this response.
*/
protected $count;
protected $trycount = array();
/**
* @var int total count of tries with this response.
*/
protected $totalcount = 0;
/**
* @var float grade for this response, normally between 0 and 1.
@ -43,32 +66,54 @@ class analysis_for_actual_response {
/**
* @param string $response
* @param float $fraction
* @param int $count defaults to zero, this param used when loading from db.
*/
public function __construct($response, $fraction, $count = 0) {
public function __construct($response, $fraction) {
$this->response = $response;
$this->fraction = $fraction;
$this->count = $count;
}
/**
* Used to count the occurrences of response sub parts.
*
* @param int $try the try number, or 0 if only keeping one count, not a count for each try.
*/
public function increment_count() {
$this->count++;
public function increment_count($try = 0) {
$this->totalcount++;
if ($try != 0) {
if (!isset($this->trycount[$try])) {
$this->trycount[$try] = 0;
}
$this->trycount[$try]++;
}
}
/**
* @param \qubaid_condition $qubaids
* @param int $questionid the question id
* @param int $variantno
* @param string $subpartid
* @param string $responseclassid
* Used to set the count of occurrences of response sub parts, when loading count from cache.
*
* @param int $try the try number, or 0 if only keeping one count, not a count for each try.
* @param int $count
*/
public function cache($qubaids, $questionid, $variantno, $subpartid, $responseclassid) {
public function set_count($try, $count) {
$this->totalcount = $this->totalcount + $count;
$this->trycount[$try] = $count;
}
/**
* Cache analysis for class.
*
* @param \qubaid_condition $qubaids which question usages have been analysed.
* @param string $whichtries which tries have been analysed?
* @param int $questionid which question.
* @param int $variantno which variant.
* @param string $subpartid which sub part is this actual response in?
* @param string $responseclassid which response class is this actual response in?
*/
public function cache($qubaids, $whichtries, $questionid, $variantno, $subpartid, $responseclassid) {
global $DB;
$row = new \stdClass();
$row->hashcode = $qubaids->get_hash_code();
$row->whichtries = $whichtries;
$row->questionid = $questionid;
$row->variant = $variantno;
$row->subqid = $subpartid;
@ -78,17 +123,29 @@ class analysis_for_actual_response {
$row->aid = $responseclassid;
}
$row->response = $this->response;
$row->rcount = $this->count;
$row->credit = $this->fraction;
$row->timemodified = time();
$DB->insert_record('question_response_analysis', $row, false);
}
public function response_matches($response) {
return $response == $this->response;
$analysisid = $DB->insert_record('question_response_analysis', $row);
if ($whichtries === \question_attempt::ALL_TRIES) {
foreach ($this->trycount as $try => $count) {
$countrow = new \stdClass();
$countrow->try = $try;
$countrow->rcount = $count;
$countrow->analysisid = $analysisid;
$DB->insert_record('question_response_count', $countrow, false);
}
} else {
$countrow = new \stdClass();
$countrow->try = 0;
$countrow->rcount = $this->totalcount;
$countrow->analysisid = $analysisid;
$DB->insert_record('question_response_count', $countrow, false);
}
}
/**
* Returns an object with a property for each column of the question response analysis table.
*
* @param string $partid
* @param string $modelresponse
* @return object
@ -99,7 +156,17 @@ class analysis_for_actual_response {
$rowdata->responseclass = $modelresponse;
$rowdata->response = $this->response;
$rowdata->fraction = $this->fraction;
$rowdata->count = $this->count;
$rowdata->totalcount = $this->totalcount;
$rowdata->trycount = $this->trycount;
return $rowdata;
}
/**
* What is the highest try number that this response has been seen?
*
* @return int try number
*/
public function get_maximum_tries() {
return max(array_keys($this->trycount));
}
}

View File

@ -26,39 +26,48 @@ namespace core_question\statistics\responses;
/**
* Represents an actual part of the response that has been classified in a class of responses for this sub part of the question.
*
* A question and it's response is represented as having one or more sub parts where the response to each sub-part might fall
* into one of one or more classes.
* Counts a class of responses for this sub part of the question.
*
* No response is one possible class of response to a question.
*
* @copyright 2010 The Open University
* - There is a separate data structure for each question or sub question's analysis
* {@link \core_question\statistics\responses\analysis_for_question}
* or {@link \core_question\statistics\responses\analysis_for_question_all_tries}.
* - There are separate analysis for each variant in this top level instance.
* - Then there are class instances representing the analysis of each of the sub parts of each variant of the question.
* {@link \core_question\statistics\responses\analysis_for_subpart}.
* - Then within the sub part analysis there are response class analysis
* {@link \core_question\statistics\responses\analysis_for_class}.
* - Then within each class analysis there are analysis for each actual response
* {@link \core_question\statistics\responses\analysis_for_actual_response}.
*
* @package core_question
* @copyright 2014 The Open University
* @author James Pratt me@jamiep.org
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class analysis_for_class {
/**
* @var string
* @var string must be unique for each response class within this sub part.
*/
protected $responseclassid;
/**
* @var string
* @var string represent this class in the response analysis table.
*/
protected $modelresponse;
/** @var string the (partial) credit awarded for this responses. */
protected $fraction;
/**
*
* @var analysis_for_actual_response[] key is the actual response represented as a string as it will be displayed in report.
/** @var analysis_for_actual_response[] key is the actual response represented as a string as it will be displayed in report.
*/
protected $actualresponses = array();
/**
* Constructor, just an easy way to set the fields.
*
* @param \question_possible_response $possibleresponse
* @param string $responseclassid
*/
@ -69,56 +78,88 @@ class analysis_for_class {
}
/**
* @param string $actualresponse
* Keep a count of a response to this question sub part that falls within this class.
*
* @param string $actualresponse
* @param float|null $fraction
* @param int $try
* @return \core_question\statistics\responses\analysis_for_actual_response
*/
public function count_response($actualresponse, $fraction) {
public function count_response($actualresponse, $fraction, $try) {
if (!isset($this->actualresponses[$actualresponse])) {
if ($fraction === null) {
$fraction = $this->fraction;
}
$this->actualresponses[$actualresponse] = new analysis_for_actual_response($actualresponse, $fraction);
$this->add_response($actualresponse, $fraction);
}
$this->actualresponses[$actualresponse]->increment_count();
$this->get_response($actualresponse)->increment_count($try);
}
/**
* @param \qubaid_condition $qubaids
* @param int $questionid the question id
* @param int $variantno
* @param string $subpartid
* Cache analysis for class.
*
* @param \qubaid_condition $qubaids which question usages have been analysed.
* @param string $whichtries which tries have been analysed?
* @param int $questionid which question.
* @param int $variantno which variant.
* @param string $subpartid which sub part.
*/
public function cache($qubaids, $questionid, $variantno, $subpartid) {
foreach ($this->actualresponses as $response => $actualresponse) {
$actualresponse->cache($qubaids, $questionid, $variantno, $subpartid, $this->responseclassid, $response);
public function cache($qubaids, $whichtries, $questionid, $variantno, $subpartid) {
foreach ($this->get_responses() as $response) {
$analysisforactualresponse = $this->get_response($response);
$analysisforactualresponse->cache($qubaids, $whichtries, $questionid, $variantno, $subpartid, $this->responseclassid);
}
}
public function add_response_and_count($response, $fraction, $count) {
$this->actualresponses[$response] = new analysis_for_actual_response($response, $fraction, $count);
/**
* Add an actual response to the data structure.
*
* @param string $response A string representing the actual response.
* @param float $fraction The fraction of grade awarded for this response.
*/
public function add_response($response, $fraction) {
$this->actualresponses[$response] = new analysis_for_actual_response($response, $fraction);
}
/**
* Used when loading cached counts.
*
* @param string $response
* @param int $try the try number, will be zero if not keeping track of try.
* @param int $count the count
*/
public function set_response_count($response, $try, $count) {
$this->actualresponses[$response]->set_count($try, $count);
}
/**
* Are there actual responses to sub parts that where classified into this class?
*
* @return bool whether this analysis has a response class with more than one
* different actual response, or if the actual response is different from
* the model response.
*/
public function has_actual_responses() {
if (count($this->actualresponses) > 1) {
$actualresponses = $this->get_responses();
if (count($actualresponses) > 1) {
return true;
} else if (count($this->actualresponses) == 1) {
$onlyactualresponse = reset($this->actualresponses);
return !$onlyactualresponse->response_matches($this->modelresponse);
} else if (count($actualresponses) === 1) {
$singleactualresponse = reset($actualresponses);
return $singleactualresponse != $this->modelresponse;
}
return false;
}
/**
* Return the data to display in the response analysis table.
*
* @param bool $responseclasscolumn
* @param string $partid
* @return object[]
*/
public function data_for_question_response_table($responseclasscolumn, $partid) {
$return = array();
if (empty($this->actualresponses)) {
if (count($this->get_responses()) == 0) {
$rowdata = new \stdClass();
$rowdata->part = $partid;
$rowdata->responseclass = $this->modelresponse;
@ -128,13 +169,47 @@ class analysis_for_class {
$rowdata->response = '';
}
$rowdata->fraction = $this->fraction;
$rowdata->count = 0;
$rowdata->totalcount = 0;
$rowdata->trycount = array();
$return[] = $rowdata;
} else {
foreach ($this->actualresponses as $actualresponse) {
$return[] = $actualresponse->data_for_question_response_table($partid, $this->modelresponse);
foreach ($this->get_responses() as $actualresponse) {
$response = $this->get_response($actualresponse);
$return[] = $response->data_for_question_response_table($partid, $this->modelresponse);
}
}
return $return;
}
/**
* What is the highest try number that an actual response of this response class has been seen?
*
* @return int try number
*/
public function get_maximum_tries() {
$max = 1;
foreach ($this->get_responses() as $actualresponse) {
$max = max($max, $this->get_response($actualresponse)->get_maximum_tries());
}
return $max;
}
/**
* Return array of the actual responses to this sub part that were classified into this class.
*
* @return string[] the actual responses we are counting tries at.
*/
protected function get_responses() {
return array_keys($this->actualresponses);
}
/**
* Get the data structure used to count the responses that match an actual response within this class of responses.
*
* @param string $response
* @return analysis_for_actual_response the instance for keeping count of tries for $response.
*/
protected function get_response($response) {
return $this->actualresponses[$response];
}
}

View File

@ -29,17 +29,29 @@ defined('MOODLE_INTERNAL') || die();
/**
* Analysis for possible responses for parts of a question. It is up to a question type designer to decide on how many parts their
* question has. A sub part might represent a sub question embedded in the question for example in a matching question there are
* question has. See {@link \question_type::get_possible_responses()} and sub classes where the sub parts and response classes are
* defined.
*
* A sub part might represent a sub question embedded in the question for example in a matching question there are
* several sub parts. A numeric question with a unit might be divided into two sub parts for the purposes of response analysis
* or the question type designer might decide to treat the answer, both the numeric and unit part,
* as a whole for the purposes of response analysis.
*
* Responses can be further divided into 'classes' in which they are classified. One or more of these 'classes' are contained in
* the responses
* - There is a separate data structure for each question or sub question's analysis
* {@link \core_question\statistics\responses\analysis_for_question}
* or {@link \core_question\statistics\responses\analysis_for_question_all_tries}.
* - There are separate analysis for each variant in this top level instance.
* - Then there are class instances representing the analysis of each of the sub parts of each variant of the question.
* {@link \core_question\statistics\responses\analysis_for_subpart}.
* - Then within the sub part analysis there are response class analysis
* {@link \core_question\statistics\responses\analysis_for_class}.
* - Then within each class analysis there are analysis for each actual response
* {@link \core_question\statistics\responses\analysis_for_actual_response}.
*
* @copyright 2013 Open University
* @author Jamie Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @package core_question
* @copyright 2014 The Open University
* @author James Pratt me@jamiep.org
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class analysis_for_question {
@ -149,6 +161,23 @@ class analysis_for_question {
return false;
}
/**
* @return bool Does this response analysis include counts for responses for multiple tries of the question?
*/
public function has_multiple_tries_data() {
return false;
}
/**
* What is the highest number of tries at this question?
*
* @return int always 1 as this class is for analysing only one try.
*/
public function get_maximum_tries() {
return 1;
}
/**
* Takes an array of {@link \question_classified_response} and adds counts of the responses to the sub parts and classes.
*
@ -162,13 +191,15 @@ class analysis_for_question {
}
/**
* @param \qubaid_condition $qubaids
* @param int $questionid the question id
* @param \qubaid_condition $qubaids which question usages have been analysed.
* @param string $whichtries which tries have been analysed?
* @param int $questionid which question.
*/
public function cache($qubaids, $questionid) {
public function cache($qubaids, $whichtries, $questionid) {
foreach ($this->get_variant_nos() as $variantno) {
foreach ($this->get_subpart_ids($variantno) as $subpartid) {
$this->get_analysis_for_subpart($variantno, $subpartid)->cache($qubaids, $questionid, $variantno, $subpartid);
$analysisforsubpart = $this->get_analysis_for_subpart($variantno, $subpartid);
$analysisforsubpart->cache($qubaids, $whichtries, $questionid, $variantno, $subpartid);
}
}
}

View 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/>.
/**
* This file contains a class to analyse all the responses for multiple tries at a particular question.
*
* @package core_question
* @copyright 2014 Open University
* @author Jamie Pratt <me@jamiep.org>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_question\statistics\responses;
/**
* Analysis for possible responses for parts of a question with multiple submitted responses.
*
* If the analysis was for a single try it would be handled by {@link \core_question\statistics\responses\analysis_for_question}.
*
* - There is a separate data structure for each question or sub question's analysis
* {@link \core_question\statistics\responses\analysis_for_question}
* or {@link \core_question\statistics\responses\analysis_for_question_all_tries}.
* - There are separate analysis for each variant in this top level instance.
* - Then there are class instances representing the analysis of each of the sub parts of each variant of the question.
* {@link \core_question\statistics\responses\analysis_for_subpart}.
* - Then within the sub part analysis there are response class analysis
* {@link \core_question\statistics\responses\analysis_for_class}.
* - Then within each class analysis there are analysis for each actual response
* {@link \core_question\statistics\responses\analysis_for_actual_response}.
*
* @package core_question
* @copyright 2014 The Open University
* @author James Pratt me@jamiep.org
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class analysis_for_question_all_tries extends analysis_for_question{
/**
* Constructor.
*
* @param int $variantno variant number
* @param \array[] $responsepartsforeachtry for question with multiple tries we expect an array with first index being try no
* then second index is subpartid and values are \question_classified_response
*/
public function count_response_parts($variantno, $responsepartsforeachtry) {
foreach ($responsepartsforeachtry as $try => $responseparts) {
foreach ($responseparts as $subpartid => $responsepart) {
$this->get_analysis_for_subpart($variantno, $subpartid)->count_response($responsepart, $try);
}
}
}
public function has_multiple_tries_data() {
return true;
}
/**
* What is the highest number of tries at this question?
*
* @return int try number
*/
public function get_maximum_tries() {
$max = 1;
foreach ($this->get_variant_nos() as $variantno) {
foreach ($this->get_subpart_ids($variantno) as $subpartid) {
$max = max($max, $this->get_analysis_for_subpart($variantno, $subpartid)->get_maximum_tries());
}
}
return $max;
}
}

View File

@ -16,24 +16,41 @@
/**
*
* 'Classes' to classify the sub parts of a question response into.
* Data structure to count responses for each of the sub parts of a question.
*
* @package core_question
* @copyright 2013 The Open University
* @copyright 2014 The Open University
* @author James Pratt me@jamiep.org
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_question\statistics\responses;
/**
* Representing the analysis of each of the sub parts of each variant of the question.
*
* - There is a separate data structure for each question or sub question's analysis
* {@link \core_question\statistics\responses\analysis_for_question}
* or {@link \core_question\statistics\responses\analysis_for_question_all_tries}.
* - There are separate analysis for each variant in this top level instance.
* - Then there are class instances representing the analysis of each of the sub parts of each variant of the question.
* {@link \core_question\statistics\responses\analysis_for_subpart}.
* - Then within the sub part analysis there are response class analysis
* {@link \core_question\statistics\responses\analysis_for_class}.
* - Then within each class analysis there are analysis for each actual response
* {@link \core_question\statistics\responses\analysis_for_actual_response}.
*
* @package core_question
* @copyright 2014 The Open University
* @author James Pratt me@jamiep.org
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class analysis_for_subpart {
/**
* Takes an array of possible_responses - ({@link \question_possible_response} objects).
* Or takes an array of {@link \question_possible_response} objects.
* Takes an array of possible_responses as returned from {@link \question_type::get_possible_responses()}.
*
* @param \question_possible_response[] $responseclasses
* @param \question_possible_response[] $responseclasses as returned from {@link \question_type::get_possible_responses()}.
*/
public function __construct(array $responseclasses = null) {
if (is_array($responseclasses)) {
@ -44,7 +61,6 @@ class analysis_for_subpart {
}
/**
*
* @var analysis_for_class[]
*/
protected $responseclasses;
@ -59,6 +75,8 @@ class analysis_for_subpart {
}
/**
* Get the instance of the class handling the analysis of $classid for this sub part.
*
* @param string $classid id for response class.
* @return analysis_for_class
*/
@ -66,40 +84,68 @@ class analysis_for_subpart {
return $this->responseclasses[$classid];
}
/**
* Whether there is more than one response class for responses in this question sub part?
*
* @return bool Are there?
*/
public function has_multiple_response_classes() {
return count($this->responseclasses) > 1;
return count($this->get_response_class_ids()) > 1;
}
/**
* Count a part of a response.
*
* @param \question_classified_response $subpart
* @param int $try the try number or zero if not keeping track of try number
*/
public function count_response($subpart) {
$this->responseclasses[$subpart->responseclassid]->count_response($subpart->response, $subpart->fraction);
public function count_response($subpart, $try = 0) {
$responseanalysisforclass = $this->get_response_class($subpart->responseclassid);
$responseanalysisforclass->count_response($subpart->response, $subpart->fraction, $try);
}
/**
* @param \qubaid_condition $qubaids
* @param int $questionid the question id
* @param int $variantno
* @param string $subpartid
* Cache analysis for sub part.
*
* @param \qubaid_condition $qubaids which question usages have been analysed.
* @param string $whichtries which tries have been analysed?
* @param int $questionid which question.
* @param int $variantno which variant.
* @param string $subpartid which sub part.
*/
public function cache($qubaids, $questionid, $variantno, $subpartid) {
foreach ($this->responseclasses as $responseclassid => $responseclass) {
$responseclass->cache($qubaids, $questionid, $variantno, $subpartid, $responseclassid);
public function cache($qubaids, $whichtries, $questionid, $variantno, $subpartid) {
foreach ($this->get_response_class_ids() as $responseclassid) {
$analysisforclass = $this->get_response_class($responseclassid);
$analysisforclass->cache($qubaids, $whichtries, $questionid, $variantno, $subpartid, $responseclassid);
}
}
/**
* Has actual responses different to the model response for this class?
*
* @return bool whether this analysis has a response class with more than one
* different actual response, or if the actual response is different from
* the model response.
*/
public function has_actual_responses() {
foreach ($this->responseclasses as $responseclass) {
if ($responseclass->has_actual_responses()) {
foreach ($this->get_response_class_ids() as $responseclassid) {
if ($this->get_response_class($responseclassid)->has_actual_responses()) {
return true;
}
}
return false;
}
/**
* What is the highest try number for this sub part?
*
* @return int max tries
*/
public function get_maximum_tries() {
$max = 1;
foreach ($this->get_response_class_ids() as $responseclassid) {
$max = max($max, $this->get_response_class($responseclassid)->get_maximum_tries());
}
return $max;
}
}

View File

@ -691,10 +691,11 @@ ORDER BY qa.slot
/**
* Load a {@link question_attempt} from the database, including all its
* steps.
*
* @param int $questionid the question to load all the attempts fors.
* @param qubaid_condition $qubaids used to restrict which usages are included
* in the query. See {@link qubaid_condition}.
* @return array of question_attempts.
* @return question_attempt[] array of question_attempts that were loaded.
*/
public function load_attempts_at_question($questionid, qubaid_condition $qubaids) {
$sql = "

View File

@ -1321,14 +1321,19 @@ class question_attempt {
}
/**
* Break down a student response by sub part and classification.
* See also {@link question_type::get_possible_responses()}
* Break down a student response by sub part and classification. See also {@link question::classify_response}.
* Used for response analysis.
*
* @return question_possible_response[] where keys are subpartid.
* @param string $whichtries which tries to analyse for response analysis. Will be one of
* question_attempt::FIRST_TRY, LAST_TRY or ALL_TRIES.
* Defaults to question_attempt::LAST_TRY.
* @return (question_classified_response|array)[] If $whichtries is question_attempt::FIRST_TRY or LAST_TRY index is subpartid
* and values are question_classified_response instances.
* If $whichtries is question_attempt::ALL_TRIES then first key is submitted response no
* and the second key is subpartid.
*/
public function classify_response() {
return $this->behaviour->classify_response();
public function classify_response($whichtries = self::LAST_TRY) {
return $this->behaviour->classify_response($whichtries);
}
/**
@ -1686,7 +1691,7 @@ class question_attempt_steps_with_submitted_response_iterator extends question_a
}
/** @return bool */
public function valid() {
return $this->offsetExists($this->submittedresponseno);
return $this->submittedresponseno >= 1 && $this->submittedresponseno <= count($this->stepswithsubmittedresponses);
}
/**

View File

@ -32,12 +32,19 @@ defined('MOODLE_INTERNAL') || die();
function question_usage_statistics_cron() {
global $DB;
$expiretime = time() - 5*HOURSECS;
$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));
$responseanlysisids = $DB->get_records_select_menu('question_response_analysis',
'timemodified < ?',
array($expiretime),
'id',
'id, id AS id2');
$DB->delete_records_list('question_response_analysis', 'id', $responseanlysisids);
$DB->delete_records_list('question_response_count', 'analysisid', $responseanlysisids);
mtrace('done.');
return true;

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2014031400.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2014031400.03; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.