Project — Customer Segmentation with K-Means
customers.csv dataset (age, annual income, spending score), scale its features, use the elbow method (Lesson 17) to choose k, fit a final KMeans model, visualize the resulting segments on a scatter plot colored by cluster, and — the step a comparison table alone never does — translate each numeric cluster into a plain-English business segment with a suggested action. Every number in this lesson is illustrative, generated for a toy dataset, not a real company's data.
The Project Brief
Lesson 17 introduced K-Means in the abstract, using generic X and no real-world framing. This project runs the FULL workflow end to end on a concrete scenario: a retail team wants to stop treating every customer the same way, and wants a small number of natural customer segments to design different marketing approaches around.
customers.csv dataset (Age, AnnualIncome in thousands, SpendingScore from 1–100), scale the features, use the elbow method to choose a number of clusters, fit a final KMeans model, visualize the resulting segments on a scatter plot, and write a short business interpretation of each segment with a suggested marketing action.StandardScaler — K-Means is distance-based, exactly like Lesson 17 stressed.The Dataset
Three numeric features per customer, deliberately simple so the resulting segments stay easy to interpret and explain to a non-technical stakeholder — exactly the kind of dataset a first segmentation project realistically starts with.
import pandas as pd df = pd.read_csv("customers.csv") print(df.head()) print(df.shape) # (200, 4)
| CustomerID | Age | AnnualIncome | SpendingScore |
|---|---|---|---|
| 1 | 28 | 42 | 78 |
| 2 | 52 | 95 | 22 |
| 3 | 35 | 88 | 85 |
| 4 | 46 | 31 | 18 |
| 5 | 61 | 67 | 45 |
AnnualIncome is in thousands of dollars, and SpendingScore is a pre-computed 1–100 score representing how much a customer spends relative to their income — a common way segmentation datasets summarize spending behavior into one comparable number.
Scaling the Features
Exactly as Lesson 17's Section 6 warned, K-Means measures raw Euclidean distance — and AnnualIncome (ranging into the 100s) would completely dominate SpendingScore (ranging 1–100) and Age (ranging maybe 18–70) if left unscaled. Every feature needs to contribute on a comparable footing.
from sklearn.preprocessing import StandardScaler features = ["Age", "AnnualIncome", "SpendingScore"] X = df[features] scaler = StandardScaler() X_scaled = scaler.fit_transform(X) print(f"Scaled means (should be ~0): {X_scaled.mean(axis=0).round(3)}") print(f"Scaled stds (should be ~1): {X_scaled.std(axis=0).round(3)}")
scaler once and reuses the SAME fitted object later to convert cluster centroids back to original units with scaler.inverse_transform(). Refitting a new scaler at that point would silently produce wrong numbers — the fitted scaler from this step needs to stay in scope for the rest of the project.The Elbow Method — Choosing k
Same loop as Lesson 17, Section 2 — applied here to the actual project data instead of a generic example.
import matplotlib.pyplot as plt from sklearn.cluster import KMeans inertias = [] k_range = range(1, 11) for k in k_range: km = KMeans(n_clusters=k, init="k-means++", n_init=10, random_state=42) km.fit(X_scaled) inertias.append(km.inertia_) plt.plot(k_range, inertias, marker="o") plt.xlabel("Number of clusters (k)") plt.ylabel("Inertia (WCSS)") plt.title("Elbow Method — Customer Segmentation") plt.show()
Just like Lesson 17's example, the drop from k=4 to k=5 (-49%) is much larger than the drop from k=5 to k=6 (-16%) — so k=5 is this project's illustrative elbow. Five segments is also a very workable number for a marketing team to design five distinct campaigns around, which matters in practice alongside the pure math.
Fitting the Final KMeans Model
With k=5 chosen, fit one final model and attach the resulting cluster label back onto the original (unscaled) DataFrame — keeping the labels next to the human-readable values makes every later step easier.
kmeans = KMeans(n_clusters=5, init="k-means++", n_init=10, random_state=42) df["Cluster"] = kmeans.fit_predict(X_scaled) print(df.groupby("Cluster")[features].mean().round(1)) print(df["Cluster"].value_counts().sort_index())
| Cluster | Avg Age | Avg Income (k$) | Avg Spending Score | Customers |
|---|---|---|---|---|
| 0 | 44.2 | 88.5 | 17.3 | 38 |
| 1 | 25.6 | 26.1 | 76.8 | 34 |
| 2 | 32.9 | 86.2 | 82.4 | 41 |
| 3 | 46.1 | 28.3 | 19.7 | 39 |
| 4 | 43.0 | 55.4 | 49.8 | 48 |
Visualizing the Segments
Income vs. spending score is the most informative pair to plot — it's the exact pair a real marketing team would look at first. Centroids get converted back to original units with scaler.inverse_transform() so they land in the same $/score scale as the raw points.
import matplotlib.pyplot as plt plt.figure(figsize=(8, 6)) scatter = plt.scatter( df["AnnualIncome"], df["SpendingScore"], c=df["Cluster"], cmap="viridis", alpha=0.7 ) # Convert centroids from scaled space back to original units centers = scaler.inverse_transform(kmeans.cluster_centers_) plt.scatter(centers[:, 1], centers[:, 2], c="red", marker="X", s=200, label="Centroids") plt.xlabel("Annual Income (k$)") plt.ylabel("Spending Score (1-100)") plt.title("Customer Segments") plt.legend() plt.colorbar(scatter, label="Cluster") plt.show()
features = ["Age", "AnnualIncome", "SpendingScore"] from Section 3 fixed the column ORDER that both X_scaled and kmeans.cluster_centers_ follow — column 0 is Age, column 1 is AnnualIncome, column 2 is SpendingScore. scaler.inverse_transform() preserves that same order, so centers[:, 1] and centers[:, 2] line up exactly with the income and spending score being plotted on the x and y axes.Interpreting the Segments in Business Terms
Five cluster IDs mean nothing to a marketing team on their own. The last real step of this project — the one a raw comparison table never does — is translating each numeric cluster average from Section 5 into a plain-English label and a concrete suggested action. Every label and action below is illustrative reasoning about a toy dataset, not a documented real-world result.
| Cluster | Profile | Suggested label | Possible action |
|---|---|---|---|
| 0 | High income, low spending | Careful Spenders | Upsell target — investigate why spend is low despite ability to pay; try a premium offer or loyalty incentive. |
| 1 | Low income, high spending | Budget Enthusiasts | Value-focused promotions, discounts, and lower-price-point product lines. |
| 2 | High income, high spending | Premium Loyalists | Highest-value segment — prioritize retention, loyalty programs, and premium/early-access offers. |
| 3 | Low income, low spending | Low Engagement | Lowest priority for marketing spend; light-touch, low-cost outreach only. |
| 4 | Mid income, mid spending | Everyday Shoppers | The largest, most "average" segment — general campaigns, broad seasonal promotions. |
The Complete Script, Start to Finish
Every step from this project, combined into one runnable segmentation pipeline.
import pandas as pd import matplotlib.pyplot as plt from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans # 1. Load df = pd.read_csv("customers.csv") features = ["Age", "AnnualIncome", "SpendingScore"] X = df[features] # 2. Scale scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # 3. Elbow method inertias = [] for k in range(1, 11): km = KMeans(n_clusters=k, init="k-means++", n_init=10, random_state=42) km.fit(X_scaled) inertias.append(km.inertia_) plt.plot(range(1, 11), inertias, marker="o") plt.xlabel("k"); plt.ylabel("Inertia"); plt.show() # 4. Fit final model (k=5 chosen from the elbow plot) kmeans = KMeans(n_clusters=5, init="k-means++", n_init=10, random_state=42) df["Cluster"] = kmeans.fit_predict(X_scaled) # 5. Visualize scatter = plt.scatter(df["AnnualIncome"], df["SpendingScore"], c=df["Cluster"], cmap="viridis", alpha=0.7) centers = scaler.inverse_transform(kmeans.cluster_centers_) plt.scatter(centers[:, 1], centers[:, 2], c="red", marker="X", s=200) plt.xlabel("Annual Income (k$)"); plt.ylabel("Spending Score"); plt.show() # 6. Business interpretation summary = df.groupby("Cluster")[features].mean().round(1) print(summary)
Use the df, X_scaled, scaler, and kmeans objects from Sections 2–5 as your starting point for each task below.
Refit
KMeans with k=4 and again with k=6 on the same scaled data. Look at df.groupby("Cluster")[features].mean() for each. Do the segment stories still make sense with fewer or more clusters, or does k=5 genuinely tell a cleaner business story?
Using Lesson 17's
DBSCAN, fit it on the same X_scaled data with a couple of eps values. How many clusters does it find, and how many customers get labeled as noise (-1)? Are the customers DBSCAN flags as noise ones that also looked like edge cases in the K-Means scatter plot?
Using Lesson 18's
PCA(n_components=2), reduce X_scaled to 2 components BEFORE running K-Means, instead of clustering on the original 3 scaled features. Compare the resulting cluster assignments to the original result with pd.crosstab. Does PCA-then-cluster produce a similar segmentation, or a meaningfully different one? Write 2–3 sentences on what you found.
💡 Show hints if you're stuck
- Task 1: With only 3 features, k=4 or k=6 will likely just split or merge one of the k=5 segments rather than reveal something totally new — that itself is a useful observation to write down.
- Task 2: DBSCAN has no direct concept of "5 segments" — expect a different, possibly smaller, number of dense clusters plus some noise points.
- Task 3: With only 3 original features, PCA(n_components=2) keeps most but not all of the variance — check
pca.explained_variance_ratio_.sum()to see exactly how much was kept before judging whether the clustering changed meaningfully.