Skip to content

K-Means Clustering Algorithm

What you’ll learn

  • the k-means objective, and why the two update steps fall out of it
  • a proof that Lloyd’s algorithm terminates, and why that does not mean it finds the best answer
  • a full iteration worked by hand on six points
  • why n_initn_init exists, measured: inertia 302.46 versus 971.25 from one unlucky start
  • the elbow and the silhouette, and which one actually answers β€œhow many clusters?”
  • three geometries where k-means fails, with adjusted Rand indices attached
  • k-means++ , MiniBatchKMeans, and when each pays off

Intuition

k-means answers one question: if you had to replace every point with one of kk representative points, which kk would lose the least information?

Those representatives are the centroids. Once you have them, every point belongs to whichever is nearest. Once you have the assignment, the best possible representative for a group is its mean. Neither half can be solved without the other, so the algorithm alternates: fix the centroids and reassign, fix the assignment and recompute the centroids. Repeat until nothing moves.

That is the entire algorithm. Everything else on this page is consequences of it.

diagram Diagram mermaid

The math

Let C1,…,CkC_1, \dots, C_k partition the nn points and let ΞΌj\boldsymbol\mu_j be the centroid of CjC_j. k-means minimises the within-cluster sum of squares, which scikit-learn calls inertia:

J=βˆ‘j=1kβˆ‘xi∈Cjβˆ₯xiβˆ’ΞΌjβˆ₯2J = \sum_{j=1}^{k} \sum_{\mathbf{x}_i \in C_j} \lVert \mathbf{x}_i - \boldsymbol\mu_j \rVert^2

Both update rules are the exact minimisers of JJ with the other variable held fixed.

The assignment step. With the centroids fixed, JJ is a sum of independent per-point terms. Each is minimised by choosing the nearest centroid:

c(i)=arg⁑min⁑jβˆ₯xiβˆ’ΞΌjβˆ₯2c(i) = \arg\min_{j} \lVert \mathbf{x}_i - \boldsymbol\mu_j \rVert^2

The update step. With the assignment fixed, differentiate the contribution of one cluster with respect to its centroid:

βˆ‚βˆ‚ΞΌjβˆ‘xi∈Cjβˆ₯xiβˆ’ΞΌjβˆ₯2=βˆ’2βˆ‘xi∈Cj(xiβˆ’ΞΌj)\frac{\partial}{\partial \boldsymbol\mu_j} \sum_{\mathbf{x}_i \in C_j} \lVert \mathbf{x}_i - \boldsymbol\mu_j \rVert^2 = -2 \sum_{\mathbf{x}_i \in C_j} (\mathbf{x}_i - \boldsymbol\mu_j)

Setting this to zero gives βˆ‘xi∈Cjxi=∣Cj∣μj\sum_{\mathbf{x}_i \in C_j} \mathbf{x}_i = |C_j| \boldsymbol\mu_j, so

ΞΌj=1∣Cjβˆ£βˆ‘xi∈Cjxi\boldsymbol\mu_j = \frac{1}{|C_j|} \sum_{\mathbf{x}_i \in C_j} \mathbf{x}_i

The centroid is the mean. Not an approximation β€” the exact minimiser. This is also why k-means is tied to squared Euclidean distance: swap in L1L_1 and the optimal representative becomes the median, which is a different algorithm (k-medians).

Why it always stops

Both steps are guaranteed not to increase JJ:

  1. The assignment step moves points only to a centroid that is nearer or equally near, so every term in the sum weakly decreases.
  2. The update step replaces each centroid with the exact minimiser for its cluster, so each cluster’s contribution weakly decreases.

So JJ is non-increasing. There are only finitely many ways to partition nn points into kk groups, and JJ is determined by the partition. A non-increasing sequence over a finite set of values that never revisits a partition must halt. Lloyd’s algorithm always terminates, typically in a handful of iterations.

It terminates at a local minimum. There is no guarantee it is the global one, and finding the global optimum of JJ is NP-hard even for k=2k = 2. That gap is what n_initn_init exists to paper over.

Worked example by hand

Six one-dimensional points: 1,2,3,10,11,121, 2, 3, 10, 11, 12. Take k=2k = 2 and the deliberately awkward initial centroids ΞΌ1=2\mu_1 = 2, ΞΌ2=4\mu_2 = 4.

