第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σ 阈值设置流程 原始数据 正态性检验 (Shapiro/Q-Q) 正态? 是 计算 μ ± kσ (k值可调) 设置上下阈值 否 数据变换/其他方法 关键检查点 • 偏度/峰度是否接近0 • Q-Q图是否直线 • p值 > 0.05 可接受 • 样本量 > 30 基本安全

这个流程简单直接:先验正态,再根据正态与否分两条路走。如果你实在懒得做检验,也可以直接用调整k值的方法——但我会建议你先画个直方图,心里有底。

3.5 实战中的几个坑

最后分享几个我踩过的雷,希望你别走弯路。

  • 数据中包含极端值:如果原始数据本身就有异常点,你用这些数据算出来的均值和标准差已经“污染”了。处理方法:先用中位数和MAD(中位数偏差)做一次粗筛,或者用去除极端值后的样本重新计算μ和σ。
  • 数据有趋势或周期性:比如一天内流量有高峰低谷,这时候直接套3-sigma会把正常的高峰判异常。要先做差分或分解,去掉趋势和周期成分,再对残差做阈值。
  • 样本量太小:少于30个样本,3-sigma几乎没用。我曾遇到只拿到10个点的数据,硬套公式,结果阈值被一个离群点完全带跑偏。后来改用IQR法(四分位距)才稳住。

避坑指南:我曾经在一个金融项目里,直接用原始数据的均值和标准差设阈值。结果因为之前发生过一次大额异常交易,μ被拉高,σ被撑大,之后相同的异常反而检测不出来了。这就是经典的“污染效应”——异常数据污染了正常基线。每次计算前,一定先做离群点剔除或使用稳健统计量(如中位数和MAD)。

好了,这一章的内容就这些。均值±标准差方法上手很快,但别忘了它背后的两个基本假设:数据正态、无污染。如果你能处理好这两个前提,这个方法就是最稳的“第一基石”。