report.php 18 KB
Newer Older
1
2
<?php
/**
3
 * This script calculates various statistics about student attempts
4
5
 *
 * @version $Id$
6
 * @author Martin Dougiamas, Jamie Pratt, Tim Hunt and others.
7
8
 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
 * @package quiz
9
 **/
10

11
define('QUIZ_REPORT_TIME_TO_CACHE_STATS', MINSECS * 15);
12
13
14
require_once($CFG->dirroot.'/mod/quiz/report/statistics/statistics_form.php');
require_once($CFG->dirroot.'/mod/quiz/report/statistics/statistics_table.php');

15
class quiz_statistics_report extends quiz_default_report {
16
17
18
19
20
21
22
23
24
25

    /**
     * Display the report.
     */
    function display($quiz, $cm, $course) {
        global $CFG, $DB;

        $context = get_context_instance(CONTEXT_MODULE, $cm->id);

        $download = optional_param('download', '', PARAM_ALPHA);
26
        $recalculate = optional_param('recalculate', 0, PARAM_BOOL);
27
28
29
30
        $pageoptions = array();
        $pageoptions['id'] = $cm->id;
        $pageoptions['q'] = $quiz->id;
        $pageoptions['mode'] = 'statistics';
31
32
33
34
35
36
        
        $questions = quiz_report_load_questions($quiz);
        // Load the question type specific information
        if (!get_question_options($questions)) {
            print_error('cannotloadquestion', 'question');
        }
37

38
        
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
        $reporturl = new moodle_url($CFG->wwwroot.'/mod/quiz/report.php', $pageoptions);

        $mform = new mod_quiz_report_statistics($reporturl);
        if ($fromform = $mform->get_data()){
            $useallattempts = $fromform->useallattempts;
            if ($fromform->useallattempts){
                set_user_preference('quiz_report_statistics_useallattempts', $fromform->useallattempts);
            } else {
                unset_user_preference('quiz_report_statistics_useallattempts');
            }
        } else {
            $useallattempts = get_user_preferences('quiz_report_statistics_useallattempts', 0);
        }

        /// find out current groups mode
        $currentgroup = groups_get_activity_group($cm, true);

56
        $nostudentsingroup = false;//true if a group is selected and their is noeone in it.
57
58
        if (!empty($currentgroup)) {
            // all users who can attempt quizzes and who are in the currently selected group
59
60
61
            $groupstudents = get_users_by_capability($context, 'mod/quiz:attempt','','','','',$currentgroup,'',false);
            if (!$groupstudents){
                $nostudentsingroup = true;
62
            }
63
64
        } else {
            $groupstudents = array();
65
66
        }

67
        
68
        $table = new quiz_report_statistics_table();
69
70
71
72
73
74
75
76
77
78
        $table->is_downloading($download, get_string('reportstatistics','quiz_statistics'),
                    "$course->shortname ".format_string($quiz->name,true));
        if (!$table->is_downloading()) {
            // Only print headers if not asked to download data
            $this->print_header_and_tabs($cm, $course, $quiz, "statistics");
        }

        if ($groupmode = groups_get_activity_groupmode($cm)) {   // Groups are being used
            if (!$table->is_downloading()) {
                groups_print_activity_menu($cm, $reporturl->out());
79
80
81
                if ($currentgroup && !$groupstudents){
                    notify(get_string('nostudentsingroup', 'quiz_statistics'));
                }
82
83
84
            }
        }

85
86
87
88
89
        if (!$table->is_downloading()) {
            // Print display options
            $mform->set_data(array('useallattempts' => $useallattempts));
            $mform->display();
        }
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
        // Print information on the number of existing attempts
        if (!$table->is_downloading()) { //do not print notices when downloading
            print_heading(get_string('quizinformation', 'quiz_statistics'));
            $quizinformationtable = new object();
            $quizinformationtable->align = array('center', 'center');
            $quizinformationtable->width = '60%';
            $quizinformationtable->class = 'generaltable titlesleft';
            $quizinformationtable->data = array();
            $quizinformationtable->data[] = array(get_string('quizname', 'quiz_statistics'), $quiz->name);
            $quizinformationtable->data[] = array(get_string('coursename', 'quiz_statistics'), $course->fullname);
            if ($cm->idnumber){
                $quizinformationtable->data[] = array(get_string('coursename', 'quiz_statistics'), $cm->idnumber);
            }
            if ($quiz->timeopen){
                $quizinformationtable->data[] = array(get_string('quizopen', 'quiz'), userdate($quiz->timeopen));
            }
            if ($quiz->timeclose){
                $quizinformationtable->data[] = array(get_string('quizclose', 'quiz'), userdate($quiz->timeclose));
            }
            if ($quiz->timeopen && $quiz->timeclose){
110
                $quizinformationtable->data[] = array(get_string('duration', 'quiz_statistics'), format_time($quiz->timeclose - $quiz->timeopen));
111
112
            }
        }
113
114
115

        $timemodified = time() - QUIZ_REPORT_TIME_TO_CACHE_STATS;
        $params = array('quizid'=>$quiz->id, 'groupid'=>$currentgroup, 'allattempts'=>$useallattempts, 'timemodified'=>$timemodified);
116
        if ($recalculate || !$quizstats = $DB->get_record_select('quiz_statistics', 'quizid = :quizid  AND groupid = :groupid AND allattempts = :allattempts AND timemodified > :timemodified', $params, '*', true)){
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
            list($s, $usingattemptsstring, $quizstats, $qstats) = $this->quiz_stats($nostudentsingroup, $quiz->id, $currentgroup, $groupstudents, $questions, $useallattempts);
            $toinsert = (object)((array)$quizstats + $params);
            $toinsert->timemodified = time();
            $quizstatisticsid = $DB->insert_record('quiz_statistics', $toinsert);
            foreach ($qstats->questions as $question){
                $question->_stats->quizstatisticsid = $quizstatisticsid;
                $DB->insert_record('quiz_question_statistics', $question->_stats, false, true);
            }
            foreach ($qstats->subquestions as $subquestion){
                $subquestion->_stats->quizstatisticsid = $quizstatisticsid;
                $DB->insert_record('quiz_question_statistics', $subquestion->_stats, false, true);
            }
            if (isset($qstats)){
                $questions = $qstats->questions;
                $subquestions = $qstats->subquestions;
            } else {
                $questions = array();
                $subquestions = array();
            }
        } else {
            if ($useallattempts){
                $usingattemptsstring = get_string('allattempts', 'quiz_statistics');
                $s = $quizstats->allattemptscount;
            } else {
                $usingattemptsstring = get_string('firstattempts', 'quiz_statistics');
                $s = $quizstats->firstattemptscount;
            }
            $questionstats = $DB->get_records('quiz_question_statistics', array('quizstatisticsid'=>$quizstats->id), 'subquestion ASC');
            $questionstats = quiz_report_index_by_keys($questionstats, array('subquestion', 'questionid'));
            if (1 < count($questionstats)){
                list($mainquestionstats, $subquestionstats) = $questionstats;
                $subqstofetch = array_keys($subquestionstats);
                $subquestions = question_load_questions($subqstofetch);
                foreach (array_keys($subquestions) as $subqid){
                    $subquestions[$subqid]->_stats = $subquestionstats[$subqid];
                }
            } else {
                $mainquestionstats = $questionstats[0];
                $subquestions = array();
            }
            foreach (array_keys($questions) as $qid){
                $questions[$qid]->_stats = $mainquestionstats[$qid];
            }
160
        }
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
        if (!$table->is_downloading()){
            if ($s==0){
                print_heading(get_string('noattempts','quiz'));
            }
            $format = array('firstattemptscount' => '',
                        'allattemptscount' => '',
                        'firstattemptsavg' => 'sumgrades_as_percentage',
                        'allattemptsavg' => 'sumgrades_as_percentage',
                        'median' => 'sumgrades_as_percentage',
                        'standarddeviation' => 'sumgrades_as_percentage',
                        'skewness' => '',
                        'kurtosis' => '',
                        'cic' => 'number_format',
                        'errorratio' => 'number_format',
                        'standarderror' => 'sumgrades_as_percentage');
            foreach ($quizstats as $property => $value){
                if (!isset($format[$property])){
                    continue;
                }
                switch ($format[$property]){
                    case 'sumgrades_as_percentage' :
                        $formattedvalue = quiz_report_scale_sumgrades_as_percentage($value, $quiz);
                        break;
                    case 'number_format' :
                        $formattedvalue = number_format($value, $quiz->decimalpoints).' %';
                        break;
                    default :
                        $formattedvalue = $value;
                }
                $quizinformationtable->data[] = array(get_string($property, 'quiz_statistics', $usingattemptsstring), $formattedvalue);
            }
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
            if (isset($quizstats->timemodified)){
                list($fromqa, $whereqa, $qaparams) = quiz_report_attempts_sql($quiz->id, $currentgroup, $groupstudents, $useallattempts);
                $sql = 'SELECT COUNT(1) ' .
                    'FROM ' .$fromqa.' '.
                    'WHERE ' .$whereqa.' AND qa.timefinish > :time';
                $a = new object();
                $a->lastcalculated = format_time(time() - $quizstats->timemodified);
                if (!$a->count = $DB->count_records_sql($sql, array('time'=>$quizstats->timemodified)+$qaparams)){
                    $a->count = 0;
                } 
                print_box_start('boxaligncenter generalbox boxwidthnormal mdl-align');
                echo get_string('lastcalculated', 'quiz_statistics', $a);
                print_single_button($reporturl->out(true), $reporturl->params()+array('recalculate'=>1),
                                    get_string('recalculatenow', 'quiz_statistics'), 'post');
                print_box_end();
            }
208
209
            print_table($quizinformationtable);
            
210
        }
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
        if (!$table->is_downloading()){
            print_heading(get_string('quizstructureanalysis', 'quiz_statistics'));
        }
        if ($s){
            $table->setup($quiz, $cm->id, $reporturl, $s);
            
            foreach ($questions as $question){
                $table->add_data_keyed($table->format_row($question));
                if (!empty($question->_stats->subquestions)){
                    $subitemstodisplay = explode(',', $question->_stats->subquestions);
                    foreach ($subitemstodisplay as $subitemid){
                        $subquestions[$subitemid]->maxgrade = $question->maxgrade;
                        $table->add_data_keyed($table->format_row($subquestions[$subitemid]));
                    }
                }
            }
227

228
229
230
231
232
233
234
235
            $table->finish_output();
        }
        return true;
    }
    
    
    function quiz_stats($nostudentsingroup, $quizid, $currentgroup, $groupstudents, $questions, $useallattempts){
        global $CFG, $DB;
236
237
238
        if (!$nostudentsingroup){
            //Calculating_MEAN_of_grades_for_all_attempts_by_students
            //http://docs.moodle.org/en/Development:Quiz_item_analysis_calculations_in_practise#Calculating_MEAN_of_grades_for_all_attempts_by_students
239
240
241
242
243
244
245
        
            list($fromqa, $whereqa, $qaparams) = quiz_report_attempts_sql($quizid, $currentgroup, $groupstudents);
    
            $sql = 'SELECT (CASE WHEN attempt=1 THEN 1 ELSE 0 END) AS isfirst, COUNT(1) AS countrecs, SUM(sumgrades) AS total ' .
                    'FROM '.$fromqa.
                    'WHERE ' .$whereqa.
                    'GROUP BY (attempt=1)';
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
            if (!$attempttotals = $DB->get_records_sql($sql, $qaparams)){
                $s = 0;
            } else {
                $firstattempt = $attempttotals[1];
                $allattempts = new object();
                $allattempts->countrecs = $firstattempt->countrecs + 
                                (isset($attempttotals[0])?$attempttotals[0]->countrecs:0);
                $allattempts->total = $firstattempt->total + 
                                (isset($attempttotals[0])?$attempttotals[0]->total:0);
                if ($useallattempts){
                    $usingattempts = $allattempts;
                    $usingattempts->attempts = get_string('allattempts', 'quiz_statistics');
                    $usingattempts->sql = '';
                } else {
                    $usingattempts = $firstattempt;
                    $usingattempts->attempts = get_string('firstattempts', 'quiz_statistics');
                    $usingattempts->sql = 'AND qa.attempt=1 ';
                }
264
                $usingattemptsstring = $usingattempts->attempts;
265
                $s = $usingattempts->countrecs;
266
                $sumgradesavg = $usingattempts->total / $usingattempts->countrecs;
267
            }
268
        } else {
269
            $s = 0;
270
        }
271
272
273
274
275
276
277
278
279
        $quizstats = new object();
        if ($s == 0){
            $quizstats->firstattemptscount = 0;
            $quizstats->allattemptscount = 0;
        } else {
            $quizstats->firstattemptscount = $firstattempt->countrecs;
            $quizstats->allattemptscount = $allattempts->countrecs;
            $quizstats->firstattemptsavg = $firstattempt->total / $firstattempt->countrecs;
            $quizstats->allattemptsavg = $allattempts->total / $allattempts->countrecs;
280
        }
281
282
283
        //recalculate sql again this time possibly including test for first attempt.
        list($fromqa, $whereqa, $qaparams) = quiz_report_attempts_sql($quizid, $currentgroup, $groupstudents, $useallattempts);
        
284
        //get the median
285
        if ($s) {
286
287

            if (($s%2)==0){
288
                //even number of attempts
289
                $limitoffset = ($s/2) - 1;
290
291
                $limit = 2;
            } else {
292
                $limitoffset = (floor($s/2)) + 1;
293
294
295
                $limit = 1;
            }
            $sql = 'SELECT id, sumgrades ' .
296
297
                'FROM ' .$fromqa.
                'WHERE ' .$whereqa.
298
                'ORDER BY sumgrades';
299
            if (!$mediangrades = $DB->get_records_sql_menu($sql, $qaparams, $limitoffset, $limit)){
300
301
302
                print_error('errormedian', 'quiz_statistics');
            }
            if (count($mediangrades)==1){
303
                $quizstats->median = array_shift($mediangrades);
304
305
306
            } else {
                $median = array_shift($mediangrades);
                $median += array_shift($mediangrades);
307
                $quizstats->median = $median /2;
308
            }
309
310
311
            if ($s>1){
                //fetch sum of squared, cubed and power 4d 
                //differences between grades and mean grade
312
                $mean = $usingattempts->total / $s;
313
                $sql = "SELECT " .
314
315
316
                    "SUM(POWER((qa.sumgrades - :mean1),2)) AS power2, " .
                    "SUM(POWER((qa.sumgrades - :mean2),3)) AS power3, ".
                    "SUM(POWER((qa.sumgrades - :mean3),4)) AS power4 ".
317
                    'FROM ' .$fromqa.
318
                    'WHERE ' .$whereqa;
319
                $params = array('mean1' => $mean, 'mean2' => $mean, 'mean3' => $mean)+$qaparams;
320
321
322
323
324
325
326
                if (!$powers = $DB->get_record_sql($sql, $params)){
                    print_error('errorpowers', 'quiz_statistics');
                }
                
                //Standard_Deviation
                //see http://docs.moodle.org/en/Development:Quiz_item_analysis_calculations_in_practise#Standard_Deviation
                
327
328
                $quizstats->standarddeviation = sqrt($powers->power2 / ($s -1));
                
329
330
331
332
333
334
335
336
337
338
339
340

                
                //Skewness_and_Kurtosis
                if ($s>2){
                    //see http://docs.moodle.org/en/Development:Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis
                    $m2= $powers->power2 / $s;
                    $m3= $powers->power3 / $s;
                    $m4= $powers->power4 / $s;
                    
                    $k2= $s*$m2/($s-1);
                    $k3= $s*$s*$m3/(($s-1)*($s-2));
                    
341
                    $quizstats->skewness = $k3 / (pow($k2, 2/3));
342
343
344
345
346
347
                }
    
    
                if ($s>3){
                    $k4= (($s*$s*$s)/(($s-1)*($s-2)*($s-3)))*((($s+1)*$m4)-(3*($s-1)*$m2*$m2));
                    
348
                    $quizstats->kurtosis = $k4 / ($k2*$k2);
349
                }
350
351
352
            }
        }
        if ($s){
353
354
            require_once("$CFG->dirroot/mod/quiz/report/statistics/qstats.php");
            $qstats = new qstats($questions, $s, $sumgradesavg);
355
            $qstats->get_records($quizid, $currentgroup, $groupstudents, $useallattempts);
356
357
            set_time_limit(0);
            $qstats->process_states();
358
359
        } else {
            $qstats = false;
360
        }
361
362
363
364
365
366
367
        if ($s>1){
            $p = count($qstats->questions);//no of positions
            if ($p > 1){
                $quizstats->cic = (100 * $p / ($p -1)) * (1 - ($qstats->sum_of_grade_variance())/$k2);
                $quizstats->errorratio = 100 * sqrt(1-($quizstats->cic/100));
                $quizstats->standarderror = ($quizstats->errorratio * $quizstats->standarddeviation / 100);
                
368
            }
369
        }
370
        return array($s, $usingattemptsstring, $quizstats, $qstats);
371
372
373
    }

}
374
375
function quiz_report_attempts_sql($quizid, $currentgroup, $groupstudents, $allattempts = true){
    $fromqa = '{quiz_attempts} qa ';
376
    $whereqa = 'qa.quiz = :quizid AND qa.preview=0 AND qa.timefinish !=0 ';
377
378
379
380
381
382
383
384
385
386
387
    $qaparams = array('quizid'=>$quizid);
    if (!empty($currentgroup) && $groupstudents) {
        list($grpsql, $grpparams) = $DB->get_in_or_equal(array_keys($groupstudents), SQL_PARAMS_NAMED, 'u0000');
        $whereqa .= 'AND qa.userid '.$grpsql.' ';
        $qaparams += $grpparams;
    }
    if (!$allattempts){
        $whereqa .= 'AND qa.attempt=1 ';
    }
    return array($fromqa, $whereqa, $qaparams);
}
388
?>