set.seed(1)
km_raw <- kmeans(select(customers, tenure, monthly_charges, total_charges),
centers = 4, nstart = 25)Exercise: segments, or just boxes?
Session 10 — Applied Data Science 2026
The telecom company’s marketing manager wants customer segments for next year’s campaigns — and a straight answer to one question: are these real groups, or did the computer just draw boxes?
Alone or in pairs. Two parts: Part A and Part B.
Nothing new to install: cluster comes with R, broom with the tidyverse.
Download telco-customers.csv from the session page into data/, create session10.qmd, and start with this setup chunk.
```{r}
#| label: setup
#| include: false
library(tidyverse)
library(broom)
library(cluster)
library(here)
i_am("session10.qmd")
raw <- read_csv(here("data", "telco-customers.csv"))
# 11 customers with tenure 0 have not been billed yet
customers <- raw |>
filter(!is.na(total_charges))
# Two variables, standardised
z <- customers |>
transmute(tenure = as.numeric(scale(tenure)),
monthly = as.numeric(scale(monthly_charges)))
```Part A — What k-means does
A1 · Look first
Plot tenure against monthly_charges, coloured by internet_service. Where do you expect groups? Which variable explains the dense band at the bottom?
A2 · The first attempt
Run k-means on the three money-and-time columns, as they are:
- Add the clusters to
customersand summarise each one: number of customers, smallest and largesttotal_charges, mediantenure. - Plot
total_chargesagainsttenure, coloured by cluster. What do the four clusters really separate? - Explain why in two sentences. Which two rules for choosing clustering variables follow from it?
A3 · Standardised, three choices of k
Run kmeans(z, centers = k, nstart = 25) for k = 3, 4 and 5 and plot tenure against monthly_charges coloured by cluster for each. Describe in one sentence each what the extra cluster splits off when you go from 3 to 4, and from 4 to 5.
ggplot(data = customers |> mutate(cluster = factor(fit$cluster)), ...) — the clusters are in the same order as the rows of customers.
A4 · Random starts
Run kmeans(z, centers = 6, nstart = 1) with set.seed(1) to set.seed(10) and collect tot.withinss each time.
- How many different results do you get?
- Plot the clusters of the start with the lowest and the one with the highest
tot.withinss. How do they differ? - What does
nstart = 25do about it?
Keep the ten fits, not only their tot.withinss:
fits <- purrr::map(1:10, \(s) {
set.seed(s)
kmeans(z, centers = 6, nstart = 1)
})which.min() and which.max() on the ten values of tot.withinss give you the two fits to plot.
Part B — How many segments, and do they mean anything?
B1 · Within-cluster sum of squares, and the elbow
- Fit k-means with k = 4 (
set.seed(1),nstart = 25) and look atwithinssandtot.withinss. What does each number measure? Which cluster is the most spread out? - For k = 1 to 8, compute
tot.withinss(withnstart = 25) and plot it against k. Where is the elbow? Why does the curve never go up — and why is the k with the smallesttot.withinssnot the answer?
glance(kmeans(...)) returns tot.withinss as a one-row table; purrr::map() over 1:8 and list_rbind() as in the live coding.
B2 · The silhouette
set.seed(2)
sample_rows <- sample(nrow(z), 2000)
distances <- dist(z[sample_rows, ])
avg_silhouette <- function(data, k, rows, d) {
set.seed(1)
fit <- kmeans(data, centers = k, nstart = 25)
mean(silhouette(fit$cluster[rows], d)[, "sil_width"])
}
purrr::map_dbl(2:8, \(k) avg_silhouette(z, k, sample_rows, distances))Which k has the highest average silhouette? Do elbow and silhouette agree?
B3 · Profile and name
With k = 4, profile the clusters in the two steps of the live coding.
- Variable roles. Which variables are the clustering variables here, and which can serve as profiling variables? Why must
churnstay out of the clustering if we want to use it in B4? - Describe by what built the clusters. For each cluster: number of customers, median tenure, median monthly bill. Give every segment a short name a marketing manager would understand.
- Compare on what did not build them. For each segment, and for all customers together, compute the share with fibre (
internet_service == "Fiber optic"), with no internet, on a month-to-month contract, and paying by electronic check. Where do the segments depart most from all customers? What does that tell you that step 2 cannot?
B4 · Churn
Compute the share of customers who left (churn == "Yes") in each segment, and overall. Then write three sentences for the manager: which segment to call first, why — and what the segments do not tell the manager about why customers leave.
B5 · The noise check
Make a dataset with no structure, as many rows as z:
set.seed(10)
noise <- tibble(a = runif(nrow(z)), b = runif(nrow(z))) |>
mutate(across(everything(), \(x) as.numeric(scale(x))))Compute the average silhouette for k = 2 to 8 (use dist(noise[sample_rows, ]) as the distances). Where is its peak, and how high? What does this mean for your answers to B1 and B2 — and which of your results in B3–B4 does the noise check not undermine?
If you finish early
a) Stability
Draw five random samples of 3,500 customers from z, run k-means with k = 4 on each, and compare the centres (fit$centers, sorted by tenure). Do the same four segments come back?
b) Hierarchical clustering
Hierarchical clustering needs all distances, so take a sample of 500 customers:
set.seed(3)
s <- sample(nrow(z), 500)
tree <- hclust(dist(z[s, ]), method = "ward.D2")
library(ggdendro) # install.packages("ggdendro") once — for this task only
ggdendrogram(tree, labels = FALSE, leaf_labels = FALSE)
groups <- cutree(tree, k = 4)A quick look without the extra package: plot(tree, labels = FALSE) draws the same tree in base R.
Compare groups with the k-means clusters of the same customers in a table(). Where do the two methods agree, and where not?
c) More variables
Add two variables: the contract length (0 month-to-month, 1 one year, 2 two years) and the number of add-on services (count the "Yes" in online_security, online_backup, device_protection, tech_support, streaming_tv, streaming_movies). Standardise all four, cluster with k = 4, and redo B4. Do the segments separate churn better? Which variables did you just make count more?