In [1]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# Preprocessing and Selection
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE

# Models
from sklearn.cluster import KMeans, AgglomerativeClustering
from sklearn.tree import DecisionTreeClassifier
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier

# Metrics
from sklearn.metrics import (classification_report, confusion_matrix, 
                             accuracy_score, precision_recall_fscore_support, 
                             silhouette_score, roc_auc_score)
from sklearn.preprocessing import label_binarize
from scipy.cluster.hierarchy import dendrogram, linkage
In [14]:
# ==========================================
# 1. DATA LOADING AND EDA
# ==========================================
def load_and_inspect_data(filepath):
    df = pd.read_csv(filepath)
    print(f"---------------- Dataset Shape: ---------------- \n{df.shape}")
    print(f"\n---------------- Dataset Info: ----------------")
    df.info()
    print(f"\n---------------- Dataset Head: ---------------- \n{df.head()}")
    return df

def perform_exploratory_analysis(df):
    print("\n---------------- Exploratory Data Analysis (EDA) ----------------")
    numeric_df = df.select_dtypes(include=[np.number])
    if 'PlayerID' in numeric_df.columns:
        numeric_df = numeric_df.drop(columns=['PlayerID'])
    
    # 1. Descriptive Stats
    stats = numeric_df.describe().transpose()
    print("---------------- Statistical Summary of Numeric Features: ----------------")
    print(stats)
    
    # 2. Correlation Matrix (New Step for Report)
    print("\n---------------- Correlation Heatmap ----------------")
    plt.figure(figsize=(10, 8))
    correlation_matrix = numeric_df.corr()
    sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', fmt=".2f")
    plt.title('Feature Correlation Matrix')
    plt.show()
    
    # Check correlation of PlayTimeHours
    if 'PlayTimeHours' in correlation_matrix.columns:
        print("\n---------------- Correlation of PlayTimeHours to other features: ----------------")
        print(correlation_matrix['PlayTimeHours'].sort_values(ascending=False))
        
    return stats

df = load_and_inspect_data("online_gaming_behavior_dataset.csv")
perform_exploratory_analysis(df)
---------------- Dataset Shape: ---------------- 
(40034, 13)

---------------- 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

---------------- Dataset Head: ---------------- 
   PlayerID  Age  Gender Location GameGenre  PlayTimeHours  InGamePurchases  \
0      9000   43    Male    Other  Strategy      16.271119                0   
1      9001   29  Female      USA  Strategy       5.525961                0   
2      9002   22  Female      USA    Sports       8.223755                0   
3      9003   35    Male      USA    Action       5.265351                1   
4      9004   33    Male   Europe    Action      15.531945                0   

  GameDifficulty  SessionsPerWeek  AvgSessionDurationMinutes  PlayerLevel  \
0         Medium                6                        108           79   
1         Medium                5                        144           11   
2           Easy               16                        142           35   
3           Easy                9                         85           57   
4         Medium                2                        131           95   

   AchievementsUnlocked EngagementLevel  
0                    25          Medium  
1                    10          Medium  
2                    41            High  
3                    47          Medium  
4                    37          Medium  

---------------- Exploratory Data Analysis (EDA) ----------------
---------------- Statistical Summary of Numeric Features: ----------------
                             count       mean        std        min  \
Age                        40034.0  31.992531  10.043227  15.000000   
PlayTimeHours              40034.0  12.024365   6.914638   0.000115   
InGamePurchases            40034.0   0.200854   0.400644   0.000000   
SessionsPerWeek            40034.0   9.471774   5.763667   0.000000   
AvgSessionDurationMinutes  40034.0  94.792252  49.011375  10.000000   
PlayerLevel                40034.0  49.655568  28.588379   1.000000   
AchievementsUnlocked       40034.0  24.526477  14.430726   0.000000   

                                 25%        50%         75%         max  
