Skip to content

Introduction to Clustering

What you’ll learn

  • what a cluster is, and why the definition is a choice rather than a fact
  • three distance metrics, computed by hand on the same pair of points
  • why scaling changes the answer, measured: ARI 0.035 → 0.891
  • the silhouette coefficient, derived and computed by hand
  • three internal validation scores, and what each one rewards
  • why “how many clusters?” has no purely mathematical answer

Intuition

Every supervised page so far had an answer key. You fit a model, compared its predictions with yy, and got a number. Clustering has no yy. You hand an algorithm a matrix of features and it hands back a group label for each row, and there is nothing to check it against.

That sounds like it should make clustering easier — no labels to collect, no leakage to worry about. It makes it harder, because the question “is this grouping right?” has no answer. It has only the question behind it: right for what?

Consider six people described by two numbers, age and annual income:

PersonAgeIncome
A2448,000
B2652,000
C2947,000
D5151,000
E5449,000
F5653,000

There are two obvious groupings. By age: {A,B,C}\{A, B, C\} and {D,E,F}\{D, E, F\}. By income: everyone in one group, because incomes barely differ. Both are defensible. Which one an algorithm returns is decided entirely by how you measure “close” — and that decision is made before the algorithm runs, by the units your columns happen to be in.

That is the single most important fact on this page. Clustering algorithms do not find structure. They find the structure implied by your distance metric.

diagram Diagram mermaid

The math

A metric on a feature space is a function d(x,y)d(\mathbf{x}, \mathbf{y}) satisfying four properties: it is non-negative, it is zero exactly when the points coincide, it is symmetric, and it obeys the triangle inequality

d(x,z)d(x,y)+d(y,z)d(\mathbf{x}, \mathbf{z}) \le d(\mathbf{x}, \mathbf{y}) + d(\mathbf{y}, \mathbf{z})

Three metrics cover almost everything you will use.

Euclidean distance (the L2L_2 norm) is straight-line distance:

d2(x,y)=j=1p(xjyj)2d_2(\mathbf{x}, \mathbf{y}) = \sqrt{\sum_{j=1}^{p} (x_j - y_j)^2}

Manhattan distance (the L1L_1 norm) is the distance walked along the axes:

d1(x,y)=j=1pxjyjd_1(\mathbf{x}, \mathbf{y}) = \sum_{j=1}^{p} |x_j - y_j|

Cosine distance ignores magnitude entirely and measures only the angle between the two vectors:

dcos(x,y)=1xyxyd_{\cos}(\mathbf{x}, \mathbf{y}) = 1 - \frac{\mathbf{x} \cdot \mathbf{y}} {\lVert \mathbf{x} \rVert \, \lVert \mathbf{y} \rVert}

Cosine distance is not a true metric — it violates the triangle inequality — but it is the right choice whenever the direction of a vector carries the meaning and its length does not. Document term-frequency vectors are the standard example: a 200-word article and a 2,000-word article about the same topic point the same way but have wildly different lengths.

Both L1L_1 and L2L_2 are special cases of the Minkowski distance:

dq(x,y)=(j=1pxjyjq)1/qd_q(\mathbf{x}, \mathbf{y}) = \left( \sum_{j=1}^{p} |x_j - y_j|^{q} \right)^{1/q}

with q=1q = 1 and q=2q = 2. As qq \to \infty this converges to the Chebyshev distance, maxjxjyj\max_j |x_j - y_j|.

Worked example by hand

Take a=(1,1)\mathbf{a} = (1, 1) and b=(4,5)\mathbf{b} = (4, 5). The differences are 33 and 44.

Euclidean:

d2=32+42=9+16=25=5d_2 = \sqrt{3^2 + 4^2} = \sqrt{9 + 16} = \sqrt{25} = 5

Manhattan:

d1=3+4=7d_1 = |3| + |4| = 7

Cosine: the dot product is 14+15=91 \cdot 4 + 1 \cdot 5 = 9. The norms are a=2=1.41421\lVert \mathbf{a} \rVert = \sqrt{2} = 1.41421 and b=41=6.40312\lVert \mathbf{b} \rVert = \sqrt{41} = 6.40312.

cosθ=91.41421×6.40312=99.05539=0.993884\cos\theta = \frac{9}{1.41421 \times 6.40312} = \frac{9}{9.05539} = 0.993884
dcos=10.993884=0.006116d_{\cos} = 1 - 0.993884 = 0.006116

which corresponds to an angle of 6.34°6.34°. By Euclidean distance these points are five units apart — substantial. By cosine distance they are practically identical, because they point almost the same way from the origin. Same two points, two completely different verdicts.

figureThree metrics, one pair of pointsmatplotlib
Three panels showing the same two points. The first joins them with a straight line labelled 5.00, the second with an L-shaped path labelled 7.00, the third draws both as arrows from the origin 6.3 degrees apart.Three panels showing the same two points. The first joins them with a straight line labelled 5.00, the second with an L-shaped path labelled 7.00, the third draws both as arrows from the origin 6.3 degrees apart.
The Euclidean and Manhattan panels agree that a and b are far apart. Cosine distance sees two vectors pointing almost the same direction and reports 0.0061 — near-zero. Nothing about the data changed; only the definition of 'close' did.

Scaling decides the answer

Here is the failure the six-person table hinted at, run for real on 180 people. Ages are drawn from two groups centred on 28 and 52; incomes are drawn from two nearly identical distributions centred on 60,000 and 64,000. The genuine structure is entirely in the age column.

The raw column standard deviations are 12.3 years and 12,698 dollars. Squared Euclidean distance adds those contributions together, so the income term is about a million times larger. The age column is arithmetically invisible.

Input to k-meansAdjusted Rand index vs the real groups
Raw columns0.035
After StandardScalerStandardScaler0.891
figureThe same data, clustered twicematplotlib
Three scatter plots of age against income. The first shows the true groups split left and right by age. The second, k-means on raw columns, splits top and bottom by income. The third, after scaling, recovers the left-right split.Three scatter plots of age against income. The first shows the true groups split left and right by age. The second, k-means on raw columns, splits top and bottom by income. The third, after scaling, recovers the left-right split.
k-means on raw columns cuts horizontally, splitting high earners from low earners — it never sees age at all. After standardising, the same algorithm on the same data recovers the two age groups. ARI goes from 0.035 to 0.891.

Scale your features before clustering, unless you have a specific reason not to. The exception is when the units are already comparable and the relative spread is meaningful — pixel intensities, or a set of columns all measured in dollars.

Scoring a clustering without labels

You cannot compute accuracy without an answer key, but you can ask whether the groups are internally tight and mutually far apart. Three scores do this.

The silhouette coefficient

For a single point ii in cluster CIC_I, define

  • a(i)a(i) = mean distance from ii to the other points in its own cluster (how tight it sits),
  • b(i)b(i) = the smallest mean distance from ii to all points of any other cluster (how far it is from the nearest rival).

Then

s(i) = \frac{b(i) - a(i)}{\max$\{a(i),\, b(i)\}$}, \qquad s(i) \in [-1, 1]

If CI=1|C_I| = 1 the convention is s(i)=0s(i) = 0. Reading the value:

  • s(i)s(i) near +1+1: much closer to its own cluster than to any other — well placed.
  • s(i)s(i) near 00: sits on the boundary between two clusters.
  • s(i)s(i) negative: closer, on average, to a different cluster than to its own — misassigned.

The silhouette score of a clustering is the mean of s(i)s(i) over every point.

Worked silhouette, by hand

Five points on a line: x=1,2,3x = 1, 2, 3 in cluster 0, and x=10,11x = 10, 11 in cluster 1. Take the point at x=2x = 2.

Its own cluster contains 11 and 33, both at distance 1:

a=1+12=1a = \frac{1 + 1}{2} = 1

The other cluster contains 10 and 11, at distances 8 and 9:

b=8+92=8.5b = \frac{8 + 9}{2} = 8.5
s=8.51max(1, 8.5)=7.58.5=0.8824s = \frac{8.5 - 1}{\max(1,\ 8.5)} = \frac{7.5}{8.5} = 0.8824

Now take the point at x=3x = 3, which sits at the edge. a=(2+1)/2=1.5a = (2 + 1)/2 = 1.5 and b=(7+8)/2=7.5b = (7 + 8)/2 = 7.5, so s=6/7.5=0.8s = 6/7.5 = 0.8. Lower, as it should be — that point is nearer the other group.

