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