Login to unlock
Machine Learning Fundamentals · Phase 6: Unsupervised Learning · Clustering & Dimensionality Reduction

Unsupervised Learning Lab: Clustering and PCA Together

Lab 3 of 4 3 min read
Back to course

Objective: Cluster an unlabeled dataset with K-means, choose K using the elbow method, then use PCA purely as a visualization tool to see the clusters in 2D, and finally check how well the discovered clusters line up with the dataset's real (held-back) labels.


Setup

Every code block below runs live in your browser using Pyodide. Click Run to execute it. We'll use the load_wine dataset bundled with scikit-learn: 178 wine samples with 13 chemical measurements each. It actually has 3 known cultivar labels, but we'll deliberately ignore them while clustering and only use them at the very end, to check our unsupervised result against ground truth.


Task 1: Scale the Data and Run the Elbow Method

Scale first, since K-means is distance-based, then compute inertia across a range of K values.

import numpy as np
from sklearn.datasets import load_wine
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans

X, y_true = load_wine(return_X_y=True)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

print("K  | inertia")
for k in range(1, 8):
    model = KMeans(n_clusters=k, n_init=10, random_state=0)
    model.fit(X_scaled)
    print(f"{k}  | {model.inertia_:.1f}")

Look for the elbow: the K value where the drop in inertia from the previous K becomes noticeably smaller. Given that this dataset actually has 3 true cultivars, you should see the elbow land at or near K=3.


Task 2: Fit K-Means With Your Chosen K

import numpy as np
from sklearn.datasets import load_wine
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans

X, y_true = load_wine(return_X_y=True)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

model = KMeans(n_clusters=3, n_init=10, random_state=0)
cluster_labels = model.fit_predict(X_scaled)

print("Points per cluster:", np.bincount(cluster_labels))
print("Cluster centers shape:", model.cluster_centers_.shape)

Task 3: Visualize the Clusters With PCA

The data has 13 dimensions, far too many to plot directly. Use PCA purely as a visualization shortcut: reduce to 2 components and color each point by its K-means cluster assignment.

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_wine
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA

X, y_true = load_wine(return_X_y=True)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

cluster_labels = KMeans(n_clusters=3, n_init=10, random_state=0).fit_predict(X_scaled)

pca = PCA(n_components=2)
X_2d = pca.fit_transform(X_scaled)
print("Variance captured by these 2 components:", round(sum(pca.explained_variance_ratio_), 3))

plt.scatter(X_2d[:, 0], X_2d[:, 1], c=cluster_labels, cmap="viridis", edgecolor="white")
plt.xlabel("Principal component 1")
plt.ylabel("Principal component 2")
plt.title("K-means clusters, visualized in 2D via PCA")
show_plot()

PCA is doing no work for the clustering itself here. K-means already ran on the full 13-dimensional scaled data. PCA is only compressing the result down to 2 dimensions so a human can look at it.


Task 4: Check Against the Real Labels

This dataset secretly has real cultivar labels, which K-means never saw. Compare the cluster assignments to those labels to see how well unsupervised clustering recovered real structure.

import numpy as np
from sklearn.datasets import load_wine
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.metrics import adjusted_rand_score

X, y_true = load_wine(return_X_y=True)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

cluster_labels = KMeans(n_clusters=3, n_init=10, random_state=0).fit_predict(X_scaled)

# Adjusted Rand Index: measures agreement between two label assignments,
# correcting for chance. 1.0 = perfect agreement, 0.0 = no better than random.
# Cluster numbers don't need to match label numbers for this to work.
score = adjusted_rand_score(y_true, cluster_labels)
print(f"Adjusted Rand Index vs. true cultivar labels: {score:.3f}")

An Adjusted Rand Index well above 0 (commonly above 0.6 to 0.7 on this dataset) means K-means recovered structure that lines up closely with the real cultivar groups, despite never being told what those groups were. That's the core promise of unsupervised learning: finding genuine structure in data without being handed the answer first.

Reflection

Notice the division of labor in this lab: K-means did the actual clustering on the full-dimensional scaled data, and PCA only stepped in afterward, purely to make the result visualizable. It's a common pattern. Don't reduce dimensions before clustering by default. Cluster on the full feature space first, then use PCA only when you need to look at the result.

Found this useful?

Finished this lab?

Mark it complete to update your Phase 6: Unsupervised Learning progress.