4. 数据预处理实战(下):归一化、重采样与特征工程
好,咱们接着聊。上一节我们把缺失值、异常值这些「脏数据」处理干净了。但说实话,那只是热身。真正让模型起飞的关键,在于今天要讲的这三个环节:归一化/标准化、时间序列重采样、特征工程。
我刚开始做功率预测那会儿,总觉得特征越多越好。结果呢?模型训练慢得像蜗牛,效果还差。后来才明白——数据不归一化,模型根本学不动。你想想看,功率值动辄几百兆瓦,温度才几十度,神经网络一算,全被大数值带偏了。
4.1 数据归一化与标准化
这两个概念经常被混用,我简单区分一下:
- 归一化(Min-Max Scaling):把数据压缩到 [0,1] 区间。公式是 (x - min) / (max - min)。适合数据分布比较均匀的情况。
- 标准化(Z-score):把数据变成均值为0、标准差为1的分布。公式是 (x - μ) / σ。适合数据有异常值或分布偏态的情况。
我个人习惯:功率预测场景下,优先用标准化。为什么?因为风电、光伏功率经常有极端天气导致的尖峰值,归一化会被这些尖峰压扁,导致正常区间内的分辨率丢失。
代码实现很简单,用 scikit-learn 就行:
from sklearn.preprocessing import StandardScaler, MinMaxScaler
# 标准化
scaler_std = StandardScaler()
df['power_scaled'] = scaler_std.fit_transform(df[['power']])
# 归一化
scaler_mm = MinMaxScaler()
df['power_norm'] = scaler_mm.fit_transform(df[['power']])
4.2 时间序列重采样
原始数据往往是秒级或分钟级的,但我们的预测目标通常是15分钟或1小时。这就涉及重采样。
重采样说白了就是改变时间频率。有两种方向:
- 降采样(Downsampling):从高频到低频。比如1分钟数据 -> 15分钟数据。常用聚合函数:均值、最大值、最小值。
- 升采样(Upsampling):从低频到高频。比如1小时数据 -> 15分钟数据。常用插值方法:线性插值、前向填充。
功率预测里,降采样更常见。我一般这样处理:
# 确保时间列是datetime类型
df['time'] = pd.to_datetime(df['time'])
df.set_index('time', inplace=True)
# 降采样到15分钟,取均值
df_15min = df.resample('15T').mean()
# 如果数据有缺失,用前向填充
df_15min.fillna(method='ffill', inplace=True)
嗯,这里要注意:重采样前一定要检查时间索引是否连续。我曾经遇到过数据采集设备跳变,导致时间戳不连续,重采样后出现了大段空白。后来我养成了习惯,先做时间索引的完整性检查:
# 检查时间是否连续
time_diff = df.index.to_series().diff()
print(time_diff.value_counts().head())
4.3 特征工程:构建滞后特征
功率预测的核心规律是什么?历史会重演。今天的功率和昨天的同一时刻、前1小时、前15分钟,都有强相关性。这就是滞后特征(Lag Features)。
我常用的滞后窗口:
| 滞后类型 | 窗口 | 说明 |
|---|---|---|
| 短期滞后 | 1步、2步、3步 | 捕捉最近时刻的惯性 |
| 周期滞后 | 96步(15分钟粒度,24小时) | 捕捉日周期性 |
| 周周期滞后 | 672步(15分钟粒度,7天) | 捕捉周周期性 |
代码实现:
# 构建滞后特征
for lag in [1, 2, 3, 96, 672]:
df[f'lag_{lag}'] = df['power'].shift(lag)
# 删除因shift产生的NaN行
df.dropna(inplace=True)
4.4 特征工程:滚动统计特征
滞后特征只能看到「点」,但功率变化往往有「趋势」。滚动统计特征就是用来捕捉这种趋势的。
常用的滚动统计量:
- 滚动均值:过去N个时刻的平均值,反映趋势
- 滚动标准差:过去N个时刻的波动程度,反映稳定性
- 滚动最大值/最小值:过去N个时刻的极值,反映边界
- 滚动偏度/峰度:过去N个时刻的分布形态,反映异常
我一般这样构建:
# 滚动窗口大小:1小时(4个15分钟点)
window = 4
df['rolling_mean'] = df['power'].rolling(window=window).mean()
df['rolling_std'] = df['power'].rolling(window=window).std()
df['rolling_max'] = df['power'].rolling(window=window).max()
df['rolling_min'] = df['power'].rolling(window=window).min()
# 删除前window-1个NaN值
df.dropna(inplace=True)
你想想看,如果只给模型看当前功率值,它怎么知道现在是「上升趋势」还是「下降趋势」?滚动均值就能告诉它——哦,过去1小时平均功率在涨,那下一时刻大概率也涨。
4.5 知识体系总览
说了这么多,我画了一张图帮你理清思路。这张图展示了数据预处理的完整流程,从原始数据到最终特征矩阵:
这张图把整个流程串起来了。从原始数据开始,经过清洗、归一化、重采样,再到特征工程,最后得到干净、规范、信息丰富的特征矩阵。每一步都有坑,但每一步也都有技巧。
好了,这一节的内容就到这里。数据预处理是模型效果的地基,地基打不牢,上面盖再高的楼也白搭。希望你能把这些方法真正用起来,下次跑模型的时候,你会发现——原来效果提升的秘诀,就藏在这些细节里。