In [1]:
# ============================================================
# IMPORTING CORE LIBRARIES
# These libraries are used for data manipulation, numerical analysis,
# machine learning model development, visualisation, and evaluation.
# ============================================================
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
# Preprocessing tools for scaling and encoding
from sklearn.preprocessing import StandardScaler, LabelEncoder
# Tools for splitting data and tuning models
from sklearn.model_selection import train_test_split, GridSearchCV
# Supervised machine learning algorithms
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC
# Model evaluation metrics
from sklearn.metrics import (
classification_report,
confusion_matrix,
accuracy_score,
roc_auc_score,
roc_curve
)
# Required for multiclass ROC-AUC
from sklearn.preprocessing import label_binarize
# Unsupervised clustering tools
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
# Ignore warnings for cleaner output
import warnings
warnings.filterwarnings("ignore")
In [2]:
# ============================================================
# LOADING THE DATASET
# The dataset contains over 40,000 online game player records.
# The target variable is 'EngagementLevel'.
# ============================================================
df = pd.read_csv("online_gaming_behavior_dataset.csv")
df.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 |
In [3]:
# ============================================================
# DATA QUALITY CHECKS
# These functions verify:
# - Missing values
# - Data types
# - Statistical distributions
# - Unique category counts
# ============================================================
df.info()
df.describe()
df.isnull().sum()
df.nunique()
<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
Out[3]:
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
In [4]:
# ============================================================
# DATA CLEANING
# Removing PlayerID as it is a unique identifier and adds no
# predictive value for engagement classification.
# ============================================================
df = df.drop("PlayerID", axis=1)
In [5]:
# ============================================================
# FEATURE SELECTION
# Separating the dataset into numerical and categorical variables.
# This allows correct preprocessing later (scaling & encoding).
# ============================================================
# Target Column
target_col = "EngagementLevel"
# Numerical Features
num_cols = [
'Age',
'PlayTimeHours',
'InGamePurchases',
'SessionsPerWeek',
'AvgSessionDurationMinutes',
'PlayerLevel',
'AchievementsUnlocked'
]
# Categorical Features
cat_cols = [
'Gender',
'Location',
'GameGenre',
'GameDifficulty'
]
In [6]:
# ============================================================
# EXPLORATORY DATA ANALYSIS (EDA)
# Visualising feature distributions.
# Understanding engagement class proportions.
# ============================================================
df[num_cols].describe().T
df[num_cols].hist(figsize=(12,12))
plt.show()
In [7]:
# Distribution of engagement levels
df[target_col].value_counts(normalize=True)
Out[7]:
EngagementLevel Medium 0.483939 High 0.258181 Low 0.257881 Name: proportion, dtype: float64
In [8]:
# Mean behaviour grouped by engagement level
df.groupby(target_col)[num_cols].mean()
Out[8]:
| Age | PlayTimeHours | InGamePurchases | SessionsPerWeek | AvgSessionDurationMinutes | PlayerLevel | AchievementsUnlocked | |
|---|---|---|---|---|---|---|---|
| EngagementLevel | |||||||
| High | 31.920085 | 12.069238 | 0.206463 | 14.254547 | 131.921827 | 50.823723 | 25.095975 |
| Low | 31.896939 | 12.104915 | 0.197307 | 4.530511 | 66.882119 | 46.101414 | 22.661565 |
| Medium | 32.082120 | 11.957503 | 0.199752 | 9.553267 | 89.856405 | 50.926293 | 25.216424 |
In [9]:
# Correlation heatmap between numerical features
plt.figure(figsize=(12,6))
sns.heatmap(df[num_cols].corr(), annot=True, cmap='YlOrRd')
plt.show()
In [10]:
# ============================================================
# ENCODING CATEGORICAL VARIABLES
# Label Encoding for target variable.
# One-Hot Encoding for categorical features.
# ============================================================
# Encode engagement levels (Low, Medium, High)
le = LabelEncoder()
df[target_col] = le.fit_transform(df[target_col])
# One-hot encode categorical variables
df = pd.get_dummies(df, columns=cat_cols, drop_first=True)
In [11]:
# ============================================================
# TRAIN-TEST SPLIT
# Stratified splitting ensures class distribution remains balanced.
# 80% training, 20% testing.
# ============================================================
Y = df[target_col]
X = df.drop(columns=[target_col])
In [12]:
X_train, X_test, y_train, y_test = train_test_split(
X, Y, test_size=0.2, random_state=42, stratify=Y
)
In [13]:
# ============================================================
# FEATURE SCALING
# StandardScaler ensures features have zero mean and unit variance.
# This is critical for SVM and K-Means.
# ============================================================
sc = StandardScaler()
X_train_scaled = sc.fit_transform(X_train)
X_test_scaled = sc.transform(X_test)
X_train_scaled = pd.DataFrame(X_train_scaled, columns=X.columns)
X_test_scaled = pd.DataFrame(X_test_scaled, columns=X.columns)
In [14]:
# ============================================================
# MODEL EVALUATION FUNCTION
# Displays classification report and confusion matrix.
# ============================================================
def metrics_score(actual, predicted):
print(classification_report(actual, predicted))
cm = confusion_matrix(actual, predicted)
plt.figure(figsize=(6,4))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.ylabel('Actual')
plt.xlabel('Predicted')
plt.title("Confusion Matrix")
plt.show()
plt.close()
In [15]:
# ============================================================
# BASELINE DECISION TREE MODEL
# A simple tree used to establish baseline performance.
# ============================================================
dt_model = DecisionTreeClassifier(random_state=42)
dt_model.fit(X_train, y_train)
y_pred_dt = dt_model.predict(X_test)
print("Decision Tree Accuracy:", accuracy_score(y_test, y_pred_dt))
metrics_score(y_test, y_pred_dt)
Decision Tree Accuracy: 0.838016735356563
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 [16]:
# ============================================================
# BASELINE SVM MODEL
# RBF kernel allows non-linear decision boundaries.
# ============================================================
svm_model = SVC(kernel='rbf', probability=True, random_state=42)
svm_model.fit(X_train_scaled, y_train)
y_pred_svm = svm_model.predict(X_test_scaled)
print("SVM Accuracy:", accuracy_score(y_test, y_pred_svm))
metrics_score(y_test, y_pred_svm)
SVM Accuracy: 0.8821031597352317
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 [17]:
# ============================================================
# MULTI-CLASS ROC–AUC EVALUATION
# Binary conversion of classes is required for ROC computation.
# ============================================================
y_test_bin = label_binarize(y_test, classes=[0, 1, 2])
y_score = svm_model.predict_proba(X_test_scaled)
auc = roc_auc_score(y_test_bin, y_score, multi_class="ovr")
print("ROC-AUC:", auc)
for i in range(3):
fpr, tpr, _ = roc_curve(y_test_bin[:, i], y_score[:, i])
plt.plot(fpr, tpr, label=f'Class {i}')
plt.plot([0,1],[0,1],'k--')
plt.legend()
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate")
plt.title("SVM ROC Curve")
plt.show()
ROC-AUC: 0.9393615637692543
In [18]:
# ============================================================
# DECISION TREE HYPERPARAMETER TUNING
# GridSearchCV improves generalisation performance.
# ============================================================
dt_params = {
"max_depth": [None, 5, 10],
"min_samples_split": [2, 5],
"min_samples_leaf": [1, 2]
}
dt_grid = GridSearchCV(
DecisionTreeClassifier(random_state=42),
dt_params,
cv=3,
scoring="accuracy",
n_jobs=-1
)
dt_grid.fit(X_train, y_train)
best_dt = dt_grid.best_estimator_
y_pred_dt_tuned = best_dt.predict(X_test)
print("Best Decision Tree Parameters:", dt_grid.best_params_)
print("Tuned Decision Tree Accuracy:", accuracy_score(y_test, y_pred_dt_tuned))
metrics_score(y_test, y_pred_dt_tuned)
Best Decision Tree Parameters: {'max_depth': 10, 'min_samples_leaf': 2, 'min_samples_split': 5}
Tuned Decision Tree Accuracy: 0.8977144998126639
precision recall f1-score support
0 0.90 0.87 0.88 2067
1 0.89 0.86 0.88 2065
2 0.90 0.93 0.92 3875
accuracy 0.90 8007
macro avg 0.90 0.89 0.89 8007
weighted avg 0.90 0.90 0.90 8007
In [19]:
# ============================================================
# SVM TUNING WITH REDUCED DATASET
# Full-grid tuning is computationally expensive on large datasets.
# A 6,000-sample stratified subset is used.
# ============================================================
X_tune, _, y_tune, _ = train_test_split(
X_train_scaled,
y_train,
train_size=6000,
stratify=y_train,
random_state=42
)
print("Tuning sample size:", X_tune.shape)
Tuning sample size: (6000, 17)
In [20]:
svm_params_6000 = {
"C": [1, 10],
"gamma": [0.1, 1],
"kernel": ["rbf"]
}
svm_grid = GridSearchCV(
SVC(probability=True, random_state=42),
svm_params_6000,
cv=3,
scoring="accuracy",
n_jobs=-1
)
svm_grid.fit(X_tune, y_tune)
best_svm = svm_grid.best_estimator_
print("Best SVM Parameters (6000 sample):", svm_grid.best_params_)
Best SVM Parameters (6000 sample): {'C': 1, 'gamma': 0.1, 'kernel': 'rbf'}
In [21]:
y_pred_svm_tuned = best_svm.predict(X_test_scaled)
print("Tuned SVM Accuracy (Full Test Set):", accuracy_score(y_test, y_pred_svm_tuned))
metrics_score(y_test, y_pred_svm_tuned)
Tuned SVM Accuracy (Full Test Set): 0.8435119270638192
precision recall f1-score support
0 0.89 0.80 0.84 2067
1 0.84 0.78 0.81 2065
2 0.83 0.90 0.86 3875
accuracy 0.84 8007
macro avg 0.85 0.83 0.84 8007
weighted avg 0.85 0.84 0.84 8007
In [22]:
# ============================================================
# K-MEANS CLUSTERING – ELBOW METHOD
# Used to determine optimal number of clusters.
# ============================================================
inertia = []
for k in range(1, 11):
kmeans = KMeans(n_clusters=k, random_state=42)
kmeans.fit(X_train_scaled)
inertia.append(kmeans.inertia_)
plt.plot(range(1, 11), inertia)
plt.xlabel("Number of Clusters")
plt.ylabel("Inertia")
plt.title("Elbow Method")
plt.show()
In [23]:
# ============================================================
# FINAL K-MEANS MODEL WITH 3 CLUSTERS
# ============================================================
kmeans = KMeans(n_clusters=3, random_state=42)
clusters = kmeans.fit_predict(X_train_scaled)
df_clusters = pd.DataFrame(X_train_scaled, columns=X.columns)
df_clusters["Cluster"] = clusters
df_clusters.head()
Out[23]:
| Age | PlayTimeHours | InGamePurchases | SessionsPerWeek | AvgSessionDurationMinutes | PlayerLevel | AchievementsUnlocked | Gender_Male | Location_Europe | Location_Other | Location_USA | GameGenre_RPG | GameGenre_Simulation | GameGenre_Sports | GameGenre_Strategy | GameDifficulty_Hard | GameDifficulty_Medium | Cluster | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 1.292395 | 0.500494 | 2.000664 | 0.093647 | 0.267054 | 1.269216 | -0.381630 | 0.822096 | 1.526062 | -0.330801 | -0.814968 | -0.495048 | -0.500663 | -0.500566 | 1.995406 | -0.503491 | -0.655282 | 2 |
| 1 | -0.798243 | 0.633889 | -0.499834 | -0.773684 | -0.366923 | 0.220099 | -0.104547 | -1.216404 | 1.526062 | -0.330801 | -0.814968 | -0.495048 | -0.500663 | -0.500566 | 1.995406 | -0.503491 | -0.655282 | 2 |
| 2 | 1.292395 | 1.142463 | 2.000664 | -0.600218 | -1.205410 | -1.598370 | -0.104547 | 0.822096 | -0.655282 | -0.330801 | 1.227042 | -0.495048 | 1.997350 | -0.500566 | -0.501151 | -0.503491 | 1.526062 | 0 |
| 3 | 0.197299 | -1.246760 | -0.499834 | -0.947150 | -0.919097 | -1.388546 | -1.697774 | -1.216404 | 1.526062 | -0.330801 | -0.814968 | 2.020006 | -0.500663 | -0.500566 | -0.501151 | -0.503491 | -0.655282 | 1 |
| 4 | 1.093287 | -0.430797 | -0.499834 | 0.614045 | -1.471271 | 0.325011 | -0.173818 | 0.822096 | -0.655282 | -0.330801 | -0.814968 | -0.495048 | -0.500663 | -0.500566 | 1.995406 | -0.503491 | -0.655282 | 2 |
In [24]:
from sklearn.metrics import silhouette_score
In [25]:
# ============================================================
# CLUSTER VALIDATION USING SILHOUETTE SCORE
# ============================================================
sil_score = silhouette_score(X_train_scaled, clusters)
print("Silhouette Score:", sil_score)
Silhouette Score: 0.08960186274127453
In [ ]: