Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
   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   * Base time splitting method.
  19   *
  20   * @package   core_analytics
  21   * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
  22   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace core_analytics\local\time_splitting;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /**
  30   * Base time splitting method.
  31   *
  32   * @package   core_analytics
  33   * @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
  34   * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  abstract class base {
  37  
  38      /**
  39       * @var string
  40       */
  41      protected $id;
  42  
  43      /**
  44       * The model id.
  45       *
  46       * @var int
  47       */
  48      protected $modelid;
  49  
  50      /**
  51       * @var \core_analytics\analysable
  52       */
  53      protected $analysable;
  54  
  55      /**
  56       * @var array
  57       */
  58      protected $ranges = [];
  59  
  60      /**
  61       * Define the time splitting methods ranges.
  62       *
  63       * 'time' value defines when predictions are executed, their values will be compared with
  64       * the current time in ready_to_predict. The ranges should be sorted by 'time' in
  65       * ascending order.
  66       *
  67       * @return array('start' => time(), 'end' => time(), 'time' => time())
  68       */
  69      abstract protected function define_ranges();
  70  
  71      /**
  72       * Returns a lang_string object representing the name for the time splitting method.
  73       *
  74       * Used as column identificator.
  75       *
  76       * If there is a corresponding '_help' string this will be shown as well.
  77       *
  78       * @return \lang_string
  79       */
  80      public static abstract function get_name() : \lang_string;
  81  
  82      /**
  83       * Returns the time splitting method id.
  84       *
  85       * @return string
  86       */
  87      public function get_id() {
  88          return '\\' . get_class($this);
  89      }
  90  
  91      /**
  92       * Assigns the analysable and updates the time ranges according to the analysable start and end dates.
  93       *
  94       * @param \core_analytics\analysable $analysable
  95       * @return void
  96       */
  97      public function set_analysable(\core_analytics\analysable $analysable) {
  98          $this->analysable = $analysable;
  99          $this->ranges = $this->define_ranges();
 100          $this->validate_ranges();
 101      }
 102  
 103      /**
 104       * Assigns the model id to this time-splitting method it case it needs it.
 105       *
 106       * @param int $modelid
 107       */
 108      public function set_modelid(int $modelid) {
 109          $this->modelid = $modelid;
 110      }
 111  
 112      /**
 113       * get_analysable
 114       *
 115       * @return \core_analytics\analysable
 116       */
 117      public function get_analysable() {
 118          return $this->analysable;
 119      }
 120  
 121      /**
 122       * Returns whether the course can be processed by this time splitting method or not.
 123       *
 124       * @param \core_analytics\analysable $analysable
 125       * @return bool
 126       */
 127      public function is_valid_analysable(\core_analytics\analysable $analysable) {
 128          return true;
 129      }
 130  
 131      /**
 132       * Should we predict this time range now?
 133       *
 134       * @param array $range
 135       * @return bool
 136       */
 137      public function ready_to_predict($range) {
 138          if ($range['time'] <= time()) {
 139              return true;
 140          }
 141          return false;
 142      }
 143  
 144      /**
 145       * Should we use this time range for training?
 146       *
 147       * @param array $range
 148       * @return bool
 149       */
 150      public function ready_to_train($range) {
 151          $now = time();
 152          if ($range['time'] <= $now && $range['end'] <= $now) {
 153              return true;
 154          }
 155          return false;
 156      }
 157  
 158      /**
 159       * Returns the ranges used by this time splitting method.
 160       *
 161       * @return array
 162       */
 163      public function get_all_ranges() {
 164          return $this->ranges;
 165      }
 166  
 167      /**
 168       * By default all ranges are for training.
 169       *
 170       * @return array
 171       */
 172      public function get_training_ranges() {
 173          return $this->ranges;
 174      }
 175  
 176      /**
 177       * Returns the distinct range indexes in this time splitting method.
 178       *
 179       * @return int[]
 180       */
 181      public function get_distinct_ranges() {
 182          if ($this->include_range_info_in_training_data()) {
 183              return array_keys($this->ranges);
 184          } else {
 185              return [0];
 186          }
 187      }
 188  
 189      /**
 190       * Returns the most recent range that can be used to predict.
 191       *
 192       * This method is only called when calculating predictions.
 193       *
 194       * @return array
 195       */
 196      public function get_most_recent_prediction_range() {
 197  
 198          $ranges = $this->get_all_ranges();
 199  
 200          // Opposite order as we are interested in the last range that can be used for prediction.
 201          krsort($ranges);
 202  
 203          // We already provided the analysable to the time splitting method, there is no need to feed it back.
 204          foreach ($ranges as $rangeindex => $range) {
 205              if ($this->ready_to_predict($range)) {
 206                  // We need to maintain the same indexes.
 207                  return array($rangeindex => $range);
 208              }
 209          }
 210  
 211          return array();
 212      }
 213  
 214      /**
 215       * Returns range data by its index.
 216       *
 217       * @param int $rangeindex
 218       * @return array|false Range data or false if the index is not part of the existing ranges.
 219       */
 220      public function get_range_by_index($rangeindex) {
 221          if (!isset($this->ranges[$rangeindex])) {
 222              return false;
 223          }
 224          return $this->ranges[$rangeindex];
 225      }
 226  
 227      /**
 228       * Generates a unique sample id (sample in a range index).
 229       *
 230       * @param int $sampleid
 231       * @param int $rangeindex
 232       * @return string
 233       */
 234      public final function append_rangeindex($sampleid, $rangeindex) {
 235          return $sampleid . '-' . $rangeindex;
 236      }
 237  
 238      /**
 239       * Returns the sample id and the range index from a uniquesampleid.
 240       *
 241       * @param string $uniquesampleid
 242       * @return array array($sampleid, $rangeindex)
 243       */
 244      public final function infer_sample_info($uniquesampleid) {
 245          return explode('-', $uniquesampleid);
 246      }
 247  
 248      /**
 249       * Whether to include the range index in the training data or not.
 250       *
 251       * By default, we consider that the different time ranges included in a time splitting method may not be
 252       * compatible between them (i.e. the indicators calculated at the end of the course can easily
 253       * differ from indicators calculated at the beginning of the course). So we include the range index as
 254       * one of the variables that the machine learning backend uses to generate predictions.
 255       *
 256       * If the indicators calculated using the different time ranges available in this time splitting method
 257       * are comparable you can overwrite this method to return false.
 258       *
 259       * Note that:
 260       *  - This is only relevant for models whose predictions are not based on assumptions
 261       *    (i.e. the ones using a machine learning backend to generate predictions).
 262       *  - The ranges can only be included in the training data when
 263       *    we know the final number of ranges the time splitting method will have. E.g.
 264       *    We can not know the final number of ranges of a 'daily' time splitting method
 265       *    as we will have one new range every day.
 266       * @return bool
 267       */
 268      public function include_range_info_in_training_data() {
 269          return true;
 270      }
 271  
 272      /**
 273       * Whether to cache or not the indicator calculations.
 274       *
 275       * Indicator calculations are stored to be reused across models. The calculations
 276       * are indexed by the calculation start and end time, and these times depend on the
 277       * time-splitting method. You should overwrite this method and return false if the time
 278       * frames generated by your time-splitting method are unique and / or can hardly be
 279       * reused by further models.
 280       *
 281       * @return bool
 282       */
 283      public function cache_indicator_calculations(): bool {
 284          return true;
 285      }
 286  
 287      /**
 288       * Is this method valid to evaluate prediction models?
 289       *
 290       * @return bool
 291       */
 292      public function valid_for_evaluation(): bool {
 293          return true;
 294      }
 295  
 296      /**
 297       * Validates the time splitting method ranges.
 298       *
 299       * @throws \coding_exception
 300       * @return void
 301       */
 302      protected function validate_ranges() {
 303          foreach ($this->ranges as $key => $range) {
 304              if (!isset($this->ranges[$key]['start']) || !isset($this->ranges[$key]['end']) ||
 305                      !isset($this->ranges[$key]['time'])) {
 306                  throw new \coding_exception($this->get_id() . ' time splitting method "' . $key .
 307                      '" range is not fully defined. We need a start timestamp and an end timestamp.');
 308              }
 309          }
 310      }
 311  }