item analysis report contributed by Enrique Castro
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
<p align="center"><b>Analysis Options</b></p>
|
||||
|
||||
<p>The range of quiz attempts that are procesed and analyzed can be selected.
|
||||
There are two parameters to specify which attempts are used: </p>
|
||||
|
||||
<p><b>Attempt selection:</b></p>
|
||||
It may be convenient to analyze just one quiz attempt for each user.
|
||||
This particular attempt may be the one with the highest overall score,
|
||||
the first attempt or the last attempt of those performed. Or all attempts data may
|
||||
be collected for a cummulative analysis.</p>
|
||||
|
||||
<p><b>Rejection of low scores:</b></p>
|
||||
<p>Sometimes the users are just browsing the quiz, exploring it,
|
||||
and not going through it for a grade. It is common that such 'trial' attempts
|
||||
get very low scores. These byassing data can be excluden from analysis by
|
||||
setting a low limit for the score of the attempts to analyze. This limit is
|
||||
specified as a percentage (0-100) of the maximum grade achievable in the quiz.</p>
|
||||
@@ -0,0 +1,61 @@
|
||||
<p align="center"><b>Item Anaysis</b></p>
|
||||
|
||||
<p>This table presents processed quiz data in a way suitable for anayzing and judging the
|
||||
performance of each question for the function of assessment. The statistical parameters used are
|
||||
calculated as explained by classical test theory (ref. 1)</p>
|
||||
|
||||
<p><b>Facility Index (% Correct)</b></p>
|
||||
<p>This is a measure of how easy or difficult is a question for quiz-takers.
|
||||
It is calculated as:
|
||||
<br>
|
||||
FI = (X<sub>average</sub>) / X<sub>max</sub>
|
||||
<br>
|
||||
|
||||
where X<sub>average</sub> is the mean credit obtained by all users attempting the item, <br>
|
||||
and X<sub>max</sub> is the maximum credit achievable for that item.<br>
|
||||
If questions can be distributed dicotomically into correct / uncorrect categories,
|
||||
this parameter coincides with the percentage of users that answer the question correctly. </p>
|
||||
|
||||
</p>
|
||||
<p><b>Standard Deviation (SD)</b></p>
|
||||
<p>This parameter measures the spread of answers in the response population. If all users
|
||||
answers the same, then SD=0. SD is calculated as the statistical stadard deviation for the
|
||||
sample of fractional scores (achieved/maximum) at each particular question.
|
||||
</p>
|
||||
|
||||
<p><b>Discrimination Index (DI)</b></p>
|
||||
<p>This provides a rough indicator of the performance of each item to separate proficient
|
||||
<i>vs.</i> less-proficient users. This parameter is calculated by first dividing learners into thirds
|
||||
based on the overall score in the quiz. Then the average score at the analyzed item is calculated for
|
||||
the groups of top and bottom performers, and the average scored substracted. The matematical expression is:
|
||||
<br/>
|
||||
DI = (X<sub>top</sub> - X<sub>bottom</sub>)/ N
|
||||
<br/>
|
||||
|
||||
where X<sub>top</sub> is the sum of the fractional credit (achieved/maximum) obtained at this item by the 1/3 of users having tha highest
|
||||
grades in the whole quiz (i.e. number of correct responses in this group), <br/>
|
||||
and X<sub>bottom</sub>) is the analog sum for users with the lower 1/3 grades for the whole quiz.</p>
|
||||
|
||||
<p>This parameter can take values between +1 and -1.
|
||||
If the index goes below 0.0 it means that more of the weaker learners got the item right than the stronger learners.
|
||||
Such items should be discarded as worthless. In fact, they reduce the accuracy of the overall score for the quiz. </p>
|
||||
|
||||
<p><b>Discrimination Coefficient (DC)</b></p>
|
||||
<p>This is another measure of the separating power of the item to distinguish proficient from weak learners.<br/>
|
||||
The discrimination coefficient is a correlation coefficient between scores at the item and at the whole quiz. Here it is calculated as:
|
||||
|
||||
<br/>
|
||||
DC = Sum(xy)/ (N * s<sub>x</sub> * s<sub>y</sub>)
|
||||
<br/>
|
||||
where Sun(xy) is the sum of the products of deviations for item scores and overall quiz scores, <br>
|
||||
N is the number of responses given to this question<br/>
|
||||
s<sub>x</sub> is the standard deviation of fractional scores for this question and, <br/>
|
||||
s<sub>y</sub> is the standard deviation of scores at the quiz as a whole.
|
||||
</p>
|
||||
<p>Again, this parameter can take values between +1 and -1. Positive values indicate items that
|
||||
do discriminate proficient learners, whereas negative indices mark items that are answered best by
|
||||
those with lowest grades. Items with negative DC are answered incorrectly by the seasoned learners
|
||||
and thus they are actually a penalty against the most proficient learners. Those items should be avoided.<br/>
|
||||
The advantage of Discrimination Coefficient vs. Discrimitation Index is that the former uses information
|
||||
from the whole population of learners, not just the extreme upper and lower thirds. Thus, this parameter may be
|
||||
more sensitive to detect item performance.</p>
|
||||
@@ -77,6 +77,7 @@ $string['penaltyfactor'] = 'Penalty factor';
|
||||
$string['penaltyscheme'] = 'Apply penalties';
|
||||
$string['previewquiz'] = 'Preview quiz';
|
||||
$string['previous'] = 'Previous state';
|
||||
$string['questiontype'] = 'Question type $a';
|
||||
$string['readytosend'] = 'You are about to finalize this quiz attempt. Are you sure you want to continue?';
|
||||
$string['rendering'] = 'Rendering';
|
||||
$string['renderingserver'] = 'Rendering Server';
|
||||
@@ -115,4 +116,10 @@ $string['validate'] = 'Validate';
|
||||
$string['withselected'] = 'With selected';
|
||||
$string['wronguse'] = 'You can not use this page like that';
|
||||
|
||||
$string['analysisoptions'] = 'Analysis options';
|
||||
$string['attemptselection'] = 'Select which attempts to analyze per user ';
|
||||
$string['lowmarkslimit'] = " Don't analyze if score is less than: ";
|
||||
$string['reportanalysis'] = 'Item analysis';
|
||||
$string['allattempts'] = 'all attempts';
|
||||
$string['analysistitle'] = 'Item Analysis Table';
|
||||
?>
|
||||
|
||||
+133
-9
@@ -399,6 +399,71 @@ class quiz_default_questiontype {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of values with the texts for all possible responses stored
|
||||
* for the question
|
||||
*
|
||||
* All answers are found and their text values isolated
|
||||
* @return array An array of values giving the responses corresponding
|
||||
* to all answers to the question. Answer ids are used as keys
|
||||
* @param object $question The question for which the answers are to
|
||||
* be retrieved. Question type specific information is
|
||||
* available.
|
||||
*/
|
||||
// ULPGC ecastro
|
||||
function get_all_responses($question, $state) {
|
||||
unset($answers);
|
||||
if (is_array($question->options->answers)) {
|
||||
foreach ($question->options->answers as $aid=>$answer) {
|
||||
unset ($r);
|
||||
$r->answer = $answer->answer;
|
||||
$r->credit = $answer->fraction;
|
||||
$answers[$aid] = $r;
|
||||
}
|
||||
} else {
|
||||
$answers[]="error"; // just for debugging, eliminate
|
||||
}
|
||||
$result->id = $question->id;
|
||||
$result->responses = $answers;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the actual response to the question in a given attempt
|
||||
* for the question
|
||||
*
|
||||
* @return compond object fractiongrade & actual response .
|
||||
*
|
||||
* @param object $question The question for which the correct answer is to
|
||||
* be retrieved. Question type specific information is
|
||||
* available.
|
||||
* @param object $state The state object that corresponds to the question,
|
||||
* for which a correct answer is needed. Question
|
||||
* type specific information is included.
|
||||
*/
|
||||
// ULPGC ecastro
|
||||
function get_actual_response($question, $state) {
|
||||
/* The default implementation only returns the raw ->responses.
|
||||
may be overridden by each type*/
|
||||
//unset($resp);
|
||||
if (isset($state->responses)) {
|
||||
return $state->responses;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ULPGC ecastro
|
||||
function get_fractional_grade($question, $state) {
|
||||
$maxgrade = $question->maxgrade;
|
||||
$grade = $state->grade;
|
||||
if ($maxgrade) {
|
||||
return (float)($grade/$maxgrade);
|
||||
} else {
|
||||
return (float)$grade;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the question including the number, grading details, content,
|
||||
* feedback and interactions
|
||||
@@ -1711,25 +1776,44 @@ function quiz_print_correctanswer($text) {
|
||||
* @param object $question The question object for which the icon is required
|
||||
* @param boolean $editlink If true then the icon is a link to the question
|
||||
* edit page.
|
||||
* @param boolean $return If true the functions returns the link as a string
|
||||
*/
|
||||
function quiz_print_question_icon($question, $editlink=true) {
|
||||
// Prints a question icon
|
||||
function quiz_print_question_icon($question, $editlink=true, $return = false) {
|
||||
// returns a question icon
|
||||
|
||||
global $QUIZ_QUESTION_TYPE;
|
||||
global $QUIZ_QTYPES;
|
||||
|
||||
$html = '<img border="0" height="16" width="16" src="questiontypes/'.
|
||||
$QUIZ_QTYPES[$question->qtype]->name().'/icon.gif" alt="'.
|
||||
get_string($QUIZ_QTYPES[$question->qtype]->name(), 'quiz').'" />';
|
||||
|
||||
if ($editlink) {
|
||||
echo "<a href=\"question.php?id=$question->id\" title=\""
|
||||
.$QUIZ_QTYPES[$question->qtype]->name()."\">";
|
||||
$html = "<a href=\"question.php?id=$question->id\" title=\""
|
||||
.$QUIZ_QTYPES[$question->qtype]->name()."\">".
|
||||
$html."</a>\n";
|
||||
}
|
||||
echo '<img border="0" height="16" width="16" src="questiontypes/';
|
||||
echo $QUIZ_QTYPES[$question->qtype]->name().'/icon.gif" alt="';
|
||||
echo get_string($QUIZ_QTYPES[$question->qtype]->name(), 'quiz').'" />';
|
||||
if ($editlink) {
|
||||
echo "</a>\n";
|
||||
if ($return) {
|
||||
return $html;
|
||||
} else {
|
||||
echo $html;
|
||||
}
|
||||
}
|
||||
|
||||
// ULPGc ecastro
|
||||
function quiz_get_question_review($quiz, $question) {
|
||||
// returns a question icon
|
||||
$qnum = $question->id;
|
||||
$context = $quiz->id ? '&contextquiz='.$quiz->id : '';
|
||||
$quiz_id = $quiz->id ? '&quizid=' . $quiz->id : '';
|
||||
return "<a title=\"$strpreview\" href=\"javascript:void();\" onClick=\"openpopup('/mod/quiz/preview.php?id=$qnum$quiz_id','$strpreview','scrollbars=yes,resizable=yes,width=700,height=480', false)\">
|
||||
<img src=\"../../pix/t/preview.gif\" border=\"0\" alt=\"$strpreview\" /></a>";
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Print the question image if there is one
|
||||
*
|
||||
@@ -1891,6 +1975,46 @@ function quiz_print_quiz_question(&$question, &$state, $number, $quiz, $options=
|
||||
$quiz, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all teacher stored answers for a given question
|
||||
*
|
||||
* Simply calls the question type specific get_all_responses() method.
|
||||
*/
|
||||
// ULPGC ecastro
|
||||
function quiz_get_question_responses($question, $state) {
|
||||
global $QUIZ_QTYPES;
|
||||
$r = $QUIZ_QTYPES[$question->qtype]->get_all_responses($question, $state);
|
||||
return $r;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the response given by the user in a particular attempt
|
||||
*
|
||||
* Simply calls the question type specific get_actual_response() method.
|
||||
*/
|
||||
// ULPGC ecastro
|
||||
function quiz_get_question_actual_response($question, $state) {
|
||||
global $QUIZ_QTYPES;
|
||||
|
||||
$r = $QUIZ_QTYPES[$question->qtype]->get_actual_response($question, $state);
|
||||
return $r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the response given by the user in a particular attempt
|
||||
*
|
||||
* Simply calls the question type specific get_actual_response() method.
|
||||
*/
|
||||
// ULPGc ecastro
|
||||
function quiz_get_question_fraction_grade($question, $state) {
|
||||
global $QUIZ_QTYPES;
|
||||
|
||||
$r = $QUIZ_QTYPES[$question->qtype]->get_fractional_grade($question, $state);
|
||||
return $r;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets the default category in a course
|
||||
*
|
||||
|
||||
@@ -291,6 +291,41 @@ class quiz_match_qtype extends quiz_default_questiontype {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ULPGC ecastro for stats report
|
||||
function get_all_responses($question, $state) {
|
||||
unset($answers);
|
||||
if (is_array($question->options->subquestions)) {
|
||||
foreach ($question->options->subquestions as $aid=>$answer) {
|
||||
unset ($r);
|
||||
$r->answer = $answer->questiontext." : ".$answer->answertext;
|
||||
$r->credit = 1;
|
||||
$answers[$aid] = $r;
|
||||
}
|
||||
} else {
|
||||
$answers[]="error"; // just for debugging, eliminate
|
||||
}
|
||||
$result->id = $question->id;
|
||||
$result->responses = $answers;
|
||||
return $result;
|
||||
}
|
||||
|
||||
// ULPGC ecastro
|
||||
function get_actual_response($question, $state) {
|
||||
unset($results);
|
||||
if (isset($state->responses)) {
|
||||
foreach($state->responses as $left=>$right){
|
||||
$lpair = $question->options->subquestions[$left]->questiontext;
|
||||
$rpair = $question->options->subquestions[$right]->answertext;
|
||||
$results[$left] = $lpair." : ".$rpair;
|
||||
}
|
||||
return $results;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
//// END OF CLASS ////
|
||||
|
||||
|
||||
@@ -371,6 +371,28 @@ class quiz_multichoice_qtype extends quiz_default_questiontype {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ULPGC ecastro
|
||||
function get_actual_response($question, $state) {
|
||||
$answers = $question->options->answers;
|
||||
/*
|
||||
if (!$question->options->single) {
|
||||
echo "Objeto State-responses: <br>tamaño= ".count($state->responses)."<br> ";
|
||||
print_object($state->responses);
|
||||
foreach ($state->responses as $resp){
|
||||
echo "esto es response id= ".$rid." <br>";
|
||||
print_object($resp);
|
||||
echo " Tamaño interno=".count($resp);
|
||||
foreach ($resp as $r){
|
||||
print_object($r);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
foreach ($state->responses as $rid){
|
||||
$answer = $answers[$rid]->answer;
|
||||
$responses[] = $answer;
|
||||
}
|
||||
return $responses;
|
||||
}
|
||||
}
|
||||
//// END OF CLASS ////
|
||||
|
||||
|
||||
@@ -163,7 +163,24 @@ class quiz_random_qtype extends quiz_default_questiontype {
|
||||
->get_correct_responses($wrappedquestion, $state);
|
||||
}
|
||||
|
||||
function print_question(&$question, &$state, $number, $quiz, $options) {
|
||||
// ULPGC ecastro
|
||||
function get_all_responses($question, $state){
|
||||
global $QUIZ_QTYPES;
|
||||
$wrappedquestion = &$state->options->question;
|
||||
return $QUIZ_QTYPES[$wrappedquestion->qtype]
|
||||
->get_all_responses($wrappedquestion, $state);
|
||||
}
|
||||
|
||||
// ULPGC ecastro
|
||||
function get_actual_response($question, $state){
|
||||
global $QUIZ_QTYPES;
|
||||
$wrappedquestion = &$state->options->question;
|
||||
return $QUIZ_QTYPES[$wrappedquestion->qtype]
|
||||
->get_actual_response($wrappedquestion, $state);
|
||||
}
|
||||
|
||||
|
||||
function print_question(&$question, &$state, &$number, $quiz, $options) {
|
||||
global $QUIZ_QTYPES;
|
||||
$wrappedquestion = &$state->options->question;
|
||||
$QUIZ_QTYPES[$wrappedquestion->qtype]
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
<?php // $Id$
|
||||
|
||||
/// Item analysis displays a table of quiz questions and their performance
|
||||
|
||||
require_once("locallib.php");
|
||||
require_once($CFG->libdir.'/tablelib.php');
|
||||
|
||||
|
||||
function stats_sumx($sum, $data){
|
||||
$sum[0] += $data[0];
|
||||
$sum[1] += $data[1];
|
||||
return $sum;
|
||||
}
|
||||
|
||||
function stats_sumx2($sum, $data){
|
||||
$sum[0] += $data[0]*$data[0];
|
||||
$sum[1] += $data[1]*$data[1];
|
||||
return $sum;
|
||||
}
|
||||
|
||||
function stats_sumxy($sum, $data){
|
||||
$sum[0] += $data[0]*$data[1];
|
||||
return $sum;
|
||||
}
|
||||
|
||||
|
||||
/// Item analysis displays a table of quiz questions and their performance
|
||||
|
||||
class quiz_report extends quiz_default_report {
|
||||
|
||||
function display($quiz, $cm, $course) { /// This function just displays the report
|
||||
|
||||
global $CFG, $SESSION, $db, $QUIZ_QTYPES;
|
||||
define(QUIZ_ALLATTEMPTS, 0);
|
||||
define(QUIZ_HIGHESTATTEMPT, 1);
|
||||
define(QUIZ_FIRSTATTEMPT, 2);
|
||||
define(QUIZ_LASTATTEMPT, 3);
|
||||
|
||||
$strnoquiz = get_string('noquiz','quiz');
|
||||
$strnoattempts = get_string('noattempts','quiz');
|
||||
$strtimeformat = get_string('strftimedatetime');
|
||||
|
||||
if (!$quiz->questions) {
|
||||
print_heading($strnoattempts);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Check to see if groups are being used in this quiz
|
||||
if ($groupmode = groupmode($course, $cm)) { // Groups are being used
|
||||
$currentgroup = setup_and_print_groups($course, $groupmode, "report.php?id=$cm->id&mode=overview");
|
||||
} else {
|
||||
$currentgroup = false;
|
||||
}
|
||||
|
||||
/// Get all users: teachers and students
|
||||
if ($currentgroup) {
|
||||
$users = get_group_users($currentgroup);
|
||||
}
|
||||
else {
|
||||
$users = get_course_users($course->id);
|
||||
}
|
||||
|
||||
/// Remove teachers
|
||||
$teachers = get_course_teachers($course->id);
|
||||
if(!empty($teachers)) {
|
||||
$keys = array_keys($teachers);
|
||||
}
|
||||
foreach($keys as $key) {
|
||||
unset($users[$key]);
|
||||
}
|
||||
|
||||
if(empty($users)) {
|
||||
print_heading($strnoattempts);
|
||||
return true;
|
||||
}
|
||||
|
||||
// set tTable and Analysis stats options
|
||||
if(!isset($SESSION->quiz_analysis_table)) {
|
||||
$SESSION->quiz_analysis_table = array('attemptselection' => 0, 'lowmarklimit' => 0);
|
||||
}
|
||||
|
||||
foreach($SESSION->quiz_analysis_table as $option => $value) {
|
||||
$urlparam = optional_param($option, NULL);
|
||||
if($urlparam === NULL) {
|
||||
$$option = $value;
|
||||
}
|
||||
else {
|
||||
$$option = $SESSION->quiz_analysis_table[$option] = $urlparam;
|
||||
}
|
||||
}
|
||||
|
||||
$scorelimit = $quiz->grade * $lowmarklimit/ 100;
|
||||
|
||||
// ULPGC ecastro DEBUG this is here to allow for differnt SQL to select attempts
|
||||
switch ($attemptselection) {
|
||||
case QUIZ_ALLATTEMPTS :
|
||||
$limit = '';
|
||||
$group = '';
|
||||
break;
|
||||
case QUIZ_HIGHESTATTEMPT :
|
||||
$limit = ', max(qa.sumgrades) ';
|
||||
$group = ' GROUP BY qa.userid ';
|
||||
break;
|
||||
case QUIZ_FIRSTATTEMPT :
|
||||
$limit = ', min(qa.timemodified) ';
|
||||
$group = ' GROUP BY qa.userid ';
|
||||
break;
|
||||
case QUIZ_LASTATTEMPT :
|
||||
$limit = ', max(qa.timemodified) ';
|
||||
$group = ' GROUP BY qa.userid ';
|
||||
break;
|
||||
}
|
||||
|
||||
$select = 'SELECT qa.* '.$limit;
|
||||
$sql = 'FROM '.$CFG->prefix.'user u '.
|
||||
'LEFT JOIN '.$CFG->prefix.'quiz_attempts qa ON u.id = qa.userid '.
|
||||
'LEFT JOIN '.$CFG->prefix.'quiz_states qr ON qr.attempt = qa.id '. // es posible
|
||||
'WHERE u.id IN ('.implode(',', array_keys($users)).') AND ( qa.quiz = '.$quiz->id.') '. // ULPGC ecastro
|
||||
' AND ( qa.sumgrades >= '.$scorelimit.' ) ';
|
||||
// ^^^^^^ es posible seleccionar aquí TODOS los quizzes, como quiere Jussi,
|
||||
// pero habría que llevar la cuenta ed cada quiz para restaura las preguntas (quizquestions, states)
|
||||
//$options = ' AND qa.sumgrades > '.$SESSION->quiz_analysis_table->minscore.' ';
|
||||
/// Fetch the attempts
|
||||
$attempts = get_records_sql($select.$sql.$group);
|
||||
|
||||
if(empty($attempts)) {
|
||||
print_heading($strnoattempts);
|
||||
$this->print_options_form($quiz, $cm, $attemptselection, $lowmarklimit);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Here we rewiew all attempts and record data to construct the table
|
||||
unset($questions);
|
||||
unset($statstable);
|
||||
foreach ($attempts as $attempt) {
|
||||
//print_object($attempt); // ULPGC ecastro debug eliminar
|
||||
// print "<br/>Layout= ".$attempt->layout." <br/>";
|
||||
$questionlist = quiz_questions_in_quiz($attempt->layout);
|
||||
$sql = "SELECT q.*, i.grade AS maxgrade, i.id AS instance".
|
||||
" FROM {$CFG->prefix}quiz_questions q,".
|
||||
" {$CFG->prefix}quiz_question_instances i".
|
||||
" WHERE i.quiz = '$quiz->id' AND q.id = i.question".
|
||||
" AND q.id IN ($questionlist)";
|
||||
|
||||
if (!$quizquestions = get_records_sql($sql)) {
|
||||
error('No questions found');
|
||||
}
|
||||
|
||||
// Load the question type specific information
|
||||
if (!quiz_get_question_options($quizquestions)) {
|
||||
error('Could not load question options');
|
||||
}
|
||||
|
||||
// Restore the question sessions to their most recent states
|
||||
// creating new sessions where required
|
||||
if (!$states = quiz_restore_question_sessions($quizquestions, $quiz, $attempt)) {
|
||||
error('Could not restore question sessions');
|
||||
}
|
||||
$numbers = explode(',', $questionlist);
|
||||
unset($statsrow);
|
||||
foreach ($numbers as $i) {
|
||||
$accepted = array(SHORTANSWER, TRUEFALSE, MULTICHOICE, RANDOM, MATCH, NUMERICAL, CALCULATED);
|
||||
if (!in_array ($quizquestions[$i]->qtype, $accepted)){
|
||||
continue;
|
||||
}
|
||||
$q = quiz_get_question_responses($quizquestions[$i], $states[$i]);
|
||||
$qid = $q->id;
|
||||
if (!isset($questions[$qid])) {
|
||||
$questions[$qid]->id = $qid;
|
||||
foreach ($q->responses as $answer => $r) {
|
||||
$r->count = 0;
|
||||
$questions[$qid]->responses[$answer] = $r->answer;
|
||||
$questions[$qid]->counts[$answer] = 0;
|
||||
$questions[$qid]->credits[$answer] = $r->credit;
|
||||
$statsrow[$qid] = 0;
|
||||
}
|
||||
}
|
||||
$responses = quiz_get_question_actual_response($quizquestions[$i], $states[$i]);
|
||||
foreach ($responses as $resp){
|
||||
//echo "resp= ".$resp." <br/>"; //debug
|
||||
//print_object($questions[$qid]->responses);
|
||||
if ($key = array_search($resp, $questions[$qid]->responses)) {
|
||||
$questions[$qid]->counts[$key]++;
|
||||
} else {
|
||||
$questions[$qid]->responses[] = $resp;
|
||||
$questions[$qid]->counts[] = 1;
|
||||
$test->responses[''] = $resp;
|
||||
//echo "Question type= ".
|
||||
if ($QUIZ_QTYPES[$quizquestions[$i]->qtype]->compare_responses($quizquestions[$i], $states[$i], $test)) {
|
||||
$questions[$qid]->credits[] = 1;
|
||||
} else {
|
||||
$questions[$qid]->credits[] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
//echo "terminado el foreach <br/>"; //debug
|
||||
$fraction = quiz_get_question_fraction_grade($quizquestions[$i], $states[$i]);
|
||||
$statsrow[$qid] += $fraction;
|
||||
}
|
||||
$attemptscores[$attempt->id] = $attempt->sumgrades;
|
||||
$statstable[$attempt->id] = $statsrow;
|
||||
} // Statistics Data table built
|
||||
|
||||
$tablecolumns = array('type', 'question', 'responses', 'counts', 'facility', 'sd','discrimination_index', 'discrimination_coeff');
|
||||
$tableheaders = array("Q#", 'Question', ' Answers ', ' Counts ', 'Facility<br/> % Correct ', 'SD',' Disc.<br/>Index', 'Disc.<br/>Coeff.');
|
||||
|
||||
$table = new flexible_table('mod-quiz-report-itemanalysis-report');
|
||||
|
||||
$table->define_columns($tablecolumns);
|
||||
$table->define_headers($tableheaders);
|
||||
$table->define_baseurl($CFG->wwwroot.'/mod/quiz/report.php?q='.$quiz->id.'&mode=analysis');
|
||||
|
||||
$table->sortable(true);
|
||||
$table->collapsible(true);
|
||||
$table->initialbars(true);
|
||||
|
||||
//$table->column_class('number', 'numcol');
|
||||
$table->column_class('type', 'numcol');
|
||||
$table->column_class('facility', 'numcol');
|
||||
$table->column_class('sd', 'numcol');
|
||||
$table->column_class('discrimination_index', 'numcol');
|
||||
$table->column_class('discrimination_coeff', 'numcol');
|
||||
|
||||
$table->set_attribute('cellspacing', '0');
|
||||
//$table->set_attribute('border', '1');
|
||||
$table->set_attribute('id', 'itemanalysis');
|
||||
$table->set_attribute('class', 'generaltable generalbox');
|
||||
//$table->set_attribute('align', 'center');
|
||||
|
||||
// Start working -- this is necessary as soon as the niceties are over
|
||||
$table->setup();
|
||||
|
||||
$top = max($attemptscores);
|
||||
$bottom = min($attemptscores);
|
||||
$gap = ($top - $bottom)/3;
|
||||
$top -=$gap;
|
||||
$bottom +=$gap;
|
||||
|
||||
foreach ($questions as $q){
|
||||
$question = get_record('quiz_questions', 'id', $q->id);
|
||||
|
||||
$qnumber = " (".link_to_popup_window('/mod/quiz/question.php?id='.$q->id,'editquestion', $q->id, 450, 550, get_string('edit'), 'none', true ).") ";
|
||||
$qname = '<div class="qname">'.format_text($question->name." : ", $question->questiontextformat, NULL, $quiz->course).'</div>';
|
||||
$qicon = quiz_print_question_icon($question, false, true);
|
||||
$qreview = quiz_get_question_review($quiz, $question);
|
||||
$qtext = format_text($question->questiontext, $question->questiontextformat, NULL, $quiz->course);
|
||||
|
||||
$qquestion = $qname."<br/>\n".$qtext."\n";
|
||||
|
||||
$qstats = $this->report_question_stats($attemptscores, $statstable, $q->id, $top, $bottom);
|
||||
|
||||
$responses = "";
|
||||
$counts = "";
|
||||
foreach ($q->responses as $aid=>$resp){
|
||||
$credit = " (".format_float($q->credits[$aid],2).") ";
|
||||
$text = format_text("$resp"."$credit", FORMAT_MOODLE, NULL, $quiz->course);
|
||||
if ($q->credits[$aid] <= 0) {
|
||||
$qclass = 'uncorrect';
|
||||
} elseif ($q->credits[$aid] == 1) {
|
||||
$qclass = 'correct';
|
||||
} else {
|
||||
$qclass = 'partialcorrect';
|
||||
}
|
||||
$responses .= str_replace("<p>", "\n".'<p class="'.$qclass.'">', $text);
|
||||
$count = $q->counts[$aid].'/'.$qstats->count.' ('.format_float($q->counts[$aid]/$qstats->count*100,0).'%) ';
|
||||
$text = format_text("$count", FORMAT_MOODLE, NULL, $quiz->course);
|
||||
$counts .= str_replace("<p>", "\n".'<p class="'.$qclass.'">', $text);
|
||||
}
|
||||
|
||||
$facility = format_float($qstats->facility*100,0)." %";
|
||||
$qsd = format_float($qstats->qsd,3);
|
||||
$di = format_float($qstats->disc_index,2);
|
||||
$dc = format_float($qstats->disc_coeff,2);
|
||||
|
||||
//$table->add_data(array($qnumber."<br/>\n".$qicon."<br>\n ".$qreview, $qquestion, $responses, $counts, $facility, $qsd, $di, $dc));
|
||||
$table->add_data(array($qnumber."\n<p>".$qicon."<p/>\n ".$qreview, $qquestion, $responses, $counts, $facility, $qsd, $di, $dc));
|
||||
}
|
||||
|
||||
echo '<div id="titlecontainer" class="quiz-report-title">';
|
||||
echo get_string("analysistitle", "quiz");
|
||||
helpbutton("itemanalysis", get_string("reportanalysis","quiz"), "quiz");
|
||||
echo '</div>';
|
||||
|
||||
echo '<div id="tablecontainer">';
|
||||
$table->print_html();
|
||||
|
||||
$this->print_options_form($quiz, $cm, $attemptselection, $lowmarklimit);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function print_options_form($quiz, $cm, $attempts, $lowlimit) {
|
||||
echo '<div class="controls">';
|
||||
echo '<form method="report.php">';
|
||||
echo '<p>'.get_string('analysisoptions', 'quiz').': ';
|
||||
echo '<input type="hidden" name="id" value="'.$cm->id.'" />';
|
||||
echo '<input type="hidden" name="q" value="'.$quiz->id.'" />';
|
||||
echo '<input type="hidden" name="mode" value="analysis" />';
|
||||
echo '<label for="attemptselection">'.get_string('attemptselection', 'quiz').'</label>';
|
||||
$options = array ( QUIZ_ALLATTEMPTS => get_string("allattempts", "quiz"),
|
||||
QUIZ_HIGHESTATTEMPT => get_string("gradehighest", "quiz"),
|
||||
QUIZ_FIRSTATTEMPT => get_string("attemptfirst", "quiz"),
|
||||
QUIZ_LASTATTEMPT => get_string("attemptlast", "quiz"));
|
||||
choose_from_menu($options, "attemptselection", "$attempts", "");
|
||||
|
||||
echo '<label for="lowmarklimit">'.get_string('lowmarkslimit', 'quiz').'</label> <input type="text" id="lowmarklimit" name="lowmarklimit" size="1" value="'.$lowlimit.'" /> % ';
|
||||
echo '<input type="submit" value="'.get_string('go').'" />';
|
||||
helpbutton("analysisoptions", get_string("analysisoptions","quiz"), "quiz");
|
||||
echo '</p>';
|
||||
echo '</form>';
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
|
||||
|
||||
function report_question_stats(&$attemptscores, &$questionscores, $qid, $top, $bottom){
|
||||
unset($qstats);
|
||||
$top_scores = $top_count = 0;
|
||||
$bottom_scores = $bottom_count = 0;
|
||||
foreach ($questionscores as $aid => $qrow){
|
||||
if (isset($qrow[$qid])){
|
||||
$qstats[] = array($attemptscores[$aid],$qrow[$qid]);
|
||||
if ($attemptscores[$aid]>=$top){
|
||||
$top_scores +=$qrow[$qid];
|
||||
$top_count++;
|
||||
}
|
||||
if ($attemptscores[$aid]<=$bottom){
|
||||
$bottom_scores +=$qrow[$qid];
|
||||
$bottom_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$n = count($qstats);
|
||||
$sumx = array_reduce($qstats, "stats_sumx");
|
||||
$sumg = $sumx[0];
|
||||
$sumq = $sumx[1];
|
||||
$sumx2 = array_reduce($qstats, "stats_sumx2");
|
||||
$sumg2 = $sumx2[0];
|
||||
$sumq2 = $sumx2[1];
|
||||
$sumxy = array_reduce($qstats, "stats_sumxy");
|
||||
$sumgq = $sumxy[0];
|
||||
|
||||
$result->count = $n;
|
||||
$result->facility = $sumq/$n;
|
||||
if ($n<2) {
|
||||
$result->qsd = sqrt(($sumq2 - $sumq*$sumq/$n)/($n));
|
||||
} else {
|
||||
$result->qsd = sqrt(($sumq2 - $sumq*$sumq/$n)/($n-1));
|
||||
}
|
||||
$result->disc_index = ($top_scores - $bottom_scores)/max($top_count, $bottom_count);
|
||||
$gsd = sqrt(($sumg2 - $sumg*$sumg/$n)/($n-1));
|
||||
$div = $n*$gsd*$result->qsd;
|
||||
if ($div!=0) {
|
||||
$result->disc_coeff = ($sumgq - $sumg*$sumq/$n)/($n*$gsd*$result->qsd);
|
||||
} else {
|
||||
$result->disc_coeff = -999;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
+53
-1
@@ -3,8 +3,15 @@
|
||||
display:block;
|
||||
}
|
||||
|
||||
.quiz-report-title {
|
||||
text-align: center;
|
||||
font-weight : bold;
|
||||
}
|
||||
|
||||
body#mod-quiz-attempts table#attempts,
|
||||
body#mod-quiz-attempts table#commands
|
||||
body#mod-quiz-attempts table#commands,
|
||||
body#mod-quiz-report table#commands,
|
||||
body#mod-quiz-report table#itemanalysis
|
||||
{
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
@@ -63,6 +70,51 @@ body#mod-quiz-attempts table#attempts .r1 {
|
||||
|
||||
|
||||
|
||||
body#mod-quiz-report table#itemanalysis {
|
||||
margin: 20px auto;
|
||||
}
|
||||
body#mod-quiz-report table#itemanalysis .header,
|
||||
body#mod-quiz-report table#itemanalysis .cell
|
||||
{
|
||||
padding: 4px;
|
||||
}
|
||||
body#mod-quiz-report table#itemanalysis .header .commands {
|
||||
display: inline;
|
||||
}
|
||||
body#mod-quiz-report table#itemanalysis td {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
}
|
||||
body#mod-quiz-report table#itemanalysis .header {
|
||||
text-align: left;
|
||||
}
|
||||
body#mod-quiz-report table#itemanalysis .numcol {
|
||||
text-align: center;
|
||||
! vertical-align : middle;
|
||||
}
|
||||
|
||||
body#mod-quiz-report table#itemanalysis .uncorrect {
|
||||
color: red;
|
||||
}
|
||||
|
||||
body#mod-quiz-report table#itemanalysis .correct {
|
||||
color: blue;
|
||||
font-weight : bold;
|
||||
}
|
||||
|
||||
body#mod-quiz-report table#itemanalysis .partialcorrect {
|
||||
!color: green;
|
||||
}
|
||||
|
||||
body#mod-quiz-report table#itemanalysis .qname {
|
||||
!color: green;
|
||||
}
|
||||
|
||||
|
||||
|
||||
body#mod-quiz-report .controls {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.editorhelptext {
|
||||
font-size:0.7em;
|
||||
|
||||
Reference in New Issue
Block a user