3、数据漂移检测实战:基于特征分布的单变量检测方法
说实话,很多团队做模型监控,第一步就卡在「到底该用什么指标」上。我见过不少项目,一上来就上深度学习做分布对比,结果数据量不够、解释性又差,最后连业务方都看不懂报告。
其实,单变量检测方法才是工业界的基石。KS检验和PSI,这两个东西你搞懂了,80%的漂移场景都能覆盖。今天我就带你手撸一遍,顺便聊聊我踩过的坑。
3.1 为什么先做单变量?
多变量检测听起来高大上,但落地时有个致命问题:可解释性差。你告诉业务方「模型特征空间发生了漂移」,人家问「哪个特征?漂了多少?」,你答不上来,那就尴尬了。
单变量检测就不一样。每个特征单独算分,哪个特征漂了、漂了多少、什么时候开始的,一目了然。我个人的习惯是:先单变量扫一遍,发现问题再上多变量。这样效率最高。
核心原则:单变量检测是漂移检测系统的「第一道防线」。它不追求完美,但追求快速、可解释、可告警。
3.2 KS检验:两样本分布是否一致?
KS检验,全称Kolmogorov-Smirnov检验。说白了,就是比较两个样本的累积分布函数(CDF)的最大垂直距离。距离越大,分布差异越大。
公式很简单:
D = max |F1(x) - F2(x)|
其中F1是参考分布的CDF,F2是当前分布的CDF。D值在0到1之间,越接近1说明漂移越严重。
3.2.1 什么时候用KS?
- 连续型特征:比如年龄、收入、点击率等数值型变量
- 样本量较大:一般要求每组样本量>50,否则统计功效不足
- 分布形状敏感:KS对分布的位置、离散度、形状变化都敏感
我的经验:KS检验对样本量非常敏感。样本量越大,即使微小的分布差异也会被检测为显著。所以我在项目中通常不只看p值,而是直接看D值。D > 0.1 才触发告警,这样能过滤掉大量无意义的波动。
3.2.2 Python实现
直接用scipy,几行代码搞定:
from scipy.stats import ks_2samp
import numpy as np
def ks_detection(reference, current, threshold=0.1):
"""
单特征KS检验漂移检测
:param reference: 参考分布(训练集)
:param current: 当前分布(生产数据)
:param threshold: D值阈值
:return: (是否漂移, D值, p值)
"""
stat, p_value = ks_2samp(reference, current)
is_drift = stat > threshold
return is_drift, stat, p_value
# 示例
ref_data = np.random.normal(0, 1, 10000)
cur_data = np.random.normal(0.2, 1.1, 10000)
drifted, d_val, p_val = ks_detection(ref_data, cur_data)
print(f"漂移状态: {drifted}, D值: {d_val:.4f}, p值: {p_val:.4f}")
你看,就这么简单。但实际落地时,有个坑我必须提醒你:
我曾经踩过的坑:KS检验对离散型特征(比如枚举值、类别型)效果很差。因为CDF在离散点上跳跃,D值容易被高估。后来我养成了习惯:连续特征用KS,离散特征用卡方检验或PSI。
3.3 PSI:群体稳定性指标
PSI(Population Stability Index)是金融风控领域的标配。它衡量的是两个分布在各分箱上的占比差异。公式如下:
PSI = Σ (P_i - Q_i) * ln(P_i / Q_i)
其中P_i是当前分布在第i个分箱的占比,Q_i是参考分布在第i个分箱的占比。
3.3.1 PSI的解读标准
| PSI值 | 含义 | 建议操作 |
|---|---|---|
| < 0.1 | 无显著漂移 | 无需处理 |
| 0.1 - 0.25 | 轻度漂移 | 需关注,建议排查 |
| > 0.25 | 严重漂移 | 立即告警,触发模型重训 |
这个标准是行业共识,但别死板套用。我在做信贷风控时,PSI阈值设到0.2就告警了,因为金融场景对稳定性要求极高。但在推荐系统里,0.3以内我都觉得正常——用户行为本来就在变。
3.3.2 Python实现
PSI的实现核心是分箱。我习惯用等频分箱,因为等距分箱容易导致某些箱子样本太少:
import numpy as np
import pandas as pd
def psi_detection(reference, current, bins=10, threshold=0.25):
"""
单特征PSI漂移检测
:param reference: 参考分布
:param current: 当前分布
:param bins: 分箱数
:param threshold: PSI阈值
:return: (是否漂移, PSI值)
"""
# 基于参考分布确定分箱边界(等频分箱)
percentiles = np.linspace(0, 100, bins + 1)
bin_edges = np.percentile(reference, percentiles)
bin_edges[0] = -np.inf
bin_edges[-1] = np.inf
# 计算各箱占比
ref_counts = np.histogram(reference, bins=bin_edges)[0]
cur_counts = np.histogram(current, bins=bin_edges)[0]
ref_pct = ref_counts / len(reference)
cur_pct = cur_counts / len(current)
# 处理零值(加极小值避免除零)
ref_pct = np.where(ref_pct == 0, 1e-6, ref_pct)
cur_pct = np.where(cur_pct == 0, 1e-6, cur_pct)
# 计算PSI
psi = np.sum((cur_pct - ref_pct) * np.log(cur_pct / ref_pct))
is_drift = psi > threshold
return is_drift, psi
# 示例
ref_data = np.random.normal(0, 1, 10000)
cur_data = np.random.normal(0.5, 1.2, 10000)
drifted, psi_val = psi_detection(ref_data, cur_data)
print(f"漂移状态: {drifted}, PSI值: {psi_val:.4f}")
小技巧:分箱数bins怎么选?我一般取10-20。太少则灵敏度不够,太多则每个箱子的统计噪声太大。对于样本量小于1000的场景,我建议bins取5-8,否则分箱太稀疏。
3.4 KS vs PSI:怎么选?
很多初学者会问:到底用KS还是PSI?我的回答是:两个都用,但看场景。
| 对比维度 | KS检验 | PSI |
|---|---|---|
| 统计原理 | 基于CDF最大距离 | 基于分箱占比差异 |
| 对分布形状敏感度 | 高 | 中 |
| 对样本量敏感度 | 高(p值受样本量影响大) | 低(占比计算相对稳定) |
| 可解释性 | 中(需要理解CDF) | 高(业务方容易理解占比变化) |
| 适用特征类型 | 连续型 | 连续型、离散型均可 |
| 工业界普及度 | 高(学术界常用) | 极高(金融风控标配) |
我个人习惯是:连续特征两个都算,取告警更严格的那个。离散特征只用PSI。这样既不会漏报,也不会因为单一指标的缺陷而误报。
3.5 知识体系总览
下面这张图,是我做漂移检测系统时的核心逻辑。你看一遍,基本就知道整个单变量检测的流程了:
3.6 落地时的三个关键点
代码写完了,理论也懂了,但落地时还有几个坑。我直接给你列出来:
- 参考分布怎么选? 我建议用最近一周或一个月的生产数据作为参考,而不是训练集。因为训练集可能已经过时了。每周更新一次参考分布,这样能捕捉到缓慢漂移。
- 阈值怎么调? 别用默认值。我一般会先跑一周的历史数据,统计PSI和KS的百分位数,然后取P95或P99作为阈值。这样能自适应你的业务场景。
- 告警后怎么办? 漂移检测只是第一步。告警后要自动触发根因分析:哪个特征漂了?漂的方向是什么?对模型输出有多大影响?这些都要串联起来。
重要提醒:别把所有特征都设成同一个阈值。重要特征(比如模型权重高的特征)阈值设严一点,次要特征可以放宽。我在一个项目中,把「用户年龄」的PSI阈值设到0.15,而「设备型号」设到0.3——因为后者本身波动就大。