From 5e9b091248336cb8a986991a3861ea822fc3caa4 Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20Mudr=C3=A1k?= Date: Fri, 22 Feb 2019 12:12:34 +0100 Subject: [PATCH] MDL-64477 analytics: Introduce a new \core_analytics\stats class The purpose of the class is to provide stats and meta data about how the analytics is used on the site. --- analytics/classes/stats.php | 78 ++++++++++++++++ analytics/tests/stats_test.php | 162 +++++++++++++++++++++++++++++++++ 2 files changed, 240 insertions(+) create mode 100644 analytics/classes/stats.php create mode 100644 analytics/tests/stats_test.php diff --git a/analytics/classes/stats.php b/analytics/classes/stats.php new file mode 100644 index 00000000000..0caf9758fc7 --- /dev/null +++ b/analytics/classes/stats.php @@ -0,0 +1,78 @@ +. + +/** + * Provides the {@link \core_analytics\stats} class. + * + * @package core_analytics + * @copyright 2019 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace core_analytics; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Provides stats and meta information about the analytics usage on this site. + * + * @copyright 2019 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class stats { + + /** + * Return the number of models enabled on this site. + * + * @return int + */ + public static function enabled_models() : int { + return count(manager::get_all_models(true)); + } + + /** + * Return the number of predictions generated by the system. + * + * @return int + */ + public static function predictions() : int { + global $DB; + + return $DB->count_records('analytics_predictions'); + } + + /** + * Return the number of suggested actions executed by users. + * + * @return int + */ + public static function actions() : int { + global $DB; + + return $DB->count_records('analytics_prediction_actions'); + } + + /** + * Return the number of suggested actions flagged as not useful. + * + * @return int + */ + public static function actions_not_useful() : int { + global $DB; + + return $DB->count_records('analytics_prediction_actions', ['actionname' => prediction::ACTION_NOT_USEFUL]); + } +} diff --git a/analytics/tests/stats_test.php b/analytics/tests/stats_test.php new file mode 100644 index 00000000000..d92f40375cb --- /dev/null +++ b/analytics/tests/stats_test.php @@ -0,0 +1,162 @@ +. + +/** + * Provides the {@link analytics_stats_testcase} class. + * + * @package core_analytics + * @category test + * @copyright 2019 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once(__DIR__ . '/fixtures/test_indicator_fullname.php'); +require_once(__DIR__ . '/fixtures/test_target_shortname.php'); + +/** + * Unit tests for the analytics stats. + * + * @copyright 2019 David Mudrák + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class analytics_stats_testcase extends advanced_testcase { + + /** + * Set up the test environment. + */ + public function setUp() { + + $this->setAdminUser(); + } + + /** + * Test the {@link \core_analytics\stats::enabled_models()} implementation. + */ + public function test_enabled_models() { + + $this->resetAfterTest(true); + + // By default, sites have {@link \core\analytics\target\no_teaching} enabled. + $this->assertEquals(1, \core_analytics\stats::enabled_models()); + + $model = \core_analytics\model::create( + \core_analytics\manager::get_target('\core\analytics\target\course_dropout'), + [ + \core_analytics\manager::get_indicator('\core\analytics\indicator\any_write_action'), + ] + ); + + // Purely adding a new model does not make it included in the stats. + $this->assertEquals(1, \core_analytics\stats::enabled_models()); + + // New models must be enabled to have them counted. + $model->enable('\core\analytics\time_splitting\quarters'); + $this->assertEquals(2, \core_analytics\stats::enabled_models()); + } + + /** + * Test the {@link \core_analytics\stats::predictions()} implementation. + */ + public function test_predictions() { + + $this->resetAfterTest(true); + + $model = \core_analytics\model::create( + \core_analytics\manager::get_target('test_target_shortname'), + [ + \core_analytics\manager::get_indicator('test_indicator_fullname'), + ] + ); + + $model->enable('\core\analytics\time_splitting\no_splitting'); + + // Train the model. + $this->getDataGenerator()->create_course(['shortname' => 'a', 'fullname' => 'a', 'visible' => 1]); + $this->getDataGenerator()->create_course(['shortname' => 'b', 'fullname' => 'b', 'visible' => 1]); + $model->train(); + + // No predictions yet. + $this->assertEquals(0, \core_analytics\stats::predictions()); + + // Get one new prediction. + $this->getDataGenerator()->create_course(['shortname' => 'aa', 'fullname' => 'aa', 'visible' => 0]); + $result = $model->predict(); + + $this->assertEquals(1, count($result->predictions)); + $this->assertEquals(1, \core_analytics\stats::predictions()); + + // Nothing changes if there is no new prediction. + $result = $model->predict(); + $this->assertFalse(isset($result->predictions)); + $this->assertEquals(1, \core_analytics\stats::predictions()); + + // Get two more predictions, we have three in total now. + $this->getDataGenerator()->create_course(['shortname' => 'bb', 'fullname' => 'bb', 'visible' => 0]); + $this->getDataGenerator()->create_course(['shortname' => 'cc', 'fullname' => 'cc', 'visible' => 0]); + + $result = $model->predict(); + $this->assertEquals(2, count($result->predictions)); + $this->assertEquals(3, \core_analytics\stats::predictions()); + } + + /** + * Test the {@link \core_analytics\stats::actions()} and {@link \core_analytics\stats::actions_not_useful()} implementation. + */ + public function test_actions() { + global $DB; + $this->resetAfterTest(true); + + $model = \core_analytics\model::create( + \core_analytics\manager::get_target('test_target_shortname'), + [ + \core_analytics\manager::get_indicator('test_indicator_fullname'), + ] + ); + + $model->enable('\core\analytics\time_splitting\no_splitting'); + + // Train the model. + $this->getDataGenerator()->create_course(['shortname' => 'a', 'fullname' => 'a', 'visible' => 1]); + $this->getDataGenerator()->create_course(['shortname' => 'b', 'fullname' => 'b', 'visible' => 1]); + $model->train(); + + // Generate two predictions. + $this->getDataGenerator()->create_course(['shortname' => 'aa', 'fullname' => 'aa', 'visible' => 0]); + $this->getDataGenerator()->create_course(['shortname' => 'bb', 'fullname' => 'bb', 'visible' => 0]); + $model->predict(); + + list($p1, $p2) = array_values($DB->get_records('analytics_predictions')); + + $p1 = new \core_analytics\prediction($p1, []); + $p2 = new \core_analytics\prediction($p2, []); + + // No actions executed at the start. + $this->assertEquals(0, \core_analytics\stats::actions()); + $this->assertEquals(0, \core_analytics\stats::actions_not_useful()); + + // The user has acknowledged the first prediction. + $p1->action_executed(\core_analytics\prediction::ACTION_FIXED, $model->get_target()); + $this->assertEquals(1, \core_analytics\stats::actions()); + $this->assertEquals(0, \core_analytics\stats::actions_not_useful()); + + // The user has marked the other prediction as not useful. + $p2->action_executed(\core_analytics\prediction::ACTION_NOT_USEFUL, $model->get_target()); + $this->assertEquals(2, \core_analytics\stats::actions()); + $this->assertEquals(1, \core_analytics\stats::actions_not_useful()); + } +} -- 2.43.0