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
/**
18
 * Abstract base target.
19
 *
20
 * @package   core_analytics
21
 * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
namespace core_analytics\local\target;
26
 
27
defined('MOODLE_INTERNAL') || die();
28
 
29
/**
30
 * Abstract base target.
31
 *
32
 * @package   core_analytics
33
 * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
34
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35
 */
36
abstract class base extends \core_analytics\calculable {
37
 
38
    /**
39
     * This target have linear or discrete values.
40
     *
41
     * @return bool
42
     */
43
    abstract public function is_linear();
44
 
45
    /**
46
     * Returns the analyser class that should be used along with this target.
47
     *
48
     * @return string The full class name as a string
49
     */
50
    abstract public function get_analyser_class();
51
 
52
    /**
53
     * Allows the target to verify that the analysable is a good candidate.
54
     *
55
     * This method can be used as a quick way to discard invalid analysables.
56
     * e.g. Imagine that your analysable don't have students and you need them.
57
     *
58
     * @param \core_analytics\analysable $analysable
59
     * @param bool $fortraining
60
     * @return true|string
61
     */
62
    abstract public function is_valid_analysable(\core_analytics\analysable $analysable, $fortraining = true);
63
 
64
    /**
65
     * Is this sample from the $analysable valid?
66
     *
67
     * @param int $sampleid
68
     * @param \core_analytics\analysable $analysable
69
     * @param bool $fortraining
70
     * @return bool
71
     */
72
    abstract public function is_valid_sample($sampleid, \core_analytics\analysable $analysable, $fortraining = true);
73
 
74
    /**
75
     * Calculates this target for the provided samples.
76
     *
77
     * In case there are no values to return or the provided sample is not applicable just return null.
78
     *
79
     * @param int $sampleid
80
     * @param \core_analytics\analysable $analysable
81
     * @param int|false $starttime Limit calculations to start time
82
     * @param int|false $endtime Limit calculations to end time
83
     * @return float|null
84
     */
85
    abstract protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false);
86
 
87
    /**
88
     * Can the provided time-splitting method be used on this target?.
89
     *
90
     * Time-splitting methods not matching the target requirements will not be selectable by models based on this target.
91
     *
92
     * @param  \core_analytics\local\time_splitting\base $timesplitting
93
     * @return bool
94
     */
95
    abstract public function can_use_timesplitting(\core_analytics\local\time_splitting\base $timesplitting): bool;
96
 
97
    /**
98
     * Is this target generating insights?
99
     *
100
     * Defaults to true.
101
     *
102
     * @return bool
103
     */
104
    public static function uses_insights() {
105
        return true;
106
    }
107
 
108
    /**
109
     * Should the insights of this model be linked from reports?
110
     *
111
     * @return bool
112
     */
113
    public function link_insights_report(): bool {
114
        return true;
115
    }
116
 
117
    /**
118
     * Based on facts (processed by machine learning backends) by default.
119
     *
120
     * @return bool
121
     */
122
    public static function based_on_assumptions() {
123
        return false;
124
    }
125
 
126
    /**
127
     * Update the last analysis time on analysable processed or always.
128
     *
129
     * If you overwrite this method to return false the last analysis time
130
     * will only be recorded in DB when the element successfully analysed. You can
131
     * safely return false for lightweight targets.
132
     *
133
     * @return bool
134
     */
135
    public function always_update_analysis_time(): bool {
136
        return true;
137
    }
138
 
139
    /**
140
     * Suggested actions for a user.
141
     *
142
     * @param \core_analytics\prediction $prediction
143
     * @param bool $includedetailsaction
144
     * @param bool $isinsightuser                       Force all the available actions to be returned as it the user who
145
     *                                                  receives the insight is the one logged in.
146
     * @return \core_analytics\prediction_action[]
147
     */
148
    public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false,
