第3章:数据预处理实战:异常值检测、缺失值填充与数据平滑
各位同学,欢迎来到数据预处理这一章。
说实话,很多做电池管理的同行,拿到数据就急着建模。我刚开始也是这样,结果模型跑出来一塌糊涂。后来才明白——数据预处理花的时间,往往比建模本身还多。你想想看,电池数据里什么情况都有:传感器偶尔抽风、通信丢包、噪声干扰……这些脏数据不处理,模型再牛也白搭。
3.1 异常值检测:把「坏点」揪出来
异常值,说白了就是那些明显偏离正常范围的数据点。比如一节电池电压突然跳到5V,或者SOC瞬间从50%变成100%。这些数据必须处理掉。
3.1.1 3σ法则:简单粗暴但有效
3σ法则基于正态分布假设。数据落在均值±3倍标准差之外的概率只有0.3%。我个人习惯用它做第一道筛选。
import numpy as np
import pandas as pd
def detect_outliers_3sigma(data, column, n_sigma=3):
"""
3σ法则异常值检测
data: DataFrame
column: 要检测的列名
n_sigma: 标准差倍数,默认3
"""
mean = data[column].mean()
std = data[column].std()
lower_bound = mean - n_sigma * std
upper_bound = mean + n_sigma * std
outliers = data[(data[column] < lower_bound) | (data[column] > upper_bound)]
return outliers, lower_bound, upper_bound
# 示例:检测电压异常
voltage_outliers, low, high = detect_outliers_3sigma(df, 'voltage')
print(f"电压正常范围: [{low:.3f}, {high:.3f}]")
print(f"检测到 {len(voltage_outliers)} 个异常点")
3.1.2 IQR方法:更稳健的选择
IQR(四分位距)方法不依赖正态分布假设。它用中位数和四分位数来界定范围,对极端值没那么敏感。
def detect_outliers_iqr(data, column, multiplier=1.5):
"""
IQR异常值检测
multiplier: 倍数,通常1.5为温和异常,3为极端异常
"""
Q1 = data[column].quantile(0.25)
Q3 = data[column].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - multiplier * IQR
upper_bound = Q3 + multiplier * IQR
outliers = data[(data[column] < lower_bound) | (data[column] > upper_bound)]
return outliers, lower_bound, upper_bound
# 示例:检测电流异常
current_outliers, low, high = detect_outliers_iqr(df, 'current', multiplier=1.5)
print(f"电流正常范围: [{low:.3f}, {high:.3f}]")
3.2 缺失值填充:别让数据「断片」
电池数据采集过程中,通信中断、存储溢出都可能导致数据缺失。直接删掉?太浪费。随便填?会引入偏差。
3.2.1 线性插值:最自然的填充方式
线性插值假设数据在两个已知点之间是线性变化的。对于电压、温度这种连续变化的参数,效果很好。
# 线性插值填充
df['voltage_interpolated'] = df['voltage'].interpolate(method='linear')
# 也可以指定时间索引
df['voltage_time_interp'] = df['voltage'].interpolate(method='time')
嗯,这里要注意:如果缺失值出现在数据开头或结尾,线性插值会失效。因为插值需要前后都有值。
3.2.2 前向填充:应对短暂通信中断
前向填充(forward fill)用上一个有效值填充当前缺失值。我经常用它处理BMS的CAN总线数据——通信中断时,保持上一帧数据不变,直到恢复。
# 前向填充:最多填充3个连续缺失值
df['soc_ffill'] = df['soc'].ffill(limit=3)
# 后向填充:有时也用到
df['soc_bfill'] = df['soc'].bfill(limit=3)
- 连续变化的数据(电压、温度)→ 线性插值
- 离散状态数据(SOC、充放电状态)→ 前向填充
- 缺失超过5个连续点 → 建议标记为无效,不要强行填充
3.3 数据平滑:把噪声「滤」掉
电池数据里总有噪声。传感器精度有限、电磁干扰、采样抖动……这些噪声会掩盖真实趋势。
3.3.1 移动平均:最简单的平滑方法
移动平均用窗口内数据的平均值代替当前点。窗口越大,曲线越平滑,但也会丢失细节。
# 简单移动平均,窗口大小为5
df['voltage_sma5'] = df['voltage'].rolling(window=5, center=True).mean()
# 窗口大小为20
df['voltage_sma20'] = df['voltage'].rolling(window=20, center=True).mean()
3.3.2 Savitzky-Golay滤波:保留细节的利器
Savitzky-Golay滤波(SG滤波)用多项式拟合窗口内的数据。它比移动平均更能保留数据的峰值和形状。
from scipy.signal import savgol_filter
# SG滤波:窗口大小11,多项式阶数3
df['voltage_sg'] = savgol_filter(df['voltage'], window_length=11, polyorder=3)
# 参数选择建议
# window_length: 奇数,越大越平滑
# polyorder: 一般2-4,太高会过拟合
为什么SG滤波更好?你想想看,移动平均只是简单求平均,而SG滤波是在局部用多项式拟合。对于电池的充放电曲线——有平台、有拐点——SG滤波能更好地保留这些特征。
- 窗口长度 = 采样频率 × 0.5~2秒(比如1Hz采样,窗口用5-21)
- 多项式阶数 = 窗口长度 ÷ 5(向上取整,不超过5)
- 先试polyorder=3,看效果再调整
3.4 实战流程:一条龙处理
好了,理论讲完了。我给你们一个完整的处理流程,这是我做项目时总结的。
def battery_data_preprocessing(df, config):
"""
电池数据预处理完整流程
config: 配置字典
"""
# 1. 异常值检测(先用IQR粗筛)
for col in ['voltage', 'current', 'temperature']:
outliers, low, high = detect_outliers_iqr(df, col, multiplier=3)
df.loc[outliers.index, col] = np.nan # 异常值置为NaN
# 2. 缺失值填充
# 电压、温度用线性插值
df['voltage'] = df['voltage'].interpolate(method='linear')
df['temperature'] = df['temperature'].interpolate(method='linear')
# 电流用前向填充(充放电状态变化快)
df['current'] = df['current'].ffill(limit=5)
# SOC用前向填充
df['soc'] = df['soc'].ffill(limit=3)
# 3. 数据平滑
# 电压用SG滤波保留细节
df['voltage_smooth'] = savgol_filter(df['voltage'],
window_length=11,
polyorder=3)
# 电流用移动平均去噪声
df['current_smooth'] = df['current'].rolling(window=5, center=True).mean()
# 4. 删除仍有缺失的行
df_clean = df.dropna()
return df_clean
# 使用示例
config = {
'iqr_multiplier': 3,
'ffill_limit': 5,
'sg_window': 11,
'sg_order': 3
}
df_clean = battery_data_preprocessing(df_raw, config)
- 先检测异常值,置为NaN
- 再填充缺失值(包括异常值转成的NaN)
- 最后做平滑
顺序搞反了,异常值会被平滑算法「美化」成正常数据,那就麻烦了。
3.5 本章小结
数据预处理没有银弹。不同的电池、不同的工况、不同的传感器,处理方式都不一样。我的建议是:
- 异常值检测:先用IQR粗筛,再用3σ法则精筛
- 缺失值填充:连续变量用线性插值,离散变量用前向填充
- 数据平滑:保留趋势用SG滤波,去噪声用移动平均
记住一句话:预处理做得好,模型训练没烦恼。下一章我们讲特征工程,到时候你会感谢今天认真做预处理的自己。
公众号:蓝海资料掘金营,微信deep3321