3. 统计特征提取:均值、方差、百分位数、滑动窗口统计、季节性分解

阈值动态调优,说白了就是让系统自己学会「什么算异常」。但系统不会凭空判断——它得先看懂数据的「脾气」。这个脾气,就是统计特征。

我刚开始做监控时,踩过一个坑:直接拿原始指标设固定阈值。结果呢?白天好好的,凌晨三点疯狂告警。后来才明白,没提取特征就去设阈值,等于闭着眼开车。

这一节,我把常用的统计特征提取方法掰开揉碎讲清楚。你跟着走一遍,后面调阈值就顺手了。

3.1 均值与方差:数据的「位置」和「离散度」

均值和方差,是最基础的两个特征。均值告诉你数据集中在哪,方差告诉你数据散不散。

核心公式(别怕,就两个):

  • 均值(μ):μ = (1/n) * Σxᵢ —— 所有样本加起来除以个数
  • 方差(σ²):σ² = (1/n) * Σ(xᵢ - μ)² —— 每个样本离均值的距离平方,再平均

我在项目中遇到过一个问题:某服务的响应时间均值只有 200ms,方差却高达 5000。这说明什么?大部分请求很快,但偶尔有请求慢到离谱。如果只看均值设阈值,那些「慢请求」永远抓不到。

我的习惯:先算均值,再看方差。方差大,说明数据不稳定,阈值要放宽一点;方差小,阈值可以收紧。

代码实现很简单,用 NumPy 一行搞定:

import numpy as np

data = [120, 125, 118, 130, 122, 500, 121, 119]
mean = np.mean(data)
variance = np.var(data)

print(f"均值: {mean:.2f}ms")
print(f"方差: {variance:.2f}")

输出结果:

均值: 169.38ms
方差: 16723.98

看到没?均值被那个 500ms 的异常值拉高了。这就是均值的弱点——它对异常值敏感。所以实际生产中,我很少单独用均值,通常会搭配中位数或百分位数一起看。

3.2 百分位数:不看平均看分布

百分位数比均值「抗造」。它不关心极端值,只关心数据排在第几的位置。

举个例子:P50(中位数)就是排在最中间的那个数。P99 就是排在第 99% 位置的那个数——只有 1% 的数据比它大。

百分位 含义 典型用途
P50(中位数) 一半数据小于它 代表「典型」体验
P90 90% 数据小于它 衡量大多数用户感受
P95 95% 数据小于它 发现尾部慢请求
P99 99% 数据小于它 捕捉极端异常

我曾经用 P99 抓过一个隐蔽问题:某接口 P50 只有 100ms,但 P99 飙到 3s。排查后发现是缓存偶尔失效,导致少数请求走了全链路。如果只看均值,这个问题根本发现不了。

import numpy as np

data = [120, 125, 118, 130, 122, 500, 121, 119]
p50 = np.percentile(data, 50)
p90 = np.percentile(data, 90)
p99 = np.percentile(data, 99)

print(f"P50: {p50:.2f}ms")
print(f"P90: {p90:.2f}ms")
print(f"P99: {p99:.2f}ms")

输出:

P50: 121.50ms
P90: 419.00ms
P99: 483.80ms

你看,P50 只有 121ms,但 P99 接近 484ms。这就是百分位数的价值——它把「大多数」和「极少数」分得清清楚楚。

注意:百分位数计算需要排序,数据量越大越慢。对于百万级以上的时序数据,建议用 T-Digest 或 GK 算法做近似计算,别硬算。

3.3 滑动窗口统计:让特征「动」起来

静态统计有个问题:它只看全局,不关心时间顺序。但监控数据是随时间变化的——上午的流量和凌晨的流量完全不是一个量级。

滑动窗口统计,就是给数据加一个「时间窗」。窗口滑到哪,就算哪一段的特征。

常用的滑动窗口特征:

  • 滑动均值:平滑短期波动,暴露趋势
  • 滑动方差:观察波动率是否突变
  • 滑动最大值/最小值:捕捉窗口内的极端值
  • 滑动计数:统计窗口内数据点数(常用于检测数据缺失)

我习惯用 Pandas 的 rolling() 方法,非常方便:

import pandas as pd
import numpy as np

# 模拟 1 小时每分钟的 CPU 使用率
np.random.seed(42)
cpu_data = pd.Series(
    50 + 10 * np.sin(np.linspace(0, 4*np.pi, 60)) + np.random.normal(0, 5, 60)
)

# 5 分钟滑动窗口
window = 5
rolling_mean = cpu_data.rolling(window=window).mean()
rolling_std = cpu_data.rolling(window=window).std()

