3. 统计方法基础:3σ原则、箱线图(IQR)、Z-Score与修正Z-Score、Grubbs检验

聊到异常检测,很多人第一反应就是上机器学习模型。但我个人习惯,拿到数据第一件事,永远是先用统计方法扫一遍。为什么?因为简单、可解释、还快。你想想看,如果3σ就能解决的问题,何必上深度学习?

这一章,我把最常用的几个统计方法串起来讲。它们都是基于一个核心假设:正常数据服从某种分布,异常点会偏离这个分布。说白了,就是找那些「不合群」的值。

核心思想:统计方法不依赖标签,直接从数据分布中定义异常。适合快速筛查、基线建立、以及作为复杂模型的预处理步骤。

统计异常检测方法体系 异常检测统计方法 3σ 原则 正态分布 ±3σ 箱线图 (IQR) Q1 - 1.5IQR / Q3 + 1.5IQR Z-Score |Z| > 3 为异常 Grubbs 检验 假设检验法 适用场景对比 • 3σ / Z-Score:数据近似正态分布,样本量较大 • IQR:分布未知或有偏态,样本量不限 • Grubbs:严格假设正态,每次只能检测一个异常点

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检验是一种假设检验方法。它每次只检测一个异常点,然后迭代进行。

步骤:

  1. 计算统计量 G = max(|xᵢ - μ|) / σ
  2. 查Grubbs临界值表,比较G与临界值
  3. 如果G大于临界值,判定该点为异常,移除后重复
  4. 直到没有异常点为止

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%的异常检测问题。下次遇到异常检测任务,先别急着上模型,试试这些方法——你会发现,很多时候它们就够了。