变压器油里那七八种溶解气体,一台百来块的油色谱在线监测装置或者一次例行化验就能拿到。但拿到之后怎么从这些数字里看出变压器内部到底在“憋什么大招”,就是DGA故障诊断要解决的事。我做这个项目的目的很直接:把溶解气体分析(DGA)从教科书里的经验规则,变成一套能运行、能验证、能出报告的代码流程,顺手把清洗好的数据也一起整理出来。整套东西适合刚接触电力设备状态检修的工程师、做故障诊断算法研究的同学,也适合想在企业内部快速搭一个诊断原型的团队。文章里的代码都用Python写,数据是结构化表格,拉到本地就能跑。
这套项目的核心不是堆模型,而是把“绝缘油里溶解气体浓度”翻译成“变压器内部故障状态”。所以我会先从物理原理和数据形态讲起,再走一遍经典IEC三比值法的代码实现,然后是机器学习建模流程,最后专门聊模型评估和部署时最容易翻车的几个细节。
1. 为什么变压器油中溶解气体能“说”出故障:DGA原理与数据形态
1.1 变压器内部故障如何变成油里的气体
油浸式变压器内部填充的绝缘油和绝缘纸,正常情况下化学性质很稳定。但当设备内部出现异常工况——比如局部放电、过热、电弧——油纸绝缘材料就会在电和热的双重作用下发生裂解,产生氢气、甲烷、乙烷、乙烯、乙炔等低分子烃类气体。这些气体一部分溶解在油里,一部分聚集在气体继电器中。
关键点在于,不同故障类型产生的气体组合和比例是有规律可循的。这就像人发烧时白细胞会升高,但具体是细菌感染还是病毒感染,要看不同指标的比例。DGA诊断的物理基础,就是气体产物的“指纹特征”与故障能量密度之间的对应关系:
- 局部放电(低能量密度):主要产氢,少量甲烷。因为放电能量不足以打断碳碳键,只够打断碳氢键,所以以氢气为主。
- 热故障(温度升高):温度超过300℃时,碳碳键开始断裂,产生乙烯、甲烷;温度越高,乙烯占比越大。
- 电弧放电(高能量密度):油和纸在电弧高温下直接裂解,产生大量乙炔,这是判断放电性故障的决定性气体。
一台正常老化的变压器也会产气,但浓度和产气速率都处在较低水平。DGA诊断的第一个动作,其实就是拿气体浓度和产气速率去对标“正常范围”,超出限度才进入故障类型判断环节。
1.2 DGA数据长什么样:特征列与标签体系
要写代码做诊断,先把数据结构定义清楚。DGA原始数据通常是一张包含特征列和标签列的表格,每行代表变压器在某次取样时的油色谱化验结果。
我的数据集里用到的核心特征列是七种气体的体积分数,单位用μL/L(也就是ppm),这是电力行业油色谱分析的通用单位:
| 气体名称 | 中文名 | 化学式 | 典型产生原因 |
|---|---|---|---|
| H2 | 氢气 | H2 | 局部放电、电弧放电 |
| CH4 | 甲烷 | CH4 | 低温过热、放电 |
| C2H6 | 乙烷 | C2H6 | 低温热分解 |
| C2H4 | 乙烯 | C2H4 | 中高温过热 |
| C2H2 | 乙炔 | C2H2 | 电弧放电、高温热解 |
| CO | 一氧化碳 | CO | 固体绝缘(纸)过热 |
| CO2 | 二氧化碳 | CO2 | 固体绝缘老化 |
标签列是我根据IEC 60599和DL/T 722等标准梳理出来的故障类型编码,完整分为七类:
| 标签值 | 故障类型 | 说明 |
|---|---|---|
| 0 | 正常 | 无需停机检修 |
| 1 | 局部放电 | 低能量放电,早期绝缘缺陷 |
| 2 | 低温过热(<300℃) | 通常由过载或油道堵塞引起 |
| 3 | 中温过热(300~700℃) | 导体接触不良等 |
| 4 | 高温过热(>700℃) | 严重过热,需尽快处理 |
| 5 | 低能放电 | 悬浮电位放电、火花放电 |
| 6 | 电弧放电 | 高能放电,最危险,需立即停电 |
1.3 构建一份规整的DGA数据集
网上能下载到的公开DGA数据集不少,但大部分存在几个通病:数值缺失、标签命名不统一、还有用“正常/异常”二分类糊弄人的。我在整理数据时做了三件事:
第一,统一单位的转换。有些数据源用的是ppm,有些是μL/L,有些甚至给百分比浓度。我在预处理阶段统一换算成μL/L,百分比数值乘以10000。
第二,删除全零行。如果某次取样所有气体浓度都是0,说明油样可能没取好或者检测仪器没跑通,这种样本放进模型只会制造噪声。
第三,标签对齐。把“PD”“局部放电”“低能量放电”这类同义表述统一映射到整数标签上。这一步看着简单,实际处理时能过滤掉将近20%的脏数据,这是很多论文复现时不会提到的坑。
完成清洗之后,最终CSV文件长这样:
H2,CH4,C2H6,C2H4,C2H2,CO,CO2,label 56,78,32,45,0,120,800,0 120,45,12,20,95,90,650,6 35,110,48,160,0,140,950,4第一行是正常样本,各类气体浓度都在合理区间;第二行乙炔浓度高达95μL/L,对应电弧放电;第三行乙烯160μL/L、乙炔为0,典型高温过热特征。做机器学习之前先人工看几十条数据,形成数感,对后面理解模型输出帮助很大。
2. 先把经典规则跑通:IEC三比值法的代码化与边界认知
2.1 三比值规则的编码逻辑回顾
IEC三比值法的核心思路不复杂:取五种关键气体,构造三个比值,每个比值按阈值范围编码成0、1、2,组合出三位编码后查表得到故障类型。
三个比值是:
- CH4/H2,反映热分解与放电分解的比例
- C2H2/C2H4,反映放电能量高低
- C2H4/C2H6,反映热分解温度高低
每个比值的编码规则如下:
| 比值范围 | CH4/H2编码 | C2H2/C2H4编码 | C2H4/C2H6编码 |
|---|---|---|---|
| < 0.1 | 1 | — | — |
| 0.1 ~ 1 | 0 | — | 0 |
| 1 ~ 3 | 2 | 0 | 1 |
| 3 ~ 10 | 2 | 1 | 2 |
| ≥ 10 | 2 | 2 | 2 |
| 其他(C2H2/C2H4 < 0.1) | — | 1 | — |
比值编码组合对应故障类型,这就是“三比值”名称的来历。逻辑本身并不复杂,适合做规则的代码落地。
2.2 用Python把三比值规则写成可复用的诊断函数
我写了一个iec_three_ratio函数,输入七个气体浓度,输出故障类型名称。实现时注意处理分母为零和比值越界的情况:
def iec_three_ratio(h2, ch4, c2h6, c2h4, c2h2, co, co2): """ IEC三比值法诊断 返回: (编码字符串, 诊断结果) """ # 处理分母为零的情况 h2 = max(h2, 1e-9) c2h4 = max(c2h4, 1e-9) c2h6 = max(c2h6, 1e-9) ratio1 = ch4 / h2 ratio2 = c2h2 / c2h4 ratio3 = c2h4 / c2h6 # 编码CH4/H2 if ratio1 < 0.1: code1 = 1 elif ratio1 < 1: code1 = 0 else: code1 = 2 # 编码C2H2/C2H4 if ratio2 < 0.1: code2 = 1 elif ratio2 < 3: code2 = 0 else: code2 = 2 # 编码C2H4/C2H6 if ratio3 < 1: code3 = 0 elif ratio3 < 3: code3 = 1 else: code3 = 2 code = f"{code1}{code2}{code3}" mapping = { "010": "局部放电", "020": "局部放电", "001": "低温过热(<300℃)", "002": "中温过热(300-700℃)", "021": "高温过热(>700℃)", "110": "低能放电", "120": "低能放电", "100": "电弧放电", "101": "电弧放电", "121": "电弧放电", "000": "正常", "200": "正常", } result = mapping.get(code, "无法判断") return code, result函数签名里把CO和CO2也加进来了,虽然三比值法用不到,但保持接口统一,后面机器学习特征工程可以直接复用同一份数据。
2.3 三比值法的典型失效场景与代码应对
实际用下来,三比值法最麻烦的是两类情况。
第一类,比值刚好落在阈值边界上。比如CH4/H2等于1.0,编码该算0还是2?标准条文里写了“1~3区间”,但现场化验数据很难精确等于整数,真遇到边界值,我的处理方式是同时输出两个可能编码,把两种故障类型都报给检修人员参考,不硬选一个。代码里可以用一个边界容差逻辑处理:
def encode_ratio_with_tolerance(value, boundary, tol=0.05): """边界值容忍编码,避免因为化验误差导致跳变""" if abs(value - boundary) < tol: return "boundary" if value < boundary: return 0 return 1第二类,五类故障之外的情况。实际数据里会出现编码组合查不到对应故障类型,比如“111”。这时候三比值法的查表直接给“无法判断”。我的经验是,遇到查不到的编码,不要强行归到某一类,而是返回原始特征让后续的机器学习模型来兜底——规则给先验,模型做精细判断,两条路并行。
3. 数据驱动的故障诊断:特征工程与机器学习建模全流程
3.1 为什么规则之后还要建模
IEC三比值法虽然经典,但它存在一个先天短板:只用了五种气体构造三个比值,信息量被压缩得很厉害。现场数据里,CO和CO2本身蕴含着固体绝缘过热的信息,三比值法完全没用上;同时,三比值法的阈值是行业经验总结出来的,未必在每一台变压器上都最优。
机器学习的思路不一样,它不做先验假设,让算法自己从大量标注样本中学出特征与故障的映射关系。我在这个项目里跑下来,树模型在七分类任务上准确率能到88%—92%,比三比值法的70%左右有明显提升。更重要的是,机器学习模型能给出概率输出,而不只是一个硬分类结果——这一点对现场检修决策非常有用,因为概率低的诊断结果,复核优先级应该更高。
3.2 特征工程与训练集划分
气体浓度的数量级跨度很大,H2可能在几十到几千ppm浮动,而CO2可能到几千甚至上万ppm。直接喂给决策树模型没问题,但如果后续要换SVM、逻辑回归这类对尺度敏感的模型,必须先做标准化。
我的特征工程方案分三步:
第一步,原始浓度做对数变换。气体浓度分布通常右偏,少数高浓度样本会把模型带偏。对数变换后分布更接近正态,模型更容易学到规律:
import numpy as np gas_cols = ["H2", "CH4", "C2H6", "C2H4", "C2H2", "CO", "CO2"] def log_transform(df, cols): df_log = df.copy() for col in cols: # 加1避免log(0)的问题 df_log[col + "_log"] = np.log1p(df[col]) return df_log第二步,保留比值特征。虽然机器学习能自动捕捉特征交互,但领域知识明确告诉我们,甲烷/氢气和乙炔/乙烯这两个比例是诊断的关键信息,直接构造出来放进特征集,相当于给模型开了小灶。
第三步,加入总可燃气体浓度THC(Total Hydrocarbon),即五种烃类气体之和。这个特征在实际检修中常用于判断设备整体劣化程度。
训练集划分有个细节:DGA数据通常来自不同地区、不同厂家、不同电压等级的变压器,如果简单随机切分,训练集中可能恰好都是某一类变压器的样本,导致模型泛化能力虚高。我用按设备ID分组的GroupShuffleSplit,确保同一台变压器的数据不会同时出现在训练集和测试集里。
3.3 模型选型与训练细节
我在项目里对比了随机森林、XGBoost和LightGBM三个经典树模型,最终默认推荐LightGBM。原因有三:
第一,训练速度快。同样的数据量,LightGBM的直方图算法比XGBoost的预排序算法快3—5倍,调参周期短很多。
第二,对类别特征友好。DGA数据里如果加入变压器的电压等级、冷却方式等现场信息,LightGBM可以直接处理类别特征,省去独热编码。
第三,内存占用低。笔记本电脑上跑几千条样本毫无压力。
核心训练代码:
import lightgbm as lgb from sklearn.model_selection import GroupShuffleSplit from sklearn.metrics import classification_report # 特征列与标签列 feature_cols = [col + "_log" for col in gas_cols] + ["CH4_H2", "C2H2_C2H4", "THC"] # 按设备ID分组切分数据 gss = GroupShuffleSplit(n_splits=1, test_size=0.25, random_state=42) train_idx, test_idx = next(gss.split(X, y, groups=device_ids)) X_train, X_test = X.iloc[train_idx], X.iloc[test_idx] y_train, y_test = y.iloc[train_idx], y.iloc[test_idx] # LightGBM分类器 model = lgb.LGBMClassifier( n_estimators=300, learning_rate=0.05, num_leaves=31, max_depth=-1, class_weight="balanced", # 处理类别不平衡 random_state=42 ) model.fit(X_train, y_train) # 测试集评估 y_pred = model.predict(X_test) print(classification_report(y_test, y_pred, target_names=class_names))class_weight="balanced"这个参数值得单独说一下。DGA数据集里“正常”样本往往远多于“电弧放电”样本,如果不管类别不平衡,模型会倾向于把所有样本都预测成多数类,整体准确率看着高,但最危险的放电故障一个都识别不出来。设置class_weight="balanced"就是让模型在训练时对少数类样本的错分给予更高惩罚,迫使它学到少数类的特征。
训练完模型之后,保存模型文件供后续调用:
import joblib joblib.dump(model, "dga_xgb_model.pkl")4. 模型评估与现场部署必须直面的几个实际问题
4.1 用混淆矩阵看清七类故障的识别短板
准确率不是唯一的评价指标,在故障诊断场景里,甚至不是最重要的指标。我习惯先打印混淆矩阵,看每一类故障具体被误判成了什么:
import matplotlib.pyplot as plt import seaborn as sns from sklearn.metrics import confusion_matrix cm = confusion_matrix(y_test, y_pred) plt.figure(figsize=(10, 8)) sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", xticklabels=class_names, yticklabels=class_names) plt.ylabel("真实故障类型") plt.xlabel("预测故障类型") plt.show()我跑出来的混淆矩阵里,有一个典型问题值得说道:中温过热(标签3)和高温过热(标签4)之间经常互相混淆。原因是这两类故障在气体比例上本身就有连续性——300℃和700℃之间并没有一个绝对的分界线,部分样本处于过渡区间,气体特征介于两者之间。这就好比黄昏时分让你判断“现在是白天还是晚上”,边界本来就是模糊的。
应对办法有两个层面。第一,在现场报告里把“中温过热”“高温过热”合并成“热故障”大类作为一级结论,再给出倾向性判断(“偏中温”或“偏高溫”),这样不违背诊断逻辑,也不会因为过细的划分导致误判被放大。第二,在模型层面引入软标签,让标签3和标签4的样本在训练时相互有一定概率,而不是一成不变的硬编码,实测能提升3—5个百分点的F1值。
4.2 样本不平衡与误报率控制
DGA数据天然存在样本不平衡问题。“正常”类样本数量多、获取成本低;“电弧放电”样本数量少,因为一旦真发生电弧放电,这台变压器就要停电检修,很难再采集到后续数据。我手里的数据集中,正常样本占比接近45%,而电弧放电样本只有8%。
处理不平衡,除了前面提到的class_weight,还可以用SMOTE过采样。但这里我要泼一盆冷水:SMOTE在合成少数类样本时,是在特征空间里的少数类样本之间线性插值生成新样本。DGA数据的不同特征之间不是完全独立的,盲目插值可能生成出物理上不合理的样本,比如氢气浓度极低但乙炔浓度极高的“合成电弧放电”——真实设备中这种情况几乎不存在。我试用过SMOTE,准确率提升不明显,反而让模型在真实数据上误报率上升。
更推荐的做法是调整判断阈值。LightGBM默认用0.5作为分类阈值,但我们可以把少数类(放电类故障)的阈值调低到0.3,让模型更“敏感”:
# 获取预测概率而不是硬标签 y_prob = model.predict_proba(X_test) # 给放电类故障(标签5,6)更低的判定阈值 thresholds = np.full(y_prob.shape[1], 0.5) thresholds[5] = 0.3 thresholds[6] = 0.3 y_pred_adj = (y_prob >= thresholds).argmax(axis=1)这样调整后,个别正常样本可能被误报成放电,但换来的是放电故障不漏报。在生产环境里,一次漏报的代价是变压器损坏、非计划停电,远高于一次误报产生的现场复核成本。孰轻孰重,做运维的老师傅心里都有杆秤。
4.3 特征重要性:让黑盒诊断结果能对着检修人员讲清楚
现场检修人员不一定信任模型输出“预测为电弧放电”这句话。他们更希望看到“因为乙炔浓度升高、且乙烯浓度上升,模型判断为电弧放电”,这样才能结合自己的经验做最终判断。所以在项目中我加入了特征重要性分析:
# 方法一:基于内置特征重要性的全局解释 import pandas as pd importance_df = pd.DataFrame({ "feature": feature_cols, "importance": model.feature_importances_ }).sort_values("importance", ascending=False) print(importance_df.head(10))我的模型跑下来,特征重要性排前三的通常是:C2H2_log(乙炔对数浓度)、C2H4_log(乙烯对数浓度)、CH4_H2(甲烷/氢气比值)。
这个结果完全符合领域知识,也提供了一道“验证防线”:如果某个模型的Top特征里居然出现了CO2这种几乎不参与故障类型判别的特征,那你就要怀疑数据集或者模型是不是出了问题。用领域知识校验模型行为,是数据驱动方法落地时最容易被忽略但最关键的一步。
如果想做单样本级别的解释,可以用SHAP库:
import shap explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(X_test.iloc[[0]]) # 解释第一号样本 shap.summary_plot(shap_values, X_test.iloc[[0]], feature_names=feature_cols)SHAP值能展示每一个特征对诊断结果的贡献方向与大小,对“乙炔浓度推高了放电类故障概率”这类结论给出数值级别的支撑。这也是我建议所有做DGA诊断模型的人都应该加上的一步——解释性不是锦上添花,而是设备和人身安全场景下的硬需求。
最后再分享一个小经验:这套诊断流程我从规则版迭代到模型版,最大的心得是不要把规则和模型对立起来。三比值法编码结果完全可以作为一维额外特征输入给LightGBM,相当于把几十年行业经验作为先验知识灌进模型里。我最终线上跑的版本,就是在原始7维气体特征之外,加上了三比值编码和总烃含量,整体准确率比只用原始特征的版本又高了2%左右。数据和完整代码我都整理好了,拿到后从data/dga_samples.csv开始跑一遍,配合本文的诊断代码,基本能覆盖日常DGA数据分析的所有流程。