Clustering is unsupervised learning — finding structure in data without labels. It is used for customer segmentation, anomaly detection, document grouping, and exploratory analysis. This guide covers the three most important clustering algorithms with Python code and practical advice on choosing between them.
K-Means Clustering
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.datasets import make_blobs
from sklearn.metrics import silhouette_score, davies_bouldin_score
X, _ = make_blobs(n_samples=500, centers=4, cluster_std=0.8, random_state=42)
X_scaled = StandardScaler().fit_transform(X)
# ── Elbow method to choose K ──────────────────────────────────
inertias, sil_scores = [], []
K_range = range(2, 11)
for k in K_range:
km = KMeans(n_clusters=k, random_state=42, n_init=10)
km.fit(X_scaled)
inertias.append(km.inertia_)
sil_scores.append(silhouette_score(X_scaled, km.labels_))
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
ax1.plot(K_range, inertias, 'bo-')
ax1.set_xlabel('K'); ax1.set_ylabel('Inertia'); ax1.set_title('Elbow Method')
ax2.plot(K_range, sil_scores, 'ro-')
ax2.set_xlabel('K'); ax2.set_ylabel('Silhouette Score')
ax2.set_title('Silhouette Score (higher = better)')
plt.tight_layout(); plt.show()
# Fit with best K
best_k = K_range[np.argmax(sil_scores)]
print(f'Best K by silhouette: {best_k}')
kmeans = KMeans(n_clusters=best_k, random_state=42, n_init=10)
labels = kmeans.fit_predict(X_scaled)
centers = kmeans.cluster_centers_
print(f'Silhouette Score: {silhouette_score(X_scaled, labels):.4f}')
print(f'Davies-Bouldin: {davies_bouldin_score(X_scaled, labels):.4f}') # lower=better
# Visualise
plt.figure(figsize=(8, 6))
plt.scatter(X_scaled[:, 0], X_scaled[:, 1], c=labels, cmap='viridis', alpha=0.7)
plt.scatter(centers[:, 0], centers[:, 1], c='red', marker='X', s=200, zorder=5)
plt.title(f'K-Means (K={best_k})')
plt.show()
Customer Segmentation Example
import pandas as pd
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
# RFM features: Recency, Frequency, Monetary
rfm = pd.DataFrame({
'recency': [10, 5, 30, 2, 45, 8, 20, 1, 60, 3],
'frequency': [5, 12, 2, 20, 1, 8, 4, 25, 1, 15],
'monetary': [500, 1200, 200, 3000, 100, 800, 350, 4500, 80, 2000]
})
scaler = StandardScaler()
rfm_scaled = scaler.fit_transform(rfm)
km = KMeans(n_clusters=4, random_state=42, n_init=10)
rfm['segment'] = km.fit_predict(rfm_scaled)
# Interpret segments
segment_means = rfm.groupby('segment').mean()
print(segment_means)
# Name segments based on characteristics
segment_names = {
segment_means['monetary'].idxmax(): 'Champions',
segment_means['recency'].idxmin(): 'Loyal',
segment_means['recency'].idxmax(): 'At Risk',
}
rfm['segment_name'] = rfm['segment'].map(segment_names).fillna('New/Potential')
print(rfm['segment_name'].value_counts())
DBSCAN – Density-Based Clustering
DBSCAN finds clusters of arbitrary shape and automatically identifies outliers as noise. Unlike K-Means, you do not specify K. It works poorly in high dimensions or when clusters have very different densities.
from sklearn.cluster import DBSCAN
from sklearn.datasets import make_moons
# Non-convex shapes — K-Means fails, DBSCAN succeeds
X_moons, _ = make_moons(n_samples=300, noise=0.05, random_state=42)
# eps = neighbourhood radius; min_samples = min points to form a core point
dbscan = DBSCAN(eps=0.2, min_samples=5)
labels = dbscan.fit_predict(X_moons)
n_clusters = len(set(labels)) - (1 if -1 in labels else 0)
n_noise = (labels == -1).sum()
print(f'Clusters: {n_clusters} | Noise points: {n_noise}')
plt.figure(figsize=(8, 5))
mask = labels != -1
plt.scatter(X_moons[mask, 0], X_moons[mask, 1], c=labels[mask], cmap='viridis', alpha=0.7)
plt.scatter(X_moons[~mask, 0], X_moons[~mask, 1], c='red', marker='x', s=100, label='Noise')
plt.title('DBSCAN on Moon Dataset')
plt.legend()
plt.show()
# Choose eps with k-distance graph
from sklearn.neighbors import NearestNeighbors
nn = NearestNeighbors(n_neighbors=5)
nn.fit(X_scaled)
distances, _ = nn.kneighbors(X_scaled)
distances = np.sort(distances[:, -1])
plt.plot(distances)
plt.ylabel('5th nearest neighbour distance')
plt.xlabel('Points sorted by distance')
plt.title('K-Distance Graph — look for "elbow" = good eps')
plt.show()
Hierarchical Clustering
from sklearn.cluster import AgglomerativeClustering
from scipy.cluster.hierarchy import dendrogram, linkage
from scipy.spatial.distance import pdist
# Dendrogram to visualise cluster merging
X_small = X_scaled[:50]
Z = linkage(X_small, method='ward') # ward minimises within-cluster variance
plt.figure(figsize=(15, 5))
dendrogram(Z, leaf_rotation=90, leaf_font_size=8)
plt.title('Hierarchical Clustering Dendrogram')
plt.xlabel('Sample Index')
plt.ylabel('Distance')
plt.show()
# Look for the longest vertical line without a horizontal line crossing it
# — cut there to find the natural number of clusters
# Fit agglomerative clustering
agg = AgglomerativeClustering(n_clusters=4, linkage='ward')
labels_agg = agg.fit_predict(X_scaled)
print(f'Silhouette: {silhouette_score(X_scaled, labels_agg):.4f}')
Gaussian Mixture Models
from sklearn.mixture import GaussianMixture
# GMM is a probabilistic version of K-Means
# Gives soft assignments (probability of belonging to each cluster)
gmm = GaussianMixture(n_components=4, covariance_type='full', random_state=42)
gmm.fit(X_scaled)
labels_gmm = gmm.predict(X_scaled)
probs = gmm.predict_proba(X_scaled) # soft assignments
# BIC to choose number of components
bics = [GaussianMixture(n_components=k, random_state=42).fit(X_scaled).bic(X_scaled)
for k in range(2, 10)]
best_k = np.argmin(bics) + 2
print(f'Best K by BIC: {best_k}')
Choosing the Right Algorithm
Use K-Means when you know roughly how many clusters you want, data is roughly spherical and similar-sized, and you have a large dataset (it scales well). Use DBSCAN when clusters have arbitrary shapes, you want automatic outlier detection, and you do not know K in advance. Use hierarchical clustering when you want to explore the data at multiple granularities via dendrogram, or when the dataset is small enough for the O(n²) computation. Use Gaussian Mixture Models when you need soft cluster probabilities rather than hard assignments.
Conclusion
Clustering is an exploratory tool — the “right” number of clusters is often a business decision as much as a statistical one. Use the elbow method and silhouette score as guides, not gospel. Always visualise your clusters after fitting — if they do not make intuitive sense, try a different algorithm or different feature engineering. In customer segmentation especially, the goal is actionable segments, not mathematically optimal ones.



