In [1]:
import pandas as pd #import libraries for functionality
import seaborn as sns
import numpy as np

from sklearn.preprocessing import OneHotEncoder, LabelEncoder # for encoding
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split

from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC
from sklearn.metrics import accuracy_score, classification_report, roc_auc_score, confusion_matrix # for evaluating models

from sklearn.model_selection import GridSearchCV # for hyperparameter tuning

from sklearn.cluster import KMeans #clustering
from sklearn.metrics import silhouette_score
from sklearn.decomposition import PCA # cluster visualisation

import matplotlib.pyplot as plt # visualisations

df = pd.read_csv("online_gaming_behavior_dataset.csv")

#Preprocessing of data - cleaning
df.head()
df.info() #shows overview of data, types, column name etc
df.describe()# basic stat summary 
<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[1]:
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 [2]:
df.head(10) #bring back the first 10 columns
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
5 9005 37 Male Europe RPG 20.561855 0 Easy 2 81 74 22 Low
6 9006 25 Male USA Action 9.752716 0 Hard 1 50 13 2 Low
7 9007 25 Female Asia RPG 4.401729 0 Medium 10 48 27 23 Medium
8 9008 38 Female Europe Simulation 18.152733 0 Easy 5 101 23 41 Medium
9 9009 38 Female Other Sports 23.942772 0 Easy 13 95 99 36 High
In [3]:
df.isnull().sum #see if each column has null values
Out[3]:
<bound method DataFrame.sum of        PlayerID    Age  Gender  Location  GameGenre  PlayTimeHours  \
0         False  False   False     False      False          False   
1         False  False   False     False      False          False   
2         False  False   False     False      False          False   
3         False  False   False     False      False          False   
4         False  False   False     False      False          False   
...         ...    ...     ...       ...        ...            ...   
40029     False  False   False     False      False          False   
40030     False  False   False     False      False          False   
40031     False  False   False     False      False          False   
40032     False  False   False     False      False          False   
40033     False  False   False     False      False          False   

       InGamePurchases  GameDifficulty  SessionsPerWeek  \
0                False           False            False   
1                False           False            False   
2                False           False            False   
3                False           False            False   
4                False           False            False   
...                ...             ...              ...   
40029            False           False            False   
40030            False           False            False   
40031            False           False            False   
40032            False           False            False   
40033            False           False            False   

       AvgSessionDurationMinutes  PlayerLevel  AchievementsUnlocked  \
0                          False        False                 False   
1                          False        False                 False   
2                          False        False                 False   
3                          False        False                 False   
4                          False        False                 False   
...                          ...          ...                   ...   
40029                      False        False                 False   
40030                      False        False                 False   
40031                      False        False                 False   
40032                      False        False                 False   
40033                      False        False                 False   

       EngagementLevel  
0                False  
1                False  
2                False  
3                False  
4                False  
...                ...  
40029            False  
40030            False  
40031            False  
40032            False  
40033            False  

[40034 rows x 13 columns]>
In [4]:
df = df.drop("PlayerID", axis = 1) # remove non essential columns for predicting
In [5]:
#identify columns type
categorical_cols = df.select_dtypes(include="object").columns
numerical_cols = df.select_dtypes(include=["int64", "float64"]).columns
categorical_cols, numerical_cols
Out[5]:
(Index(['Gender', 'Location', 'GameGenre', 'GameDifficulty', 'EngagementLevel'], dtype='object'),
 Index(['Age', 'PlayTimeHours', 'InGamePurchases', 'SessionsPerWeek',
        'AvgSessionDurationMinutes', 'PlayerLevel', 'AchievementsUnlocked'],
       dtype='object'))
In [6]:
#encoding as target is multiclass
#sparate target from features
X = df.drop("EngagementLevel", axis = 1)
y = df["EngagementLevel"]
# for target
label_encoder = LabelEncoder()
y_encoded = label_encoder.fit_transform(y)

# for categorical features
preprocessor = ColumnTransformer(
    transformers=[
        ("cat", OneHotEncoder(handle_unknown="ignore"), categorical_cols),
        ("num", "passthrough", numerical_cols)
    ]
)

