时间序列特征构建:滑动窗口、滞后特征、差分、季节分解

时间序列的特征构建,跟普通表格数据完全是两码事。你想想看,表格数据哪一行的顺序调换都不影响模型,但时序数据一旦打乱顺序,整个预测逻辑就塌了。所以处理时序数据时,我们得靠一系列专门的手法把时间依赖性“拆”出来变成特征。下面这张图是我个人习惯的框架——从原始序列出发,四种核心特征构建方法协同输出,最终融合成模型能吃的特征集。

时间序列特征构建协同流程 原始时间序列 滑动窗口 窗口统计量 滞后特征 时序偏移值 差分 平稳性处理 季节分解 趋势·季节·残差 特征融合 协同使用可大幅提升时序模型预测能力
我的一点体会:这四种方法不是互斥的,实际项目中我经常把它们叠加使用。比如先做季节分解去掉趋势,再对残差做滑动窗口和滞后特征,效果往往比单一方法好不少。

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))
坑点提醒:滑动窗口会产生 NaN 值,尤其是序列开头的那一段。我刚开始做时直接 dropna 把数据丢掉了一大半……后来才学会用 min_periods 参数来控制最少有效值数量,或者用边界填充策略。

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))
选阶技巧:对于日频数据,滞后7和滞后14通常能捕捉周周期;对于小时频数据,滞后24和滞后48抓日周期。我的习惯是先画PACF图,取截尾点之前的阶数作为候选。

嗯,这里要注意:滞后特征跟滑动窗口特征经常一起用,但它们的含义完全不同。滞后特征是“某个特定时刻的原始值”,而滑动窗口特征是“一段时间的统计量”。两者互补,不是替代关系。

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))
我曾经踩过的坑:差分会减少有效样本量,一阶差分丢掉1条,季节差分丢掉一个周期。如果数据集本来就小,差分后可能捉襟见肘。另外,差分后的序列如果过度差分,会引入本不存在的负自相关,反而破坏模型。我的经验是:差分后做一次ADF检验,确认平稳了就收手。

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())
核心原则:不要为了做特征而做特征。每增加一个特征,都要问自己——它跟目标变量有合理的因果或统计关联吗?尤其在时序场景里,特征的时间对齐一旦出错,整个模型就废了。我每次构建完特征集后,都会随机抽几条样本,手动追溯一遍每个特征值是怎么算出来的,确保逻辑链条完整。

好了,这四种方法就讲到这儿。它们就像工具箱里的四把扳手,单独用都能干活,但真正体现功力的地方是你知道什么时候该用哪一把、怎么配合着用。下次你拿到一个时间序列数据集,不妨先画张图看看趋势和周期,然后按这个流程走一遍,特征质量应该不会差。