MDL-59211 analytics: Make cibot happy

Part of MDL-57791 epic.
This commit is contained in:
David Monllao
2017-07-24 08:36:49 +02:00
parent 1611308b58
commit 413f19bc49
138 changed files with 3002 additions and 394 deletions
+1 -1
View File
@@ -81,4 +81,4 @@ theme/boost/scss/fontawesome/
theme/bootstrapbase/less/bootstrap/
theme/bootstrapbase/javascript/html5shiv.js
theme/bootstrapbase/amd/src/bootstrap.js
theme/bootstrapbase/less/fontawesome/
theme/bootstrapbase/less/fontawesome/
+1 -1
View File
@@ -82,4 +82,4 @@ theme/boost/scss/fontawesome/
theme/bootstrapbase/less/bootstrap/
theme/bootstrapbase/javascript/html5shiv.js
theme/bootstrapbase/amd/src/bootstrap.js
theme/bootstrapbase/less/fontawesome/
theme/bootstrapbase/less/fontawesome/
+3 -2
View File
@@ -13,11 +13,12 @@ define(['jquery', 'core/str', 'core/modal_factory', 'core/notification'], functi
/**
* Prepares a modal info for a log's results.
*
* @access public
* @param {int} id
* @return {String} HTML info
* @param {string[]} info
*/
loadInfo : function(id, info) {
loadInfo: function(id, info) {
var link = $('[data-model-log-id="' + id + '"]');
str.get_string('loginfo', 'tool_models').then(function(langString) {
@@ -40,12 +40,22 @@ require_once($CFG->dirroot . '/completion/completion_completion.php');
*/
class course_dropout extends \core_analytics\local\target\binary {
protected $coursegradeitem = null;
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('target:coursedropout', 'tool_models');
}
/**
* prediction_actions
*
* @param \core_analytics\prediction $prediction
* @param bool $includedetailsaction
* @return \core_analytics\prediction_action[]
*/
public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false) {
global $USER;
@@ -57,17 +67,24 @@ class course_dropout extends \core_analytics\local\target\binary {
// Send a message.
$url = new \moodle_url('/message/index.php', array('user' => $USER->id, 'id' => $studentid));
$pix = new \pix_icon('t/message', get_string('sendmessage', 'message'));
$actions['studentmessage'] = new \core_analytics\prediction_action('studentmessage', $prediction, $url, $pix, get_string('sendmessage', 'message'));
$actions['studentmessage'] = new \core_analytics\prediction_action('studentmessage', $prediction, $url, $pix,
get_string('sendmessage', 'message'));
// View outline report.
$url = new \moodle_url('/report/outline/user.php', array('id' => $studentid, 'course' => $sampledata['course']->id,
'mode' => 'outline'));
$pix = new \pix_icon('i/report', get_string('outlinereport'));
$actions['viewoutlinereport'] = new \core_analytics\prediction_action('viewoutlinereport', $prediction, $url, $pix, get_string('outlinereport'));
$actions['viewoutlinereport'] = new \core_analytics\prediction_action('viewoutlinereport', $prediction, $url, $pix,
get_string('outlinereport'));
return $actions;
}
/**
* classes_description
*
* @return string[]
*/
protected static function classes_description() {
return array(
get_string('labelstudentdropoutno', 'tool_models'),
@@ -86,10 +103,22 @@ class course_dropout extends \core_analytics\local\target\binary {
return array();
}
/**
* get_analyser_class
*
* @return string
*/
public function get_analyser_class() {
return '\\core_analytics\\local\\analyser\\student_enrolments';
}
/**
* is_valid_analysable
*
* @param \core_analytics\analysable $course
* @param bool $fortraining
* @return true|string
*/
public function is_valid_analysable(\core_analytics\analysable $course, $fortraining = true) {
global $DB;
@@ -145,7 +174,7 @@ class course_dropout extends \core_analytics\local\target\binary {
* @param int $sampleid
* @param \core_analytics\analysable $course
* @param bool $fortraining
* @return bool
* @return true|string
*/
public function is_valid_sample($sampleid, \core_analytics\analysable $course, $fortraining = true) {
return true;
@@ -160,7 +189,9 @@ class course_dropout extends \core_analytics\local\target\binary {
*
* @param int $sampleid
* @param \core_analytics\analysable $course
* @return void
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $course, $starttime = false, $endtime = false) {
@@ -44,10 +44,22 @@ class no_teaching extends \core_analytics\local\target\binary {
return true;
}
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('target:noteachingactivity', 'tool_models');
}
/**
* prediction_actions
*
* @param \core_analytics\prediction $prediction
* @param mixed $includedetailsaction
* @return \core_analytics\prediction_action[]
*/
public function prediction_actions(\core_analytics\prediction $prediction, $includedetailsaction = false) {
global $USER;
@@ -79,6 +91,11 @@ class no_teaching extends \core_analytics\local\target\binary {
return $actions;
}
/**
* classes_description
*
* @return string[]
*/
protected static function classes_description() {
return array(
get_string('labelteachingyes', 'tool_models'),
@@ -96,10 +113,22 @@ class no_teaching extends \core_analytics\local\target\binary {
return array(0);
}
/**
* get_analyser_class
*
* @return string
*/
public function get_analyser_class() {
return '\\core_analytics\\local\\analyser\\site_courses';
}
/**
* is_valid_analysable
*
* @param \core_analytics\analysable $analysable
* @param mixed $fortraining
* @return true|string
*/
public function is_valid_analysable(\core_analytics\analysable $analysable, $fortraining = true) {
// The analysable is the site, so yes, it is always valid.
return true;
@@ -108,10 +137,10 @@ class no_teaching extends \core_analytics\local\target\binary {
/**
* Only process samples which start date is getting close.
*
* @param mixed $sampleid
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param bool $fortraining
* @return void
* @return true|string
*/
public function is_valid_sample($sampleid, \core_analytics\analysable $analysable, $fortraining = true) {
@@ -132,7 +161,9 @@ class no_teaching extends \core_analytics\local\target\binary {
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @return void
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
@@ -24,6 +24,8 @@
namespace tool_models\output\form;
defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot.'/lib/formslib.php');
/**
+15 -1
View File
@@ -24,8 +24,10 @@
namespace tool_models\output;
defined('MOODLE_INTERNAL') || die();
/**
* Typical crappy helper class with tiny functions.
* Helper class with general purpose tiny functions.
*
* @package tool_models
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
@@ -33,6 +35,12 @@ namespace tool_models\output;
*/
class helper {
/**
* Converts a class full name to a select option key
*
* @param string $class
* @return string
*/
public static function class_to_option($class) {
// Form field is PARAM_ALPHANUMEXT and we are sending fully qualified class names
// as option names, but replacing the backslash for a string that is really unlikely
@@ -40,6 +48,12 @@ class helper {
return str_replace('\\', '2015102400ouuu', $class);
}
/**
* option_to_class
*
* @param string $option
* @return string
*/
public static function option_to_class($option) {
// Really unlikely but yeah, I'm a bad booyyy.
return str_replace('2015102400ouuu', '\\', $option);
@@ -35,8 +35,19 @@ defined('MOODLE_INTERNAL') || die();
*/
class models_list implements \renderable, \templatable {
/**
* models
*
* @var \core_analytics\model[]
*/
protected $models = array();
/**
* __construct
*
* @param \core_analytics\model[] $models
* @return void
*/
public function __construct($models) {
$this->models = $models;
}
@@ -106,7 +106,8 @@ class renderer extends plugin_renderer_base {
if (isset($result->score)) {
// Score.
$output .= $OUTPUT->heading(get_string('accuracy', 'tool_models') . ': ' . round(floatval($result->score), 4) * 100 . '%', 4);
$output .= $OUTPUT->heading(get_string('accuracy', 'tool_models') . ': ' .
round(floatval($result->score), 4) * 100 . '%', 4);
}
if (!empty($result->info)) {
@@ -1,5 +1,4 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
@@ -25,6 +24,8 @@
namespace tool_models\task;
defined('MOODLE_INTERNAL') || die();
/**
* Predict system models with new data available.
*
@@ -34,10 +35,20 @@ namespace tool_models\task;
*/
class predict_models extends \core\task\scheduled_task {
/**
* get_name
*
* @return string
*/
public function get_name() {
return get_string('predictmodels', 'tool_models');
}
/**
* Executes the prediction task.
*
* @return void
*/
public function execute() {
global $OUTPUT, $PAGE;
@@ -1,5 +1,4 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
@@ -25,6 +24,8 @@
namespace tool_models\task;
defined('MOODLE_INTERNAL') || die();
/**
* Train system models with new data available.
*
@@ -34,10 +35,20 @@ namespace tool_models\task;
*/
class train_models extends \core\task\scheduled_task {
/**
* get_name
*
* @return string
*/
public function get_name() {
return get_string('trainmodels', 'tool_models');
}
/**
* Executes the prediction task.
*
* @return void
*/
public function execute() {
global $OUTPUT, $PAGE;
@@ -101,6 +101,13 @@ foreach ($courses as $course) {
$courses->close();
/**
* tool_models_calculate_course_dates
*
* @param stdClass $course
* @param array $options CLI options
* @return void
*/
function tool_models_calculate_course_dates($course, $options) {
global $DB, $OUTPUT;
@@ -153,7 +160,8 @@ function tool_models_calculate_course_dates($course, $options) {
if ($options['update']) {
format_weeks::update_end_date($course->id);
$course->enddate = $DB->get_field('course', 'enddate', array('id' => $course->id));
$notification .= PHP_EOL . ' ' . get_string('weeksenddateautomaticallyset', 'tool_models') . ': ' . userdate($course->enddate);
$notification .= PHP_EOL . ' ' . get_string('weeksenddateautomaticallyset', 'tool_models') . ': ' .
userdate($course->enddate);
} else {
// We can't provide more info without actually updating it in db.
$notification .= PHP_EOL . ' ' . get_string('weeksenddatedefault', 'tool_models');
@@ -194,4 +202,6 @@ function tool_models_calculate_course_dates($course, $options) {
echo mtrace($notification);
}
echo mtrace(get_string('success'));
exit(0);
+4 -2
View File
@@ -17,11 +17,13 @@
/**
* This file defines tasks performed by the tool.
*
* @package tool_monitor
* @copyright 2014 Mark Nelson <markn@moodle.com>
* @package tool_models
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
// List of tasks.
$tasks = array(
array(
+7
View File
@@ -24,6 +24,13 @@
defined('MOODLE_INTERNAL') || die();
/**
* Deletes models created by tool_models
*
* @package tool_models
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
function xmldb_tool_models_uninstall() {
global $DB;
+1 -1
View File
@@ -25,7 +25,7 @@
require_once(__DIR__ . '/../../../config.php');
require_once($CFG->libdir . '/adminlib.php');
admin_externalpage_setup('analyticmodels', '', null, '', array('pagelayout'=>'report'));
admin_externalpage_setup('analyticmodels', '', null, '', array('pagelayout' => 'report'));
$models = \core_analytics\manager::get_all_models();
+1 -1
View File
@@ -17,7 +17,7 @@
/**
* Model-related actions.
*
* @package tool_model
* @package tool_models
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@@ -15,6 +15,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Extension to show an error message if the selected predictor is not available.
*
* @package core_analytics
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
@@ -27,6 +28,13 @@ defined('MOODLE_INTERNAL') || die();
require_once(__DIR__ . '/../../lib/adminlib.php');
/**
* Extension to show an error message if the selected predictor is not available.
*
* @package core_analytics
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class admin_setting_predictor extends \admin_setting_configselect {
/**
@@ -40,7 +48,7 @@ class admin_setting_predictor extends \admin_setting_configselect {
return '';
}
if (!array_key_exists($data, $this->choices)) {
return ''; // ignore it
return '';
}
// Calling it here without checking if it is ready because we check it below and show it as a controlled case.
+25
View File
@@ -15,6 +15,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Any element analysers can analyse.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
@@ -26,6 +27,7 @@ namespace core_analytics;
defined('MOODLE_INTERNAL') || die();
/**
* Any element analysers can analyse.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
@@ -33,13 +35,36 @@ defined('MOODLE_INTERNAL') || die();
*/
interface analysable {
/**
* Max timestamp.
*/
const MAX_TIME = 9999999999;
/**
* The analysable unique identifier in the site.
*
* @return int.
*/
public function get_id();
/**
* The analysable context.
*
* @return \context
*/
public function get_context();
/**
* The start of the analysable if there is one.
*
* @return int|false
*/
public function get_start();
/**
* The end of the analysable if there is one.
*
* @return int|false
*/
public function get_end();
}
+21 -3
View File
@@ -35,10 +35,29 @@ defined('MOODLE_INTERNAL') || die();
*/
abstract class calculable {
/**
* Neutral calculation outcome.
*/
const OUTCOME_NEUTRAL = 0;
/**
* Very positive calculation outcome.
*/
const OUTCOME_VERY_POSITIVE = 1;
/**
* Positive calculation outcome.
*/
const OUTCOME_OK = 2;
/**
* Negative calculation outcome.
*/
const OUTCOME_NEGATIVE = 3;
/**
* Very negative calculation outcome.
*/
const OUTCOME_VERY_NEGATIVE = 4;
/**
@@ -243,7 +262,6 @@ abstract class calculable {
* Merges arrays recursively keeping the same keys the original arrays have.
*
* @link http://php.net/manual/es/function.array-merge-recursive.php#114818
* @param array
* @return array
*/
private function array_merge_recursive_keep_keys() {
@@ -256,8 +274,8 @@ abstract class calculable {
if (is_array($value) && !empty($base[$key]) && is_array($base[$key])) {
$base[$key] = $this->array_merge_recursive_keep_keys($base[$key], $value);
} else {
if(isset($base[$key]) && is_int($key)) {
$key++;
if (isset($base[$key]) && is_int($key)) {
$key++;
}
$base[$key] = $value;
}
+131 -16
View File
@@ -15,6 +15,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Moodle course analysable
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
@@ -30,6 +31,7 @@ require_once($CFG->dirroot . '/lib/gradelib.php');
require_once($CFG->dirroot . '/lib/enrollib.php');
/**
* Moodle course analysable
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
@@ -37,24 +39,81 @@ require_once($CFG->dirroot . '/lib/enrollib.php');
*/
class course implements \core_analytics\analysable {
/**
* @var \core_analytics\course[] $instances
*/
protected static $instances = array();
protected static $studentroles = [];
protected static $teacherroles = [];
/**
* Course object
*
* @var \stdClass
*/
protected $course = null;
/**
* The course context.
*
* @var \context_course
*/
protected $coursecontext = null;
/**
* The course activities organized by activity type.
*
* @var array
*/
protected $courseactivities = array();
/**
* Course start time.
*
* @var int
*/
protected $starttime = null;
/**
* Has the course already started?
*
* @var bool
*/
protected $started = null;
/**
* Course end time.
*
* @var int
*/
protected $endtime = null;
/**
* Is the course finished?
*
* @var bool
*/
protected $finished = null;
/**
* Course students ids.
*
* @var int[]
*/
protected $studentids = [];
/**
* Course teachers ids
*
* @var int[]
*/
protected $teacherids = [];
/**
* Cached copy of the total number of logs in the course.
*
* @var int
*/
protected $ntotallogs = null;
/**
@@ -78,18 +137,14 @@ class course implements \core_analytics\analysable {
$this->coursecontext = \context_course::instance($this->course->id);
if (empty(self::$studentroles)) {
self::$studentroles = array_keys(get_archetype_roles('student'));
}
if (empty(self::$teacherroles)) {
self::$teacherroles = array_keys(get_archetype_roles('editingteacher') + get_archetype_roles('teacher'));
}
$this->now = time();
// Get the course users, including users assigned to student and teacher roles at an higher context.
$this->studentids = $this->get_user_ids(self::$studentroles);
$this->teacherids = $this->get_user_ids(self::$teacherroles);
$studentroles = array_keys(get_archetype_roles('student'));
$this->studentids = $this->get_user_ids($studentroles);
$teacherroles = array_keys(get_archetype_roles('editingteacher') + get_archetype_roles('teacher'));
$this->teacherids = $this->get_user_ids($teacherroles);
}
/**
@@ -114,10 +169,20 @@ class course implements \core_analytics\analysable {
return self::$instances[$courseid];
}
/**
* get_id
*
* @return int
*/
public function get_id() {
return $this->course->id;
}
/**
* get_context
*
* @return \context
*/
public function get_context() {
if ($this->coursecontext === null) {
$this->coursecontext = \context_course::instance($this->course->id);
@@ -147,6 +212,11 @@ class course implements \core_analytics\analysable {
return $this->starttime;
}
/**
* Guesses the start of the course based on students' activity and enrolment start dates.
*
* @return int
*/
public function guess_start() {
global $DB;
@@ -168,8 +238,10 @@ class course implements \core_analytics\analysable {
$select = "userid = :userid AND contextlevel = :contextlevel AND contextinstanceid = :contextinstanceid";
$params = array('userid' => $studentid, 'contextlevel' => CONTEXT_COURSE, 'contextinstanceid' => $this->get_id());
$events = $logstore->get_events_select($select, $params, 'timecreated ASC', 0, 1);
$event = reset($events);
$firstlogs = $event->timecreated;
if ($events) {
$event = reset($events);
$firstlogs[] = $event->timecreated;
}
}
if (empty($firstlogs)) {
// Can't guess if no student accesses.
@@ -257,6 +329,11 @@ class course implements \core_analytics\analysable {
return $this->median($studentlastaccesses);
}
/**
* Returns a course plain object.
*
* @return \stdClass
*/
public function get_course_data() {
return $this->course;
}
@@ -362,6 +439,12 @@ class course implements \core_analytics\analysable {
return $this->ntotallogs;
}
/**
* Returns all the activities of the provided type the course has.
*
* @param string $activitytype
* @return array
*/
public function get_all_activities($activitytype) {
// Using is set because we set it to false if there are no activities.
@@ -383,6 +466,12 @@ class course implements \core_analytics\analysable {
return $this->courseactivities[$activitytype];
}
/**
* Returns the course students grades.
*
* @param array $courseactivities
* @return array
*/
public function get_student_grades($courseactivities) {
if (empty($courseactivities)) {
@@ -410,9 +499,18 @@ class course implements \core_analytics\analysable {
return $grades;
}
/**
* Guesses all activities that were available during a period of time.
*
* @param string $activitytype
* @param int $starttime
* @param int $endtime
* @param \stdClass $student
* @return array
*/
public function get_activities($activitytype, $starttime, $endtime, $student = false) {
// $student may not be available, default to not calculating dynamic data.
// Var $student may not be available, default to not calculating dynamic data.
$studentid = -1;
if ($student) {
$studentid = $student->id;
@@ -432,6 +530,14 @@ class course implements \core_analytics\analysable {
return $timerangeactivities;
}
/**
* Was the activity supposed to be completed during the provided time range?.
*
* @param \cm_info $activity
* @param int $starttime
* @param int $endtime
* @return bool
*/
protected function completed_by(\cm_info $activity, $starttime, $endtime) {
// We can't check uservisible because:
@@ -455,7 +561,7 @@ class course implements \core_analytics\analysable {
}
}
//// We skip activities in sections that were not yet visible or their 'until' was not in this $starttime - $endtime range.
// We skip activities in sections that were not yet visible or their 'until' was not in this $starttime - $endtime range.
$section = $activity->get_modinfo()->get_section_info($activity->sectionnum);
if ($section->availability) {
$info = new \core_availability\info_section($section);
@@ -524,6 +630,14 @@ class course implements \core_analytics\analysable {
return false;
}
/**
* Check if the activity/section should have been completed during the provided period according to its availability rules.
*
* @param \core_availability\info $info
* @param int $starttime
* @param int $endtime
* @return bool|null
*/
protected function availability_completed_by(\core_availability\info $info, $starttime, $endtime) {
$dateconditions = $info->get_availability_tree()->get_all_children('\availability_date\condition');
@@ -576,6 +690,7 @@ class course implements \core_analytics\analysable {
/**
* Returns the query and params used to filter the logstore by this course students.
*
* @param string $prefix
* @return array
*/
protected function course_students_query_filter($prefix = false) {
+60 -3
View File
@@ -15,6 +15,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Datasets manager.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
@@ -26,6 +27,7 @@ namespace core_analytics;
defined('MOODLE_INTERNAL') || die();
/**
* Datasets manager.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
@@ -33,8 +35,19 @@ defined('MOODLE_INTERNAL') || die();
*/
class dataset_manager {
/**
* File area for labelled datasets.
*/
const LABELLED_FILEAREA = 'labelled';
/**
* File area for unlabelled datasets.
*/
const UNLABELLED_FILEAREA = 'unlabelled';
/**
* Evaluation file file name.
*/
const EVALUATION_FILENAME = 'evaluation.csv';
/**
@@ -71,8 +84,13 @@ class dataset_manager {
protected $includetarget;
/**
* Simple constructor.
* Constructor method.
*
* @param int $modelid
* @param int $analysableid
* @param string $timesplittingid
* @param bool $evaluation
* @param bool $includetarget
* @return void
*/
public function __construct($modelid, $analysableid, $timesplittingid, $evaluation = false, $includetarget = false) {
@@ -90,7 +108,8 @@ class dataset_manager {
*/
public function init_process() {
$lockkey = 'modelid:' . $this->modelid . '-analysableid:' . $this->analysableid .
'-timesplitting:' . self::clean_time_splitting_id($this->timesplittingid) . '-includetarget:' . (int)$this->includetarget;
'-timesplitting:' . self::clean_time_splitting_id($this->timesplittingid) .
'-includetarget:' . (int)$this->includetarget;
// Large timeout as processes may be quite long.
$lockfactory = \core\lock\lock_config::get_lock_factory('core_analytics');
@@ -168,6 +187,13 @@ class dataset_manager {
'/timesplitting/' . self::clean_time_splitting_id($timesplittingid) . '/', self::EVALUATION_FILENAME);
}
/**
* Deletes previous evaluation files of this model.
*
* @param int $modelid
* @param string $timesplittingid
* @return bool
*/
public static function delete_previous_evaluation_file($modelid, $timesplittingid) {
$fs = get_file_storage();
if ($file = self::get_previous_evaluation_file($modelid, $timesplittingid)) {
@@ -178,6 +204,14 @@ class dataset_manager {
return false;
}
/**
* Returns this (model + analysable + time splitting) file.
*
* @param int $modelid
* @param int $analysableid
* @param string $timesplittingid
* @return \stored_file
*/
public static function get_evaluation_analysable_file($modelid, $analysableid, $timesplittingid) {
// Delete previous file if it exists.
@@ -196,7 +230,6 @@ class dataset_manager {
* Important! It is the caller responsability to ensure that the datasets are compatible.
*
* @param array $files
* @param string $filename
* @param int $modelid
* @param string $timesplittingid
* @param bool $evaluation
@@ -277,6 +310,12 @@ class dataset_manager {
return $fs->create_file_from_pathname($filerecord, $tmpfilepath);
}
/**
* Returns the dataset file data structured by sampleids using the indicators and target column names.
*
* @param \stored_file $dataset
* @return array
*/
public static function get_structured_data(\stored_file $dataset) {
if ($dataset->get_filearea() !== 'unlabelled') {
@@ -315,6 +354,12 @@ class dataset_manager {
return $calculations;
}
/**
* Delete all files of a model.
*
* @param int $modelid
* @return bool
*/
public static function clear_model_files($modelid) {
$fs = get_file_storage();
return $fs->delete_area_files(\context_system::instance()->id, 'analytics', false, $modelid);
@@ -331,6 +376,12 @@ class dataset_manager {
return clean_param($timesplittingid, PARAM_ALPHANUMEXT);
}
/**
* Returns the file name to be used.
*
* @param strinbool $evaluation
* @return string
*/
protected static function get_filename($evaluation) {
if ($evaluation === true) {
@@ -343,6 +394,12 @@ class dataset_manager {
return $filename;
}
/**
* Returns the file area to be used.
*
* @param bool $includetarget
* @return string
*/
protected static function get_filearea($includetarget) {
if ($includetarget === true) {
+127 -21
View File
@@ -15,6 +15,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Analysers base class.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
@@ -26,6 +27,7 @@ namespace core_analytics\local\analyser;
defined('MOODLE_INTERNAL') || die();
/**
* Analysers base class.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
@@ -33,16 +35,59 @@ defined('MOODLE_INTERNAL') || die();
*/
abstract class base {
/**
* @var int
*/
protected $modelid;
/**
* The model target.
*
* @var \core_analytics\local\target\base
*/
protected $target;
/**
* The model indicators.
*
* @var \core_analytics\local\indicator\base[]
*/
protected $indicators;
/**
* Time splitting methods to use.
*
* Multiple time splitting methods during evaluation and 1 single
* time splitting method once the model is enabled.
*
* @var \core_analytics\local\time_splitting\base[]
*/
protected $timesplittings;
/**
* Execution options.
*
* @var array
*/
protected $options;
/**
* Simple log array.
*
* @var string[]
*/
protected $log;
/**
* Constructor method.
*
* @param int $modelid
* @param \core_analytics\local\target\base $target
* @param \core_analytics\local\indicator\base[] $indicators
* @param \core_analytics\local\time_splitting\base[] $timesplittings
* @param array $options
* @return void
*/
public function __construct($modelid, \core_analytics\local\target\base $target, $indicators, $timesplittings, $options) {
$this->modelid = $modelid;
$this->target = $target;
@@ -61,7 +106,7 @@ abstract class base {
}
/**
* This function returns the list of samples that can be calculated.
* This function returns this analysable list of samples.
*
* @param \core_analytics\analysable $analysable
* @return array array[0] = int[] (sampleids) and array[1] = array (samplesdata)
@@ -69,7 +114,7 @@ abstract class base {
abstract protected function get_all_samples(\core_analytics\analysable $analysable);
/**
* get_samples
* This function returns the samples data from a list of sample ids.
*
* @param int[] $sampleids
* @return array array[0] = int[] (sampleids) and array[1] = array (samplesdata)
@@ -77,7 +122,7 @@ abstract class base {
abstract public function get_samples($sampleids);
/**
* get_sample_analysable
* Returns the analysable of a sample.
*
* @param int $sampleid
* @return \core_analytics\analysable
@@ -85,13 +130,15 @@ abstract class base {
abstract public function get_sample_analysable($sampleid);
/**
* get_samples_origin
* Returns the sample's origin in moodle database.
*
* @return string
*/
abstract protected function get_samples_origin();
/**
* Returns the context of a sample.
*
* moodle/analytics:listinsights will be required at this level to access the sample predictions.
*
* @param int $sampleid
@@ -100,7 +147,7 @@ abstract class base {
abstract public function sample_access_context($sampleid);
/**
* sample_description
* Describes a sample with a description summary and a \renderable (an image for example)
*
* @param int $sampleid
* @param int $contextid
@@ -109,10 +156,6 @@ abstract class base {
*/
abstract public function sample_description($sampleid, $contextid, $sampledata);
protected function provided_sample_data() {
return array($this->get_samples_origin());
}
/**
* Main analyser method which processes the site analysables.
*
@@ -121,14 +164,34 @@ abstract class base {
* In most of the cases you should have enough extending from one of these classes so you don't need
* to reimplement this method.
*
* @param bool $includetarget
* @return \stored_file[]
*/
abstract public function get_analysable_data($includetarget);
/**
* Samples data this analyser provides.
*
* @return string[]
*/
protected function provided_sample_data() {
return array($this->get_samples_origin());
}
/**
* Returns labelled data (training and evaluation).
*
* @return array
*/
public function get_labelled_data() {
return $this->get_analysable_data(true);
}
/**
* Returns unlabelled data (prediction).
*
* @return array
*/
public function get_unlabelled_data() {
return $this->get_analysable_data(false);
}
@@ -151,7 +214,7 @@ abstract class base {
}
/**
* check_indicator_requirements
* Checks that this analyser satisfies the provided indicator requirements.
*
* @param \core_analytics\local\indicator\base $indicator
* @return true|string[] True if all good, missing requirements list otherwise
@@ -248,7 +311,7 @@ abstract class base {
$a = new \stdClass();
$a->analysableid = $analysable->get_id();
$a->errors = implode(', ', $errors);
$a->errors = implode(', ', $errors);
$this->add_log(get_string('analysablenotused', 'analytics', $a));
}
@@ -256,7 +319,7 @@ abstract class base {
}
/**
* add_log
* Adds a register to the analysis log.
*
* @param string $string
* @return void
@@ -266,7 +329,7 @@ abstract class base {
}
/**
* get_logs
* Returns the analysis logs.
*
* @return string[]
*/
@@ -274,12 +337,20 @@ abstract class base {
return $this->log;
}
/**
* Processes the analysable samples using the provided time splitting method.
*
* @param \core_analytics\local\time_splitting\base $timesplitting
* @param \core_analytics\analysable $analysable
* @param \core_analytics\local\target\base|false $target
* @return \stdClass Results object.
*/
protected function process_time_splitting($timesplitting, $analysable, $target = false) {
$result = new \stdClass();
if (!$timesplitting->is_valid_analysable($analysable)) {
$result->status = \core_analytics\model::ANALYSE_REJECTED_RANGE_PROCESSOR;
$result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
$result->message = get_string('invalidanalysablefortimesplitting', 'analytics',
$timesplitting->get_name());
return $result;
@@ -287,7 +358,8 @@ abstract class base {
$timesplitting->set_analysable($analysable);
if (CLI_SCRIPT && !PHPUNIT_TEST) {
mtrace('Analysing id "' . $analysable->get_id() . '" with "' . $timesplitting->get_name() . '" time splitting method...');
mtrace('Analysing id "' . $analysable->get_id() . '" with "' . $timesplitting->get_name() .
'" time splitting method...');
}
// What is a sample is defined by the analyser, it can be an enrolment, a course, a user, a question
@@ -295,7 +367,7 @@ abstract class base {
list($sampleids, $samplesdata) = $this->get_all_samples($analysable);
if (count($sampleids) === 0) {
$result->status = \core_analytics\model::ANALYSE_REJECTED_RANGE_PROCESSOR;
$result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
$result->message = get_string('nodata', 'analytics');
return $result;
}
@@ -312,7 +384,7 @@ abstract class base {
if ($this->options['evaluation'] === false) {
if (empty($ranges)) {
$result->status = \core_analytics\model::ANALYSE_REJECTED_RANGE_PROCESSOR;
$result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
$result->message = get_string('nonewdata', 'analytics');
return $result;
}
@@ -321,7 +393,7 @@ abstract class base {
$sampleids = $this->filter_out_train_samples($sampleids, $timesplitting);
if (count($sampleids) === 0) {
$result->status = \core_analytics\model::ANALYSE_REJECTED_RANGE_PROCESSOR;
$result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
$result->message = get_string('nonewdata', 'analytics');
return $result;
}
@@ -335,7 +407,7 @@ abstract class base {
}
if (count($ranges) === 0) {
$result->status = \core_analytics\model::ANALYSE_REJECTED_RANGE_PROCESSOR;
$result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
$result->message = get_string('nonewtimeranges', 'analytics');
return $result;
}
@@ -375,13 +447,12 @@ abstract class base {
$target->add_sample_data($samplesdata);
}
// Here we start the memory intensive process that will last until $data var is
// unset (until the method is finished basically).
$data = $timesplitting->calculate($sampleids, $this->get_samples_origin(), $this->indicators, $ranges, $target);
if (!$data) {
$result->status = \core_analytics\model::ANALYSE_REJECTED_RANGE_PROCESSOR;
$result->status = \core_analytics\model::ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD;
$result->message = get_string('novaliddata', 'analytics');
$dataset->close_process();
return $result;
@@ -410,6 +481,12 @@ abstract class base {
return $result;
}
/**
* Returns the ranges of a time splitting that can be used to predict.
*
* @param \core_analytics\local\time_splitting\base $timesplitting
* @return array
*/
protected function get_prediction_ranges($timesplitting) {
$now = time();
@@ -426,6 +503,13 @@ abstract class base {
return $predictionranges;
}
/**
* Filters out samples that have already been used for training.
*
* @param int[] $sampleids
* @param \core_analytics\local\time_splitting\base $timesplitting
* @return int[]
*/
protected function filter_out_train_samples($sampleids, $timesplitting) {
global $DB;
@@ -448,6 +532,13 @@ abstract class base {
return $sampleids;
}
/**
* Filters out samples that have already been used for prediction.
*
* @param array $ranges
* @param \core_analytics\local\time_splitting\base $timesplitting
* @return int[]
*/
protected function filter_out_prediction_ranges($ranges, $timesplitting) {
global $DB;
@@ -465,6 +556,14 @@ abstract class base {
}
/**
* Saves samples that have just been used for training.
*
* @param int[] $sampleids
* @param \core_analytics\local\time_splitting\base $timesplitting
* @param \stored_file $file
* @return bool
*/
protected function save_train_samples($sampleids, $timesplitting, $file) {
global $DB;
@@ -481,6 +580,13 @@ abstract class base {
return $DB->insert_record('analytics_train_samples', $trainingsamples);
}
/**
* Saves samples that have just been used for prediction.
*
* @param array $ranges
* @param \core_analytics\local\time_splitting\base $timesplitting
* @return void
*/
protected function save_prediction_ranges($ranges, $timesplitting) {
global $DB;
@@ -15,6 +15,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Abstract analyser in course basis.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
@@ -26,6 +27,7 @@ namespace core_analytics\local\analyser;
defined('MOODLE_INTERNAL') || die();
/**
* Abstract analyser in course basis.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
@@ -33,6 +35,11 @@ defined('MOODLE_INTERNAL') || die();
*/
abstract class by_course extends base {
/**
* Return the list of courses to analyse.
*
* @return \core_analytics\course[]
*/
public function get_courses() {
// Default to all system courses.
@@ -58,6 +65,12 @@ abstract class by_course extends base {
return $analysables;
}
/**
* Returns the analysed data
*
* @param bool $includetarget
* @return \stored_file[]
*/
public function get_analysable_data($includetarget) {
$status = array();
@@ -82,6 +95,13 @@ abstract class by_course extends base {
return $timesplittingfiles;
}
/**
* Merges analysable dataset files into 1.
*
* @param array $filesbytimesplitting
* @param bool $includetarget
* @return \stored_file[]
*/
protected function merge_analysable_files($filesbytimesplitting, $includetarget) {
$timesplittingfiles = array();
+21 -5
View File
@@ -35,12 +35,17 @@ defined('MOODLE_INTERNAL') || die();
*/
class courses extends by_course {
/**
* Samples origin is course table.
*
* @return string
*/
public function get_samples_origin() {
return 'course';
}
/**
* get_sample_analysable
* Returns the analysable of a sample
*
* @param int $sampleid
* @return \core_analytics\analysable
@@ -49,16 +54,27 @@ class courses extends by_course {
return \core_analytics\course::instance($sampleid);
}
/**
* This provides samples' course and context.
*
* @return string[]
*/
protected function provided_sample_data() {
return array('course', 'context');
}
/**
* Returns the context of a sample.
*
* @param int $sampleid
* @return \context
*/
public function sample_access_context($sampleid) {
return \context_course::instance($sampleid);
}
/**
* get_all_samples
* This will return just one course as we analyse 'by_course'.
*
* @param \core_analytics\analysable $course
* @return array
@@ -76,7 +92,7 @@ class courses extends by_course {
}
/**
* get_samples
* Returns samples data from sample ids.
*
* @param int[] $sampleids
* @return array
@@ -99,12 +115,12 @@ class courses extends by_course {
}
/**
* sample_description
* Returns the sample description
*
* @param int $sampleid
* @param int $contextid
* @param array $sampledata
* @return array
* @return array array(string, \renderable)
*/
public function sample_description($sampleid, $contextid, $sampledata) {
$description = format_string($sampledata['course']->fullname, true, array('context' => $sampledata['context']));
@@ -35,12 +35,17 @@ defined('MOODLE_INTERNAL') || die();
*/
class site_courses extends sitewide {
/**
* Samples origin is course table.
*
* @return string
*/
public function get_samples_origin() {
return 'course';
}
/**
* get_sample_analysable
* Returns the sample analysable
*
* @param int $sampleid
* @return \core_analytics\analysable
@@ -49,14 +54,31 @@ class site_courses extends sitewide {
return new \core_analytics\site();
}
/**
* Data this analyer samples provide.
*
* @return string[]
*/
protected function provided_sample_data() {
return array('course', 'context');
}
/**
* Returns the sample context.
*
* @param int $sampleid
* @return \context
*/
public function sample_access_context($sampleid) {
return \context_system::instance();
}
/**
* Returns all site courses.
*
* @param \core_analytics\analysable $site
* @return array
*/
protected function get_all_samples(\core_analytics\analysable $site) {
global $DB;
@@ -77,6 +99,12 @@ class site_courses extends sitewide {
return array($sampleids, $courses);
}
/**
* Return all complete samples data from sample ids.
*
* @param int[] $sampleids
* @return array
*/
public function get_samples($sampleids) {
global $DB;
@@ -95,12 +123,12 @@ class site_courses extends sitewide {
}
/**
* sample_description
* Returns the description of a sample.
*
* @param int $sampleid
* @param int $contextid
* @param array $sampledata
* @return void
* @return array array(string, \renderable)
*/
public function sample_description($sampleid, $contextid, $sampledata) {
$description = format_string($sampledata['course']->fullname, true, array('context' => $sampledata['context']));
+10 -6
View File
@@ -15,6 +15,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Site-level contents abstract analysable.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
@@ -26,6 +27,7 @@ namespace core_analytics\local\analyser;
defined('MOODLE_INTERNAL') || die();
/**
* Site-level contents abstract analysable.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
@@ -33,14 +35,16 @@ defined('MOODLE_INTERNAL') || die();
*/
abstract class sitewide extends base {
protected function get_site() {
return new \core_analytics\site();
}
/**
* Returns the analysable data.
*
* @param bool $includetarget
* @return \stored_file[] One file for each time splitting method.
*/
public function get_analysable_data($includetarget) {
// Here there is a single analysable and it is the system.
$analysable = $this->get_site();
$analysable = new \core_analytics\site();
$return = array();
@@ -54,7 +58,7 @@ abstract class sitewide extends base {
\core_analytics\dataset_manager::delete_previous_evaluation_file($this->modelid, $timesplittingid);
}
// We use merge but it is just a copy
// We use merge but it is just a copy.
$files[$timesplittingid] = \core_analytics\dataset_manager::merge_datasets(array($file), $this->modelid,
$timesplittingid, $this->options['evaluation'], $includetarget);
}
@@ -15,6 +15,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Student enrolments analyser.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
@@ -28,6 +29,9 @@ defined('MOODLE_INTERNAL') || die();
require_once($CFG->dirroot . '/lib/enrollib.php');
/**
* Student enrolments analyser.
*
* It does return all student enrolments including the suspended ones.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
@@ -40,28 +44,52 @@ class student_enrolments extends by_course {
*/
protected $samplecourses = array();
/**
* Defines the origin of the samples in the database.
*
* @return string
*/
protected function get_samples_origin() {
return 'user_enrolments';
}
/**
* Returns the student enrolment course context.
*
* @param int $sampleid
* @return \context
*/
public function sample_access_context($sampleid) {
return \context_course::instance($this->get_sample_courseid($sampleid));
}
/**
* Returns the student enrolment course.
*
* @param int $sampleid
* @return \core_analytics\analysable
*/
public function get_sample_analysable($sampleid) {
$course = enrol_get_course_by_user_enrolment_id($sampleid);
return \core_analytics\course::instance($course);
}
/**
* Data provided by get_all_samples & get_samples.
*
* @return string[]
*/
protected function provided_sample_data() {
return array('user_enrolments', 'context', 'course', 'user');
}
/**
* All course enrolments.
* All course student enrolments.
*
* It does return all student enrolments including the suspended ones.
*
* @param \core_analytics\analysable $course
* @return void
* @return array
*/
protected function get_all_samples(\core_analytics\analysable $course) {
@@ -111,6 +139,12 @@ class student_enrolments extends by_course {
return array(array_combine($enrolids, $enrolids), $samplesdata);
}
/**
* Returns all samples from the samples ids.
*
* @param int[] $sampleids
* @return array
*/
public function get_samples($sampleids) {
global $DB;
@@ -189,7 +223,7 @@ class student_enrolments extends by_course {
* @param int $sampleid
* @param int $contextid
* @param array $sampledata
* @return array
* @return array array(string, \renderable)
*/
public function sample_description($sampleid, $contextid, $sampledata) {
$description = fullname($sampledata['user'], true, array('context' => $contextid));
@@ -35,14 +35,33 @@ defined('MOODLE_INTERNAL') || die();
*/
class any_access_after_end extends binary {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:accessesafterend', 'analytics');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
return array('user', 'course', 'context');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $samplesorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $samplesorigin, $starttime = false, $endtime = false) {
global $DB;
@@ -35,14 +35,33 @@ defined('MOODLE_INTERNAL') || die();
*/
class any_access_before_start extends binary {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:accessesbeforestart', 'analytics');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
return array('user', 'course', 'context');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $samplesorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $samplesorigin, $starttime = false, $endtime = false) {
global $DB;
@@ -35,15 +35,34 @@ defined('MOODLE_INTERNAL') || die();
*/
class any_write_action extends binary {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:anywrite', 'analytics');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
// User is not required, calculate_sample can handle its absence.
return array('context');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $sampleorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
global $DB;
+40 -16
View File
@@ -35,12 +35,18 @@ defined('MOODLE_INTERNAL') || die();
*/
abstract class base extends \core_analytics\calculable {
/**
* Min value an indicator can return.
*/
const MIN_VALUE = -1;
/**
* Max value an indicator can return.
*/
const MAX_VALUE = 1;
/**
* Converts the calculated indicators to feature/s.
* Converts the calculated indicators to dataset feature/s.
*
* @param float|int[] $calculatedvalues
* @return array
@@ -60,26 +66,57 @@ abstract class base extends \core_analytics\calculable {
*/
abstract protected function calculate_sample($sampleid, $sampleorigin, $starttime, $endtime);
/**
* Should this value be displayed?
*
* Indicators providing multiple features can be used this method to discard some of them.
*
* @param float $value
* @param string $subtype
* @return bool
*/
public function should_be_displayed($value, $subtype) {
// We should everything by default.
return true;
}
/**
* @return null|string
* Allows indicators to specify data they need.
*
* e.g. A model using courses as samples will not provide users data, but an indicator like
* "user is hungry" needs user data.
*
* @return null|string[] Name of the required elements (use the database tablename)
*/
public static function required_sample_data() {
return null;
}
/**
* Returns an instance of the indicator.
*
* Useful to reset cached data.
*
* @return \core_analytics\local\indicator\base
*/
public static function instance() {
return new static();
}
/**
* Returns the maximum value an indicator calculation can return.
*
* @return float
*/
public static function get_max_value() {
return self::MAX_VALUE;
}
/**
* Returns the minimum value an indicator calculation can return.
*
* @return float
*/
public static function get_min_value() {
return self::MIN_VALUE;
}
@@ -89,7 +126,7 @@ abstract class base extends \core_analytics\calculable {
*
* Returns an array of values which size matches $sampleids size.
*
* @param array $sampleids
* @param int[] $sampleids
* @param string $samplesorigin
* @param integer $starttime Limit the calculation to this timestart
* @param integer $endtime Limit the calculation to this timeend
@@ -118,17 +155,4 @@ abstract class base extends \core_analytics\calculable {
return $calculations;
}
protected static function add_samples_averages() {
return false;
}
protected static function get_average_columns() {
return array('mean');
}
protected function calculate_averages($values) {
$mean = array_sum($values) / count($values);
return array($mean);
}
}
@@ -35,6 +35,11 @@ defined('MOODLE_INTERNAL') || die();
*/
abstract class binary extends discrete {
/**
* get_classes
*
* @return array
*/
public static final function get_classes() {
// It does not really matter, all \core_analytics\local\indicator\discrete get_classes calls have been overwriten as we
// only need 1 column here.
@@ -79,11 +84,22 @@ abstract class binary extends discrete {
}
}
/**
* get_feature_headers
*
* @return array
*/
public static function get_feature_headers() {
// Just 1 single feature obtained from the calculated value.
return array(get_called_class());
}
/**
* to_features
*
* @param array $calculatedvalues
* @return array
*/
protected function to_features($calculatedvalues) {
// Indicators with binary values have only 1 feature for indicator, here we do nothing else
// than converting each sample scalar value to an array of scalars with 1 element.
@@ -35,10 +35,12 @@ defined('MOODLE_INTERNAL') || die();
*/
abstract class community_of_inquiry_activity extends linear {
protected $course = null;
/**
* TODO This should ideally be reused by cognitive depth and social breadth.
*
* @var \core_analytics\course
*/
protected $course = null;
/**
* @var array Array of logs by [contextid][userid]
*/
protected $activitylogs = null;
@@ -49,12 +51,12 @@ abstract class community_of_inquiry_activity extends linear {
protected $grades = null;
/**
* @const Constant cognitive indicator type.
* Constant cognitive indicator type.
*/
const INDICATOR_COGNITIVE = "cognitve";
/**
* @const Constant social indicator type.
* Constant social indicator type.
*/
const INDICATOR_SOCIAL = "social";
@@ -63,7 +65,7 @@ abstract class community_of_inquiry_activity extends linear {
*
* @var string The activity name (e.g. assign or quiz)
*/
final protected function get_activity_type() {
protected final function get_activity_type() {
$class = get_class($this);
$package = stristr($class, "\\", true);
$type = str_replace("mod_", "", $package);
@@ -73,21 +75,47 @@ abstract class community_of_inquiry_activity extends linear {
return $type;
}
/**
* Returns the potential level of cognitive depth.
*
* @param \cm_info $cm
* @return int
*/
protected function get_cognitive_depth_level(\cm_info $cm) {
throw new \coding_exception('Overwrite get_cognitive_depth_level method to set your activity potential cognitive ' .
'depth level');
}
/**
* Returns the potential level of social breadth.
*
* @param \cm_info $cm
* @return int
*/
protected function get_social_breadth_level(\cm_info $cm) {
throw new \coding_exception('Overwrite get_social_breadth_level method to set your activity potential social ' .
'breadth level');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
// Only course because the indicator is valid even without students.
return array('course');
}
/**
* Do activity logs contain any log of user in this context?
*
* If user is empty we look for any log in this context.
*
* @param int $contextid
* @param \stdClass|false $user
* @return bool
*/
protected final function any_log($contextid, $user) {
if (empty($this->activitylogs[$contextid])) {
return false;
@@ -103,6 +131,15 @@ abstract class community_of_inquiry_activity extends linear {
return false;
}
/**
* Do activity logs contain any write log of user in this context?
*
* If user is empty we look for any write log in this context.
*
* @param int $contextid
* @param \stdClass|false $user
* @return bool
*/
protected final function any_write_log($contextid, $user) {
if (empty($this->activitylogs[$contextid])) {
return false;
@@ -127,6 +164,17 @@ abstract class community_of_inquiry_activity extends linear {
return false;
}
/**
* Is there any feedback activity log for this user in this context?
*
* This method returns true if $user is empty and there is any feedback activity logs.
*
* @param string $action
* @param \cm_info $cm
* @param int $contextid
* @param \stdClass|false $user
* @return bool
*/
protected function any_feedback($action, \cm_info $cm, $contextid, $user) {
if (!in_array($action, 'submitted', 'replied', 'viewed')) {
@@ -175,29 +223,76 @@ abstract class community_of_inquiry_activity extends linear {
return $this->feedback_post_action($cm, $contextid, $userid, $this->feedback_viewed_events(), $after);
}
/**
* $cm is used for this method overrides.
*
* This function must be fast.
*
* @param \cm_info $cm
* @param mixed $contextid
* @param mixed $userid
* @param int $after Timestamp, defaults to the graded date or false if we don't check the date.
* @return bool
*/
protected function feedback_replied(\cm_info $cm, $contextid, $userid, $after = null) {
return $this->feedback_post_action($cm, $contextid, $userid, $this->feedback_replied_events(), $after);
}
/**
* $cm is used for this method overrides.
*
* This function must be fast.
*
* @param \cm_info $cm
* @param mixed $contextid
* @param mixed $userid
* @param int $after Timestamp, defaults to the graded date or false if we don't check the date.
* @return bool
*/
protected function feedback_submitted(\cm_info $cm, $contextid, $userid, $after = null) {
return $this->feedback_post_action($cm, $contextid, $userid, $this->feedback_submitted_events(), $after);
}
/**
* Returns the list of events that involve viewing feedback from other users.
*
* @return string[]
*/
protected function feedback_viewed_events() {
throw new \coding_exception('Activities with a potential cognitive or social level that include viewing feedback ' .
'should define "feedback_viewed_events" method or should override feedback_viewed method.');
}
/**
* Returns the list of events that involve replying to feedback from other users.
*
* @return string[]
*/
protected function feedback_replied_events() {
throw new \coding_exception('Activities with a potential cognitive or social level that include replying to feedback ' .
'should define "feedback_replied_events" method or should override feedback_replied method.');
}
/**
* Returns the list of events that involve submitting something after receiving feedback from other users.
*
* @return string[]
*/
protected function feedback_submitted_events() {
throw new \coding_exception('Activities with a potential cognitive or social level that include viewing feedback ' .
'should define "feedback_submitted_events" method or should override feedback_submitted method.');
}
/**
* Whether this user in this context did any of the provided actions (events)
*
* @param \cm_info $cm
* @param int $contextid
* @param int $userid
* @param string[] $eventnames
* @param int|false $after
* @return bool
*/
protected function feedback_post_action(\cm_info $cm, $contextid, $userid, $eventnames, $after = null) {
if ($after === null) {
if ($this->feedback_check_grades()) {
@@ -237,7 +332,7 @@ abstract class community_of_inquiry_activity extends linear {
}
/**
* get_graded_date
* Returns the date a user was graded.
*
* @param int $contextid
* @param int $userid
@@ -268,6 +363,15 @@ abstract class community_of_inquiry_activity extends linear {
return $after;
}
/**
* Returns the activities the user had access to between a time period.
*
* @param int $sampleid
* @param string $tablename
* @param int $starttime
* @param int $endtime
* @return array
*/
protected function get_student_activities($sampleid, $tablename, $starttime, $endtime) {
// May not be available.
@@ -307,6 +411,14 @@ abstract class community_of_inquiry_activity extends linear {
return $useractivities;
}
/**
* Fetch acitivity logs from database
*
* @param array $activities
* @param int $starttime
* @param int $endtime
* @return array
*/
protected function fetch_activity_logs($activities, $starttime = false, $endtime = false) {
global $DB;
@@ -330,7 +442,7 @@ abstract class community_of_inquiry_activity extends linear {
$processedevents[$event->contextid][$event->userid] = array();
}
// contextid and userid have already been used to index the events, the next field to index by is eventname:
// Contextid and userid have already been used to index the events, the next field to index by is eventname:
// crud is unique per eventname, courseid is the same for all records and we append timecreated.
if (!isset($processedevents[$event->contextid][$event->userid][$event->eventname])) {
@@ -358,7 +470,7 @@ abstract class community_of_inquiry_activity extends linear {
/**
* Whether grades should be checked or not when looking for feedback.
*
* @return void
* @return bool
*/
protected function feedback_check_grades() {
return true;
@@ -367,10 +479,10 @@ abstract class community_of_inquiry_activity extends linear {
/**
* cognitive_calculate_sample
*
* @param $sampleid
* @param $tablename
* @param bool $starttime
* @param bool $endtime
* @param int $sampleid
* @param string $tablename
* @param int $starttime
* @param int $endtime
* @return float|int|null
* @throws \coding_exception
*/
@@ -404,7 +516,7 @@ abstract class community_of_inquiry_activity extends linear {
$score += $scoreperlevel * 5;
break;
}
// The user didn't reach the activity max cognitive depth, continue with level 2.
// The user didn't reach the activity max cognitive depth, continue with level 2.
case 4:
// Cognitive level 4 is to comment on feedback.
@@ -412,7 +524,7 @@ abstract class community_of_inquiry_activity extends linear {
$score += $scoreperlevel * 4;
break;
}
// The user didn't reach the activity max cognitive depth, continue with level 2.
// The user didn't reach the activity max cognitive depth, continue with level 2.
case 3:
// Cognitive level 3 is to view feedback.
@@ -422,7 +534,7 @@ abstract class community_of_inquiry_activity extends linear {
$score += $scoreperlevel * 3;
break;
}
// The user didn't reach the activity max cognitive depth, continue with level 2.
// The user didn't reach the activity max cognitive depth, continue with level 2.
case 2:
// Cognitive depth level 2 is to submit content.
@@ -431,7 +543,7 @@ abstract class community_of_inquiry_activity extends linear {
$score += $scoreperlevel * 2;
break;
}
// The user didn't reach the activity max cognitive depth, continue with level 1.
// The user didn't reach the activity max cognitive depth, continue with level 1.
case 1:
// Cognitive depth level 1 is just accessing the activity.
@@ -456,10 +568,10 @@ abstract class community_of_inquiry_activity extends linear {
/**
* social_calculate_sample
*
* @param $sampleid
* @param $tablename
* @param bool $starttime
* @param bool $endtime
* @param int $sampleid
* @param string $tablename
* @param int $starttime
* @param int $endtime
* @return float|int|null
*/
protected function social_calculate_sample($sampleid, $tablename, $starttime = false, $endtime = false) {
@@ -486,14 +598,15 @@ abstract class community_of_inquiry_activity extends linear {
// TODO Add support for other levels than 2.
switch ($potentiallevel) {
case 2:
// Social breadth level 2 is to view feedback. (Same as cognitive level 3)
// Social breadth level 2 is to view feedback. (Same as cognitive level 3).
if ($this->any_feedback('viewed', $cm, $contextid, $user)) {
// Max score for level 2.
$score += $scoreperlevel * 2;
break;
}
// The user didn't reach the activity max social breadth, continue with level 1.
// The user didn't reach the activity max social breadth, continue with level 1.
case 1:
// Social breadth level 1 is just accessing the activity.
if ($this->any_log($contextid, $user)) {
@@ -515,12 +628,12 @@ abstract class community_of_inquiry_activity extends linear {
/**
* calculate_sample
*
* @throws \coding_exception
* @param int $sampleid
* @param string $tablename
* @param bool $starttime
* @param bool $endtime
* @param int $starttime
* @param int $endtime
* @return float|int|null
* @throws \coding_exception
*/
protected function calculate_sample($sampleid, $tablename, $starttime = false, $endtime = false) {
if ($this->get_indicator_type() == self::INDICATOR_COGNITIVE) {
@@ -534,7 +647,7 @@ abstract class community_of_inquiry_activity extends linear {
/**
* Defines indicator type.
*
* @return mixed
* @return string
*/
abstract protected function get_indicator_type();
}
@@ -44,6 +44,11 @@ abstract class discrete extends base {
throw new \coding_exception('Please overwrite get_classes() specifying your discrete-values\' indicator classes');
}
/**
* Returns 1 feature header for each of the classes.
*
* @return string[]
*/
public static function get_feature_headers() {
$fullclassname = get_called_class();
@@ -55,6 +60,13 @@ abstract class discrete extends base {
return $headers;
}
/**
* Whether the value should be displayed or not.
*
* @param float $value
* @param string $subtype
* @return bool
*/
public function should_be_displayed($value, $subtype) {
if ($value != static::get_max_value()) {
// Discrete values indicators are converted internally to 1 feature per indicator, we are only interested
@@ -94,6 +106,14 @@ abstract class discrete extends base {
return \core_analytics\calculable::OUTCOME_NEUTRAL;
}
/**
* From calculated values to dataset features.
*
* One column for each class.
*
* @param float[] $calculatedvalues
* @return float[]
*/
protected function to_features($calculatedvalues) {
$classes = self::get_classes();
@@ -44,6 +44,11 @@ abstract class linear extends base {
return true;
}
/**
* get_feature_headers
*
* @return array
*/
public static function get_feature_headers() {
$fullclassname = get_called_class();
@@ -57,6 +62,13 @@ abstract class linear extends base {
return $headers;
}
/**
* should_be_displayed
*
* @param float $value
* @param string $subtype
* @return bool
*/
public function should_be_displayed($value, $subtype) {
if ($subtype != false) {
return false;
@@ -91,6 +103,12 @@ abstract class linear extends base {
}
}
/**
* Converts the calculated values to a list of features for the dataset.
*
* @param array $calculatedvalues
* @return array
*/
protected function to_features($calculatedvalues) {
// Null mean if all calculated values are null.
@@ -35,15 +35,34 @@ defined('MOODLE_INTERNAL') || die();
*/
class read_actions extends linear {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:readactions', 'analytics');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
// User is not required, calculate_sample can handle its absence.
return array('context');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $sampleorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
$select = '';
@@ -67,7 +86,7 @@ class read_actions extends linear {
// # Done absolutely nothing
// # Not much really, just accessing the course once a week
// # More than just accessing the course, some interaction
// # Significant contribution, will depend on the course anyway
// # Significant contribution, will depend on the course anyway.
// We need to adapt the limits to the time range duration.
$nweeks = $this->get_time_range_weeks_number($starttime, $endtime);
@@ -35,15 +35,33 @@ defined('MOODLE_INTERNAL') || die();
*/
class user_profile_set extends linear {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:completeduserprofile', 'analytics');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
return array('user');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $sampleorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $sampleorigin, $starttime = false, $endtime = false) {
global $CFG;
@@ -35,14 +35,33 @@ defined('MOODLE_INTERNAL') || die();
*/
class user_track_forums extends binary {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:userforumstracking', 'analytics');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
return array('user');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $samplesorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $samplesorigin, $starttime = false, $endtime = false) {
$user = $this->retrieve('user', $sampleid);
return ($user->trackforums) ? self::get_max_value() : self::get_min_value();
+4 -3
View File
@@ -186,7 +186,6 @@ abstract class base extends \core_analytics\calculable {
$message->smallmessage = get_string('insightinfomessage', 'analytics', $insighturl->out());
$message->contexturl = $insighturl->out(false);
message_send($message);
}
}
@@ -240,7 +239,7 @@ abstract class base extends \core_analytics\calculable {
* Should the model callback be triggered?
*
* @param mixed $predictedvalue
* @param float $predictedscore
* @param float $predictionscore
* @return bool
*/
public function triggers_callback($predictedvalue, $predictionscore) {
@@ -292,7 +291,8 @@ abstract class base extends \core_analytics\calculable {
$calculatedvalue = $this->calculate_sample($sampleid, $analysable, $starttime, $endtime);
if (!is_null($calculatedvalue)) {
if ($this->is_linear() && ($calculatedvalue > static::get_max_value() || $calculatedvalue < static::get_min_value())) {
if ($this->is_linear() &&
($calculatedvalue > static::get_max_value() || $calculatedvalue < static::get_min_value())) {
throw new \coding_exception('Calculated values should be higher than ' . static::get_min_value() .
' and lower than ' . static::get_max_value() . '. ' . $calculatedvalue . ' received');
} else if (!$this->is_linear() && static::is_a_class($calculatedvalue) === false) {
@@ -310,6 +310,7 @@ abstract class base extends \core_analytics\calculable {
*
* @param int[] $sampleids
* @param \core_analytics\analysable $analysable
* @param bool $fortraining
* @return void
*/
public function filter_out_invalid_samples(&$sampleids, \core_analytics\analysable $analysable, $fortraining = true) {
+17 -2
View File
@@ -35,9 +35,12 @@ defined('MOODLE_INTERNAL') || die();
*/
abstract class binary extends discrete {
/**
* is_linear
*
* @return bool
*/
public function is_linear() {
// TODO Remove this discrete override once prediction processors support
// multiclass classifiers; this method will be moved to discrete.
return false;
}
@@ -62,6 +65,13 @@ abstract class binary extends discrete {
return array(0);
}
/**
* Is the calculated value a positive outcome of this target?
*
* @param string $value
* @param string $ignoredsubtype
* @return int
*/
public function get_calculation_outcome($value, $ignoredsubtype = false) {
if (!self::is_a_class($value)) {
@@ -81,6 +91,11 @@ abstract class binary extends discrete {
return self::OUTCOME_VERY_POSITIVE;
}
/**
* classes_description
*
* @return string[]
*/
protected static function classes_description() {
return array(
get_string('yes'),
+19 -6
View File
@@ -35,11 +35,22 @@ defined('MOODLE_INTERNAL') || die();
*/
abstract class discrete extends base {
/**
* Are this target calculations linear values?
*
* @return bool
*/
public function is_linear() {
// Not supported yet.
throw new \coding_exception('Sorry, this version\'s prediction processors only support targets with binary values.');
}
/**
* Is the provided class one of this target valid classes?
*
* @param string $class
* @return bool
*/
protected static function is_a_class($class) {
return (in_array($class, static::get_classes()));
}
@@ -48,7 +59,7 @@ abstract class discrete extends base {
* get_display_value
*
* @param float $value
* @param string $subtype
* @param string $ignoredsubtype
* @return string
*/
public function get_display_value($value, $ignoredsubtype = false) {
@@ -57,17 +68,19 @@ abstract class discrete extends base {
throw new \moodle_exception('errorpredictionformat', 'analytics');
}
// array_values to discard any possible weird keys devs used.
// To discard any possible weird keys devs used.
$classes = array_values(static::get_classes());
$descriptions = array_values(static::classes_description());
if (count($classes) !== count($descriptions)) {
throw new \coding_exception('You need to describe all your classes (' . json_encode($classes) . ') in self::classes_description');
throw new \coding_exception('You need to describe all your classes (' . json_encode($classes) .
') in self::classes_description');
}
$key = array_search($value, $classes);
if ($key === false) {
throw new \coding_exception('You need to describe all your classes (' . json_encode($classes) . ') in self::classes_description');
throw new \coding_exception('You need to describe all your classes (' . json_encode($classes) .
') in self::classes_description');
}
return $descriptions[$key];
@@ -91,8 +104,8 @@ abstract class discrete extends base {
return self::OUTCOME_OK;
}
debugging('Please overwrite \core_analytics\local\target\discrete::get_calculation_outcome, all your target classes are styled ' .
'the same way otherwise', DEBUG_DEVELOPER);
debugging('Please overwrite \core_analytics\local\target\discrete::get_calculation_outcome, all your target ' .
'classes are styled the same way otherwise', DEBUG_DEVELOPER);
return self::OUTCOME_OK;
}
+12
View File
@@ -35,11 +35,23 @@ defined('MOODLE_INTERNAL') || die();
*/
abstract class linear extends base {
/**
* Are the calculated values this target returns linear values?
*
* @return bool
*/
public function is_linear() {
// Not supported yet.
throw new \coding_exception('Sorry, this version\'s prediction processors only support targets with binary values.');
}
/**
* How positive is this calculated value?
*
* @param float $value
* @param string $ignoredsubtype
* @return int
*/
public function get_calculated_outcome($value, $ignoredsubtype = false) {
// This is very generic, targets will probably be interested in overwriting this.
@@ -26,6 +26,13 @@ namespace core_analytics\local\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* Base time splitting method.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class base {
/**
@@ -59,8 +66,22 @@ abstract class base {
*/
protected static $indicators = [];
/**
* Define the time splitting methods ranges.
*
* 'time' value defines when predictions are executed, their values will be compared with
* the current time in ready_to_predict
*
* @return array('start' => time(), 'end' => time(), 'time' => time())
*/
abstract protected function define_ranges();
/**
* The time splitting method name.
*
* It is very recommendable to overwrite this method as this name appears in the UI.
* @return string
*/
public function get_name() {
return $this->get_id();
}
@@ -86,6 +107,11 @@ abstract class base {
$this->validate_ranges();
}
/**
* get_analysable
*
* @return \core_analytics\analysable
*/
public function get_analysable() {
return $this->analysable;
}
@@ -93,12 +119,19 @@ abstract class base {
/**
* Returns whether the course can be processed by this time splitting method or not.
*
* @param \core_analytics\analysable $analysable
* @return bool
*/
public function is_valid_analysable(\core_analytics\analysable $analysable) {
return true;
}
/**
* Should we predict this time range now?
*
* @param array $range
* @return bool
*/
public function ready_to_predict($range) {
if ($range['time'] <= time()) {
return true;
@@ -111,7 +144,7 @@ abstract class base {
*
* @param array $sampleids
* @param string $samplesorigin
* @param \core_analytics\local\indicator\base $indicators
* @param \core_analytics\local\indicator\base[] $indicators
* @param array $ranges
* @param \core_analytics\local\target\base $target
* @return array
@@ -155,6 +188,10 @@ abstract class base {
/**
* Calculates indicators.
*
* @param array $sampleids
* @param string $samplesorigin
* @param \core_analytics\local\indicator\base[] $indicators
* @param array $ranges
* @return array
*/
protected function calculate_indicators($sampleids, $samplesorigin, $indicators, $ranges) {
@@ -197,6 +234,8 @@ abstract class base {
*
* This will identify the sample as belonging to a specific range.
*
* @param array $dataset
* @param array $calculatedtarget
* @return void
*/
protected function fill_dataset(&$dataset, $calculatedtarget = false) {
@@ -244,6 +283,9 @@ abstract class base {
* .....
* ----------------------------------------------------
*
* @param array $dataset
* @param \core_analytics\local\indicator\base[] $indicators
* @param \core_analytics\local\target\base|false $target
* @return void
*/
protected function add_metadata(&$dataset, $indicators, $target = false) {
@@ -284,6 +326,7 @@ abstract class base {
/**
* get_range_by_index
*
* @param int $rangeindex
* @return array
*/
public function get_range_by_index($rangeindex) {
@@ -293,14 +336,34 @@ abstract class base {
return $this->ranges[$rangeindex];
}
/**
* Generates a unique sample id (sample in a range index).
*
* @param int $sampleid
* @param int $rangeindex
* @return string
*/
public function append_rangeindex($sampleid, $rangeindex) {
return $sampleid . '-' . $rangeindex;
}
public function infer_sample_info($sampleid) {
return explode('-', $sampleid);
/**
* Returns the sample id and the range index from a uniquesampleid.
*
* @param string $uniquesampleid
* @return array array($sampleid, $rangeindex)
*/
public function infer_sample_info($uniquesampleid) {
return explode('-', $uniquesampleid);
}
/**
* Returns the headers for the csv file based on the indicators and the target.
*
* @param \core_analytics\local\indicator\base[] $indicators
* @param \core_analytics\local\target\base|false $target
* @return string[]
*/
protected function get_headers($indicators, $target = false) {
// 3rd column will contain the indicator ids.
$headers = array();
@@ -335,7 +398,7 @@ abstract class base {
/**
* Validates the time splitting method ranges.
*
* @throw \coding_exception
* @throws \coding_exception
* @return void
*/
protected function validate_ranges() {
@@ -35,10 +35,20 @@ defined('MOODLE_INTERNAL') || die();
*/
class deciles extends base {
/**
* get_name
*
* @return string
*/
public function get_name() {
return get_string('timesplitting:deciles', 'analytics');
}
/**
* define_ranges
*
* @return array
*/
protected function define_ranges() {
$rangeduration = floor(($this->analysable->get_end() - $this->analysable->get_start()) / 10);
@@ -35,10 +35,20 @@ defined('MOODLE_INTERNAL') || die();
*/
class deciles_accum extends base {
/**
* get_name
*
* @return string
*/
public function get_name() {
return get_string('timesplitting:decilesaccum', 'analytics');
}
/**
* define_ranges
*
* @return array
*/
protected function define_ranges() {
$rangeduration = floor(($this->analysable->get_end() - $this->analysable->get_start()) / 10);
@@ -28,16 +28,41 @@ namespace core_analytics\local\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* No time splitting method.
*
* Used when time is not a factor to consider into the equation.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class no_splitting extends base {
/**
* get_name
*
* @return string
*/
public function get_name() {
return get_string('timesplitting:nosplitting', 'analytics');
}
/**
* ready_to_predict
*
* @param array $range
* @return true
*/
public function ready_to_predict($range) {
return true;
}
/**
* define_ranges
*
* @return array
*/
protected function define_ranges() {
return [
[
@@ -35,10 +35,20 @@ defined('MOODLE_INTERNAL') || die();
*/
class quarters extends base {
/**
* get_name
*
* @return string
*/
public function get_name() {
return get_string('timesplitting:quarters', 'analytics');
}
/**
* define_ranges
*
* @return array
*/
protected function define_ranges() {
$duration = floor(($this->analysable->get_end() - $this->analysable->get_start()) / 4);
return [
@@ -35,10 +35,20 @@ defined('MOODLE_INTERNAL') || die();
*/
class quarters_accum extends base {
/**
* get_name
*
* @return string
*/
public function get_name() {
return get_string('timesplitting:quartersaccum', 'analytics');
}
/**
* define_ranges
*
* @return array
*/
protected function define_ranges() {
$duration = floor(($this->analysable->get_end() - $this->analysable->get_start()) / 4);
return [
@@ -26,14 +26,31 @@ namespace core_analytics\local\time_splitting;
defined('MOODLE_INTERNAL') || die();
/**
* Single time splitting method.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class single_range extends base {
/**
* get_name
*
* @return string
*/
public function get_name() {
return get_string('timesplitting:singlerange', 'analytics');
}
/**
* One single range covering all analysable duration.
*
* @return array
*/
protected function define_ranges() {
// time == 0 because we want it to start predicting from the beginning.
// Key 'time' == 0 because we want it to start predicting from the beginning.
return [
[
'start' => $this->analysable->get_start(),
@@ -1,80 +0,0 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Weekly time splitting method.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_analytics\local\time_splitting;
defined('MOODLE_INTERNAL') || die();
abstract class weekly extends base {
public function get_name() {
return get_string('timesplitting:weekly', 'analytics');
}
public function is_valid_analysable(\core_analytics\analysable $analysable) {
$diff = $analysable->get_end() - $analysable->get_start();
$nweeks = round($diff / WEEKSECS);
if ($nweeks > 520) {
// More than 10 years...
return get_string('coursetoolong', 'analytics');
}
return parent::is_valid_analysable($analysable);
}
protected function define_ranges() {
$ranges = array();
// It is more important to work with a proper end date than the start date.
$i = 0;
do {
$dt = new \DateTime();
$dt->setTimestamp($this->analysable->get_end());
$dt->modify('-' . $i . ' weeks');
$rangeend = $dt->getTimestamp();
$dt->modify('-1 weeks');
$rangestart = $dt->getTimestamp();
$ranges[] = array(
'start' => $rangestart,
'end' => $rangeend,
'time' => $rangeend
);
$i++;
} while ($this->analysable->get_start() < $rangestart);
$ranges = array_reverse($ranges, false);
// Is not worth trying to predict during the first weeks.
array_shift($ranges);
array_shift($ranges);
return $ranges;
}
}
@@ -1,82 +0,0 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Weekly time splitting method.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_analytics\local\time_splitting;
defined('MOODLE_INTERNAL') || die();
abstract class weekly_accum extends base {
public function get_name() {
return get_string('timesplitting:weeklyaccum', 'analytics');
}
public function is_valid_analysable(\core_analytics\analysable $analysable) {
$diff = $analysable->get_end() - $analysable->get_start();
$nweeks = round($diff / WEEKSECS);
if ($nweeks > 520) {
// More than 10 years...
return false;
}
return parent::is_valid_analysable($analysable);
}
protected function define_ranges() {
$ranges = array();
// It is more important to work with a proper end date than start date.
$i = 0;
do {
$dt = new \DateTime();
$dt->setTimestamp($this->analysable->get_end());
$dt->modify('-' . $i . ' weeks');
$rangeend = $dt->getTimestamp();
// Used to calculate when we are done creating new ranges.
$dt->modify('-1 weeks');
$rangestart = $dt->getTimestamp();
// Accumulative, always from the course start.
$ranges[] = array(
'start' => $this->analysable->get_start(),
'end' => $rangeend,
'time' => $rangeend
);
$i++;
} while ($this->analysable->get_start() < $rangestart);
$ranges = array_reverse($ranges, false);
// Is not worth trying to predict during the first weeks.
array_shift($ranges);
array_shift($ranges);
return $ranges;
}
}
+2 -2
View File
@@ -335,7 +335,7 @@ class manager {
self::check_can_list_insights($context);
$models = \core_analytics\manager::get_all_models(true, true, $context);
$models = self::get_all_models(true, true, $context);
foreach ($models as $key => $model) {
// Check that it not only have predictions but also generates insights from them.
if (!$model->uses_insights()) {
@@ -366,7 +366,7 @@ class manager {
$context = \context::instance_by_id($predictionobj->contextid);
}
\core_analytics\manager::check_can_list_insights($context);
self::check_can_list_insights($context);
$model = new \core_analytics\model($predictionobj->modelid);
$sampledata = $model->prediction_sample_data($predictionobj);
+46 -6
View File
@@ -35,19 +35,59 @@ defined('MOODLE_INTERNAL') || die();
*/
class model {
/**
* All as expected.
*/
const OK = 0;
/**
* There was a problem.
*/
const GENERAL_ERROR = 1;
/**
* No dataset to analyse.
*/
const NO_DATASET = 2;
/**
* Model with low prediction accuracy.
*/
const EVALUATE_LOW_SCORE = 4;
/**
* Not enough data to evaluate the model properly.
*/
const EVALUATE_NOT_ENOUGH_DATA = 8;
const ANALYSE_REJECTED_RANGE_PROCESSOR = 4;
/**
* Invalid analysable for the time splitting method.
*/
const ANALYSABLE_REJECTED_TIME_SPLITTING_METHOD = 4;
/**
* Invalid analysable for all time splitting methods.
*/
const ANALYSABLE_STATUS_INVALID_FOR_RANGEPROCESSORS = 8;
/**
* Invalid analysable for the target
*/
const ANALYSABLE_STATUS_INVALID_FOR_TARGET = 16;
/**
* Minimum score to consider a non-static prediction model as good.
*/
const MIN_SCORE = 0.7;
/**
* Maximum standard deviation between different evaluation repetitions to consider that evaluation results are stable.
*/
const ACCEPTED_DEVIATION = 0.05;
/**
* Number of evaluation repetitions.
*/
const EVALUATION_ITERATIONS = 10;
/**
@@ -552,7 +592,7 @@ class model {
if ($this->is_static()) {
// Prediction based on assumptions.
$result->status = \core_analytics\model::OK;
$result->status = self::OK;
$result->info = [];
$result->predictions = $this->get_static_predictions($indicatorcalculations);
@@ -589,7 +629,7 @@ class model {
if ($predictorresult->predictions) {
foreach ($predictorresult->predictions as $sampleinfo) {
// We parse each prediction
// We parse each prediction.
switch (count($sampleinfo)) {
case 1:
// For whatever reason the predictions processor could not process this sample, we
@@ -620,7 +660,7 @@ class model {
* Execute the prediction callbacks defined by the target.
*
* @param \stdClass[] $predictions
* @param array $predictions
* @param array $indicatorcalculations
* @return array
*/
protected function execute_prediction_callbacks($predictions, $indicatorcalculations) {
@@ -636,8 +676,8 @@ class model {
list($sampleid, $rangeindex) = $this->get_time_splitting()->infer_sample_info($uniquesampleid);
// Store the predicted values.
$samplecontext = $this->save_prediction($sampleid, $rangeindex, $prediction->prediction, $prediction->predictionscore,
json_encode($indicatorcalculations[$uniquesampleid]));
$samplecontext = $this->save_prediction($sampleid, $rangeindex, $prediction->prediction,
$prediction->predictionscore, json_encode($indicatorcalculations[$uniquesampleid]));
// Also store all samples context to later generate insights or whatever action the target wants to perform.
$samplecontexts[$samplecontext->id] = $samplecontext;
+44 -1
View File
@@ -15,6 +15,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Representation of a prediction.
*
* @package core_analytics
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
@@ -26,6 +27,7 @@ namespace core_analytics;
defined('MOODLE_INTERNAL') || die();
/**
* Representation of a prediction.
*
* @package core_analytics
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
@@ -33,12 +35,28 @@ defined('MOODLE_INTERNAL') || die();
*/
class prediction {
/**
* @var \stdClass
*/
private $prediction;
/**
* @var array
*/
private $sampledata;
/**
* @var array
*/
private $calculations = array();
/**
* Constructor
*
* @param \stdClass $prediction
* @param array $sampledata
* @return void
*/
public function __construct($prediction, $sampledata) {
global $DB;
@@ -52,18 +70,38 @@ class prediction {
$this->format_calculations();
}
/**
* Get prediction object data.
*
* @return \stdClass
*/
public function get_prediction_data() {
return $this->prediction;
}
/**
* Get prediction sample data.
*
* @return array
*/
public function get_sample_data() {
return $this->sampledata;
}
/**
* Gets the prediction calculations
*
* @return array
*/
public function get_calculations() {
return $this->calculations;
}
/**
* format_calculations
*
* @return \stdClass[]
*/
private function format_calculations() {
$calculations = json_decode($this->prediction->calculations, true);
@@ -86,6 +124,12 @@ class prediction {
}
}
/**
* parse_feature_name
*
* @param string $featurename
* @return string[]
*/
private function parse_feature_name($featurename) {
$indicatorclass = $featurename;
@@ -100,5 +144,4 @@ class prediction {
return array($indicatorclass, $subtype);
}
}
+6 -1
View File
@@ -15,6 +15,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Representation of a suggested action associated with a prediction.
*
* @package core_analytics
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
@@ -26,6 +27,7 @@ namespace core_analytics;
defined('MOODLE_INTERNAL') || die();
/**
* Representation of a suggested action associated with a prediction.
*
* @package core_analytics
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
@@ -52,7 +54,8 @@ class prediction_action {
public function __construct($actionname, \core_analytics\prediction $prediction, \moodle_url $actionurl, \pix_icon $icon, $text, $primary = false) {
// We want to track how effective are our suggested actions, we pass users through a script that will log these actions.
$params = array('action' => $actionname, 'predictionid' => $prediction->get_prediction_data()->id, 'forwardurl' => $actionurl->out(false));
$params = array('action' => $actionname, 'predictionid' => $prediction->get_prediction_data()->id,
'forwardurl' => $actionurl->out(false));
$url = new \moodle_url('/report/insights/action.php', $params);
if ($primary === false) {
@@ -63,6 +66,8 @@ class prediction_action {
}
/**
* Returns the link to the action.
*
* @return \action_menu_link
*/
public function get_action_link() {
+31
View File
@@ -35,11 +35,42 @@ defined('MOODLE_INTERNAL') || die();
*/
interface predictor {
/**
* Is it ready to predict?
*
* @return bool
*/
public function is_ready();
/**
* Train the provided dataset.
*
* @param int $modelid
* @param \stored_file $dataset
* @param string $outputdir
* @return \stdClass
*/
public function train($modelid, \stored_file $dataset, $outputdir);
/**
* Predict the provided dataset samples.
*
* @param int $modelid
* @param \stored_file $dataset
* @param string $outputdir
* @return \stdClass
*/
public function predict($modelid, \stored_file $dataset, $outputdir);
/**
* evaluate
*
* @param int $modelid
* @param float $maxdeviation
* @param int $niterations
* @param \stored_file $dataset
* @param string $outputdir
* @return \stdClass
*/
public function evaluate($modelid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir);
}
+2
View File
@@ -15,6 +15,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Moodle site analysable.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
@@ -26,6 +27,7 @@ namespace core_analytics;
defined('MOODLE_INTERNAL') || die();
/**
* Moodle site analysable.
*
* @package core_analytics
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
+35 -10
View File
@@ -33,6 +33,11 @@ defined('MOODLE_INTERNAL') || die();
*/
class core_analytics_course_activities_testcase extends advanced_testcase {
/**
* availability_levels
*
* @return array
*/
public function availability_levels() {
return array(
'activity' => array('activity'),
@@ -51,7 +56,7 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
list($course, $stu1) = $this->setup_course();
// forum1 is ignored as section 0 does not count.
// Forum1 is ignored as section 0 does not count.
$forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
$courseman = new \core_analytics\course($course);
@@ -89,18 +94,27 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
$cm = $modinfo->get_cm($forum->cmid);
// Condition from after provided end time.
$this->assertCount(0, $courseman->get_activities('forum', strtotime('2015-10-20 00:00:00 GMT'), strtotime('2015-10-21 00:00:00 GMT'), $stu1));
$this->assertCount(0, $courseman->get_activities('forum', strtotime('2015-10-20 00:00:00 GMT'),
strtotime('2015-10-21 00:00:00 GMT'), $stu1));
// Condition until before provided start time
$this->assertCount(0, $courseman->get_activities('forum', strtotime('2015-10-25 00:00:00 GMT'), strtotime('2015-10-26 00:00:00 GMT'), $stu1));
$this->assertCount(0, $courseman->get_activities('forum', strtotime('2015-10-25 00:00:00 GMT'),
strtotime('2015-10-26 00:00:00 GMT'), $stu1));
// Condition until after provided end time.
$this->assertCount(0, $courseman->get_activities('forum', strtotime('2015-10-22 00:00:00 GMT'), strtotime('2015-10-23 00:00:00 GMT'), $stu1));
$this->assertCount(0, $courseman->get_activities('forum', strtotime('2015-10-22 00:00:00 GMT'),
strtotime('2015-10-23 00:00:00 GMT'), $stu1));
// Condition until after provided start time and before provided end time.
$this->assertCount(1, $courseman->get_activities('forum', strtotime('2015-10-22 00:00:00 GMT'), strtotime('2015-10-25 00:00:00 GMT'), $stu1));
$this->assertCount(1, $courseman->get_activities('forum', strtotime('2015-10-22 00:00:00 GMT'),
strtotime('2015-10-25 00:00:00 GMT'), $stu1));
}
/**
* test_get_activities_with_weeks
*
* @return void
*/
public function test_get_activities_with_weeks() {
$startdate = gmmktime('0', '0', '0', 10, 24, 2015);
@@ -112,7 +126,7 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
list($course, $stu1) = $this->setup_course($record);
// forum1 is ignored as section 0 does not count.
// Forum1 is ignored as section 0 does not count.
$forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
array('section' => 0));
$forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
@@ -138,9 +152,14 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
$this->assertCount(2, $courseman->get_activities('forum', $forth, $forth + WEEKSECS, $stu1));
}
/**
* test_get_activities_by_section
*
* @return void
*/
public function test_get_activities_by_section() {
// This makes debugging easier, sorry WA's +8 :)
// This makes debugging easier, sorry WA's +8 :).
$this->setTimezone('UTC');
// 1 year.
@@ -156,7 +175,7 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
list($course, $stu1) = $this->setup_course($record);
// forum1 is ignored as section 0 does not count.
// Forum1 is ignored as section 0 does not count.
$forum1 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
array('section' => 0));
$forum2 = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
@@ -187,8 +206,8 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
// Split the course in as many parts as sections.
$duration = ($enddate - $startdate) / $numsections;
for($i = 1; $i <= $numsections; $i++) {
// -1 because section 1 start represents the course start.
for ($i = 1; $i <= $numsections; $i++) {
// The -1 because section 1 start represents the course start.
$timeranges[$i] = $startdate + ($duration * ($i - 1));
}
$this->assertCount(1, $courseman->get_activities('forum', $timeranges[1], $timeranges[1] + $duration, $stu1));
@@ -202,6 +221,12 @@ class core_analytics_course_activities_testcase extends advanced_testcase {
$this->assertCount(0, $courseman->get_activities('forum', $timeranges[3], $timeranges[3] + $duration, $stu1));
}
/**
* setup_course
*
* @param stdClass $courserecord
* @return array
*/
protected function setup_course($courserecord = null) {
global $CFG;
+1 -2
View File
@@ -47,7 +47,6 @@ class core_analytics_course_testcase extends advanced_testcase {
$this->editingteacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
$this->teacherroleid = $DB->get_field('role', 'id', array('shortname' => 'teacher'));
$this->getDataGenerator()->enrol_user($this->stu1->id, $this->course->id, $this->studentroleid);
$this->getDataGenerator()->enrol_user($this->stu2->id, $this->course->id, $this->studentroleid);
$this->getDataGenerator()->enrol_user($this->both->id, $this->course->id, $this->studentroleid);
@@ -69,7 +68,7 @@ class core_analytics_course_testcase extends advanced_testcase {
$this->assertCount(2, $courseman->get_user_ids(array($this->editingteacherroleid)));
$this->assertCount(1, $courseman->get_user_ids(array($this->teacherroleid)));
// Distinct is applied
// Distinct is applied.
$this->assertCount(3, $courseman->get_user_ids(array($this->editingteacherroleid, $this->teacherroleid)));
$this->assertCount(4, $courseman->get_user_ids(array($this->editingteacherroleid, $this->studentroleid)));
}
+50 -1
View File
@@ -1,15 +1,65 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Test indicator.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Test indicator.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_indicator_fullname extends \core_analytics\local\indicator\linear {
/**
* include_averages
*
* @return bool
*/
protected static function include_averages() {
return false;
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
return array('course');
}
/**
* calculate_sample
*
* @param int $sampleid
* @param string $samplesorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $samplesorigin, $starttime, $endtime) {
global $DB;
@@ -26,5 +76,4 @@ class test_indicator_fullname extends \core_analytics\local\indicator\linear {
return self::MAX_VALUE;
}
}
}
+41
View File
@@ -1,6 +1,47 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Test indicator.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Test indicator.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_indicator_max extends \core_analytics\local\indicator\binary {
/**
* calculate_sample
*
* @param int $sampleid
* @param string $samplesorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $samplesorigin, $starttime, $endtime) {
return self::MAX_VALUE;
}
+41
View File
@@ -1,6 +1,47 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Test indicator.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Test indicator.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_indicator_min extends \core_analytics\local\indicator\binary {
/**
* calculate_sample
*
* @param int $sampleid
* @param string $samplesorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $samplesorigin, $starttime, $endtime) {
return self::MIN_VALUE;
}
+40 -2
View File
@@ -1,10 +1,48 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Test indicator.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Test indicator.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_indicator_random extends \core_analytics\local\indicator\binary {
/**
* calculate_sample
*
* @param int $sampleid
* @param string $samplesorigin
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, $samplesorigin, $starttime, $endtime) {
global $DB;
return mt_rand(-1, 1);
}
}
+87 -12
View File
@@ -1,13 +1,59 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Test static target.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Test static target.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_static_target_shortname extends \core_analytics\local\target\binary {
/**
* predictions
*
* @var array
*/
protected $predictions = array();
/**
* get_analyser_class
*
* @return string
*/
public function get_analyser_class() {
return '\core_analytics\local\analyser\site_courses';
}
/**
* classes_description
*
* @return string[]
*/
public static function classes_description() {
return array(
'Course fullname first char is A',
@@ -17,7 +63,7 @@ class test_static_target_shortname extends \core_analytics\local\target\binary {
/**
* We don't want to discard results.
* @return float
* @return null
*/
protected function min_prediction_score() {
return null;
@@ -31,24 +77,53 @@ class test_static_target_shortname extends \core_analytics\local\target\binary {
return array();
}
/**
* is_valid_analysable
*
* @param \core_analytics\analysable $analysable
* @param bool $fortraining
* @return bool
*/
public function is_valid_analysable(\core_analytics\analysable $analysable, $fortraining = true) {
// This is testing, let's make things easy.
return true;
}
protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
global $DB;
$sample = $DB->get_record('course', array('id' => $sampleid));
if ($sample->visible == 0) {
// We skip not-visible courses as a way to emulate the training data / prediction data difference.
// In normal circumstances targets will return null when they receive a sample that can not be
// processed, that same sample may be used for prediction.
// We can not do this in is_valid_analysable because the analysable there is the site not the course.
return null;
/**
* is_valid_sample
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param bool $fortraining
* @return bool
*/
public function is_valid_sample($sampleid, \core_analytics\analysable $analysable, $fortraining = true) {
// We skip not-visible courses during training as a way to emulate the training data / prediction data difference.
// In normal circumstances is_valid_sample will return false when they receive a sample that can not be
// processed.
if (!$fortraining) {
return true;
}
$sample = $this->retrieve('course', $sampleid);
if ($sample->visible == 0) {
return false;
}
return true;
}
/**
* calculate_sample
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
$sample = $this->retrieve('course', $sampleid);
$firstchar = substr($sample->shortname, 0, 1);
if ($firstchar === 'a') {
return 1;
+70
View File
@@ -1,13 +1,59 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Test target.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
/**
* Test target.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class test_target_shortname extends \core_analytics\local\target\binary {
/**
* predictions
*
* @var array
*/
protected $predictions = array();
/**
* get_analyser_class
*
* @return string
*/
public function get_analyser_class() {
return '\core_analytics\local\analyser\site_courses';
}
/**
* classes_description
*
* @return string[]
*/
public static function classes_description() {
return array(
'Course fullname first char is A',
@@ -31,11 +77,26 @@ class test_target_shortname extends \core_analytics\local\target\binary {
return array();
}
/**
* is_valid_analysable
*
* @param \core_analytics\analysable $analysable
* @param bool $fortraining
* @return bool
*/
public function is_valid_analysable(\core_analytics\analysable $analysable, $fortraining = true) {
// This is testing, let's make things easy.
return true;
}
/**
* is_valid_sample
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param bool $fortraining
* @return bool
*/
public function is_valid_sample($sampleid, \core_analytics\analysable $analysable, $fortraining = true) {
// We skip not-visible courses during training as a way to emulate the training data / prediction data difference.
// In normal circumstances is_valid_sample will return false when they receive a sample that can not be
@@ -51,6 +112,15 @@ class test_target_shortname extends \core_analytics\local\target\binary {
return true;
}
/**
* calculate_sample
*
* @param int $sampleid
* @param \core_analytics\analysable $analysable
* @param int $starttime
* @param int $endtime
* @return float
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $analysable, $starttime = false, $endtime = false) {
$sample = $this->retrieve('course', $sampleid);
+23 -3
View File
@@ -17,7 +17,7 @@
/**
* Unit tests for the model.
*
* @package analytics
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@@ -32,7 +32,7 @@ require_once(__DIR__ . '/fixtures/test_target_shortname.php');
/**
* Unit tests for the model.
*
* @package analytics
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@@ -142,12 +142,32 @@ class analytics_model_testcase extends advanced_testcase {
}
}
/**
* Testable version to change methods' visibility.
*
* @package core_analytics
* @copyright 2017 David Monllaó {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class testable_model extends \core_analytics\model {
/**
* get_output_dir
*
* @param array $subdirs
* @return string
*/
public function get_output_dir($subdirs = array()) {
return parent::get_output_dir($subdirs);
}
/**
* init_analyser
*
* @param array $options
* @return void
*/
public function init_analyser($options = array()) {
return parent::init_analyser($options);
parent::init_analyser($options);
}
}
+44 -7
View File
@@ -40,9 +40,12 @@ require_once(__DIR__ . '/fixtures/test_target_shortname.php');
class core_analytics_prediction_testcase extends advanced_testcase {
/**
* test_ml_training_and_prediction
*
* @dataProvider provider_ml_training_and_prediction
* @param string $timesplittingid
* @param int $npredictedranges
* @param string $predictionsprocessorclass
* @return void
*/
public function test_ml_training_and_prediction($timesplittingid, $npredictedranges, $predictionsprocessorclass) {
@@ -112,7 +115,7 @@ class core_analytics_prediction_testcase extends advanced_testcase {
// They will not be skipped for prediction though.
$result = $model->predict();
// $course1 predictions should be 1 == 'a', $course2 predictions should be 0 == 'b'.
// Var $course1 predictions should be 1 == 'a', $course2 predictions should be 0 == 'b'.
$correct = array($course1->id => 1, $course2->id => 0);
foreach ($result->predictions as $uniquesampleid => $predictiondata) {
list($sampleid, $rangeindex) = $model->get_time_splitting()->infer_sample_info($uniquesampleid);
@@ -127,7 +130,8 @@ class core_analytics_prediction_testcase extends advanced_testcase {
$this->assertEquals(1, $DB->count_records('analytics_used_files',
array('modelid' => $model->get_id(), 'action' => 'predicted')));
// 2 predictions for each range.
$this->assertEquals(2 * $npredictedranges, $DB->count_records('analytics_predictions', array('modelid' => $model->get_id())));
$this->assertEquals(2 * $npredictedranges, $DB->count_records('analytics_predictions',
array('modelid' => $model->get_id())));
// No new generated files nor records as there are no new courses available.
$model->predict();
@@ -135,9 +139,15 @@ class core_analytics_prediction_testcase extends advanced_testcase {
$this->assertEquals($npredictedranges, count($trainedsamples));
$this->assertEquals(1, $DB->count_records('analytics_used_files',
array('modelid' => $model->get_id(), 'action' => 'predicted')));
$this->assertEquals(2 * $npredictedranges, $DB->count_records('analytics_predictions', array('modelid' => $model->get_id())));
$this->assertEquals(2 * $npredictedranges, $DB->count_records('analytics_predictions',
array('modelid' => $model->get_id())));
}
/**
* provider_ml_training_and_prediction
*
* @return array
*/
public function provider_ml_training_and_prediction() {
$cases = array(
'no_splitting' => array('\core_analytics\local\time_splitting\no_splitting', 1),
@@ -153,6 +163,11 @@ class core_analytics_prediction_testcase extends advanced_testcase {
* Basic test to check that prediction processors work as expected.
*
* @dataProvider provider_ml_test_evaluation
* @param string $modelquality
* @param int $ncourses
* @param array $expected
* @param string $predictionsprocessorclass
* @return void
*/
public function test_ml_evaluation($modelquality, $ncourses, $expected, $predictionsprocessorclass) {
$this->resetAfterTest(true);
@@ -172,7 +187,6 @@ class core_analytics_prediction_testcase extends advanced_testcase {
throw new \coding_exception('Only perfect and random accepted as $modelquality values');
}
// Generate training data.
$params = array(
'startdate' => mktime(0, 0, 0, 10, 24, 2015),
@@ -202,12 +216,19 @@ class core_analytics_prediction_testcase extends advanced_testcase {
// We check that the returned status includes at least $expectedcode code.
foreach ($results as $timesplitting => $result) {
$message = 'The returned status code ' . $result->status . ' should include ' . $expected[$timesplitting];
$this->assertEquals($expected[$timesplitting], $result->status & $expected[$timesplitting], $message);
$filtered = $result->status & $expected[$timesplitting];
$this->assertEquals($expected[$timesplitting], $filtered, $message);
}
}
/**
* provider_ml_test_evaluation
*
* @return array
*/
public function provider_ml_test_evaluation() {
$notenoughandlowscore = \core_analytics\model::EVALUATE_NOT_ENOUGH_DATA + \core_analytics\model::EVALUATE_LOW_SCORE;
$cases = array(
'bad-and-no-enough-data' => array(
'modelquality' => 'random',
@@ -216,8 +237,8 @@ class core_analytics_prediction_testcase extends advanced_testcase {
// The course duration is too much to be processed by in weekly basis.
'\core_analytics\local\time_splitting\weekly' => \core_analytics\model::NO_DATASET,
// 10 samples is not enough to process anything.
'\core_analytics\local\time_splitting\single_range' => \core_analytics\model::EVALUATE_NOT_ENOUGH_DATA + \core_analytics\model::EVALUATE_LOW_SCORE,
'\core_analytics\local\time_splitting\quarters' => \core_analytics\model::EVALUATE_NOT_ENOUGH_DATA + \core_analytics\model::EVALUATE_LOW_SCORE,
'\core_analytics\local\time_splitting\single_range' => $notenoughandlowscore,
'\core_analytics\local\time_splitting\quarters' => $notenoughandlowscore,
)
),
'bad' => array(
@@ -244,6 +265,11 @@ class core_analytics_prediction_testcase extends advanced_testcase {
return $this->add_prediction_processors($cases);
}
/**
* add_random_model
*
* @return \core_analytics\model
*/
protected function add_random_model() {
$target = \core_analytics\manager::get_target('test_target_shortname');
@@ -258,6 +284,11 @@ class core_analytics_prediction_testcase extends advanced_testcase {
return new \core_analytics\model($model->get_id());
}
/**
* add_perfect_model
*
* @return \core_analytics\model
*/
protected function add_perfect_model() {
$target = \core_analytics\manager::get_target('test_target_shortname');
@@ -272,6 +303,12 @@ class core_analytics_prediction_testcase extends advanced_testcase {
return new \core_analytics\model($model->get_id());
}
/**
* add_prediction_processors
*
* @param array $cases
* @return array
*/
protected function add_prediction_processors($cases) {
$return = array();
@@ -35,10 +35,20 @@ defined('MOODLE_INTERNAL') || die();
*/
class no_teacher extends \core_analytics\local\indicator\binary {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:noteacher', 'moodle');
}
/**
* required_sample_data
*
* @return string[]
*/
public static function required_sample_data() {
// We require course because, although calculate_sample only reads context, we need the context to be course
// or below.
+1
View File
@@ -83,6 +83,7 @@ class core_component {
'MatthiasMullie\\Minify' => 'lib/minify/matthiasmullie-minify/src/',
'MatthiasMullie\\PathConverter' => 'lib/minify/matthiasmullie-pathconverter/src/',
'IMSGlobal\LTI' => 'lib/ltiprovider/src',
'Phpml' => 'lib/mlbackend/php/phpml/src/Phpml',
);
/**
@@ -70,7 +70,8 @@ class prediction_action_started extends \core\event\base {
* @return string
*/
public function get_description() {
return "The user with id '$this->userid' has started '{$this->other['actionname']}' action for the prediction with id '".$this->objectid."'.";
return "The user with id '$this->userid' has started '{$this->other['actionname']}' action for the prediction with id '" .
$this->objectid . "'.";
}
/**
@@ -95,6 +96,11 @@ class prediction_action_started extends \core\event\base {
}
}
/**
* get_objectid_mapping
*
* @return array
*/
public static function get_objectid_mapping() {
return array('db' => 'analytics_predictions');
}
+4 -2
View File
@@ -2084,7 +2084,8 @@ function xmldb_main_upgrade($oldversion) {
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
// Adding indexes to table analytics_train_samples.
$table->add_index('modelidandanalysableidandtimesplitting', XMLDB_INDEX_NOTUNIQUE, array('modelid', 'analysableid', 'timesplitting'));
$table->add_index('modelidandanalysableidandtimesplitting', XMLDB_INDEX_NOTUNIQUE,
array('modelid', 'analysableid', 'timesplitting'));
// Conditionally launch create table for analytics_train_samples.
if (!$dbman->table_exists($table)) {
@@ -2106,7 +2107,8 @@ function xmldb_main_upgrade($oldversion) {
$table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
// Adding indexes to table analytics_predict_ranges.
$table->add_index('modelidandanalysableidandtimesplitting', XMLDB_INDEX_NOTUNIQUE, array('modelid', 'analysableid', 'timesplitting'));
$table->add_index('modelidandanalysableidandtimesplitting', XMLDB_INDEX_NOTUNIQUE,
array('modelid', 'analysableid', 'timesplitting'));
// Conditionally launch create table for analytics_predict_ranges.
if (!$dbman->table_exists($table)) {
+12 -5
View File
@@ -1434,9 +1434,9 @@ function enrol_get_course_by_user_enrolment_id($ueid) {
*
* @param int $courseid Course id or false if using $uefilter (user enrolment ids may belong to different courses)
* @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
* @param array usersfilter Limit the results obtained to this list of user ids. $uefilter compatibility not guaranteed.
* @param array uefilter Limit the results obtained to this list of user enrolment ids. $usersfilter compatibility not guaranteed.
* @return stdClass
* @param array $usersfilter Limit the results obtained to this list of user ids. $uefilter compatibility not guaranteed.
* @param array $uefilter Limit the results obtained to this list of user enrolment ids. $usersfilter compatibility not guaranteed.
* @return stdClass[]
*/
function enrol_get_course_users($courseid = false, $onlyactive = false, $usersfilter = array(), $uefilter = array()) {
global $DB;
@@ -1460,8 +1460,10 @@ function enrol_get_course_users($courseid = false, $onlyactive = false, $usersfi
}
if ($onlyactive) {
$conditions[] = "ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND (ue.timeend = 0 OR ue.timeend > :now2)";
$params['now1'] = round(time(), -2); // improves db caching
$conditions[] = "ue.status = :active AND e.status = :enabled AND ue.timestart < :now1 AND " .
"(ue.timeend = 0 OR ue.timeend > :now2)";
// Improves db caching.
$params['now1'] = round(time(), -2);
$params['now2'] = $params['now1'];
$params['active'] = ENROL_USER_ACTIVE;
$params['enabled'] = ENROL_INSTANCE_ENABLED;
@@ -1483,8 +1485,13 @@ function enrol_get_course_users($courseid = false, $onlyactive = false, $usersfi
}
/**
* Enrolment plugins abstract class.
*
* All enrol plugins should be based on this class,
* this is also the main source of documentation.
*
* @copyright 2010 Petr Skoda {@link http://skodak.org}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class enrol_plugin {
protected $config = null;
+65 -14
View File
@@ -24,20 +24,12 @@
namespace mlbackend_php;
// TODO No support for 3rd party plugins psr4??
spl_autoload_register(function($class) {
// Autoload Phpml classes.
$path = __DIR__ . '/../phpml/src/' . str_replace('\\', '/', $class) . '.php';
if (file_exists($path)) {
require_once($path);
}
});
defined('MOODLE_INTERNAL') || die();
use Phpml\Preprocessing\Normalizer;
use Phpml\CrossValidation\RandomSplit;
use Phpml\Dataset\ArrayDataset;
defined('MOODLE_INTERNAL') || die();
use Phpml\ModelManager;
/**
* PHP predictions processor.
@@ -48,12 +40,31 @@ defined('MOODLE_INTERNAL') || die();
*/
class processor implements \core_analytics\predictor {
/**
* Size of training / prediction batches.
*/
const BATCH_SIZE = 5000;
/**
* Number of train iterations.
*/
const TRAIN_ITERATIONS = 500;
/**
* File name of the serialised model.
*/
const MODEL_FILENAME = 'model.ser';
/**
* @var bool
*/
protected $limitedsize = false;
/**
* Checks if the processor is ready to use.
*
* @return bool
*/
public function is_ready() {
if (version_compare(phpversion(), '7.0.0') < 0) {
return get_string('errorphp7required', 'mlbackend_php');
@@ -61,12 +72,20 @@ class processor implements \core_analytics\predictor {
return true;
}
/**
* Trains a machine learning algorithm with the provided training set.
*
* @param string $uniqueid
* @param \stored_file $dataset
* @param string $outputdir
* @return \stdClass
*/
public function train($uniqueid, \stored_file $dataset, $outputdir) {
// Output directory is already unique to the model.
$modelfilepath = $outputdir . DIRECTORY_SEPARATOR . self::MODEL_FILENAME;
$modelmanager = new \Phpml\ModelManager();
$modelmanager = new ModelManager();
if (file_exists($modelfilepath)) {
$classifier = $modelmanager->restoreFromFile($modelfilepath);
@@ -114,6 +133,14 @@ class processor implements \core_analytics\predictor {
return $resultobj;
}
/**
* Predicts the provided samples
*
* @param string $uniqueid
* @param \stored_file $dataset
* @param string $outputdir
* @return \stdClass
*/
public function predict($uniqueid, \stored_file $dataset, $outputdir) {
// Output directory is already unique to the model.
@@ -123,7 +150,7 @@ class processor implements \core_analytics\predictor {
throw new \moodle_exception('errorcantloadmodel', 'analytics', '', $modelfilepath);
}
$modelmanager = new \Phpml\ModelManager();
$modelmanager = new ModelManager();
$classifier = $modelmanager->restoreFromFile($modelfilepath);
$fh = $dataset->get_content_file_handle();
@@ -204,7 +231,7 @@ class processor implements \core_analytics\predictor {
// Just an approximation, will depend on PHP version, compile options...
// Double size + zval struct (6 bytes + 8 bytes + 16 bytes) + array bucket (96 bytes)
// https://nikic.github.io/2011/12/12/How-big-are-PHP-arrays-really-Hint-BIG.html
// https://nikic.github.io/2011/12/12/How-big-are-PHP-arrays-really-Hint-BIG.html.
$floatsize = (PHP_INT_SIZE * 2) + 6 + 8 + 16 + 96;
}
@@ -219,7 +246,7 @@ class processor implements \core_analytics\predictor {
if (empty($CFG->mlbackend_php_no_evaluation_limits)) {
// We allow admins to disable evaluation memory usage limits by modifying config.php.
// We will have plenty of missing values in the dataset so it should be a conservative approximation:
// We will have plenty of missing values in the dataset so it should be a conservative approximation.
$samplessize = $samplessize + (count($sampledata) * $floatsize);
// Stop fetching more samples.
@@ -251,8 +278,17 @@ class processor implements \core_analytics\predictor {
return $this->get_evaluation_result_object($dataset, $phis, $maxdeviation);
}
/**
* Returns the results objects from all evaluations.
*
* @param \stored_file $dataset
* @param array $phis
* @param float $maxdeviation
* @return \stdClass
*/
protected function get_evaluation_result_object(\stored_file $dataset, $phis, $maxdeviation) {
// Average phi of all evaluations as final score.
if (count($phis) === 1) {
$avgphi = reset($phis);
} else {
@@ -300,6 +336,13 @@ class processor implements \core_analytics\predictor {
return $resultobj;
}
/**
* Returns the Phi correlation coefficient.
*
* @param array $testlabels
* @param array $predictedlabels
* @return float
*/
protected function get_phi($testlabels, $predictedlabels) {
// Binary here only as well.
@@ -320,6 +363,14 @@ class processor implements \core_analytics\predictor {
return $phi;
}
/**
* Extracts metadata from the dataset file.
*
* The file poiter should be located at the top of the file.
*
* @param resource $fh
* @return array
*/
protected function extract_metadata($fh) {
$metadata = fgetcsv($fh);
return array_combine($metadata, fgetcsv($fh));
@@ -1,4 +1,26 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for component 'mlbackend_php'
*
* @package mlbackend_php
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['datasetsizelimited'] = 'Only a part of the evaluation dataset has been evaluated due to its size. Set $CFG->mlbackend_php_no_memory_limit if you are confident that your system can cope a {$a} dataset';
$string['errorcantloadmodel'] = 'Model file {$a} does not exist, ensure the model has been trained before using it to predict.';
+44 -1
View File
@@ -35,11 +35,19 @@ defined('MOODLE_INTERNAL') || die();
*/
class processor implements \core_analytics\predictor {
/**
* The required version of the python package that performs all calculations.
*/
const REQUIRED_PIP_PACKAGE_VERSION = '0.0.9';
/**
* Is the plugin ready to be used?.
*
* @return bool
*/
public function is_ready() {
# Check the installed pip package version.
// Check the installed pip package version.
$cmd = 'python -m moodleinspire.version';
$output = null;
@@ -63,8 +71,17 @@ class processor implements \core_analytics\predictor {
return get_string('pythonpackagenotinstalled', 'mlbackend_python', $cmd);
}
/**
* Trains a machine learning algorithm with the provided dataset.
*
* @param string $uniqueid
* @param \stored_file $dataset
* @param string $outputdir
* @return \stdClass
*/
public function train($uniqueid, \stored_file $dataset, $outputdir) {
// Obtain the physical route to the file.
$datasetpath = $this->get_file_path($dataset);
$cmd = 'python -m moodleinspire.training ' .
@@ -95,8 +112,17 @@ class processor implements \core_analytics\predictor {
return $resultobj;
}
/**
* Returns predictions for the provided dataset samples.
*
* @param string $uniqueid
* @param \stored_file $dataset
* @param string $outputdir
* @return \stdClass
*/
public function predict($uniqueid, \stored_file $dataset, $outputdir) {
// Obtain the physical route to the file.
$datasetpath = $this->get_file_path($dataset);
$cmd = 'python -m moodleinspire.prediction ' .
@@ -127,8 +153,19 @@ class processor implements \core_analytics\predictor {
return $resultobj;
}
/**
* Evaluates the provided dataset.
*
* @param string $uniqueid
* @param float $maxdeviation
* @param int $niterations
* @param \stored_file $dataset
* @param string $outputdir
* @return \stdClass
*/
public function evaluate($uniqueid, $maxdeviation, $niterations, \stored_file $dataset, $outputdir) {
// Obtain the physical route to the file.
$datasetpath = $this->get_file_path($dataset);
$cmd = 'python -m moodleinspire.evaluation ' .
@@ -158,6 +195,12 @@ class processor implements \core_analytics\predictor {
return $resultobj;
}
/**
* Returns the path to the dataset file.
*
* @param \stored_file $file
* @return string
*/
protected function get_file_path(\stored_file $file) {
// From moodle filesystem to the local file system.
// This is not ideal, but there is no read access to moodle filesystem files.
@@ -1,4 +1,26 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Strings for component 'mlbackend_python'
*
* @package mlbackend_python
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
$string['packageinstalledshouldbe'] = '"moodleinspire" python package should be updated. The required version is "{$a->required}" and the installed version is "{$a->installed}"';
$string['pluginname'] = 'Python machine learning backend';
+5 -3
View File
@@ -31,9 +31,11 @@ defined('MOODLE_INTERNAL') || die();
*/
class core_component_testcase extends advanced_testcase {
// To be changed if number of subsystems increases/decreases,
// this is defined here to annoy devs that try to add more without any thinking,
// always verify that it does not collide with any existing add-on modules and subplugins!!!
/**
* To be changed if number of subsystems increases/decreases,
* this is defined here to annoy devs that try to add more without any thinking,
* always verify that it does not collide with any existing add-on modules and subplugins!!!
*/
const SUBSYSTEMCOUNT = 67;
public function setUp() {
@@ -35,10 +35,20 @@ defined('MOODLE_INTERNAL') || die();
*/
abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity {
/**
* feedback_viewed_events
*
* @return string[]
*/
protected function feedback_viewed_events() {
return array('\mod_assign\event\feedback_viewed');
}
/**
* feedback_check_grades
*
* @return bool
*/
protected function feedback_check_grades() {
// We need the grade to be released to the student to consider that feedback has been provided.
return true;
@@ -35,22 +35,52 @@ defined('MOODLE_INTERNAL') || die();
*/
class cognitive_depth extends activity_base {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:cognitivedepthassign', 'mod_assign');
}
/**
* get_indicator_type
*
* @return string
*/
protected function get_indicator_type() {
return self::INDICATOR_COGNITIVE;
}
/**
* get_cognitive_depth_level
*
* @param \cm_info $cm
* @return int
*/
protected function get_cognitive_depth_level(\cm_info $cm) {
return 5;
}
/**
* feedback_submitted_events
*
* @return string[]
*/
protected function feedback_submitted_events() {
return array('\mod_assign\event\assessable_submitted');
}
/**
* feedback_replied
*
* @param \cm_info $cm
* @param int $contextid
* @param int $userid
* @param int $after
* @return bool
*/
protected function feedback_replied(\cm_info $cm, $contextid, $userid, $after = false) {
// No level 4.
return false;
@@ -35,14 +35,30 @@ defined('MOODLE_INTERNAL') || die();
*/
class social_breadth extends activity_base {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:socialbreadthassign', 'mod_assign');
}
/**
* get_indicator_type
*
* @return string
*/
protected function get_indicator_type() {
return self::INDICATOR_SOCIAL;
}
/**
* get_social_breadth_level
*
* @param \cm_info $cm
* @return int
*/
protected function get_social_breadth_level(\cm_info $cm) {
return 2;
}
@@ -35,14 +35,30 @@ defined('MOODLE_INTERNAL') || die();
*/
class cognitive_depth extends activity_base {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:cognitivedepthbook', 'mod_book');
}
/**
* get_indicator_type
*
* @return string
*/
protected function get_indicator_type() {
return self::INDICATOR_COGNITIVE;
}
/**
* get_cognitive_depth_level
*
* @param \cm_info $cm
* @return int
*/
protected function get_cognitive_depth_level(\cm_info $cm) {
return 1;
}
@@ -35,14 +35,30 @@ defined('MOODLE_INTERNAL') || die();
*/
class social_breadth extends activity_base {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:socialbreadthbook', 'mod_book');
}
/**
* get_indicator_type
*
* @return string
*/
protected function get_indicator_type() {
return self::INDICATOR_SOCIAL;
}
/**
* get_social_breadth_level
*
* @param \cm_info $cm
* @return int
*/
protected function get_social_breadth_level(\cm_info $cm) {
return 1;
}
@@ -35,11 +35,35 @@ defined('MOODLE_INTERNAL') || die();
*/
abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity {
/**
* feedback_viewed_events
*
* @return string[]
*/
protected function feedback_viewed_events() {
return array('\mod_chat\event\course_module_viewed', '\mod_chat\event\message_sent',
'\mod_chat\event\sessions_viewed');
}
/**
* feedback_replied_events
*
* @return string[]
*/
protected function feedback_replied_events() {
return array('\mod_chat\event\message_sent');
}
/**
* feedback_post_action
*
* @param \cm_info $cm
* @param int $contextid
* @param int $userid
* @param string[] $eventnames
* @param int $after
* @return bool
*/
protected function feedback_post_action(\cm_info $cm, $contextid, $userid, $eventnames, $after = false) {
if (empty($this->activitylogs[$contextid][$userid])) {
@@ -75,6 +99,11 @@ abstract class activity_base extends \core_analytics\local\indicator\community_o
return false;
}
/**
* feedback_check_grades
*
* @return bool
*/
protected function feedback_check_grades() {
// Chat's feedback is not contained in grades.
return false;
@@ -35,20 +35,31 @@ defined('MOODLE_INTERNAL') || die();
*/
class cognitive_depth extends activity_base {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:cognitivedepthchat', 'mod_chat');
}
/**
* get_indicator_type
*
* @return string
*/
protected function get_indicator_type() {
return self::INDICATOR_COGNITIVE;
}
/**
* get_cognitive_depth_level
*
* @param \cm_info $cm
* @return int
*/
public function get_cognitive_depth_level(\cm_info $cm) {
return 4;
}
protected function feedback_replied_events() {
return array('\mod_chat\event\message_sent');
}
}
@@ -35,14 +35,30 @@ defined('MOODLE_INTERNAL') || die();
*/
class social_breadth extends activity_base {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:socialbreadthchat', 'mod_chat');
}
/**
* get_indicator_type
*
* @return string
*/
protected function get_indicator_type() {
return self::INDICATOR_SOCIAL;
}
/**
* get_social_breadth_level
*
* @param \cm_info $cm
* @return int
*/
protected function get_social_breadth_level(\cm_info $cm) {
return 2;
}
@@ -35,8 +35,18 @@ defined('MOODLE_INTERNAL') || die();
*/
abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity {
/**
* choicedata
*
* @var array
*/
protected $choicedata = array();
/**
* feedback_viewed_events
*
* @return string[]
*/
protected function feedback_viewed_events() {
return array('\mod_choice\event\course_module_viewed', '\mod_choice\event\answer_updated');
}
@@ -51,10 +61,20 @@ abstract class activity_base extends \core_analytics\local\indicator\community_o
global $DB;
if (!isset($this->choicedata[$cm->instance])) {
$this->choicedata[$cm->instance] = $DB->get_record('choice', array('id' => $cm->instance), 'id, showresults, timeclose', MUST_EXIST);
$this->choicedata[$cm->instance] = $DB->get_record('choice', array('id' => $cm->instance),
'id, showresults, timeclose', MUST_EXIST);
}
}
/**
* feedback_viewed
*
* @param \cm_info $cm
* @param int $contextid
* @param int $userid
* @param int $after
* @return bool
*/
protected function feedback_viewed(\cm_info $cm, $contextid, $userid, $after = null) {
// If results are shown after they answer a write action counts as feedback viewed.
@@ -35,14 +35,30 @@ defined('MOODLE_INTERNAL') || die();
*/
class cognitive_depth extends activity_base {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:cognitivedepthchoice', 'mod_choice');
}
/**
* get_indicator_type
*
* @return string
*/
protected function get_indicator_type() {
return self::INDICATOR_COGNITIVE;
}
/**
* get_cognitive_depth_level
*
* @param \cm_info $cm
* @return int
*/
protected function get_cognitive_depth_level(\cm_info $cm) {
$this->fill_choice_data($cm);
@@ -35,14 +35,30 @@ defined('MOODLE_INTERNAL') || die();
*/
class social_breadth extends activity_base {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:socialbreadthchoice', 'mod_choice');
}
/**
* get_indicator_type
*
* @return string
*/
protected function get_indicator_type() {
return self::INDICATOR_SOCIAL;
}
/**
* get_social_breadth_level
*
* @param \cm_info $cm
* @return int
*/
protected function get_social_breadth_level(\cm_info $cm) {
$this->fill_choice_data($cm);
return 2;
@@ -35,14 +35,30 @@ defined('MOODLE_INTERNAL') || die();
*/
class cognitive_depth extends activity_base {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:cognitivedepthdata', 'mod_data');
}
/**
* get_indicator_type
*
* @return string
*/
protected function get_indicator_type() {
return self::INDICATOR_COGNITIVE;
}
/**
* get_cognitive_depth_level
*
* @param \cm_info $cm
* @return int
*/
public function get_cognitive_depth_level(\cm_info $cm) {
return 2;
}
@@ -35,14 +35,30 @@ defined('MOODLE_INTERNAL') || die();
*/
class social_breadth extends activity_base {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:socialbreadthdata', 'mod_data');
}
/**
* get_indicator_type
*
* @return string
*/
protected function get_indicator_type() {
return self::INDICATOR_SOCIAL;
}
/**
* get_social_breadth_level
*
* @param \cm_info $cm
* @return int
*/
protected function get_social_breadth_level(\cm_info $cm) {
return 1;
}
@@ -40,6 +40,12 @@ abstract class activity_base extends \core_analytics\local\indicator\community_o
*/
protected $publishstats = array();
/**
* fill_publishstats
*
* @param \cm_info $cm
* @return void
*/
protected function fill_publishstats(\cm_info $cm) {
global $DB;
@@ -55,7 +61,7 @@ abstract class activity_base extends \core_analytics\local\indicator\community_o
* @param int $contextid
* @param int $userid
* @param int $after
* @return int
* @return bool
*/
protected function feedback_viewed(\cm_info $cm, $contextid, $userid, $after = null) {
// If stats are published any write action counts as viewed feedback.
@@ -35,14 +35,30 @@ defined('MOODLE_INTERNAL') || die();
*/
class cognitive_depth extends activity_base {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:cognitivedepthfeedback', 'mod_feedback');
}
/**
* get_indicator_type
*
* @return string
*/
protected function get_indicator_type() {
return self::INDICATOR_COGNITIVE;
}
/**
* get_cognitive_depth_level
*
* @param \cm_info $cm
* @return int
*/
protected function get_cognitive_depth_level(\cm_info $cm) {
$this->fill_publishstats($cm);
@@ -35,14 +35,30 @@ defined('MOODLE_INTERNAL') || die();
*/
class social_breadth extends activity_base {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:socialbreadthfeedback', 'mod_feedback');
}
/**
* get_indicator_type
*
* @return string
*/
protected function get_indicator_type() {
return self::INDICATOR_SOCIAL;
}
/**
* get_social_breadth_level
*
* @param \cm_info $cm
* @return int
*/
protected function get_social_breadth_level(\cm_info $cm) {
$this->fill_publishstats($cm);
@@ -35,14 +35,30 @@ defined('MOODLE_INTERNAL') || die();
*/
class cognitive_depth extends activity_base {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:cognitivedepthfolder', 'mod_folder');
}
/**
* get_indicator_type
*
* @return string
*/
protected function get_indicator_type() {
return self::INDICATOR_COGNITIVE;
}
/**
* get_cognitive_depth_level
*
* @param \cm_info $cm
* @return int
*/
protected function get_cognitive_depth_level(\cm_info $cm) {
return 1;
}
@@ -35,14 +35,30 @@ defined('MOODLE_INTERNAL') || die();
*/
class social_breadth extends activity_base {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:socialbreadthfolder', 'mod_folder');
}
/**
* get_indicator_type
*
* @return string
*/
protected function get_indicator_type() {
return self::INDICATOR_SOCIAL;
}
/**
* get_social_breadth_level
*
* @param \cm_info $cm
* @return int
*/
protected function get_social_breadth_level(\cm_info $cm) {
return 1;
}
@@ -35,11 +35,27 @@ defined('MOODLE_INTERNAL') || die();
*/
abstract class activity_base extends \core_analytics\local\indicator\community_of_inquiry_activity {
/**
* feedback_viewed_events
*
* @return string[]
*/
protected function feedback_viewed_events() {
// We could add any forum event, but it will make feedback_post_action slower
return array('\mod_forum\event\assessable_uploaded', '\mod_forum\event\course_module_viewed', '\mod_forum\event\discussion_viewed');
// We could add any forum event, but it will make feedback_post_action slower.
return array('\mod_forum\event\assessable_uploaded', '\mod_forum\event\course_module_viewed',
'\mod_forum\event\discussion_viewed');
}
/**
* feedback_post_action
*
* @param \cm_info $cm
* @param int $contextid
* @param int $userid
* @param string[] $eventnames
* @param int $after
* @return bool
*/
protected function feedback_post_action(\cm_info $cm, $contextid, $userid, $eventnames, $after = false) {
if (empty($this->activitylogs[$contextid][$userid])) {
@@ -35,22 +35,48 @@ defined('MOODLE_INTERNAL') || die();
*/
class cognitive_depth extends activity_base {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:cognitivedepthforum', 'mod_forum');
}
/**
* get_indicator_type
*
* @return string
*/
protected function get_indicator_type() {
return self::INDICATOR_COGNITIVE;
}
/**
* get_cognitive_depth_level
*
* @param \cm_info $cm
* @return int
*/
public function get_cognitive_depth_level(\cm_info $cm) {
return 4;
}
/**
* feedback_check_grades
*
* @return bool
*/
protected function feedback_check_grades() {
return false;
}
/**
* feedback_replied_events
*
* @return string[]
*/
protected function feedback_replied_events() {
return array('\mod_forum\event\assessable_uploaded');
}
@@ -35,14 +35,30 @@ defined('MOODLE_INTERNAL') || die();
*/
class social_breadth extends activity_base {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:socialbreadthforum', 'mod_forum');
}
/**
* get_indicator_type
*
* @return string
*/
protected function get_indicator_type() {
return self::INDICATOR_SOCIAL;
}
/**
* get_social_breadth_level
*
* @param \cm_info $cm
* @return int
*/
protected function get_social_breadth_level(\cm_info $cm) {
return 2;
}
@@ -35,14 +35,30 @@ defined('MOODLE_INTERNAL') || die();
*/
class cognitive_depth extends activity_base {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:cognitivedepthglossary', 'mod_glossary');
}
/**
* get_indicator_type
*
* @return string
*/
protected function get_indicator_type() {
return self::INDICATOR_COGNITIVE;
}
/**
* get_cognitive_depth_level
*
* @param \cm_info $cm
* @return int
*/
public function get_cognitive_depth_level(\cm_info $cm) {
return 2;
}
@@ -35,14 +35,30 @@ defined('MOODLE_INTERNAL') || die();
*/
class social_breadth extends activity_base {
/**
* get_name
*
* @return string
*/
public static function get_name() {
return get_string('indicator:socialbreadthglossary', 'mod_glossary');
}
/**
* get_indicator_type
*
* @return string
*/
protected function get_indicator_type() {
return self::INDICATOR_SOCIAL;
}
/**
* get_social_breadth_level
*
* @param \cm_info $cm
* @return int
*/
protected function get_social_breadth_level(\cm_info $cm) {
return 1;
}

Some files were not shown because too many files have changed in this diff Show More