Two clusterings of the same 486 customers came back with silhouette scores of 0.5918 and 0.2395. Higher is supposed to be better, so the first one wins. It is also completely useless, and understanding why that is true is most of what this part has to teach.
Key takeaways
- Unscaled k means scored 0.5918 and had simply binned TotalCharges into four buckets. Scaling dropped the score to 0.2395 and produced segments a retention team can act on.
- Silhouette peaked at k equals 2 with 0.3469, but k equals 4 gave the useful answer. Metric and decision disagreed, and the metric lost.
- Four segments ranged from 4.7 percent churn to 47.6 percent against a base rate of 25.3 percent.
- Adding cluster labels as model features raised AUC from 0.8326 to 0.8367, and every point of that gain disappeared when clustering happened inside the fold. Honest score: 0.8323.
- PCA to two components kept 51.9 percent of variance and scored 0.8406 AUC, slightly ahead of all fifteen raw features.
Every model in this series so far has had an answer key. Churn was labelled, so a score told us whether we were right. Clustering removes the key. Nothing in the maths knows what a good segment is, which means the algorithm will always return an answer and will never tell you the answer is meaningless. Judgement moves from the tuning loop into the setup, and the two decisions that matter most are made before you fit anything: what you feed it, and on what scale.
Where the churn project stands after Part 12
Last part we finished the supervised leg. A tuned random forest reached roughly 0.836 AUC against a logistic regression at roughly 0.833 on the same folds, and I argued that the linear model was the one to ship. This part uses the identical feature frame, 486 rows and 15 columns built in Part 6, but throws the label away. What we add is a segmentation the retention team can read, and a compressed representation of the same customers in two dimensions instead of fifteen.
Both techniques answer questions a classifier cannot. A churn probability tells you who to call. A segment tells you what to say when they pick up, and whether the twelve people in the retention team should be running four campaigns or one. That distinction is worth holding onto, because it explains why a clustering that adds nothing to your AUC can still be the most valuable thing you produce that quarter.
Scaling decides what a cluster means
K means groups points by squared Euclidean distance, so a column with a wide numeric range contributes more to that distance than a column with a narrow one. Our frame has tenure running 1 to 72, MonthlyCharges 18.95 to 116.05, and TotalCharges 19.15 to 8468.20, alongside twelve dummy columns that are only ever 0 or 1. TotalCharges spans a range roughly seven thousand times wider than a dummy. Feed that in raw and the algorithm is not clustering customers, it is cutting one column into slices.
Those four unscaled centroids differ on TotalCharges and on essentially nothing else. Contract type, internet product and payment method all had a vote, and all of them were drowned out. Silhouette rewards this because cutting a single continuous variable into four contiguous bands genuinely does produce compact, well separated groups in that variable. Compactness is not relevance. Every dummy column in the frame was effectively ignored, and no retention manager has ever asked to see customers sorted by lifetime spend into quartiles.
Gotcha
Unsupervised metrics measure geometry, not usefulness. A higher silhouette can mean your clustering has collapsed onto one dominant column. Whenever a score jumps, print the centroids in original units before you celebrate. It takes one line and it has saved me more embarrassment than any other habit in this part of the workflow.
Choosing a value for k
K means needs the number of clusters up front. Two standard aids exist. Inertia, the sum of squared distances from each point to its own centroid, falls monotonically as k rises, so you look for a bend rather than a minimum. Silhouette compares how close each point is to its own cluster against the nearest rival cluster, and ranges from minus one to one. Neither is a decision procedure. Both are inputs to one.
| k | Inertia | Silhouette | Reading |
|---|---|---|---|
| 2 | 5152.1 | 0.3469 | Best score, splits internet from no internet |
| 3 | 4175.4 | 0.2672 | Big inertia drop, score falls |
| 4 | 3722.2 | 0.2395 | Lowest score in range, best segments |
| 5 | 3356.9 | 0.2544 | Small rebound, splits an existing group |
| 6 | 3121.8 | 0.2493 | Curve flattening |
| 7 | 2904.9 | 0.2363 | Too many for a campaign plan |
| 8 | 2731.0 | 0.2387 | Noise, not structure |
Read that table as a maximiser and you pick k equals 2, which separates customers with internet service from customers without it. True, obvious, and useless: you already have that column. Read it as an engineer and you notice inertia falls hard from 5152 to 4175 to 3722 and then settles into a steady grind of roughly 200 per extra cluster. Structure worth having is spent by about k equals 4. Silhouette bottoms out at exactly that point, at 0.2395, because four real segments overlap at their boundaries in a way two blunt halves do not. Overlap is what a genuine population looks like.
My rule after enough of these: pick k from the business constraint first, then check the maths has not vetoed it. If a retention team can run four distinct campaigns and not eight, the candidate range is three to five, and the job of the metrics is to say whether anything in that range is a disaster. Nothing here was. Going the other way, letting silhouette nominate k and then asking the business to live with it, has produced two segments and a wasted fortnight for me more than once.
Four customer segments, profiled
A cluster label is a number until you profile it. Profiling means joining the labels back onto the original untransformed columns and describing each group in units a human recognises. Here is where the churn label comes back, not to fit anything, but to check whether the segments the features found happen to line up with the outcome we care about.
| Cluster | n | Mean tenure | Mean monthly | Two year | Fiber | Churn rate |
|---|---|---|---|---|---|---|
| 0 | 164 | 19.2 | 81.11 | 0.6% | 82.9% | 47.6% |
| 1 | 94 | 32.4 | 21.14 | 42.6% | 0.0% | 7.4% |
| 2 | 127 | 58.1 | 87.84 | 51.2% | 56.7% | 4.7% |
| 3 | 101 | 17.7 | 55.95 | 6.9% | 13.9% | 31.7% |
Cluster 0 is the interesting one. Short tenure at 19.2 months, high monthly bill at 81.11, 82.9 percent on fiber, almost nobody on a two year contract, and 47.6 percent churn against a base rate of 25.3 percent. That is a recognisable person: someone who bought the expensive product recently, has no contractual reason to stay, and is deciding month by month. Cluster 2 is the mirror image, similar bills but 58 months of tenure and half on two year terms, churning at 4.7 percent. Same spend, opposite behaviour, and the difference is commitment rather than price.
None of that came from the label. K means was shown fifteen feature columns and no outcome, and it still sorted the population into groups whose churn rates differ by a factor of ten. When that happens, your features carry real signal about the outcome, which is a useful and free diagnostic. When it does not happen, and every cluster sits within a point or two of the base rate, treat that as a warning about your feature set well before you start blaming the model.
Principal component analysis on the same frame
PCA finds new axes that are linear combinations of your columns, ordered so the first captures the most variance, the second the most of what is left, and so on. Each new axis is orthogonal to the ones before it. You keep the first few and discard the rest, trading a little information for a lot fewer dimensions. Like k means it is distance based, so it needs standardised inputs for the same reason and with the same failure mode if you skip that step.
Those three columns load at 0.406 apiece because they are the same fact recorded three times. A customer with no internet service is flagged as having no internet service under OnlineSecurity, under TechSupport, and under InternetService itself. Our one hot encoding from Part 6 duplicated that indicator across every internet dependent product. PCA found the redundancy instantly, and PC1 is essentially a no internet axis with monthly spend loading in the opposite direction at minus 0.405. PC2 is a tenure and commitment axis, led by tenure at 0.504 and two year contract at 0.412.
Two components hold 51.9 percent of the variance, eight are needed for 90 percent, and nine for 95 percent. Components 14 and 15 explain nothing at all, to four decimal places, because those directions are exactly determined by the others. Fifteen columns of real rank about nine. That is not a criticism of the encoding, it is what one hot encoding of correlated products does, and knowing the number is more useful than pretending you have fifteen independent measurements.
Plotting the clusters in PC space explains the silhouette score better than any number could. Cluster 1, the no internet group, is genuinely isolated on the right of PC1. Clusters 0, 2 and 3 occupy the same region and separate mainly along PC2 and along axes the plot cannot show. That is a fair picture of a customer base. People do not fall into four tidy islands, and a segmentation that claimed they did would be describing an artefact.
Cluster labels as model features
A tempting move at this point is to feed the cluster label back into the classifier as a categorical feature. It sounds free. It is not, and the reason is the same leakage argument from Part 11. If you fit k means on the whole dataset and then cross validate, every training fold carries labels that were computed with the held out rows in the room. K means saw no churn values, so it feels safe, but it saw the held out feature vectors, and that is enough to inflate a score.
War story
On a subscription client in 2023 I shipped exactly this feature and reported a lift of about 0.006 AUC from cluster membership. Two sprints later a colleague rebuilt the pipeline with the clustering inside a scikit-learn Pipeline object so it refitted per fold, and the lift vanished to roughly zero. Four days of my work, plus a slide I had to walk back in a steering meeting, all because KMeans looked harmless enough to fit outside the loop. Now anything with a fit method goes inside the Pipeline, including the ones that never see the target.
Why does the honest version add nothing? Because a k means label is a coarse summary of columns the classifier already has, and logistic regression can read those columns directly at full resolution. Compressing tenure, contract and product into a four valued category throws information away rather than creating it. Cluster features earn their place when they encode something the model cannot see, a behavioural sequence for instance, or when the downstream model is too simple to find interactions on its own. Neither applies here.
PCA components as features tell a slightly different story. Two components, holding just over half the variance, scored 0.8406 AUC against 0.8326 for all fifteen raw columns, with fold standard deviations of 0.0259 and 0.0304 respectively. Discarding half the information made the model marginally better, which is a mild regularisation effect on a small sample rather than a discovery. On 486 rows and five folds, a difference of 0.008 sits inside the noise, and I would not claim it. What I would claim is that fifteen correlated columns were not buying anything over two clean ones, and that is worth knowing before someone proposes adding thirty more.
| Feature set | Mean AUC | Fold std |
|---|---|---|
| PCA, 2 components | 0.8406 | 0.0259 |
| PCA, 8 components | 0.8380 | 0.0332 |
| PCA, 10 components | 0.8376 | 0.0326 |
| Cluster labels, leaky | 0.8367 | 0.0322 |
| All 15 raw columns | 0.8326 | 0.0304 |
| Cluster labels, honest | 0.8323 | 0.0291 |
| PCA, 5 components | 0.8321 | 0.0311 |
Two errors you will hit in the first hour
Both of these caught me on the way to the numbers above. First, sweeping k from 1 rather than 2, which is a natural thing to write and immediately fatal, because a silhouette is undefined when every point belongs to the same cluster.
Second, and much nastier in production, calling predict on a batch of new customers whose dummy columns do not match the ones the model was fitted on. If a scoring batch happens to contain no fiber customers, get_dummies simply does not create that column, and you arrive here.
Fix is to stop using get_dummies for anything that will be scored later and switch to OneHotEncoder with handle_unknown set to ignore, inside a ColumnTransformer that remembers the training categories. Ad hoc dummy encoding is fine for a notebook and a liability the moment a second dataset touches the model. Part 21 rebuilds this properly as a package, and this is one of the concrete reasons it needs doing.
DBSCAN, t-SNE and where each one fits
DBSCAN groups points by density instead of distance to a centroid, finds the number of clusters itself, and can label points as noise. That sounds like an upgrade until you run it on one hot encoded data, where most of the space is empty and density is close to meaningless. At eps 2.0 it returned 26 clusters and marked 230 of 486 customers as noise. At eps 2.5 it collapsed to 3 clusters and 17 noise points. Nothing in between was usable. Density methods want continuous, roughly uniform feature spaces, and a frame that is 80 percent binary is not that.
t-SNE and UMAP are the other two names you will meet. Both produce striking two dimensional pictures, and both are for visual inspection only. Neither preserves global distances, so the gap between two visual blobs carries no reliable meaning, and t-SNE in particular will produce apparent clusters from pure noise if you set perplexity badly. Use them to look, never to decide, and never as features for a downstream model.
What I would use clustering for on the churn project
Ship the four segments as a campaign artefact, not as model features. Cluster 0 is 164 customers at 47.6 percent churn, recent buyers of the expensive product with no contract holding them, and the obvious offer is a discounted twelve or twenty four month term. Cluster 2 proves the mechanism works, since the same spend on a two year contract churns at 4.7 percent. That is a specific, testable retention hypothesis, and it came out of an algorithm that never saw a single churn value.
Keep the classifier from Part 12 exactly as it is. Cluster labels bought 0.8323 against a baseline of 0.8326, so they cost complexity and returned nothing measurable. Keep PCA in the toolkit for two jobs: reading loadings as a redundancy audit on any new feature frame, and producing the two dimensional plot when you need to show non technical stakeholders that the segments are real. Run the segment offer as a controlled experiment rather than a blanket rollout, using the design from A/B testing for analysts, because a 47.6 percent churn group will regress toward the mean on its own and you need a control arm to tell that apart from an effect.
Phase 2 closes here. Part 14 opens the advanced leg with imbalanced data, and our frame is already mildly imbalanced at roughly one churner in four, which is gentle compared to the fraud and defect problems where resampling decisions start to hurt. Before you move on, rerun the k sweep on your own copy with the scaler removed and watch silhouette climb while the segments turn to sand. Seeing a better score attached to a worse answer, on your own screen, is the lesson that sticks.
References
- scikit-learn user guide, Clustering, including the silhouette coefficient and the properties of k means
- sklearn.cluster.KMeans API reference, for the n_init auto default introduced in version 1.4
- scikit-learn user guide, Decomposing signals in components, for PCA and explained variance
- Telco Customer Churn dataset, the public IBM sample file this series works from


DrJha