统计方法一: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的计算步骤:
- 计算数据的中位数 M
- 计算每个点与中位数的绝对偏差 |x_i - M|
- 取这些绝对偏差的中位数
- 乘上缩放因子 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后,误报率直接降了60%。这让我深刻体会到:选错方法比选错阈值更致命。
还有几个细节得注意:
- MAD等于0的情况:如果超过一半的数据完全相同,MAD可能为零。代码里要加除数保护
- 多模态分布:Z-score家族默认数据是单峰的。如果数据有两个峰,算出来的均值和MAD都没意义。这种情况我会换DBSCAN或其他密度方法
- 阈值不要死记:3.5是约定俗成的推荐值,不代表它适合你的数据。跑一遍看看效果,再微调才是正道
一句话总结
原版Z-score适合干净数据快速筛查,修正Z-score适合含离群点的实际数据。如果你不确定数据的污染程度,从修正Z-score入手更安全。绝大多数工业场景,我都会默认上修正Z-score——省心,少踩坑。