订阅更新

某指标突增200%是真的还是脏数据?——统计学异常检测落地

数据质量手册 第 6 题 约 9 分钟读完

本文目录
  1. 先说结论
  2. 方法一:3σ法则(最经典)
  3. 方法二:MAD(中位数绝对偏差,更鲁棒)
  4. 方法三:时间序列分解(处理周期性)
  5. 三种方法对比
  6. 实战:我在出海硬件的混合方案
  7. 面试高频追问
  8. 实战踩坑:误报炸锅的那次大促
  9. 总结

先说结论

异常检测的三层方法论:固定阈值(最简单)→ 统计学方法(3σ/MAD/Z-score)→ 时序模型(Prophet/STL分解)。每一层适用不同场景:

• 固定阈值:业务有明确边界的字段,比如年龄0-150

• 统计学方法:分布稳定的指标,比如订单量、转化率

• 时序模型:有明显周期性的指标,比如DAU、GMV(周末/节假日波动大)

不要一刀切用一种方法。混合使用、按指标特性选择,才能既减少误报又抓住真异常。

先把这段讲顺;面试官追问时,再展开下面的原理和细节。

详细讲解

「老板说今天的DAU突然涨了200%,让你赶紧看看是真是假」——这种场景每个数据人都遇到过。靠肉眼看波动、靠经验拍脑袋判断都不靠谱。这道题考的是你能不能用统计学方法做异常检测,给出置信度高的结论。我在出海硬件公司搭建过一套基于3σ+MAD+季节性分解的异常检测体系,覆盖800+核心指标,今天聊聊到底怎么做。

方法一:3σ法则(最经典)

假设数据近似正态分布,超过均值±3倍标准差就是异常(概率<0.3%)。

-- 用过去30天数据计算基线(均值+标准差)
WITH baseline AS (
  SELECT
    AVG(metric_value) AS mu,
    STDDEV(metric_value) AS sigma
  FROM dwd_metric_daily
  WHERE metric = 'dau'
    AND dt BETWEEN DATE_SUB('${bizdate}', 30) AND DATE_SUB('${bizdate}', 1)
),
today AS (
  SELECT metric_value AS today_v FROM dwd_metric_daily
  WHERE metric = 'dau' AND dt = '${bizdate}'
)
SELECT today_v, mu, sigma,
  (today_v - mu) / sigma AS z_score,
  CASE WHEN ABS((today_v-mu)/sigma) > 3 THEN 'ANOMALY' ELSE 'NORMAL' END AS flag
FROM today, baseline;

适用场景:分布近似正态、无强周期性的指标。

不适用:有周期性的指标(DAU周末天然偏低)、长尾分布的指标(订单金额这类有少量超大值)。强行用3σ会误报一片。

方法二:MAD(中位数绝对偏差,更鲁棒)

3σ的弱点是均值和标准差对离群值敏感——一个极端值就会把sigma拉大,导致后续异常检测失效。MAD用中位数代替均值,鲁棒性强很多:

import numpy as np
 
def mad_anomaly_detect(history, today_value, k=3):
    """基于MAD的异常检测
    history: 过去N天的值
    today_value: 今日值
    k: 阈值倍数,常用2.5-3
    """
    median = np.median(history)
    mad = np.median(np.abs(history - median))
    # 经验系数1.4826把MAD换算成标准差等价值
    modified_z = 0.6745 * (today_value - median) / mad
    return abs(modified_z) > k, modified_z
 
# 实战:DAU过去30天 [10000, 10500, 9800, ..., 50000(异常值)]
# 3σ会被那个50000拉偏,MAD不会

MAD是我在出海硬件公司日常用得最多的方法。订单量、用户量这类指标偶尔会因为运营活动出现脉冲,MAD能稳住基线不被脉冲带偏。

方法三:时间序列分解(处理周期性)

DAU周末比工作日低、电商大促期间GMV会暴涨——这些都是「正常波动」,简单的3σ/MAD会误报。时间序列分解(STL Decomposition)把指标拆成趋势+季节性+残差,对残差做异常检测才准确。

from statsmodels.tsa.seasonal import STL
import pandas as pd
 
# 把每日DAU序列做STL分解
ts = pd.Series(dau_history, index=pd.date_range('2024-01-01', periods=90))
stl = STL(ts, period=7, robust=True)  # period=7 周周期
result = stl.fit()
 
trend = result.trend           # 趋势项
seasonal = result.seasonal     # 周期项
resid = result.resid           # 残差项(这是要检测异常的部分)
 
