3. 统计方法(一)Z-Score

聊到异常检测,很多人第一反应就是上机器学习模型。但说实话,在我多年的实战经验里,最简单的统计方法往往最管用。尤其是刚接触一个系统、数据还没摸透的时候,Z-Score 是你最趁手的工具。

这一章,我们就把它彻底讲透。

3.1 Z-Score 的核心思想:你在人群中的哪个位置?

想象一下,你参加了一场马拉松。你的成绩是 3 小时 30 分。这个成绩算好还是坏?

光看值,你没法判断。但如果告诉你:平均成绩是 4 小时,标准差是 20 分钟。那你心里就有数了——你比平均快了 30 分钟,也就是 1.5 个标准差。

这个“1.5”,就是你的 Z-Score。

Z-Score 的数学定义很简单:

Z = (X - μ) / σ
  • X:当前观测值
  • μ:总体均值
  • σ:总体标准差

它回答了一个核心问题:这个数据点,距离平均水平有多远? 距离越远,越可能是异常。

核心直觉:Z-Score 的值越大,数据点越“离群”。通常认为 |Z| > 3 的点,就值得怀疑了。

3.2 为什么 Z-Score 能检测异常?

这背后依赖一个假设:你的数据大致服从正态分布。

在正态分布下:

  • 约 68% 的数据落在 ±1 个标准差内
  • 约 95% 的数据落在 ±2 个标准差内
  • 约 99.7% 的数据落在 ±3 个标准差内

所以,如果一个点的 Z-Score 超过 3,它属于正常数据的概率不到 0.3%。这基本就是小概率事件了——我们倾向于认为它是异常。

嗯,这里要注意:这个“3σ”法则不是万能的。我在项目中遇到过一些场景,数据分布偏得厉害,用 3σ 会漏掉很多真正的异常。后面我们会聊怎么调整。

3.3 手写 Z-Score:从零实现

别急着调包。我建议你至少手写一次,才能真正理解它的计算过程。

import numpy as np
import pandas as pd

def zscore_detect(series, threshold=3):
    """
    基于 Z-Score 的单维异常检测
    :param series: pandas Series 或 list
    :param threshold: Z-Score 阈值,默认 3
    :return: 布尔数组,True 表示异常
    """
    # 计算均值和标准差
    mean = np.mean(series)
    std = np.std(series, ddof=0)  # 总体标准差

    # 计算每个点的 Z-Score
    z_scores = (series - mean) / std

    # 标记异常
    is_anomaly = np.abs(z_scores) > threshold

    return is_anomaly, z_scores

# 示例数据
data = [10, 12, 11, 13, 12, 11, 10, 100, 12, 11]
anomaly_flags, z_vals = zscore_detect(data)

print("Z-Scores:", z_vals)
print("异常标记:", anomaly_flags)

跑一下这段代码,你会发现第 8 个点(值 100)的 Z-Score 高达 3.2 左右,被标记为异常。其他点都在 ±1 以内,很正常。

个人习惯:我在做初步探索时,通常先用 Z-Score 扫一遍。它快,不依赖训练,能帮我快速定位那些“一眼假”的数据。但注意,它只适合单维、近似正态的数据。

3.4 阈值选择策略:3 不是唯一答案

你可能会问:为什么阈值是 3?能不能用 2?或者 4?

答案是:看场景。

阈值的选择,本质是在召回率和精确率之间做权衡。

阈值 含义 适用场景
2.0 约 95% 正常数据被覆盖 对异常敏感,宁可误报也不漏报(如:金融欺诈)
2.5 约 98.8% 正常数据被覆盖 折中方案,适合大多数通用场景
3.0 约 99.7% 正常数据被覆盖 最常用,适合对误报容忍度低的场景
3.5 约 99.95% 正常数据被覆盖 数据量大,只关注极端异常
4.0 约 99.99% 正常数据被覆盖 非常保守,仅用于物理设备等极严格场景

我曾经踩过一个坑:在一个电商的流量监控项目里,我直接用 3σ 阈值,结果发现每天凌晨的低谷流量被频繁误报。后来一查,流量分布是右偏的,均值被白天的高峰拉高了。解决办法是:先对数据做对数变换,或者改用分位数方法。记住,Z-Score 对数据分布敏感,用之前一定要看一眼分布形状。

3.5 实战:单维指标异常检测

光说不练假把式。我们拿一个真实的场景来走一遍。

场景:监控某 API 接口的响应时间(单位:ms)。你有一周的历史数据,想实时检测今天的响应时间是否异常。

步骤 1:加载并预览数据

# 模拟一周的响应时间数据(每分钟一个点)
np.random.seed(42)
normal_data = np.random.normal(loc=200, scale=30, size=7*24*60)

# 注入一些异常点
normal_data[1000:1010] = 500  # 突发延迟
normal_data[5000] = 800       # 极端异常

df = pd.DataFrame({'response_time': normal_data})
print(df.describe())

步骤 2:计算 Z-Score 并标记异常

anomaly_flags, z_scores = zscore_detect(df['response_time'], threshold=3)
df['is_anomaly'] = anomaly_flags
df['z_score'] = z_scores

# 看看异常点
anomalies = df[df['is_anomaly'] == True]
print(f"检测到 {len(anomalies)} 个异常点")
print(anomalies.head())

步骤 3:可视化结果

虽然我们不能用图片,但你可以想象:在时间序列图上,正常点围绕 200ms 波动,而那几个 500ms 和 800ms 的点被清晰地标红。

实战要点:

  • 计算 Z-Score 时,不要用全量数据。我建议用历史窗口(比如过去 7 天)计算均值和标准差,然后检测当前数据点。这样可以避免异常点“污染”统计量。
  • 如果数据有明显的周期性(比如白天高、晚上低),最好按周期分段计算。比如对每个小时单独算均值和标准差。
  • Z-Score 对小样本不友好。如果窗口内数据少于 30 个点,标准差估计会很不稳定。

3.6 Z-Score 的局限性

聊了这么多优点,也得说说它的短板。你想想看,如果数据分布是这样的:

  • 多峰分布:比如用户访问量在工作日和周末明显不同。Z-Score 会把两个峰之间的正常点误判为异常。
  • 严重偏态:比如收入数据,少数人收入极高。Z-Score 会漏掉这些“高值异常”,因为标准差被拉大了。
  • 存在离群值:均值和标准差本身对离群值敏感。一个极端异常点会把均值拉偏,导致其他正常点被误判。

说白了,Z-Score 是一个好用的起点,但不是终点。遇到复杂情况,我们后面会讲更鲁棒的方法,比如 Median Absolute Deviation (MAD) 和 IQR 方法。

3.7 本章知识体系

下面这张图帮你梳理 Z-Score 异常检测的完整流程:

Z-Score 异常检测流程 Z-Score 异常检测流程 输入单维数据 步骤1:计算均值 μ 和标准差 σ 步骤2:计算每个点的 Z = (X-μ)/σ |Z| > 阈值? (默认3) 是 标记为异常 否 标记为正常 阈值可调:2.0 / 2.5 / 3.0 / 3.5 / 4.0 根据业务场景选择

这张图把整个流程串起来了。从输入数据开始,计算统计量,算 Z-Score,然后根据阈值做决策。每一步都很清晰。

3.8 小结

这一章我们聊了 Z-Score 的原理、实现、阈值选择,还走了一遍实战。总结几个关键点:

  • Z-Score 衡量的是数据点偏离均值的标准差倍数
  • 阈值 3 是默认值,但要根据业务场景灵活调整
  • 实战中要注意数据分布和窗口选择
  • Z-Score 是起点,不是终点