30 K-Means算法 股市波动聚类
30.1 引言无监督学习与聚类分析
K-Means算法是经典的聚类算法:
- 无监督学习: 不需要标签
- 划分式聚类: 将数据分为K个簇
- 金融应用: 股票分类、风险分组
30.2 本章学习目标
先修内容:阅读本章前,建议先完成第 章节 10 章(Pandas 数据框基础)、第 章节 14 章(读取 Excel 文件)与第 章节 6 章(NumPy 数组运算,涉及向量距离计算)。
- K-Means 的目标函数(簇内平方和 SSE)与”分配—更新”迭代逻辑;
- 手工推演 K-Means 的迭代过程,理解初始质心、距离分配与质心更新三个要素;
- 用肘部法根据 SSE 曲线选择簇数 K;
- 在教学平台完成股指波动聚类任务,并解读簇标签、质心与簇均值画像;
- 认识 K-Means 对初始质心、簇形状与特征量纲的敏感性。
30.3 算法原理
K-Means目标: 最小化簇内平方和(SSE)
\[ SSE = \sum_{i=1}^{K}\sum_{x \in C_i} ||x - \mu_i||^2 \]
其中:
- \(K\): 簇的数量
- \(C_i\): 第i个簇
- \(\mu_i\): 第i个簇的质心(均值)
算法步骤:
- 随机初始化K个质心
- 将每个样本分配到最近的质心
- 重新计算每个簇的质心
- 重复2-3直到收敛
30.4 K-Means 的直觉:一次手工迭代
公式读起来有些抽象,我们用 6 个二维点把算法完整地”跑”一遍。设已从 6 只股指的历史行情中提取两个特征:x 轴为平均收益率(%),y 轴为波动率(%),股指A/B/C 明显是”低收益低波动”组,股指D/E/F 是”高收益高波动”组。手工指定两个初始质心 \(\mu_0=(1.0, 2.0)\) 与 \(\mu_1=(2.5, 2.5)\)——注意这两个质心都落在左下角,是一次”不好的初始化”,正好可以观察算法如何在迭代中自我纠正。
每一轮迭代只有两个动作:分配与更新。分配步骤计算每个样本到各质心的欧氏距离,把样本归入距离最近的质心所在簇;更新步骤把质心移动到本簇全体成员的均值位置。两个动作交替执行,直到分组不再变化为止。
import numpy as np # 导入NumPy数值计算库
points = np.array([[1.0, 2.0], [1.5, 1.8], [2.0, 2.2], [8.0, 9.0], [8.5, 8.6], [9.0, 9.4]]) # 6只股指的(平均收益率%,波动率%)特征
names = ['股指A', '股指B', '股指C', '股指D', '股指E', '股指F'] # 给每个样本命名,便于阅读输出
centroids = np.array([[1.0, 2.0], [2.5, 2.5]]) # 手工指定两个初始质心(故意都放在左下角)
def one_round(points, names, centroids, round_id): # 定义函数:完成一轮"分配-更新"
dist = np.array([[np.linalg.norm(p - c) for c in centroids] for p in points]) # 距离矩阵:每行是该样本到两个质心的欧氏距离
labels = dist.argmin(axis=1) # 分配步骤:每个样本归入距离更近的质心所在簇
print('第{}轮:各样本到两质心的距离(列依次为质心0、质心1):'.format(round_id)) # 输出标题
for i, name in enumerate(names): # 逐行打印距离与分配结果
print(' {}: 距离=({}, {}) -> 簇{}'.format(name, round(dist[i, 0], 2), round(dist[i, 1], 2), labels[i])) # 打印距离与簇编号
new_centroids = np.array([points[labels == k].mean(axis=0) for k in (0, 1)]) # 更新步骤:取各簇成员的均值作为新质心
print(' 更新后质心: 簇0=({:.2f}, {:.2f}), 簇1=({:.2f}, {:.2f})'.format(*new_centroids[0], *new_centroids[1])) # 打印新质心
return labels, new_centroids # 返回本轮分组与新质心
labels1, centroids = one_round(points, names, centroids, 1) # 第一轮:分配后更新质心
labels2, centroids = one_round(points, names, centroids, 2) # 第二轮:用新质心重新分配再更新
labels3, centroids = one_round(points, names, centroids, 3) # 第三轮:再次重复,检验是否收敛
print('第三轮与第二轮分组是否一致:', (labels2 == labels3).all()) # 一致说明分组不再变化,算法收敛对照输出可以看清”质心—分配—更新”循环是如何工作的。第一轮中,股指C 到质心1 的距离(0.58)小于到质心0 的距离(1.02),被误分入右上的簇,股指D/E/F 也全部聚到簇1;更新后簇1 的质心从 \((2.5, 2.5)\) 跳到 \((6.88, 7.30)\),大幅向真实簇中心移动。第二轮中,股指C 到簇0 新质心的距离(0.81)远小于到簇1 的距离(7.06),于是被重新分配回左下簇;第三轮分组与第二轮完全一致、质心也不再移动,算法收敛,最终得到”低收益低波动”与”高收益高波动”两组。这就是 K-Means 的全部机制:质心是簇内均值,分配依据最近距离,更新依据取均值,如此循环直至稳定。
30.5 肘部法:如何选 K
K-Means 需要预先给定簇数 K,而 SSE 本身不能直接用来选 K:SSE 随 K 增大单调下降,极端情况下 K 等于样本数时每个点自成一簇、SSE 为 0,但这样的聚类毫无意义。肘部法(Elbow Method)的思路是:让 K 从 1 逐步增大并逐一拟合 K-Means,绘制 SSE 折线。K 较小时,每多一个簇都能”买到”大幅的 SSE 下降;一旦 K 超过数据的真实簇数,新增的簇只是在切割本来就紧凑的簇,SSE 的下降骤然变缓,曲线在该处形成”肘部拐点”,拐点附近的 K 即为较合适的选择。下方平台任务代码的第一个循环(for k in range(1,10))做的正是这件事:对每个 k 拟合模型并记录 inertia_(即簇内平方和),再绘制 SSE 随 K 变化的折线图。需要提醒的是,肘部位置并不总是尖锐,常只能圈定一个候选范围(如 K=2~4),实务中还应结合业务含义与轮廓系数交叉验证;本章平台任务按平台题面取 K=3。平台绘制的肘部图形状以平台运行结果为准。
30.6 股指波动聚类
任务要求:将下方代码原样输入教学平台并运行(注释可省略);注意题面代码存在缩进错误,请按平台原始题面原样输入,以平台判定为准。该代码先用循环绘制肘部图,再用 K=3 对股指数据聚类,输出每个样本的簇标签。
# 注:case3.1.xlsx数据文件本地没有,但平台已经内置
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
# 注:该代码块存在缩进错误,请按平台原始题面原样输入,以平台判定为准
import pandas as pd # 导入Pandas数据分析库
from sklearn.cluster import KMeans # 导入Scikit-learn的KMeans模块
import matplotlib.pyplot as plt # 导入Matplotlib绘图库
data = pd.read_excel("case3.1.xlsx") # 从Excel文件读取数据存入data
SSE = [] # 定义列表SSE
cols = data.iloc[:, 2:] # 提取数值型特征列(跳过前两列的非数值信息)
for k in range(1,10): # 遍历range(1,10)中的每个k
estimator = KMeans(n_clusters=k) # 初始化K-Means聚类模型
estimator.fit(cols) # 在数据上训练estimator模型
SSE.append(estimator.inertia_) # 将当前K值的簇内误差平方和(SSE)添加到列表
plt.plot(range(1,10), SSE,'*-') # 绘制折线图
plt.title('Elbow Method') # 设置图表标题
plt.xlabel('K Clusters') # 设置X轴标签
plt.ylabel('SSE') # 设置Y轴标签
plt.savefig("1.png") #过程展示一
data1=data.drop('股指',axis=1) # 删除指定行或列
clu = KMeans(n_clusters=3,random_state=30) # 初始化K-Means聚类模型
clu.fit(data1) # 在数据上训练clu模型
label = clu.labels_ # 获取聚类模型对每个样本的簇标签
print(label)# 结果展示预期输出:平台先输出一张 “Elbow Method” 折线图(横轴 K Clusters、纵轴 SSE),随后打印 label 数组,即每只股指的簇编号(取值 0/1/2)。判读要点:观察 SSE 随 K 增大而下降的速率在何处明显放缓(拐点),以此核对 K=3 是否落在合理区间;label 中编号相同的股指被归入同一簇,可结合下文的簇均值画像判断各簇属于”高波动”组还是”低波动”组。case3.1.xlsx 数据文件本地没有、平台已内置,具体数值以平台运行结果为准。
30.6.1 方法说明
细心的读者会发现,平台代码两步使用的特征集并不相同:肘部法则循环用 data.iloc[:, 2:] 提取特征(跳过前两列),而最终 KMeans(n_clusters=3) 聚类用 data.drop('股指', axis=1) 只剔除了股指名称列,两步进入模型的列存在差异,严格比较时应先统一特征集(例如两处都用 data.drop('股指', axis=1)),再核对肘部拐点与最终分簇结论是否一致。
另外,K-Means 以欧氏距离度量样本相似度,若各特征的量纲差异较大(如百分比与绝对金额同列),量纲大的特征会主导聚类结果。距离型算法通常需要先对特征做标准化,具体做法可参见下一章(第 章节 31 章)将介绍的 KNN 案例中的 StandardScaler 步骤。
30.7 局限与注意
- 对初始质心敏感:上一节的手工迭代已经显示,初始质心放得不好时第一轮会误分(股指C 曾被误分),好在后续迭代能够自我纠正;但当不同初始化收敛到不同的局部最优时,K-Means 并不能保证找到全局最优解。sklearn 通过 k-means++ 初始化策略缓解这一问题,但
random_state取值不同,结果仍可能不同(本章平台代码即固定random_state=30)。 - 假设簇接近球形:K-Means 以欧氏距离度量相似度、以均值作质心,隐含假设各簇是凸的、大致球形且大小相近;对环形、月牙形等非凸形状的簇,或簇大小悬殊的情形,它往往会给出看似合理却错误的划分,此类数据更适合 DBSCAN 等基于密度的算法。
- 需要预先指定 K:K 不是算法学出来的,而是使用者的先验选择;肘部法、轮廓系数只能提供参考,最终还应接受业务合理性的检验。
- 量纲影响大:不同量纲的特征会让欧氏距离失衡,量纲大的特征主导聚类结果;距离型算法通常应先做标准化(参见上文”方法说明”与下一章(第 章节 31 章)将介绍的 KNN 案例中的
StandardScaler步骤)。
30.8 执行聚类分析
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# ==================== 准备聚类特征数据 ====================
data_features = data.drop('股指', axis=1) # 删除非数值列'股指',axis=1表示按列删除
# 只保留数值型特征用于聚类分析
# ==================== 执行K-Means聚类 ====================
kmeans = KMeans(n_clusters=3, random_state=30) # 创建K-Means模型,设置聚类数为3
# random_state=30设置随机种子,与之前不同会得到不同的初始质心
kmeans.fit(data_features) # 在数据上拟合K-Means模型
# ==================== 获取聚类结果 ====================
labels = kmeans.labels_ # 获取每个样本的聚类标签(0, 1, 2)
# labels_属性存储了每个数据点所属的簇编号
# ==================== 获取聚类质心 ====================
centroids = kmeans.cluster_centers_ # 获取每个簇的质心坐标
# cluster_centers_属性存储了K个质心的位置
# ==================== 将聚类标签添加到原数据 ====================
data['Cluster'] = labels # 在原数据中新增Cluster列,存储聚类结果
# 这样可以方便地按簇进行后续分析
print('聚类标签:') # 输出标题
print(labels) # 显示每个样本的聚类标签
print('\n各簇样本数:') # 输出标题
print(pd.Series(labels).value_counts().sort_index()) # 统计每个簇的样本数
# pd.Series(labels).value_counts()统计每个标签出现的次数
# sort_index()按簇编号排序,使输出更清晰
print('\n聚类质心:') # 输出标题
for i, centroid in enumerate(centroids): # 遍历每个簇的质心
print(f'\n簇{i}质心:') # 输出簇编号
for j, value in enumerate(centroid): # 遍历质心的每个维度
print(f' 特征{j+1}: {value:.4f}') # 输出每个特征维度的质心坐标,保留4位小数30.9 结果解读
# 注:该代码块依赖的数据来自上方平台任务代码块,本块承接上方平台任务的代码与数据(变量沿用平台任务)
# ==================== 按簇统计特征均值 ====================
cluster_stats = data.groupby('Cluster').mean() # 按簇分组,计算各特征的平均值
print('各簇平均特征:') # 输出标题
print(cluster_stats) # 显示每个簇的特征均值
# 这有助于理解每个簇的特点和商业含义
# ==================== 可视化聚类结果 ====================
plt.figure(figsize=(10, 6)) # 创建10x6英寸的画布
scatter = plt.scatter(
data_features.iloc[:, 0], # x轴为第1个特征
data_features.iloc[:, 1], # y轴为第2个特征
c=labels, # 颜色由聚类标签决定
cmap='viridis', # 使用viridis配色方案
s=100, # 散点大小为100
alpha=0.6 # 透明度为0.6
)
plt.colorbar(scatter, label='Cluster') # 添加颜色条,显示簇编号
plt.scatter(
centroids[:, 0], # 质心的x坐标
centroids[:, 1], # 质心的y坐标
c='red', # 质心颜色为红色
marker='X', # 质心标记为X形
s=300, # 质心大小为300
linewidths=2, # 边框线宽为2
edgecolors='black', # 边框颜色为黑色
label='质心' # 图例标签
)
plt.title('K-Means聚类结果', fontsize=14) # 设置图表标题
plt.xlabel('特征1', fontsize=12) # 设置x轴标签
plt.ylabel('特征2', fontsize=12) # 设置y轴标签
plt.legend() # 显示图例
plt.grid(True, alpha=0.3) # 显示网格线
plt.tight_layout() # 自动调整布局
plt.show() # 显示图表如何读取输出:cluster_stats 按簇列出各股指波动与收益特征的平均水平,据此可以概括每个簇的总体画像(如高波动簇、低波动簇),再结合 label 数组即可判断每只股指落入哪个簇、哪些股指被归为同类。散点图中同色点越紧凑、红色 X 质心之间相距越远,说明簇内相似度高、簇间分离度好。平台任务代码块输出的肘部图则用于确定 K:SSE 随 K 增大而单调下降,拐点(下降明显放缓)处的 K 即为较合适的簇数,本章最终按平台代码取 K=3。
30.10 应用场景
金融中的聚类应用:
- 股票分类: 根据波动率、收益率等特征分组
- 客户分层: RFM模型之外的补充
- 风险分组: 识别高风险组合
- 异常检测: 远离所有质心的点可能是异常
30.11 本章小结
要点:
- K-Means 以簇内平方和 SSE 为目标函数,通过”分配(最近质心)—更新(簇内均值)“交替迭代直至收敛;
- 质心是簇成员的均值位置,分配依据是欧氏距离,整个过程不需要任何标签,属于无监督学习;
- 肘部法利用”SSE 下降骤缓处的拐点”帮助选择 K,但只是一种参考性准则;
- 聚类结果的解读依靠簇标签、质心与簇均值画像,本章据此把股指分为高波动组与低波动组。
易错点:
- 未做标准化就直接聚类,让量纲大的特征主导了距离计算;
- 忽视平台任务中肘部图与最终聚类两步特征集不一致的问题(见”方法说明”);
- 把某个肘部位置的 K 当成唯一正确答案,或把
random_state不同的运行结果差异当作代码错误; - 忘记聚类标签的编号本身没有含义(0/1/2 可任意置换),有意义的只是”谁与谁同簇”。
30.12 动手与思考
以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。
输出预测:把
lst-ch30-manual-iteration中两个初始质心都改为右上角的 \((8.0, 9.0)\) 与 \((8.5, 8.6)\),先预测第一轮分组(哪些股指会被误分?算法最终能否收敛到与正文相同的分组?),再修改代码运行验证。参考答案(先写下你的预测再点开)
解题思路:逐轮推演“分配—更新”过程。第一轮:股指D 到质心0 的距离恰为 0,被分入簇0;股指A/B/C 虽远离两个质心,但到质心0 的距离(9.90、9.70、9.07)都略小于到质心1 的距离(9.99、9.76、9.12),也全部进入簇0——误分的是股指A/B/C/D 这一整组,簇0 变成 {股指A, 股指B, 股指C, 股指D};股指E 到质心1 距离为 0、股指F 到质心1(0.94)小于到质心0(1.08),簇1 只含 {股指E, 股指F}。更新后簇0 质心被四个成员拉到 (3.12, 3.75)。第二轮:股指D 到簇1 新质心 (8.75, 9.00) 的距离只有 0.75,远小于到簇0 质心的 7.16,立即被纠正回右上簇。第三轮分组与第二轮完全一致、质心不再移动,算法收敛,最终仍得到 {股指A, 股指B, 股指C} 与 {股指D, 股指E, 股指F} 两组,与正文结果相同——这正印证了“局限与注意”第一条:初始质心放得不好时第一轮会误分,但本例中后续迭代能够自我纠正。
# 验证脚本:两个初始质心都放到右上角后重跑手工迭代 import numpy as np # 导入NumPy数值计算库 points = np.array([[1.0, 2.0], [1.5, 1.8], [2.0, 2.2], [8.0, 9.0], [8.5, 8.6], [9.0, 9.4]]) # 6只股指的(平均收益率%,波动率%)特征 names = ['股指A', '股指B', '股指C', '股指D', '股指E', '股指F'] # 给每个样本命名 centroids = np.array([[8.0, 9.0], [8.5, 8.6]]) # 两个初始质心都改到右上角 def one_round(points, names, centroids, round_id): # 定义函数:完成一轮"分配-更新" dist = np.array([[np.linalg.norm(p - c) for c in centroids] for p in points]) # 距离矩阵:各样本到两质心的欧氏距离 labels = dist.argmin(axis=1) # 分配步骤:每个样本归入距离最近的质心所在簇 print('第{}轮分组:'.format(round_id), [names[i] + '->簇' + str(labels[i]) for i in range(len(names))]) # 打印本轮分组 new_centroids = np.array([points[labels == k].mean(axis=0) for k in (0, 1)]) # 更新步骤:取各簇成员均值作新质心 print(' 新质心: 簇0=({:.2f}, {:.2f}), 簇1=({:.2f}, {:.2f})'.format(*new_centroids[0], *new_centroids[1])) # 打印新质心 return labels, new_centroids # 返回本轮分组与新质心 labels1, centroids = one_round(points, names, centroids, 1) # 第一轮:分配后更新质心 labels2, centroids = one_round(points, names, centroids, 2) # 第二轮:用新质心重新分配再更新 labels3, centroids = one_round(points, names, centroids, 3) # 第三轮:再次重复,检验是否收敛 print('第三轮与第二轮分组是否一致:', (labels2 == labels3).all()) # 一致说明分组不再变化,算法收敛预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
第1轮分组: ['股指A->簇0', '股指B->簇0', '股指C->簇0', '股指D->簇0', '股指E->簇1', '股指F->簇1'] 新质心: 簇0=(3.12, 3.75), 簇1=(8.75, 9.00) 第2轮分组: ['股指A->簇0', '股指B->簇0', '股指C->簇0', '股指D->簇1', '股指E->簇1', '股指F->簇1'] 新质心: 簇0=(1.50, 2.00), 簇1=(8.50, 9.00) 第3轮分组: ['股指A->簇0', '股指B->簇0', '股指C->簇0', '股指D->簇1', '股指E->簇1', '股指F->簇1'] 新质心: 簇0=(1.50, 2.00), 簇1=(8.50, 9.00) 第三轮与第二轮分组是否一致: True回扣本章:对应本章小结“要点”第 1 条(“分配—更新”交替迭代直至收敛)与“易错点”第 3 条——初始质心不同带来的首轮分组差异属正常现象,算法能自我纠正时应关注收敛后的分组,而非第一轮的误分。
概念辨析:K-Means 中的”质心”与”簇”分别指什么?“分配”与”更新”两步各依据什么准则?为什么说 K-Means 是无监督学习,而下一章(第 章节 31 章)将介绍的 KNN 是监督学习?
参考答案(点开前请先独立完成)
解题思路:逐点作答。第一,“簇”是按相似性归并起来的样本子集,同一簇内的股指被认为波动与收益特征相近;“质心”是每个簇全体成员在各特征上的均值坐标,即簇的中心代表点,注意它由均值算出,不一定是某个真实样本。第二,“分配”依据最近距离准则:计算每个样本到各质心的欧氏距离,把样本归入距离最近的质心所在簇;“更新”依据簇内均值准则:把质心移动到本簇全体成员的均值位置,两步交替直至分组不再变化。第三,K-Means 全程不需要任何标签——输入只有特征矩阵,算法自行从数据的距离结构中发现分组,因此是无监督学习;而下一章(第 章节 31 章)将介绍的 KNN 需要带类别标签的训练样本(如“是否购买”),预测时由 K 个近邻按已知标签投票,标签直接决定输出,因此是监督学习。由此也可分清两个“K”的含义:K-Means 的 K 是簇的个数,KNN 的 K 是参与投票的邻居数。
回扣本章:对应本章小结“要点”第 1、2 条——质心即簇内均值、分配依据欧氏距离、更新依据取均值,且整个过程不需要任何标签。
变式任务:在手工迭代数据中新增一个点 \((5.0, 5.5)\),重复三轮迭代,观察它最终落入哪个簇、需要几轮才收敛;进一步思考:若新增点恰好位于两质心连线的中点附近,分组结果对初始质心的位置会有多敏感?
参考答案(点开前请先独立完成)
解题思路:新增点 G=(5.0, 5.5) 位于两组之间偏右上的位置。第一轮沿用正文初始质心 \((\mu_0=(1.0, 2.0))\) 与 \((\mu_1=(2.5, 2.5))\):G 到质心1 的距离 3.91 小于到质心0 的 5.32,与股指C/D/E/F 同入簇1,簇1 质心被拉到 (6.50, 6.94);第二轮股指C 回到簇0,G 仍留在簇1(到簇1 新质心仅 2.08,远小于到簇0 的 5.20),簇1 质心移到 (7.62, 8.12);第三轮分组与第二轮一致,算法收敛。结论:G 最终落入“高收益高波动”的簇1,与正文一样三轮收敛。进一步思考:若新增点恰在两质心(连线)中点附近,它到两个质心的距离几乎相等,落在两簇分界的“边界带”上——初始质心稍作移动、或迭代路径稍有不同,它就可能被分到另一侧,甚至带动邻近样本一起换簇;边界点正是聚类结果对初始质心最敏感的位置,实务中可通过更换
random_state多次聚类、检查该点簇归属是否稳定来评估分组可信度。# 变式脚本:新增点(5.0, 5.5)后重跑三轮手工迭代 import numpy as np # 导入NumPy数值计算库 points = np.array([[1.0, 2.0], [1.5, 1.8], [2.0, 2.2], [8.0, 9.0], [8.5, 8.6], [9.0, 9.4], [5.0, 5.5]]) # 6只股指特征加新增点G names = ['股指A', '股指B', '股指C', '股指D', '股指E', '股指F', '新增点G'] # 给每个样本命名 centroids = np.array([[1.0, 2.0], [2.5, 2.5]]) # 沿用正文的两个初始质心 def one_round(points, names, centroids, round_id): # 定义函数:完成一轮"分配-更新" dist = np.array([[np.linalg.norm(p - c) for c in centroids] for p in points]) # 距离矩阵:各样本到两质心的欧氏距离 labels = dist.argmin(axis=1) # 分配步骤:归入距离最近的质心所在簇 print('第{}轮分组:'.format(round_id), [names[i] + '->簇' + str(labels[i]) for i in range(len(names))]) # 打印本轮分组 new_centroids = np.array([points[labels == k].mean(axis=0) for k in (0, 1)]) # 更新步骤:取各簇成员均值作新质心 print(' 新质心: 簇0=({:.2f}, {:.2f}), 簇1=({:.2f}, {:.2f})'.format(*new_centroids[0], *new_centroids[1])) # 打印新质心 return labels, new_centroids # 返回本轮分组与新质心 labels1, centroids = one_round(points, names, centroids, 1) # 第一轮:分配后更新质心 labels2, centroids = one_round(points, names, centroids, 2) # 第二轮:用新质心重新分配再更新 labels3, centroids = one_round(points, names, centroids, 3) # 第三轮:再次重复,检验是否收敛 print('第三轮与第二轮分组是否一致:', (labels2 == labels3).all()) # 一致说明分组不再变化,算法收敛 print('新增点G最终所在簇: 簇{}'.format(labels3[-1])) # 输出新增点的簇归属预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):
第1轮分组: ['股指A->簇0', '股指B->簇0', '股指C->簇1', '股指D->簇1', '股指E->簇1', '股指F->簇1', '新增点G->簇1'] 新质心: 簇0=(1.25, 1.90), 簇1=(6.50, 6.94) 第2轮分组: ['股指A->簇0', '股指B->簇0', '股指C->簇0', '股指D->簇1', '股指E->簇1', '股指F->簇1', '新增点G->簇1'] 新质心: 簇0=(1.50, 2.00), 簇1=(7.62, 8.12) 第3轮分组: ['股指A->簇0', '股指B->簇0', '股指C->簇0', '股指D->簇1', '股指E->簇1', '股指F->簇1', '新增点G->簇1'] 新质心: 簇0=(1.50, 2.00), 簇1=(7.62, 8.12) 第三轮与第二轮分组是否一致: True 新增点G最终所在簇: 簇1注意:以上为本题变式的独立代码;列表 30.1 对应正文的演示代码与 列表 30.2 对应的平台原始代码块均保持原样,不要用本变式替换。
回扣本章:对应本章小结“易错点”第 3 条——分组对初始质心的敏感要通过多起点重跑来检验,而非当作代码错误,以及“局限与注意”第一条关于初始质心敏感性的讨论。
思考题:本章平台任务在肘部图与最终聚类两步使用了不同的特征列(见”方法说明”)。若让你统一特征集后重跑,你会如何设计对照实验,检验”K=3、高低波动分组”的结论是否稳健?
参考答案(点开前请先独立完成)
解题思路:改造思路——只动“特征集”这一处口径,其余保持不变。第一步,把肘部循环里的
data.iloc[:, 2:]改成与最终聚类一致的data.drop('股指', axis=1),使两步进入模型的列完全相同;第二步,固定random_state(如沿用平台的 30),保证对照结果只反映特征集差异、不混入随机性;第三步,设计 2×2 对照组合:{特征集不一致(原平台代码),特征集统一} × {random_state=30,再另取 1—2 个种子},对每种组合记录三样东西——肘部图中 SSE 下降明显放缓的位置、K=3 时的簇标签分配、各簇特征均值画像(哪个簇是高波动组、哪个是低波动组)。结构性判读——若统一特征集后肘部拐点仍落在 K=3 附近、簇标签只发生编号置换或个别边界样本换簇、各簇“高/低波动”的画像方向不变,则“K=3、高低波动分组”的结论稳健;若拐点明显移到 K=2 或 K=4,或某些股指在两种口径下分属高、低波动两组,则说明结论部分依赖特征集的选择,须回到业务含义重新斟酌。判读时还应记得“局限与注意”的提醒:不同种子收敛到不同局部最优属正常现象,应关注分组结构的稳定性而非簇编号本身。预期输出:数据文件由教学平台内置(case3.1.xlsx 本地没有),本机无法复现,具体数值以平台运行结果为准。
注意:以上为对照实验的设计思路;列表 30.2 对应的平台原始代码块仍须原样输入教学平台,不要用本变式替换。
回扣本章:对应本章小结“易错点”第 2 条(肘部图与最终聚类两步特征集不一致)与第 3 条(不把单一口径下的 K 或分组当成唯一正确答案)。