In [1]:
#IMPORT NECESSARY LIBRARIES
import os
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.preprocessing import RobustScaler
from sklearn.neighbors import NearestNeighbors
from sklearn.cluster import DBSCAN, KMeans, AgglomerativeClustering
from sklearn.decomposition import PCA
from sklearn.metrics import (
    silhouette_score, davies_bouldin_score, calinski_harabasz_score
)
from scipy.cluster.hierarchy import linkage, dendrogram, fcluster
from IPython.display import FileLink
from sklearn.metrics import silhouette_samples

RANDOM_STATE = 42
np.random.seed(RANDOM_STATE)
plt.rcParams["figure.figsize"] = (9, 6)
plt.rcParams["axes.grid"] = True
sns.set_context("notebook")
In [2]:
# DATA LOADING
df = pd.read_csv("CC GENERAL.csv")
def load_credit_card_data(csv_path="CC GENERAL.csv"):
    if not os.path.exists(csv_path):
        raise FileNotFoundError(
            f"Could not find {csv_path}. Place 'CC GENERAL.csv' in this folder."
        )
    df = pd.read_csv(csv_path)
    # Strip spaces from column names if any
    df.columns = [c.strip() for c in df.columns]
    # Keep numeric columns only, drop CUST_ID
    num_df = df.select_dtypes(include=[np.number]).copy()
    if "CUST_ID" in df.columns:
        df_id = df["CUST_ID"]
    else:
        df_id = pd.Series(np.arange(len(df)), name="CUST_ID")
    return df, num_df, df_id
def impute_missing_median(df_numeric):
    return df_numeric.fillna(df_numeric.median())
def robust_scale(df_numeric):
    scaler = RobustScaler()
    scaled = scaler.fit_transform(df_numeric.values)
    return scaled, scaler
In [3]:
#DATA VIEWING
df = pd.read_csv("CC GENERAL.csv")
print("First 5 rows of the dataset:")
display(df.head())
First 5 rows of the dataset:
CUST_ID BALANCE BALANCE_FREQUENCY PURCHASES ONEOFF_PURCHASES INSTALLMENTS_PURCHASES CASH_ADVANCE PURCHASES_FREQUENCY ONEOFF_PURCHASES_FREQUENCY PURCHASES_INSTALLMENTS_FREQUENCY CASH_ADVANCE_FREQUENCY CASH_ADVANCE_TRX PURCHASES_TRX CREDIT_LIMIT PAYMENTS MINIMUM_PAYMENTS PRC_FULL_PAYMENT TENURE
0 C10001 40.900749 0.818182 95.40 0.00 95.4 0.000000 0.166667 0.000000 0.083333 0.000000 0 2 1000.0 201.802084 139.509787 0.000000 12
1 C10002 3202.467416 0.909091 0.00 0.00 0.0 6442.945483 0.000000 0.000000 0.000000 0.250000 4 0 7000.0 4103.032597 1072.340217 0.222222 12
2 C10003 2495.148862 1.000000 773.17 773.17 0.0 0.000000 1.000000 1.000000 0.000000 0.000000 0 12 7500.0 622.066742 627.284787 0.000000 12
3 C10004 1666.670542 0.636364 1499.00 1499.00 0.0 205.788017 0.083333 0.083333 0.000000 0.083333 1 1 7500.0 0.000000 NaN 0.000000 12
4 C10005 817.714335 1.000000 16.00 16.00 0.0 0.000000 0.083333 0.083333 0.000000 0.000000 0 1 1200.0 678.334763 244.791237 0.000000 12
In [4]:
# DATASET SHAPE
# View dataset dimensions
print("\n Dataset shape (rows, columns):", df.shape)

# Check all column names and data types
print("\n Dataset info:")
print(df.info())

# Check for missing values in each column
print("\n Missing values per column:")
print(df.isnull().sum())

# View summary statistics for numeric columns
print("\n Statistical summary:")
display(df.describe())

print("\n Random sample from dataset:")
display(df.sample(10))
 Dataset shape (rows, columns): (8950, 18)

 Dataset info:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 8950 entries, 0 to 8949
Data columns (total 18 columns):
 #   Column                            Non-Null Count  Dtype  
---  ------                            --------------  -----  
 0   CUST_ID                           8950 non-null   object 
 1   BALANCE                           8950 non-null   float64
 2   BALANCE_FREQUENCY                 8950 non-null   float64
 3   PURCHASES                         8950 non-null   float64
 4   ONEOFF_PURCHASES                  8950 non-null   float64
 5   INSTALLMENTS_PURCHASES            8950 non-null   float64
 6   CASH_ADVANCE                      8950 non-null   float64
 7   PURCHASES_FREQUENCY               8950 non-null   float64
 8   ONEOFF_PURCHASES_FREQUENCY        8950 non-null   float64
 9   PURCHASES_INSTALLMENTS_FREQUENCY  8950 non-null   float64
 10  CASH_ADVANCE_FREQUENCY            8950 non-null   float64
 11  CASH_ADVANCE_TRX                  8950 non-null   int64  
 12  PURCHASES_TRX                     8950 non-null   int64  
 13  CREDIT_LIMIT                      8949 non-null   float64
 14  PAYMENTS                          8950 non-null   float64
 15  MINIMUM_PAYMENTS                  8637 non-null   float64
 16  PRC_FULL_PAYMENT                  8950 non-null   float64
 17  TENURE                            8950 non-null   int64  
dtypes: float64(14), int64(3), object(1)
memory usage: 1.2+ MB
None

 Missing values per column:
CUST_ID                               0
BALANCE                               0
BALANCE_FREQUENCY                     0
PURCHASES                             0
ONEOFF_PURCHASES                      0
INSTALLMENTS_PURCHASES                0
CASH_ADVANCE                          0
PURCHASES_FREQUENCY                   0
ONEOFF_PURCHASES_FREQUENCY            0
PURCHASES_INSTALLMENTS_FREQUENCY      0
CASH_ADVANCE_FREQUENCY                0
CASH_ADVANCE_TRX                      0
PURCHASES_TRX                         0
CREDIT_LIMIT                          1
PAYMENTS                              0
MINIMUM_PAYMENTS                    313
PRC_FULL_PAYMENT                      0
TENURE                                0
dtype: int64

 Statistical summary:
BALANCE BALANCE_FREQUENCY PURCHASES ONEOFF_PURCHASES INSTALLMENTS_PURCHASES CASH_ADVANCE PURCHASES_FREQUENCY ONEOFF_PURCHASES_FREQUENCY PURCHASES_INSTALLMENTS_FREQUENCY CASH_ADVANCE_FREQUENCY CASH_ADVANCE_TRX PURCHASES_TRX CREDIT_LIMIT PAYMENTS MINIMUM_PAYMENTS PRC_FULL_PAYMENT TENURE
count 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8949.000000 8950.000000 8637.000000 8950.000000 8950.000000
mean 1564.474828 0.877271 1003.204834 592.437371 411.067645 978.871112 0.490351 0.202458 0.364437 0.135144 3.248827 14.709832 4494.449450 1733.143852 864.206542 0.153715 11.517318
std 2081.531879 0.236904 2136.634782 1659.887917 904.338115 2097.163877 0.401371 0.298336 0.397448 0.200121 6.824647 24.857649 3638.815725 2895.063757 2372.446607 0.292499 1.338331
min 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 50.000000 0.000000 0.019163 0.000000 6.000000
25% 128.281915 0.888889 39.635000 0.000000 0.000000 0.000000 0.083333 0.000000 0.000000 0.000000 0.000000 1.000000 1600.000000 383.276166 169.123707 0.000000 12.000000
50% 873.385231 1.000000 361.280000 38.000000 89.000000 0.000000 0.500000 0.083333 0.166667 0.000000 0.000000 7.000000 3000.000000 856.901546 312.343947 0.000000 12.000000
75% 2054.140036 1.000000 1110.130000 577.405000 468.637500 1113.821139 0.916667 0.300000 0.750000 0.222222 4.000000 17.000000 6500.000000 1901.134317 825.485459 0.142857 12.000000
max 19043.138560 1.000000 49039.570000 40761.250000 22500.000000 47137.211760 1.000000 1.000000 1.000000 1.500000 123.000000 358.000000 30000.000000 50721.483360 76406.207520 1.000000 12.000000
 Random sample from dataset:
CUST_ID BALANCE BALANCE_FREQUENCY PURCHASES ONEOFF_PURCHASES INSTALLMENTS_PURCHASES CASH_ADVANCE PURCHASES_FREQUENCY ONEOFF_PURCHASES_FREQUENCY PURCHASES_INSTALLMENTS_FREQUENCY CASH_ADVANCE_FREQUENCY CASH_ADVANCE_TRX PURCHASES_TRX CREDIT_LIMIT PAYMENTS MINIMUM_PAYMENTS PRC_FULL_PAYMENT TENURE
7669 C17875 16.834929 0.454545 15.00 15.00 0.00 209.025389 0.090909 0.090909 0.000000 0.090909 1 1 7500.0 430.213001 86.959785 0.000000 11
6125 C16296 540.020858 1.000000 612.23 495.61 116.62 1708.923217 0.666667 0.166667 0.500000 0.333333 10 10 2000.0 1642.068707 419.956251 0.000000 12
7028 C17219 119.237712 1.000000 342.74 0.00 342.74 0.000000 1.000000 0.000000 1.000000 0.000000 0 20 2000.0 327.166041 165.207233 0.000000 12
3019 C13108 894.081947 1.000000 1901.71 1853.11 48.60 206.618780 0.666667 0.666667 0.416667 0.083333 1 33 1500.0 947.130141 220.745296 0.000000 12
3479 C13576 1294.145453 1.000000 3059.10 1836.98 1222.12 0.000000 1.000000 0.416667 1.000000 0.000000 0 42 7000.0 5560.033502 497.637767 0.083333 12
4525 C14650 32.229139 0.727273 145.85 0.00 145.85 0.000000 0.583333 0.000000 0.583333 0.000000 0 7 1000.0 263.681773 130.725349 0.125000 12
6696 C16884 812.839893 0.875000 1160.09 932.09 228.00 3129.703318 0.875000 0.375000 0.500000 0.875000 8 13 2000.0 4900.523879 197.894736 0.200000 8
2471 C12545 1054.905969 1.000000 885.44 0.00 885.44 0.000000 0.833333 0.000000 0.833333 0.000000 0 111 1000.0 1057.254150 1613.401207 0.000000 12
2885 C12969 600.777518 1.000000 760.38 269.53 490.85 0.000000 1.000000 0.166667 1.000000 0.000000 0 35 4500.0 829.712416 186.053091 0.000000 12
6408 C16584 2184.965504 1.000000 0.00 0.00 0.00 2893.023143 0.000000 0.000000 0.000000 0.916667 26 0 8000.0 1730.364781 481.160634 0.000000 12
In [5]:
# DATA CLEANING
# Copy original dataset to preserve raw data
df_clean = df.copy()

# Drop non-numeric columns (CUST_ID is not used for clustering)
df_clean = df_clean.drop(columns=["CUST_ID"])

# Check again for missing values
print("Missing values before cleaning:")
print(df_clean.isnull().sum())

# Handle missing values — fill with column medians (robust to outliers)
df_clean = df_clean.fillna(df_clean.median())

# Confirm missing values handled
print("\n Missing values after cleaning:")
print(df_clean.isnull().sum())

# Check if any duplicate rows exist
duplicates = df_clean.duplicated().sum()
print(f"\n Number of duplicate rows found: {duplicates}")
if duplicates > 0:
    df_clean = df_clean.drop_duplicates()
    print("Duplicate rows removed.")
else:
    print("No duplicates found.")

# Confirm the cleaned dataset size
print("\n Dataset shape after cleaning:", df_clean.shape)

# Check statistical summary after cleaning
print("\n Statistical Summary (after cleaning):")
display(df_clean.describe())

# CHECK OUTLIERS
# Plot boxplot for key financial columns to visualize outliers
plt.figure(figsize=(12,6))
sns.boxplot(data=df_clean[['BALANCE', 'PURCHASES', 'CASH_ADVANCE', 'CREDIT_LIMIT', 'PAYMENTS']])
plt.title("Boxplot of Key Financial Variables (outliers)")
plt.show()
Missing values before cleaning:
BALANCE                               0
BALANCE_FREQUENCY                     0
PURCHASES                             0
ONEOFF_PURCHASES                      0
INSTALLMENTS_PURCHASES                0
CASH_ADVANCE                          0
PURCHASES_FREQUENCY                   0
ONEOFF_PURCHASES_FREQUENCY            0
PURCHASES_INSTALLMENTS_FREQUENCY      0
CASH_ADVANCE_FREQUENCY                0
CASH_ADVANCE_TRX                      0
PURCHASES_TRX                         0
CREDIT_LIMIT                          1
PAYMENTS                              0
MINIMUM_PAYMENTS                    313
PRC_FULL_PAYMENT                      0
TENURE                                0
dtype: int64

 Missing values after cleaning:
