统计方法一:Z-score与修正Z-score

异常值检测,说白了就是在一堆数据里找出那些「不太对劲」的点。而Z-score,是最经典、也最直观的方法之一。我个人习惯把它叫做「标准差尺子」——用数据分布的平均值和离散程度,给每个点量个「身高」,偏离太远的就被标记出来。

Z-score的核心原理

Z-score的本质很简单:它告诉你一个数据点离平均值有多远,用标准差做单位。公式长这样:

z = (x - μ) / σ

其中x是原始值,μ是均值,σ是标准差。算出来的z值如果是2.0,意味着这个点比平均值高了两个标准差。

为什么这能检测异常?因为正态分布下,超过99.7%的数据都落在±3个标准差之内。z值大于3或小于-3的点,天然就很可疑。

判定规则

  • |z| > 3:强烈异常,建议剔除
  • 2 < |z| ≤ 3:疑似异常,需人工复核
  • |z| ≤ 2:正常范围

阈值灵活调整

在实际项目中,3这个阈值不是死规矩。对业务风险高的场景(比如医疗指标),我可能会降到2.5;对噪声容忍度高的场景(比如日志采集),用到3.5都行。说白了,阈值就是个开关,你自己拧到合适的位置就好。

Python实现:手写一个Z-score检测

搞懂原理后,我们动手写代码。我习惯用numpy实现,这样能利用向量化运算,跑大型数据集也不怕。

import numpy as np
import pandas as pd

def zscore_detection(data, threshold=3.0):
    """
    基于Z-score的异常值检测

    参数:
        data: 一维数组或Series
        threshold: z值阈值,默认3.0

    返回:
        anomalies: 异常值位置(布尔索引)
        z_scores: 每个点的z值
    """
    data = np.array(data)
    mean = np.mean(data)
    std = np.std(data)

    z_scores = (data - mean) / std
    anomalies = np.abs(z_scores) > threshold

    return anomalies, z_scores

# 示例数据
np.random.seed(42)
normal = np.random.normal(50, 10, 1000)
outliers = np.array([10, 95, 120])
data = np.concatenate([normal, outliers])

anomalies, z_scores = zscore_detection(data, threshold=3.0)
print(f"检测到异常值数量: {np.sum(anomalies)}")
print(f"最大z值: {np.max(np.abs(z_scores)):.4f}")

输出结果很直接。运行时你会发现,那几个人工添加的离群点全被标红了。嗯,基本盘就是这样。

Z-score的硬伤:均值与标准差不抗揍

到这里你可能会觉得:这不挺好吗?但我得说说Z-score的致命弱点——它怕极端值。

均值μ和标准差σ,本身对异常值极度敏感。想一想,如果数据里混入了一个1000的大离群点,均值会被拉偏,标准差会被撑大。结果就是,原本正常的点可能被「掩盖」成正常,而真正的异常反而漏检了。这就是所谓的掩蔽效应。

掩蔽效应的实战教训

我曾经在一个传感器数据清洗项目中吃过这个亏。一批温度数据里混了几个坏值,用普通Z-score一跑,阈值自动升高,坏值都被「合法」地放过去了。查了两天才发现问题出在均值太脆皮上。从那以后,我再也不在原始Z-score上裸奔了。

修正Z-score:用中位数和MAD来救场

既然均值不抗揍,我们就换它。修正Z-score(也叫稳健Z-score)的核心在于:把均值换成中位数,把标准差换成中位数绝对偏差(MAD)。

MAD的计算步骤:

  1. 计算数据的中位数 M
  2. 计算每个点与中位数的绝对偏差 |x_i - M|
  3. 取这些绝对偏差的中位数
  4. 乘上缩放因子 1.4826(为了保证与正态分布标准差的一致性)
def mad_based_zscore(data, threshold=3.5):
    """
    基于MAD的修正Z-score

    参数:
        data: 一维数组或Series
        threshold: 修正z值阈值,常用3.5

    返回:
        anomalies: 异常位置(布尔索引)
        mod_z_scores: 修正后的z值
    """
    data = np.array(data)
    median = np.median(data)
    mad = np.median(np.abs(data - median))
    # 缩放因子
    scaled_mad = mad * 1.4826

    # 避免除以零
    if scaled_mad == 0:
        scaled_mad = np.finfo(float).eps

    mod_z_scores = 0.6745 * (data - median) / scaled_mad
    anomalies = np.abs(mod_z_scores) > threshold

    return anomalies, mod_z_scores

0.6745这个数从哪来?

简单说,0.6745是为了让修正Z-score在正态分布下与原Z-score对齐。它的倒数刚好是1.4826。你不用背,用的时候直接乘即可。

两种方法的核心对比

我把它们放一起,方便你直观感受差异:

对比维度 原版Z-score 修正Z-score
中心度量 均值(易受异常影响) 中位数(稳健)
离散度量 标准差(易受异常影响) MAD(稳健)
对异常的抗性 弱,易产生掩蔽效应 强,破坏点率高达50%
推荐阈值 3.0 3.5
适用数据量 ≥ 30 ≥ 10

什么时候该用哪个?

我个人经验是这样的:

  • 数据干净、分布对称、无极端异常:用原版Z-score,计算快,理解成本低
  • 数据含明显离群点、分布偏斜:用修正Z-score,它扛得住
  • 小样本(10-30个点):修正Z-score更靠谱,原版在小样本时方差太大
  • 实时流式数据:我会谨慎用两者,因为均值/中位数需要不断更新,更喜欢后续章节的滑动窗口法

完整流程的可视化

下面这张图帮你理清两种方法的选择路径:

Z-score与修正Z-score检测流程 Z-score 与 修正 Z-score 检测流程对比 原始数据 数据 分布对称? 原版 Z-score 用均值+标准差 阈值 |z| > 3 修正 Z-score 用中位数+MAD 阈值 |z| > 3.5 输出异常标记及z值 中小样本、含离群点 → 优先选修正Z-score 大样本、无异常 → 原版Z-score

避坑指南与实用建议

我曾经在金融交易数据上同时跑两种方法做对比。原版Z-score把正常的波动都标成了异常,因为均值被几个巨额交易拉偏了;改用修正Z-score后,误报率直接降了60%。这让我深刻体会到:选错方法比选错阈值更致命。

还有几个细节得注意:

  • MAD等于0的情况:如果超过一半的数据完全相同,MAD可能为零。代码里要加除数保护
  • 多模态分布:Z-score家族默认数据是单峰的。如果数据有两个峰,算出来的均值和MAD都没意义。这种情况我会换DBSCAN或其他密度方法
  • 阈值不要死记:3.5是约定俗成的推荐值,不代表它适合你的数据。跑一遍看看效果,再微调才是正道

一句话总结

原版Z-score适合干净数据快速筛查,修正Z-score适合含离群点的实际数据。如果你不确定数据的污染程度,从修正Z-score入手更安全。绝大多数工业场景,我都会默认上修正Z-score——省心,少踩坑。