1. Import Libraries¶

In [1]:
#  1.1
#  Import All Required Libraries

#  pandas for loading and working with tables of data
import pandas as pd

#  numpy for basic maths and handling arrays
import numpy as np

#  tools to split our data and try out different model settings
from sklearn.model_selection import train_test_split, GridSearchCV

#  machine learning models we need for this assignment
from sklearn.tree import DecisionTreeClassifier    #  decision tree model
from sklearn.svm import SVC                        #  support vector machine model
from sklearn.cluster import KMeans                 #  k-means for clustering

#  metrics to check how well our models are doing
from sklearn.metrics import (
    accuracy_score,
    precision_score,
    recall_score,
    f1_score,
    classification_report,
    confusion_matrix,
    roc_auc_score,
    silhouette_score
)

#  plotting libraries to make graphs and charts
import matplotlib.pyplot as plt
import seaborn as sns

2. Load The Dataset¶

In [2]:
#  2.1
#  Load The Dataset

#  path to where the csv file is saved
data_file_path = "online_gaming_behavior_dataset.csv"

#  load the csv into a dataframe so we can work with it easily
gaming_data = pd.read_csv(data_file_path)

#  show the first few rows just to check the data loaded properly
gaming_data.head()
Out[2]:
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

3. Basic Dataset Exploration¶

In [3]:
#  3.1
#  Check The Shape of The Dataset (Number of Rows and Columns)

#  print the number of rows and columns so we know the dataset size
print("The Number of Rows and Columns", gaming_data.shape)
The Number of Rows and Columns (40034, 13)
In [4]:
#  3.2
#  View Column Data Types and Check for Missing Values

#  show the data types for each column and see if anything is missing
#  this helps us understand what we are working with
gaming_data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 40034 entries, 0 to 40033
Data columns (total 13 columns):
 #   Column                     Non-Null Count  Dtype  
---  ------                     --------------  -----  
 0   PlayerID                   40034 non-null  int64  
 1   Age                        40034 non-null  int64  
 2   Gender                     40034 non-null  object 
 3   Location                   40034 non-null  object 
 4   GameGenre                  40034 non-null  object 
 5   PlayTimeHours              40034 non-null  float64
 6   InGamePurchases            40034 non-null  int64  
 7   GameDifficulty             40034 non-null  object 
 8   SessionsPerWeek            40034 non-null  int64  
 9   AvgSessionDurationMinutes  40034 non-null  int64  
 10  PlayerLevel                40034 non-null  int64  
 11  AchievementsUnlocked       40034 non-null  int64  
 12  EngagementLevel            40034 non-null  object 
dtypes: float64(1), int64(7), object(5)
memory usage: 4.0+ MB
In [5]:
#  3.3
#  Summary Statistics For Numeric Columns

#  show basic statistics for the numeric columns
#  this helps us see the ranges and if anything looks unusual
gaming_data.describe()
Out[5]:
PlayerID Age PlayTimeHours InGamePurchases SessionsPerWeek AvgSessionDurationMinutes PlayerLevel AchievementsUnlocked
count 40034.000000 40034.000000 40034.000000 40034.000000 40034.000000 40034.000000 40034.000000 40034.000000
mean 29016.500000 31.992531 12.024365 0.200854 9.471774 94.792252 49.655568 24.526477
std 11556.964675 10.043227 6.914638 0.400644 5.763667 49.011375 28.588379 14.430726
min 9000.000000 15.000000 0.000115 0.000000 0.000000 10.000000 1.000000 0.000000
25% 19008.250000 23.000000 6.067501 0.000000 4.000000 52.000000 25.000000 12.000000
50% 29016.500000 32.000000 12.008002 0.000000 9.000000 95.000000 49.000000 25.000000
75% 39024.750000 41.000000 17.963831 0.000000 14.000000 137.000000 74.000000 37.000000
max 49033.000000 49.000000 23.999592 1.000000 19.000000 179.000000 99.000000 49.000000
In [6]:
#  3.4
#  Check For Missing Values in Each Column

#  count how many missing values each column has
missing_values = gaming_data.isnull().sum()

