4. 特征工程(上):滞后特征、滚动窗口统计量、时间特征
各位同学,咱们今天聊聊特征工程的上半场。说实话,很多做时序预测的朋友,模型调了半天效果上不去,问题往往出在特征上。我个人习惯把特征工程看作「给模型喂什么饭」——饭喂对了,模型自然长得好。
4.1 滞后特征(Lag Features)——让模型看到「昨天」
滞后特征,说白了就是把过去时刻的值拿过来当特征。比如预测今天的销量,把昨天的销量、前天的销量都塞进去。这招简单粗暴,但极其有效。
为什么需要滞后特征?
时序数据有个核心假设:历史会重复。今天的值跟昨天的值往往高度相关。你想想看,如果今天突然暴热,空调销量肯定涨,但模型不知道啊——它得看到昨天的温度才知道趋势。
Lag_k = X_{t-k},其中 k 是滞后阶数
我在项目中遇到过一个问题:预测电商平台的日活用户。一开始我只用了当天的特征,效果很差。后来加了滞后7天的特征(因为周周期性),准确率直接提升了15%。嗯,这里要注意:滞后阶数不是越多越好。
我曾经在预测股票时,一口气加了滞后30天的特征。结果模型过拟合得一塌糊涂。为什么?因为太久远的历史信息其实跟当前关系不大,反而引入了噪声。
代码实现很简单,用 pandas 的 shift 方法:
import pandas as pd
# 假设 df 是时序数据,'value' 是目标列
df['lag_1'] = df['value'].shift(1) # 昨天
df['lag_7'] = df['value'].shift(7) # 上周同一天
df['lag_365'] = df['value'].shift(365) # 去年同一天(如果有年周期)
# 注意:shift 后会产生 NaN,需要处理
df = df.dropna()
4.2 滚动窗口统计量(Rolling Statistics)——捕捉「近期趋势」
滞后特征只能看到单个时间点的值,但很多时候我们需要「一段时间的整体情况」。比如过去7天的平均销量、过去30天的波动幅度。这就是滚动窗口统计量。
常用的滚动统计量:
- 滚动均值:平滑短期波动,看趋势
- 滚动标准差:衡量波动性,比如股价的波动率
- 滚动最大值/最小值:捕捉极端值
- 滚动中位数:抗噪声能力强
我个人习惯用滚动均值+滚动标准差组合。为什么?因为很多模型对「异常波动」特别敏感。比如预测电力负荷,突然有一天工厂停工导致负荷骤降,滚动统计量能帮模型识别出这是异常,而不是趋势变化。
窗口大小怎么选?我一般先看数据的周期性。如果是日数据,窗口选7(一周)、30(一个月)、365(一年)。如果是小时数据,窗口选24(一天)、168(一周)。记住:窗口越大,平滑效果越强,但也会丢失短期细节。
代码示例:
# 滚动窗口统计量
df['rolling_mean_7'] = df['value'].rolling(window=7).mean()
df['rolling_std_7'] = df['value'].rolling(window=7).std()
df['rolling_max_30'] = df['value'].rolling(window=30).max()
df['rolling_min_30'] = df['value'].rolling(window=30).min()
# 注意:滚动窗口也会产生 NaN(前 window-1 个点)
df = df.dropna()
我曾经犯过一个低级错误:用未来的数据计算滚动统计量。比如预测 t 时刻的值,却用了 t+1 到 t+7 的数据算均值。这叫「数据泄露」,模型在训练时表现完美,上线后直接崩盘。切记:滚动窗口只能用历史数据,不能包含未来。
4.3 时间特征(年/月/日/星期)——让模型理解「时间本身」
时间特征,就是把时间戳拆解成可理解的数值。比如年、月、日、星期几、小时、分钟。这些特征看似简单,但能帮模型捕捉周期性规律。
为什么需要时间特征?
你想想看,很多业务都有明显的周期性:
- 电商:周末销量高,工作日低
- 交通:早晚高峰,周末平峰
- 能源:夏天用电多,冬天用气多
模型如果不理解「今天是星期几」,它怎么知道周末该涨还是该跌?
代码实现:
# 假设 df 的索引是 DatetimeIndex
df['year'] = df.index.year
df['month'] = df.index.month
df['day'] = df.index.day
df['dayofweek'] = df.index.dayofweek # 0=周一,6=周日
df['hour'] = df.index.hour
df['is_weekend'] = (df['dayofweek'] >= 5).astype(int) # 是否周末
对于周期性特征(如月份、星期几),直接用数值(1-12)可能有问题。因为模型会认为「12月」比「1月」大,但实际上12月和1月是相邻的。我建议用正弦/余弦编码:
df['month_sin'] = np.sin(2 * np.pi * df['month'] / 12)df['month_cos'] = np.cos(2 * np.pi * df['month'] / 12)这样能保留周期性关系。
4.4 知识体系总览
下面这张图总结了本章的核心内容,帮你快速建立知识框架:
4.5 实战组合拳
在实际项目中,我通常把这三种特征组合使用。举个例子:
# 完整特征工程示例
def create_features(df, target_col='value'):
df = df.copy()
# 1. 滞后特征
df['lag_1'] = df[target_col].shift(1)
df['lag_7'] = df[target_col].shift(7)
df['lag_30'] = df[target_col].shift(30)
# 2. 滚动窗口统计量
df['rolling_mean_7'] = df[target_col].rolling(7).mean()
df['rolling_std_7'] = df[target_col].rolling(7).std()
df['rolling_max_30'] = df[target_col].rolling(30).max()
# 3. 时间特征
df['month'] = df.index.month
df['dayofweek'] = df.index.dayofweek
df['is_weekend'] = (df['dayofweek'] >= 5).astype(int)
# 4. 周期性编码(月份)
df['month_sin'] = np.sin(2 * np.pi * df['month'] / 12)
df['month_cos'] = np.cos(2 * np.pi * df['month'] / 12)
# 删除NaN
df = df.dropna()
return df
特征不是越多越好。我一般先加滞后1、7、30,滚动均值7、30,再加星期几和月份。然后看特征重要性,把没用的删掉。记住:少而精的特征,比多而杂的效果好得多。
好了,特征工程的上半场就到这里。这三种特征——滞后、滚动统计量、时间特征——是时序预测的基石。你想想看,如果连「昨天发生了什么」「最近一周的趋势」「今天是星期几」都不知道,模型怎么可能预测准?
下一节咱们继续聊特征工程的下半场,到时候会讲更高级的特征构造方法。今天就先消化这些基础内容,动手跑跑代码,感受一下特征对模型的影响。
公众号:蓝海资料掘金营,微信deep3321