Technology · Notes
Customer segmentation, and how k-means lies to you.
The algorithm is four lines and it will always give you an answer. That is the problem. This walks through it on a customer base whose real segments we know in advance — so you can see, each time, whether it found them.
Every number on this page is computed live from the same code the tests run against. The headline one: on this data, k-means with unscaled features recovers 76% of the real segments and looks entirely convincing doing it. Scaled, it recovers 100%. Nothing in the output tells you which one you got.
What the algorithm actually does
Pick k points to be cluster centres. Assign every customer to the nearest one. Move each centre to the average of the customers who chose it. Repeat until nobody changes hands.
That is the whole algorithm. It is minimising one quantity — the total squared distance from each customer to their own centre.
Each step can only lower that number, which is why it always settles. Settling is not the same as being right — it settles just as happily on a bad answer.
The step that quietly decides your answer
Our customers carry three numbers: days since last order, orders per year, average order value. Recency runs to about 300, frequency to about 30, order value past 1,000.
Distance treats those as if they were the same kind of thing. A $200 difference in spend counts two hundred times more than a difference of one order per year — so the clusters come out as spend brackets, and frequency was barely consulted.
Standardising gives each feature the same say. It is one line, it is usually skipped, and on this data it is the difference between recovering 76% of the real segments and recovering all of them.
Switch the panel to raw units. The clusters become horizontal bands. That striping is what a segmentation looks like when one feature has eaten the others.
In raw units the horizontal bands are the giveaway: the clusters are order-value brackets, and frequency has barely been consulted.
Same data, same k, different answer
k-means starts from a guess and improves it. Different starting guesses reach different stopping points — all of them settled, all of them confident.
k-means++ helps by spreading the initial centres out, choosing each new one with probability proportional to its squared distance from the nearest existing centre. It makes a bad start much less likely. Not impossible.
The only real defence is to run it several times and keep the lowest inertia. Every serious library does this by default — scikit-learn runs ten. Writing this page, two seeds in three landed on a segmentation that merged two real groups, and the third found them exactly.
Reseed a few times with plain random seeding. Watch the inertia against the best-of-twelve. When it sits above, you are looking at an answer that is stuck — and nothing on the screen would tell you.
Recovers 100% here. Reseed a few times with plain random seeding and watch both the picture and the inertia change — the algorithm is as confident either way.
Choosing k, honestly
You have to tell k-means how many segments to find. It has no opinion. Ask for eight and you get eight, even if there are three.
Inertia cannot decide for you: more clusters always fit better, all the way down to one cluster per customer. That is why people look for an “elbow” — the point where the improvement stops being worth it — and why it is a judgement call rather than a calculation.
Silhouette is better. For each customer it compares how close they sit to their own cluster against the nearest rival, and it has a genuine maximum.
On this data it peaks at four, which is the number we planted. It will not always be so obliging — silhouette also assumes compact, separated clusters, so it agrees with k-means about what a cluster is.
What you actually hand to the business
A cluster label is not a segment. Convert the centres back to real units and you have something someone can act on: how recently this group bought, how often, and for how much.
The names in this table are cheating slightly — this data is synthetic and we know which segment each customer came from. On real data a cluster arrives with no name, and somebody has to look at the numbers and decide what it means. That step is not statistics, and it is the step that decides whether any of this was worth doing.
| Segment | n | Recency | Freq | Value |
|---|---|---|---|---|
| Champions | 46 | 13 d | 23.8 | $144 |
| Slipping away | 48 | 210 d | 3.9 | $130 |
| Steady regulars | 58 | 40 d | 9.0 | $123 |
| Big but rare | 38 | 54 d | 3.0 | $882 |
Centres converted back to real units — the only form anyone can act on. Hover a row to isolate it.
The failure nobody catches
Run k-means on customers with no segments at all — spend and frequency spread smoothly, no groups anywhere — and it returns k tidy clusters with respectable-looking centres. It cannot report that there is no structure, because you did not ask it that. You asked it for four groups.
Which is the honest summary of the whole method: k-means answers the question you asked, in the units you gave it, from the starting point it happened to pick. Every one of those is a decision, and none of them show up in the output.
The code behind this page is in lib/kmeans.ts, with tests that pin each claim — including that inertia never rises, that it falls monotonically with k, and that on uniform noise the algorithm still hands back exactly k clusters.