机器学习项目:客户分群——K-Means 聚类从选参到业务画像的完整实战
商场用户分群:
一、前言
在商场运营中,面对成千上万的顾客,如果用同一套营销策略对待所有人,效果往往事倍功半。高收入的中年人和月光族的年轻人等等群体,消费习惯和偏好截然不同——这就需要用户分群(Customer Segmentation):根据用户的年龄、收入、消费行为等特征,将相似的用户归为一类,然后针对每一类人群制定差异化的营销方案。
本文将基于 Mall Customers 商场客户数据集,使用 K-Means 聚类算法完成一次完整的用户分群实战:
- 聚类前的特征选择:为什么性别不参与聚类,而是留做事后画像
- 数据标准化对 K-Means 欧氏距离的影响
- 肘部法则(Elbow Method)与轮廓系数(Silhouette Score)两种选 k 方法
- 指标最优 != 业务可解释最优:k=5 与 k=6 的权衡思考
- 聚类中心反标准化,还原真实业务数值
- 五个用户群体的详细画像与性别构成分析
- 二维散点图可视化分群结果
- 针对每类人群的差异化营销策略
数据集包含 200 位商场会员的信息,特征包括性别、年龄、年收入和消费得分。
二、数据探索
2.1 导入数据与基本信息
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# 设置 pandas 显示优化
pd.set_option('display.max_columns', None)
pd.set_option('display.width', 2000)
pd.set_option('display.max_colwidth', 35)
# 设置显示中文
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 读取数据
df = pd.read_csv("7.Mall_Customers.csv")
# 初步观察数据情况
print(df.columns.tolist())
print(f"数据形状:{df.shape}")
print(f"\n前五行数据:\n{df.head()}")
print(f"\n数据类型与缺失值情况:")
print(df.info())
print(df.describe())
['CustomerID', 'Gender', 'Age', 'Annual Income (k$)', 'Spending Score (1-100)']
数据形状:(200, 5)
前五行数据:
CustomerID Gender Age Annual Income (k$) Spending Score (1-100)
0 1 Male 19 15 39
1 2 Male 21 15 81
2 3 Female 20 16 6
3 4 Female 23 16 77
4 5 Female 31 17 40
数据类型与缺失值情况:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 200 entries, 0 to 199
Data columns (total 5 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 CustomerID 200 non-null int64
1 Gender 200 non-null object
2 Age 200 non-null int64
3 Annual Income (k$) 200 non-null int64
4 Spending Score (1-100) 200 non-null int64
dtypes: int64(4), object(1)
memory usage: 7.9+ KB
None
CustomerID Age Annual Income (k$) Spending Score (1-100)
count 200.000000 200.000000 200.000000 200.000000
mean 100.500000 38.850000 60.560000 50.200000
std 57.879185 13.969007 26.264721 25.823522
min 1.000000 18.000000 15.000000 1.000000
25% 50.750000 28.750000 41.500000 34.750000
50% 100.500000 36.000000 61.500000 50.000000
75% 150.250000 49.000000 78.000000 73.000000
max 200.000000 70.000000 137.000000 99.000000
数据共 200 条、5 个特征,无缺失值。其中 CustomerID 是唯一标识,没有实际业务意义,需要去除。年龄(1870)、年收入(15137k$)、消费得分(1~99)三个数值特征的量纲差异很大,K-Means 基于欧氏距离,要做标准化处理。
2.2 特征选择:性别要不要加入聚类?
这是一个值得讨论的问题:
- 利:性别可能带来消费行为差异,理论上可以提供额外信息。
- 弊:
Gender是分类特征(男/女),K-Means 依靠欧氏距离,直接塞字符串不行,必须做独热编码;- 这个数据集的业务经典做法是不把性别放进聚类特征。原因:年龄、年收入、消费得分是连续的行为核心指标;性别属于人口属性标签,可以在聚类完成之后,事后用来做画像统计,而不是参与距离计算。
因此最终选择 Age、Annual Income (k$)、Spending Score (1-100) 三个连续特征作为聚类输入。
2.3 性别分布
先看一下整体性别分布,留作后续画像对比。
""" 查看性别分布 """
df.value_counts("Gender")
Gender
Female 112
Male 88
Name: count, dtype: int64
女性(56%)略多于男性(44%),整体相差不大。
三、数据标准化
K-Means 的核心是欧氏距离,如果不做标准化,年收入(几十到上百)的数值会完全"淹没"年龄和消费得分的差异,导致聚类结果被收入主导。
""" 标准化 """
from sklearn.preprocessing import StandardScaler
feature_cols = ["Age", "Annual Income (k$)", "Spending Score (1-100)"]
X_raw = df[feature_cols].copy()
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_raw)
标准化后三个特征均值为 0、标准差为 1,在距离计算中权重平等。
四、确定最佳聚类数 k
K-Means 需要预先指定聚类数量 k,这里用两种方法来辅助决策。
4.1 肘部法则
肘部法则的核心思想:随着 k 增大,簇内平方和(inertia,即惯性)会持续下降,但下降速度会在某个 k 值后明显变缓,形成一个"肘部"拐点。
""" 肘部法则,遍历 k, 计算 inertia 惯性 """
from sklearn.cluster import KMeans
inertia_list = []
k_range = range(2, 11)
for k in k_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init="auto")
kmeans.fit(X_scaled)
inertia_list.append(kmeans.inertia_)
# 绘制肘部图
plt.figure(figsize=(8, 5))
plt.plot(k_range, inertia_list, marker="o")
plt.xlabel("聚类数量 k")
plt.ylabel("惯性 inertia(簇内平方和)")
plt.title("K-Means肘部法则")
plt.grid(1)
plt.show()

