Customer segmentation
What can a shop learn by grouping similar measurements?
A shop wants to understand broad purchasing patterns before planning an optional customer survey. It has measurements, but no verified customer categories. Could it organize similar records into groups that are easier to describe and investigate?
Customer segmentation means dividing customer records into groups for a defined purpose. In this project, clustering forms those groups from similarities in selected numerical inputs. Unlike a classifier, it is not trained against known correct category labels. A fitted group is an observation about the chosen measurements, not a discovery of a person’s motives or needs.
We retain six fictional income/spending records, deliberately separated into two groups. They are not the Mall Customers dataset. Review K-means and centers and standardization if needed. The aim is to understand and question a small project before attempting a real analysis.
Six measurements, two fitted group centers
Circles: A–C · lower measured income and score
Diamonds: D–F · higher measured income and score
Crosses: group means, not extra customers
What you’ll learn
- Define the row, measurement period and units before grouping customer records.
- Fit a scaled K-means pipeline and describe its groups in original units rather than by arbitrary IDs.
- Assign new records through the existing scaler and centers, and identify a distant or borderline input.
- Compare geometric scores and repeated fits while explaining what they cannot prove about real customers.
Define what one row and each measurement mean
Each row represents one fictional customer. Annual income is an invented measurement in thousands of dollars for one year: 20 represents $20,000. The spending score is a fictional 0–100 index for that same period; it is not dollars, purchase count or a standard industry measure. We use the score to practice the method, not to claim a real scoring formula.
| Customer | Income ($1,000) | Spending score |
|---|---|---|
| A | 20 | 15 |
| B | 22 | 18 |
| C | 24 | 20 |
| D | 80 | 75 |
| E | 82 | 78 |
| F | 84 | 80 |
The letters identify rows; they are not inputs to the distance calculation. In real data, define the score’s construction, period, source and missing-value rules. Keep individuals and households distinct, and do not treat a missing measurement as zero. Collect only fields needed for the question; a shop may find observed purchasing measures more appropriate than income.
Make the meaning of “similar” explicit
K-means uses distances between input vectors. Here a vector is the pair [annual_income, spending_score]. Larger numerical scales can dominate raw Euclidean distance, so our pipeline standardizes each column using its fitted mean and standard deviation. This gives one training standard deviation a comparable scale in either column.
Standardization does not establish that income and spending deserve equal importance. Feature choice and weighting still define what similar means. Converting income from thousands of dollars to dollars should not silently change the intended relationship; fitting StandardScaler compensates for that positive unit conversion, but does not resolve an unsuitable measurement.
The means here are 52 and about 47.667. An income of 22 lies below its mean and therefore has a negative standardized coordinate. That coordinate is a position relative to the sample, not a negative income. Fit preparation once for this exploratory dataset, then reuse it when assigning new records.
Fit two centers, without inventing ground-truth categories
We specify n_clusters=2 for the teaching example. K-means alternates between assigning each scaled record to a nearest center and updating centers from their assigned records. It seeks a small total squared distance within the groups. The algorithm does not independently prove that two customer types exist.
n_init=10 runs ten initializations and retains the result with the lowest inertia, its total within-group squared distance. random_state=7 makes the initialization process repeatable for these inputs. These settings reduce one source of variation; they do not guarantee a globally optimal or meaningful segmentation.
A pipeline connects StandardScaler to KMeans so fitting and later prediction use the same sequence. See the KMeans parameters and fitted attributes for the library’s behavior.
Fit the original pipeline and compare the group means
Prepare and run
Use Python with NumPy, pandas and scikit-learn in the course environment. All six fictional records are included. No download or separate file is needed, and each program runs independently.
Python · complete runnable example
import numpy as np
import pandas as pd
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
# Fictional aggregate customers, not the Mall Customers dataset.
data = pd.DataFrame({"annual_income": [20, 22, 24, 80, 82, 84], "spending_score": [15, 18, 20, 75, 78, 80]})
model = make_pipeline(StandardScaler(), KMeans(n_clusters=2, n_init=10, random_state=7))
labels = model.fit_predict(data)
report = data.assign(cluster=labels).groupby("cluster").mean().sort_values("annual_income")
print("Low-income group mean:", round(report.iloc[0]["annual_income"], 1))
print("High-income group mean:", round(report.iloc[1]["annual_income"], 1))
print("Same-group first customers:", bool(labels[0] == labels[1]))Expected output
Low-income group mean: 22.0
High-income group mean: 82.0
Same-group first customers: Truefit_predict(data) fits both steps and returns one cluster ID per row in the original order. data.assign(cluster=labels) adds those IDs to a copy of the original table. Grouping that table and taking means reports measurements in original units, rather than standardized coordinates.
The first three records are grouped together, with income mean 22; the last three have mean 82. Sorting by income makes the report’s ordering meaningful. Cluster 0 is not a permanent “low-income” category: another fit can exchange the numeric IDs while keeping exactly the same memberships.
These are descriptive summaries of this constructed sample. We have not predicted an outcome, demonstrated causation or measured classification accuracy.
Inspect counts, ranges and centers in readable units
Prepare and run
Use Python with NumPy, pandas and scikit-learn in the course environment. All six fictional records are included. No download or separate file is needed, and each program runs independently.
Python · complete runnable example
import numpy as np
import pandas as pd
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
# Fictional aggregate customers, not the Mall Customers dataset.
data = pd.DataFrame({"annual_income": [20, 22, 24, 80, 82, 84], "spending_score": [15, 18, 20, 75, 78, 80]})
model = make_pipeline(StandardScaler(), KMeans(n_clusters=2, n_init=10, random_state=7))
labels = model.fit_predict(data)
scaler = model.steps[0][1]
kmeans = model.steps[1][1]
centers = scaler.inverse_transform(kmeans.cluster_centers_)
print("Fitted column means:", np.round(scaler.mean_, 3).tolist())
print("Fitted column standard deviations:", np.round(scaler.scale_, 3).tolist())
for group in np.argsort(centers[:, 0]):
rows = data.loc[labels == group]
print("Group size:", len(rows))
print("Income mean:", round(float(rows.annual_income.mean()), 3))
print("Score mean:", round(float(rows.spending_score.mean()), 3))
print("Income range:", int(rows.annual_income.min()), "to", int(rows.annual_income.max()))
print("Center in original units:", np.round(centers[group], 3).tolist())Expected output
Fitted column means: [52.0, 47.667]
Fitted column standard deviations: [30.044, 30.07]
Group size: 3
Income mean: 22.0
Score mean: 17.667
Income range: 20 to 24
Center in original units: [22.0, 17.667]
Group size: 3
Income mean: 82.0
Score mean: 77.667
Income range: 80 to 84
Center in original units: [82.0, 77.667]The scaler learns the displayed column means and spreads from these six records. The K-means centers live in that standardized space. inverse_transform returns them to the original units so they can be compared with the group summaries.
The lower measured group contains three customers, income range 20–24 and score mean about 17.667. The higher measured group also contains three, income range 80–84 and score mean about 77.667. A center is a numerical summary, not an additional customer. In this converged fit, the inverse-transformed centers agree with the original-unit group means.
A mean alone hides variation. In larger datasets, also report counts, ranges or quantiles and the amount of overlap. A tiny group may be too small to support a useful conclusion even when its center looks distinct.
Describe the measurements, not a supposed kind of person
“Lower measured income and spending score in this sample” is a defensible description. “Uninterested customers” is not: no input measures interest. “Less valuable people” is a judgment that the data does not support. A segmentation should use names tied to observed features and the stated purpose.
The six values were chosen to show a clean pattern, so they cannot establish that lower-income customers generally spend less. Real records can show high spending at low income, low spending at high income, overlapping groups and changes over time. Individual behavior does not follow automatically from a group average.
For the shop’s survey, a possible next step is to check whether different measured purchasing patterns suggest different survey questions. Even that use needs representative records and feedback. Clustering alone cannot establish which message, product or offer will work better.
Assign new inputs with the fitted pipeline
After fitting, model.predict(new_rows) standardizes each new row using the existing means and spreads, then assigns it to a nearest fitted center. It does not learn new centers. Keep the same column names, order and meanings; a new score definition is not a harmless replacement for the old score.
The next program repeats the complete original training code and inspects three inputs: [23, 19], near the lower measured group; [52, 48], near the middle; and [200, 200], far beyond the teaching measurements. The last score also exceeds our defined 0–100 range, so a real input check should reject it before prediction. It is included deliberately to reveal the estimator’s limitation.
There is no distance-based rejection built into this fitted K-means assignment: even an implausible input receives a group ID. A nearest group is not necessarily a close match. Distances are in standardized coordinates, not dollars, score points or probabilities.
Inspect distances for three new inputs
Prepare and run
Use Python with NumPy, pandas and scikit-learn in the course environment. All six fictional records are included. No download or separate file is needed, and each program runs independently.
Python · complete runnable example
import numpy as np
import pandas as pd
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
# Fictional aggregate customers, not the Mall Customers dataset.
data = pd.DataFrame({"annual_income": [20, 22, 24, 80, 82, 84], "spending_score": [15, 18, 20, 75, 78, 80]})
model = make_pipeline(StandardScaler(), KMeans(n_clusters=2, n_init=10, random_state=7))
labels = model.fit_predict(data)
scaler = model.steps[0][1]
kmeans = model.steps[1][1]
centers = scaler.inverse_transform(kmeans.cluster_centers_)
# Descriptions are based on measurements, not arbitrary numeric IDs.
names = {int(group): name for group, name in zip(
np.argsort(centers[:, 0]), ["lower measured values", "higher measured values"])}
new_rows = pd.DataFrame({"annual_income": [23, 52, 200],
"spending_score": [19, 48, 200]})
scaled = scaler.transform(new_rows)
for row, point, group in zip(new_rows.to_numpy(), scaled, model.predict(new_rows)):
distances = np.linalg.norm(kmeans.cluster_centers_ - point, axis=1)
ordered = distances[np.argsort(centers[:, 0])]
print("Input:", row.tolist())
print("Distances to lower/higher centers:", np.round(ordered, 3).tolist())
print("Assigned:", names[int(group)])Expected output
Input: [23, 19]
Distances to lower/higher centers: [0.055, 2.768]
Assigned: lower measured values
Input: [52, 48]
Distances to lower/higher centers: [1.419, 1.404]
Assigned: higher measured values
Input: [200, 200]
Distances to lower/higher centers: [8.477, 5.655]
Assigned: higher measured valuesnp.linalg.norm measures each scaled input’s Euclidean distance to each fitted center. Sorting centers by original income presents the lower and higher descriptions consistently, even if cluster IDs exchange. predict selects a nearest center using the same scaled space.
The first input is close to the lower center. The middle input has similar distances to both, so its assignment is a fragile description rather than a strong category. The out-of-range input still receives the higher group despite being far away. Validate units and allowed ranges first, and review unusual distances under an explicitly designed policy.
Never fit a fresh scaler on these three query rows. That would change their coordinate system while leaving the old centers in the training system. If you refit the entire segmentation for a later period, treat it as a new model and compare its groups before reusing descriptions.
Ask what a geometric score is actually measuring
Inertia sums squared distances from records to their assigned centers. With the same records and scaling, increasing the number of centers can reduce the best achievable inertia simply by dividing groups more finely. Lower inertia alone is not a reason to keep adding customer segments.
Silhouette compares how close a record is to its own group with how close it is to its nearest other group. Values near 1 indicate strong separation under the chosen distance; near 0 suggests a boundary, while negative values can suggest a closer alternative group. The mean summarizes those per-record values. See the silhouette definition and valid label counts.
Neither score uses known correct customer categories. A high score on six deliberately separated records confirms simple geometry, not representative sampling, successful marketing or a universally best number of groups. Interpret scores alongside visible memberships and the intended use.
Compare group counts and repeat the initialization
Prepare and run
Use Python with NumPy, pandas and scikit-learn in the course environment. All six fictional records are included. No download or separate file is needed, and each program runs independently.
Python · complete runnable example
import numpy as np
import pandas as pd
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
# Fictional aggregate customers, not the Mall Customers dataset.
data = pd.DataFrame({"annual_income": [20, 22, 24, 80, 82, 84], "spending_score": [15, 18, 20, 75, 78, 80]})
model = make_pipeline(StandardScaler(), KMeans(n_clusters=2, n_init=10, random_state=7))
labels = model.fit_predict(data)
from sklearn.metrics import silhouette_score, adjusted_rand_score
# Compare geometry in the same fitted standardized space.
scaled = model.steps[0][1].transform(data)
for k in [2, 3, 4]:
trial = KMeans(n_clusters=k, n_init=10, random_state=7).fit(scaled)
print("k:", k, "inertia:", round(float(trial.inertia_), 3),
"silhouette:", round(float(silhouette_score(scaled, trial.labels_)), 3))
print("Same-row agreement across initializations:")
for seed in [0, 1, 2]:
other = KMeans(n_clusters=2, n_init=10, random_state=seed).fit_predict(scaled)
print("Seed:", seed, "adjusted Rand index:", round(float(adjusted_rand_score(labels, other)), 3))Expected output
k: 2 inertia: 0.046 silhouette: 0.95
k: 3 inertia: 0.027 silhouette: 0.603
k: 4 inertia: 0.009 silhouette: 0.258
Same-row agreement across initializations:
Seed: 0 adjusted Rand index: 1.0
Seed: 1 adjusted Rand index: 1.0
Seed: 2 adjusted Rand index: 1.0This program compares two, three and four groups in the same standardized space. The two-group silhouette is high because the constructed points form two separated clouds. More groups reduce inertia but split those small clouds into still smaller groups, including singletons. The score comparison is exploratory; there is no held-out business outcome here.
The second check fits two centers with three other random seeds on exactly the same six scaled rows. Adjusted Rand index compares pairs of records that are together or apart, ignoring the numeric names of groups. A value of 1 means these fitted partitions agree exactly, even if their IDs differ.
Agreement across these seeds tests initialization sensitivity on this fixed table. It does not test new customers, changed measurements or next year’s behavior. For a larger real dataset, examine representative resamples and later periods; compare original-unit profiles and memberships on shared records. Do not compare raw cluster IDs across refits as if they were permanent labels.
Practice: fix a misleading group description
Your task: A teammate names the lower measured group “customers who dislike the shop”, promises that membership will remain unchanged next year, and scales each new survey batch separately before passing it to the old centers. Identify the three mistakes and propose a better approach.
Worked answer
First, neither income nor the fictional spending index measures whether someone likes the shop. Describe the observed measurements and ask appropriate survey questions instead of inventing a motive. Second, this six-record example gives no evidence about next year. Changed behavior or sampling can alter both profiles and membership.
Third, a separately fitted scaler puts the new batch in a different coordinate system. Use the existing fitted pipeline to assign compatible new records. If a later-period refit is justified, fit preparation and clustering together, evaluate the new segmentation and match profiles carefully before carrying old group names forward.
What changes when you move beyond six invented rows?
Start with the decision the analysis is meant to support. Define the customer population, row granularity, observation period and measurements. A purchase-only table may omit people who visited without buying; repeated rows for one customer can give that customer extra influence. Check duplicates, missing values and unusual inputs before deciding how to prepare them.
If using an external CSV, inspect its actual schema and source terms. A “Spending Score” column is not automatically comparable to our fictional index. Map column names and units explicitly, rather than pasting a filename into this program and assuming the data matches.
K-means favors compact groups under its distance measure and is sensitive to extreme inputs. Review alternative feature sets and methods, such as hierarchical clustering, when the question or geometry warrants them. Standardization does not remove outliers or make every group shape suitable.
Validate any practical action separately: explore whether group descriptions are stable and useful, then test the proposed service or communication with an appropriate outcome measure. Broad exploratory clusters are not a sufficient basis for denying services or making sensitive individual decisions.
Check an exchanged cluster ID
Check an out-of-range assignment
Check what repeated fits establish
Report the measurements and the limits together
You have fitted a complete scaled K-means project, described the two centers in original units and inspected new-record assignments. The pipeline groups selected measurements; it does not learn verified human categories or explain why customers behave as they do.
- Inputs: six fictional customers, annual income in thousands of dollars and a defined teaching spending score.
- Profiles: three records per group; income/score means of about 22/17.667 and 82/77.667.
- New records: reuse the same scaler and centers, check input validity, and distinguish nearest from genuinely nearby.
- Evaluation: geometric scores and initialization agreement describe this constructed sample, not future stability or business success.
A useful report records the data definition, preparation, feature choices, group count, settings, profile summaries and validation limits. Before acting on a segmentation, explain why its measurements and evidence answer the intended question.
Reaching the recap and completing the quizzes are separate milestones.