Age                        23.000000  32.000000   41.000000   49.000000  
PlayTimeHours               6.067501  12.008002   17.963831   23.999592  
InGamePurchases             0.000000   0.000000    0.000000    1.000000  
SessionsPerWeek             4.000000   9.000000   14.000000   19.000000  
AvgSessionDurationMinutes  52.000000  95.000000  137.000000  179.000000  
PlayerLevel                25.000000  49.000000   74.000000   99.000000  
AchievementsUnlocked       12.000000  25.000000   37.000000   49.000000  

---------------- Correlation Heatmap ----------------
No description has been provided for this image
---------------- Correlation of PlayTimeHours to other features: ----------------
PlayTimeHours                1.000000
AchievementsUnlocked         0.003913
Age                          0.002462
AvgSessionDurationMinutes   -0.001925
SessionsPerWeek             -0.003655
PlayerLevel                 -0.005152
InGamePurchases             -0.006067
Name: PlayTimeHours, dtype: float64
Out[14]:
count mean std min 25% 50% 75% max
Age 40034.0 31.992531 10.043227 15.000000 23.000000 32.000000 41.000000 49.000000
PlayTimeHours 40034.0 12.024365 6.914638 0.000115 6.067501 12.008002 17.963831 23.999592
InGamePurchases 40034.0 0.200854 0.400644 0.000000 0.000000 0.000000 0.000000 1.000000
SessionsPerWeek 40034.0 9.471774 5.763667 0.000000 4.000000 9.000000 14.000000 19.000000
AvgSessionDurationMinutes 40034.0 94.792252 49.011375 10.000000 52.000000 95.000000 137.000000 179.000000
PlayerLevel 40034.0 49.655568 28.588379 1.000000 25.000000 49.000000 74.000000 99.000000
AchievementsUnlocked 40034.0 24.526477 14.430726 0.000000 12.000000 25.000000 37.000000 49.000000
In [3]:
# ==========================================
# 2. PREPROCESSING
# ==========================================
def preprocess_data(df):
    print("\n---------------- Preprocessing ----------------")
    
    # Drop ID columns
    cols_to_drop = ['PlayerID'] if 'PlayerID' in df.columns else []
    
    # ---------------- FEATURE SELECTION DECISION ----------------
    # Based on t-SNE observation PlayTimeHours causes noise/blurring in clusters. 
    # It's likely redundant with SessionsPerWeek & AvgSessionDuration.
    # Dropped intentionally here to improve cluster seperation
    if 'PlayTimeHours' in df.columns:
        print(" -> Dropping 'PlayTimeHours' to reduce multicollinearity and improve cluster separation.")
        cols_to_drop.append('PlayTimeHours')
        
    if cols_to_drop:
        print(f"Dropping column(s): {cols_to_drop}")
        df = df.drop(cols_to_drop, axis=1)

    # Outlier Removal (SMART IQR Method)
    print("---------------- Extreme Outliers Checking (3.0x IQR Method) ----------------")
    numeric_cols = df.select_dtypes(include=[np.number]).columns
    
    Q1 = df[numeric_cols].quantile(0.25)
    Q3 = df[numeric_cols].quantile(0.75)
    IQR = Q3 - Q1
    
    cols_to_check = IQR[IQR > 0].index.tolist()
    
    if cols_to_check:
        print(f" -> Calculating outliers on: {cols_to_check}")
        condition = ~((df[cols_to_check] < (Q1[cols_to_check] - 3 * IQR[cols_to_check])) | 
                      (df[cols_to_check] > (Q3[cols_to_check] + 3 * IQR[cols_to_check]))).any(axis=1)
        
        df_clean = df[condition].copy()
        print(f" -> Removed {len(df) - len(df_clean)} extreme outlier rows.")
    else:
        print(" -> No columns suitable for outlier removal.")
        df_clean = df.copy()

    # Separating Target Feature and other Features
    target_col = 'EngagementLevel'
    y = df_clean[target_col].copy()
    X_raw = df_clean.drop(target_col, axis=1)

    # Mapping Categorical Features
    # Order matters for difficulty mapping, so manually mapped
    difficulty_map = {'Easy': 0, 'Medium': 1, 'Hard': 2}
    if 'GameDifficulty' in X_raw.columns:
         X_raw['GameDifficulty'] = X_raw['GameDifficulty'].map(difficulty_map)

    # Mapping with One-Hot Encoding for Gender, Location, Genre
    categorical_cols = ['Gender', 'Location', 'GameGenre']
    existing_cat_cols = [col for col in categorical_cols if col in X_raw.columns]
    
    if existing_cat_cols:
        print(f" -> One-Hot Encoding columns: {existing_cat_cols}")
        X_encoded = pd.get_dummies(X_raw, columns=existing_cat_cols, drop_first=True)
    else:
        X_encoded = pd.get_dummies(X_raw, drop_first=True)

    # Scaling
    scaler = StandardScaler()
    X_scaled = scaler.fit_transform(X_encoded)
    X_scaled_df = pd.DataFrame(X_scaled, columns=X_encoded.columns)
    
    # Encode Target
    le = LabelEncoder()
    y_encoded = le.fit_transform(y)
    
    return X_scaled_df, y_encoded, le, X_raw, X_encoded.columns

