K-Means (Clustering): Complete Guide — Principles, Examples and Python Implementation
Summary
K-Means clustering is the most widely used unsupervised grouping (clustering) algorithm in data science. Simple, effective, and intuitive, it partitions a dataset into K distinct groups called clusters, where each point belongs to the cluster whose centroid (center of gravity) is closest. Since its proposal by Stuart Lloyd in 1957, K-Means has become an indispensable pillar of unsupervised learning, with applications ranging from customer segmentation to image compression, genomic analysis, and anomaly detection.
In this complete guide, you will discover the mathematical foundations of K-Means, its iterative algorithm, the elbow method for choosing the optimal number of clusters, and a step-by-step implementation in Python with scikit-learn.
Mathematical Principle
Minimizing Inertia (WCSS)
K-Means is based on a precise mathematical objective: minimizing intra-cluster inertia, also called WCSS (Within-Cluster Sum of Squares). Inertia measures the sum of squared distances between each point in a cluster and the centroid of that cluster:
where:
– K is the number of clusters set a priori,
– c_k is the centroid of cluster k (the mean of the points belonging to it),
– x is a data point belonging to cluster k,
– ||x − c_k||² is the squared Euclidean distance.
The algorithm therefore seeks the partition that makes clusters as compact as possible: points in the same cluster should be close to each other, while points in different clusters should be far apart.
Alternating Algorithm: Assignment → Centroid Update
K-Means works by iterative alternation between two steps:
- Assignment step: Each data point is assigned to the cluster whose centroid is closest (according to Euclidean distance). Formally, point x_i belongs to cluster k that minimizes the squared distance between x_i and centroid c_j.
- Centroid update step: For each cluster, its centroid is recalculated as the arithmetic mean of all points assigned to it. Mathematically, each centroid c_k equals the sum of the cluster’s points divided by their count.
These two steps are repeated until convergence, i.e., when centroids no longer move significantly (or a maximum number of iterations is reached). At each iteration, the WCSS inertia can only decrease or remain stable, which guarantees the algorithm’s convergence to a local minimum.
K-Means++ Initialization
One of the major weaknesses of classical K-Means is its sensitivity to initialization. If initial centroids are poorly placed (e.g., two centroids very close in the same natural group), the algorithm may converge to a poor local minimum.
K-Means++ initialization solves this problem by placing initial centroids more intelligently:
- The first centroid is chosen randomly from the data points.
- For each subsequent point, we compute the distance D(x) to the nearest already-selected centroid.
- The next centroid is chosen with a probability proportional to D(x)² — in other words, points far from existing centroids are more likely to be selected.
This strategy guarantees initial centroids that are well distributed in space, which almost always leads to better convergence and a better final result. This is also the default initialization in scikit-learn (init=’k-means++’).
Intuition Behind K-Means
Imagine you run a chain of stores and want to group your customers by purchasing behavior similarity. You start with thousands of customers, each described by features like purchase frequency, average basket, preferred product category, etc.
K-Means works exactly like a progressive refinement process:
- We choose K centers at random — say 4 centers representing 4 hypothetical customer types.
- We assign each customer to the nearest center — customer A resembles center 1, customer B center 2, etc.
- We move each center toward the mean of its customers — the center of the “loyal customers” group moves closer to the typical profile of that group.
- We repeat until convergence — at each round, the groups become more precise, the centers stabilize.
Over iterations, clusters become increasingly homogeneous. It’s a fascinating process: from randomly placed centers, we naturally converge toward meaningful groups. Visualize it as magnets attracting the nearest points, then repositioning themselves at the center of gravity of their points — again and again, until everything stabilizes.
However, there is an important trap: K-Means assumes clusters are spherical and of comparable size. If your data forms crescent-shaped groups, rings, or very different densities, K-Means will produce unsatisfactory results. In these situations, algorithms like DBSCAN or hierarchical clustering are more suitable.
Python Implementation with scikit-learn
Installing Dependencies
pip install scikit-learn matplotlib numpy
Creating Sample Data and K-Means Clustering
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
# 1. Generate synthetic data
X, y_true = make_blobs(
n_samples=500,
centers=4,
cluster_std=1.0,
random_state=42
)
# 2. Apply K-Means
kmeans = KMeans(
n_clusters=4,
init='k-means++',
n_init=10,
max_iter=300,
random_state=42
)
kmeans.fit(X)
labels = kmeans.labels_
centroids = kmeans.cluster_centers_
inertia = kmeans.inertia_
print(f"Inertia: {inertia:.2f}")
print("Centroids:")
print(centroids)
# 3. Visualize clusters
plt.figure(figsize=(10, 5))
# Plot 1: True data
plt.subplot(1, 2, 1)
plt.scatter(X[:, 0], X[:, 1], c=y_true, cmap='viridis', s=30, edgecolors='k', alpha=0.7)
plt.title('True clusters')
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
# Plot 2: K-Means clusters
plt.subplot(1, 2, 2)
plt.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis', s=30, edgecolors='k', alpha=0.7)
plt.scatter(centroids[:, 0], centroids[:, 1], c='red', marker='X', s=200, label='Centroids')
plt.title('K-Means (K={})'.format(len(centroids)))
plt.xlabel('Feature 1')
plt.ylabel('Feature 2')
plt.legend()
plt.tight_layout()
plt.savefig('kmeans_result.png', dpi=150, bbox_inches='tight')
plt.show()
Elbow Method for Choosing K
How to determine the right number of clusters? The elbow method consists of plotting inertia as a function of K and identifying the “elbow” — the point where the decrease in inertia begins to slow.
# Elbow method for finding optimal K
inertias = []
K_range = range(1, 11)
for k in K_range:
km = KMeans(
n_clusters=k,
init='k-means++',
n_init=10,
max_iter=300,
random_state=42
)
km.fit(X)
inertias.append(km.inertia_)
# Visualize the elbow method
plt.figure(figsize=(8, 5))
plt.plot(K_range, inertias, 'bo-', linewidth=2, markersize=8)
plt.axvline(x=4, color='red', linestyle='--', label='Optimal K')
plt.title('Elbow Method — Inertia as a Function of K')
plt.xlabel('Number of clusters (K)')
plt.ylabel('Inertia (WCSS)')
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig('elbow_method.png', dpi=150, bbox_inches='tight')
plt.show()
The elbow is typically at K=4 in our example: beyond that, each added cluster only marginally reduces inertia. This is the sign that the natural structure of the data contains 4 groups.
Silhouette Score: Validating Clustering Quality
The silhouette score measures clustering quality on a scale from -1 to 1:
– Close to 1: the point is well assigned to its cluster, far from other clusters.
– Close to 0: the point is on the boundary between two clusters.
– Close to -1: the point is probably poorly assigned.
# Compute silhouette score for different K values
silhouette_scores = []
for k in range(2, 11):
km = KMeans(
n_clusters=k,
init='k-means++',
n_init=10,
max_iter=300,
random_state=42
)
labels_k = km.fit_predict(X)
score = silhouette_score(X, labels_k)
silhouette_scores.append(score)
print(f"K={k}: Silhouette score = {score:.4f}")
# Visualize silhouette scores
plt.figure(figsize=(8, 5))
plt.plot(range(2, 11), silhouette_scores, 'go-', linewidth=2, markersize=8)
k_optimal = np.argmax(silhouette_scores) + 2
plt.axvline(x=k_optimal, color='red', linestyle='--',
label=f'Optimal K ({k_optimal})')
plt.title('Silhouette Score as a Function of K')
plt.xlabel('Number of clusters (K)')
plt.ylabel('Silhouette score')
plt.legend()
plt.grid(True, alpha=0.3)
plt.savefig('silhouette_score.png', dpi=150, bbox_inches='tight')
plt.show()
The silhouette score complements the elbow method: if both point to the same K, we can be reasonably confident in the choice.
Predicting on New Data
One advantage of K-Means is its ability to classify new observations:
# New data to classify
new_data = np.array([[2.0, 3.0], [-5.0, -4.0], [8.0, 1.0]])
# Assign to existing clusters
new_labels = kmeans.predict(new_data)
print("Assignments:", new_labels)
# Distance to centroids
distances = kmeans.transform(new_data)
print("Distances to centroids:")
print(distances)
Essential Hyperparameters
K-Means in scikit-learn offers several hyperparameters that are crucial to understand:
| Hyperparameter | Role | Default Value | Practical Advice |
|---|---|---|---|
| n_clusters | Number of clusters K | 8 | To be determined by elbow method or silhouette score |
| init | Initialization method | ‘k-means++’ | Always use ‘k-means++’ for better results |
| n_init | Number of runs with different initializations | ‘auto’ (10 in v1.2+) | Increase for more robustness (15-30 on complex data) |
| max_iter | Maximum number of iterations per run | 300 | Generally sufficient; increase if no convergence |
| tol | Convergence tolerance (relative inertia change) | 1e-4 | Reduce for stricter convergence |
| algorithm | Distance computation algorithm | ‘lloyd’ (v1.1+) | ‘elkan’ for dense high-dimensional data |
# Recommended configuration for robust results
kmeans = KMeans(
n_clusters=5,
init='k-means++',
n_init=20,
max_iter=500,
tol=1e-5,
algorithm='elkan',
random_state=42
)
Note on Standardization
K-Means uses Euclidean distance, making it sensitive to feature scale. If one variable is measured in thousands (e.g., annual income) and another in units (e.g., number of purchases), the first will unfairly dominate the clustering. Always standardize your data with StandardScaler before applying K-Means:
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
scaler = StandardScaler()
X_standardized = scaler.fit_transform(X_original)
kmeans = KMeans(n_clusters=4, random_state=42)
kmeans.fit(X_standardized)
Advantages of K-Means
- Simplicity and interpretability — The algorithm is easy to understand, implement, and explain to non-specialists.
- Computational efficiency — Complexity O(n × K × d × i), where n is the number of points, K the number of clusters, d the dimension, and i the number of iterations. Very fast in practice thanks to optimizations like the Elkan algorithm.
- Scalability — Works well on large datasets. Variants like Mini-Batch K-Means (MiniBatchKMeans in scikit-learn) can handle millions of points.
- Generalization — Able to classify new data without retraining, unlike some clustering algorithms.
- Guaranteed convergence — Inertia decreases at each iteration, ensuring convergence to a local minimum.
Limitations of K-Means
- Number of clusters must be specified — K must be known a priori. A poor choice of K leads to irrelevant results.
- Sensitivity to outliers — Extreme values pull centroids and degrade clustering quality. Preprocessing (outlier detection and removal) is often necessary.
- Spherical cluster assumption — K-Means assumes clusters are convex and isotropic. It fails on non-spherical structures (crescents, rings, strips).
- Local minimum — Convergence to a local minimum is not guaranteed to be the global minimum, hence the importance of n_init and K-Means++ initialization.
- Sensitivity to scale — Requires data standardization, as Euclidean distance is dominated by high-variance features.
- Forced equal partitioning — K-Means tends to produce clusters of similar size, even when the natural data structure would have very different group sizes.
4 Concrete Use Cases of K-Means
1. Customer Segmentation in Marketing
An e-commerce business groups its customers into 4 segments based on clustering: frequent big spenders, occasional buyers, deal seekers, and dormant customers. Each segment receives a personalized marketing campaign — targeted newsletters, specific offers, tailored loyalty programs — which increases the conversion rate by 35%.
# Simplified customer segmentation example
from sklearn.preprocessing import StandardScaler
customer_data = np.column_stack([
purchase_frequency, # number of purchases per month
average_basket, # average order amount
product_diversity, # number of different categories
seniority # months since first purchase
])
scaler = StandardScaler()
X = scaler.fit_transform(customer_data)
kmeans = KMeans(n_clusters=4, init='k-means++', n_init=15, random_state=42)
segments = kmeans.fit_predict(X)
2. Image Compression via Color Reduction
K-Means can compress an image by reducing the number of unique colors. Each pixel is treated as a point in RGB space (3 dimensions), K-Means is applied with K=64, then each pixel is replaced by its centroid’s color. The result: a 64-color image instead of 16.7 million, with minimal visual loss.
3. Document Analysis and Text Clustering
In Natural Language Processing (NLP), documents are vectorized with TF-IDF or embeddings (Word2Vec, BERT), then K-Means is applied to automatically group articles, customer reviews, or support tickets into coherent themes. This allows organizing large collections of texts without manual labeling.
4. Anomaly Detection in Industrial Systems
In industry, K-Means identifies anomalies by measuring the distance of each observation to its cluster centroid. Points very far from all centroids (beyond a defined threshold) are alarm signals: sensor failure, abnormal machine behavior, or fraud in financial transactions.
# Anomaly detection based on distance to centroid
centroid_distances = kmeans.transform(X_standardized).min(axis=1)
threshold = np.percentile(centroid_distances, 99) # top 1% farthest
anomalies = X_standardized[centroid_distances > threshold]
print(f"Number of anomalies detected: {len(anomalies)}")
Best Practices for K-Means in Production
- Always standardize data with StandardScaler.
- Run K-Means multiple times (n_init >= 10) to avoid local minima.
- Combine elbow method and silhouette score to choose K.
- Check cluster size distribution — a cluster that is too small or empty is a warning signal.
- Visualize centroids — if centroids don’t make business sense, the number of clusters is probably poorly chosen.
- Consider alternatives when data is not spherical: DBSCAN, hierarchical clustering, or Gaussian Mixture Models.
Comparison Table: K-Means vs Alternatives
| Criterion | K-Means | DBSCAN | Hierarchical Clustering | Gaussian Mixture |
|---|---|---|---|---|
| K required | Yes | No (ε, minPts) | No | Yes |
| Cluster shape | Spherical | Arbitrary | Arbitrary | Elliptical |
| Outliers | Sensitive | Robust | Sensitive | Moderately sensitive |
| Complexity | O(n×K×d×i) | O(n²) | O(n²) or O(n³) | O(n×K×d×i) |
| Large n | Excellent | Medium | Low | Good |
| Variable cluster sizes | No | Yes | Yes | Yes |
| Probabilistic | No | No | No | Yes |
Recommended Resources
- Book: Pattern Recognition and Machine Learning by Christopher Bishop (chapter 9 on mixture models).
- Book: Hands-On Machine Learning with Scikit-Learn, Keras & TensorFlow by Aurélien Géron (chapter 9 on unsupervised clustering).
- Foundational paper: Lloyd, S. (1982). Least squares quantization in PCM. IEEE Transactions on Information Theory.
- K-Means++ paper: Arthur, D. and Vassilvitskii, S. (2007). k-means++: The Advantages of Careful Seeding. SODA.
- scikit-learn documentation: sklearn.cluster.KMeans.
- scikit-learn documentation: Clustering — User Guide.
See Also
- Mastering Divisor Sum Calculation with Python: Complete Guide and Efficient Tips
- Creating a Sliding Puzzle Game in Python: Complete Guide and Tips for Developers