149
            $isinsightuser = false) {
150
        global $PAGE;
151
 
152
        $predictionid = $prediction->get_prediction_data()->id;
153
        $contextid = $prediction->get_prediction_data()->contextid;
154
        $modelid = $prediction->get_prediction_data()->modelid;
155
 
156
        $actions = array();
157
 
158
        if ($this->link_insights_report() && $includedetailsaction) {
159
 
160
            $predictionurl = new \moodle_url('/report/insights/prediction.php', array('id' => $predictionid));
161
            $detailstext = $this->get_view_details_text();
162
 
163
            $actions[] = new \core_analytics\prediction_action(\core_analytics\prediction::ACTION_PREDICTION_DETAILS, $prediction,
164
                $predictionurl, new \pix_icon('t/preview', $detailstext),
165
                $detailstext, false, [], \core_analytics\action::TYPE_NEUTRAL);
166
        }
167
 
168
        return $actions;
169
    }
170
 
171
    /**
172
     * Suggested bulk actions for a user.
173
     *
174
     * @param  \core_analytics\prediction[]     $predictions List of predictions suitable for the bulk actions to use.
175
     * @return \core_analytics\bulk_action[]                 The list of bulk actions.
176
     */
177
    public function bulk_actions(array $predictions) {
178
 
179
        $analyserclass = $this->get_analyser_class();
180
        if ($analyserclass::one_sample_per_analysable()) {
181
            // Default actions are useful / not useful.
182
            $actions = [
183
                \core_analytics\default_bulk_actions::useful(),
184
                \core_analytics\default_bulk_actions::not_useful()
185
            ];
186
 
187
        } else {
188
            // Accept and not applicable.
189
 
190
            $actions = [
191
                \core_analytics\default_bulk_actions::accept(),
192
                \core_analytics\default_bulk_actions::not_applicable()
193
            ];
194
 
195
            if (!self::based_on_assumptions()) {
196
                // We include incorrectly flagged.
197
                $actions[] = \core_analytics\default_bulk_actions::incorrectly_flagged();
198
            }
199
        }
200
 
201
        return $actions;
202
    }
203
 
204
    /**
205
     * Adds the JS required to run the bulk actions.
206
     */
207
    public function add_bulk_actions_js() {
208
        global $PAGE;
209
        $PAGE->requires->js_call_amd('report_insights/actions', 'initBulk', ['.insights-bulk-actions']);
210
    }
211
 
212
    /**
213
     * Returns the view details link text.
214
     * @return string
215
     */
216
    private function get_view_details_text() {
217
        if ($this->based_on_assumptions()) {
218
            $analyserclass = $this->get_analyser_class();
219
            if ($analyserclass::one_sample_per_analysable()) {
220
                $detailstext = get_string('viewinsightdetails', 'analytics');
221
            } else {
222
                $detailstext = get_string('viewdetails', 'analytics');
223
            }
224
        } else {
225
            $detailstext = get_string('viewprediction', 'analytics');
226
        }
227
 
228
        return $detailstext;
229
    }
230
 
231
    /**
232
     * Callback to execute once a prediction has been returned from the predictions processor.
233
     *
234
     * Note that the analytics_predictions db record is not yet inserted.
235
     *
236
     * @param int $modelid
237
     * @param int $sampleid
238
     * @param int $rangeindex
239
     * @param \context $samplecontext
240
     * @param float|int $prediction
241
     * @param float $predictionscore
242
     * @return void
243
     */
244
    public function prediction_callback($modelid, $sampleid, $rangeindex, \context $samplecontext, $prediction, $predictionscore) {
245
        return;
246
    }
247
 
248
    /**
249
     * Generates insights notifications
250
     *
251
     * @param int $modelid
252
     * @param \context[] $samplecontexts
253
     * @param  \core_analytics\prediction[] $predictions
254
     * @return void
255
     */
256
    public function generate_insight_notifications($modelid, $samplecontexts, array $predictions = []) {
257
        // Delegate the processing of insights to the insights_generator.
258
        $insightsgenerator = new \core_analytics\insights_generator($modelid, $this);
259
        $insightsgenerator->generate($samplecontexts, $predictions);
260
    }
