Clustering and manifold learning
KMeans and MiniBatchKMeans, DBSCAN and AgglomerativeClustering, silhouette scores, PCA for compression and t-SNE for exploration.
KMeans and its variants
from sklearn.cluster import KMeans, MiniBatchKMeans
from sklearn.metrics import silhouette_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
km = Pipeline([("scale", StandardScaler()),
("km", KMeans(n_clusters=5, n_init=10, random_state=42))])
labels = km.fit_predict(X)
print(np.bincount(labels))
print(round(silhouette_score(km[:-1].transform(X), labels), 3))
# for large data, MiniBatchKMeans trades a little quality for speed
mb = MiniBatchKMeans(n_clusters=5, batch_size=1024, n_init=10, random_state=42)
print(km.named_steps["km"].cluster_centers_.shape) # (5, n_features)
print(km.named_steps["km"].inertia_) # within-cluster sum of squaresn_init=10runs the initialisation ten times and keeps the best inertia; the old default of a single run is why clusters sometimes looked arbitrary.- Always scale first. KMeans minimises Euclidean distance, so the widest-range column decides the clusters.
- Set
random_statefor reproducibility, and use.predict(X_new)to assign new rows to the fitted centroids.
Density-based and hierarchical clustering
from sklearn.cluster import AgglomerativeClustering, DBSCAN
from sklearn.metrics import adjusted_rand_score
db = DBSCAN(eps=0.7, min_samples=10).fit(X_scaled)
print(np.unique(db.labels_, return_counts=True)) # -1 marks noise points
agg = AgglomerativeClustering(n_clusters=5, linkage="ward")
agg.fit(X_scaled)
print(adjusted_rand_score(db.labels_, agg.labels_)) # agreement between two views| Method | Finds | Needs | Fails when |
|---|---|---|---|
| KMeans | Compact, similar-sized groups | k, scaling | Clusters are elongated or unevenly sized |
| MiniBatchKMeans | The same, faster on large data | k, batch size | Clusters are very small |
| DBSCAN | Dense regions and noise | eps, min_samples | Density varies across the data |
| AgglomerativeClustering | A merge hierarchy | Linkage, distance, cut | Data is large; distance metric is wrong |
| HDBSCAN (third party) | Clusters of varying density | Almost nothing | High dimensionality |
⚠️
A high silhouette score proves the geometry is tidy, not that the segments are useful. Validate clusters against something you did not cluster on, and check that the sizes are large enough to act on before anyone builds a strategy around them.
PCA and t-SNE
from sklearn.decomposition import PCA
from sklearn.manifold import TSNE
pca = PCA(n_components=0.95, whiten=False, random_state=42).fit(X_train_scaled)
print(pca.n_components_, round(pca.explained_variance_ratio_.sum(), 3))
X_train_pca = pca.transform(X_train_scaled)
X_test_pca = pca.transform(X_test_scaled) # reuse the fitted components
import matplotlib.pyplot as plt
plt.scatter(X_train_pca[:, 0], X_train_pca[:, 1], c=y_train, s=6, alpha=0.6)
tsne = TSNE(n_components=2, perplexity=30, init="pca",
learning_rate="auto", random_state=42)
X_2d = tsne.fit_transform(PCA(n_components=50, random_state=42)
.fit_transform(X_train_scaled))- PCA is a linear projection that maximises retained variance; it can be fitted inside a pipeline and applied to new rows.
- t-SNE preserves local neighbourhoods only. Distances between distant blobs, and empty space, mean nothing.
- Never feed t-SNE coordinates to a model as features — it has no
transformmethod, so new rows cannot be placed consistently. - If the first two PCA components carry a tiny share of the variance, a flat scatter plot is expected and not a bug.
FAQ
How do I choose k for KMeans?
Fit a range of k values and plot inertia (which always falls) next to the silhouette score (which peaks). Then pick the smallest k whose segments you can describe and act on, not the mathematical optimum.
Is a cluster label a class label?
No. Clusters are arbitrary partitions with no inherent meaning, and their numeric ids change between runs and do not align across train and test. Interpret them by profiling the centroids against business metrics.
Related
Feature engineering and text features Missing data and outliers
Last refreshed 2026-09-18.