In [1]:
# Data manipulation and analysis
import pandas as pd           
import numpy as np
In [2]:
# Visualization libraries
import matplotlib.pyplot as plt   
import seaborn as sns             
In [3]:
# Splitting data & hyperparameter tuning
from sklearn.model_selection import (
    train_test_split,   
    GridSearchCV,       
    RandomizedSearchCV, 
    cross_val_score     
)
In [4]:
# Data preprocessing
from sklearn.preprocessing import StandardScaler, OneHotEncoder  
from sklearn.compose import ColumnTransformer                    
from sklearn.pipeline import Pipeline                            
from sklearn.impute import SimpleImputer                         
In [5]:
# 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                  
)
In [6]:
# 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                        
In [7]:
import joblib
import time
In [8]:
# To ignore warnings
import warnings
warnings.filterwarnings('ignore')
In [9]:
# Set Seaborn plot style
sns.set(style='whitegrid')
In [13]:
# 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  
Out[13]:
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
In [81]:
# 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.

In [14]:
# checking unique values 
df.nunique()
Out[14]:
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.

In [15]:
df = df.drop(['PlayerID'], axis=1)
In [16]:
# creating numerical columns
num_cols = ['Age', 'PlayTimeHours', 'SessionsPerWeek', 'AvgSessionDurationMinutes', 
            'PlayerLevel', 'AchievementsUnlocked']
In [17]:
# creating categorical columns
cat_cols = ['Gender', 'Location', 'GameGenre', 'InGamePurchases', 'GameDifficulty']
In [18]:
#  Checking summary statistics
df[num_cols].describe().T
Out[18]:
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

In [19]:
# Creating histograms 
df [num_cols].hist(figsize=(14, 14))
plt.show()
No description has been provided for this image

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

In [20]:
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

In [21]:
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()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
In [22]:
# Checking how numeric behaviour differs between players who purchase in game items and those who don’t
df.groupby('InGamePurchases')[num_cols].mean()
Out[22]:
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.

In [23]:
plt.figure(figsize=(15,8))
sns.heatmap(df[num_cols].corr(), annot=True, fmt='.2f', cmap='YlOrRd')
plt.show()
No description has been provided for this image

Model Building Approach: • Data Preparation • Train Test Split • Build Baseline Models • Hyperparameter Tuning • Evaluate on Test Data • Select the Best Model Data Preparation

In [24]:
# 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
In [25]:
# 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
)
In [26]:
# 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

In [27]:
# Scaling the data using StandardScaler for SVM
sc = StandardScaler()
In [28]:
# Fit on training data only
X_train_scaled = sc.fit_transform(X_train)
X_train_scaled = pd.DataFrame(X_train_scaled, columns=X.columns)
In [29]:
# Transform test data
X_test_scaled = sc.transform(X_test)
X_test_scaled = pd.DataFrame(X_test_scaled, columns=X.columns)
In [30]:
# 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

In [31]:
# Creating the linear SVM model
svm_linear = SVC(kernel='linear', probability=True, class_weight='balanced', random_state=42)
In [32]:
# Fitting on scaled training data (X_train_scaled, y_train must exist)
svm_linear.fit(X_train_scaled, y_train)
Out[32]:
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)
In [33]:
# Predict on training set 
y_pred_train_svm = svm_linear.predict(X_train_scaled)
In [ ]:
 
In [34]:
# 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

No description has been provided for this image
In [ ]:
 
In [35]:
# Predicting on the test set using the trained SVM model
y_pred_test_svm = svm_linear.predict(X_test_scaled)
In [36]:
# 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

No description has been provided for this image

RBF Kernel

In [37]:
from sklearn.preprocessing import label_binarize
from sklearn.metrics import precision_recall_curve
import matplotlib.pyplot as plt
In [38]:
# Binarize labels for all three classes
y_train_bin = label_binarize(y_train, classes=['High', 'Medium', 'Low'])
In [43]:
# Train the RBF SVM
svm_rbf = SVC(kernel='rbf', probability=True, class_weight='balanced', random_state=42)
svm_rbf.fit(X_train, y_train)
Out[43]:
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)
In [44]:
# Get probability scores (needed for threshold curves)
y_scores_rbf = svm_rbf.predict_proba(X_train)
In [45]:
# Class names
class_names = ['High', 'Medium', 'Low']
In [50]:
# 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()
No description has been provided for this image

Hyperparameter tuning for RBF SVM (GridSearchCV)

In [51]:
# Hyperparameter grid
param_grid = {
    'C': [0.1, 1, 10],        
    'gamma': ['scale', 0.01, 0.1, 1],  
    'kernel': ['rbf'],
    'class_weight': [None, 'balanced']
}
In [52]:
# base estimator
svm_rbf = SVC(probability=True, random_state=42)
In [58]:
# GridSearchCV setup
gs = GridSearchCV(
    estimator=svm_rbf,
    param_grid=param_grid,
    scoring='f1_weighted',   
    cv=3,                    
    n_jobs=1,
    verbose=2
)
In [59]:
# 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
Out[59]:
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)
In [60]:
import os
os.makedirs('models', exist_ok=True)
In [61]:
# Save best model
joblib.dump(gs.best_estimator_, 'models/svm_rbf_grid_best.joblib')
Out[61]:
['models/svm_rbf_grid_best.joblib']
In [62]:
# 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
In [63]:
# Evaluate on test data
best_svm = gs.best_estimator_
y_test_pred = best_svm.predict(X_test_scaled)
In [64]:
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

No description has been provided for this image

Decision Tree

In [65]:
# Create baseline Decision Tree
dt = DecisionTreeClassifier(random_state=42)
In [66]:
# Fit on training data
dt.fit(X_train, y_train)
Out[66]:
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)
In [67]:
print("Baseline Decision Tree trained.")
Baseline Decision Tree trained.

Evaluating baseline Decision Tree on train and test

In [68]:
# 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

No description has been provided for this image
In [69]:
# 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

In [70]:
# 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

No description has been provided for this image
In [71]:
# 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

In [72]:
param_grid_small = {
    'criterion': ['gini', 'entropy'],
    'max_depth': [5, 10, None],
    'min_samples_split': [2, 5]
}
In [73]:
grid_dt_small = GridSearchCV(
    DecisionTreeClassifier(random_state=42),
    param_grid_small,
    scoring='f1_weighted',
    cv=3,
    n_jobs=1,
    verbose=2
)
In [74]:
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
In [75]:
# 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

No description has been provided for this image
Test accuracy (tuned DT): 0.8954291899092498

Comparing Decision Tree vs SVM

In [76]:
# Predictions
y_pred_svm = best_svm.predict(X_test_scaled)   
y_pred_dt = best_dt.predict(X_test)            
In [77]:
# 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']
    }
In [78]:
summary = pd.DataFrame([
    summary_metrics(y_test, y_pred_dt),
    summary_metrics(y_test, y_pred_svm)
], index=['Decision Tree (tuned)', 'SVM (tuned)'])
In [79]:
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

In [80]:
# 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()
No description has been provided for this image
In [ ]: