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()
No description has been provided for this image
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()
No description has been provided for this image
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()
No description has been provided for this image
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()
No description has been provided for this image
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()
No description has been provided for this image
In [11]:
sns.boxplot(data=data, x='EngagementLevel', y='SessionsPerWeek', order=['Low', 'Medium', 'High'])
plt.title('sessions played by engagement level')
plt.show()
No description has been provided for this image
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()
No description has been provided for this image
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
No description has been provided for this image
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()
No description has been provided for this image
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()
No description has been provided for this image
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()
No description has been provided for this image
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()
No description has been provided for this image
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()
No description has been provided for this image
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()
No description has been provided for this image
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
No description has been provided for this image
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))
No description has been provided for this image
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()
No description has been provided for this image
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()
No description has been provided for this image
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
No description has been provided for this image
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