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 |
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()
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()
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()
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()
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
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
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
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
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()
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()
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
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
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
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}")
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
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()
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()
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
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
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
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
[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()
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()
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
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
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
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()
In [ ]: