第3章 基于均值与标准差的阈值:3-sigma、正态性检验与k值选择
讲阈值设置,最经典的方法就是拿均值和标准差做文章。你可能在想——这东西不是统计课就学过了吗?嗯,没错,但真正落地时坑可不少。
我个人习惯先用这个方法做快速判断。它简单、直观,而且大部分业务场景都能用。
3.1 3-sigma 原则:教科书里的金标准
假设你的数据服从正态分布。那么均值±3个标准差的范围,理论上能覆盖99.73%的数据。这个范围之外的,就标记为异常。
听起来很完美对吧?但我在项目中遇到过一个问题:很多真实数据根本不是正态的。硬套3-sigma,结果要么漏报一堆异常,要么把正常值全判成异常。
核心公式:上界 = μ + 3σ,下界 = μ - 3σ。超出上下界的点为异常点。
举个实际例子。你监控服务器CPU使用率,正常情况下在30%~70%之间波动。如果某天突然跳到99%,那肯定是异常。但如果是持续的小幅抖动(比如50%~55%),用3-sigma一般抓不住。这就是它的问题——对分布形态太敏感。
3.2 前提条件:数据正态性检验
说实话,我早期做异常检测时,拿到数据就套3-sigma,根本不管正态不正态。结果被老板叫过去问:你这阈值怎么一天一个样?后来才意识到,必须先做正态性检验。
常用的检验方法有几种:
| 方法 | 适用场景 | 注意事项 |
|---|---|---|
| Shapiro-Wilk | 小样本(n < 5000) | 最推荐,但大数据量太慢 |
| Kolmogorov-Smirnov | 任意样本量 | 需要指定分布参数,容易误判 |
| D'Agostino’s K² | 中大型样本 | 综合偏度和峰度,较稳健 |
避坑指南:我曾经用K-S检验去验证一个双峰分布的数据,p值显示“正态”,但实际直方图明显有两个峰。后来改用D'Agostino检验才发现问题。记住,p值不是万能的,一定要结合可视化。
当然,你还可以用Q-Q图(分位数-分位数图)肉眼判断。如果散点大致落在45度线上,说明数据近似正态。这个在我之前的项目里帮了大忙——那次客户数据量太大,所有统计检验都跑不动,我就用随机采样画Q-Q图,快速过了样本检查。
import numpy as np
from scipy import stats
# 模拟数据
data = np.random.normal(loc=50, scale=10, size=1000)
# Shapiro-Wilk检验
stat, p_value = stats.shapiro(data)
print(f'Shapiro-Wilk p-value: {p_value:.4f}')
# 如果p_value > 0.05,可以认为数据服从正态分布
if p_value > 0.05:
print('数据近似正态,放心用3-sigma')
else:
print('数据非正态,考虑其他方法或做变换')
3.3 k值选择:μ±kσ 的灵活调整
3-sigma只是一个特例。实际项目中,k取多少要看你对异常的容忍度。我通常把它看作一个超参数,通过业务损失函数来调。
为什么这么灵活?因为不同场景对“异常”的定义不一样。
- 银行反欺诈:假阳性代价高(误判好客户),可以用k=4~5,只暴露最极端的交易
- 工业质检:假阴性代价高(漏掉次品),可以用k=2~2.5,宁可误杀一千
- 运营监控:一般k=3是平衡点,既不过严也不过松
我做过一个电商大促流量监控的项目。平时流量平稳,但大促当天流量暴涨5倍,如果还用k=3,90%的数据都会判为异常——显然不对。后来我把k提高到5,只抓真正的“异常尖刺”(比如突然的0流量)。
调参建议:先画出数据直方图和箱线图,观察离群点的分布。然后从k=3开始,用验证集反复测试,找到使F1分数最大的k值。迭代3~5次就够了。
下面这个表格总结不同k值对应的覆盖范围,供你快速参考:
| k值 | 覆盖比例(正态) | 典型场景 |
|---|---|---|
| 1.0 | 68.27% | 极宽松,用于敏感检测 |
| 2.0 | 95.45% | 一般监控,中等容忍 |
| 3.0 | 99.73% | 经典阈值,平衡默认 |
| 4.0 | 99.9937% | 高容忍,仅在极端异常时告警 |
| 5.0 | 99.99994% | 极高容忍,几乎只抓离群点 |
注意:如果你的数据不是正态分布,这个覆盖比例就不靠谱了。所以前面说的正态性检验,一定不能跳过。
import numpy as np
def dynamic_k_sigma(data, k=3):
mean = np.mean(data)
std = np.std(data)
lower = mean - k * std
upper = mean + k * std
anomalies = (data < lower) | (data > upper)
return lower, upper, anomalies
# 示例:用k=3.5试试
data = [100, 102, 98, 101, 150, 97, 103]
l, u, flag = dynamic_k_sigma(data, k=3.5)
print(f'阈值范围: [{l:.2f}, {u:.2f}]')
print(f'异常标记: {flag}')
3.4 完整流程图:从原始数据到阈值
下面这张图是我自己梳理的决策流程,你可以直接套用。
这个流程简单直接:先验正态,再根据正态与否分两条路走。如果你实在懒得做检验,也可以直接用调整k值的方法——但我会建议你先画个直方图,心里有底。
3.5 实战中的几个坑
最后分享几个我踩过的雷,希望你别走弯路。
- 数据中包含极端值:如果原始数据本身就有异常点,你用这些数据算出来的均值和标准差已经“污染”了。处理方法:先用中位数和MAD(中位数偏差)做一次粗筛,或者用去除极端值后的样本重新计算μ和σ。
- 数据有趋势或周期性:比如一天内流量有高峰低谷,这时候直接套3-sigma会把正常的高峰判异常。要先做差分或分解,去掉趋势和周期成分,再对残差做阈值。
- 样本量太小:少于30个样本,3-sigma几乎没用。我曾遇到只拿到10个点的数据,硬套公式,结果阈值被一个离群点完全带跑偏。后来改用IQR法(四分位距)才稳住。
避坑指南:我曾经在一个金融项目里,直接用原始数据的均值和标准差设阈值。结果因为之前发生过一次大额异常交易,μ被拉高,σ被撑大,之后相同的异常反而检测不出来了。这就是经典的“污染效应”——异常数据污染了正常基线。每次计算前,一定先做离群点剔除或使用稳健统计量(如中位数和MAD)。
好了,这一章的内容就这些。均值±标准差方法上手很快,但别忘了它背后的两个基本假设:数据正态、无污染。如果你能处理好这两个前提,这个方法就是最稳的“第一基石”。