BALANCE                             0
BALANCE_FREQUENCY                   0
PURCHASES                           0
ONEOFF_PURCHASES                    0
INSTALLMENTS_PURCHASES              0
CASH_ADVANCE                        0
PURCHASES_FREQUENCY                 0
ONEOFF_PURCHASES_FREQUENCY          0
PURCHASES_INSTALLMENTS_FREQUENCY    0
CASH_ADVANCE_FREQUENCY              0
CASH_ADVANCE_TRX                    0
PURCHASES_TRX                       0
CREDIT_LIMIT                        0
PAYMENTS                            0
MINIMUM_PAYMENTS                    0
PRC_FULL_PAYMENT                    0
TENURE                              0
dtype: int64

 Number of duplicate rows found: 0
No duplicates found.

 Dataset shape after cleaning: (8950, 17)

 Statistical Summary (after cleaning):
BALANCE BALANCE_FREQUENCY PURCHASES ONEOFF_PURCHASES INSTALLMENTS_PURCHASES CASH_ADVANCE PURCHASES_FREQUENCY ONEOFF_PURCHASES_FREQUENCY PURCHASES_INSTALLMENTS_FREQUENCY CASH_ADVANCE_FREQUENCY CASH_ADVANCE_TRX PURCHASES_TRX CREDIT_LIMIT PAYMENTS MINIMUM_PAYMENTS PRC_FULL_PAYMENT TENURE
count 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000 8950.000000
mean 1564.474828 0.877271 1003.204834 592.437371 411.067645 978.871112 0.490351 0.202458 0.364437 0.135144 3.248827 14.709832 4494.282473 1733.143852 844.906767 0.153715 11.517318
std 2081.531879 0.236904 2136.634782 1659.887917 904.338115 2097.163877 0.401371 0.298336 0.397448 0.200121 6.824647 24.857649 3638.646702 2895.063757 2332.792322 0.292499 1.338331
min 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 50.000000 0.000000 0.019163 0.000000 6.000000
25% 128.281915 0.888889 39.635000 0.000000 0.000000 0.000000 0.083333 0.000000 0.000000 0.000000 0.000000 1.000000 1600.000000 383.276166 170.857654 0.000000 12.000000
50% 873.385231 1.000000 361.280000 38.000000 89.000000 0.000000 0.500000 0.083333 0.166667 0.000000 0.000000 7.000000 3000.000000 856.901546 312.343947 0.000000 12.000000
75% 2054.140036 1.000000 1110.130000 577.405000 468.637500 1113.821139 0.916667 0.300000 0.750000 0.222222 4.000000 17.000000 6500.000000 1901.134317 788.713501 0.142857 12.000000
max 19043.138560 1.000000 49039.570000 40761.250000 22500.000000 47137.211760 1.000000 1.000000 1.000000 1.500000 123.000000 358.000000 30000.000000 50721.483360 76406.207520 1.000000 12.000000
No description has been provided for this image
In [6]:
# Scale the cleaned data using RobustScaler
scaler = RobustScaler()
scaled_data = scaler.fit_transform(df_clean)
print("Data scaling completed. Shape of scaled data:", scaled_data.shape)
Data scaling completed. Shape of scaled data: (8950, 17)
In [7]:
# Seraching eps for clusters
X = scaled_data
eps_values = [1.0, 1.4, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0]
min_samples = 5
print("Searching eps for 5 clusters (excluding noise):\n")

for eps in eps_values:
    db = DBSCAN(eps=eps, min_samples=min_samples)
    labels = db.fit_predict(X)

    # count clusters excluding noise (-1)
    unique_labels = set(labels)
    n_clusters = len(unique_labels) - (1 if -1 in unique_labels else 0)
    n_noise = (labels == -1).sum()
    print(f"eps = {eps:.2f} | clusters = {n_clusters} | noise points = {n_noise}")
Searching eps for 5 clusters (excluding noise):

eps = 1.00 | clusters = 33 | noise points = 4942
eps = 1.40 | clusters = 17 | noise points = 3083
eps = 1.80 | clusters = 10 | noise points = 1980
eps = 2.00 | clusters = 9 | noise points = 1586
eps = 2.20 | clusters = 6 | noise points = 1293
eps = 2.40 | clusters = 4 | noise points = 1058
eps = 2.60 | clusters = 3 | noise points = 868
eps = 2.80 | clusters = 2 | noise points = 746
eps = 3.00 | clusters = 5 | noise points = 630
In [8]:
# DBSCAN

# Run DBSCAN
dbscan = DBSCAN(eps=3.0, min_samples=5)
labels = dbscan.fit_predict(scaled_data)

# Add labels to dataframe
df_clean['DBSCAN_Label'] = labels

#Count total points in each label (including noise)
print("Cluster label counts (including noise):")
print(df_clean['DBSCAN_Label'].value_counts())

#Count noise points only (-1)
noise_points = np.sum(labels == -1)
print(f"\nNumber of noise points detected: {noise_points}")

#Count number of clusters excluding noise
num_clusters = len(set(labels)) - (1 if -1 in labels else 0)
print(f"Number of clusters (excluding noise): {num_clusters}")
Cluster label counts (including noise):
DBSCAN_Label
 0    8300
-1     630
 2       6
 3       5
 1       5
 4       4
Name: count, dtype: int64

Number of noise points detected: 630
Number of clusters (excluding noise): 5
In [9]:
# Apply PCA for 2D visualization
pca = PCA(n_components=2)
pca_data = pca.fit_transform(scaled_data)
# Create a scatter plot
plt.figure(figsize=(10, 6))
sns.scatterplot( x=pca_data[:, 0], y=pca_data[:,1], hue=labels, palette="Set1", s=50 )
plt.title("DBSCAN Clusters (Noise = -1)")
plt.xlabel("PCA Component 1")
plt.ylabel("PCA Component 2")
plt.legend(title="Cluster Label", bbox_to_anchor=(1.05, 1), loc='upper left')
plt.show()
No description has been provided for this image
In [10]:
# k-Distance Plot
neighbors = NearestNeighbors(n_neighbors=5)
neighbors_fit = neighbors.fit(scaled_data)
distances, indices = neighbors_fit.kneighbors(scaled_data)
distances = np.sort(distances[:, 4])  # 5th nearest neighbor
plt.plot(distances)
plt.title("k-Distance Graph (Elbow = Suggested eps)")
plt.xlabel("Points")
plt.ylabel("5th Nearest Neighbor Distance")
plt.show()
No description has been provided for this image
In [11]:
# NOISE REMOVAL - DBSCAN
# Create a mask for non-noise points (label != -1)
non_noise_mask = df_clean['DBSCAN_Label'] != -1

# Create a new dataframe without noise
df_dbscan_clean = df_clean[non_noise_mask].copy()

# create a noise-free scaled data array for further analysis
scaled_dbscan_clean = scaled_data[non_noise_mask]

print("\nNoise removal completed.")
print("Shape before noise removal :", df_clean.shape)
print("Shape after noise removal  :", df_dbscan_clean.shape)
print("\nCluster label counts after noise removal (no -1):")
print(df_dbscan_clean['DBSCAN_Label'].value_counts())
print(f"Number of clusters: {num_clusters}")
Noise removal completed.
Shape before noise removal : (8950, 18)
Shape after noise removal  : (8320, 18)

Cluster label counts after noise removal (no -1):
DBSCAN_Label
0    8300
2       6
3       5
1       5
4       4
Name: count, dtype: int64
Number of clusters: 5
In [12]:
# VIEW DATASET AFTER NOISE REMOVAL
print("Dataset shape after adding dbscan labels:", df_dbscan_clean.shape)

print("\nColumns in noise-removed dataset:")
for col in df_dbscan_clean.columns:
    print("-", col)

print("\nFirst 10 rows of noise-removed dataset:")
display(df_dbscan_clean.head(10))
Dataset shape after adding dbscan labels: (8320, 18)

Columns in noise-removed dataset:
- BALANCE
- BALANCE_FREQUENCY
- PURCHASES
- ONEOFF_PURCHASES
- INSTALLMENTS_PURCHASES
- CASH_ADVANCE
- PURCHASES_FREQUENCY
- ONEOFF_PURCHASES_FREQUENCY
- PURCHASES_INSTALLMENTS_FREQUENCY
- CASH_ADVANCE_FREQUENCY
- CASH_ADVANCE_TRX
- PURCHASES_TRX
- CREDIT_LIMIT
- PAYMENTS
- MINIMUM_PAYMENTS
- PRC_FULL_PAYMENT
- TENURE
- DBSCAN_Label

First 10 rows of noise-removed dataset:
BALANCE BALANCE_FREQUENCY PURCHASES ONEOFF_PURCHASES INSTALLMENTS_PURCHASES CASH_ADVANCE PURCHASES_FREQUENCY ONEOFF_PURCHASES_FREQUENCY PURCHASES_INSTALLMENTS_FREQUENCY CASH_ADVANCE_FREQUENCY CASH_ADVANCE_TRX PURCHASES_TRX CREDIT_LIMIT PAYMENTS MINIMUM_PAYMENTS PRC_FULL_PAYMENT TENURE DBSCAN_Label
0 40.900749 0.818182 95.40 0.00 95.40 0.000000 0.166667 0.000000 0.083333 0.000000 0 2 1000.0 201.802084 139.509787 0.000000 12 0
1 3202.467416 0.909091 0.00 0.00 0.00 6442.945483 0.000000 0.000000 0.000000 0.250000 4 0 7000.0 4103.032597 1072.340217 0.222222 12 0
2 2495.148862 1.000000 773.17 773.17 0.00 0.000000 1.000000 1.000000 0.000000 0.000000 0 12 7500.0 622.066742 627.284787 0.000000 12 0
3 1666.670542 0.636364 1499.00 1499.00 0.00 205.788017 0.083333 0.083333 0.000000 0.083333 1 1 7500.0 0.000000 312.343947 0.000000 12 0
4 817.714335 1.000000 16.00 16.00 0.00 0.000000 0.083333 0.083333 0.000000 0.000000 0 1 1200.0 678.334763 244.791237 0.000000 12 0
5 1809.828751 1.000000 1333.28 0.00 1333.28 0.000000 0.666667 0.000000 0.583333 0.000000 0 8 1800.0 1400.057770 2407.246035 0.000000 12 0
6 627.260806 1.000000 7091.01 6402.63 688.38 0.000000 1.000000 1.000000 1.000000 0.000000 0 64 13500.0 6354.314328 198.065894 1.000000 12 0
7 1823.652743 1.000000 436.20 0.00 436.20 0.000000 1.000000 0.000000 1.000000 0.000000 0 12 2300.0 679.065082 532.033990 0.000000 12 0
8 1014.926473 1.000000 861.49 661.49 200.00 0.000000 0.333333 0.083333 0.250000 0.000000 0 5 7000.0 688.278568 311.963409 0.000000 12 0
9 152.225975 0.545455 1281.60 1281.60 0.00 0.000000 0.166667 0.166667 0.000000 0.000000 0 3 11000.0 1164.770591 100.302262 0.000000 12 0
In [13]:
# PCA VISUALISATION AFTER NOISE REMOVAL

# Apply PCA on noise-free scaled data
pca_clean = PCA(n_components=2)
pca_data_clean = pca_clean.fit_transform(scaled_dbscan_clean)
plt.figure(figsize=(10, 6))
sns.scatterplot(
    x=pca_data_clean[:, 0],
    y=pca_data_clean[:, 1],
    hue=df_dbscan_clean['DBSCAN_Label'],
    palette="Set1",
    s=50
)
plt.title("DBSCAN Clusters(PCA 2D)")
plt.xlabel("PCA Component 1")
plt.ylabel("PCA Component 2")
plt.legend(title="Cluster Label", bbox_to_anchor=(1.05, 1), loc='upper left')
plt.show()
No description has been provided for this image
In [14]:
# HEATMAP - DBSCAN

# Remove DBSCAN label columns if they exist
cols_to_drop = ["DBSCAN_Label", "DBSCAN_Raw"]  
feature_cols = [col for col in df_dbscan_clean.columns if col not in cols_to_drop]
# Compute cluster-wise mean values
cluster_means = df_dbscan_clean.groupby('DBSCAN_Label')[feature_cols].mean()
cluster_means_normalized = (cluster_means - cluster_means.min()) / (cluster_means.max() - cluster_means.min())
plt.figure(figsize=(18, 8))
sns.heatmap(
    cluster_means_normalized,
    cmap="coolwarm",
    linewidths=0.5,
    linecolor="black",
    cbar_kws={'label': 'Normalized Feature Value'},
    annot=True,
    fmt=".2f"
)
plt.title("DBSCAN Cluster Profiles (Normalized Heatmap of Mean Features)", fontsize=16)
plt.xlabel("Features", fontsize=12)
plt.ylabel("Cluster Label", fontsize=12)
plt.xticks(rotation=45, ha='right')
plt.yticks(rotation=0)
plt.tight_layout()
plt.show()
No description has been provided for this image
In [15]:
# SILHOUETTE - DBSCAN
X_db = scaled_dbscan_clean
labels_db = df_dbscan_clean["DBSCAN_Label"].values

# Silhouette score
sil_dbscan = silhouette_score(X_db, labels_db)
print(f"DBSCAN Silhouette Score: {sil_dbscan:.4f}")

# Simple bar plot
plt.figure(figsize=(5, 4))
plt.bar(["DBSCAN"], [sil_dbscan])
plt.title("Silhouette Score – DBSCAN")
plt.ylabel("Silhouette Score (higher is better)")
plt.ylim(0, sil_dbscan + 0.2)
plt.grid(axis="y", linestyle="--", alpha=0.6)
plt.tight_layout()
plt.show()
DBSCAN Silhouette Score: 0.2843
No description has been provided for this image
In [16]:
# DAVIES–BOULDIN INDEX (DBI) - DBSCAN
X_db = scaled_dbscan_clean
labels_db = df_dbscan_clean["DBSCAN_Label"].values

