Day 46: K-Means Clustering: Customer Segments
✅ Today you will learn
- Clustering: groups without labels
- K-Means step by step
- Scale first, then choose k (elbow, silhouette)
- Turn clusters into marketing personas
- When to use DBSCAN or hierarchical clustering
🏢 In the real world
A Pune mall has 200 loyalty-card members with age, income and a spending score. Marketing wants to stop sending the same SMS to everyone. Nobody has labelled “types” of customers – clustering discovers them, and each group gets its own offer.
Look at the data first
import pandas as pd
import matplotlib.pyplot as plt
m = pd.read_csv("mall_customers.csv")
print(m.head()); print(m.describe().round(1).loc[["mean", "min", "max"]])
plt.figure(figsize=(7.5, 4.5))
plt.scatter(m["annual_income_lakh"], m["spending_score"], s=22, color="#64748b")
plt.xlabel("annual income (₹ lakh)"); plt.ylabel("spending score (1–100)")
plt.title("Can you see the groups?"); plt.show()
How K-Means works
- 1Pick k random centres
- 2Assign every customer to the nearest centre
- 3Move each centre to the average of its customers
- 4Repeat until centres stop moving
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
m = pd.read_csv("mall_customers.csv")
X = m[["annual_income_lakh", "spending_score"]].to_numpy()
Xs = (X - X.mean(0)) / X.std(0)
rng = np.random.default_rng(3)
centres = Xs[rng.choice(len(Xs), 5, replace=False)]
fig, ax = plt.subplots(1, 4, figsize=(14, 3.4), sharey=True)
for it in range(4):
lab = np.argmin(((Xs[:, None, :] - centres[None]) ** 2).sum(-1), axis=1)
ax[it].scatter(Xs[:, 0], Xs[:, 1], c=lab, cmap="tab10", s=10)
ax[it].scatter(centres[:, 0], centres[:, 1], c="black", marker="X", s=120)
ax[it].set_title(f"iteration {it + 1}")
centres = np.array([Xs[lab == k].mean(0) for k in range(5)])
plt.tight_layout(); plt.show()
K-Means with scikit-learn
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
m = pd.read_csv("mall_customers.csv")
X = StandardScaler().fit_transform(m[["annual_income_lakh", "spending_score"]])
km = KMeans(n_clusters=5, n_init=10, random_state=0).fit(X)
m["cluster"] = km.labels_
print(m["cluster"].value_counts().sort_index().to_dict())
print("inertia (within-cluster spread):", round(km.inertia_, 1))K-Means uses distances. Income (2–10 lakh) and score (1–100) are on different scales – without scaling, the spending score alone would decide the clusters.
Choosing k
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
m = pd.read_csv("mall_customers.csv")
X = StandardScaler().fit_transform(m[["annual_income_lakh", "spending_score"]])
ks = range(2, 11); inertia, sil = [], []
for k in ks:
km = KMeans(n_clusters=k, n_init=10, random_state=0).fit(X)
inertia.append(km.inertia_); sil.append(silhouette_score(X, km.labels_))
fig, ax = plt.subplots(1, 2, figsize=(11, 3.6))
ax[0].plot(ks, inertia, marker="o", color="#4f46e5"); ax[0].set_title("Elbow: inertia drops slowly after the bend"); ax[0].set_xlabel("k")
ax[1].plot(ks, sil, marker="o", color="#16a34a"); ax[1].set_title("Silhouette: higher is better"); ax[1].set_xlabel("k")
best = list(ks)[sil.index(max(sil))]; ax[1].axvline(best, color="#dc2626", ls="--")
plt.tight_layout(); plt.show()
print("best k by silhouette:", best, "| scores:", [round(s, 3) for s in sil])
For each customer: how close to its own cluster vs the next nearest cluster, from −1 (wrong group) to +1 (perfect). The average tells you how well separated the clusters are.
From clusters to personas
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
m = pd.read_csv("mall_customers.csv")
X = StandardScaler().fit_transform(m[["annual_income_lakh", "spending_score"]])
m["cluster"] = KMeans(n_clusters=5, n_init=10, random_state=0).fit_predict(X)
prof = m.groupby("cluster")[["age", "annual_income_lakh", "spending_score"]].mean().round(1)
prof["customers"] = m["cluster"].value_counts().sort_index()
def name(r):
hi_inc, hi_sp = r.annual_income_lakh > 6.5, r.spending_score > 60
lo_inc, lo_sp = r.annual_income_lakh < 4, r.spending_score < 40
if hi_inc and hi_sp: return "Premium spenders"
if hi_inc and lo_sp: return "Careful earners"
if lo_inc and hi_sp: return "Young trend-seekers"
if lo_inc and lo_sp: return "Budget shoppers"
return "Average regulars"
prof["persona"] = prof.apply(name, axis=1)
print(prof.to_string())
m["persona"] = m["cluster"].map(prof["persona"])
plt.figure(figsize=(8, 4.8))
for p, g in m.groupby("persona"):
plt.scatter(g["annual_income_lakh"], g["spending_score"], s=22, label=p)
plt.xlabel("annual income (₹ lakh)"); plt.ylabel("spending score"); plt.legend(fontsize=8)
plt.title("Five customer personas"); plt.show()
Other clustering methods
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import DBSCAN, AgglomerativeClustering
from sklearn.metrics import silhouette_score
m = pd.read_csv("mall_customers.csv")
X = StandardScaler().fit_transform(m[["annual_income_lakh", "spending_score"]])
db = DBSCAN(eps=0.35, min_samples=5).fit(X)
print("DBSCAN clusters:", len(set(db.labels_)) - (1 if -1 in db.labels_ else 0), "| noise points:", (db.labels_ == -1).sum())
ag = AgglomerativeClustering(n_clusters=5, linkage="ward").fit(X)
print("Hierarchical (ward) silhouette:", round(silhouette_score(X, ag.labels_), 3))| Method | Strength | Weakness |
|---|---|---|
| K-Means | Fast, simple, scales to millions | Needs k; assumes round, similar-size clusters |
| DBSCAN | Finds any shape, flags outliers (noise) | Sensitive to eps; struggles with varying density |
| Hierarchical | Dendrogram shows nested groups | Slow on large data |
🏅 Job skills unlocked today
Practice tasks
🏋️ Practice – do it yourself
Show solution
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
m = pd.read_csv("mall_customers.csv")
X = StandardScaler().fit_transform(m[["age", "annual_income_lakh", "spending_score"]])
print({k: round(silhouette_score(X, KMeans(k, n_init=10, random_state=0).fit_predict(X)), 3) for k in range(2, 9)})Show solution
import pandas as pd
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
s = pd.read_csv("sales_data.csv"); s["rev"] = s["quantity"] * s["unit_price"] * (1 - s["discount"])
c = s.groupby("city").agg(revenue=("rev", "sum"), aov=("rev", "mean"), prepaid=("payment_mode", lambda p: (p != "Cash on Delivery").mean()))
c["cluster"] = KMeans(3, n_init=10, random_state=0).fit_predict(StandardScaler().fit_transform(c))
print(c.round(2).sort_values("cluster"))Quick quiz
🧠 Quick Quiz – test yourself
Interview corner
💼 Interview Corner
How do you choose the number of clusters?
Use the elbow of the inertia curve, silhouette scores, and – most importantly – business usefulness: segments must be distinct, large enough and actionable. Check stability with different seeds.
Limitations of K-Means?
Needs k in advance, sensitive to scaling and outliers, assumes spherical similar-size clusters, and can land in local minima (use n_init > 1). Categorical data needs other methods (k-modes) or encoding.
Summary
- K-Means: assign to nearest centre, move centres, repeat.
- Scale features; choose k with elbow + silhouette + business sense.
- Profile clusters into personas and actions.
- DBSCAN for odd shapes/outliers, hierarchical for nested groups.
Day 47: PCA – reduce many columns to a few powerful components and visualise high-dimensional data.