Iteration 1 β€” assign. Compare ∣xβˆ’2∣|x - 2| against ∣xβˆ’4∣|x - 4|:

xxto ΞΌ1=2\mu_1 = 2to ΞΌ2=4\mu_2 = 4goes to
113C1C_1
202C1C_1
311C1C_1 (tie, lowest index)
1086C2C_2
1197C2C_2
12108C2C_2

Iteration 1 β€” update.

ΞΌ1=1+2+33=2,ΞΌ2=10+11+123=11\mu_1 = \frac{1 + 2 + 3}{3} = 2, \qquad \mu_2 = \frac{10 + 11 + 12}{3} = 11

Iteration 2 β€” assign. With centroids at 2 and 11, points 1,2,31, 2, 3 are all nearer 2 (distances 1, 0, 1 against 10, 9, 8) and 10,11,1210, 11, 12 are all nearer 11. No assignment changed, so the algorithm stops.

Final inertia:

J=(1βˆ’2)2+(2βˆ’2)2+(3βˆ’2)2⏟2+(10βˆ’11)2+(11βˆ’11)2+(12βˆ’11)2⏟2=4J = \underbrace{(1-2)^2 + (2-2)^2 + (3-2)^2}_{2} + \underbrace{(10-11)^2 + (11-11)^2 + (12-11)^2}_{2} = 4

Now watch what a bad start does. Initialise at ΞΌ1=1\mu_1 = 1, ΞΌ2=2\mu_2 = 2 instead:

  • Assign: 1β†’C11 \to C_1; everything else is nearer 2, so 2,3,10,11,12β†’C22, 3, 10, 11, 12 \to C_2.
  • Update: ΞΌ1=1\mu_1 = 1, ΞΌ2=(2+3+10+11+12)/5=7.6\mu_2 = (2+3+10+11+12)/5 = 7.6.
  • Assign: is 3 nearer 1 or 7.6? ∣3βˆ’1∣=2|3-1| = 2 against ∣3βˆ’7.6∣=4.6|3-7.6| = 4.6 β€” nearer 1. So 1,2,3β†’C11, 2, 3 \to C_1 and 10,11,12β†’C210, 11, 12 \to C_2.
  • Update: back to ΞΌ1=2\mu_1 = 2, ΞΌ2=11\mu_2 = 11. Same answer, one iteration later.

This particular bad start recovers. Many do not, which is the next section.

figureLloyd's algorithm from a deliberately bad startmatplotlib
Four scatter plots of the same three blobs. In the first, all three centroids sit off to one side and the colouring is wrong. By the third they have settled onto the blobs and the fourth is identical.Four scatter plots of the same three blobs. In the first, all three centroids sit off to one side and the colouring is wrong. By the third they have settled onto the blobs and the fourth is identical.
All three centroids started in the top-left corner. Inertia falls 3041.57 to 678.10 to 463.46 and then stops changing β€” the algorithm converged after 4 iterations, and every later iteration is a no-op.

See it move

sketch Assign, update, repeat p5.js
Squares are centroids, dots are points coloured by their current assignment. Each beat performs one assign-and-update pass; the inertia readout falls until it stops.

Initialisation is not a detail

Run k-means on five well-separated blobs with a single random start and you get a different answer depending on nothing but the seed:

InitialisationInertia
init="random"init="random", n_init=1n_init=1, seed 5302.46
init="random"init="random", n_init=1n_init=1, seed 9971.25
init="k-means++"init="k-means++", n_init=10n_init=10302.46

The seed-9 run is over three times worse. Look at what happened: two centroids landed inside the same blob and split it, leaving two genuine blobs merged into one cluster. Both steps of the algorithm behave perfectly; there is simply no single-point reassignment that escapes.

figureOne unlucky start, three times the inertiamatplotlib
Three panels of five blobs. The first and third correctly assign one centroid per blob. The middle panel has two centroids sharing one blob and one centroid covering two blobs.Three panels of five blobs. The first and third correctly assign one centroid per blob. The middle panel has two centroids sharing one blob and one centroid covering two blobs.
Middle panel: two centroids split a single blob while another cluster swallows two. Inertia 971.25 against 302.46 for the good runs. n_init=10 keeps the best of ten tries and finds the good answer every time.

