In [1]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
# sklearn stuff for the models
from sklearn.model_selection import train_test_split
from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import StandardScaler
from sklearn.tree import DecisionTreeClassifier
from sklearn.tree import plot_tree
from sklearn.svm import SVC
from sklearn.metrics import accuracy_score
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score
from sklearn.metrics import f1_score
from sklearn.metrics import roc_auc_score
from sklearn.metrics import classification_report
from sklearn.metrics import ConfusionMatrixDisplay
from sklearn.metrics import roc_curve, auc
from sklearn.preprocessing import label_binarize
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
from sklearn.decomposition import PCA
print('imports done')
imports done
In [2]:
# loading the csv file
data = pd.read_csv('online_gaming_behavior_dataset.csv')
# checking it loaded properly
print('number of rows and columns:', data.shape)
data.head()
number of rows and columns: (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 |
In [3]:
# looking at what columns we have and what type they are
print(data.dtypes)
PlayerID int64 Age int64 Gender object Location object GameGenre object PlayTimeHours float64 InGamePurchases int64 GameDifficulty object SessionsPerWeek int64 AvgSessionDurationMinutes int64 PlayerLevel int64 AchievementsUnlocked int64 EngagementLevel object dtype: object
In [4]:
#Validating the PlayTimeHours due to the backtick in the data
print(data.PlayTimeHours)
0 16.271119
1 5.525961
2 8.223755
3 5.265351
4 15.531945
...
40029 20.619662
40030 13.539280
40031 0.240057
40032 14.017818
40033 10.083804
Name: PlayTimeHours, Length: 40034, dtype: float64
In [5]:
# looking at engagement level which is what we are trying to predict
print('counts of each engagement level:')
print(data['EngagementLevel'].value_counts())
print('')
print('as percentages:')
print(data['EngagementLevel'].value_counts(normalize=True).round(3) * 100)
counts of each engagement level: EngagementLevel Medium 19374 High 10336 Low 10324 Name: count, dtype: int64 as percentages: EngagementLevel Medium 48.4 High 25.8 Low 25.8 Name: proportion, dtype: float64
In [6]:
# plotting engagement level counts
engagement_counts = data['EngagementLevel'].value_counts()
plt.figure(figsize=(8, 5))
plt.bar(engagement_counts.index, engagement_counts.values, color=['green', 'orange', 'red'])
plt.title('How many players in each Engagement Level')
plt.xlabel('Engagement Level')
plt.ylabel('Number of Players')
plt.show()
In [7]:
# looking at how play time relates to engagement
plt.figure(figsize=(9, 5))
sns.boxplot(data=data, x='EngagementLevel', y='PlayTimeHours', order=['Low', 'Medium', 'High'])
plt.title('Play Time Hours by Engagement Level')
plt.xlabel('Engagement Level')
plt.ylabel('Play Time Hours')
plt.show()
In [8]:
# same but for sessions per week
plt.figure(figsize=(9, 5))
sns.boxplot(data=data, x='EngagementLevel', y='SessionsPerWeek', order=['Low', 'Medium', 'High'])
plt.title('Sessions Per Week by Engagement Level')
plt.xlabel('Engagement Level')
plt.ylabel('Sessions Per Week')
plt.show()
In [9]:
# checking player level vs engagement
plt.figure(figsize=(9, 5))
sns.boxplot(data=data, x='EngagementLevel', y='PlayerLevel', order=['Low', 'Medium', 'High'])
plt.title('Player Level by Engagement Level')
plt.xlabel('Engagement Level')
plt.ylabel('Player Level')
plt.show()
In [10]:
# achievements unlocked vs engagement
plt.figure(figsize=(9, 5))
sns.boxplot(data=data, x='EngagementLevel', y='Age', order=['Low', 'Medium', 'High'])
plt.title('Achievements Unlocked by Engagement Level')
plt.xlabel('Engagement Level')
plt.ylabel('Achievements Unlocked')
plt.show()
In [11]:
sns.boxplot(data=data, x='EngagementLevel', y='SessionsPerWeek', order=['Low', 'Medium', 'High'])
plt.title('sessions played by engagement level')
plt.show()
In [12]:
# correlation heatmap for the numeric columns only
numeric_cols = ['Age', 'PlayTimeHours', 'InGamePurchases', 'SessionsPerWeek',
'AvgSessionDurationMinutes', 'PlayerLevel', 'AchievementsUnlocked']
corr_matrix = data[numeric_cols].corr()
plt.figure(figsize=(10, 7))
sns.heatmap(corr_matrix, annot=True, fmt='.2f', cmap='coolwarm')
plt.title('Correlation Between Numeric Features')
plt.show()
In [13]:
# looking at game genre and engagement level together
genre_engagement = pd.crosstab(data['GameGenre'], data['EngagementLevel'])
print(genre_engagement)
genre_engagement.plot(kind='bar', figsize=(10, 5))
plt.title('Game Genre vs Engagement Level')
plt.xlabel('Game Genre')
plt.ylabel('Number of Players')
plt.xticks(rotation=30)
plt.legend(title='Engagement Level')
plt.tight_layout()
plt.show()
EngagementLevel High Low Medium GameGenre Action 2064 2059 3916 RPG 1988 2097 3867 Simulation 2104 2088 3791 Sports 2083 2074 3891 Strategy 2097 2006 3909
In [14]:
# making a copy so i dont mess up the original
df_model = data.copy()
# dont need player id for modelling
df_model = df_model.drop(columns=['PlayerID'])
print('columns after dropping PlayerID:')
print(df_model.columns.tolist())
columns after dropping PlayerID: ['Age', 'Gender', 'Location', 'GameGenre', 'PlayTimeHours', 'InGamePurchases', 'GameDifficulty', 'SessionsPerWeek', 'AvgSessionDurationMinutes', 'PlayerLevel', 'AchievementsUnlocked', 'EngagementLevel']
In [15]:
# need to convert text columns to numbers for the models
# using label encoder for this
le_gender = LabelEncoder()
df_model['Gender'] = le_gender.fit_transform(df_model['Gender'])
le_location = LabelEncoder()
df_model['Location'] = le_location.fit_transform(df_model['Location'])
le_genre = LabelEncoder()
df_model['GameGenre'] = le_genre.fit_transform(df_model['GameGenre'])
le_difficulty = LabelEncoder()
df_model['GameDifficulty'] = le_difficulty.fit_transform(df_model['GameDifficulty'])
print('categories encoded')
print(df_model.head(3))
categories encoded Age Gender Location GameGenre PlayTimeHours InGamePurchases \ 0 43 1 2 4 16.271119 0 1 29 0 3 4 5.525961 0 2 22 0 3 3 8.223755 0 GameDifficulty SessionsPerWeek AvgSessionDurationMinutes PlayerLevel \ 0 2 6 108 79 1 2 5 144 11 2 0 16 142 35 AchievementsUnlocked EngagementLevel 0 25 Medium 1 10 Medium 2 41 High
In [16]:
# encoding the target variable - engagement level
# low = 0, medium = 1, high = 2
engagement_map = {'Low': 0, 'Medium': 1, 'High': 2}
df_model['EngagementLevel'] = df_model['EngagementLevel'].map(engagement_map)
print('engagement level after encoding:')
print(df_model['EngagementLevel'].value_counts())
engagement level after encoding: EngagementLevel 1 19374 2 10336 0 10324 Name: count, dtype: int64
In [17]:
# splitting into features (X) and target (y)
X = df_model.drop(columns=['EngagementLevel'])
y = df_model['EngagementLevel']
print('X shape:', X.shape)
print('y shape:', y.shape)
X shape: (40034, 11) y shape: (40034,)
In [18]:
# splitting into training and testing sets
# using 70% for training and 30% for testing
# random state 42 so results are reproducible
# stratify means each split keeps same proportion of engagement levels
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.3,
random_state=42,
stratify=y
)
print('training set size:', X_train.shape[0])
print('testing set size:', X_test.shape[0])
print('')
print('testing split is roughly', round(X_test.shape[0] / len(X) * 100), '% of total data')
print('training split is roughly', round(X_train.shape[0] / len(X) * 100), '% of total data')
training set size: 28023 testing set size: 12011 testing split is roughly 30 % of total data training split is roughly 70 % of total data
In [19]:
# checking engagement level proportions are similar in train vs test
print('engagement level proportions in training set:')
print(y_train.value_counts(normalize=True).reindex([0, 1, 2]))
print('')
print('engagement level proportions in test set:')
print(y_test.value_counts(normalize=True).reindex([0, 1, 2]))
engagement level proportions in training set: EngagementLevel 0 0.257895 1 0.483924 2 0.258181 Name: proportion, dtype: float64 engagement level proportions in test set: EngagementLevel 0 0.257847 1 0.483973 2 0.258180 Name: proportion, dtype: float64
In [20]:
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
print('scaling done')
scaling done
In [21]:
# training a basic decision tree first
dt_model = DecisionTreeClassifier(random_state=42)
dt_model.fit(X_train, y_train)
# making predictions on the test set
y_pred_dt = dt_model.predict(X_test)
y_prob_dt = dt_model.predict_proba(X_test)
print('decision tree trained')
dt_accuracy = accuracy_score(y_test, y_pred_dt)
dt_precision = precision_score(y_test, y_pred_dt, average='weighted')
dt_recall = recall_score(y_test, y_pred_dt, average='weighted')
dt_f1 = f1_score(y_test, y_pred_dt, average='weighted')
dt_auc = roc_auc_score(y_test, y_prob_dt, multi_class='ovr', average='weighted')
decision tree trained
In [22]:
print('=== Decision Tree Results ===')
print('Accuracy: ', round(dt_accuracy, 4))
print('Precision:', round(dt_precision, 4))
print('Recall: ', round(dt_recall, 4))
print('F1 Score: ', round(dt_f1, 4))
print('AUC-ROC: ', round(dt_auc, 4))
=== Decision Tree Results === Accuracy: 0.8262 Precision: 0.8268 Recall: 0.8262 F1 Score: 0.8264 AUC-ROC: 0.8637
In [23]:
# visualising the top 3 levels of the tree
plt.figure(figsize=(20, 8))
plot_tree(
dt_model,
feature_names=X.columns,
class_names=['Low', 'Medium', 'High'],
filled=True,
max_depth=3,
fontsize=9
)
plt.title('Decision Tree Structure (showing first 3 levels only)')
plt.tight_layout()
plt.show()
In [24]:
# which features does the decision tree think are most important
feature_importances = pd.Series(dt_model.feature_importances_, index=X.columns)
feature_importances = feature_importances.sort_values(ascending=True)
feature_importances.plot(kind='barh', figsize=(9, 6), color='steelblue')
plt.title('Decision Tree - Feature Importances for Predicting Engagement Level')
plt.xlabel('Importance Score')
plt.tight_layout()
plt.show()
In [25]:
# confusion matrix for decision tree
fig, ax = plt.subplots(figsize=(7, 5))
ConfusionMatrixDisplay.from_predictions(
y_test, y_pred_dt,
display_labels=['Low', 'Medium', 'High'],
ax=ax,
cmap='Blues',
colorbar=False
)
ax.set_title('Decision Tree - Confusion Matrix')
plt.tight_layout()
plt.show()
In [26]:
# training the SVM - using scaled data for this one
# note: this can take a while on the full dataset
sample_size = len(X_train)
svm_model = SVC(kernel='linear', probability=True, random_state=42)
svm_model.fit(X_train_scaled[:sample_size], y_train.iloc[:sample_size])
y_pred_svm = svm_model.predict(X_test_scaled)
y_prob_svm = svm_model.predict_proba(X_test_scaled)
print('svm trained')
svm trained
In [27]:
# metrics for SVM
svm_accuracy = accuracy_score(y_test, y_pred_svm)
svm_precision = precision_score(y_test, y_pred_svm, average='weighted')
svm_recall = recall_score(y_test, y_pred_svm, average='weighted')
svm_f1 = f1_score(y_test, y_pred_svm, average='weighted')
svm_auc = roc_auc_score(y_test, y_prob_svm, multi_class='ovr', average='weighted')
print('=== SVM Results ===')
print('Accuracy: ', round(svm_accuracy, 4))
print('Precision:', round(svm_precision, 4))
print('Recall: ', round(svm_recall, 4))
print('F1 Score: ', round(svm_f1, 4))
print('AUC-ROC: ', round(svm_auc, 4))
=== SVM Results === Accuracy: 0.8249 Precision: 0.8254 Recall: 0.8249 F1 Score: 0.8239 AUC-ROC: 0.9113
In [28]:
print(classification_report(y_test, y_pred_svm, target_names=['Low', 'Medium', 'High']))
precision recall f1-score support
Low 0.79 0.71 0.75 3097
Medium 0.81 0.87 0.84 5813
High 0.89 0.84 0.87 3101
accuracy 0.82 12011
macro avg 0.83 0.81 0.82 12011
weighted avg 0.83 0.82 0.82 12011
In [29]:
# confusion matrix for svm
fig, ax = plt.subplots(figsize=(7, 5))
ConfusionMatrixDisplay.from_predictions(
y_test, y_pred_svm,
display_labels=['Low', 'Medium', 'High'],
ax=ax,
cmap='Purples',
colorbar=False
)
ax.set_title('SVM - Confusion Matrix')
plt.tight_layout()
plt.show()
In [30]:
# putting the results side by side
results_table = pd.DataFrame({
'Model': ['Decision Tree', 'SVM'],
'Accuracy': [round(dt_accuracy, 4), round(svm_accuracy, 4)],
'Precision': [round(dt_precision, 4), round(svm_precision, 4)],
'Recall': [round(dt_recall, 4), round(svm_recall, 4)],
'F1 Score': [round(dt_f1, 4), round(svm_f1, 4)],
'AUC-ROC': [round(dt_auc, 4), round(svm_auc, 4)]
})
results_table = results_table.set_index('Model')
print(results_table)
Accuracy Precision Recall F1 Score AUC-ROC Model Decision Tree 0.8262 0.8268 0.8262 0.8264 0.8637 SVM 0.8249 0.8254 0.8249 0.8239 0.9113
In [31]:
# bar chart comparing the two models
results_table.plot(kind='bar', figsize=(12, 5), rot=0)
plt.title('Decision Tree vs SVM - Performance on Predicting Engagement Level')
plt.ylabel('Score')
plt.ylim(0, 1.1)
plt.legend(loc='lower right')
plt.tight_layout()
plt.show()
In [32]:
# ROC curves for both models - doing one vs rest for each engagement class
y_test_binarized = label_binarize(y_test, classes=[0, 1, 2])
class_labels = ['Low', 'Medium', 'High']
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
for ax, model_name, y_prob in zip(axes, ['Decision Tree', 'SVM'], [y_prob_dt, y_prob_svm]):
for i in range(3):
fpr, tpr, thresholds = roc_curve(y_test_binarized[:, i], y_prob[:, i])
roc_auc_val = auc(fpr, tpr)
ax.plot(fpr, tpr, label=class_labels[i] + ' AUC = ' + str(round(roc_auc_val, 3)))
ax.plot([0, 1], [0, 1], 'k--')
ax.set_xlabel('False Positive Rate')
ax.set_ylabel('True Positive Rate')
ax.set_title(model_name + ' - ROC Curves by Engagement Level')
ax.legend(loc='lower right')
plt.tight_layout()
plt.show()
In [33]:
# tuning the decision tree using grid search
# trying different values for max depth and other settings
dt_param_grid = {
'max_depth': [3, 5, 10, 20, None],
'min_samples_split': [2, 5, 10],
'min_samples_leaf': [1, 2, 5],
'criterion': ['gini', 'entropy']
}
# cv=5 means 5-fold cross validation
dt_grid_search = GridSearchCV(
DecisionTreeClassifier(random_state=42),
dt_param_grid,
cv=5,
scoring='f1_weighted',
n_jobs=-1
)
dt_grid_search.fit(X_train, y_train)
print('best parameters found:')
print(dt_grid_search.best_params_)
print('')
print('best cross validation f1 score:', round(dt_grid_search.best_score_, 4))
best parameters found:
{'criterion': 'entropy', 'max_depth': 10, 'min_samples_leaf': 5, 'min_samples_split': 2}
best cross validation f1 score: 0.9007
In [34]:
# evaluating the tuned decision tree
dt_tuned = dt_grid_search.best_estimator_
y_pred_dt_tuned = dt_tuned.predict(X_test)
y_prob_dt_tuned = dt_tuned.predict_proba(X_test)
dt_tuned_accuracy = accuracy_score(y_test, y_pred_dt_tuned)
dt_tuned_f1 = f1_score(y_test, y_pred_dt_tuned, average='weighted')
dt_tuned_auc = roc_auc_score(y_test, y_prob_dt_tuned, multi_class='ovr', average='weighted')
print('=== Tuned Decision Tree ===')
print(classification_report(y_test, y_pred_dt_tuned, target_names=['Low', 'Medium', 'High']))
=== Tuned Decision Tree ===
precision recall f1-score support
Low 0.90 0.87 0.88 3097
Medium 0.91 0.94 0.92 5813
High 0.92 0.88 0.90 3101
accuracy 0.91 12011
macro avg 0.91 0.90 0.90 12011
weighted avg 0.91 0.91 0.91 12011
In [35]:
# tuning the SVM now
svm_param_grid = {
'C': [0.1, 1, 10],
'kernel': ['rbf', 'linear'],
'gamma': ['scale', 'auto']
}
svm_grid_search = GridSearchCV(
SVC(probability=True, random_state=42),
svm_param_grid,
cv=3,
scoring='f1_weighted',
n_jobs=-1
)
# using subset again to speed up
svm_grid_search.fit(X_train_scaled[:], y_train.iloc[:])
print('best svm parameters:')
print(svm_grid_search.best_params_)
print('')
print('best cv score:', round(svm_grid_search.best_score_, 4))
best svm parameters:
{'C': 1, 'gamma': 'auto', 'kernel': 'rbf'}
best cv score: 0.8915
In [36]:
# evaluating tuned svm
svm_tuned = svm_grid_search.best_estimator_
y_pred_svm_tuned = svm_tuned.predict(X_test_scaled)
y_prob_svm_tuned = svm_tuned.predict_proba(X_test_scaled)
svm_tuned_accuracy = accuracy_score(y_test, y_pred_svm_tuned)
svm_tuned_f1 = f1_score(y_test, y_pred_svm_tuned, average='weighted')
svm_tuned_auc = roc_auc_score(y_test, y_prob_svm_tuned, multi_class='ovr', average='weighted')
print('=== Tuned SVM ===')
print(classification_report(y_test, y_pred_svm_tuned, target_names=['Low', 'Medium', 'High']))
=== Tuned SVM ===
precision recall f1-score support
Low 0.90 0.84 0.87 3097
Medium 0.88 0.94 0.91 5813
High 0.92 0.86 0.89 3101
accuracy 0.90 12011
macro avg 0.90 0.88 0.89 12011
weighted avg 0.90 0.90 0.90 12011
In [37]:
# comparing all 4 models together
all_models = pd.DataFrame({
'Model': ['DT Default', 'DT Tuned', 'SVM Default', 'SVM Tuned'],
'Accuracy': [
round(dt_accuracy, 4),
round(dt_tuned_accuracy, 4),
round(svm_accuracy, 4),
round(svm_tuned_accuracy, 4)
],
'F1 Score': [
round(dt_f1, 4),
round(dt_tuned_f1, 4),
round(svm_f1, 4),
round(svm_tuned_f1, 4)
],
'AUC-ROC': [
round(dt_auc, 4),
round(dt_tuned_auc, 4),
round(svm_auc, 4),
round(svm_tuned_auc, 4)
]
})
all_models = all_models.set_index('Model')
print('=== All Models Comparison ===')
print(all_models)
all_models.plot(kind='bar', figsize=(12, 5), rot=20)
plt.title('All Models - Before and After Tuning (Engagement Level Prediction)')
plt.ylabel('Score')
plt.ylim(0, 1.1)
plt.tight_layout()
plt.show()
=== All Models Comparison ===
Accuracy F1 Score AUC-ROC
Model
DT Default 0.8262 0.8264 0.8637
DT Tuned 0.9059 0.9057 0.9333
SVM Default 0.8249 0.8239 0.9113
SVM Tuned 0.8962 0.8956 0.9441
In [38]:
# choosing features that make sense for grouping players
# focusing on behaviour related to engagement
cluster_cols = [
'PlayTimeHours',
'SessionsPerWeek',
'AvgSessionDurationMinutes',
'PlayerLevel',
'AchievementsUnlocked',
'InGamePurchases'
]
X_for_clustering = data[cluster_cols].copy()
# scaling needed before clustering
scaler2 = StandardScaler()
X_cluster_scaled = scaler2.fit_transform(X_for_clustering)
print('clustering data prepared, shape:', X_for_clustering.shape)
clustering data prepared, shape: (40034, 6)
In [39]:
# using elbow method to figure out best number of clusters
inertia_values = []
silhouette_values = []
k_values = [2, 3, 4, 5, 6, 7, 8, 9, 10]
for k in k_values:
kmeans_temp = KMeans(n_clusters=k, random_state=42, n_init=10)
cluster_labels_temp = kmeans_temp.fit_predict(X_cluster_scaled)
inertia_values.append(kmeans_temp.inertia_)
silhouette_values.append(silhouette_score(X_cluster_scaled, cluster_labels_temp))
print('done testing different k values')
done testing different k values
In [40]:
# plotting elbow and silhouette charts
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
axes[0].plot(k_values, inertia_values, 'bo-')
axes[0].set_title('Elbow Method - Inertia vs Number of Clusters')
axes[0].set_xlabel('Number of Clusters (k)')
axes[0].set_ylabel('Inertia')
axes[1].plot(k_values, silhouette_values, 'rs-')
axes[1].set_title('Silhouette Score vs Number of Clusters')
axes[1].set_xlabel('Number of Clusters (k)')
axes[1].set_ylabel('Silhouette Score')
plt.tight_layout()
plt.show()
# picking k with best silhouette score
best_k = k_values[silhouette_values.index(max(silhouette_values))]
print('best k based on silhouette score:', best_k)
print('silhouette score at best k:', round(max(silhouette_values), 4))
best k based on silhouette score: 2 silhouette score at best k: 0.2366
In [41]:
# fitting final kmeans with the best k
kmeans_final = KMeans(n_clusters=best_k, random_state=42, n_init=10)
cluster_labels = kmeans_final.fit_predict(X_cluster_scaled)
# adding cluster labels back to original data
data_with_clusters = data.copy()
data_with_clusters['Cluster'] = cluster_labels
final_silhouette = silhouette_score(X_cluster_scaled, cluster_labels)
final_inertia = kmeans_final.inertia_
print('final kmeans results:')
print('number of clusters:', best_k)
print('silhouette score:', round(final_silhouette, 4))
print('inertia:', round(final_inertia, 2))
print('')
print('players in each cluster:')
print(data_with_clusters['Cluster'].value_counts().sort_index())
final kmeans results: number of clusters: 2 silhouette score: 0.2366 inertia: 200165.39 players in each cluster: Cluster 0 8041 1 31993 Name: count, dtype: int64
In [42]:
cluster_summary = data_with_clusters.groupby('Cluster')[cluster_cols].mean().round(2)
# heatmap of cluster profiles
plt.figure(figsize=(10, 5))
sns.heatmap(
cluster_summary.T,
annot=True,
fmt='.1f',
cmap='BuGn',
linewidths=0.5
)
plt.title('Cluster Profiles - Average Feature Values per Cluster')
plt.ylabel('Feature')
plt.xlabel('Cluster')
plt.tight_layout()
plt.show()
In [43]:
# using PCA to show clusters in 2D since we have 6 features
pca_model = PCA(n_components=2, random_state=42)
X_pca_result = pca_model.fit_transform(X_cluster_scaled)
plt.figure(figsize=(9, 6))
scatter = plt.scatter(
X_pca_result[:, 0],
X_pca_result[:, 1],
c=cluster_labels,
cmap='Set1',
alpha=0.9,
s=5
)
plt.colorbar(scatter, label='Cluster')
plt.title('Player Clusters Visualised with PCA (2 components)')
plt.xlabel('Principal Component 1 (' + str(round(pca_model.explained_variance_ratio_[0]*100, 1)) + '% variance)')
plt.ylabel('Principal Component 2 (' + str(round(pca_model.explained_variance_ratio_[1]*100, 1)) + '% variance)')
plt.tight_layout()
plt.show()
In [44]:
# seeing how engagement level maps to each cluster
cluster_engagement = pd.crosstab(
data_with_clusters['Cluster'],
data_with_clusters['EngagementLevel'],
normalize='index'
).round(3)
print('proportion of each engagement level per cluster:')
print(cluster_engagement)
cluster_engagement.plot(kind='bar', stacked=True, figsize=(10, 5), rot=0)
plt.title('Engagement Level Breakdown per Cluster')
plt.xlabel('Cluster')
plt.ylabel('Proportion of Players')
plt.legend(title='Engagement Level')
plt.tight_layout()
plt.show()
proportion of each engagement level per cluster: EngagementLevel High Low Medium Cluster 0 0.265 0.253 0.481 1 0.256 0.259 0.485
In [45]:
print('Train/Test Split: 70% training, 30% testing')
print('Training samples:', X_train.shape[0])
print('Testing samples: ', X_test.shape[0])
print('')
print('--- Classification Results (Predicting Engagement Level) ---')
print(all_models)
print('')
print('--- Clustering Results ---')
print('Number of clusters (k):', best_k)
print('Silhouette Score: ', round(final_silhouette, 4))
print('Inertia: ', round(final_inertia, 2))
Train/Test Split: 70% training, 30% testing
Training samples: 28023
Testing samples: 12011
--- Classification Results (Predicting Engagement Level) ---
Accuracy F1 Score AUC-ROC
Model
DT Default 0.8262 0.8264 0.8637
DT Tuned 0.9059 0.9057 0.9333
SVM Default 0.8249 0.8239 0.9113
SVM Tuned 0.8962 0.8956 0.9441
--- Clustering Results ---
Number of clusters (k): 2
Silhouette Score: 0.2366
Inertia: 200165.39