4. 改进Z-Score:MAD方法、稳健Z-Score、实战对比
上一节我们聊了传统Z-Score,你可能会问:这方法看着挺美,但实际用起来咋样?
说实话,我在真实项目中吃过它的亏。有一次处理某流域的水质监测数据,pH值有十几个点明显异常,但Z-Score愣是没识别出来。后来一查原因——数据里有个极端离群值,直接把均值和标准差都带偏了。
这就是经典Z-Score的命门:它不稳健。均值和标准差对异常值太敏感了。一个极端值就能把整个判断标准给扭曲掉。
那怎么办?我们需要更皮实的方法。今天的主角——MAD(中位数绝对偏差)和稳健Z-Score,就是来救场的。
4.1 为什么MAD比标准差更靠谱?
先想一个问题:为什么均值容易被带偏?
因为均值是“求和再平均”,一个极端大的数,直接拉高整个和。中位数就不一样了,它只关心中间那个数。你数据里有一万个1和一个一亿,中位数还是1,稳如泰山。
MAD就是基于中位数的离散度度量。它的计算分三步:
- 先算数据的中位数(Median)
- 每个数据点减去中位数,取绝对值
- 再对这些绝对值取中位数
公式长这样:
MAD = median(|xᵢ - median(X)|)
举个例子,假设我们有5个pH值数据:
数据:6.8, 7.1, 7.3, 7.0, 9.5
中位数 = 7.1
绝对偏差:|6.8-7.1|=0.3, |7.1-7.1|=0, |7.3-7.1|=0.2, |7.0-7.1|=0.1, |9.5-7.1|=2.4
排序后:0, 0.1, 0.2, 0.3, 2.4
MAD = 0.2
看到了吗?那个9.5的异常值,虽然它的绝对偏差很大(2.4),但MAD取的是中位数,所以完全不受影响。换成标准差试试?早就被这个9.5给拉飞了。
核心要点:MAD的“稳健性”来源于中位数。只要异常值不超过一半,MAD就能保持稳定。这在环境数据里太重要了——你永远不知道传感器什么时候会抽风。
4.2 稳健Z-Score:给Z-Score穿上防弹衣
有了MAD这个稳健的离散度指标,我们就可以改造Z-Score了。把原来的均值换成中位数,标准差换成MAD,就得到了稳健Z-Score:
稳健Z-Score = (xᵢ - median(X)) / (MAD × 1.4826)
等等,这个1.4826是啥?
嗯,这里有个小细节。为了让MAD和标准差在正态分布下“尺度一致”,需要乘一个常数。对于正态分布,这个常数大约是1.4826。说白了,就是让稳健Z-Score的阈值(比如±3)和传统Z-Score有可比性。
判断标准不变:
- |稳健Z-Score| > 3 → 视为异常
- |稳健Z-Score| > 2.5 → 视为可疑
我个人习惯用3作为硬阈值。但如果你数据量小(比如少于20个样本),可以适当放宽到2.5。我在处理某河流的溶解氧数据时就遇到过这种情况——样本只有15个,用3会漏掉一些明显的异常点。
4.3 实战对比:传统Z-Score vs 稳健Z-Score
光说不练假把式。我们拿一组真实的环境监测数据来比比看。
这是某污水处理厂一周的进水COD浓度数据(单位:mg/L):
数据:320, 335, 310, 345, 328, 330, 340, 315, 325, 332,
318, 342, 327, 338, 322, 335, 310, 328, 340, 325,
330, 335, 320, 328, 332, 338, 325, 330, 335, 328,
500, 320, 335, 328, 330
注意第31个数据是500,明显是个异常值(可能是传感器故障或进水管冲击)。
我们分别用两种方法算一下:
| 方法 | 中心值 | 离散度 | 第31个点的Z值 | 判定结果 |
|---|---|---|---|---|
| 传统Z-Score | 均值=332.6 | 标准差=28.4 | (500-332.6)/28.4 = 5.89 | ✅ 异常 |
| 稳健Z-Score | 中位数=330 | MAD=8.9 × 1.4826 = 13.2 | (500-330)/13.2 = 12.88 | ✅ 异常 |
看起来两种方法都识别出了这个异常。但别急,我们再看看其他数据点。
比如第3个数据310:
- 传统Z-Score:(310-332.6)/28.4 = -0.80 → 正常
- 稳健Z-Score:(310-330)/13.2 = -1.52 → 正常
再比如第8个数据315:
- 传统Z-Score:(315-332.6)/28.4 = -0.62 → 正常
- 稳健Z-Score:(315-330)/13.2 = -1.14 → 正常
看起来差不多?那我们换个场景——多个异常值污染的情况。
假设数据里有5个异常值(把最后5个数据改成600, 580, 620, 590, 610):
| 方法 | 中心值 | 离散度 | 正常点(320)的Z值 | 异常点(600)的Z值 |
|---|---|---|---|---|
| 传统Z-Score | 均值=368.4 | 标准差=98.7 | (320-368.4)/98.7 = -0.49 | (600-368.4)/98.7 = 2.35 |
| 稳健Z-Score | 中位数=330 | MAD=8.9 × 1.4826 = 13.2 | (320-330)/13.2 = -0.76 | (600-330)/13.2 = 20.45 |
发现问题了吗?
传统Z-Score被5个异常值拉偏了——均值从332.6飙升到368.4,标准差从28.4膨胀到98.7。结果那个600的异常值,Z值只有2.35,连3都不到,被判定为正常!
而稳健Z-Score呢?中位数和MAD几乎纹丝不动,600的Z值高达20.45,异常判定稳如磐石。
我曾经踩过这个坑:有一次分析某湖泊的总磷数据,用了传统Z-Score,结果把一批真正的异常值全放过去了。后来复盘才发现,数据里混入了几个极端值,把均值和标准差都污染了。从那以后,但凡做异常检测,我第一反应就是上稳健方法。
4.4 什么时候用MAD?什么时候用标准差?
你可能会问:那是不是以后都用MAD就行了?
也不是。我总结了几条经验:
- 数据干净、样本量大(>100) → 传统Z-Score够用,计算快
- 可能有少量异常值(<10%) → 稳健Z-Score更安全
- 异常值比例高(10%-40%) → 必须用MAD,传统方法基本失效
- 数据接近正态分布 → 两种方法差别不大,但稳健方法更保守
- 数据严重偏态 → 建议用MAD,甚至考虑分位数方法
我个人习惯是:只要数据量不大(<500),直接上稳健Z-Score。多花几毫秒计算时间,换来更可靠的结论,这笔买卖划算。
4.5 代码实现:Python示例
最后给个Python代码,方便你直接拿去用:
import numpy as np
def robust_zscore(data, threshold=3.0):
"""
计算稳健Z-Score并标记异常值
"""
data = np.array(data)
median = np.median(data)
mad = np.median(np.abs(data - median))
# 防止MAD为0(所有数据相同的情况)
if mad == 0:
mad = np.mean(np.abs(data - median))
# 计算稳健Z-Score
robust_z = 0.6745 * (data - median) / mad
# 标记异常
outliers = np.abs(robust_z) > threshold
return robust_z, outliers
# 示例数据
data = [320, 335, 310, 345, 328, 330, 340, 315, 325, 332,
318, 342, 327, 338, 322, 335, 310, 328, 340, 325,
330, 335, 320, 328, 332, 338, 325, 330, 335, 328,
500, 320, 335, 328, 330]
z_scores, outlier_flags = robust_zscore(data)
print("异常值索引:", np.where(outlier_flags)[0])
print("异常值数据:", np.array(data)[outlier_flags])
注意代码里有个小细节:我用了0.6745而不是1.4826。这两个常数是等价的,只是表达形式不同。0.6745是1/1.4826的近似值。用哪个都行,别搞混就好。
避坑指南:如果数据里所有值都相同(比如传感器死机),MAD会等于0。这时候代码里要加个保护,用均值绝对偏差代替。我就在一次批量处理时遇到过——1000个数据点全是0,直接除零报错,排查了半天。
4.6 本章小结
好了,我们来捋一捋:
- 传统Z-Score的软肋:均值和标准差不稳健,容易被异常值带偏
- MAD方法:用中位数代替均值,用中位数绝对偏差代替标准差,皮实耐造
- 稳健Z-Score:在MAD基础上乘个常数(1.4826或0.6745),让阈值和传统方法对齐
- 实战对比:数据被污染时,传统方法可能漏判,稳健方法依然可靠
下一节我们会聊另一种思路——基于分位数的异常检测方法(IQR和Tukey's fences)。到时候你会发现,不同方法各有各的脾气,选对工具比会算更重要。
公众号:蓝海资料掘金营,微信deep3321