From 778e581459b50b65b1618cb9137966bc24569052 Mon Sep 17 00:00:00 2001 From: Alex Scofield Date: Thu, 29 Sep 2022 19:13:59 +0200 Subject: [PATCH 1/6] Added comments in stdata and cleaned up the code. I will continue to clean up the code in order to add more methods in a more elegant manner. --- documentation.txt | 9 ++++++++- src/stgraphs.py | 14 ++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/documentation.txt b/documentation.txt index 994d5b0..bc00017 100644 --- a/documentation.txt +++ b/documentation.txt @@ -10,6 +10,7 @@ count_users: Takes a dictionary as a parameter to specify which users to count. 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. +days_tracked_data: Returns useful information regarding the days that users track their data, such as the average amount of days, the standard deviation and certain percentiles. 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. @@ -18,6 +19,12 @@ list_skills_by_ease: Returns a dictionary with each skill and its completion rat 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. +get_challenge_completion_rate: Returns number of people who have completed, are in progress and have started each challenge, as well as the ease of said challenge, measured as a fraction of Completed/Started. +get_challenge_ease: Returns a dictionary with each Challenge and its completion rate. + + + + GRAPH METHODS: @@ -27,7 +34,7 @@ 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 +graph_number_skills_completed: Bar chart with the number of skills completed. 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). diff --git a/src/stgraphs.py b/src/stgraphs.py index 6151311..e79bde8 100644 --- a/src/stgraphs.py +++ b/src/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) - From 8f10de647fa2d3926b6a1d86fcc6df8987303a8c Mon Sep 17 00:00:00 2001 From: Alex Scofield Date: Sun, 2 Oct 2022 13:04:22 +0200 Subject: [PATCH 2/6] Added base class for Skills and Challenges as they share most of their methods. --- src/example.py | 33 +++++++++++ src/play.py | 3 + src/stdata.py | 149 +++++++++++++++++++++++++++++++++++-------------- 3 files changed, 142 insertions(+), 43 deletions(-) create mode 100644 src/example.py diff --git a/src/example.py b/src/example.py new file mode 100644 index 0000000..30f96ac --- /dev/null +++ b/src/example.py @@ -0,0 +1,33 @@ +''' +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}) diff --git a/src/play.py b/src/play.py index 326fcea..a747de4 100644 --- a/src/play.py +++ b/src/play.py @@ -1,2 +1,5 @@ import stdata import stgraphs + +oso = stdata.SkillData().order_by_popularity() +print(stdata.SkillData().id_to_title_and_level(oso)) \ No newline at end of file diff --git a/src/stdata.py b/src/stdata.py index a56c0b7..af26214 100644 --- a/src/stdata.py +++ b/src/stdata.py @@ -1,59 +1,93 @@ from pymongo import MongoClient from pymongo.server_api import ServerApi import os +from collections import Counter, OrderedDict +import pandas as pd -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 +# 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: - self.client.close() + DataObject.client.close() + +# Includes methods common to skills and challenges +class ActionData (DataObject): + def __init__(self): + self.data_type = None + self.completed = None + self.find_description = None + + #### NOT QUITE THERE YET. STILL HAVE TO FIGURE OUT HOW TO GO FROM ZIP TO DICT + 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) class UserData (DataObject): + # Count total users def count_users(self, parameter={}) -> int: - return len(list(self.users.find(parameter))) + return len(list(DataObject.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 + # Count users per timezone + def timezone_counter(self, parameter={}) -> OrderedDict: + # First create a list with the timezones each user has, then apply a Counter to it, and then package it all into an Ordered Dict + return count_and_order([str(user["timezone"]) for user in DataObject.users.find(parameter)]) - 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 + # Dictonary with number of skills users have completed + def number_skills_completed_dict(self, parameter={}) -> OrderedDict: + return count_and_order([len(user["skillscompleted"]) for user in self.users.find(parameter)]) + # Describe the skills completed data 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 + return pd.Series([len(user["skillscompleted"]) for user in self.users.find(parameter)]).describe() + + # Describe the days tracked data + def days_tracked_data(self, parameter={}) -> str: + return pd.Series([user["numDaysTracked"] for user in self.users.find(parameter)]).describe() + +# SkillData object, inheriting from DataObject + + +### REWRITING ALL METHODS SUCH THAT THE RETURN IS IN TERMS OF ID. THAT WAY VARIOUS WAYS OF RETURNING DATA WITH EXTRA METHODS +class SkillData(ActionData): + def __init__(self): + super().__init__() + self.data_type = DataObject.skills + self.completed = "skillscompleted" -class SkillData(DataObject): + 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())) + + ## REWRITING THIS METHOD 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()) + # First create a list with the lists of skills that each user has completed and then unpack that list. + skill_list = [user["skillscompleted"] for user in self.users.find(user_parameter)] + + ### FIX TOTAL_LIST (FOR THE MOMENT IT RETURNS SKILL_LIST). USE INDECES + total_list = [skill for skill in skill_list] + total_dictionary = count_and_order(total_list) skills = total_dictionary.keys() skill_descriptions = [self.skills.find_one({"_id":skill})["goals"] for skill in skills] @@ -73,7 +107,7 @@ def get_skill_completion_rate(self, user_parameter={}, skill_parameter={}) -> di if completed in skills: completed_list.append(completed) for progress in user["skillsinprogress"]: - if completed in skills: + if progress in skills: progress_list.append(progress) completed_counted = Counter(completed_list) @@ -85,15 +119,14 @@ def get_skill_completion_rate(self, user_parameter={}, skill_parameter={}) -> di 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()] + keys = [self.skills.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 ChallengeData(DataObject): +class ChallengeData(ActionData): 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: @@ -107,4 +140,34 @@ def order_challenges_by_popularity(self, user_parameter={}) -> list: title_count = dict(zip(challenge_descriptions, total_dictionary.values())) return title_count - \ No newline at end of file + + def get_challenge_completion_rate(self, user_parameter={}, challenge_parameter={}) -> dict: + users = self.users.find(user_parameter) + challenges = [challenge["_id"] for challenge in self.challenges.find(challenge_parameter)] + completed_list = [] + progress_list = [] + + for user in users: + for completed in user["challengescompleted"]: + if completed in challenges: + completed_list.append(completed) + for progress in user["challengesinprogress"]: + if progress in challenges: + 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_challenge_ease(self, challenge_parameter={}) -> dict: + data = self.get_challenge_completion_rate(challenge_parameter=challenge_parameter) + keys = [self.challenges.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 + + +print(SkillData().id_to_goals(SkillData().order_by_popularity())) From 5b49812c6fc8e550cf3d211950c79969c2b442bc Mon Sep 17 00:00:00 2001 From: Alex Scofield Date: Sun, 11 Jun 2023 21:00:04 +0200 Subject: [PATCH 3/6] New refactored version. --- README.md | 19 +- documentation.txt | 44 -- requirements.txt | 3 +- {src => src (outdated)}/__init__.py | 0 {src => src (outdated)}/example.py | 1 + src (outdated)/gui.py | 31 ++ src (outdated)/play.py | 4 + src (outdated)/stdata.py | 129 ++++++ {src => src (outdated)}/stgraphs.py | 0 src/outlier_analysis.ipynb | 601 ++++++++++++++++++++++++++++ src/play.py | 5 - src/stdata.py | 173 -------- src/utilities/__init__.py | 0 src/utilities/data.py | 53 +++ src/utilities/skills.py | 37 ++ src/utilities/users.py | 45 +++ 16 files changed, 918 insertions(+), 227 deletions(-) delete mode 100644 documentation.txt rename {src => src (outdated)}/__init__.py (100%) rename {src => src (outdated)}/example.py (99%) create mode 100644 src (outdated)/gui.py create mode 100644 src (outdated)/play.py create mode 100644 src (outdated)/stdata.py rename {src => src (outdated)}/stgraphs.py (100%) create mode 100644 src/outlier_analysis.ipynb delete mode 100644 src/play.py delete mode 100644 src/stdata.py create mode 100644 src/utilities/__init__.py create mode 100644 src/utilities/data.py create mode 100644 src/utilities/skills.py create mode 100644 src/utilities/users.py 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 bc00017..0000000 --- a/documentation.txt +++ /dev/null @@ -1,44 +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. -days_tracked_data: Returns useful information regarding the days that users track their data, such as the average amount of days, the standard deviation and certain percentiles. - -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. -get_challenge_completion_rate: Returns number of people who have completed, are in progress and have started each challenge, as well as the ease of said challenge, measured as a fraction of Completed/Started. -get_challenge_ease: Returns a dictionary with each Challenge and its completion rate. - - - - - - -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: Bar chart with the number of skills completed. - -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/example.py b/src (outdated)/example.py similarity index 99% rename from src/example.py rename to src (outdated)/example.py index 30f96ac..b1c7e93 100644 --- a/src/example.py +++ b/src (outdated)/example.py @@ -31,3 +31,4 @@ # 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 100% rename from src/stgraphs.py rename to src (outdated)/stgraphs.py diff --git a/src/outlier_analysis.ipynb b/src/outlier_analysis.ipynb new file mode 100644 index 0000000..a298f85 --- /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 a747de4..0000000 --- a/src/play.py +++ /dev/null @@ -1,5 +0,0 @@ -import stdata -import stgraphs - -oso = stdata.SkillData().order_by_popularity() -print(stdata.SkillData().id_to_title_and_level(oso)) \ No newline at end of file diff --git a/src/stdata.py b/src/stdata.py deleted file mode 100644 index af26214..0000000 --- a/src/stdata.py +++ /dev/null @@ -1,173 +0,0 @@ -from pymongo import MongoClient -from pymongo.server_api import ServerApi -import os -from collections import Counter, OrderedDict -import pandas as pd - - -# 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.find_description = None - - #### NOT QUITE THERE YET. STILL HAVE TO FIGURE OUT HOW TO GO FROM ZIP TO DICT - 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) - -class UserData (DataObject): - # Count total users - def count_users(self, parameter={}) -> int: - return len(list(DataObject.users.find(parameter))) - - # Count users per timezone - def timezone_counter(self, parameter={}) -> OrderedDict: - # First create a list with the timezones each user has, then apply a Counter to it, and then package it all into an Ordered Dict - return count_and_order([str(user["timezone"]) for user in DataObject.users.find(parameter)]) - - # Dictonary with number of skills users have completed - def number_skills_completed_dict(self, parameter={}) -> OrderedDict: - return count_and_order([len(user["skillscompleted"]) for user in self.users.find(parameter)]) - - # Describe the skills completed data - def number_skills_completed_data(self, parameter={}) -> str: - return pd.Series([len(user["skillscompleted"]) for user in self.users.find(parameter)]).describe() - - # Describe the days tracked data - def days_tracked_data(self, parameter={}) -> str: - return pd.Series([user["numDaysTracked"] for user in self.users.find(parameter)]).describe() - -# SkillData object, inheriting from DataObject - - -### REWRITING ALL METHODS SUCH THAT THE RETURN IS IN TERMS OF ID. THAT WAY VARIOUS WAYS OF RETURNING DATA WITH EXTRA METHODS -class SkillData(ActionData): - def __init__(self): - super().__init__() - self.data_type = DataObject.skills - self.completed = "skillscompleted" - - 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())) - - ## REWRITING THIS METHOD - def order_skills_by_popularity(self, user_parameter={}) -> list: - - # First create a list with the lists of skills that each user has completed and then unpack that list. - skill_list = [user["skillscompleted"] for user in self.users.find(user_parameter)] - - ### FIX TOTAL_LIST (FOR THE MOMENT IT RETURNS SKILL_LIST). USE INDECES - total_list = [skill for skill in skill_list] - total_dictionary = count_and_order(total_list) - 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 progress 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})["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 ChallengeData(ActionData): - def order_challenges_by_popularity(self, user_parameter={}) -> list: - 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 - - def get_challenge_completion_rate(self, user_parameter={}, challenge_parameter={}) -> dict: - users = self.users.find(user_parameter) - challenges = [challenge["_id"] for challenge in self.challenges.find(challenge_parameter)] - completed_list = [] - progress_list = [] - - for user in users: - for completed in user["challengescompleted"]: - if completed in challenges: - completed_list.append(completed) - for progress in user["challengesinprogress"]: - if progress in challenges: - 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_challenge_ease(self, challenge_parameter={}) -> dict: - data = self.get_challenge_completion_rate(challenge_parameter=challenge_parameter) - keys = [self.challenges.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 - - -print(SkillData().id_to_goals(SkillData().order_by_popularity())) 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..a3ee751 --- /dev/null +++ b/src/utilities/skills.py @@ -0,0 +1,37 @@ +''' +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 \ No newline at end of file diff --git a/src/utilities/users.py b/src/utilities/users.py new file mode 100644 index 0000000..c528f64 --- /dev/null +++ b/src/utilities/users.py @@ -0,0 +1,45 @@ +''' +Includes useful methods to process and analyse user data. +''' + +import pandas as pd + + +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() \ No newline at end of file From fd64b535f4b6131e47614d069ece81f44baa5de4 Mon Sep 17 00:00:00 2001 From: Alex Scofield Date: Tue, 13 Jun 2023 14:03:57 +0200 Subject: [PATCH 4/6] Skill Correlation notebook began. --- src/skill_correlation.ipynb | 478 ++++++++++++++++++++++++++++++++++++ src/utilities/users.py | 21 +- 2 files changed, 498 insertions(+), 1 deletion(-) create mode 100644 src/skill_correlation.ipynb diff --git a/src/skill_correlation.ipynb b/src/skill_correlation.ipynb new file mode 100644 index 0000000..5d0bc8c --- /dev/null +++ b/src/skill_correlation.ipynb @@ -0,0 +1,478 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Skills Correlations\n", + "In this Notebook we will analyse the correlations that there are between the completion rates of different skills." + ] + }, + { + "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", + "\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 non-null users.\n", + "users = uu.process(uu.non_null(users))" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "SKILLSC 1\n", + "Name: 5, dtype: object\n", + "relationshipsC 1\n", + "Name: 5, dtype: object\n", + "skillsC 1\n", + "Name: 5, dtype: object\n", + "MENTAL HEALTHC 1\n", + "Name: 5, dtype: object\n", + "masculinityC 1\n", + "Name: 5, dtype: object\n", + "disciplineC 1\n", + "Name: 5, dtype: object\n", + "SKILLSC 2\n", + "Name: 5, dtype: object\n", + "MENTAL HEALTHC 1\n", + "Name: 8, dtype: object\n", + "fitnessC 1\n", + "Name: 8, dtype: object\n", + "SKILLSC 1\n", + "Name: 8, dtype: object\n", + "DietC 1\n", + "Name: 8, dtype: object\n", + "MENTAL HEALTHC 2\n", + "Name: 8, dtype: object\n", + "skillsC 1\n", + "Name: 8, dtype: object\n", + "mindfulnessC 1\n", + "Name: 8, dtype: object\n", + "disciplineC 1\n", + "Name: 8, dtype: object\n", + "SKILLSC 2\n", + "Name: 8, dtype: object\n", + "SKILLSC 1\n", + "Name: 16, dtype: object\n", + "MENTAL HEALTHC 1\n", + "Name: 16, dtype: object\n", + "skillsC 1\n", + "Name: 16, dtype: object\n", + "fitnessC 1\n", + "Name: 16, dtype: object\n", + "MENTAL HEALTHC 2\n", + "Name: 16, dtype: object\n", + "SKILLSC 2\n", + "Name: 16, dtype: object\n", + "relationshipsC 1\n", + "Name: 16, dtype: object\n", + "SKILLSC 3\n", + "Name: 16, dtype: object\n", + "masculinityC 1\n", + "Name: 16, dtype: object\n", + "SKILLSC 4\n", + "Name: 16, dtype: object\n", + "disciplineC 1\n", + "Name: 16, dtype: object\n", + "SKILLSC 5\n", + "Name: 16, dtype: object\n", + "MENTAL HEALTHC 1\n", + "Name: 22, dtype: object\n", + "fitnessC 1\n", + "Name: 22, dtype: object\n", + "SKILLSC 1\n", + "Name: 22, dtype: object\n", + "disciplineC 1\n", + "Name: 22, dtype: object\n", + "MENTAL HEALTHC 2\n", + "Name: 22, dtype: object\n", + "routinesC 1\n", + "Name: 22, dtype: object\n", + "DietC 1\n", + "Name: 22, dtype: object\n", + "mindfulnessC 1\n", + "Name: 22, dtype: object\n", + "masculinityC 1\n", + "Name: 22, dtype: object\n", + "SKILLSC 2\n", + "Name: 22, dtype: object\n", + "mindfulnessC 2\n", + "Name: 22, dtype: object\n", + "screentimeC 1\n", + "Name: 22, dtype: object\n", + "fitnessC 2\n", + "Name: 22, dtype: object\n", + "SKILLSC 3\n", + "Name: 22, dtype: object\n", + "mindfulnessC 3\n", + "Name: 22, dtype: object\n", + "routinesC 2\n", + "Name: 22, dtype: object\n", + "hydrationC 1\n", + "Name: 22, dtype: object\n", + "screentimeC 2\n", + "Name: 22, dtype: object\n", + "routinesC 3\n", + "Name: 22, dtype: object\n", + "hydrationC 2\n", + "Name: 22, dtype: object\n", + "SKILLSC 1\n", + "Name: 23, dtype: object\n", + "disciplineC 1\n", + "Name: 23, dtype: object\n", + "skillsC 1\n", + "Name: 23, dtype: object\n", + "SKILLSC 2\n", + "Name: 23, dtype: object\n", + "SKILLSC 3\n", + "Name: 23, dtype: object\n", + "SKILLSC 4\n", + "Name: 23, dtype: object\n", + "SKILLSC 5\n", + "Name: 23, dtype: object\n", + "SKILLSC 6\n", + "Name: 23, dtype: object\n", + "SKILLSC 7\n", + "Name: 23, dtype: object\n", + "SKILLSC 8\n", + "Name: 23, dtype: object\n", + "MENTAL HEALTHC 1\n", + "Name: 23, dtype: object\n", + "MENTAL HEALTHC 2\n", + "Name: 23, dtype: object\n", + "fitnessC 1\n", + "Name: 23, dtype: object\n", + "SKILLSC 9\n", + "Name: 23, dtype: object\n", + "relationshipsC 1\n", + "Name: 23, dtype: object\n", + "DietC 1\n", + "Name: 23, dtype: object\n" + ] + } + ], + "source": [ + "# This cell can take some time to execute.\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "users = uu.add_completions_per_category(users.head(), skills)\n", + "warnings.resetwarnings()" + ] + }, + { + "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", + " \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", + "
_idxpxpHistoryitemsskillscompletedskillsinprogresschallengescompletedchallengesinprogresscharactertimezone...mindfulnessCroutinesCDietChydrationCattractionCscreentimeCrelationshipsCdisciplineCmasculinityCskillsC
562c4f336953318ddc80d82a0300[0, 300, 300, 300, 300][ObjectId('62c382d46cac02c487e243cb'), ObjectI...[ObjectId('62c226cf9efefadfd10e20b2'), ObjectI...[ObjectId('62c226d89efefadfd10e219a'), ObjectI...[][]male-8.0...0000000000
862c4f357953318ddc80d82c7100[0, 100][ObjectId('62c382d46cac02c487e243cb'), ObjectI...[ObjectId('62c226cf9efefadfd10e20ad'), ObjectI...[ObjectId('62c226d19efefadfd10e20d9'), ObjectI...[][ObjectId('62c226d09efefadfd10e20bb')]male-5.0...0000000000
1662c4f3fe953318ddc80d8369860[0, 860][ObjectId('62c382d46cac02c487e243cb'), ObjectI...[ObjectId('62c226cf9efefadfd10e20b2'), ObjectI...[ObjectId('62c226d09efefadfd10e20b6'), ObjectI...[ObjectId('62c226d09efefadfd10e20bb')][]male-5.0...0000000000
2262c4f691953318ddc80d84c16890[0, 1170, 1840, 2890, 2890, 3490, 4290, 4290, ...[ObjectId('62c382d46cac02c487e243cb'), ObjectI...[ObjectId('62c226cf9efefadfd10e20ad'), ObjectI...[ObjectId('62c226d89efefadfd10e21a0'), ObjectI...[ObjectId('62c226d09efefadfd10e20bb'), ObjectI...[]male-5.0...0000000000
2362c4f6a1953318ddc80d84cc1000[0, 1000][ObjectId('62c382d46cac02c487e243cb'), ObjectI...[ObjectId('62c226cf9efefadfd10e20b2'), ObjectI...[ObjectId('62c226d89efefadfd10e2197'), ObjectI...[ObjectId('62c226e09efefadfd10e2267'), ObjectI...[]male-4.0...0000000000
\n", + "

5 rows × 27 columns

\n", + "
" + ], + "text/plain": [ + " _id xp \\\n", + "5 62c4f336953318ddc80d82a0 300 \n", + "8 62c4f357953318ddc80d82c7 100 \n", + "16 62c4f3fe953318ddc80d8369 860 \n", + "22 62c4f691953318ddc80d84c1 6890 \n", + "23 62c4f6a1953318ddc80d84cc 1000 \n", + "\n", + " xpHistory \\\n", + "5 [0, 300, 300, 300, 300] \n", + "8 [0, 100] \n", + "16 [0, 860] \n", + "22 [0, 1170, 1840, 2890, 2890, 3490, 4290, 4290, ... \n", + "23 [0, 1000] \n", + "\n", + " items \\\n", + "5 [ObjectId('62c382d46cac02c487e243cb'), ObjectI... \n", + "8 [ObjectId('62c382d46cac02c487e243cb'), ObjectI... \n", + "16 [ObjectId('62c382d46cac02c487e243cb'), ObjectI... \n", + "22 [ObjectId('62c382d46cac02c487e243cb'), ObjectI... \n", + "23 [ObjectId('62c382d46cac02c487e243cb'), ObjectI... \n", + "\n", + " skillscompleted \\\n", + "5 [ObjectId('62c226cf9efefadfd10e20b2'), ObjectI... \n", + "8 [ObjectId('62c226cf9efefadfd10e20ad'), ObjectI... \n", + "16 [ObjectId('62c226cf9efefadfd10e20b2'), ObjectI... \n", + "22 [ObjectId('62c226cf9efefadfd10e20ad'), ObjectI... \n", + "23 [ObjectId('62c226cf9efefadfd10e20b2'), ObjectI... \n", + "\n", + " skillsinprogress \\\n", + "5 [ObjectId('62c226d89efefadfd10e219a'), ObjectI... \n", + "8 [ObjectId('62c226d19efefadfd10e20d9'), ObjectI... \n", + "16 [ObjectId('62c226d09efefadfd10e20b6'), ObjectI... \n", + "22 [ObjectId('62c226d89efefadfd10e21a0'), ObjectI... \n", + "23 [ObjectId('62c226d89efefadfd10e2197'), ObjectI... \n", + "\n", + " challengescompleted \\\n", + "5 [] \n", + "8 [] \n", + "16 [ObjectId('62c226d09efefadfd10e20bb')] \n", + "22 [ObjectId('62c226d09efefadfd10e20bb'), ObjectI... \n", + "23 [ObjectId('62c226e09efefadfd10e2267'), ObjectI... \n", + "\n", + " challengesinprogress character timezone ... \\\n", + "5 [] male -8.0 ... \n", + "8 [ObjectId('62c226d09efefadfd10e20bb')] male -5.0 ... \n", + "16 [] male -5.0 ... \n", + "22 [] male -5.0 ... \n", + "23 [] male -4.0 ... \n", + "\n", + " mindfulnessC routinesC DietC hydrationC attractionC screentimeC \\\n", + "5 0 0 0 0 0 0 \n", + "8 0 0 0 0 0 0 \n", + "16 0 0 0 0 0 0 \n", + "22 0 0 0 0 0 0 \n", + "23 0 0 0 0 0 0 \n", + "\n", + " relationshipsC disciplineC masculinityC skillsC \n", + "5 0 0 0 0 \n", + "8 0 0 0 0 \n", + "16 0 0 0 0 \n", + "22 0 0 0 0 \n", + "23 0 0 0 0 \n", + "\n", + "[5 rows x 27 columns]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "users.head()\n", + "#users[[category + \"C\" for category in skills[\"category\"].unique()]].corr()" + ] + }, + { + "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/users.py b/src/utilities/users.py index c528f64..4633da7 100644 --- a/src/utilities/users.py +++ b/src/utilities/users.py @@ -42,4 +42,23 @@ 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() \ No newline at end of file + 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[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"] + user[category+"C"] = user[category+"C"] + 1 + return users_extended \ No newline at end of file From d7aceb21a38d463c7f68c6881d1fd7a7f29b1802 Mon Sep 17 00:00:00 2001 From: Alex Scofield Date: Mon, 19 Jun 2023 16:51:34 +0200 Subject: [PATCH 5/6] New Notebook added analysing the correlations between completion rates of different skills. --- src/outlier_analysis.ipynb | 2 +- src/skill_correlation.ipynb | 525 +++++++++--------------------------- src/utilities/users.py | 4 +- 3 files changed, 133 insertions(+), 398 deletions(-) diff --git a/src/outlier_analysis.ipynb b/src/outlier_analysis.ipynb index a298f85..90f5358 100644 --- a/src/outlier_analysis.ipynb +++ b/src/outlier_analysis.ipynb @@ -448,7 +448,7 @@ { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 9, diff --git a/src/skill_correlation.ipynb b/src/skill_correlation.ipynb index 5d0bc8c..e1248c7 100644 --- a/src/skill_correlation.ipynb +++ b/src/skill_correlation.ipynb @@ -6,7 +6,15 @@ "metadata": {}, "source": [ "# Skills Correlations\n", - "In this Notebook we will analyse the correlations that there are between the completion rates of different skills." + "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" ] }, { @@ -19,430 +27,78 @@ "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": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "# Execute only if you want to fetch the data from the Database.\n", + "# 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": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "users, challenges, items, skills, tasks = ud.read_data(DATA_DIR)\n", - "\n", - "# We are only interested in non-null users.\n", "users = uu.process(uu.non_null(users))" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "SKILLSC 1\n", - "Name: 5, dtype: object\n", - "relationshipsC 1\n", - "Name: 5, dtype: object\n", - "skillsC 1\n", - "Name: 5, dtype: object\n", - "MENTAL HEALTHC 1\n", - "Name: 5, dtype: object\n", - "masculinityC 1\n", - "Name: 5, dtype: object\n", - "disciplineC 1\n", - "Name: 5, dtype: object\n", - "SKILLSC 2\n", - "Name: 5, dtype: object\n", - "MENTAL HEALTHC 1\n", - "Name: 8, dtype: object\n", - "fitnessC 1\n", - "Name: 8, dtype: object\n", - "SKILLSC 1\n", - "Name: 8, dtype: object\n", - "DietC 1\n", - "Name: 8, dtype: object\n", - "MENTAL HEALTHC 2\n", - "Name: 8, dtype: object\n", - "skillsC 1\n", - "Name: 8, dtype: object\n", - "mindfulnessC 1\n", - "Name: 8, dtype: object\n", - "disciplineC 1\n", - "Name: 8, dtype: object\n", - "SKILLSC 2\n", - "Name: 8, dtype: object\n", - "SKILLSC 1\n", - "Name: 16, dtype: object\n", - "MENTAL HEALTHC 1\n", - "Name: 16, dtype: object\n", - "skillsC 1\n", - "Name: 16, dtype: object\n", - "fitnessC 1\n", - "Name: 16, dtype: object\n", - "MENTAL HEALTHC 2\n", - "Name: 16, dtype: object\n", - "SKILLSC 2\n", - "Name: 16, dtype: object\n", - "relationshipsC 1\n", - "Name: 16, dtype: object\n", - "SKILLSC 3\n", - "Name: 16, dtype: object\n", - "masculinityC 1\n", - "Name: 16, dtype: object\n", - "SKILLSC 4\n", - "Name: 16, dtype: object\n", - "disciplineC 1\n", - "Name: 16, dtype: object\n", - "SKILLSC 5\n", - "Name: 16, dtype: object\n", - "MENTAL HEALTHC 1\n", - "Name: 22, dtype: object\n", - "fitnessC 1\n", - "Name: 22, dtype: object\n", - "SKILLSC 1\n", - "Name: 22, dtype: object\n", - "disciplineC 1\n", - "Name: 22, dtype: object\n", - "MENTAL HEALTHC 2\n", - "Name: 22, dtype: object\n", - "routinesC 1\n", - "Name: 22, dtype: object\n", - "DietC 1\n", - "Name: 22, dtype: object\n", - "mindfulnessC 1\n", - "Name: 22, dtype: object\n", - "masculinityC 1\n", - "Name: 22, dtype: object\n", - "SKILLSC 2\n", - "Name: 22, dtype: object\n", - "mindfulnessC 2\n", - "Name: 22, dtype: object\n", - "screentimeC 1\n", - "Name: 22, dtype: object\n", - "fitnessC 2\n", - "Name: 22, dtype: object\n", - "SKILLSC 3\n", - "Name: 22, dtype: object\n", - "mindfulnessC 3\n", - "Name: 22, dtype: object\n", - "routinesC 2\n", - "Name: 22, dtype: object\n", - "hydrationC 1\n", - "Name: 22, dtype: object\n", - "screentimeC 2\n", - "Name: 22, dtype: object\n", - "routinesC 3\n", - "Name: 22, dtype: object\n", - "hydrationC 2\n", - "Name: 22, dtype: object\n", - "SKILLSC 1\n", - "Name: 23, dtype: object\n", - "disciplineC 1\n", - "Name: 23, dtype: object\n", - "skillsC 1\n", - "Name: 23, dtype: object\n", - "SKILLSC 2\n", - "Name: 23, dtype: object\n", - "SKILLSC 3\n", - "Name: 23, dtype: object\n", - "SKILLSC 4\n", - "Name: 23, dtype: object\n", - "SKILLSC 5\n", - "Name: 23, dtype: object\n", - "SKILLSC 6\n", - "Name: 23, dtype: object\n", - "SKILLSC 7\n", - "Name: 23, dtype: object\n", - "SKILLSC 8\n", - "Name: 23, dtype: object\n", - "MENTAL HEALTHC 1\n", - "Name: 23, dtype: object\n", - "MENTAL HEALTHC 2\n", - "Name: 23, dtype: object\n", - "fitnessC 1\n", - "Name: 23, dtype: object\n", - "SKILLSC 9\n", - "Name: 23, dtype: object\n", - "relationshipsC 1\n", - "Name: 23, dtype: object\n", - "DietC 1\n", - "Name: 23, dtype: object\n" - ] - } - ], + "outputs": [], "source": [ - "# This cell can take some time to execute.\n", - "import warnings\n", - "warnings.filterwarnings('ignore')\n", - "users = uu.add_completions_per_category(users.head(), skills)\n", - "warnings.resetwarnings()" + "# 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": 5, + "execution_count": null, "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", - " \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", - "
_idxpxpHistoryitemsskillscompletedskillsinprogresschallengescompletedchallengesinprogresscharactertimezone...mindfulnessCroutinesCDietChydrationCattractionCscreentimeCrelationshipsCdisciplineCmasculinityCskillsC
562c4f336953318ddc80d82a0300[0, 300, 300, 300, 300][ObjectId('62c382d46cac02c487e243cb'), ObjectI...[ObjectId('62c226cf9efefadfd10e20b2'), ObjectI...[ObjectId('62c226d89efefadfd10e219a'), ObjectI...[][]male-8.0...0000000000
862c4f357953318ddc80d82c7100[0, 100][ObjectId('62c382d46cac02c487e243cb'), ObjectI...[ObjectId('62c226cf9efefadfd10e20ad'), ObjectI...[ObjectId('62c226d19efefadfd10e20d9'), ObjectI...[][ObjectId('62c226d09efefadfd10e20bb')]male-5.0...0000000000
1662c4f3fe953318ddc80d8369860[0, 860][ObjectId('62c382d46cac02c487e243cb'), ObjectI...[ObjectId('62c226cf9efefadfd10e20b2'), ObjectI...[ObjectId('62c226d09efefadfd10e20b6'), ObjectI...[ObjectId('62c226d09efefadfd10e20bb')][]male-5.0...0000000000
2262c4f691953318ddc80d84c16890[0, 1170, 1840, 2890, 2890, 3490, 4290, 4290, ...[ObjectId('62c382d46cac02c487e243cb'), ObjectI...[ObjectId('62c226cf9efefadfd10e20ad'), ObjectI...[ObjectId('62c226d89efefadfd10e21a0'), ObjectI...[ObjectId('62c226d09efefadfd10e20bb'), ObjectI...[]male-5.0...0000000000
2362c4f6a1953318ddc80d84cc1000[0, 1000][ObjectId('62c382d46cac02c487e243cb'), ObjectI...[ObjectId('62c226cf9efefadfd10e20b2'), ObjectI...[ObjectId('62c226d89efefadfd10e2197'), ObjectI...[ObjectId('62c226e09efefadfd10e2267'), ObjectI...[]male-4.0...0000000000
\n", - "

5 rows × 27 columns

\n", - "
" - ], - "text/plain": [ - " _id xp \\\n", - "5 62c4f336953318ddc80d82a0 300 \n", - "8 62c4f357953318ddc80d82c7 100 \n", - "16 62c4f3fe953318ddc80d8369 860 \n", - "22 62c4f691953318ddc80d84c1 6890 \n", - "23 62c4f6a1953318ddc80d84cc 1000 \n", - "\n", - " xpHistory \\\n", - "5 [0, 300, 300, 300, 300] \n", - "8 [0, 100] \n", - "16 [0, 860] \n", - "22 [0, 1170, 1840, 2890, 2890, 3490, 4290, 4290, ... \n", - "23 [0, 1000] \n", - "\n", - " items \\\n", - "5 [ObjectId('62c382d46cac02c487e243cb'), ObjectI... \n", - "8 [ObjectId('62c382d46cac02c487e243cb'), ObjectI... \n", - "16 [ObjectId('62c382d46cac02c487e243cb'), ObjectI... \n", - "22 [ObjectId('62c382d46cac02c487e243cb'), ObjectI... \n", - "23 [ObjectId('62c382d46cac02c487e243cb'), ObjectI... \n", - "\n", - " skillscompleted \\\n", - "5 [ObjectId('62c226cf9efefadfd10e20b2'), ObjectI... \n", - "8 [ObjectId('62c226cf9efefadfd10e20ad'), ObjectI... \n", - "16 [ObjectId('62c226cf9efefadfd10e20b2'), ObjectI... \n", - "22 [ObjectId('62c226cf9efefadfd10e20ad'), ObjectI... \n", - "23 [ObjectId('62c226cf9efefadfd10e20b2'), ObjectI... \n", - "\n", - " skillsinprogress \\\n", - "5 [ObjectId('62c226d89efefadfd10e219a'), ObjectI... \n", - "8 [ObjectId('62c226d19efefadfd10e20d9'), ObjectI... \n", - "16 [ObjectId('62c226d09efefadfd10e20b6'), ObjectI... \n", - "22 [ObjectId('62c226d89efefadfd10e21a0'), ObjectI... \n", - "23 [ObjectId('62c226d89efefadfd10e2197'), ObjectI... \n", - "\n", - " challengescompleted \\\n", - "5 [] \n", - "8 [] \n", - "16 [ObjectId('62c226d09efefadfd10e20bb')] \n", - "22 [ObjectId('62c226d09efefadfd10e20bb'), ObjectI... \n", - "23 [ObjectId('62c226e09efefadfd10e2267'), ObjectI... \n", - "\n", - " challengesinprogress character timezone ... \\\n", - "5 [] male -8.0 ... \n", - "8 [ObjectId('62c226d09efefadfd10e20bb')] male -5.0 ... \n", - "16 [] male -5.0 ... \n", - "22 [] male -5.0 ... \n", - "23 [] male -4.0 ... \n", - "\n", - " mindfulnessC routinesC DietC hydrationC attractionC screentimeC \\\n", - "5 0 0 0 0 0 0 \n", - "8 0 0 0 0 0 0 \n", - "16 0 0 0 0 0 0 \n", - "22 0 0 0 0 0 0 \n", - "23 0 0 0 0 0 0 \n", - "\n", - " relationshipsC disciplineC masculinityC skillsC \n", - "5 0 0 0 0 \n", - "8 0 0 0 0 \n", - "16 0 0 0 0 \n", - "22 0 0 0 0 \n", - "23 0 0 0 0 \n", - "\n", - "[5 rows x 27 columns]" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "users.head()\n", - "#users[[category + \"C\" for category in skills[\"category\"].unique()]].corr()" + "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*." ] }, { @@ -450,7 +106,86 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [] + "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": { diff --git a/src/utilities/users.py b/src/utilities/users.py index 4633da7..b045aab 100644 --- a/src/utilities/users.py +++ b/src/utilities/users.py @@ -53,12 +53,12 @@ def add_completions_per_category(users: pd.DataFrame, skills: pd.DataFrame) -> p import re users_extended = users for category in skills["category"].unique(): - users_extended[category+"C"]=0 + 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"] - user[category+"C"] = user[category+"C"] + 1 + users_extended.at[i, category+"C"] = users_extended.loc[i, category+"C"] + 1 return users_extended \ No newline at end of file From 5ff0aef04a31cda4516a65e7f39e1a0415128f41 Mon Sep 17 00:00:00 2001 From: Alex Scofield Date: Tue, 20 Jun 2023 17:26:08 +0200 Subject: [PATCH 6/6] Distances between users implemented. --- src/user_clustering.ipynb | 101 ++++++++++++++++++++++++++++++++++++++ src/utilities/skills.py | 8 ++- src/utilities/users.py | 47 +++++++++++++++++- 3 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 src/user_clustering.ipynb 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/skills.py b/src/utilities/skills.py index a3ee751..863a138 100644 --- a/src/utilities/skills.py +++ b/src/utilities/skills.py @@ -34,4 +34,10 @@ def add_in_progress(skills, users): count += 1 skills["completed"][i] = count - return skills \ No newline at end of file + 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 index b045aab..78a3eda 100644 --- a/src/utilities/users.py +++ b/src/utilities/users.py @@ -3,6 +3,7 @@ ''' import pandas as pd +import numpy as np def process(users: pd.DataFrame) -> pd.DataFrame: @@ -61,4 +62,48 @@ def add_completions_per_category(users: pd.DataFrame, skills: pd.DataFrame) -> p skill = skills[skills["_id"] == completed] category = skill["category"] users_extended.at[i, category+"C"] = users_extended.loc[i, category+"C"] + 1 - return users_extended \ No newline at end of file + 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) +