MDL-59212 analytics: Analyser and static methods tests
Part of MDL-57791 epic.
This commit is contained in:
@@ -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) {
|
||||
|
||||
|
||||
@@ -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
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user