# Compute DBI
dbi_dbscan = davies_bouldin_score(X_db, labels_db)
print(f"DBSCAN Davies–Bouldin Index: {dbi_dbscan:.4f}")

# Simple bar plot
plt.figure(figsize=(5, 4))
plt.bar(["DBSCAN"], [dbi_dbscan], color="skyblue")
plt.title("Davies–Bouldin Index – DBSCAN")
plt.ylabel("DBI (lower is better)")
plt.ylim(0, dbi_dbscan + 0.5)   # small margin above
plt.grid(axis='y', linestyle='--', alpha=0.6)
plt.tight_layout()
plt.show()
DBSCAN Davies–Bouldin Index: 0.5675
No description has been provided for this image
In [17]:
# CALINSKI–HARABASZ (CH) - DBSCAN
X_db = scaled_dbscan_clean
labels_db = df_dbscan_clean["DBSCAN_Label"].values

# Compute CH score
ch_dbscan = calinski_harabasz_score(X_db, labels_db)
print(f"DBSCAN Calinski–Harabasz Score: {ch_dbscan:.4f}")

# Simple bar plot
plt.figure(figsize=(5, 4))
plt.bar(["DBSCAN"], [ch_dbscan], color="orange")
plt.title("Calinski–Harabasz Score – DBSCAN")
plt.ylabel("CH Score (higher is better)")
plt.ylim(0, ch_dbscan + (0.1 * ch_dbscan))  # small headroom
plt.grid(axis="y", linestyle="--", alpha=0.6)
plt.tight_layout()
plt.show()
DBSCAN Calinski–Harabasz Score: 34.4979
No description has been provided for this image
In [18]:
# CLUSTER PROFILE - DBSCAN

# Select numeric financial features
dbscan_feature_cols = [
    "BALANCE",
    "PURCHASES",
    "CASH_ADVANCE",
    "CREDIT_LIMIT",
    "PAYMENTS",
    "MINIMUM_PAYMENTS",
    "PRC_FULL_PAYMENT"
]

# Compute cluster-wise mean values
dbscan_cluster_profile = (
    df_dbscan_clean
    .groupby("DBSCAN_Label")[dbscan_feature_cols]
    .mean()
    .round(2)
)
print("\nDBSCAN CLUSTER PROFILE")
print(f"Clusters found: {len(dbscan_cluster_profile)}\n")
display(dbscan_cluster_profile)
DBSCAN CLUSTER PROFILE
Clusters found: 5

BALANCE PURCHASES CASH_ADVANCE CREDIT_LIMIT PAYMENTS MINIMUM_PAYMENTS PRC_FULL_PAYMENT
DBSCAN_Label
0 1363.78 720.16 784.43 4166.78 1283.16 624.40 0.15
1 7958.47 5183.41 0.00 14820.00 2672.61 2175.93 0.00
2 6179.52 10.78 11144.81 9750.00 8467.30 2061.92 0.03
3 680.29 8393.86 47.41 8500.00 7502.99 201.64 0.84
4 266.58 2687.02 0.00 2937.50 2807.84 2548.20 0.84
In [19]:
# BUSINESS INTERPRETATION - DBSCAN

# Select important financial features
dbscan_feature_cols = [
    "BALANCE",
    "PURCHASES",
    "CASH_ADVANCE",
    "CREDIT_LIMIT",
    "PAYMENTS",
    "MINIMUM_PAYMENTS",
    "PRC_FULL_PAYMENT"
]

df_dbscan_biz = df_dbscan_clean.copy()
if -1 in df_dbscan_biz["DBSCAN_Label"].unique():
    df_dbscan_biz = df_dbscan_biz[df_dbscan_biz["DBSCAN_Label"] != -1]

# Cluster-wise mean profile
dbscan_profile = (
    df_dbscan_biz
    .groupby("DBSCAN_Label")[dbscan_feature_cols]
    .mean()
    .round(2)
)

# Overall mean & std for z-scores
overall_means_db = df_dbscan_biz[dbscan_feature_cols].mean()
overall_stds_db  = df_dbscan_biz[dbscan_feature_cols].std().replace(0, 1)
print("BUSINESS INTERPRETATION – DBSCAN")

for cluster_id, row in dbscan_profile.iterrows():
    print(f"\nDBSCAN Cluster {cluster_id}")
    print("Feature Means:")
    print(row)

    # Z-score for each feature in this cluster
    z = (row - overall_means_db) / overall_stds_db

    # Top 3 HIGH and LOW features
    top3_high = z.sort_values(ascending=False).head(3)
    top3_low  = z.sort_values().head(3)

    print("\nHigh Characteristics (above overall average):")
    for feat, val in top3_high.items():
        print(f" • {feat}: HIGH (z = {val:.2f})")

    print("\nLow Characteristics (below overall average):")
    for feat, val in top3_low.items():
        print(f" • {feat}: LOW (z = {val:.2f})")

    # Simple rule-based business label
    label = "Mixed-Behavior Customer Segment"

    if "CASH_ADVANCE" in top3_high.index and "PAYMENTS" in top3_low.index:
        label = "High Cash-Advance / Revolving Credit Customers"

    elif "PURCHASES" in top3_high.index and "PRC_FULL_PAYMENT" in top3_high.index:
        label = "High-Spend, Full-Payment Loyal Customers"

    elif "BALANCE" in top3_low.index and "PURCHASES" in top3_low.index:
        label = "Low-Usage / Dormant Customers"

    elif "BALANCE" in top3_high.index and "PAYMENTS" in top3_low.index:
        label = "High Balance, Low Repayment Risky Customers"
        
    print(f"\nSuggested Business Label: {label}")
    print("-" * 50)
BUSINESS INTERPRETATION – DBSCAN

DBSCAN Cluster 0
Feature Means:
BALANCE             1363.78
PURCHASES            720.16
CASH_ADVANCE         784.43
CREDIT_LIMIT        4166.78
PAYMENTS            1283.16
MINIMUM_PAYMENTS     624.40
PRC_FULL_PAYMENT       0.15
Name: 0, dtype: float64

High Characteristics (above overall average):
 • PRC_FULL_PAYMENT: HIGH (z = -0.00)
 • MINIMUM_PAYMENTS: HIGH (z = -0.00)
 • BALANCE: HIGH (z = -0.00)

Low Characteristics (below overall average):
 • PURCHASES: LOW (z = -0.01)
 • PAYMENTS: LOW (z = -0.01)
 • CASH_ADVANCE: LOW (z = -0.00)

Suggested Business Label: High Balance, Low Repayment Risky Customers
--------------------------------------------------

DBSCAN Cluster 1
Feature Means:
BALANCE              7958.47
PURCHASES            5183.41
CASH_ADVANCE            0.00
CREDIT_LIMIT        14820.00
PAYMENTS             2672.61
MINIMUM_PAYMENTS     2175.93
PRC_FULL_PAYMENT        0.00
Name: 1, dtype: float64

High Characteristics (above overall average):
 • PURCHASES: HIGH (z = 4.26)
 • BALANCE: HIGH (z = 3.69)
 • CREDIT_LIMIT: HIGH (z = 3.22)

Low Characteristics (below overall average):
 • CASH_ADVANCE: LOW (z = -0.52)
 • PRC_FULL_PAYMENT: LOW (z = -0.52)
 • PAYMENTS: LOW (z = 0.93)

Suggested Business Label: High Balance, Low Repayment Risky Customers
--------------------------------------------------

DBSCAN Cluster 2
Feature Means:
BALANCE              6179.52
PURCHASES              10.78
CASH_ADVANCE        11144.81
CREDIT_LIMIT         9750.00
PAYMENTS             8467.30
MINIMUM_PAYMENTS     2061.92
PRC_FULL_PAYMENT        0.03
Name: 2, dtype: float64

High Characteristics (above overall average):
 • CASH_ADVANCE: HIGH (z = 6.86)
 • PAYMENTS: HIGH (z = 4.85)
 • BALANCE: HIGH (z = 2.69)

Low Characteristics (below overall average):
 • PURCHASES: LOW (z = -0.69)
 • PRC_FULL_PAYMENT: LOW (z = -0.42)
 • MINIMUM_PAYMENTS: LOW (z = 1.40)

Suggested Business Label: Mixed-Behavior Customer Segment
--------------------------------------------------

DBSCAN Cluster 3
Feature Means:
BALANCE              680.29
PURCHASES           8393.86
CASH_ADVANCE          47.41
CREDIT_LIMIT        8500.00
PAYMENTS            7502.99
MINIMUM_PAYMENTS     201.64
PRC_FULL_PAYMENT       0.84
Name: 3, dtype: float64

High Characteristics (above overall average):
 • PURCHASES: HIGH (z = 7.33)
 • PAYMENTS: HIGH (z = 4.20)
 • PRC_FULL_PAYMENT: HIGH (z = 2.38)

Low Characteristics (below overall average):
 • CASH_ADVANCE: LOW (z = -0.49)
 • MINIMUM_PAYMENTS: LOW (z = -0.41)
 • BALANCE: LOW (z = -0.39)

Suggested Business Label: High-Spend, Full-Payment Loyal Customers
--------------------------------------------------

DBSCAN Cluster 4
Feature Means:
BALANCE              266.58
PURCHASES           2687.02
CASH_ADVANCE           0.00
CREDIT_LIMIT        2937.50
PAYMENTS            2807.84
MINIMUM_PAYMENTS    2548.20
PRC_FULL_PAYMENT       0.84
Name: 4, dtype: float64

High Characteristics (above overall average):
 • PRC_FULL_PAYMENT: HIGH (z = 2.38)
 • PURCHASES: HIGH (z = 1.87)
 • MINIMUM_PAYMENTS: HIGH (z = 1.87)

Low Characteristics (below overall average):
 • BALANCE: LOW (z = -0.62)
 • CASH_ADVANCE: LOW (z = -0.52)
 • CREDIT_LIMIT: LOW (z = -0.38)

Suggested Business Label: High-Spend, Full-Payment Loyal Customers
--------------------------------------------------
In [20]:
# HIERARCHICAL CLUSTERING

Z = linkage(scaled_dbscan_clean, method="ward")
plt.figure(figsize=(12, 6))
dendrogram(
    Z,
    truncate_mode='lastp',
    p=30,
    show_leaf_counts=True
)
plt.title("Dendrogram – Hierarchical Clustering")
plt.xlabel("Cluster ID")
plt.ylabel("Distance")
plt.show()
cut_height = 150 
# Cut dendrogram to get natural clusters
hier_auto_labels = fcluster(Z, t=cut_height, criterion="distance")
print("Unique clusters found:", np.unique(hier_auto_labels))
print("Number of natural clusters =", len(np.unique(hier_auto_labels)))
hier = AgglomerativeClustering(n_clusters=5, linkage="ward")
hier_labels = hier.fit_predict(scaled_dbscan_clean)
df_hier = df_dbscan_clean.copy()
df_hier["HIER"] = hier_labels
No description has been provided for this image
Unique clusters found: [1 2 3 4 5]
Number of natural clusters = 5
In [21]:
# HIERARCHICAL SUMMARY 
print("\nHIERARCHICAL SUMMARY ")

# Dataset Size
print("\nDataset Shape (rows, columns):", df_hier.shape)

# Column Names
print("\nColumn Names:")
print(list(df_hier.columns))

# Cluster Sizes
print("\nCluster Sizes:")
print(df_hier["HIER"].value_counts().sort_index())

# Cluster-wise Mean Values
print("\nCluster-wise Mean Values:")
display(
    df_hier.groupby("HIER")[df_hier.columns].mean().round(2)
)
HIERARCHICAL SUMMARY 

Dataset Shape (rows, columns): (8320, 19)

Column Names:
['BALANCE', 'BALANCE_FREQUENCY', 'PURCHASES', 'ONEOFF_PURCHASES', 'INSTALLMENTS_PURCHASES', 'CASH_ADVANCE', 'PURCHASES_FREQUENCY', 'ONEOFF_PURCHASES_FREQUENCY', 'PURCHASES_INSTALLMENTS_FREQUENCY', 'CASH_ADVANCE_FREQUENCY', 'CASH_ADVANCE_TRX', 'PURCHASES_TRX', 'CREDIT_LIMIT', 'PAYMENTS', 'MINIMUM_PAYMENTS', 'PRC_FULL_PAYMENT', 'TENURE', 'DBSCAN_Label', 'HIER']

Cluster Sizes:
HIER
0    1313
1    3811
2    1270
3    1110
4     816
Name: count, dtype: int64

