3. 数据预处理实战:缺失值处理、异常值检测与修正、时间戳规范化

各位同学,欢迎来到数据预处理这一章。

说实话,在电力负荷预测这个领域摸爬滚打这么多年,我最大的体会就是:模型再花哨,数据不行全白搭。你想想看,原始数据从SCADA系统或者电表采集过来,那叫一个「脏」。缺失、异常、时间戳错乱,简直是家常便饭。今天我就把压箱底的处理套路分享给你。

3.1 缺失值处理:别让空值毁了你的模型

先聊聊缺失值。电力数据里,缺失值太常见了——通信中断、采集设备故障、人为误操作,原因五花八门。我个人习惯,拿到数据第一件事就是画个缺失值热力图,看看哪些列、哪些时间段在「裸奔」。

3.1.1 插值法:用邻居的值来补

对于短时间、连续的缺失(比如连续缺了3个点),插值法是最稳妥的。我推荐用线性插值三次样条插值

import pandas as pd
import numpy as np

# 模拟一段负荷数据,中间有缺失
ts = pd.Series([100, np.nan, np.nan, 130, 145, np.nan, 170],
               index=pd.date_range('2024-01-01', periods=7, freq='H'))

# 线性插值
ts_linear = ts.interpolate(method='linear')
print(ts_linear)

# 三次样条插值(适合曲线较平滑的负荷)
ts_spline = ts.interpolate(method='spline', order=3)
print(ts_spline)
我的经验: 如果缺失段落在负荷平稳期(比如深夜),线性插值就够了。如果是在早晚高峰这种陡变段,用三次样条插值效果更好。我在一个省级电网项目里,就是用样条插值把一段30分钟的缺失数据补得跟真实值几乎一样。

3.1.2 填充法:用统计量来填

如果缺失值比较多(比如超过10%),或者缺失段落在边界上,插值就不太靠谱了。这时候我会用填充法。

  • 前向填充(ffill):用上一个非缺失值填充。适合负荷缓慢变化的场景。
  • 后向填充(bfill):用下一个非缺失值填充。适合数据采集刚恢复的场景。
  • 均值/中位数填充:用同一时刻的历史均值填充。比如用过去7天同一时刻的负荷均值来补今天的缺失。
# 前向填充
ts_ffill = ts.fillna(method='ffill')

# 用历史同期均值填充(假设有7天数据)
def fill_with_historical_mean(df, target_col, window=7):
    """用过去N天同一时刻的均值填充缺失值"""
    df[target_col] = df[target_col].fillna(
        df[target_col].rolling(window=window, min_periods=1).mean()
    )
    return df
注意: 千万别用全局均值填充!电力负荷有明显的日周期性和周周期性,用全局均值会把峰谷特征抹平。我曾经见过一个团队这么干,结果模型预测出来的负荷曲线跟一条直线似的,完全失去了波动性。

3.2 异常值检测与修正:揪出那些「离谱」的数据点

异常值比缺失值更隐蔽,也更危险。缺失值顶多是信息丢失,异常值会直接带偏模型参数。我把它分为两类:全局异常局部异常

3.2.1 全局异常检测:3σ法则与箱线图

全局异常,说白了就是数值超出了正常范围。比如某台变压器的负荷突然飙到额定容量的2倍,这明显不合理。

3σ法则:假设数据服从正态分布,超出均值±3倍标准差的值视为异常。

def detect_outliers_3sigma(series, n=3):
    mean = series.mean()
    std = series.std()
    outliers = (series < mean - n*std) | (series > mean + n*std)
    return outliers

# 找出异常值
outliers = detect_outliers_3sigma(ts)
print(f"发现 {outliers.sum()} 个异常值")

箱线图法:用四分位数来界定。超出Q1-1.5*IQR或Q3+1.5*IQR的视为异常。这个方法对非正态分布的数据更友好。

3.2.2 局部异常检测:滑动窗口法

全局方法有个问题:它会把正常的高峰负荷误判为异常。比如夏天中午的尖峰负荷,数值很高,但它是正常的。这时候就需要局部检测。

我的做法是:用滑动窗口计算局部均值和标准差,看当前点是否偏离窗口内的正常波动范围。

def detect_local_outliers(series, window=24, threshold=3):
    """滑动窗口检测局部异常"""
    rolling_mean = series.rolling(window=window, center=True).mean()
    rolling_std = series.rolling(window=window, center=True).std()
    
    z_scores = (series - rolling_mean) / rolling_std
    outliers = z_scores.abs() > threshold
    return outliers

# 窗口大小设为24(小时),适合日周期数据
local_outliers = detect_local_outliers(ts, window=24, threshold=3)

修正策略: 发现异常值后,别直接删掉。我通常的做法是:

  1. 先标记异常点
  2. 用前后正常值的插值替换
  3. 或者用历史同期值替换

直接删除会导致时间序列不连续,后续的时序模型会报错。

3.3 时间戳规范化:让时间对齐

时间戳问题,看着简单,坑却不少。不同数据源的时间格式五花八门:有的用'2024-01-01 00:00:00',有的用'2024/01/01 0:00',还有的用时间戳整数。更头疼的是时区问题和采样间隔不一致。

3.3.1 统一格式与时区

我建议所有数据统一转为UTC时间,再转成pandas的DatetimeIndex。这样后续的resample、shift操作才方便。

# 统一转为DatetimeIndex
df['timestamp'] = pd.to_datetime(df['timestamp'], utc=True)
df.set_index('timestamp', inplace=True)

# 如果数据是东八区,转成UTC再处理
df.index = df.index.tz_convert('Asia/Shanghai')

3.3.2 重采样到固定频率

电力负荷数据最常见的频率是15分钟、30分钟或1小时。但原始数据可能因为采集延迟,出现时间戳偏移。比如本该是整点的数据,记录成了00:01或23:59。

我的处理流程:

  1. 先检查数据是否等间隔
  2. 如果不是,用resample重采样到目标频率
  3. 重采样时用均值或中位数聚合
# 检查采样间隔是否均匀
time_diffs = df.index.to_series().diff().dropna()
print(f"最小间隔: {time_diffs.min()}, 最大间隔: {time_diffs.max()}")

# 重采样到1小时,用均值填充
df_hourly = df.resample('1H').mean()

# 如果重采样后还有缺失,用插值补
df_hourly = df_hourly.interpolate(method='time')
避坑指南: 我曾经在一个项目里,发现数据明明标着15分钟间隔,但实际有10分钟、20分钟、甚至5分钟的。后来一查,是采集系统在数据积压时自动调整了采样频率。所以,永远不要相信数据标注的频率,一定要自己算一遍时间差

3.4 本章知识体系总览

下面这张图,是我自己总结的数据预处理核心流程。你可以把它当作操作手册,每次拿到新数据,按这个步骤走一遍,基本不会出大问题。

数据预处理核心流程 原始负荷数据 缺失值处理 插值法 填充法 异常值检测与修正 全局检测 局部检测 时间戳规范化 统一格式 重采样 干净可用的数据

好了,数据预处理这块的核心内容就这些。记住一句话:预处理花的时间,会在模型训练和调参阶段加倍还给你。别嫌麻烦,每一步都走扎实了,后面的工作才能顺风顺水。


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