# Data manipulation and analysis
import pandas as pd
import numpy as np
# Visualization libraries
import matplotlib.pyplot as plt
import seaborn as sns
# Splitting data & hyperparameter tuning
from sklearn.model_selection import (
train_test_split,
GridSearchCV,
RandomizedSearchCV,
cross_val_score
)
# Data preprocessing
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
# Model evaluation metrics
from sklearn.metrics import (
accuracy_score,
precision_score,
recall_score,
f1_score,
confusion_matrix,
classification_report,
roc_auc_score,
roc_curve,
auc
)
# ALgorithms to use
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.svm import SVC
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from sklearn.decomposition import PCA
import joblib
import time
# To ignore warnings
import warnings
warnings.filterwarnings('ignore')
# Set Seaborn plot style
sns.set(style='whitegrid')
# LOADING the data:
df = pd.read_csv("/Users/matthewhatherly/Downloads/online_gaming_behavior_dataset.csv")
df.head() # load of the first 5 rows of the dataset
| PlayerID | Age | Gender | Location | GameGenre | PlayTimeHours | InGamePurchases | GameDifficulty | SessionsPerWeek | AvgSessionDurationMinutes | PlayerLevel | AchievementsUnlocked | EngagementLevel | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 9000 | 43 | Male | Other | Strategy | 16.271119 | 0 | Medium | 6 | 108 | 79 | 25 | Medium |
| 1 | 9001 | 29 | Female | USA | Strategy | 5.525961 | 0 | Medium | 5 | 144 | 11 | 10 | Medium |
| 2 | 9002 | 22 | Female | USA | Sports | 8.223755 | 0 | Easy | 16 | 142 | 35 | 41 | High |
| 3 | 9003 | 35 | Male | USA | Action | 5.265351 | 1 | Easy | 9 | 85 | 57 | 47 | Medium |
| 4 | 9004 | 33 | Male | Europe | Action | 15.531945 | 0 | Medium | 2 | 131 | 95 | 37 | Medium |
# Let us see the info of the data
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 40034 entries, 0 to 40033 Data columns (total 18 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Age 40034 non-null int64 1 PlayTimeHours 40034 non-null float64 2 SessionsPerWeek 40034 non-null int64 3 AvgSessionDurationMinutes 40034 non-null int64 4 PlayerLevel 40034 non-null int64 5 AchievementsUnlocked 40034 non-null int64 6 EngagementLevel 40034 non-null object 7 Gender_Male 40034 non-null bool 8 Location_Europe 40034 non-null bool 9 Location_Other 40034 non-null bool 10 Location_USA 40034 non-null bool 11 GameGenre_RPG 40034 non-null bool 12 GameGenre_Simulation 40034 non-null bool 13 GameGenre_Sports 40034 non-null bool 14 GameGenre_Strategy 40034 non-null bool 15 InGamePurchases_1 40034 non-null bool 16 GameDifficulty_Hard 40034 non-null bool 17 GameDifficulty_Medium 40034 non-null bool dtypes: bool(11), float64(1), int64(5), object(1) memory usage: 2.6+ MB
Observations:
There are 40034 observations and 18 columns in the dataset. All the columns have 40034 non-null values, i.e., there are no missing values in the data.
# checking unique values
df.nunique()
PlayerID 40034 Age 35 Gender 2 Location 4 GameGenre 5 PlayTimeHours 40034 InGamePurchases 2 GameDifficulty 3 SessionsPerWeek 20 AvgSessionDurationMinutes 170 PlayerLevel 99 AchievementsUnlocked 50 EngagementLevel 3 dtype: int64
Observations:
PlayerID is an identifier which is unique for each player, and we can drop this column as it would not add any value to our analysis.
df = df.drop(['PlayerID'], axis=1)
# creating numerical columns
num_cols = ['Age', 'PlayTimeHours', 'SessionsPerWeek', 'AvgSessionDurationMinutes',
'PlayerLevel', 'AchievementsUnlocked']
# creating categorical columns
cat_cols = ['Gender', 'Location', 'GameGenre', 'InGamePurchases', 'GameDifficulty']
# Checking summary statistics
df[num_cols].describe().T
| count | mean | std | min | 25% | 50% | 75% | max | |
|---|---|---|---|---|---|---|---|---|
| Age | 40034.0 | 31.992531 | 10.043227 | 15.000000 | 23.000000 | 32.000000 | 41.000000 | 49.000000 |
| PlayTimeHours | 40034.0 | 12.024365 | 6.914638 | 0.000115 | 6.067501 | 12.008002 | 17.963831 | 23.999592 |
| SessionsPerWeek | 40034.0 | 9.471774 | 5.763667 | 0.000000 | 4.000000 | 9.000000 | 14.000000 | 19.000000 |
| AvgSessionDurationMinutes | 40034.0 | 94.792252 | 49.011375 | 10.000000 | 52.000000 | 95.000000 | 137.000000 | 179.000000 |
| PlayerLevel | 40034.0 | 49.655568 | 28.588379 | 1.000000 | 25.000000 | 49.000000 | 74.000000 | 99.000000 |
| AchievementsUnlocked | 40034.0 | 24.526477 | 14.430726 | 0.000000 | 12.000000 | 25.000000 | 37.000000 | 49.000000 |
Observations:
The average player age is around 32 years. The average play time in hours is 6.9 hours The average sessions per week is 9.5 The average session time in minutes is 95 minutes The average player level is 49.7 The average amount of achievements unlocked is 24.5
# Creating histograms
df [num_cols].hist(figsize=(14, 14))
plt.show()
Average age ranges extremely between 15 and 50 Play time in hours is fairly balanced, but tops out at 5 hours Sessions per week is also balanced, but peaks between 5 and 7.5 Average sessions per week is extremely balanced Player levels differ with it peaking at around 20 Achievements unlocked are extremely balanced
Checking each categorical variable separately
for col in cat_cols:
print(df[col].value_counts(normalize=True))
print('*' * 40)
Gender Male 0.598466 Female 0.401534 Name: proportion, dtype: float64 **************************************** Location USA 0.399660 Europe 0.299845 Asia 0.202203 Other 0.098291 Name: proportion, dtype: float64 **************************************** GameGenre Sports 0.201029 Action 0.200804 Strategy 0.200130 Simulation 0.199406 RPG 0.198631 Name: proportion, dtype: float64 **************************************** InGamePurchases 0 0.799146 1 0.200854 Name: proportion, dtype: float64 **************************************** GameDifficulty Easy 0.49995 Medium 0.30002 Hard 0.20003 Name: proportion, dtype: float64 ****************************************
More male players than female Most common location is USA Most common game genre is sports, with least common being RPG Most players dont participate in in-game purchases Most players select an easy difficulty, with fewest selecting hard difficulty
Bivariate analysis for categorical variables
for col in cat_cols:
if col != 'InGamePurchases':
(pd.crosstab(df[col], df['InGamePurchases'], normalize='index') * 100).plot(
kind='bar', figsize=(8,4), stacked=True
)
plt.ylabel('Percentage of Players')
plt.title(f'{col} vs In-Game Purchases')
plt.show()
# Checking how numeric behaviour differs between players who purchase in game items and those who don’t
df.groupby('InGamePurchases')[num_cols].mean()
| Age | PlayTimeHours | SessionsPerWeek | AvgSessionDurationMinutes | PlayerLevel | AchievementsUnlocked | |
|---|---|---|---|---|---|---|
| InGamePurchases | ||||||
| 0 | 31.993467 | 12.045395 | 9.456944 | 94.867408 | 49.562060 | 24.525771 |
| 1 | 31.988807 | 11.940694 | 9.530780 | 94.493222 | 50.027609 | 24.529287 |
Checking correlations between numerical features to understand their relationships and spot any strong positive or negative patterns before modeling. A correlation matrix and heatmap will be created to visualise these patterns.
plt.figure(figsize=(15,8))
sns.heatmap(df[num_cols].corr(), annot=True, fmt='.2f', cmap='YlOrRd')
plt.show()
Model Building Approach: • Data Preparation • Train Test Split • Build Baseline Models • Hyperparameter Tuning • Evaluate on Test Data • Select the Best Model Data Preparation
# Creating dummy variables for categorical columns
df = pd.get_dummies(df, columns=cat_cols, drop_first=True)
# Separating independent (X) and dependent (y) variables
X = df.drop('EngagementLevel', axis=1)
y = df['EngagementLevel']
# Quick check
print(X.shape, y.shape)
print(y.value_counts())
(40034, 17) (40034,) EngagementLevel Medium 19374 High 10336 Low 10324 Name: count, dtype: int64
# Train/test split
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.3, # 30% test, 70% train
random_state=1, # ensures reproducible results
stratify=y # keeps class distribution same in train/test
)
# Quick check
print(X_train.shape, X_test.shape)
print(y_train.value_counts(normalize=True))
print(y_test.value_counts(normalize=True))
(28023, 17) (12011, 17) EngagementLevel Medium 0.483924 High 0.258181 Low 0.257895 Name: proportion, dtype: float64 EngagementLevel Medium 0.483973 High 0.258180 Low 0.257847 Name: proportion, dtype: float64
Scaling
# Scaling the data using StandardScaler for SVM
sc = StandardScaler()
# Fit on training data only
X_train_scaled = sc.fit_transform(X_train)
X_train_scaled = pd.DataFrame(X_train_scaled, columns=X.columns)
# Transform test data
X_test_scaled = sc.transform(X_test)
X_test_scaled = pd.DataFrame(X_test_scaled, columns=X.columns)
# Function to print evaluation metrics and confusion matrix
# This will help evaluate how well the model predicts in-game purchases
def metrics_score(actual, predicted):
print(classification_report(actual, predicted))
cm = confusion_matrix(actual, predicted)
plt.figure(figsize=(8,5))
sns.heatmap(cm, annot=True, fmt='.2f',
xticklabels=['No Purchase', 'Purchase'],
yticklabels=['No Purchase', 'Purchase'])
plt.ylabel('Actual')
plt.xlabel('Predicted')
plt.show()
Support Vector Machine
# Creating the linear SVM model
svm_linear = SVC(kernel='linear', probability=True, class_weight='balanced', random_state=42)
# Fitting on scaled training data (X_train_scaled, y_train must exist)
svm_linear.fit(X_train_scaled, y_train)
SVC(class_weight='balanced', kernel='linear', probability=True, random_state=42)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
SVC(class_weight='balanced', kernel='linear', probability=True, random_state=42)
# Predict on training set
y_pred_train_svm = svm_linear.predict(X_train_scaled)
# Evaluate on training set
metrics_score(y_train, y_pred_train_svm)
precision recall f1-score support
High 0.81 0.91 0.86 7235
Low 0.69 0.80 0.74 7227
Medium 0.86 0.73 0.79 13561
accuracy 0.80 28023
macro avg 0.79 0.81 0.80 28023
weighted avg 0.80 0.80 0.80 28023
# Predicting on the test set using the trained SVM model
y_pred_test_svm = svm_linear.predict(X_test_scaled)
# Evaluating performance on the test set
metrics_score(y_test, y_pred_test_svm)
precision recall f1-score support
High 0.81 0.90 0.85 3101
Low 0.70 0.80 0.75 3097
Medium 0.86 0.74 0.80 5813
accuracy 0.80 12011
macro avg 0.79 0.81 0.80 12011
weighted avg 0.81 0.80 0.80 12011
RBF Kernel
from sklearn.preprocessing import label_binarize
from sklearn.metrics import precision_recall_curve
import matplotlib.pyplot as plt
# Binarize labels for all three classes
y_train_bin = label_binarize(y_train, classes=['High', 'Medium', 'Low'])
# Train the RBF SVM
svm_rbf = SVC(kernel='rbf', probability=True, class_weight='balanced', random_state=42)
svm_rbf.fit(X_train, y_train)
SVC(class_weight='balanced', probability=True, random_state=42)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
SVC(class_weight='balanced', probability=True, random_state=42)
# Get probability scores (needed for threshold curves)
y_scores_rbf = svm_rbf.predict_proba(X_train)
# Class names
class_names = ['High', 'Medium', 'Low']
# Create subplots: 1 row, 3 columns
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
for i, class_name in enumerate(class_names):
precisions, recalls, thresholds = precision_recall_curve(
y_train_bin[:, i],
y_scores_rbf[:, i]
)
ax = axes[i]
ax.plot(thresholds, precisions[:-1], 'b--', label='Precision')
ax.plot(thresholds, recalls[:-1], 'g-', label='Recall')
ax.set_xlabel('Threshold')
ax.set_ylabel('Score')
ax.set_title(f'Precision & Recall vs Threshold - {class_name}')
ax.set_ylim([0, 1])
ax.legend(loc='upper left')
plt.tight_layout()
plt.show()
Hyperparameter tuning for RBF SVM (GridSearchCV)
# Hyperparameter grid
param_grid = {
'C': [0.1, 1, 10],
'gamma': ['scale', 0.01, 0.1, 1],
'kernel': ['rbf'],
'class_weight': [None, 'balanced']
}
# base estimator
svm_rbf = SVC(probability=True, random_state=42)
# GridSearchCV setup
gs = GridSearchCV(
estimator=svm_rbf,
param_grid=param_grid,
scoring='f1_weighted',
cv=3,
n_jobs=1,
verbose=2
)
# Fit grid on scaled training data
gs.fit(X_train_scaled, y_train)
Fitting 3 folds for each of 24 candidates, totalling 72 fits [CV] END ..C=0.1, class_weight=None, gamma=scale, kernel=rbf; total time= 47.7s [CV] END ..C=0.1, class_weight=None, gamma=scale, kernel=rbf; total time= 48.1s [CV] END ..C=0.1, class_weight=None, gamma=scale, kernel=rbf; total time= 48.3s [CV] END ...C=0.1, class_weight=None, gamma=0.01, kernel=rbf; total time= 50.4s [CV] END ...C=0.1, class_weight=None, gamma=0.01, kernel=rbf; total time= 50.1s [CV] END ...C=0.1, class_weight=None, gamma=0.01, kernel=rbf; total time= 50.4s [CV] END ....C=0.1, class_weight=None, gamma=0.1, kernel=rbf; total time= 52.7s [CV] END ....C=0.1, class_weight=None, gamma=0.1, kernel=rbf; total time= 52.6s [CV] END ....C=0.1, class_weight=None, gamma=0.1, kernel=rbf; total time= 54.0s [CV] END ......C=0.1, class_weight=None, gamma=1, kernel=rbf; total time= 2.3min [CV] END ......C=0.1, class_weight=None, gamma=1, kernel=rbf; total time= 2.3min [CV] END ......C=0.1, class_weight=None, gamma=1, kernel=rbf; total time= 2.3min [CV] END C=0.1, class_weight=balanced, gamma=scale, kernel=rbf; total time= 58.0s [CV] END C=0.1, class_weight=balanced, gamma=scale, kernel=rbf; total time= 57.1s [CV] END C=0.1, class_weight=balanced, gamma=scale, kernel=rbf; total time= 57.3s [CV] END C=0.1, class_weight=balanced, gamma=0.01, kernel=rbf; total time= 1.0min [CV] END C=0.1, class_weight=balanced, gamma=0.01, kernel=rbf; total time= 1.0min [CV] END C=0.1, class_weight=balanced, gamma=0.01, kernel=rbf; total time= 1.0min [CV] END C=0.1, class_weight=balanced, gamma=0.1, kernel=rbf; total time= 1.1min [CV] END C=0.1, class_weight=balanced, gamma=0.1, kernel=rbf; total time= 1.1min [CV] END C=0.1, class_weight=balanced, gamma=0.1, kernel=rbf; total time= 1.1min [CV] END ..C=0.1, class_weight=balanced, gamma=1, kernel=rbf; total time= 2.0min [CV] END ..C=0.1, class_weight=balanced, gamma=1, kernel=rbf; total time= 2.0min [CV] END ..C=0.1, class_weight=balanced, gamma=1, kernel=rbf; total time= 2.0min [CV] END ....C=1, class_weight=None, gamma=scale, kernel=rbf; total time= 36.6s [CV] END ....C=1, class_weight=None, gamma=scale, kernel=rbf; total time= 36.4s [CV] END ....C=1, class_weight=None, gamma=scale, kernel=rbf; total time= 37.4s [CV] END .....C=1, class_weight=None, gamma=0.01, kernel=rbf; total time= 38.5s [CV] END .....C=1, class_weight=None, gamma=0.01, kernel=rbf; total time= 37.5s [CV] END .....C=1, class_weight=None, gamma=0.01, kernel=rbf; total time= 37.5s [CV] END ......C=1, class_weight=None, gamma=0.1, kernel=rbf; total time= 40.2s [CV] END ......C=1, class_weight=None, gamma=0.1, kernel=rbf; total time= 40.0s [CV] END ......C=1, class_weight=None, gamma=0.1, kernel=rbf; total time= 40.7s [CV] END ........C=1, class_weight=None, gamma=1, kernel=rbf; total time= 3.0min [CV] END ........C=1, class_weight=None, gamma=1, kernel=rbf; total time= 3.0min [CV] END ........C=1, class_weight=None, gamma=1, kernel=rbf; total time= 3.0min [CV] END C=1, class_weight=balanced, gamma=scale, kernel=rbf; total time= 42.8s [CV] END C=1, class_weight=balanced, gamma=scale, kernel=rbf; total time= 43.2s [CV] END C=1, class_weight=balanced, gamma=scale, kernel=rbf; total time= 43.4s [CV] END .C=1, class_weight=balanced, gamma=0.01, kernel=rbf; total time= 47.0s [CV] END .C=1, class_weight=balanced, gamma=0.01, kernel=rbf; total time= 47.0s [CV] END .C=1, class_weight=balanced, gamma=0.01, kernel=rbf; total time= 47.0s [CV] END ..C=1, class_weight=balanced, gamma=0.1, kernel=rbf; total time= 47.2s [CV] END ..C=1, class_weight=balanced, gamma=0.1, kernel=rbf; total time= 47.5s [CV] END ..C=1, class_weight=balanced, gamma=0.1, kernel=rbf; total time= 47.0s [CV] END ....C=1, class_weight=balanced, gamma=1, kernel=rbf; total time= 3.1min [CV] END ....C=1, class_weight=balanced, gamma=1, kernel=rbf; total time= 3.1min [CV] END ....C=1, class_weight=balanced, gamma=1, kernel=rbf; total time= 3.1min [CV] END ...C=10, class_weight=None, gamma=scale, kernel=rbf; total time= 42.0s [CV] END ...C=10, class_weight=None, gamma=scale, kernel=rbf; total time= 42.1s [CV] END ...C=10, class_weight=None, gamma=scale, kernel=rbf; total time= 41.0s [CV] END ....C=10, class_weight=None, gamma=0.01, kernel=rbf; total time= 36.9s [CV] END ....C=10, class_weight=None, gamma=0.01, kernel=rbf; total time= 36.5s [CV] END ....C=10, class_weight=None, gamma=0.01, kernel=rbf; total time= 36.2s [CV] END .....C=10, class_weight=None, gamma=0.1, kernel=rbf; total time= 47.8s [CV] END .....C=10, class_weight=None, gamma=0.1, kernel=rbf; total time= 47.6s [CV] END .....C=10, class_weight=None, gamma=0.1, kernel=rbf; total time= 46.6s [CV] END .......C=10, class_weight=None, gamma=1, kernel=rbf; total time= 4.1min [CV] END .......C=10, class_weight=None, gamma=1, kernel=rbf; total time= 4.1min [CV] END .......C=10, class_weight=None, gamma=1, kernel=rbf; total time= 4.1min [CV] END C=10, class_weight=balanced, gamma=scale, kernel=rbf; total time= 44.5s [CV] END C=10, class_weight=balanced, gamma=scale, kernel=rbf; total time= 45.1s [CV] END C=10, class_weight=balanced, gamma=scale, kernel=rbf; total time= 43.7s [CV] END C=10, class_weight=balanced, gamma=0.01, kernel=rbf; total time= 42.5s [CV] END C=10, class_weight=balanced, gamma=0.01, kernel=rbf; total time= 43.5s [CV] END C=10, class_weight=balanced, gamma=0.01, kernel=rbf; total time= 42.9s [CV] END .C=10, class_weight=balanced, gamma=0.1, kernel=rbf; total time= 48.5s [CV] END .C=10, class_weight=balanced, gamma=0.1, kernel=rbf; total time= 49.7s [CV] END .C=10, class_weight=balanced, gamma=0.1, kernel=rbf; total time= 48.1s [CV] END ...C=10, class_weight=balanced, gamma=1, kernel=rbf; total time= 4.1min [CV] END ...C=10, class_weight=balanced, gamma=1, kernel=rbf; total time= 4.1min [CV] END ...C=10, class_weight=balanced, gamma=1, kernel=rbf; total time= 4.1min
GridSearchCV(cv=3, estimator=SVC(probability=True, random_state=42), n_jobs=1,
param_grid={'C': [0.1, 1, 10], 'class_weight': [None, 'balanced'],
'gamma': ['scale', 0.01, 0.1, 1], 'kernel': ['rbf']},
scoring='f1_weighted', verbose=2)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
GridSearchCV(cv=3, estimator=SVC(probability=True, random_state=42), n_jobs=1,
param_grid={'C': [0.1, 1, 10], 'class_weight': [None, 'balanced'],
'gamma': ['scale', 0.01, 0.1, 1], 'kernel': ['rbf']},
scoring='f1_weighted', verbose=2)SVC(C=10, gamma=0.01, probability=True, random_state=42)
SVC(C=10, gamma=0.01, probability=True, random_state=42)
import os
os.makedirs('models', exist_ok=True)
# Save best model
joblib.dump(gs.best_estimator_, 'models/svm_rbf_grid_best.joblib')
['models/svm_rbf_grid_best.joblib']
# Show best params and score
print("Best params:", gs.best_params_)
print("Best CV f1_weighted:", gs.best_score_)
Best params: {'C': 10, 'class_weight': None, 'gamma': 0.01, 'kernel': 'rbf'}
Best CV f1_weighted: 0.8881265940308049
# Evaluate on test data
best_svm = gs.best_estimator_
y_test_pred = best_svm.predict(X_test_scaled)
metrics_score(y_test, y_test_pred)
precision recall f1-score support
High 0.93 0.85 0.88 3101
Low 0.89 0.83 0.86 3097
Medium 0.87 0.94 0.90 5813
accuracy 0.89 12011
macro avg 0.90 0.87 0.88 12011
weighted avg 0.89 0.89 0.89 12011
Decision Tree
# Create baseline Decision Tree
dt = DecisionTreeClassifier(random_state=42)
# Fit on training data
dt.fit(X_train, y_train)
DecisionTreeClassifier(random_state=42)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
DecisionTreeClassifier(random_state=42)
print("Baseline Decision Tree trained.")
Baseline Decision Tree trained.
Evaluating baseline Decision Tree on train and test
# Predict on train
y_pred_train_dt = dt.predict(X_train)
print("=== TRAIN SET PERFORMANCE ===")
metrics_score(y_train, y_pred_train_dt)
=== TRAIN SET PERFORMANCE ===
precision recall f1-score support
High 1.00 1.00 1.00 7235
Low 1.00 1.00 1.00 7227
Medium 1.00 1.00 1.00 13561
accuracy 1.00 28023
macro avg 1.00 1.00 1.00 28023
weighted avg 1.00 1.00 1.00 28023
# Quick train accuracy
train_acc_dt = (y_pred_train_dt == y_train).mean()
print(f"Train accuracy (Decision Tree baseline): {train_acc_dt:.4f}\n")
Train accuracy (Decision Tree baseline): 1.0000
# Predict on test
y_pred_test_dt = dt.predict(X_test)
print("=== TEST SET PERFORMANCE ===")
metrics_score(y_test, y_pred_test_dt)
=== TEST SET PERFORMANCE ===
precision recall f1-score support
High 0.81 0.81 0.81 3101
Low 0.79 0.80 0.80 3097
Medium 0.87 0.86 0.86 5813
accuracy 0.83 12011
macro avg 0.82 0.82 0.82 12011
weighted avg 0.83 0.83 0.83 12011
# Quick test accuracy
test_acc_dt = (y_pred_test_dt == y_test).mean()
print(f"Test accuracy (Decision Tree baseline): {test_acc_dt:.4f}")
Test accuracy (Decision Tree baseline): 0.8321
Turning Decision Tree
param_grid_small = {
'criterion': ['gini', 'entropy'],
'max_depth': [5, 10, None],
'min_samples_split': [2, 5]
}
grid_dt_small = GridSearchCV(
DecisionTreeClassifier(random_state=42),
param_grid_small,
scoring='f1_weighted',
cv=3,
n_jobs=1,
verbose=2
)
grid_dt_small.fit(X_train, y_train)
best_dt = grid_dt_small.best_estimator_
joblib.dump(best_dt, 'models/decision_tree_grid_best_small.joblib')
print("Best params (small grid):", grid_dt_small.best_params_)
print("Best CV f1_weighted (small grid):", grid_dt_small.best_score_)
Fitting 3 folds for each of 12 candidates, totalling 36 fits
[CV] END ...criterion=gini, max_depth=5, min_samples_split=2; total time= 0.1s
[CV] END ...criterion=gini, max_depth=5, min_samples_split=2; total time= 0.1s
[CV] END ...criterion=gini, max_depth=5, min_samples_split=2; total time= 0.1s
[CV] END ...criterion=gini, max_depth=5, min_samples_split=5; total time= 0.1s
[CV] END ...criterion=gini, max_depth=5, min_samples_split=5; total time= 0.1s
[CV] END ...criterion=gini, max_depth=5, min_samples_split=5; total time= 0.1s
[CV] END ..criterion=gini, max_depth=10, min_samples_split=2; total time= 0.1s
[CV] END ..criterion=gini, max_depth=10, min_samples_split=2; total time= 0.1s
[CV] END ..criterion=gini, max_depth=10, min_samples_split=2; total time= 0.1s
[CV] END ..criterion=gini, max_depth=10, min_samples_split=5; total time= 0.1s
[CV] END ..criterion=gini, max_depth=10, min_samples_split=5; total time= 0.1s
[CV] END ..criterion=gini, max_depth=10, min_samples_split=5; total time= 0.1s
[CV] END criterion=gini, max_depth=None, min_samples_split=2; total time= 0.2s
[CV] END criterion=gini, max_depth=None, min_samples_split=2; total time= 0.1s
[CV] END criterion=gini, max_depth=None, min_samples_split=2; total time= 0.1s
[CV] END criterion=gini, max_depth=None, min_samples_split=5; total time= 0.1s
[CV] END criterion=gini, max_depth=None, min_samples_split=5; total time= 0.1s
[CV] END criterion=gini, max_depth=None, min_samples_split=5; total time= 0.2s
[CV] END criterion=entropy, max_depth=5, min_samples_split=2; total time= 0.1s
[CV] END criterion=entropy, max_depth=5, min_samples_split=2; total time= 0.1s
[CV] END criterion=entropy, max_depth=5, min_samples_split=2; total time= 0.1s
[CV] END criterion=entropy, max_depth=5, min_samples_split=5; total time= 0.1s
[CV] END criterion=entropy, max_depth=5, min_samples_split=5; total time= 0.1s
[CV] END criterion=entropy, max_depth=5, min_samples_split=5; total time= 0.1s
[CV] END criterion=entropy, max_depth=10, min_samples_split=2; total time= 0.1s
[CV] END criterion=entropy, max_depth=10, min_samples_split=2; total time= 0.1s
[CV] END criterion=entropy, max_depth=10, min_samples_split=2; total time= 0.1s
[CV] END criterion=entropy, max_depth=10, min_samples_split=5; total time= 0.1s
[CV] END criterion=entropy, max_depth=10, min_samples_split=5; total time= 0.1s
[CV] END criterion=entropy, max_depth=10, min_samples_split=5; total time= 0.1s
[CV] END criterion=entropy, max_depth=None, min_samples_split=2; total time= 0.1s
[CV] END criterion=entropy, max_depth=None, min_samples_split=2; total time= 0.2s
[CV] END criterion=entropy, max_depth=None, min_samples_split=2; total time= 0.1s
[CV] END criterion=entropy, max_depth=None, min_samples_split=5; total time= 0.1s
[CV] END criterion=entropy, max_depth=None, min_samples_split=5; total time= 0.1s
[CV] END criterion=entropy, max_depth=None, min_samples_split=5; total time= 0.1s
Best params (small grid): {'criterion': 'gini', 'max_depth': 10, 'min_samples_split': 5}
Best CV f1_weighted (small grid): 0.8901997664628478
# Evaluating tuned Decision Tree on test
y_pred_test_best_dt = best_dt.predict(X_test)
print("=== TUNED DECISION TREE: TEST PERFORMANCE ===")
metrics_score(y_test, y_pred_test_best_dt)
print("Test accuracy (tuned DT):", (y_pred_test_best_dt == y_test).mean())
=== TUNED DECISION TREE: TEST PERFORMANCE ===
precision recall f1-score support
High 0.91 0.87 0.89 3101
Low 0.88 0.85 0.87 3097
Medium 0.89 0.93 0.91 5813
accuracy 0.90 12011
macro avg 0.90 0.88 0.89 12011
weighted avg 0.90 0.90 0.90 12011
Test accuracy (tuned DT): 0.8954291899092498
Comparing Decision Tree vs SVM
# Predictions
y_pred_svm = best_svm.predict(X_test_scaled)
y_pred_dt = best_dt.predict(X_test)
# Summary function to extract key metrics per model
def summary_metrics(y_true, y_pred):
cr = classification_report(y_true, y_pred, output_dict=True)
acc = accuracy_score(y_true, y_pred)
return {
'accuracy': acc,
'precision_weighted': cr['weighted avg']['precision'],
'recall_weighted': cr['weighted avg']['recall'],
'f1_weighted': cr['weighted avg']['f1-score']
}
summary = pd.DataFrame([
summary_metrics(y_test, y_pred_dt),
summary_metrics(y_test, y_pred_svm)
], index=['Decision Tree (tuned)', 'SVM (tuned)'])
print(summary.round(3))
accuracy precision_weighted recall_weighted \
Decision Tree (tuned) 0.895 0.895 0.895
SVM (tuned) 0.887 0.889 0.887
f1_weighted
Decision Tree (tuned) 0.895
SVM (tuned) 0.887
K-Means
# Basic inspection of the relationship between Age and PlayTimeHours
plt.figure(figsize=(10,6))
plt.scatter(df["Age"], df["PlayTimeHours"], alpha=0.7)
plt.xlabel("Age")
plt.ylabel("Weekly Play Time (hrs)")
plt.title("Distribution of Player Age vs Weekly Play Time")
plt.show()