# categorical and numerical columns
categorical_cols = X.select_dtypes(include="object").columns
numerical_cols = X.select_dtypes(include=["int64", "float64"]).columns

print("Categorical columns:", categorical_cols)
print("Numerical columns:", numerical_cols)

# encode the target
label_encoder = LabelEncoder()
y_encoded = label_encoder.fit_transform(y)

# column transformer: one-hot for categoricals, passthrough for numericals
preprocessor = ColumnTransformer(
    transformers=[
        ("cat", OneHotEncoder(handle_unknown="ignore"), categorical_cols),
        ("num", "passthrough", numerical_cols),
    ]
)

# preprocessing to X
X_processed = preprocessor.fit_transform(X)

# Scale the processed features
scaler = StandardScaler(with_mean=False)
X_scaled = scaler.fit_transform(X_processed)

# train-test split
X_train, X_test, y_train, y_test = train_test_split(
    X_scaled, y_encoded, test_size=0.2, random_state=42, stratify=y_encoded
)

print("Shapes:")
print("X_train:", X_train.shape)
print("X_test:", X_test.shape)
print("y_train:", y_train.shape)
print("y_test:", y_test.shape)
Categorical columns: Index(['Gender', 'Location', 'GameGenre', 'GameDifficulty'], dtype='object')
Numerical columns: Index(['Age', 'PlayTimeHours', 'InGamePurchases', 'SessionsPerWeek',
       'AvgSessionDurationMinutes', 'PlayerLevel', 'AchievementsUnlocked'],
      dtype='object')
Shapes:
X_train: (32027, 21)
X_test: (8007, 21)
y_train: (32027,)
y_test: (8007,)
In [7]:
# Decision tree model
dt = DecisionTreeClassifier(random_state=42)
dt.fit(X_train, y_train)

y_pred_dt = dt.predict(X_test)

# Decision Tree results
cm_dt = confusion_matrix(y_test, y_pred_dt)
print("Decision Tree Accuracy:", accuracy_score(y_test, y_pred_dt))
print("\nConfusion Matrix:\n", cm_dt)
print("\nClassification Report:\n",classification_report(y_test, y_pred_dt))


plt.figure(figsize=(6, 5))
sns.heatmap(cm_dt, annot=True, fmt="d", cmap="Blues",
            xticklabels=label_encoder.classes_,
            yticklabels=label_encoder.classes_)
plt.title("Decision Tree Confusion Matrix")
plt.xlabel("Predicted")
plt.ylabel("Actual")
plt.show()
Decision Tree Accuracy: 0.8357687023854128

Confusion Matrix:
 [[1691  140  236]
 [ 147 1662  256]
 [ 259  277 3339]]

Classification Report:
               precision    recall  f1-score   support

           0       0.81      0.82      0.81      2067
           1       0.80      0.80      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 [8]:
# ROC-AU curve
y_proba_dt = dt.predict_proba(X_test)

dt_auc = roc_auc_score(y_test, y_proba_dt, multi_class='ovr')
print("Decision Tree AUC-ROC:", dt_auc)
Decision Tree AUC-ROC: 0.8711691104624956
In [9]:
# Support Vector Machine model
svm_clf = SVC(kernel='rbf', probability=True, random_state=42)
svm_clf.fit(X_train, y_train)

y_pred_svm = svm_clf.predict(X_test)

# svm results
cm_svm = confusion_matrix(y_test, y_pred_svm)
print("SVM Accuracy:", accuracy_score(y_test, y_pred_svm))
print("\nClassification Report:\n",classification_report(y_test, y_pred_svm, target_names=label_encoder.classes_))
print("\nConfusion Matrix:\n", cm_svm)

plt.figure(figsize=(6, 5))
sns.heatmap(cm_svm, annot=True, fmt="d", cmap="Blues",
            xticklabels=label_encoder.classes_,
            yticklabels=label_encoder.classes_)
plt.title("SVM Confusion Matrix")
plt.xlabel("Predicted")
plt.ylabel("Actual")
plt.show()
SVM Accuracy: 0.884351192706382

