简介:这份资源是百度大数据竞赛2018「充电桩故障分类与检测」赛题的完整实战方案,面向参加数据挖掘竞赛、机器学习课程设计或希望积累分类项目经验的大学生与算法初学者。包内共9个文件,以3个py脚本、3个ipynb笔记本、2个csv数据集和1份md说明为主,压缩包约2.75MB,体积轻便却覆盖了从数据读取、特征处理到模型训练与评估的完整链路。其中notebook记录了kNN、逻辑回归、网格搜索交叉验证等方法的实现过程,脚本则提供XGBoost与主流程的工程化封装,csv数据可直接用于复现实验。该方案在赛题上取得f1-score 1.0000的成绩,已有272人学习参考,读者可借此理解故障分类任务的建模思路、调参策略与评估指标,并对照源码快速跑通自己的实验流程,适合作为竞赛入门与算法巩固的参考案例。
1. 充电桩故障分类与检测:从 F1-score 1.0000 说起
2018 年百度大数据竞赛有一道赛题叫“充电桩故障分类与检测”,赛题数据来自真实充电站的传感器日志,要求参赛者判断每个充电桩在某个时间窗口内是否发生故障,以及故障属于哪一类。这个赛题在当年讨论度很高,原因不只是充电桩本身是新能源基础设施的热点,更因为榜首队伍在评测集上拿到了 F1-score 1.0000 的满分。很多做工业设备故障诊断的工程师看到这个数字的第一反应是“数据泄漏了吧”,第二反应是“如果没泄漏,那特征工程到底做对了什么”。
这篇文章面向的是正在做设备故障分类、异常检测、时序信号诊断的从业者,尤其是手上有类似传感器日志、想复现一套高精度分类流程的人。我会把这道赛题拆成可落地的技术路径:数据长什么样、标签怎么构造、特征怎么提、模型怎么选、F1-score 为什么能到 1.0000、以及在实际产线数据上复现时会在哪里翻车。读完你应该能判断这个方案值不值得迁移到自己的场景,以及迁移时需要改哪些参数。
2. 赛题数据与任务定义:先搞清楚标签是怎么来的
2.1 充电桩故障分类到底在分什么
充电桩的传感器日志通常包含电压、电流、功率、温度、SOC、充电时长、启停状态等字段。故障分类任务不是简单地把整条充电记录打一个标签,而是要在时间维度上定位异常片段,再判断异常类型。2018 年这道赛题的数据组织方式,常见做法是把每个充电桩的连续运行数据切成固定长度的时间窗口,每个窗口对应一个故障类别标签,没有故障的窗口归为正常类。
这里有一个容易被忽略的点:故障类别是不均衡的。正常样本占绝大多数,某些故障类型可能只占千分之几。如果直接上准确率,模型只要全预测正常就能拿到很高的分数,所以赛题用 F1-score 作为评价指标。F1-score 是精确率和召回率的调和平均,对少数类更敏感,这也是为什么满分 1.0000 值得单独拿出来说——它意味着模型在每一类故障上都做到了精确率和召回率同时接近 1。
从任务本质看,这是一个多分类问题,但带有强时序依赖和强不均衡两个附加难度。很多队伍一开始把它当普通表格分类做,用 XGBoost 直接怼原始特征,结果 F1 卡在 0.7 左右上不去。问题不在模型,而在窗口切分和特征表达没有把故障的物理模式暴露出来。
2.2 数据字段与标签构造的常见做法
赛题没有给出一份统一的字段说明,但根据同类充电桩监控数据的结构,可以推断出几类关键字段。下面这张表是我在复现类似赛题时整理的最小字段集,实际数据可能字段名不同,但物理含义基本对应。
| 字段类型 | 典型字段名 | 物理含义 | 对故障分类的作用 |
|---|---|---|---|
| 电气量 | 电压、电流、功率 | 充电过程中的电参数 | 过压、欠压、过流类故障的核心判据 |
| 热学量 | 模块温度、枪温 | 充电模块和充电枪温度 | 过热故障、散热异常的直接指标 |
| 状态量 | 充电状态、连接状态 | 设备当前工作阶段 | 区分正常启停和异常中断 |
| 统计量 | 充电时长、SOC | 单次充电的累计信息 | 辅助判断故障发生的阶段 |
| 标签 | fault_type | 故障类别或正常 | 监督学习的监督信号 |
标签构造通常有两种方式。一种是赛题直接给了每个时间窗口的 fault_type,另一种是只给原始日志,需要自己根据告警记录对齐时间戳生成标签。后者更接近真实工程,也更容易出错。我一般会先做时间对齐检查:把告警记录的时间戳和传感器采样时间戳画在同一根时间轴上,看告警时刻前后传感器数值有没有明显跳变。如果没有跳变,要么是告警延迟,要么是采样频率太低,这两种情况都会让标签噪声变大。
提示:如果标签是外部告警系统生成的,一定要检查告警时间和传感器时间是否在同一时区、同一时钟源。我见过因为时区差 8 小时导致所有故障窗口错位的案例,模型怎么调都上不去。
2.3 评测指标 F1-score 的计算口径
多分类任务的 F1-score 有 micro、macro、weighted 三种平均方式。赛题通常用 macro-F1 或 weighted-F1。macro-F1 对每个类别单独算 F1 再取平均,少数类的表现会被放大;weighted-F1 按类别样本数加权,更接近整体表现。如果赛题没有明确说明,我一般会两个都算,以 macro-F1 为主做模型选择,因为故障诊断场景下少数类漏报的代价更高。
计算 macro-F1 的代码不复杂,但有几个细节容易写错:零除保护、标签顺序对齐、以及多分类下 precision 和 recall 的聚合方式。下面这段代码是我常用的实现,直接基于 sklearn 的 classification_report 做二次聚合。
from sklearn.metrics import precision_recall_fscore_support import numpy as np def macro_f1(y_true, y_pred, labels=None): """ 计算 macro-F1,带零除保护。 labels 用于固定类别顺序,避免不同 batch 顺序不一致。 """ if labels is None: labels = sorted(set(y_true) | set(y_pred)) p, r, f1, support = precision_recall_fscore_support( y_true, y_pred, labels=labels, zero_division=0 ) # 只对 support > 0 的类别求平均,避免不存在的类别拉低分数 valid = support > 0 return float(np.mean(f1[valid])), p, r, f1, support逻辑说明:precision_recall_fscore_support 返回每个类别的精确率、召回率、F1 和支持度。zero_division=0 保证某个类别没有被预测到时不会报错。valid 掩码过滤掉支持度为 0 的类别,这些类别在评测集里不存在,不应该参与平均。参数 labels 建议显式传入,尤其是在分布式评测或分批预测时,否则不同批次的类别顺序可能不一致,导致 F1 计算错误。
3. 从原始日志到特征矩阵:窗口切分与特征工程
3.1 时间窗口怎么切才不丢故障信息
窗口切分是这道赛题的第一个分水岭。切得太短,故障的完整波形被截断,模型看不到故障的全貌;切得太长,正常片段混入故障窗口,标签噪声变大。常见做法是滑动窗口加重叠采样,窗口长度根据故障持续时间的中位数来定。如果故障平均持续 30 秒,采样频率 1 Hz,窗口长度可以设 60 到 90 个点,步长设 15 到 30 个点。
我一般会先做一件事:统计每个故障类别的持续时长分布,取中位数和 75 分位数,窗口长度设在中位数和 75 分位数之间。这样大部分故障能完整落在窗口内,少数长故障会被切成多个窗口,但每个窗口仍然带有故障特征。步长设窗口长度的四分之一到三分之一,保证相邻窗口有足够重叠,不会因为切分点刚好落在故障边界而漏掉。
import numpy as np def sliding_window(data, window_size, step): """ data: shape (T, F) 的时序数组 返回 shape (N, window_size, F) 的窗口数组 """ T, F = data.shape if T < window_size: return np.empty((0, window_size, F)) starts = range(0, T - window_size + 1, step) windows = np.stack([data[s:s + window_size] for s in starts]) return windows # 示例:窗口 90 个点,步长 30 X = sliding_window(sensor_data, window_size=90, step=30)逻辑说明:sliding_window 按固定步长在时间轴上滑动,每个窗口保留完整的 F 个特征通道。参数 window_size 决定单个样本的时间跨度,step 决定样本之间的重叠程度。step 越小,样本越多,但相邻样本相关性越强,训练时容易过拟合。我通常用 step = window_size // 3,在样本量和独立性之间取平衡。
3.2 时域、频域、统计域特征怎么组合
原始窗口如果直接展平成一维向量,维度是 window_size × F,对于 90 × 10 的窗口就是 900 维。这个维度不算高,但直接展平会丢失时序结构,而且对相位偏移敏感。更稳的做法是提取三类特征:时域统计量、频域能量、以及基于物理含义的派生量。
时域统计量包括均值、标准差、最大值、最小值、峰峰值、偏度、峰度、过零率。这些特征计算快,对过压、欠压、过流类故障很敏感。频域特征用 FFT 或小波变换提取各频段能量,对周期性异常和振荡类故障更有效。派生量则是根据充电桩物理模型构造的,比如功率因数、温升速率、电压电流比值,这些量在故障时往往比原始量更稳定。
from scipy.stats import skew, kurtosis from scipy.fft import rfft, rfftfreq import numpy as np def extract_features(window, fs=1.0): """ window: shape (window_size, F) 返回一维特征向量 """ feats = [] for ch in range(window.shape[1]): x = window[:, ch] feats.extend([ np.mean(x), np.std(x), np.max(x), np.min(x), np.max(x) - np.min(x), skew(x), kurtosis(x), np.mean(np.abs(np.diff(x))) # 平均绝对差分,反映波动 ]) # 频域:前 5 个频段能量 spec = np.abs(rfft(x - np.mean(x))) freqs = rfftfreq(len(x), d=1.0 / fs) for i in range(5): lo, hi = i * len(spec) // 5, (i + 1) * len(spec) // 5 feats.append(np.sum(spec[lo:hi] ** 2)) return np.array(feats)逻辑说明:每个通道提取 8 个时域特征和 5 个频域特征,10 个通道就是 130 维。skew 和 kurtosis 对冲击类故障敏感,平均绝对差分对缓变类故障敏感。频域能量按等分频段聚合,避免维度随窗口长度增长。参数 fs 是采样频率,必须和实际数据一致,否则频段划分没有物理意义。
注意:频域特征对窗口长度敏感。如果训练集和测试集的窗口长度不一致,频域能量不可比。我一般会在特征提取前统一重采样到固定长度,或者在窗口切分时就保证长度一致。
3.3 特征筛选与降维的取舍
130 维特征不算多,但如果加上原始展平特征和更多派生量,维度可能到几百。这时候需要做特征筛选。常见做法是用树模型的特征重要性做初筛,再用递归特征消除或方差阈值做二次筛选。我一般会保留重要性排名前 60% 的特征,同时强制保留每个物理通道至少一个时域和一个频域特征,避免某个通道被完全丢弃。
降维方面,PCA 和 t-SNE 在故障分类里要慎用。PCA 是线性降维,对非线性故障模式可能把判别信息压掉;t-SNE 适合可视化但不适合作为分类器的输入,因为它的映射不稳定,训练集和测试集不能共用同一个映射。如果一定要降维,我会用 LDA 或者基于树模型的特征重要性做硬筛选,而不是做线性投影。
4. 模型选型与训练:为什么 F1-score 能到 1.0000
4.1 树模型、时序模型、混合模型的对比
在充电桩故障分类这个任务上,模型选型取决于特征表达的程度。如果特征工程已经把故障模式暴露得很清楚,树模型就足够;如果特征工程做得浅,需要模型自己学时序模式,那就要上时序模型。
| 模型类型 | 代表算法 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|---|
| 树模型 | XGBoost、LightGBM | 训练快、可解释、对不均衡有内置处理 | 不擅长原始时序 | 特征工程充分时 |
| 时序模型 | LSTM、TCN、Transformer | 自动学时序依赖 | 需要大量数据、训练慢 | 原始窗口直接输入 |
| 混合模型 | CNN + LSTM、树模型 + 残差 | 兼顾局部模式和时序 | 调参复杂 | 追求极致 F1 |
2018 年赛题榜首队伍拿到 1.0000,常见的技术路径是:精细的窗口切分 + 充分的物理特征工程 + 树模型集成。树模型在这个任务上有天然优势,因为故障判据很多是阈值型的,比如温度超过某个值就报警,树模型的分裂节点正好对应这种阈值逻辑。时序模型反而可能因为数据量不够而欠拟合。
4.2 LightGBM 多分类训练与参数设置
LightGBM 是我在这个任务上首选的基线模型。它训练快,支持类别特征,对不均衡数据可以用 class_weight 或 scale_pos_weight 调节。下面是一个可复现的训练脚本。
import lightgbm as lgb from sklearn.model_selection import StratifiedKFold from sklearn.metrics import f1_score import numpy as np # X: (N, D) 特征矩阵, y: (N,) 标签 params = { 'objective': 'multiclass', 'num_class': len(np.unique(y)), 'metric': 'multi_logloss', 'learning_rate': 0.05, 'num_leaves': 63, 'max_depth': -1, 'min_data_in_leaf': 20, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 5, 'lambda_l2': 1.0, 'verbose': -1, 'seed': 42, } skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) oof_pred = np.zeros((len(y), params['num_class'])) models = [] for fold, (tr, va) in enumerate(skf.split(X, y)): dtrain = lgb.Dataset(X[tr], label=y[tr]) dvalid = lgb.Dataset(X[va], label=y[va]) model = lgb.train( params, dtrain, num_boost_round=2000, valid_sets=[dvalid], callbacks=[lgb.early_stopping(100), lgb.log_evaluation(0)] ) oof_pred[va] = model.predict(X[va]) models.append(model) # macro-F1 评估 y_pred = oof_pred.argmax(axis=1) print('macro-F1:', f1_score(y, y_pred, average='macro'))逻辑说明:objective 设为 multiclass,num_class 等于类别数。learning_rate 0.05 配合 early_stopping 100 轮,避免过拟合。num_leaves 63 是中等复杂度,min_data_in_leaf 20 防止叶子节点样本过少。feature_fraction 和 bagging_fraction 都设 0.8,增加模型多样性。5 折 StratifiedKFold 保证每折的类别分布一致,oof_pred 保存折外预测,用于后续集成和阈值调优。
参数调整上,如果 F1 上不去,优先调三个:num_leaves 降到 31 看是否过拟合,min_data_in_leaf 升到 50 看是否噪声太大,learning_rate 降到 0.02 配合更多轮数看是否欠拟合。class_weight 可以设 balanced,但在 macro-F1 下效果不一定好,因为 balanced 会过度偏向少数类,导致多数类召回下降。
4.3 不均衡处理与阈值调优
多分类的不均衡处理比二分类复杂。常见做法有重采样、代价敏感学习、以及后处理阈值调优。重采样里,SMOTE 对时序特征不一定合适,因为插值出来的样本可能不符合物理规律。我更倾向于用类别权重,让损失函数对少数类更敏感。
阈值调优是提分的关键一步。树模型输出的概率经过 argmax 得到类别,但 argmax 假设各类别先验相同。如果评测集和训练集分布不一致,或者某些类别的 F1 明显偏低,可以通过调整各类别的决策阈值来提升 macro-F1。做法是先在验证集上搜索每个类别的概率缩放系数,再应用到测试集。
from scipy.optimize import minimize import numpy as np def optimize_weights(proba, y_true, n_class): """ 搜索每个类别的概率缩放系数,最大化 macro-F1。 proba: (N, n_class) 概率矩阵 """ def neg_f1(w): w = np.abs(w) pred = (proba * w).argmax(axis=1) return -f1_score(y_true, pred, average='macro') best = minimize(neg_f1, np.ones(n_class), method='Nelder-Mead', options={'maxiter': 500}) return np.abs(best.x) weights = optimize_weights(oof_pred, y, params['num_class']) final_pred = (test_proba * weights).argmax(axis=1)逻辑说明:optimize_weights 用 Nelder-Mead 搜索一组正权重,对每个类别的概率做缩放后再 argmax。权重越大,该类别的预测倾向越强。用 abs 保证权重为正。这个方法在验证集上做,避免在测试集上过拟合。参数 maxiter 500 足够收敛,如果类别数很多可以适当增加。
提示:阈值调优要在折外预测上做,不要在训练集预测上做。训练集上模型已经过拟合,调出来的权重没有泛化性。
5. 避坑与排查:F1-score 1.0000 背后的血泪经验
5.1 现象:验证集 F1 很高,测试集掉到 0.6
原因:最常见的是数据泄漏。窗口切分时如果先切窗口再划分训练测试集,同一个充电桩的相邻窗口会同时出现在训练集和测试集,模型记住了设备 ID 相关的模式。另一个原因是特征里包含了未来信息,比如用整个充电过程的统计量作为每个窗口的特征。
解决:按充电桩 ID 或时间顺序划分训练测试集,保证同一设备的窗口只出现在一个集合。特征提取时严格只用当前窗口及之前的数据,不要用全局统计量。我一般会加一个时间戳检查,确保测试集的时间戳晚于训练集。
5.2 现象:某个故障类别的 F1 始终为 0
原因:该类别的样本太少,模型完全没有学到。或者该类别的特征和其他类别高度重叠,树模型的分裂节点无法区分。也有可能是标签编码错误,该类别的标签在训练集里根本不存在。
解决:先检查训练集里该类别的样本数,如果少于 50 个,考虑合并到相似类别或用手工规则兜底。如果样本数够但 F1 为 0,检查特征分布,用 t-SNE 或 PCA 可视化看该类是否和其他类混在一起。如果是标签编码错误,重新核对标签映射表。
5.3 现象:训练集 F1 1.0,验证集 F1 0.7
原因:过拟合。树模型的 num_leaves 太大、min_data_in_leaf 太小、或者特征维度太高而样本量不够。也有可能是特征里有 ID 类特征,模型直接记住了 ID 和标签的对应关系。
解决:降低 num_leaves 到 31 或 15,提高 min_data_in_leaf 到 50 或 100,增加 lambda_l2 正则。检查特征里有没有设备 ID、时间戳、序号这类泄漏特征,有的话直接删掉。如果样本量确实少,考虑用交叉验证的折外预测做模型选择,而不是看训练集分数。
5.4 现象:F1-score 在 0.99 和 1.00 之间抖动
原因:少数类样本太少,验证集里某个类别只有几个样本,预测对了一个 F1 就跳,错了一个就掉。这是评测集划分的随机性导致的,不是模型问题。
解决:用多次交叉验证取平均,而不是单次划分。如果赛题只给一次评测机会,在本地做 10 次不同随机种子的划分,看 F1 的均值和标准差。标准差大于 0.01 说明评测集太小,模型选择要更保守。
5.5 现象:推理时单条样本预测正常,批量预测结果不一致
原因:特征提取时用了批量统计量,比如对整个 batch 做归一化,导致单条预测和批量预测的归一化参数不同。或者模型输入的特征顺序在训练和推理时不一致。
解决:把归一化参数固化到模型里,推理时用训练集统计的均值和标准差,不要用当前 batch 的统计量。特征顺序用列表固定,训练和推理共用同一个特征提取函数。我一般会把特征提取和模型打包成一个 Pipeline,避免手动对齐出错。
6. 进阶技巧:把 1.0000 的偶然变成可复现的必然
F1-score 1.0000 在竞赛里可能是数据干净、特征到位、模型合适三者叠加的结果,但在真实产线数据上,满分几乎不可能。更有价值的做法是建立一套可复现的验证流程,让模型在数据分布变化时仍然稳定。我一般会做三件事。
第一,建立时间外推验证。把数据按时间切成三段,前两段训练,第三段测试。如果第三段 F1 比第二段掉超过 5 个点,说明模型对时间漂移敏感,需要加入时间衰减权重或在线更新机制。
第二,做特征稳定性监控。对每个特征计算训练集和测试集的 PSI(Population Stability Index),PSI 大于 0.2 的特征要重点检查。充电桩的传感器老化、季节温度变化都会导致特征分布漂移,PSI 能提前发现这些问题。
| 监控项 | 阈值 | 处理方式 |
|---|---|---|
| PSI | > 0.2 | 检查传感器是否漂移,考虑重新校准 |
| 类别分布 | 偏差 > 10% | 调整类别权重或重新采样 |
| macro-F1 | 下降 > 5% | 触发模型重训 |
| 单类召回 | < 0.8 | 单独调该类阈值或补样本 |
第三,保留人工复核通道。F1 再高,故障诊断场景下也不能完全自动决策。我习惯把模型概率在 0.5 到 0.7 之间的样本挑出来,交给人工复核,这些样本往往是模型不确定的边界案例,复核结果可以回流做增量训练。
最后说一个我自己的习惯:每次跑完实验,不管 F1 多高,我都会把当次的特征列表、参数配置、数据划分随机种子写进一个 run_log 文件。竞赛里靠记忆复现实验,十次有九次会对不上。这个习惯让我在后来的产线项目里少踩了很多“上次明明能跑通”的坑。希望帮到你。
本文还有配套的精品资源,点击获取