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 知识体系总览

下面这张图总结了本章的核心内容,帮你快速建立知识框架:

特征工程(上)知识体系 滞后特征 • 定义:X_{t-k} • 作用:捕捉历史值 • 常用阶数:1,7,30,365 • 实现:shift() ⚠ 注意: 阶数过多易过拟合 需处理NaN 滚动窗口统计量 • 均值:平滑趋势 • 标准差:波动性 • 最大/最小值:极端值 • 中位数:抗噪声 ⚠ 注意: 窗口大小选周期性 避免数据泄露 时间特征 • 年/月/日 • 星期几 • 小时/分钟 • 是否周末 ⚠ 注意: 周期性用正弦/余弦 避免数值大小误导 核心原则:用历史信息预测未来,避免数据泄露,保留周期性

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