漂移检测的数学基础:统计假设检验与分布距离度量

说实话,很多同学一听到「数学基础」四个字就头大。我当年也一样。但做了几年漂移检测系统后,我发现真正在线上用得上的数学工具,其实就那么几个。今天咱们就把它们掰开揉碎了讲清楚。

漂移检测的核心,说白了就是回答两个问题:

  • 当前数据和历史数据,是不是来自同一个分布?
  • 如果不是,差别有多大?

第一个问题靠假设检验,第二个问题靠距离度量。咱们一个一个来。

漂移检测数学基础知识体系 漂移检测数学基础 统计假设检验 KS 检验 卡方检验 连续分布 vs 连续分布 对分布形状敏感 分类特征 vs 期望分布 分布距离度量 Wasserstein 距离 KL 散度 推土机距离,考虑分布形状 对平移敏感,梯度平滑 非对称度量,对尾部敏感

统计假设检验:判断「有没有变」

假设检验的思路很直观。我们先假设「数据没变」,然后看当前数据有多大概率出现。如果概率太小,就拒绝假设,认为漂移发生了。

我习惯把假设检验比作「抓小偷」。你先假设他是好人,但发现他的行为太反常了——正常人不会这样——那你就抓他。嗯,就是这个道理。

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 距离不能直接比较。我一般会做归一化,或者用百分比表示。