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,然后根据阈值做决策。每一步都很清晰。
3.8 小结
这一章我们聊了 Z-Score 的原理、实现、阈值选择,还走了一遍实战。总结几个关键点:
- Z-Score 衡量的是数据点偏离均值的标准差倍数
- 阈值 3 是默认值,但要根据业务场景灵活调整
- 实战中要注意数据分布和窗口选择
- Z-Score 是起点,不是终点