X_scaled, y_encoded, label_encoder, X_raw, feature_names = preprocess_data(df)
---------------- Preprocessing ----------------
 -> Dropping 'PlayTimeHours' to reduce multicollinearity and improve cluster separation.
Dropping column(s): ['PlayerID', 'PlayTimeHours']
---------------- Extreme Outliers Checking (3.0x IQR Method) ----------------
 -> Calculating outliers on: ['Age', 'SessionsPerWeek', 'AvgSessionDurationMinutes', 'PlayerLevel', 'AchievementsUnlocked']
 -> Removed 0 extreme outlier rows.
 -> One-Hot Encoding columns: ['Gender', 'Location', 'GameGenre']
In [15]:
# ==========================================
# 3. HELPER: VISUALIZE PROFILES
# ==========================================
def visualize_cluster_interpretation(X_analysis, clusters):
    # Generates visual profiles for the clusters
    print("\n---------------- Cluster Interpretation Plots ----------------")
    # Work on a copy to avoid SettingWithCopy warnings on the main df
    X_vis = X_analysis.copy()
    X_vis['Cluster'] = clusters
    
    # ---------------- NUMERIC HEATMAP ----------------
    numeric_cols = X_vis.select_dtypes(include=[np.number]).columns.tolist()
    if 'Cluster' in numeric_cols: numeric_cols.remove('Cluster')
    
    # Calculate means
    cluster_means = X_vis.groupby('Cluster')[numeric_cols].mean()
    
    # Standardize row-wise (Z-score) just for the heatmap so scales match
    scaler = StandardScaler()
    cluster_means_scaled = pd.DataFrame(
        scaler.fit_transform(cluster_means),
        index=cluster_means.index,
        columns=cluster_means.columns
    )
    
    plt.figure(figsize=(12, 6))
    sns.heatmap(cluster_means_scaled.transpose(), annot=True, cmap='RdBu', center=0)
    plt.title('Cluster Profiles (Numeric Features) | Blue = Higher than Avg, Red = Lower than Avg')
    plt.xlabel('Cluster')
    plt.tight_layout()
    plt.show()
    
    # ---------------- CATEGORICAL DISTRIBUTIONS ----------------
    categorical_cols = X_vis.select_dtypes(include=['object', 'category']).columns
    
    for col in categorical_cols:
        plt.figure(figsize=(10, 5))
        sns.countplot(data=X_vis, x='Cluster', hue=col, palette='viridis')
        plt.title(f'Cluster Distribution by {col}')
        plt.legend(title=col, bbox_to_anchor=(1.05, 1), loc='upper left')
        plt.tight_layout()
        plt.show()