The other two

Davies-Bouldin index. For each cluster, find the worst-case ratio of “sum of the two clusters’ average internal spread” to “distance between their centroids”, then average those worst cases:

DB=1ki=1kmaxjiσi+σjd(ci,cj)\mathrm{DB} = \frac{1}{k}\sum_{i=1}^{k} \max_{j \ne i} \frac{\sigma_i + \sigma_j}{d(\mathbf{c}_i, \mathbf{c}_j)}

Lower is better, and 0 is perfect. This is the one score on this page where a smaller number means a better clustering — a common source of sign errors.

Calinski-Harabasz index (the variance-ratio criterion) is between-cluster dispersion over within-cluster dispersion, scaled by the degrees of freedom:

CH=tr(Bk)tr(Wk)×nkk1\mathrm{CH} = \frac{\operatorname{tr}(B_k)}{\operatorname{tr}(W_k)} \times \frac{n - k}{k - 1}

Higher is better. It is by far the cheapest of the three — O(n)O(n) rather than the silhouette’s O(n2)O(n^2) — which matters once nn passes a few tens of thousands.

ScoreRangeBetterCostBiased toward
Silhouette[1,1][-1, 1]higherO(n2)O(n^2)convex, equally sized clusters
Davies-Bouldin[0,)[0, \infty)lowerO(n2)O(n^2)convex clusters
Calinski-Harabasz[0,)[0, \infty)higherO(n)O(n)convex clusters, many of them

All three assume clusters are roughly round blobs. On the two-moons dataset, a correct clustering scores worse than a wrong one on all three. Internal scores are a tool for comparing runs on similar geometry, not a verdict on truth.

figureReading a silhouette plotmatplotlib
A silhouette plot of four clusters beside the scatter plot it describes. Two clusters have wide, tall silhouettes; two are thinner with values near zero.A silhouette plot of four clusters beside the scatter plot it describes. Two clusters have wide, tall silhouettes; two are thinner with values near zero.
Each horizontal bar is one point, sorted within its cluster. Clusters 0 and 2 are cleanly separated and average 0.664 and 0.642. Clusters 1 and 3 overlap in the scatter on the right and average 0.330 and 0.319, with the worst point at -0.018 — that point is closer to a rival cluster than to its own. Overall mean: 0.489.

See it move

sketch Unlabelled data becomes clustered data p5.js
The same points on both sides. On the left they are just points; on the right a clustering algorithm assigns each one a group and the centroids settle in.

And here is the metric choice made tangible — drag the slider by clicking, and watch which pairs count as “near” flip as the exponent qq of the Minkowski distance changes.

sketch The unit circle under three metrics p5.js
Every point on each curve is exactly distance 1 from the centre under that metric. The shape of the neighbourhood is what the metric actually is.

Not every shape is a blob

Different algorithms make different assumptions about what a cluster looks like, and those assumptions are the whole difference between them. Here are four geometries and two algorithms.

figureTwo algorithms, four geometriesmatplotlib
A two by four grid. The top row shows k-means on round blobs, sheared blobs, blobs of different spread, and two moons. The bottom row shows DBSCAN on the same four datasets.A two by four grid. The top row shows k-means on round blobs, sheared blobs, blobs of different spread, and two moons. The bottom row shows DBSCAN on the same four datasets.
k-means handles round equal blobs and nothing else: it slices the sheared clusters across their length and cuts the moons in half. DBSCAN recovers the sheared clusters and the moons exactly. It fails on the third column, where one global density threshold cannot serve a wide cluster and a tight one at once.

That third column matters. Neither algorithm is universally better; each fails where its assumption is violated. The two later pages in this phase are the detailed versions of these two columns.

In code

first_clustering.py
import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
from sklearn.metrics import (
    calinski_harabasz_score,
    davies_bouldin_score,
    silhouette_score,
)
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
 
X, y_true = make_blobs(n_samples=400, centers=4, cluster_std=0.9, random_state=11)
 
# Scaling belongs inside the pipeline, exactly as in supervised work.
model = make_pipeline(StandardScaler(), KMeans(n_clusters=4, n_init=10, random_state=0))
labels = model.fit_predict(X)
 
