100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Machine Learning with Scikit-learn
30 minintermediate

K-Means Clustering

K-Means Clustering

Welcome to Module 5 — Unsupervised Learning. Up to now every algorithm you've studied has relied on labelled training data: you told the model which rows were fraudulent, which emails were spam, which scores counted as 'high'. Unsupervised learning removes that luxury. You hand the algorithm raw, unlabelled data and ask it to find hidden structure on its own. This mirrors how a scout analyses thousands of match clips without pre-existing categories — the patterns emerge from the data itself.

K-Means is the most widely used clustering algorithm. Its goal is deceptively simple: partition N data points into K groups such that each point belongs to the cluster whose centre (centroid) is nearest. The algorithm iterates — moving centroids, reassigning points — until assignments stabilise. Despite its simplicity, K-Means underpins customer segmentation, image compression, document grouping, and player profiling in sports analytics.

Analogy🏏Cricket
🏏 Think of it like cricket: imagine you have ball-tracking data for 500 deliveries — speed, line, length, and turn. Without labels you want to discover natural 'delivery types'. K-Means acts like a bowling coach who studies all 500 deliveries and gradually separates them into K clusters — say, yorkers, bouncers, off-breaks, googlies. Each delivery gets assigned to whichever cluster centre it's closest to, and those centres keep shifting until every delivery feels it truly belongs to its cluster. No pre-existing category list — the clusters emerge from the physics of the ball.

The K-Means Algorithm Step by Step

The algorithm runs in four repeating steps. (1) Initialise: randomly place K centroids in the feature space. (2) Assign: label every point with the index of its nearest centroid using Euclidean distance. (3) Update: recompute each centroid as the mean of all points currently assigned to it. (4) Repeat steps 2–3 until no point changes cluster (convergence) or a maximum iteration count is reached. The result depends on the initial centroid placement, which is why scikit-learn runs the algorithm multiple times (`n_init`) and keeps the best result.

Analogy🏏Cricket
🏏 Think of it like cricket: imagine grouping a mob of net players around K coaching cones with no fixed plan. Just as you first drop the cones randomly across the ground, K-Means initialises K centroids at random positions. Just as each player then jogs to whichever cone is nearest, every data point is assigned to its closest centroid by Euclidean distance. Just as a coach then moves each cone to the exact centre of the cluster of players who gathered around it, K-Means updates each centroid to the mean of its assigned points. Just as players re-shuffle to the newly-moved cone nearest them and the cones settle again, the assign-and-update steps repeat. And just as the drill stops once nobody switches cones anymore, the algorithm converges when no point changes cluster (or hits max iterations). This simple back-and-forth — assign, recentre, repeat — reliably settles a crowd into K tidy groups without anyone dictating the final layout.

Convergence is guaranteed but not to a global optimum — K-Means finds a local minimum of the inertia (sum of squared distances from each point to its centroid). The smart initialisation strategy `k-means++` (scikit-learn's default) seeds centroids far apart to reduce the chance of a bad local minimum. It picks the first centroid uniformly at random, then each subsequent centroid with probability proportional to the squared distance from the nearest existing centroid.

Inertia and the Elbow Method

Inertia measures cluster tightness: the lower, the better. However, inertia always decreases as K increases — at K=N every point is its own cluster with zero inertia. The Elbow Method plots inertia vs K and looks for a 'kink' where the rate of decrease flattens. This kink suggests a reasonable K. Silhouette score provides a complementary view: it measures how similar a point is to its own cluster vs the next nearest cluster, ranging from −1 (wrong cluster) to +1 (perfect fit), with values near 0 indicating overlapping clusters.

Analogy🏏Cricket
🏏 Think of it like cricket: inertia is how tightly each group of players huddles around its coaching cone — smaller huddles mean cleaner groupings. Just as splitting players into ever more cones always tightens every huddle — with one cone per player the spread is zero — inertia always falls as K rises, so you can't just chase the lowest number. Just as a coach plots huddle-tightness against the number of cones and looks for the point where adding another cone barely helps, the Elbow Method plots inertia versus K and finds the kink where the drop flattens — that's your sensible K. And just as you'd double-check by asking whether each player truly fits his own group better than the neighbouring one, the silhouette score confirms it by measuring how much closer a point sits to its own cluster than to the next. Together they stop you over-splitting the squad into meaningless micro-groups.
python
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score

# ── Synthetic cricket delivery dataset ───────────────────────────────────────
# Features: [speed_kmh, line_deg, length_m, lateral_movement_cm]
X, _ = make_blobs(n_samples=400, centers=4, cluster_std=1.2, random_state=42)
X = StandardScaler().fit_transform(X)   # always scale before K-Means

# ── Elbow + Silhouette sweep ─────────────────────────────────────────────────
inertias, sil_scores = [], []
K_range = range(2, 10)

for k in K_range:
    km = KMeans(n_clusters=k, init='k-means++', n_init=10, random_state=42)
    km.fit(X)
    inertias.append(km.inertia_)
    sil_scores.append(silhouette_score(X, km.labels_))

fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].plot(K_range, inertias, 'bo-')
axes[0].set(xlabel='K', ylabel='Inertia', title='Elbow Method')
axes[1].plot(K_range, sil_scores, 'rs-')
axes[1].set(xlabel='K', ylabel='Silhouette Score', title='Silhouette Score')
plt.tight_layout(); plt.show()

# ── Fit final model ──────────────────────────────────────────────────────────
best_k = 4
km_final = KMeans(n_clusters=best_k, init='k-means++', n_init=10, random_state=42)
labels = km_final.fit_predict(X)
print(f"Cluster sizes: {np.bincount(labels)}")
print(f"Inertia: {km_final.inertia_:.2f}")
print(f"Silhouette Score: {silhouette_score(X, labels):.4f}")

Fitting K-Means in Scikit-learn

K-Means follows the familiar scikit-learn API: `KMeans(n_clusters=K).fit(X)`. Key hyperparameters are: `n_clusters` (K — the most important choice), `init` (`k-means++` or `random`), `n_init` (number of random restarts, default 10), `max_iter` (maximum EM iterations per run, default 300), and `tol` (convergence tolerance). After fitting, `km.labels_` contains the cluster index for each training point, `km.cluster_centers_` holds the K centroid coordinates, and `km.inertia_` is the total within-cluster sum of squares. To assign new points, call `km.predict(X_new)`.

Analogy🏏Cricket
🏏 Think of it like cricket: setting up a K-Means run is like briefing a fielding coach with a few clear instructions. Just as the single biggest call is how many zones to split the outfield into, n_clusters (K) is your most important choice. Just as a smart coach starts fielders in well-spread positions rather than bunched together, init='k-means++' seeds centroids sensibly instead of purely at random. Just as you'd re-run a fielding drill several times and keep the best arrangement to avoid a fluke bad start, n_init repeats the whole fit (default 10) and keeps the tightest result. Just as you cap a drill at a set number of reps, max_iter limits the update rounds, and tol says 'stop once movements are tiny.' After the drill, just as you'd note where each fielder ended up, km.labels_ hands you every point's cluster. Same familiar fit-then-read API, with a handful of dials that decide quality.
python
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
import numpy as np

# Simulated IPL batting stats: [strike_rate, boundary_pct, dot_ball_pct, avg]
np.random.seed(0)
batting = np.random.randn(200, 4)
batting = StandardScaler().fit_transform(batting)

# Fit K-Means
km = KMeans(n_clusters=3, init='k-means++', n_init=10,
            max_iter=300, random_state=42)
km.fit(batting)

# Inspect results
print("Cluster centres (scaled):")
print(np.round(km.cluster_centers_, 3))

print(f"\nLabel distribution: {np.bincount(km.labels_)}")

# Assign new unseen batters
new_batter = np.array([[0.8, 1.2, -0.5, 0.3]])   # already scaled
cluster = km.predict(new_batter)
print(f"\nNew batter assigned to cluster: {cluster[0]}")

Limitations and When K-Means Fails

K-Means has important assumptions baked in. It assumes clusters are (1) roughly spherical, (2) similarly sized, and (3) of similar density. When these assumptions break down — think crescent-shaped clusters, or one tiny tight cluster next to a vast diffuse one — K-Means produces misleading results. It is also sensitive to outliers because a single extreme point can pull a centroid far from the true group centre. Always scale features before clustering, since K-Means uses Euclidean distance which is dominated by features with the largest numeric range.

Analogy🏏Cricket
🏏 Think of it like cricket: K-Means assumes every fielding group is a neat circular huddle of similar size and density around its cone. Just as this works when players cluster in tidy round pockets, it groups spherical, evenly-sized clusters well. But just as a fielding pattern strung out along the boundary rope — a long crescent — can't be captured by one central cone, K-Means mangles crescent-shaped clusters. Just as pairing one tiny tight slip cordon next to a vast scattered deep field confuses a single-cone rule, it fails when clusters differ wildly in size or density. And just as one player who wanders far off to the sightscreen can drag the coach's cone away from where the real group stands, a single outlier can pull a centroid far from the true centre. Knowing these blind spots tells you when to reach for DBSCAN or agglomerative clustering instead of forcing round assumptions onto non-round data.

K-Means also requires you to specify K in advance, which is not always obvious. The Elbow and Silhouette methods help but do not always yield a clean answer. When the true cluster structure is unknown, it is good practice to compare K-Means results with at least one alternative (hierarchical clustering, DBSCAN) before committing to an interpretation. You will encounter those in the next two lessons.

⚠️ Never feed raw categorical or mixed-type data to K-Means — it expects continuous numerical features. Encode categoricals first (ordinal or target encoding) and scale everything. K-Means results on unscaled data are almost always meaningless.

Mini-Batch K-Means for Large Datasets

Standard K-Means loads the entire dataset into memory each iteration, making it slow for millions of rows. `MiniBatchKMeans` processes random subsets (mini-batches) each iteration, giving approximate centroids much faster. The API is identical to `KMeans`. Inertia is slightly higher than exact K-Means but the speed gain is dramatic. For datasets above ~100 K rows, prefer `MiniBatchKMeans`.

Analogy🏏Cricket
🏏 Think of it like cricket: standard K-Means is like a coach who insists on assembling all ten thousand academy players on the ground every single time before nudging his cones — thorough, but painfully slow. Just as a busy selector instead watches a fresh random sample of a few hundred players each session and adjusts his groupings from that, MiniBatchKMeans processes small random subsets each iteration to update centroids far faster. Just as sampling gives slightly rougher group centres than reviewing every player would, its inertia is a touch higher than exact K-Means. But just as covering a huge talent pool quickly matters more than perfect precision when there are millions to sift, the speed gain is dramatic on large data — with an identical fit API. The rule of thumb is simple: past roughly 100,000 rows, trade a sliver of accuracy for a huge time saving and reach for the mini-batch version.
python
from sklearn.cluster import MiniBatchKMeans
from sklearn.datasets import make_blobs
from sklearn.preprocessing import StandardScaler
import time

X_large, _ = make_blobs(n_samples=200_000, centers=5, random_state=42)
X_large = StandardScaler().fit_transform(X_large)

t0 = time.time()
mb_km = MiniBatchKMeans(n_clusters=5, batch_size=1024,
                         n_init=3, random_state=42)
mb_km.fit(X_large)
print(f"MiniBatchKMeans time: {time.time()-t0:.2f}s  |  Inertia: {mb_km.inertia_:.0f}")

💡 K-Means++ vs Random Init: With random initialisation K-Means sometimes converges to a poor local minimum where two centroids end up in the same true cluster. K-Means++ spreads initial centroids probabilistically across the data space, reducing this risk and typically converging 2–5× faster. It is the default in scikit-learn and there is almost never a reason to switch to `init='random'`.

Practical Pipeline: Clustering Customer Segments

python
import pandas as pd
import numpy as np
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.metrics import silhouette_score

# Simulated e-commerce customer features
np.random.seed(7)
n = 300
df = pd.DataFrame({
    'recency_days':    np.random.exponential(30, n),
    'frequency':       np.random.poisson(5, n),
    'monetary_value':  np.random.lognormal(6, 1.2, n),
    'avg_session_min': np.random.gamma(3, 5, n),
})

X = df.values

# Build pipeline: scale → cluster
pipe = Pipeline([
    ('scaler', StandardScaler()),
    ('kmeans', KMeans(n_clusters=4, n_init=10, random_state=42))
])
pipe.fit(X)
df['segment'] = pipe.named_steps['kmeans'].labels_

# Profile segments
print(df.groupby('segment').agg({
    'recency_days': 'mean',
    'frequency': 'mean',
    'monetary_value': 'mean',
    'avg_session_min': 'mean'
}).round(1))

X_scaled = pipe.named_steps['scaler'].transform(X)
print(f"\nSilhouette: {silhouette_score(X_scaled, df['segment']):.4f}")
  • K-Means partitions data into K clusters by iterating between centroid assignment and update steps until convergence.
  • Always StandardScale features before K-Means — Euclidean distance is sensitive to feature magnitude differences.
  • Use the Elbow Method (inertia vs K) and Silhouette Score together to choose K; neither gives a perfect answer alone.
  • K-Means++ initialisation spreads starting centroids to avoid poor local minima — it is the scikit-learn default.
  • K-Means assumes spherical, equal-density clusters; it fails on non-convex shapes, heavy outliers, or unequal cluster sizes.
  • MiniBatchKMeans offers near-identical results with far less compute time for datasets above ~100 K rows.
  • Always embed K-Means in a Pipeline so scaling and clustering are applied consistently to train and test data.
Lesson 25 of 35
0% complete