# ==========================================
# 4. UNSUPERVISED LEARNING: K-MEANS
# ==========================================
def run_kmeans_analysis(X_scaled_df, X_original_df):
    print("\n---------------- Unsupervised Learning: K-Means Clustering ----------------")
    
    wcss = [] 
    silhouette_scores = []
    k_range = range(2, 8) 
    
    print("Calculating optimal K...")
    for k in k_range:
        kmeans = KMeans(n_clusters=k, init='k-means++', random_state=42)
        kmeans.fit(X_scaled_df)
        wcss.append(kmeans.inertia_)
        score = silhouette_score(X_scaled_df, kmeans.labels_, sample_size=3000, random_state=42)
        silhouette_scores.append(score)

    fig, ax1 = plt.subplots(figsize=(10, 5))
    ax1.set_xlabel('Number of Clusters (k)')
    ax1.set_ylabel('Inertia (WCSS)', color='tab:blue')
    ax1.plot(k_range, wcss, marker='o', color='tab:blue')
    
    ax2 = ax1.twinx() 
    ax2.set_ylabel('Silhouette Score', color='tab:red')
    ax2.plot(k_range, silhouette_scores, marker='s', color='tab:red')
    plt.title('K-Means Performance: Elbow Method & Silhouette Score')
    plt.show()

    best_k = k_range[np.argmax(silhouette_scores)]
    print(f"Optimal K determined: {best_k}")
    
    kmeans_final = KMeans(n_clusters=best_k, init='k-means++', random_state=42)
    clusters = kmeans_final.fit_predict(X_scaled_df)
    
    print("\n---------------- K-Means Cluster Interpretation ----------------")
    X_analysis = X_original_df.copy()
    X_analysis['Cluster'] = clusters
    
    numeric_cols = X_analysis.select_dtypes(include=[np.number]).columns
    feat_cols = [c for c in numeric_cols if c != 'Cluster']
    print(X_analysis.groupby('Cluster')[feat_cols].mean().transpose())

    # --- CALL VISUALIZATION FUNCTION ---
    visualize_cluster_interpretation(X_original_df, clusters)

    print("\n---------------- t-SNE Visualization for K-Means ----------------")
    sample_size = min(3000, len(X_scaled_df))
    indices = np.random.choice(len(X_scaled_df), sample_size, replace=False)
    
    tsne = TSNE(n_components=2, perplexity=40, random_state=42)
    tsne_results = tsne.fit_transform(X_scaled_df.iloc[indices])
    
    plt.figure(figsize=(10, 6))
    sns.scatterplot(x=tsne_results[:,0], y=tsne_results[:,1], hue=clusters[indices], palette='viridis', s=60)
    plt.title(f'K-Means t-SNE Visualization (k={best_k})')
    plt.show()
    
    return best_k

# ==========================================
# 5. HIERARCHICAL
# ==========================================
def run_hierarchical_analysis(X_scaled_df, n_clusters, X_original_df):
    print("\n---------------- Unsupervised Learning: Hierarchical (Agglomerative) Clustering ----------------")
    
    sample_size = min(3000, len(X_scaled_df))
    print(f"Sampling {sample_size} rows for Dendrogram and Clustering...")
    
    X_sample_scaled = X_scaled_df.sample(n=sample_size, random_state=42)
    X_sample_original = X_original_df.loc[X_sample_scaled.index]
    
    print("Generating Dendrogram...")
    plt.figure(figsize=(12, 6))
    plt.title("Hierarchical Clustering Dendrogram")
    plt.xlabel("Sample Index")
    plt.ylabel("Distance (Ward's Method)")
    
    Z = linkage(X_sample_scaled, method='ward')
    dendrogram(Z, leaf_rotation=90., leaf_font_size=8., truncate_mode='lastp', p=30)
    plt.axhline(y=30, color='r', linestyle='--')
    plt.show()
    
    print(f"---------------- Agglomerative Clustering with k={n_clusters} (from K-Means) ----------------")
    hc = AgglomerativeClustering(n_clusters=n_clusters, metric='euclidean', linkage='ward')
    clusters_hc = hc.fit_predict(X_sample_scaled)
    
    score = silhouette_score(X_sample_scaled, clusters_hc)
    print(f"Hierarchical Clustering Silhouette Score: {score:.4f}")

    print("\n---------------- Hierarchial Cluster Interpretation ----------------")
    X_analysis = X_sample_original.copy()
    X_analysis['Cluster'] = clusters_hc
    
    numeric_cols = X_analysis.select_dtypes(include=[np.number]).columns
    feat_cols = [c for c in numeric_cols if c != 'Cluster']
    print(X_analysis.groupby('Cluster')[feat_cols].mean().transpose())

    # --- CALL VISUALIZATION FUNCTION ---
    visualize_cluster_interpretation(X_sample_original, clusters_hc)
    
    print("---------------- t-SNE Visualization for Hierarchical Clustering ----------------")
    tsne = TSNE(n_components=2, perplexity=40, random_state=42)
    tsne_results = tsne.fit_transform(X_sample_scaled)
    
    plt.figure(figsize=(10, 6))
    sns.scatterplot(x=tsne_results[:,0], y=tsne_results[:,1], hue=clusters_hc, palette='cool', s=60)
    plt.title(f'Hierarchical Clustering Visualization (k={n_clusters})')
    plt.show()
    
