数据清洗与特征工程:异常值检测与处理
说实话,做风电预测这几年,我踩过最大的坑就是数据质量。你想想看,风机叶片上挂着的传感器,风吹日晒雨淋,数据能有多干净?我接手第一个风电项目时,拿到数据就急着建模,结果模型预测误差大得离谱。后来一查,原来是数据里混着好几个异常值,直接把模型带偏了。
所以这一章,咱们就聊聊数据清洗和特征工程。说白了,就是给数据「洗个澡」,再给它「化个妆」,让它能更好地为模型服务。
核心观点:数据清洗不是可有可无的步骤,而是决定模型成败的关键。我个人的习惯是,花 70% 的时间在数据准备上,30% 的时间建模调参。别嫌多,值得。
4.1 异常值检测与处理
异常值,就是那些明显偏离正常范围的数据点。比如风速突然飙到 50m/s,或者出力突然变成负值。这些数据要么是传感器故障,要么是通信异常,必须处理掉。
4.1.1 3-sigma 法则
这个方法简单粗暴,但很实用。它假设数据服从正态分布,那么 99.7% 的数据会落在均值 ±3 个标准差范围内。超出这个范围的,就是异常值。
我在项目中遇到过这样的情况:某台风机的出力数据,用 3-sigma 一查,发现有好几个点明显偏低。后来去现场排查,发现是变桨系统出了故障。你看,数据清洗还能帮我们发现设备问题。
import numpy as np
import pandas as pd
def detect_outliers_3sigma(data, column):
mean = data[column].mean()
std = data[column].std()
lower_bound = mean - 3 * std
upper_bound = mean + 3 * std
outliers = data[(data[column] < lower_bound) | (data[column] > upper_bound)]
return outliers
# 示例
df = pd.read_csv('wind_power.csv')
outliers = detect_outliers_3sigma(df, 'active_power')
print(f"检测到 {len(outliers)} 个异常值")
小技巧:如果数据分布偏态严重,3-sigma 的效果会打折扣。这时候可以先做对数变换,或者改用 IQR 方法。
4.1.2 IQR 方法
IQR(四分位距)方法不依赖正态分布假设,更稳健。它用 Q1(25% 分位数)和 Q3(75% 分位数)来定义正常范围:
正常范围 = [Q1 - 1.5 * IQR, Q3 + 1.5 * IQR]
嗯,这里要注意:1.5 这个系数是经验值。如果你觉得数据波动大,可以放宽到 3。我曾经处理过一个极端案例,用 1.5 倍 IQR 筛掉了 10% 的数据,后来发现那些其实是正常波动。所以,参数要灵活调整。
def detect_outliers_iqr(data, column):
Q1 = data[column].quantile(0.25)
Q3 = data[column].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = data[(data[column] < lower_bound) | (data[column] > upper_bound)]
return outliers
4.2 缺失值插补
数据缺失是家常便饭。风机停机检修、通信中断、传感器故障,都会导致数据缺失。直接删除缺失行?太浪费了。尤其是时间序列数据,每一行都宝贵。
4.2.1 线性插值
线性插值假设数据在缺失区间内是线性变化的。对于短时间缺失(比如几分钟),效果很好。我习惯用 pandas 的 interpolate() 方法,一行代码搞定。
# 线性插值
df['active_power'] = df['active_power'].interpolate(method='linear')
# 也可以指定方向
df['active_power'] = df['active_power'].interpolate(method='linear', limit_direction='both')
注意:如果缺失区间太长(比如超过 1 小时),线性插值就不靠谱了。这时候我会考虑用 KNN 或者干脆标记为缺失。
4.2.2 KNN 插值
KNN 插值的思想是:找到与缺失样本最相似的 K 个邻居,用它们的平均值来填充。这个方法能利用多个特征的信息,比线性插值更智能。
我记得有一次,某台风机连续缺失了 3 个小时的数据。线性插值出来的曲线很假,但用 KNN 插值后,曲线跟相邻风机的出力模式非常吻合。这就是 KNN 的优势——它能学到数据的内在结构。
from sklearn.impute import KNNImputer
# 准备特征矩阵(注意:KNN 需要多维特征)
features = df[['wind_speed', 'wind_direction', 'temperature', 'active_power']]
# KNN 插值
imputer = KNNImputer(n_neighbors=5)
features_imputed = imputer.fit_transform(features)
# 替换回原数据
df['active_power'] = features_imputed[:, 3]
4.3 特征构造
原始数据就那么几列:风速、风向、温度、出力。但模型需要更多信息才能学得好。特征构造就是「无中生有」,从原始数据中衍生出更有价值的特征。
4.3.1 时间特征
风电出力有明显的周期性:白天高、晚上低;冬天高、夏天低。所以时间特征非常有用。
- 小时特征:0-23,反映日内变化
- 月份特征:1-12,反映季节变化
- 星期特征:0-6,反映周内变化(虽然风电不太受工作日影响,但用电负荷会)
- 是否节假日:0/1 二值特征
# 构造时间特征
df['hour'] = df['timestamp'].dt.hour
df['month'] = df['timestamp'].dt.month
df['day_of_week'] = df['timestamp'].dt.dayofweek
df['is_weekend'] = df['day_of_week'].isin([5, 6]).astype(int)
4.3.2 滞后特征
风电出力有很强的自相关性。前一时刻的出力,往往能预测下一时刻的出力。滞后特征就是利用这种时间依赖关系。
我个人习惯构造 1 步、3 步、6 步、12 步的滞后特征。为什么选这些步长?因为 1 步反映瞬时变化,3 步反映短期趋势,6 步和 12 步反映中期趋势。当然,具体步长要根据数据采样频率来定。
# 构造滞后特征
lags = [1, 3, 6, 12]
for lag in lags:
df[f'power_lag_{lag}'] = df['active_power'].shift(lag)
# 注意:滞后会导致前几行出现 NaN,需要处理
df = df.dropna()
避坑指南:我曾经犯过一个错误——用未来的数据构造滞后特征。比如用 t+1 时刻的数据预测 t 时刻,这在训练时没问题,但上线后就会「未来泄露」。切记,滞后特征只能用过去的数据。
4.3.3 滚动统计特征
滚动统计特征能捕捉数据的局部统计特性。比如过去 1 小时的平均风速、最大出力、出力波动率等。这些特征能反映风况的稳定性。
# 滚动窗口统计
window = 6 # 假设数据是 10 分钟采样,6 个点就是 1 小时
df['power_rolling_mean'] = df['active_power'].rolling(window=window).mean()
df['power_rolling_std'] = df['active_power'].rolling(window=window).std()
df['power_rolling_max'] = df['active_power'].rolling(window=window).max()
df['power_rolling_min'] = df['active_power'].rolling(window=window).min()
# 波动率:最大值与最小值之差
df['power_rolling_range'] = df['power_rolling_max'] - df['power_rolling_min']
你想想看,如果过去 1 小时风速忽高忽低,滚动标准差就会很大。这个特征能告诉模型:「现在风况不稳定,预测要保守一些。」
知识体系总览
下面这张图总结了本章的核心内容。你可以把它当作一个检查清单,做数据清洗时对照着来。
好了,这一章的内容就到这里。数据清洗和特征工程是风电预测的基石,花再多时间都不为过。下一章我们会聊特征选择与降维,到时候见。
公众号:蓝海资料掘金营,微信deep3321