从肘部图来看,k=4 或 k=5 附近出现了明显的拐点,但肘部法则的判断有一定主观性,需要结合轮廓系数进一步确认。
4.2 轮廓系数
轮廓系数衡量的是:一个样本与自己所在簇的相似度(凝聚度),与其他簇的相似度(分离度)之间的平衡。取值范围 [-1, 1],越接近 1 说明聚类效果越好。
""" 计算每个 k 对应的轮廓系数 """
from sklearn.metrics import silhouette_score
sil_score_list = []
for k in k_range:
kmeans = KMeans(n_clusters=k, random_state=42, n_init="auto")
labels = kmeans.fit_predict(X_scaled)
sil = silhouette_score(X_scaled, labels)
sil_score_list.append(sil)
plt.figure(figsize=(8, 5))
plt.plot(k_range, sil_score_list, marker="o", color="orange")
plt.xlabel("聚类数量 k")
plt.ylabel("轮廓系数 silhouette score")
plt.title("不同 k 的轮廓系数")
plt.grid(1)
plt.show()
for k, score in zip(k_range, sil_score_list):
print(f"k = {k}, 轮廓系数 = {score:.4f}")

k = 2, 轮廓系数 = 0.3355
k = 3, 轮廓系数 = 0.3579
k = 4, 轮廓系数 = 0.4040
k = 5, 轮廓系数 = 0.4085
k = 6, 轮廓系数 = 0.4311
k = 7, 轮廓系数 = 0.4101
k = 8, 轮廓系数 = 0.3674
k = 9, 轮廓系数 = 0.3744
k = 10, 轮廓系数 = 0.3619
4.3 k=5 vs k=6:指标最优 ≠ 业务可解释最优
从轮廓系数看,k=6 数值最高(0.4311),但这个数据集业界经典的业务分群是 k=5。这里体现了无监督学习的关键点:
- k=6:数学指标最好,但有可能其中某一类人群样本量很小,业务含义模糊;
- k=5:轮廓系数 0.4085 略低于 6,但 5 个客户群体每一类都有清晰的商业画像,商场运营可以直接落地做营销。
综合考虑业务可解释性和营销落地成本,最终选择 k=5 作为聚类方案(后面会有 k=6 的分类情况说明为什么不用 6)。
五、k=5 K-Means 聚类
5.1 训练最终模型并打标签
""" 训练 k=5 最终 K-Means 模型,给原始 df 打上簇标签 """
kmeans_final = KMeans(n_clusters=5, random_state=42, n_init="auto")
cluster_labels = kmeans_final.fit_predict(X_scaled)
df["cluster"] = cluster_labels # 把聚类标签追加回原始 DataFrame, 方便后续画像分析
print("每个簇样本数量:")
print(df["cluster"].value_counts().sort_index())
# 获取聚类中心点(标准化后的)
centers_scaled = kmeans_final.cluster_centers_
# 把中心点 反标准化,还原回真实业务数值(年龄、年收入、消费分数)
centers_origin = scaler.inverse_transform(centers_scaled)
centers_df = pd.DataFrame(centers_origin, columns=feature_cols)
print("\n各簇中心真实业务数值:")
print(centers_df.round(2))
每个簇样本数量:
cluster
0 58
1 40
2 26
3 45
4 31
Name: count, dtype: int64
各簇中心真实业务数值:
Age Annual Income (k$) Spending Score (1-100)
0 55.28 47.62 41.71
1 32.88 86.10 81.53
2 25.77 26.12 74.85
3 26.73 54.31 40.91
4 44.39 89.77 18.48
从簇中心可以看出一个关键洞察:消费行为不完全由收入和年龄决定,消费意愿是一个独立维度。比如簇 2 收入最低(26k\()但消费得分高达 74.85,而簇 4 收入最高(89.77k\))消费得分却只有 18.48——这正是聚类挖掘出来的有价值信息。
六、用户画像分析
6.1 各簇详细统计
遍历每个簇,查看年龄、消费得分、年收入的详细分布。
""" 遍历每个簇,查看【年龄-消费得分】统计 """
for c_id in sorted(df["cluster"].unique()):
sub = df[df["cluster"] == c_id]
print(f"\n======== 簇 {c_id} 样本量:{len(sub)} ========")
print(f"年龄均值: {sub['Age'].mean():.2f}, min:{sub['Age'].min()}, max:{sub['Age'].max()}")
print(f"消费得分均值: {sub['Spending Score (1-100)'].mean():.2f}, min:{sub['Spending Score (1-100)'].min()}, max:{sub['Spending Score (1-100)'].max()}")
print(f"年收入均值: {sub['Annual Income (k$)'].mean():.2f}")
======== 簇 0 样本量:58 ========
年龄均值: 55.28, min:40, max:70
消费得分均值: 41.71, min:3, max:60
年收入均值: 47.62
======== 簇 1 样本量:40 ========
年龄均值: 32.88, min:27, max:40
消费得分均值: 81.53, min:58, max:97
年收入均值: 86.10
======== 簇 2 样本量:26 ========
年龄均值: 25.77, min:18, max:35
消费得分均值: 74.85, min:39, max:99
年收入均值: 26.12
======== 簇 3 样本量:45 ========
年龄均值: 26.73, min:18, max:40
消费得分均值: 40.91, min:5, max:60
年收入均值: 54.31
======== 簇 4 样本量:31 ========
年龄均值: 44.39, min:32, max:59
消费得分均值: 18.48, min:1, max:39
年收入均值: 89.77
6.2 各簇性别分布
聚类时没有使用性别特征,现在事后统计每个簇的男女比例,丰富画像维度。
""" 统计每个簇的男女比例 """
print("每个簇性别分布")
gender_cluster = pd.crosstab(df["cluster"], df["Gender"], normalize="index")
gender_cluster.round(3)
每个簇性别分布
Gender Female Male
cluster
0 0.569 0.431
1 0.550 0.450
2 0.577 0.423
3 0.600 0.400
4 0.484 0.516
前四个簇都是女性略多,而簇 4(高收入节俭人群)男性比例(51.6%)略高于女性,这也是一个有趣的业务发现。
6.3 二维散点图可视化
分别绘制"年龄-消费得分"和"年收入-消费得分"两个维度的散点图,直观展示分群效果。
""" 绘制二维散点图,区分人群 """
plt.figure(figsize=(16, 6))
# 图1:年龄-消费得分
plt.subplot(1, 2, 1)
scatter1 = plt.scatter(
df["Age"], # x
df["Spending Score (1-100)"], # y
c=df["cluster"], # 按聚类簇编号给点上色
cmap="tab10", # 配色方案,tab10 自带 10 种
s=60, # 散点大小
alpha=0.7 # 透明度
)
plt.xlabel("Age")
plt.ylabel("Spending Score")
plt.title("年龄-消费得分分布")
plt.legend(*scatter1.legend_elements(), title="簇编号")
plt.grid(alpha=0.3)
# 图2:年收入-消费得分
plt.subplot(1, 2, 2)
scatter2 = plt.scatter(
df["Annual Income (k$)"],
df["Spending Score (1-100)"],
c=df["cluster"],
cmap="tab10",
s=60,
alpha=0.7
)
plt.xlabel("Annual Income(k$)")
plt.ylabel("Spending Score")
plt.title("年收入-消费得分分布")
plt.legend(*scatter2.legend_elements(), title="簇编号")
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()

