Skip to content

K-means

After this lesson, you will be able to:

  • Understand the fundamentals of unsupervised learning and the clustering problem.
  • Master the inertia objective function JJ and Lloyd’s algorithm with its two alternating phases: cluster assignment and centroid update.
  • Explain why random initialization is prone to poor local minima and how K-means++ resolves this vulnerability.
  • Select an appropriate number of clusters kk using the Elbow method and Silhouette scores.

Imagine you manage a retail fashion chain with tens of thousands of customers. For each customer, you record monthly visit frequency and average transaction spending. You want to group customers to tailor marketing promotions, but possess no predefined labels such as “VIP”, “Budget”, or “Bargain Hunter”.

This is the domain of unsupervised learning and clustering: grouping similar observations together such that points within the same group are as close as possible, while separate groups remain well separated.

A natural way to define a cluster is to appoint a representative center—a centroid. Suppose you want to partition data into k=3k = 3 clusters:

  1. Place 3 candidate centroids at arbitrary locations across the store space.
  2. Each customer observes the centroids and associates with the one closest to them, forming 3 temporary clusters.
  3. Each centroid steps to the center of gravity (arithmetic mean) of all customers assigned to it.
  4. As centroids relocate, some customers find another centroid now closer, switching groups accordingly.

This process alternates. After several rounds, assignments stabilize and centroids stop moving. The algorithm has converged.

Our working dataset comprises 36 2D points arranged into 3 natural clusters (12 points each) centered near (2,2)(2, 2), (8,3)(8, 3), and (5,8)(5, 8). Representative coordinates include:

Sample IndexCoordinates (x1,x2)(x_1, x_2)Visual Region
0(1.39,1.86)(1.39, 1.86)Bottom-left cluster
1(1.78,2.45)(1.78, 2.45)Bottom-left cluster
12(8.85,3.21)(8.85, 3.21)Bottom-right cluster
24(5.20,7.82)(5.20, 7.82)Upper cluster

The human eye easily detects three isolated clusters. A computer, however, sees only an unlabeled 36×236 \times 2 coordinate matrix.

The objective is: partition mm unlabeled observations into kk clusters minimizing the total sum of squared Euclidean distances from each point to its assigned cluster centroid.

Given mm samples x(1),x(2),…,x(m)∈Rn\mathbf{x}^{(1)}, \mathbf{x}^{(2)}, \dots, \mathbf{x}^{(m)} \in \mathbb{R}^n without target labels yy, we partition them into kk disjoint subsets:

C1∪C2∪⋯∪Ck={1,2,…,m},Ci∩Cj=∅  (∀i≠j)C_1 \cup C_2 \cup \dots \cup C_k = \{1, 2, \dots, m\}, \quad C_i \cap C_j = \emptyset \; (\forall i \ne j)

Each cluster j∈{1,…,k}j \in \{1, \dots, k\} possesses centroid μj∈Rn\boldsymbol{\mu}_j \in \mathbb{R}^n. Let c(i)∈{1,…,k}c^{(i)} \in \{1, \dots, k\} denote the cluster assigned to sample ii.

The objective to minimize is total squared distance from points to centroids, termed inertia or distortion:

J(c,μ)=∑i=1m∥x(i)−μc(i)∥2J(\mathbf{c}, \boldsymbol{\mu}) = \sum_{i=1}^m \lVert \mathbf{x}^{(i)} - \boldsymbol{\mu}_{c^{(i)}} \rVert^2

where:

  • c=[c(1),c(2),…,c(m)]⊤\mathbf{c} = [c^{(1)}, c^{(2)}, \dots, c^{(m)}]^\top represents sample assignments.
  • μ={μ1,…,μk}\boldsymbol{\mu} = \{\boldsymbol{\mu}_1, \dots, \boldsymbol{\mu}_k\} denotes the set of centroids.

Simultaneous optimization over both discrete assignments c\mathbf{c} and continuous coordinates μ\boldsymbol{\mu} is NP-hard. We turn to an alternating iterative heuristic: Lloyd’s algorithm (commonly referred to as standard K-means).

Lloyd’s Algorithm: Two Alternating Steps

Section titled “Lloyd’s Algorithm: Two Alternating Steps”

Lloyd’s algorithm alternates between optimizing assignments c\mathbf{c} and centroids μ\boldsymbol{\mu}:

Initialize k candidate centroids μ_1, ..., μ_k
Repeat until convergence:
Step 1 (Assignment): Fix μ, find optimal assignments c
Step 2 (Update): Fix c, find optimal centroids μ

Holding centroids μ1,…,μk\boldsymbol{\mu}_1, \dots, \boldsymbol{\mu}_k fixed, minimizing JJ decouples across individual samples. Each sample x(i)\mathbf{x}^{(i)} is assigned to its nearest centroid:

c(i)=arg⁡min⁡j∈{1,…,k}∥x(i)−μj∥2c^{(i)} = \arg\min_{j \in \{1, \dots, k\}} \lVert \mathbf{x}^{(i)} - \boldsymbol{\mu}_j \rVert^2

Ties are broken arbitrarily. At this step, inertia is guaranteed non-increasing: J(c(t+1),μ(t))≤J(c(t),μ(t))J(\mathbf{c}^{(t+1)}, \boldsymbol{\mu}^{(t)}) \le J(\mathbf{c}^{(t)}, \boldsymbol{\mu}^{(t)}).

Holding assignments c\mathbf{c} fixed, we optimize coordinates μj\boldsymbol{\mu}_j to minimize JJ:

J(c,μ)=∑j=1k∑i∈Cj∥x(i)−μj∥2J(\mathbf{c}, \boldsymbol{\mu}) = \sum_{j=1}^k \sum_{i \in C_j} \lVert \mathbf{x}^{(i)} - \boldsymbol{\mu}_j \rVert^2

Because clusters partition samples independently, we minimize each cluster sum separately. Differentiating with respect to centroid μj\boldsymbol{\mu}_j:

∇μjJ=∇μj∑i∈Cj(x(i)−μj)⊤(x(i)−μj)=∑i∈Cj−2(x(i)−μj)=−2(∑i∈Cjx(i)−∣Cj∣μj)\begin{aligned} \nabla_{\boldsymbol{\mu}_j} J &= \nabla_{\boldsymbol{\mu}_j} \sum_{i \in C_j} (\mathbf{x}^{(i)} - \boldsymbol{\mu}_j)^\top (\mathbf{x}^{(i)} - \boldsymbol{\mu}_j) \\ &= \sum_{i \in C_j} -2(\mathbf{x}^{(i)} - \boldsymbol{\mu}_j) \\ &= -2 \left( \sum_{i \in C_j} \mathbf{x}^{(i)} - |C_j| \boldsymbol{\mu}_j \right) \end{aligned}

Setting the gradient to 0\mathbf{0}:

∑i∈Cjx(i)−∣Cj∣μj=0  ⟹  μj=1∣Cj∣∑i∈Cjx(i)\sum_{i \in C_j} \mathbf{x}^{(i)} - |C_j| \boldsymbol{\mu}_j = \mathbf{0} \implies \boldsymbol{\mu}_j = \frac{1}{|C_j|} \sum_{i \in C_j} \mathbf{x}^{(i)}

The optimal centroid is the arithmetic mean (centroid) of all samples currently in that cluster. Inertia is again non-increasing: J(c(t+1),μ(t+1))≤J(c(t+1),μ(t))J(\mathbf{c}^{(t+1)}, \boldsymbol{\mu}^{(t+1)}) \le J(\mathbf{c}^{(t+1)}, \boldsymbol{\mu}^{(t)}).

Initialization and the K-means++ Algorithm

Section titled “Initialization and the K-means++ Algorithm”

Although guaranteed to converge, Lloyd’s algorithm settles at a local minimum dictated heavily by initial centroid placement:

  • Poor initial seeds (e.g., placing two initial centroids within a single natural cluster) can split one true cluster while merging two separate clusters.
  • Empty clusters (∣Cj∣=0|C_j| = 0) can emerge if an initial centroid attracts no samples.

Strategy 1: Multiple Random Restarts (n_init)

Section titled “Strategy 1: Multiple Random Restarts (n_init)”

The simplest mitigation is running Lloyd’s algorithm across multiple independent random initializations (historically n_init=10 in scikit-learn), retaining the solution achieving lowest final inertia JJ.

Arthur & Vassilvitskii (2007) proposed K-means++, establishing that initial centroids should be seeded as far apart as possible.