Classification Report:
               precision    recall  f1-score   support

        High       0.92      0.85      0.88      2067
         Low       0.89      0.82      0.85      2065
      Medium       0.87      0.93      0.90      3875

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


Confusion Matrix:
 [[1760   70  237]
 [  57 1702  306]
 [ 106  150 3619]]
No description has been provided for this image
In [10]:
#SVM ROC-AU value
y_proba_svm = svm_clf.predict_proba(X_test)
svm_auc = roc_auc_score(y_test, y_proba_svm, multi_class='ovr')
print("SVM AUC-ROC:", svm_auc)
SVM AUC-ROC: 0.938886086273358
In [11]:
#Comparativley, SVM performed better in terms of precision, recal and f1.
# however took a long time to execute in contrast to decision tree
In [21]:
#Hyperparameter tuning for optimisation

# Decision tree model
dt_base = DecisionTreeClassifier(random_state=42)

# Hyperparameter grid for Decision Tree
#depth of tree, node sample controls
param_grid_dt = {
    "max_depth": [5, 10, 20, None],
    "min_samples_split": [2, 10, 20],
    "min_samples_leaf": [1, 5, 10],
    "criterion": ["gini", "entropy"] # gini impurity - how often random element labeled incorrect, entropy - info gain
}
#grid search
grid_dt = GridSearchCV(
    estimator=dt_base,
    param_grid=param_grid_dt,
    cv=3,
    scoring="f1_macro",
    n_jobs=-1
)

grid_dt.fit(X_train, y_train)

print("Best params (Decision Tree):", grid_dt.best_params_)
print("Best CV F1-macro (Decision Tree):", grid_dt.best_score_)

# use the best model
dt_best = grid_dt.best_estimator_

# evaluate on test set
y_pred_dt_best = dt_best.predict(X_test)
y_proba_dt_best = dt_best.predict_proba(X_test)

cm_dt_best = confusion_matrix(y_test, y_pred_dt_best)

print("\nTuned Decision Tree Accuracy:", accuracy_score(y_test, y_pred_dt_best))
print("\nTuned Confusion Matrix:\n", cm_dt_best)
print("\nTunde Decision Tree Classification Report:")
print(classification_report(y_test, y_pred_dt_best, target_names=label_encoder.classes_))

y_proba_dt_best = dt_best.predict_proba(X_test)



plt.figure(figsize=(6, 5))
sns.heatmap(cm_dt_best, annot=True, fmt="d", cmap="Blues",
            xticklabels=label_encoder.classes_,
            yticklabels=label_encoder.classes_)
plt.title("Decision Tree Confusion Matrix")
plt.xlabel("Predicted")
plt.ylabel("Actual")
plt.show()

# ROC-AU score
dt_best_auc = roc_auc_score(y_test, y_proba_dt_best, multi_class="ovr")
print("Tuned Decision Tree AUC-ROC (OvR):", dt_best_auc)
Best params (Decision Tree): {'criterion': 'entropy', 'max_depth': 10, 'min_samples_leaf': 10, 'min_samples_split': 2}
Best CV F1-macro (Decision Tree): 0.8998601937029015

Tuned Decision Tree Accuracy: 0.901835893593106

Tuned Confusion Matrix:
 [[1819   72  176]
 [  62 1801  202]
 [ 113  161 3601]]

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

        High       0.91      0.88      0.90      2067
         Low       0.89      0.87      0.88      2065
      Medium       0.91      0.93      0.92      3875

    accuracy                           0.90      8007
   macro avg       0.90      0.89      0.90      8007
weighted avg       0.90      0.90      0.90      8007

No description has been provided for this image
Tuned Decision Tree AUC-ROC (OvR): 0.9362038622777876
In [24]:
# Hypertuning for SVM model

# Initially, grid search was taking over 15 minutes to finish executing
# therefore creating smaller training set for speed, parameter adjused accordingly
max_samples = 10000  

n_train = X_train.shape[0]
subset_size = min(max_samples, n_train)

np.random.seed(42)
subset_idx = np.random.choice(n_train, subset_size, replace=False)

X_train_small = X_train[subset_idx]
y_train_small = y_train[subset_idx]

