Exercise: segments, or just boxes?

Session 10 — Applied Data Science 2026

Published

30 11 2026

NoteThe setting

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.

ImportantStart here

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:

set.seed(1)
km_raw <- kmeans(select(customers, tenure, monthly_charges, total_charges),
                 centers = 4, nstart = 25)
  1. Add the clusters to customers and summarise each one: number of customers, smallest and largest total_charges, median tenure.
  2. Plot total_charges against tenure, coloured by cluster. What do the four clusters really separate?
  3. 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.

  1. How many different results do you get?
  2. Plot the clusters of the start with the lowest and the one with the highest tot.withinss. How do they differ?
  3. What does nstart = 25 do 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

  1. Fit k-means with k = 4 (set.seed(1), nstart = 25) and look at withinss and tot.withinss. What does each number measure? Which cluster is the most spread out?
  2. For k = 1 to 8, compute tot.withinss (with nstart = 25) and plot it against k. Where is the elbow? Why does the curve never go up — and why is the k with the smallest tot.withinss not 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.

  1. Variable roles. Which variables are the clustering variables here, and which can serve as profiling variables? Why must churn stay out of the clustering if we want to use it in B4?
  2. 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.
  3. 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?