时间序列数据基础:平稳性、趋势、季节性

说实话,刚接触时间序列那会儿,我犯过一个特蠢的错误。直接把没处理的股价数据扔进 LSTM 模型,结果训练 loss 死活降不下去。导师看了一眼说:“你瞅瞅这数据,它平稳吗?”我当场就懵了。什么叫平稳?这玩意儿还有讲究?

从那以后,我再也不敢跳过数据检查这步了。今天咱们就把时间序列的三个核心概念——平稳性、趋势、季节性——掰开揉碎了讲清楚。

时间序列到底长啥样

时间序列,说白了就是按时间顺序排好的一串数字。每 5 分钟采样一次 CPU 使用率、每天记录一次服务器错误数、每小时采集一次网络流量……这些都算。

你想想看,如果把这串数字画成折线图,大概能看出几种规律:

  • 长期向上或向下——这叫趋势
  • 周期性的起伏——这叫季节性
  • 剩下的杂乱波动——就是残差

我个人习惯,拿到任何时间序列数据,第一件事就是拆成这三个成分。为什么?因为 LSTM 对不同的成分,反应完全不同。

核心观点:异常检测的本质,是找到那些「不该出现」的波动。如果你连数据本身的正常规律都没摸清楚,凭什么判断什么是异常?

平稳性:为什么 LSTM 需要它

平稳性的定义其实很简单:统计性质不随时间变化。均值不变、方差不变、自相关结构也不变。

我举个例子。你测量机房温度,如果平均值始终在 22°C 上下,波动幅度也差不多,那这个序列就是平稳的。但如果温度从早上 20°C 一路升到下午 35°C,均值一直在变,那就是非平稳。

LSTM 这种模型,天生喜欢平稳数据。为什么?因为它要学习的是“模式”,而不是“数值”。如果均值一直在漂,模型学到的东西就不断作废,就像在跑步机上跑步——累死也原地踏步。

特性 平稳序列 非平稳序列
均值 稳定,围绕固定值波动 随时间变化(上升/下降)
方差 大致不变 可能随时间扩大或缩小
自相关 衰减快,短期记忆 衰减慢,长期依赖假象
对 LSTM 影响 容易收敛,学习稳定模式 难以收敛,容易过拟合伪规律

怎么判断数据平不平稳

嗯,这里有个小技巧。别光靠肉眼,要用统计检验。我最常用的是 ADF 检验(Augmented Dickey-Fuller Test)。

它的逻辑很简单:假设数据是非平稳的,然后看证据够不够推翻这个假设。p 值小于 0.05 就算平稳。

from statsmodels.tsa.stattools import adfuller
import pandas as pd

# 读取你的时间序列数据
data = pd.read_csv('server_cpu.csv', index_col='timestamp', parse_dates=True)
series = data['cpu_usage']

# 执行 ADF 检验
result = adfuller(series.dropna())
print(f'ADF 统计量: {result[0]:.4f}')
print(f'p 值: {result[1]:.4f}')

if result[1] < 0.05:
    print('数据平稳,可以直接使用')
else:
    print('数据非平稳,需要差分处理')

曾经有个坑:我见过有人拿着 p 值 0.049 就欢呼平稳,结果模型上线后表现一塌糊涂。0.05 是经验值,不是铁律。如果 p 值在 0.05 附近,我建议你再做一次 KPSS 检验交叉验证。

趋势:该去掉还是保留

趋势,就是数据长期的大方向。服务器流量随着用户量增长而攀升,这算趋势。磁盘使用率一直上升直到满盘,也算趋势。

问题来了:LSTM 到底要不要学这个趋势?

我个人经验:如果趋势是线性稳定的,比如每月增长 5%,可以保留。模型能学会这种匀速增长。但如果趋势是非线性突变的,比如某天流量突然翻倍,模型就会乱掉。

  • 线性趋势:取一阶差分就能消除
  • 非线性趋势:需要对数变换或多项式拟合去除
  • 周期性趋势:其实是季节性的表现,后面另说