261
 
262
    /**
263
     * Returns the list of users that will receive insights notifications.
264
     *
265
     * Feel free to overwrite if you need to but keep in mind that moodle/analytics:listinsights
266
     * or moodle/analytics:listowninsights capability is required to access the list of insights.
267
     *
268
     * @param \context $context
269
     * @return array
270
     */
271
    public function get_insights_users(\context $context) {
272
        if ($context->contextlevel === CONTEXT_USER) {
273
            if (!has_capability('moodle/analytics:listowninsights', $context, $context->instanceid)) {
274
                $users = [];
275
            } else {
276
                $users = [$context->instanceid => \core_user::get_user($context->instanceid)];
277
            }
278
 
279
        } else if ($context->contextlevel >= CONTEXT_COURSE) {
280
            // At course level or below only enrolled users although this is not ideal for
281
            // teachers assigned at category level.
282
            $users = get_enrolled_users($context, 'moodle/analytics:listinsights', 0, 'u.*', null, 0, 0, true);
283
        } else {
284
            $users = get_users_by_capability($context, 'moodle/analytics:listinsights');
285
        }
286
        return $users;
287
    }
288
 
289
    /**
290
     * URL to the insight.
291
     *
292
     * @param  int $modelid
293
     * @param  \context $context
294
     * @return \moodle_url
295
     */
296
    public function get_insight_context_url($modelid, $context) {
297
        return new \moodle_url('/report/insights/insights.php?modelid=' . $modelid . '&contextid=' . $context->id);
298
    }
299
 
300
    /**
301
     * The insight notification subject.
302
     *
303
     * This is just a default message, you should overwrite it for a custom insight message.
304
     *
305
     * @param  int $modelid
306
     * @param  \context $context
307
     * @return string
308
     */
309
    public function get_insight_subject(int $modelid, \context $context) {
310
        return get_string('insightmessagesubject', 'analytics', $context->get_context_name());
311
    }
312
 
313
    /**
314
     * Returns the body message for an insight with multiple predictions.
315
     *
316
     * This default method is executed when the analysable used by the model generates multiple insight
317
     * for each analysable (one_sample_per_analysable === false)
318
     *
319
     * @param  \context     $context
320
     * @param  string       $contextname
321
     * @param  \stdClass    $user
322
     * @param  \moodle_url  $insighturl
323
     * @return string[]                     The plain text message and the HTML message
324
     */
325
    public function get_insight_body(\context $context, string $contextname, \stdClass $user, \moodle_url $insighturl): array {
326
        global $OUTPUT;
327
 
328
        $fullmessage = get_string('insightinfomessageplain', 'analytics', $insighturl->out(false));
329
        $fullmessagehtml = $OUTPUT->render_from_template('core_analytics/insight_info_message',
330
            ['url' => $insighturl->out(false), 'insightinfomessage' => get_string('insightinfomessagehtml', 'analytics')]
331
        );
332
 
333
        return [$fullmessage, $fullmessagehtml];
334
    }
335
 
336
    /**
337
     * Returns the body message for an insight for a single prediction.
338
     *
339
     * This default method is executed when the analysable used by the model generates one insight
340
     * for each analysable (one_sample_per_analysable === true)
341
     *
342
     * @param  \context                             $context
343
     * @param  \stdClass                            $user
344
     * @param  \core_analytics\prediction           $prediction
345
     * @param  \core_analytics\action[]             $actions        Passed by reference to remove duplicate links to actions.
346
     * @return array                                                Plain text msg, HTML message and the main URL for this
347
     *                                                              insight (you can return null if you are happy with the
348
     *                                                              default insight URL calculated in prediction_info())
349
     */
350
    public function get_insight_body_for_prediction(\context $context, \stdClass $user, \core_analytics\prediction $prediction,
351
            array &$actions) {
352
        // No extra message by default.
353
        return [FORMAT_PLAIN => '', FORMAT_HTML => '', 'url' => null];
354
    }