Cluster-wise Mean Values:
BALANCE BALANCE_FREQUENCY PURCHASES ONEOFF_PURCHASES INSTALLMENTS_PURCHASES CASH_ADVANCE PURCHASES_FREQUENCY ONEOFF_PURCHASES_FREQUENCY PURCHASES_INSTALLMENTS_FREQUENCY CASH_ADVANCE_FREQUENCY CASH_ADVANCE_TRX PURCHASES_TRX CREDIT_LIMIT PAYMENTS MINIMUM_PAYMENTS PRC_FULL_PAYMENT TENURE DBSCAN_Label HIER
HIER
0 3721.28 0.98 288.84 163.93 124.93 3065.04 0.24 0.09 0.17 0.38 9.73 5.00 6172.18 2386.14 1562.16 0.03 11.85 0.01 0.0
1 1229.05 0.97 459.03 193.30 266.05 449.33 0.46 0.13 0.35 0.11 1.86 9.39 3119.23 777.58 590.69 0.04 11.33 0.00 1.0
2 208.72 0.44 316.72 185.77 131.48 461.48 0.30 0.07 0.22 0.05 1.12 4.88 3537.97 967.56 201.82 0.06 11.54 0.00 2.0
3 128.00 0.81 1330.49 643.26 687.26 29.81 0.75 0.26 0.59 0.01 0.13 19.96 4869.80 1457.86 184.07 0.81 11.54 0.03 3.0
4 1744.65 0.98 2510.27 1836.23 674.10 271.96 0.84 0.66 0.53 0.06 1.05 35.42 5981.60 2230.03 556.51 0.10 11.90 0.01 4.0
In [22]:
# PCA - HIERARCHICAL
pca = PCA(n_components=2, random_state=42)
X_pca_hier = pca.fit_transform(scaled_dbscan_clean)

# Create PCA dataframe
pca_df_hier = pd.DataFrame({
    "PC1": X_pca_hier[:, 0],
    "PC2": X_pca_hier[:, 1],
    "HIER": df_hier["HIER"]
})

# Plot
plt.figure(figsize=(8, 6))
sns.scatterplot(
    data=pca_df_hier,
    x="PC1",
    y="PC2",
    hue="HIER",
    palette="Set2",
    s=35,
)
plt.title("PCA – Hierarchical Clustering (k = 5)")
plt.xlabel("Principal Component 1")
plt.ylabel("Principal Component 2")
plt.legend(title="Cluster")
plt.tight_layout()
plt.show()
No description has been provided for this image
In [23]:
# HEATMAP – HIERARCHICAL CLUSTERING 
feature_cols = [
    "BALANCE",
    "PURCHASES",
    "CASH_ADVANCE",
    "CREDIT_LIMIT",
    "PAYMENTS",
    "MINIMUM_PAYMENTS",
    "PRC_FULL_PAYMENT"
]

# Calculate cluster-wise means
hier_means = df_hier.groupby("HIER")[feature_cols].mean().round(2)

# Plot heatmap
plt.figure(figsize=(15, 7))
sns.heatmap(
    hier_means,
    annot=True,
    cmap="viridis",
    fmt=".2f"
)
plt.title("Hierarchical Clustering (k = 5) – Heatmap of Cluster Means")
plt.tight_layout()
plt.show()
No description has been provided for this image
In [24]:
# SILHOUETTE – HIERARCHICAL
hier_labels = df_hier["HIER"].values

# Compute silhouette score
sil_score_hier = silhouette_score(scaled_dbscan_clean, hier_labels)
sil_values = silhouette_samples(scaled_dbscan_clean, hier_labels)
print(f"\nHierarchical Silhouette Score (k=5): {sil_score_hier:.4f}")

# Plot silhouette
plt.figure(figsize=(8, 6))
y_lower = 10
n_clusters = len(np.unique(hier_labels))

for i in range(n_clusters):
    cluster_sil_values = sil_values[hier_labels == i]
    cluster_sil_values.sort()
    size_cluster = cluster_sil_values.shape[0]
    y_upper = y_lower + size_cluster

    plt.fill_betweenx(
        np.arange(y_lower, y_upper),
        0,
        cluster_sil_values,
        alpha=0.7,
    )
    plt.text(-0.05, y_lower + size_cluster / 2, str(i))
    y_lower = y_upper + 10
plt.axvline(sil_score_hier, color="red", linestyle="--")
plt.title("Silhouette Plot – Hierarchical Clustering (k = 5)")
plt.xlabel("Silhouette Coefficient")
plt.ylabel("Cluster")
plt.tight_layout()
plt.show()
Hierarchical Silhouette Score (k=5): 0.2118
No description has been provided for this image
In [25]:
# DBI – HIERARCHICAL

# Calculate DBI
hier_labels = df_hier["HIER"].values
dbi_hier = davies_bouldin_score(scaled_dbscan_clean, hier_labels)
print("\nDavies–Bouldin Index for Hierarchical (k = 5):", round(dbi_hier, 4))

# Bar plot for DBI
plt.figure(figsize=(5, 4))
plt.bar(["Hierarchical (k=5)"], [dbi_hier])
plt.title("Davies–Bouldin Index – Hierarchical (k = 5)")
plt.ylabel("DBI (lower is better)")
plt.tight_layout()
plt.show()
Davies–Bouldin Index for Hierarchical (k = 5): 1.4862
No description has been provided for this image
In [26]:
# CALINSKI–HARABASZ – HIERARCHICAL

# Calculate CH score
hier_labels = df_hier["HIER"].values
ch_hier = calinski_harabasz_score(scaled_dbscan_clean, hier_labels)
print("\nCalinski–Harabasz Score for Hierarchical (k = 5):", round(ch_hier, 4))

# Plot CH score
plt.figure(figsize=(5, 4))
plt.bar(["Hier (k=5)"], [ch_hier])
plt.title("Calinski–Harabasz Score – Hierarchical (k = 5)")
plt.ylabel("CH (higher is better)")
plt.tight_layout()
plt.show()
Calinski–Harabasz Score for Hierarchical (k = 5): 1532.2808
No description has been provided for this image
In [27]:
# CLUSTER PROFILE - HIERARCHICAL

# Important financial features
hier_feature_cols = [
    "BALANCE",
    "PURCHASES",
    "CASH_ADVANCE",
    "CREDIT_LIMIT",
    "PAYMENTS",
    "MINIMUM_PAYMENTS",
    "PRC_FULL_PAYMENT"
]

# Compute cluster-wise mean values
hier_cluster_profile = (
    df_hier
    .groupby("HIER")[hier_feature_cols]
    .mean()
    .round(2)
)
print("\nHIERARCHICAL CLUSTER PROFILE")
print(f"Clusters found: {len(hier_cluster_profile)}\n")
display(hier_cluster_profile)
HIERARCHICAL CLUSTER PROFILE
Clusters found: 5

BALANCE PURCHASES CASH_ADVANCE CREDIT_LIMIT PAYMENTS MINIMUM_PAYMENTS PRC_FULL_PAYMENT
HIER
0 3721.28 288.84 3065.04 6172.18 2386.14 1562.16 0.03
1 1229.05 459.03 449.33 3119.23 777.58 590.69 0.04
2 208.72 316.72 461.48 3537.97 967.56 201.82 0.06
3 128.00 1330.49 29.81 4869.80 1457.86 184.07 0.81
4 1744.65 2510.27 271.96 5981.60 2230.03 556.51 0.10
In [28]:
# BUSINESS INTERPRETATION - HIERARCHICAL

# Important financial features
hier_feature_cols = [
    "BALANCE",
    "PURCHASES",
    "CASH_ADVANCE",
    "CREDIT_LIMIT",
    "PAYMENTS",
    "MINIMUM_PAYMENTS",
    "PRC_FULL_PAYMENT"
]

# Work on a copy
df_hier_biz = df_hier.copy()

# Cluster-wise mean profile
hier_profile = (
    df_hier_biz
    .groupby("HIER")[hier_feature_cols]
    .mean()
    .round(2)
)

# Overall mean & std for z-scores
overall_means_hier = df_hier_biz[hier_feature_cols].mean()
overall_stds_hier  = df_hier_biz[hier_feature_cols].std().replace(0, 1)

print("BUSINESS INTERPRETATION – HIERARCHICAL")
for cluster_id, row in hier_profile.iterrows():
    print(f"\nHIER Cluster {cluster_id}")
    print("Feature Means:")
    print(row)

    # Z-score for each feature in this cluster
    z = (row - overall_means_hier) / overall_stds_hier

    # Top 3 HIGH and LOW features
    top3_high = z.sort_values(ascending=False).head(3)
    top3_low  = z.sort_values().head(3)

    print("\nHigh Characteristics (above overall average):")
    for feat, val in top3_high.items():
        print(f" • {feat}: HIGH (z = {val:.2f})")

    print("\nLow Characteristics (below overall average):")
    for feat, val in top3_low.items():
        print(f" • {feat}: LOW (z = {val:.2f})")

    # Simple rule-based business label
    label = "Mixed-Behavior Customer Segment"

    if "CASH_ADVANCE" in top3_high.index and "PAYMENTS" in top3_low.index:
        label = "High Cash-Advance / Revolving Credit Customers"

    elif "PURCHASES" in top3_high.index and "PRC_FULL_PAYMENT" in top3_high.index:
        label = "High-Spend, Full-Payment Loyal Customers"

    elif "BALANCE" in top3_low.index and "PURCHASES" in top3_low.index:
        label = "Low-Usage / Dormant Customers"

    elif "BALANCE" in top3_high.index and "PAYMENTS" in top3_low.index:
        label = "High Balance, Low Repayment Risky Customers"

    print(f"\nSuggested Business Label: {label}")
    print("-" * 50)
BUSINESS INTERPRETATION – HIERARCHICAL

HIER Cluster 0
Feature Means:
BALANCE             3721.28
PURCHASES            288.84
CASH_ADVANCE        3065.04
CREDIT_LIMIT        6172.18
PAYMENTS            2386.14
MINIMUM_PAYMENTS    1562.16
PRC_FULL_PAYMENT       0.03
Name: 0, dtype: float64

High Characteristics (above overall average):
 • CASH_ADVANCE: HIGH (z = 1.51)
 • BALANCE: HIGH (z = 1.32)
 • MINIMUM_PAYMENTS: HIGH (z = 0.91)

Low Characteristics (below overall average):
 • PURCHASES: LOW (z = -0.42)
 • PRC_FULL_PAYMENT: LOW (z = -0.42)
 • CREDIT_LIMIT: LOW (z = 0.60)

Suggested Business Label: Mixed-Behavior Customer Segment
--------------------------------------------------

HIER Cluster 1
Feature Means:
BALANCE             1229.05
PURCHASES            459.03
CASH_ADVANCE         449.33
CREDIT_LIMIT        3119.23
PAYMENTS             777.58
MINIMUM_PAYMENTS     590.69
PRC_FULL_PAYMENT       0.04
Name: 1, dtype: float64

High Characteristics (above overall average):
 • MINIMUM_PAYMENTS: HIGH (z = -0.04)
 • BALANCE: HIGH (z = -0.08)
 • CASH_ADVANCE: HIGH (z = -0.23)

Low Characteristics (below overall average):
 • PRC_FULL_PAYMENT: LOW (z = -0.38)
 • PAYMENTS: LOW (z = -0.35)
 • CREDIT_LIMIT: LOW (z = -0.32)

Suggested Business Label: High Cash-Advance / Revolving Credit Customers
--------------------------------------------------

HIER Cluster 2
Feature Means:
BALANCE              208.72
PURCHASES            316.72
CASH_ADVANCE         461.48
CREDIT_LIMIT        3537.97
PAYMENTS             967.56
MINIMUM_PAYMENTS     201.82
PRC_FULL_PAYMENT       0.06
Name: 2, dtype: float64

High Characteristics (above overall average):
 • CREDIT_LIMIT: HIGH (z = -0.19)
 • CASH_ADVANCE: HIGH (z = -0.22)
 • PAYMENTS: HIGH (z = -0.22)

Low Characteristics (below overall average):
 • BALANCE: LOW (z = -0.65)
 • MINIMUM_PAYMENTS: LOW (z = -0.41)
 • PURCHASES: LOW (z = -0.39)

Suggested Business Label: Low-Usage / Dormant Customers
--------------------------------------------------

HIER Cluster 3
Feature Means:
BALANCE              128.00
PURCHASES           1330.49
CASH_ADVANCE          29.81
CREDIT_LIMIT        4869.80
PAYMENTS            1457.86
MINIMUM_PAYMENTS     184.07
PRC_FULL_PAYMENT       0.81
Name: 3, dtype: float64

High Characteristics (above overall average):
 • PRC_FULL_PAYMENT: HIGH (z = 2.27)
 • PURCHASES: HIGH (z = 0.58)
 • CREDIT_LIMIT: HIGH (z = 0.21)

Low Characteristics (below overall average):
 • BALANCE: LOW (z = -0.70)
 • CASH_ADVANCE: LOW (z = -0.50)
 • MINIMUM_PAYMENTS: LOW (z = -0.43)

Suggested Business Label: High-Spend, Full-Payment Loyal Customers
--------------------------------------------------

HIER Cluster 4
Feature Means:
BALANCE             1744.65
PURCHASES           2510.27
CASH_ADVANCE         271.96
CREDIT_LIMIT        5981.60
PAYMENTS            2230.03
MINIMUM_PAYMENTS     556.51
PRC_FULL_PAYMENT       0.10
Name: 4, dtype: float64

High Characteristics (above overall average):
 • PURCHASES: HIGH (z = 1.70)
 • PAYMENTS: HIGH (z = 0.63)
 • CREDIT_LIMIT: HIGH (z = 0.54)

