跳转至

11.3   聚类分析

社区活动室里,小率把同学们的兴趣问卷贴满桌面:有人爱运动,有人爱熬夜打游戏,有人每天阅读很久。没有任何“标准答案”,但他想把相似的人自然分成几组。

图 11.3.1 小率把问卷卡片按相似性分堆

没有标签,也没人告诉我谁属于哪组,这还能分析吗?
能。聚类就是在无标签数据里,让相似对象靠在一起。

11.3.1   聚类先定义什么叫相似

聚类分析(Cluster Analysis)要解决的问题是:把样本分成若干组,让组内尽量相似,组间尽量不同。

图 11.3.2 三种常见聚类思路

最先要定的是距离:

距离 适合数据 直觉
欧氏距离 连续变量、球形簇 两点直线距离
曼哈顿距离 高维、偏离群 沿网格走路
余弦距离 文本、方向相似 看方向不看长度
Jaccard 距离 标签集合 看共同元素比例

聚类前先标准化

如果一个变量是“月消费金额”,另一个变量是“登录天数”,不标准化会让金额主导距离,聚类结果几乎变成按钱分组。

11.3.2   KMeans:先定中心再抢点

KMeans 假设簇大致像圆球。它先放 \(K\) 个中心,再重复两步:

  1. 每个点归到最近的中心。
  2. 每个中心移动到本组点的均值。

目标函数是最小化簇内平方和:

\[ \min \sum_{k=1}^{K}\sum_{x_i\in C_k}\|x_i-\mu_k\|^2 \]
所以 KMeans 喜欢“圆圆的一团”,不太会处理月牙形?
正是。算法的偏好就是它的边界。

11.3.3   层次聚类:把分组画成一棵树

层次聚类(Hierarchical Clustering)从每个点自己一组开始,逐步合并最近的组,最后得到一棵树状图。你可以在树上切一刀,得到任意数量的簇。

常见 linkage:

  • ward:合并后组内方差增加最小,常用。
  • complete:看两组之间最远点。
  • average:看平均距离。
  • single:看最近点,容易产生长链。

什么时候用

样本量不大、想看层级结构时,层次聚类很适合。样本几十万时,它通常太慢。

11.3.4   DBSCAN:哪里点密,哪里就是一群

DBSCAN 根据密度找簇。它需要两个参数:

  • \(\varepsilon\):邻域半径。
  • min_samples:邻域内至少多少个点才算密集。

点分三类:

  • 核心点:邻域里点足够多。
  • 边界点:靠近核心点,但自己不够密。
  • 噪声点:不属于任何密集区域。

DBSCAN 的好处是能找到弯曲形状,还能标记噪声;缺点是参数敏感,而且不同密度的簇不好同时处理。

11.3.5   怎么选 K

KMeans 必须先给 \(K\)。常用方法:

方法 看什么 局限
肘部法 簇内平方和下降是否变慢 肘部不一定明显
轮廓系数 组内近、组间远的综合分 高维时可能不稳定
业务解释 分出的组是否能命名 需要领域判断

轮廓系数(Silhouette)范围是 \([-1,1]\),越大通常越好。

如果轮廓系数高,但分出来的组讲不出意义呢?
那仍然不能用。聚类最后要回到人能理解的分组。

11.3.6   用 Python 比较三种聚类

完整脚本放在:

docs/assets/scripts/ch11_multivariate/03_cluster_analysis/main.py
from sklearn.cluster import KMeans, AgglomerativeClustering, DBSCAN
from sklearn.datasets import make_blobs
from sklearn.metrics import silhouette_score
from sklearn.preprocessing import StandardScaler

X, _ = make_blobs(n_samples=300, centers=3, random_state=42)
X = StandardScaler().fit_transform(X)

kmeans = KMeans(n_clusters=3, n_init=10, random_state=42).fit(X)
hier = AgglomerativeClustering(n_clusters=3, linkage="ward").fit(X)
db = DBSCAN(eps=0.45, min_samples=5).fit(X)

for name, labels in [
    ("KMeans", kmeans.labels_),
    ("层次聚类", hier.labels_),
    ("DBSCAN", db.labels_),
]:
    valid = labels != -1
    score = silhouette_score(X[valid], labels[valid]) if len(set(labels[valid])) > 1 else float("nan")
    print(name, "簇标签:", sorted(set(labels)), "轮廓系数:", round(score, 3))

小率的笔记本

聚类没有标准答案。先标准化,再定义距离,再选算法。KMeans 快但偏爱球形簇;层次聚类能看嵌套结构但慢;DBSCAN 能找任意形状和噪声,但参数敏感。最后一定要检查分组能不能解释。