4. 数据清洗实战:缺失值处理与异常值修正
各位工程师朋友,咱们接着聊。上一章我们把数据从历史库里拽了出来,但说实话,那些数据就像刚从泥地里挖出来的电缆——脏得很。今天这章,我就带你亲手把这些数据洗干净。
数据清洗,说白了就是三件事:缺的补上、错的改掉、重复的删掉。听起来简单?我在项目里踩过的坑可不少。咱们一个一个来。
4.1 缺失值处理:别让空值毁了你的模型
SCADA系统里,缺失值太常见了。传感器掉线、通讯中断、存储故障……原因五花八门。但模型训练时,空值就是毒药。
我个人习惯,先看看缺失率。低于5%的,直接删掉那几行;超过30%的,这变量基本废了,考虑直接扔掉。中间那档,才值得花功夫去补。
4.1.1 插值法:线性与样条
插值法适合连续变化的物理量,比如温度、压力、流量。我常用的有两种:
- 线性插值:两点之间画直线。简单粗暴,适合变化平缓的场景。
- 样条插值:用多项式曲线连接。更平滑,适合有波动的数据。
举个例子,某条管线的压力数据在10:00和10:05都有值,但10:02缺失了。线性插值就是按时间比例算个中间值。我在某石化项目里,用样条插值补过一段振动数据,效果比线性好不少——因为振动本身就不是线性变化的。
import pandas as pd
import numpy as np
from scipy.interpolate import interp1d
# 模拟数据
time = np.array([0, 1, 2, 3, 4, 5])
value = np.array([10.2, np.nan, 10.5, np.nan, 10.8, 11.0])
# 线性插值
df = pd.DataFrame({'time': time, 'value': value})
df['linear'] = df['value'].interpolate(method='linear')
# 样条插值(需要先去掉NaN)
valid = df[df['value'].notna()]
f_spline = interp1d(valid['time'], valid['value'], kind='cubic')
df['spline'] = f_spline(df['time'])
print(df)
4.1.2 前向填充与后向填充
前向填充,就是用上一个有效值填充后面的空位。后向填充正好相反。
什么时候用?比如设备状态信号——设备一旦启动,状态就是1,直到它停下来。中间如果丢了几个采样点,用前向填充完全合理。我在一个水处理厂的项目里,液位计偶尔会跳变到0然后又恢复,这时候用前向填充就能把那些假零值盖掉。
# 前向填充
df['ffill'] = df['value'].fillna(method='ffill')
# 后向填充
df['bfill'] = df['value'].fillna(method='bfill')
4.1.3 模型预测填充
这是最复杂但也是最精准的方法。用其他相关变量来预测缺失值。比如,用电机电流和转速来预测温度——因为这三者之间有物理关系。
我常用的模型是随机森林或线性回归。注意,训练时只能用完整的数据行,预测时再把缺失的行喂进去。
from sklearn.ensemble import RandomForestRegressor
# 假设有特征A、B,目标变量C有缺失
train = df[df['C'].notna()]
test = df[df['C'].isna()]
model = RandomForestRegressor(n_estimators=100)
model.fit(train[['A', 'B']], train['C'])
df.loc[df['C'].isna(), 'C_pred'] = model.predict(test[['A', 'B']])
4.2 异常值修正:别让一个坏点带偏全局
异常值比缺失值更隐蔽。它看起来有数值,但那个数值是错的。SCADA系统里,异常值通常来自传感器故障、通讯干扰或人为误操作。
4.2.1 截断法
设定一个合理范围,超出范围的直接拉到边界。比如,某温度传感器正常范围是0-100°C,突然蹦出个500°C,直接截断到100°C。
我习惯用3σ原则或IQR(四分位距)来确定边界。3σ就是均值±3倍标准差,IQR则是Q1-1.5*IQR到Q3+1.5*IQR。
# 3σ截断
mean = df['value'].mean()
std = df['value'].std()
df['value_clipped'] = df['value'].clip(mean - 3*std, mean + 3*std)
# IQR截断
Q1 = df['value'].quantile(0.25)
Q3 = df['value'].quantile(0.75)
IQR = Q3 - Q1
df['value_clipped'] = df['value'].clip(Q1 - 1.5*IQR, Q3 + 1.5*IQR)
4.2.2 替换法
截断只是把异常值拉到边界,替换则是用更合理的值换掉它。常用的替换值有:
- 中位数:对异常值不敏感,适合有离群点的数据
- 前后均值:取异常点前后几个点的平均值
- 模型预测值:和缺失值处理一样,用模型算一个
我在一个钢铁厂的项目里,遇到过轧机电流频繁跳变。后来发现是某个传感器接触不良。我用了前后5个点的均值替换,效果很好——既去掉了毛刺,又保留了趋势。
# 用中位数替换异常值
median = df['value'].median()
df.loc[df['value'] > mean + 3*std, 'value'] = median
df.loc[df['value'] < mean - 3*std, 'value'] = median
4.2.3 标记法
有时候,异常值本身也是信息。比如设备故障前的异常波动,你把它改掉了,反而丢了故障特征。
我的做法是:加一列标记位。异常点标1,正常点标0。这样模型既能用正常数据训练,又能知道哪些点是异常的。
df['is_anomaly'] = 0
df.loc[(df['value'] > mean + 3*std) | (df['value'] < mean - 3*std), 'is_anomaly'] = 1
4.3 重复数据去重:别让相同的数据算两遍
重复数据在SCADA系统里很常见。比如,数据采集程序重启后,可能会把缓存里的数据再发一遍。或者,两个不同的采集点采集了同一个信号。
去重很简单,但要注意:
- 完全重复:所有列都一样,直接删掉
- 时间戳重复但数值不同:保留最新的,或者取平均
- 数值重复但时间戳不同:可能是设备没变化,保留即可
# 完全去重
df_clean = df.drop_duplicates()
# 按时间戳去重,保留最后一条
df_clean = df.drop_duplicates(subset=['timestamp'], keep='last')
# 按时间戳去重,取平均值
df_clean = df.groupby('timestamp').mean().reset_index()
4.4 本章知识体系
下面这张图,是我梳理的数据清洗整体流程。你把它打印出来贴在工位上,每次处理数据前看一眼,能少走很多弯路。
数据清洗这件事,说难不难,说简单也不简单。关键是要理解你的数据从哪来、到哪去。我在项目里见过太多人,上来就套模型,结果被脏数据坑得怀疑人生。记住一句话:垃圾进,垃圾出。花在清洗上的时间,永远值得。
公众号:蓝海资料掘金营,微信deep3321