4. 数据清洗基础:缺失值处理、重复值检测与异常值识别

各位同行,大家好。今天我们来聊聊数据清洗。说实话,我刚开始做风电运维那会儿,觉得数据分析就是跑跑算法、画几张漂亮的图。后来被现实狠狠教育了一回——有一次我拿到的SCADA数据,光缺失值就占了15%,直接导致后续的功率曲线分析全偏了。从那以后,我养成了一个习惯:拿到数据,先花30%的时间做清洗。

数据清洗,说白了就是给数据“洗澡”。脏数据不洗掉,后面再高级的分析都是白搭。今天咱们重点讲四个基本功:缺失值处理、重复值检测、异常值识别,还有时间序列索引的设置。

核心原则:清洗策略没有绝对的对错,关键是要理解你的数据来源和业务场景。我个人的经验是——宁可少用数据,也别用错数据。

4.1 缺失值处理:删除还是填充?

缺失值在风电数据里太常见了。传感器故障、通信中断、维护停机……都会导致数据点丢失。处理方式无非两种:删除或填充。

4.1.1 直接删除缺失值

什么时候该删?我一般遵循这个原则:

  • 缺失比例很低(比如小于5%),直接删,省事。
  • 缺失是随机发生的,不是系统性缺失。
  • 后续分析对数据完整性要求高,比如做FFT频谱分析。
import pandas as pd
import numpy as np

# 读取数据
df = pd.read_csv('wind_turbine_data.csv')

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

# 删除所有包含缺失值的行
df_dropped = df.dropna()

# 或者只删除某列缺失的行
df_dropped_col = df.dropna(subset=['wind_speed', 'power_output'])

注意:我曾经犯过一个错——直接删除了大量缺失行,结果发现删除后的数据全是白天时段的,因为夜间通信经常中断。这导致了严重的采样偏差。所以删除前,一定要检查缺失模式。

4.1.2 填充缺失值

如果缺失比例较高,或者数据本身很珍贵,那就得填充。常用的方法有:

填充方法 适用场景 代码示例
固定值填充 已知传感器量程范围,比如风速不可能为负 df.fillna(0)
均值/中位数填充 数据分布较稳定,没有剧烈波动 df.fillna(df.mean())
前向填充 时间序列数据,短时间缺失用前一时刻值 df.fillna(method='ffill')
插值法 数据变化平滑,线性或样条插值 df.interpolate(method='linear')
# 前向填充:适合短时间缺失
df_ffill = df.fillna(method='ffill', limit=3)  # 最多连续填充3个

# 线性插值:适合风速、功率等连续变量
df_interp = df.interpolate(method='linear')

# 按机组分组填充:不同机组工况不同
df_grouped = df.groupby('turbine_id').transform(lambda x: x.fillna(x.mean()))

我的习惯:对于风速、功率这类物理量,我优先用插值法。对于状态信号(比如停机标志),我一般用前向填充。记住,填充不是编造数据,而是合理估计。

4.2 重复值检测与删除

重复数据在风电数据里也挺烦人。有时候是数据采集系统重复上传,有时候是数据库写入时出了bug。重复数据会放大某些特征的权重,导致模型过拟合。

检测重复值其实很简单:

# 检测完全重复的行
duplicates = df.duplicated()
print(f"重复行数: {duplicates.sum()}")

# 查看重复的具体行
df_dups = df[df.duplicated(keep=False)]

# 删除重复行,保留第一次出现的
df_unique = df.drop_duplicates()

# 按特定列判断重复(比如时间戳)
df_unique_time = df.drop_duplicates(subset=['timestamp'])

嗯,这里要注意一点:不是所有重复都要删。比如同一时刻不同传感器的数据,虽然时间戳重复,但数据本身有意义。我一般会先看重复的原因,再决定怎么处理。

避坑指南:我曾经遇到过一个项目,重复值检测出来有2000多条,我直接全删了。结果发现那些“重复”其实是同一时刻不同机舱位置的温度传感器数据。删完之后,温度场分析完全没法做。所以,先理解业务,再动手删

4.3 异常值初步识别:3σ原则

异常值在风电数据里太常见了。传感器漂移、雷击干扰、叶片结冰……都会产生异常值。3σ原则是最简单粗暴的方法,但很实用。

3σ原则的核心思想:假设数据服从正态分布,那么99.7%的数据会落在均值±3个标准差范围内。超出这个范围的,就视为异常。

def detect_outliers_3sigma(df, column):
    mean = df[column].mean()
    std = df[column].std()
    lower_bound = mean - 3 * std
    upper_bound = mean + 3 * std
    
    outliers = df[(df[column] < lower_bound) | (df[column] > upper_bound)]
    return outliers, lower_bound, upper_bound

# 检测风速异常值
outliers, low, high = detect_outliers_3sigma(df, 'wind_speed')
print(f"风速正常范围: [{low:.2f}, {high:.2f}]")
print(f"检测到异常值: {len(outliers)} 个")

重要提醒:3σ原则对正态分布的数据效果最好。但风电数据往往不是完美的正态分布——风速是威布尔分布,功率是截断分布。所以3σ只能作为初步筛选,不能完全依赖。

我个人习惯的做法是:先用3σ筛一遍,把明显离谱的值(比如风速200m/s、功率负值)标记出来。然后再结合业务知识做二次判断。比如风速超过切出风速(通常25m/s)但机组还在发电,这肯定是异常。

4.4 时间序列索引设置

风电数据本质上是时间序列。如果不把时间戳设成索引,很多时间相关的操作会非常麻烦。这一步看似简单,但做不好后面全是坑。

# 假设时间戳列名为 'timestamp'
df['timestamp'] = pd.to_datetime(df['timestamp'])

# 设置为索引
df.set_index('timestamp', inplace=True)

# 检查时间索引是否连续
print(f"时间频率: {pd.infer_freq(df.index)}")
print(f"缺失时间点: {pd.date_range(start=df.index.min(), end=df.index.max(), freq='10min').difference(df.index)}")

设置完索引后,很多操作就方便了:

  • 重采样:把10分钟数据聚合为小时数据
  • 滑动窗口:计算滚动平均值
  • 时间切片:快速提取某段时间的数据
# 重采样为小时数据
df_hourly = df.resample('1H').mean()

# 滚动窗口计算(比如30分钟平均)
df_rolling = df['power_output'].rolling(window=3).mean()

# 时间切片
df_jan = df['2024-01-01':'2024-01-31']

一个小技巧:设置索引前,先检查时间戳有没有重复或乱序。我习惯用 df.sort_index() 排个序,确保时间轴是递增的。不然做重采样时,结果会乱得一塌糊涂。

4.5 本章知识体系

下面这张图是我自己总结的数据清洗流程,大家可以参考:

数据清洗基础流程 原始SCADA数据 步骤1:缺失值处理 删除(缺失<5%) 填充(插值/前向) 步骤2:重复值检测与删除 步骤3:异常值识别(3σ)

这张图把今天的核心内容串起来了。从原始数据开始,先处理缺失值(删还是填),再干掉重复数据,最后用3σ原则初步识别异常。每一步做完,都建议回头检查一下数据分布有没有变化。

好了,数据清洗的基础就讲到这里。记住,清洗不是一次性工作,而是一个迭代过程。你可能会反复回来调整策略——这很正常。我做了这么多年,每次拿到新数据,还是会先跑一遍清洗流程,看看有没有什么新问题。

专注资料整理