optimal_k = run_kmeans_analysis(X_scaled, X_raw)
        
run_hierarchical_analysis(X_scaled, optimal_k, X_raw)
---------------- Unsupervised Learning: K-Means Clustering ----------------
Calculating optimal K...
No description has been provided for this image
Optimal K determined: 6

---------------- K-Means Cluster Interpretation ----------------
Cluster                            0          1          2          3  \
Age                        31.962389  32.102681  31.976197  31.703499   
InGamePurchases             0.193139   0.197806   0.208552   0.202185   
GameDifficulty              0.696061   0.688956   0.697481   0.710275   
SessionsPerWeek             9.442440   9.509885   9.503737   9.443092   
AvgSessionDurationMinutes  94.943837  95.332367  95.688486  94.345457   
PlayerLevel                50.181957  49.812856  49.901744  49.584566   
AchievementsUnlocked       24.154511  24.513301  24.639773  24.747338   

Cluster                            4          5  
Age                        32.002712  32.161380  
InGamePurchases             0.200292   0.200473  
GameDifficulty              0.715210   0.699499  
SessionsPerWeek             9.416858   9.469950  
AvgSessionDurationMinutes  93.137284  94.635364  
PlayerLevel                49.539745  49.057179  
AchievementsUnlocked       24.444398  24.466472  

---------------- Cluster Interpretation Plots ----------------
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
---------------- t-SNE Visualization for K-Means ----------------
No description has been provided for this image
---------------- Unsupervised Learning: Hierarchical (Agglomerative) Clustering ----------------
Sampling 3000 rows for Dendrogram and Clustering...
Generating Dendrogram...
No description has been provided for this image
---------------- Agglomerative Clustering with k=6 (from K-Means) ----------------
Hierarchical Clustering Silhouette Score: 0.1373

---------------- Hierarchial Cluster Interpretation ----------------
Cluster                            0          1          2          3  \
Age                        31.910880  32.116667  32.680141  31.816327   
InGamePurchases             0.010417   0.016667   0.188049   0.197279   
GameDifficulty              0.660880   0.704762   0.706503   0.646259   
SessionsPerWeek             9.584491   9.092857   9.342707   9.384354   
AvgSessionDurationMinutes  94.428241  97.314286  98.337434  91.972789   
PlayerLevel                48.932870  47.390476  49.576450  52.363946   
AchievementsUnlocked       24.893519  24.840476  25.472759  23.744898   

Cluster                            4          5  
Age                        32.443431  31.655738  
InGamePurchases             0.222628   1.000000  
GameDifficulty              0.748175   0.718033  
SessionsPerWeek             9.250000   9.836066  
AvgSessionDurationMinutes  96.702555  92.836066  
PlayerLevel                50.434307  49.547541  
AchievementsUnlocked       24.211679  25.514754  

---------------- Cluster Interpretation Plots ----------------
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
---------------- t-SNE Visualization for Hierarchical Clustering ----------------
No description has been provided for this image
In [73]:
# ==========================================
# 4. SUPERVISED LEARNING: COMPARISON
# ==========================================