#  print the results so we can see if anything needs fixing
print("Missing Values Per Column\n")
print(missing_values)
Missing Values Per Column

PlayerID                     0
Age                          0
Gender                       0
Location                     0
GameGenre                    0
PlayTimeHours                0
InGamePurchases              0
GameDifficulty               0
SessionsPerWeek              0
AvgSessionDurationMinutes    0
PlayerLevel                  0
AchievementsUnlocked         0
EngagementLevel              0
dtype: int64

4. Quick Visual Check¶

In [7]:
#  4.1
#  Plot The Distribution of Player Playtime

#  make a histogram to see how play time is spread across players
plt.figure(figsize=(6, 4))
sns.histplot(gaming_data['PlayTimeHours'], kde=True)
plt.title("Distribution of PlayTimeHours")
plt.xlabel("PlayTimeHours")
plt.ylabel("Count")
plt.show()
No description has been provided for this image
In [8]:
#  4.2
#  Visualise How Many Players Fall Into Each EngagementLevel Category

#  count how many players are in each engagement level
#  this helps us see if the classes are balanced or not
engagement_counts = gaming_data['EngagementLevel'].value_counts()
print(engagement_counts)

#  make a bar chart to show the number of players in each engagement level
plt.figure(figsize=(5, 4))
sns.countplot(x='EngagementLevel', data=gaming_data)
plt.title("Number of Players by Engagement Level")
plt.show()
EngagementLevel
Medium    19374
High      10336
Low       10324
Name: count, dtype: int64
No description has been provided for this image
In [9]:
#  4.3
#  Relationship Between Playtime and Engagement Level

#  make a boxplot to compare play time across the different engagement levels
plt.figure(figsize=(6, 4))
sns.boxplot(data=gaming_data, x='EngagementLevel', y='PlayTimeHours')
plt.title("PlayTimeHours by EngagementLevel")
plt.xlabel("Engagement Level")
plt.ylabel("Play Time Hours")

#  add light grid lines to make it easier to read the values
plt.grid(axis='y', linestyle='--', alpha=0.5)

plt.show()
No description has been provided for this image
In [10]:
#  4.4
#  Correlation Heatmap For Numeric Features

#  list of the numeric columns we want to check for correlations
numeric_cols = [
    'Age',
    'PlayTimeHours',
    'SessionsPerWeek',
    'AvgSessionDurationMinutes',
    'InGamePurchases',
    'PlayerLevel',
    'AchievementsUnlocked'
]

#  make the figure for the heatmap
plt.figure(figsize=(8, 6))

#  calculate the correlation matrix for the numeric columns
corr_matrix = gaming_data[numeric_cols].corr()

#  draw the heatmap so we can see how the numeric features relate to each other
sns.heatmap(corr_matrix, annot=True, fmt=".2f", cmap='coolwarm')
plt.title("Correlation Heatmap of Numeric Features")
plt.show()
No description has been provided for this image

5. Encoding The Target¶

In [11]:
#  5.1
#  Encode The Engagementlevel Column Into Numbers

#  labelencoder turns text labels into numbers so the model can use them
from sklearn.preprocessing import LabelEncoder

#  create the encoder
label_encoder = LabelEncoder()

#  fit the encoder on the engagement level column and make a new numeric column
gaming_data['EngagementEncoded'] = label_encoder.fit_transform(gaming_data['EngagementLevel'])

#  show how the numbers match back to the original labels
label_mapping = dict(zip(label_encoder.transform(label_encoder.classes_), label_encoder.classes_))
print("label mapping (number -> engagement level):", label_mapping)

#  show a few example rows to confirm the new encoded values look correct
gaming_data[['EngagementLevel', 'EngagementEncoded']].head()
label mapping (number -> engagement level): {np.int64(0): 'High', np.int64(1): 'Low', np.int64(2): 'Medium'}
Out[11]:
EngagementLevel EngagementEncoded
0 Medium 2
1 Medium 2
2 High 0
3 Medium 2
4 Medium 2

6. One-Hot Encode Categorical Features¶