355
 
356
    /**
357
     * Returns an instance of the child class.
358
     *
359
     * Useful to reset cached data.
360
     *
361
     * @return \core_analytics\base\target
362
     */
363
    public static function instance() {
364
        return new static();
365
    }
366
 
367
    /**
368
     * Defines a boundary to ignore predictions below the specified prediction score.
369
     *
370
     * Value should go from 0 to 1.
371
     *
372
     * @return float
373
     */
374
    protected function min_prediction_score() {
375
        // The default minimum discards predictions with a low score.
376
        return \core_analytics\model::PREDICTION_MIN_SCORE;
377
    }
378
 
379
    /**
380
     * This method determines if a prediction is interesing for the model or not.
381
     *
382
     * @param mixed $predictedvalue
383
     * @param float $predictionscore
384
     * @return bool
385
     */
386
    public function triggers_callback($predictedvalue, $predictionscore) {
387
 
388
        $minscore = floatval($this->min_prediction_score());
389
        if ($minscore < 0) {
390
            debugging(get_class($this) . ' minimum prediction score is below 0, please update it to a value between 0 and 1.');
391
        } else if ($minscore > 1) {
392
            debugging(get_class($this) . ' minimum prediction score is above 1, please update it to a value between 0 and 1.');
393
        }
394
 
395
        // We need to consider that targets may not have a min score.
396
        if (!empty($minscore) && floatval($predictionscore) < $minscore) {
397
            return false;
398
        }
399
 
400
        return true;
401
    }
402
 
403
    /**
404
     * Calculates the target.
405
     *
406
     * Returns an array of values which size matches $sampleids size.
407
     *
408
     * Rows with null values will be skipped as invalid by time splitting methods.
409
     *
410
     * @param array $sampleids
411
     * @param \core_analytics\analysable $analysable
412
     * @param int $starttime
413
     * @param int $endtime
414
     * @return array The format to follow is [userid] = scalar|null
415
     */
416
    public function calculate($sampleids, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
417
 
418
        if (!PHPUNIT_TEST && CLI_SCRIPT) {
419
            echo '.';
420
        }
421
 
422
        $calculations = [];
423
        foreach ($sampleids as $sampleid => $unusedsampleid) {
424
 
425
            // No time limits when calculating the target to train models.
426
            $calculatedvalue = $this->calculate_sample($sampleid, $analysable, $starttime, $endtime);
427
 
428
            if (!is_null($calculatedvalue)) {
429
                if ($this->is_linear() &&
430
                        ($calculatedvalue > static::get_max_value() || $calculatedvalue < static::get_min_value())) {
431
                    throw new \coding_exception('Calculated values should be higher than ' . static::get_min_value() .
432
                        ' and lower than ' . static::get_max_value() . '. ' . $calculatedvalue . ' received');
433
                } else if (!$this->is_linear() && static::is_a_class($calculatedvalue) === false) {
434
                    throw new \coding_exception('Calculated values should be one of the target classes (' .
435
                        json_encode(static::get_classes()) . '). ' . $calculatedvalue . ' received');
436
                }
437
            }
438
            $calculations[$sampleid] = $calculatedvalue;
439
        }
440
        return $calculations;
441
    }
442
 
443
    /**
444
     * Filters out invalid samples for training.
445
     *
446
     * @param int[] $sampleids
447
     * @param \core_analytics\analysable $analysable
448
     * @param bool $fortraining
449
     * @return void
450
     */
451
    public function filter_out_invalid_samples(&$sampleids, \core_analytics\analysable $analysable, $fortraining = true) {
452
        foreach ($sampleids as $sampleid => $unusedsampleid) {
453
            if (!$this->is_valid_sample($sampleid, $analysable, $fortraining)) {
454
                // Skip it and remove the sample from the list of calculated samples.
455
                unset($sampleids[$sampleid]);
456
            }
457
        }
458
    }
459
}