K-means++ seeding steps:

  1. Choose the first centroid μ1\boldsymbol{\mu}_1 uniformly at random from the dataset.
  2. For each sample x(i)\mathbf{x}^{(i)}, compute its distance D(x(i))D(\mathbf{x}^{(i)}) to the nearest already chosen centroid: D(x(i))=min⁡j∥x(i)−μj∥D(\mathbf{x}^{(i)}) = \min_{j} \lVert \mathbf{x}^{(i)} - \boldsymbol{\mu}_j \rVert
  3. Select the next centroid with probability proportional to squared distance: P(x(i))=D(x(i))2∑l=1mD(x(l))2P(\mathbf{x}^{(i)}) = \frac{D(\mathbf{x}^{(i)})^2}{\sum_{l=1}^m D(\mathbf{x}^{(l)})^2}
  4. Repeat steps 2 and 3 until kk centroids are chosen.

Samples remote from established centroids exhibit large D(x)2D(\mathbf{x})^2, drastically boosting their odds of becoming subsequent centroids. This dramatically curbs poor local minima and accelerates convergence. K-means++ is the standard default across modern libraries (init='k-means++').

Choosing Number of Clusters k: Elbow and Silhouette

Section titled “Choosing Number of Clusters k: Elbow and Silhouette”

In real applications, ground-truth cluster count kk is typically unknown. Why not simply select kk that minimizes inertia JJ?

Because inertia strictly decreases as kk increases. At k=mk = m, every sample is its own centroid, achieving J=0J = 0 trivially. This extreme lacks practical grouping utility. Two quantitative heuristics guide selection:

Plot inertia JJ across k∈{1,2,… }k \in \{1, 2, \dots\}. As kk advances toward the true cluster count, JJ plunges sharply. Once past the true count, additional centroids merely subdivide natural clusters, causing JJ to flatten.

The resulting curve exhibits an angle resembling a bent elbow. The location of this “elbow” provides an intuitive trade-off between compactness and model complexity.

When the elbow curve is ambiguous, the Silhouette score (Rousseeuw, 1987) measures cluster quality directly per sample via intra-cluster cohesion and inter-cluster separation.

For each sample x(i)∈CI\mathbf{x}^{(i)} \in C_I:

  1. Compute a(i)a^{(i)}: average distance from x(i)\mathbf{x}^{(i)} to all other points within its assigned cluster CIC_I (intra-cluster cohesion; lower is better): a(i)=1∣CI∣−1∑j∈CI,j≠i∥x(i)−x(j)∥a^{(i)} = \frac{1}{|C_I| - 1} \sum_{j \in C_I, j \ne i} \lVert \mathbf{x}^{(i)} - \mathbf{x}^{(j)} \rVert
  2. Compute b(i)b^{(i)}: smallest average distance from x(i)\mathbf{x}^{(i)} to all points in any other cluster CJ≠CIC_J \ne C_I (nearest-cluster separation; higher is better): b(i)=min⁡J≠I1∣CJ∣∑j∈CJ∥x(i)−x(j)∥b^{(i)} = \min_{J \ne I} \frac{1}{|C_J|} \sum_{j \in C_J} \lVert \mathbf{x}^{(i)} - \mathbf{x}^{(j)} \rVert
  3. The Silhouette coefficient of sample ii is: s(i)=b(i)−a(i)max⁡(a(i),b(i))∈[−1,1]s^{(i)} = \frac{b^{(i)} - a^{(i)}}{\max(a^{(i)}, b^{(i)})} \in [-1, 1]

Interpretation of s(i)s^{(i)}:

  • s(i)≈1s^{(i)} \approx 1: a(i)≪b(i)a^{(i)} \ll b^{(i)}; the sample is deeply embedded within its cluster, far from neighboring clusters (ideal).
  • s(i)≈0s^{(i)} \approx 0: a(i)≈b(i)a^{(i)} \approx b^{(i)}; the sample sits right on the boundary between two clusters.
  • s(i)<0s^{(i)} < 0: a(i)>b(i)a^{(i)} > b^{(i)}; the sample is closer to an adjacent cluster than its assigned cluster (misclustered).