In [12]:
#  6.1
#  One-Hot Encode Categorical Features

#  list of the columns that need to be turned into dummy variables
categorical_columns = ['Gender', 'Location', 'GameGenre', 'GameDifficulty']

#  use get_dummies to convert the text categories into 0/1 columns
#  drop_first=true removes one column from each group to avoid duplication
gaming_data_encoded = pd.get_dummies(gaming_data, columns=categorical_columns, drop_first=True)

#  print the shapes so we can see how many new columns were added
print("Original Shape:", gaming_data.shape)
print("Encoded Shape :", gaming_data_encoded.shape)

#  show a few rows so we can check that the new dummy columns look correct
gaming_data_encoded.head()
Original Shape: (40034, 14)
Encoded Shape : (40034, 20)
Out[12]:
PlayerID Age PlayTimeHours InGamePurchases SessionsPerWeek AvgSessionDurationMinutes PlayerLevel AchievementsUnlocked EngagementLevel EngagementEncoded Gender_Male Location_Europe Location_Other Location_USA GameGenre_RPG GameGenre_Simulation GameGenre_Sports GameGenre_Strategy GameDifficulty_Hard GameDifficulty_Medium
0 9000 43 16.271119 0 6 108 79 25 Medium 2 True False True False False False False True False True
1 9001 29 5.525961 0 5 144 11 10 Medium 2 False False False True False False False True False True
2 9002 22 8.223755 0 16 142 35 41 High 0 False False False True False False True False False False
3 9003 35 5.265351 1 9 85 57 47 Medium 2 True False False True False False False False False False
4 9004 33 15.531945 0 2 131 95 37 Medium 2 True True False False False False False False False True

7. Define Features (X) and Target (y)¶

In [13]:
#  7.1
#  Create X and Y

#  y is the column we want the model to predict
y = gaming_data_encoded['EngagementEncoded']

#  x holds all the input features the model uses to make predictions
#  we remove engagementlevel (text), engagementencoded (target), and playerid (just an id)
X = gaming_data_encoded.drop(['EngagementLevel', 'EngagementEncoded', 'PlayerID'], axis=1)

#  print the shapes so we can check that x and y look correct
print("X Shape:", X.shape)
print("Y Shape:", y.shape)
X Shape: (40034, 17)
Y Shape: (40034,)

8. Train/Test Split¶

In [14]:
#  8.1
#  Split The Data

#  split the data into training and test sets
#  stratify=y makes sure the engagement levels stay balanced across both sets
X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2,
    random_state=42,
    stratify=y
)

#  print the sizes so we know how much data went into each set
print("Training Set Size:", X_train.shape[0])
print("Test Set Size    :", X_test.shape[0])
Training Set Size: 32027
Test Set Size    : 8007

9. Scale Features (SVM)¶

In [15]:
#  9.1
#  Scale Features For SVM

#  standardise the features because svm works better when values are on a similar scale
from sklearn.preprocessing import StandardScaler

#  create the scaler and learn the scaling values from the training data
svm_scaler = StandardScaler()
X_train_svm = svm_scaler.fit_transform(X_train)

#  use the same scaling on the test data (important so both sets match)
X_test_svm = svm_scaler.transform(X_test)

#  print the shapes just to confirm everything transformed correctly
print("Scaled Training Features Shape:", X_train_svm.shape)
print("Scaled Test Features Shape    :", X_test_svm.shape)
Scaled Training Features Shape: (32027, 17)
Scaled Test Features Shape    : (8007, 17)

10. Train a Basic Decision Tree Classifier¶

In [16]:
#  10.1
#  Train A Basic Decision Tree Classifier

#  make a basic decision tree model with a fixed random_state for repeatable results
tree_model = DecisionTreeClassifier(random_state=42)

#  train the model using the training data
tree_model.fit(X_train, y_train)

#  make predictions on the test data
y_pred_dt = tree_model.predict(X_test)