def evaluate_model(model, X_test, y_test, class_names, model_name):
    # Evaluates model and returns a dictionary of metrics for comparison.
    y_pred = model.predict(X_test)
    
    # Calculate Metrics
    acc = accuracy_score(y_test, y_pred)
    # Weighted average is best for multi-class balance
    precision, recall, f1, _ = precision_recall_fscore_support(y_test, y_pred, average='weighted')
    
    print(f"\n--- Evaluation: {model_name} ---")
    print(f"Accuracy: {acc:.4f}")
    print("Classification Report:")
    print(classification_report(y_test, y_pred, target_names=class_names))
    
    # Confusion Matrix Plot
    cm = confusion_matrix(y_test, y_pred)
    plt.figure(figsize=(6, 5))
    sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', xticklabels=class_names, yticklabels=class_names)
    plt.title(f'Confusion Matrix: {model_name}')
    plt.xlabel('Predicted Label')
    plt.ylabel('Actual Label')
    plt.show()
    
    # ROC-AUC Calculation
    auc_score = 0
    if hasattr(model, "predict_proba") or hasattr(model, "decision_function"):
        try:
            y_test_bin = label_binarize(y_test, classes=range(len(class_names)))
            if hasattr(model, "predict_proba"):
                y_score = model.predict_proba(X_test)
            else:
                y_score = model.decision_function(X_test)
            auc_score = roc_auc_score(y_test_bin, y_score, multi_class='ovr', average='weighted')
            print(f"Weighted AUC-ROC Score: {auc_score:.4f}")
        except Exception as e:
            print(f"Could not calculate ROC-AUC: {e}")
            
    # Return metrics for the comparison chart
    return {
        'Model': model_name,
        'Accuracy': acc,
        'Precision': precision,
        'Recall': recall,
        'F1-Score': f1,
        'AUC-ROC': auc_score
    }

def compare_models(results):
    # Plots a grouped bar chart comparing all models.
    print("\n---------------- Model Comparison Summary ----------------")
    results_df = pd.DataFrame(results)
    
    # Melt dataframe for Seaborn plotting
    df_melted = results_df.melt(id_vars="Model", var_name="Metric", value_name="Score")
    
    plt.figure(figsize=(12, 6))
    sns.barplot(x="Metric", y="Score", hue="Model", data=df_melted, palette="viridis")
    plt.title("Model Performance Comparison")
    plt.ylim(0.5, 1.0)
    plt.ylabel("Score (Zoomed in for visualisation purposes | 0.0 - 1.0)")
    plt.legend(bbox_to_anchor=(1.05, 1))
    plt.grid(axis='y', linestyle='--', alpha=0.8)
    plt.tight_layout()
    plt.show()
    
    print(results_df)

def run_supervised_learning(X, y, class_names):
    print("\n---------------- Supervised Learning: Model Development & Tuning ----------------")
    
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
    model_results = [] # Store results here
    
    # ----------------------------------------
    # 1. Decision Tree (DT)
    # ----------------------------------------
    print("\n---------------- Decision Tree Classifier ----------------")
    dt_params = {
        'max_depth': [None, 5, 10, 20],
        'min_samples_split': [2, 5, 10],
        'criterion': ['gini', 'entropy']
    }
    dt = DecisionTreeClassifier(random_state=42)
    dt_grid = ll(dt, dt_params, cv=5, scoring='accuracy', n_jobs=-1)
    dt_grid.fit(X_train, y_train)
    
    print(f"Best DT Parameters: {dt_grid.best_params_}")
    res_dt = evaluate_model(dt_grid.best_estimator_, X_test, y_test, class_names, "Decision Tree")
    model_results.append(res_dt)

    # ----------------------------------------
    # 2. Support Vector Machine (SVM)
    # ----------------------------------------
    print("\n---------------- Support Vector Machine (SVM) ----------------")
    print("Sampling 5,000 rows for SVM training to prevent crash")
    
    sample_size = min(5000, len(X_train))
    X_train_svm, _, y_train_svm, _ = train_test_split(
        X_train, y_train, train_size=sample_size, stratify=y_train, random_state=42
    )

    svm_params = {
        'C': [0.1, 1, 10],
        'kernel': ['rbf', 'linear'],
        'gamma': ['scale', 'auto']
    }
    svm = SVC(probability=True, random_state=42) 
    svm_grid = GridSearchCV(svm, svm_params, cv=3, scoring='accuracy', n_jobs=-1)
    svm_grid.fit(X_train_svm, y_train_svm)
    
    print(f"Best SVM Parameters: {svm_grid.best_params_}")
    res_svm = evaluate_model(svm_grid.best_estimator_, X_test, y_test, class_names, "SVM")
    model_results.append(res_svm)

    # ----------------------------------------
    # 3. K-Nearest Neighbors (KNN)
    # ----------------------------------------
    print("\n---------------- Training K-Nearest Neighbors (KNN) ----------------")
    knn_params = {
        'n_neighbors': [3, 5, 7, 11, 15],
        'weights': ['uniform', 'distance'],
        'metric': ['euclidean', 'manhattan']
    }
    knn = KNeighborsClassifier()
    knn_grid = GridSearchCV(knn, knn_params, cv=3, scoring='accuracy', n_jobs=-1)
    knn_grid.fit(X_train, y_train)
    
    print(f"Best KNN Parameters: {knn_grid.best_params_}")
    res_knn = evaluate_model(knn_grid.best_estimator_, X_test, y_test, class_names, "KNN")
    model_results.append(res_knn)
    
    # ----------------------------------------
    # 4. Compare All
    # ----------------------------------------
    compare_models(model_results)

