Everything you have built so far came with an answer key. Regression predicted a number you already knew for each example; classification sorted rows into labels you were given. That is supervised learning — the supervision is the label, the correct answer handed to the model on every row.
A great deal of real data arrives without one. A shop has thousands of transactions but no column saying which customer each belongs to. A support inbox holds a million tickets but no field grouping them by the actual problem underneath. Nobody labelled them, and labelling by hand is the slow, expensive part of any machine-learning project. Unsupervised learning is what you reach for when the answers are missing: the algorithm must find the structure on its own.
Clustering is the most common unsupervised task: group the rows so that items in the same group resemble each other and items in different groups do not, with the grouping proposed from the shape of the data alone.
flowchart LR
subgraph S["supervised: answers given"]
A1["data + label"] --> A2["model maps input<br/>to the known answer"]
end
subgraph U["unsupervised: no answers"]
B1["data only"] --> B2["model discovers<br/>the groups itself"]
end
style A1 fill:#10b981,color:#fff
style B1 fill:#b45309,color:#fff
Similarity means distance
If there is no label to copy, the only thing left to group by is how alike two rows are. We turn that into a number with a distance measure. The everyday choice is Euclidean distance — the straight-line gap between two points, the formula you get from Pythagoras. Two rows that sit close together under that measure are judged similar; two that sit far apart are judged different.
That single idea — near means similar — is the whole engine of clustering. Pick a measure of distance and almost every clustering algorithm you will ever meet reduces to grouping points that are near each other and keeping apart those that are far. The measure you choose decides what near means, which is why a poor measure produces poor clusters even from clean data.
import math
def distance(p, q):
return math.sqrt(sum((a - b) ** 2 for a, b in zip(p, q)))
centers = [(0.0, 0.0), (10.0, 10.0)]
point = (3.0, 2.0)
for i, c in enumerate(centers):
print('dist to center', i, '=', round(distance(point, c), 2))
Run that and the idea becomes concrete. The point (3, 2) lies about 3.6 units from the centre at the origin and about 10.6 from the one at (10, 10). Closer means more similar, so this point clearly belongs with the first centre, not the second. Nothing was labelled — the decision fell out of arithmetic on the features alone.
This is the atom of every clustering method: turn each row into a point, pick some centres, and let distance decide the rest. Everything fancier you will meet later is a variation on assigning points to the centre they sit nearest, so it is worth seeing that one move cleanly before it gets buried inside a larger algorithm.
import math
def distance(p, q):
return math.sqrt(sum((a - b) ** 2 for a, b in zip(p, q)))
def nearest(point, centers):
best = 0
best_d = distance(point, centers[0])
for i in range(1, len(centers)):
d = distance(point, centers[i])
if d < best_d:
best = i
best_d = d
return best
centers = [(0.0, 0.0), (10.0, 10.0)]
points = [(1.0, 2.0), (2.0, 1.0), (9.0, 11.0), (11.0, 9.0)]
for p in points:
print(p, '-> cluster', nearest(p, centers))
What clustering is good for
Clustering earns its keep on tasks where the groups are real but unnamed. Customer segmentation — finding the natural bunches in purchase behaviour so a business can speak to each one differently. Document grouping — surfacing the topics that recur across thousands of articles without anyone reading them first. Anomaly detection — points that sit far from every cluster are often the errors, the fraud, the broken sensor reading.
What unites these is that the structure is worth finding even though nobody wrote it down. Wherever you have a haystack and the shapes of the haystack itself are the interesting part, clustering is the first tool to reach for. Notice what did the work in the run above, too: the centres. Move a centre and the whole grouping shifts, because the clusters are defined entirely by where the centres sit.
flowchart TD C["clustering suits"] --> T1["customer segmentation"] C --> T2["document or topic grouping"] C --> T3["anomaly detection"] C --> M["clustering misleads"] M --> N1["when true groups overlap"] M --> N2["when k is wrong"] style C fill:#10b981,color:#fff style M fill:#b91c1c,color:#fff
The scale trap
Look at one more run. The income column is measured in thousands of dollars and age in single years, so the distance is almost entirely income — a thirty-year age gap counts for less than a thousand-dollar income gap. When features live on wildly different scales, the largest numbers hijack the distance and the smallest contribute almost nothing.
import math
def distance(p, q):
return math.sqrt(sum((a - b) ** 2 for a, b in zip(p, q)))
# (age in years, income in dollars)
a = (30, 30000)
b = (32, 31000)
c = (60, 30000)
print('a vs b:', round(distance(a, b), 1))
print('a vs c:', round(distance(a, c), 1))
flowchart LR P["points in space"] --> D["measure distance<br/>to each center"] D --> G["assign to nearest center"] G --> CL["a cluster"] style D fill:#3776ab,color:#fff
Write distance(p, q) returning the Euclidean (straight-line) distance between two equal-length points. Use math.sqrt and a generator over zip(p, q).
import math
def distance(p, q):
# straight-line gap, Pythagoras
pass
Square each (a - b), sum them, then take the square root.
math.sqrt(sum((a - b) ** 2 for a, b in zip(p, q))) does it in one expression.
import math
def distance(p, q):
return math.sqrt(sum((a - b) ** 2 for a, b in zip(p, q)))
What does this print? The distance from the origin to (6, 8) is a clean Pythagorean triple.
import math
def distance(p, q):
return math.sqrt(sum((a - b) ** 2 for a, b in zip(p, q)))
print(round(distance((0, 0), (6, 8)), 2))
6 squared plus 8 squared is 36 + 64 = 100.
The square root of 100 is 10.0.
This nearest function is meant to return the index of the CLOSEST centre, but the comparison is backwards, so it picks the FARTHEST one. A clustering built on this would assign every point to the wrong group. Fix the one operator.
import math
def distance(p, q):
return math.sqrt(sum((a - b) ** 2 for a, b in zip(p, q)))
def nearest(point, centers):
best, best_d = 0, distance(point, centers[0])
for i in range(1, len(centers)):
d = distance(point, centers[i])
if d > best_d: # bug: picks the FARTHEST center
best, best_d = i, d
return best
You want the smallest distance, so keep a centre only when its distance is smaller.
Change the > to a <.
import math
def distance(p, q):
return math.sqrt(sum((a - b) ** 2 for a, b in zip(p, q)))
def nearest(point, centers):
best, best_d = 0, distance(point, centers[0])
for i in range(1, len(centers)):
d = distance(point, centers[i])
if d < best_d:
best, best_d = i, d
return best
Which task is clustering best suited for?
Clustering is unsupervised: it finds groups in data that has no labels. The tickets carry no pre-assigned categories, so discovering the natural groupings is exactly its job. The other three need labelled answers — supervised tasks.
Write assign(points, centers) that returns a list of cluster indices — one per point — where each point is assigned to the index of its nearest centre. Include distance yourself so the function stands alone.
import math
def distance(p, q):
return math.sqrt(sum((a - b) ** 2 for a, b in zip(p, q)))
def assign(points, centers):
# one label per point: the index of its nearest center
pass
Loop over points; for each, track the index of the smallest distance.
Collect those indices into a list and return it.
import math
def distance(p, q):
return math.sqrt(sum((a - b) ** 2 for a, b in zip(p, q)))
def assign(points, centers):
labels = []
for p in points:
best, best_d = 0, distance(p, centers[0])
for i in range(1, len(centers)):
d = distance(p, centers[i])
if d < best_d:
best, best_d = i, d
labels.append(best)
return labels
Recap
- Supervised learning has labels — an answer key; unsupervised does not, and must find structure on its own.
- Clustering groups rows by similarity, which means distance: near points are alike, far points are not.
- Euclidean distance is the default measure, and the measure you choose defines what near means.
- Clustering suits tasks where the groups are real but unnamed — segmentation, topic discovery, anomaly detection.
- It is scale-sensitive and always returns clusters whether or not real ones exist, so its output must be checked, not assumed.
Next you build the most famous clustering algorithm — k-means — by writing the loop that assigns points to centres and then moves those centres to fit the data.