从"年收入-消费得分"散点图可以非常清晰地看到五个簇的分布:左上角(高收入低消费)、右上角(高收入高消费)、左下角(低收入低消费)、右下角(低收入高消费)、以及中间区域,分群边界明确。
6.4 五群画像总结
将以上所有统计信息汇总成一张完整的画像表:
| 簇号 | 样本数 | 平均年龄 | 平均年收入(k$) | 平均消费得分 | 性别构成(女:男) | 人群画像总结 |
|---|---|---|---|---|---|---|
| 0 | 58 | 55.28 | 47.62 | 41.71 | 56.9% : 43.1% | 中老年保守群体,年龄集中 40-70 岁,中等收入,消费克制,不会疯狂消费。 |
| 1 | 40 | 32.88 | 86.10 | 81.53 | 55.0% : 45.0% | 核心高价值客户,中青年,高收入、高消费意愿,消费得分普遍 58-97,是商场最重要客群。 |
| 2 | 26 | 25.77 | 26.12 | 74.85 | 57.7% : 42.3% | 低收入高消费年轻人(月光族),年龄 18-35,收入不高,但愿意花钱享乐,消费欲望很强。 |
| 3 | 45 | 26.73 | 54.31 | 40.91 | 60.0% : 40.0% | 普通青年群体,年轻、中等收入,消费理性克制,不会冲动大额消费。 |
| 4 | 31 | 44.39 | 89.77 | 18.48 | 48.4% : 51.6% | 高收入节俭人群,中年,收入全场最高,但消费得分极低,有钱但是不愿意在商场消费。 |
七、业务营销策略
针对五个不同群体,制定差异化的营销方案:
簇 0:中老年保守群体
- 策略:性价比高、实用型商品,促销折扣,避免过度推销。
- 中老年客户消费理性,更看重实用性和性价比,过度营销反而会引起反感。
簇 1:高价值核心客户
- 策略:VIP 会员、专属折扣、高端新品优先推送,维护留存,防止流失。
- 这部分客户收入高、消费意愿强,是商场利润的核心来源,投入最多资源维护。
簇 2:年轻月光低收入高消费
- 策略:主推潮流、平价爆款,分期活动,刺激消费。
- 虽然收入不高,但消费意愿强,对价格敏感但愿意为喜欢的东西花钱,适合用促销和分期撬动。
簇 3:普通青年理性消费
- 策略:推送刚需日用品,做常规满减活动。
- 这部分群体消费克制但基数不小,适合用常规的满减、日用品促销维持稳定消费。
簇 4:高收入极度节俭
- 策略:挖掘潜在需求,推送高端刚需产品。
- 普通折扣很难打动这部分人,他们有钱但消费意愿低,需要精准匹配其真实需求,而不是简单打折。
八、k=6 补充对比
为了验证之前的判断,跑一下 k=6 的情况,看看多出来的一簇到底是什么。
""" 跑一下 k=6 时的情况 """
kmeans6 = KMeans(n_clusters=6, random_state=42, n_init="auto")
df["cluster_6"] = kmeans6.fit_predict(X_scaled)
print("k=6 每个簇样本数量:")
print(df["cluster_6"].value_counts().sort_index())
k=6 每个簇样本数量:
cluster_6
0 45
1 39
2 25
3 40
4 30
5 21
Name: count, dtype: int64
# 获取k=6聚类中心,还原原始业务数值
centers6_scaled = kmeans6.cluster_centers_
centers6_origin = scaler.inverse_transform(centers6_scaled)
centers6_df = pd.DataFrame(centers6_origin, columns=feature_cols)
print("k=6 各簇中心:")
print(centers6_df.round(2))
k=6 各簇中心:
Age Annual Income (k$) Spending Score (1-100)
0 56.33 54.27 49.07
1 32.69 86.54 82.13
2 25.56 26.48 76.24
3 26.12 59.42 44.45
4 44.00 90.13 17.93
5 45.52 26.29 19.38
k=6 多出来的簇 5 是"中年低收入低消费"人群(平均年龄 45.5 岁,年收入 26.29k$,消费得分 19.38)。在 k=5 时,这部分人被合并到了簇 0(中老年保守群体)中;k=6 时把他们单独拆分了出来。
业务层面权衡
- k=6 数学上轮廓系数更高,新增簇 5 在统计上是独立一簇。
- 但从业务实操角度:商场做营销,维护 6 套营销策略的成本比 5 套更高。
k=6 轮廓系数指标最优,可以拆分出中年低收入低消费客群;但 k=5 分组更少,每一类画像简洁清晰,业务落地成本更低。综合选择 k=5 作为最终聚类方案。
九、项目总结
项目目标
基于商场客户数据集,使用 K‑Means 无监督聚类完成客户分群,挖掘不同人群画像,为商场营销提供依据。特征:Age年龄、Annual Income (k$)年收入、Spending Score (1‑100)消费得分;性别不参与聚类,用于事后画像统计。
数据预处理
- 无缺失值;
CustomerID为标识符,丢弃不参与聚类;Gender 分类特征不进入距离计算。 - K‑Means 对特征量纲敏感,使用
StandardScaler标准化全部连续特征。
确定聚类数量 k
- 肘部法则:观察惯性(簇内平方和),拐点区间 k=4‑6。
- 轮廓系数:取值 [-1,1],数值越高聚类质量越好;k=6 轮廓系数 0.4311 最高,k=5 为 0.4085 略低。
- 对比实验 k=5 与 k=6:
- k=6:指标最优,样本无极端小簇,拆分出中年低收入低消费人群;但需要维护 6 套业务策略。
- k=5:指标略低,5 类客户画像清晰易懂,营销落地简单,本项目最终选择 k=5。
得出:无监督聚类不能单纯依靠指标选择 k,必须结合业务可解释性。
k=5 客户画像 & 营销策略
| 簇号 | 人群画像 | 核心特征 | 营销建议 |
|---|---|---|---|
| 0 | 中老年保守群体 | 年龄偏大,中等收入,消费克制 | 主推实用高性价比商品,折扣促销 |
| 1 | 核心高价值客户 | 中青年,高收入、高消费得分 | VIP 服务,高端新品优先,重点留存 |
| 2 | 月光年轻群体 | 年龄小,收入低,但消费意愿极强 | 潮流平价商品,分期活动,刺激冲动消费 |
| 3 | 普通理性青年 | 年轻中等收入,消费克制 | 刚需日用品,常规满减活动 |
| 4 | 高收入节俭人群 | 收入很高,但消费意愿极低 | 不适合普通折扣,挖掘高端刚需精准推送 |
十、结语
由于我做的时候使用的是 Jupyter Notebook,所以代码都是一段一段的看起来可能不方便,还请见谅!此外如果聪明的你发现了代码和表述有错误或者有更好的提议,可以在评论区写下你的建议,谢谢(●'◡'●)!数据下载:
浙公网安备 33010602011771号