Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace quiz_statistics;
18
defined('MOODLE_INTERNAL') || die();
19
 
20
/**
21
 * Class to calculate and also manage caching of quiz statistics.
22
 *
23
 * These quiz statistics calculations are described here :
24
 *
25
 * http://docs.moodle.org/dev/Quiz_statistics_calculations#Test_statistics
26
 *
27
 * @package    quiz_statistics
28
 * @copyright  2013 The Open University
29
 * @author     James Pratt me@jamiep.org
30
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31
 */
32
class calculator {
33
 
34
    /**
35
     * @var \core\progress\base
36
     */
37
    protected $progress;
38
 
39
    public function __construct(\core\progress\base $progress = null) {
40
        if ($progress === null) {
41
            $progress = new \core\progress\none();
42
        }
43
        $this->progress = $progress;
44
    }
45
 
46
    /**
47
     * Compute the quiz statistics.
48
     *
49
     * @param int   $quizid            the quiz id.
50
     * @param int $whichattempts which attempts to use, represented internally as one of the constants as used in
51
     *                                   $quiz->grademethod ie.
52
     *                                   QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
53
     *                                   we calculate stats based on which attempts would affect the grade for each student.
54
     * @param \core\dml\sql_join $groupstudentsjoins Contains joins, wheres, params for students in this group.
55
     * @param int   $p                 number of positions (slots).
56
     * @param float $sumofmarkvariance sum of mark variance, calculated as part of question statistics
57
     * @return calculated $quizstats The statistics for overall attempt scores.
58
     */
59
    public function calculate($quizid, $whichattempts, \core\dml\sql_join $groupstudentsjoins, $p, $sumofmarkvariance) {
60
 
61
        $this->progress->start_progress('', 3);
62
 
63
        $quizstats = new calculated($whichattempts);
64
 
65
        $countsandaverages = $this->attempt_counts_and_averages($quizid, $groupstudentsjoins);
66
        $this->progress->progress(1);
67
 
68
        foreach ($countsandaverages as $propertyname => $value) {
69
            $quizstats->{$propertyname} = $value;
70
        }
71
 
72
        $s = $quizstats->s();
73
        if ($s != 0) {
74
 
75
            // Recalculate sql again this time possibly including test for first attempt.
76
            list($fromqa, $whereqa, $qaparams) =
77
                quiz_statistics_attempts_sql($quizid, $groupstudentsjoins, $whichattempts);
78
 
79
            $quizstats->median = $this->median($s, $fromqa, $whereqa, $qaparams);
80
            $this->progress->progress(2);
81
 
82
            if ($s > 1) {
83
 
84
                $powers = $this->sum_of_powers_of_difference_to_mean($quizstats->avg(), $fromqa, $whereqa, $qaparams);
85
                $this->progress->progress(3);
86
 
87
                $quizstats->standarddeviation = sqrt($powers->power2 / ($s - 1));
88
 
89
                // Skewness.
90
                if ($s > 2) {
91
                    // See http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise#Skewness_and_Kurtosis.
92
                    $m2 = $powers->power2 / $s;
93
                    $m3 = $powers->power3 / $s;
94
                    $m4 = $powers->power4 / $s;
95
 
96
                    $k2 = $s * $m2 / ($s - 1);
97
                    $k3 = $s * $s * $m3 / (($s - 1) * ($s - 2));
98
                    if ($k2 != 0) {
99
                        $quizstats->skewness = $k3 / (pow($k2, 3 / 2));
100
 
101
                        // Kurtosis.
102
                        if ($s > 3) {
103
                            $k4 = $s * $s * ((($s + 1) * $m4) - (3 * ($s - 1) * $m2 * $m2)) / (($s - 1) * ($s - 2) * ($s - 3));
104
                            $quizstats->kurtosis = $k4 / ($k2 * $k2);
105
                        }
106
 
107
                        if ($p > 1) {
108
                            $quizstats->cic = (100 * $p / ($p - 1)) * (1 - ($sumofmarkvariance / $k2));
109
                            $quizstats->errorratio = 100 * sqrt(1 - ($quizstats->cic / 100));
110
                            $quizstats->standarderror = $quizstats->errorratio *
111
                                $quizstats->standarddeviation / 100;
112
                        }
113
                    }
114
 
115
                }
116
            }
117
 
118
            $quizstats->cache(quiz_statistics_qubaids_condition($quizid, $groupstudentsjoins, $whichattempts));
119
        }
120
        $this->progress->end_progress();
121
        return $quizstats;
122
    }
123
 
124
    /**
125
     * @var int previously, the time after which statistics are automatically recomputed.
126
     * @deprecated since Moodle 4.3. Use of pre-computed stats is no longer time-limited.
127
     * @todo MDL-78091 Final deprecation in Moodle 4.7
128
     */
129
    const TIME_TO_CACHE = 900; // 15 minutes.
130
 
131
    /**
132
     * Load cached statistics from the database.
133
     *
134
     * @param \qubaid_condition $qubaids
135
     * @return calculated|false The statistics for overall attempt scores or false if not cached.
136
     */
137
    public function get_cached($qubaids) {
138
        global $DB;
139
 
140
        $lastcalculatedtime = $this->get_last_calculated_time($qubaids);
141
        if (!$lastcalculatedtime) {
142
            return false;
143
        }
144
        $fromdb = $DB->get_record('quiz_statistics', ['hashcode' => $qubaids->get_hash_code(),
145
                'timemodified' => $lastcalculatedtime]);
146
        $stats = new calculated();
147
        $stats->populate_from_record($fromdb);
148
        return $stats;
149
    }
150
 
151
    /**
152
     * Find time of non-expired statistics in the database.
153
     *
154
     * @param $qubaids \qubaid_condition
155
     * @return int|bool Time of cached record that matches this qubaid_condition or false is non found.
156
     */
157
    public function get_last_calculated_time($qubaids) {
158
        global $DB;
159
        $lastcalculatedtime = $DB->get_field('quiz_statistics', 'COALESCE(MAX(timemodified), 0)',
160
                ['hashcode' => $qubaids->get_hash_code()]);
161
        if ($lastcalculatedtime) {
162
            return $lastcalculatedtime;
163
        } else {
164
            return false;
165
        }
166
    }
167
 
168
    /**
169
     * Given a particular quiz grading method return a lang string describing which attempts contribute to grade.
170
     *
171
     * Note internally we use the grading method constants to represent which attempts we are calculating statistics for, each
172
     * grading method corresponds to different attempts for each user.
173
     *
174
     * @param  int $whichattempts which attempts to use, represented internally as one of the constants as used in
175
     *                                   $quiz->grademethod ie.
176
     *                                   QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
177
     *                                   we calculate stats based on which attempts would affect the grade for each student.
178
     * @return string the appropriate lang string to describe this option.
179
     */
180
    public static function using_attempts_lang_string($whichattempts) {
181
         return get_string(static::using_attempts_string_id($whichattempts), 'quiz_statistics');
182
    }
183
 
184
    /**
185
     * Given a particular quiz grading method return a string id for use as a field name prefix in mdl_quiz_statistics or to
186
     * fetch the appropriate language string describing which attempts contribute to grade.
187
     *
188
     * Note internally we use the grading method constants to represent which attempts we are calculating statistics for, each
189
     * grading method corresponds to different attempts for each user.
190
     *
191
     * @param  int $whichattempts which attempts to use, represented internally as one of the constants as used in
192
     *                                   $quiz->grademethod ie.
193
     *                                   QUIZ_GRADEAVERAGE, QUIZ_GRADEHIGHEST, QUIZ_ATTEMPTLAST or QUIZ_ATTEMPTFIRST
194
     *                                   we calculate stats based on which attempts would affect the grade for each student.
195
     * @return string the string id for this option.
196
     */
197
    public static function using_attempts_string_id($whichattempts) {
198
        switch ($whichattempts) {
199
            case QUIZ_ATTEMPTFIRST :
200
                return 'firstattempts';
201
            case QUIZ_GRADEHIGHEST :
202
                return 'highestattempts';
203
            case QUIZ_ATTEMPTLAST :
204
                return 'lastattempts';
205
            case QUIZ_GRADEAVERAGE :
206
                return 'allattempts';
207
        }
208
    }
209
 
210
    /**
211
     * Calculating count and mean of marks for first and ALL attempts by students.
212
     *
213
     * See : http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
214
     *                                      #Calculating_MEAN_of_grades_for_all_attempts_by_students
215
     * @param int $quizid
216
     * @param \core\dml\sql_join $groupstudentsjoins Contains joins, wheres, params for students in this group.
217
     * @return \stdClass with properties with count and avg with prefixes firstattempts, highestattempts, etc.
218
     */
219
    protected function attempt_counts_and_averages($quizid, \core\dml\sql_join $groupstudentsjoins) {
220
        global $DB;
221
 
222
        $attempttotals = new \stdClass();
223
        foreach (array_keys(quiz_get_grading_options()) as $which) {
224
 
225
            list($fromqa, $whereqa, $qaparams) = quiz_statistics_attempts_sql($quizid, $groupstudentsjoins, $which);
226
 
227
            $fromdb = $DB->get_record_sql("SELECT COUNT(*) AS rcount, AVG(sumgrades) AS average FROM $fromqa WHERE $whereqa",
228
                                            $qaparams);
229
            $fieldprefix = static::using_attempts_string_id($which);
230
            $attempttotals->{$fieldprefix.'avg'} = $fromdb->average;
231
            $attempttotals->{$fieldprefix.'count'} = $fromdb->rcount;
232
        }
233
        return $attempttotals;
234
    }
235
 
236
    /**
237
     * Median mark.
238
     *
239
     * http://docs.moodle.org/dev/Quiz_statistics_calculations#Median_Score
240
     *
241
     * @param $s integer count of attempts
242
     * @param $fromqa string
243
     * @param $whereqa string
244
     * @param $qaparams string
245
     * @return float
246
     */
247
    protected function median($s, $fromqa, $whereqa, $qaparams) {
248
        global $DB;
249
 
250
        if ($s % 2 == 0) {
251
            // An even number of attempts.
252
            $limitoffset = $s / 2 - 1;
253
            $limit = 2;
254
        } else {
255
            $limitoffset = floor($s / 2);
256
            $limit = 1;
257
        }
258
        $sql = "SELECT quiza.id, quiza.sumgrades
259
                  FROM $fromqa
260
                 WHERE $whereqa
261
              ORDER BY sumgrades";
262
 
263
        $medianmarks = $DB->get_records_sql_menu($sql, $qaparams, $limitoffset, $limit);
264
 
265
        return array_sum($medianmarks) / count($medianmarks);
266
    }
267
 
268
    /**
269
     * Fetch the sum of squared, cubed and to the power 4 differences between sumgrade and it's mean.
270
     *
271
     * Explanation here : http://docs.moodle.org/dev/Quiz_item_analysis_calculations_in_practise
272
     *              #Calculating_Standard_Deviation.2C_Skewness_and_Kurtosis_of_grades_for_all_attempts_by_students
273
     *
274
     * @param $mean
275
     * @param $fromqa
276
     * @param $whereqa
277
     * @param $qaparams
278
     * @return stdClass with properties power2, power3, power4
279
     */
280
    protected function sum_of_powers_of_difference_to_mean($mean, $fromqa, $whereqa, $qaparams) {
281
        global $DB;
282
 
283
        $sql = "SELECT
284
                    SUM(POWER((quiza.sumgrades - $mean), 2)) AS power2,
285
                    SUM(POWER((quiza.sumgrades - $mean), 3)) AS power3,
286
                    SUM(POWER((quiza.sumgrades - $mean), 4)) AS power4
287
                  FROM $fromqa
288
                 WHERE $whereqa";
289
        $params = ['mean1' => $mean, 'mean2' => $mean, 'mean3' => $mean] + $qaparams;
290
 
291
        return $DB->get_record_sql($sql, $params, MUST_EXIST);
292
    }
293
 
294
}