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()
No description has been provided for this image
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()
No description has been provided for this image
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

No description has been provided for this image
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

No description has been provided for this image
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
No description has been provided for this image
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

No description has been provided for this image
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

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