1. Imports¶
In [1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split, GridSearchCV, StratifiedShuffleSplit
from sklearn.preprocessing import LabelEncoder, StandardScaler, label_binarize
from sklearn.metrics import (
accuracy_score, precision_score, recall_score, f1_score,
classification_report, confusion_matrix, ConfusionMatrixDisplay,
roc_auc_score, roc_curve, auc
)
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.svm import SVC
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from sklearn.decomposition import PCA
import warnings
warnings.filterwarnings("ignore")
sns.set(style="whitegrid")
RANDOM_STATE = 42
2. Load dataset¶
In [2]:
# Load the dataset (Excel)
DATAFILE = "AIdataset.xlsx"
df = pd.read_excel(DATAFILE)
print("Rows, cols:", df.shape)
df.head()
Rows, cols: (40034, 13)
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. Exploratory Data Analysis (EDA)¶
In [3]:
print(df.info())
print("\nMissing values per column:")
print(df.isna().sum())
print("\nTarget distribution:")
print(df["EngagementLevel"].value_counts())
plt.figure(figsize=(6,4))
sns.countplot(x=df["EngagementLevel"], order=df["EngagementLevel"].value_counts().index)
plt.title("Engagement Level Distribution")
plt.tight_layout()
plt.show()
<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 None 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 Target distribution: EngagementLevel Medium 19374 High 10336 Low 10324 Name: count, dtype: int64
In [4]:
# Basic numeric summary and correlation
numeric_cols = df.select_dtypes(include=["int64", "float64"]).columns.tolist()
numeric_cols = [c for c in numeric_cols if c not in ["PlayerID"]] # exclude ID from analysis
plt.figure(figsize=(9,6))
corr = df[numeric_cols].corr()
sns.heatmap(corr, annot=False, cmap="coolwarm")
plt.title("Correlation Heatmap (Numeric Features)")
plt.tight_layout()
plt.show()
4. Preprocessing¶
In [5]:
# Encode target
le = LabelEncoder()
y = le.fit_transform(df["EngagementLevel"]) # class order depends on data; keep le for decoding
class_names = le.classes_
print("Classes:", class_names)
# Prepare features
X_raw = df.drop(columns=["EngagementLevel"])
# One-hot encode categorical features
X = pd.get_dummies(X_raw, drop_first=True)
# Train/test split (stratified)
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=RANDOM_STATE, stratify=y
)
# Scale features (important for SVM and K-Means)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
print("X_train:", X_train.shape, "X_test:", X_test.shape)
Classes: ['High' 'Low' 'Medium'] X_train: (32027, 18) X_test: (8007, 18)
5. Supervised Learning: Decision Tree¶
In [6]:
# 5.1 Baseline Decision Tree
dt = DecisionTreeClassifier(random_state=RANDOM_STATE)
dt.fit(X_train_scaled, y_train)
y_pred_dt = dt.predict(X_test_scaled)
print("Decision Tree (Baseline)")
print("Accuracy:", accuracy_score(y_test, y_pred_dt))
print(classification_report(y_test, y_pred_dt, target_names=class_names, zero_division=0))
Decision Tree (Baseline)
Accuracy: 0.8347695766204571
precision recall f1-score support
High 0.80 0.80 0.80 2067
Low 0.80 0.82 0.81 2065
Medium 0.87 0.86 0.86 3875
accuracy 0.83 8007
macro avg 0.83 0.83 0.83 8007
weighted avg 0.84 0.83 0.83 8007
In [7]:
# 5.2 Tuned Decision Tree (GridSearchCV)
param_grid_dt = {
"criterion": ["gini", "entropy"],
"max_depth": [5, 10, None],
"min_samples_split": [2, 5, 10],
"min_samples_leaf": [1, 5, 10]
}
dt_grid = GridSearchCV(
DecisionTreeClassifier(random_state=RANDOM_STATE),
param_grid_dt,
cv=5,
scoring="f1_macro",
n_jobs=-1
)
dt_grid.fit(X_train_scaled, y_train)
best_dt = dt_grid.best_estimator_
y_pred_dt_tuned = best_dt.predict(X_test_scaled)
print("Best DT params:", dt_grid.best_params_)
print("Decision Tree (Tuned)")
print("Accuracy:", accuracy_score(y_test, y_pred_dt_tuned))
print(classification_report(y_test, y_pred_dt_tuned, target_names=class_names, zero_division=0))
Best DT params: {'criterion': 'entropy', 'max_depth': 10, 'min_samples_leaf': 10, 'min_samples_split': 2}
Decision Tree (Tuned)
Accuracy: 0.9028350193580617
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
In [8]:
# 5.3 Decision Tree confusion matrix
plt.figure(figsize=(6,5))
ConfusionMatrixDisplay.from_predictions(y_test, y_pred_dt_tuned, display_labels=class_names, cmap="Blues")
plt.title("Confusion Matrix: Tuned Decision Tree")
plt.tight_layout()
plt.show()
<Figure size 600x500 with 0 Axes>
6. Supervised Learning: Support Vector Machine (SVM)¶
In [9]:
# 6.1 Baseline SVM (RBF)
svm = SVC(kernel="rbf", probability=True, random_state=RANDOM_STATE)
svm.fit(X_train_scaled, y_train)
y_pred_svm = svm.predict(X_test_scaled)
print("SVM (Baseline)")
print("Accuracy:", accuracy_score(y_test, y_pred_svm))
print(classification_report(y_test, y_pred_svm, target_names=class_names, zero_division=0))
SVM (Baseline)
Accuracy: 0.8804795803671788
precision recall f1-score support
High 0.91 0.84 0.88 2067
Low 0.88 0.83 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.88 0.88 0.88 8007
In [10]:
# 6.2 Tuned SVM
sss = StratifiedShuffleSplit(n_splits=1, test_size=0.8, random_state=RANDOM_STATE)
tune_idx, _ = next(sss.split(X_train_scaled, y_train))
X_tune = X_train_scaled[tune_idx]
y_tune = y_train[tune_idx]
param_grid_svm = {
"C": [0.1, 1, 10],
"gamma": ["scale", 0.01, 0.1],
"kernel": ["rbf"]
}
svm_grid = GridSearchCV(
SVC(probability=True, random_state=RANDOM_STATE),
param_grid_svm,
cv=3,
scoring="f1_macro",
n_jobs=-1
)
svm_grid.fit(X_tune, y_tune)
best_svm = svm_grid.best_estimator_
best_svm.fit(X_train_scaled, y_train)
y_pred_svm_tuned = best_svm.predict(X_test_scaled)
print("Best SVM params:", svm_grid.best_params_)
print("SVM (Tuned)")
print("Accuracy:", accuracy_score(y_test, y_pred_svm_tuned))
print(classification_report(y_test, y_pred_svm_tuned, target_names=class_names, zero_division=0))
Best SVM params: {'C': 10, 'gamma': 0.01, 'kernel': 'rbf'}
SVM (Tuned)
Accuracy: 0.8867241163981516
precision recall f1-score support
High 0.92 0.85 0.88 2067
Low 0.89 0.83 0.86 2065
Medium 0.87 0.93 0.90 3875
accuracy 0.89 8007
macro avg 0.89 0.87 0.88 8007
weighted avg 0.89 0.89 0.89 8007
In [11]:
# 6.3 SVM confusion matrix
plt.figure(figsize=(6,5))
ConfusionMatrixDisplay.from_predictions(y_test, y_pred_svm_tuned, display_labels=class_names, cmap="Oranges")
plt.title("Confusion Matrix: Tuned SVM")
plt.tight_layout()
plt.show()
<Figure size 600x500 with 0 Axes>
7. Model Evaluation and Comparison¶
In [12]:
# 7.1 Performance comparison table (macro averaged metrics)
comparison = pd.DataFrame({
"Model": ["Decision Tree (Tuned)", "SVM (Tuned)"],
"Accuracy": [
accuracy_score(y_test, y_pred_dt_tuned),
accuracy_score(y_test, y_pred_svm_tuned)
],
"Precision (macro)": [
precision_score(y_test, y_pred_dt_tuned, average="macro", zero_division=0),
precision_score(y_test, y_pred_svm_tuned, average="macro", zero_division=0)
],
"Recall (macro)": [
recall_score(y_test, y_pred_dt_tuned, average="macro", zero_division=0),
recall_score(y_test, y_pred_svm_tuned, average="macro", zero_division=0)
],
"F1-score (macro)": [
f1_score(y_test, y_pred_dt_tuned, average="macro", zero_division=0),
f1_score(y_test, y_pred_svm_tuned, average="macro", zero_division=0)
]
}).round(4)
comparison
Out[12]:
| Model | Accuracy | Precision (macro) | Recall (macro) | F1-score (macro) | |
|---|---|---|---|---|---|
| 0 | Decision Tree (Tuned) | 0.9028 | 0.9019 | 0.8947 | 0.8982 |
| 1 | SVM (Tuned) | 0.8867 | 0.8919 | 0.8729 | 0.8814 |
8. ROC-AUC Evaluation (multi-class OvR)¶
In [13]:
# ROC-AUC requires probability estimates
classes = np.unique(y_test)
y_test_bin = label_binarize(y_test, classes=classes)
dt_probs = best_dt.predict_proba(X_test_scaled)
svm_probs = best_svm.predict_proba(X_test_scaled)
dt_auc = roc_auc_score(y_test_bin, dt_probs, multi_class="ovr", average="macro")
svm_auc = roc_auc_score(y_test_bin, svm_probs, multi_class="ovr", average="macro")
print("Decision Tree ROC-AUC (macro, OvR):", round(dt_auc, 4))
print("SVM ROC-AUC (macro, OvR):", round(svm_auc, 4))
Decision Tree ROC-AUC (macro, OvR): 0.9344 SVM ROC-AUC (macro, OvR): 0.9401
In [14]:
# ROC curves per class
def plot_multiclass_roc(y_true_bin, y_prob, title):
plt.figure(figsize=(7,6))
for i, cls in enumerate(classes):
fpr, tpr, _ = roc_curve(y_true_bin[:, i], y_prob[:, i])
roc_auc = auc(fpr, tpr)
plt.plot(fpr, tpr, label=f"Class {cls} (AUC={roc_auc:.3f})")
plt.plot([0,1], [0,1], "k--", label="Random")
plt.xlabel("False Positive Rate")
plt.ylabel("True Positive Rate")
plt.title(title)
plt.legend()
plt.tight_layout()
plt.show()
plot_multiclass_roc(y_test_bin, dt_probs, "Decision Tree ROC Curves (OvR)")
plot_multiclass_roc(y_test_bin, svm_probs, "SVM ROC Curves (OvR)")
9. Unsupervised Learning: K-Means Clustering¶
In [15]:
# Use full feature space (scaled) for clustering (no labels used)
X_full_scaled = scaler.transform(X) # scale full dataset using training-fitted scaler
# Elbow + silhouette on a sample for speed
np.random.seed(RANDOM_STATE)
sample_size = min(8000, X_full_scaled.shape[0])
sample_idx = np.random.choice(X_full_scaled.shape[0], size=sample_size, replace=False)
X_sample = X_full_scaled[sample_idx]
k_values = range(2, 11)
inertias, sils = [], []
for k in k_values:
km = KMeans(n_clusters=k, random_state=RANDOM_STATE, n_init="auto")
labels = km.fit_predict(X_sample)
inertias.append(km.inertia_)
sils.append(silhouette_score(X_sample, labels))
plt.figure(figsize=(6,4))
plt.plot(list(k_values), inertias, marker="o")
plt.title("K-Means Elbow (sample)")
plt.xlabel("k")
plt.ylabel("Inertia")
plt.tight_layout()
plt.show()
plt.figure(figsize=(6,4))
plt.plot(list(k_values), sils, marker="o")
plt.title("K-Means Silhouette (sample)")
plt.xlabel("k")
plt.ylabel("Silhouette score")
plt.tight_layout()
plt.show()
best_k = list(k_values)[int(np.argmax(sils))]
print("Best k by silhouette (sample):", best_k)
Best k by silhouette (sample): 4
In [16]:
# 9.1 Fit final K-Means on full dataset using chosen k
kmeans_full = KMeans(n_clusters=best_k, random_state=RANDOM_STATE, n_init="auto")
clusters_full = kmeans_full.fit_predict(X_full_scaled)
df_clusters = df.copy()
df_clusters["Cluster"] = clusters_full
df_clusters[["EngagementLevel", "Cluster"]].head()
Out[16]:
| EngagementLevel | Cluster | |
|---|---|---|
| 0 | Medium | 1 |
| 1 | Medium | 1 |
| 2 | High | 0 |
| 3 | Medium | 0 |
| 4 | Medium | 0 |
10. Cluster Interpretation and Visualisation¶
In [17]:
# Cluster vs engagement distribution (percent)
cluster_mix_full = pd.crosstab(
df_clusters["Cluster"], df_clusters["EngagementLevel"], normalize="index"
) * 100
cluster_mix_full = cluster_mix_full.round(1)
cluster_mix_full
Out[17]:
| EngagementLevel | High | Low | Medium |
|---|---|---|---|
| Cluster | |||
| 0 | 25.8 | 25.7 | 48.5 |
| 1 | 26.2 | 25.0 | 48.8 |
| 2 | 25.0 | 26.4 | 48.6 |
| 3 | 26.4 | 26.2 | 47.5 |
In [18]:
ax = cluster_mix_full.plot(kind="bar", stacked=True, figsize=(10,6))
plt.title("Engagement Level Distribution by Cluster (Full Dataset)")
plt.xlabel("Cluster")
plt.ylabel("Percentage of players")
plt.legend(title="Engagement Level", bbox_to_anchor=(1.02, 1))
plt.tight_layout()
plt.show()
In [19]:
# Profile clusters using key behavioural features
key_features = [
"PlayTimeHours", "InGamePurchases", "SessionsPerWeek",
"AvgSessionDurationMinutes", "PlayerLevel", "AchievementsUnlocked"
]
df_clusters.groupby("Cluster")[key_features].mean().round(2)
Out[19]:
| PlayTimeHours | InGamePurchases | SessionsPerWeek | AvgSessionDurationMinutes | PlayerLevel | AchievementsUnlocked | |
|---|---|---|---|---|---|---|
| Cluster | ||||||
| 0 | 12.07 | 0.20 | 9.49 | 94.91 | 49.85 | 24.49 |
| 1 | 12.08 | 0.21 | 9.54 | 95.64 | 49.90 | 24.58 |
| 2 | 12.01 | 0.20 | 9.41 | 93.74 | 49.43 | 24.52 |
| 3 | 11.90 | 0.20 | 9.44 | 94.74 | 49.25 | 24.55 |
In [20]:
# PCA 2D projection for cluster visualisation
pca = PCA(n_components=2, random_state=RANDOM_STATE)
X_pca = pca.fit_transform(X_full_scaled)
plt.figure(figsize=(7,6))
plt.scatter(X_pca[:, 0], X_pca[:, 1], c=clusters_full, s=10)
plt.title("K-Means Clusters (PCA 2D Projection)")
plt.xlabel("PC1")
plt.ylabel("PC2")
plt.tight_layout()
plt.show()
print("PCA explained variance (%):", (pca.explained_variance_ratio_ * 100).round(1))
PCA explained variance (%): [8.5 7.4]
11. Feature Importance (Decision Tree)¶
In [21]:
# Feature importance from tuned Decision Tree
importance = pd.Series(best_dt.feature_importances_, index=X_train.columns).sort_values(ascending=False)
top10 = importance.head(10)
plt.figure(figsize=(8,5))
top10.sort_values().plot(kind="barh")
plt.title("Top 10 Feature Importances (Tuned Decision Tree)")
plt.xlabel("Importance")
plt.tight_layout()
plt.show()
top10
Out[21]:
SessionsPerWeek 0.446113 AvgSessionDurationMinutes 0.435923 AchievementsUnlocked 0.042517 PlayerLevel 0.039862 PlayerID 0.009335 PlayTimeHours 0.008522 Age 0.008065 Gender_Male 0.001822 GameDifficulty_Medium 0.001729 GameGenre_Simulation 0.001262 dtype: float64