1. 单体压差数据采集与基线建立

为什么先做“基线”,而不是先看告警

单体压差排障最容易犯的错误,是拿一个瞬时最大值直接判断“电池有问题”。压差本身随 SOC、温度、电流倍率、静置时长变化很大:同一组电池在 50% SOC 静置时可能只有 8 mV,在大倍率放电末端却可能到 60 mV。因此第一步不是找故障,而是用历史数据建立正常工况下的压差分布,后续任何异常值都要和对应工况的基线比,而不是和某个固定数字比。

本例的任务是:从 BMS 历史数据中提取各单体电压,逐帧计算最大压差,按工况分箱统计,得到可复用的基线表。核心指标定义如下(全文统一):

  • 单体压差 ΔV = 同一时刻 max(V_cell) − min(V_cell),单位 mV;
  • 工况分箱键 = SOC 区间 + 温度区间 + 电流状态(静置/充电/放电);
  • 基线取分箱内的 P50(典型值)与 P95(正常上限参考),样本数不足的分箱标记为“待积累”。

数据形态与提取要点

典型 BMS 历史数据是“一帧一行、单体电压多列”的宽表,或“一帧多行、每行一个单体”的长表。宽表更适合逐帧求极差,长表更适合做单体级趋势。本例按宽表处理,字段约定为:ts(时间戳)、soc(%)、temp_max(℃)、current(A,充电为正、放电为负)、v01…v96(单体电压,V)。

提取前必须做三件事:剔除单体电压为 0 或明显超量程的无效帧;确认单体编号与列名一一对应(换过 BMS 从板的船尤其容易错位);统一单位,BMS 常以 mV 存储,而分析脚本按 V 计算,混用会得到 1000 倍的假压差。

计算与分箱的实现

下面是一段可直接运行的 Python 示例,输入为上述宽表 CSV,输出分箱基线表。示例数据为教学构造,运行结果属于示例输出,不代表任何实船实测值。

import pandas as pd
import numpy as np

CELL_COLS = [f"v{i:02d}" for i in range(1, 97)]

def load_frames(path):
    df = pd.read_csv(path, parse_dates=["ts"])
    v = df[CELL_COLS]
    # 有效性过滤:单体电压应在 2.0~4.5 V 之间
    valid = (v > 2.0).all(axis=1) & (v < 4.5).all(axis=1)
    df = df[valid].copy()
    df["dv_mv"] = (df[CELL_COLS].max(axis=1) - df[CELL_COLS].min(axis=1)) * 1000
    return df

def bin_key(df):
    soc_bin = pd.cut(df["soc"], bins=[0, 20, 40, 60, 80, 100],
                     labels=["0-20", "20-40", "40-60", "60-80", "80-100"])
    temp_bin = pd.cut(df["temp_max"], bins=[-20, 0, 15, 30, 45, 60],
                      labels=["<0", "0-15", "15-30", "30-45", ">45"])
    mode = np.where(df["current"].abs() < 2, "静置",
             np.where(df["current"] >= 2, "充电", "放电"))
    return pd.DataFrame({"soc_bin": soc_bin, "temp_bin": temp_bin, "mode": mode})

def build_baseline(df, min_n=200):
    key = bin_key(df)
    g = df.groupby([key["soc_bin"], key["temp_bin"], key["mode"]])["dv_mv"]
    base = g.agg(n="count", p50="median",
                 p95=lambda s: s.quantile(0.95)).reset_index()
    base["status"] = np.where(base["n"] >= min_n, "可用", "待积累")
    return base.sort_values(["mode", "soc_bin", "temp_bin"])

df = load_frames("bms_history.csv")
baseline = build_baseline(df)
print(baseline.head(10).to_string(index=False))
baseline.to_csv("dv_baseline.csv", index=False)

几个参数需要按船型调整:min_n=200 是分箱最少帧数,样本太少时 P95 不稳定;静置判定阈值 |I| < 2 A 要结合 CT 精度和实际待机电流设定;温度用 temp_max 而非平均温度,是因为高温单体更容易触发压差扩大,用最大值更保守。

基线结果怎么读

基线表不是一张“合格证”,而是一张对照尺。判读时抓住三点:同一工况下 P50 是否随 SOC 单调上升(放电末端压差扩大属正常);P95 与 P50 的差距是否过大(差距大说明该工况下压差离散,基线本身不稳);“待积累”分箱占比是否过高(说明历史数据覆盖不足,后续排障结论要降级)。

BMS 历史数据 ts / soc / temp_max current / v01…v96 有效性过滤 剔除 0 V / 超量程帧 校验单体列与编号对应 逐帧计算 ΔV max(V) − min(V) 统一换算为 mV 工况分箱 SOC × 温度 × 电流态 P50 / P95 / n 分箱矩阵(行:SOC 区间,列:温度区间,每格再分静置/充电/放电) SOC \ 温度 <0 ℃ 0–15 ℃ 15–30 ℃ 30–45 ℃ >45 ℃ 0–20 % 待积累 P50 6 / P95 12 P50 5 / P95 10 P50 7 / P95 14 待积累 20–40 % P50 8 / P95 15 P50 7 / P95 13 P50 6 / P95 12 P50 9 / P95 17 待积累 40–60 % P50 9 / P95 16 P50 8 / P95 14 P50 7 / P95 13 P50 10 / P95 19 待积累 60–80 % P50 11 / P95 20 P50 10 / P95 18 P50 9 / P95 16 P50 13 / P95 24 待积累 80–100 % 待积累 P50 14 / P95 26 P50 13 / P95 24 P50 16 / P95 30 待积累 样本充足,基线可用(n ≥ 200) 样本不足,待积累 注:表中 P50/P95 数值为教学示例,单位 mV;实际基线须用本船历史数据重新计算。

交付物:压差基线表

把上面的输出整理成可归档、可对照的基线表,字段固定,后续排障直接查表。示例行如下(数值为教学示例):

工况键SOC 区间温度区间电流状态样本数 nP50 (mV)P95 (mV)状态
B-0340–60%15–30 ℃静置1280713可用
B-1180–100%30–45 ℃放电6401630可用
B-070–20%<0 ℃充电85——待积累

归档时同时保存三样东西:基线表 CSV、生成脚本、以及数据覆盖说明(时间范围、航次类型、BMS 版本)。没有覆盖说明的基线,半年后没人敢用。

验证基线是否可信

基线建好后做一次自检,通过再进入排障环节:

  1. 单调性检查:同一温度、同一电流状态下,P50 应随 SOC 升高而上升(放电末端压差扩大)。若出现明显倒挂,多半是分箱边界或数据混入了不同电池组。
  2. 离散度检查:P95/P50 比值普遍超过 3,说明该工况下压差波动大,基线只能作粗参考,排障时需结合单体级趋势。
  3. 覆盖率检查:“待积累”分箱占比超过 30%,说明历史数据不足以支撑全工况判断,应补充数据或缩小分箱粒度。
  4. 回放验证:随机抽 20 帧正常工况数据,用基线表反查其 ΔV 是否落在 P95 以内,命中率应接近 95%。
基线是“随船生长”的资产:每次完成一次排障并确认电池正常后,把该航次数据并入基线重算,P50/P95 会逐步收敛,后续误报会明显减少。