๐ฏ K-Means Foundations & Intuition
๐ฏ The Unsupervised Workhorse
K-Means (MacQueen, 1967) is a centroid-based clustering algorithm that partitions n observations into k clusters, where each observation belongs to the cluster with the nearest mean. It minimizes within-cluster variance (inertia). It is simple, scalable, and widely used for exploratory data analysis.
Deep Explanation
Objective: Minimize ฮฃแตข ฮฃโฑผ ||xแตข - ฮผโฑผ||ยฒ where ฮผโฑผ is the centroid of cluster j. The algorithm alternates between assignment (E-step) and update (M-step). It converges to a local minimum (not global). The result depends on initialization.
Why it matters: K-Means is the foundation of many clustering applications: customer segmentation, image compression, anomaly detection, and more. Its simplicity and speed make it a first-choice algorithm.
from sklearn.cluster import KMeans
import numpy as np
# Generate synthetic data
X = np.random.rand(300, 2) * 10
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
kmeans.fit(X)
print(f"Cluster centers:\n{kmeans.cluster_centers_}")
print(f"Inertia: {kmeans.inertia_:.2f}")
print(f"Labels: {kmeans.labels_[:10]}")โ๏ธ Lloyd's Algorithm
๐ฏ The Standard Implementation
Iterative two-step process: 1) Assign each point to nearest centroid. 2) Recompute centroids as mean of assigned points. Repeat until convergence (centroids stabilize).
Convergence & Complexity
Time complexity is O(t * k * n * d) where t is iterations, k clusters, n samples, d features. Typically t is small (10-30). The algorithm is guaranteed to converge, but may require multiple restarts (n_init) to find a good solution.
# Manual implementation
def kmeans_manual(X, k, max_iters=100):
centroids = X[np.random.choice(len(X), k, replace=False)]
for _ in range(max_iters):
labels = np.argmin(np.linalg.norm(X[:, None] - centroids, axis=2), axis=1)
new_centroids = np.array([X[labels == i].mean(axis=0) for i in range(k)])
if np.allclose(centroids, new_centroids):
break
centroids = new_centroids
return centroids, labels๐ Distance Metrics
๐ฏ Beyond Euclidean
Euclidean (L2) is default, but Manhattan (L1), Cosine, and Mahalanobis distances can be used depending on the data nature and scale.
Choosing the Right Metric
Euclidean works well for spherical clusters. Manhattan is robust to outliers. Cosine is used for high-dimensional sparse data (text). The metric should align with the domain and data distribution.
from sklearn.metrics.pairwise import euclidean_distances, manhattan_distances, cosine_distances
X = np.array([[1,2], [3,4], [5,6]])
print("Euclidean:\n", euclidean_distances(X))
print("Manhattan:\n", manhattan_distances(X))
print("Cosine:\n", cosine_distances(X))๐ข Choosing K (Elbow, Silhouette)
๐ฏ Determining the Number of Clusters
Elbow method: plot inertia vs. k and look for an "elbow". Silhouette score measures cohesion and separation (higher is better). Gap statistic compares to random data.
Practical Heuristics
Domain knowledge often guides K. The silhouette score ranges from -1 to 1; 0.5+ indicates good clustering. The elbow is subjective but widely used. For large datasets, use the Gap statistic.
from sklearn.metrics import silhouette_score
silhouette_scores = []
for k in range(2, 10):
km = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = km.fit_predict(X)
silhouette_scores.append(silhouette_score(X, labels))
print(silhouette_scores)๐ฒ Initialization (K-Means++)
๐ฏ Smart Seeds
K-Means++ spreads initial centroids: choose first randomly, then subsequent centroids with probability proportional to squared distance from nearest existing centroid.
Impact on Convergence
K-Means++ significantly improves the quality of solutions and reduces the number of restarts needed. It is the default in scikit-learn (n_init=10 with k-means++).
# K-Means++ is default in sklearn
km = KMeans(n_clusters=3, init='k-means++', n_init=10, random_state=42)
km.fit(X)๐ Feature Scaling & Preprocessing
๐ฏ Why Scaling Matters
K-Means is sensitive to feature scales. Variables with larger ranges dominate distance calculations. Standardization (z-score) or Min-Max scaling is essential.
Transformations
StandardScaler (mean=0, std=1) or RobustScaler for outliers. For skewed distributions, apply log or Box-Cox transformations before scaling.
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
km = KMeans(n_clusters=3, random_state=42).fit(X_scaled)๐ฒ Hard vs Soft Clustering
๐ฏ Hard: One Cluster per Point
K-Means is a hard clustering algorithm: each point belongs to exactly one cluster. Soft clustering (e.g., Gaussian Mixture Models) assigns probabilities to each cluster.
When Soft is Better
Soft clustering captures uncertainty and overlapping clusters. It is useful in probabilistic applications, but is computationally heavier.
from sklearn.mixture import GaussianMixture
gmm = GaussianMixture(n_components=3, random_state=42)
gmm.fit(X)
probs = gmm.predict_proba(X)
print(probs[:5]) # Soft probabilities๐ Kernel K-Means
๐ฏ Non-Linear Separability
Kernel K-Means applies the kernel trick to map data to a higher-dimensional space where clusters become linearly separable. It uses the kernel matrix instead of explicit coordinates.
RBF Kernel
Commonly used with RBF (Gaussian) kernel. It can capture complex cluster shapes. However, it is computationally expensive (O(nยฒ)) and not suitable for large datasets.
# Kernel K-Means can be implemented using the kernel matrix
# Example with RBF kernel
from sklearn.metrics.pairwise import rbf_kernel
K = rbf_kernel(X, gamma=0.5)
# Then perform K-Means on the kernel matrix (approximation via spectral)๐ Variants (Mini-Batch, Bisecting)
๐ฏ Scalable Alternatives
Mini-Batch K-Means uses random subsamples for faster convergence on large datasets. Bisecting K-Means is a hierarchical approach that splits clusters recursively.
Performance vs. Quality
Mini-Batch is 10x faster with a slight loss in quality. Bisecting K-Means produces more consistent results and is less sensitive to initialization.
from sklearn.cluster import MiniBatchKMeans, BisectingKMeans
mbk = MiniBatchKMeans(n_clusters=3, batch_size=100, random_state=42)
mbk.fit(X)
bisect = BisectingKMeans(n_clusters=3, random_state=42)
bisect.fit(X)๐ Evaluation Metrics
๐ฏ Internal & External Validation
Internal: Inertia, Silhouette, Davies-Bouldin, Calinski-Harabasz. External: Adjusted Rand Index (ARI), Mutual Information (NMI) when true labels are known.
Choosing the Right Metric
Silhouette is a good general-purpose metric. Davies-Bouldin is simpler (lower is better). ARI is preferred for benchmarking against ground truth.
from sklearn.metrics import davies_bouldin_score, calinski_harabasz_score
db = davies_bouldin_score(X, labels)
ch = calinski_harabasz_score(X, labels)
print(f"Davies-Bouldin: {db:.2f}, Calinski-Harabasz: {ch:.2f}")๐ Anomaly Detection
๐ฏ Distance to Centroid
Anomalies are points far from their assigned centroid. The distance to the nearest centroid can serve as an anomaly score.
Practical Use
Set a threshold on the distance to centroid (e.g., 95th percentile). This is a simple and interpretable baseline for outlier detection.
distances = np.linalg.norm(X - kmeans.cluster_centers_[kmeans.labels_], axis=1)
threshold = np.percentile(distances, 95)
anomalies = X[distances > threshold]
print(f"Anomalies: {len(anomalies)}")๐ผ๏ธ Image Segmentation
๐ฏ Color Quantization
K-Means can reduce the number of colors in an image by clustering pixel colors (RGB values) and replacing each pixel with the nearest centroid.
Compression & Stylization
This creates a "posterized" effect and compresses the image. It is a classic application of K-Means in computer vision.
# Assuming image is a (h, w, 3) array
# Reshape to (h*w, 3) and cluster
pixels = image.reshape(-1, 3)
kmeans = KMeans(n_clusters=16, random_state=42).fit(pixels)
quantized = kmeans.cluster_centers_[kmeans.labels_].reshape(image.shape)๐ High-Dimensional Challenges
๐ฏ Curse of Dimensionality
In high dimensions, distances become less discriminative (all points are far apart). K-Means struggles with "empty clusters" and instability.
Mitigation Strategies
Dimensionality reduction (PCA, t-SNE) before clustering. Use subspace clustering or feature selection. Alternative metrics like cosine distance can help.
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)
km = KMeans(n_clusters=3, random_state=42).fit(X_pca)๐ Deployment & Production
๐ฏ From Model to Service
Save the trained K-Means model (pickle, joblib), create an API that assigns new points to clusters, and monitor cluster drift over time.
MLOps & Monitoring
Track centroids drift, reassignments per cluster, and the proportion of outliers. Retrain periodically or when drift exceeds a threshold.
import joblib
joblib.dump(kmeans, 'kmeans_model.pkl')
# In API:
model = joblib.load('kmeans_model.pkl')
cluster = model.predict(new_data)[0]