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()
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
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()
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()
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()
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()
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)
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()
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 |