print("Using subset of size:", X_train_small.shape[0])


#SVM mode
svm_base = SVC(kernel="rbf", probability=True, random_state=42)


param_grid_svm = {
    "C": [1, 10], # C - regularisation
    "gamma": ["scale", 0.1] # controls boundary
}

 #grid search same as previous cell, for the SVM
grid_svm = GridSearchCV(
    estimator=svm_base,
    param_grid=param_grid_svm,
    cv=3,
    scoring="f1_macro",
    n_jobs=-1
)
print("Tuning SVM... takes a while.")
grid_svm.fit(X_train_small, y_train_small)

print("\nBest SVM parameters (subset):", grid_svm.best_params_)
print("Best CV F1-macro (subset):", grid_svm.best_score_)


# # use the best model ( on full traing set)
best_params = grid_svm.best_params_

svm_best_full = SVC(
    kernel="rbf",
    probability=True,
    random_state=42,
    **best_params
)

svm_best_full.fit(X_train, y_train)

# evaluate on test set
y_pred_svm_best = svm_best_full.predict(X_test)

cm_svm_best = confusion_matrix(y_test, y_pred_svm_best)

print("\nTuned SVM Accuracy:", accuracy_score(y_test, y_pred_svm_best))
print("\nTuned Confusion Matrix:\n", cm_svm_best)
print("\nTuned SVM Classification Report:")
print(classification_report(y_test, y_pred_svm_best, target_names=label_encoder.classes_))


# ROC-AU score
y_proba_svm_best = svm_best_full.predict_proba(X_test)

svm_best_auc = roc_auc_score(y_test, y_proba_svm_best, multi_class="ovr")
print("Tuned SVM AUC-ROC:", svm_best_auc)


plt.figure(figsize=(6, 5))
sns.heatmap(cm_svm_best, annot=True, fmt="d", cmap="Blues",
            xticklabels=label_encoder.classes_,
            yticklabels=label_encoder.classes_)
plt.title("Decision Tree Confusion Matrix")
plt.xlabel("Predicted")
plt.ylabel("Actual")
plt.show()
Using subset of size: 10000
Tuning SVM... takes a while.

Best SVM parameters (subset): {'C': 1, 'gamma': 'scale'}
Best CV F1-macro (subset): 0.8575215967290023

Tuned SVM Accuracy: 0.884351192706382

Tuned Confusion Matrix:
 [[1760   70  237]
 [  57 1702  306]
 [ 106  150 3619]]

Tuned SVM Classification Report:
              precision    recall  f1-score   support

        High       0.92      0.85      0.88      2067
         Low       0.89      0.82      0.85      2065
      Medium       0.87      0.93      0.90      3875

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

Tuned SVM AUC-ROC: 0.938886086273358
No description has been provided for this image
In [14]:
# Clustering: K-means and the Elbow method
# cluster range
k_values = range(2, 10)
inertias = []
sil_scores = []

for k in k_values: #this was a pain
    print(f"Fitting KMeans for k = {k}...")
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    # Fit on the scaled data
    cluster_labels = kmeans.fit_predict(X_scaled)
    
# sum of squared distances to cluster centres, less is best
    inertias.append(kmeans.inertia_)

# how separated clusters are (higher the better)
    sil = silhouette_score(X_scaled, cluster_labels)
    sil_scores.append(sil)
    print(f"  Inertia: {kmeans.inertia_:.2f}, Silhouette: {sil:.3f}")

print("\nInertias:", inertias)
print("Silhouette scores:", sil_scores)

# Plot inertia (Elbow method)
plt.figure()
plt.plot(list(k_values), inertias, marker="o")
plt.xlabel("Number of clusters (k)")
plt.ylabel("Inertia")
plt.title("Elbow Method for K-Means")
plt.show()

# Plot silhouette scores
plt.figure()
plt.plot(list(k_values), sil_scores, marker="o")
plt.xlabel("Number of clusters (k)")
plt.ylabel("Silhouette score")
plt.title("Silhouette Scores for Different k")
plt.show()
Fitting KMeans for k = 2...
  Inertia: 760626.29, Silhouette: 0.098
