Skip to main content
MahiDev — Learn. Build. Become AI-ready.

Lesson 17

K-Means Clustering

Group rows without labels using KMeans, read the cluster centres and labels, choose the number of clusters with inertia, and describe each group.

Intermediate25 min
K-Means Clustering hero Module 27 Lesson 17K-Means Clustering hero Module 27 Lesson 17

Clustering is unsupervised: there is no answer column, only rows. K-means places a chosen number of centres, gives every row to its nearest centre, moves each centre to the middle of its rows, and repeats until nothing changes. The result is a group number for every row, which you then interpret yourself.

Points in three coloured clusters with a cross at the centre of eachPoints in three coloured clusters with a cross at the centre of each

Groups without labels

make_blobs builds invented points in clear clumps, which is perfect for seeing what KMeans does. Note that fit receives only X. Setting n_init runs the algorithm several times from different starts and keeps the best.

python
import numpy as np
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs

points, _ = make_blobs(n_samples=150, centers=3, cluster_std=1.1, random_state=7)
model = KMeans(n_clusters=3, n_init=10, random_state=0).fit(points)

print(model.labels_[:12])
print(np.bincount(model.labels_))

Output:

text
[0 1 0 1 2 1 0 2 1 0 1 1]
[50 50 50]

Cluster centres

cluster_centers_ holds one centre per group, with one value per feature. The group numbers are arbitrary: cluster 0 is not first or best.

python
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs

points, _ = make_blobs(n_samples=150, centers=3, cluster_std=1.1, random_state=7)
model = KMeans(n_clusters=3, n_init=10, random_state=0).fit(points)
print(model.cluster_centers_.round(2))

Output:

text
[[ 9.6   0.77]
 [-8.39  5.47]
 [-1.48  4.53]]

Assign new rows

predict gives each new row the number of its nearest centre, and transform shows the distance to every centre.

python
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs

points, _ = make_blobs(n_samples=150, centers=3, cluster_std=1.1, random_state=7)
model = KMeans(n_clusters=3, n_init=10, random_state=0).fit(points)

fresh = [[-8.0, -6.0], [0.0, 0.0]]
print(model.predict(fresh))
print(model.transform(fresh).round(1))

Output:

text
[1 2]
[[18.9 11.5 12.4]
 [ 9.6 10.   4.8]]

How many clusters

inertia_ adds up each row's squared distance to its centre, so lower is tighter. It always falls as clusters are added; look for the elbow where the fall suddenly slows.

python
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs

points, _ = make_blobs(n_samples=150, centers=3, cluster_std=1.1, random_state=7)
for n in range(1, 7):
    model = KMeans(n_clusters=n, n_init=10, random_state=0).fit(points)
    print(n, round(model.inertia_, 1))

Output:

text
1 9181.9
2 1544.2
3 328.5
4 276.6
5 233.4
6 197.9

The drop from 2 to 3 is still large and the drop from 3 to 4 is small, so 3 is the elbow, matching the three clumps.

Describe groups of learners

On real columns, scale first because K-means measures distance. Then attach the labels to the table and describe each group with groupby. The score was not used for clustering, yet the groups differ in it.

python
import numpy as np
import pandas as pd

rng = np.random.default_rng(27)
learners = pd.DataFrame({
    "hours": rng.uniform(1, 12, 200).round(1),
    "practice": rng.integers(0, 41, 200),
    "video_minutes": rng.integers(0, 3001, 200),
    "track": rng.choice(["Python", "Data", "Web"], 200),
})
bonus = learners["track"].map({"Python": 0, "Data": 6, "Web": -5})
noise = rng.normal(0, 6, 200)
learners["score"] = (
    22 + learners["hours"] * 3.4 + learners["practice"] * 0.55 + bonus + noise
).clip(0, 100).round()
learners["passed"] = (learners["score"] >= 60).astype(int)

from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler

scaled = StandardScaler().fit_transform(learners[["hours", "practice"]])
learners["group"] = KMeans(n_clusters=3, n_init=10, random_state=0).fit_predict(scaled)

print(learners.groupby("group")[["hours", "practice", "score"]].mean().round(1))
print(learners["group"].value_counts().sort_index().tolist())

Output:

text
       hours  practice  score
group                        
0        3.2      23.3   45.8
1        9.0      31.0   69.7
2        8.4       7.7   56.5
[72, 65, 63]

Key takeaways

  • Clustering finds groups in rows without any labels

  • KMeans needs n_clusters; set n_init and random_state for repeatable runs

  • labels_ gives each row's group; cluster_centers_ gives each group's centre

  • inertia_ falls as clusters are added; pick the elbow

  • Scale features first, then describe each group with groupby

  • Next: Saving and Loading Models

  • Previous: Hyperparameter Tuning

Loading playground…

Check your understanding

K-Means Clustering Quiz

What makes clustering unsupervised?

1 / 10