#  calculate the evaluation scores for the decision tree
dt_accuracy = accuracy_score(y_test, y_pred_dt)
dt_precision = precision_score(y_test, y_pred_dt, average='weighted', zero_division=0)
dt_recall = recall_score(y_test, y_pred_dt, average='weighted', zero_division=0)
dt_f1 = f1_score(y_test, y_pred_dt, average='weighted', zero_division=0)

#  print out the basic evaluation results
print("Decision Tree Accuracy :", dt_accuracy)
print("Decision Tree Precision:", dt_precision)
print("Decision Tree Recall   :", dt_recall)
print("Decision Tree F1 Score :", dt_f1)

#  show a more detailed breakdown of performance for each class
print("\nClassification Report For Decision Tree:")
print(classification_report(y_test, y_pred_dt, zero_division=0))
Decision Tree Accuracy : 0.838016735356563
Decision Tree Precision: 0.838350815900565
Decision Tree Recall   : 0.838016735356563
Decision Tree F1 Score : 0.8381592851804845

Classification Report For Decision Tree:
              precision    recall  f1-score   support

           0       0.81      0.82      0.81      2067
           1       0.80      0.81      0.80      2065
           2       0.87      0.86      0.87      3875

    accuracy                           0.84      8007
   macro avg       0.83      0.83      0.83      8007
weighted avg       0.84      0.84      0.84      8007

In [17]:
#  10.2
#  Confusion Matrix For Decision Tree

#  work out the confusion matrix comparing true vs predicted labels
dt_cm = confusion_matrix(y_test, y_pred_dt)

#  draw the confusion matrix as a heatmap so it is easier to read
plt.figure(figsize=(5, 4))
sns.heatmap(dt_cm, annot=True, fmt='d', cmap="Blues")
plt.title("Decision Tree Confusion Matrix")
plt.xlabel("Predicted Label")
plt.ylabel("True Label")
plt.show()
No description has been provided for this image

11. Support Vector Machine Classifier¶

In [18]:
#  11.1
#  Train A Basic SVM Classifier

#  create an svm model using the rbf kernel
svm_model = SVC(kernel='rbf', probability=True, random_state=42)

#  train the svm model on the scaled training data
svm_model.fit(X_train_svm, y_train)

#  make predictions on the scaled test data
y_pred_svm = svm_model.predict(X_test_svm)

#  calculate the evaluation scores for the svm model
svm_accuracy = accuracy_score(y_test, y_pred_svm)
svm_precision = precision_score(y_test, y_pred_svm, average='weighted', zero_division=0)
svm_recall = recall_score(y_test, y_pred_svm, average='weighted', zero_division=0)
svm_f1 = f1_score(y_test, y_pred_svm, average='weighted', zero_division=0)

#  print out the main performance results
print("SVM Accuracy :", svm_accuracy)
print("SVM Precision:", svm_precision)
print("SVM Recall   :", svm_recall)
print("SVM F1 Score :", svm_f1)

#  show a detailed report for each engagement level class
print("\nClassification Report For SVM:")
print(classification_report(y_test, y_pred_svm, zero_division=0))
SVM Accuracy : 0.8821031597352317
SVM Precision: 0.8831558418958544
SVM Recall   : 0.8821031597352317
SVM F1 Score : 0.8815568480098688

Classification Report For SVM:
              precision    recall  f1-score   support

           0       0.91      0.84      0.88      2067
           1       0.88      0.83      0.86      2065
           2       0.87      0.93      0.90      3875

    accuracy                           0.88      8007
   macro avg       0.89      0.87      0.88      8007
weighted avg       0.88      0.88      0.88      8007

In [19]:
#  11.2
#  Confusion Matrix For SVM

#  compute the confusion matrix for the svm predictions
svm_cm = confusion_matrix(y_test, y_pred_svm)

#  draw the confusion matrix as a heatmap so we can easily see correct and wrong predictions
plt.figure(figsize=(5, 4))
sns.heatmap(svm_cm, annot=True, fmt='d', cmap="Blues")
plt.title("SVM Confusion Matrix")
plt.xlabel("Predicted Label")
plt.ylabel("True Label")
plt.show()
No description has been provided for this image

12. AUC-ROC (Multi-Class)¶

