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
No description has been provided for this image
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  
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
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

No description has been provided for this image
Decision Tree ROC-AUC (macro, OVR): 0.8729
No description has been provided for this image
No description has been provided for this image
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

No description has been provided for this image
Decision Tree (Tuned) ROC-AUC (macro, OVR): 0.9348
No description has been provided for this image
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

No description has been provided for this image
SVM (RBF) ROC-AUC (macro, OVR): 0.9415
No description has been provided for this image
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

No description has been provided for this image
SVM (Tuned) ROC-AUC (macro, OVR): 0.9420
No description has been provided for this image
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  
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
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)
                             ^^^^^^^^^^^^
No description has been provided for this image
No description has been provided for this image
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>
No description has been provided for this image
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  
No description has been provided for this image
No description has been provided for this image