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 数据预处理:把脏数据洗干净
真实数据往往不完美。比如电流传感器有零点漂移,电压测量有高频噪声。我们需要做几步预处理:
- 去噪: 用移动平均或低通滤波平滑电压和电流信号
- 剔除异常点: 比如电压突然跳变超过0.5V,那肯定是测量错误
- 时间对齐: 确保采样间隔均匀,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()
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%。嗯,这就是一个标准的恒流放电过程。如果你看到电压曲线有毛刺或者跳变,那就要回头检查数据预处理那一步了。
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真正跑起来。说实话,每次看到数据曲线在屏幕上展开,我都觉得特别踏实——因为我知道,接下来的算法工作,有了坚实的基础。
各位同学,动手试试吧。把上面的代码跑一遍,看看你的数据长什么样。有任何问题,欢迎在课程群里交流。