class_names = label_encoder.classes_
run_supervised_learning(X_scaled, y_encoded, class_names)
---------------- Supervised Learning: Model Development & Tuning ----------------

---------------- Decision Tree Classifier ----------------
Best DT Parameters: {'criterion': 'entropy', 'max_depth': 10, 'min_samples_split': 10}

--- Evaluation: Decision Tree ---
Accuracy: 0.8976
Classification Report:
              precision    recall  f1-score   support

        High       0.90      0.88      0.89      2035
         Low       0.89      0.86      0.88      2093
      Medium       0.90      0.93      0.91      3879

    accuracy                           0.90      8007
   macro avg       0.90      0.89      0.89      8007
weighted avg       0.90      0.90      0.90      8007

No description has been provided for this image
Weighted AUC-ROC Score: 0.9290

---------------- Support Vector Machine (SVM) ----------------
   [SAFE MODE] Sampling 5,000 rows for SVM training to prevent crash...
Best SVM Parameters: {'C': 1, 'gamma': 'auto', 'kernel': 'rbf'}

--- Evaluation: SVM ---
Accuracy: 0.8620
Classification Report:
              precision    recall  f1-score   support

        High       0.89      0.85      0.87      2035
         Low       0.87      0.78      0.82      2093
      Medium       0.85      0.91      0.88      3879

    accuracy                           0.86      8007
   macro avg       0.87      0.85      0.86      8007
weighted avg       0.86      0.86      0.86      8007

No description has been provided for this image
Weighted AUC-ROC Score: 0.9334

---------------- Training K-Nearest Neighbors (KNN) ----------------
Best KNN Parameters: {'metric': 'manhattan', 'n_neighbors': 15, 'weights': 'distance'}

--- Evaluation: KNN ---
Accuracy: 0.7828
Classification Report:
              precision    recall  f1-score   support

        High       0.87      0.74      0.80      2035
         Low       0.84      0.59      0.69      2093
      Medium       0.73      0.91      0.81      3879

    accuracy                           0.78      8007
   macro avg       0.81      0.75      0.77      8007
weighted avg       0.80      0.78      0.78      8007

No description has been provided for this image
Weighted AUC-ROC Score: 0.8946

---------------- Model Comparison Summary ----------------
No description has been provided for this image
           Model  Accuracy  Precision    Recall  F1-Score   AUC-ROC
0  Decision Tree  0.897590   0.897506  0.897590  0.897358  0.928957
1            SVM  0.861996   0.863079  0.861996  0.861206  0.933361
2            KNN  0.782815   0.796061  0.782815  0.778149  0.894602
In [ ]: