数据分布漂移检测:训练集与线上数据对比

模型上线后识别率下降,十有八九是数据分布变了。我见过太多团队花几周调模型结构,最后发现是线上数据跟训练集根本不是一码事。说白了,数据分布漂移是模型退化的头号元凶。

这一章我们聚焦三个核心手段:训练集与线上数据对比、PSI(群体稳定性指标)、特征分布可视化(KS检验)。掌握了这些,你就能快速定位「数据变了」还是「模型坏了」。

为什么数据分布会漂移?

先看几个真实场景:

  • 用户行为变化:疫情期间线上购物暴增,疫情后回落。模型在疫情期间训练,解封后自然不准。
  • 数据采集方式变更:换了埋点 SDK,某个字段的缺失率从 5% 变成 30%。
  • 业务策略调整:推荐系统改了排序规则,用户点击分布完全变了。

我个人习惯把漂移分成两类:协变量漂移(特征分布变了)和概念漂移(特征与标签的关系变了)。本章主要处理前者。

核心原则:先确认数据分布是否漂移,再谈模型优化。不要一上来就调参。

训练集与线上数据对比:第一步该做什么

拿到线上数据后,我通常会做三件事:

  1. 统计量对比:均值、方差、缺失率、极值。每个特征都算一遍。
  2. 分布形状对比:直方图叠在一起看,或者用核密度估计图。
  3. 类别特征频次对比:看每个类别的占比是否剧烈变化。

举个例子,假设训练集里「用户年龄」均值是 32 岁,线上突然变成 28 岁。这就要警惕了——是不是新用户群体涌入?

我的经验:对比时别只看均值。有一次我排查一个 CTR 模型,均值完全没变,但方差翻了一倍。后来发现是某个渠道的流量质量波动了。均值掩盖了问题。

PSI(群体稳定性指标):量化分布差异

PSI 是金融风控领域的老牌指标,用来衡量两个样本集在同一个特征上的分布差异。公式不复杂:

PSI = Σ (P_i - Q_i) * ln(P_i / Q_i)

其中:
P_i = 线上数据在第 i 个分箱中的占比
Q_i = 训练集在第 i 个分箱中的占比

计算步骤:

  1. 对特征分箱(等频或等距,通常 10 箱)。
  2. 分别统计训练集和线上数据在每个箱中的占比。
  3. 代入公式求和。

判断标准:

PSI 值 含义 建议操作
< 0.1 无显著漂移 继续监控
0.1 - 0.25 轻度漂移 需关注,准备重训
> 0.25 严重漂移 立即排查,考虑重训或回滚

避坑指南:PSI 对分箱方式敏感。我曾经用等距分箱算出一个特征 PSI=0.3,换成等频分箱变成 0.08。建议两种都试一下,取更保守的结果。

代码实现很简单:

import numpy as np
import pandas as pd

def calculate_psi(train, online, bins=10):
    # 等频分箱
    _, edges = pd.qcut(train, q=bins, retbins=True, duplicates='drop')
    train_bins = pd.cut(train, bins=edges, include_lowest=True).value_counts(normalize=True).sort_index()
    online_bins = pd.cut(online, bins=edges, include_lowest=True).value_counts(normalize=True).sort_index()
    
    # 防止除零
    online_bins = online_bins.replace(0, 1e-6)
    train_bins = train_bins.replace(0, 1e-6)
    
    psi = np.sum((online_bins - train_bins) * np.log(online_bins / train_bins))
    return psi

特征分布可视化:KS 检验

PSI 给出一个全局数值,但有时候我们需要更细粒度的判断——某个特征在训练集和线上数据上的分布是否显著不同。这时候用 KS 检验(Kolmogorov-Smirnov test)。

KS 统计量是两个累积分布函数(CDF)的最大垂直距离。值越大,分布差异越大。

KS = max |F_train(x) - F_online(x)|

其中 F 是累积分布函数

我一般配合可视化一起看:

  • 画两条 CDF 曲线,标出最大差距的位置。
  • 如果 KS 值 > 0.1 且 p-value < 0.05,认为分布有显著差异。

个人习惯:KS 检验对样本量敏感。线上数据量很大时,很小的差异也会显著。这时候别只看 p-value,结合业务判断「这个差异有没有实际影响」。

可视化代码:

import matplotlib.pyplot as plt
from scipy.stats import ks_2samp

def plot_ks(train, online, feature_name):
    stat, p_value = ks_2samp(train, online)
    
    # 计算 CDF
    sorted_train = np.sort(train)
    sorted_online = np.sort(online)
    cdf_train = np.arange(1, len(sorted_train)+1) / len(sorted_train)
    cdf_online = np.arange(1, len(sorted_online)+1) / len(sorted_online)
    
    plt.figure(figsize=(10, 6))
    plt.plot(sorted_train, cdf_train, label='Train', color='#3498db')
    plt.plot(sorted_online, cdf_online, label='Online', color='#e74c3c')
    
    # 标出最大差距
    diffs = np.abs(cdf_train - np.interp(sorted_train, sorted_online, cdf_online))
    max_idx = np.argmax(diffs)
    plt.vlines(sorted_train[max_idx], cdf_train[max_idx], 
               np.interp(sorted_train[max_idx], sorted_online, cdf_online),
               colors='gray', linestyles='--', label=f'KS={stat:.3f}')
    
    plt.xlabel(feature_name)
    plt.ylabel('CDF')
    plt.legend()
    plt.title(f'KS Test: {feature_name} (p={p_value:.2e})')
    plt.show()

整体检测流程

下面这张图概括了数据分布漂移检测的完整流程:

数据分布漂移检测流程 训练集 + 线上数据 第一步:统计量对比(均值/方差/缺失率) PSI 计算(全局量化) KS 检验 + 可视化(逐特征) 综合判断:哪些特征漂移?严重程度? 输出:漂移报告 + 建议行动 说明: PSI 适合快速扫描所有特征,KS 检验适合深入分析关键特征。两者互补,不要只用一个。

实际案例:一次完整的漂移排查

去年我接手一个广告点击率模型,线上 AUC 从 0.78 掉到 0.71。团队慌了,准备重新训练。我说先别急,看看数据。

排查过程:

  1. 统计量对比:发现「用户活跃天数」这个特征,训练集均值 12.3 天,线上均值 8.1 天。方差也变了。
  2. PSI 计算:该特征 PSI = 0.31,严重漂移。
  3. KS 检验:KS = 0.24,p-value < 0.001,确认分布显著不同。
  4. 根因:原来业务方上线了一个新用户拉新活动,大量低活跃用户涌入。模型没见过这种分布。

解决方案不是调模型,而是:

  • 用最近 7 天的线上数据做增量训练。
  • 在特征工程中加入「用户生命周期阶段」这个新特征。
  • 建立 PSI 监控看板,每天自动报警。

关键教训:数据漂移检测不是一次性工作。我建议每周跑一次 PSI,每次模型迭代前也跑一次。养成习惯后,你能在识别率下降之前就发现问题。

总结一下

数据分布漂移检测是模型诊断的第一道防线。记住三个工具:

  • 统计量对比:快速扫描,发现异常特征。
  • PSI:量化漂移程度,适合批量监控。
  • KS 检验 + 可视化:深入分析,确认分布差异的具体位置。

嗯,这一章就到这里。工具和方法都给了,关键是用起来。下次模型掉点,先别急着调参,跑一遍漂移检测再说。