4. 特征工程基础:时间特征提取、滞后特征与滚动窗口统计

各位同学,咱们今天聊点实在的。

特征工程这东西,说白了就是「喂给模型什么数据」。你想想看,同样的算法,有人跑出SOTA,有人跑出笑话,差距往往就在特征上。我个人习惯把特征工程分成三块:时间特征、滞后特征、滚动统计特征。今天咱们一个一个拆开讲。

4.1 时间特征提取:把时钟变成模型能懂的语言

风电功率预测,本质上是和时间打交道。但模型不认识「2025年3月15日下午3点」这种格式。你得帮它翻译成数字。

核心思路:把时间戳拆解成周期性分量和趋势性分量。

4.1.1 小时特征

小时是24小时循环的。但直接扔个0~23的数字进去,模型会以为23和0离得很远——其实它们只差1小时。我建议用正弦余弦编码:

import numpy as np
import pandas as pd

def encode_hour(df, col='timestamp'):
    """对小时做正弦余弦编码"""
    hour = df[col].dt.hour
    df['hour_sin'] = np.sin(2 * np.pi * hour / 24)
    df['hour_cos'] = np.cos(2 * np.pi * hour / 24)
    return df

嗯,这里要注意:正弦余弦要一起用,单用一个会丢失相位信息。我在项目里吃过这个亏,只用了正弦,结果模型把凌晨3点和下午3点搞混了——因为sin(3)和sin(15)数值一样。

4.1.2 季节与月份特征

风电功率有明显的季节性。冬天风大,夏天风小,这是常识。但月份也是循环的,12月和1月挨着。同样用正弦余弦:

def encode_month(df, col='timestamp'):
    month = df[col].dt.month
    df['month_sin'] = np.sin(2 * np.pi * month / 12)
    df['month_cos'] = np.cos(2 * np.pi * month / 12)
    return df

如果你觉得月份粒度太粗,可以加一个「季节」标签:春季(3-5)、夏季(6-8)、秋季(9-11)、冬季(12-2)。我个人习惯两者都保留,让模型自己学哪个更重要。

小技巧:还可以提取「一年中的第几天」作为特征。这个值从1到365,能反映更细粒度的季节变化。我一般在做长期预测(72小时以上)时才会加这个。

4.2 滞后特征构建:让模型看到「过去」

风电功率有很强的自相关性。说白了,现在的风速和几分钟前的风速关系很大。滞后特征就是把这些「过去的值」拿过来当特征。

4.2.1 什么是滞后特征?

假设当前时刻是t,那么t-1时刻的功率值就是滞后1步的特征。t-2就是滞后2步,以此类推。

def create_lag_features(df, col='power', lags=[1, 2, 3, 6, 12, 24]):
    """构建滞后特征"""
    for lag in lags:
        df[f'lag_{lag}'] = df[col].shift(lag)
    return df

我曾经犯过一个错误:对全数据集直接shift,导致训练集里混进了未来的信息。记住,一定要在分组后、按时间顺序shift。比如按风机ID分组:

df['lag_1'] = df.groupby('turbine_id')['power'].shift(1)

4.2.2 滞后步长怎么选?

这个没有标准答案。我一般会先做自相关分析(ACF图),看看哪些滞后步长相关性高。通常:

  • 短期预测(1-4小时):滞后1、2、3、6步就够了
  • 中期预测(24小时):需要滞后12、24、48步
  • 长期预测(72小时以上):滞后步长可以稀疏一些,比如24、48、72

避坑指南:滞后特征越多,模型越容易过拟合。我曾经在一个项目里加了30个滞后特征,训练集R²高达0.98,测试集直接崩到0.6。后来砍到6个滞后特征,测试集反而升到0.85。记住:少即是多。

4.3 滚动窗口统计特征:捕捉局部趋势

滞后特征只给了单个时间点的值,但风电功率的「趋势」也很重要。滚动窗口统计特征就是计算过去一段时间内的统计量,比如均值、标准差、最大值、最小值。

4.3.1 常用滚动统计量

def create_rolling_features(df, col='power', windows=[3, 6, 12, 24]):
    """构建滚动窗口统计特征"""
    for w in windows:
        df[f'rolling_mean_{w}'] = df.groupby('turbine_id')[col].transform(
            lambda x: x.rolling(w, min_periods=1).mean()
        )
        df[f'rolling_std_{w}'] = df.groupby('turbine_id')[col].transform(
            lambda x: x.rolling(w, min_periods=1).std()
        )
        df[f'rolling_max_{w}'] = df.groupby('turbine_id')[col].transform(
            lambda x: x.rolling(w, min_periods=1).max()
        )
        df[f'rolling_min_{w}'] = df.groupby('turbine_id')[col].transform(
            lambda x: x.rolling(w, min_periods=1).min()
        )
    return df

你想想看,滚动均值能反映「最近一段时间的平均水平」,滚动标准差能反映「波动剧烈程度」。这两个特征在风电预测中特别有用——风大的时候波动也大,模型需要知道这个规律。

4.3.2 窗口大小怎么定?

预测场景 推荐窗口大小 说明
超短期(15分钟-1小时) 3、6、12 捕捉分钟级波动
短期(1-6小时) 6、12、24 捕捉小时级趋势
中期(6-24小时) 12、24、48 捕捉日周期变化
长期(24-72小时) 24、48、72 捕捉多日趋势

个人经验:滚动窗口不要太多,3~4个不同尺度的窗口就够了。我一般选一个短窗口(比如3步)、一个中窗口(12步)、一个长窗口(24步)。再多的话特征维度爆炸,收益却不大。

4.4 特征工程整体流程

好了,咱们把三个部分串起来。下面这张图展示了完整的特征工程流程:

风电功率预测特征工程流程 原始时间序列 时间特征提取 滞后特征构建 滚动窗口统计 子特征 • 小时正弦/余弦 • 月份正弦/余弦 • 季节标签 子特征 • lag_1, lag_2, lag_3 • lag_6, lag_12, lag_24 • 按风机分组shift 子特征 • rolling_mean • rolling_std • rolling_max/min 特征合并 → 训练模型

流程很简单:原始时间序列进来,分三条路走。时间特征负责「现在是什么时候」,滞后特征负责「刚才发生了什么」,滚动统计负责「最近一段时间的趋势」。最后合并成一张大特征表,喂给模型。

4.5 实战中的避坑指南

最后,我把自己踩过的坑总结一下:

  1. 数据泄露问题:滞后特征和滚动特征都要在分组后、按时间顺序计算。千万别用未来的数据算过去的特征。
  2. 特征冗余:滞后24步和滚动均值24步,信息有重叠。我一般会做特征重要性排序,去掉冗余的。
  3. 缺失值处理:滚动窗口刚开始时会有NaN。用min_periods=1可以缓解,但前几个样本的特征值不太准。我习惯直接丢掉前几个样本。
  4. 特征标准化:时间特征(正弦余弦)天然在[-1,1]区间,但滞后特征和滚动特征的量级可能很大。记得做标准化或归一化。

一句话总结:时间特征告诉模型「什么时候」,滞后特征告诉模型「刚才怎样」,滚动特征告诉模型「趋势如何」。三者结合,风电功率预测的准确率能提升10%~20%。

好了,特征工程的基础就这些。下一节咱们聊聊数值天气预报特征怎么融合进来——那又是另一个故事了。


公众号:蓝海资料掘金营,微信deep3321