上游分布悄悄变了——数据漂移(Data Drift)你怎么发现?
本文目录
先说结论
数据漂移分三类:协变量漂移(X分布变)、标签漂移(Y分布变)、概念漂移(X→Y关系变)。数据质量主要关心前两类。
检测方法三大类:分布距离指标(PSI/KL散度/JS散度)、统计假设检验(KS检验/卡方检验)、特征监控(均值/方差/分位数对比)。每种方法擅长不同场景。
最重要的不是检测方法本身,而是基线管理——把过去N天的分布固化为基线,每天对比当天分布与基线的距离。
先把这段讲顺;面试官追问时,再展开下面的原理和细节。
详细讲解
Data Drift(数据漂移)是数据质量里最隐蔽的一类问题——格式没变、空值率没变、行数没变,但数据的分布悄悄变了。比如某个字段过去90%是A类,突然变成60%;某个数值字段均值从100漂到150。这种漂移DQC的常规规则根本抓不到,但下游模型/报表会直接受影响。这道题在涉及数据科学/特征工程的面试里加分极高。
数据漂移的常见场景
我在出海硬件公司遇到过的真实漂移:
• 某SDK升级后,某个埋点字段的取值集合从{A,B,C}变成{A,B,C,D,E},但DQC没拦——枚举校验只检查「在白名单内」,新增了C/D/E也算通过
• 海外业务扩展到欧洲后,country字段的分布从「美国占60%」变成「美国40%、德国20%、法国15%」——下游一个按country分桶的特征工程模型预测准确率掉了10%
• 某个金额字段的均值从100漂到150(汇率变了),下游的「异常高额订单」规则瞬间失效
方法一:PSI(Population Stability Index,最常用)
PSI是金融/风控领域最经典的漂移指标。计算方式:把字段值分桶,比较新旧两期每个桶的占比,加权求和。
-- 计算字段amount的PSI(旧分布 vs 新分布)
WITH old_dist AS (
SELECT
NTILE(10) OVER (ORDER BY amount) AS bucket,
COUNT(*) * 1.0 / SUM(COUNT(*)) OVER() AS old_pct
FROM dwd_order WHERE dt BETWEEN '20240101' AND '20240131'
GROUP BY NTILE(10) OVER (ORDER BY amount)
),
new_dist AS (
SELECT bucket, COUNT(*) * 1.0 / SUM(COUNT(*)) OVER() AS new_pct
FROM dwd_order WHERE dt = '${bizdate}'
GROUP BY bucket
)
SELECT SUM((new_pct - old_pct) * LN(new_pct / old_pct)) AS psi
FROM old_dist o JOIN new_dist n USING (bucket);
-- PSI解读:
-- < 0.1: 分布稳定,无漂移
-- 0.1 - 0.25: 中度漂移,需关注
-- > 0.25: 严重漂移,必须告警
PSI优点:可解释性强、阈值固定(业界共识)、计算简单。生产环境我90%的连续值字段用PSI监控。
方法二:KS检验(Kolmogorov-Smirnov,统计严谨)
PSI是经验指标,KS检验是严格的统计假设检验,给出「分布是否相同」的p-value。
from scipy.stats import ks_2samp
import numpy as np
# 抽样:旧分布30天 vs 今日分布
old_sample = sample_from_history(field='amount', days=30, n=10000)
new_sample = sample_from_today(field='amount', n=10000)
ks_stat, p_value = ks_2samp(old_sample, new_sample)
# 解读:p_value < 0.01 表示分布显著不同(强烈漂移信号)
if p_value < 0.01:
alert(f'amount distribution drift detected, p={p_value:.4f}')
KS检验适用:连续型数值字段。对类别字段不适用,类别字段用卡方检验。
方法三:类别字段的卡方检验和频次对比
from scipy.stats import chi2_contingency
# 类别字段(country)的频次对比
old_freq = {'US': 6000, 'DE': 2000, 'FR': 1500, 'OTHER': 500}
new_freq = {'US': 4000, 'DE': 2000, 'FR': 1500, 'OTHER': 2500}
# 构建contingency table
table = [[old_freq[k] for k in old_freq], [new_freq[k] for k in old_freq]]
chi2, p, dof, expected = chi2_contingency(table)
if p < 0.01:
alert(f'country distribution drift, chi2={chi2:.2f}')
# 还要检测「新出现的类别」
new_categories = set(new_freq.keys()) - set(old_freq.keys())
if new_categories:
alert(f'New categories appeared: {new_categories}')
三种检测方法对比
| 方法 | 适用字段类型 | 可解释性 | 敏感度 | 适用场景 |
|---|---|---|---|---|
| PSI | 数值/类别(分桶后) | 强 | 中 | 通用监控、风控、信贷 |
| KS检验 | 连续数值 | 中 | 高 | 数值字段精细监控 |
| 卡方检验 | 类别 | 强 | 高 | 类别字段、枚举值 |
| 分位数对比 | 数值 | 强 | 低 | 粗粒度趋势监控 |
| 新值检测 | 类别 | 极强 | 极高 | 枚举字段、ID字段 |
Drift监控的工程实现
# 数据漂移监控的标准管线
def drift_monitor_pipeline(table, columns, dt):
for col in columns:
col_type = get_column_type(table, col)
# 加载基线(过去30天分布)
baseline = load_baseline(table, col, dt_range=(dt-30, dt-1))
today = load_today_distribution(table, col, dt)
if col_type == 'numeric':
psi = compute_psi(baseline, today)
ks_stat, p = ks_2samp(baseline.sample, today.sample)
if psi > 0.25 or p < 0.01:
alert('P0', f'{table}.{col} drift: PSI={psi:.3f}, KS_p={p:.4f}')
elif psi > 0.1:
alert('P1', f'{table}.{col} mild drift: PSI={psi:.3f}')
elif col_type == 'categorical':
chi2_stat, p = chi2_test(baseline.freq, today.freq)
new_cats = set(today.cats) - set(baseline.cats)
if new_cats:
alert('P0', f'{table}.{col} new categories: {new_cats}')
if p < 0.01:
alert('P1', f'{table}.{col} drift: chi2_p={p:.4f}')
# 持久化指标到时序表,用于趋势分析
save_metric(table, col, dt, {'psi': psi, 'p': p})
面试高频追问
追问一:模型训练时的Drift和数据质量Drift什么关系?
数据质量Drift是上游问题(数据本身分布变了),模型Drift是下游表现(模型在新数据上效果变差)。两者经常同时发生,但根因不同。生产实践里数据质量Drift是早期信号,提前几天发现,能避免模型上线后才发现性能下降。我在出海硬件公司给推荐模型加了上游数据Drift监控,提前3天发现了数据源问题,模型团队来得及切换数据源。
追问二:Drift检测会不会和业务正常变化(季节性、活动)混淆?
会。两个应对:第一,基线滚动更新——基线不固定,每周更新一次,自然适应缓慢漂移;第二,分场景配阈值——大促期间放宽阈值,平日严格。最重要的是引入业务标注闭环,运营/产品标注「这次漂移是预期的」,模型学习这种标注,下次类似情况自动豁免。
追问三:实时数据流怎么做Drift检测?
实时Drift检测有两个挑战:算分布需要批量数据、状态存储成本高。常见方案:①小窗口聚合(5分钟桶)+ Flink State维护近1小时分布;②采样落到Hologres/Doris,每分钟跑一次SQL对比;③用流式直方图算法(如DDSketch)增量维护分位数。我在某元宇宙社交应用用方案②,简单可控,延迟5分钟可接受。
实战踩坑:埋点版本升级悄悄改了枚举值
出海硬件公司有次客户端SDK升级,把「device_type」字段的取值从{phone, tablet}扩成了{phone, tablet, watch, tv}。开发同学没通知数据团队,DQC的枚举校验是「在白名单内」——新增的watch/tv在白名单外!但DQC规则是「白名单匹配率>99%」,新增类型只有2%占比,规则没触发。
一周后下游某模型预测精度下降——它按device_type分桶训练,新出现的watch/tv被归到「其他」类,特征丢失。模型团队排查了一周才找到根因。
解决方案:加上类别Drift检测——「新增类别立即P0告警」。修复后又类似事件出现3次,每次SDK升级都被Drift监控提前抓到。
-- 类别字段的「新值检测」
-- 这种规则比传统DQC更敏感
WITH historical_set AS (
SELECT DISTINCT device_type FROM dwd_event
WHERE dt BETWEEN DATE_SUB('${bizdate}', 30) AND DATE_SUB('${bizdate}', 1)
),
today_set AS (
SELECT DISTINCT device_type FROM dwd_event WHERE dt = '${bizdate}'
)
SELECT t.device_type AS new_value
FROM today_set t LEFT JOIN historical_set h USING (device_type)
WHERE h.device_type IS NULL;
-- 任何新值出现立即P0告警
教训:传统DQC规则是「形式校验」,Drift检测是「分布校验」,两者互补。只有Drift能抓到「形式没错但分布漂了」这种隐蔽问题。生产环境强烈建议两者都上。
总结
数据漂移三类(协变量/标签/概念)+ 三大检测方法(PSI/KS/卡方)+ 工程化基线管理。Drift是DQC的盲点,必须用专门方法。面试时按方法对比+工程化实现+SDK升级故事讲清楚,能体现你对数据质量的纵深理解。
下一篇聊一个超高频Schema话题:上游schema改了下游全挂——Schema Evolution怎么治?
你们生产环境有Drift监控吗?用什么方法?评论区聊聊~
关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长
数据质量面试通关手册
第11-15篇:Schema、契约、止损
Wood聊大数据
9年大数据老兵,聊技术、聊职场、聊成长