3、数据预处理(上):缺失值处理、异常值检测与修正、数据清洗实战

各位同学,咱们今天聊聊数据预处理。说实话,这步看着不起眼,但我在实际项目中吃过不少亏。你想想看,模型再牛,喂进去的是垃圾,吐出来的只能是更垃圾。我见过太多人把精力全花在调参上,结果数据本身一塌糊涂——嗯,这其实是在沙滩上盖大楼。

数据预处理,说白了就是给原始数据「洗澡」。把脏的、乱的、不合理的统统处理掉。咱们今天先讲上半部分:缺失值、异常值,以及数据清洗的实战套路。

核心观点:数据预处理占整个建模工作量的 60% 以上。别嫌烦,这一步做扎实了,后面模型训练会顺滑很多。

数据预处理(上)知识体系 缺失值处理 删除 · 填充 · 插值 异常值检测 3σ · IQR · 业务规则 数据清洗实战 去重 · 格式统一 · 逻辑校验 原始数据 → 缺失值处理 → 异常值检测 → 数据清洗 → 干净数据 ⚠️ 顺序很重要:先处理缺失,再检测异常,最后做清洗

一、缺失值处理:别让「空」坑了你

电力数据里,缺失值太常见了。采集器掉线、通信故障、存储异常——随便哪个环节出点问题,数据就空了。我个人习惯,拿到数据第一件事就是跑个 df.isnull().sum(),看看哪些列在「偷懒」。

处理缺失值,主要有三种思路:

  1. 直接删除——简单粗暴,但数据量少的时候别用。我有个项目,删完缺失行后只剩 60% 的数据,模型直接崩了。
  2. 填充——用均值、中位数、众数填。功率数据我一般用中位数,因为抗异常值干扰。
  3. 插值——时间序列数据,前后值线性插值效果不错。比如 10:00 缺失,就用 09:55 和 10:05 的平均值。

我的小技巧:对于光伏功率数据,如果缺失时间短(< 15分钟),用线性插值;如果缺失时间长(> 1小时),建议用前一天同时段的数据填充。我在西北某光伏电站就是这么干的,效果比均值填充好 30%。

# 缺失值处理示例
import pandas as pd
import numpy as np

# 读取数据
df = pd.read_csv('power_data.csv', parse_dates=['time'])

# 检查缺失情况
print(df.isnull().sum())

# 方法1:删除缺失严重的列(缺失率 > 50%)
df = df.dropna(thresh=len(df)*0.5, axis=1)

# 方法2:中位数填充(功率列)
df['power'] = df['power'].fillna(df['power'].median())

# 方法3:线性插值(时间序列推荐)
df['power'] = df['power'].interpolate(method='linear')

二、异常值检测:揪出那些「捣乱分子」

异常值是什么?就是那些明显不合理的数据点。比如功率突然飙到 2000 MW,而装机容量才 100 MW——这明显是传感器抽风了。

我常用的检测方法有三种:

方法 适用场景 阈值 我的评价
3σ 原则 数据近似正态分布 均值 ± 3倍标准差 简单好用,但电力数据往往偏态
IQR 四分位法 非正态分布、有离群点 Q1 - 1.5IQR 到 Q3 + 1.5IQR 我最常用,鲁棒性强
业务规则法 有明确物理约束 如功率 ≤ 额定容量 必须结合,别光靠统计

注意:我曾经犯过一个错——用 3σ 原则处理风电数据,结果把正常的高风速发电点全删了。后来才发现风电功率分布是右偏的,3σ 根本不适用。所以,一定要先看数据分布,再选方法。

# 异常值检测实战
# IQR 方法
Q1 = df['power'].quantile(0.25)
Q3 = df['power'].quantile(0.75)
IQR = Q3 - Q1

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

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

# 业务规则:功率不能为负,不能超过额定容量
rated_capacity = 100  # MW
df['is_invalid'] = (df['power'] < 0) | (df['power'] > rated_capacity)

三、异常值修正:不是所有异常都要删

发现异常值后怎么办?直接删?别急。我一般分三种情况处理:

  • 明显错误(如负功率、超容量)→ 修正为 NaN,然后用缺失值方法处理
  • 统计离群点(如 3σ 外的点)→ 先看业务上是否合理,不合理再修正
  • 临界值(如略超阈值)→ 用上下限截断(Winsorize 处理)

修正原则:能不删就不删。电力数据每一秒都宝贵,删了可能破坏时间连续性。我一般先用截断,实在不行才删除。

# 异常值修正示例
# 截断处理(Winsorize)
df['power'] = df['power'].clip(lower=lower_bound, upper=upper_bound)

# 或者:将异常值替换为边界值
df.loc[df['power'] < lower_bound, 'power'] = lower_bound
df.loc[df['power'] > upper_bound, 'power'] = upper_bound

四、数据清洗实战:把「毛刺」磨平

数据清洗,说白了就是让数据变得「规整」。我总结了几条实战经验:

  1. 去重——时间戳重复的数据要小心。我遇到过采集器重复上报,同一时间点两条数据,功率还不一样。这时候一般取平均值或保留第一条。
  2. 格式统一——时间格式必须一致。有人用 "2024-01-01 10:00",有人用 "2024/01/01 10:00:00",不统一的话 pandas 会报错。
  3. 逻辑校验——比如功率不能大于装机容量,风速不能为负,温度不能超过物理极限。这些规则要写进清洗脚本里。

避坑指南:我曾经在处理某风电场数据时,发现功率曲线有「阶梯状」跳变。查了半天,原来是数据采集器的时间戳精度不够,导致数据被「四舍五入」到了整点。后来我加了时间戳精度校验,才把问题解决。所以,清洗时别忘了检查时间戳的精度。

# 数据清洗完整流程
def clean_power_data(df):
    # 1. 去重
    df = df.drop_duplicates(subset='time', keep='first')
    
    # 2. 时间格式统一
    df['time'] = pd.to_datetime(df['time'], format='%Y-%m-%d %H:%M')
    
    # 3. 逻辑校验
    df = df[(df['power'] >= 0) & (df['power'] <= rated_capacity)]
    df = df[(df['wind_speed'] >= 0) & (df['wind_speed'] <= 40)]  # 风速合理范围
    
    # 4. 排序
    df = df.sort_values('time').reset_index(drop=True)
    
    return df

# 执行清洗
df_clean = clean_power_data(df)
print(f"清洗后数据量: {len(df_clean)} 条")

好了,数据预处理的上半部分就讲到这里。记住一句话:数据质量决定模型上限。你花 80% 的时间在数据上,都不为过。下一节咱们接着聊数据标准化、特征工程那些事——嗯,到时候再细说。


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