第4章 Z‑score 方法 —— 最基础的统计异常检测

Z‑score 方法,说白了就是看你的数据点偏离平均值的“标准差个数”。我刚开始做数据清洗时,第一个学会的异常检测方法就是它。简单、直观,而且能快速跑出结果。不过你想想看,它这么简单,是不是有什么坑?嗯,我们一点点拆开讲。

什么是 Z‑score?

Z‑score 又叫标准分数。它告诉你一个数据点和均值的距离,用标准差做尺子量。公式很简单:

Z = (x − μ) / σ

算出来如果是 2.5,意思就是该点比均值高出 2.5 个标准差。如果 Z 的值很大,那这个点就可能是异常。我通常把 |Z| > 3 当作可疑点,但这只是经验值,具体看场景。

阈值怎么选?

阈值不是拍脑袋定的。我习惯先画个分布图,看看数据长什么样。下面这张表你参考一下:

阈值(|Z|)对应概率(正态分布)每百万数据中的异常数(近似)
2.095.45%45,500
2.598.76%12,400
3.099.73%2,700
3.599.95%465
4.099.993%63

看明白了吗?阈值设 2 会抓到很多正常点(误报高),设 4 又可能漏掉真正的异常。我一般从 3 开始试,再根据业务容忍度微调。

Z‑score 的适用条件

这个方法有个大前提:数据近似服从正态分布。如果不满足,计算出来的 Z‑score 可能不准。我在项目中遇到过一回路基站的流量数据,分布是长尾的,直接用 Z‑score 阈值 3,结果漏了一大半异常。后来改用修正的 Z‑score(用中位数替代均值,用 MAD 替代标准差)才解决问题。

所以用之前,先做两步:

  • 画直方图或 Q-Q 图,看数据是否大致对称、钟形。
  • 检查有没有严重离群点——它们会拉偏均值,让 Z‑score 失效。

另外,Z‑score 对样本量也有要求。样本太少(比如小于 10),均值和标准差不稳定,算出来的 Z 值没啥参考意义。我建议至少 30 个样本再开始玩 Z‑score。

Python 实战:用 Z‑score 检测异常

写代码之前,先看看整体流程。我画了张图,帮你理清思路:

Z‑score 异常检测流程 输入数据 x 计算 μ 与 σ 计算每个点的 Z |Z| > 阈值 T? 标记为异常 标记为正常 常见阈值 T 2.0 / 2.5 / 3.0 / 3.5

下面是我常用的 Z‑score 检测函数,加了可选的阈值参数,业务直接调:

import numpy as np

def detect_outliers_zscore(data, threshold=3.0):
    """
    基于 Z-score 的异常检测
    :param data: array-like
    :param threshold: 阈值,默认 3.0
    :return: (布尔数组 True=异常, 异常索引, 异常值列表)
    """
    data = np.array(data)
    mean = np.mean(data)
    std = np.std(data)
    if std == 0:
        raise ValueError("标准差为0,数据无变化,无法计算Z-score")
    z_scores = (data - mean) / std
    outlier_mask = np.abs(z_scores) > threshold
    outlier_indices = np.where(outlier_mask)[0]
    outlier_values = data[outlier_mask]
    return outlier_mask, outlier_indices, outlier_values

# 举个栗子
data = [2.1, 2.2, 2.0, 2.3, 2.1, 25.0, 2.2, 2.0, 2.1]
mask, idx, vals = detect_outliers_zscore(data, threshold=3.0)
print(f"异常索引: {idx}")
print(f"异常值: {vals}")

运行结果你会看到索引5的 25.0 被揪出来了。嗯,干活就是这么快。

优缺点与避坑

先说说好处:计算成本极低,O(n) 搞定;结果可解释性强;适合做初筛。但缺点也不少:

  • 对异常值敏感:一个极端值就能把均值和标准差拉跑偏。效果是 Z‑score 可能“免疫”了真正的异常。
  • 依赖正态分布:数据严重偏态时误差很大。我曾经处理过用户点击时长数据,分布是右偏的,直接 Z‑score 结果惨不忍睹。后来改用中位数和中位差(MAD)才解决了。
  • 阈值主观:没有“标准答案”,需要结合业务调参。
我曾经踩过的坑: 有一回做交易流水的异常检测,数据里混进了一个巨额转账,均值被拉得很高,导致其他正常大额交易算出来的 Z‑score 反而很小。我一查,原来第一版代码用的就是原生 Z‑score。后来我改成先剔除明显离群点后再算均值,或者直接用稳健版 Z‑score,这才稳了。

另外,Z‑score 也假设数据是独立同分布的。如果你的数据有时序相关性(比如传感器连续采集),那直接套 Z‑score 会有一堆误报,因为趋势变化会被当成异常。这种情况请参考后续章节的移动窗口法。

小技巧: 可以用 scipy.stats.zscore 快速计算,但它不带阈值判断,自己封装一层就够了。

总结一下:Z‑score 是一把快刀,但要看清楚脑袋的形状再用。做异常检测没有银弹,Z‑score 价值在于简单、可解释、能快速落地。我建议你把它当作检测流水线的第一道筛子,先把明显离谱的点标记出来,再上更精细的方法。