Clustering customer segments
You've got a database of 100,000 customers. Marketing wants personalized campaigns. One strategy per customer is impossible. One strategy for all of them is wasteful. The middle ground is segmentation: group customers into clusters that share behavior, then tailor your approach to each group.
This is where unsupervised learning earns its keep. You don't need labels. The algorithm finds the structure on its own.
Why unsupervised learning
Supervised approaches require someone to define the segments upfront — "high value," "at risk," "new." That bakes in human bias and misses patterns nobody thought to look for. Clustering lets the data speak. You might find a segment that buys rarely but spends a lot per order, or a group that engages daily and never converts. Predefined labels would never surface either of those.
K-Means: the workhorse
K-Means is the algorithm I reach for first. It's fast, the math is intuitive, and it works fine when your clusters are roughly spherical and similar in size.
The loop is short:
- Pick K initial centroids (random, or via K-means++)
- Assign each point to its nearest centroid
- Recompute each centroid as the mean of its assigned points
- Repeat 2 and 3 until nothing moves
Here it is running on real data. Press play — this is an actual k-means run, deliberately started from a bad init (all four centroids clumped in one corner) so you can watch the reassignment do its work:
Lloyd's algorithm on 150 points. The crosses are the centroids; each iteration moves them to the mean of their assigned points and re-colors every point by its nearest centroid. Note how fast it converges — 27 points switch clusters on the first pass, then 11, 6, 2, and it's essentially done by iteration 5. The inertia (total squared distance to centroids) drops from 6,108 to 856 and flattens. That flattening is exactly what the elbow method below looks for. ```python import pandas as pd import numpy as np from sklearn.cluster import KMeans from sklearn.preprocessing import StandardScaler # Load and prepare customer data df = pd.read_csv('customers.csv') features = df[['recency', 'frequency', 'monetary', 'avg_session_duration', 'support_tickets', 'days_since_signup']] # Scaling is critical - K-Means uses Euclidean distance scaler = StandardScaler() X = scaler.fit_transform(features) # Fit K-Means kmeans = KMeans(n_clusters=5, init='k-means++', n_init=10, random_state=42) df['cluster'] = kmeans.fit_predict(X)
One detail you cannot skip: K-Means uses Euclidean distance, so every feature has to be on the same scale. A "monetary" column in dollars (range 0 to 10,000) will steamroll a "frequency" column (range 0 to 50) unless you standardize first. StandardScaler centers each feature at zero with unit variance, which is what you want.
Choosing K: elbow method and silhouette scores
Picking K is the hardest part of K-Means. Two methods get you most of the way there.
The elbow method plots the within-cluster sum of squares (inertia) against K. As K grows, inertia drops. The "elbow" is where the drop flattens out and adding more clusters stops paying off.
import matplotlib.pyplot as plt
inertias = []
K_range = range(2, 11)
for k in K_range:
km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42)
km.fit(X)
inertias.append(km.inertia_)
plt.plot(K_range, inertias, 'bo-')
plt.xlabel('Number of Clusters (K)')
plt.ylabel('Inertia')
plt.title('Elbow Method')
plt.show()The elbow is often ambiguous on real data. When it is, fall back to the silhouette score. For each point, the silhouette coefficient compares how similar it is to its own cluster against the nearest neighboring cluster. Scores run from -1 (wrong cluster) to 1 (well placed). Average across all points and you get one number to compare different K values.
from sklearn.metrics import silhouette_score
for k in K_range:
km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42)
labels = km.fit_predict(X)
score = silhouette_score(X, labels)
print(f"K={k}: Silhouette Score = {score:.3f}")In practice I look at both and then ask whether the result is operationally useful. K=7 might score slightly better than K=5, but if marketing can't run seven distinct campaigns, ship with K=5.
Feature engineering: RFM analysis
Raw transactions aren't ready to cluster. The standard starting point for customer segmentation is RFM analysis:
- Recency: days since last purchase. Lower is better.
- Frequency: number of purchases in a window. Higher means more engaged.
- Monetary: total spend. Your revenue signal.
from datetime import datetime
snapshot_date = datetime(2025, 10, 1)
rfm = df.groupby('customer_id').agg({
'purchase_date': lambda x: (snapshot_date - x.max()).days, # Recency
'order_id': 'nunique', # Frequency
'amount': 'sum' # Monetary
}).rename(columns={
'purchase_date': 'recency',
'order_id': 'frequency',
'amount': 'monetary'
})RFM is a starting point, not a finish line. Depending on the business, I'll layer in average order value, number of product categories purchased, customer tenure, support tickets, email open rate, or session duration. More features sometimes help, but they also blow up dimensionality and add noise. Be deliberate.
Beyond K-Means: DBSCAN for non-spherical clusters
K-Means assumes clusters are convex and roughly equal in size. Real customer data breaks both assumptions all the time. You'll have a huge cluster of casual buyers and a tight, tiny cluster of power users. K-Means will either split the big one or swallow the small one.
DBSCAN (Density-Based Spatial Clustering of Applications with Noise) finds clusters of arbitrary shape based on point density. It takes two parameters:
- eps: radius of the neighborhood around each point
- min_samples: the minimum number of points needed to call a region dense
Points that don't belong to any dense region get labeled as noise, which is genuinely useful. Not every customer fits a segment, and pretending they do is how you end up with garbage personas.
from sklearn.cluster import DBSCAN
dbscan = DBSCAN(eps=0.8, min_samples=10)
df['cluster'] = dbscan.fit_predict(X)
n_clusters = len(set(df['cluster'])) - (1 if -1 in df['cluster'].values else 0)
n_noise = (df['cluster'] == -1).sum()
print(f"Clusters: {n_clusters}, Noise points: {n_noise}")Tuning eps is the hard part. Too small and everything is noise. Too large and everything collapses into one cluster. A k-distance plot — sort the distance to the k-th nearest neighbor for each point, look for the elbow — usually gets you in the right neighborhood.
Interpreting and acting on clusters
Clustering isn't the end of the work, it's the beginning of the conversation with your stakeholders. For each cluster, compute summary stats:
summary = df.groupby('cluster')[['recency', 'frequency', 'monetary']].agg(['mean', 'median', 'std'])
print(summary)Then name them. A typical 5-cluster RFM segmentation often shakes out like this:
| Cluster | Recency | Frequency | Monetary | Label |
|---|---|---|---|---|
| 0 | Low | High | High | Champions |
| 1 | Low | Low | Low | New Customers |
| 2 | High | High | High | At-Risk Loyalists |
| 3 | High | Low | Low | Lost |
| 4 | Medium | Medium | Medium | Potential Loyalists |
Each one wants a different play. Champions get loyalty rewards and early access. At-Risk Loyalists get win-back campaigns before they churn. New Customers get onboarding flows. Lost customers may simply not be worth the cost to re-acquire — that's a real answer too.
Common pitfalls
Forgetting to scale. The number one mistake. Unscaled features make Euclidean distance meaningless.
Throwing in too many features. The curse of dimensionality makes distance metrics less and less informative as you add columns. Past 10 to 15 features, run PCA before clustering.
Treating clusters as permanent. Customer behavior moves. Rerun the clustering monthly or quarterly and watch how customers migrate between segments — that migration is often the most useful signal you have.
Ignoring stability. Run K-Means multiple times with different random seeds. If customers bounce between clusters across runs, the segmentation isn't robust at that K.
Wrapping up
Customer segmentation is one of the clearest cases of unsupervised learning delivering real business value. The technical pieces — K-Means, silhouette scores, RFM features — are well established. The hard part is the translation layer: turning cluster statistics into something marketing, product, and support teams can actually execute on. The best segmentation is the one that changes how the organization makes decisions, not the one with the prettiest silhouette plot.
