4. K-Means聚类算法原理
说实话,K-Means是我入行量化时用的第一个聚类算法。那时候刚接触股票分类,觉得这东西太神奇了——一堆股票数据扔进去,它自己就能分出几类来。后来踩了不少坑,才真正理解它背后的门道。
今天咱们就把K-Means彻底聊透。从核心思想到数学原理,从K值选择到实战避坑,我把自己这些年积累的经验都摊开来讲。
4.1 算法核心思想:说白了就是找“中心”
K-Means的思想其实特别朴素。你想想看,如果让你把一堆点分成K堆,你会怎么分?
正常人都会这么做:先随便选K个点当“老大”,然后其他点看谁离得近就跟谁。接着每个“帮派”重新选个中心点,再重新分配成员。反复几次,直到稳定下来。
嗯,K-Means就是这么干的。
核心逻辑一句话: 找到K个聚类中心,让每个样本点到它所属中心点的距离之和最小。
我在做A股风格分类时,就用这个逻辑把3000多只股票按市盈率、市净率、换手率等指标分了类。效果嘛,至少比拍脑袋分要靠谱得多。
4.2 数学原理:其实就两个步骤反复迭代
K-Means的数学表达不复杂,我尽量用大白话讲清楚。
假设我们有N个样本点,每个点有d个特征。我们要把它们分成K个簇。算法就干两件事:
4.2.1 第一步:分配样本(E步)
对于每个样本点,计算它到K个中心点的距离,把它分给最近的那个中心。
数学上这么写:
对于样本 x_i,找到最近的簇中心 c_j:
argmin_j ||x_i - c_j||²
说白了就是算欧氏距离,谁近跟谁走。
4.2.2 第二步:更新中心(M步)
每个簇有了新成员后,重新算一下这个簇的中心点。怎么算?取平均值。
c_j = (1/|S_j|) * Σ x_i (x_i ∈ S_j)
其中S_j是第j个簇的所有样本,|S_j|是样本个数。
就这么两步,反复迭代,直到中心点不再变化,或者变化很小了,算法就收敛了。
我的一点经验: 实际项目中,我一般设置最大迭代次数300次,收敛阈值1e-4。别设太小,否则可能死循环。我曾经有一次忘了设最大迭代次数,结果跑了半小时还没停...
4.3 K值选择方法:最头疼的问题
K-Means有个硬伤——你得事先告诉它要分几类。这在股票分类里特别要命,因为鬼才知道市场到底该分几类。
我常用的方法有两个,各有千秋。
4.3.1 肘部法则(Elbow Method)
这个思路很直观:随着K增大,每个簇内的样本离中心点更近,误差自然越来越小。但K大到一定程度后,误差下降的速度会变慢。
那个“拐点”就是最佳K值。
# 肘部法则的简单实现
import numpy as np
from sklearn.cluster import KMeans
import matplotlib.pyplot as plt
def elbow_method(X, max_k=10):
inertias = []
for k in range(1, max_k+1):
kmeans = KMeans(n_clusters=k, random_state=42)
kmeans.fit(X)
inertias.append(kmeans.inertia_)
# 画图看拐点
plt.plot(range(1, max_k+1), inertias, 'bo-')
plt.xlabel('K值')
plt.ylabel('簇内误差平方和')
plt.title('肘部法则确定最佳K值')
plt.show()
注意: 肘部法则不是万能的。我遇到过很多次,曲线平滑得像条直线,根本找不到拐点。这时候就别硬找了,换个方法吧。
4.3.2 轮廓系数(Silhouette Coefficient)
这个方法我更喜欢,因为它综合考虑了“簇内紧密度”和“簇间分离度”。
每个样本的轮廓系数计算公式:
s(i) = (b(i) - a(i)) / max(a(i), b(i))
其中:
a(i) = 样本i到同簇其他样本的平均距离(簇内不相似度)
b(i) = 样本i到其他簇样本的最小平均距离(簇间不相似度)
轮廓系数的范围是[-1, 1]。越接近1,说明分类越合理;接近0,说明样本在两个簇的边界上;负数?那说明分错了。
from sklearn.metrics import silhouette_score
def silhouette_method(X, max_k=10):
scores = []
for k in range(2, max_k+1): # 轮廓系数至少需要2个簇
kmeans = KMeans(n_clusters=k, random_state=42)
labels = kmeans.fit_predict(X)
score = silhouette_score(X, labels)
scores.append(score)
best_k = np.argmax(scores) + 2
return best_k, scores
我的习惯: 两个方法一起用。肘部法则给个大致范围,轮廓系数精确定位。如果两者冲突,我倾向于相信轮廓系数。为什么?因为它在实际项目中表现更稳定,尤其当数据分布不均匀的时候。
4.4 算法优缺点:没有银弹
做量化这些年,我越来越明白一个道理:没有完美的算法,只有合适的场景。
4.4.1 优点
- 简单高效: 时间复杂度O(NKd),处理几万条数据毫无压力。我拿它分过全A股4000多只股票,几秒钟就出结果。
- 可解释性强: 每个簇的中心点就是该类股票的平均特征,一眼就能看出“高估值组”、“低波动组”之类的含义。
- 收敛速度快: 通常几十次迭代就稳定了,不像某些算法要跑几百轮。
4.4.2 缺点
- K值要事先指定: 这是最大的痛点。我刚开始做的时候,经常为选K值纠结半天。
- 对初始值敏感: 不同的初始中心点可能导致完全不同的结果。我建议用K-Means++初始化,能大大改善这个问题。
- 只能发现球形簇: 如果数据分布是长条形的、月牙形的,K-Means就抓瞎了。我在处理某些非线性相关的股票特征时就吃过这个亏。
- 对异常值敏感: 一个离群点就能把中心点拉偏。所以数据预处理特别重要,我一般会先做标准化,再剔除3倍标准差以外的异常值。
4.5 适用场景:什么时候用它?
根据我的经验,K-Means最适合以下场景:
| 场景 | 说明 | 我的案例 |
|---|---|---|
| 股票风格分类 | 按估值、成长、波动等特征分类 | 把沪深300成分股分成价值、成长、混合三类 |
| 客户分群 | 按交易行为、持仓特征分群 | 把散户分成高频交易型、价值投资型、趋势跟踪型 |
| 数据预处理 | 作为其他算法的前置步骤 | 先用K-Means降维,再训练分类模型 |
| 异常检测 | 远离所有簇中心的点可能是异常 | 识别财报数据异常的公司 |
避坑指南: 我曾经用K-Means去分类时间序列数据,结果一塌糊涂。后来才明白,时间序列有前后依赖关系,K-Means根本处理不了。如果你要处理时序数据,试试DTW距离+K-Means,或者直接用时间序列聚类算法。
4.6 本章小结
K-Means就像一把瑞士军刀——简单、实用、上手快。但别因为它简单就轻视它。我见过太多人把数据扔进去就跑,结果出来一堆垃圾还怪算法不好。
记住三点:
- 数据预处理比算法本身更重要
- K值选择要结合业务理解,别光看指标
- 结果一定要可视化,用眼睛验证比用指标靠谱
下一章咱们就实战了,我会用真实的A股数据,手把手带你跑一遍K-Means聚类。到时候你就知道,理论归理论,实战才是真功夫。