From 4f5ffac0225c143c5aab0ea6184fdc39ce79044e Mon Sep 17 00:00:00 2001 From: jamiesensei Date: Fri, 18 Jul 2008 14:36:24 +0000 Subject: [PATCH] MDL-15268 "Content for Quiz Statistics report table" seperated quiz item / position stats code into a seperate class in qstats.php and now calculating item statistics too. --- lang/en_utf8/quiz_statistics.php | 2 + lib/questionlib.php | 15 ++ mod/quiz/report/statistics/qstats.php | 179 ++++++++++++++++++ mod/quiz/report/statistics/report.php | 159 ++-------------- .../report/statistics/statistics_table.php | 32 +++- 5 files changed, 231 insertions(+), 156 deletions(-) create mode 100644 mod/quiz/report/statistics/qstats.php diff --git a/lang/en_utf8/quiz_statistics.php b/lang/en_utf8/quiz_statistics.php index c51bef67a8..901acd9385 100644 --- a/lang/en_utf8/quiz_statistics.php +++ b/lang/en_utf8/quiz_statistics.php @@ -42,4 +42,6 @@ $string['nostudentsingroup'] = 'There are no students in this group yet'; $string['discrimination_index'] = 'Discrimination Index'; $string['discriminative_efficiency'] = 'Discriminative Efficiency'; $string['effective_weight'] = 'Effective weight'; +$string['errorrandom'] = 'Error getting sub item data'; +$string['erroritemappearsmorethanoncewithdifferentweight'] = 'Question ($a) appears more than once with different weights in different positions of the test. This is not currently supported by the statistics report and may make the statistics for this question unreliable.'; ?> \ No newline at end of file diff --git a/lib/questionlib.php b/lib/questionlib.php index 0813ffb77c..88a724c383 100644 --- a/lib/questionlib.php +++ b/lib/questionlib.php @@ -2479,4 +2479,19 @@ function get_filesdir_from_context($context){ } return $courseid; } +/** + * Get the real question id for a random question. + * @param object $state with property answer. + * @return mixed return integer real question id or false if there was an + * error.. + */ +function question_get_real_questionid($state){ + $matches = array(); + if (!preg_match('|^random([0-9]+)-|', $state->answer, $matches)){ + notify(get_string('errorrandom', 'quiz_statistics')); + return false; + } else { + return $matches[1]; + } +} ?> diff --git a/mod/quiz/report/statistics/qstats.php b/mod/quiz/report/statistics/qstats.php new file mode 100644 index 0000000000..4bad4952df --- /dev/null +++ b/mod/quiz/report/statistics/qstats.php @@ -0,0 +1,179 @@ +s = $s; + $this->sumgradesavg = $sumgradesavg; + foreach (array_keys($questions) as $qid){ + $questions[$qid]->_stats = $this->stats_init_object(); + } + $this->questions = $questions; + } + function stats_init_object(){ + $statsinit = new object(); + $statsinit->s = 0; + $statsinit->totalgrades = 0; + $statsinit->totalothergrades = 0; + $statsinit->gradevariancesum = 0; + $statsinit->othergradevariancesum = 0; + $statsinit->covariancesum = 0; + $statsinit->covariancemaxsum = 0; + $statsinit->covariancewithoverallgradesum = 0; + $statsinit->gradearray = array(); + $statsinit->othergradesarray = array(); + $statsinit->subitems = array(); + return $statsinit; + } + function get_records($fromqa, $whereqa, $usingattempts, $qaparams){ + global $DB; + $sql = 'SELECT qs.id, ' . + 'qs.question, ' . + 'qa.sumgrades, ' . + 'qs.grade, ' . + 'qs.answer ' . + 'FROM ' . + '{question_sessions} qns, ' . + '{question_states} qs, '. + $fromqa.' '. + 'WHERE ' .$whereqa. + 'AND qns.attemptid = qa.uniqueid '. + $usingattempts. + 'AND qns.newgraded = qs.id'; + $this->states = $DB->get_records_sql($sql, $qaparams); + if ($this->states === false){ + print_error('errorstatisticsquestions', 'quiz_statistics'); + } + } + + function _initial_states_walker($state, &$stats, $positionstat = true){ + $stats->s++; + $stats->totalgrades += $state->grade; + if ($positionstat){ + $stats->totalothergrades += $state->sumgrades - $state->grade; + } else { + $stats->totalothergrades += $state->sumgrades; + } + //need to sort these to calculate max covariance : + $stats->gradearray[] = $state->grade; + if ($positionstat){ + $stats->othergradesarray[] = $state->sumgrades - $state->grade; + } else { + $stats->othergradesarray[] = $state->sumgrades; + } + } + + function _secondary_states_walker($state, &$stats){ + $gradedifference = ($state->grade - $stats->gradeaverage); + $othergradedifference = (($state->sumgrades - $state->grade) - $stats->othergradeaverage); + $overallgradedifference = $state->sumgrades - $this->sumgradesavg; + $sortedgradedifference = (array_shift($stats->gradearray) - $stats->gradeaverage); + $sortedothergradedifference = (array_shift($stats->othergradesarray) - $stats->othergradeaverage); + $stats->gradevariancesum += pow($gradedifference,2); + $stats->othergradevariancesum += pow($othergradedifference,2); + $stats->covariancesum += $gradedifference * $othergradedifference; + $stats->covariancemaxsum += $sortedgradedifference * $sortedothergradedifference; + $stats->covariancewithoverallgradesum += $gradedifference * $overallgradedifference; + } + + + function _initial_question_walker(&$stats, $grade){ + $stats->gradeaverage = $stats->totalgrades / $stats->s; + $stats->facility = $stats->gradeaverage / $grade; + $stats->othergradeaverage = $stats->totalothergrades / $stats->s; + sort($stats->gradearray, SORT_NUMERIC); + sort($stats->othergradesarray, SORT_NUMERIC); + } + function _secondary_question_walker(&$stats){ + $stats->gradevariance = $stats->gradevariancesum / ($stats->s -1); + $stats->othergradevariance = $stats->othergradevariancesum / ($stats->s -1); + $stats->covariance = $stats->covariancesum / ($stats->s -1); + $stats->covariancemax = $stats->covariancemaxsum / ($stats->s -1); + $stats->covariancewithoverallgrade = $stats->covariancewithoverallgradesum / ($stats->s-1); + $stats->sd = sqrt($stats->gradevariancesum / ($stats->s -1)); + //avoid divide by zero + if ($stats->gradevariance * $stats->othergradevariance){ + $stats->discriminationindex = 100*$stats->covariance + / sqrt($stats->gradevariance * $stats->othergradevariance); + } else { + $stats->discriminationindex = ''; + } + if ($stats->covariancemax){ + $stats->discriminativeefficiency = 100*$stats->covariance / $stats->covariancemax; + } else { + $stats->discriminativeefficiency = ''; + } + } + + function process_states(){ + foreach ($this->states as $state){ + $this->_initial_states_walker($state, $this->questions[$state->question]->_stats); + //if this is a random question what is the real item being used? + if ($this->questions[$state->question]->qtype == 'random'){ + if ($itemid = question_get_real_questionid($state)){ + if (!isset($subquestionstats[$itemid])){ + $subquestionstats[$itemid] = $this->stats_init_object(); + $subquestionstats[$itemid]->usedin = array(); + $subquestionstats[$itemid]->differentweights = false; + $subquestionstats[$itemid]->grade = $this->questions[$state->question]->grade; + } else if ($subquestionstats[$itemid]->grade != $this->questions[$state->question]->grade){ + $subquestionstats[$itemid]->differentweights = true; + } + $this->_initial_states_walker($state, $subquestionstats[$itemid], false); + $subquestionstats[$itemid]->usedin[] = $state->question; + $this->questions[$state->question]->_stats->subitems[] = $itemid; + } + } + } + $this->subquestions = question_load_questions(array_keys($subquestionstats)); + foreach (array_keys($this->subquestions) as $qid){ + $this->subquestions[$qid]->_stats = $subquestionstats[$qid]; + $this->subquestions[$qid]->grade = $this->subquestions[$qid]->_stats->grade; + $this->subquestions[$qid]->subquestion = true; + $this->_initial_question_walker($this->subquestions[$qid]->_stats, $this->subquestions[$qid]->_stats->grade); + if ($subquestionstats[$qid]->differentweights){ + notify(get_string('erroritemappearsmorethanoncewithdifferentweight', 'quiz_statistics', $this->subquestions[$qid]->name)); + } + } + foreach (array_keys($this->questions) as $qid){ + $this->_initial_question_walker($this->questions[$qid]->_stats, $this->questions[$qid]->grade); + $this->questions[$qid]->subquestion = false; + } + //go through the records one more time + foreach ($this->states as $state){ + $this->_secondary_states_walker($state, $this->questions[$state->question]->_stats); + if ($this->questions[$state->question]->qtype == 'random'){ + if ($itemid = question_get_real_questionid($state)){ + $this->_secondary_states_walker($state, $this->subquestions[$itemid]->_stats); + } + } + } + $sumofcovariancewithoverallgrade = 0; + foreach (array_keys($this->questions) as $qid){ + $this->_secondary_question_walker($this->questions[$qid]->_stats); + $this->sumofgradevariance += $this->questions[$qid]->_stats->gradevariance; + $sumofcovariancewithoverallgrade += sqrt($this->questions[$qid]->_stats->covariancewithoverallgrade); + } + foreach (array_keys($this->subquestions) as $qid){ + $this->_secondary_question_walker($this->subquestions[$qid]->_stats); + } + foreach (array_keys($this->questions) as $qid){ + $this->questions[$qid]->_stats->effectiveweight = 100 * sqrt($this->questions[$qid]->_stats->covariancewithoverallgrade) + / $sumofcovariancewithoverallgrade; + } + } + /** + * Needed by quiz stats calculations. + */ + function sum_of_grade_variance(){ + return $this->sumofgradevariance; + } +} +?> \ No newline at end of file diff --git a/mod/quiz/report/statistics/report.php b/mod/quiz/report/statistics/report.php index 9e4c2659c4..4acff501d9 100644 --- a/mod/quiz/report/statistics/report.php +++ b/mod/quiz/report/statistics/report.php @@ -244,155 +244,17 @@ class quiz_statistics_report extends quiz_default_report { } } if ($s){ -/* //CIC, ER and SE. - //http://docs.moodle.org/en/Development:Quiz_item_analysis_calculations_in_practise#CIC.2C_ER_and_SE - list($qsql, $sqlparams) = $DB->get_in_or_equal(array_keys($questions), SQL_PARAMS_NAMED); - $sqlparams += $qaparams;//put quiz id in at beginning of array - $qgradeavgsql = "SELECT qs.question, " . - "AVG(qs.grade) AS gradeaverage " . - "AVG(qa.sumgrades - qs.grade) AS sumgradeaverage " . - "FROM " . - "{question_sessions} qns, " . - "{question_states} qs, " . - $fromqa.' '. - 'WHERE ' .$whereqa. - 'AND qns.attemptid = qa.uniqueid '. - 'AND qs.question '.$qsql.' ' . - $usingattempts->sql. - 'AND qns.newgraded = qs.id GROUP BY qs.question'; - $qgradeavgs = $DB->get_records_sql($qgradeavgsql, $sqlparams); - - $sum = 0; - $sql = 'SELECT COUNT(1) as s,' . - 'SUM(POWER((qs.grade - :mean),2)) AS power2 ' . - 'FROM ' . - '{question_sessions} qns, ' . - '{question_states} qs, '. - $fromqa.' '. - 'WHERE ' .$whereqa. - 'AND qns.attemptid = qa.uniqueid '. - 'AND qs.question = :qid ' . - $usingattempts->sql. - 'AND qns.newgraded = qs.id'; - foreach (array_keys($questions) as $qid){ - $params = array('mean' => $qgradeavgs[$qid], 'qid' => $qid)+ $qaparams; - $fromdb = $DB->get_record_sql($sql, $params); - if ($fromdb === false){ - print_error('errorpowerquestions', 'quiz_statistics'); - } - $questions[$qid]->s = $fromdb->s; - if ($s>1){ - $questions[$qid]->facility = $qgradeavgs[$qid] / $questions[$qid]->grade; - $questions[$qid]->sd = sqrt($fromdb->power2 / ($questions[$qid]->s -1)); - } - $sum += $fromdb->power2; - } - //Discrimination index - $sql = 'SELECT qs.id, ' . - 'qs.question, ' . - 'qa.sumgrades - qs.grade AS sum, ' . - 'qs.grade ' . - 'FROM ' . - '{question_sessions} qns, ' . - '{question_states} qs, '. - $fromqa.' '. - 'WHERE ' .$whereqa. - 'AND qns.attemptid = qa.uniqueid '. - $usingattempts->sql. - 'AND qns.newgraded = qs.id'; - $fromdbrs = $DB->get_recordset_sql($sql, $qaparams); - if ($fromdbrs === false){ - print_error('errorpowerquestions', 'quiz_statistics'); - } - foreach ($fromdbrs as $record){ - - }*/ - $sql = 'SELECT qs.id, ' . - 'qs.question, ' . - 'qa.sumgrades, ' . - 'qs.grade ' . - 'FROM ' . - '{question_sessions} qns, ' . - '{question_states} qs, '. - $fromqa.' '. - 'WHERE ' .$whereqa. - 'AND qns.attemptid = qa.uniqueid '. - $usingattempts->sql. - 'AND qns.newgraded = qs.id'; - $fromdbrs = $DB->get_recordset_sql($sql, $qaparams); - if ($fromdbrs === false){ - print_error('errorstatisticsquestions', 'quiz_statistics'); - } - foreach (array_keys($questions) as $qid){ - $questions[$qid]->s = 0; - $questions[$qid]->totalgrades = 0; - $questions[$qid]->totalothergrades = 0; - $questions[$qid]->gradevariancesum = 0; - $questions[$qid]->othergradevariancesum = 0; - $questions[$qid]->covariancesum = 0; - $questions[$qid]->covariancemaxsum = 0; - $questions[$qid]->covariancewithoverallgradesum = 0; - $questions[$qid]->gradearray = array(); - $questions[$qid]->othergradesarray = array(); - } - - foreach ($fromdbrs as $record){ - $questions[$record->question]->s++; - $questions[$record->question]->totalgrades += $record->grade; - $questions[$record->question]->totalothergrades += $record->sumgrades - $record->grade; - //need to sort these to calculate max covariance : - $questions[$record->question]->gradearray[] = $record->grade; - $questions[$record->question]->othergradesarray[] = $record->sumgrades - $record->grade; - } - foreach (array_keys($questions) as $qid){ - $questions[$qid]->gradeaverage = $questions[$qid]->totalgrades / $s; - $questions[$qid]->facility = $questions[$qid]->gradeaverage / $questions[$qid]->grade; - $questions[$qid]->othergradeaverage = $questions[$qid]->totalothergrades / $s; - sort($questions[$qid]->gradearray, SORT_NUMERIC); - sort($questions[$qid]->othergradesarray, SORT_NUMERIC); - } - //go through the records one more time - foreach ($fromdbrs as $record){ - $gradedifference = ($record->grade - $questions[$record->question]->gradeaverage); - $othergradedifference = (($record->sumgrades - $record->grade) - $questions[$record->question]->othergradeaverage); - $overallgradedifference = $record->sumgrades - $sumgradesavg; - $sortedgradedifference = (array_shift($questions[$qid]->gradearray) - $questions[$record->question]->gradeaverage); - $sortedothergradedifference = (array_shift($questions[$qid]->othergradesarray) - $questions[$record->question]->othergradeaverage); - $questions[$record->question]->gradevariancesum += pow($gradedifference,2); - $questions[$record->question]->othergradevariancesum += pow($othergradedifference,2); - $questions[$record->question]->covariancesum += $gradedifference * $othergradedifference; - $questions[$record->question]->covariancemaxsum += $sortedgradedifference * $sortedothergradedifference; - $questions[$record->question]->covariancewithoverallgradesum += $gradedifference * $overallgradedifference; - } - $sumofcovariancewithoverallgrade = 0; - $sumofgradevariance =0; - foreach (array_keys($questions) as $qid){ - $questions[$qid]->gradevariance = $questions[$qid]->gradevariancesum / ($s -1); - $questions[$qid]->othergradevariance = $questions[$qid]->othergradevariancesum / ($s -1); - $questions[$qid]->covariance = $questions[$qid]->covariancesum / ($s -1); - $questions[$qid]->covariancemax = $questions[$qid]->covariancemaxsum / ($s -1); - $sumofgradevariance += $questions[$qid]->gradevariance; - $questions[$qid]->covariancewithoverallgrade = $questions[$qid]->covariancewithoverallgradesum / ($s-1); - $sumofcovariancewithoverallgrade += sqrt($questions[$qid]->covariancewithoverallgrade); - $questions[$qid]->sd = sqrt($questions[$qid]->gradevariancesum / ($s -1)); - //avoid divide by zero - if (sqrt($questions[$qid]->gradevariance * $questions[$qid]->othergradevariance)){ - $questions[$qid]->discriminationindex = 100*$questions[$qid]->covariance - / sqrt($questions[$qid]->gradevariance * $questions[$qid]->othergradevariance); - } else { - $questions[$qid]->discriminationindex = ''; - } - $questions[$qid]->discriminativeefficiency = 100*$questions[$qid]->covariance / $questions[$qid]->covariancemax; - } - foreach (array_keys($questions) as $qid){ - $questions[$qid]->effectiveweight = 100 * sqrt($questions[$qid]->covariancewithoverallgrade)/$sumofcovariancewithoverallgrade; - } + require_once("$CFG->dirroot/mod/quiz/report/statistics/qstats.php"); + $qstats = new qstats($questions, $s, $sumgradesavg); + $qstats->get_records($fromqa, $whereqa, $usingattempts->sql, $qaparams); + set_time_limit(0); + $qstats->process_states(); } if (!$table->is_downloading()){ if ($s>1){ $p = count($questions);//no of positions if ($p > 1){ - $cic = (100 * $p / ($p -1)) * (1 - ($sumofgradevariance)/$k2); + $cic = (100 * $p / ($p -1)) * (1 - ($qstats->sum_of_grade_variance())/$k2); $quizattsstatistics->data[] = array(get_string('cic', 'quiz_statistics'), number_format($cic, $quiz->decimalpoints).' %'); $errorratio = 100 * sqrt(1-($cic/100)); $quizattsstatistics->data[] = array(get_string('errorratio', 'quiz_statistics'), number_format($errorratio, $quiz->decimalpoints).' %'); @@ -409,8 +271,13 @@ class quiz_statistics_report extends quiz_default_report { print_heading(get_string('quizstructureanalysis', 'quiz_statistics')); } $table->setup($quiz, $cm->id, $reporturl, $s); - foreach ($questions as $question){ - $table->add_data_keyed($table->format_row($question)); + if (isset($qstats)){ + foreach ($qstats->questions as $question){ + $table->add_data_keyed($table->format_row($question)); + foreach ($question->_stats->subitems as $itemid){ + $table->add_data_keyed($table->format_row($qstats->subquestions[$itemid])); + } + } } $table->finish_output(); return true; diff --git a/mod/quiz/report/statistics/statistics_table.php b/mod/quiz/report/statistics/statistics_table.php index ebf425363f..580ce9e031 100644 --- a/mod/quiz/report/statistics/statistics_table.php +++ b/mod/quiz/report/statistics/statistics_table.php @@ -108,7 +108,11 @@ class quiz_report_statistics_table extends flexible_table { } function col_number($question){ - return $question->number; + if (!$question->subquestion){ + return $question->number; + } else { + return ''; + } } function col_actions($question){ return quiz_question_action_icons($this->quiz, $this->cmid, $question, $this->baseurl); @@ -120,17 +124,25 @@ class quiz_report_statistics_table extends flexible_table { return quiz_report_scale_sumgrades_as_percentage($question->grade, $this->quiz); } function col_effective_weight($question){ - return number_format($question->effectiveweight, 2).' %'; + if (!$question->subquestion){ + return number_format($question->_stats->effectiveweight, 2).' %'; + } else { + return ''; + } } function col_discrimination_index($question){ - if (is_numeric($question->discriminationindex)){ - return number_format($question->discriminationindex, 2).' %'; + if (is_numeric($question->_stats->discriminationindex)){ + return number_format($question->_stats->discriminationindex, 2).' %'; } else { - return $question->discriminationindex; + return $question->_stats->discriminationindex; } } function col_discriminative_efficiency($question){ - return number_format($question->discriminativeefficiency, 2).' %'; + if (is_numeric($question->_stats->discriminativeefficiency)){ + return number_format($question->_stats->discriminativeefficiency, 2).' %'; + } else { + return ''; + } } function col_random_guess_score($question){ $randomguessscore = question_get_random_guess_score($question); @@ -142,17 +154,17 @@ class quiz_report_statistics_table extends flexible_table { } function col_sd($question){ - return number_format($question->sd*100 / $question->grade, 2).' %'; + return number_format($question->_stats->sd*100 / $question->grade, 2).' %'; } function col_s($question){ - if (isset($question->s)){ - return $question->s; + if (isset($question->_stats->s)){ + return $question->_stats->s; } else { return 0; } } function col_facility($question){ - return number_format($question->facility*100, 2).' %'; + return number_format($question->_stats->facility*100, 2).' %'; } } ?> -- 2.39.5