4、数据清洗实战:缺失值处理、异常值检测与修正、时间戳对齐

各位同学,欢迎来到数据清洗这一章。

说实话,很多做电价预测的朋友,模型调得再好,最后死就死在数据上。我见过太多人,拿到数据直接丢进模型,结果预测结果一塌糊涂,还以为是算法不行。其实啊,问题八成出在数据清洗这一步。

今天我们就来聊聊,电价数据清洗里最核心的三个环节:缺失值怎么补、异常值怎么抓、时间戳怎么对齐。嗯,一个一个来。

4.1 缺失值处理:别急着删,先看看为什么缺

电价数据里,缺失值太常见了。可能是采集设备故障,可能是通信中断,也可能是某个时段数据没上传。我个人习惯,第一步不是补,而是先搞清楚缺失的规律。

核心原则: 先分析缺失模式,再选择填充策略。

我一般把缺失分成三类:

  • 随机缺失: 比如某个小时的数据丢了,前后都正常。这种最好办,用插值就行。
  • 连续缺失: 比如连续几个小时甚至几天都没数据。这时候插值就不太靠谱了,得用更复杂的办法。
  • 结构性缺失: 比如每天凌晨2点到3点数据全没有。这往往是系统设计问题,得找业务方确认。

我在项目中遇到过最头疼的一次,是某省电网的负荷数据,每天中午12点到1点准时缺失。一开始我以为是随机问题,后来发现是采集系统在那个时段做数据归档,锁了数据库。嗯,这种就得跟业务方沟通,不能自己瞎补。

常用的填充方法,我列个表给大家参考:

缺失类型 推荐方法 适用场景
单个缺失 线性插值 前后数据平稳
连续少量缺失 三次样条插值 数据有趋势变化
连续大量缺失 前向填充 + 后向填充 缺失比例小于5%
结构性缺失 同日期历史均值 有周期性规律

代码实现其实不复杂,我给大家一个常用的片段:

import pandas as pd
import numpy as np

# 假设df是电价数据,'price'是电价列
# 线性插值
df['price_interp'] = df['price'].interpolate(method='linear')

# 前向填充,最多填充3个缺失
df['price_ffill'] = df['price'].fillna(method='ffill', limit=3)

# 同日期历史均值填充(假设有日期列)
df['price_hist'] = df.groupby(df['timestamp'].dt.hour)['price'].transform(
    lambda x: x.fillna(x.mean())
)
小技巧: 我建议先做缺失值可视化,用热力图看看缺失分布。如果缺失集中在某个时段,那大概率是系统问题,别急着补。

4.2 异常值检测:电价里的「妖魔鬼怪」

电价数据里的异常值,说白了就是那些「不该出现」的数字。比如正常电价在200-800元/MWh之间,突然蹦出来一个5000,那肯定有问题。

我曾经接手过一个项目,模型预测精度死活上不去。我查了三天数据,最后发现是某个变电站的计量表坏了,连续三个月输出都是0。你想想看,模型看到0电价,它当然学不会正常规律。

异常值检测,我常用的方法有这几种:

  • 3σ原则: 假设数据服从正态分布,超过均值±3倍标准差的就是异常。简单粗暴,但对电价这种有尖峰的数据不太友好。
  • 箱线图法: 用四分位数来界定异常,不受极端值影响。我个人比较推荐这个。
  • 孤立森林: 机器学习方法,适合高维数据。如果数据量大,可以考虑。

给大家看个箱线图的代码示例:

# 箱线图法检测异常
Q1 = df['price'].quantile(0.25)
Q3 = df['price'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

# 标记异常
df['is_outlier'] = (df['price'] < lower_bound) | (df['price'] > upper_bound)

# 查看异常值
print(df[df['is_outlier']]['price'].describe())
注意: 电价数据本身就有尖峰特性,比如极端天气下的高价。别把真实的市场行为当成异常给删了。我建议先结合业务知识判断,再决定是否修正。

4.3 异常值修正:不是删,是「修」

检测出异常值之后,怎么办?直接删掉?别急。

我个人的经验是:能修就不删。因为删除数据会破坏时间序列的连续性,尤其是电价这种有强时序依赖的数据。

修正方法我常用这几种:

  • 替换为前后均值: 适用于单个孤立异常点。
  • 替换为同周期历史值: 比如用昨天同一小时的电价替换今天的异常值。
  • 用模型预测值替换: 如果异常值比较多,可以先用正常数据训练一个简单模型,预测异常点的合理值。

代码实现:

# 替换为前后均值
df['price_corrected'] = df['price'].copy()
outlier_idx = df[df['is_outlier']].index

for idx in outlier_idx:
    # 取前后各3个正常值的均值
    window = df.loc[idx-3:idx+3, 'price']
    window = window[~window.isna() & ~df.loc[window.index, 'is_outlier']]
    if len(window) > 0:
        df.loc[idx, 'price_corrected'] = window.mean()
避坑指南: 我曾经犯过一个错误,把连续几天的异常值都用同一个方法修正,结果引入了一个「假趋势」。后来我改用分段修正,效果好了很多。

4.4 时间戳对齐:让数据「步调一致」

电价数据往往来自多个来源:电网调度系统、交易中心、气象站……每个系统的时间精度可能不一样。有的精确到秒,有的只到小时。时间戳不对齐,模型根本没法用。

我遇到过最离谱的一次,两个数据源的时间差了整整8个小时。一个用的是UTC时间,一个用的是北京时间。你想想看,把中午12点的电价和凌晨0点的负荷放在一起训练,模型能学出什么来?

时间戳对齐的核心步骤:

  1. 统一时区: 全部转为北京时间(UTC+8),或者全部用UTC。
  2. 统一频率: 比如全部重采样到15分钟间隔或1小时间隔。
  3. 处理重复时间戳: 同一个时间点出现多条数据,需要去重或聚合。
  4. 处理缺失时间戳: 比如某个时间点没有数据,需要补上。

代码示例:

# 统一时区
df['timestamp'] = pd.to_datetime(df['timestamp'], utc=True)
df['timestamp'] = df['timestamp'].dt.tz_convert('Asia/Shanghai')

# 重采样到1小时间隔
df = df.set_index('timestamp').resample('1H').mean().reset_index()

# 检查是否有重复时间戳
duplicates = df[df.duplicated(subset='timestamp', keep=False)]
if len(duplicates) > 0:
    print(f"发现{len(duplicates)}个重复时间戳,正在处理...")
    df = df.drop_duplicates(subset='timestamp', keep='first')
核心要点: 时间戳对齐不是简单的「对一下时间」,而是要确保所有数据在同一个时间坐标系下,频率一致,没有空洞。

4.5 本章知识体系

下面这张图,是我自己总结的数据清洗流程。你看一遍,应该就能明白整个逻辑了。

电价数据清洗核心流程 原始电价数据 缺失值处理 线性插值 历史均值 前向填充 样条插值 异常值检测 3σ原则 箱线图法 孤立森林 业务规则 异常值修正 前后均值 历史替换 模型预测 业务修正 时间戳对齐

嗯,这张图把整个流程串起来了。从原始数据开始,经过缺失值处理、异常值检测、异常值修正,最后到时间戳对齐,每一步都有对应的处理方法。你在实际项目中,可以按照这个流程一步步来。

好了,数据清洗这部分就讲到这里。记住一句话:数据清洗花的时间,永远值得。你在这里省下的功夫,后面模型会加倍还给你。


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