机器学习项目:客户分群——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 特征选择:性别要不要加入聚类?

这是一个值得讨论的问题:

  • :性别可能带来消费行为差异,理论上可以提供额外信息。
    1. Gender 是分类特征(男/女),K-Means 依靠欧氏距离,直接塞字符串不行,必须做独热编码;
    2. 这个数据集的业务经典做法是不把性别放进聚类特征。原因:年龄、年收入、消费得分是连续的行为核心指标;性别属于人口属性标签,可以在聚类完成之后,事后用来做画像统计,而不是参与距离计算

因此最终选择 AgeAnnual 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()

7

从肘部图来看,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}")

7

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()

7.客户分群

从"年收入-消费得分"散点图可以非常清晰地看到五个簇的分布:左上角(高收入低消费)、右上角(高收入高消费)、左下角(低收入低消费)、右下角(低收入高消费)、以及中间区域,分群边界明确。

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 时把他们单独拆分了出来。

业务层面权衡

  1. k=6 数学上轮廓系数更高,新增簇 5 在统计上是独立一簇。
  2. 但从业务实操角度:商场做营销,维护 6 套营销策略的成本比 5 套更高。

k=6 轮廓系数指标最优,可以拆分出中年低收入低消费客群;但 k=5 分组更少,每一类画像简洁清晰,业务落地成本更低。综合选择 k=5 作为最终聚类方案


九、项目总结

项目目标

基于商场客户数据集,使用 K‑Means 无监督聚类完成客户分群,挖掘不同人群画像,为商场营销提供依据。特征:Age年龄、Annual Income (k$)年收入、Spending Score (1‑100)消费得分;性别不参与聚类,用于事后画像统计。

数据预处理

  1. 无缺失值;CustomerID为标识符,丢弃不参与聚类;Gender 分类特征不进入距离计算。
  2. K‑Means 对特征量纲敏感,使用StandardScaler标准化全部连续特征。

确定聚类数量 k

  1. 肘部法则:观察惯性(簇内平方和),拐点区间 k=4‑6。
  2. 轮廓系数:取值 [-1,1],数值越高聚类质量越好;k=6 轮廓系数 0.4311 最高,k=5 为 0.4085 略低。
  3. 对比实验 k=5 与 k=6:
    • k=6:指标最优,样本无极端小簇,拆分出中年低收入低消费人群;但需要维护 6 套业务策略。
    • k=5:指标略低,5 类客户画像清晰易懂,营销落地简单,本项目最终选择 k=5。

得出:无监督聚类不能单纯依靠指标选择 k,必须结合业务可解释性。

k=5 客户画像 & 营销策略

簇号 人群画像 核心特征 营销建议
0 中老年保守群体 年龄偏大,中等收入,消费克制 主推实用高性价比商品,折扣促销
1 核心高价值客户 中青年,高收入、高消费得分 VIP 服务,高端新品优先,重点留存
2 月光年轻群体 年龄小,收入低,但消费意愿极强 潮流平价商品,分期活动,刺激冲动消费
3 普通理性青年 年轻中等收入,消费克制 刚需日用品,常规满减活动
4 高收入节俭人群 收入很高,但消费意愿极低 不适合普通折扣,挖掘高端刚需精准推送

十、结语

由于我做的时候使用的是 Jupyter Notebook,所以代码都是一段一段的看起来可能不方便,还请见谅!此外如果聪明的你发现了代码和表述有错误或者有更好的提议,可以在评论区写下你的建议,谢谢(●'◡'●)!数据下载:

posted @ 2026-09-08 16:01  myLv  阅读(0)  评论(0)    收藏  举报