4、电池数据预处理实战:异常值检测与剔除、数据插值与重采样、SOC-OCV曲线平滑处理
各位同学,欢迎来到第四讲。
上一章我们聊了数据采集的那些坑。今天,咱们进入真正的「预处理」环节。
你想想看,从实验室或者车上拿到的原始数据,能直接用吗?
我告诉你,基本不能。传感器偶尔抽风、通信丢包、噪声干扰……这些都会让数据变得「脏兮兮」的。我见过最夸张的一次,一个电压传感器在某个瞬间跳到了100V,而实际电池才3.7V。这种数据要是直接喂给算法,模型直接崩给你看。
所以,预处理不是可选项,是必选项。今天我们就来搞定三件事:异常值检测、数据插值与重采样、SOC-OCV曲线平滑。
4.1 异常值检测与剔除
异常值,说白了就是那些「明显不对劲」的数据点。比如电压突然跳变、电流瞬间归零又恢复。这些点必须处理掉。
常用的方法有两种:3σ准则和IQR方法。
4.1.1 3σ准则(拉依达准则)
这个方法的前提是数据服从正态分布。嗯,严格来说,电池数据并不完全服从正态分布,但在局部窗口内,比如连续100个采样点,近似正态是没问题的。
公式很简单:
- 计算均值 μ 和标准差 σ
- 如果某个数据点 x 满足 |x - μ| > 3σ,就判定为异常值
我个人习惯用滑动窗口的方式来做。比如窗口大小设为50,每次滑动1个点,计算当前窗口的μ和σ,然后判断窗口中心点是否异常。
核心要点:
3σ准则对极端值敏感。如果数据本身包含多个异常值,均值和标准差会被「拉偏」,导致漏检。所以,我建议先做一次粗筛,再用3σ做精筛。
import numpy as np
import pandas as pd
def detect_outliers_3sigma(data, window=50, threshold=3):
"""
滑动窗口3σ异常检测
data: 一维数组
window: 窗口大小
threshold: 阈值,默认3
"""
outliers = np.zeros(len(data), dtype=bool)
for i in range(window, len(data) - window):
segment = data[i - window:i + window]
mean = np.mean(segment)
std = np.std(segment)
if abs(data[i] - mean) > threshold * std:
outliers[i] = True
return outliers
# 使用示例
voltage = pd.read_csv('battery_data.csv')['voltage'].values
outlier_mask = detect_outliers_3sigma(voltage)
cleaned_voltage = voltage[~outlier_mask]
我的经验:
阈值不一定死磕3。对于噪声较大的数据,我有时会放宽到3.5。对于高精度实验室数据,可以收紧到2.5。灵活调整,别死板。
4.1.2 IQR方法(四分位距法)
这个方法不依赖正态分布假设,更鲁棒。它基于数据的四分位数。
计算步骤:
- 计算第一四分位数 Q1(25%分位点)和第三四分位数 Q3(75%分位点)
- 计算 IQR = Q3 - Q1
- 定义下界 = Q1 - 1.5 * IQR,上界 = Q3 + 1.5 * IQR
- 超出上下界的数据即为异常值
我曾经在一个项目中,用3σ准则死活检测不出某些异常点,换成IQR方法一下就揪出来了。为什么?因为那些异常值虽然偏离了局部均值,但幅度不够3σ,而IQR对尾部数据更敏感。
def detect_outliers_iqr(data):
"""
IQR异常检测
"""
Q1 = np.percentile(data, 25)
Q3 = np.percentile(data, 75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = (data < lower_bound) | (data > upper_bound)
return outliers
# 使用示例
outlier_mask_iqr = detect_outliers_iqr(voltage)
cleaned_voltage_iqr = voltage[~outlier_mask_iqr]
避坑指南:
我曾经犯过一个错误——对整个数据集一次性做IQR检测。结果发现,电池在低SOC区域电压本来就低,被误判为异常值。正确的做法是:按SOC区间分段检测。比如每5% SOC一个区间,分别计算Q1、Q3和IQR。
4.2 数据插值与重采样
剔除异常值后,数据会出现空缺。另外,不同来源的数据采样频率可能不一致。这时候就需要插值和重采样。
4.2.1 插值方法
常用的插值方法有:
| 方法 | 适用场景 | 优缺点 |
|---|---|---|
| 线性插值 | 数据变化平缓 | 简单快速,但不够平滑 |
| 三次样条插值 | 数据有曲率变化 | 平滑性好,但可能过冲 |
| 前向填充 | 短时间缺失 | 保持最后值,简单粗暴 |
| 多项式插值 | 数据点较少 | 高阶易震荡,慎用 |
我个人最常用的是三次样条插值。为什么?因为电池的电压-容量曲线本身是光滑的,线性插值会留下「折角」,影响后续微分分析。
from scipy import interpolate
import numpy as np
# 假设有缺失数据
time = np.array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
voltage = np.array([3.0, 3.1, np.nan, 3.3, np.nan, 3.5, 3.6, np.nan, 3.8, 3.9, 4.0])
# 去除NaN点
mask = ~np.isnan(voltage)
time_clean = time[mask]
voltage_clean = voltage[mask]
# 三次样条插值
f = interpolate.CubicSpline(time_clean, voltage_clean)
voltage_interp = f(time)
print("插值结果:", voltage_interp)
我的建议:
如果缺失数据连续超过5个点,插值结果可能不可靠。这时候我宁愿放弃这段数据,也不要强行插值。记住:垃圾进,垃圾出。
4.2.2 重采样
重采样就是把不同频率的数据统一到同一个时间基准上。比如电流是1Hz采集的,电压是10Hz采集的,需要统一到1Hz或10Hz。
常用的重采样策略:
- 降采样:高频转低频,用平均值或中位数聚合
- 升采样:低频转高频,用插值填充
我一般倾向于降采样到最低频率。为什么?因为升采样不会增加信息量,只是「看起来」数据多了,实际信息密度没变。
import pandas as pd
# 假设有1Hz的电流数据和10Hz的电压数据
current_1hz = pd.DataFrame({
'time': pd.date_range('2024-01-01', periods=10, freq='1S'),
'current': np.random.randn(10)
})
voltage_10hz = pd.DataFrame({
'time': pd.date_range('2024-01-01', periods=100, freq='0.1S'),
'voltage': 3.7 + 0.1 * np.random.randn(100)
})
# 设置时间索引
current_1hz.set_index('time', inplace=True)
voltage_10hz.set_index('time', inplace=True)
# 降采样电压到1Hz,取平均值
voltage_1hz = voltage_10hz.resample('1S').mean()
# 合并数据
merged_data = pd.merge(current_1hz, voltage_1hz, left_index=True, right_index=True)
print(merged_data.head())
4.3 SOC-OCV曲线平滑处理
SOC-OCV曲线是BMS的核心参考曲线。但实测数据往往有噪声,直接使用会导致SOC估算偏差。
平滑处理的目标是:保留曲线的整体趋势,去除局部抖动。
4.3.1 移动平均法
最简单的方法。窗口大小是关键参数。
- 窗口太小:平滑效果差
- 窗口太大:会抹掉真实拐点
我一般用窗口大小为5-11个点,具体看采样密度。
def smooth_moving_average(data, window=7):
"""
移动平均平滑
"""
return np.convolve(data, np.ones(window)/window, mode='same')
# 使用示例
soc = np.linspace(0, 100, 1000)
ocv_raw = 3.0 + 0.01 * soc + 0.1 * np.random.randn(1000) # 模拟带噪声的OCV
ocv_smooth = smooth_moving_average(ocv_raw, window=11)
4.3.2 Savitzky-Golay滤波
这个方法比移动平均更高级。它用局部多项式拟合来平滑数据,能更好地保留曲线的形状特征,比如峰值和谷值。
from scipy.signal import savgol_filter
# Savitzky-Golay滤波
# window_length: 窗口长度(必须为奇数)
# polyorder: 多项式阶数
ocv_sg = savgol_filter(ocv_raw, window_length=11, polyorder=3)
参数选择经验:
对于SOC-OCV曲线,我推荐:
- window_length = 9 或 11
- polyorder = 2 或 3
polyorder太高会导致过拟合,反而放大噪声。记住:低阶多项式更稳健。
4.3.3 局部加权回归(LOWESS)
这个方法对非线性数据特别友好。它给每个点附近的点赋予权重,距离越近权重越大。
import statsmodels.api as sm
# LOWESS平滑
lowess = sm.nonparametric.lowess
ocv_lowess = lowess(ocv_raw, soc, frac=0.1) # frac控制平滑程度
ocv_smoothed = ocv_lowess[:, 1]
我的偏好:
三种方法我都用过。如果数据噪声不大,移动平均就够了。如果噪声中等,Savitzky-Golay是首选。如果曲线形状复杂、噪声也大,LOWESS效果最好,但计算量也最大。
我曾经在一个项目中,用Savitzky-Golay处理SOC-OCV曲线,结果在SOC 20%附近出现了一个假拐点。换成LOWESS后,问题解决了。所以,没有万能方法,多试试。
4.4 预处理流程总结
好了,我们把今天的流程串起来:
- 原始数据 → 电压、电流、温度等
- 异常值检测 → 3σ准则或IQR方法,建议分段处理
- 异常值剔除 → 标记为NaN或直接删除
- 数据插值 → 三次样条插值填补空缺
- 重采样 → 统一到相同频率,建议降采样
- 曲线平滑 → Savitzky-Golay或LOWESS
- 输出 → 干净、规整、平滑的数据
这个流程看起来步骤多,但每一步都有它的道理。你想想看,如果跳过异常值检测,模型可能学到错误模式。如果跳过平滑,SOC估算会来回跳。
下一章,我们会用处理好的数据来提取健康特征。到时候你就知道,今天花时间做预处理,绝对值。
最后提醒一句:
预处理不是一次性工作。每次拿到新数据,都要重新审视参数是否合适。我曾经因为偷懒,直接套用上一批数据的参数,结果模型精度掉了5%。从那以后,我再也不敢「一键预处理」了。
好,今天就到这里。动手试试吧,把代码跑起来,看看你的数据里藏着多少「惊喜」。