时间序列特征构建:滑动窗口、滞后特征、差分、季节分解
时间序列的特征构建,跟普通表格数据完全是两码事。你想想看,表格数据哪一行的顺序调换都不影响模型,但时序数据一旦打乱顺序,整个预测逻辑就塌了。所以处理时序数据时,我们得靠一系列专门的手法把时间依赖性“拆”出来变成特征。下面这张图是我个人习惯的框架——从原始序列出发,四种核心特征构建方法协同输出,最终融合成模型能吃的特征集。
4.1 滑动窗口:捕捉局部动态
滑动窗口大概是时间序列特征构建里最直觉的操作了。说白了,就是用一段固定宽度的“窗口”在序列上滚动,每到一个位置就计算窗口内的统计量作为新特征。窗口宽度和步长是两个关键参数——宽度决定你看多远的局部信息,步长决定特征的密集程度。
我在做电商销量预测时遇到过一个问题:窗口设太小,特征噪声大;窗口设太大,又反应迟钝。后来我习惯先用自相关图(ACF)看一眼序列的周期长度,把窗口设在周期长度的1/3到1/2之间,算是一个比较安全的起点。
下面这段代码演示了最常用的几种窗口统计量:
import pandas as pd
import numpy as np
# 生成示例数据:每日销量
np.random.seed(42)
dates = pd.date_range('2024-01-01', periods=200, freq='D')
sales = 100 + np.cumsum(np.random.randn(200) * 0.5) + \
np.sin(np.linspace(0, 12, 200)) * 10
df = pd.DataFrame({'date': dates, 'sales': sales})
df.set_index('date', inplace=True)
# 滑动窗口特征
window = 7 # 7天窗口
df['rolling_mean'] = df['sales'].rolling(window=window).mean()
df['rolling_std'] = df['sales'].rolling(window=window).std()
df['rolling_max'] = df['sales'].rolling(window=window).max()
df['rolling_min'] = df['sales'].rolling(window=window).min()
df['rolling_range'] = df['rolling_max'] - df['rolling_min']
# 注意:前 window-1 行是 NaN
print(df.head(10))
4.2 滞后特征:让过去说话
滞后特征就是把当前时刻之前某个时间点的值直接拿过来当特征。听起来简单,但滞后阶数怎么选,里面门道不少。
我曾经在一个设备故障预测项目里栽过跟头——当时一股脑把滞后1到50阶全塞进模型,结果特征维度爆炸,模型反而过拟合了。后来我学乖了,先用偏自相关图(PACF)来定位显著的滞后阶数,再结合业务理解筛选。
代码实现非常直接,pandas 的 shift() 就能搞定:
# 生成滞后特征
lags = [1, 2, 3, 7, 14] # 常用滞后阶数
for lag in lags:
df[f'lag_{lag}'] = df['sales'].shift(lag)
# 查看效果
print(df[[ 'sales'] + [f'lag_{l}' for l in lags]].head(20))
嗯,这里要注意:滞后特征跟滑动窗口特征经常一起用,但它们的含义完全不同。滞后特征是“某个特定时刻的原始值”,而滑动窗口特征是“一段时间的统计量”。两者互补,不是替代关系。
4.3 差分:消除趋势的利器
差分是最简单的平稳化手段。一阶差分就是今天的值减去昨天的值,季节差分就是今天减去上一周期同一天的值。为什么要做差分?因为很多时序模型(比如ARIMA)要求输入是平稳的,而现实中的序列往往有趋势。
我建议你把差分看作一种特征变换,而不是数据预处理。差分后的序列反映了“变化量”而非“绝对值”,有时预测变化量比预测绝对值更有实际意义——比如股价预测,大家更关心涨跌幅度而不是具体价格。
# 一阶差分
df['diff_1'] = df['sales'].diff(1)
# 季节差分(7天周期)
df['diff_seasonal_7'] = df['sales'].diff(7)
# 二阶差分(很少用到,除非一阶后仍不平稳)
df['diff_2'] = df['sales'].diff(1).diff(1)
# 查看差分效果
print(df[['sales', 'diff_1', 'diff_seasonal_7']].head(15))
4.4 季节分解:拆解时间序列
季节分解把时间序列拆成趋势、季节性和残差三部分。常见的分解模型有加法和乘法两种:加法适用于季节性振幅不随时间变化的情况,乘法适用于振幅随趋势增长而放大的情况。
我个人觉得 STL 分解(Seasonal-Trend decomposition using LOESS)是最好用的,它对异常值鲁棒,而且能处理非固定周期的季节性。
from statsmodels.tsa.seasonal import STL
# STL 分解
stl = STL(df['sales'], period=7, robust=True)
result = stl.fit()
# 提取分量
df['trend'] = result.trend
df['seasonal'] = result.seasonal
df['resid'] = result.resid
# 查看分解结果
print(df[['sales', 'trend', 'seasonal', 'resid']].head(10))
4.5 协同策略:组合的艺术
到这里你可能已经感觉到了——这四种方法单独用都有局限性,但组合起来能互相弥补。下面这个表格是我根据自己的项目经验整理的对比,帮你快速选型:
| 方法 | 核心作用 | 主要参数 | 适用场景 | 常见陷阱 |
|---|---|---|---|---|
| 滑动窗口 | 局部统计特征 | 窗口宽度、步长 | 有短期依赖的序列 | 窗口大小敏感,易引入NaN |
| 滞后特征 | 直接时序偏移 | 滞后阶数 | 周期性明显的序列 | 阶数过多导致过拟合 |
| 差分 | 平稳化/变化量 | 差分阶数、周期 | 非平稳序列 | 过度差分破坏自相关结构 |
| 季节分解 | 拆解趋势/季节/残差 | 周期长度、分解模型 | 强季节性序列 | 周期选择错误扭曲分解结果 |
在实际项目中,我常用的组合套路是:先用季节分解把趋势和季节性剥离,对残差序列做滑动窗口和滞后特征,最后把差分特征作为补充加入。这样既保留了原始序列的绝对水平信息,又纳入了局部动态和变化量信号。
# 组合特征构建示例
# 1. 季节分解
stl = STL(df['sales'], period=7, robust=True)
result = stl.fit()
df['trend'] = result.trend
df['seasonal'] = result.seasonal
df['resid'] = result.resid
# 2. 对残差做滑动窗口
df['resid_rolling_mean'] = df['resid'].rolling(7).mean()
# 3. 对残差做滞后特征
for lag in [1, 2, 3, 7]:
df[f'resid_lag_{lag}'] = df['resid'].shift(lag)
# 4. 差分特征
df['sales_diff'] = df['sales'].diff(1)
df['sales_diff_seasonal'] = df['sales'].diff(7)
print(df.columns.tolist())
好了,这四种方法就讲到这儿。它们就像工具箱里的四把扳手,单独用都能干活,但真正体现功力的地方是你知道什么时候该用哪一把、怎么配合着用。下次你拿到一个时间序列数据集,不妨先画张图看看趋势和周期,然后按这个流程走一遍,特征质量应该不会差。