第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σ法则对极端值敏感。如果数据本身包含大量异常,均值和标准差会被拉偏。我曾经在一个老化数据集上吃过亏——电池容量衰减到80%以下,3σ法则把正常的老化数据也判成了异常。所以,先做一次粗略清洗,再用3σ法则

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σ法则就够了。但对于电流这种变化剧烈的参数,IQR方法更靠谱。我在做BMS标定时,经常用IQR来检测电流传感器的零点漂移。

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()
⚠️ 避坑指南:我曾经用移动平均处理电池的OCV数据,窗口设得太大,结果把电压平台期的微小变化全抹平了。后来做SOC标定时才发现误差很大。窗口大小一般设为采样频率的1/10到1/5,比如1Hz采样的数据,窗口用5-10。

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)
处理顺序很重要:
  1. 先检测异常值,置为NaN
  2. 再填充缺失值(包括异常值转成的NaN)
  3. 最后做平滑

顺序搞反了,异常值会被平滑算法「美化」成正常数据,那就麻烦了。

3.5 本章小结

数据预处理没有银弹。不同的电池、不同的工况、不同的传感器,处理方式都不一样。我的建议是:

  • 异常值检测:先用IQR粗筛,再用3σ法则精筛
  • 缺失值填充:连续变量用线性插值,离散变量用前向填充
  • 数据平滑:保留趋势用SG滤波,去噪声用移动平均

记住一句话:预处理做得好,模型训练没烦恼。下一章我们讲特征工程,到时候你会感谢今天认真做预处理的自己。


公众号:蓝海资料掘金营,微信deep3321