# 一阶差分去除线性趋势
differenced = series.diff().dropna()
print(f'差分后数据长度: {len(differenced)}')

# 再次检验平稳性
result = adfuller(differenced)
print(f'差分后 p 值: {result[1]:.4f}')

季节性:周期模式怎么提取

季节性听起来高大上,其实就是重复出现的模式。每天晚上的流量高峰、每周一的 CPU 负载飙升、每年双十一的订单暴增……这些都是季节性。

我记得有一次,一个同学跑 LSTM 做服务器异常检测,模型老是报警。查了半天,发现每周五下午数据都会有个小尖峰。那不是异常,是定时任务跑批。模型没学会这个周期,所以对它敏感。

如何处理季节性?

  1. 先用 STL 分解(Seasonal-Trend decomposition using LOESS)把季节成分拎出来
  2. 决定要不要去掉:如果你检测的是「偏离常规模式」的异常,通常要去掉季节性
  3. 或者,让 LSTM 自己学:把时间特征作为输入(比如小时、星期几)
from statsmodels.tsa.seasonal import seasonal_decompose

# 假设数据按小时采集,周期为 24 小时
decomposition = seasonal_decompose(series, model='additive', period=24)

# 提取各成分
trend = decomposition.trend
seasonal = decomposition.seasonal
residual = decomposition.resid

# 去掉季节性的数据
deseasonalized = series - seasonal

小建议:如果你不确定周期长度,可以用自相关图(ACF)来看。峰值出现的间隔就是周期。频率数据用 24 作为周期,周数据用 7,月数据用 12。但别死板,根据实际业务来。

一张图总结:时间序列核心概念

下面这张 SVG 把三者的关系串起来了。你看看,是不是一下子就清楚了?

时间序列核心概念结构图 时间序列核心概念 原始时间序列 趋势 (Trend) 季节性 (Seasonal) 残差 (Residual) 关键理解: • 趋势是长期方向;季节性是周期性波动;残差是剔除前两者后的随机噪声。 • LSTM 异常检测重点关注残差 —— 残差中的异常点就是潜在故障信号。 处理策略:差分去趋势 → 分解去季节 → 平稳性检验

实践中我是这样用的

我的工作流:

  1. 拿到数据,先画图,肉眼判断趋势和季节
  2. 做 ADF 检验,确认是否平稳
  3. 如果不平稳,先差分去趋势,再分解去季节
  4. 用处理后的残差训练 LSTM
  5. 预测时对比残差大小,超出阈值就是异常

你可能会问:为什么不直接用原始数据?

嗯,我记得之前做过一个网络流量异常检测项目。原始流量有明显的周季节性和日趋势。直接扔进 LSTM,模型把每个周一的正常高峰也当特征学进去了。结果周一真有异常时,它反而不报警——因为已经“习惯”了那个波动。

后来我把趋势和季节去掉,只对残差建模。模型立刻变得敏感多了。真正的异常——比如凌晨三点的流量暴增,原来被星期一的正常高峰淹没,现在一眼就能揪出来。

避坑:别把所有时间序列都处理成纯残差。有些业务的异常本身就藏在趋势变化里,比如客户流失前的缓慢下降。这时候趋势就是信号,不是噪声。我的建议:做两个模型,一个用残差,一个用原始序列,交叉验证。

几点心得

说了这么多,总结下来其实就三句话:

  • 平稳性是 LSTM 能稳定学习的前提,差分是去非平稳最粗暴但有效的手段
  • 趋势看情况处理,线性趋势可保留,非线性趋势必须去除
  • 季节性是双刃剑,学会它模型更准,但异常也可能被它掩盖

你把这些基础打扎实了,后面讲 LSTM 结构时才能理解:为什么模型要对输入做归一化?为什么某些激活函数更好?根都在这里——数据性质决定了模型设计。