print("silhouette       ", round(silhouette_score(X, labels), 4))    # 0.7475
print("davies-bouldin   ", round(davies_bouldin_score(X, labels), 4))  # 0.3578, lower is better
print("calinski-harabasz", round(calinski_harabasz_score(X, labels), 1))  # 3241.8
first_clustering.py
import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
from sklearn.metrics import (
    calinski_harabasz_score,
    davies_bouldin_score,
    silhouette_score,
)
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
 
X, y_true = make_blobs(n_samples=400, centers=4, cluster_std=0.9, random_state=11)
 
# Scaling belongs inside the pipeline, exactly as in supervised work.
model = make_pipeline(StandardScaler(), KMeans(n_clusters=4, n_init=10, random_state=0))
labels = model.fit_predict(X)
 
print("silhouette       ", round(silhouette_score(X, labels), 4))    # 0.7475
print("davies-bouldin   ", round(davies_bouldin_score(X, labels), 4))  # 0.3578, lower is better
print("calinski-harabasz", round(calinski_harabasz_score(X, labels), 1))  # 3241.8

Note fit_predictfit_predict. Clustering estimators have no yy argument, so fit(X)fit(X) then .labels_.labels_, or fit_predict(X)fit_predict(X) in one step. Only some of them — k-means and Gaussian mixtures — can also predictpredict on unseen rows; DBSCAN and agglomerative clustering cannot, because their labels are defined relative to the training set.

scaling_matters.py
import numpy as np
from sklearn.cluster import KMeans
from sklearn.metrics import adjusted_rand_score
from sklearn.preprocessing import StandardScaler
 
rng = np.random.default_rng(0)
age = np.concatenate([rng.normal(28, 4, 90), rng.normal(52, 4, 90)])
income = np.concatenate([rng.normal(60_000, 12_000, 90), rng.normal(64_000, 12_000, 90)])
X = np.column_stack([age, income])
truth = np.concatenate([np.zeros(90), np.ones(90)])
 
raw = KMeans(n_clusters=2, n_init=10, random_state=0).fit_predict(X)
scaled = KMeans(n_clusters=2, n_init=10, random_state=0).fit_predict(
    StandardScaler().fit_transform(X)
)
 
print("column stds:", X.std(axis=0).round(1))   # [   12.3 12697.7]
print("ARI raw   ", round(adjusted_rand_score(truth, raw), 4))      # 0.0348
print("ARI scaled", round(adjusted_rand_score(truth, scaled), 4))   # 0.8914
scaling_matters.py
import numpy as np
from sklearn.cluster import KMeans
from sklearn.metrics import adjusted_rand_score
from sklearn.preprocessing import StandardScaler
 
rng = np.random.default_rng(0)
age = np.concatenate([rng.normal(28, 4, 90), rng.normal(52, 4, 90)])
income = np.concatenate([rng.normal(60_000, 12_000, 90), rng.normal(64_000, 12_000, 90)])
X = np.column_stack([age, income])
truth = np.concatenate([np.zeros(90), np.ones(90)])
 
raw = KMeans(n_clusters=2, n_init=10, random_state=0).fit_predict(X)
scaled = KMeans(n_clusters=2, n_init=10, random_state=0).fit_predict(
    StandardScaler().fit_transform(X)
)
 
print("column stds:", X.std(axis=0).round(1))   # [   12.3 12697.7]
print("ARI raw   ", round(adjusted_rand_score(truth, raw), 4))      # 0.0348
print("ARI scaled", round(adjusted_rand_score(truth, scaled), 4))   # 0.8914

When you do have labels

Sometimes you have ground truth — you are evaluating a clustering method on a benchmark, or you withheld a label column deliberately. Then use an external score, one that is invariant to how the clusters are numbered.

The adjusted Rand index counts pairs of points that the two labellings agree about (both put them together, or both put them apart), then corrects for the agreement expected by chance:

ARI=RIE[RI]max(RI)E[RI]\mathrm{ARI} = \frac{\mathrm{RI} - \mathbb{E}[\mathrm{RI}]}{\max(\mathrm{RI}) - \mathbb{E}[\mathrm{RI}]}

It is 1.0 for a perfect match, about 0.0 for random labelling, and can go slightly negative. Every comparison in this phase that has a ground truth is reported as an ARI.

Normalised mutual information measures how much knowing one labelling tells you about the other, normalised to [0,1][0, 1]. It is more forgiving than ARI when one labelling splits a true cluster into several pieces.

SituationUse
No labels at all (the usual case)silhouette, Davies-Bouldin, Calinski-Harabasz
Labels available for evaluationadjusted Rand index, normalised mutual information
Comparing k across runs on one datasetsilhouette or Calinski-Harabasz
Comparing algorithms with different geometry assumptionsyour eyes, and the downstream task

Clustering as a feature

A cluster assignment is a categorical feature, and one-hot encoding it can help a supervised model enormously. If you have a regionregion column with 800 distinct values, clustering the region-level statistics into 12 groups gives a supervised model something it can actually learn from.

The same leakage rule as everywhere else applies: the clusterer must be fitted inside the cross-validation fold, on training rows only. KMeansKMeans is a transformer as well as an estimator — its transformtransform returns the distance from each row to each centroid, which is often more useful than the hard label.

cluster_as_feature.py
from sklearn.cluster import KMeans
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
 
# X and y here are your own supervised dataset.
# KMeans.transform gives k distance columns, which then feed the classifier.
pipe = make_pipeline(
    StandardScaler(),
    KMeans(n_clusters=12, n_init=10, random_state=0),  # acts as a transformer here
    LogisticRegression(max_iter=2000),
)
# cross_val_score refits the KMeans on each training fold — no leakage.
scores = cross_val_score(pipe, X, y, cv=5)
cluster_as_feature.py
from sklearn.cluster import KMeans
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
 
# X and y here are your own supervised dataset.
# KMeans.transform gives k distance columns, which then feed the classifier.
pipe = make_pipeline(
    StandardScaler(),
    KMeans(n_clusters=12, n_init=10, random_state=0),  # acts as a transformer here
    LogisticRegression(max_iter=2000),
)
# cross_val_score refits the KMeans on each training fold — no leakage.
scores = cross_val_score(pipe, X, y, cv=5)
algorithmClustering (the task, not one algorithm)Unsupervised — grouping

APIsklearn.cluster

Assumes

  • There is meaningful group structure in the feature space you built
  • Your distance metric reflects the similarity you actually care about
  • Features are on comparable scales (or you scaled them)
  • The specific algorithm's shape assumption matches your data

Cost

train
k-means O(n k p i); DBSCAN O(n log n) with an index; agglomerative O(n^2 log n)
predict
k-means O(k p); DBSCAN and agglomerative cannot predict at all
memory
agglomerative needs the O(n^2) distance matrix; k-means needs O(n p)

d(x, y) — distance; k — number of clusters; s(i) — silhouette of point i; C_I — the cluster containing i

Hyperparameters that matter

  • the distance metricdefault euclideanDecides the answer before the algorithm runs. Cosine for text, euclidean for most else.
  • feature scalingdefault none — you must add itWithout it, the widest-ranging column dominates every distance.
  • number of clustersdefault algorithm-specifick-means and agglomerative need it up front; DBSCAN and HDBSCAN infer it.

Reach for it when

  • You want to explore an unlabelled dataset before committing to a model
  • You need to segment customers, documents, regions or sensors
  • You want a compact categorical feature for a supervised model
  • You want to compress a dataset by replacing points with centroids

Look elsewhere when

  • You have labels — a supervised model will beat any clustering at that task
  • You cannot articulate what a 'group' means for your problem
  • The features are arbitrary and unscaled and you have not looked at them

Pitfalls

Clustering unscaled features. Measured above: ARI 0.035 versus 0.891 on the same data. This is the single most common clustering mistake and it is silent — you get plausible-looking clusters that encode nothing but your unit choices.

Treating the silhouette as truth. It rewards round, equally sized, well-separated clusters. A correct DBSCAN clustering of two moons scores badly on it. Use it to compare similar runs, never to declare one geometry better than another.

Reading meaning into cluster numbers. Cluster 0 in one run is cluster 2 in the next; the labelling is arbitrary and changes with random_staterandom_state. Never join on the raw integer, never write if label == 0if label == 0 in production code. Map clusters to named segments explicitly after inspecting them.

Forgetting that k-means always returns k clusters. Hand it uniform noise and ask for three groups and it will give you three groups, with a silhouette of 0.378 — respectable-looking. Nothing in the algorithm tells you the structure was not there.

Clustering with a distance metric that ignores your categorical columns. One-hot encoding puts every category exactly 2\sqrt{2} apart in Euclidean space, which is rarely what you mean. Consider Gower distance, or cluster the numeric block and treat the categories separately.

Fitting the scaler on all the data before splitting off a validation set. Clustering feels exploratory so this feels harmless, but the moment the cluster label becomes a feature for a supervised model, it is leakage like any other.

Compare

AlgorithmCluster shapeNeeds k?Handles noiseCan predict new pointsScales to 1M rows
k-meansconvex, similar sizeyesnoyesyes, with MiniBatch
Agglomerativedepends on linkageyes, or a cut heightnonono — O(n2)O(n^2) memory
DBSCANany shapenoyesnoyes, with an index
HDBSCANany shape, varying densitynoyesapproximatelyyes
Gaussian mixtureellipsoidalyessoftyesmoderate
Spectralany shapeyesnonono — O(n3)O(n^3)
quizCheck yourself
  1. Your customer table has age (18-80) and lifetime spend (0-50,000). You run k-means on the raw columns. What will the clusters encode?

    Show answer

    B — Almost entirely spend, because its squared differences dwarf age's — Squared Euclidean distance adds the two contributions. Spend varies over 50,000 units and age over 62, so the spend term is roughly a million times larger. The measured version of this on the page moved ARI from 0.035 to 0.891 once the columns were standardised.

  2. A point has silhouette coefficient -0.3. What does that mean?

    Show answer

    B — Its average distance to some other cluster is smaller than to its own cluster — s = (b - a) / max(a, b) is negative exactly when a > b — the point is on average closer to a rival cluster than to its own. It is a misassignment signal, not an outlier signal.

  3. Which of these scores is better when it is LOWER?

    Show answer

    B — Davies-Bouldin — Davies-Bouldin averages worst-case spread-over-separation ratios, so 0 is perfect. The other three are all higher-is-better, which makes this the easiest sign error to make in a model-selection loop.

  4. You cluster 5,000 news articles by their TF-IDF vectors. Which distance metric fits best?

    Show answer

    B — Cosine, because article length should not make two articles on the same topic look far apart — TF-IDF vector length scales with document length. Cosine distance measures only direction, so a 200-word and a 2,000-word article on the same topic land close together. This is the standard choice for text.

  5. You run k-means with k=3 on uniformly random 2-D points. What happens?

    Show answer

    B — It returns three clusters with a plausible-looking silhouette of about 0.38 — Measured on the page: uniform noise, k=3, silhouette 0.3775 and ARI 0.000. The algorithm always returns exactly k groups. No internal score reliably tells you the structure was absent — that is your job.

🧪 Try It Yourself

Exercise 1 – Three metrics on one pair

Exercise 2 – Scaling flips the answer

Exercise 3 – Silhouette by hand, then by sklearn

Exercise 4 – The three internal scores disagree

Exercise 5 – A good clustering that scores badly

Recap

  • Clustering has no answer key, so the distance metric is not a detail — it is the definition of the problem.
  • Euclidean, Manhattan and cosine gave 5.00, 7.00 and 0.0061 for the same pair of points.
  • Scale first. ARI went from 0.035 to 0.891 on identical data with one StandardScalerStandardScaler.
  • The silhouette of a point is (ba)/max(a,b)(b - a)/\max(a, b); negative means misassigned.
  • Davies-Bouldin is the one score where lower is better.
  • All three internal scores reward round blobs. A perfect DBSCAN clustering of two moons scores 0.329 against k-means’ wrong-but-round 0.479.
  • k-means will always return exactly kk clusters, including on pure noise.

Exercise 6 – Cluster data that has no clusters

Next

K-Means Clustering Algorithm — Lloyd’s algorithm iteration by iteration, why it always converges but not always well, and the three geometries where it fails outright.

If this helped you, consider buying me a coffee ☕

Buy me a coffee

Was this page helpful?

Let us know how we did