weekly release 3.4dev
[moodle.git] / analytics / classes / local / analyser / base.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/**
413f19bc 18 * Analysers base class.
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\local\analyser;
26
27defined('MOODLE_INTERNAL') || die();
28
29/**
413f19bc 30 * Analysers base class.
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 */
36abstract class base {
37
413f19bc
DM
38 /**
39 * @var int
40 */
369389c9
DM
41 protected $modelid;
42
413f19bc
DM
43 /**
44 * The model target.
45 *
46 * @var \core_analytics\local\target\base
47 */
369389c9 48 protected $target;
413f19bc 49
0690a271
DM
50 /**
51 * A $this->$target copy loaded with the ongoing analysis analysable.
52 *
53 * @var \core_analytics\local\target\base
54 */
55 protected $analysabletarget;
56
413f19bc
DM
57 /**
58 * The model indicators.
59 *
60 * @var \core_analytics\local\indicator\base[]
61 */
369389c9 62 protected $indicators;
413f19bc
DM
63
64 /**
65 * Time splitting methods to use.
66 *
67 * Multiple time splitting methods during evaluation and 1 single
68 * time splitting method once the model is enabled.
69 *
70 * @var \core_analytics\local\time_splitting\base[]
71 */
369389c9
DM
72 protected $timesplittings;
73
413f19bc
DM
74 /**
75 * Execution options.
76 *
77 * @var array
78 */
369389c9
DM
79 protected $options;
80
413f19bc
DM
81 /**
82 * Simple log array.
83 *
84 * @var string[]
85 */
369389c9
DM
86 protected $log;
87
413f19bc
DM
88 /**
89 * Constructor method.
90 *
91 * @param int $modelid
92 * @param \core_analytics\local\target\base $target
93 * @param \core_analytics\local\indicator\base[] $indicators
94 * @param \core_analytics\local\time_splitting\base[] $timesplittings
95 * @param array $options
96 * @return void
97 */
369389c9
DM
98 public function __construct($modelid, \core_analytics\local\target\base $target, $indicators, $timesplittings, $options) {
99 $this->modelid = $modelid;
100 $this->target = $target;
101 $this->indicators = $indicators;
102 $this->timesplittings = $timesplittings;
103
104 if (empty($options['evaluation'])) {
105 $options['evaluation'] = false;
106 }
107 $this->options = $options;
108
109 // Checks if the analyser satisfies the indicators requirements.
110 $this->check_indicators_requirements();
111
112 $this->log = array();
113 }
114
a8ccc5f2
DM
115 /**
116 * Returns the list of analysable elements available on the site.
117 *
118 * \core_analytics\local\analyser\by_course and \core_analytics\local\analyser\sitewide are implementing
119 * this method returning site courses (by_course) and the whole system (sitewide) as analysables.
120 *
121 * @return \core_analytics\analysable[]
122 */
123 abstract public function get_analysables();
124
369389c9 125 /**
413f19bc 126 * This function returns this analysable list of samples.
369389c9
DM
127 *
128 * @param \core_analytics\analysable $analysable
a40952d3 129 * @return array array[0] = int[] (sampleids) and array[1] = array (samplesdata)
369389c9
DM
130 */
131 abstract protected function get_all_samples(\core_analytics\analysable $analysable);
132
a40952d3 133 /**
413f19bc 134 * This function returns the samples data from a list of sample ids.
a40952d3
DM
135 *
136 * @param int[] $sampleids
137 * @return array array[0] = int[] (sampleids) and array[1] = array (samplesdata)
138 */
369389c9
DM
139 abstract public function get_samples($sampleids);
140
a40952d3 141 /**
413f19bc 142 * Returns the analysable of a sample.
a40952d3
DM
143 *
144 * @param int $sampleid
145 * @return \core_analytics\analysable
146 */
147 abstract public function get_sample_analysable($sampleid);
148
149 /**
413f19bc 150 * Returns the sample's origin in moodle database.
a40952d3
DM
151 *
152 * @return string
153 */
a8ccc5f2 154 abstract public function get_samples_origin();
369389c9
DM
155
156 /**
413f19bc
DM
157 * Returns the context of a sample.
158 *
369389c9
DM
159 * moodle/analytics:listinsights will be required at this level to access the sample predictions.
160 *
161 * @param int $sampleid
162 * @return \context
163 */
164 abstract public function sample_access_context($sampleid);
165
a40952d3 166 /**
413f19bc 167 * Describes a sample with a description summary and a \renderable (an image for example)
a40952d3
DM
168 *
169 * @param int $sampleid
170 * @param int $contextid
171 * @param array $sampledata
172 * @return array array(string, \renderable)
173 */
369389c9
DM
174 abstract public function sample_description($sampleid, $contextid, $sampledata);
175
369389c9
DM
176 /**
177 * Main analyser method which processes the site analysables.
178 *
413f19bc 179 * @param bool $includetarget
369389c9
DM
180 * @return \stored_file[]
181 */
a8ccc5f2
DM
182 public function get_analysable_data($includetarget) {
183
184 $filesbytimesplitting = array();
185
186 $analysables = $this->get_analysables();
187 foreach ($analysables as $analysable) {
188
189 $files = $this->process_analysable($analysable, $includetarget);
190
191 // Later we will need to aggregate data by time splitting method.
192 foreach ($files as $timesplittingid => $file) {
193 $filesbytimesplitting[$timesplittingid][$analysable->get_id()] = $file;
194 }
195 }
196
197 // We join the datasets by time splitting method.
198 $timesplittingfiles = $this->merge_analysable_files($filesbytimesplitting, $includetarget);
199
200 return $timesplittingfiles;
201 }
369389c9 202
413f19bc
DM
203 /**
204 * Samples data this analyser provides.
205 *
206 * @return string[]
207 */
208 protected function provided_sample_data() {
209 return array($this->get_samples_origin());
210 }
211
212 /**
213 * Returns labelled data (training and evaluation).
214 *
215 * @return array
216 */
369389c9
DM
217 public function get_labelled_data() {
218 return $this->get_analysable_data(true);
219 }
220
413f19bc
DM
221 /**
222 * Returns unlabelled data (prediction).
223 *
224 * @return array
225 */
369389c9
DM
226 public function get_unlabelled_data() {
227 return $this->get_analysable_data(false);
228 }
229
230 /**
231 * Checks if the analyser satisfies all the model indicators requirements.
232 *
233 * @throws \core_analytics\requirements_exception
234 * @return void
235 */
236 protected function check_indicators_requirements() {
237
238 foreach ($this->indicators as $indicator) {
239 $missingrequired = $this->check_indicator_requirements($indicator);
240 if ($missingrequired !== true) {
241 throw new \core_analytics\requirements_exception(get_class($indicator) . ' indicator requires ' .
242 json_encode($missingrequired) . ' sample data which is not provided by ' . get_class($this));
243 }
244 }
245 }
246
a8ccc5f2
DM
247 /**
248 * Merges analysable dataset files into 1.
249 *
250 * @param array $filesbytimesplitting
251 * @param bool $includetarget
252 * @return \stored_file[]
253 */
254 protected function merge_analysable_files($filesbytimesplitting, $includetarget) {
255
256 $timesplittingfiles = array();
257 foreach ($filesbytimesplitting as $timesplittingid => $files) {
258
259 if ($this->options['evaluation'] === true) {
260 // Delete the previous copy. Only when evaluating.
261 \core_analytics\dataset_manager::delete_previous_evaluation_file($this->modelid, $timesplittingid);
262 }
263
264 // Merge all course files into one.
265 if ($includetarget) {
266 $filearea = \core_analytics\dataset_manager::LABELLED_FILEAREA;
267 } else {
268 $filearea = \core_analytics\dataset_manager::UNLABELLED_FILEAREA;
269 }
270 $timesplittingfiles[$timesplittingid] = \core_analytics\dataset_manager::merge_datasets($files,
271 $this->modelid, $timesplittingid, $filearea, $this->options['evaluation']);
272 }
273
274 return $timesplittingfiles;
275 }
276
369389c9 277 /**
413f19bc 278 * Checks that this analyser satisfies the provided indicator requirements.
369389c9
DM
279 *
280 * @param \core_analytics\local\indicator\base $indicator
281 * @return true|string[] True if all good, missing requirements list otherwise
282 */
283 public function check_indicator_requirements(\core_analytics\local\indicator\base $indicator) {
284
285 $providedsampledata = $this->provided_sample_data();
286
287 $requiredsampledata = $indicator::required_sample_data();
288 if (empty($requiredsampledata)) {
289 // The indicator does not need any sample data.
290 return true;
291 }
292 $missingrequired = array_diff($requiredsampledata, $providedsampledata);
293
294 if (empty($missingrequired)) {
295 return true;
296 }
297
298 return $missingrequired;
299 }
300
301 /**
302 * Processes an analysable
303 *
304 * This method returns the general analysable status, an array of files by time splitting method and
305 * an error message if there is any problem.
306 *
307 * @param \core_analytics\analysable $analysable
308 * @param bool $includetarget
309 * @return \stored_file[] Files by time splitting method
310 */
311 public function process_analysable($analysable, $includetarget) {
312
313 // Default returns.
314 $files = array();
315 $message = null;
316
317 // Target instances scope is per-analysable (it can't be lower as calculations run once per
318 // analysable, not time splitting method nor time range).
0690a271 319 $this->analysabletarget = call_user_func(array($this->target, 'instance'));
369389c9
DM
320
321 // We need to check that the analysable is valid for the target even if we don't include targets
322 // as we still need to discard invalid analysables for the target.
0690a271 323 $result = $this->analysabletarget->is_valid_analysable($analysable, $includetarget);
369389c9
DM
324 if ($result !== true) {
325 $a = new \stdClass();
326 $a->analysableid = $analysable->get_id();
327 $a->result = $result;
a40952d3 328 $this->add_log(get_string('analysablenotvalidfortarget', 'analytics', $a));
369389c9
DM
329 return array();
330 }
331
332 // Process all provided time splitting methods.
333 $results = array();
334 foreach ($this->timesplittings as $timesplitting) {
335
336 // For evaluation purposes we don't need to be that strict about how updated the data is,
337 // if this analyser was analysed less that 1 week ago we skip generating a new one. This
338 // helps scale the evaluation process as sites with tons of courses may a lot of time to
339 // complete an evaluation.
340 if (!empty($this->options['evaluation']) && !empty($this->options['reuseprevanalysed'])) {
341
342 $previousanalysis = \core_analytics\dataset_manager::get_evaluation_analysable_file($this->modelid,
343 $analysable->get_id(), $timesplitting->get_id());
1611308b 344 // 1 week is a partly random time interval, no need to worry about DST.
369389c9
DM
345 $boundary = time() - WEEKSECS;
346 if ($previousanalysis && $previousanalysis->get_timecreated() > $boundary) {
347 // Recover the previous analysed file and avoid generating a new one.
348
349 // Don't bother filling a result object as it is only useful when there are no files generated.
350 $files[$timesplitting->get_id()] = $previousanalysis;
351 continue;
352 }
353 }
354
0690a271 355 $result = $this->process_time_splitting($timesplitting, $analysable, $includetarget);
369389c9
DM
356
357 if (!empty($result->file)) {
358 $files[$timesplitting->get_id()] = $result->file;
359 }
360 $results[] = $result;
361 }
362
363 if (empty($files)) {
364 $errors = array();
365 foreach ($results as $timesplittingid => $result) {
366 $errors[] = $timesplittingid . ': ' . $result->message;
367 }
368
369 $a = new \stdClass();
370 $a->analysableid = $analysable->get_id();
413f19bc 371 $a->errors = implode(', ', $errors);
a40952d3 372 $this->add_log(get_string('analysablenotused', 'analytics', $a));
369389c9
DM
373 }
374
375 return $files;
376 }
377
a40952d3 378 /**
413f19bc 379 * Adds a register to the analysis log.
a40952d3
DM
380 *
381 * @param string $string
382 * @return void
383 */
384 public function add_log($string) {
385 $this->log[] = $string;
386 }
387
388 /**
413f19bc 389 * Returns the analysis logs.
a40952d3
DM
390 *
391 * @return string[]
392 */
369389c9
DM
393 public function get_logs() {
394 return $this->log;
395 }
396
413f19bc
DM
397 /**
398 * Processes the analysable samples using the provided time splitting method.
399 *
400 * @param \core_analytics\local\time_splitting\base $timesplitting
401 * @param \core_analytics\analysable $analysable
0690a271 402 * @param bool $includetarget
413f19bc
DM
403 * @return \stdClass Results object.
404 */
0690a271 405 protected function process_time_splitting($timesplitting, $analysable, $includetarget = false) {
369389c9
DM
406
407 $result = new \stdClass();
408
409 if (!$timesplitting->is_valid_analysable($analysable)) {
413f19bc 410 $result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
369389c9
DM
411 $result->message = get_string('invalidanalysablefortimesplitting', 'analytics',
412 $timesplitting->get_name());
413 return $result;
414 }
415 $timesplitting->set_analysable($analysable);
416
417 if (CLI_SCRIPT && !PHPUNIT_TEST) {
413f19bc
DM
418 mtrace('Analysing id "' . $analysable->get_id() . '" with "' . $timesplitting->get_name() .
419 '" time splitting method...');
369389c9
DM
420 }
421
422 // What is a sample is defined by the analyser, it can be an enrolment, a course, a user, a question
423 // attempt... it is on what we will base indicators calculations.
424 list($sampleids, $samplesdata) = $this->get_all_samples($analysable);
425
426 if (count($sampleids) === 0) {
413f19bc 427 $result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
369389c9
DM
428 $result->message = get_string('nodata', 'analytics');
429 return $result;
430 }
431
0690a271 432 if ($includetarget) {
369389c9
DM
433 // All ranges are used when we are calculating data for training.
434 $ranges = $timesplitting->get_all_ranges();
435 } else {
00da1e60
DM
436 // The latest range that has not yet been used for prediction (it depends on the time range where we are right now).
437 $ranges = $this->get_most_recent_prediction_range($timesplitting);
369389c9
DM
438 }
439
440 // There is no need to keep track of the evaluated samples and ranges as we always evaluate the whole dataset.
441 if ($this->options['evaluation'] === false) {
442
443 if (empty($ranges)) {
413f19bc 444 $result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
00da1e60 445 $result->message = get_string('noranges', 'analytics');
369389c9
DM
446 return $result;
447 }
448
00da1e60
DM
449 // We skip all samples that are already part of a training dataset, even if they have not been used for prediction.
450 $this->filter_out_train_samples($sampleids, $timesplitting);
369389c9
DM
451
452 if (count($sampleids) === 0) {
413f19bc 453 $result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
369389c9
DM
454 $result->message = get_string('nonewdata', 'analytics');
455 return $result;
456 }
457
369389c9 458 // Only when processing data for predictions.
0690a271 459 if (!$includetarget) {
00da1e60
DM
460 // We also filter out samples and ranges that have already been used for predictions.
461 $this->filter_out_prediction_samples_and_ranges($sampleids, $ranges, $timesplitting);
462 }
463
464 if (count($sampleids) === 0) {
465 $result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
466 $result->message = get_string('nonewdata', 'analytics');
467 return $result;
369389c9
DM
468 }
469
470 if (count($ranges) === 0) {
413f19bc 471 $result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
00da1e60 472 $result->message = get_string('nonewranges', 'analytics');
369389c9
DM
473 return $result;
474 }
475 }
476
56d4981e
DM
477 if (!empty($target)) {
478 $filearea = \core_analytics\dataset_manager::LABELLED_FILEAREA;
479 } else {
480 $filearea = \core_analytics\dataset_manager::UNLABELLED_FILEAREA;
481 }
369389c9 482 $dataset = new \core_analytics\dataset_manager($this->modelid, $analysable->get_id(), $timesplitting->get_id(),
56d4981e 483 $filearea, $this->options['evaluation']);
369389c9
DM
484
485 // Flag the model + analysable + timesplitting as being analysed (prevent concurrent executions).
1611308b
DM
486 if (!$dataset->init_process()) {
487 // If this model + analysable + timesplitting combination is being analysed we skip this process.
488 $result->status = \core_analytics\model::NO_DATASET;
489 $result->message = get_string('analysisinprogress', 'analytics');
490 return $result;
491 }
492
0690a271
DM
493 // Remove samples the target consider invalid.
494 $this->analysabletarget->add_sample_data($samplesdata);
495 $this->analysabletarget->filter_out_invalid_samples($sampleids, $analysable, $includetarget);
1611308b
DM
496
497 if (!$sampleids) {
498 $result->status = \core_analytics\model::NO_DATASET;
499 $result->message = get_string('novalidsamples', 'analytics');
500 $dataset->close_process();
501 return $result;
502 }
369389c9
DM
503
504 foreach ($this->indicators as $key => $indicator) {
505 // The analyser attaches the main entities the sample depends on and are provided to the
506 // indicator to calculate the sample.
a40952d3
DM
507 $this->indicators[$key]->add_sample_data($samplesdata);
508 }
369389c9
DM
509
510 // Here we start the memory intensive process that will last until $data var is
511 // unset (until the method is finished basically).
0690a271
DM
512 if ($includetarget) {
513 $data = $timesplitting->calculate($sampleids, $this->get_samples_origin(), $this->indicators, $ranges,
514 $this->analysabletarget);
515 } else {
516 $data = $timesplitting->calculate($sampleids, $this->get_samples_origin(), $this->indicators, $ranges);
517 }
369389c9
DM
518
519 if (!$data) {
413f19bc 520 $result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
369389c9 521 $result->message = get_string('novaliddata', 'analytics');
1611308b 522 $dataset->close_process();
369389c9
DM
523 return $result;
524 }
525
10658a1c
DM
526 // Add extra metadata.
527 $this->add_model_metadata($data);
5c5cb3ee 528
369389c9
DM
529 // Write all calculated data to a file.
530 $file = $dataset->store($data);
531
532 // Flag the model + analysable + timesplitting as analysed.
533 $dataset->close_process();
534
535 // No need to keep track of analysed stuff when evaluating.
536 if ($this->options['evaluation'] === false) {
537 // Save the samples that have been already analysed so they are not analysed again in future.
538
0690a271 539 if ($includetarget) {
369389c9
DM
540 $this->save_train_samples($sampleids, $timesplitting, $file);
541 } else {
00da1e60 542 $this->save_prediction_samples($sampleids, $ranges, $timesplitting);
369389c9
DM
543 }
544 }
545
546 $result->status = \core_analytics\model::OK;
547 $result->message = get_string('successfullyanalysed', 'analytics');
548 $result->file = $file;
549 return $result;
550 }
551
413f19bc 552 /**
00da1e60 553 * Returns the most recent range that can be used to predict.
413f19bc
DM
554 *
555 * @param \core_analytics\local\time_splitting\base $timesplitting
556 * @return array
557 */
00da1e60 558 protected function get_most_recent_prediction_range($timesplitting) {
369389c9
DM
559
560 $now = time();
00da1e60
DM
561 $ranges = $timesplitting->get_all_ranges();
562
563 // Opposite order as we are interested in the last range that can be used for prediction.
e4584b81 564 krsort($ranges);
369389c9
DM
565
566 // We already provided the analysable to the time splitting method, there is no need to feed it back.
00da1e60 567 foreach ($ranges as $rangeindex => $range) {
369389c9
DM
568 if ($timesplitting->ready_to_predict($range)) {
569 // We need to maintain the same indexes.
00da1e60 570 return array($rangeindex => $range);
369389c9
DM
571 }
572 }
573
00da1e60 574 return array();
369389c9
DM
575 }
576
413f19bc
DM
577 /**
578 * Filters out samples that have already been used for training.
579 *
580 * @param int[] $sampleids
581 * @param \core_analytics\local\time_splitting\base $timesplitting
413f19bc 582 */
00da1e60 583 protected function filter_out_train_samples(&$sampleids, $timesplitting) {
369389c9
DM
584 global $DB;
585
586 $params = array('modelid' => $this->modelid, 'analysableid' => $timesplitting->get_analysable()->get_id(),
587 'timesplitting' => $timesplitting->get_id());
588
589 $trainingsamples = $DB->get_records('analytics_train_samples', $params);
590
591 // Skip each file trained samples.
592 foreach ($trainingsamples as $trainingfile) {
593
594 $usedsamples = json_decode($trainingfile->sampleids, true);
595
596 if (!empty($usedsamples)) {
597 // Reset $sampleids to $sampleids minus this file's $usedsamples.
598 $sampleids = array_diff_key($sampleids, $usedsamples);
599 }
600 }
369389c9
DM
601 }
602
413f19bc
DM
603 /**
604 * Filters out samples that have already been used for prediction.
605 *
00da1e60 606 * @param int[] $sampleids
413f19bc
DM
607 * @param array $ranges
608 * @param \core_analytics\local\time_splitting\base $timesplitting
413f19bc 609 */
00da1e60 610 protected function filter_out_prediction_samples_and_ranges(&$sampleids, &$ranges, $timesplitting) {
369389c9
DM
611 global $DB;
612
00da1e60
DM
613 if (count($ranges) > 1) {
614 throw new \coding_exception('$ranges argument should only contain one range');
615 }
616
617 $rangeindex = key($ranges);
618
369389c9 619 $params = array('modelid' => $this->modelid, 'analysableid' => $timesplitting->get_analysable()->get_id(),
00da1e60
DM
620 'timesplitting' => $timesplitting->get_id(), 'rangeindex' => $rangeindex);
621 $predictedrange = $DB->get_record('analytics_predict_samples', $params);
369389c9 622
00da1e60
DM
623 if (!$predictedrange) {
624 // Nothing to filter out.
625 return;
369389c9
DM
626 }
627
00da1e60
DM
628 $predictedrange->sampleids = json_decode($predictedrange->sampleids, true);
629 $missingsamples = array_diff_key($sampleids, $predictedrange->sampleids);
630 if (count($missingsamples) === 0) {
631 // All samples already calculated.
632 unset($ranges[$rangeindex]);
633 return;
634 }
369389c9 635
00da1e60
DM
636 // Replace the list of samples by the one excluding samples that already got predictions at this range.
637 $sampleids = $missingsamples;
369389c9
DM
638 }
639
413f19bc
DM
640 /**
641 * Saves samples that have just been used for training.
642 *
643 * @param int[] $sampleids
644 * @param \core_analytics\local\time_splitting\base $timesplitting
645 * @param \stored_file $file
00da1e60 646 * @return void
413f19bc 647 */
369389c9
DM
648 protected function save_train_samples($sampleids, $timesplitting, $file) {
649 global $DB;
650
651 $trainingsamples = new \stdClass();
652 $trainingsamples->modelid = $this->modelid;
653 $trainingsamples->analysableid = $timesplitting->get_analysable()->get_id();
654 $trainingsamples->timesplitting = $timesplitting->get_id();
655 $trainingsamples->fileid = $file->get_id();
656
369389c9
DM
657 $trainingsamples->sampleids = json_encode($sampleids);
658 $trainingsamples->timecreated = time();
659
00da1e60 660 $DB->insert_record('analytics_train_samples', $trainingsamples);
369389c9
DM
661 }
662
413f19bc
DM
663 /**
664 * Saves samples that have just been used for prediction.
665 *
00da1e60 666 * @param int[] $sampleids
413f19bc
DM
667 * @param array $ranges
668 * @param \core_analytics\local\time_splitting\base $timesplitting
669 * @return void
670 */
00da1e60 671 protected function save_prediction_samples($sampleids, $ranges, $timesplitting) {
369389c9
DM
672 global $DB;
673
00da1e60
DM
674 if (count($ranges) > 1) {
675 throw new \coding_exception('$ranges argument should only contain one range');
676 }
677
678 $rangeindex = key($ranges);
369389c9 679
00da1e60
DM
680 $params = array('modelid' => $this->modelid, 'analysableid' => $timesplitting->get_analysable()->get_id(),
681 'timesplitting' => $timesplitting->get_id(), 'rangeindex' => $rangeindex);
682 if ($predictionrange = $DB->get_record('analytics_predict_samples', $params)) {
683 // Append the new samples used for prediction.
684 $prevsamples = json_decode($predictionrange->sampleids, true);
685 $predictionrange->sampleids = json_encode($prevsamples + $sampleids);
686 $predictionrange->timemodified = time();
687 $DB->update_record('analytics_predict_samples', $predictionrange);
688 } else {
689 $predictionrange = (object)$params;
690 $predictionrange->sampleids = json_encode($sampleids);
691 $predictionrange->timecreated = time();
692 $predictionrange->timemodified = $predictionrange->timecreated;
693 $DB->insert_record('analytics_predict_samples', $predictionrange);
369389c9
DM
694 }
695 }
5c5cb3ee
DM
696
697 /**
698 * Adds target metadata to the dataset.
699 *
700 * @param array $data
701 * @return void
702 */
10658a1c
DM
703 protected function add_model_metadata(&$data) {
704 global $CFG;
705
706 $metadata = array(
707 'moodleversion' => $CFG->version,
708 'targetcolumn' => $this->analysabletarget->get_id()
709 );
5c5cb3ee 710 if ($this->analysabletarget->is_linear()) {
10658a1c
DM
711 $metadata['targettype'] = 'linear';
712 $metadata['targetmin'] = $this->analysabletarget::get_min_value();
713 $metadata['targetmax'] = $this->analysabletarget::get_max_value();
5c5cb3ee 714 } else {
10658a1c
DM
715 $metadata['targettype'] = 'discrete';
716 $metadata['targetclasses'] = json_encode($this->analysabletarget::get_classes());
717 }
718
719 foreach ($metadata as $varname => $value) {
720 $data[0][] = $varname;
721 $data[1][] = $value;
5c5cb3ee
DM
722 }
723 }
369389c9 724}