diff --git a/README.md b/README.md index f730108..0bfcda3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,16 @@ -# Skill-Tree-Data-Analytics -A python program to compile useful insights from users' skill tree data. -# Installation +# Skill-Tree Data-Analytics +Python library that streamlines the process of data analysis for Project Skill Tree. + +## Installation To install the necessary dependencies run the command pip install -r requirements.txt -To access the Database you must use a Database User and a Database Password, stored as enviornment variables as "STDB_USER" and "STDB_PASSWORD" respectively. \ No newline at end of file + +To access the Database you must use a Database User and a Database Password, stored as enviornment variables as "STDB_USER" and "STDB_PASSWORD" respectively. + +## Usage +There are two parts to the repository: +1) The utilities package. +2) Jupyter notebooks on which the Data Analysis takes place. + +The utilities package contains methods that process the data. There is a pandas DataFrame for each type of data, which can be manipulated as needed in the notebooks. + +Currently the old version of the source code is still in the repository, until refactoring is completed. \ No newline at end of file diff --git a/documentation.txt b/documentation.txt deleted file mode 100644 index 994d5b0..0000000 --- a/documentation.txt +++ /dev/null @@ -1,37 +0,0 @@ -Here is a small reference of the methods to easily access and visualise the SkillTree data. - -## Marks methods that currently present issues - - -DATA METHODS: - -UserData: -count_users: Takes a dictionary as a parameter to specify which users to count. If left empty, it will return the total number of SkillTree users. -timezone_counter: Returns a dictionary whose keys are the timezones and whose values are the amount of users. Takes optional parameter. -number_skills_completed_dict: Returns a dictionary with the amount of skills completed as a key and the number of people who have completed those skills as a value. -number_skills_completed_data: Returns information regarding how many skills users complete. - -SkillData: -order_skills_by_popularity: Returns an ordered dictionary with each skill as a key and its number of completions as a value. It takes the optional user_parameter parameter to generate a list for specific types of users. -get_skill_completion_rate: Returns an ordered dictionary whose key is the id of each skill, and whose value is a dictionary containing the amount of users that have started, that are in progress and that have completed each skill, along with a score (indicating, from 0 to 1, the completion rate of the skill). Accepts parameters for skill and users. -list_skills_by_ease: Returns a dictionary with each skill and its completion rate - -ChallengeData: -order_challenges_by_popularity: Returns an ordered dictionary with each challenge as a key and its number of completions as a value. It takes the optional user_parameter parameter to generate a list for specific types of users. - - -GRAPH METHODS: -Note: for all graphs there exists a parameter called tight_layout, which, if set to False deactivates layout optimisation. This can be used if the plots are not displaying properly. - -UserGraph: -graph_xp_distribution: Returns a graph with a distribution of users' xp, using a logarithmic scale. -pie_timezones: Pie chart of the different timezones -bar_timezones: Bar chart of the different timezones -## graph_number_skills_completed: To be constructed - -SkillGraph: -graph_skills_by_popularity: Returns a horizontal bar chart of the most popular skills. It takes an optional user_parameter parameter to specify which types of users to analyse and an amount parameter, to specify the amount of skills to graph (note, if graph_all is set to True, all skills will be displayed). -## graph_skills_by_ease: Graphs skills by ease. Can take parameter. - -ChallengeGraph: -graph_challenges_by_popularity: Returns a horizontal bar chart of the most popular challenges. It takes an optional user_parameter parameter to specify which types of users to analyse and an amount parameter, to specify the amount of skills to graph (note, if graph_all is set to True, all skills will be displayed). diff --git a/requirements.txt b/requirements.txt index ed3a068..c9b0494 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ matplotlib pymongo pymongo[srv] -pandas \ No newline at end of file +pandas +seaborn \ No newline at end of file diff --git a/src/__init__.py b/src (outdated)/__init__.py similarity index 100% rename from src/__init__.py rename to src (outdated)/__init__.py diff --git a/src (outdated)/example.py b/src (outdated)/example.py new file mode 100644 index 0000000..b1c7e93 --- /dev/null +++ b/src (outdated)/example.py @@ -0,0 +1,34 @@ +''' +There are two modules that can be used to access the SkillTree data. +We will begin by looking at stdata, which includes a series of methods whose +objective is to perform calculations using the raw data from the data compiling it into +more comprehensible formats. +''' +import stdata + +''' +There are now various ways of proceeding. The way the stdata module is structured is very +simple. There is a class for UserData, one for SkillData and one for ChallengeData. Each of +these is equiped with a wide range of methods, which we must call in order to perform +our analysis on the data. The easiest way of doing so is by creating an instance in place as follows. +''' + +number_users = stdata.UserData().count_users() + +''' +If you're going to call various methods of the UserData class, it might be better to use the +following code. +''' + +userData = stdata.UserData() +number_users = userData.count_users() +timezone_info = userData.timezone_counter() + +''' +In order to make queries more interesting it can be very useful to make use of the parameters +that are available in each of the methods. +''' + +# Completion rate of the skills belonging to the fitness category, where users are in timezone 0 +data = stdata.SkillData().get_skill_completion_rate(skill_parameter={"category":"fitness"}, user_parameter={"timezone":0}) +print(data) \ No newline at end of file diff --git a/src (outdated)/gui.py b/src (outdated)/gui.py new file mode 100644 index 0000000..c4bd3e9 --- /dev/null +++ b/src (outdated)/gui.py @@ -0,0 +1,31 @@ +from stdata import * +from stgraphs import * +from PyQt6.QtWidgets import * +import sys + +# Definition of PyQt App, Layout and Window +app = QApplication(sys.argv) + + +class MainWin(QMainWindow): + def __init__(self): + super().__init__() + self.button = QPushButton('Top') + self.button.clicked.connect(self.show_new_window) + self.setCentralWidget(self.button) + + def show_new_window(self, checked): + self.w = SkillWin() + self.w.show() + +class SkillWin(QWidget): + def __init__(self): + super().__init__() + layout = QVBoxLayout() + label = QLabel(str(SkillData().get_ease())) + layout.addWidget(label) + self.setLayout(layout) + +mainWin = MainWin() +mainWin.show() +app.exec() diff --git a/src (outdated)/play.py b/src (outdated)/play.py new file mode 100644 index 0000000..c35bfa7 --- /dev/null +++ b/src (outdated)/play.py @@ -0,0 +1,4 @@ +from stdata import * +import stgraphs + +stgraphs.SkillGraph().graph_skills_by_ease() \ No newline at end of file diff --git a/src (outdated)/stdata.py b/src (outdated)/stdata.py new file mode 100644 index 0000000..0b81510 --- /dev/null +++ b/src (outdated)/stdata.py @@ -0,0 +1,129 @@ +from pymongo import MongoClient +from pymongo.server_api import ServerApi +import os +from collections import Counter, OrderedDict +import pandas as pd + +################ CREATE GET SCV + + +# Useful function to make sense of the raw data +def count_and_order(list_to_order) -> OrderedDict: + return OrderedDict(Counter(list_to_order).most_common()) + +# Base class for all the different types of data +class DataObject(): + db_user = os.getenv("STDB_USER") + db_password = os.getenv("STDB_PASS") + client = MongoClient(f'mongodb+srv://{db_user}:{db_password}@adonis.n0u0i.mongodb.net/Database?retryWrites=true&w=majority', server_api=ServerApi('1')) + db = client.Database + users = db.Users + challenges = db.Challenges + items = db.Items + skills = db.Skills + tasks = db.Tasks + + # Run after each call to close the connection with the Database + def close(self) -> None: + DataObject.client.close() + +# Includes methods common to skills and challenges +class ActionData (DataObject): + def __init__(self): + self.data_type = None + self.completed = None + self.progress = None + self.find_description = None + + def id_to_goals(self, dictionary) -> dict: + descriptions = [self.data_type.find_one({"_id":item})["goals"]for item in dictionary] + return list(zip(descriptions, list(dictionary.values()))) + + def order_by_popularity(self, user_parameter={}) -> dict: + # First create a list with the lists of skills that each user has completed and then unpack that list. + list = [user[self.completed] for user in DataObject.users.find(user_parameter)] + total_list = [item for sublist in list for item in sublist] + return count_and_order(total_list) + + def get_completion_rate(self, user_parameter={}, action_parameter={}) -> dict: + from collections import Counter + users = self.users.find(user_parameter) + items = [item["_id"] for item in self.data_type.find(action_parameter)] + completed_list = [] + progress_list = [] + + for user in users: + for completed in user[self.completed]: + if completed in items: + completed_list.append(completed) + for progress in user[self.progress]: + if progress in items: + progress_list.append(progress) + + completed_counted = Counter(completed_list) + progress_counted = Counter(progress_list) + data_unordered = {key: {'Started': value + completed_counted[key], 'Progress': value, 'Completed': completed_counted[key], 'Score':float(completed_counted[key])/float(value+completed_counted[key])} for (key, value) in progress_counted.items()} + data_ordered = dict(sorted(data_unordered.items(), key=lambda x:x[1]['Score'])) + + return data_ordered + + def get_ease(self, action_parameter={}) -> dict: + data = self.get_completion_rate(action_parameter=action_parameter) + keys = [self.data_type.find_one({"_id":id})["goals"][0] for id in data.keys()] + values = [value['Score'] for value in data.values()] + total_dict = dict(zip(keys, values)) + return total_dict + + +class UserData (DataObject): + def count_users(self, parameter={}) -> int: + return len(list(DataObject.users.find(parameter))) + + def timezone_counter(self, parameter={}) -> OrderedDict: + return count_and_order([str(user["timezone"]) for user in DataObject.users.find(parameter)]) + + def number_skills_completed_dict(self, parameter={}) -> OrderedDict: + return count_and_order([len(user["skillscompleted"]) for user in self.users.find(parameter)]) + + def number_skills_completed_data(self, parameter={}) -> str: + return pd.Series([len(user["skillscompleted"]) for user in self.users.find(parameter)]).describe() + + def days_tracked_data(self, parameter={}) -> str: + return pd.Series([user["numDaysTracked"] for user in self.users.find(parameter)]).describe() + + +class SkillData(ActionData): + def __init__(self): + super().__init__() + self.data_type = DataObject.skills + self.completed = "skillscompleted" + self.progress = "skillsinprogress" + + def id_to_title_and_level(self, dictionary) -> dict: + title_and_id = [(self.data_type.find_one({"_id":item})["title"], self.data_type.find_one({"_id":item})["level"]) for item in dictionary] + return dict(zip(title_and_id, dictionary.values())) + + def get_skills_csv(self) -> None: + import csv + data = self.get_completion_rate() + titles = list(self.id_to_title_and_level(data).keys()) + goals = [item[0] for item in self.id_to_goals(data)] + started = [data[datum]["Started"] for datum in data] + progress = [data[datum]["Progress"] for datum in data] + completed = [data[datum]["Completed"] for datum in data] + score = [data[datum]["Score"] for datum in data] + rows = [[titles[i], goals[i], started[i], progress[i], completed[i], score[i]] for i in range(len(titles))] + + with open('skills.csv', 'w', encoding='UTF8') as f: + writer = csv.writer(f, delimiter=';') + for row in rows: + writer.writerow(row) + +class ChallengeData(ActionData): + def __init__(self): + super().__init__() + self.data_type = DataObject.challenges + self.completed = "challengescompleted" + self.progress = "challengesinprogress" + +SkillData().get_skills_csv() \ No newline at end of file diff --git a/src/stgraphs.py b/src (outdated)/stgraphs.py similarity index 86% rename from src/stgraphs.py rename to src (outdated)/stgraphs.py index 6151311..e79bde8 100644 --- a/src/stgraphs.py +++ b/src (outdated)/stgraphs.py @@ -27,9 +27,16 @@ def pie_timezones(self, user_parameter={}, tight_layout=True) -> None: def bar_timezones(self, user_parameter={}, tight_layout=True) -> None: data = UserData().timezone_counter(parameter=user_parameter) x = data.keys() - y= data.values() + y = data.values() plt.bar(x, y) self.set_plot("Users per timezone", tight_layout) + + def graph_number_skills_completed(self, user_parameter={}, tight_layout=True) -> None: + data = UserData().number_skills_completed_dict(parameter=user_parameter) + x = data.keys() + y = data.values() + plt.bar(x, y) + self.set_plot("Number of skills completed", tight_layout) class SkillGraph(GraphObject): def graph_skills_by_popularity(self, user_parameter={}, amount=10, graph_all=False, tight_layout=True) -> None: @@ -49,9 +56,9 @@ def graph_skills_by_popularity(self, user_parameter={}, amount=10, graph_all=Fal self.set_plot("Skill Popularity", tight_layout) - def graph_skills_by_ease(self, skill_parameter={}, tight_layout=False) -> None: + def graph_skills_by_ease(self, skill_parameter={}, tight_layout=False, amount=10) -> None: data = SkillData().list_skills_by_ease(skill_parameter=skill_parameter) - plt.bar(data.keys(), data.values()) + plt.barh( list(data.keys())[:amount], list(data.values())[:amount]) plt.xlabel("Completion_rate") self.set_plot("Skills by ease", tight_layout=tight_layout) @@ -72,4 +79,3 @@ def graph_challenges_by_popularity(self, user_parameter={}, amount=10, graph_all plt.text(v + 1, i, str(v), color='blue', fontweight='bold') self.set_plot("Challenge Popularity", tight_layout) - diff --git a/src/outlier_analysis.ipynb b/src/outlier_analysis.ipynb new file mode 100644 index 0000000..90f5358 --- /dev/null +++ b/src/outlier_analysis.ipynb @@ -0,0 +1,601 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Outlier Users\n", + "In this notebook we will analyse outliers in the dataset. These come in various shapes and forms. We will look at 3 types of outliers. \n", + "1) Those with negative *xp*.\n", + "2) Those with very high *xp*.\n", + "3) Those whose character is neither *male* nor *female*." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load data" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import utilities.data as ud\n", + "import utilities.users as uu\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "\n", + "DATA_DIR = \"./data\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Execute only if you want to fetch the data from the Database.\n", + "ud.fetch_data(DATA_DIR)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "users, challenges, items, skills, tasks = ud.read_data(DATA_DIR)\n", + "\n", + "# We are only interested in active users.\n", + "users = uu.process(uu.active(users))" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Analysis" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First we will have a look at users with less than 0 xp points. The existence of such users suggests a bug in the system, possibly caused by a player unmarking a skill as complete." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
_idxpxpHistoryitemsskillscompletedskillsinprogresschallengescompletedchallengesinprogresscharactertimezonebaselocationlastTrackednumDaysTrackedreminderSent
466363e598ddb0f5c03625155c75-60[0][ObjectId('62c382d46cac02c487e243cb'), ObjectI...[][ObjectId('62c226cf9efefadfd10e20ad'), ObjectI...[][]male0.010543760219751138692023-02-15 03:32:30.21161
\n", + "
" + ], + "text/plain": [ + " _id xp xpHistory \\\n", + "4663 63e598ddb0f5c03625155c75 -60 [0] \n", + "\n", + " items skillscompleted \\\n", + "4663 [ObjectId('62c382d46cac02c487e243cb'), ObjectI... [] \n", + "\n", + " skillsinprogress challengescompleted \\\n", + "4663 [ObjectId('62c226cf9efefadfd10e20ad'), ObjectI... [] \n", + "\n", + " challengesinprogress character timezone baselocation \\\n", + "4663 [] male 0.0 1054376021975113869 \n", + "\n", + " lastTracked numDaysTracked reminderSent \n", + "4663 2023-02-15 03:32:30.211 6 1 " + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "users[users[\"xp\"]<0]" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here is the data for the 0.1% top users." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
_idxpxpHistoryitemsskillscompletedskillsinprogresschallengescompletedchallengesinprogresscharactertimezonebaselocationlastTrackednumDaysTrackedreminderSent
38162c9e671c6fc4a6d588902dc166390[0, 0, 2420, 11020, 16640, 27340, 38740, 43840...[ObjectId('62c382d46cac02c487e243cb'), ObjectI...[ObjectId('62c226cf9efefadfd10e20ad'), ObjectI...[ObjectId('62c226d69efefadfd10e2167'), ObjectI...[ObjectId('62c226d09efefadfd10e20bb'), ObjectI...[]male-4.09539241922591703352023-04-01 09:26:19.6371491
40662cbb64d17466f8557f81ee5138860[0, 5190, 8840, 12340, 12340, 15940, 23640, 33...[ObjectId('62c382d46cac02c487e243cb'), ObjectI...[ObjectId('62c226cf9efefadfd10e20ad'), ObjectI...[ObjectId('62c226d69efefadfd10e216e'), ObjectI...[ObjectId('62c226d09efefadfd10e20bb'), ObjectI...[]male-8.03338051417465159802023-06-10 22:16:02.511364-1
93662f928203ab35244f0edb52b214050[0, 0, 0, 560, 2660, 6110, 9130, 15480, 22480,...[ObjectId('62c382d46cac02c487e243cb'), ObjectI...[ObjectId('62c226cf9efefadfd10e20b2'), ObjectI...[ObjectId('62c226d19efefadfd10e20d6')][ObjectId('62c226d09efefadfd10e20bb'), ObjectI...[]male0.09758595729694556462023-06-11 18:36:20.631234-1
140362fec820f73481669ecc9eb1137150[0, 0, 340, 1440, 6290, 10680, 13580, 14980, 2...[ObjectId('62c382d46cac02c487e243cb'), ObjectI...[ObjectId('62c226cf9efefadfd10e20b2'), ObjectI...[][ObjectId('62c226df9efefadfd10e2242'), ObjectI...[]male-6.09539241922591703352023-04-10 01:01:35.8874751
\n", + "
" + ], + "text/plain": [ + " _id xp \\\n", + "381 62c9e671c6fc4a6d588902dc 166390 \n", + "406 62cbb64d17466f8557f81ee5 138860 \n", + "936 62f928203ab35244f0edb52b 214050 \n", + "1403 62fec820f73481669ecc9eb1 137150 \n", + "\n", + " xpHistory \\\n", + "381 [0, 0, 2420, 11020, 16640, 27340, 38740, 43840... \n", + "406 [0, 5190, 8840, 12340, 12340, 15940, 23640, 33... \n", + "936 [0, 0, 0, 560, 2660, 6110, 9130, 15480, 22480,... \n", + "1403 [0, 0, 340, 1440, 6290, 10680, 13580, 14980, 2... \n", + "\n", + " items \\\n", + "381 [ObjectId('62c382d46cac02c487e243cb'), ObjectI... \n", + "406 [ObjectId('62c382d46cac02c487e243cb'), ObjectI... \n", + "936 [ObjectId('62c382d46cac02c487e243cb'), ObjectI... \n", + "1403 [ObjectId('62c382d46cac02c487e243cb'), ObjectI... \n", + "\n", + " skillscompleted \\\n", + "381 [ObjectId('62c226cf9efefadfd10e20ad'), ObjectI... \n", + "406 [ObjectId('62c226cf9efefadfd10e20ad'), ObjectI... \n", + "936 [ObjectId('62c226cf9efefadfd10e20b2'), ObjectI... \n", + "1403 [ObjectId('62c226cf9efefadfd10e20b2'), ObjectI... \n", + "\n", + " skillsinprogress \\\n", + "381 [ObjectId('62c226d69efefadfd10e2167'), ObjectI... \n", + "406 [ObjectId('62c226d69efefadfd10e216e'), ObjectI... \n", + "936 [ObjectId('62c226d19efefadfd10e20d6')] \n", + "1403 [] \n", + "\n", + " challengescompleted challengesinprogress \\\n", + "381 [ObjectId('62c226d09efefadfd10e20bb'), ObjectI... [] \n", + "406 [ObjectId('62c226d09efefadfd10e20bb'), ObjectI... [] \n", + "936 [ObjectId('62c226d09efefadfd10e20bb'), ObjectI... [] \n", + "1403 [ObjectId('62c226df9efefadfd10e2242'), ObjectI... [] \n", + "\n", + " character timezone baselocation lastTracked \\\n", + "381 male -4.0 953924192259170335 2023-04-01 09:26:19.637 \n", + "406 male -8.0 333805141746515980 2023-06-10 22:16:02.511 \n", + "936 male 0.0 975859572969455646 2023-06-11 18:36:20.631 \n", + "1403 male -6.0 953924192259170335 2023-04-10 01:01:35.887 \n", + "\n", + " numDaysTracked reminderSent \n", + "381 149 1 \n", + "406 364 -1 \n", + "936 234 -1 \n", + "1403 475 1 " + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "top_users = users[users[\"xp\"]>users[\"xp\"].quantile(0.999)]\n", + "top_users" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will now calculate the coefficient of variation $C_v=\\frac{\\sigma}{\\mu}$, for the top users. As a reminder, a higher value of $C_v$ corresponds to higher variation in the dataset. " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The coefficient of variation is 0.2187\n" + ] + } + ], + "source": [ + "cv = uu.coeff_variation(top_users, \"xp\")\n", + "print(f\"The coefficient of variation is {cv:.4f}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will also calculate the coefficient of variation of the entire dataset. A high value (over 1) here indicates that the data is very spread out." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The coefficient of variation is 3.5567\n" + ] + } + ], + "source": [ + "cv = uu.coeff_variation(users, \"xp\")\n", + "print(f\"The coefficient of variation is {cv:.4f}\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "A good way to quantify exactly how extreme their *xp* level is in comparison to other active users is to look at how many *Standard deviations* they stray away from the mean. For refference, if the data was normally distributed, we'd expect 99.7% of the data to be within 3 standards deviations of the mean." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The user at poisition number 1 is 18.359 stds away from the mean.\n", + "The user at poisition number 2 is 14.208 stds away from the mean.\n", + "The user at poisition number 3 is 11.811 stds away from the mean.\n", + "The user at poisition number 4 is 11.662 stds away from the mean.\n" + ] + } + ], + "source": [ + "top_users = top_users.sort_values(\"xp\", ascending=False)\n", + "\n", + "j = 0\n", + "\n", + "for i, user in top_users.iterrows():\n", + " j += 1\n", + " v = abs(user[\"xp\"]-users[\"xp\"].mean())/users[\"xp\"].std()\n", + " print(f\"The user at poisition number {j} is {v:.3f} stds away from the mean.\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Another way to view the outliers in the dataset is to view the *xp* level and the number of days tracked. This lets us see two dimensions of outliers. Those who have been using SkillTree consistently for a very long time, and those who have high *xp*. It is also important to note that those users who have a high *xp* level and low number of tracked days are \"intense users\" in the sense that they have been able to progress enormously in a short amount of time." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAkIAAAGzCAYAAADDgXghAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjUuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/NK7nSAAAACXBIWXMAAA9hAAAPYQGoP6dpAACG+UlEQVR4nOzdd3gUVdvA4d+ZbekNUggdQZBqR1SsCCgWxALYULGAoCJWbGDF8lo/C3bsICoWmiLVgiAoSlGkKQgkQEJ62TLn+2OTJUuymw2kEPa5ryuv786cOfPskGSfnKq01hohhBBCiDBkNHQAQgghhBANRRIhIYQQQoQtSYSEEEIIEbYkERJCCCFE2JJESAghhBBhSxIhIYQQQoQtSYSEEEIIEbYkERJCCCFE2JJESAghhBBhSxIhIYRoRP755x+UUvzvf/+r0/ucdtppnHbaaXV6DyEOBpIICXGIuuKKK4iIiODvv/+udO6JJ55AKcWMGTN8x5RSvi/DMEhPT6dv374sXLgw5HuuWrUKpRTLli0LWOa0007zu09cXBwdO3bkyiuvZO7cuTV6j/VlwoQJfs8n0JckDkI0PtaGDkAIUTeeffZZZs2axYgRI5g/f77v+ObNm3n44Ye56KKLOPfcc/2uOeuss7jqqqvQWrN582ZeeeUVzjjjDGbOnMnZZ59d7T1nzpxJSkoKxx13XNByLVq0YOLEiQAUFhayYcMGPv/8cz744AMuvfRSPvjgA2w2236867oxaNAg2rdv73tdUFDAyJEjufDCCxk0aJDveGpqakOEJ4Q4EFoIcch6/fXXNaAnT57sO9a/f38dFxen//vvP7+ygB41apTfsT/++EMDum/fviHdr3fv3nrYsGFBy5x66qm6S5culY673W590003aUDfddddId2voezatUsDevz48UHLFRcXa4/HU6v33rx5swb0008/Xav17uvUU0/Vp556ap3eQ4iDgXSNCXEIu+666zjppJO44447yMrKYsqUKcyZM4dHH32U5s2bV3t9t27daNq0KZs3b662bE5ODj/99BMDBgzYr1gtFgsvvvginTt35qWXXiI3N9d37p133uGMM84gJSUFh8NB586defXVV/2uHzZsGE2bNsXlclWqu2/fvnTs2NH3eu7cuZx88skkJCQQExNDx44duffee/cr7nILFy5EKcWUKVO4//77ad68OVFRUeTl5ZGdnc0dd9xBt27diImJIS4ujrPPPpvff/+9Uj0lJSVMmDCBww8/nIiICJo1a8agQYPYuHFjwHtrrbnhhhuw2+18/vnnvuMffPABxxxzDJGRkSQlJTFkyBC2bt1a6frXX3+dww47jMjISI4//ni+//77A3oWQjQm0jUmxCFMKcVrr73GUUcdxciRI/n+++859thjGTVqVEjX79mzhz179vh1CwXyzTffoJSib9+++x2vxWJh6NChPPDAA/zwww++pOrVV1+lS5cunH/++VitVr7++mtuuukmTNP0vZcrr7yS9957j2+++cavyy8jI4P58+czfvx4ANasWcO5555L9+7defjhh3E4HGzYsIEff/xxv+Ou6JFHHsFut3PHHXdQWlqK3W5n7dq1fPHFF1xyySW0bduWzMxMXnvtNU499VTWrl1Leno6AB6Ph3PPPZd58+YxZMgQbr31VvLz85k7dy6rV6/msMMOq3Q/j8fDtddey9SpU5k+fbrvmT322GM88MADXHrppVx33XXs2rWL//u//+OUU07ht99+IyEhAYC33nqLG2+8kRNPPJExY8awadMmzj//fJKSkmjZsmWtPBMhDmoN3SQlhKh748aN04C2WCx6xYoVVZYB9PDhw/WuXbv0zp079dKlS/WZZ56pAf3MM89Ue48rr7wypK6UQF1j5aZPn64B/cILL/iOFRUVVSrXr18/3a5dO99rj8ejW7RooQcPHuxX7tlnn9VKKb1p0yattdbPPfecBvSuXbuqjTWQqrrGFixYoAHdrl27SvGWlJRU6iLbvHmzdjgc+uGHH/Yde/vttzWgn3322Ur3NE3Tdx1lXWMul0sPHjxYR0ZG6m+++cZX9p9//tEWi0U/9thjfnWsWrVKW61W33Gn06lTUlL0kUceqUtLS33lyrtUpWtMhAPpGhMiDDRt2hSA9PR0unbtGrDcW2+9RXJyMikpKfTs2ZMff/yRsWPHMmbMmKD1m6bJnDlz9rtbrKKYmBgA8vPzfcciIyN9/z83N5fdu3dz6qmnsmnTJl8XmmEYXH755Xz11Vd+13744YeceOKJtG3bFsDXEvLll19imuYBx7uvYcOG+cUL4HA4MAzvr1uPx0NWVpavS+7XX3/1lfvss89o2rQpN998c6V6lVJ+r51OJ5dccgkzZsxg1qxZfi1xn3/+OaZp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liRCQgghhAhbkggJIYQQImxJIiSEEEKIsCWJkBBCCCHCliRCDeDll1+mTZs2RERE0LNnT5YtW9bQIdWpxYsXc95555Geno5Sii+++MLvvNaaBx98kGbNmhEZGUmfPn1Yv369X5ns7Gwuv/xy4uLiSEhIYPjw4RQUFNTju6hdEydO5LjjjiM2NpaUlBQGDhzIunXr/MqUlJQwatQomjRpQkxMDBdddBGZmZl+ZbZs2cKAAQOIiooiJSWFO++8E7fbXZ9vpVa9+uqrdO/e3bfKba9evZg9e7bvfDg+k6o88cQTKKUYM2aM71g4PpsJEyaglPL76tSpk+98OD6Tctu2beOKK66gSZMmREZG0q1bN5YvX+47H46/dwPSol5NmTJF2+12/fbbb+s1a9bo66+/XickJOjMzMyGDq3OzJo1S9933336888/14CePn263/knnnhCx8fH6y+++EL//vvv+vzzz9dt27bVxcXFvjL9+/fXPXr00D///LP+/vvvdfv27fXQoUPr+Z3Unn79+ul33nlHr169Wq9cuVKfc845ulWrVrqgoMBXZsSIEbply5Z63rx5evny5fqEE07QJ554ou+82+3WXbt21X369NG//fabnjVrlm7atKkeN25cQ7ylWvHVV1/pmTNn6r///luvW7dO33vvvdpms+nVq1drrcPzmexr2bJluk2bNrp79+761ltv9R0Px2czfvx43aVLF71jxw7f165du3znw/GZaK11dna2bt26tb766qv10qVL9aZNm/Q333yjN2zY4CsTjr93A5FEqJ4df/zxetSoUb7XHo9Hp6en64kTJzZgVPVn30TINE2dlpamn376ad+xnJwc7XA49Mcff6y11nrt2rUa0L/88ouvzOzZs7VSSm/btq3eYq9LO3fu1IBetGiR1tr7DGw2m542bZqvzJ9//qkBvWTJEq21N8E0DENnZGT4yrz66qs6Li5Ol5aW1u8bqEOJiYn6zTfflGeitc7Pz9cdOnTQc+fO1aeeeqovEQrXZzN+/Hjdo0ePKs+F6zPRWuu7775bn3zyyQHPy+9df9I1Vo+cTicrVqygT58+vmOGYdCnTx+WLFnSgJE1nM2bN5ORkeH3TOLj4+nZs6fvmSxZsoSEhASOPfZYX5k+ffpgGAZLly6t95jrQm5uLgBJSUkArFixApfL5fdcOnXqRKtWrfyeS7du3UhNTfWV6devH3l5eaxZs6Yeo68bHo+HKVOmUFhYSK9eveSZAKNGjWLAgAF+zwDC+/tl/fr1pKen065dOy6//HK2bNkChPcz+eqrrzj22GO55JJLSElJ4aijjuKNN97wnZffu/4kEapHu3fvxuPx+P3QAaSmppKRkdFAUTWs8vcd7JlkZGSQkpLid95qtZKUlHRIPDfTNBkzZgwnnXQSXbt2Bbzv2W63k5CQ4Fd23+dS1XMrP9dYrVq1ipiYGBwOByNGjGD69Ol07tw5rJ8JwJQpU/j111+ZOHFipXPh+mx69uzJ5MmTmTNnDq+++iqbN2+md+/e5Ofnh+0zAdi0aROvvvoqHTp04JtvvmHkyJHccsstvPvuu4D83t2XtaEDECLcjRo1itWrV/PDDz80dCgHhY4dO7Jy5Upyc3P59NNPGTZsGIsWLWrosBrU1q1bufXWW5k7dy4RERENHc5B4+yzz/b9/+7du9OzZ09at27NJ598QmRkZANG1rBM0+TYY4/l8ccfB+Coo45i9erVTJo0iWHDhjVwdAcfaRGqR02bNsVisVSatZCZmUlaWloDRdWwyt93sGeSlpbGzp07/c673W6ys7Mb/XMbPXo0M2bMYMGCBbRo0cJ3PC0tDafTSU5Ojl/5fZ9LVc+t/FxjZbfbad++PccccwwTJ06kR48evPDCC2H9TFasWMHOnTs5+uijsVqtWK1WFi1axIsvvojVaiX1/9u5f5DWoSgM4EfQFIvUCC21CC0dFBUXqSjBsSI4iVMRB9FBVNy6dHF0dXFz0UHBTcRFlNYKChaUlBaE+oeqiyAIYkURId8bxGD0dfNZfPf7QaAkl3BzCJePNCd+v7K1+UjXdWlpaZHz83Ol75dAICDt7e2OfW1tbfbfhqqvu58xCP0gTdMkEolIMpm091mWJclkUgzDqODMKiccDktjY6OjJg8PD5LJZOyaGIYh9/f3cnx8bI9JpVJiWZb09PT8+Jy/AwCZmZmR9fV1SaVSEg6HHccjkYjU1NQ46lIoFOT6+tpRl3w+71isdnZ2xOPxfFkEfzPLsuTl5UXpmkSjUcnn85LNZu2tq6tLRkZG7N+q1uajx8dHubi4kEAgoPT90tvb++VzHKenpxIKhURE3XW3rEq/ra2atbU1uFwuLC8v4+TkBBMTE9B13dG18L8plUowTROmaUJEMD8/D9M0cXV1BeCtjVPXdWxsbCCXy2FwcPCvbZydnZ3IZDLY399Hc3Pzr27jnJqaQn19PdLptKP19+npyR4zOTmJYDCIVCqFo6MjGIYBwzDs4++tv/39/chms9ja2oLP5/vVrb+JRAJ7e3soFovI5XJIJBKoqqrC9vY2ADVrUs7HrjFAzdrE43Gk02kUi0UcHBygr68PXq8Xt7e3ANSsCfD2iYXq6mrMzc3h7OwMq6urcLvdWFlZsceouO6WwyBUAQsLCwgGg9A0Dd3d3Tg8PKz0lP6p3d1diMiXbXR0FMBbK+fs7Cz8fj9cLhei0SgKhYLjHHd3dxgeHkZdXR08Hg/GxsZQKpUqcDXf42/1EBEsLS3ZY56fnzE9PY2Ghga43W4MDQ3h5ubGcZ7Ly0sMDAygtrYWXq8X8Xgcr6+vP3w132d8fByhUAiapsHn8yEajdohCFCzJuV8DkIq1iYWiyEQCEDTNDQ1NSEWizm+laNiTd5tbm6io6MDLpcLra2tWFxcdBxXcd0tpwoAKvMsioiIiKiy+I4QERERKYtBiIiIiJTFIERERETKYhAiIiIiZTEIERERkbIYhIiIiEhZDEJERESkLAYhIiIiUhaDEBERESmLQYiIiIiUxSBEREREyvoDclGlBulRCFoAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.title(\"XP / Days Tracked\")\n", + "plt.scatter(users[\"numDaysTracked\"], users[\"xp\"], c=users[\"reminderSent\"])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Finally we will look at the last type of outlier, the users who have the gender of the character set to neither male nor female." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
_idxpxpHistoryitemsskillscompletedskillsinprogresschallengescompletedchallengesinprogresscharactertimezonebaselocationlastTrackednumDaysTrackedreminderSent
14956301810ef73481669ecd49a81900[0, 0, 500, 900, 900, 1600, 1900, 1900][ObjectId('62c226d09efefadfd10e20c6'), ObjectI...[ObjectId('62c226cf9efefadfd10e20ad'), ObjectI...[ObjectId('62c226d19efefadfd10e20d9'), ObjectI...[][ObjectId('62c226df9efefadfd10e2242'), ObjectI...ok-6.04972224881457889292022-09-29 21:57:05.492311
\n", + "
" + ], + "text/plain": [ + " _id xp xpHistory \\\n", + "1495 6301810ef73481669ecd49a8 1900 [0, 0, 500, 900, 900, 1600, 1900, 1900] \n", + "\n", + " items \\\n", + "1495 [ObjectId('62c226d09efefadfd10e20c6'), ObjectI... \n", + "\n", + " skillscompleted \\\n", + "1495 [ObjectId('62c226cf9efefadfd10e20ad'), ObjectI... \n", + "\n", + " skillsinprogress challengescompleted \\\n", + "1495 [ObjectId('62c226d19efefadfd10e20d9'), ObjectI... [] \n", + "\n", + " challengesinprogress character timezone \\\n", + "1495 [ObjectId('62c226df9efefadfd10e2242'), ObjectI... ok -6.0 \n", + "\n", + " baselocation lastTracked numDaysTracked \\\n", + "1495 497222488145788929 2022-09-29 21:57:05.492 31 \n", + "\n", + " reminderSent \n", + "1495 1 " + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "users[(users[\"character\"] != \"male\") & (users[\"character\"] != \"female\")]" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/play.py b/src/play.py deleted file mode 100644 index 326fcea..0000000 --- a/src/play.py +++ /dev/null @@ -1,2 +0,0 @@ -import stdata -import stgraphs diff --git a/src/skill_correlation.ipynb b/src/skill_correlation.ipynb new file mode 100644 index 0000000..e1248c7 --- /dev/null +++ b/src/skill_correlation.ipynb @@ -0,0 +1,213 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Skills Correlations\n", + "In this Notebook we will analyse the correlations that exist between the completion rates of different skills." + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Imports and Load Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import utilities.data as ud\n", + "import utilities.users as uu\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import seaborn as sns\n", + "\n", + "DATA_DIR = \"./data\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Execute this cell *only* if you wish to fetch the data from the Database.\n", + "ud.fetch_data(DATA_DIR)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Data processing\n", + "Note that we're only interested in non-null users (those with more than 0 *xp* points) for the entirety of this Notebook." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "users, challenges, items, skills, tasks = ud.read_data(DATA_DIR)\n", + "users = uu.process(uu.non_null(users))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This cell may take some time to execute.\n", + "users = uu.add_completions_per_category(users, skills)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Correlation Matrix\n", + "In the following section we will be analysing the *correlation matrix* of the completion rates of skills per category.\n", + "\n", + "Any negative values in this matrix should be analysed in detail, as this would mean that completing certain skills would make it less likely for users to complete others. Of course, this result is not expected." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "users_completion_rates = users[[category + \"C\" for category in skills[\"category\"].unique()]]\n", + "corr_matrix = users_completion_rates.corr()\n", + "corr_matrix" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The following heatmap provides a more intuitive visualisation of the *correlation matrix*." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sns.heatmap(corr_matrix)" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We now calculate the *mean correlation* for each of the categories. This is a measure of the *leverage* that each category has. A higher number means that users that complete skills in that category are more likely to complete skills in other ones. It would therefore be a good idea to encourage users to complete high leverage skills, to encourage more activity throughout the entire *Skill Tree*.\n", + "\n", + "It is important to interpret these results in context. For example, if the score for *MENTAL HEALTH* is low, it could mean that some of its skills are very easy to complete and that some users have only completed skills in this category. It is also important to note that certain categories do not contain many skills.\n", + "\n", + "It would also be possible to modify this Notebook in order to only analyse users in the upper quartiles of *xp*, to minimise the effect explained above." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "corr_matrix.mean()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Another informative way to view the correlations is with respect to the mean correlation. In this way, a value of 2 in the following matrix would mean that those two skills correlate twice as much as the mean correlation. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "corr_matrix/corr_matrix.unstack().mean()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Another visualisation for completion rates is the following scatter plot. " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# This is an example for mindfulness and screentime.\n", + "plt.title(\"Completions per category\")\n", + "plt.ylabel(\"Mindfulness\")\n", + "plt.xlabel(\"Screentime\")\n", + "\n", + "plt.scatter(users_completion_rates[\"screentimeC\"], users_completion_rates[\"mindfulnessC\"])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can display all such *scatter plots* at once in the following *pair plot*. Also note that the diagonal displays a univariate distribution plot to show the marginal distribution of the data in each column." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sns.pairplot(users_completion_rates)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/stdata.py b/src/stdata.py deleted file mode 100644 index a56c0b7..0000000 --- a/src/stdata.py +++ /dev/null @@ -1,110 +0,0 @@ -from pymongo import MongoClient -from pymongo.server_api import ServerApi -import os - - -class DataObject: - def __init__(self) -> None: - db_user = os.getenv("STDB_USER") - db_password = os.getenv("STDB_PASS") - self.client = MongoClient(f'mongodb+srv://{db_user}:{db_password}@adonis.n0u0i.mongodb.net/Database?retryWrites=true&w=majority', server_api=ServerApi('1')) - self.db = self.client.Database - self.users = self.db.Users - self.challenges = self.db.Challenges - self.items = self.db.Items - self.skills = self.db.Skills - self.tasks = self.db.Tasks - - def close(self) -> None: - self.client.close() - - -class UserData (DataObject): - def count_users(self, parameter={}) -> int: - return len(list(self.users.find(parameter))) - - def timezone_counter(self, parameter={}) -> dict: - from collections import Counter, OrderedDict - time_zone_list = [] - users = self.users.find(parameter) - for user in users: - time_zone_list.append(str(user["timezone"])) - timezone_dict = OrderedDict(Counter(time_zone_list).most_common()) - return timezone_dict - - def number_skills_completed_dict(self, parameter={}) -> dict: - from collections import Counter - data = [len(user["skillscompleted"]) for user in self.users.find(parameter)] - final_dict = Counter(data) - return final_dict - - def number_skills_completed_data(self, parameter={}) -> str: - import pandas as pd - data = [len(user["skillscompleted"]) for user in self.users.find(parameter)] - total = pd.Series(data).describe() - return total - -class SkillData(DataObject): - def order_skills_by_popularity(self, user_parameter={}) -> list: - from collections import Counter, OrderedDict - total_list = [] - users = self.users.find(user_parameter) - for user in users: - for skill in user["skillscompleted"]: - total_list.append(skill) - - total_dictionary = OrderedDict(Counter(total_list).most_common()) - skills = total_dictionary.keys() - - skill_descriptions = [self.skills.find_one({"_id":skill})["goals"] for skill in skills] - title_count = dict(zip(skill_descriptions, total_dictionary.values())) - - return title_count - - def get_skill_completion_rate(self, user_parameter={}, skill_parameter={}) -> dict: - from collections import Counter - users = self.users.find(user_parameter) - skills = [skill["_id"] for skill in self.skills.find(skill_parameter)] - completed_list = [] - progress_list = [] - - for user in users: - for completed in user["skillscompleted"]: - if completed in skills: - completed_list.append(completed) - for progress in user["skillsinprogress"]: - if completed in skills: - progress_list.append(progress) - - completed_counted = Counter(completed_list) - progress_counted = Counter(progress_list) - data_unordered = {key: {'Started': value + completed_counted[key], 'Progress': value, 'Completed': completed_counted[key], 'Score':float(completed_counted[key])/float(value+completed_counted[key])} for (key, value) in progress_counted.items()} - data_ordered = dict(sorted(data_unordered.items(), key=lambda x:x[1]['Score'])) - - return data_ordered - - def list_skills_by_ease(self, skill_parameter={}) -> dict: - data = self.get_skill_completion_rate(skill_parameter=skill_parameter) - keys = [self.skills.find_one({"_id":id})["title"] for id in data.keys()] - values = [value['Score'] for value in data.values()] - total_dict = dict(zip(keys, values)) - return total_dict - - -class ChallengeData(DataObject): - def order_challenges_by_popularity(self, user_parameter={}) -> list: - from collections import Counter, OrderedDict - total_list = [] - users = self.users.find(user_parameter) - for user in users: - for challenge in user["challengescompleted"]: - total_list.append(challenge) - - total_dictionary = OrderedDict(Counter(total_list).most_common()) - challenges = total_dictionary.keys() - - challenge_descriptions = [self.challenges.find_one({"_id":challenge})["goals"][0] for challenge in challenges] - title_count = dict(zip(challenge_descriptions, total_dictionary.values())) - - return title_count - \ No newline at end of file diff --git a/src/user_clustering.ipynb b/src/user_clustering.ipynb new file mode 100644 index 0000000..2fd2ab8 --- /dev/null +++ b/src/user_clustering.ipynb @@ -0,0 +1,101 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# User Distances\n", + "In this Notebook we will be looking at different ways of calculating distances between users. This way we can suggest skills by comparing to completed skills of other similar users." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import utilities.data as ud\n", + "import utilities.skills as us\n", + "import utilities.users as uu\n", + "import pandas as pd\n", + "import matplotlib as plt\n", + "import seaborn as sns\n", + "import numpy as np\n", + "\n", + "DATA_DIR = \"./data\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Execute only if you want to fetch the data from the Database.\n", + "ud.fetch_data(DATA_DIR)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "users, challenges, items, skills, tasks = ud.read_data(DATA_DIR)\n", + "users = uu.process(uu.non_null(users))\n", + "#We will be using users in the upper quantiles for these examples.\n", + "users = users[users[\"xp\"]>users[\"xp\"].quantile(0.8)]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "users = uu.add_skills_completed(users, skills)\n", + "users.head()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Here's an example of an epsilon neighborhood.\n", + "uu.epsilon_neighborhood(users.iloc[4], users, skills, 6)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/utilities/__init__.py b/src/utilities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utilities/data.py b/src/utilities/data.py new file mode 100644 index 0000000..1e12a91 --- /dev/null +++ b/src/utilities/data.py @@ -0,0 +1,53 @@ +from pymongo import MongoClient +from pymongo.server_api import ServerApi +import os +import pandas as pd + +def fetch_data(dir: str) -> None: + ''' + Stores the data contained in the database locally. + ''' + + db_user = os.getenv("STDB_USER") + db_password = os.getenv("STDB_PASS") + client = MongoClient(f'mongodb+srv://{db_user}:{db_password}@adonis.n0u0i.mongodb.net/Database?retryWrites=true&w=majority', server_api=ServerApi('1')) + + db = client.Database + db_users = db.Users + db_challenges = db.Challenges + db_items = db.Items + db_skills = db.Skills + db_tasks = db.Tasks + + users = pd.DataFrame(list(db_users.find({}))) + challenges = pd.DataFrame(list(db_challenges.find({}))) + items = pd.DataFrame(list(db_items.find({}))) + skills = pd.DataFrame(list(db_skills.find({}))) + tasks = pd.DataFrame(list(db_tasks.find({}))) + + try: + os.mkdir(dir) + except FileExistsError: + pass + + users.to_csv(dir+"/users.csv") + challenges.to_csv(dir+"/challenges.csv") + items.to_csv(dir+"/items.csv") + skills.to_csv(dir+"/skills.csv") + tasks.to_csv(dir+"/tasks.csv") + + client.close() + +def read_data(dir: str) -> tuple: + ''' + Returns the data as a tuple of Dataframes + of users, challenges, items, skills and tasks. + ''' + + users = pd.read_csv(dir+"/users.csv") + challenges = pd.read_csv(dir+"/challenges.csv") + items = pd.read_csv(dir+"/items.csv") + skills = pd.read_csv(dir+"/skills.csv") + tasks = pd.read_csv(dir+"/tasks.csv") + + return users, challenges, items, skills, tasks diff --git a/src/utilities/skills.py b/src/utilities/skills.py new file mode 100644 index 0000000..863a138 --- /dev/null +++ b/src/utilities/skills.py @@ -0,0 +1,43 @@ +''' +Provides useful methods regarding skills. +''' + +import pandas as pd + +def add_completed(skills, users) -> pd.DataFrame: + ''' + Adds a column containing information on how many skills have been completed. + ''' + skills["completed"] = 0 + for i, skill in skills.iterrows(): + id = skill["_id"] + count = 0 + for j, user in users.iterrows(): + for completed in user["skillscompleted"]: + if completed==id: + count += 1 + skills["completed"][i] = count + + return skills + +def add_in_progress(skills, users): + ''' + Adds a column containing information on how many skills have been completed. + ''' + skills["completed"] = 0 + for i, skill in skills.iterrows(): + id = skill["_id"] + count = 0 + for j, user in users.iterrows(): + for completed in user["skillsinprogress"]: + if completed==id: + count += 1 + skills["completed"][i] = count + + return skills + +def id_to_title(skills: pd.DataFrame, id: str) -> str: + ''' + Takes the id of a skill and returns its title. + ''' + return str(skills[skills["_id"] == id]["title"][0]) \ No newline at end of file diff --git a/src/utilities/users.py b/src/utilities/users.py new file mode 100644 index 0000000..78a3eda --- /dev/null +++ b/src/utilities/users.py @@ -0,0 +1,109 @@ +''' +Includes useful methods to process and analyse user data. +''' + +import pandas as pd +import numpy as np + + +def process(users: pd.DataFrame) -> pd.DataFrame: + ''' + Deletes unnecessary information and modifies certain columns. + ''' + users = users.drop(["__v", "discordid", "Unnamed: 0"], axis=1) + users["reminderSent"] = users["reminderSent"].replace({True: 1, False: -1}) + return users + +def active(users: pd.DataFrame) -> pd.DataFrame: + ''' + Returns a new Dataframe including only active users. + ''' + return users[users["numDaysTracked"] > 0] + +def non_null(users: pd.DataFrame) -> pd.DataFrame: + ''' + Returns a new Dataframe including only users with non-null xp. + ''' + return users[users["xp"] != 0] + +def null(users: pd.DataFrame) -> pd.DataFrame: + ''' + Returns a new Dataframe including only users with null xp. + ''' + return users[users["xp"] == 0] + +def current(users: pd.DataFrame) -> pd.DataFrame: + ''' + Returns a new Dataframe including only users who have tracked in the week prior to + the current date. + ''' + raise NotImplementedError + +def coeff_variation(users: pd.DataFrame, field: str) -> pd.DataFrame: + ''' + Calculates the coefficient of variation of users for a given field. + ''' + return users[field].std()/users[field].mean() + +def add_completions_per_category(users: pd.DataFrame, skills: pd.DataFrame) -> pd.DataFrame: + ''' + Adds a new column per category of skills detailing the amount of completions each user has + for that given category. + ''' + import ast + import re + users_extended = users + for category in skills["category"].unique(): + users_extended.loc[:, category+"C"] = 0 + + for i, user in users_extended.iterrows(): + clean_str = re.sub(r"ObjectId\('(.+?)'\)", r"'\1'", user["skillscompleted"]) + for completed in ast.literal_eval(clean_str): + skill = skills[skills["_id"] == completed] + category = skill["category"] + users_extended.at[i, category+"C"] = users_extended.loc[i, category+"C"] + 1 + return users_extended + +def add_skills_completed(users:pd.DataFrame, skills:pd.DataFrame) -> pd.DataFrame: + ''' + Adds a new column per skill indicating wether the user has completed said skill. + ''' + import re + import ast + + users_extended = users + for i, skill in skills.iterrows(): + users_extended.loc[:, skill["title"]] = 0 + + for i, user in users_extended.iterrows(): + clean_str = re.sub(r"ObjectId\('(.+?)'\)", r"'\1'", user["skillscompleted"]) + for completed in ast.literal_eval(clean_str): + skill = skills[skills["_id"] == completed] + users_extended.at[i, skill["title"]] = users_extended.loc[i, skill["title"]] + 1 + + return users_extended + +def distance(user1: pd.Series, user2: pd.Series, skills: pd.DataFrame) -> float: + ''' + Calculates the completion distance between two users. + + Note that the completion rates must have been added. + ''' + user1_vect = user1[[title for title in skills["title"].unique()]] + user2_vect = user2[[title for title in skills["title"].unique()]] + return np.sqrt(np.array(user1_vect)@np.array(user2_vect)) + +def epsilon_neighborhood(user:pd.DataFrame, users:pd.DataFrame, skills:pd.DataFrame, epsilon:float) -> pd.DataFrame: + ''' + Returns a DataFrame containing all the users that are at a certain distance away from a given user. + + Note that the completion rates must have been added. + ''' + series = [] + for i, user2 in users.iterrows(): + if user2["_id"] != user["_id"]: + if distance(user, user2, skills) <= epsilon: + series.append(user2) + + return pd.DataFrame(series) +