MDL-57791 analytics: Increase time splitting field size
[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/**
b94dbb55 18 * Prediction model representation.
369389c9
DM
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/**
b94dbb55 30 * Prediction model representation.
369389c9
DM
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
d16cf374
DM
319 /**
320 * Removes the model.
321 *
322 * @return void
323 */
324 public static function delete() {
325 global $DB;
326 $this->clear_model();
327 $DB->delete_record('analytics_models', array('id' => $this->model->id));
328 }
329
369389c9
DM
330 /**
331 * Evaluates the model datasets.
332 *
333 * Model datasets should already be available in Moodle's filesystem.
334 *
335 * @param array $options
336 * @return \stdClass[]
337 */
338 public function evaluate($options = array()) {
339
340 // Increase memory limit.
341 $this->increase_memory();
342
343 $options['evaluation'] = true;
344 $this->init_analyser($options);
345
346 if (empty($this->get_indicators())) {
347 throw new \moodle_exception('errornoindicators', 'analytics');
348 }
349
350 // Before get_labelled_data call so we get an early exception if it is not ready.
351 $predictor = \core_analytics\manager::get_predictions_processor();
352
353 $datasets = $this->get_analyser()->get_labelled_data();
354
355 // No datasets generated.
356 if (empty($datasets)) {
357 $result = new \stdClass();
358 $result->status = self::NO_DATASET;
359 $result->info = $this->get_analyser()->get_logs();
360 return array($result);
361 }
362
363 if (!PHPUNIT_TEST && CLI_SCRIPT) {
364 echo PHP_EOL . get_string('processingsitecontents', 'analytics') . PHP_EOL;
365 }
366
367 $results = array();
368 foreach ($datasets as $timesplittingid => $dataset) {
369
370 $timesplitting = \core_analytics\manager::get_time_splitting($timesplittingid);
371
372 $result = new \stdClass();
373
374 $dashestimesplittingid = str_replace('\\', '', $timesplittingid);
375 $outputdir = $this->get_output_dir(array('evaluation', $dashestimesplittingid));
376
377 // Evaluate the dataset, the deviation we accept in the results depends on the amount of iterations.
378 $predictorresult = $predictor->evaluate($this->model->id, self::ACCEPTED_DEVIATION,
379 self::EVALUATION_ITERATIONS, $dataset, $outputdir);
380
381 $result->status = $predictorresult->status;
382 $result->info = $predictorresult->info;
383
384 if (isset($predictorresult->score)) {
385 $result->score = $predictorresult->score;
386 } else {
387 // Prediction processors may return an error, default to 0 score in that case.
388 $result->score = 0;
389 }
390
391 $dir = false;
392 if (!empty($predictorresult->dir)) {
393 $dir = $predictorresult->dir;
394 }
395
396 $result->logid = $this->log_result($timesplitting->get_id(), $result->score, $dir, $result->info);
397
398 $results[$timesplitting->get_id()] = $result;
399 }
400
401 return $results;
402 }
403
404 /**
405 * train
406 *
407 * @return \stdClass
408 */
409 public function train() {
410 global $DB;
411
412 // Increase memory limit.
413 $this->increase_memory();
414
415 if ($this->model->enabled == false || empty($this->model->timesplitting)) {
416 throw new \moodle_exception('invalidtimesplitting', 'analytics', '', $this->model->id);
417 }
418
419 if (empty($this->get_indicators())) {
420 throw new \moodle_exception('errornoindicators', 'analytics');
421 }
422
423 // Before get_labelled_data call so we get an early exception if it is not writable.
424 $outputdir = $this->get_output_dir(array('execution'));
425
426 // Before get_labelled_data call so we get an early exception if it is not ready.
427 $predictor = \core_analytics\manager::get_predictions_processor();
428
429 $datasets = $this->get_analyser()->get_labelled_data();
430
431 // No training if no files have been provided.
432 if (empty($datasets) || empty($datasets[$this->model->timesplitting])) {
433
434 $result = new \stdClass();
435 $result->status = self::NO_DATASET;
436 $result->info = $this->get_analyser()->get_logs();
437 return $result;
438 }
439 $samplesfile = $datasets[$this->model->timesplitting];
440
441 // Train using the dataset.
442 $predictorresult = $predictor->train($this->get_unique_id(), $samplesfile, $outputdir);
443
444 $result = new \stdClass();
445 $result->status = $predictorresult->status;
446 $result->info = $predictorresult->info;
447
448 $this->flag_file_as_used($samplesfile, 'trained');
449
450 // Mark the model as trained if it wasn't.
451 if ($this->model->trained == false) {
452 $this->mark_as_trained();
453 }
454
455 return $result;
456 }
457
458 /**
459 * predict
460 *
461 * @return \stdClass
462 */
463 public function predict() {
464 global $DB;
465
466 // Increase memory limit.
467 $this->increase_memory();
468
469 if ($this->model->enabled == false || empty($this->model->timesplitting)) {
470 throw new \moodle_exception('invalidtimesplitting', 'analytics', '', $this->model->id);
471 }
472
473 if (empty($this->get_indicators())) {
474 throw new \moodle_exception('errornoindicators', 'analytics');
475 }
476
477 // Before get_unlabelled_data call so we get an early exception if it is not writable.
478 $outputdir = $this->get_output_dir(array('execution'));
479
480 // Before get_unlabelled_data call so we get an early exception if it is not ready.
481 $predictor = \core_analytics\manager::get_predictions_processor();
482
483 $samplesdata = $this->get_analyser()->get_unlabelled_data();
484
485 // Get the prediction samples file.
486 if (empty($samplesdata) || empty($samplesdata[$this->model->timesplitting])) {
487
488 $result = new \stdClass();
489 $result->status = self::NO_DATASET;
490 $result->info = $this->get_analyser()->get_logs();
491 return $result;
492 }
493 $samplesfile = $samplesdata[$this->model->timesplitting];
494
495 // We need to throw an exception if we are trying to predict stuff that was already predicted.
496 $params = array('modelid' => $this->model->id, 'fileid' => $samplesfile->get_id(), 'action' => 'predicted');
497 if ($predicted = $DB->get_record('analytics_used_files', $params)) {
498 throw new \moodle_exception('erroralreadypredict', 'analytics', '', $samplesfile->get_id());
499 }
500
501 $predictorresult = $predictor->predict($this->get_unique_id(), $samplesfile, $outputdir);
502
503 $result = new \stdClass();
504 $result->status = $predictorresult->status;
505 $result->info = $predictorresult->info;
506
507 $calculations = \core_analytics\dataset_manager::get_structured_data($samplesfile);
508
509 // Here we will store all predictions' contexts, this will be used to limit which users will see those predictions.
510 $samplecontexts = array();
511
512 if ($predictorresult) {
513 $result->predictions = $predictorresult->predictions;
514 foreach ($result->predictions as $sampleinfo) {
515
516 // We parse each prediction
517 switch (count($sampleinfo)) {
518 case 1:
519 // For whatever reason the predictions processor could not process this sample, we
520 // skip it and do nothing with it.
521 debugging($this->model->id . ' model predictions processor could not process the sample with id ' .
522 $sampleinfo[0], DEBUG_DEVELOPER);
523 continue;
524 case 2:
525 // Prediction processors that do not return a prediction score will have the maximum prediction
526 // score.
527 list($uniquesampleid, $prediction) = $sampleinfo;
528 $predictionscore = 1;
529 break;
530 case 3:
531 list($uniquesampleid, $prediction, $predictionscore) = $sampleinfo;
532 break;
533 default:
534 break;
535 }
536
537 if ($this->get_target()->triggers_callback($prediction, $predictionscore)) {
538
539 // The unique sample id contains both the sampleid and the rangeindex.
540 list($sampleid, $rangeindex) = $this->get_time_splitting()->infer_sample_info($uniquesampleid);
541
542 // Store the predicted values.
543 $samplecontext = $this->save_prediction($sampleid, $rangeindex, $prediction, $predictionscore,
544 json_encode($calculations[$uniquesampleid]));
545
546 // Also store all samples context to later generate insights or whatever action the target wants to perform.
547 $samplecontexts[$samplecontext->id] = $samplecontext;
548
549 $this->get_target()->prediction_callback($this->model->id, $sampleid, $rangeindex, $samplecontext,
550 $prediction, $predictionscore);
551 }
552 }
553 }
554
555 if (!empty($samplecontexts)) {
556 // Notify the target that all predictions have been processed.
557 $this->get_target()->generate_insights($this->model->id, $samplecontexts);
558
559 // Aggressive invalidation, the cost of filling up the cache is not high.
560 $cache = \cache::make('core', 'modelswithpredictions');
561 foreach ($samplecontexts as $context) {
562 $cache->delete($context->id);
563 }
564 }
565
566 $this->flag_file_as_used($samplesfile, 'predicted');
567
568 return $result;
569 }
570
571 /**
572 * save_prediction
573 *
574 * @param int $sampleid
575 * @param int $rangeindex
576 * @param int $prediction
577 * @param float $predictionscore
578 * @param string $calculations
579 * @return \context
580 */
581 protected function save_prediction($sampleid, $rangeindex, $prediction, $predictionscore, $calculations) {
582 global $DB;
583
584 $context = $this->get_analyser()->sample_access_context($sampleid);
585
586 $record = new \stdClass();
587 $record->modelid = $this->model->id;
588 $record->contextid = $context->id;
589 $record->sampleid = $sampleid;
590 $record->rangeindex = $rangeindex;
591 $record->prediction = $prediction;
592 $record->predictionscore = $predictionscore;
593 $record->calculations = $calculations;
594 $record->timecreated = time();
595 $DB->insert_record('analytics_predictions', $record);
596
597 return $context;
598 }
599
600 /**
601 * enable
602 *
603 * @param string $timesplittingid
604 * @return void
605 */
606 public function enable($timesplittingid = false) {
607 global $DB;
608
609 $now = time();
610
611 if ($timesplittingid && $timesplittingid !== $this->model->timesplitting) {
612
613 if (!\core_analytics\manager::is_valid($timesplittingid, '\core_analytics\local\time_splitting\base')) {
614 throw new \moodle_exception('errorinvalidtimesplitting', 'analytics');
615 }
616
617 if (substr($timesplittingid, 0, 1) !== '\\') {
618 throw new \moodle_exception('errorinvalidtimesplitting', 'analytics');
619 }
620
621 $this->model->timesplitting = $timesplittingid;
622 $this->model->version = $now;
623 }
624 $this->model->enabled = 1;
625 $this->model->timemodified = $now;
626
627 // We don't always update timemodified intentionally as we reserve it for target, indicators or timesplitting updates.
628 $DB->update_record('analytics_models', $this->model);
629
630 // It needs to be reset (just in case, we may already used it).
631 $this->uniqueid = null;
632 }
633
634 /**
635 * is_enabled
636 *
637 * @return bool
638 */
639 public function is_enabled() {
640 return (bool)$this->model->enabled;
641 }
642
643 /**
644 * is_trained
645 *
646 * @return bool
647 */
648 public function is_trained() {
649 return (bool)$this->model->trained;
650 }
651
652 /**
653 * mark_as_trained
654 *
655 * @return void
656 */
657 public function mark_as_trained() {
658 global $DB;
659
660 $this->model->trained = 1;
661 $DB->update_record('analytics_models', $this->model);
662 }
663
664 /**
665 * get_predictions_contexts
666 *
667 * @return \stdClass[]
668 */
669 public function get_predictions_contexts() {
670 global $DB;
671
672 $sql = "SELECT DISTINCT contextid FROM {analytics_predictions} WHERE modelid = ?";
673 return $DB->get_records_sql($sql, array($this->model->id));
674 }
675
676 /**
677 * Whether predictions exist for this context.
678 *
679 * @param \context $context
680 * @return bool
681 */
682 public function predictions_exist(\context $context) {
683 global $DB;
684
685 // Filters out previous predictions keeping only the last time range one.
686 $select = "modelid = :modelid AND contextid = :contextid";
6ec2ae0f 687 $params = array('modelid' => $this->model->id, 'contextid' => $context->id);
369389c9
DM
688 return $DB->record_exists_select('analytics_predictions', $select, $params);
689 }
690
691 /**
692 * Gets the predictions for this context.
693 *
694 * @param \context $context
695 * @return \core_analytics\prediction[]
696 */
697 public function get_predictions(\context $context) {
698 global $DB;
699
700 // Filters out previous predictions keeping only the last time range one.
701 $sql = "SELECT tip.*
702 FROM {analytics_predictions} tip
703 JOIN (
704 SELECT sampleid, max(rangeindex) AS rangeindex
705 FROM {analytics_predictions}
706 WHERE modelid = ? and contextid = ?
707 GROUP BY sampleid
708 ) tipsub
709 ON tip.sampleid = tipsub.sampleid AND tip.rangeindex = tipsub.rangeindex
710 WHERE tip.modelid = ? and tip.contextid = ?";
711 $params = array($this->model->id, $context->id, $this->model->id, $context->id);
712 if (!$predictions = $DB->get_records_sql($sql, $params)) {
713 return array();
714 }
715
716 // Get predicted samples' ids.
717 $sampleids = array_map(function($prediction) {
718 return $prediction->sampleid;
719 }, $predictions);
720
721 list($unused, $samplesdata) = $this->get_analyser()->get_samples($sampleids);
722
723 // Add samples data as part of each prediction.
724 foreach ($predictions as $predictionid => $predictiondata) {
725
726 $sampleid = $predictiondata->sampleid;
727
728 // Filter out predictions which samples are not available anymore.
729 if (empty($samplesdata[$sampleid])) {
730 unset($predictions[$predictionid]);
731 continue;
732 }
733
734 // Replace stdClass object by \core_analytics\prediction objects.
735 $prediction = new \core_analytics\prediction($predictiondata, $samplesdata[$sampleid]);
736
737 $predictions[$predictionid] = $prediction;
738 }
739
740 return $predictions;
741 }
742
743 /**
744 * prediction_sample_data
745 *
746 * @param \stdClass $predictionobj
747 * @return array
748 */
749 public function prediction_sample_data($predictionobj) {
750
751 list($unused, $samplesdata) = $this->get_analyser()->get_samples(array($predictionobj->sampleid));
752
753 if (empty($samplesdata[$predictionobj->sampleid])) {
754 throw new \moodle_exception('errorsamplenotavailable', 'analytics');
755 }
756
757 return $samplesdata[$predictionobj->sampleid];
758 }
759
760 /**
761 * prediction_sample_description
762 *
763 * @param \core_analytics\prediction $prediction
764 * @return array 2 elements: list(string, \renderable)
765 */
766 public function prediction_sample_description(\core_analytics\prediction $prediction) {
767 return $this->get_analyser()->sample_description($prediction->get_prediction_data()->sampleid,
768 $prediction->get_prediction_data()->contextid, $prediction->get_sample_data());
769 }
770
771 /**
772 * Returns the output directory for prediction processors.
773 *
774 * Directory structure as follows:
775 * - Evaluation runs:
776 * models/$model->id/$model->version/evaluation/$model->timesplitting
777 * - Training & prediction runs:
778 * models/$model->id/$model->version/execution
779 *
780 * @param array $subdirs
781 * @return string
782 */
783 protected function get_output_dir($subdirs = array()) {
784 global $CFG;
785
786 $subdirstr = '';
787 foreach ($subdirs as $subdir) {
788 $subdirstr .= DIRECTORY_SEPARATOR . $subdir;
789 }
790
791 $outputdir = get_config('analytics', 'modeloutputdir');
792 if (empty($outputdir)) {
793 // Apply default value.
794 $outputdir = rtrim($CFG->dataroot, '/') . DIRECTORY_SEPARATOR . 'models';
795 }
796
797 // Append model id and version + subdirs.
798 $outputdir .= DIRECTORY_SEPARATOR . $this->model->id . DIRECTORY_SEPARATOR . $this->model->version . $subdirstr;
799
800 make_writable_directory($outputdir);
801
802 return $outputdir;
803 }
804
805 /**
806 * get_unique_id
807 *
808 * @return string
809 */
810 public function get_unique_id() {
811 global $CFG;
812
813 if (!is_null($this->uniqueid)) {
814 return $this->uniqueid;
815 }
816
817 // Generate a unique id for this site, this model and this time splitting method, considering the last time
818 // that the model target and indicators were updated.
819 $ids = array($CFG->wwwroot, $CFG->dirroot, $CFG->prefix, $this->model->id, $this->model->version);
820 $this->uniqueid = sha1(implode('$$', $ids));
821
822 return $this->uniqueid;
823 }
824
825 /**
826 * Exports the model data.
827 *
828 * @return \stdClass
829 */
830 public function export() {
831 $data = clone $this->model;
832 $data->target = $this->get_target()->get_name();
833
834 if ($timesplitting = $this->get_time_splitting()) {
835 $data->timesplitting = $timesplitting->get_name();
836 }
837
838 $data->indicators = array();
839 foreach ($this->get_indicators() as $indicator) {
840 $data->indicators[] = $indicator->get_name();
841 }
842 return $data;
843 }
844
845 /**
846 * flag_file_as_used
847 *
848 * @param \stored_file $file
849 * @param string $action
850 * @return void
851 */
852 protected function flag_file_as_used(\stored_file $file, $action) {
853 global $DB;
854
855 $usedfile = new \stdClass();
856 $usedfile->modelid = $this->model->id;
857 $usedfile->fileid = $file->get_id();
858 $usedfile->action = $action;
859 $usedfile->time = time();
860 $DB->insert_record('analytics_used_files', $usedfile);
861 }
862
863 /**
864 * log_result
865 *
866 * @param string $timesplittingid
867 * @param float $score
868 * @param string $dir
869 * @param array $info
870 * @return int The inserted log id
871 */
872 protected function log_result($timesplittingid, $score, $dir = false, $info = false) {
873 global $DB, $USER;
874
875 $log = new \stdClass();
876 $log->modelid = $this->get_id();
877 $log->version = $this->model->version;
878 $log->target = $this->model->target;
879 $log->indicators = $this->model->indicators;
880 $log->timesplitting = $timesplittingid;
881 $log->dir = $dir;
882 if ($info) {
883 // Ensure it is not an associative array.
884 $log->info = json_encode(array_values($info));
885 }
886 $log->score = $score;
887 $log->timecreated = time();
888 $log->usermodified = $USER->id;
889
890 return $DB->insert_record('analytics_models_log', $log);
891 }
892
893 /**
894 * Utility method to return indicator class names from a list of indicator objects
895 *
896 * @param \core_analytics\local\indicator\base[] $indicators
897 * @return string[]
898 */
899 private static function indicator_classes($indicators) {
900
901 // What we want to check and store are the indicator classes not the keys.
902 $indicatorclasses = array();
903 foreach ($indicators as $indicator) {
904 if (!\core_analytics\manager::is_valid($indicator, '\core_analytics\local\indicator\base')) {
905 if (!is_object($indicator) && !is_scalar($indicator)) {
906 $indicator = strval($indicator);
907 } else if (is_object($indicator)) {
908 $indicator = get_class($indicator);
909 }
910 throw new \moodle_exception('errorinvalidindicator', 'analytics', '', $indicator);
911 }
912 $indicatorclasses[] = '\\' . get_class($indicator);
913 }
914
915 return $indicatorclasses;
916 }
917
918 /**
919 * Clears the model training and prediction data.
920 *
921 * Executed after updating model critical elements like the time splitting method
922 * or the indicators.
923 *
924 * @return void
925 */
926 private function clear_model() {
927 global $DB;
928
929 $DB->delete_records('analytics_predict_ranges', array('modelid' => $this->model->id));
930 $DB->delete_records('analytics_predictions', array('modelid' => $this->model->id));
931 $DB->delete_records('analytics_train_samples', array('modelid' => $this->model->id));
932 $DB->delete_records('analytics_used_files', array('modelid' => $this->model->id));
933
934 $cache = \cache::make('core', 'modelswithpredictions');
935 $result = $cache->purge();
936 }
937
938 private function increase_memory() {
939 if (ini_get('memory_limit') != -1) {
940 raise_memory_limit(MEMORY_HUGE);
941 }
942 }
943
944}