4. 统计学方法:Z-score、IQR规则、Grubbs检验

做异常检测,统计方法是入门首选。你不用上来就上深度学习,很多时候一个 Z-score 就能把脏数据揪出来。我个人习惯是先做一轮快速统计筛查,再看要不要上复杂模型。这节我们就聊三个最常用的统计工具:Z-score、IQR 规则和 Grubbs 检验。

4.1 Z-score:标准化距离法

Z-score 说白了就是告诉你:「这个点离均值有多远?」公式很简单:

Z = (x - μ) / σ

其中 μ 是均值,σ 是标准差。如果一个点的 Z-score 值超过某个阈值,就标记为异常。常用阈值是 3。为什么呢?因为正态分布下,超过均值 ±3σ 的概率只有 0.27% 左右。

我在项目中遇到过一个问题:某电商的支付金额数据,明明呈长尾分布,硬套 Z-score 阈值 3,结果把正常的大额交易全标成了异常。后来我改用修正的 Z-score(基于中位数和 MAD),效果才好转。

小技巧:对非正态分布的数据,别直接用原始 Z-score。试试 median + IQR 或 MAD 版 Z-score。

Python 实现也很直接:

import numpy as np
from scipy import stats

def detect_zscore(data, threshold=3):
    z = np.abs(stats.zscore(data))
    return np.where(z > threshold)[0]

# 示例
data = [10, 12, 12, 13, 12, 11, 14, 100, 13, 12]
outliers = detect_zscore(data)
print(outliers)  # 索引 7

4.2 IQR规则:不用假设分布

Z-score 对分布有要求,IQR 规则就不一样了。它只看数据的分位数,完全不关心数据是不是正态的。公式:

IQR = Q3 - Q1
上界 = Q3 + 1.5 * IQR
下界 = Q1 - 1.5 * IQR

超出上下界的就认为是异常。这个 1.5 倍是 Tukey 老爷子提出来的经验值,用在箱线图上特别经典。

我记得有一次做传感器故障诊断,数据是明显的偏态分布,Z-score 完全失灵。换成 IQR 规则后,一下就找出了十几个传感器漂移点。嗯,这里要注意:如果数据本身就有很多异常,Q1/Q3 的估计会被污染。我建议先用少量干净数据算 IQR 边界,再用这个边界去筛查全量数据。

注意:IQR 规则对尾部极端值敏感度较低。如果异常点恰好落在 Q1 和 Q3 之间,它可能漏检。也别忘了,1.5 倍只是默认值,你可以根据业务容忍度调整到 2.0 或 3.0。

代码实现:

def detect_iqr(data, multiplier=1.5):
    q1 = np.percentile(data, 25)
    q3 = np.percentile(data, 75)
    iqr = q3 - q1
    lower = q1 - multiplier * iqr
    upper = q3 + multiplier * iqr
    return np.where((data < lower) | (data > upper))[0]

data = [10, 12, 13, 12, 11, 14, 100, 13, 12]
print(detect_iqr(data))  # [6]

4.3 Grubbs检验:单点离群检测

Grubbs 检验是更「严谨」的方法。它假设数据来自正态分布,然后检验最大值或最小值是不是离群点。检验统计量就是 Z-score 的变体:

G = max(|xi - μ|) / σ

然后查表(或者用 p 值判断)看是否拒绝「无离群点」的原假设。Grubbs 检验一次只能检测一个极端点,检测完如果发现异常,剔除它,再对剩下的数据重复检验。

我在一个金融风控项目里用过它。客户要求每个异常点都要有统计显著性说明,Z-score 和 IQR 都拿不出 p 值,Grubbs 检验正好满足合规要求。当然,它也有个坑:数据量太小的时候检验力不足,少于 6 个样本就别用了。

适用场景对比:
  • Z-score:数据近似正态,样本量大,快速筛查。
  • IQR:分布未知或偏态,箱线图场景,无参数假设。
  • Grubbs:需要统计显著性证明,单点逐一检验。

Python 里用 outliers 库或自己实现:

from scipy import stats as sp_stats

def grubbs_test(data, alpha=0.05):
    mean = np.mean(data)
    std = np.std(data, ddof=1)
    z = np.abs(data - mean) / std
    max_idx = np.argmax(z)
    g = z[max_idx]
    n = len(data)
    # 计算临界值
    t_crit = sp_stats.t.ppf(1 - alpha / (2 * n), n - 2)
    g_crit = (n - 1) / np.sqrt(n) * np.sqrt(t_crit**2 / (n - 2 + t_crit**2))
    if g > g_crit:
        return max_idx  # 异常点索引
    return None

data = [10, 12, 13, 12, 11, 14, 100, 13, 12]
print(grubbs_test(data))  # 6

4.4 三种方法的选择策略

你想想看,什么时候用什么?我整理了一个表格,方便你快速对号入座:

方法分布假设检出能力适用场景主要限制
Z-score正态多异常同时初步快速筛查对偏态、多模态失效
IQR规则无多异常同时箱线图、探索性分析尾部异常灵敏度低
Grubbs检验正态单点逐一需要 p 值的正式报告一次只能检一个,多次检验膨胀 I 类错误

我曾经接过一个需求:传感器数据流每小时跑一次异常检测,样本量 100 左右。我第一版用 IQR 加滑动窗口,结果早晚高峰的正常波动也被标记了。后来改成 Z-score 用滚动均值和滚动标准差,阈值设成 3.5,才算稳定。所以啊,没有万能方法,得根据数据特点调。

4.5 流程总览

下面这张图概括了三个方法的使用路径。你可以把它贴在工位上,下次选方法时扫一眼。

统计异常检测方法选择流程 数据输入 正态分布? Z-score 快速筛查 IQR规则(无参数) Grubbs 逐点检验 是 否 需p值 标记离群点 建议:先用 IQR 做快速探索,再用 Z-score 或 Grubbs 做正式验证

4.6 避坑指南

我来给你盘点几个我踩过的坑:

  • 多重比较问题:Grubbs 多次应用后,I 类错误率会膨胀。我用过 Bonferroni 校正来修正显著性水平。
  • 样本量太小:少于 30 个样本时,Z-score 和 Grubbs 的统计效力都不高。这时候我倾向用 IQR 加上领域知识。
  • 异常比例过高:如果数据里异常占了 10% 以上,Z-score 的均值和标准差会被拉偏。我曾经用 Winsorize(截尾)预处理后再算统计量。
  • 业务规则优先级:统计方法只告诉你「统计上的异常」,不一定代表业务上有用。记得加上业务判断。
曾经我遇到的坑:有一个批次检测数据,用 Z-score 彻底没检出异常,但我知道生产上确实有故障。一查原因,数据是双峰分布——正态假设错了。换成 IQR 后立刻找出真异常。所以第一步永远是画分布图,别盲目套公式。

4.7 小结

三种方法各有脾气。Z-score 快但挑分布,IQR 皮实但尾部钝感,Grubbs 严谨但只能单挑。实践中我通常这样搭配:先用 IQR 做个箱线图快速定位,再用 Grubbs 给关键异常点算个 p 值写进报告。如果数据量大且近似正态,直接上 Z-score 配合滑动窗口。你根据手里数据的性格来选,肯定不翻车。