Fitting KMeans for k = 3...
  Inertia: 720383.74, Silhouette: 0.080
Fitting KMeans for k = 4...
  Inertia: 692543.37, Silhouette: 0.107
Fitting KMeans for k = 5...
  Inertia: 640476.57, Silhouette: 0.158
Fitting KMeans for k = 6...
  Inertia: 633709.21, Silhouette: 0.125
Fitting KMeans for k = 7...
  Inertia: 602962.62, Silhouette: 0.123
Fitting KMeans for k = 8...
  Inertia: 592773.81, Silhouette: 0.110
Fitting KMeans for k = 9...
  Inertia: 581311.34, Silhouette: 0.128

Inertias: [760626.2855287316, 720383.7391917335, 692543.3680414914, 640476.5740687092, 633709.2107689696, 602962.6198734271, 592773.8050668229, 581311.3426554655]
Silhouette scores: [np.float64(0.09849561478579541), np.float64(0.07957789355580693), np.float64(0.10682966659024622), np.float64(0.15834381897691024), np.float64(0.12526677671152817), np.float64(0.12332369511565947), np.float64(0.10966340651739227), np.float64(0.12823749392307357)]
No description has been provided for this image
No description has been provided for this image
In [15]:
# Choose number of clusters
best_k = 5

kmeans_final = KMeans(n_clusters=best_k, random_state=42, n_init=10)
cluster_labels = kmeans_final.fit_predict(X_scaled)

# Add cluster labels back to the original dataframe
df["Cluster"] = cluster_labels

# Check how many players per cluster
print(df["Cluster"].value_counts())
Cluster
0    8048
4    8039
3    8012
1    7983
2    7952
Name: count, dtype: int64
In [16]:
#silhoutte score
final_sil = silhouette_score(X_scaled, cluster_labels)
print("Final silhouette score (k = {}): {:.3f}".format(best_k, final_sil))
Final silhouette score (k = 5): 0.158
In [17]:
# what each cluster looks like in terms of feature
numeric_features_to_inspect = [
    "Age",
    "PlayTimeHours",
    "InGamePurchases",
    "SessionsPerWeek",
    "AvgSessionDurationMinutes",
    "PlayerLevel",
    "AchievementsUnlocked"
]

cluster_profile = df.groupby("Cluster")[numeric_features_to_inspect].mean().round(2)
print(cluster_profile)
           Age  PlayTimeHours  InGamePurchases  SessionsPerWeek  \
Cluster                                                           
0        31.99          11.97             0.20             9.50   
1        32.19          11.90             0.20             9.44   
2        31.81          12.01             0.20             9.41   
3        32.02          12.08             0.21             9.54   
4        31.95          12.16             0.19             9.47   

         AvgSessionDurationMinutes  PlayerLevel  AchievementsUnlocked  
Cluster                                                                
0                            95.05        49.89                 24.40  
1                            94.74        49.25                 24.55  
2                            93.74        49.43                 24.52  
3                            95.64        49.90                 24.58  
4                            94.78        49.80                 24.58  
In [18]:
# change features to 2D for visualization
pca = PCA(n_components=2, random_state=42)
X_pca = pca.fit_transform(X_scaled)

plt.figure(figsize=(8,6))

scatter = plt.scatter(
    X_pca[:, 0], 
    X_pca[:, 1], 
    c=df["Cluster"], 
    cmap="tab10", 
    alpha=0.6
)

plt.title("K-Means Clusters Visualised in 2D (PCA)")
plt.xlabel("PCA Component 1")
plt.ylabel("PCA Component 2")

plt.legend(*scatter.legend_elements(), title="Clusters")
plt.show()
No description has been provided for this image
In [19]:
# cluster to to engagment level
cluster_engagement = df.groupby(["Cluster", "EngagementLevel"]).size().unstack(fill_value=0)
print(cluster_engagement)
EngagementLevel  High   Low  Medium
Cluster                            
0                2083  2074    3891
1                2104  2088    3791
2                1988  2097    3867
3                2097  2006    3909
4                2064  2059    3916
In [ ]: