4. 数据漂移检测实战

在金融风控、信用评分和量化交易场景中,模型上线只是起点。当输入特征分布或标签分布随时间发生偏移,模型的预测能力会悄然衰减。本节聚焦五类主流漂移度量指标,从原理、计算流程到Python落地,帮助你把“感觉模型变差了”转化为可量化、可告警、可追溯的工程信号。

一、核心原理:五种指标在度量什么

漂移检测的本质,是比较两个分布之间的差异。参考分布通常来自训练集或上一个稳定窗口,当前分布来自最新生产数据。不同指标对差异的敏感维度不同:

  • PSI(群体稳定性指数):基于分箱后的占比差异,计算 Σ(实际占比-预期占比)×ln(实际占比/预期占比),对分箱边界和样本量敏感,是金融风控最常用的稳定性指标。
  • KS检验:基于经验累积分布函数的最大垂直距离,输出统计量与p值,适合连续变量,对分布整体位置和形状变化敏感。
  • KL散度:衡量用当前分布编码参考分布时的信息损失,非对称,要求两分布支撑集重叠,否则趋于无穷。
  • JS散度:KL散度的对称化与平滑版本,取值在0到1之间(以2为底),对零概率更稳健。
  • Wasserstein距离:又称推土机距离,衡量把一个分布“搬运”成另一个分布的最小代价,保留几何结构,对分布平移和缩放敏感。
选择建议:风控评分卡优先PSI;连续特征快速筛查用KS;需要对称且稳健的分布距离用JS;关注分布整体迁移幅度且样本量充足时用Wasserstein。
五种漂移指标:敏感维度与适用场景 PSI 分箱占比差异 风控评分卡 阈值:0.1/0.25 KS检验 CDF最大距离 连续变量筛查 输出p值 KL散度 信息损失 非对称 需支撑集重叠 JS散度 对称平滑 取值0~1 对零概率稳健 Wasserstein 搬运代价 保留几何 平移敏感 参考分布 训练期分布 当前分布 生产期分布 分布偏移

二、方法流程:从数据准备到告警输出

  1. 确定参考窗口与当前窗口:参考窗口建议取训练集或最近一个稳定周期,当前窗口取最近7天或30天,避免窗口过短导致噪声。
  2. 特征筛选与预处理:连续变量做分箱或核密度估计,类别变量做频次编码,缺失值单独成箱。
  3. 逐指标计算:对每个特征分别计算PSI、KS、KL、JS、Wasserstein,形成特征级漂移矩阵。
  4. 阈值判定与分级:结合业务容忍度设定绿/黄/红三级阈值,输出告警清单。
  5. 根因定位:对红色特征下钻到分箱、时间切片和客群维度,定位漂移来源。

三、Python实现:五指标统一计算函数

下面代码以信用评分场景为例,使用scipy与numpy实现五指标计算。注意KL与JS需先对连续变量做分箱或直方图归一化,避免零概率导致无穷值。

import numpy as np
from scipy import stats
from scipy.spatial.distance import jensenshannon

def psi(expected, actual, bins=10):
    breakpoints = np.percentile(expected, np.linspace(0, 100, bins + 1))
    breakpoints[0], breakpoints[-1] = -np.inf, np.inf
    e_perc = np.histogram(expected, bins=breakpoints)[0] / len(expected)
    a_perc = np.histogram(actual, bins=breakpoints)[0] / len(actual)
    e_perc = np.where(e_perc == 0, 1e-6, e_perc)
    a_perc = np.where(a_perc == 0, 1e-6, a_perc)
    return np.sum((a_perc - e_perc) * np.log(a_perc / e_perc))

def ks_test(expected, actual):
    stat, p_value = stats.ks_2samp(expected, actual)
    return stat, p_value

def kl_divergence(expected, actual, bins=50):
    hist_range = (min(expected.min(), actual.min()), max(expected.max(), actual.max()))
    e_hist, _ = np.histogram(expected, bins=bins, range=hist_range, density=True)
    a_hist, _ = np.histogram(actual, bins=bins, range=hist_range, density=True)
    e_hist = np.where(e_hist == 0, 1e-6, e_hist)
    a_hist = np.where(a_hist == 0, 1e-6, a_hist)
    return stats.entropy(e_hist, a_hist)

def js_divergence(expected, actual, bins=50):
    hist_range = (min(expected.min(), actual.min()), max(expected.max(), actual.max()))
    e_hist, _ = np.histogram(expected, bins=bins, range=hist_range, density=True)
    a_hist, _ = np.histogram(actual, bins=bins, range=hist_range, density=True)
    e_hist = np.where(e_hist == 0, 1e-6, e_hist)
    a_hist = np.where(a_hist == 0, 1e-6, a_hist)
    return jensenshannon(e_hist, a_hist) ** 2

def wasserstein_distance(expected, actual):
    return stats.wasserstein_distance(expected, actual)

# 示例:对特征 credit_score 计算五指标
ref = np.random.normal(650, 50, 10000)
cur = np.random.normal(620, 55, 8000)
print("PSI:", round(psi(ref, cur), 4))
print("KS:", ks_test(ref, cur))
print("KL:", round(kl_divergence(ref, cur), 4))
print("JS:", round(js_divergence(ref, cur), 4))
print("Wasserstein:", round(wasserstein_distance(ref, cur), 4))
KL散度对零概率极其敏感,若某分箱在参考分布中为0,结果会趋于无穷。工程上必须做平滑处理,或改用JS散度。PSI的分箱边界必须固定为参考分布的分位数,否则跨窗口不可比。

四、案例与阈值清单

某消费金融公司对credit_score、debt_ratio、query_count_30d三个特征做周度漂移检测。参考窗口为训练集,当前窗口为最近一周。结果如下表:

特征 PSI KS统计量 JS散度 Wasserstein 判定
credit_score 0.08 0.06 0.02 12.4 绿色
debt_ratio 0.31 0.18 0.09 0.07 红色
query_count_30d 0.14 0.09 0.04 0.35 黄色

复盘发现,debt_ratio漂移源于新接入渠道客群负债结构差异,属于渠道结构变化而非模型失效。团队随后按渠道分层重算PSI,红色告警降为黄色,并触发渠道准入策略复核。

五、常见误区与复盘要点

  • 误区一:只看PSI不看样本量。小样本下PSI波动剧烈,建议当前窗口样本量不低于5000,否则改用KS或JS。
  • 误区二:所有特征用同一阈值。评分卡核心变量阈值应更严,辅助变量可放宽,需按特征重要性分级。
  • 误区三:忽略分箱边界一致性。参考窗口与当前窗口必须使用同一套分箱边界,否则PSI不可比。
  • 误区四:把漂移等同于模型失效。漂移是信号,需结合业务事件、渠道变化、客群迁移做根因诊断,再决定是否重训。
  • 复盘要点:建立特征级漂移台账,记录每次告警的时间、指标值、根因和处置动作,形成可追溯的模型治理闭环。
落地建议:将五指标封装为统一漂移检测函数,按日/周调度,输出特征级漂移矩阵与分级告警。对红色特征自动触发根因下钻任务,对连续三周黄色特征提前预警,把被动救火转为主动治理。