Clustering: structure without labels
Supervised learning needed a label column. Often you don't have one — just a pile of users, documents, or error logs — and the question is "what natural groups are in here?" That's unsupervised learning, and its workhorse is clustering.
k-means, demystified
Run the editor. The whole algorithm is two alternating moves:
- Assign each point to its nearest center.
- Update each center to the mean of its assigned points.
Repeat until the centers stop moving. Watch the run: from rough
guesses (0, 4, 6), the centers move for two iterations and then
freeze on the three real groups (~1, ~5, ~9) — that freeze is
convergence. The sklearn version is
KMeans(n_clusters=3).fit(X) — same two moves, any number of
dimensions.
The two problems nobody solves for you
You choose k. The algorithm finds k groups because you asked for k groups — it will happily split 2 real clusters into 5, or mash 5 into 2. The standard aid is the elbow method: plot the total within-cluster distance ("inertia") as k grows and look for where improvement flattens. It's a heuristic, not an oracle; pair it with actually reading samples from each cluster.
Clusters have no names. k-means returns "group 0, group 1, group 2" — the meaning is your job. The professional move: for each cluster, pull ten members and the cluster's feature means, and write one sentence per cluster ("low-login, high-ticket users — the struggling cohort"). A clustering nobody can describe is a random partition with extra steps.
What it's for in real products
Customer segments for targeting, grouping similar support tickets before writing playbooks, deduplicating near-identical documents, spotting "this error log doesn't fit any cluster" anomalies. In every case clustering is a lens for humans first — it proposes structure; you verify it means something.
Where AI specifically gets this wrong
The big one first: unscaled features. Distance treats every unit
equally, so an income column in dollars dwarfs logins in
counts, and clusters become "income bands" no matter what else
varies. Standardize features (mean 0, std 1) before clustering —
generated code frequently skips this.
- Trusting k. Cursor picks
n_clusters=8because you said "segment my users." Eight is a guess wearing confidence. - Skipping the read-the-clusters step. A notebook that prints cluster sizes and stops has produced numbers, not insight.