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聚类。到时候你就知道,理论归理论,实战才是真功夫。


专注资料整理