3. 统计方法基础:3σ原则、箱线图(IQR)、Z-Score与修正Z-Score、Grubbs检验
聊到异常检测,很多人第一反应就是上机器学习模型。但我个人习惯,拿到数据第一件事,永远是先用统计方法扫一遍。为什么?因为简单、可解释、还快。你想想看,如果3σ就能解决的问题,何必上深度学习?
这一章,我把最常用的几个统计方法串起来讲。它们都是基于一个核心假设:正常数据服从某种分布,异常点会偏离这个分布。说白了,就是找那些「不合群」的值。
核心思想:统计方法不依赖标签,直接从数据分布中定义异常。适合快速筛查、基线建立、以及作为复杂模型的预处理步骤。
3.1 3σ原则:最直观的边界
3σ原则,也叫拉依达准则。它假设数据服从正态分布,那么:
- 约68%的数据落在 μ ± σ 内
- 约95%的数据落在 μ ± 2σ 内
- 约99.7%的数据落在 μ ± 3σ 内
落在3σ之外的概率只有0.3%。所以,超过 μ ± 3σ 的点,我们就认为是异常。
我的经验:3σ对数据质量要求很高。如果数据本身有离群点,均值和标准差会被「拉偏」,导致漏检。我曾经在传感器数据上吃过这个亏——几个极端值把σ撑大了,真正的异常反而被包进去了。
代码实现很简单:
import numpy as np
def detect_3sigma(data):
mean = np.mean(data)
std = np.std(data)
lower = mean - 3 * std
upper = mean + 3 * std
anomalies = [x for x in data if x < lower or x > upper]
return anomalies, (lower, upper)
3.2 箱线图与IQR:不怕偏态
箱线图(Boxplot)是我最常用的可视化工具。它不依赖正态假设,用分位数来定义异常边界。
核心概念:
- Q1(第一四分位数):25%分位数
- Q3(第三四分位数):75%分位数
- IQR(四分位距):Q3 - Q1
- 异常边界:Q1 - 1.5×IQR 和 Q3 + 1.5×IQR
超出这个范围的点,就是箱线图定义的异常值。
注意:1.5倍IQR是经验值,不是金科玉律。在金融风控等场景,我有时会调到3倍IQR来减少误报。调参时记得结合业务容忍度。
代码示例:
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
anomalies = [x for x in data if x < lower or x > upper]
return anomalies, (lower, upper)
3.3 Z-Score与修正Z-Score
Z-Score衡量一个点偏离均值多少个标准差:
Z = (x - μ) / σ
通常 |Z| > 3 视为异常。但问题来了——如果数据本身包含异常,μ和σ会被污染。这就是Z-Score的脆弱性。
修正Z-Score(Modified Z-Score)用中位数和MAD(中位数偏差)代替均值和标准差:
MAD = median(|xᵢ - median(x)|)
MZ = 0.6745 × (x - median(x)) / MAD
0.6745这个系数,是为了让MAD在正态分布下与标准差一致。修正Z-Score对异常点更鲁棒,我强烈推荐在预处理阶段使用它。
对比总结:
| 方法 | 中心度量 | 离散度量 | 对异常鲁棒性 |
|---|---|---|---|
| Z-Score | 均值 | 标准差 | 低 |
| 修正Z-Score | 中位数 | MAD | 高 |
def modified_zscore(data, threshold=3.5):
median = np.median(data)
mad = np.median(np.abs(data - median))
if mad == 0:
return [] # 数据全相等,无异常
mz_scores = 0.6745 * (data - median) / mad
anomalies = data[np.abs(mz_scores) > threshold]
return anomalies.tolist()
避坑指南:我曾经在基因表达数据上用Z-Score,结果全是异常。后来换成修正Z-Score,才找到真正的差异表达基因。记住:数据有偏态或离群点,就别用Z-Score。
3.4 Grubbs检验:有统计依据的异常检测
Grubbs检验是一种假设检验方法。它每次只检测一个异常点,然后迭代进行。
步骤:
- 计算统计量 G = max(|xᵢ - μ|) / σ
- 查Grubbs临界值表,比较G与临界值
- 如果G大于临界值,判定该点为异常,移除后重复
- 直到没有异常点为止
Grubbs检验的优点是有严格的统计显著性,但缺点也很明显:
- 要求数据严格服从正态分布
- 每次只能检测一个点,效率低
- 样本量太小(<6)时基本不能用
我的建议:Grubbs检验适合小样本、高精度要求的场景,比如实验室测量数据。在大规模生产环境中,我很少用它——太慢了,而且正态假设太强。
from scipy import stats
def grubbs_test(data, alpha=0.05):
data = np.array(data)
n = len(data)
while True:
mean = np.mean(data)
std = np.std(data, ddof=1)
g = np.max(np.abs(data - mean)) / std
# 计算临界值
t_crit = 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:
# 移除最远的点
idx = np.argmax(np.abs(data - mean))
data = np.delete(data, idx)
n = len(data)
else:
break
return data.tolist() # 返回剔除异常后的数据
3.5 方法选型指南
讲了这么多,到底用哪个?我整理了一个选型表:
| 场景 | 推荐方法 | 原因 |
|---|---|---|
| 数据近似正态,样本量大 | 3σ / Z-Score | 简单快速,可解释性强 |
| 数据有偏态或离群点 | IQR / 修正Z-Score | 鲁棒性好,不受极端值影响 |
| 小样本,高精度要求 | Grubbs检验 | 有统计显著性,误报率可控 |
| 快速可视化探索 | 箱线图 | 直观,一眼看出异常分布 |
| 数据分布未知 | IQR | 无分布假设,适用性最广 |
最后说一句:统计方法不是万能的。它们假设异常是「极端值」,但现实中的异常可能是「模式异常」——比如传感器突然跳变后又恢复正常,均值没变,但方差变了。这种情况,统计方法就抓不住了。不过别急,后续章节会讲更高级的方法。
嗯,这一章的内容就到这里。统计方法虽然基础,但用好了能解决80%的异常检测问题。下次遇到异常检测任务,先别急着上模型,试试这些方法——你会发现,很多时候它们就够了。