In [20]:
#  12.1
#  Compute AUC-ROC For Decision Tree And SVM

#  get the predicted probabilities for each class from the decision tree
y_proba_dt = tree_model.predict_proba(X_test)

#  compute the auc-roc score using one-vs-rest for the decision tree
tree_auc = roc_auc_score(y_test, y_proba_dt, multi_class='ovr')
print("Decision Tree AUC-ROC (ovr):", tree_auc)

#  get the predicted probabilities for each class from the svm model
y_proba_svm = svm_model.predict_proba(X_test_svm)

#  compute the auc-roc score using one-vs-rest for the svm model
svm_auc = roc_auc_score(y_test, y_proba_svm, multi_class='ovr')
print("SVM AUC-ROC (ovr)          :", svm_auc)
Decision Tree AUC-ROC (ovr): 0.8727977942405611
SVM AUC-ROC (ovr)          : 0.9393615637692543
In [21]:
#  12.2
#  Summary Table For Baseline Decision Tree And SVM

#  make a table to compare the main scores for both baseline models
baseline_results = pd.DataFrame({
    'Model': ['Decision Tree (baseline)', 'SVM (baseline)'],
    'Accuracy': [dt_accuracy, svm_accuracy],
    'Precision (weighted)': [dt_precision, svm_precision],
    'Recall (weighted)': [dt_recall, svm_recall],
    'F1 (weighted)': [dt_f1, svm_f1],
    'AUC-ROC (ovr)': [tree_auc, svm_auc]
})

#  show the table so we can easily compare the two models
baseline_results
Out[21]:
Model Accuracy Precision (weighted) Recall (weighted) F1 (weighted) AUC-ROC (ovr)
0 Decision Tree (baseline) 0.838017 0.838351 0.838017 0.838159 0.872798
1 SVM (baseline) 0.882103 0.883156 0.882103 0.881557 0.939362

13. Hyperparameter Tuning¶

In [22]:
#  13.1
#  Hyperparameter Tuning For Decision Tree Using GridSearchCV

#  set up the different values we want to try for the decision tree
param_grid_tree = {
    'max_depth': [None, 5, 10, 15],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4],
    'criterion': ['gini', 'entropy']
}

#  make a gridsearch object that will test all the parameter combinations
grid_tree = GridSearchCV(
    estimator=DecisionTreeClassifier(random_state=42),
    param_grid=param_grid_tree,
    cv=3,
    scoring='accuracy',
    n_jobs=1
)

#  fit the gridsearch on the training data so it can find the best settings
grid_tree.fit(X_train, y_train)

#  print the best parameter combination and its accuracy score
print("Best Parameters For Decision Tree:", grid_tree.best_params_)
print("Best Cross-Validation Accuracy For Decision Tree:", grid_tree.best_score_)
Best Parameters For Decision Tree: {'criterion': 'entropy', 'max_depth': 10, 'min_samples_leaf': 4, 'min_samples_split': 10}
Best Cross-Validation Accuracy For Decision Tree: 0.8986481032843335
In [23]:
#  13.2
#  Evaluate Best Decision Tree Model On Test Set

#  get the best decision tree model found by gridsearch
best_tree_model = grid_tree.best_estimator_

#  make predictions on the test set using the tuned model
y_pred_best_dt = best_tree_model.predict(X_test)

#  calculate the evaluation scores for the tuned decision tree
best_dt_accuracy = accuracy_score(y_test, y_pred_best_dt)
best_dt_precision = precision_score(y_test, y_pred_best_dt, average='weighted', zero_division=0)
best_dt_recall = recall_score(y_test, y_pred_best_dt, average='weighted', zero_division=0)
best_dt_f1 = f1_score(y_test, y_pred_best_dt, average='weighted', zero_division=0)