The mean Silhouette score across all samples sˉ=1m∑i=1ms(i)\bar{s} = \frac{1}{m} \sum_{i=1}^m s^{(i)} peaks at the optimal cluster count kk.

  • Requires predetermining kk. K-means cannot automatically deduce intrinsic cluster counts.
  • Assumes convex, isotropic clusters. Because it minimizes Euclidean distance, K-means partitions space into linear Voronoi polyhedra. It fails when clusters have non-convex geometries (such as interlocking crescents or concentric circles) or drastically disparate densities.
  • Sensitive to outliers. Squaring Euclidean distances gives distant anomalies outsized leverage, pulling centroids away from dense modes.
  • Demands feature scaling. If one feature has vastly larger numerical magnitude than others, Euclidean distance is dominated by that single dimension.

The table below traces Lloyd’s algorithm across our 36-sample dataset with 3 initial centroids seeded at samples 2, 20, and 1:

Iteration ttPhaseInertia JJDynamics Summary
0Init—Samples 2, 20, and 1 selected as initial seeds μ1,μ2,μ3\boldsymbol{\mu}_1, \boldsymbol{\mu}_2, \boldsymbol{\mu}_3
1Assign406.70Samples assign to nearest candidate centroid
1Update241.67Centroids shift to means of assigned points, dropping JJ
2Assign230.88Boundary samples switch to closer updated centers
2Update200.73Centroids migrate toward natural point cloud centers
3Assign139.13Reassignments induce structural partition shift
3Update83.62Inertia drops sharply as 3 distinct clusters crystallize
4Assign58.49Final boundary points settle
4Update52.12Centroids stabilize at (1.92,2.19)(1.92, 2.19), (8.20,3.09)(8.20, 3.09), (5.41,7.90)(5.41, 7.90)
5Assign52.12Zero reassignments occur →\to Convergence reached

Key takeaways:

  1. Inertia JJ declines monotonically at each step: from 406.70 to 241.67, 230.88, 200.73, 139.13, 83.62, 58.49, halting at 52.12.
  2. At iteration 5, assignments are identical to iteration 4, confirming convergence after 4 updates. Final cluster sizes partition evenly: 12, 12, 12.

The three cells execute independently.

The first cell implements Lloyd’s algorithm from scratch with NumPy: computing pairwise distance matrices via array broadcasting, updating centroids via conditional cluster means, and terminating when assignments freeze.

Lloyd's algorithm step-by-step with NumPy

import numpy as np
from sklearn.datasets import make_blobs
# 36 2D points into 3 clusters, rounded to 2 decimals.
X, _ = make_blobs(n_samples=36, centers=[[2, 2], [8, 3], [5, 8]],
cluster_std=1.0, random_state=10)
X = np.round(X, 2)
k = 3
mu = X[[2, 20, 1]].copy() # 3 initial centroids: points 2, 20 and 1
c = np.full(len(X), -1) # no point assigned yet
for t in range(1, 101):
# Assignment step: squared distance from each point to each centroid.
d2 = ((X[:, None, :] - mu[None, :, :]) ** 2).sum(axis=2)
c_new = d2.argmin(axis=1) # index of closest centroid
if np.array_equal(c_new, c): # no point changed cluster: stop
break
c = c_new
J = d2[np.arange(len(X)), c].sum()
print(f"iter {t}: assign J = {J:7.2f}", end=" ")
# Update step: each centroid becomes the mean of points in its cluster.
for j in range(k):
if np.any(c == j): # skip empty cluster
mu[j] = X[c == j].mean(axis=0)
J = ((X - mu[c]) ** 2).sum()
print(f"update J = {J:7.2f}")
print(f"converged, sizes = {np.bincount(c, minlength=k)}")
print("centroids:")
print(np.round(mu, 2))

The first run downloads Python, about 10 MB.

The console output matches our theoretical trace: converging in 4 iterations at J=52.12J = 52.12. Centroids settle at (1.92,2.19)(1.92, 2.19), (8.20,3.09)(8.20, 3.09), and (5.41,7.90)(5.41, 7.90)—closely reproducing true data-generation centers (2,2)(2, 2), (8,3)(8, 3), and (5,8)(5, 8).

The second cell uses scikit-learn’s KMeans to illustrate local minimum vulnerabilities: comparing 50 random initializations (init="random") against init="k-means++".

Random initialization, K-means++, and local minimum traps