k-means++

Rather than sampling initial centroids uniformly, k-means++ spreads them out:

  1. Choose the first centroid uniformly at random from the data.
  2. For each remaining point x\mathbf{x}, compute D(x)D(\mathbf{x}), the distance to the nearest centroid already chosen.
  3. Choose the next centroid from the data with probability proportional to D(x)2D(\mathbf{x})^2.
  4. Repeat until kk centroids are chosen.

The squared weighting makes far-away points overwhelmingly likely to be picked, so the seeds tend to land one per blob. The theoretical guarantee is that the expected inertia is within O(log⁑k)O(\log k) of the global optimum β€” before a single Lloyd iteration runs.

This is scikit-learn’s default, and n_initn_init defaults to "auto""auto" (1 for k-means++k-means++, 10 for "random""random"). If you are doing anything that matters, set n_init=10n_init=10 explicitly.

Choosing k

Inertia cannot choose kk for you. It falls monotonically β€” with k=nk = n every point is its own centroid and J=0J = 0. Here is that fall, on 400 points containing 4 genuine blobs:

kkInertiaSilhouette
116534.68β€”
26850.110.5723
31842.910.7329
4646.920.7475
5584.310.6277
6526.730.5359
7474.910.4494
8424.390.3214
9373.480.3394
10341.730.3511

The elbow method looks for the kink where the marginal improvement collapses. From k=3k = 3 to k=4k = 4 inertia drops by 1196; from 4 to 5 it drops by 63. That is the elbow, and it points at 4.

The silhouette has an actual maximum at 0.7475, also at k=4k = 4. When the two agree, you are done. When they disagree, trust the silhouette β€” the elbow is a judgement call about the shape of a curve, while the silhouette is a number you can argmax.

figureInertia never rises; the silhouette peaksmatplotlib
Two line plots. The left shows inertia falling steeply then flattening after k equals 4. The right shows the mean silhouette rising to a peak at k equals 4 then falling.Two line plots. The left shows inertia falling steeply then flattening after k equals 4. The right shows the mean silhouette rising to a peak at k equals 4 then falling.
Inertia keeps improving forever, which is why you cannot argmin it. The silhouette peaks at k = 4 with 0.7475, circled. Both agree here on 400 points containing 4 real blobs.

Neither is authoritative. On real data the curve is often smooth with no elbow at all, and the silhouette peaks at k=2k = 2 because two coarse groups always look cleanly separated. When that happens, the domain decides: how many customer segments can your marketing team actually operate?

Straight-edged cells

Because assignment compares squared distances to centroids, the boundary between two clusters is the set of points equidistant from both β€” a straight line, and in higher dimensions a hyperplane: the perpendicular bisector of the segment joining them. The partition of space is a Voronoi diagram.

figurek-means partitions the entire planematplotlib
A scatter plot of four blobs with the plane shaded into four regions separated by straight lines meeting at junction points.A scatter plot of four blobs with the plane shaded into four regions separated by straight lines meeting at junction points.
Every boundary is straight, because it is the set of points equally distant from two centroids. This is why k-means cannot produce a curved or concave cluster β€” the shape of the region is fixed by the geometry, not learned from the data.

This single geometric fact explains every k-means failure in the next section.

Where it fails

figureThree geometries, three failuresmatplotlib
A two by three grid. The top row shows the true groups for sheared blobs, blobs of different spread, and uniform noise. The bottom row shows what k-means returns for each, with adjusted Rand indices of 0.587, 0.803 and 0.000.A two by three grid. The top row shows the true groups for sheared blobs, blobs of different spread, and uniform noise. The bottom row shows what k-means returns for each, with adjusted Rand indices of 0.587, 0.803 and 0.000.
Sheared clusters: k-means cuts across the diagonal bands rather than along them (ARI 0.587). Different spreads: the wide cluster gets carved up and donates points to its tight neighbours (ARI 0.803). Uniform noise: there is nothing there, and k-means confidently returns three groups anyway (ARI 0.000, silhouette 0.378).
FailureWhyWhat to use instead
Sheared / elongated clustersVoronoi cells are isotropic; a stretched cluster costs less to cut across than alongGaussian mixture with covariance_type="full"covariance_type="full"
Very different spreadsSquared distance charges the wide cluster more, so it loses points to tight neighboursGaussian mixture, or DBSCAN if densities differ enough
Non-convex shapes (moons, rings)Straight boundaries cannot bendDBSCAN, spectral clustering
No structure at allNothing in the algorithm can say β€œthere are no clusters”Compare the silhouette against a shuffled null
OutliersThe mean is not robust; one distant point drags a centroidk-medoids, or remove outliers first

