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方法(四分位距法)

这个方法不依赖正态分布假设,更鲁棒。它基于数据的四分位数。

计算步骤:

  1. 计算第一四分位数 Q1(25%分位点)和第三四分位数 Q3(75%分位点)
  2. 计算 IQR = Q3 - Q1
  3. 定义下界 = Q1 - 1.5 * IQR,上界 = Q3 + 1.5 * IQR
  4. 超出上下界的数据即为异常值

我曾经在一个项目中,用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 预处理流程总结

好了,我们把今天的流程串起来:

  1. 原始数据 → 电压、电流、温度等
  2. 异常值检测 → 3σ准则或IQR方法,建议分段处理
  3. 异常值剔除 → 标记为NaN或直接删除
  4. 数据插值 → 三次样条插值填补空缺
  5. 重采样 → 统一到相同频率,建议降采样
  6. 曲线平滑 → Savitzky-Golay或LOWESS
  7. 输出 → 干净、规整、平滑的数据

这个流程看起来步骤多,但每一步都有它的道理。你想想看,如果跳过异常值检测,模型可能学到错误模式。如果跳过平滑,SOC估算会来回跳。

下一章,我们会用处理好的数据来提取健康特征。到时候你就知道,今天花时间做预处理,绝对值。

最后提醒一句:

预处理不是一次性工作。每次拿到新数据,都要重新审视参数是否合适。我曾经因为偷懒,直接套用上一批数据的参数,结果模型精度掉了5%。从那以后,我再也不敢「一键预处理」了。

好,今天就到这里。动手试试吧,把代码跑起来,看看你的数据里藏着多少「惊喜」。