#  print the tuned model results so we can compare them with the baseline model
print("Tuned Decision Tree Accuracy :", best_dt_accuracy)
print("Tuned Decision Tree Precision:", best_dt_precision)
print("Tuned Decision Tree Recall   :", best_dt_recall)
print("Tuned Decision Tree F1 Score :", best_dt_f1)
Tuned Decision Tree Accuracy : 0.9024603471962033
Tuned Decision Tree Precision: 0.9023715514791402
Tuned Decision Tree Recall   : 0.9024603471962033
Tuned Decision Tree F1 Score : 0.9023000083849064
In [24]:
#  13.3
#  Create A Subset Of The Training Data For RBF SVM Tuning

#  tuning the rbf svm can take a long time, so we use a smaller sample if the dataset is big
from sklearn.model_selection import train_test_split

#  set the maximum number of rows we want to use for tuning
max_samples = 1500  #  maximum number of rows to use for tuning

#  if the training data is larger than the limit, take a smaller sample
#  stratify makes sure the engagement level proportions stay the same
if len(y_train) > max_samples:
    X_tune_svm, _, y_tune_svm, _ = train_test_split(
        X_train_svm,
        y_train,
        train_size=max_samples,
        stratify=y_train,
        random_state=42
    )
    print("Using Subset For RBF SVM Tuning:", X_tune_svm.shape[0], "Samples")
else:
    #  if the training set is already small, just use the full scaled training data
    X_tune_svm = X_train_svm
    y_tune_svm = y_train
    print("Using Full Training Data For RBF SVM Tuning:", X_tune_svm.shape[0], "Samples")
Using Subset For RBF SVM Tuning: 1500 Samples
In [25]:
#  13.4
#  Grid Search For RBF SVM

#  set up a small grid of values to try for the rbf svm
#  c controls how strict the model is, and gamma affects how sensitive it is
param_grid_svm = {
    'C': [1, 10],
    'gamma': ['scale', 0.1],
    'kernel': ['rbf'],
}

#  make the grid search object to test different parameter choices
#  using probability=false keeps the training faster
grid_svm = GridSearchCV(
    estimator=SVC(),
    param_grid=param_grid_svm,
    cv=3,
    scoring='accuracy',
    n_jobs=1,
)

print("Starting RBF SVM Grid Search...")

#  run the grid search on the smaller tuning dataset
grid_svm.fit(X_tune_svm, y_tune_svm)

print("RBF SVM Grid Search Completed")

#  print the best combination of parameters and the matching score
print("Best SVM Parameters (RBF):", grid_svm.best_params_)
print("Best CV Accuracy (RBF):", grid_svm.best_score_)
Starting RBF SVM Grid Search...
RBF SVM Grid Search Completed
Best SVM Parameters (RBF): {'C': 1, 'gamma': 'scale', 'kernel': 'rbf'}
Best CV Accuracy (RBF): 0.8033333333333333
In [26]:
#  13.5
#  Train The Final RBF SVM Model Using The Best Parameters

#  get the best parameters that the grid search found
best_params_rbf = grid_svm.best_params_
print("Best Parameters For Final RBF SVM:", best_params_rbf)

#  make a new svm model using the best settings
#  probability=true lets us get probability scores later for auc roc
best_svm_rbf = SVC(
    kernel='rbf',
    C=best_params_rbf['C'],
    gamma=best_params_rbf['gamma'],
    probability=True,
    random_state=42
)

#  train this final model on the full scaled training data
best_svm_rbf.fit(X_train_svm, y_train)

print("Final RBF SVM Model Trained Successfully")
Best Parameters For Final RBF SVM: {'C': 1, 'gamma': 'scale', 'kernel': 'rbf'}
Final RBF SVM Model Trained Successfully
In [27]:
#  13.6
#  Evaluate The Tuned RBF SVM On The Test Set

#  make predictions on the scaled test data using the tuned rbf svm
y_pred_best_rbf = best_svm_rbf.predict(X_test_svm)

#  calculate the main evaluation scores for the tuned svm model
acc_rbf = accuracy_score(y_test, y_pred_best_rbf)
prec_rbf = precision_score(y_test, y_pred_best_rbf, average='weighted')
rec_rbf = recall_score(y_test, y_pred_best_rbf, average='weighted')
f1_rbf = f1_score(y_test, y_pred_best_rbf, average='weighted')

