时间序列数据基础:平稳性、趋势、季节性
说实话,刚接触时间序列那会儿,我犯过一个特蠢的错误。直接把没处理的股价数据扔进 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 做服务器异常检测,模型老是报警。查了半天,发现每周五下午数据都会有个小尖峰。那不是异常,是定时任务跑批。模型没学会这个周期,所以对它敏感。
如何处理季节性?
- 先用 STL 分解(Seasonal-Trend decomposition using LOESS)把季节成分拎出来
- 决定要不要去掉:如果你检测的是「偏离常规模式」的异常,通常要去掉季节性
- 或者,让 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 把三者的关系串起来了。你看看,是不是一下子就清楚了?
实践中我是这样用的
我的工作流:
- 拿到数据,先画图,肉眼判断趋势和季节
- 做 ADF 检验,确认是否平稳
- 如果不平稳,先差分去趋势,再分解去季节
- 用处理后的残差训练 LSTM
- 预测时对比残差大小,超出阈值就是异常
你可能会问:为什么不直接用原始数据?
嗯,我记得之前做过一个网络流量异常检测项目。原始流量有明显的周季节性和日趋势。直接扔进 LSTM,模型把每个周一的正常高峰也当特征学进去了。结果周一真有异常时,它反而不报警——因为已经“习惯”了那个波动。
后来我把趋势和季节去掉,只对残差建模。模型立刻变得敏感多了。真正的异常——比如凌晨三点的流量暴增,原来被星期一的正常高峰淹没,现在一眼就能揪出来。
避坑:别把所有时间序列都处理成纯残差。有些业务的异常本身就藏在趋势变化里,比如客户流失前的缓慢下降。这时候趋势就是信号,不是噪声。我的建议:做两个模型,一个用残差,一个用原始序列,交叉验证。
几点心得
说了这么多,总结下来其实就三句话:
- 平稳性是 LSTM 能稳定学习的前提,差分是去非平稳最粗暴但有效的手段
- 趋势看情况处理,线性趋势可保留,非线性趋势必须去除
- 季节性是双刃剑,学会它模型更准,但异常也可能被它掩盖
你把这些基础打扎实了,后面讲 LSTM 结构时才能理解:为什么模型要对输入做归一化?为什么某些激活函数更好?根都在这里——数据性质决定了模型设计。