本章会用到的数据
公开下载:公司基本信息。代码会在首次运行时下载并保存到 data/course/。
这些数据能做什么:根据上市年限和发行价等特征对公司进行分组。
分析时注意:聚类没有预先给定的正确标签,结果要结合变量含义和稳定性解释。
判断结果:比较不同聚类数和随机起点,并说明分组是否容易解释。
【课堂核心】2.5 学时学习安排(1/2)
| 动机与先修 |
10 |
| K-Means 机制 |
25 |
| 合成案例输入 |
20 |
| 合成案例决策 |
25 |
【课堂核心】2.5 学时学习安排(2/2)
| 形成性检查与反馈 |
10 |
| 独立任务与反馈 |
30 |
| 公开案例 |
20 |
| 小结 |
10 |
【可选拓展】:未列入本表的层次聚类、额外二维机制图与完整推导安排课后。
本章议程:在未知中寻找规律
1. 核心概念
- 为何需要无监督学习?
- 什么是聚类分析?
- 目标:高内聚、低耦合
3. Python实战
- 数据预处理关键步骤
- 提出并验证候选 \(K\)
- 解读与验证聚类结果
回顾:监督学习的世界
在之前的章节中,我们的任务是有明确目标的预测。
- 我们拥有包含“问题” (特征
X) 和“答案” (标签 y) 的数据集。
- 目标: 训练一个模型 \(f\),使其能够根据新的
X 准确预测出 y。
- 例子: 根据贷款申请人的信息 (
X),预测其是否会违约 (y)。
我们总是在一位“导师”的指导下学习。
核心问题:当没有“正确答案”时,我们如何发现规律?
但是,如果现实世界没有给我们提供 y 呢?
- 我们只有一大堆数据点
X,没有任何预先定义的标签。
- 我们不知道数据的内在结构。
- 我们无法计算“准确率”,因为没有“正确答案”可以参照。
这时,我们就从“预测”转向了“发现”。
引入:无监督学习
无监督学习 (Unsupervised Learning) 是一类机器学习任务,其目标是从无标签的数据中发现隐藏的模式或内在结构。
它不是为了预测一个特定的输出,而是为了理解数据本身。
一个直观的类比:监督 vs. 无监督
监督学习
如同跟着一本有标准答案的习题册学习。每做一道题,都可以对照答案,知道自己是对是错,并不断修正方法。
无监督学习
如同在没有地图和导航的土地上探索。你需要自己观察地形,识别出哪里是森林,哪里是河流,哪里是山脉,从而绘制出地图。
核心问题:监督学习 vs. 无监督学习
这两种方法论代表了数据分析的两种不同思维范式。
| 数据 |
包含特征 (X) 和标签 (y) |
只包含特征 (X) |
| 目标 |
预测标签 y |
发现数据 X 中的隐藏结构 |
| 核心问题 |
“这个新客户会违约吗?” |
“我的客户可以被分成哪几类?” |
| 主要任务 |
分类、回归 |
聚类、降维、关联规则挖掘 |
聚类分析是无监督学习中最重要、最基础的工具。
引入案例:候选客群能否通过独立结果标签检查?
想象一家银行先按可观测特征形成候选客群,再用独立、足量、点时的结果标签检验群体差异。
- 挑战: 银行拥有的只是客户的基本信息(收入、负债、工龄等),并没有一个现成的“高风险客户”标签。
- 问题: 客户特征能形成哪些描述性群体?这些群体是否与风险有关,必须由聚类之外的结果标签和样本外设计回答。
这就是聚类分析要解决的问题。
可视化问题:一片混沌的客户数据
我们的起点是一堆看似杂乱无章的客户数据点。每个点代表一个客户,其在图上的位置由其经济特征(如收入、负债)决定。
我们的目标是找到一种方法,让计算机自动地为这些点“着色”,把相似的客户圈在一起。
聚类分析的目标:让“相似的”数据“在一起”
聚类分析 (Clustering) 是一种将数据集中的样本划分为若干个不相交子集(称为“簇”,Cluster)的过程。
核心原则:
- 簇内相似性 (Intra-cluster similarity) 高: 同一个簇内的数据点彼此相似。
- 簇间相似性 (Inter-cluster similarity) 低: 不同簇之间的数据点彼此不相似。
在贷款模拟案例中,我们只在选定特征与距离尺度下寻找两个描述性客群;簇号与高、低违约风险类别没有对应关系。
图解聚类原则
一个好的聚类结果,应该像下图这样:簇内部的点都紧紧地抱在一起,而不同颜色的簇之间则泾渭分明。
我们将聚焦于最经典的算法:K-均值 (K-Means)
K-均值法是一种非常流行且直观的聚类算法。它在各种应用中都表现出色,是每个数据科学家都必须掌握的基础工具。
观察下图中,即使我们不知道每个点的“标签”,我们的直觉也能清晰地将这些数据点分为三个不同的群组。
K-均值算法就是用数学的方式来形式化并实现这一过程。
K-均值的视觉直觉
代码
# 为“K-均值的视觉直觉”导入 `matplotlib.pyplot` 并绑定 `plt`,用于构建当前任务的坐标轴并呈现比较结果。
import matplotlib.pyplot as plt
# 为“K-均值的视觉直觉”导入 `numpy` 并绑定 `np`,用于执行当前任务的数组、数值或随机机制计算。
import numpy as np
# 为“K-均值的视觉直觉”,从 `sklearn.datasets` 导入`make_blobs` 用于生成聚类机制演示所需的成团样本。
from sklearn.datasets import make_blobs
# 生成三个中心明确的二维点群,作为 K-means 最近质心直觉的机制输入。
X, y = make_blobs(n_samples=300, centers=3, cluster_std=0.8, random_state=42)
# 为“K-均值的视觉直觉”建立投影画布,给坐标、图例与标注预留空间。
plt.figure(figsize=(8, 5))
# 以 `X[:, 0]` 为横轴、`X[:, 1]` 为纵轴绘制散点,展示“一个可以被清晰地分为三个簇的数据集”。
plt.scatter(X[:, 0], X[:, 1], s=40, edgecolors='k', alpha=0.8, c='#808080')
# 将图题设为“直觉告诉我们这里有三个群体”,直接说明当前图形的比较目的。
plt.title('直觉告诉我们这里有三个群体', fontsize=16)
# 将横轴标为“特征 1 (Feature 1)”,明确横向编码的变量。
plt.xlabel('特征 1 (Feature 1)')
# 将纵轴标为“特征 2 (Feature 2)”,明确纵向编码的变量。
plt.ylabel('特征 2 (Feature 2)')
# 为“K-均值的视觉直觉”添加辅助网格,便于比较位置、斜率或组间差异。
plt.grid(True)
# 显示尚未拟合或着色的灰色机制点云,只用空间间隔建立“三个自然点群”的视觉直觉。
plt.show()
解构算法名称: “K-Means”
这个名字本身就揭示了算法的两个核心要素:
- K: 这是我们需要预先指定的超参数,代表我们希望将数据分成多少个簇。
- 贷款案例把
K=2 作为事先确定教学候选;K 不能由事后风险标签或希望得到的业务故事决定。
- 对于上一页的数据,最合理的选择显然是
K=3。
- Means (均值): 这指的是算法的核心操作——计算簇内数据点逐坐标的算术平均值,得到簇的质心 (Centroid)。在平方欧氏距离下,该质心最小化簇内平方和。
如何用数学语言定义一个“好”的聚类?
为了让计算机执行聚类,我们必须将“好的聚类”这个模糊概念,转化为一个可以计算和优化的数学目标。
这需要两个关键的数学工具:
- 距离度量 (Distance Metric): 一种量化两个数据点之间“不相似度”的方法。
- 目标函数 (Objective Function): 一个评判整个聚类结果优劣的单一数值指标。
基础:使用平方欧几里得距离度量差异
最常用的距离度量方法是平方欧几里得距离 (Squared Euclidean Distance)。
对于两个 m 维的数据点 \(x^{(i)}\) 和 \(x^{(j)}\),它们之间的平方欧几里得距离定义为:
\[ \large{d^2(x^{(i)}, x^{(j)}) = \sum_{k=1}^{m} (x_k^{(i)} - x_k^{(j)})^2 = ||x^{(i)} - x^{(j)}||^2} \]
直观理解: 这本质上就是我们在二维或三维空间中熟悉的直线距离的平方,并推广到了更高维度。
图解欧几里得距离
在二维空间中,它就是勾股定理的应用。这个距离是我们衡量“相似性”的基石。
目标:最小化簇内离差平方和 (WCSS)
有了距离度量,我们就可以定义一个簇 \(C_k\) 的“紧凑程度”。我们使用簇内离差平方和 (Within-Cluster Sum of Squares, WCSS) 来衡量。
它等于簇内每个数据点到该簇中心点 (Centroid) \(\mu_k\) 距离的平方和。
\[ \large{\text{WCSS}(C_k) = \sum_{x_i \in C_k} ||x_i - \mu_k||^2} \]
- \(\mu_k\) 是簇 \(k\) 的中心点(即该簇所有点的均值)。
- WCSS 值越小,说明簇 \(k\) 内部的数据点越紧凑。
图解 WCSS
WCSS 的值就是下图中所有红色虚线长度的平方之和。我们的目标是让这个总和尽可能小。
K-均值的最终目标函数
现在,我们可以明确 K-均值算法的最终优化目标了。
假设我们要将数据分为 \(K\) 个簇 \(C_1, C_2, \dots, C_K\),我们的目标是找到这样一种划分方式,使得所有簇的 WCSS 之和 (即 Total WCSS) 最小:
\[ \large{\min_{C_1, \dots, C_K} \sum_{k=1}^{K} \text{WCSS}(C_k) = \min_{C_1, \dots, C_K} \sum_{k=1}^{K} \sum_{x_i \in C_k} ||x_i - \mu_k||^2} \]
这个公式是固定 \(K\)、距离、缩放与样本规格下的内部优化目标和拟合指标。WCSS 随 \(K\) 增大必然不升,不能跨 \(K\) 或跨缩放方案直接充当最终质量判断;簇数、稳定性与业务采用还须结合轮廓系数、跨 seed ARI、外部或时间外检查及不采用条件。
求解挑战:一个NP-Hard问题
直接求解上述目标函数是一个极其困难的组合优化问题 (NP-hard)。
- 对于一个有 \(N\) 个数据点和 \(K\) 个簇的数据集,可能的划分方式数量是天文数字。
- 暴力搜索所有可能性是完全不可行的。
幸运的是,我们有一个非常高效的启发式算法,可以在实践中快速找到一个很好的局部最优解。
K-均值算法流程:一个优雅的迭代过程
这个算法通过交替执行两个核心步骤来逐步优化聚类结果,直到收敛。
- 分配 (Assignment Step): 将每个数据点分配给离它最近的簇中心。
- 更新 (Update Step): 将每个簇的中心点更新为该簇内所有数据点的均值。
算法不断重复这两个步骤,就像跳着一支优雅的华尔兹,直到簇的分配不再发生变化为止。
算法分步图解:起点
我们从一堆无标签的数据点开始。假设我们决定要找 K=2 个簇。
算法分步图解:步骤 1 (初始化)
随机在数据空间中选择 K 个点作为初始的簇中心(质心, Centroids)。
算法分步图解:步骤 2a (分配)
对于每个数据点,计算它到两个簇中心的距离,并将其分配给最近的那个簇。数据点开始有了“颜色”。
算法分步图解:步骤 2b (更新)
对于每个簇,重新计算其中心点,即该簇内所有数据点逐坐标的算术平均(质心);在平方欧氏距离下,这个更新使当前簇内平方和最小。
本例重新计算得到:蓝簇质心为 ((200+250+180+280+230)/5, (150+120+200+180+210)/5) = (228, 172);橙簇质心为 (576, 234)。
\[ \large{\mu_k = \frac{1}{|C_k|} \sum_{x_i \in C_k} x_i} \]
本例质心移动到 (228,172) 与 (576,234)
算法分步图解:迭代循环
接下来,我们基于新的簇中心,不断重复分配和更新这两个步骤。
算法分步图解:最终收敛
最终,簇中心会稳定在各自数据群的“重心”,分配结果不再变化,算法就收敛了。我们成功地发现了数据中的两个群组。
K-均值算法的动态演示
四个面板依次展示 \(K=3\) 的第 1、2、3 次迭代与收敛状态:质心向点云重心移动,簇标签逐步稳定。
代码
# 为“K-均值算法的动态演示”导入 `matplotlib.pyplot` 并绑定 `plt`,用于构建当前任务的坐标轴并呈现比较结果。
import matplotlib.pyplot as plt
# 为“K-均值算法的动态演示”导入 `numpy` 并绑定 `np`,用于执行当前任务的数组、数值或随机机制计算。
import numpy as np
# 为“K-均值算法的动态演示”,从 `sklearn.datasets` 导入`make_blobs` 用于生成聚类机制演示所需的成团样本。
from sklearn.datasets import make_blobs
# 为“K-均值算法的动态演示”,从 `sklearn.cluster` 导入 `KMeans`,执行 K-means 客户分群。
from sklearn.cluster import KMeans
# 生成固定三簇点群,供四个面板复现初始化、分配和质心更新过程。
X, y_true = make_blobs(n_samples=300, centers=3, cluster_std=0.8, random_state=42)
# 封装单次 K-means 数据版本,把样本簇标签、质心位置与迭代编号绘到指定面板。
def plot_kmeans_iteration(X, kmeans, iteration, ax, show_legend=False):
# 预测并获取中心点
labels = kmeans.predict(X)
# 读取 `centers` 的簇中心坐标,标出 K-means 当前迭代的位置。
centers = kmeans.cluster_centers_
# 绘制数据点
ax.scatter(X[:, 0], X[:, 1], c=labels, s=40, cmap='viridis', alpha=0.7, edgecolors='k')
# 绘制中心点
ax.scatter(centers[:, 0], centers[:, 1], c='red', s=200, alpha=0.9, marker='X', label='Centroids', edgecolors='k')
# 将图题设为“迭代 {iteration}”,直接说明当前图形的比较目的。
ax.set_title(f'迭代 {iteration}', fontsize=18)
ax.tick_params(labelsize=16) # 统一各迭代面板的刻度字号
# 为“K-均值算法的动态演示”添加辅助网格,便于比较位置、斜率或组间差异。
ax.grid(True)
# 仅在指定面板显示图例,避免四个分面重复占用空间。
if show_legend:
# 显示“K-均值算法的动态演示”图例,使颜色或线型与比较对象一一对应。
ax.legend()
# 创建二乘二面板画布,按迭代顺序展示 K-means 的分配与质心移动。
fig, axes = plt.subplots(2, 2, figsize=(12, 6))
# 创建 `axes` 画布,承载“K-均值聚类的迭代步骤 (K=3)”的并排视觉比较。
axes = axes.flatten()
# 迭代 1
kmeans_iter1 = KMeans(n_clusters=3, init='random', n_init=1, max_iter=1, random_state=100)
# 用 `X` 更新 `kmeans_iter1` 的簇中心与样本分配,完成本次 K-means 拟合。
kmeans_iter1.fit(X)
# 绘制 K-means 的指定迭代数据版本,展示质心移动与簇分配如何收敛。
plot_kmeans_iteration(X, kmeans_iter1, 1, axes[0])
# 迭代 2
kmeans_iter2 = KMeans(n_clusters=3, init=kmeans_iter1.cluster_centers_, n_init=1, max_iter=1, random_state=1)
# 用 `X` 更新 `kmeans_iter2` 的簇中心与样本分配,完成本次 K-means 拟合。
kmeans_iter2.fit(X)
# 绘制 K-means 的指定迭代数据版本,展示质心移动与簇分配如何收敛。
plot_kmeans_iteration(X, kmeans_iter2, 2, axes[1])
# 迭代 3
kmeans_iter3 = KMeans(n_clusters=3, init=kmeans_iter2.cluster_centers_, n_init=1, max_iter=1, random_state=1)
# 用 `X` 更新 `kmeans_iter3` 的簇中心与样本分配,完成本次 K-means 拟合。
kmeans_iter3.fit(X)
# 绘制 K-means 的指定迭代数据版本,展示质心移动与簇分配如何收敛。
plot_kmeans_iteration(X, kmeans_iter3, 3, axes[2])
# 迭代 10 (收敛)
kmeans_final = KMeans(n_clusters=3, init='random', n_init=1, max_iter=10, random_state=100)
# 用 `X` 更新 `kmeans_final` 的簇中心与样本分配,完成本次 K-means 拟合。
kmeans_final.fit(X)
# 绘制 K-means 的指定迭代数据版本,展示质心移动与簇分配如何收敛。
plot_kmeans_iteration(X, kmeans_final, '最终 (收敛)', axes[3], show_legend=True)
# 为“K-均值算法的动态演示”,压缩子图留白,避免标题和坐标标签相互遮挡。
fig.tight_layout()
# 显示 K-means 各迭代阶段的分配与质心移动,检查目标函数通过交替更新逐步下降。
plt.show()
K-均值:一个强大但非完美的工具
K-均值是一个强大而简洁的算法,但理解其短板同样重要。我们需要知道在什么情况下它会表现不佳,以及如何规避这些问题。
| 速度快、效率高: 对于大规模数据集,算法收敛速度很快。 |
局部最优: 结果受初始化的影响,可能收敛到局部最优而非全局最优解。 |
| 原理简单: 易于理解和实现。 |
对特征尺度敏感: 距离计算受变量单位的影响。 |
| 可解释性强: 聚类结果(簇中心)有直观的物理解释。 |
需要预先指定K值: 在许多应用中,我们并不知道最佳的簇数量。 |
| 适用性广: 能够处理各种维度的数据。 |
对非球形簇效果不佳:倾向于发现大小相似的球形簇。 |
缺点 1:结果受随机初始化影响
由于算法从随机的中心点开始,不同的初始值可能导致完全不同的聚类结果。
- 问题: 算法只能保证收敛到一个局部最优解,但不能保证这是全局最优解。
解决方案:多次初始化 (n_init)
解决方案: 实践中,我们会进行多次随机初始化,独立运行 K-均值算法,然后选择最终 WCSS 最小的那个结果作为最终答案。
幸运的是,scikit-learn 中的 KMeans 类通过 n_init 参数自动完成了这个过程。
代码
# 为“解决方案:多次初始化 (`n_init`)”,导入 KMeans,演示多次初始化后按最低 WCSS 选择聚类结果。
from sklearn.cluster import KMeans
# 本章显式 n_init=10,运行 10 个初始化并保留最低 inertia;sklearn 1.7.2 的 auto 次数取决于 init
# 并返回WCSS最低的那个结果
# 固定随机种子并重复十次初始化,保留簇内平方和最低的解。
kmeans = KMeans(n_clusters=3, n_init=10, random_state=42)
这可降低落入较差局部解的风险,但仍不保证全局最优。
缺点 2:对特征的尺度敏感
K-均值基于欧几里得距离,这意味着数值范围大的特征会在距离计算中占据主导地位。
- 例子: 假设我们有两个特征:
年收入 (范围: 20,000 - 200,000) 和 住房数量 (范围: 0 - 5)。在计算距离时,收入的巨大差异会完全掩盖住房数量的影响。
解决方案:先说明距离与缩放规则
- Z-score:量纲不可比、且希望各列具有同等初始权重时的常用选项。
- 保留业务尺度:原量纲本身已有明确业务权重时。
- 稳健变换:重尾或离群值明显时,比较
log1p、winsorize 或 robust scaling。
- 共同约束:缩放参数只在当前拟合样本估计,并检查簇成员、轮廓系数与原量纲画像的敏感性。
代码
# 为“解决方案:特征标准化”导入逐特征估计拟合样本均值与标准差的缩放器,避免大尺度变量主导距离。
from sklearn.preprocessing import StandardScaler
# 逐特征估计当前聚类拟合样本的均值与标准差。
scaler = StandardScaler()
# 使用同一组拟合样本逐特征统计量将财务特征转换为可比尺度。
X_scaled = scaler.fit_transform(X)
本页代码演示“各列同等初始权重”口径下的 Z-score;是否采用必须由上述目标与敏感性证据决定。
缺点 3:必须预先确定 K 的值
这是 K-均值最核心的挑战。如果选择的 K 值不当,聚类结果将毫无意义。
如下方 图 3 所示,如果我们错误地将数据分为 K=5 个簇,算法会强行将一个自然的群体拆分开,产生误导性的结论。
那么,我们如何科学地选择 K 呢?
代码
# 建立错误的五簇 K-means,演示自然三簇结构被过度切分的后果。
kmeans_wrong_k = KMeans(n_clusters=5, random_state=42, n_init=10)
# 拟合聚类器并得到 `y_pred`,让每条公司观测对应一个簇标签。
y_pred = kmeans_wrong_k.fit_predict(X)
# 为“缺点 3:必须预先确定 K 的值”建立投影画布,给坐标、图例与标注预留空间。
plt.figure(figsize=(9, 3.0))
# 以 `X[:, 0]` 为横轴、`X[:, 1]` 为纵轴绘制散点,展示“选择错误的K值 (K=5) 导致了糟糕的聚类结果”。
plt.scatter(X[:, 0], X[:, 1], c=y_pred, s=40, cmap='viridis', edgecolors='k', alpha=0.8)
# 读取 `centers` 的簇中心坐标,标出 K-means 当前迭代的位置。
centers = kmeans_wrong_k.cluster_centers_
# 以 `centers[:, 0]` 为横轴、`centers[:, 1]` 为纵轴绘制散点,展示“选择错误的K值 (K=5) 导致了糟糕的聚类结果”。
plt.scatter(centers[:, 0], centers[:, 1], c='red', s=200, alpha=0.9, marker='X', edgecolors='k')
# 将图题设为“当 K=5 时,自然的簇被强行分割”,直接说明当前图形的比较目的。
plt.title('当 K=5 时,自然的簇被强行分割', fontsize=18)
plt.xticks(fontsize=16) # 放大横轴刻度以适配投影
plt.yticks(fontsize=16) # 放大纵轴刻度以适配投影
# 将横轴标为“特征 1 (Feature 1)”,明确横向编码的变量。
plt.xlabel('特征 1 (Feature 1)')
# 将纵轴标为“特征 2 (Feature 2)”,明确纵向编码的变量。
plt.ylabel('特征 2 (Feature 2)')
# 为“缺点 3:必须预先确定 K 的值”添加辅助网格,便于比较位置、斜率或组间差异。
plt.grid(True)
# 显示单个 K=5 聚类结果及五个质心,检查自然三簇被过度切分的失败模式。
plt.show()
解决方案:用“肘部法则”提出候选 K
“肘部法则”是根据当前样本 WCSS 边际下降提出候选 K 的启发式方法。
- 对一系列不同的
K 值(例如,从 1 到 10)运行 K-均值算法。
- 为每个
K 值计算最终的总簇内离差平方和 (Total WCSS)。
- 将 Total WCSS 随
K 值的变化绘制成图。
- WCSS 下降率变缓的“手肘”只是候选复杂度折中,须再查稳定性、轮廓及外部/业务有效性。
“肘部法则”的直觉
- 单调性:WCSS 随 \(K\) 增大必然不升。
- 本章现象:在已知三中心的模拟数据中,\(K\approx3\) 附近的 WCSS 边际下降变缓,只是与生成设定一致的可视启发。
- 解释边界:折点不能识别真实结构、证明业务有效性或给出无条件“最佳 \(K\)”。
- 真实任务:事先确定候选,并检查稳定性、轮廓以及外部或业务证据。
“肘部法则”的 Python 实现与可视化
在同一生成样本上比较 \(K=1,\ldots,10\):图 4 在 \(K=3\) 附近由陡转缓,只提出候选值;还需结合稳定性、轮廓系数与业务解释。
代码
# 为““肘部法则”的 Python 实现与可视化”,从 `sklearn.cluster` 导入 `KMeans`,执行 K-means 客户分群。
from sklearn.cluster import KMeans
# 初始化 WCSS 路径,随后为每个候选簇数保存同一数据上的簇内平方和。
wcss = []
# 建立 `k_values` 的有序取值网格,用于展示“使用肘部法则提出候选 K”随参数变化的比较结果。
k_values = range(1, 11)
# 遍历 `k in k_values` 的候选超参数,逐个拟合并记录““肘部法则”的 Python 实现与可视化”的评价量。
for k in k_values:
# 创建 `KMeans` 实例 `kmeans`,用于按样本到质心的距离执行 K-means 聚类。
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
# 用 `X` 更新 `kmeans` 的簇中心与样本分配,完成本次 K-means 拟合。
kmeans.fit(X)
wcss.append(kmeans.inertia_) # .inertia_ 属性就是 Total WCSS
# 将候选 K 与 WCSS 组成肘部曲线,检查边际下降的转折位置。
# 为““肘部法则”的 Python 实现与可视化”建立投影画布,给坐标、图例与标注预留空间。
plt.figure(figsize=(8, 6))
# 以 `k_values` 为横轴、`wcss` 为纵轴绘制曲线,展示“使用肘部法则提出候选 K”。
plt.plot(k_values, wcss, 'o-', color='crimson')
# 将图题设为“肘部法则示意图”,直接说明当前图形的比较目的。
plt.title('肘部法则示意图', fontsize=16)
# 将横轴标为“簇的数量 (K)”,明确横向编码的变量。
plt.xlabel('簇的数量 (K)')
# 将纵轴标为“总簇内离差平方和 (Total WCSS)”,明确纵向编码的变量。
plt.ylabel('总簇内离差平方和 (Total WCSS)')
# 为““肘部法则”的 Python 实现与可视化”,设置横轴刻度位置或标签,明确候选取值。
plt.xticks(k_values)
# 标注肘部点
plt.axvline(x=3, color='grey', linestyle='--', linewidth=2)
# 以箭头标注““手肘”点 (K=3)”,指出“使用肘部法则提出候选 K”中的候选拐点或阈值。
plt.annotate('“手肘”点 (K=3)', xy=(3, wcss[2]), xytext=(4, 2000),
# 指定 `arrowprops` 为注释箭头样式,细化“使用肘部法则提出候选 K”的输出。
arrowprops=dict(facecolor='black', shrink=0.05, width=1.5, headwidth=8),
# 将肘点文字设为 14pt 并置于白底上,避免曲线穿过标注。
fontsize=14, backgroundcolor='white')
# 为““肘部法则”的 Python 实现与可视化”添加辅助网格,便于比较位置、斜率或组间差异。
plt.grid(True)
# 显示 WCSS 随 K 增加的下降曲线,寻找边际改善明显放缓的肘部候选。
plt.show()
实战案例:对贷款客户进行风险分群
现在,我们把理论应用到一个合成的贷款风险教学案例。它只用于演示聚类流程,不能代表任何真实借款人群体。
- 数据: 代码按预设机制生成客户特征与结果标签;不是公开贷款数据。
- 目标: 在不使用“是否违约”这个标签的前提下,仅根据客户的经济特征,将他们划分为不同的风险群体。
- 内部核对: 完成聚类后,再用未参与聚类的合成结果标签做教学性 sanity check;由于标签由同一生成机制产生,这不是外部有效性证据。
确定依赖与随机种子
我们的分析将主要依赖于 pandas 进行数据处理,sklearn 进行机器学习,以及 matplotlib 和 seaborn 进行可视化。
pandas |
用于数据读取、操作和清洗 (DataFrames) |
numpy |
用于高效的数值计算 |
sklearn.cluster |
包含 KMeans 等聚类算法 |
sklearn.preprocessing |
包含按拟合样本逐特征估计均值与标准差的 StandardScaler 等预处理工具 |
matplotlib.pyplot |
用于创建静态、可定制化的图表 |
seaborn |
基于 matplotlib 的高级可视化库 |
代码
# 为“实战步骤 1:导入必要的 Python 库”导入 `pandas` 并绑定 `pd`,用于整理当前任务的表格、字段与时间索引。
import pandas as pd
# 为“实战步骤 1:导入必要的 Python 库”导入 `numpy` 并绑定 `np`,用于执行当前任务的数组、数值或随机机制计算。
import numpy as np
# 为“实战步骤 1:导入必要的 Python 库”,从 `sklearn.cluster` 导入 `KMeans`,执行 K-means 客户分群。
from sklearn.cluster import KMeans
# 为“实战步骤 1:导入必要的 Python 库”,导入 `StandardScaler`,按本次无监督机制样本的特征列统一量纲后再计算聚类距离。
from sklearn.preprocessing import StandardScaler
# 为“实战步骤 1:导入必要的 Python 库”导入 `matplotlib.pyplot` 并绑定 `plt`,用于构建当前任务的坐标轴并呈现比较结果。
import matplotlib.pyplot as plt
# 为“实战步骤 1:导入必要的 Python 库”导入 `seaborn` 并绑定 `sns`,用于编码当前任务的统计分布或分组关系。
import seaborn as sns
读取后立即限定字段与样本
本节生成明确标记的模拟贷款数据,只解释 K-means 机制,不作为真实信贷或风险证据。
我们将聚焦于几个关键的经济特征:annual_inc (年收入), dti (债务收入比), emp_length (工龄), 和 home_ownership (住房情况)。
代码
# 为了教学演示,我们创建一个模拟的贷款数据集
# 真实世界的数据需要更复杂的清洗过程
np.random.seed(42)
# 为“实战步骤 2:数据读取与初步探索”,创建模拟数据
n_samples = 1000
annual_inc = np.random.lognormal(10.5, 0.5, n_samples) # 年收入
dti = np.random.normal(15, 5, n_samples) # 债务收入比
emp_length_num = np.random.uniform(0, 10, n_samples) # 工龄
home_ownership_num = np.random.choice([0, 1, 2], n_samples) # 住房情况
# 创建合成结果标签(基于预设特征逻辑)
# 该机制只服务教学,不能解释真实借款人的信用结果
default_prob = (1 / (1 + np.exp(-(-5 +
# 年收入每增加 1 元,线性违约得分按 -0.00003 调整。
-0.00003 * annual_inc +
# 债务收入比每增加 1 单位,线性违约得分增加 0.1。
0.1 * dti +
# 工龄每增加 1 年,线性违约得分减少 0.2。
-0.2 * emp_length_num))))
# 按给定概率抽取 `default` 的 0/1 标签,固定“实战步骤 2:数据读取与初步探索”的分类结果。
default = np.random.binomial(1, default_prob, n_samples)
# 创建DataFrame
df = pd.DataFrame({
# 保留年收入作为客户偿债能力特征。
'annual_inc': annual_inc,
# 保留债务收入比作为负债压力特征。
'dti': dti,
# 保留数值化就业年限作为职业稳定性特征。
'emp_length_num': emp_length_num,
# 保留数值化住房状态作为资产条件特征。
'home_ownership_num': home_ownership_num,
# 附上模拟事件字段,只用于代码流检查而不参与 K-means 拟合或画像命名。
'default': default
# 完成 `df` 的客户特征与违约标签字段,保证聚类输入和事后画像使用同一观测。
})
# 我们关心的特征列
feature_cols = ['annual_inc', 'dti', 'emp_length_num', 'home_ownership_num']
# 记录模拟事件所需字段,不把它作为监督目标或外部验证标签。
target_col = 'default'
# 创建一个新的DataFrame用于分析,并移除含有缺失值的行以简化流程
df_cluster = df[feature_cols + [target_col]].dropna().copy()
# 打印 包含 3 项的键值记录。
print({'样本数': len(df_cluster), '特征数': len(feature_cols), '缺失单元格': int(df_cluster.isna().sum().sum())}) # 用单行摘要替代会滚动的head与info输出
{'样本数': 1000, '特征数': 4, '缺失单元格': 0}
Scaler 只在训练窗拟合
这是 K-均值聚类前最关键的一步。annual_inc 的数值范围远大于其他特征,我们必须对其进行标准化。
我们将使用 StandardScaler,逐特征估计本次待聚类样本的均值与标准差,再把这批拟合样本转换到可比尺度;本段不声称对未来样本保持相同列统计量。
代码
# 从客户表提取余额、收入、年龄等事先确定聚类字段,排除标签与标识列。
X = df_cluster[feature_cols]
# 建立逐特征标准化器,准备仅由聚类拟合样本估计均值与尺度。
scaler = StandardScaler()
# 用全部待聚类拟合样本逐特征估计均值与标准差;本段是描述性分群,不声明未来预测性能。
X_scaled = scaler.fit_transform(X)
# 将标准化数组恢复为带字段名的表格,便于检查每列量纲与后续画像解释。
X_scaled_df = pd.DataFrame(X_scaled, columns=feature_cols)
# 打印 包含 2 项的键值记录。
print({'标准化前均值': X.mean().round(2).to_dict(), '标准化后均值绝对值上限': float(X_scaled_df.mean().abs().max())}) # 只投影尺度检查所需摘要
{'标准化前均值': {'annual_inc': 41434.56, 'dti': 15.35, 'emp_length_num': 4.97, 'home_ownership_num': 1.03}, '标准化后均值绝对值上限': 3.410605131648481e-16}
K 是验证候选而非肘部真理
图 5 显示 \(K=2\) 或 \(K=3\) 均可作为候选。本例事先确定并执行 \(K=2\);\(K=3\) 仅列为未执行的课后敏感性候选,不属于本次结果,不能据此声称稳健。
代码
# 初始化簇内平方和累积列表,按候选 K 保存每个模型的惯性指标。
wcss = []
# 建立 `k_range` 的有序取值网格,用于展示“贷款客户数据集的肘部法则分析”随参数变化的比较结果。
k_range = range(1, 11)
# 遍历 `k in k_range` 的候选超参数,逐个拟合并记录“实战步骤 4:使用肘部法则提出候选 K”的评价量。
for k in k_range:
# 创建 `KMeans` 实例 `kmeans_elbow`,用于按样本到质心的距离执行 K-means 聚类。
kmeans_elbow = KMeans(n_clusters=k, n_init=10, random_state=42)
# 用 `X_scaled_df` 更新 `kmeans_elbow` 的簇中心与样本分配,完成本次 K-means 拟合。
kmeans_elbow.fit(X_scaled_df)
# 把 K-means 簇内离差 `kmeans_elbow.inertia_` 加入 `wcss`,形成 K 值的肘部曲线。
wcss.append(kmeans_elbow.inertia_)
# 为“实战步骤 4:使用肘部法则提出候选 K”建立投影画布,给坐标、图例与标注预留空间。
plt.figure(figsize=(10, 6))
# 以 `k_range` 为横轴、`wcss` 为纵轴绘制曲线,展示“贷款客户数据集的肘部法则分析”。
plt.plot(k_range, wcss, marker='o', linestyle='--')
# 将图题设为“贷款客户聚类的肘部法则”,直接说明当前图形的比较目的。
plt.title('贷款客户聚类的肘部法则', fontsize=16)
# 将横轴标为“簇的数量 K”,明确横向编码的变量。
plt.xlabel('簇的数量 K')
# 将纵轴标为“总簇内离差平方和 (WCSS)”,明确纵向编码的变量。
plt.ylabel('总簇内离差平方和 (WCSS)')
# 为“实战步骤 4:使用肘部法则提出候选 K”,设置横轴刻度位置或标签,明确候选取值。
plt.xticks(k_range)
# 在 `x=2` 处添加垂直参考线,标出“贷款客户数据集的肘部法则分析”的基准或阈值。
plt.axvline(x=2, color='red', linestyle='--', label='K=2(事先确定候选)')
# 显示“实战步骤 4:使用肘部法则提出候选 K”图例,使颜色或线型与比较对象一一对应。
plt.legend()
# 显示合成客户特征上的 WCSS 路径,检查选定 K 是否位于边际下降转折处。
plt.show()
确定 K 后再拟合 K-Means
我们选定了 K=2,现在可以正式运行 K-均值算法,并为每个客户分配一个簇标签(0 或 1)。
我们将把这个新的 cluster 标签添加回我们原始的 df_cluster DataFrame 中,以便进行后续分析。
代码
# 用选定簇数在标准化客户特征上拟合 K-means 并生成簇标签。
# 固定两簇方案与随机种子,生成可复现的客户分群基线。
kmeans = KMeans(n_clusters=2, n_init=10, random_state=42)
# 拟合聚类器并得到 `cluster_labels`,让每条公司观测对应一个簇标签。
cluster_labels = kmeans.fit_predict(X_scaled_df)
# 将每条客户观测的簇标签回填到原表,供分簇画像与违约率聚合。
df_cluster['cluster'] = cluster_labels
# 报告“簇样本数”中的 `df_cluster`,核对“实战步骤 5:执行 K-均值聚类 (K=2)”的样本形状。
print({'簇样本数': df_cluster['cluster'].value_counts().sort_index().to_dict()}) # 用单行字典核对分群规模,避免stdout滚动
{'簇样本数': {0: 492, 1: 508}}
簇标签须由特征画像命名
聚类本身只是第一步,最重要的工作是理解每个簇代表了什么样的客户群体。
我们通过计算每个簇中各个特征的平均值来为它们“画像”。
代码
# 按簇汇总原量纲特征均值,形成可解释的客户画像表。
cluster_profiles = df_cluster.groupby('cluster')[feature_cols].mean()
# 同时汇总样本量、模拟事件与原量纲画像,令讲解直接接受执行结果检查。
cluster_audit = df_cluster.groupby('cluster').agg(sample_count=('default', 'size'), default_count=('default', 'sum'), simulated_default_rate=('default', 'mean'), annual_inc=('annual_inc', 'mean'), dti=('dti', 'mean'), emp_length_num=('emp_length_num', 'mean'), home_ownership_num=('home_ownership_num', 'mean')).reset_index()
# 核对两个簇覆盖全部模拟观测,避免画像丢行。
assert cluster_audit['sample_count'].sum() == len(df_cluster)
# 确定当前随机种子的稀少模拟事件总数,阻止稳定风险叙事。
assert cluster_audit['default_count'].sum() == int(df_cluster['default'].sum()) == 5
# 建立短中文表头的展示副本,保留底层英文变量供后续检查代码复用。
cluster_audit_display = cluster_audit.rename(columns={'cluster': '簇', 'sample_count': '样本数', 'default_count': '模拟事件数', 'simulated_default_rate': '模拟事件率', 'annual_inc': '年收入', 'dti': 'DTI', 'emp_length_num': '工龄', 'home_ownership_num': '住房编码'})
# 输出两行八列紧凑画像与事件检查表,确保最右侧住房编码完整可见。
display(cluster_audit_display.round({'模拟事件率': 4, '年收入': 2, 'DTI': 2, '工龄': 2, '住房编码': 2}))
固定种子下的描述性解读:
- 两簇年收入接近(41,679 对 41,198);可见均值差异主要是工龄(5.89 对 4.08)、债务收入比(14.85 对 15.85)与住房编码(0.35 对 1.69)。
- 簇编号只是算法标签;画像是本次模拟样本的描述,不是“高/低风险”类别。
所有精确数值以本页代码生成表为准,不从簇号或五个模拟事件推断总体风险。
可视化解读:簇画像对比
- 变换来源:沿用 K-means 拟合前的
StandardScaler。
- 柱高含义:簇均值相对全样本同一特征均值的 Z-score。
- 比较边界:只在同一特征内比较两簇,不跨异质量纲特征解释柱高。
代码
# 使用拟合 K-means 输入时的逐特征均值与标准差转换两个簇的原量纲均值。
profile_z = pd.DataFrame(scaler.transform(cluster_profiles), index=cluster_profiles.index, columns=feature_cols).T
# 绘制逐特征跨簇可比的 Z-score 并列柱图,比较描述性客户画像。
profile_z.plot(kind='bar', figsize=(10, 6))
# 将图题设为“标准化的簇特征均值对比”,直接说明当前图形的比较目的。
plt.title('簇均值相对全样本的逐特征 Z-score', fontsize=16)
# 将纵轴标为“标准化均值 (Z-score)”,明确纵向编码的变量。
plt.ylabel('相对全样本均值的 Z-score')
# 将横轴标为“特征”,明确横向编码的变量。
plt.xlabel('特征')
# 为“可视化解读:簇画像对比”,设置横轴刻度位置或标签,明确候选取值。
plt.xticks(rotation=45)
# 显示“可视化解读:簇画像对比”图例,使颜色或线型与比较对象一一对应。
plt.legend(title='Cluster')
# 为“可视化解读:簇画像对比”添加辅助网格,便于比较位置、斜率或组间差异。
plt.grid(axis='y', linestyle='--')
# 显示逐特征标准化画像,并以原始单位表作为业务解释依据。
plt.show()
合成违约率只作内部核对
- 用途:用合成违约率检查分群是否与本次生成机制一致。
- 独立性:
default 未参与聚类,但它不是独立收集的真实标签。
- 不能推出:样本外风险识别能力。
代码
# 从画像检查表提取样本数、模拟事件数与比例,保持两页输出同源。
default_audit = cluster_audit[['cluster', 'sample_count', 'default_count', 'simulated_default_rate']].rename(columns={'cluster': 'Cluster', 'sample_count': '模拟样本数', 'default_count': '模拟违约数', 'simulated_default_rate': '模拟违约率'})
# 再次核对总事件数为五,防止图表把稀少事件误写成稳定风险排序。
assert int(default_audit['模拟违约数'].sum()) == 5
# 输出两个簇的分母、事件数与描述性比例。
display(default_audit.round({'模拟违约率': 4}))
本次随机种子下的观察:
- 簇 0 与簇 1 分别只有 2 与 3 个模拟违约,相差仅 1 个事件,总计只有 5 个事件。
- 这些比例无法支持稳定排序、显著性或总体风险推断,只用于核对本次生成机制。
可视化验证结果
代码
# 为“可视化验证结果”建立投影画布,给坐标、图例与标注预留空间。
plt.figure(figsize=(8, 5))
# 以簇编号分组并用模拟事件数作为柱高,避免把极少事件比例视觉放大。
sns.barplot(data=default_audit, x='Cluster', y='模拟违约数', palette='viridis')
# 用图题显式报告五个模拟事件的证据边界。
plt.title('5 个模拟违约事件:仅作描述性核对', fontsize=16)
# 将纵轴标为模拟事件数,避免误读为稳定总体风险率。
plt.ylabel('模拟违约事件数')
# 固定包含零点的五事件纵轴,保持绝对数量容易检查。
plt.ylim(0, 5)
# 遍历两个簇,在柱顶同时标注事件数、样本数与描述性比例。
for _, audit_row in default_audit.iterrows():
# 标注当前簇的完整分子、分母与比例,不隐藏小样本边界。
plt.text(audit_row['Cluster'], audit_row['模拟违约数'] + 0.15, f"{int(audit_row['模拟违约数'])}/{int(audit_row['模拟样本数'])} ({audit_row['模拟违约率']:.1%})", color='black', ha='center', fontsize=12)
# 显示五个模拟事件的描述性计数,并与前页检查表交叉核对。
plt.show()
当前两簇仅相差一个模拟事件,不能据此声称风险关联或排序。真实用途需要足量外部标签、不确定性分析、时间外验证与稳定性检验。
可视化我们的发现
我们可以通过散点图来更直观地展示两个簇在关键维度上的差异。
图 8 展示两簇在收入—DTI 平面的明显重叠;均值差异以代码生成画像表为准,不能仅凭散点或簇号命名风险。
代码
# 为“可视化我们的发现”建立投影画布,给坐标、图例与标注预留空间。
plt.figure(figsize=(9, 3.8))
# 绘制年收入—债务收入比散点,并以簇标签着色。
sns.scatterplot(
data=df_cluster, # 使用所有数据
# 指定 `x` 为横轴变量,细化“客户分群在年收入和债务收入比维度上的分布”的输出。
x='annual_inc',
# 指定 `y` 为纵轴变量,细化“客户分群在年收入和债务收入比维度上的分布”的输出。
y='dti',
# 指定 `hue` 为分组着色字段,细化“客户分群在年收入和债务收入比维度上的分布”的输出。
hue='cluster',
style='cluster',
# 指定 `palette` 为分组色板,细化“客户分群在年收入和债务收入比维度上的分布”的输出。
palette='viridis',
markers={0: 'o', 1: '^'},
# 将散点透明度设为 0.6,减轻重叠观测的遮挡。
alpha=0.6
# 完成 `scatterplot` 的轴、分组和色板参数,按簇显示收入—负债率分布。
)
# 将图题设为“客户分群的可视化结果”,直接说明当前图形的比较目的。
plt.title('客户分群的可视化结果', fontsize=16)
# 将横轴标为“年收入 (Annual Income)”,明确横向编码的变量。
plt.xlabel('年收入 (Annual Income)')
# 将纵轴标为“债务收入比 (Debt-to-Income Ratio)”,明确纵向编码的变量。
plt.ylabel('债务收入比 (Debt-to-Income Ratio)')
plt.xlim(0, 250000) # 限制x轴范围以便更好地观察
# 显示“可视化我们的发现”图例,使颜色或线型与比较对象一一对应。
plt.legend(title='客户分群')
# 显示按簇着色的收入—债务收入比散点,不绘制质心;检查簇间重叠并限定可视化不等同于外部验证。
plt.show()
结论:聚类分析是强大的探索性工具
- 从未知中创造价值: 即使没有明确的标签,聚类分析也能帮助我们从数据中发现有意义的、可操作的群体。
- 商业应用广泛:
- 市场营销: 客户细分,识别不同偏好的用户群体以进行精准营销。
- 金融: 形成候选客群,再用独立标签验证风险差异或异常交易识别效果。
- 投资: 将股票或资产根据其风险收益特征进行分组。
- 重要提示: K-均值只是众多聚类算法中的一种,它最适合发现球形的簇。
超越 K-均值:一瞥其他聚类方法
对于更复杂的非球形或密度不均的数据,我们可能需要探索如 DBSCAN 或层次聚类等其他方法。
检查:尺度、K 与局部最优
独立回答三问:
- 为何必须先固定缩放口径?
- K-均值与 KNN 的
K 分别代表什么?
- 单个 seed 的最低 WCSS 能否证明该
K 是真实业务分群?
反馈:两个 K 对应不同任务
- 缩放:距离受量纲支配,所以缩放口径必须先确定。
- 两个 K:K-均值的
K 是簇数,KNN 的 K 是邻居数。
- WCSS 边界:单次 WCSS 只衡量当前规格的簇内平方和,不能证明真实结构、业务效用或稳定性。
- 反馈路径:漏任一项者回看 相关内容,三项均分清者进入候选 K 检查。
独立任务:解释聚类选择边界
- 固定候选:事先确定
candidate_k=[2,3,4] 与 seeds=[0,1,2,3,4],保持特征、缩放和 n_init=10 不变。
- 逐 K 提交:平均 WCSS、平均轮廓系数,以及相对 seed 0 的最小 ARI。
- 入选后提交:仅对按事先确定规则入选的
selected_k 提交原量纲画像与模拟事件检查。
- 采用规则:先保留
min ARI >= 0.90 的 K,再选平均轮廓系数最高者;轮廓差在 0.01 内时取更小 K。
- 失败与边界:WCSS 只作边际下降描述;无稳定候选则“不采用”。即使通过,缺外部或时间外业务标签时也只能作探索性画像。
课堂练习与思考 (3/3)
3. 场景思考
除了课程中提到的描述性客户细分,请再构思两个你认为可以使用 K-均值分析方法解决的经济或商业问题。
请简要说明:
- 对象:业务问题、观测单位与时间粒度。
- 特征:入群字段、信息时点与未纳入聚类的结果字段。
- 距离/缩放:距离度量、变换或权重及其业务理由。
- 原量纲画像:回到原始单位报告每簇中心、离散与中性名称。
- 检查:指定未参与聚类的外部结果或时间外窗口。
- 结果不理想时如何解释:事先确定不稳定、无增量或不采用标准。
练习:先完成再查看答案:聚类检查先最终测试
- 候选比较:提交三个 K 的 WCSS、轮廓系数、跨 seed ARI 与事先确定的采用规则。
- 入选结果:提交入选 K 的原量纲画像和合成事件检查。
- 两个自拟场景:分别提交对象、特征、距离/缩放、原量纲画像、外部或时间外检查、失败/不采用条件;六项各 1 分。
- 失败处理:若无候选通过稳定门槛,画像与事件统一写
N/A,并提交“不采用”说明。
- 解释边界:不得用模拟事件率反向命名真实风险;缺任一项请重做,提交后进入答案。
练习参考答案与自查标准
- 候选 K:同一缩放输入上比较 K=2、3、4;轮廓系数衡量簇内凝聚与簇间分离,ARI 衡量初始化稳定性,二者都不等于业务有效性。
- 采用顺序:先过
min ARI >= 0.90,再按平均轮廓系数选择,0.01 内取更小 K;若无候选过门槛则不采用。
- 完成内容口径:每个 K 只交 WCSS、轮廓系数与最小 ARI;仅
selected_k 交原量纲画像与模拟事件检查。无稳定 K 时后两项为 N/A,并写“不采用”。
- 场景答案框架:必须写明对象、特征、距离、原量纲画像、外部或时间外检查与结果不理想时如何解释;只有“找客户群”不得分。
场景示范一:长三角制造企业经营画像
- 问题与对象:对沪苏浙皖非金融上市公司年度公司—年观测做探索性经营画像,不预先命名“优质/风险”簇。
- 特征与距离:只用公告时点已可得的 ROA、资产负债率、营收增长率、经营现金流/资产与规模;在每个开发期内 winsorize 后逐特征 z-score,采用平方欧氏距离。
- 画像用途:回到原量纲报告中位数、四分位距与行业构成,再由分析师给出中性描述名;簇号本身没有经济含义。
- 验证:跨随机种子报告 ARI,并在下一年度确定 scaler/质心后检查成员稳定性;另用未参与聚类的下一年现金流波动作外部描述性检查。
- 不采用条件:无候选 K 通过稳定门槛、时间外成员大幅漂移、单一行业主导,或画像对缩放/极值处理敏感时,结论为“不采用”。聚类不识别经营变化的因果来源。
场景示范二:银行网点现金需求分群
- 问题与对象:以已授权的中国银行网点—月为对象,形成补钞与运钞排班候选组;不把簇直接命名为欺诈或高风险。
- 特征与距离:使用历史日均取款额、周末占比、波动率、节假日峰值比和地理配送时长;对偏态金额取
log1p,再用训练期 robust scaling 与平方欧氏距离。
- 画像用途:在原量纲中报告现金需求、波动与配送时长,供运营团队设计候选服务等级,而不是自动决定资源分配。
- 验证:用未来三个月确定规则检验簇内需求预测误差与成员稳定性,并以实际缺钞次数作为未参与聚类的外部结果;同时按城市层级检查公平与覆盖。
- 不采用条件:未来误差不优于单一排班基线、成员漂移超过事先确定阈值、授权字段缺失或城市覆盖失衡时停止采用。外部结果差异仍是关联证据,不是因果标签。
候选 K 答案(1/3):几何与稳定性
代码
display(k_selection_audit.round(4)) # 展示三个候选的几何与初始化稳定性
print({'采用规则': 'min_ARI>=0.90 后最大 mean_silhouette;差0.01内取较小K', 'selected_K': selected_k, 'status': '探索性画像' if selected_k is not None else '不采用'}) # 动态报告事先确定决策
{'采用规则': 'min_ARI>=0.90 后最大 mean_silhouette;差0.01内取较小K', 'selected_K': 4, 'status': '探索性画像'}
候选 K 答案(2/3):入选 K 原量纲画像
代码
display(profile_delivery.round(2)) # 仅展示入选 K 画像;无稳定 K 时明确完成内容 N/A
候选 K 答案(3/3):模拟事件检查
代码
display(event_delivery.round(4)) # 仅展示入选 K 事件检查;无稳定 K 时明确完成内容 N/A
候选 K 结果只支持探索性结构
- 标签不变性:ARI 对簇编号置换不变。
- 证据范围:轮廓与稳定性通过只支持当前特征空间中的探索性结构。
- 画像规则:按预先声明的特征规则匹配。
- 合成事件边界:五个事件只核对代码流,不支撑真实违约预测、因果效应或信贷决策公平性。
公开中国数据练习:公司分群稳定性
代码
display(local_k_audit.round(4)) # 展示公开 WCSS、轮廓与跨 seed 稳定性
print({'文件': basic_path.name, '样本数': len(cluster_sample), 'selected_K': local_selected_k, '边界': '探索性结构;仍需外部或时间外业务检查'}) # 输出同要求的采用状态与边界
{'文件': 'stock_basic_data.h5', '样本数': 5402, 'selected_K': 3, '边界': '探索性结构;仍需外部或时间外业务检查'}
本章小结
- 能解释分配—更新—停止,并用标准化、多个初始化与 ARI 检查稳定性。
- \(K\) 是验证候选,不由肘部图自动给出“真值”;簇名必须来自特征画像。
- 稳定聚类仍是探索性结构,不等于风险类别、因果类型或信贷决策依据。
- 下一章进入缺失数据,因为聚类前的样本保留与插补也会改变几何结构。