The uniform-noise case deserves emphasis. Silhouette 0.378 is not obviously bad β€” you would plausibly ship that. The defence is to compare against a null: shuffle each column independently (destroying the joint structure but preserving the marginals), cluster the shuffled data, and check that your real silhouette is meaningfully higher.

In code

kmeans_basics.py
import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
 
X, _ = make_blobs(n_samples=280, centers=3, cluster_std=0.95, random_state=4)
 
km = KMeans(n_clusters=3, n_init=10, random_state=0).fit(X)
 
print("inertia   ", round(km.inertia_, 4))
print("iterations", km.n_iter_)
print("centroids\n", km.cluster_centers_.round(3))
print("labels (first 10)", km.labels_[:10])
 
# The only clustering estimator in this phase that can label unseen points.
print("new point ->", km.predict([[0.0, 0.0]]))
 
# transform() returns distance to every centroid β€” often more useful than the label
print("distances ->", km.transform([[0.0, 0.0]]).round(3))
kmeans_basics.py
import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
 
X, _ = make_blobs(n_samples=280, centers=3, cluster_std=0.95, random_state=4)
 
km = KMeans(n_clusters=3, n_init=10, random_state=0).fit(X)
 
print("inertia   ", round(km.inertia_, 4))
print("iterations", km.n_iter_)
print("centroids\n", km.cluster_centers_.round(3))
print("labels (first 10)", km.labels_[:10])
 
# The only clustering estimator in this phase that can label unseen points.
print("new point ->", km.predict([[0.0, 0.0]]))
 
# transform() returns distance to every centroid β€” often more useful than the label
print("distances ->", km.transform([[0.0, 0.0]]).round(3))
choosing_k.py
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
from sklearn.metrics import silhouette_score
 
X, _ = make_blobs(n_samples=400, centers=4, cluster_std=0.9, random_state=11)
 
for k in range(1, 11):
    km = KMeans(n_clusters=k, n_init=10, random_state=0).fit(X)
    sil = silhouette_score(X, km.labels_) if k > 1 else float("nan")
    print(f"k={k:2d}  inertia {km.inertia_:9.2f}  silhouette {sil:.4f}")
 
# k= 1  inertia  16534.68  silhouette nan
# k= 2  inertia   6850.11  silhouette 0.5723
# k= 3  inertia   1842.91  silhouette 0.7329
# k= 4  inertia    646.92  silhouette 0.7475   <- both agree
# k= 5  inertia    584.31  silhouette 0.6277
choosing_k.py
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
from sklearn.metrics import silhouette_score
 
X, _ = make_blobs(n_samples=400, centers=4, cluster_std=0.9, random_state=11)
 
for k in range(1, 11):
    km = KMeans(n_clusters=k, n_init=10, random_state=0).fit(X)
    sil = silhouette_score(X, km.labels_) if k > 1 else float("nan")
    print(f"k={k:2d}  inertia {km.inertia_:9.2f}  silhouette {sil:.4f}")
 
# k= 1  inertia  16534.68  silhouette nan
# k= 2  inertia   6850.11  silhouette 0.5723
# k= 3  inertia   1842.91  silhouette 0.7329
# k= 4  inertia    646.92  silhouette 0.7475   <- both agree
# k= 5  inertia    584.31  silhouette 0.6277

MiniBatchKMeans

Full k-means touches every point on every iteration. MiniBatchKMeansMiniBatchKMeans samples a small batch each step and nudges the affected centroids toward it, which turns the per-iteration cost from O(n)O(n) into O(b)O(b) for batch size bb.