import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
X, _ = make_blobs(n_samples=36, centers=[[2, 2], [8, 3], [5, 8]],
cluster_std=1.0, random_state=10)
X = np.round(X, 2)
def run(init, n_init):
# Run KMeans with 50 different seeds, record inertia (J).
J = [KMeans(3, init=init, n_init=n_init, random_state=s).fit(X).inertia_
for s in range(50)]
J = np.round(J, 2)
stuck = np.sum(J > 52.12) # times stuck at J > 52.12
print(f"{init:>9}, n_init={n_init:>2}: min J = {J.min():6.2f}, "
f"max J = {J.max():6.2f}, stuck {stuck}/50")
run("random", 1) # 3 random points as centroids, run once
run("k-means++", 1) # k-means++, run once
run("random", 10) # random, run 10 times, keep the one with lowest J
# One run that gets stuck: J, cluster sizes and centroids.
km = KMeans(3, init="random", n_init=1, random_state=13).fit(X)
print(f"seed 13: J = {km.inertia_:.2f}, sizes = {np.bincount(km.labels_)}")
print(np.round(km.cluster_centers_, 2))
# Default: init='k-means++', n_init='auto' (1 run with k-means++).
km = KMeans(3, random_state=0).fit(X)
print(f"default: J = {km.inertia_:.2f}")

The first run downloads Python, about 10 MB.

Under single random restarts (init="random", n_init=1), 3 of 50 trials (6%) become trapped in suboptimal local minima with J≈230.40J \approx 230.40. In contrast, K-means++ achieves the global minimum 52.1252.12 across all 50/50 trials without trapping.

The third cell sweeps kk from 1 to 6 using scikit-learn, generating Elbow and Silhouette diagnostic curves.

Selecting k via Elbow and Silhouette methods

import matplotlib.pyplot as plt
import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
from sklearn.metrics import silhouette_score
X, _ = make_blobs(n_samples=36, centers=[[2, 2], [8, 3], [5, 8]],
cluster_std=1.0, random_state=10)
X = np.round(X, 2)
ks = range(1, 7)
inertia, sil = [], []
for k in ks:
km = KMeans(k, n_init=10, random_state=0).fit(X)
inertia.append(km.inertia_)
# Silhouette requires at least 2 clusters.
sil.append(silhouette_score(X, km.labels_) if k > 1 else np.nan)
print(f"k = {k}: J = {km.inertia_:6.2f}, silhouette = {sil[-1]:.3f}")
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(9, 3.5))
ax1.plot(ks, inertia, "o-")
ax1.set_xlabel("k")
ax1.set_ylabel("inertia J")
ax1.set_title("Elbow")
ax2.plot(ks, sil, "o-", color="tab:orange")
ax2.set_xlabel("k")
ax2.set_ylabel("mean silhouette")
ax2.set_title("Silhouette")
fig.tight_layout()
plt.show()

The first run downloads Python, about 10 MB.

Both diagnostics cleanly identify k=3k = 3 as optimal:

  1. The Elbow plot displays an inflection point at k=3k = 3, after which reductions in JJ plateau.
  2. The Silhouette curve achieves an isolated maximum at k=3k = 3 with score 0.7280.728.
Question 1 of 4 You run K-means on the same dataset with k=3k = 3 and k=4k = 4. How do their inertia values JJ compare?
Question 2 of 4 In the update step of Lloyd's algorithm, why is the new centroid μj\boldsymbol{\mu}_j computed as the arithmetic mean of all points assigned to that cluster?
Question 3 of 4 How does the K-means++ initialization algorithm select each subsequent centroid?
Question 4 of 4 A data point has a Silhouette coefficient of s≈−0.6s \approx -0.6. What does this indicate?

Key Takeaways:

  • K-means is a foundational unsupervised algorithm partitioning mm unlabeled samples into kk clusters by minimizing inertia J=∑i=1m∥x(i)−μc(i)∥2J = \sum_{i=1}^m \lVert \mathbf{x}^{(i)} - \boldsymbol{\mu}_{c^{(i)}} \rVert^2.
  • Lloyd’s algorithm alternates between assigning points to closest centroids and updating centroids to cluster arithmetic means, guaranteed to reach local convergence.
  • K-means++ initialization disperses initial seeds using D(x)2D(\mathbf{x})^2 probability weighting, preventing poor local minima.
  • Optimal cluster count kk is identified via Elbow inflection points and maximum Silhouette scores. Always scale features beforehand.

Further Reading:

Content licensed under CC BY-NC-SA 4.0. Code under the MIT license.About