漂移检测的数学基础:统计假设检验与分布距离度量
说实话,很多同学一听到「数学基础」四个字就头大。我当年也一样。但做了几年漂移检测系统后,我发现真正在线上用得上的数学工具,其实就那么几个。今天咱们就把它们掰开揉碎了讲清楚。
漂移检测的核心,说白了就是回答两个问题:
- 当前数据和历史数据,是不是来自同一个分布?
- 如果不是,差别有多大?
第一个问题靠假设检验,第二个问题靠距离度量。咱们一个一个来。
统计假设检验:判断「有没有变」
假设检验的思路很直观。我们先假设「数据没变」,然后看当前数据有多大概率出现。如果概率太小,就拒绝假设,认为漂移发生了。
我习惯把假设检验比作「抓小偷」。你先假设他是好人,但发现他的行为太反常了——正常人不会这样——那你就抓他。嗯,就是这个道理。
KS 检验(Kolmogorov-Smirnov Test)
KS 检验是我在连续特征上用得最多的方法。它比较两个经验分布函数的最大垂直距离。
公式长这样:
D = max|F1(x) - F2(x)|
其中:
- F1(x) 是参考分布的经验 CDF
- F2(x) 是当前分布的经验 CDF
- D 是两者之间的最大差值
D 值越大,说明两个分布差异越大。当 D 超过某个临界值时,我们就说漂移发生了。
我的经验:KS 检验对分布的位置和形状都很敏感。我在做金融风控模型监控时,KS 检验帮我抓到了好几次特征分布的整体偏移。但它有个缺点——对分布尾部的变化不太敏感。如果你的数据在尾巴上出了问题,KS 可能反应迟钝。
用 Python 实现很简单:
from scipy import stats
# 参考数据:模型训练时的特征分布
reference = [0.1, 0.3, 0.5, 0.7, 0.9]
# 当前数据:线上实时特征
current = [0.2, 0.4, 0.6, 0.8, 1.0]
statistic, p_value = stats.ks_2samp(reference, current)
print(f"KS 统计量: {statistic:.4f}")
print(f"p 值: {p_value:.4f}")
if p_value < 0.05:
print("检测到漂移!")
else:
print("未检测到漂移")
卡方检验(Chi-Square Test)
卡方检验适合分类特征。比如用户的地域分布、设备类型、渠道来源这些离散变量。
它的思路是:比较观测频数和期望频数的差异。
χ² = Σ (Oᵢ - Eᵢ)² / Eᵢ
其中:
- Oᵢ 是第 i 个类别的观测频数
- Eᵢ 是第 i 个类别的期望频数
注意:卡方检验要求每个类别的期望频数不小于 5。我曾经在一个冷启动项目里踩过这个坑——某个类别只有 2 个样本,卡方检验直接报错。后来我改用 Fisher 精确检验才搞定。
代码示例:
from scipy import stats
# 参考分布:训练集各品类占比
reference = [500, 300, 200] # 品类 A, B, C
# 当前分布:线上各品类数量
current = [480, 310, 210]
chi2, p_value = stats.chisquare(current, f_exp=reference)
print(f"卡方统计量: {chi2:.4f}")
print(f"p 值: {p_value:.4f}")
分布距离度量:量化「差多少」
假设检验告诉你「有没有变」,但有时候你还想知道「变了多少」。这时候就需要距离度量了。
你想想看,如果 KS 检验告诉你漂移了,但漂移程度是 0.01 还是 0.5?这对业务决策影响很大。0.01 可能只是正常波动,0.5 就得赶紧拉警报了。
Wasserstein 距离(Earth Mover's Distance)
Wasserstein 距离有个很形象的名字——推土机距离。它衡量的是「把一堆土从一个形状推到另一个形状需要的最小工作量」。
公式:
W(P, Q) = inf ∫ |x - y| dγ(x, y)
其中 γ 是 P 和 Q 的联合分布
说白了,就是算两个分布之间「搬土」的最小成本。
为什么我偏爱 Wasserstein 距离?
- 它对分布的位置平移非常敏感。均值漂移 0.1 和漂移 1.0,Wasserstein 距离会给出明显不同的值。
- 它的梯度是平滑的。这在做渐进式漂移检测时特别有用——你不会看到突然的跳变。
- 它考虑了两个分布的整体形状,不只是某个点上的差异。
实现代码:
import numpy as np
from scipy.stats import wasserstein_distance
# 两个连续分布
ref = np.random.normal(0, 1, 1000)
cur = np.random.normal(0.5, 1, 1000)
dist = wasserstein_distance(ref, cur)
print(f"Wasserstein 距离: {dist:.4f}")
# 输出:大约 0.5(因为均值偏移了 0.5)
KL 散度(Kullback-Leibler Divergence)
KL 散度衡量的是「用一个分布去近似另一个分布时,损失了多少信息」。
公式:
KL(P || Q) = Σ P(x) * log(P(x) / Q(x))
KL 散度的坑:
- 非对称性:KL(P||Q) ≠ KL(Q||P)。你选哪个方向,结果完全不同。
- 对尾部敏感:如果 Q(x) 接近 0 而 P(x) 不为 0,KL 散度会爆炸。
- 需要平滑:实际计算时,我通常会给概率加一个很小的 epsilon,防止 log(0)。
代码示例:
import numpy as np
def kl_divergence(p, q, eps=1e-10):
p = np.clip(p, eps, 1)
q = np.clip(q, eps, 1)
return np.sum(p * np.log(p / q))
# 两个离散分布
p = np.array([0.4, 0.3, 0.2, 0.1])
q = np.array([0.5, 0.2, 0.2, 0.1])
kl = kl_divergence(p, q)
print(f"KL(P||Q): {kl:.4f}")
如何选择?一张表说清楚
| 方法 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| KS 检验 | 连续特征,整体分布对比 | 无参数假设,计算快 | 对尾部不敏感 |
| 卡方检验 | 分类特征,频数对比 | 直观,解释性强 | 要求期望频数 ≥ 5 |
| Wasserstein 距离 | 连续特征,需要量化漂移程度 | 梯度平滑,对平移敏感 | 计算成本略高 |
| KL 散度 | 概率分布对比,信息论场景 | 信息论意义明确 | 非对称,对尾部敏感 |
我的选型建议:
- 刚开始做漂移检测,先用 KS 检验 + 卡方检验打底。简单、稳定、好解释。
- 如果业务要求「量化漂移程度」,加上 Wasserstein 距离。
- KL 散度我一般用在特征重要性分析和模型诊断上,不太直接用于告警。
避坑指南
做漂移检测这几年,我踩过的坑不少。挑几个典型的说说:
- p 值不是万能的。样本量大的时候,p 值很容易小于 0.05,哪怕漂移程度很小。我建议结合效应量(effect size)一起看。
- 多重比较问题。如果你有 100 个特征,每个都做 KS 检验,那大概率会有几个「假阳性」。记得做 Bonferroni 校正或者 FDR 控制。
- 分布重叠不是没漂移。两个分布可能均值相同但方差不同。KS 检验能抓到,但如果你只看均值,就漏了。
- Wasserstein 距离的尺度问题。不同特征的 Wasserstein 距离不能直接比较。我一般会做归一化,或者用百分比表示。