The price is a slightly worse optimum. On 100,000 points in 8 blobs, MiniBatch’s inertia came out 0.04% higher than full k-means; at 500,000 points, 0.01% higher. That is nothing. Use it whenever nn is large enough that full k-means is inconvenient.

minibatch.py
from sklearn.cluster import KMeans, MiniBatchKMeans
from sklearn.datasets import make_blobs
 
X, _ = make_blobs(n_samples=100_000, centers=8, cluster_std=1.0, random_state=0)
 
full = KMeans(n_clusters=8, n_init=3, random_state=0).fit(X)
mini = MiniBatchKMeans(n_clusters=8, n_init=3, batch_size=1024, random_state=0).fit(X)
 
print("full     ", round(full.inertia_, 1))            # 179598.7
print("minibatch", round(mini.inertia_, 1))            # 179671.3
print(f"penalty   {100 * (mini.inertia_ / full.inertia_ - 1):.2f}%")   # 0.04%
minibatch.py
from sklearn.cluster import KMeans, MiniBatchKMeans
from sklearn.datasets import make_blobs
 
X, _ = make_blobs(n_samples=100_000, centers=8, cluster_std=1.0, random_state=0)
 
full = KMeans(n_clusters=8, n_init=3, random_state=0).fit(X)
mini = MiniBatchKMeans(n_clusters=8, n_init=3, batch_size=1024, random_state=0).fit(X)
 
print("full     ", round(full.inertia_, 1))            # 179598.7
print("minibatch", round(mini.inertia_, 1))            # 179671.3
print(f"penalty   {100 * (mini.inertia_ / full.inertia_ - 1):.2f}%")   # 0.04%

Wall-clock timings vary far too much with thread count and BLAS build to quote a speedup you can rely on. The algorithmic claim is the durable one: each MiniBatch iteration reads batch_sizebatch_size rows instead of all nn.

