欢迎来到聚类分析的世界
想象你是腾讯视频的CEO,手握数亿用户的观影数据。
- 如何向用户推荐他们可能喜欢的电影?
- 如何为不同的市场制作迎合当地口味的原创内容?
- 如何发现新兴的、小众的观影群体并服务他们?
学习目标与开始前回顾
先写下两个判断,再揭示答案。
90 分钟学习安排
- 0–15 分钟:分类与聚类、距离及缩放;完成缩放检查。
- 15–35 分钟:质心、内部/外部指标与轮廓系数;完成指标选择检查。
- 35–60 分钟:K-Means 迭代与预设 K 规则。
- 60–82 分钟:长三角真实公司分析步骤、K=6 证据、画像与稳定性。
- 82–90 分钟:综合练习与反馈。
学习顺序
缩放 → 轮廓系数 → 真实案例 → 本讲回顾。层次聚类、DBSCAN 与 GMM 可在课后选学。
本章学习内容概览
我们将通过一次深度旅程,全面掌握聚类分析的核心思想与实践。
核心问题:企业为何要不遗余力地了解客户?
更好的策略是:识别出具有相似品味的用户群体,然后针对性地进行推荐。
聚类 vs. 分类:标签决定学习任务
在进入聚类世界前,必须厘清一个重要概念:聚类 (Clustering) 与 分类 (Classification) 的区别。
分类 (监督学习)
- 目标: 预测已知的标签。
- 输入: 带标签的数据 (e.g., 这封邮件是/不是垃圾邮件)。
- 过程: 学习从特征到标签的映射规则。
- 产出: 一个可以预测新数据标签的模型。
分类图解:由已知标签学习决策边界
聚类 (无监督学习)
- 目标: 发现数据中隐藏的群组。
- 输入: 不带标签的数据。
- 过程: 根据数据内在的相似性进行分组。
- 产出: 数据的簇划分和对这些簇的解读。
聚类:在没有标准答案的数据中寻找结构
聚类是一种经典的无监督学习 (Unsupervised Learning) 方法。
- 监督学习:数据有明确的标签(例如,邮件被标记为“垃圾邮件”或“非垃圾邮件”),我们学习如何预测这些标签。
- 无监督学习:数据没有标签。我们的目标是发现数据本身内在的、隐藏的结构或模式。
聚类的任务,就是将相似的数据点划归到同一个组(簇)中。
硬聚类的形式化定义:一次严谨的数据划分
给定一个包含 N 个样本的数据集 \(X = \{x_1, x_2, \ldots, x_N\}\),K-Means 等硬聚类把它划分为 K 个簇 \(\{C_1, C_2, \ldots, C_K\}\)。
以下三个性质只定义硬划分:DBSCAN 还允许噪声集 \(C_0\),GMM 则用责任度 \(r_{nk}\in[0,1]\) 表示软归属,均不能被这一定义一概而论。
划分性质 (1/3): 非空性 (Non-empty)
每个被定义的簇,都必须真实地包含至少一个数据点。我们不做没有意义的划分。
\[ \large{\forall k \in \{1, \dots, K\}, C_k \neq \emptyset} \]
划分性质 (2/3): 不相交 (Mutually Exclusive)
每个数据点只能属于唯一的一个簇,不能脚踏两只船。
\[ \large{\forall k \neq l, C_k \cap C_l = \emptyset} \]
划分性质 (3/3): 全覆盖 (Exhaustive)
数据集中的每一个数据点都必须被划分到某个簇中,不能有“漏网之鱼”。
\[ \large{\bigcup_{k=1}^{K} C_k = X} \]
聚类的直观理解:物以类聚,人以群分
聚类的核心思想非常直观:
簇内样本应尽可能“相似”,而簇间样本应尽可能“不相似”。
聚类的两大核心构件
要实现“物以类聚”,我们必须首先解决两个根本性问题:
构件一:如何用数学语言描述“相似”?
在聚类算法中,“不相似”的程度通常由“距离”来衡量。距离越远,越不相似。
一个有效的距离测度 \(DM(x, y)\) 必须满足以下四个性质:
| 非负性 |
\(DM(x, y) \geq 0\) |
两个客户之间的差异度不能是负数。 |
| 同一性 |
\(DM(x, y) = 0 \iff x=y\) |
距离为零当且仅当两个客户的特征向量相同。 |
| 对称性 |
\(DM(x, y) = DM(y, x)\) |
客户A到B的差异度等于B到A的差异度。 |
| 三角不等式 |
\(DM(x, z) \leq DM(x, y) + DM(y, z)\) |
经由第三者引入的间接差异,不会小于直接差异。 |
常用距离测度(1): 欧氏距离
这是最常用、最直观的距离定义,即空间中两点间的直线距离。
对于两个 d 维向量 \(x=(x_1, \dots, x_d)\) 和 \(y=(y_1, \dots, y_d)\),其欧氏距离为:
\[ \large{DM_{euc}(x, y) = \sqrt{\sum_{i=1}^{d} (x_i - y_i)^2}} \]
应用场景
当数据的各个维度是可比较的,并且我们关心绝对的物理距离时,欧氏距离是首选。
常用距离测度(2): 曼哈顿距离
也称为“城市街区距离”,计算的是两点在标准坐标系上的绝对轴距总和。
\[ \large{DM_{man}(x, y) = \sum_{i=1}^{d} |x_i - y_i|} \]
应用场景
距离测度可视化:欧氏 vs. 曼哈顿
关键实践:特征缩放
问题: 如果不进行缩放,收入维度上100美元的差异,其平方将远大于年龄维度上5岁的差异,导致聚类结果完全被收入主导。
解决方案: 使用 StandardScaler 等工具将所有特征缩放到相似的尺度上(例如,均值为0,方差为1)。
检查 1: 若把“市值”从亿元改成元,但其他字段不变,未缩放 K-Means 的结果应不变吗?请先判断。
- 揭示: 不应期待不变;欧氏距离随单位改变。答“不变”时回到距离公式,把该维差值乘以 \(10^8\) 再比较。
特征缩放的影响
如果不进行缩放,距离计算会被数值范围大的特征“绑架”。
构件二:簇如何获得代表或由密度连通?
K-Means 显式使用质心;密度聚类不必定义单一中心,而是由局部密度与连通关系形成簇。
1. 基于均值的簇中心
- 代表算法: K-Means
- 定义: 簇内所有样本点的算术平均值。
- 公式: \[ \large{\mu_k = \frac{1}{|C_k|} \sum_{x_n \in C_k} x_n} \]
- 特点: 直观,但对异常值 (outliers) 敏感。
2. 基于密度连通的簇
- 代表算法: DBSCAN
- 定义: 由 \(\varepsilon\) 邻域内至少
min_samples 个点的核心点出发,以密度可达与密度相连扩展簇。
- 归属: 区分核心点、边界点与噪声点;DBSCAN 没有唯一“簇中心”。
- 对照: “高密度且远离其他高密度点”的中心判据属于 CFSFDP 等密度峰值方法。
K-Means 质心与 DBSCAN 密度连通
评估聚类效果:我们的分组真的有意义吗?
聚类需要一套客观的指标来评估其效果。评估方法分为两大类:
外部指标 (External Measures)
- 前提: 拥有“真实”的分类标签(仅用于评估)。
- 目标: 衡量聚类结果与真实标签的一致性。
- 代表指标:
- Purity (纯度)
- Entropy (熵)
- Homogeneity (同质性)
- Completeness (完整性)
内部指标 (Internal Measures)
- 前提: 没有真实标签。
- 目标: 仅根据数据本身的特征来评估聚类的质量。
- 核心思想: 簇内是否足够紧凑,簇间是否足够分离。
- 代表指标:
- Silhouette Coefficient (轮廓系数)
外部指标(1): 纯度 (Purity)
- 对于每个聚类簇 \(C_k\),找到其中占比最高的真实类别 \(L_s\)。
- 计算该类别样本数 \(|C_k \cap L_s|\) 占簇内总样本数 \(|C_k|\) 的比例。
- 整体的Purity是所有簇的加权平均。
\[ \large{\text{Purity} = \sum_{k=1}^{K} \frac{|C_k|}{N} \max_s \left( \frac{|C_k \cap L_s|}{|C_k|} \right)} \]
- 优点: 简单直观。限制:簇越碎,Purity 越可能虚高;一点一簇会得到 1,所以还要约束 K 或报告内部指标。
纯度计算示例
假设我们有10个样本,真实类别是5个菱形和5个六边形。聚类算法将其分为两个簇 K1 和 K2。
| 簇 K1 |
5 (主导) |
1 |
| 簇 K2 |
0 |
4 (主导) |
计算 K1 的纯度: \(Purity(K_1) = \max(\frac{5}{6}, \frac{1}{6}) = \frac{5}{6}\)
计算 K2 的纯度: \(Purity(K_2) = \max(\frac{0}{4}, \frac{4}{4}) = \frac{4}{4} = 1\)
计算整体纯度 (加权平均):
\[
\large{\begin{aligned}
Purity_{total} &= \frac{|K1|}{N} Purity(K_1) + \frac{|K_2|}{N} Purity(K_2) \\
&= \frac{6}{10}\frac{5}{6} + \frac{4}{10} \times 1 = 0.9
\end{aligned}}
\]
外部指标(2): 熵 (Entropy)
思想
衡量一个簇内部的“混乱”程度。如果一个簇里各种真实类别的样本混杂在一起,它的熵就高;反之,如果它很“纯”,熵就低。
计算方法: 对于簇 \(C_k\),其熵的计算公式为:
\[ \large{\text{Entropy}(C_k) = - \sum_{s=1}^{S} p_{ks} \log_2(p_{ks})}
\]
其中,\(p_{ks} = \frac{|C_k \cap L_s|}{|C_k|}\) 是真实类别 \(s\) 在簇 \(k\) 中出现的概率。
熵计算示例
我们继续使用上个例子中的簇 K1 来计算它的熵。
K1 中有6个样本。
- 属于“菱形”类别的概率是 \(p_{菱形} = 5/6\)。
- 属于“六边形”类别的概率是 \(p_{六边形} = 1/6\)。
\[ \large{\text{Entropy}(K_1) = - \left( \frac{5}{6} \log_2\left(\frac{5}{6}\right) + \frac{1}{6} \log_2\left(\frac{1}{6}\right) \right) \approx 0.65}
\]
这个值越接近0,说明簇越纯净。而 K2 中只有六边形,所以 \(p_{六边形} = 1\), \(\text{Entropy}(K_2) = -1 \log_2(1) = 0\),是完全纯净的。
外部指标(3): 同质性 (Homogeneity) & 完整性 (Completeness)
Purity和Entropy都只看了一方面。同质性和完整性提供了一个更全面的视角。
- 同质性 (Homogeneity):
- 完整性 (Completeness):
这两个指标常常是相互制约的。
同质性 vs. 完整性:一个直观的例子
假设真实情况是 {3个苹果, 3个梨}。
完美聚类
- 簇1: {🍎, 🍎, 🍎}
- 簇2: {🍐, 🍐, 🍐}
- 同质性 = 1
- 完整性 = 1
高同质性, 低完整性
- 簇1: {🍎}
- 簇2: {🍎, 🍎}
- 簇3: {🍐, 🍐, 🍐}
- 同质性 = 1 (每个簇都很纯)
- 完整性 < 1 (苹果被分开了)
低同质性, 高完整性
- 簇1: {🍎, 🍎, 🍎, 🍐, 🍐, 🍐}
- 同质性 < 1 (簇1不纯)
- 完整性 = 1 (同类样本都在一起)
内部指标: 轮廓系数 (Silhouette Coefficient)
没有真实标签时,轮廓系数同时衡量簇内凝聚与簇间分离。
思想:每个点应 靠近本簇、远离最近的其他簇。
计算方法:对于样本 \(x_n\)
- \(a(x_n)\): \(x_n\) 到其所在簇内所有其他点的平均距离(衡量簇内凝聚度)。
- \(b(x_n)\): \(x_n\) 到最近的那个其他簇的所有点的平均距离(衡量簇间分离度)。
\[
SC(x_n) = \frac{b(x_n) - a(x_n)}{\max\{a(x_n), b(x_n)\}}
\]
核心内容数值检查:手算一个轮廓系数
- 先算: 若某点的簇内平均距离 \(a(x_n)=2\),最近其他簇平均距离 \(b(x_n)=5\),则 \(SC(x_n)\) 是多少?结果更接近 1、0 还是 -1?
轮廓系数的计算可视化
解读轮廓系数
轮廓系数 \(SC(x_n)\) 的取值范围在 [-1, 1] 之间。
- SC ≈ 1: 非常好!说明 \(a(x_n)\) 远小于 \(b(x_n)\),该点与簇内点很近,与簇外点很远。
- SC ≈ 0: 说明点可能位于两个簇的边界上。
- SC ≈ -1: 非常差!说明该点可能被分到了错误的簇。
我们可以计算所有数据点的平均轮廓系数,作为整个聚类结果的评价指标。
算法一:K-均值聚类 (K-Means Clustering)
K-Means 是最著名、最广泛使用的聚类算法之一。
- 核心思想: 通过迭代,将数据划分为 K 个簇,使得每个数据点都属于离它最近的那个簇中心,并且每个簇的簇内平方和(Inertia)最小。
- 优点: 算法简单,计算速度快,在处理球状、大小相似的簇时效果极佳。
- 缺点: 需要事先选择簇的数量 K,对初始中心点敏感,对非球状簇和异常值效果不佳。
K-Means 的目标函数
K-Means 的目标是最小化所有簇的簇内误差平方和 (Sum of Squared Errors, SSE),也称为惯性 (Inertia)。
\[ \large{\text{minimize} \sum_{k=1}^{K} \sum_{x_n \in C_k} ||x_n - \mu_k||^2} \]
其中:
- \(K\) 是簇的数量。
- \(C_k\) 是第 \(k\) 个簇。
- \(\mu_k\) 是第 \(k\) 个簇的中心点。
- \(||x_n - \mu_k||^2\) 是数据点 \(x_n\) 到其所属簇中心的欧氏距离的平方。
算法的每一步(分配和更新)都是为了让这个总误差值变得更小。
K-Means 算法的迭代过程
K-Means的算法过程就像一场“抢地盘”的游戏,分四步循环进行:
- 初始化 (Initialize): 随机选择 K 个数据点作为初始的簇中心。
- 分配 (Assign): 对于每一个数据点,计算它到 K 个簇中心的距离,并将其分配给距离最近的那个簇。
- 更新 (Update): 对于每一个簇,重新计算其中心点(即该簇所有数据点的均值)。
- 重复 (Repeat): 重复步骤2和3,直到簇中心不再发生显著变化,或者数据点的分配不再改变。
K-Means 步骤可视化 (1/4): 初始化
假设 K=3。我们随机在数据空间中选择三个点作为初始质心。
K-Means 步骤可视化 (2/4): 分配
根据每个点到这三个初始中心的距离,将它们涂上最近中心的颜色。
K-Means 步骤可视化 (3/4): 更新
对每个颜色的簇,计算其所有点的均值,并将簇中心移动到这个新位置。
K-Means 步骤可视化 (4/4): 收敛
重复“分配-更新”步骤,直到簇中心不再移动。此时,我们得到了最终的聚类结果。
K-Means 初始化的阿喀琉斯之踵
随机初始化可能会导致次优的聚类结果,甚至完全错误的结果。
- 问题: 如果初始中心点选得不好(例如,都选在同一个簇里),算法可能会陷入一个局部最优解,而无法找到全局最优解。
- 解决方案: K-Means++
- 第一个中心均匀随机选择;以后每个样本按其到最近已选中心的平方距离 \(D(x)^2\) 成比例抽样,而不是确定性地选“最远点”。
- 这通常改善初始覆盖,但不保证 Lloyd 迭代找到全局最优。实践中仍应使用多个随机重启并报告稳定性。
K-Means++ 初始化策略
实践中最关键的问题:如何选择 K 值?
K-Means 算法本身无法告诉我们最佳的 K 是多少。这是一个需要我们利用领域知识和数据洞察来决定的超参数。
幸运的是,我们有两种常用的启发式方法来辅助决策:
- 肘部法则 (Elbow Method)
- 轮廓分析 (Silhouette Analysis)
选择 K:规则必须先于结果
Inertia(簇内平方和):随 K 增大单调下降,只用于识别边际改善是否明显放缓;它不能单独“证明”某个 K。
平均轮廓系数:同时考虑簇内凝聚与簇间分离;本讲预先规定在 \(K=2,\ldots,10\) 中选择其最大值。
稳定性:在另一时期或重抽样中检查公司配对是否仍相近;簇编号可置换,不能按编号比较。
下一节不展示任何手绘 K 值结论,而让同一条真实数据分析步骤生成 inertia、silhouette、最终 K、公司散点、质心和画像。
轮廓系数的定义与解释参见 Rousseeuw (1987)。
案例数据:长三角估值字段
Code
from pathlib import Path # 使用路径对象声明本地数据来源
import numpy as np # 提供对数变换与分位数裁剪
import pandas as pd # 读取并合并公司基础信息与估值因子
# 公网下载:https://assets.qiufei.site/data/stock/valuation_factors_quarterly_15_years.h5
# 下载后把下一行改为本机文件位置;按课程结构存放时可用 Path("data/stock/valuation_factors_quarterly_15_years.h5")。
# Windows:Path(r"C:\qiufei\data\stock\valuation_factors_quarterly_15_years.h5")
# macOS:Path("/Users/你的用户名/data/stock/valuation_factors_quarterly_15_years.h5")
# Linux:Path("/home/你的用户名/data/stock/valuation_factors_quarterly_15_years.h5")
valuation_path = Path("/home/ubuntu/r2_data_mount/data/stock/valuation_factors_quarterly_15_years.h5")
# 公网下载:https://assets.qiufei.site/data/stock/stock_basic_data.h5
# 下载后把下一行改为本机文件位置;按课程结构存放时可用 Path("data/stock/stock_basic_data.h5")。
# Windows:Path(r"C:\qiufei\data\stock\stock_basic_data.h5")
# macOS:Path("/Users/你的用户名/data/stock/stock_basic_data.h5")
# Linux:Path("/home/你的用户名/data/stock/stock_basic_data.h5")
basic_path = Path("/home/ubuntu/r2_data_mount/data/stock/stock_basic_data.h5")
valuation_data = pd.read_hdf(valuation_path, key='valuation_factors').reset_index() # 读取真实季度估值因子
basic_data = pd.read_hdf(basic_path, key='stock_basic_info') # 读取公司地区与行业信息
valuation_data['date'] = pd.to_datetime(valuation_data['date']) # 将季度索引转换为日期
snapshot_date = valuation_data.loc[valuation_data['date'] <= '2024-12-31', 'date'].max() # 选择不晚于截止日的最新可用季度
yangtze_provinces = ['上海市', '江苏省', '浙江省', '安徽省'] # 明确长三角筛选范围
company_filter = basic_data['province'].isin(yangtze_provinces) & basic_data['sector_code_name'].ne('金融') # 排除金融行业以增强可比性
company_columns = ['order_book_id', 'symbol', 'province', 'industry_name'] # 保留画像所需公司字段
snapshot = valuation_data.loc[valuation_data['date'].eq(snapshot_date)].merge(basic_data.loc[company_filter, company_columns], on='order_book_id') # 合并同季度公司信息
cluster_features = ['pe_ratio_ttm', 'pb_ratio_lf', 'dividend_yield_ttm', 'market_cap'] # 定义四个具有真实语义的聚类字段
analysis_frame = snapshot.dropna(subset=cluster_features).copy() # 删除关键字段缺失观测
analysis_frame['log_market_cap'] = np.log(analysis_frame['market_cap'].clip(lower=1)) # 对市值取对数降低偏态
cluster_features = ['pe_ratio_ttm', 'pb_ratio_lf', 'dividend_yield_ttm', 'log_market_cap'] # 用对数市值更新模型输入
analysis_frame[cluster_features] = analysis_frame[cluster_features].clip(analysis_frame[cluster_features].quantile(.01), analysis_frame[cluster_features].quantile(.99), axis=1) # 以1%和99%分位缩尾
display(analysis_frame[['symbol', 'province', 'industry_name'] + cluster_features].head()) # 展示真实字段而非模拟客户
如何根据实际结果选择 K
所有候选 K 都使用同一份 2024-12-31 公司数据、同一组缩放变量和同一随机种子。选择方法是在 \(K=2,\ldots,10\) 中寻找平均轮廓系数最高的 K。
案例实现:同一分析步骤选择并拟合 K
预先规定规则:在 \(K=2\ldots10\) 中选择平均轮廓系数最高者;inertia 曲线用于检查“增加复杂度的边际收益”,不硬编码 K。
Code
from sklearn.cluster import KMeans # 拟合不同K值的K-Means
from sklearn.metrics import silhouette_score # 计算簇内凝聚与簇间分离的综合指标
from sklearn.preprocessing import StandardScaler # 消除不同量纲对欧氏距离的支配
scaler = StandardScaler() # 只用当前季度样本估计缩放参数
scaled_features = scaler.fit_transform(analysis_frame[cluster_features]) # 标准化四个估值画像字段
candidate_k_values = range(2, 11) # 预先限定候选K为2至10
inertia_by_k = {} # 保存每个K的簇内平方和
silhouette_by_k = {} # 保存每个K的平均轮廓系数
for candidate_k in candidate_k_values: # 对每个候选K执行同一拟合流程
candidate_model = KMeans(n_clusters=candidate_k, random_state=42, n_init=20) # 固定种子并多次初始化
candidate_labels = candidate_model.fit_predict(scaled_features) # 生成该K下的实际簇标签
inertia_by_k[candidate_k] = candidate_model.inertia_ # 记录由当前数据计算的inertia
silhouette_by_k[candidate_k] = silhouette_score(scaled_features, candidate_labels) # 记录平均轮廓系数
selected_k = max(silhouette_by_k, key=silhouette_by_k.get) # 按预设规则选择轮廓系数最高的K
kmeans = KMeans(n_clusters=selected_k, random_state=42, n_init=20) # 用选定K建立最终模型
analysis_frame['Cluster'] = kmeans.fit_predict(scaled_features) # 将聚类结果写回当前公司数据
cluster_profile = analysis_frame.groupby('Cluster')[cluster_features].agg(['mean', 'median', 'count']) # 从真实标签生成簇画像
display(pd.DataFrame({'inertia': inertia_by_k, 'silhouette': silhouette_by_k})) # 展示K选择的可以检查证据
display(cluster_profile) # 展示每个簇的规模、均值与中位数
实际结果
数据日期为 2024-12-31,n=1,871;K=2…10 的 silhouette 依次为 0.2883、0.3153、0.3004、0.3279、0.3472、0.2937、0.2733、0.2639、0.2501,因此选择 K=6。
案例证据:K选择曲线由实际数据生成
Code
import matplotlib.pyplot as plt # 绘制由实际计算结果生成的K选择证据
k_table = pd.DataFrame({'K': list(candidate_k_values), 'inertia': list(inertia_by_k.values()), 'silhouette': list(silhouette_by_k.values())}) # 汇总同一分析步骤的两个指标
figure, left_axis = plt.subplots(figsize=(9, 4.8)) # 创建适合幻灯片的双轴画布
left_axis.plot(k_table['K'], k_table['inertia'], marker='o', color='#33658A') # 绘制随K单调下降的inertia
left_axis.set_xlabel('K') # 标注候选簇数
left_axis.set_ylabel('Inertia', color='#33658A') # 标注左轴含义
right_axis = left_axis.twinx() # 建立轮廓系数的独立纵轴
right_axis.plot(k_table['K'], k_table['silhouette'], marker='s', color='#F26419') # 绘制实际平均轮廓系数
right_axis.axvline(selected_k, color='#F26419', linestyle='--', alpha=.6) # 标出预设规则选出的K
right_axis.set_ylabel('平均轮廓系数', color='#F26419') # 标注右轴含义
figure.tight_layout() # 防止轴标签被裁切
plt.show() # 显示由当前数据生成的图形
案例结果:实际公司散点与逆变换质心
Code
import seaborn as sns # 以统一配色绘制真实公司散点
centroids = scaler.inverse_transform(kmeans.cluster_centers_) # 将质心还原到原始估值量纲
figure, axis = plt.subplots(figsize=(9, 5)) # 创建单页可见画布
sns.scatterplot(data=analysis_frame, x='pe_ratio_ttm', y='pb_ratio_lf', hue='Cluster', palette='viridis', s=55, alpha=.7, ax=axis) # 按实际标签绘制公司
axis.scatter(centroids[:, 0], centroids[:, 1], s=220, c='red', marker='X', edgecolor='black', label='质心') # 标出模型估计质心
axis.set_title(f'实际公司聚类:K={selected_k}') # 从运行结果读取K
axis.set_xlabel('滚动市盈率') # 解释横轴字段
axis.set_ylabel('最新市净率') # 解释纵轴字段
axis.legend(title='实际簇', bbox_to_anchor=(1.02, 1), loc='upper left') # 使用中性簇编号
figure.tight_layout() # 防止图例与坐标裁切
plt.show() # 显示当前fit_predict结果
案例结果:业务解读规则
本次执行画像(均值,n)
Cluster 0 为正 PE 17.96、PB 1.91、股息率 4.8%、n=253;
Cluster 1 为高 PE/PB、大市值、n=457;
Cluster 2 为中等 PE/PB、较小市值、n=1,006;
Cluster 3 为极高正 PE、n=54;
Cluster 4 为负 PE 且高 PB、n=54;
Cluster 5 为极端负 PE、n=47。
极端均值提示该簇更适合被标为“异常估值形态”,不能解释为优劣等级。
聚类只描述样本中的相似性。它不证明某类公司未来回报更高,也不应直接触发投资或资源配置决策。
形成性检查:为什么本案必须写 K=6?
- 题目: 本案 K=6 的 silhouette 为 0.3472,K=7 为 0.2937。若 K=7 更容易讲业务故事,应如何报告?
先选择“坚持 K=6 / 无说明改 K=7 / 透明报告覆盖”,再揭示。
综合练习
Code
comparison_date = valuation_data.loc[valuation_data['date'] <= '2023-12-31', 'date'].max() # 选择截止日前真实最新季度
comparison_snapshot = valuation_data.loc[valuation_data['date'].eq(comparison_date)].merge(basic_data.loc[company_filter, company_columns], on='order_book_id') # 复用相同公司筛选
comparison_df = comparison_snapshot.dropna(subset=['pe_ratio_ttm', 'pb_ratio_lf', 'dividend_yield_ttm', 'market_cap']).copy() # 保持字段口径一致
comparison_df['log_market_cap'] = np.log(comparison_df['market_cap'].clip(lower=1)) # 使用相同对数变换
comparison_df[cluster_features] = comparison_df[cluster_features].clip(comparison_df[cluster_features].quantile(.01), comparison_df[cluster_features].quantile(.99), axis=1) # 在比较期样本内缩尾
comparison_scaler = StandardScaler() # 只用比较期样本估计缩放参数
comparison_scaled = comparison_scaler.fit_transform(comparison_df[cluster_features]) # 标准化相同四字段
comparison_scores = {} # 保存2023候选轮廓系数
for comparison_k in range(2, 11): # 执行与2024相同的候选范围
comparison_labels = KMeans(n_clusters=comparison_k, random_state=42, n_init=20).fit_predict(comparison_scaled) # 固定初始化规则
comparison_scores[comparison_k] = silhouette_score(comparison_scaled, comparison_labels) # 记录实际得分
comparison_selected_k = max(comparison_scores, key=comparison_scores.get) # 按相同规则选K
comparison_df['Cluster'] = KMeans(n_clusters=comparison_selected_k, random_state=42, n_init=20).fit_predict(comparison_scaled) # 拟合2023最终标签
common_firms = analysis_frame[['order_book_id', 'Cluster']].merge(comparison_df[['order_book_id', 'Cluster']], on='order_book_id', suffixes=('_2024', '_2023')) # 仅对齐两期共同公司
from sklearn.metrics import adjusted_rand_score # 使用不受簇编号置换影响的稳定性指标
stability_ari = adjusted_rand_score(common_firms['Cluster_2023'], common_firms['Cluster_2024']) # 计算共同公司ARI
print(comparison_date, len(comparison_df), comparison_selected_k, len(common_firms), round(stability_ari, 4)) # 报告完整关键输出
2023-06-30 00:00:00 1765 6 1762 0.358
综合练习:结果与解释
- 完整参考输出:
2023-06-30(截止日前最新可用季度)、n=1,765、K=6;两期共同公司 n=1,762,ARI=0.3580。
ARI 远低于 1,说明画像对市场状态有明显敏感性;它不受簇编号置换影响,但仍不能证明簇具有经济因果机制。
- 答题提示: 若直接比较簇编号,回到“标签可置换”;若 ARI 用了非共同公司,回到样本对齐;若把稳定性写成因果,回到探索性边界。
核心内容总结:从距离到可检查结论
- 先定义表示:量纲不共同就缩放,业务权重必须事先说明。
- 再确定选择:用预设 silhouette 规则选 K,并报告覆盖规则与稳定性,而不是按故事挑 K。
- 最后限制解释:簇编号可置换,画像来自执行结果;聚类描述相似性,不证明收益或因果。
核心内容到此结束。需要比较其他簇形状假设时再进入下一页拓展内容。
来源与延伸阅读
- MacQueen, J. (1967), “Some Methods for Classification and Analysis of Multivariate Observations.”
- Rousseeuw, P. J. (1987), “Silhouettes”, Journal of Computational and Applied Mathematics, 20, 53–65;对应本案的 K 选择规则。
- scikit-learn User Guide:Clustering performance evaluation。
- 数据:本地
valuation_factors_quarterly_15_years.h5 / valuation_factors 与 stock_basic_data.h5 / stock_basic_info;案例图由本页代码生成。
算法二:层次聚类 (Hierarchical Clustering)
与K-Means一次性将数据分成K个簇不同,层次聚类会创建一个聚类的层级结构。
- 核心思想: 它不产生单一的聚类结果,而是生成一个树状图(Dendrogram),展示了数据点是如何逐层合并(或分裂)的。
- 两种主要策略:
- 凝聚式 (Agglomerative): 自底向上。开始时每个点都是一个簇,然后逐步合并最相似的簇。
- 分裂式 (Divisive): 自顶向下。开始时所有点都在一个簇,然后逐步分裂最不相似的簇。
凝聚式层次聚类的工作流程
- 开始: 将每个数据点视为一个独立的簇。
- 合并: 找到所有簇中距离最近的两个簇,并将它们合并成一个新的簇。
- 重复: 重复步骤2,直到所有数据点都被合并到同一个簇中。
整个过程的合并历史,就构成了一个树状的层次结构。
关键问题:如何计算“簇与簇”之间的距离?
这通过链接准则 (Linkage Criterion) 来定义。
层次聚类的可视化:树状图 (Dendrogram)
树状图是层次聚类的标志性输出。它直观地展示了簇的合并过程。
- 纵轴: 代表簇之间的距离或不相似度。合并发生时的高度,表示被合并的两个簇有多不相似。
- 横轴: 代表各个数据点。
我们可以通过在某个高度“横切”树状图,来得到指定数量的簇。
算法三:基于密度的聚类 (DBSCAN)
当K-Means遇到瓶颈时
K-Means假设簇是球状的,对于不规则形状的簇(如月牙形、环形)则无能为力。
DBSCAN (Density-Based Spatial Clustering of Applications with Noise) 的登场:
- 核心思想: 只要样本点的密度足够大,就将它们划分为同一个簇。它能够发现任意形状的簇,并且能自动识别出噪声点。
- 优点: 无需事先选择簇的数量,能处理任意形状的簇,对噪声点不敏感。
- 缺点: 对两个核心参数(
eps 和 min_samples)的选择很敏感,处理密度不均的数据集时效果不佳。
DBSCAN 的核心概念
DBSCAN 的工作依赖于三个关键定义,由两个参数 eps (半径) 和 min_samples (最少点数) 控制。
DBSCAN 算法流程的直观理解
DBSCAN 的过程就像“滚雪球”:
- 随机选择一个未被访问的点
P。
- 检查
P 是否为核心点。
- 如果是:
- 如果不是 (是边界点或噪声点): 暂时标记为噪声点,继续处理下一个点。
- 重复步骤1和2,直到所有点都被访问过。
DBSCAN案例:K-Means 的滑铁卢,DBSCAN 的高光时刻
让我们看一个K-Means无法处理,而DBSCAN能完美解决的例子:“双月”数据集。
算法四:高斯混合模型 (GMM)
超越“硬”聚类
K-Means 对每个点进行“非黑即白”的硬分配 (Hard Assignment)。但有时,一个点可能位于两个簇的边界,我们更希望得到一个概率描述。
高斯混合模型 (Gaussian Mixture Model, GMM) 提供了一种软聚类 (Soft Clustering) 方法。
- 核心思想:
- 结果: GMM 不会直接告诉我们每个点属于哪个簇,而是给出该点属于每个簇的概率。
GMM vs. K-Means
| 簇形状 |
假设为球形 (圆形) |
可适应椭圆形 |
| 分配方式 |
硬分配 (Hard Assignment) |
软分配 (Soft Assignment) |
| 数学基础 |
基于距离 |
基于概率 (期望最大化算法) |
| 灵活性 |
较低 |
较高,能更好地拟合复杂数据 |
GMM 可以看作是 K-Means 的一个概率泛化版本。
GMM案例: 拟合椭圆形簇
当簇的形状不是圆形而是椭圆形时,GMM 的优势就体现出来了。
拓展内容 总结:如何选择正确的聚类算法?
今天我们学习了四种主流的聚类算法,它们各有千秋。在实际应用中,没有“最好”的算法,只有“最合适”的算法。
| K-Means |
速度快,简单易懂 |
需预设K,对球形簇敏感 |
大规模、簇结构简单的数据集 |
| 层次聚类 |
拟合层次结构前无需预设 K |
平面划分仍须记录切割高度或簇数规则;计算复杂度高 (O(N^2)) |
需要探索数据层次结构,小数据集 |
| DBSCAN |
无需预设K,可发现任意形状 |
对参数敏感,密度不均时困难 |
簇形状不规则,含噪声的数据集 |
| GMM |
软聚类,簇形状灵活(椭圆) |
算法复杂,计算成本高 |
需要概率输出,簇有重叠 |
算法选择决策树
这是一个简化的决策流程,帮助你在实践中选择合适的聚类算法。
聚类分析的商业价值
聚类分析不仅仅是一套算法,更是一种强大的商业洞察工具。
- 市场营销: 识别客户分群,实现精准广告投放和个性化推荐。(e.g., Amazon, Netflix)
- 金融风控: 发现异常交易模式,识别潜在的欺诈行为。(e.g., American Express)
- 城市规划: 根据居民出行模式对城市进行功能区划分。(e.g., 智慧城市项目)
- 生物信息: 对基因表达数据进行聚类,发现具有相似功能的基因群。(e.g., 癌症研究)
- 社交网络: 发现社区和兴趣小组,进行好友推荐。(e.g., Facebook, LinkedIn)
掌握聚类,就是掌握了从海量数据中发现结构、创造价值的关键钥匙。