# 对残差做MAD检测
mad = np.median(np.abs(resid - np.median(resid)))
modified_z = 0.6745 * (resid.iloc[-1] - np.median(resid)) / mad
is_anomaly = abs(modified_z) > 3

Prophet是Facebook开源的时序预测库,自带异常检测能力,比手写STL方便。我在出海硬件公司做大促期间的GMV监控就是用Prophet——它能自动学到「双11、黑五、Prime Day」这类高峰,避免大促当天误报。

三种方法对比

方法假设优点缺点适用场景
固定阈值业务有明确边界极简单、无误报覆盖窄数值范围、枚举校验
3σ正态分布、无周期易实现对离群值敏感、不抗周期稳定平稳指标
MAD无分布假设鲁棒、抗离群点计算稍复杂大部分业务指标
STL/Prophet有趋势+周期处理周期性最准需要历史数据多DAU/GMV等周期性指标

实战:我在出海硬件的混合方案

单一方法不够,我设计了一套「漏斗式」检测体系:

def hybrid_detect(metric_name, today_value, history):
    # Layer 1: 固定阈值(业务边界)
    if today_value < 0 or today_value > BUSINESS_MAX[metric_name]:
        return ('P0', 'business_boundary_violated')
    
    # Layer 2: 数据缺失检测
    if today_value is None or today_value == 0:
        return ('P0', 'data_missing')
    
    # Layer 3: STL分解 + MAD检测残差
    if len(history) >= 30 and has_seasonality(metric_name):
        is_anomaly, score = stl_mad_detect(history, today_value)
    else:
        is_anomaly, score = mad_detect(history, today_value)
    
    # Layer 4: 动态分级
    if is_anomaly:
        if abs(score) > 5: return ('P0', f'severe_anomaly_z={score:.1f}')
        if abs(score) > 3: return ('P1', f'anomaly_z={score:.1f}')
        return ('P2', f'mild_anomaly_z={score:.1f}')
    return ('OK', None)

面试高频追问

追问一:业务大促或者突发活动怎么办?算异常吗?

两个思路:第一,白名单机制——把已知活动日期录入系统(双11、Black Friday),那一天阈值放大3倍或直接跳过;第二,Prophet的holidays参数——把节假日和大促作为已知事件输入,模型自动学习这些日子的高峰。我在出海硬件公司用第二种,效果非常好——双11当天涨80%系统自动判定为「正常的预期高峰」。

追问二:异常检测出来很多假阳性,怎么调?

三步:第一,复盘历史告警,把假阳性按原因分类(周期性/突发活动/真异常);第二,对症下药——周期性问题加STL、突发活动加白名单、真异常调整阈值;第三,引入人工反馈闭环——值班同学标记「这是误报」,下次模型自动学习。我在出海硬件公司这套做下来,假阳性率从30%降到5%以下。

追问三:实时异常检测和T+1有什么区别?

T+1可以用复杂模型(Prophet训练慢但效果好);实时必须轻量(移动平均+EWMA+滑动MAD)。实时检测的难点不是算法,而是基线维护——必须用滑动窗口,否则历史一变阈值就跟着变。Flink里实现滑动MAD用Window+ProcessFunction,存最近1000个值,每条新数据来增量更新中位数。

实战踩坑:误报炸锅的那次大促

Black Friday大促前夜,我把所有指标的异常检测都设成3σ告警。第二天早上7点钉钉群被告警炸成了刷屏:DAU涨150%、GMV涨300%、广告点击涨200%……值班同学打了20+电话以为出大事了,结果都是大促真实流量。

根因:3σ对周期性和突发活动完全无防御。当晚我加了三个补救机制:1)白名单大促日期;2)所有日级指标改用STL+MAD;3)告警必须带「同环比+历史对比图」,让值班一眼看出是「真异常」还是「预期高峰」。

教训:异常检测的部署要先观察一周再正式上线告警。看完一周的告警分布,调好阈值和白名单,再正式接入告警通道。直接上线一定会有海量误报,把告警的可信度毁掉。

总结

异常检测三层方法:固定阈值/3σ/MAD/STL+Prophet。生产环境用混合方案,按指标特性选择。配合白名单+人工反馈+分级响应,才能既抓异常又不刷屏。面试时把方法对比+混合架构+大促误报故事讲完,这道题就稳了。

下一篇聊一个流批一致性的硬核话题:实时数仓的数据和离线对不上——流批对账(Reconciliation)框架怎么搭?

你们生产环境用什么方法做异常检测?踩过什么坑?评论区聊聊~

关注「Wood聊大数据」| 9年大数据老兵,聊技术、聊职场、聊成长

刷题进度保存在当前浏览器