#  print the scores so we can compare them with the baseline svm
print("Tuned RBF SVM - Accuracy :", acc_rbf)
print("Tuned RBF SVM - Precision:", prec_rbf)
print("Tuned RBF SVM - Recall   :", rec_rbf)
print("Tuned RBF SVM - F1-Score :", f1_rbf)

#  show a detailed performance report for each engagement class
print("\nClassification Report (Tuned RBF SVM):\n")
print(classification_report(y_test, y_pred_best_rbf))
Tuned RBF SVM - Accuracy : 0.8821031597352317
Tuned RBF SVM - Precision: 0.8831558418958544
Tuned RBF SVM - Recall   : 0.8821031597352317
Tuned RBF SVM - F1-Score : 0.8815568480098688

Classification Report (Tuned RBF SVM):

              precision    recall  f1-score   support

           0       0.91      0.84      0.88      2067
           1       0.88      0.83      0.86      2065
           2       0.87      0.93      0.90      3875

    accuracy                           0.88      8007
   macro avg       0.89      0.87      0.88      8007
weighted avg       0.88      0.88      0.88      8007

In [28]:
#  13.7
#  Confusion Matrix Heatmap For The Tuned RBF SVM

#  compute the confusion matrix for the tuned rbf svm
#  rows show the real classes and columns show what the model predicted
cm_rbf = confusion_matrix(y_test, y_pred_best_rbf)

#  draw the confusion matrix as a heatmap to make it easy to read
plt.figure(figsize=(5, 4))
sns.heatmap(cm_rbf, annot=True, fmt='d', cmap='Greens')
plt.title("Tuned RBF SVM Confusion Matrix")
plt.xlabel("Predicted Label")
plt.ylabel("True Label")
plt.show()

#  get the predicted probabilities for the tuned decision tree
y_proba_best_dt = best_tree_model.predict_proba(X_test)

#  compute the auc roc score for the tuned decision tree
best_dt_auc = roc_auc_score(y_test, y_proba_best_dt, multi_class='ovr')
print("Tuned Decision Tree AUC-ROC (ovr):", best_dt_auc)

#  get the predicted probabilities for the tuned rbf svm
y_proba_best_rbf = best_svm_rbf.predict_proba(X_test_svm)

#  compute the auc roc score for the tuned rbf svm
best_rbf_auc = roc_auc_score(y_test, y_proba_best_rbf, multi_class='ovr')
print("Tuned RBF SVM AUC-ROC (ovr)     :", best_rbf_auc)
No description has been provided for this image
Tuned Decision Tree AUC-ROC (ovr): 0.93324541061391
Tuned RBF SVM AUC-ROC (ovr)     : 0.9393615637692543
In [29]:
#  13.8
#  Summary Of Baseline vs Tuned Models

#  make a table to compare the scores from all baseline and tuned models
comparison_results = pd.DataFrame({
    'Model': [
        'Decision Tree (baseline)',
        'Decision Tree (tuned)',
        'SVM RBF (baseline)',
        'SVM RBF (tuned)'
    ],
    'Accuracy': [dt_accuracy, best_dt_accuracy, svm_accuracy, acc_rbf],
    'Precision (weighted)': [dt_precision, best_dt_precision, svm_precision, prec_rbf],
    'Recall (weighted)': [dt_recall, best_dt_recall, svm_recall, rec_rbf],
    'F1 (weighted)': [dt_f1, best_dt_f1, svm_f1, f1_rbf],
    'AUC-ROC (ovr)': [tree_auc, best_dt_auc, svm_auc, best_rbf_auc]
})

#  show the comparison table so we can easily see which models performed best
comparison_results
Out[29]:
Model Accuracy Precision (weighted) Recall (weighted) F1 (weighted) AUC-ROC (ovr)
0 Decision Tree (baseline) 0.838017 0.838351 0.838017 0.838159 0.872798
1 Decision Tree (tuned) 0.902460 0.902372 0.902460 0.902300 0.933245
2 SVM RBF (baseline) 0.882103 0.883156 0.882103 0.881557 0.939362
3 SVM RBF (tuned) 0.882103 0.883156 0.882103 0.881557 0.939362

