2. 环境搭建与数据准备:工欲善其事,必先利其器

各位同学,欢迎来到第二章。上一章我们聊了EKF的理论框架,说实话,光看公式确实有点枯燥。但别急,这一章我们就要动手了——把Python环境搭好,把真实数据请进来,让算法跑在实实在在的电池数据上。

我个人习惯,做任何算法开发前,先把环境收拾利索。你想想看,如果连NumPy都报错,后面还怎么玩EKF?所以这一章,咱们一步步来。

2.1 Python环境配置:别让工具拖后腿

我建议你用Anaconda来管理环境,省心。当然,如果你喜欢轻量级,直接用pip也行。这里我给出我的标准配置:

# 创建一个干净的虚拟环境
conda create -n ekf_soc python=3.9

# 激活环境
conda activate ekf_soc

# 安装核心库
pip install numpy==1.24.3 scipy==1.10.1 matplotlib==3.7.1 pandas==1.5.3

# 可选:安装jupyter lab方便交互
pip install jupyterlab

嗯,这里要注意版本兼容性。我曾经遇到过scipy 1.11版本和numpy 1.25搭配时,某些矩阵运算会报奇怪的警告。所以上面这几个版本号,是我实测过没问题的组合。

我的小技巧: 装完环境后,先跑一个简单的测试脚本,确认所有库都能正常导入。别等到写代码时才发现缺包,那感觉就像你调好算法却发现电池没电一样尴尬。

2.2 导入真实的电池充放电数据集

数据是算法的灵魂。我手头有一组来自实验室的18650锂电池测试数据,包含恒流放电、动态应力测试(DST)和城市道路循环(UDDS)工况。这里我以恒流放电数据为例,带大家看看数据长什么样。

数据文件是CSV格式,列名包括:

列名 含义 单位
time 时间戳 秒 (s)
voltage 端电压 伏特 (V)
current 电流(放电为正) 安培 (A)
temperature 电池表面温度 摄氏度 (°C)
soc 真实SOC(参考值) 百分比 (%)

加载数据的代码很简单:

import pandas as pd
import numpy as np

# 读取数据
df = pd.read_csv('battery_data_18650_25deg.csv')

# 看一眼前5行
print(df.head())

# 检查是否有缺失值
print(df.isnull().sum())

为什么我要先检查缺失值?因为在实际项目中,传感器偶尔会掉线,导致数据出现NaN。如果你不处理,EKF的协方差矩阵更新时就会崩掉。我曾经在一个项目中,就因为一个NaN值,排查了整整两天……后来养成了习惯,数据进来第一件事就是检查完整性。

2.3 数据预处理:把脏数据洗干净

真实数据往往不完美。比如电流传感器有零点漂移,电压测量有高频噪声。我们需要做几步预处理:

  1. 去噪: 用移动平均或低通滤波平滑电压和电流信号
  2. 剔除异常点: 比如电压突然跳变超过0.5V,那肯定是测量错误
  3. 时间对齐: 确保采样间隔均匀,EKF要求固定步长

看代码:

from scipy.signal import savgol_filter

# 对电压做Savitzky-Golay滤波,窗口长度11,多项式阶数3
df['voltage_filtered'] = savgol_filter(df['voltage'], window_length=11, polyorder=3)

# 剔除电流异常点(电流绝对值超过5A的认为是异常)
df = df[np.abs(df['current']) < 5.0]

# 重采样到固定间隔1秒
df = df.set_index('time').resample('1S').mean().interpolate().reset_index()
注意: 滤波窗口长度不要选太大,否则会抹掉电池电压的快速变化特征。我一般用11~15个点,具体看采样频率。采样率1Hz时,窗口11对应约11秒,够用了。

2.4 数据可视化:一图胜千言

预处理完,一定要画图看看。我习惯把电压、电流、SOC画在同一张图上,这样能直观感受数据质量。

import matplotlib.pyplot as plt

fig, axes = plt.subplots(3, 1, figsize=(12, 8), sharex=True)

# 电压
axes[0].plot(df['time'], df['voltage'], label='原始电压', alpha=0.5)
axes[0].plot(df['time'], df['voltage_filtered'], label='滤波后电压', linewidth=2)
axes[0].set_ylabel('电压 (V)')
axes[0].legend()
axes[0].grid(True)

# 电流
axes[1].plot(df['time'], df['current'], label='电流', color='orange')
axes[1].set_ylabel('电流 (A)')
axes[1].legend()
axes[1].grid(True)

# SOC
axes[2].plot(df['time'], df['soc'], label='真实SOC', color='green')
axes[2].set_xlabel('时间 (s)')
axes[2].set_ylabel('SOC (%)')
axes[2].legend()
axes[2].grid(True)

plt.tight_layout()
plt.show()

画完图,你会看到:电压随着放电缓慢下降,电流基本恒定在1A左右,SOC从100%线性下降到0%。嗯,这就是一个标准的恒流放电过程。如果你看到电压曲线有毛刺或者跳变,那就要回头检查数据预处理那一步了。

核心要点: 数据质量决定了EKF的估计精度。花80%的时间在数据准备上,剩下20%的时间调算法,这是最划算的投入。我见过太多人一上来就调卡尔曼增益,结果数据本身就有问题,白费功夫。

2.5 保存处理后的数据

预处理完的数据,建议保存成新文件,方便后续章节直接使用:

# 只保留需要的列
df_clean = df[['time', 'voltage_filtered', 'current', 'temperature', 'soc']]
df_clean.columns = ['time', 'voltage', 'current', 'temperature', 'soc']

# 保存为numpy格式,加载更快
np.savez('battery_data_clean.npz', 
         time=df_clean['time'].values,
         voltage=df_clean['voltage'].values,
         current=df_clean['current'].values,
         temperature=df_clean['temperature'].values,
         soc=df_clean['soc'].values)

print(f"数据保存完成,共 {len(df_clean)} 个采样点")

好了,到这一步,我们的数据已经整整齐齐、干干净净。下一章,我们就要开始搭建电池模型,然后让EKF真正跑起来。说实话,每次看到数据曲线在屏幕上展开,我都觉得特别踏实——因为我知道,接下来的算法工作,有了坚实的基础。

各位同学,动手试试吧。把上面的代码跑一遍,看看你的数据长什么样。有任何问题,欢迎在课程群里交流。