MDL-59212 analytics: Analyser and static methods tests

Part of MDL-57791 epic.
This commit is contained in:
David Monllao
2017-07-24 08:36:59 +02:00
parent 3a396286b1
commit b3d6879489
5 changed files with 261 additions and 113 deletions
@@ -153,11 +153,6 @@ class student_enrolments extends by_course {
// Some course enrolments.
list($enrolsql, $params) = $DB->get_in_or_equal($sampleids, SQL_PARAMS_NAMED);
$sql = "SELECT ue.id AS enrolmentid, u.* FROM {user_enrolments} ue
JOIN {user} u on ue.userid = u.id
WHERE ue.id $enrolsql";
$enrolments = $DB->get_recordset_sql($sql, $params);
$samplesdata = array();
foreach ($enrolments as $userenrolmentid => $user) {
+176
View File
@@ -0,0 +1,176 @@
<?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/>.
/**
* Unit tests for core analysers.
*
* @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();
require_once(__DIR__ . '/fixtures/test_target_shortname.php');
require_once(__DIR__ . '/../../lib/enrollib.php');
/**
* Unit tests for core analysers.
*
* @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 analytics_analysers_testcase extends advanced_testcase {
/**
* test_courses_analyser
*
* @return void
*/
public function test_courses_analyser() {
$this->resetAfterTest(true);
$course = $this->getDataGenerator()->create_course();
$coursecontext = \context_course::instance($course->id);
$target = new test_target_shortname();
$analyser = new \core_analytics\local\analyser\courses(1, $target, [], [], []);
$analysable = new \core_analytics\course($course);
$this->assertInstanceOf('\core_analytics\course', $analyser->get_sample_analysable($course->id));
$this->assertInstanceOf('\context_course', $analyser->sample_access_context($course->id));
// Just 1 sample per course.
$class = new ReflectionClass('\core_analytics\local\analyser\courses');
$method = $class->getMethod('get_all_samples');
$method->setAccessible(true);
list($sampleids, $samplesdata) = $method->invoke($analyser, $analysable);
$this->assertCount(1, $sampleids);
$sampleid = reset($sampleids);
$this->assertEquals($course->id, $sampleid);
$this->assertEquals($course->fullname, $samplesdata[$sampleid]['course']->fullname);
$this->assertEquals($coursecontext, $samplesdata[$sampleid]['context']);
// To compare it later.
$prevsampledata = $samplesdata[$sampleid];
list($sampleids, $samplesdata) = $analyser->get_samples(array($sampleid));
$this->assertEquals($prevsampledata['context'], $samplesdata[$sampleid]['context']);
$this->assertEquals($prevsampledata['course']->shortname, $samplesdata[$sampleid]['course']->shortname);
}
/**
* test_site_courses_analyser
*
* @return void
*/
public function test_site_courses_analyser() {
$this->resetAfterTest(true);
$course1 = $this->getDataGenerator()->create_course();
$course2 = $this->getDataGenerator()->create_course();
$course3 = $this->getDataGenerator()->create_course();
$course1context = \context_course::instance($course1->id);
$target = new test_target_shortname();
$analyser = new \core_analytics\local\analyser\site_courses(1, $target, [], [], []);
$analysable = new \core_analytics\site();
$this->assertInstanceOf('\core_analytics\site', $analyser->get_sample_analysable($course1->id));
$this->assertInstanceOf('\core_analytics\site', $analyser->get_sample_analysable($course2->id));
$this->assertInstanceOf('\context_system', $analyser->sample_access_context($course1->id));
$this->assertInstanceOf('\context_system', $analyser->sample_access_context($course3->id));
$class = new ReflectionClass('\core_analytics\local\analyser\site_courses');
$method = $class->getMethod('get_all_samples');
$method->setAccessible(true);
list($sampleids, $samplesdata) = $method->invoke($analyser, $analysable);
$this->assertCount(3, $sampleids);
// Use course1 it does not really matter.
$this->assertArrayHasKey($course1->id, $sampleids);
$sampleid = $course1->id;
$this->assertEquals($course1->id, $sampleid);
$this->assertEquals($course1->fullname, $samplesdata[$sampleid]['course']->fullname);
$this->assertEquals($course1context, $samplesdata[$sampleid]['context']);
// To compare it later.
$prevsampledata = $samplesdata[$sampleid];
list($sampleids, $samplesdata) = $analyser->get_samples(array($sampleid));
$this->assertEquals($prevsampledata['context'], $samplesdata[$sampleid]['context']);
$this->assertEquals($prevsampledata['course']->shortname, $samplesdata[$sampleid]['course']->shortname);
}
/**
* test_site_courses_analyser
*
* @return void
*/
public function test_student_enrolments_analyser() {
global $DB;
$this->resetAfterTest(true);
$course = $this->getDataGenerator()->create_course();
$coursecontext = \context_course::instance($course->id);
$user1 = $this->getDataGenerator()->create_user();
$user2 = $this->getDataGenerator()->create_user();
$user3 = $this->getDataGenerator()->create_user();
// Checking that suspended users are also included.
$this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student');
$this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student', 'manual', 0, 0, ENROL_USER_SUSPENDED);
$this->getDataGenerator()->enrol_user($user3->id, $course->id, 'editingteacher');
$enrol = $DB->get_record('enrol', array('courseid' => $course->id, 'enrol' => 'manual'));
$ue1 = $DB->get_record('user_enrolments', array('userid' => $user1->id, 'enrolid' => $enrol->id));
$ue2 = $DB->get_record('user_enrolments', array('userid' => $user2->id, 'enrolid' => $enrol->id));
$target = new test_target_shortname();
$analyser = new \core_analytics\local\analyser\student_enrolments(1, $target, [], [], []);
$analysable = new \core_analytics\course($course);
$this->assertInstanceOf('\core_analytics\course', $analyser->get_sample_analysable($ue1->id));
$this->assertInstanceOf('\context_course', $analyser->sample_access_context($ue1->id));
$class = new ReflectionClass('\core_analytics\local\analyser\student_enrolments');
$method = $class->getMethod('get_all_samples');
$method->setAccessible(true);
list($sampleids, $samplesdata) = $method->invoke($analyser, $analysable);
// Only students.
$this->assertCount(2, $sampleids);
$this->assertArrayHasKey($ue1->id, $sampleids);
$this->assertArrayHasKey($ue2->id, $sampleids);
// Shouldn't matter which one we select.
$sampleid = $ue1->id;
$this->assertEquals($ue1, $samplesdata[$sampleid]['user_enrolments']);
$this->assertEquals($course->fullname, $samplesdata[$sampleid]['course']->fullname);
$this->assertEquals($coursecontext, $samplesdata[$sampleid]['context']);
$this->assertEquals($user1->firstname, $samplesdata[$sampleid]['user']->firstname);
// To compare it later.
$prevsampledata = $samplesdata[$sampleid];
list($sampleids, $samplesdata) = $analyser->get_samples(array($sampleid));
$this->assertEquals($prevsampledata['user_enrolments'], $samplesdata[$sampleid]['user_enrolments']);
$this->assertEquals($prevsampledata['context'], $samplesdata[$sampleid]['context']);
$this->assertEquals($prevsampledata['course']->shortname, $samplesdata[$sampleid]['course']->shortname);
$this->assertEquals($prevsampledata['user']->firstname, $samplesdata[$sampleid]['user']->firstname);
}
}
+13 -89
View File
@@ -22,113 +22,37 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/test_target_shortname.php');
defined('MOODLE_INTERNAL') || die();
/**
* Test static target.
*
* Testing target extension to make it static and to use a different analyser
* just to try a different one. Method calculate_sample is exactly the same.
*
* @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 {
class test_static_target_shortname extends test_target_shortname {
/**
* predictions
* based_on_assumptions
*
* @var array
* @return bool
*/
protected $predictions = array();
public static function based_on_assumptions() {
return true;
}
/**
* get_analyser_class
* Different analyser just to test a different one.
*
* @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',
'Course fullname first char is not A'
);
}
/**
* We don't want to discard results.
* @return null
*/
protected function min_prediction_score() {
return null;
}
/**
* We don't want to discard results.
* @return array
*/
protected function ignored_predicted_classes() {
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
// 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;
} else {
return 0;
}
return '\core_analytics\local\analyser\courses';
}
}
+71 -18
View File
@@ -29,6 +29,7 @@ require_once(__DIR__ . '/fixtures/test_indicator_min.php');
require_once(__DIR__ . '/fixtures/test_indicator_fullname.php');
require_once(__DIR__ . '/fixtures/test_indicator_random.php');
require_once(__DIR__ . '/fixtures/test_target_shortname.php');
require_once(__DIR__ . '/fixtures/test_static_target_shortname.php');
/**
* Unit tests for evaluation, training and prediction.
@@ -39,6 +40,65 @@ require_once(__DIR__ . '/fixtures/test_target_shortname.php');
*/
class core_analytics_prediction_testcase extends advanced_testcase {
/**
* test_static_prediction
*
* @return void
*/
public function test_static_prediction() {
global $DB;
$this->resetAfterTest(true);
$this->setAdminuser();
$model = $this->add_perfect_model('test_static_target_shortname');
$model->enable('\\core_analytics\\local\\time_splitting\\no_splitting');
$this->assertEquals(1, $model->is_enabled());
$this->assertEquals(1, $model->is_trained());
// No training for static models.
$results = $model->train();
$trainedsamples = $DB->get_records('analytics_train_samples', array('modelid' => $model->get_id()));
$this->assertEmpty($trainedsamples);
$this->assertEmpty($DB->count_records('analytics_used_files',
array('modelid' => $model->get_id(), 'action' => 'trained')));
// Now we create 2 hidden courses (only hidden courses are getting predictions).
$courseparams = array('shortname' => 'aaaaaa', 'fullname' => 'aaaaaa', 'visible' => 0);
$course1 = $this->getDataGenerator()->create_course($courseparams);
$courseparams = array('shortname' => 'bbbbbb', 'fullname' => 'bbbbbb', 'visible' => 0);
$course2 = $this->getDataGenerator()->create_course($courseparams);
$result = $model->predict();
// 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);
// The range index is not important here, both ranges prediction will be the same.
$this->assertEquals($correct[$sampleid], $predictiondata->prediction);
}
// 1 range for each analysable.
$predictedranges = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id()));
$this->assertCount(2, $predictedranges);
$this->assertEquals(1, $DB->count_records('analytics_used_files',
array('modelid' => $model->get_id(), 'action' => 'predicted')));
// 2 predictions for each range.
$this->assertEquals(2, $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();
$predictedranges = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id()));
$this->assertCount(2, $predictedranges);
$this->assertEquals(1, $DB->count_records('analytics_used_files',
array('modelid' => $model->get_id(), 'action' => 'predicted')));
$this->assertEquals(2, $DB->count_records('analytics_predictions',
array('modelid' => $model->get_id())));
}
/**
* test_ml_training_and_prediction
*
@@ -88,30 +148,22 @@ class core_analytics_prediction_testcase extends advanced_testcase {
$this->assertEquals(0, $DB->count_records('analytics_train_samples', array('modelid' => $model->get_id())));
$results = $model->train();
$this->assertEquals(1, $model->get_model_obj()->enabled);
$this->assertEquals(1, $model->get_model_obj()->trained);
$this->assertEquals(1, $model->is_enabled());
$this->assertEquals(1, $model->is_trained());
// 1 training file was created.
$trainedsamples = $DB->get_records('analytics_train_samples', array('modelid' => $model->get_id()));
$this->assertEquals(1, count($trainedsamples));
$this->assertCount(1, $trainedsamples);
$samples = json_decode(reset($trainedsamples)->sampleids, true);
$this->assertEquals($ncourses * 2, count($samples));
$this->assertCount($ncourses * 2, $samples);
$this->assertEquals(1, $DB->count_records('analytics_used_files',
array('modelid' => $model->get_id(), 'action' => 'trained')));
// Now we create 2 hidden courses (they should not be used for training by the target).
$courseparams = $params + array('shortname' => 'aaaaaa', 'fullname' => 'aaaaaa', 'visible' => 0);
$course1 = $this->getDataGenerator()->create_course($courseparams);
$courseparams = $params + array('shortname' => 'bbbbbb', 'fullname' => 'bbbbbb', 'visible' => 0);
$course2 = $this->getDataGenerator()->create_course($courseparams);
// No more files should be created as the 2 new courses should be skipped by the target (not ready for training).
$results = $model->train();
$trainedsamples = $DB->get_records('analytics_train_samples', array('modelid' => $model->get_id()));
$this->assertEquals(1, count($trainedsamples));
$this->assertEquals(1, $DB->count_records('analytics_used_files',
array('modelid' => $model->get_id(), 'action' => 'trained')));
// They will not be skipped for prediction though.
$result = $model->predict();
@@ -125,8 +177,8 @@ class core_analytics_prediction_testcase extends advanced_testcase {
}
// 2 ranges will be predicted.
$trainedsamples = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id()));
$this->assertEquals($npredictedranges, count($trainedsamples));
$predictedranges = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id()));
$this->assertCount($npredictedranges, $predictedranges);
$this->assertEquals(1, $DB->count_records('analytics_used_files',
array('modelid' => $model->get_id(), 'action' => 'predicted')));
// 2 predictions for each range.
@@ -135,8 +187,8 @@ class core_analytics_prediction_testcase extends advanced_testcase {
// No new generated files nor records as there are no new courses available.
$model->predict();
$trainedsamples = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id()));
$this->assertEquals($npredictedranges, count($trainedsamples));
$predictedranges = $DB->get_records('analytics_predict_ranges', array('modelid' => $model->get_id()));
$this->assertCount($npredictedranges, $predictedranges);
$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',
@@ -287,11 +339,12 @@ class core_analytics_prediction_testcase extends advanced_testcase {
/**
* add_perfect_model
*
* @param string $targetclass
* @return \core_analytics\model
*/
protected function add_perfect_model() {
protected function add_perfect_model($targetclass = 'test_target_shortname') {
$target = \core_analytics\manager::get_target('test_target_shortname');
$target = \core_analytics\manager::get_target($targetclass);
$indicators = array('test_indicator_max', 'test_indicator_min', 'test_indicator_fullname');
foreach ($indicators as $key => $indicator) {
$indicators[$key] = \core_analytics\manager::get_indicator($indicator);
+1 -1
View File
@@ -1476,7 +1476,7 @@ function enrol_get_course_users($courseid = false, $onlyactive = false, $usersfi
}
if ($uefilter) {
list($uesql, $ueparams) = $DB->get_in_or_equal($usersfilter, SQL_PARAMS_NAMED);
list($uesql, $ueparams) = $DB->get_in_or_equal($uefilter, SQL_PARAMS_NAMED);
$conditions[] = "ue.id $uesql";
$params = $params + $ueparams;
}