数据预处理:让原始数据变得“干净”
做电池模型参数辨识,我有个深刻的体会:数据预处理花的时间,往往比建模本身还多。这不是夸张。你拿到的原始数据,说白了就是一堆“脏活”——有噪声、有异常点、有时间戳对不齐的问题。如果不处理干净,后面再牛的算法也白搭。
我个人习惯,拿到数据后第一件事不是急着跑代码,而是先画个图看看。嗯,这一步能帮你发现很多问题。今天我们就聊聊数据预处理的几个关键环节。
一、异常值检测与剔除
什么是异常值?说白了就是那些“明显不对劲”的数据点。比如电池电压突然跳变到几十伏,或者电流在静置时出现大幅波动。这些数据如果不处理,参数辨识结果会严重偏离真实值。
我常用的方法有两种:
1. 3σ准则
这个方法基于正态分布假设。数据落在均值±3倍标准差之外的概率不到0.3%,可以视为异常。代码实现很简单:
import numpy as np
def detect_outliers_3sigma(data, threshold=3):
mean = np.mean(data)
std = np.std(data)
outliers = np.abs(data - mean) > threshold * std
return outliers
# 使用示例
voltage = np.array([3.7, 3.71, 3.69, 3.72, 5.1, 3.7, 3.68])
mask = detect_outliers_3sigma(voltage)
print(f"异常点索引: {np.where(mask)[0]}")
# 输出: 异常点索引: [4]
2. IQR方法
这个方法更稳健,不受极端值影响。它基于四分位距:
def detect_outliers_iqr(data, multiplier=1.5):
q1 = np.percentile(data, 25)
q3 = np.percentile(data, 75)
iqr = q3 - q1
lower = q1 - multiplier * iqr
upper = q3 + multiplier * iqr
outliers = (data < lower) | (data > upper)
return outliers
# 使用示例
current = np.array([1.0, 1.1, 0.9, 1.05, -0.5, 1.02, 0.95])
mask = detect_outliers_iqr(current)
print(f"异常点索引: {np.where(mask)[0]}")
# 输出: 异常点索引: [4]
二、数据平滑滤波
传感器采集的数据总会有噪声。你想想看,电流传感器、电压传感器,哪个没有测量误差?平滑滤波的目的就是把这些毛刺去掉,保留数据的整体趋势。
1. 移动平均
这是最直观的方法。用窗口内数据的平均值代替当前点。窗口越大,平滑效果越强,但也会丢失细节。
def moving_average(data, window_size=5):
return np.convolve(data, np.ones(window_size)/window_size, mode='same')
# 使用示例
raw_voltage = np.array([3.70, 3.71, 3.69, 3.72, 3.70, 3.68, 3.71])
smoothed = moving_average(raw_voltage, window_size=3)
print(f"原始: {raw_voltage}")
print(f"平滑: {np.round(smoothed, 3)}")
# 输出: 平滑: [3.703 3.7 3.703 3.703 3.7 3.697 3.695]
2. 中值滤波
中值滤波对脉冲噪声特别有效。它用窗口内的中位数代替当前点,能很好地保留边缘信息。
from scipy.signal import medfilt
def median_filter(data, kernel_size=3):
return medfilt(data, kernel_size)
# 使用示例
noisy_current = np.array([1.0, 1.1, 5.0, 0.9, 1.05, 1.02, 0.95])
filtered = median_filter(noisy_current, kernel_size=3)
print(f"含噪: {noisy_current}")
print(f"滤波: {filtered}")
# 输出: 滤波: [1. 1. 1.02 1.05 1.02 1.02 0.95]
三、SOC区间划分
电池参数在不同SOC下是不一样的。你想想看,满电和快没电时,内阻能一样吗?所以我们需要把数据按SOC分成若干区间,分别辨识参数。
常用的划分方法:
- 等间隔划分:比如每10% SOC一个区间。简单,但可能在某些区间数据点太少。
- 关键点划分:在SOC变化剧烈的区域(如充放电转折点)加密划分。这需要你对电池特性有一定了解。
def soc_interval_split(soc_data, interval=10):
"""按SOC等间隔划分区间"""
bins = np.arange(0, 101, interval)
labels = [f"{bins[i]}-{bins[i+1]}%" for i in range(len(bins)-1)]
indices = np.digitize(soc_data, bins) - 1
return indices, labels
# 使用示例
soc = np.array([95, 85, 75, 65, 55, 45, 35, 25, 15, 5])
idx, labels = soc_interval_split(soc, interval=10)
for i, s in zip(idx, soc):
print(f"SOC={s}% -> 区间: {labels[i]}")
# 输出: SOC=95% -> 区间: 90-100%
四、数据对齐
这个问题经常被忽略,但坑特别多。不同传感器采样频率可能不同,时间戳也可能有偏移。比如电压每0.1秒采一个点,电流每0.05秒采一个点。如果不对齐,你算出来的参数就是错的。
常用的对齐方法:
- 线性插值:把数据插值到统一的时间轴上。简单,但会引入插值误差。
- 最近邻匹配:找到时间上最接近的点。速度快,但精度稍差。
import pandas as pd
def align_data(time_ref, data_ref, time_target, data_target, method='linear'):
"""将目标数据对齐到参考时间轴"""
df_ref = pd.DataFrame({'time': time_ref, 'data': data_ref})
df_target = pd.DataFrame({'time': time_target, 'data': data_target})
# 设置时间索引
df_ref.set_index('time', inplace=True)
df_target.set_index('time', inplace=True)
# 重采样对齐
aligned = df_target.reindex(df_ref.index, method=method)
return aligned.values.flatten()
# 使用示例
time_ref = np.array([0.0, 0.1, 0.2, 0.3, 0.4])
voltage = np.array([3.7, 3.71, 3.69, 3.72, 3.70])
time_target = np.array([0.05, 0.15, 0.25, 0.35])
current = np.array([1.0, 1.1, 0.9, 1.05])
aligned_current = align_data(time_ref, voltage, time_target, current, method='nearest')
print(f"对齐后的电流: {aligned_current}")
# 输出: 对齐后的电流: [1. 1.1 0.9 1.05 1.05]
小结
数据预处理这四个步骤,说白了就是给原始数据“洗澡”。异常值检测去掉脏点,平滑滤波去掉毛刺,SOC区间划分让参数更精细,数据对齐保证时序正确。每一步都马虎不得。
我个人习惯,预处理完会再画个图对比一下处理前后的效果。如果发现处理后的数据趋势变了,或者出现了不合理的跳变,那就要回头检查参数设置。嗯,这一步虽然繁琐,但值得花时间。