14. K-Means Clustering¶

In [30]:
#  14.1
#  Select Numeric Features For Clustering

#  choose the numeric columns we want to use for clustering
numeric_cols = [
    'Age',
    'PlayTimeHours',
    'SessionsPerWeek',
    'AvgSessionDurationMinutes',
    'PlayerLevel',
    'AchievementsUnlocked'
]

#  make a new dataset with just these numeric features
#  dropna removes any rows with missing values so clustering works properly
X_cluster = gaming_data[numeric_cols].dropna()

#  show the first few rows to confirm it looks correct
X_cluster.head()
Out[30]:
Age PlayTimeHours SessionsPerWeek AvgSessionDurationMinutes PlayerLevel AchievementsUnlocked
0 43 16.271119 6 108 79 25
1 29 5.525961 5 144 11 10
2 22 8.223755 16 142 35 41
3 35 5.265351 9 85 57 47
4 33 15.531945 2 131 95 37
In [31]:
#  14.2
#  Scale Data For Clustering

#  standardise the numeric features so they are all on a similar scale
scaler_cluster = StandardScaler()
X_cluster_scaled = scaler_cluster.fit_transform(X_cluster)

#  print the shape to make sure the scaling worked correctly
print("Cluster Data Shape:", X_cluster_scaled.shape)
Cluster Data Shape: (40034, 6)
In [32]:
#  14.3
#  Use Elbow Method To Choose K

#  calculate inertia values for different numbers of clusters
#  inertia helps us see how well the data fits each k value
inertias = []
k_values = range(2, 10)

for k in k_values:
    kmeans_temp = KMeans(n_clusters=k, random_state=42, n_init=10)
    kmeans_temp.fit(X_cluster_scaled)
    inertias.append(kmeans_temp.inertia_)

#  plot k against inertia so we can look for the elbow point
plt.figure(figsize=(6, 4))
plt.plot(k_values, inertias, marker='o')
plt.title("Elbow Method For K Selection")
plt.xlabel("Number of Clusters (K)")
plt.ylabel("Inertia")
plt.xticks(k_values)
plt.grid(True)
plt.show()
No description has been provided for this image
In [33]:
#  14.4
#  Fit Final K-Means Model

#  pick the number of clusters we want to use
k = 3
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)

#  fit the k-means model and get the cluster labels for each record
cluster_labels = kmeans.fit_predict(X_cluster_scaled)

#  add the cluster labels to the numeric dataset so we can look at each group
data_clusters = X_cluster.copy()
data_clusters['Cluster'] = cluster_labels

#  show the first few rows to check the cluster labels were added correctly
data_clusters.head()
Out[33]:
Age PlayTimeHours SessionsPerWeek AvgSessionDurationMinutes PlayerLevel AchievementsUnlocked Cluster
0 43 16.271119 6 108 79 25 0
1 29 5.525961 5 144 11 10 2
2 22 8.223755 16 142 35 41 1
3 35 5.265351 9 85 57 47 2
4 33 15.531945 2 131 95 37 0
In [34]:
#  14.5
#  Silhouette Score For Clustering Quality

#  calculate the silhouette score to see how well the clusters are separated
sil_score = silhouette_score(X_cluster_scaled, cluster_labels)

#  print the score so we can judge the overall cluster quality
print("Silhouette Score:", sil_score)
Silhouette Score: 0.11136879695125085
In [35]:
#  14.6
#  Interpret Clusters

#  look at the average values in each cluster to see how the groups differ
cluster_summary = data_clusters.groupby('Cluster').mean()

#  show the summary so we can start understanding each cluster's behaviour
cluster_summary
Out[35]:
Age PlayTimeHours SessionsPerWeek AvgSessionDurationMinutes PlayerLevel AchievementsUnlocked
Cluster
0 38.225959 18.550465 9.378893 95.437025 49.227961 24.727360
1 21.434979 11.919374 9.276254 95.063690 50.120924 24.996606
2 38.445802 5.639922 9.799105 93.823229 49.523604 23.761802