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 qbank_statistics;
18
 
19
use core_question\statistics\questions\all_calculated_for_qubaid_condition;
20
use core_component;
21
 
22
/**
23
 * Helper for statistics
24
 *
25
 * @package    qbank_statistics
26
 * @copyright  2021 Catalyst IT Australia Pty Ltd
27
 * @author     Nathan Nguyen <nathannguyen@catalyst-au.net>
28
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29
 */
30
class helper {
31
 
32
    /**
33
     * @var float Threshold to determine 'Needs checking?'
34
     */
35
    private const NEED_FOR_REVISION_LOWER_THRESHOLD = 30;
36
 
37
    /**
38
     * @var float Threshold to determine 'Needs checking?'
39
     */
40
    private const NEED_FOR_REVISION_UPPER_THRESHOLD = 50;
41
 
42
    /**
43
     * For a list of questions find all the places, defined by (component, contextid) where there are attempts.
44
     *
45
     * @param int[] $questionids array of question ids that we are interested in.
46
     * @return \stdClass[] list of objects with fields ->component and ->contextid.
47
     * @deprecated since Moodle 4.3 please use the method from statistics_bulk_loader.
48
     * @todo MDL-78090 Final deprecation in Moodle 4.7
49
     */
50
    private static function get_all_places_where_questions_were_attempted(array $questionids): array {
51
        global $DB;
52
 
53
        [$questionidcondition, $params] = $DB->get_in_or_equal($questionids);
54
        // The MIN(qu.id) is just to ensure that the rows have a unique key.
55
        $places = $DB->get_records_sql("
56
                SELECT MIN(qu.id) AS somethingunique, qu.component, qu.contextid, " .
57
                       \context_helper::get_preload_record_columns_sql('ctx') . "
58
                  FROM {question_usages} qu
59
                  JOIN {question_attempts} qa ON qa.questionusageid = qu.id
60
                  JOIN {context} ctx ON ctx.id = qu.contextid
61
                 WHERE qa.questionid $questionidcondition
62
              GROUP BY qu.component, qu.contextid, " .
63
                       implode(', ', array_keys(\context_helper::get_preload_record_columns('ctx'))) . "
64
              ORDER BY qu.contextid ASC
65
                ", $params);
66
 
67
        // Strip out the unwanted ids.
68
        $places = array_values($places);
69
        foreach ($places as $place) {
70
            unset($place->somethingunique);
71
            \context_helper::preload_from_record($place);
72
        }
73
 
74
        return $places;
75
    }
76
 
77
    /**
78
     * Load the question statistics for all the attempts belonging to a particular component in a particular context.
79
     *
80
     * @param string $component frankenstyle component name, e.g. 'mod_quiz'.
81
     * @param \context $context the context to load the statistics for.
82
     * @return all_calculated_for_qubaid_condition|null question statistics.
83
     * @deprecated since Moodle 4.3 please use the method from statistics_bulk_loader.
84
     * @todo MDL-78090 Final deprecation in Moodle 4.7
85
     */
86
    private static function load_statistics_for_place(string $component, \context $context): ?all_calculated_for_qubaid_condition {
87
        // This check is basically if (component_exists).
88
        if (empty(core_component::get_component_directory($component))) {
89
            return null;
90
        }
91
 
92
        if (!component_callback_exists($component, 'calculate_question_stats')) {
93
            return null;
94
        }
95
 
96
        return component_callback($component, 'calculate_question_stats', [$context]);
97
    }
98
 
99
    /**
100
     * Extract the value for one question and one type of statistic from a set of statistics.
101
     *
102
     * @param all_calculated_for_qubaid_condition $statistics the batch of statistics.
103
     * @param int $questionid a question id.
104
     * @param string $item one of the field names in all_calculated_for_qubaid_condition, e.g. 'facility'.
105
     * @return float|null the required value.
106
     * @deprecated since Moodle 4.3 please use the method from statistics_bulk_loader.
107
     * @todo MDL-78090 Final deprecation in Moodle 4.7
108
     */
109
    private static function extract_item_value(all_calculated_for_qubaid_condition $statistics,
110
            int $questionid, string $item): ?float {
111
 
112
        // Look in main questions.
113
        foreach ($statistics->questionstats as $stats) {
114
            if ($stats->questionid == $questionid && isset($stats->$item)) {
115
                return $stats->$item;
116
            }
117
        }
118
 
119
        // If not found, look in sub questions.
120
        foreach ($statistics->subquestionstats as $stats) {
121
            if ($stats->questionid == $questionid && isset($stats->$item)) {
122
                return $stats->$item;
123
            }
124
        }
125
 
126
        return null;
127
    }
128
 
129
    /**
130
     * Calculate average for a stats item on a list of questions.
131
     *
132
     * @param int[] $questionids list of ids of the questions we are interested in.
133
     * @param string $item one of the field names in all_calculated_for_qubaid_condition, e.g. 'facility'.
134
     * @return array array keys are question ids and the corresponding values are the average values.
135
     *      Only questions for which there are data are included.
136
     * @deprecated since Moodle 4.3 please use the method from statistics_bulk_loader.
137
     * @todo MDL-78090 Final deprecation in Moodle 4.7
138
     */
139
    private static function calculate_average_question_stats_item(array $questionids, string $item): array {
140
        $places = self::get_all_places_where_questions_were_attempted($questionids);
141
 
142
        $counts = [];
143
        $sums = [];
144
 
145
        foreach ($places as $place) {
146
            $statistics = self::load_statistics_for_place($place->component,
147
                    \context::instance_by_id($place->contextid));
148
            if ($statistics === null) {
149
                continue;
150
            }
151
 
152
            foreach ($questionids as $questionid) {
153
                $value = self::extract_item_value($statistics, $questionid, $item);
154
                if ($value === null) {
155
                    continue;
156
                }
157
 
158
                $counts[$questionid] = ($counts[$questionid] ?? 0) + 1;
159
                $sums[$questionid] = ($sums[$questionid] ?? 0) + $value;
160
            }
161
        }
162
 
163
        // Return null if there is no quizzes.
164
        $averages = [];
165
        foreach ($sums as $questionid => $sum) {
166
            $averages[$questionid] = $sum / $counts[$questionid];
167
        }
168
        return $averages;
169
    }
170
 
171
    /**
172
     * Calculate average facility index
173
     *
174
     * @param int $questionid
175
     * @return float|null
176
     * @deprecated since Moodle 4.3 please use the method from statistics_bulk_loader.
177
     * @todo MDL-78090 Final deprecation in Moodle 4.7
178
     */
179
    public static function calculate_average_question_facility(int $questionid): ?float {
180
        debugging('Deprecated: please use statistics_bulk_loader instead, ' .
181
                'or get_required_statistics_fields in your question bank column class.', DEBUG_DEVELOPER);
182
        $averages = self::calculate_average_question_stats_item([$questionid], 'facility');
183
        return $averages[$questionid] ?? null;
184
    }
185
 
186
    /**
187
     * Calculate average discriminative efficiency
188
     *
189
     * @param int $questionid question id
190
     * @return float|null
191
     * @deprecated since Moodle 4.3 please use the method from statistics_bulk_loader.
192
     * @todo MDL-78090 Final deprecation in Moodle 4.7
193
     */
194
    public static function calculate_average_question_discriminative_efficiency(int $questionid): ?float {
195
        debugging('Deprecated: please use statistics_bulk_loader instead, ' .
196
                'or get_required_statistics_fields in your question bank column class.', DEBUG_DEVELOPER);
197
        $averages = self::calculate_average_question_stats_item([$questionid], 'discriminativeefficiency');
198
        return $averages[$questionid] ?? null;
199
    }
200
 
201
    /**
202
     * Calculate average discriminative efficiency
203
     *
204
     * @param int $questionid question id
205
     * @return float|null
206
     * @deprecated since Moodle 4.3 please use the method from statistics_bulk_loader.
207
     * @todo MDL-78090 Final deprecation in Moodle 4.7
208
     */
209
    public static function calculate_average_question_discrimination_index(int $questionid): ?float {
210
        debugging('Deprecated: please use statistics_bulk_loader instead, ' .
211
                'or get_required_statistics_fields in your question bank column class.', DEBUG_DEVELOPER);
212
        $averages = self::calculate_average_question_stats_item([$questionid], 'discriminationindex');
213
        return $averages[$questionid] ?? null;
214
    }
215
 
216
    /**
217
     * Format a number to a localised percentage with specified decimal points.
218
     *
219
     * @param float|null $number The number being formatted
220
     * @param bool $fraction An indicator for whether the number is a fraction or is already multiplied by 100
221
     * @param int $decimals Sets the number of decimal points
222
     * @return string
223
     * @throws \coding_exception
224
     */
225
    public static function format_percentage(?float $number, bool $fraction = true, int $decimals = 2): string {
226
        if (is_null($number)) {
227
            return get_string('na', 'qbank_statistics');
228
        }
229
        $coefficient = $fraction ? 100 : 1;
230
        return get_string('percents', 'moodle', format_float($number * $coefficient, $decimals));
231
    }
232
 
233
    /**
234
     * Format discrimination index (Needs checking?).
235
     *
236
     * @param float|null $value stats value
237
     * @return array
238
     */
239
    public static function format_discrimination_index(?float $value): array {
240
        if (is_null($value)) {
241
            $content = get_string('emptyvalue', 'qbank_statistics');
242
            $classes = '';
243
        } else if ($value < self::NEED_FOR_REVISION_LOWER_THRESHOLD) {
244
            $content = get_string('verylikely', 'qbank_statistics');
245
            $classes = 'alert-danger';
246
        } else if ($value < self::NEED_FOR_REVISION_UPPER_THRESHOLD) {
247
            $content = get_string('likely', 'qbank_statistics');
248
            $classes = 'alert-warning';
249
        } else {
250
            $content = get_string('unlikely', 'qbank_statistics');
251
            $classes = 'alert-success';
252
        }
253
        return [$content, $classes];
254
    }
255
}