01Loading the dataset
We'll use the Online Retail dataset from the UCI Machine Learning Repository — real transactional data from a UK-based online retailer, covering invoices, products, quantities and customer IDs over one year.
import pandas as pd
import numpy as np
# UCI Online Retail dataset (real transactional data)
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/00352/Online%20Retail.xlsx"
df = pd.read_excel(url)
print(df.shape)
print(df.head())
print(df['CustomerID'].nunique(), "unique customers")
~540,000 transaction rows across ~4,300 unique customers. Unlike our previous projects, there's no target label here — clustering is unsupervised, meaning the model finds structure in the data without being told the "right answer".
02Exploratory data analysis
For customer segmentation, we don't analyse raw transactions — we engineer RFM features (Recency, Frequency, Monetary value), the industry-standard way to describe customer behaviour for clustering.
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import timedelta
# Clean: remove cancelled orders and missing customer IDs
df = df.dropna(subset=['CustomerID'])
df = df[df['Quantity'] > 0]
df['TotalPrice'] = df['Quantity'] * df['UnitPrice']
# Engineer RFM features per customer
snapshot_date = df['InvoiceDate'].max() + timedelta(days=1)
rfm = df.groupby('CustomerID').agg({
'InvoiceDate': lambda x: (snapshot_date - x.max()).days, # Recency
'InvoiceNo': 'nunique', # Frequency
'TotalPrice': 'sum' # Monetary
})
rfm.columns = ['Recency', 'Frequency', 'Monetary']
print(rfm.describe())
# Distribution of each RFM feature
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
for ax, col in zip(axes, rfm.columns):
sns.histplot(rfm[col], ax=ax, color='#1a4a3a')
ax.set_title(f'{col} Distribution')
plt.savefig('rfm_distributions.png')
03Preprocessing & the elbow method
K-Means needs us to choose K (the number of clusters) in advance. The elbow method helps us pick a sensible K by plotting inertia (within-cluster variance) against different values of K and looking for the point where adding more clusters stops helping much.
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
# Scale RFM features — essential since they're on very different ranges
scaler = StandardScaler()
rfm_scaled = scaler.fit_transform(rfm)
# Try K from 2 to 10, record inertia for each
inertias = []
K_range = range(2, 11)
for k in K_range:
km = KMeans(n_clusters=k, random_state=42, n_init=10)
km.fit(rfm_scaled)
inertias.append(km.inertia_)
plt.figure(figsize=(8, 5))
plt.plot(K_range, inertias, marker='o', color='#1a4a3a')
plt.xlabel('Number of Clusters (K)')
plt.ylabel('Inertia')
plt.title('Elbow Method for Optimal K')
plt.savefig('elbow_method.png')
The inertia drops sharply until K=4, then flattens out — this "elbow" suggests K=4 is a good balance between capturing meaningful structure and avoiding over-segmentation.
04Training the model
With K=4 chosen, we fit the final K-Means model and assign each customer to a cluster.
# Train final model with K=4
kmeans = KMeans(n_clusters=4, random_state=42, n_init=10)
rfm['Cluster'] = kmeans.fit_predict(rfm_scaled)
# Examine each cluster's average RFM profile
cluster_profile = rfm.groupby('Cluster').agg({
'Recency': 'mean',
'Frequency': 'mean',
'Monetary': 'mean',
'Cluster': 'count'
}).rename(columns={'Cluster': 'CustomerCount'})
print(cluster_profile)
| Cluster | Recency (days) | Frequency | Monetary (£) | Segment name |
|---|---|---|---|---|
| 0 | 18 | 12.4 | 5,820 | Champions — recent, frequent, big spenders |
| 1 | 245 | 1.8 | 410 | Lost customers — haven't purchased in months |
| 2 | 42 | 5.1 | 1,650 | Loyal customers — steady, moderate spenders |
| 3 | 89 | 2.3 | 720 | At-risk — used to buy, slowing down |
05Evaluation & visualisation
Since there's no ground truth label, we evaluate clustering differently — using the Silhouette Score (how well-separated clusters are) and visualising the clusters with PCA, which compresses our 3 RFM dimensions down to 2D for plotting.
from sklearn.metrics import silhouette_score
from sklearn.decomposition import PCA
silhouette = silhouette_score(rfm_scaled, rfm['Cluster'])
print(f"Silhouette Score: {silhouette:.4f}")
# Reduce to 2D for visualization
pca = PCA(n_components=2)
rfm_pca = pca.fit_transform(rfm_scaled)
plt.figure(figsize=(9, 7))
scatter = plt.scatter(rfm_pca[:, 0], rfm_pca[:, 1],
c=rfm['Cluster'], cmap='Greens', alpha=0.6)
plt.xlabel('PCA Component 1')
plt.ylabel('PCA Component 2')
plt.title('Customer Segments (PCA-Reduced)')
plt.colorbar(scatter, label='Cluster')
plt.savefig('customer_segments_pca.png')
Silhouette Score of 0.58 indicates reasonably well-separated, meaningful clusters (scores range from -1 to 1, with higher being better-separated). The PCA plot shows four visually distinct groupings, confirming the elbow method's choice of K=4 was sound.
06Real-world use case
Where this exact technique is used in industry
This project demonstrates the unsupervised learning workflow: when you don't have labels, you can still extract genuinely useful structure from data — the RFM + K-Means combination here is one of the most widely deployed clustering techniques in retail analytics today.
Want to build this with guided support?
Clustering, feature engineering and customer analytics are all covered hands-on in our Machine Learning training programme.
View Training Programs WhatsApp Us