algorithmK-Means (Lloyd's algorithm)Unsupervised β€” partitional clustering

APIsklearn.cluster.KMeans

Assumes

  • Clusters are convex and roughly spherical
  • Clusters have comparable spread
  • Clusters have comparable numbers of points
  • Squared Euclidean distance is the right notion of similarity
  • You know k, or can defend a choice of it

Cost

train
O(n k p i) per run, times n_init runs
predict
O(k p) per point
memory
O(n p + k p)

J β€” inertia (within-cluster sum of squares); mu_j β€” centroid of cluster j; C_j β€” the points assigned to j

Hyperparameters that matter

  • n_clustersdefault 8The whole answer. Choose with the silhouette, the elbow, or the domain.
  • initdefault 'k-means++'Spreads the seeds out. Expected inertia within O(log k) of optimal before iterating.
  • n_initdefault 'auto' (1 for k-means++)Restarts. Set to 10 for anything that matters: 302.46 vs 971.25 on this page's data.
  • max_iterdefault 300Rarely binds β€” convergence usually takes under 20 iterations.
  • toldefault 1e-4Stops when the centroid shift falls below this. Loosen for speed on huge data.
  • algorithmdefault 'lloyd''elkan' uses the triangle inequality to skip distance computations; faster on well-separated data.

Reach for it when

  • Clusters are blob-shaped and you roughly know how many
  • You need to label new points later (k-means can; DBSCAN cannot)
  • n is large β€” this is the most scalable clustering algorithm in the phase
  • You want centroids as an interpretable summary of each group

Look elsewhere when

  • Clusters are elongated, nested, or ring-shaped
  • Cluster sizes or densities differ a lot
  • The data has outliers you have not handled
  • You cannot justify any particular k

Pitfalls

Leaving n_initn_init at the default. Measured above: 971.25 against 302.46, on well-separated blobs. On messier data the gap is worse. n_init=10n_init=10 costs ten times the compute of a sub-second fit.

Arg-minimising inertia over k. Inertia is monotonically decreasing in kk; the argmin is always k=nk = n. Use it for the elbow’s shape, never for its minimum.

Not scaling. Covered on the previous page: one StandardScalerStandardScaler moved ARI from 0.035 to 0.891.

Trusting cluster indices across runs. Cluster 0 changes identity with random_staterandom_state. If you persist labels, persist the centroids and re-derive.

Applying k-means to one-hot encoded categoricals. The mean of a set of one-hot vectors is a vector of proportions, which is not a valid category. Use k-modes, or embed first.

Believing the clusters are real. k-means returns exactly kk groups from uniform noise, with a silhouette of 0.378. Always compare against a shuffled null.

Forgetting outliers move centroids. One point at distance 100 contributes 10,000 to the objective and will visibly drag its centroid. Handle outliers before clustering, or run a robust detector first.

Compare

k-meansk-medoidsGaussian mixtureMiniBatchKMeans
Representativemeanan actual data pointmean + covariancemean
Distancesquared Euclideanany metricMahalanobissquared Euclidean
Cluster shapesphericalmetric-dependentellipsoidalspherical
Robust to outliersnoyesnono
Soft assignmentnonoyesno
Cost per iterationO(nkp)O(nkp)O(n2)O(n^2)O(nkp2)O(nkp^2)O(bkp)O(bkp)
Scales to millionsyesnomoderateyes
quizCheck yourself
  1. Why is the centroid update step the MEAN of the assigned points, rather than the median?

    Show answer

    B β€” Because the objective is a sum of SQUARED distances, and the mean is its exact minimiser β€” Differentiating the sum of squared distances with respect to the centroid and setting the derivative to zero gives exactly the mean. If the objective used absolute distances instead, the minimiser would be the median β€” that algorithm is k-medians.

  2. Lloyd's algorithm is guaranteed to terminate. What is it NOT guaranteed to do?

    Show answer

    B β€” Find the global minimum of the inertia β€” Both steps weakly decrease inertia and there are finitely many partitions, so it halts. But it halts at a local minimum β€” finding the global one is NP-hard even for k=2. That is what n_init is for.

  3. You plot inertia against k and it falls smoothly with no visible elbow. What now?

    Show answer

    B β€” Check the silhouette, and if that is also flat let the domain decide β€” Inertia's minimum is always k=n, so its argmin is useless. The silhouette has a genuine maximum, and when even that is flat the honest answer is that the data has no strong k β€” pick the number your downstream process can actually use.

  4. Why can k-means never produce a crescent-shaped cluster?

    Show answer

    B β€” Because assignment is by distance to a centroid, so every boundary is a straight perpendicular bisector β€” The set of points equidistant from two centroids is a hyperplane. The partition is therefore a Voronoi diagram with straight edges, and no amount of iteration bends them. Density-based methods have no such constraint.

  5. MiniBatchKMeans on 100,000 points gave inertia 0.04% higher than full k-means. What does that tell you?

    Show answer

    B β€” The approximation cost is negligible, so use it whenever n makes full k-means inconvenient β€” A 0.04% inertia penalty is far below the run-to-run variation you get from different seeds. Each MiniBatch iteration reads batch_size rows instead of all n, so its cost per iteration does not grow with the dataset.

πŸ§ͺ Try It Yourself

Exercise 1 – One iteration by hand, in code

Exercise 2 – Prove the mean minimises the objective

Exercise 3 – What one bad seed costs

Exercise 4 – Confidently clustering pure noise

Exercise 5 – Sheared clusters break it

Recap

  • k-means minimises the within-cluster sum of squares; assignment and mean-update are the exact minimisers of that objective with the other variable held fixed.
  • It always terminates β€” non-increasing objective, finitely many partitions β€” but only at a local minimum.
  • The worked example converged to centroids 2 and 11 with inertia 4 in two iterations.
  • Initialisation matters enormously: 971.25 versus 302.46 from nothing but a different seed. Set n_init=10n_init=10.
  • Inertia cannot pick kk (it is monotone in kk). The silhouette can: it peaked at 0.7475 for k=4k = 4.
  • Boundaries are straight perpendicular bisectors, so sheared (ARI 0.587) and unequal-spread (ARI 0.803) geometries break it β€” and pure noise still yields three confident clusters.
  • MiniBatchKMeans costs 0.04% inertia at 100k points and reads a fixed batch per iteration.

Exercise 6 – Measure the seed lottery

Next

Hierarchical Clustering (Dendrograms) β€” a method that does not need kk up front, builds the whole nesting of clusters at once, and lets you choose the cut afterwards.

If this helped you, consider buying me a coffee β˜•

Buy me a coffee

Was this page helpful?

Let us know how we did