Low Characteristics (below overall average):
 • CASH_ADVANCE: LOW (z = -0.34)
 • PRC_FULL_PAYMENT: LOW (z = -0.17)
 • MINIMUM_PAYMENTS: LOW (z = -0.07)

Suggested Business Label: Mixed-Behavior Customer Segment
--------------------------------------------------
In [29]:
# K-MEANS CLUSTERING

# ELBOW METHOD FOR K-MEANS

# Use noise-free data
X_elbow = scaled_dbscan_clean

Ks = range(2, 11)
inertia = []
silhouette_scores = []
dbi_scores = []

for k in Ks:
    km = KMeans(n_clusters=k, random_state=42, n_init=10)
    labels = km.fit_predict(X_elbow)

    inertia.append(km.inertia_)
    silhouette_scores.append(silhouette_score(X_elbow, labels))
    dbi_scores.append(davies_bouldin_score(X_elbow, labels))

# Elbow Plot (Inertia vs k)
plt.figure(figsize=(6,4))
plt.plot(Ks, inertia, marker='o')
plt.title("Elbow Method – K-Means (after DBSCAN noise removal)")
plt.xlabel("Number of clusters (k)")
plt.ylabel("Inertia")
plt.xticks(Ks)
plt.grid(True)
plt.show()

# Silhouette vs k
plt.figure(figsize=(6,4))
plt.plot(Ks, silhouette_scores, marker='o')
plt.title("Silhouette Score vs k – K-Means")
plt.xlabel("k")
plt.ylabel("Silhouette Score")
plt.xticks(Ks)
plt.grid(True)
plt.show()

# DBI vs k
plt.figure(figsize=(6,4))
plt.plot(Ks, dbi_scores, marker='o')
plt.title("Davies–Bouldin Index vs k – K-Means")
plt.xlabel("k")
plt.ylabel("DBI (lower is better)")
plt.xticks(Ks)
plt.grid(True)
plt.show()

print("\nK-Means Evaluation (k = 2 to 10)")
for i, k in enumerate(Ks):
    print(f"k={k} | Inertia={inertia[i]:.1f} | Silhouette={silhouette_scores[i]:.3f} | DBI={dbi_scores[i]:.3f}")
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
K-Means Evaluation (k = 2 to 10)
k=2 | Inertia=179941.0 | Silhouette=0.261 | DBI=1.859
k=3 | Inertia=149360.8 | Silhouette=0.284 | DBI=1.502
k=4 | Inertia=128777.6 | Silhouette=0.278 | DBI=1.450
k=5 | Inertia=112192.0 | Silhouette=0.295 | DBI=1.302
k=6 | Inertia=102240.2 | Silhouette=0.297 | DBI=1.178
k=7 | Inertia=93629.1 | Silhouette=0.308 | DBI=1.125
k=8 | Inertia=87744.2 | Silhouette=0.270 | DBI=1.224
k=9 | Inertia=83477.1 | Silhouette=0.267 | DBI=1.271
k=10 | Inertia=79407.7 | Silhouette=0.221 | DBI=1.340
In [31]:
# ELBOW DETECTION
K = range(2, 11)
inertia_scores = []

# Compute inertia for each k
for k in K:
    km = KMeans(n_clusters=k, random_state=42, n_init=10)
    km.fit(X_elbow)
    inertia_scores.append(km.inertia_)

# Line between first and last points
x1, y1 = 2, inertia_scores[0]
x2, y2 = 10, inertia_scores[-1]

# Distances
distances = []
for i, k in enumerate(K):
    x0, y0 = k, inertia_scores[i]

    # Distance formula
    num = abs((y2 - y1)*x0 - (x2 - x1)*y0 + x2*y1 - y2*x1)
    den = np.sqrt((y2 - y1)**2 + (x2 - x1)**2)
    distances.append(num / den)

# Elbow k = maximum curvature point
elbow_k = K[np.argmax(distances)]

print("Distances from curve:", distances)
print("------------------------------------")
print(f"Correct Optimal k by Elbow Method: {elbow_k}")

# Plot elbow graph
plt.figure(figsize=(8,5))
plt.plot(K, inertia_scores, marker='o')
plt.title("Elbow Method (Correct Maximum Curvature)")
plt.xlabel("k")
plt.ylabel("Inertia")
plt.grid(True)

# Highlight elbow
plt.scatter(elbow_k, inertia_scores[elbow_k-2], color='red', s=120, label=f"Elbow k = {elbow_k}")
plt.legend()
plt.show()
Distances from curve: [np.float64(5.789887966037872e-16), np.float64(1.4334402135282007), np.float64(2.071357902337916), np.float64(2.3911706380958435), np.float64(2.1830889037476258), np.float64(1.8683183359896198), np.float64(1.336617434369817), np.float64(0.6761686564347023), np.float64(5.789887966037872e-16)]
------------------------------------
Correct Optimal k by Elbow Method: 5
No description has been provided for this image
In [32]:
# K-MEANS SUMMARY
df_kmeans = df_hier.copy().reset_index(drop=True)

# Run K-Means on the SAME scaled data (noise-free)
k = 5
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = kmeans.fit_predict(scaled_dbscan_clean)   # scaled_dbscan_clean must also be 8082 rows

# Assign labels
df_kmeans["KMEANS"] = labels

# K-MEANS SUMMARY
print("\nK-MEANS SUMMARY")
print("\nDataset Shape (rows, columns):", df_kmeans.shape)
print("\nColumn Names:")
print(list(df_kmeans.columns))
print("\nIndex Range (Start → End):")
print("Start:", df_kmeans.index.min())
print("End:", df_kmeans.index.max())
print("\nCluster Sizes:")
print(df_kmeans["KMEANS"].value_counts().sort_index())
print("\nCluster-wise Mean Values:")
display(
    df_kmeans.groupby("KMEANS")[df_kmeans.columns].mean().round(2)
)
K-MEANS SUMMARY

Dataset Shape (rows, columns): (8320, 20)

Column Names:
['BALANCE', 'BALANCE_FREQUENCY', 'PURCHASES', 'ONEOFF_PURCHASES', 'INSTALLMENTS_PURCHASES', 'CASH_ADVANCE', 'PURCHASES_FREQUENCY', 'ONEOFF_PURCHASES_FREQUENCY', 'PURCHASES_INSTALLMENTS_FREQUENCY', 'CASH_ADVANCE_FREQUENCY', 'CASH_ADVANCE_TRX', 'PURCHASES_TRX', 'CREDIT_LIMIT', 'PAYMENTS', 'MINIMUM_PAYMENTS', 'PRC_FULL_PAYMENT', 'TENURE', 'DBSCAN_Label', 'HIER', 'KMEANS']

Index Range (Start → End):
Start: 0
End: 8319

Cluster Sizes:
KMEANS
0    1208
1    1031
2    4097
3    1148
4     836
Name: count, dtype: int64

Cluster-wise Mean Values:
BALANCE BALANCE_FREQUENCY PURCHASES ONEOFF_PURCHASES INSTALLMENTS_PURCHASES CASH_ADVANCE PURCHASES_FREQUENCY ONEOFF_PURCHASES_FREQUENCY PURCHASES_INSTALLMENTS_FREQUENCY CASH_ADVANCE_FREQUENCY CASH_ADVANCE_TRX PURCHASES_TRX CREDIT_LIMIT PAYMENTS MINIMUM_PAYMENTS PRC_FULL_PAYMENT TENURE DBSCAN_Label HIER KMEANS
KMEANS
0 4045.12 0.98 325.03 169.03 156.04 3386.48 0.27 0.10 0.18 0.40 10.38 5.70 6424.80 2369.53 1872.93 0.02 11.54 0.01 0.22 0.0
1 94.05 0.84 956.79 349.87 607.04 31.20 0.78 0.22 0.62 0.01 0.14 16.23 4473.16 1108.61 165.53 0.81 11.40 0.02 2.81 1.0
2 1176.01 0.97 426.16 221.28 205.15 430.44 0.43 0.14 0.31 0.11 1.85 8.22 3187.25 857.34 506.53 0.04 11.56 0.00 1.12 2.0
3 151.50 0.37 300.45 176.67 124.37 412.10 0.28 0.07 0.20 0.05 1.06 4.46 3497.01 846.20 181.15 0.08 11.24 0.00 2.02 3.0
4 1704.82 0.98 3093.41 2064.50 1028.97 261.06 0.91 0.65 0.69 0.05 0.96 44.50 6370.13 2719.81 598.83 0.18 11.91 0.02 3.43 4.0
In [76]:
# PCA – K-MEANS CLUSTERING

# Apply PCA on noise-free scaled data
pca_km = PCA(n_components=2, random_state=42)
X_pca_km = pca_km.fit_transform(scaled_dbscan_clean)

# Create PCA dataframe
pca_df_km = pd.DataFrame({
    "PC1": X_pca_km[:, 0],
    "PC2": X_pca_km[:, 1],
    "KMEANS": df_kmeans["KMEANS"].values
})

# Scatter plot
plt.figure(figsize=(8, 6))
sns.scatterplot(
    data=pca_df_km,
    x="PC1",
    y="PC2",
    hue="KMEANS",
    palette="Set2",
    s=40
)
plt.title("PCA – K-Means Clustering (k = 5)", fontsize=14)
plt.xlabel("Principal Component 1")
plt.ylabel("Principal Component 2")
plt.legend(title="Cluster")
plt.tight_layout()
plt.show()
No description has been provided for this image
In [34]:
# HEATMAP – K-MEANS

# Choose important business features
feature_cols_km = [
    "BALANCE",
    "PURCHASES",
    "CASH_ADVANCE",
    "CREDIT_LIMIT",
    "PAYMENTS",
    "MINIMUM_PAYMENTS",
    "PRC_FULL_PAYMENT"
]

# Cluster-wise mean values
kmeans_means = df_kmeans.groupby("KMEANS")[feature_cols_km].mean().round(2)

# Heatmap
plt.figure(figsize=(15, 7))
sns.heatmap(
    kmeans_means,
    annot=True,
    cmap="viridis",
    fmt=".2f",
    linewidths=0.5,
    linecolor="black"
)
plt.title("K-Means (k = 5) – Heatmap of Cluster Profiles")
plt.xlabel("Features")
plt.ylabel("K-Means Cluster")
plt.tight_layout()
plt.show()
No description has been provided for this image
In [35]:
# SILHOUETTE - K-MEANS
X_km = scaled_dbscan_clean
labels_km = df_kmeans["KMEANS"].values

# Silhouette score
sil_km = silhouette_score(X_km, labels_km)
print(f"K-Means Silhouette Score: {sil_km:.4f}")

# Simple bar plot
plt.figure(figsize=(5, 4))
plt.bar(["K-Means"], [sil_km], color="green")
plt.title("Silhouette Score – K-Means")
plt.ylabel("Silhouette Score (higher is better)")
plt.ylim(0, sil_km + 0.2)
plt.grid(axis="y", linestyle="--", alpha=0.6)
plt.tight_layout()
plt.show()
K-Means Silhouette Score: 0.2949
No description has been provided for this image
In [39]:
# DAVIES–BOULDIN INDEX (DBI) - K-MEANS
X_km = scaled_dbscan_clean
labels_km = df_kmeans["KMEANS"].values

# Compute DBI
dbi_km = davies_bouldin_score(X_km, labels_km)
print(f"K-Means Davies–Bouldin Index: {dbi_km:.4f}")

# Simple bar plot
plt.figure(figsize=(5, 4))
plt.bar(["K-Means"], [dbi_km], color="blue")
plt.title("Davies–Bouldin Index – K-Means")
plt.ylabel("DBI (lower is better)")
plt.ylim(0, dbi_km + 0.5)     # small headroom
plt.grid(axis='y', linestyle='--', alpha=0.6)
plt.tight_layout()
plt.show()
K-Means Davies–Bouldin Index: 1.3018
No description has been provided for this image
In [40]:
# CALINSKI–HARABASZ - K-MEANS
X_km = scaled_dbscan_clean
labels_km = df_kmeans["KMEANS"].values

# Compute CH Score
ch_km = calinski_harabasz_score(X_km, labels_km)
print(f"K-Means Calinski–Harabasz Score: {ch_km:.4f}")

# Simple bar plot
plt.figure(figsize=(5, 4))
plt.bar(["K-Means"], [ch_km], color="purple")
plt.title("Calinski–Harabasz Score – K-Means")
plt.ylabel("CH Score (higher is better)")
plt.ylim(0, ch_km + ch_km * 0.1)
plt.grid(axis="y", linestyle="--", alpha=0.6)
plt.tight_layout()
plt.show()
K-Means Calinski–Harabasz Score: 1958.1308
No description has been provided for this image
In [41]:
# CLUSTER PROFILE - K-MEANS

# Important financial features (same as DBSCAN & Hier)
kmeans_feature_cols = [
    "BALANCE",
    "PURCHASES",
    "CASH_ADVANCE",
    "CREDIT_LIMIT",
    "PAYMENTS",
    "MINIMUM_PAYMENTS",
    "PRC_FULL_PAYMENT"
]

# Compute cluster-wise mean values
kmeans_cluster_profile = (
    df_kmeans
    .groupby("KMEANS")[kmeans_feature_cols]
    .mean()
    .round(2)
)
print("\nK-MEANS CLUSTER PROFILE")
print(f"Clusters found: {len(kmeans_cluster_profile)}\n")
display(kmeans_cluster_profile)
K-MEANS CLUSTER PROFILE
Clusters found: 5

