MDL-59057 analytics: static models changes

Part of MDL-57791 epic.
This commit is contained in:
David Monllao 2017-06-09 12:52:49 +02:00
parent a40952d384
commit 357507b4e6
13 changed files with 291 additions and 54 deletions

View File

@ -137,6 +137,17 @@ class course_dropout extends \core_analytics\local\target\binary {
return true;
}
/**
* is_valid_sample
*
* @param int $sampleid
* @param \core_analytics\analysable $course
* @return bool
*/
public function is_valid_sample($sampleid, \core_analytics\analysable $course) {
return true;
}
/**
* calculate_sample
*
@ -148,11 +159,9 @@ class course_dropout extends \core_analytics\local\target\binary {
* @param \core_analytics\analysable $course
* @return void
*/
public function calculate_sample($sampleid, \core_analytics\analysable $course) {
protected function calculate_sample($sampleid, \core_analytics\analysable $course, $starttime = false, $endtime = false) {
global $DB;
// TODO Even if targets are aware of the data the analyser returns, we can probably still feed samples
// data with cached data.
$sql = "SELECT ue.* FROM {user_enrolments} ue JOIN {user} u ON u.id = ue.userid WHERE ue.id = :ueid";
$userenrol = $DB->get_record_sql($sql, array('ueid' => $sampleid));

View File

@ -0,0 +1,128 @@
<?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/>.
/**
* No teaching target.
*
* @package tool_models
* @copyright 2016 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace tool_models\analytics\target;
defined('MOODLE_INTERNAL') || die();
/**
* No teaching target.
*
* @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
*/
class no_teaching_activity extends \core_analytics\local\target\binary {
/**
* Machine learning backends are not required to predict.
*
* @return bool
*/
public static function based_on_assumptions() {
return true;
}
public static function get_name() {
return get_string('target:noteachingactivity', 'tool_models');
}
public function prediction_actions(\core_analytics\prediction $prediction) {
global $USER;
// No need to call the parent as the only default action is view details and this target only have 1 feature.
$actions = array();
$sampledata = $prediction->get_sample_data();
$course = $sampledata['course'];
if (has_capability('moodle/course:enrolreview', $sampledata['context'])) {
$url = new \moodle_url('/enrol/users.php', array('id' => $course->id));
$pix = new \pix_icon('i/enrolusers', get_string('enrolledusers', 'enrol'));
$actions['enrolusers'] = new \core_analytics\prediction_action('enrolusers', $prediction,
$url, $pix, get_string('enrolledusers', 'enrol'));
}
if (has_capability('moodle/course:viewparticipants', $sampledata['context'])) {
$url = new \moodle_url('/user/index.php', array('id' => $course->id));
$pix = new \pix_icon('i/cohort', get_string('participants'));
$actions['viewparticipants'] = new \core_analytics\prediction_action('viewparticipants', $prediction,
$url, $pix, get_string('participants'));
}
return $actions;
}
protected static function classes_description() {
return array(
get_string('labelteachingactivityyes', 'tool_models'),
get_string('labelteachingactivityno', 'tool_models'),
);
}
/**
* Returns the predicted classes that will be ignored.
*
* @return array
*/
protected function ignored_predicted_classes() {
// No need to list the course if there is teaching activity.
return array(0);
}
public function get_analyser_class() {
return '\\core_analytics\\local\\analyser\\courses';
}
public function is_valid_analysable(\core_analytics\analysable $site, $fortraining = true) {
// The analysable is the site, so yes, it is always valid.
return true;
}
public function is_valid_sample($sampleid, \core_analytics\analysable $site) {
$course = $this->retrieve('course', $sampleid);
if (!$course->startdate || $course->startdate - WEEKSECS > time()) {
return false;
}
return true;
}
/**
* calculate_sample
*
* @param int $sampleid
* @param \core_analytics\analysable $site
* @return void
*/
protected function calculate_sample($sampleid, \core_analytics\analysable $site, $starttime = false, $endtime = false) {
$noteachersindicator = $this->retrieve('\core_course\analytics\indicator\no_teacher', $sampleid);
if ($noteachersindicator != \core_course\analytics\indicator\no_teacher::get_min_value()) {
// No teachers flagged as 1.
return 1;
}
return 0;
}
}

View File

@ -80,17 +80,19 @@ class models_list implements \renderable, \templatable {
// Actions.
$actionsmenu = new \action_menu();
$actionsmenu->set_menu_trigger(get_string('edit'));
$actionsmenu->set_menu_trigger(get_string('actions'));
$actionsmenu->set_owner_selector('model-actions-' . $model->get_id());
$actionsmenu->set_alignment(\action_menu::TL, \action_menu::BL);
// Edit model.
$url = new \moodle_url('model.php', array('action' => 'edit', 'id' => $model->get_id()));
$icon = new \action_menu_link_secondary($url, new \pix_icon('t/edit', get_string('edit')), get_string('edit'));
$actionsmenu->add($icon);
if (!$model->is_static()) {
$url = new \moodle_url('model.php', array('action' => 'edit', 'id' => $model->get_id()));
$icon = new \action_menu_link_secondary($url, new \pix_icon('t/edit', get_string('edit')), get_string('edit'));
$actionsmenu->add($icon);
}
// Evaluate model.
if ($model->get_indicators()) {
// Evaluate machine-learning-based models.
if ($model->get_indicators() && !$model->is_static()) {
$url = new \moodle_url('model.php', array('action' => 'evaluate', 'id' => $model->get_id()));
$icon = new \action_menu_link_secondary($url, new \pix_icon('i/calc', get_string('evaluate', 'tool_models')),
get_string('evaluate', 'tool_models'));
@ -98,16 +100,19 @@ class models_list implements \renderable, \templatable {
}
if ($modeldata->enabled && !empty($modeldata->timesplitting)) {
$url = new \moodle_url('model.php', array('action' => 'execute', 'id' => $model->get_id()));
$url = new \moodle_url('model.php', array('action' => 'getpredictions', 'id' => $model->get_id()));
$icon = new \action_menu_link_secondary($url, new \pix_icon('i/notifications',
get_string('executemodel', 'tool_models')), get_string('executemodel', 'tool_models'));
get_string('getpredictions', 'tool_models')), get_string('getpredictions', 'tool_models'));
$actionsmenu->add($icon);
}
$url = new \moodle_url('model.php', array('action' => 'log', 'id' => $model->get_id()));
$icon = new \action_menu_link_secondary($url, new \pix_icon('i/report', get_string('viewlog', 'tool_models')),
get_string('viewlog', 'tool_models'));
$actionsmenu->add($icon);
// Machine-learning-based models evaluation log.
if (!$model->is_static()) {
$url = new \moodle_url('model.php', array('action' => 'log', 'id' => $model->get_id()));
$icon = new \action_menu_link_secondary($url, new \pix_icon('i/report', get_string('viewlog', 'tool_models')),
get_string('viewlog', 'tool_models'));
$actionsmenu->add($icon);
}
$modeldata->actions = $actionsmenu->export_for_template($output);

View File

@ -70,10 +70,10 @@ class renderer extends plugin_renderer_base {
* Web interface evaluate results.
*
* @param \stdClass[] $results
* @param string[] $executionlog
* @param string[] $logs
* @return string HTML
*/
public function render_evaluate_results($results, $executionlog = array()) {
public function render_evaluate_results($results, $logs = array()) {
global $OUTPUT;
$output = '';
@ -90,9 +90,9 @@ class renderer extends plugin_renderer_base {
$langstrdata = (object)array('name' => $timesplitting->get_name(), 'id' => $timesplittingid);
if (CLI_SCRIPT) {
$output .= $OUTPUT->heading(get_string('executionresultscli', 'tool_models', $langstrdata), 3);
$output .= $OUTPUT->heading(get_string('getpredictionsresultscli', 'tool_models', $langstrdata), 3);
} else {
$output .= $OUTPUT->heading(get_string('executionresults', 'tool_models', $langstrdata), 3);
$output .= $OUTPUT->heading(get_string('getpredictionsresults', 'tool_models', $langstrdata), 3);
}
}
@ -120,10 +120,10 @@ class renderer extends plugin_renderer_base {
}
}
// Info logged during execution.
if (!empty($executionlog) && debugging()) {
// Info logged during evaluation.
if (!empty($logs) && debugging()) {
$output .= $OUTPUT->heading(get_string('extrainfo', 'tool_models'), 3);
foreach ($executionlog as $log) {
foreach ($logs as $log) {
$output .= $OUTPUT->notification($log, \core\output\notification::NOTIFY_WARNING);
}
}
@ -137,7 +137,7 @@ class renderer extends plugin_renderer_base {
/**
* Web interface execution results.
* Web interface training & prediction results.
*
* @param array $trainresults
* @param string[] $trainlogs
@ -145,7 +145,7 @@ class renderer extends plugin_renderer_base {
* @param string[] $predictlogs
* @return string HTML
*/
public function render_execute_results($trainresults = false, $trainlogs = array(), $predictresults = false, $predictlogs = array()) {
public function render_getpredictions_results($trainresults = false, $trainlogs = array(), $predictresults = false, $predictlogs = array()) {
global $OUTPUT;
$output = '';
@ -159,7 +159,7 @@ class renderer extends plugin_renderer_base {
$output .= $OUTPUT->notification(get_string('trainingprocessfinished', 'tool_models'),
\core\output\notification::NOTIFY_SUCCESS);
} else if ($trainresults->status === \core_analytics\model::NO_DATASET) {
$output .= $OUTPUT->notification(get_string('nodatatotrain', 'analytics'),
$output .= $OUTPUT->notification(get_string('nodatatotrain', 'tool_models'),
\core\output\notification::NOTIFY_WARNING);
} else {
$output .= $OUTPUT->notification(get_string('generalerror', 'analytics', $result->status),
@ -175,7 +175,7 @@ class renderer extends plugin_renderer_base {
}
if ($predictresults || (!empty($predictlogs) && debugging())) {
$output .= $OUTPUT->heading(get_string('predictionresults', 'tool_models'), 3);
$output .= $OUTPUT->heading(get_string('predictionresults', 'tool_models'), 3, 'main m-t-3');
}
if ($predictresults) {
@ -183,7 +183,7 @@ class renderer extends plugin_renderer_base {
$output .= $OUTPUT->notification(get_string('predictionprocessfinished', 'tool_models'),
\core\output\notification::NOTIFY_SUCCESS);
} else if ($predictresults->status === \core_analytics\model::NO_DATASET) {
$output .= $OUTPUT->notification(get_string('nodatatopredict', 'analytics'),
$output .= $OUTPUT->notification(get_string('nodatatopredict', 'tool_models'),
\core\output\notification::NOTIFY_WARNING);
} else {
$output .= $OUTPUT->notification(get_string('generalerror', 'analytics', $result->status),

View File

@ -54,7 +54,7 @@ class predict_models extends \core\task\scheduled_task {
if ($result) {
echo $OUTPUT->heading(get_string('modelresults', 'tool_models', $model->get_target()->get_name()));
$renderer = $PAGE->get_renderer('tool_models');
echo $renderer->render_execute_results(false, array(), $result, $model->get_analyser()->get_logs());
echo $renderer->render_getpredictions_results(false, array(), $result, $model->get_analyser()->get_logs());
}
}

View File

@ -49,12 +49,17 @@ class train_models extends \core\task\scheduled_task {
foreach ($models as $modelobj) {
$model = new \core_analytics\model($modelobj);
if ($model->is_static()) {
// Skip models based on assumptions.
continue;
}
$result = $model->train();
if ($result) {
echo $OUTPUT->heading(get_string('modelresults', 'tool_models', $model->get_target()->get_name()));
$renderer = $PAGE->get_renderer('tool_models');
echo $renderer->render_execute_results($result, $model->get_analyser()->get_logs());
echo $renderer->render_getpredictions_results($result, $model->get_analyser()->get_logs());
}
}
}

View File

@ -122,7 +122,7 @@ if (!empty($validdatasets) && !$model->is_enabled() && $options['non-interactive
// Set the time splitting method file and enable it.
$model->enable($input);
mtrace(get_string('executingmodel', 'tool_models'));
mtrace(get_string('trainandpredictmodel', 'tool_models'));
// Train the model with the selected time splitting method and start predicting.
$model->train();

View File

@ -31,12 +31,17 @@ defined('MOODLE_INTERNAL') || die();
*/
function xmldb_tool_models_install() {
// TODO All of them for the moment, we will define a limited set of them once in core.
// Students at risk of dropping out of courses.
$target = \core_analytics\manager::get_target('\tool_models\analytics\target\course_dropout');
$indicators = \core_analytics\manager::get_all_indicators();
// We need the model to be created in order to know all its potential indicators and set them.
$model = \core_analytics\model::create($target, array());
// TODO All of them for the moment, we will define a limited set of them once in core.
$model->update(0, $model->get_potential_indicators());
// Course without teachers.
$target = \core_analytics\manager::get_target('\tool_models\analytics\target\no_teaching_activity');
$weekbeforestart = '\core_analytics\local\time_splitting\week_before_course_start';
$noteacher = \core_analytics\manager::get_indicator('\core_course\analytics\indicator\no_teacher');
$key = '\\' . get_class($noteacher);
\core_analytics\model::create($target, array($key => $noteacher), $weekbeforestart);
}

View File

@ -26,32 +26,37 @@ $string['accuracy'] = 'Accuracy';
$string['allindicators'] = 'All indicators';
$string['analysingsitedata'] = 'Analysing the site';
$string['analyticmodels'] = 'Analytic models';
$string['bettercli'] = 'Models\' evaluation and execution are heavy processes, it is better to run them through command line interface';
$string['bettercli'] = 'To evaluate models and to get predictions are heavy processes, it is better to run them through command line interface';
$string['cantguessstartdate'] = 'Can\'t guess the start date';
$string['cantguessenddate'] = 'Can\'t guess the end date';
$string['clienablemodel'] = 'You can enable the model by selecting a time splitting method by its id. Note that you can also enable it later using the web interface (\'none\' to exit)';
$string['coursenotyetstarted'] = 'The course is not yet started';
$string['coursenotyetfinished'] = 'The course is not yet finished';
$string['editmodel'] = 'Edit model {$a}';
$string['editmodel'] = 'Edit "{$a}" model';
$string['edittrainedwarning'] = 'This model has already been trained, note that changing its indicators or its time splitting method will delete its previous predictions and start generating the new ones';
$string['enabled'] = 'Enabled';
$string['errorcantenablenotimesplitting'] = 'You need to select a time splitting method before enabling the model';
$string['errornoenabledandtrainedmodels'] = 'There are not enabled and trained models to predict';
$string['errornoenabledmodels'] = 'There are not enabled models to train';
$string['errornostaticedit'] = 'Models based on assumptions can not be edited';
$string['errornostaticevaluated'] = 'Models based on assumptions can not be evaluated, they are always 100% correct according to how they were defined';
$string['errornostaticlog'] = 'Models based on assumptions can not be evaluated, there is no preformance log';
$string['evaluate'] = 'Evaluate';
$string['evaluatemodel'] = 'Evaluate model';
$string['evaluationinbatches'] = 'The site contents are calculated and stored in batches, during evaluation you can stop the process at any moment, the next time you run it it will continue from the point you stopped it.';
$string['executemodel'] = 'Execute';
$string['executingmodel'] = 'Training model and calculating predictions';
$string['executionresultscli'] = 'Results using {$a->name} (id: {$a->id}) course duration splitting';
$string['executionresults'] = 'Results using {$a->name} course duration splitting';
$string['trainandpredictmodel'] = 'Training model and calculating predictions';
$string['getpredictionsresultscli'] = 'Results using {$a->name} (id: {$a->id}) course duration splitting';
$string['getpredictionsresults'] = 'Results using {$a->name} course duration splitting';
$string['extrainfo'] = 'Info';
$string['generalerror'] = 'Evaluation error. Status code {$a}';
$string['goodmodel'] = 'This is a good model and it can be used to predict, enable it and execute it to start getting predictions.';
$string['getpredictions'] = 'Get predictions';
$string['goodmodel'] = 'This is a good model and it can be used to predict, enable it to start getting predictions.';
$string['indicators'] = 'Indicators';
$string['info'] = 'Info';
$string['labelstudentdropoutyes'] = 'Student at risk of dropping out';
$string['labelstudentdropoutno'] = 'Not at risk';
$string['labelteachingactivityyes'] = 'Teaching activity';
$string['labelteachingactivityno'] = 'No teaching activity';
$string['loginfo'] = 'Log extra info';
$string['lowaccuracy'] = 'The model accuracy is low';
$string['modelresults'] = '{$a} results';
@ -61,9 +66,11 @@ $string['nocompletiondetection'] = 'No method available to detect course complet
$string['nocourseactivity'] = 'Not enough course activity between the start and the end of the course';
$string['nocourseendtime'] = 'The course does not have an end time';
$string['nocoursesections'] = 'No course sections';
$string['nocoursestarttime'] = 'The course does not have an start time';
$string['nocoursestudents'] = 'No students';
$string['nodatatoevaluate'] = 'There is no data to evaluate the model';
$string['nodatatopredict'] = 'There is no data to use for predictions';
$string['nodatatopredict'] = 'No new elements to get predictions for';
$string['nodatatotrain'] = 'There is no new data that can be used for training';
$string['notdefined'] = 'Not yet defined';
$string['pluginname'] = 'Analytic models';
$string['predictionresults'] = 'Prediction results';
@ -74,6 +81,7 @@ $string['samestartdate'] = 'Current start date is good';
$string['sameenddate'] = 'Current end date is good';
$string['target'] = 'Target';
$string['target:coursedropout'] = 'Students at risk of dropping out';
$string['target:noteachingactivity'] = 'No teaching activity';
$string['target:coursedropoutinfo'] = 'Here you can find a list of students at risk of dropping out.';
$string['timemodified'] = 'Last modification';
$string['trainingprocessfinished'] = 'Training process finished';

View File

@ -45,8 +45,8 @@ switch ($action) {
case 'evaluate':
$title = get_string('evaluatemodel', 'tool_models');
break;
case 'execute':
$title = get_string('executemodel', 'tool_models');
case 'getpredictions':
$title = get_string('getpredictions', 'tool_models');
break;
case 'log':
$title = get_string('viewlog', 'tool_models');
@ -65,6 +65,11 @@ switch ($action) {
case 'edit':
if ($model->is_static()) {
echo $OUTPUT->header();
throw new moodle_exception('errornostaticedit', 'tool_models');
}
$customdata = array(
'id' => $model->get_id(),
'model' => $model,
@ -103,6 +108,11 @@ switch ($action) {
case 'evaluate':
echo $OUTPUT->header();
if ($model->is_static()) {
throw new moodle_exception('errornostaticevaluate', 'tool_models');
}
// Web interface is used by people who can not use CLI nor code stuff, always use
// cached stuff as they will change the model through the web interface as well
// which invalidates the previously analysed stuff.
@ -111,7 +121,7 @@ switch ($action) {
echo $renderer->render_evaluate_results($results, $model->get_analyser()->get_logs());
break;
case 'execute':
case 'getpredictions':
echo $OUTPUT->header();
$trainresults = $model->train();
@ -123,11 +133,16 @@ switch ($action) {
$predictlogs = $model->get_analyser()->get_logs();
$renderer = $PAGE->get_renderer('tool_models');
echo $renderer->render_execute_results($trainresults, $trainlogs, $predictresults, $predictlogs);
echo $renderer->render_getpredictions_results($trainresults, $trainlogs, $predictresults, $predictlogs);
break;
case 'log':
echo $OUTPUT->header();
if ($model->is_static()) {
throw new moodle_exception('errornostaticlog', 'tool_models');
}
$renderer = $PAGE->get_renderer('tool_models');
$modellogstable = new \tool_models\output\model_logs('model-' . $model->get_id(), $model->get_id());
echo $renderer->render_table($modellogstable);

View File

@ -0,0 +1,62 @@
<?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/>.
/**
* No teacher indicator.
*
* @package core_course
* @copyright 2017 David Monllao {@link http://www.davidmonllao.com}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace core_course\analytics\indicator;
defined('MOODLE_INTERNAL') || die();
/**
* No teacher indicator.
*
* @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_teacher extends \core_analytics\local\indicator\binary {
public static function get_name() {
return get_string('indicator:noteacher', 'moodle');
}
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.
return array('context', 'course');
}
public function calculate_sample($sampleid, $sampleorigin, $notusedstarttime = false, $notusedendtime = false) {
$context = $this->retrieve('context', $sampleid);
$teacherroles = get_config('analytics', 'teacherroles');
$teacherroleids = explode(',', $teacherroles);
foreach ($teacherroleids as $role) {
if (get_role_users($role, $context)) {
return self::get_max_value();
}
}
return self::get_min_value();
}
}

View File

@ -61,11 +61,11 @@ class prediction implements \renderable, \templatable {
$data = new \stdClass();
// Sample info (determined by the analyser).
list($data->sampledescription, $sampleimage) = $this->model->prediction_sample_description($this->prediction);
list($data->sampledescription, $samplerenderable) = $this->model->prediction_sample_description($this->prediction);
// Sampleimage is a renderable we should pass it to HTML.
if ($sampleimage) {
$data->sampleimage = $output->render($sampleimage);
if ($samplerenderable) {
$data->samplelink = $output->render($samplerenderable);
}
// Prediction info.

View File

@ -34,19 +34,19 @@
}}
<div class="card">
<div class="row m-a-1">
{{#sampleimage}}
{{#samplelink}}
<div class="col-sm-1 span1 m-b-1">
{{{sampleimage}}}
{{{samplelink}}}
</div>
<div class="col-sm-3 span3 m-b-1">
{{{sampledescription}}}
</div>
{{/sampleimage}}
{{^sampleimage}}
{{/samplelink}}
{{^samplelink}}
<div class="col-sm-4 span4 m-b-1">
{{{sampledescription}}}
</div>
{{/sampleimage}}
{{/samplelink}}
<div class="col-sm-4 span4">
<div class="{{predictionstyle}}">{{predictiondisplayvalue}}</div>
</div>