4. 特征工程:电压、电流、温度特征提取、时间序列特征构造、特征选择方法
各位同学,欢迎来到第四讲。
上一章我们聊了数据预处理,把原始数据洗得干干净净。但光洗干净还不够,你得让数据“说话”。
说白了,原始电压、电流、温度只是一堆数字。SOC预测需要的是这些数字背后的规律。怎么把规律挖出来?这就是特征工程要干的事。
我个人习惯把特征工程看作“厨师备菜”。菜洗好了(预处理),现在要切丁、切片、腌制(特征提取),最后还要挑出最入味的几样(特征选择)。
4.1 电压特征提取:不只是看瞬时值
电压是SOC预测最直接的信号。但直接用原始电压,效果往往不好。
为什么?因为电池电压有“回弹效应”。放电时电压骤降,静置后又回升。你直接拿当前电压去预测SOC,误差会很大。
我常用的电压特征:
- 电压差(ΔV):当前电压与上一时刻电压的差值。反映电压变化速率。
- 电压变化率(dV/dt):单位时间内的电压变化。放电末期这个值会急剧增大,是SOC接近0%的强烈信号。
- 电压滑动窗口统计:过去N个点的均值、标准差、最大值、最小值。比如过去10秒的平均电压,比瞬时值稳定得多。
- 电压与开路电压的差值:如果有OCV曲线,计算当前电压与对应SOC的OCV差值。这个差值反映了极化内阻的影响。
重要提示:电压特征对温度极其敏感。同一SOC下,0℃和40℃的电压能差0.3V以上。所以电压特征一定要和温度特征搭配使用。
4.2 电流特征提取:积分不是万能的
电流特征,大家第一反应就是“安时积分”。没错,安时积分是SOC估算的基石。但光靠积分,误差会累积。
我在项目中遇到过一个问题:一个客户用纯安时积分,跑了100个循环后,SOC误差漂到了15%。为什么?因为电流传感器有零点漂移,每次积分都多算一点点。
所以电流特征要这样提取:
- 累积容量(Ah):对电流时间积分。这是基础特征。
- 平均电流:过去一段时间内的平均放电/充电电流。反映负载强度。
- 电流变化率(dI/dt):电流突变的剧烈程度。大电流脉冲会导致极化电压突变,影响SOC估算。
- 电流方向标志:充电为+1,放电为-1,静置为0。这个离散特征有时比连续值更有用。
- 电流平方(I²):与焦耳热成正比。间接反映电池内部温升。
我的小技巧:电流特征最好做“分区间统计”。比如分别统计充电段、放电段、静置段的平均电流和累积容量。这样模型能学到不同工况下的特性。
4.3 温度特征提取:别只看表面温度
温度影响化学反应速率,直接影响SOC估算精度。但很多人只用一个表面温度传感器,这不够。
嗯,这里要注意:电池内部温度和表面温度可以差5-10℃,特别是在大倍率充放电时。
温度特征我这样处理:
- 表面温度(T_surf):直接测量值。
- 温度变化率(dT/dt):温升速率。异常温升往往预示着内阻增大或热失控前兆。
- 温升(ΔT):当前温度与初始温度的差值。反映累积发热量。
- 温度与电流的交互特征:比如 T × I,T × I²。温度越高、电流越大,极化效应越明显。
我曾经踩过的坑:有一次模型在夏天表现很好,冬天却完全失效。后来发现是温度特征没做归一化。夏天30℃和冬天-10℃的数据混在一起训练,模型学偏了。记住:温度特征一定要做标准化,或者至少缩放到[0,1]区间。
4.4 时间序列特征构造:让模型记住历史
SOC预测本质上是一个时间序列问题。当前SOC不仅取决于当前测量值,还取决于过去一段时间的状态。
说白了,电池有“记忆效应”。你刚经历了一个大电流放电,和刚经历了一个小电流放电,即使当前电压相同,SOC也可能不同。
时间序列特征构造方法:
- 滑动窗口特征:取过去N个时间步的电压、电流、温度,计算统计量(均值、方差、偏度、峰度)。窗口大小一般取10-30个点(对应1-3秒)。
- 差分特征:一阶差分(当前值减上一时刻值)、二阶差分。捕捉变化趋势。
- 滞后特征(Lag Features):直接把过去几个时刻的值作为新特征。比如 t-1, t-2, t-3 时刻的电压。
- 累积特征:从放电开始到现在的累积容量、累积能量。反映放电深度。
下面是我常用的一个时间序列特征构造代码示例:
import pandas as pd
import numpy as np
def create_ts_features(df, window=10):
"""
构造时间序列特征
df: 包含 voltage, current, temperature 的DataFrame
window: 滑动窗口大小
"""
df_feat = df.copy()
# 滑动窗口统计
for col in ['voltage', 'current', 'temperature']:
df_feat[f'{col}_mean_{window}'] = df[col].rolling(window).mean()
df_feat[f'{col}_std_{window}'] = df[col].rolling(window).std()
df_feat[f'{col}_max_{window}'] = df[col].rolling(window).max()
df_feat[f'{col}_min_{window}'] = df[col].rolling(window).min()
# 差分特征
df_feat['voltage_diff'] = df['voltage'].diff()
df_feat['current_diff'] = df['current'].diff()
# 滞后特征(取过去3个点)
for lag in [1, 2, 3]:
df_feat[f'voltage_lag_{lag}'] = df['voltage'].shift(lag)
df_feat[f'current_lag_{lag}'] = df['current'].shift(lag)
# 累积容量(安时积分)
dt = 0.1 # 假设采样间隔0.1秒
df_feat['cum_ah'] = (df['current'] * dt).cumsum()
return df_feat.dropna()
注意:构造滞后特征和滑动窗口特征时,前N行会变成NaN。记得用 dropna() 或填充策略处理。我一般直接丢掉前N行,因为数据量通常足够大。
4.5 特征选择方法:别让模型“消化不良”
特征构造完了,你可能得到了几十甚至上百个特征。但特征不是越多越好。
你想想看,如果加入一个完全无关的特征(比如当前时间戳的秒数),模型不仅学不到东西,还可能过拟合。特征选择就是帮你“去芜存菁”。
我常用的三种特征选择方法:
| 方法 | 原理 | 适用场景 | 我的经验 |
|---|---|---|---|
| 过滤法(Filter) | 计算每个特征与目标变量的相关性(皮尔逊系数、互信息) | 初步筛选,快速剔除无关特征 | 先跑一遍,把相关系数低于0.1的特征直接扔掉 |
| 包裹法(Wrapper) | 用模型反复训练,评估特征子集的效果(如递归特征消除RFE) | 特征数量较少(<50)时效果好 | 计算量大,但结果可靠。我一般用随机森林做RFE |
| 嵌入法(Embedded) | 在模型训练过程中自动进行特征选择(如Lasso回归、树模型特征重要性) | 特征数量多,且模型本身支持 | 最推荐。LightGBM或XGBoost训练完直接看feature_importance |
具体操作步骤:
- 第一步:过滤法快速筛选。计算每个特征与SOC的皮尔逊相关系数,保留绝对值大于0.2的特征。
- 第二步:嵌入法精挑细选。用LightGBM训练一次,查看特征重要性,保留累计重要性达到95%的前K个特征。
- 第三步:人工复核。看看选出来的特征是否合理。比如“当前秒数”这种特征重要性很高,那大概率是数据泄露,要剔除。
核心原则:特征选择不是一锤子买卖。我建议每次模型迭代后都重新做一次特征选择。因为随着数据量增加,特征的重要性排序可能会变化。
4.6 本章知识体系总览
下面这张图总结了本章的核心逻辑。从原始数据到最终特征集,一共四步:
好了,特征工程这部分就讲到这里。记住一句话:好的特征让模型事半功倍,坏的特征让模型事倍功半。下一章我们会把这些特征喂给模型,看看实际效果如何。
公众号:蓝海资料掘金营,微信deep3321