MDL-58859 analytics: Analytics API added to core
[moodle.git] / analytics / classes / model.php
CommitLineData
369389c9
DM
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 * Inspire tool model representation.
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
25namespace core_analytics;
26
27defined('MOODLE_INTERNAL') || die();
28
29/**
30 * Inspire tool model representation.
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 */
36class model {
37
38 const OK = 0;
39 const GENERAL_ERROR = 1;
40 const NO_DATASET = 2;
41
42 const EVALUATE_LOW_SCORE = 4;
43 const EVALUATE_NOT_ENOUGH_DATA = 8;
44
45 const ANALYSE_INPROGRESS = 2;
46 const ANALYSE_REJECTED_RANGE_PROCESSOR = 4;
47 const ANALYSABLE_STATUS_INVALID_FOR_RANGEPROCESSORS = 8;
48 const ANALYSABLE_STATUS_INVALID_FOR_TARGET = 16;
49
50 const MIN_SCORE = 0.7;
51 const ACCEPTED_DEVIATION = 0.05;
52 const EVALUATION_ITERATIONS = 10;
53
54 /**
55 * @var \stdClass
56 */
57 protected $model = null;
58
59 /**
60 * @var \core_analytics\local\analyser\base
61 */
62 protected $analyser = null;
63
64 /**
65 * @var \core_analytics\local\target\base
66 */
67 protected $target = null;
68
69 /**
70 * @var \core_analytics\local\indicator\base[]
71 */
72 protected $indicators = null;
73
74 /**
75 * Unique Model id created from site info and last model modification.
76 *
77 * @var string
78 */
79 protected $uniqueid = null;
80
81 /**
82 * __construct
83 *
84 * @param int|stdClass $model
85 * @return void
86 */
87 public function __construct($model) {
88 global $DB;
89
90 if (is_scalar($model)) {
91 $model = $DB->get_record('analytics_models', array('id' => $model));
92 }
93 $this->model = $model;
94 }
95
96 /**
97 * get_id
98 *
99 * @return int
100 */
101 public function get_id() {
102 return $this->model->id;
103 }
104
105 /**
106 * get_model_obj
107 *
108 * @return \stdClass
109 */
110 public function get_model_obj() {
111 return $this->model;
112 }
113
114 /**
115 * get_target
116 *
117 * @return \core_analytics\local\target\base
118 */
119 public function get_target() {
120 if ($this->target !== null) {
121 return $this->target;
122 }
123 $instance = \core_analytics\manager::get_target($this->model->target);
124 $this->target = $instance;
125
126 return $this->target;
127 }
128
129 /**
130 * get_indicators
131 *
132 * @return \core_analytics\local\indicator\base[]
133 */
134 public function get_indicators() {
135 if ($this->indicators !== null) {
136 return $this->indicators;
137 }
138
139 $fullclassnames = json_decode($this->model->indicators);
140
141 if (!is_array($fullclassnames)) {
142 throw new \coding_exception('Model ' . $this->model->id . ' indicators can not be read');
143 }
144
145 $this->indicators = array();
146 foreach ($fullclassnames as $fullclassname) {
147 $instance = \core_analytics\manager::get_indicator($fullclassname);
148 if ($instance) {
149 $this->indicators[$fullclassname] = $instance;
150 } else {
151 debugging('Can\'t load ' . $fullclassname . ' indicator', DEBUG_DEVELOPER);
152 }
153 }
154
155 return $this->indicators;
156 }
157
158 /**
159 * Returns the list of indicators that could potentially be used by the model target.
160 *
161 * It includes the indicators that are part of the model.
162 *
163 * @return \core_analytics\local\indicator\base
164 */
165 public function get_potential_indicators() {
166
167 $indicators = \core_analytics\manager::get_all_indicators();
168
169 if (empty($this->analyser)) {
170 $this->init_analyser(array('evaluation' => true));
171 }
172
173 foreach ($indicators as $classname => $indicator) {
174 if ($this->analyser->check_indicator_requirements($indicator) !== true) {
175 unset($indicators[$classname]);
176 }
177 }
178 return $indicators;
179 }
180
181 /**
182 * get_analyser
183 *
184 * @return \core_analytics\local\analyser\base
185 */
186 public function get_analyser() {
187 if ($this->analyser !== null) {
188 return $this->analyser;
189 }
190
191 // Default initialisation with no options.
192 $this->init_analyser();
193
194 return $this->analyser;
195 }
196
197 /**
198 * init_analyser
199 *
200 * @param array $options
201 * @return void
202 */
203 protected function init_analyser($options = array()) {
204
205 $target = $this->get_target();
206 $indicators = $this->get_indicators();
207
208 if (empty($target)) {
209 throw new \moodle_exception('errornotarget', 'analytics');
210 }
211
212 if (!empty($options['evaluation'])) {
213 // The evaluation process will run using all available time splitting methods unless one is specified.
214 if (!empty($options['timesplitting'])) {
215 $timesplitting = \core_analytics\manager::get_time_splitting($options['timesplitting']);
216 $timesplittings = array($timesplitting->get_id() => $timesplitting);
217 } else {
218 $timesplittings = \core_analytics\manager::get_enabled_time_splitting_methods();
219 }
220 } else {
221
222 if (empty($this->model->timesplitting)) {
223 throw new \moodle_exception('invalidtimesplitting', 'analytics', '', $this->model->id);
224 }
225
226 // Returned as an array as all actions (evaluation, training and prediction) go through the same process.
227 $timesplittings = array($this->model->timesplitting => $this->get_time_splitting());
228 }
229
230 if (empty($timesplittings)) {
231 throw new \moodle_exception('errornotimesplittings', 'analytics');
232 }
233
234 $classname = $target->get_analyser_class();
235 if (!class_exists($classname)) {
236 throw \coding_exception($classname . ' class does not exists');
237 }
238
239 // Returns a \core_analytics\local\analyser\base class.
240 $this->analyser = new $classname($this->model->id, $target, $indicators, $timesplittings, $options);
241 }
242
243 /**
244 * get_time_splitting
245 *
246 * @return \core_analytics\local\time_splitting\base
247 */
248 public function get_time_splitting() {
249 if (empty($this->model->timesplitting)) {
250 return false;
251 }
252 return \core_analytics\manager::get_time_splitting($this->model->timesplitting);
253 }
254
255 /**
256 * create
257 *
258 * @param \core_analytics\local\target\base $target
259 * @param \core_analytics\local\indicator\base[] $indicators
260 * @return \core_analytics\model
261 */
262 public static function create(\core_analytics\local\target\base $target, array $indicators) {
263 global $USER, $DB;
264
265 $indicatorclasses = self::indicator_classes($indicators);
266
267 $now = time();
268
269 $modelobj = new \stdClass();
270 $modelobj->target = '\\' . get_class($target);
271 $modelobj->indicators = json_encode($indicatorclasses);
272 $modelobj->version = $now;
273 $modelobj->timecreated = $now;
274 $modelobj->timemodified = $now;
275 $modelobj->usermodified = $USER->id;
276
277 $id = $DB->insert_record('analytics_models', $modelobj);
278
279 // Get db defaults.
280 $modelobj = $DB->get_record('analytics_models', array('id' => $id), '*', MUST_EXIST);
281
282 return new static($modelobj);
283 }
284
285 public function update($enabled, $indicators, $timesplitting = '') {
286 global $USER, $DB;
287
288 $now = time();
289
290 $indicatorclasses = self::indicator_classes($indicators);
291
292 $indicatorsstr = json_encode($indicatorclasses);
293 if ($this->model->timesplitting !== $timesplitting ||
294 $this->model->indicators !== $indicatorsstr) {
295 // We update the version of the model so different time splittings are not mixed up.
296 $this->model->version = $now;
297
298 // Delete generated predictions.
299 $this->clear_model();
300
301 // Purge all generated files.
302 \core_analytics\dataset_manager::clear_model_files($this->model->id);
303
304 // Reset trained flag.
305 $this->model->trained = 0;
306 }
307 $this->model->enabled = $enabled;
308 $this->model->indicators = $indicatorsstr;
309 $this->model->timesplitting = $timesplitting;
310 $this->model->timemodified = $now;
311 $this->model->usermodified = $USER->id;
312
313 $DB->update_record('analytics_models', $this->model);
314
315 // It needs to be reset (just in case, we may already used it).
316 $this->uniqueid = null;
317 }
318
319 /**
320 * Evaluates the model datasets.
321 *
322 * Model datasets should already be available in Moodle's filesystem.
323 *
324 * @param array $options
325 * @return \stdClass[]
326 */
327 public function evaluate($options = array()) {
328
329 // Increase memory limit.
330 $this->increase_memory();
331
332 $options['evaluation'] = true;
333 $this->init_analyser($options);
334
335 if (empty($this->get_indicators())) {
336 throw new \moodle_exception('errornoindicators', 'analytics');
337 }
338
339 // Before get_labelled_data call so we get an early exception if it is not ready.
340 $predictor = \core_analytics\manager::get_predictions_processor();
341
342 $datasets = $this->get_analyser()->get_labelled_data();
343
344 // No datasets generated.
345 if (empty($datasets)) {
346 $result = new \stdClass();
347 $result->status = self::NO_DATASET;
348 $result->info = $this->get_analyser()->get_logs();
349 return array($result);
350 }
351
352 if (!PHPUNIT_TEST && CLI_SCRIPT) {
353 echo PHP_EOL . get_string('processingsitecontents', 'analytics') . PHP_EOL;
354 }
355
356 $results = array();
357 foreach ($datasets as $timesplittingid => $dataset) {
358
359 $timesplitting = \core_analytics\manager::get_time_splitting($timesplittingid);
360
361 $result = new \stdClass();
362
363 $dashestimesplittingid = str_replace('\\', '', $timesplittingid);
364 $outputdir = $this->get_output_dir(array('evaluation', $dashestimesplittingid));
365
366 // Evaluate the dataset, the deviation we accept in the results depends on the amount of iterations.
367 $predictorresult = $predictor->evaluate($this->model->id, self::ACCEPTED_DEVIATION,
368 self::EVALUATION_ITERATIONS, $dataset, $outputdir);
369
370 $result->status = $predictorresult->status;
371 $result->info = $predictorresult->info;
372
373 if (isset($predictorresult->score)) {
374 $result->score = $predictorresult->score;
375 } else {
376 // Prediction processors may return an error, default to 0 score in that case.
377 $result->score = 0;
378 }
379
380 $dir = false;
381 if (!empty($predictorresult->dir)) {
382 $dir = $predictorresult->dir;
383 }
384
385 $result->logid = $this->log_result($timesplitting->get_id(), $result->score, $dir, $result->info);
386
387 $results[$timesplitting->get_id()] = $result;
388 }
389
390 return $results;
391 }
392
393 /**
394 * train
395 *
396 * @return \stdClass
397 */
398 public function train() {
399 global $DB;
400
401 // Increase memory limit.
402 $this->increase_memory();
403
404 if ($this->model->enabled == false || empty($this->model->timesplitting)) {
405 throw new \moodle_exception('invalidtimesplitting', 'analytics', '', $this->model->id);
406 }
407
408 if (empty($this->get_indicators())) {
409 throw new \moodle_exception('errornoindicators', 'analytics');
410 }
411
412 // Before get_labelled_data call so we get an early exception if it is not writable.
413 $outputdir = $this->get_output_dir(array('execution'));
414
415 // Before get_labelled_data call so we get an early exception if it is not ready.
416 $predictor = \core_analytics\manager::get_predictions_processor();
417
418 $datasets = $this->get_analyser()->get_labelled_data();
419
420 // No training if no files have been provided.
421 if (empty($datasets) || empty($datasets[$this->model->timesplitting])) {
422
423 $result = new \stdClass();
424 $result->status = self::NO_DATASET;
425 $result->info = $this->get_analyser()->get_logs();
426 return $result;
427 }
428 $samplesfile = $datasets[$this->model->timesplitting];
429
430 // Train using the dataset.
431 $predictorresult = $predictor->train($this->get_unique_id(), $samplesfile, $outputdir);
432
433 $result = new \stdClass();
434 $result->status = $predictorresult->status;
435 $result->info = $predictorresult->info;
436
437 $this->flag_file_as_used($samplesfile, 'trained');
438
439 // Mark the model as trained if it wasn't.
440 if ($this->model->trained == false) {
441 $this->mark_as_trained();
442 }
443
444 return $result;
445 }
446
447 /**
448 * predict
449 *
450 * @return \stdClass
451 */
452 public function predict() {
453 global $DB;
454
455 // Increase memory limit.
456 $this->increase_memory();
457
458 if ($this->model->enabled == false || empty($this->model->timesplitting)) {
459 throw new \moodle_exception('invalidtimesplitting', 'analytics', '', $this->model->id);
460 }
461
462 if (empty($this->get_indicators())) {
463 throw new \moodle_exception('errornoindicators', 'analytics');
464 }
465
466 // Before get_unlabelled_data call so we get an early exception if it is not writable.
467 $outputdir = $this->get_output_dir(array('execution'));
468
469 // Before get_unlabelled_data call so we get an early exception if it is not ready.
470 $predictor = \core_analytics\manager::get_predictions_processor();
471
472 $samplesdata = $this->get_analyser()->get_unlabelled_data();
473
474 // Get the prediction samples file.
475 if (empty($samplesdata) || empty($samplesdata[$this->model->timesplitting])) {
476
477 $result = new \stdClass();
478 $result->status = self::NO_DATASET;
479 $result->info = $this->get_analyser()->get_logs();
480 return $result;
481 }
482 $samplesfile = $samplesdata[$this->model->timesplitting];
483
484 // We need to throw an exception if we are trying to predict stuff that was already predicted.
485 $params = array('modelid' => $this->model->id, 'fileid' => $samplesfile->get_id(), 'action' => 'predicted');
486 if ($predicted = $DB->get_record('analytics_used_files', $params)) {
487 throw new \moodle_exception('erroralreadypredict', 'analytics', '', $samplesfile->get_id());
488 }
489
490 $predictorresult = $predictor->predict($this->get_unique_id(), $samplesfile, $outputdir);
491
492 $result = new \stdClass();
493 $result->status = $predictorresult->status;
494 $result->info = $predictorresult->info;
495
496 $calculations = \core_analytics\dataset_manager::get_structured_data($samplesfile);
497
498 // Here we will store all predictions' contexts, this will be used to limit which users will see those predictions.
499 $samplecontexts = array();
500
501 if ($predictorresult) {
502 $result->predictions = $predictorresult->predictions;
503 foreach ($result->predictions as $sampleinfo) {
504
505 // We parse each prediction
506 switch (count($sampleinfo)) {
507 case 1:
508 // For whatever reason the predictions processor could not process this sample, we
509 // skip it and do nothing with it.
510 debugging($this->model->id . ' model predictions processor could not process the sample with id ' .
511 $sampleinfo[0], DEBUG_DEVELOPER);
512 continue;
513 case 2:
514 // Prediction processors that do not return a prediction score will have the maximum prediction
515 // score.
516 list($uniquesampleid, $prediction) = $sampleinfo;
517 $predictionscore = 1;
518 break;
519 case 3:
520 list($uniquesampleid, $prediction, $predictionscore) = $sampleinfo;
521 break;
522 default:
523 break;
524 }
525
526 if ($this->get_target()->triggers_callback($prediction, $predictionscore)) {
527
528 // The unique sample id contains both the sampleid and the rangeindex.
529 list($sampleid, $rangeindex) = $this->get_time_splitting()->infer_sample_info($uniquesampleid);
530
531 // Store the predicted values.
532 $samplecontext = $this->save_prediction($sampleid, $rangeindex, $prediction, $predictionscore,
533 json_encode($calculations[$uniquesampleid]));
534
535 // Also store all samples context to later generate insights or whatever action the target wants to perform.
536 $samplecontexts[$samplecontext->id] = $samplecontext;
537
538 $this->get_target()->prediction_callback($this->model->id, $sampleid, $rangeindex, $samplecontext,
539 $prediction, $predictionscore);
540 }
541 }
542 }
543
544 if (!empty($samplecontexts)) {
545 // Notify the target that all predictions have been processed.
546 $this->get_target()->generate_insights($this->model->id, $samplecontexts);
547
548 // Aggressive invalidation, the cost of filling up the cache is not high.
549 $cache = \cache::make('core', 'modelswithpredictions');
550 foreach ($samplecontexts as $context) {
551 $cache->delete($context->id);
552 }
553 }
554
555 $this->flag_file_as_used($samplesfile, 'predicted');
556
557 return $result;
558 }
559
560 /**
561 * save_prediction
562 *
563 * @param int $sampleid
564 * @param int $rangeindex
565 * @param int $prediction
566 * @param float $predictionscore
567 * @param string $calculations
568 * @return \context
569 */
570 protected function save_prediction($sampleid, $rangeindex, $prediction, $predictionscore, $calculations) {
571 global $DB;
572
573 $context = $this->get_analyser()->sample_access_context($sampleid);
574
575 $record = new \stdClass();
576 $record->modelid = $this->model->id;
577 $record->contextid = $context->id;
578 $record->sampleid = $sampleid;
579 $record->rangeindex = $rangeindex;
580 $record->prediction = $prediction;
581 $record->predictionscore = $predictionscore;
582 $record->calculations = $calculations;
583 $record->timecreated = time();
584 $DB->insert_record('analytics_predictions', $record);
585
586 return $context;
587 }
588
589 /**
590 * enable
591 *
592 * @param string $timesplittingid
593 * @return void
594 */
595 public function enable($timesplittingid = false) {
596 global $DB;
597
598 $now = time();
599
600 if ($timesplittingid && $timesplittingid !== $this->model->timesplitting) {
601
602 if (!\core_analytics\manager::is_valid($timesplittingid, '\core_analytics\local\time_splitting\base')) {
603 throw new \moodle_exception('errorinvalidtimesplitting', 'analytics');
604 }
605
606 if (substr($timesplittingid, 0, 1) !== '\\') {
607 throw new \moodle_exception('errorinvalidtimesplitting', 'analytics');
608 }
609
610 $this->model->timesplitting = $timesplittingid;
611 $this->model->version = $now;
612 }
613 $this->model->enabled = 1;
614 $this->model->timemodified = $now;
615
616 // We don't always update timemodified intentionally as we reserve it for target, indicators or timesplitting updates.
617 $DB->update_record('analytics_models', $this->model);
618
619 // It needs to be reset (just in case, we may already used it).
620 $this->uniqueid = null;
621 }
622
623 /**
624 * is_enabled
625 *
626 * @return bool
627 */
628 public function is_enabled() {
629 return (bool)$this->model->enabled;
630 }
631
632 /**
633 * is_trained
634 *
635 * @return bool
636 */
637 public function is_trained() {
638 return (bool)$this->model->trained;
639 }
640
641 /**
642 * mark_as_trained
643 *
644 * @return void
645 */
646 public function mark_as_trained() {
647 global $DB;
648
649 $this->model->trained = 1;
650 $DB->update_record('analytics_models', $this->model);
651 }
652
653 /**
654 * get_predictions_contexts
655 *
656 * @return \stdClass[]
657 */
658 public function get_predictions_contexts() {
659 global $DB;
660
661 $sql = "SELECT DISTINCT contextid FROM {analytics_predictions} WHERE modelid = ?";
662 return $DB->get_records_sql($sql, array($this->model->id));
663 }
664
665 /**
666 * Whether predictions exist for this context.
667 *
668 * @param \context $context
669 * @return bool
670 */
671 public function predictions_exist(\context $context) {
672 global $DB;
673
674 // Filters out previous predictions keeping only the last time range one.
675 $select = "modelid = :modelid AND contextid = :contextid";
676 $params = array($this->model->id, $context->id);
677 return $DB->record_exists_select('analytics_predictions', $select, $params);
678 }
679
680 /**
681 * Gets the predictions for this context.
682 *
683 * @param \context $context
684 * @return \core_analytics\prediction[]
685 */
686 public function get_predictions(\context $context) {
687 global $DB;
688
689 // Filters out previous predictions keeping only the last time range one.
690 $sql = "SELECT tip.*
691 FROM {analytics_predictions} tip
692 JOIN (
693 SELECT sampleid, max(rangeindex) AS rangeindex
694 FROM {analytics_predictions}
695 WHERE modelid = ? and contextid = ?
696 GROUP BY sampleid
697 ) tipsub
698 ON tip.sampleid = tipsub.sampleid AND tip.rangeindex = tipsub.rangeindex
699 WHERE tip.modelid = ? and tip.contextid = ?";
700 $params = array($this->model->id, $context->id, $this->model->id, $context->id);
701 if (!$predictions = $DB->get_records_sql($sql, $params)) {
702 return array();
703 }
704
705 // Get predicted samples' ids.
706 $sampleids = array_map(function($prediction) {
707 return $prediction->sampleid;
708 }, $predictions);
709
710 list($unused, $samplesdata) = $this->get_analyser()->get_samples($sampleids);
711
712 // Add samples data as part of each prediction.
713 foreach ($predictions as $predictionid => $predictiondata) {
714
715 $sampleid = $predictiondata->sampleid;
716
717 // Filter out predictions which samples are not available anymore.
718 if (empty($samplesdata[$sampleid])) {
719 unset($predictions[$predictionid]);
720 continue;
721 }
722
723 // Replace stdClass object by \core_analytics\prediction objects.
724 $prediction = new \core_analytics\prediction($predictiondata, $samplesdata[$sampleid]);
725
726 $predictions[$predictionid] = $prediction;
727 }
728
729 return $predictions;
730 }
731
732 /**
733 * prediction_sample_data
734 *
735 * @param \stdClass $predictionobj
736 * @return array
737 */
738 public function prediction_sample_data($predictionobj) {
739
740 list($unused, $samplesdata) = $this->get_analyser()->get_samples(array($predictionobj->sampleid));
741
742 if (empty($samplesdata[$predictionobj->sampleid])) {
743 throw new \moodle_exception('errorsamplenotavailable', 'analytics');
744 }
745
746 return $samplesdata[$predictionobj->sampleid];
747 }
748
749 /**
750 * prediction_sample_description
751 *
752 * @param \core_analytics\prediction $prediction
753 * @return array 2 elements: list(string, \renderable)
754 */
755 public function prediction_sample_description(\core_analytics\prediction $prediction) {
756 return $this->get_analyser()->sample_description($prediction->get_prediction_data()->sampleid,
757 $prediction->get_prediction_data()->contextid, $prediction->get_sample_data());
758 }
759
760 /**
761 * Returns the output directory for prediction processors.
762 *
763 * Directory structure as follows:
764 * - Evaluation runs:
765 * models/$model->id/$model->version/evaluation/$model->timesplitting
766 * - Training & prediction runs:
767 * models/$model->id/$model->version/execution
768 *
769 * @param array $subdirs
770 * @return string
771 */
772 protected function get_output_dir($subdirs = array()) {
773 global $CFG;
774
775 $subdirstr = '';
776 foreach ($subdirs as $subdir) {
777 $subdirstr .= DIRECTORY_SEPARATOR . $subdir;
778 }
779
780 $outputdir = get_config('analytics', 'modeloutputdir');
781 if (empty($outputdir)) {
782 // Apply default value.
783 $outputdir = rtrim($CFG->dataroot, '/') . DIRECTORY_SEPARATOR . 'models';
784 }
785
786 // Append model id and version + subdirs.
787 $outputdir .= DIRECTORY_SEPARATOR . $this->model->id . DIRECTORY_SEPARATOR . $this->model->version . $subdirstr;
788
789 make_writable_directory($outputdir);
790
791 return $outputdir;
792 }
793
794 /**
795 * get_unique_id
796 *
797 * @return string
798 */
799 public function get_unique_id() {
800 global $CFG;
801
802 if (!is_null($this->uniqueid)) {
803 return $this->uniqueid;
804 }
805
806 // Generate a unique id for this site, this model and this time splitting method, considering the last time
807 // that the model target and indicators were updated.
808 $ids = array($CFG->wwwroot, $CFG->dirroot, $CFG->prefix, $this->model->id, $this->model->version);
809 $this->uniqueid = sha1(implode('$$', $ids));
810
811 return $this->uniqueid;
812 }
813
814 /**
815 * Exports the model data.
816 *
817 * @return \stdClass
818 */
819 public function export() {
820 $data = clone $this->model;
821 $data->target = $this->get_target()->get_name();
822
823 if ($timesplitting = $this->get_time_splitting()) {
824 $data->timesplitting = $timesplitting->get_name();
825 }
826
827 $data->indicators = array();
828 foreach ($this->get_indicators() as $indicator) {
829 $data->indicators[] = $indicator->get_name();
830 }
831 return $data;
832 }
833
834 /**
835 * flag_file_as_used
836 *
837 * @param \stored_file $file
838 * @param string $action
839 * @return void
840 */
841 protected function flag_file_as_used(\stored_file $file, $action) {
842 global $DB;
843
844 $usedfile = new \stdClass();
845 $usedfile->modelid = $this->model->id;
846 $usedfile->fileid = $file->get_id();
847 $usedfile->action = $action;
848 $usedfile->time = time();
849 $DB->insert_record('analytics_used_files', $usedfile);
850 }
851
852 /**
853 * log_result
854 *
855 * @param string $timesplittingid
856 * @param float $score
857 * @param string $dir
858 * @param array $info
859 * @return int The inserted log id
860 */
861 protected function log_result($timesplittingid, $score, $dir = false, $info = false) {
862 global $DB, $USER;
863
864 $log = new \stdClass();
865 $log->modelid = $this->get_id();
866 $log->version = $this->model->version;
867 $log->target = $this->model->target;
868 $log->indicators = $this->model->indicators;
869 $log->timesplitting = $timesplittingid;
870 $log->dir = $dir;
871 if ($info) {
872 // Ensure it is not an associative array.
873 $log->info = json_encode(array_values($info));
874 }
875 $log->score = $score;
876 $log->timecreated = time();
877 $log->usermodified = $USER->id;
878
879 return $DB->insert_record('analytics_models_log', $log);
880 }
881
882 /**
883 * Utility method to return indicator class names from a list of indicator objects
884 *
885 * @param \core_analytics\local\indicator\base[] $indicators
886 * @return string[]
887 */
888 private static function indicator_classes($indicators) {
889
890 // What we want to check and store are the indicator classes not the keys.
891 $indicatorclasses = array();
892 foreach ($indicators as $indicator) {
893 if (!\core_analytics\manager::is_valid($indicator, '\core_analytics\local\indicator\base')) {
894 if (!is_object($indicator) && !is_scalar($indicator)) {
895 $indicator = strval($indicator);
896 } else if (is_object($indicator)) {
897 $indicator = get_class($indicator);
898 }
899 throw new \moodle_exception('errorinvalidindicator', 'analytics', '', $indicator);
900 }
901 $indicatorclasses[] = '\\' . get_class($indicator);
902 }
903
904 return $indicatorclasses;
905 }
906
907 /**
908 * Clears the model training and prediction data.
909 *
910 * Executed after updating model critical elements like the time splitting method
911 * or the indicators.
912 *
913 * @return void
914 */
915 private function clear_model() {
916 global $DB;
917
918 $DB->delete_records('analytics_predict_ranges', array('modelid' => $this->model->id));
919 $DB->delete_records('analytics_predictions', array('modelid' => $this->model->id));
920 $DB->delete_records('analytics_train_samples', array('modelid' => $this->model->id));
921 $DB->delete_records('analytics_used_files', array('modelid' => $this->model->id));
922
923 $cache = \cache::make('core', 'modelswithpredictions');
924 $result = $cache->purge();
925 }
926
927 private function increase_memory() {
928 if (ini_get('memory_limit') != -1) {
929 raise_memory_limit(MEMORY_HUGE);
930 }
931 }
932
933}