BALANCE PURCHASES CASH_ADVANCE CREDIT_LIMIT PAYMENTS MINIMUM_PAYMENTS PRC_FULL_PAYMENT
KMEANS
0 4045.12 325.03 3386.48 6424.80 2369.53 1872.93 0.02
1 94.05 956.79 31.20 4473.16 1108.61 165.53 0.81
2 1176.01 426.16 430.44 3187.25 857.34 506.53 0.04
3 151.50 300.45 412.10 3497.01 846.20 181.15 0.08
4 1704.82 3093.41 261.06 6370.13 2719.81 598.83 0.18
In [42]:
# BUSINESS INTERPRETATION - K-MEANS

# Important financial features (same as DBSCAN & Hierarchical)
kmeans_feature_cols = [
    "BALANCE",
    "PURCHASES",
    "CASH_ADVANCE",
    "CREDIT_LIMIT",
    "PAYMENTS",
    "MINIMUM_PAYMENTS",
    "PRC_FULL_PAYMENT"
]

# Work on a copy
df_kmeans_biz = df_kmeans.copy()

# Cluster-wise mean profile for K-Means
kmeans_profile = (
    df_kmeans_biz
    .groupby("KMEANS")[kmeans_feature_cols]
    .mean()
    .round(2)
)

# Overall mean & std for z-scores
overall_means_km = df_kmeans_biz[kmeans_feature_cols].mean()
overall_stds_km  = df_kmeans_biz[kmeans_feature_cols].std().replace(0, 1)

print("BUSINESS INTERPRETATION – K-MEANS")
for cluster_id, row in kmeans_profile.iterrows():
    print(f"\n--- K-Means Cluster {cluster_id} ---")
    print("Feature Means:")
    print(row)

    # Z-score for each feature in this cluster
    z = (row - overall_means_km) / overall_stds_km

    # Top 3 HIGH and LOW features
    top3_high = z.sort_values(ascending=False).head(3)
    top3_low  = z.sort_values().head(3)

    print("\nHigh Characteristics (above overall average):")
    for feat, val in top3_high.items():
        print(f" • {feat}: HIGH (z = {val:.2f})")

    print("\nLow Characteristics (below overall average):")
    for feat, val in top3_low.items():
        print(f" • {feat}: LOW (z = {val:.2f})")

    # Simple rule-based business label
    label = "Mixed-Behavior Customer Segment"

    if "CASH_ADVANCE" in top3_high.index and "PAYMENTS" in top3_low.index:
        label = "High Cash-Advance / Revolving Credit Customers"

    elif "PURCHASES" in top3_high.index and "PRC_FULL_PAYMENT" in top3_high.index:
        label = "High-Spend, Full-Payment Loyal Customers"

    elif "BALANCE" in top3_low.index and "PURCHASES" in top3_low.index:
        label = "Low-Usage / Dormant Customers"

    elif "BALANCE" in top3_high.index and "PAYMENTS" in top3_low.index:
        label = "High Balance, Low Repayment Risky Customers"

    print(f"\nSuggested Business Label: {label}")
    print("-" * 50)
BUSINESS INTERPRETATION – K-MEANS

--- K-Means Cluster 0 ---
Feature Means:
BALANCE             4045.12
PURCHASES            325.03
CASH_ADVANCE        3386.48
CREDIT_LIMIT        6424.80
PAYMENTS            2369.53
MINIMUM_PAYMENTS    1872.93
PRC_FULL_PAYMENT       0.02
Name: 0, dtype: float64

High Characteristics (above overall average):
 • CASH_ADVANCE: HIGH (z = 1.72)
 • BALANCE: HIGH (z = 1.50)
 • MINIMUM_PAYMENTS: HIGH (z = 1.21)

Low Characteristics (below overall average):
 • PRC_FULL_PAYMENT: LOW (z = -0.45)
 • PURCHASES: LOW (z = -0.39)
 • CREDIT_LIMIT: LOW (z = 0.68)

Suggested Business Label: Mixed-Behavior Customer Segment
--------------------------------------------------

--- K-Means Cluster 1 ---
Feature Means:
BALANCE               94.05
PURCHASES            956.79
CASH_ADVANCE          31.20
CREDIT_LIMIT        4473.16
PAYMENTS            1108.61
MINIMUM_PAYMENTS     165.53
PRC_FULL_PAYMENT       0.81
Name: 1, dtype: float64

High Characteristics (above overall average):
 • PRC_FULL_PAYMENT: HIGH (z = 2.27)
 • PURCHASES: HIGH (z = 0.22)
 • CREDIT_LIMIT: HIGH (z = 0.09)

Low Characteristics (below overall average):
 • BALANCE: LOW (z = -0.71)
 • CASH_ADVANCE: LOW (z = -0.50)
 • MINIMUM_PAYMENTS: LOW (z = -0.45)

Suggested Business Label: High-Spend, Full-Payment Loyal Customers
--------------------------------------------------

--- K-Means Cluster 2 ---
Feature Means:
BALANCE             1176.01
PURCHASES            426.16
CASH_ADVANCE         430.44
CREDIT_LIMIT        3187.25
PAYMENTS             857.34
MINIMUM_PAYMENTS     506.53
PRC_FULL_PAYMENT       0.04
Name: 2, dtype: float64

High Characteristics (above overall average):
 • BALANCE: HIGH (z = -0.11)
 • MINIMUM_PAYMENTS: HIGH (z = -0.12)
 • CASH_ADVANCE: HIGH (z = -0.24)

Low Characteristics (below overall average):
 • PRC_FULL_PAYMENT: LOW (z = -0.38)
 • CREDIT_LIMIT: LOW (z = -0.30)
 • PAYMENTS: LOW (z = -0.30)

Suggested Business Label: High Cash-Advance / Revolving Credit Customers
--------------------------------------------------

--- K-Means Cluster 3 ---
Feature Means:
BALANCE              151.50
PURCHASES            300.45
CASH_ADVANCE         412.10
CREDIT_LIMIT        3497.01
PAYMENTS             846.20
MINIMUM_PAYMENTS     181.15
PRC_FULL_PAYMENT       0.08
Name: 3, dtype: float64

High Characteristics (above overall average):
 • CREDIT_LIMIT: HIGH (z = -0.21)
 • PRC_FULL_PAYMENT: HIGH (z = -0.24)
 • CASH_ADVANCE: HIGH (z = -0.25)

Low Characteristics (below overall average):
 • BALANCE: LOW (z = -0.68)
 • MINIMUM_PAYMENTS: LOW (z = -0.43)
 • PURCHASES: LOW (z = -0.41)

Suggested Business Label: Low-Usage / Dormant Customers
--------------------------------------------------

--- K-Means Cluster 4 ---
Feature Means:
BALANCE             1704.82
PURCHASES           3093.41
CASH_ADVANCE         261.06
CREDIT_LIMIT        6370.13
PAYMENTS            2719.81
MINIMUM_PAYMENTS     598.83
PRC_FULL_PAYMENT       0.18
Name: 4, dtype: float64

High Characteristics (above overall average):
 • PURCHASES: HIGH (z = 2.26)
 • PAYMENTS: HIGH (z = 0.96)
 • CREDIT_LIMIT: HIGH (z = 0.66)

Low Characteristics (below overall average):
 • CASH_ADVANCE: LOW (z = -0.35)
 • MINIMUM_PAYMENTS: LOW (z = -0.03)
 • PRC_FULL_PAYMENT: LOW (z = 0.10)

Suggested Business Label: Mixed-Behavior Customer Segment
--------------------------------------------------
In [43]:
# HYBRID CLUSTERING

# SANITY CHECK – ALL INPUTS ALIGNED
print("\n[HYBRID] Shape checks (rows should all match):")
print("df_dbscan_clean rows    :", df_dbscan_clean.shape[0])
print("df_hier rows            :", df_hier.shape[0])
print("df_kmeans rows          :", df_kmeans.shape[0])
print("scaled_dbscan_clean rows:", scaled_dbscan_clean.shape[0])

# Align indices
df_dbscan_clean = df_dbscan_clean.reset_index(drop=True)
df_hier        = df_hier.reset_index(drop=True)
df_kmeans      = df_kmeans.reset_index(drop=True)
[HYBRID] Shape checks (rows should all match):
df_dbscan_clean rows    : 8320
df_hier rows            : 8320
df_kmeans rows          : 8320
scaled_dbscan_clean rows: 8320
In [44]:
# BUILD ONE-HOT ENCODINGS OF ALL THREE MODELS
print("\n[HYBRID] Building one-hot encodings for DBSCAN, HIER, KMEANS...")

# One-hot encodings of cluster labels from each model
dbscan_oh = pd.get_dummies(df_kmeans["DBSCAN_Label"], prefix="DB")
hier_oh   = pd.get_dummies(df_kmeans["HIER"],          prefix="HIER")
km_oh     = pd.get_dummies(df_kmeans["KMEANS"],        prefix="KM")

print("[HYBRID] One-hot shapes:")
print("  DBSCAN:", dbscan_oh.shape)
print("  HIER  :", hier_oh.shape)
print("  KMEANS:", km_oh.shape)
[HYBRID] Building one-hot encodings for DBSCAN, HIER, KMEANS...
[HYBRID] One-hot shapes:
  DBSCAN: (8320, 5)
  HIER  : (8320, 5)
  KMEANS: (8320, 5)
In [45]:
# Combine Original Scaled Features + One-Hot Encodings to Build Hybrid Feature Matrix
print("\n[HYBRID] Combining original scaled features + one-hot cluster encodings...")

# Original scaled financial features
orig_scaled_df = pd.DataFrame(
    scaled_dbscan_clean,
    index=df_kmeans.index
)

# Concatenate along columns: [original_scaled_features | DB one-hot | HIER one-hot | KM one-hot]
hybrid_full_df = pd.concat([orig_scaled_df, dbscan_oh, hier_oh, km_oh], axis=1)
hybrid_features = hybrid_full_df.values
print("[HYBRID] Final hybrid feature matrix shape:", hybrid_features.shape)
[HYBRID] Combining original scaled features + one-hot cluster encodings...
[HYBRID] Final hybrid feature matrix shape: (8320, 32)
In [77]:
# SEARCH BEST k FOR HYBRID

# Define range of k values to test
Ks_h = range(3, 10)

# Lists to store metrics for each k
inertia_h    = []
silhouette_h = []
dbi_h_vals   = []
ch_h_vals    = []

print("\n[HYBRID] Searching best k for Hybrid")

# Loop over each k and compute all 4 metrics
for k in Ks_h:
    # Create K-Means model for current k
    km_h = KMeans(n_clusters=k, random_state=42, n_init=10)

    # Fit on hybrid_features and get labels
    lbls_h = km_h.fit_predict(hybrid_features)

    # Inertia (sum of squared distances to cluster centers)
    inertia_h.append(km_h.inertia_)

    # Silhouette Score (higher = better)
    sil = silhouette_score(hybrid_features, lbls_h)
    silhouette_h.append(sil)

    # Davies–Bouldin Index (lower = better)
    dbi = davies_bouldin_score(hybrid_features, lbls_h)
    dbi_h_vals.append(dbi)

    # Calinski–Harabasz Score (higher = better)
    ch = calinski_harabasz_score(hybrid_features, lbls_h)
    ch_h_vals.append(ch)

# Elbow detection code that defines elbow_k_hybrid
inertia_arr = np.array(inertia_h)
d1 = np.diff(inertia_arr)
d2 = np.diff(d1)
best_elbow_index = np.argmax(-d2)
elbow_k_hybrid = list(Ks_h)[best_elbow_index + 1]


# Print numeric results in a small table
print("\n[HYBRID] k-results on hybrid feature space:")
print("k | Inertia       | Silhouette | DBI      | CH")
print("--|---------------|------------|----------|-------------")
for i, k in enumerate(Ks_h):
    print(
        f"{k} | {inertia_h[i]:<13.1f} | "
        f"{silhouette_h[i]:<10.3f} | "
        f"{dbi_h_vals[i]:<8.3f} | "
        f"{ch_h_vals[i]:<11.1f}"
    )

# Select best k based on highest Silhouette
best_k_hybrid = list(Ks_h)[np.argmax(silhouette_h)]
print(f"\n[HYBRID] Best k by Silhouette on hybrid features = {best_k_hybrid}")

# Convert Ks_h to a list for plotting
Ks_list = list(Ks_h)

# Inertia vs k (Elbow)
plt.figure(figsize=(7, 5))
plt.plot(Ks_list, inertia_h, marker='o')
plt.title("Hybrid – Inertia vs Number of Clusters")
plt.xlabel("Number of clusters (k)")
plt.ylabel("Inertia (lower is better)")
plt.grid(True)
plt.tight_layout()
plt.show()

# Davies–Bouldin Index vs k
plt.figure(figsize=(7, 5))
plt.plot(Ks_list, dbi_h_vals, marker='o')
plt.title("Hybrid – Davies–Bouldin Index vs Number of Clusters")
plt.xlabel("Number of clusters (k)")
plt.ylabel("DBI (lower is better)")
plt.grid(True)
plt.tight_layout()
plt.show()