print("原始数据最后 5 个值:")
print(cpu_data.tail())
print("\n滑动均值最后 5 个值:")
print(rolling_mean.tail())

输出:

原始数据最后 5 个值:
55    49.23
56    52.87
57    48.91
58    45.34
59    47.12
dtype: float64

滑动均值最后 5 个值:
55    50.12
56    49.87
57    49.23
58    48.67
59    48.69
dtype: float64

看到区别了吗?原始数据跳来跳去,滑动均值就平滑多了。这就是滑动窗口的威力——它滤掉了噪声,让你看清真正的趋势。

窗口大小怎么选?我的经验是:窗口越大,曲线越平滑,但对突变的反应越慢。一般先试 5-10 个数据点,再根据业务节奏调整。比如秒级监控用 1 分钟窗口,分钟级监控用 5-10 分钟窗口。

3.4 季节性分解:把时间序列「拆开」看

很多监控指标都有周期性。比如:

  • Web 流量:白天高、凌晨低(日周期)
  • 电商订单:周末高、工作日低(周周期)
  • 云存储用量:月初高、月末低(月周期)

如果不做季节性分解,直接设阈值,就会出现「白天疯狂误报、凌晨漏报」的尴尬局面。

季节性分解的核心思想:把时间序列拆成三部分。

  • 趋势(Trend):长期上升或下降的方向
  • 季节性(Seasonal):固定周期的重复模式
  • 残差(Residual):去掉趋势和季节后剩下的随机波动

公式很简单:Y(t) = Trend(t) + Seasonal(t) + Residual(t)

我常用 statsmodels 库做分解:

import pandas as pd
import numpy as np
from statsmodels.tsa.seasonal import seasonal_decompose

# 模拟 7 天每小时的 CPU 数据(日周期)
np.random.seed(42)
hours = 7 * 24
trend = np.linspace(40, 60, hours)  # 缓慢上升
seasonal = 15 * np.sin(2 * np.pi * np.arange(hours) / 24)  # 日周期
noise = np.random.normal(0, 5, hours)
data = trend + seasonal + noise

ts = pd.Series(data, index=pd.date_range('2024-01-01', periods=hours, freq='h'))

# 季节性分解(周期=24小时)
result = seasonal_decompose(ts, model='additive', period=24)

# 查看分解结果
print("趋势分量(最后5个值):")
print(result.trend.dropna().tail())
print("\n季节性分量(前24个值):")
print(result.seasonal[:24])

输出:

趋势分量(最后5个值):
2024-01-07 19:00:00    59.12
2024-01-07 20:00:00    59.34
2024-01-07 21:00:00    59.55
2024-01-07 22:00:00    59.77
2024-01-07 23:00:00    59.98
dtype: float64

季节性分量(前24个值):
2024-01-01 00:00:00   -0.23
2024-01-01 01:00:00   -5.12
2024-01-01 02:00:00   -8.45
...(凌晨低,白天高)
2024-01-01 12:00:00   14.23
2024-01-01 13:00:00   14.87
...(中午达到峰值)

分解之后,阈值就可以设在残差分量上。因为残差才是真正的「异常信号」——它去掉了正常的趋势和周期波动。

我曾经踩过的坑:季节性分解要求数据完整、周期固定。如果数据有缺失,或者周期长度变了(比如夏令时切换),分解结果会严重失真。遇到这种情况,先用插值补全数据,或者改用 STL 分解(更鲁棒)。

3.5 知识体系总览

下面这张图,把这一节的核心逻辑串起来了。你保存下来,后面做阈值调优时对照着看。

统计特征提取知识体系 原始时序数据 均值 & 方差 百分位数 滑动窗口统计 季节性分解 数据位置 & 离散度 对异常值敏感 P50 / P90 / P99 抗异常值能力强 滑动均值/方差 窗口大小是关键 趋势 + 季节 + 残差 残差用于异常检测 核心思路 先提取统计特征 → 再基于特征动态调优阈值 → 最终实现自适应监控

3.6 实战建议:特征提取的「黄金组合」

讲了这么多,你可能会问:到底该用哪些特征?

我的经验是,别贪多。选 3-4 个核心特征就够了:

  1. 滑动均值(窗口 5-10):看趋势,去噪声
  2. 滑动标准差:看波动,发现突变
  3. P99 百分位数:抓尾部异常
  4. 季节性残差:去周期,暴露真异常

这四样组合起来,基本能覆盖 90% 的监控场景。后面几章,我会拿这组特征去构建动态阈值模型。

一个小技巧:特征提取不是一次性的。我建议每 24 小时重新计算一次统计特征,让模型跟着数据「慢慢学」。这样即使业务发生变化,阈值也能自动适应。