In [24]:
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, StratifiedKFold, GridSearchCV, RandomizedSearchCV
)
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder, LabelEncoder
from sklearn.impute import SimpleImputer
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.svm import SVC
from sklearn.metrics import (
accuracy_score, precision_score, recall_score, f1_score,
classification_report, confusion_matrix, roc_auc_score,
roc_curve
)
from sklearn.preprocessing import label_binarize
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', context='notebook')
DATAFILE = 'online_gaming_behavior_dataset.csv'
df = pd.read_csv(DATAFILE)
print('Rows, cols:', df.shape)
df.head()
Rows, cols: (40034, 13)
Out[24]:
| 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 [25]:
# Basic EDA: Info, Missing Values, Target Dist
# Ensure EngagementLevel is treated as categorical (string)
if df['EngagementLevel'].dtype != 'object' and \
not pd.api.types.is_categorical_dtype(df['EngagementLevel']):
df['EngagementLevel'] = df['EngagementLevel'].astype(str)
print('\n--- Dataset info ---')
print(df.info())
print('\n--- Missing values per column ---')
print(df.isnull().sum())
print('\n--- Target distribution ---')
print(df['EngagementLevel'].value_counts())
# Helper plots
def plot_target_distribution(y):
plt.figure(figsize=(6,4))
sns.countplot(x=y, order=y.value_counts().index)
plt.title('Engagement Level Distribution')
plt.xlabel('EngagementLevel')
plt.ylabel('Count')
plt.tight_layout()
plt.show()
plot_target_distribution(df['EngagementLevel'])
--- Dataset info --- <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 [26]:
# Numeric & Categorical Distributions
# Automatically detect numeric and categorical columns
numeric_cols = df.select_dtypes(include=['int64', 'float64']).columns.tolist()
# Exclude target if it accidentally appears numeric
numeric_cols = [c for c in numeric_cols if c != 'EngagementLevel']
categorical_cols = df.select_dtypes(include=['object', 'category']).columns.tolist()
categorical_cols = [c for c in categorical_cols if c != 'EngagementLevel']
print('\nNumeric columns:', numeric_cols)
print('Categorical columns:', categorical_cols)
print('\n--- Numeric summary ---')
print(df[numeric_cols].describe())
# Numeric Distributions (Hist + KDE)
for col in numeric_cols:
plt.figure(figsize=(6, 4))
sns.histplot(df[col], kde=True)
plt.title(f'Distribution of {col}')
plt.tight_layout()
plt.show()
# Boxplots for numeric columns (outliers)
for col in numeric_cols:
plt.figure(figsize=(6, 4))
sns.boxplot(x=df[col])
plt.title(f'Boxplot of {col}')
plt.tight_layout()
plt.show()
# Categorical distributions
for col in categorical_cols:
plt.figure(figsize=(6, 4))
sns.countplot(x=df[col], order=df[col].value_counts().index)
plt.title(f'Distribution of {col}')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
# Correlation heatmap (numeric)
plt.figure(figsize=(8,6))
corr = df[numeric_cols].corr()
sns.heatmap(corr, annot=True, cmap='coolwarm', fmt='.2f')
plt.title('Correlation Heatmap (Numeric Features)')
plt.tight_layout()
plt.show()
Numeric columns: ['PlayerID', 'Age', 'PlayTimeHours', 'InGamePurchases', 'SessionsPerWeek', 'AvgSessionDurationMinutes', 'PlayerLevel', 'AchievementsUnlocked']
Categorical columns: ['Gender', 'Location', 'GameGenre', 'GameDifficulty']
--- Numeric summary ---
PlayerID Age PlayTimeHours InGamePurchases \
count 40034.000000 40034.000000 40034.000000 40034.000000
mean 29016.500000 31.992531 12.024365 0.200854
std 11556.964675 10.043227 6.914638 0.400644
min 9000.000000 15.000000 0.000115 0.000000
25% 19008.250000 23.000000 6.067501 0.000000
50% 29016.500000 32.000000 12.008002 0.000000
75% 39024.750000 41.000000 17.963831 0.000000
max 49033.000000 49.000000 23.999592 1.000000
SessionsPerWeek AvgSessionDurationMinutes PlayerLevel \
count 40034.000000 40034.000000 40034.000000
mean 9.471774 94.792252 49.655568
std 5.763667 49.011375 28.588379
min 0.000000 10.000000 1.000000
25% 4.000000 52.000000 25.000000
50% 9.000000 95.000000 49.000000
75% 14.000000 137.000000 74.000000
max 19.000000 179.000000 99.000000
AchievementsUnlocked
count 40034.000000
mean 24.526477
std 14.430726
min 0.000000
25% 12.000000
50% 25.000000
75% 37.000000
max 49.000000
In [27]:
# 5. Prepare Data for Supervised Learning
# (Decision Tree & SVM)
# Features and target
X = df.drop('EngagementLevel', axis=1)
y = df['EngagementLevel']
# Encode target labels for later metrics (especially for ROC/AUC)
label_encoder = LabelEncoder()
y_encoded = label_encoder.fit_transform(y)
class_names = label_encoder.classes_
print('Classes:', class_names)
# Train-test split (stratified by target)
X_train, X_test, y_train, y_test = train_test_split(
X, y_encoded, test_size=0.2, random_state=42, stratify=y_encoded
)
# Preprocessing for numeric and categorical features
numeric_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='median')),
('scaler', StandardScaler())
])
categorical_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='most_frequent')),
('onehot', OneHotEncoder(handle_unknown='ignore'))
])
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_cols),
('cat', categorical_transformer, categorical_cols)
]
)
# Helper function to evaluate classification model
def evaluate_model(model, X_test, y_test, model_name="Model"):
y_pred = model.predict(X_test)
acc = accuracy_score(y_test, y_pred)
prec = precision_score(y_test, y_pred, average='weighted', zero_division=0)
rec = recall_score(y_test, y_pred, average='weighted', zero_division=0)
f1 = f1_score(y_test, y_pred, average='weighted', zero_division=0)
print(f'\n--- {model_name} Performance ---')
print(f'Accuracy : {acc:.4f}')
print(f'Precision: {prec:.4f}')
print(f'Recall : {rec:.4f}')
print(f'F1-score : {f1:.4f}')
print('\nClassification Report:')
print(classification_report(y_test, y_pred, target_names=class_names, zero_division=0))
# Confusion matrix
cm = confusion_matrix(y_test, y_pred)
plt.figure(figsize=(6,5))
sns.heatmap(cm, annot=True, fmt='d', xticklabels=class_names, yticklabels=class_names, cmap='Blues')
plt.ylabel('True label')
plt.xlabel('Predicted label')
plt.title(f'{model_name} - Confusion Matrix')
plt.tight_layout()
plt.show()
return acc, prec, rec, f1
# Helper for multi-class ROC-AUC (one-vs-rest)
def multiclass_roc_auc(model, X_test, y_test, model_name='Model'):
# Need probability estimates or decision function
if hasattr(model, "predict_proba"):
y_score = model.predict_proba(X_test)
elif hasattr(model, "decision_function"):
y_score = model.decision_function(X_test)
else:
print(f"{model_name} does not support probability estimates / decision_function for ROC.")
return None
# Binarize labels for multi-class ROC
y_test_binarized = label_binarize(y_test, classes=np.arange(len(class_names)))
# Compute overall AUC using one-vs-rest
auc = roc_auc_score(y_test_binarized, y_score, average='macro', multi_class='ovr')
print(f'{model_name} ROC-AUC (macro, OVR): {auc:.4f}')
# Plot ROC curve for each class
plt.figure(figsize=(7,6))
for i, class_label in enumerate(class_names):
fpr, tpr, _ = roc_curve(y_test_binarized[:, i], y_score[:, i])
plt.plot(fpr, tpr, label=f'Class {class_label}')
plt.plot([0,1], [0,1], 'k--')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title(f'{model_name} - ROC Curves')
plt.legend()
plt.tight_layout()
plt.show()
return auc
Classes: ['High' 'Low' 'Medium']
In [28]:
# Decision Tree Classifier - Baseline
dt_clf = DecisionTreeClassifier(random_state=42)
dt_pipeline = Pipeline(steps=[
('preprocessor', preprocessor),
('classifier', dt_clf)
])
dt_pipeline.fit(X_train, y_train)
dt_acc, dt_prec, dt_rec, dt_f1 = evaluate_model(dt_pipeline, X_test, y_test, model_name='Decision Tree')
dt_auc = multiclass_roc_auc(dt_pipeline, X_test, y_test, model_name='Decision Tree')
# Optional: Visualise the tree (might be large!)
# Extract the trained tree from pipeline
dt_model = dt_pipeline.named_steps['classifier']
feature_names_num = numeric_cols
feature_names_cat = list(dt_pipeline.named_steps['preprocessor'] \
.named_transformers_['cat'] \
.named_steps['onehot'] \
.get_feature_names_out(categorical_cols))
feature_names_full = feature_names_num + feature_names_cat
plt.figure(figsize=(16,10))
plot_tree(dt_model,
feature_names=feature_names_full,
class_names=class_names,
filled=True,
max_depth=3, # limit for readability
fontsize=8)
plt.title('Decision Tree (First 3 Levels)')
plt.show()
--- Decision Tree Performance ---
Accuracy : 0.8378
Precision: 0.8383
Recall : 0.8378
F1-score : 0.8380
Classification Report:
precision recall f1-score support
High 0.81 0.81 0.81 2067
Low 0.80 0.81 0.81 2065
Medium 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
Decision Tree ROC-AUC (macro, OVR): 0.8729
In [29]:
# Decision Tree - Hyperparameter Tuning
dt_param_grid = {
'classifier__criterion': ['gini', 'entropy'],
'classifier__max_depth': [3, 5, 7, 10, None],
'classifier__min_samples_split': [2, 5, 10, 20],
'classifier__min_samples_leaf': [1, 2, 4, 10]
}
dt_grid = GridSearchCV(
estimator=dt_pipeline,
param_grid=dt_param_grid,
cv=5,
scoring='f1_weighted',
n_jobs=-1
)
dt_grid.fit(X_train, y_train)
print('\nBest Decision Tree params:')
print(dt_grid.best_params_)
best_dt = dt_grid.best_estimator_
best_dt_acc, best_dt_prec, best_dt_rec, best_dt_f1 = evaluate_model(best_dt, X_test, y_test, model_name='Decision Tree (Tuned)')
best_dt_auc = multiclass_roc_auc(best_dt, X_test, y_test, model_name='Decision Tree (Tuned)')
Best Decision Tree params:
{'classifier__criterion': 'entropy', 'classifier__max_depth': 10, 'classifier__min_samples_leaf': 10, 'classifier__min_samples_split': 2}
--- Decision Tree (Tuned) Performance ---
Accuracy : 0.9025
Precision: 0.9024
Recall : 0.9025
F1-score : 0.9023
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
Decision Tree (Tuned) ROC-AUC (macro, OVR): 0.9348
In [30]:
# Support Vector Machine (SVM) - Baseline
#Using RBF kernel SVC
svm_clf = SVC(kernel='rbf', probability=True, random_state=42)
svm_pipeline = Pipeline(steps=[
('preprocessor', preprocessor),
('classifier', svm_clf)
])
svm_pipeline.fit(X_train, y_train)
svm_acc, svm_prec, svm_rec, svm_f1 = evaluate_model(svm_pipeline, X_test, y_test, model_name='SVM (RBF)')
svm_auc = multiclass_roc_auc(svm_pipeline, X_test, y_test, model_name='SVM (RBF)')
--- SVM (RBF) Performance ---
Accuracy : 0.8943
Precision: 0.8951
Recall : 0.8943
F1-score : 0.8939
Classification Report:
precision recall f1-score support
High 0.91 0.85 0.88 2067
Low 0.90 0.85 0.87 2065
Medium 0.88 0.94 0.91 3875
accuracy 0.89 8007
macro avg 0.90 0.88 0.89 8007
weighted avg 0.90 0.89 0.89 8007
SVM (RBF) ROC-AUC (macro, OVR): 0.9415
In [31]:
# SVM - Hyperparameter Tuning
svm_param_grid = {
'classifier__C': [0.1, 1, 10, 100],
'classifier__gamma': ['scale', 'auto', 0.01, 0.001],
'classifier__kernel': ['rbf']
}
svm_grid = GridSearchCV(
estimator=svm_pipeline,
param_grid=svm_param_grid,
cv=5,
scoring='f1_weighted',
n_jobs=-1
)
svm_grid.fit(X_train, y_train)
print('\nBest SVM params:')
print(svm_grid.best_params_)
best_svm = svm_grid.best_estimator_
best_svm_acc, best_svm_prec, best_svm_rec, best_svm_f1 = evaluate_model(best_svm, X_test, y_test, model_name='SVM (Tuned)')
best_svm_auc = multiclass_roc_auc(best_svm, X_test, y_test, model_name='SVM (Tuned)')
Best SVM params:
{'classifier__C': 100, 'classifier__gamma': 0.01, 'classifier__kernel': 'rbf'}
--- SVM (Tuned) Performance ---
Accuracy : 0.8965
Precision: 0.8972
Recall : 0.8965
F1-score : 0.8960
Classification Report:
precision recall f1-score support
High 0.91 0.86 0.88 2067
Low 0.90 0.85 0.88 2065
Medium 0.88 0.94 0.91 3875
accuracy 0.90 8007
macro avg 0.90 0.88 0.89 8007
weighted avg 0.90 0.90 0.90 8007
SVM (Tuned) ROC-AUC (macro, OVR): 0.9420
In [32]:
# Model Comparison (Decision Tree vs SVM)
results = pd.DataFrame({
'Model': [
'Decision Tree (Base)',
'Decision Tree (Tuned)',
'SVM (Base)',
'SVM (Tuned)'
],
'Accuracy': [dt_acc, best_dt_acc, svm_acc, best_svm_acc],
'Precision (weighted)': [dt_prec, best_dt_prec, svm_prec, best_svm_prec],
'Recall (weighted)': [dt_rec, best_dt_rec, svm_rec, best_svm_rec],
'F1-score (weighted)': [dt_f1, best_dt_f1, svm_f1, best_svm_f1],
'ROC-AUC (macro)': [dt_auc, best_dt_auc, svm_auc, best_svm_auc]
})
print(results)
# Bar plots for quick visual comparison
metrics_to_plot = ['Accuracy', 'Precision (weighted)', 'Recall (weighted)', 'F1-score (weighted)', 'ROC-AUC (macro)']
for metric in metrics_to_plot:
plt.figure(figsize=(7,4))
sns.barplot(x='Model', y=metric, data=results)
plt.title(f'Model Comparison - {metric}')
plt.xticks(rotation=20)
plt.ylim(0, 1.0)
plt.tight_layout()
plt.show()
Model Accuracy Precision (weighted) Recall (weighted) \ 0 Decision Tree (Base) 0.837767 0.838252 0.837767 1 Decision Tree (Tuned) 0.902460 0.902450 0.902460 2 SVM (Base) 0.894342 0.895053 0.894342 3 SVM (Tuned) 0.896466 0.897222 0.896466 F1-score (weighted) ROC-AUC (macro) 0 0.837970 0.872931 1 0.902287 0.934781 2 0.893859 0.941509 3 0.895982 0.942030
In [33]:
# Unsupervised Learning - KMeans Clustering
# For clustering, we typically use only feature space (no labels).
# Reuse preprocessor to transform X into numeric array.
X_processed = preprocessor.fit_transform(X) # fit on full data for clustering
# Try different values of k
k_values = range(2, 11)
inertias = []
sil_scores = []
for k in k_values:
kmeans = KMeans(n_clusters=k, random_state=42, n_init='auto')
cluster_labels = kmeans.fit_predict(X_processed)
inertias.append(kmeans.inertia_)
sil = silhouette_score(X_processed, cluster_labels)
sil_scores.append(sil)
# Plot inertia (Elbow method)
plt.figure(figsize=(6,4))
plt.plot(k_values, inertias, marker='o')
plt.xlabel('Number of clusters (k)')
plt.ylabel('Inertia (Within-cluster SSE)')
plt.title('KMeans - Elbow Method')
plt.tight_layout()
plt.show()
# Plot silhouette scores
plt.figure(figsize=(6,4))
plt.plot(k_values, sil_scores, marker='o')
plt.xlabel('Number of clusters (k)')
plt.ylabel('Silhouette Score')
plt.title('KMeans - Silhouette Scores for Different k')
plt.tight_layout()
plt.show()
# Choose k with best silhouette score (you could also use elbow visually)
best_k = k_values[int(np.argmax(sil_scores))]
print(f'Best k by silhouette score: {best_k}')
kmeans_final = KMeans(n_clusters=best_k, random_state=42, n_init='auto')
clusters = kmeans_final.fit_predict(X_processed)
# Attach cluster labels back to dataframe
df_clusters = df.copy()
df_clusters['Cluster'] = clusters
df_clusters[['EngagementLevel', 'Cluster']].head()
File "C:\Users\mathe\anaconda3\Lib\site-packages\joblib\externals\loky\backend\context.py", line 257, in _count_physical_cores
cpu_info = subprocess.run(
"wmic CPU Get NumberOfCores /Format:csv".split(),
capture_output=True,
text=True,
)
File "C:\Users\mathe\anaconda3\Lib\subprocess.py", line 554, in run
with Popen(*popenargs, **kwargs) as process:
~~~~~^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\mathe\anaconda3\Lib\subprocess.py", line 1039, in __init__
self._execute_child(args, executable, preexec_fn, close_fds,
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pass_fds, cwd, env,
^^^^^^^^^^^^^^^^^^^
...<5 lines>...
gid, gids, uid, umask,
^^^^^^^^^^^^^^^^^^^^^^
start_new_session, process_group)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\mathe\anaconda3\Lib\subprocess.py", line 1554, in _execute_child
hp, ht, pid, tid = _winapi.CreateProcess(executable, args,
~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
# no special security
^^^^^^^^^^^^^^^^^^^^^
...<4 lines>...
cwd,
^^^^
startupinfo)
^^^^^^^^^^^^
Best k by silhouette score: 2
Out[33]:
| EngagementLevel | Cluster | |
|---|---|---|
| 0 | Medium | 1 |
| 1 | Medium | 0 |
| 2 | High | 1 |
| 3 | Medium | 1 |
| 4 | Medium | 1 |
In [34]:
# Cluster Profiling & Visualisation (PCA)
# Engagement level distribution per cluster
cluster_engagement = pd.crosstab(df_clusters['Cluster'], df_clusters['EngagementLevel'], normalize='index') * 100
print('\nPercentage of EngagementLevel per Cluster (%):')
print(cluster_engagement.round(2))
plt.figure(figsize=(8,5))
cluster_engagement.plot(kind='bar', stacked=True)
plt.ylabel('Percentage of Players')
plt.title('Engagement Level Distribution per Cluster')
plt.legend(title='EngagementLevel', bbox_to_anchor=(1.05, 1), loc='upper left')
plt.tight_layout()
plt.show()
# Numeric feature means per cluster
cluster_numeric_means = df_clusters.groupby('Cluster')[numeric_cols].mean()
print('\nMean of numeric features per cluster:')
print(cluster_numeric_means.round(2))
plt.figure(figsize=(10,6))
sns.heatmap(cluster_numeric_means.round(2), annot=True, cmap='viridis')
plt.title('Cluster Profiles - Mean Numeric Features')
plt.tight_layout()
plt.show()
# PCA for 2D visualisation of clusters
pca = PCA(n_components=2, random_state=42)
X_pca = pca.fit_transform(X_processed)
plt.figure(figsize=(7,6))
sns.scatterplot(x=X_pca[:,0], y=X_pca[:,1], hue=clusters, palette='tab10')
plt.title('KMeans Clusters (PCA 2D Projection)')
plt.xlabel('PC1')
plt.ylabel('PC2')
plt.legend(title='Cluster')
plt.tight_layout()
plt.show()
Percentage of EngagementLevel per Cluster (%): EngagementLevel High Low Medium Cluster 0 24.77 28.92 46.31 1 26.86 22.67 50.47
<Figure size 800x500 with 0 Axes>
Mean of numeric features per cluster:
PlayerID Age PlayTimeHours InGamePurchases SessionsPerWeek \
Cluster
0 28900.76 31.97 11.97 0.2 9.36
1 29131.95 32.01 12.08 0.2 9.58
AvgSessionDurationMinutes PlayerLevel AchievementsUnlocked
Cluster
0 95.20 49.41 12.02
1 94.38 49.90 37.01