# Calinski–Harabasz Score vs k
plt.figure(figsize=(7, 5))
plt.plot(Ks_list, ch_h_vals, marker='o')
plt.title("Hybrid – Calinski–Harabasz Score vs Number of Clusters")
plt.xlabel("Number of clusters (k)")
plt.ylabel("CH Score (higher is better)")
plt.grid(True)
plt.tight_layout()
plt.show()

# HYBRID – Silhouette vs k plot
plt.figure(figsize=(8, 5))

# Plot silhouette curve
plt.plot(list(Ks_h), silhouette_h, marker='o', linewidth=2, color='blue')

# Highlight BEST silhouette point in RED
best_k_sil = list(Ks_h)[np.argmax(silhouette_h)]
best_sil_value = max(silhouette_h)
plt.scatter(
    best_k_sil,
    best_sil_value,
    color='red',
    s=120,
    label=f"Best Silhouette k = {best_k_sil}"
)

plt.title("Hybrid – Silhouette Score vs k", fontsize=14)
plt.xlabel("Number of clusters (k)", fontsize=12)
plt.ylabel("Silhouette Score (Higher is Better)", fontsize=12)
plt.grid(True, linestyle='--', alpha=0.6)
plt.legend()
plt.show()

# FINAL k SELECTION
# Best k from Silhouette (primary metric)
best_k_silhouette = list(Ks_h)[np.argmax(silhouette_h)]
print(f"\n[HYBRID] Best k by Silhouette Score = {best_k_silhouette}")

# Best k from Elbow (inertia)
best_k_elbow = elbow_k_hybrid   # from elbow detection block
print(f"[HYBRID] Best k by Elbow Method     = {best_k_elbow}")

#  Silhouette is the most reliable, so we choose it as final k.
best_k_hybrid = best_k_silhouette
print(f"\n FINAL SELECTED k FOR HYBRID = {best_k_hybrid}\n")
[HYBRID] Searching best k for Hybrid

[HYBRID] k-results on hybrid feature space:
k | Inertia       | Silhouette | DBI      | CH
--|---------------|------------|----------|-------------
3 | 156970.4      | 0.292      | 1.494    | 1924.6     
4 | 133674.9      | 0.291      | 1.412    | 1989.5     
5 | 114897.0      | 0.315      | 1.254    | 2075.5     
6 | 105046.6      | 0.319      | 1.147    | 1971.8     
7 | 96540.5       | 0.321      | 1.106    | 1909.8     
8 | 91256.2       | 0.320      | 1.140    | 1800.4     
9 | 87081.5       | 0.283      | 1.273    | 1700.4     

[HYBRID] Best k by Silhouette on hybrid features = 7
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
[HYBRID] Best k by Silhouette Score = 7
[HYBRID] Best k by Elbow Method     = 8

 FINAL SELECTED k FOR HYBRID = 7

In [48]:
# FINAL HYBRID CLUSTERING WITH SELECTED k
final_k_hybrid = best_k_hybrid

print(f"\n[HYBRID] Running final K-Means on hybrid features with k = {final_k_hybrid}...")

# Fit K-Means on the HYBRID feature matrix
hybrid_kmeans = KMeans(
    n_clusters=final_k_hybrid,
    random_state=42,
    n_init=10
)
hybrid_labels = hybrid_kmeans.fit_predict(hybrid_features)

# Attach HYBRID labels to a copy of df_kmeans
df_hybrid = df_kmeans.copy()
df_hybrid["HYBRID"] = hybrid_labels

# Basic summary of Hybrid clusters
print("\n[HYBRID] Final cluster summary")
print("Unique HYBRID clusters:", np.unique(hybrid_labels))
print("\nCluster sizes:")
print(df_hybrid["HYBRID"].value_counts().sort_index())
print("\nHybrid DataFrame shape:", df_hybrid.shape)
print("\nColumns in df_hybrid:")
print(df_hybrid.columns.tolist())
display(df_hybrid.head())
[HYBRID] Running final K-Means on hybrid features with k = 7...

[HYBRID] Final cluster summary
Unique HYBRID clusters: [0 1 2 3 4 5 6]

Cluster sizes:
HYBRID
0    1061
1     831
2    1010
3    1088
4     494
5      78
6    3758
Name: count, dtype: int64

Hybrid DataFrame shape: (8320, 21)

Columns in df_hybrid:
['BALANCE', 'BALANCE_FREQUENCY', 'PURCHASES', 'ONEOFF_PURCHASES', 'INSTALLMENTS_PURCHASES', 'CASH_ADVANCE', 'PURCHASES_FREQUENCY', 'ONEOFF_PURCHASES_FREQUENCY', 'PURCHASES_INSTALLMENTS_FREQUENCY', 'CASH_ADVANCE_FREQUENCY', 'CASH_ADVANCE_TRX', 'PURCHASES_TRX', 'CREDIT_LIMIT', 'PAYMENTS', 'MINIMUM_PAYMENTS', 'PRC_FULL_PAYMENT', 'TENURE', 'DBSCAN_Label', 'HIER', 'KMEANS', 'HYBRID']
BALANCE BALANCE_FREQUENCY PURCHASES ONEOFF_PURCHASES INSTALLMENTS_PURCHASES CASH_ADVANCE PURCHASES_FREQUENCY ONEOFF_PURCHASES_FREQUENCY PURCHASES_INSTALLMENTS_FREQUENCY CASH_ADVANCE_FREQUENCY ... PURCHASES_TRX CREDIT_LIMIT PAYMENTS MINIMUM_PAYMENTS PRC_FULL_PAYMENT TENURE DBSCAN_Label HIER KMEANS HYBRID
0 40.900749 0.818182 95.40 0.00 95.4 0.000000 0.166667 0.000000 0.083333 0.000000 ... 2 1000.0 201.802084 139.509787 0.000000 12 0 2 2 6
1 3202.467416 0.909091 0.00 0.00 0.0 6442.945483 0.000000 0.000000 0.000000 0.250000 ... 0 7000.0 4103.032597 1072.340217 0.222222 12 0 0 0 3
2 2495.148862 1.000000 773.17 773.17 0.0 0.000000 1.000000 1.000000 0.000000 0.000000 ... 12 7500.0 622.066742 627.284787 0.000000 12 0 1 2 6
3 1666.670542 0.636364 1499.00 1499.00 0.0 205.788017 0.083333 0.083333 0.000000 0.083333 ... 1 7500.0 0.000000 312.343947 0.000000 12 0 2 3 0
4 817.714335 1.000000 16.00 16.00 0.0 0.000000 0.083333 0.083333 0.000000 0.000000 ... 1 1200.0 678.334763 244.791237 0.000000 12 0 1 2 6

5 rows × 21 columns

In [78]:
# PCA VISUALISATION – HYBRID CLUSTERING

# PCA on ORIGINAL SCALED DATA
pca_hybrid = PCA(n_components=2, random_state=42)
X_pca_hybrid = pca_hybrid.fit_transform(scaled_dbscan_clean)

# Build PCA DataFrame for plotting
pca_df_hybrid = pd.DataFrame({
    "PC1": X_pca_hybrid[:, 0],
    "PC2": X_pca_hybrid[:, 1],
    "HYBRID": df_hybrid["HYBRID"].values   # final hybrid labels
})

plt.figure(figsize=(10, 6))
sns.scatterplot(
    data=pca_df_hybrid,
    x="PC1",
    y="PC2",
    hue="HYBRID",
    palette="Set1",
    s=35,
    alpha=0.8
)

plt.title(f"PCA – Hybrid Clustering (k = {best_k_hybrid})", fontsize=14)
plt.xlabel("Principal Component 1", fontsize=12)
plt.ylabel("Principal Component 2", fontsize=12)
plt.legend(title="Hybrid Cluster", bbox_to_anchor=(1.05, 1), loc='upper left')
plt.grid(True, linestyle="--", alpha=0.4)
plt.tight_layout()
plt.show()
No description has been provided for this image
In [50]:
# HEATMAP - HYBRID

# Financial feature columns
hybrid_feature_cols = [
    "BALANCE",
    "PURCHASES",
    "CASH_ADVANCE",
    "CREDIT_LIMIT",
    "PAYMENTS",
    "MINIMUM_PAYMENTS",
    "PRC_FULL_PAYMENT"
]

# Compute cluster means
hybrid_profile = (
    df_hybrid
    .groupby("HYBRID")[hybrid_feature_cols]
    .mean()
    .round(2)
)

# Heatmap Visualization
plt.figure(figsize=(15, 7))
sns.heatmap(
    hybrid_profile,
    annot=True,
    cmap="viridis",
    fmt=".2f",
    linewidths=0.5,
    linecolor="black",
    cbar_kws={"label": "Mean Value"}
)

plt.title(f"Hybrid Clustering – Cluster Profile Heatmap (k = {best_k_hybrid})", fontsize=15)
plt.xlabel("Financial Features", fontsize=12)
plt.ylabel("Hybrid Cluster", fontsize=12)
plt.xticks(rotation=45, ha='right')
plt.yticks(rotation=0)
plt.tight_layout()
plt.show()
No description has been provided for this image
In [79]:
# SILHOUETTE - HYBRID

# Use original scaled features for evaluation
X_hybrid_eval = scaled_dbscan_clean    
hybrid_labels  = df_hybrid["HYBRID"].values

# Overall Silhouette Score
sil_hybrid = silhouette_score(X_hybrid_eval, hybrid_labels)
print(f"Hybrid Silhouette Score (k = {best_k_hybrid}): {sil_hybrid:.4f}")

# Per-sample Silhouette values
sil_values = silhouette_samples(X_hybrid_eval, hybrid_labels)

# Unique clusters
cluster_ids = np.unique(hybrid_labels)
n_clusters  = len(cluster_ids)

# Silhouette Plot
plt.figure(figsize=(10, 6))

y_lower = 10  # starting position on y-axis

for c in cluster_ids:
    # Take silhouette values for cluster c
    c_sil_vals = sil_values[hybrid_labels == c]
    c_sil_vals.sort()

    size_c = c_sil_vals.shape[0]
    y_upper = y_lower + size_c

    # Fill region for this cluster
    plt.fill_betweenx(
        np.arange(y_lower, y_upper),
        0,
        c_sil_vals,
        alpha=0.7
    )

    # Label cluster number on the plot
    plt.text(-0.05, y_lower + 0.5 * size_c, str(c))

    # Next cluster starts a bit below
    y_lower = y_upper + 10

# Vertical line for overall silhouette
plt.axvline(x=sil_hybrid, color="red", linestyle="--",
            label=f"Overall Silhouette = {sil_hybrid:.3f}")

plt.title(f"Silhouette Plot – Hybrid Clustering (k = {best_k_hybrid})")
plt.xlabel("Silhouette Coefficient")
plt.ylabel("Cluster")
plt.xlim([-0.2, 1.0])
plt.legend(loc="upper right")
plt.grid(True, axis='x', linestyle='--', alpha=0.4)
plt.tight_layout()
plt.show()
Hybrid Silhouette Score (k = 7): 0.3062
No description has been provided for this image
In [52]:
# Davies–Bouldin Index (DBI) - HYBRID

# Use original financial features for evaluation
X_hybrid_eval = scaled_dbscan_clean
hybrid_labels = df_hybrid["HYBRID"].values

# Compute DBI
dbi_hybrid = davies_bouldin_score(X_hybrid_eval, hybrid_labels)
print(f"Hybrid DBI Score (k = {best_k_hybrid}): {dbi_hybrid:.4f}")

# Simple DBI visualisation (bar chart)
plt.figure(figsize=(6, 4))
plt.bar(["HYBRID"], [dbi_hybrid], color="purple")
plt.title(f"Hybrid – DBI Score (k = {best_k_hybrid})")
plt.ylabel("Davies–Bouldin Index (Lower is Better)")
plt.grid(axis="y", linestyle="--", alpha=0.5)
plt.tight_layout()
plt.show()
Hybrid DBI Score (k = 7): 1.1273
No description has been provided for this image
In [53]:
# Calinski–Harabasz (CH) - HYBRID
# Use original scaled features
X_hybrid_eval = scaled_dbscan_clean
hybrid_labels = df_hybrid["HYBRID"].values

# Compute CH Score
ch_hybrid = calinski_harabasz_score(X_hybrid_eval, hybrid_labels)
print(f"Hybrid Calinski–Harabasz Score (k = {best_k_hybrid}): {ch_hybrid:.4f}")

# Simple CH bar plot
plt.figure(figsize=(6, 4))
plt.bar(["HYBRID"], [ch_hybrid], color="green")
plt.title(f"Hybrid – Calinski–Harabasz Score (k = {best_k_hybrid})")
plt.ylabel("Calinski–Harabasz Score (Higher is Better)")
plt.grid(axis="y", linestyle="--", alpha=0.5)
plt.tight_layout()
plt.show()
Hybrid Calinski–Harabasz Score (k = 7): 1836.1542
No description has been provided for this image
In [54]:
# CLUSTER PROFILE - HYBRID

# Important financial features (same list as other models)
hybrid_feature_cols = [
    "BALANCE",
    "PURCHASES",
    "CASH_ADVANCE",
    "CREDIT_LIMIT",
    "PAYMENTS",
    "MINIMUM_PAYMENTS",
    "PRC_FULL_PAYMENT"
]

# Compute cluster-wise mean values for HYBRID clusters
hybrid_cluster_profile = (
    df_hybrid
    .groupby("HYBRID")[hybrid_feature_cols]
    .mean()
    .round(2)
)

