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
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]]
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
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
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)]
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()
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 [ ]: