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.
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.