print("\nHYBRID CLUSTER PROFILE")
print(f"Clusters found: {len(hybrid_cluster_profile)}\n")
display(hybrid_cluster_profile)
HYBRID CLUSTER PROFILE
Clusters found: 7

BALANCE PURCHASES CASH_ADVANCE CREDIT_LIMIT PAYMENTS MINIMUM_PAYMENTS PRC_FULL_PAYMENT
HYBRID
0 131.71 307.91 373.19 3590.74 876.02 181.01 0.09
1 1700.21 3095.74 255.67 6370.50 2726.65 602.89 0.18
2 94.31 968.04 30.27 4541.22 1123.70 167.13 0.82
3 4213.85 319.05 3596.30 6768.71 2547.75 1424.52 0.02
4 939.13 320.02 1130.07 2346.36 484.67 329.03 0.06
5 2135.76 377.32 387.21 2621.79 1133.39 8059.12 0.01
6 1207.47 437.58 382.56 3287.10 886.93 535.95 0.03
In [55]:
# BUSINESS INTERPRETATION - HYBRID

# Important financial features (same as other models)
hybrid_feature_cols = [
    "BALANCE",
    "PURCHASES",
    "CASH_ADVANCE",
    "CREDIT_LIMIT",
    "PAYMENTS",
    "MINIMUM_PAYMENTS",
    "PRC_FULL_PAYMENT"
]

# Work on a copy of the hybrid DF
df_hybrid_biz = df_hybrid.copy()

# Cluster-wise mean profile
hybrid_profile = (
    df_hybrid_biz
    .groupby("HYBRID")[hybrid_feature_cols]
    .mean()
    .round(2)
)

# Overall mean & std (for z-scores)
overall_means_hy = df_hybrid_biz[hybrid_feature_cols].mean()
overall_stds_hy  = df_hybrid_biz[hybrid_feature_cols].std().replace(0, 1)

print("BUSINESS INTERPRETATION – HYBRID")

for cluster_id, row in hybrid_profile.iterrows():
    print(f"\n--- Hybrid Cluster {cluster_id} ---")
    print("Feature Means:")
    print(row)

    # Z-score calculation
    z = (row - overall_means_hy) / overall_stds_hy

    # Top 3 high & low features
    top3_high = z.sort_values(ascending=False).head(3)
    top3_low  = z.sort_values().head(3)

    print("\nHigh Characteristics (above dataset average):")
    for feat, val in top3_high.items():
        print(f" • {feat}: HIGH (z = {val:.2f})")

    print("\nLow Characteristics (below dataset average):")
    for feat, val in top3_low.items():
        print(f" • {feat}: LOW (z = {val:.2f})")

    # Suggested Rule-based Label
    label = "Mixed-Behavior Customer Segment"

    if "CASH_ADVANCE" in top3_high.index and "PAYMENTS" in top3_low.index:
        label = "High Cash-Advance / Revolving Credit Customers"

    elif "PURCHASES" in top3_high.index and "PRC_FULL_PAYMENT" in top3_high.index:
        label = "High-Spend, Full-Payment Loyal Customers"

    elif "BALANCE" in top3_low.index and "PURCHASES" in top3_low.index:
        label = "Low-Usage / Dormant Customers"

    elif "BALANCE" in top3_high.index and "PAYMENTS" in top3_low.index:
        label = "High Balance, Low Repayment Risky Customers"

    print(f"\nSuggested Business Label: {label}")
    print("-" * 50)
BUSINESS INTERPRETATION – HYBRID

--- Hybrid Cluster 0 ---
Feature Means:
BALANCE              131.71
PURCHASES            307.91
CASH_ADVANCE         373.19
CREDIT_LIMIT        3590.74
PAYMENTS             876.02
MINIMUM_PAYMENTS     181.01
PRC_FULL_PAYMENT       0.09
Name: 0, dtype: float64

High Characteristics (above dataset average):
 • CREDIT_LIMIT: HIGH (z = -0.18)
 • PRC_FULL_PAYMENT: HIGH (z = -0.21)
 • CASH_ADVANCE: HIGH (z = -0.28)

Low Characteristics (below dataset average):
 • BALANCE: LOW (z = -0.69)
 • MINIMUM_PAYMENTS: LOW (z = -0.43)
 • PURCHASES: LOW (z = -0.40)

Suggested Business Label: Low-Usage / Dormant Customers
--------------------------------------------------

--- Hybrid Cluster 1 ---
Feature Means:
BALANCE             1700.21
PURCHASES           3095.74
CASH_ADVANCE         255.67
CREDIT_LIMIT        6370.50
PAYMENTS            2726.65
MINIMUM_PAYMENTS     602.89
PRC_FULL_PAYMENT       0.18
Name: 1, dtype: float64

High Characteristics (above dataset average):
 • PURCHASES: HIGH (z = 2.26)
 • PAYMENTS: HIGH (z = 0.97)
 • CREDIT_LIMIT: HIGH (z = 0.66)

Low Characteristics (below dataset average):
 • CASH_ADVANCE: LOW (z = -0.35)
 • MINIMUM_PAYMENTS: LOW (z = -0.02)
 • PRC_FULL_PAYMENT: LOW (z = 0.10)

Suggested Business Label: Mixed-Behavior Customer Segment
--------------------------------------------------

--- Hybrid Cluster 2 ---
Feature Means:
BALANCE               94.31
PURCHASES            968.04
CASH_ADVANCE          30.27
CREDIT_LIMIT        4541.22
PAYMENTS            1123.70
MINIMUM_PAYMENTS     167.13
PRC_FULL_PAYMENT       0.82
Name: 2, dtype: float64

High Characteristics (above dataset average):
 • PRC_FULL_PAYMENT: HIGH (z = 2.31)
 • PURCHASES: HIGH (z = 0.23)
 • CREDIT_LIMIT: HIGH (z = 0.11)

Low Characteristics (below dataset average):
 • BALANCE: LOW (z = -0.71)
 • CASH_ADVANCE: LOW (z = -0.50)
 • MINIMUM_PAYMENTS: LOW (z = -0.45)

Suggested Business Label: High-Spend, Full-Payment Loyal Customers
--------------------------------------------------

--- Hybrid Cluster 3 ---
Feature Means:
BALANCE             4213.85
PURCHASES            319.05
CASH_ADVANCE        3596.30
CREDIT_LIMIT        6768.71
PAYMENTS            2547.75
MINIMUM_PAYMENTS    1424.52
PRC_FULL_PAYMENT       0.02
Name: 3, dtype: float64

High Characteristics (above dataset average):
 • CASH_ADVANCE: HIGH (z = 1.86)
 • BALANCE: HIGH (z = 1.59)
 • PAYMENTS: HIGH (z = 0.85)

Low Characteristics (below dataset average):
 • PRC_FULL_PAYMENT: LOW (z = -0.45)
 • PURCHASES: LOW (z = -0.39)
 • MINIMUM_PAYMENTS: LOW (z = 0.78)

Suggested Business Label: Mixed-Behavior Customer Segment
--------------------------------------------------

--- Hybrid Cluster 4 ---
Feature Means:
BALANCE              939.13
PURCHASES            320.02
CASH_ADVANCE        1130.07
CREDIT_LIMIT        2346.36
PAYMENTS             484.67
MINIMUM_PAYMENTS     329.03
PRC_FULL_PAYMENT       0.06
Name: 4, dtype: float64

High Characteristics (above dataset average):
 • CASH_ADVANCE: HIGH (z = 0.22)
 • BALANCE: HIGH (z = -0.24)
 • MINIMUM_PAYMENTS: HIGH (z = -0.29)

Low Characteristics (below dataset average):
 • CREDIT_LIMIT: LOW (z = -0.55)
 • PAYMENTS: LOW (z = -0.55)
 • PURCHASES: LOW (z = -0.39)

Suggested Business Label: High Cash-Advance / Revolving Credit Customers
--------------------------------------------------

--- Hybrid Cluster 5 ---
Feature Means:
BALANCE             2135.76
PURCHASES            377.32
CASH_ADVANCE         387.21
CREDIT_LIMIT        2621.79
PAYMENTS            1133.39
MINIMUM_PAYMENTS    8059.12
PRC_FULL_PAYMENT       0.01
Name: 5, dtype: float64

High Characteristics (above dataset average):
 • MINIMUM_PAYMENTS: HIGH (z = 7.24)
 • BALANCE: HIGH (z = 0.43)
 • PAYMENTS: HIGH (z = -0.11)

Low Characteristics (below dataset average):
 • PRC_FULL_PAYMENT: LOW (z = -0.48)
 • CREDIT_LIMIT: LOW (z = -0.47)
 • PURCHASES: LOW (z = -0.34)

Suggested Business Label: Mixed-Behavior Customer Segment
--------------------------------------------------

--- Hybrid Cluster 6 ---
Feature Means:
BALANCE             1207.47
PURCHASES            437.58
CASH_ADVANCE         382.56
CREDIT_LIMIT        3287.10
PAYMENTS             886.93
MINIMUM_PAYMENTS     535.95
PRC_FULL_PAYMENT       0.03
Name: 6, dtype: float64

High Characteristics (above dataset average):
 • MINIMUM_PAYMENTS: HIGH (z = -0.09)
 • BALANCE: HIGH (z = -0.09)
 • CREDIT_LIMIT: HIGH (z = -0.27)

Low Characteristics (below dataset average):
 • PRC_FULL_PAYMENT: LOW (z = -0.42)
 • PURCHASES: LOW (z = -0.28)
 • PAYMENTS: LOW (z = -0.28)

Suggested Business Label: High Balance, Low Repayment Risky Customers
--------------------------------------------------
In [56]:
# MODEL COMPARISON TABLE

# DBSCAN
metrics_dbscan = {
    "Model": "DBSCAN",
    "k (clusters)": len(np.unique(df_dbscan_clean["DBSCAN_Label"])),
    "Silhouette Score": sil_dbscan,
    "Davies–Bouldin Index": dbi_dbscan,
    "Calinski–Harabasz Score": ch_dbscan
}

# HIERARCHICAL
metrics_hier = {
    "Model": "Hierarchical (Ward)",
    "k (clusters)": len(np.unique(df_hier["HIER"])),
    "Silhouette Score": sil_score_hier,      # your variable name
    "Davies–Bouldin Index": dbi_hier,
    "Calinski–Harabasz Score": ch_hier
}

# K-MEANS (use your variable names: sil_km, dbi_km, ch_km)
metrics_kmeans = {
    "Model": "K-Means",
    "k (clusters)": len(np.unique(df_kmeans["KMEANS"])),
    "Silhouette Score": sil_km,
    "Davies–Bouldin Index": dbi_km,
    "Calinski–Harabasz Score": ch_km
}

# HYBRID
metrics_hybrid = {
    "Model": "Hybrid",
    "k (clusters)": best_k_hybrid,
    "Silhouette Score": sil_hybrid,
    "Davies–Bouldin Index": dbi_hybrid,
    "Calinski–Harabasz Score": ch_hybrid
}

# Build DataFrame
comparison_df = pd.DataFrame([
    metrics_dbscan,
    metrics_hier,
    metrics_kmeans,
    metrics_hybrid
])

comparison_df = comparison_df.round(4)

# Display table
print("\nMODEL COMPARISON – INTERNAL VALIDATION METRICS")
display(comparison_df)
MODEL COMPARISON – INTERNAL VALIDATION METRICS
Model k (clusters) Silhouette Score Davies–Bouldin Index Calinski–Harabasz Score
0 DBSCAN 5 0.2843 0.5675 34.4979
1 Hierarchical (Ward) 5 0.2118 1.4862 1532.2808
2 K-Means 5 0.2949 1.3018 1958.1308
3 Hybrid 7 0.3062 1.1273 1836.1542
In [57]:
# COMPARISON GRAPHS
models = ["DBSCAN", "HIER", "K-MEANS", "HYBRID"]

# 1) Collect metric values (make sure these vars are already defined)
sil_values = [
    sil_dbscan,
    sil_score_hier,
    sil_km,
    sil_hybrid
]

dbi_values = [
    dbi_dbscan,
    dbi_hier,
    dbi_km,
    dbi_hybrid
]

ch_values = [
    ch_dbscan,
    ch_hier,
    ch_km,
    ch_hybrid
]

# SILHOUETTE SCORE
plt.figure(figsize=(8, 5))
plt.bar(models, sil_values)
plt.title("Silhouette Score Comparison (Higher is Better)")
plt.ylabel("Silhouette Score")
plt.grid(axis="y", linestyle="--", alpha=0.5)
plt.tight_layout()
plt.show()

# DAVIES–BOULDIN INDEX
plt.figure(figsize=(8, 5))
plt.bar(models, dbi_values)
plt.title("Davies–Bouldin Index Comparison (Lower is Better)")
plt.ylabel("DBI Score")
plt.grid(axis="y", linestyle="--", alpha=0.5)
plt.tight_layout()
plt.show()

# CALINSKI–HARABASZ SCORE
plt.figure(figsize=(8, 5))
plt.bar(models, ch_values)
plt.title("Calinski–Harabasz Score Comparison (Higher is Better)")
plt.ylabel("CH Score")
plt.grid(axis="y", linestyle="--", alpha=0.5)
plt.tight_layout()
plt.show()
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 [ ]: