
简介电力窃漏电用户自动识别实战资源包面向具备一定Python基础、希望系统学习数据挖掘与机器学习完整流程的读者聚焦反窃电场景中的异常识别问题。压缩包共13个文件包含Python脚本、Excel数据表、numpy数组数据、训练好的模型及pickle序列化文件等整体仅23KB代码与配套数据高度凝练。目前已有443人学习下载兼具案例代表性与实用性。读者可以对照代码和数据集掌握缺失值插补、特征工程、模型选择与评估等关键环节并直接复用训练所得模型将识别思路拓展到其他工业异常检测任务。该案例覆盖从数据预处理到模型应用的全链路是快速上手电力数据挖掘实战的轻量级资料。1. 电力窃漏电用户自动识别为什么这个实战项目值得反复做做数据挖掘的人手里多半存着几个经典数据集但很少有一个项目像电力窃漏电用户自动识别这样把业务问题、特征工程、模型选型和落地评估完整串在一起。它不是 Kaggle 上那种清洗好的竞赛题而是带着明显噪音、不平衡标签和业务解释压力的真实场景——窃漏电用户占少数模型要能抓出这批人同时不能把正常用户投诉到门口。这个项目最吸引人的部分是它自带代码和数据集不需要你自己去网上拼凑脱敏数据。拿来就能跑通全流程从 pandas 清洗到特征构造再到机器学习模型训练和评估适合两类人一是刚学完 Python 基础和 sklearn 但没做过完整项目的人想看看真实业务里数据长什么样二是已经在做风控、反作弊或者设备异常检测的从业者想找个公开业务背景练手看看自己的特征工程思路和别人有什么差别。我当年第一次跑这个项目时最深的感受是数据量不大但坑一个不少。标签分布严重倾斜、时间窗口特征怎么构造、漏检和误检的代价不对称——这些在教科书例子里根本不会出现但恰恰是实际投产时必须面对的。下面我把整个方案按我自己的习惯拆开讲从数据理解到模型评估每一步都给出可复现的命令和参数并把最容易翻车的几个地方单独列出来。2. 窃漏电检测的业务逻辑与数据形态先搞懂电是怎么丢的2.1 电力线损率与窃漏电的关系为什么不用逐个查表在展开代码之前先花两分钟理解业务。供电公司关心的是线损率也就是供电量和售电量之间的差。理论上输电线路上有固定损耗但差值异常偏高通常意味着电在某个环节被截走了要么是线路老化要么是用户窃电或者计量装置故障。逐个去现场排查成本极高所以要用数据挖掘的办法从海量用户档案和用电记录里筛出嫌疑名单。这个项目的核心思路是窃漏电用户在用电行为上和正常用户有明显的统计差异。比如窃电用户常出现某个时间段用电量突然归零或者断崖式下降因为窃电行为往往伴随电表被人为干扰。再比如用电负荷曲线变得异常平稳缺少正常家庭或工厂的周期性波动。这些差异不需要领域专家一条条肉眼找而是可以通过特征工程量化然后用机器学习模型自动学习边界。项目自带的数据集通常包含两类文件一类是用户的基本档案信息另一类是电量采集记录。原始数据一般长这样用户编号、时间点、电量、电表状态等字段。处理的时候主要靠 pandas 做宽表转换和缺失值补齐构造出每个用户在某个统计窗口内的用电特征再和标签表合并。2.2 数据集字段说明与常见预测口径不同版本的电力窃漏电项目数据集字段略有差别但通常遵循同一套逻辑。先把常见的字段名、含义和处理优先级列成一张表后面代码都按这张表来操作字段名含义特征工程中的角色user_id用户编号分组聚合的键record_date采集日期时间窗口切分依据power_consumption当日用电量核心原始特征line_loss_rate线损率可做平滑处理或直接入模is_theft是否窃漏电标签训练目标1 为窃漏电读入数据后第一件事不是建模而是先检查标签分布。正常的窃漏电数据集里正样本占比通常在 5% 到 15% 之间如果某个版本里正样本超过了 30%就要怀疑数据构造方式是否合理。我一般用 pandas 的 value_counts(normalizeTrue) 看比例这个动作只花不到十秒钟但能避免后面所有评估环节被带偏。2.3 基础数据清洗与缺失值处理代码先写一段最基础的读取和清洗代码这一步决定了后面所有特征能否成立。项目里数据通常是 CSV 格式但实际抄表数据常带一些脏行比如某天采集失败导致电量字段为空或者用户档案表里存在重复记录。import pandas as pd import numpy as np # 读取用电记录日期列先按字符串读入不要提前转格式 df pd.read_csv(power_data.csv, parse_dates[record_date]) # 剔除 user_id 为空的行这类行无法关联到用户档案 df df.dropna(subset[user_id]) # 电量字段出现负值或超过某个物理上限时视为采集异常 df df[(df[power_consumption] 0) (df[power_consumption] 100000)] # 同一个用户同一天有多条记录时保留最后一条以采集时间倒序 df df.sort_values([user_id, record_date, collect_time], ascending[True, True, False]) df df.drop_duplicates(subset[user_id, record_date], keepfirst) # 查看每个用户有多少天的有效记录 record_count df.groupby(user_id)[record_date].count() print(record_count.describe())这段代码有几个值得留意的点collect_time 字段不是所有版本都有如果没有这个字段sort_values 里要删掉它电量上限 100000 只是我习惯用的粗筛阈值不同数据集单位不同如果单位是千瓦时居民用户单日超过这个数基本是异常但如果是工业用户就要放宽。每组完 groupby 后看一眼 record_count.describe() 的中位数如果有些用户只有几天记录后面构造统计特征时会缺很多值最好设定一个最少采样天数作为过滤门槛。3. 特征工程从电量序列到窃漏电嫌疑量化指标3.1 为什么用电趋势特征比原始电量更重要模型能学到什么程度取决于你给它的特征。直接把每日电量作为特征丢给分类器通常效果很差因为不同用户的用电基数差异太大居民用户一天用 20 度电和工厂一天用 2000 度电没有可比性。窃漏电检测真正有效的是变化模式比如电量下降的斜率、用电是否为持续异常低值、线损率的突变点。所以特征工程的核心是把时序数据压缩成统计量。常见做法是把每个用户最近 N 天的电量序列转换成一个固定长度的特征向量里面放均值、标准差、最大下降幅度、线损率波动等。这样每个用户变成一行训练样本标签从用户表关联进来。这里有一个容易踩坑的点时间窗口的选择不能拍脑袋。窗口太短比如只取 7 天捕捉不到窃电行为的持续特征窗口太长比如取 180 天很多用户的早期数据缺失严重而且窃电行为可能只发生在最近一个月早期正常用电反而稀释了信号。我一般先跑一组对比实验分别用 14 天、30 天、60 天窗口构造特征看验证集上的 AUC 差异再决定最终口径。3.2 构造用电特征与线损率突变特征先写特征构造的核心代码。假设清洗后的数据已经按用户和时间排序现在要为每个用户生成一个特征行。def build_features(df, window_size30): features [] for user_id, grp in df.groupby(user_id): grp grp.sort_values(record_date).tail(window_size) # 如果有效记录天数不足跳过该用户 if len(grp) window_size * 0.7: continue power grp[power_consumption].values loss grp[line_loss_rate].values feat { user_id: user_id, avg_power: np.mean(power), std_power: np.std(power), min_power: np.min(power), max_power_drop: np.max(np.diff(power)) * -1, zero_day_ratio: np.sum(power 1) / len(power), avg_loss_rate: np.mean(loss), loss_rising_ratio: np.sum(np.diff(loss) 0) / (len(loss) - 1), } features.append(feat) return pd.DataFrame(features) train_feats build_features(df_train, window_size30) print(train_feats.shape)这个函数里最需要注意的参数是 max_power_drop 的计算逻辑。np.diff(power) 得到的是电量序列的后一项减前一项正值表示电量上升负值表示下降。取负号再取最大值实际得到的是「最近一段时间内单日最大下降量」这个特征对窃电行为很敏感因为窃电往往是突然发生的电量曲线会出现一个明显的台阶。zero_day_ratio 则用来捕捉电表被短接或者人为标记为零的情况正常用户的零电量天数占比通常很低。另一个重要参数是 window_size * 0.7 这个阈值。它表示用户必须有至少 70% 的采样天数才能进入建模否则特征不可靠。这个比例可以根据数据质量调整数据特别完整的生产环境可以放到 0.9但像抄表经常漏采的环境 0.5 也合理。注意这里是按用户维度过滤而不是全局删除缺失值避免把整个数据集切成碎片。3.3 标签合并与训练集划分的注意事项特征构造完成后需要把标签合并进来。标签表里通常只有一部分用户有明确标记还有一部分用户从未被稽查过这类样本不能盲目当作负样本。常见做法是只保留标签明确的用户做训练或者把未稽查用户单独放一组做后续打分排序而不是混入训练集。label_df pd.read_csv(label.csv) train_df pd.merge(train_feats, label_df, onuser_id, howinner) # 查看正负样本比例 print(train_df[is_theft].value_counts(normalizeTrue)) # 按用户分层划分训练集和验证集 from sklearn.model_selection import StratifiedKFold skf StratifiedKFold(n_splits5, shuffleTrue, random_state42)合并时要特别注意 inner join 和 left join 的选择。如果标签表里只有 2000 个用户有标注而特征表有 5 万个用户inner join 会直接丢掉没有标签的用户这没问题。但如果有标签的用户在特征构造时被过滤掉了要检查到底是哪些用户被过滤、为什么被过滤避免由于数据缺失产生的系统性偏差。分层采样也是必须的直接用 train_test_split 而不分层在正样本只有 8% 的情况下很容易把验证集里正样本切得特别少影响评估稳定性。4. 模型训练与参数调节从逻辑回归到树模型的迭代路线4.1 第一版模型为什么选逻辑回归而不是直接上 XGBoost很多人在拿到特征后直接上了 XGBoost然后发现验证集效果不错但换一批数据就立刻退化。这不是 XGBoost 有问题而是缺少一个基准模型来做参照。我一般先训一个逻辑回归把所有特征做标准化看基础 AUC 是多少。逻辑回归在这个项目里并不弱因为窃漏电特征和标签之间确有线性关系比如线损率均值升高、零电量天数增加这些都是单调的趋势信号。逻辑回归的另一个价值是系数可解释。对业务方来说「哪个特征贡献最大」比「模型 AUC 是 0.87」更有说服力。可以用系数绝对值排名来佐证特征工程的方向对不对。如果发现某个理论上很强的特征系数接近零那可能是特征构造出了问题需要回去查。from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.metrics import roc_auc_score, classification_report feature_cols [c for c in train_df.columns if c not in (user_id, is_theft)] X train_df[feature_cols].values y train_df[is_theft].values scaler StandardScaler() X_scaled scaler.fit_transform(X) lr LogisticRegression(max_iter1000, class_weightbalanced, random_state42) lr.fit(X_scaled, y) # 用训练集上的交叉验证看稳定性同时输出系数排名 coef_rank pd.Series(lr.coef_[0], indexfeature_cols).abs().sort_values(ascendingFalse) print(coef_rank.head(10))class_weightbalanced 是这里最关键的参数。如果不加逻辑回归在正样本只有 8% 的数据上会倾向于把所有样本预测为负类AUC 照样可能不错但精确率和召回率完全无法看。加上 balanced 后损失函数里会按类别频率自动加权正样本的惩罚增大模型才会真正去学窃漏电用户的特征。max_iter1000 通常是必要的因为特征量虽然不多但标准化后某些特征之间的相关性可能让坐标下降收敛变慢。如果看到警告说没有收敛把 max_iter 继续往上调或者换用 liblinear 求解器。4.2 树模型怎么调才能不玄学以 LightGBM 为例逻辑回归给出基线后再用树模型做第二轮。LightGBM 在这个项目上不需要太多调参就能有不错的表现但有几个参数必须手动控制否则很容易过拟合。第一个是 min_child_samples它控制叶子节点最少样本数在窃漏电这种不平衡数据上叶子节点样本太少会导致学出只覆盖几个用户的极端路径。第二个是 scale_pos_weight用来处理类别不平衡可以用负样本数除以正样本数作为初值。from sklearn.model_selection import cross_val_predict from sklearn.model_selection import StratifiedKFold from lightgbm import LGBMClassifier params { n_estimators: 300, learning_rate: 0.05, max_depth: 5, num_leaves: 31, min_child_samples: 50, scale_pos_weight: int((y 0).sum() / (y 1).sum()), subsample: 0.8, colsample_bytree: 0.8, random_state: 42, } lgb LGBMClassifier(**params, verbose-1) cv_scores cross_val_predict(lgb, X, y, cvskf, methodpredict_proba)[:, 1] print(LightGBM AUC:, roc_auc_score(y, cv_scores))scale_pos_weight 的取值可以从负样本与正样本的比例直接得到比如负样本 1800 个、正样本 200 个就设 9。但这个值不是固定的我一般会在 5 到 15 之间配合验证集效果网格搜索一轮。subsample 和 colsample_bytree 分别表示行采样和列采样比例在数据量只有几千行的情况下不要设得太低小于 0.7否则每棵树看到的样本太少方差反而变大。这里有一个容易忽略的点用 cross_val_predict 得到的是整个数据集上的预测概率直接拿它算 AUC 会比真实泛化效果乐观一些因为同一个样本在不同折里被模型见到的信息有交叉。正确做法是把数据集拆成独立的 train 和 validation或者至少用 repeated stratified k-fold 多次取平均。生产项目里我倾向于先做一次 hold-out 切分用 70% 训练、15% 验证、15% 测试验证集和测试集只用一次防止手贱反复看测试集导致信息泄露。4.3 阈值选择把概率转成嫌疑名单的正确方式模型的输出是 0 到 1 之间的概率最终落地需要把概率转成「是否列入稽查名单」的二分类决策。默认阈值 0.5 在这个场景下完全不可用因为正样本比例低且漏掉一个窃电用户损失是几百几千元误判一个正常用户只是让稽查人员白跑一趟。所以阈值应该根据业务代价来选。from sklearn.metrics import precision_recall_curve precision, recall, thresholds precision_recall_curve(y, proba) f1_scores 2 * (precision * recall) / (precision recall 1e-9) # 取 F1 最大的阈值作为参考 best_idx np.argmax(f1_scores) print(best F1:, f1_scores[best_idx]) print(best threshold:, thresholds[best_idx])但 F1 最大不等于业务最优。如果稽查人力有限每天只能去现场查 50 户那应该按预测概率排序取前 50 个而不是按阈值切。换句话说这个项目真正交付的不是一个硬分类器而是一个打分排序模型。阈值只是给业务方一个转速表让他们根据现场反馈动态调整。实际操作中我会输出一张包含用户编号、预测概率、特征值的 CSV让稽查人员按概率从高到低排查每周统计查实率再用查实率反馈重新校准阈值。5. 避坑指南电力窃漏电项目里最容易翻车的五个细节5.1 时间穿越用未来数据预测过去的行为现象训练时 AUC 高达 0.95但换到新数据上完全失效。原因是什么特征构造时用了整段时间窗口的数据包括标签确定之后的数据导致模型学到了「事后信息」。在模型训练阶段窃漏电用户在标签确定后仍然持续异常特征里包含了这些未来信息自然能完美分辨。解决构造特征时只允许使用标签时间点之前的数据比如标签是 2024 年 6 月确定的特征只能用 6 月之前 30 天的电量记录。实际项目里时间穿越是头号杀手也是最难排查的。5.2 标签泄漏未稽查用户被当成负样本现象训练集里负样本特别多模型召回率高但精确率低。原因很多未标记用户被直接当作正常用户写入训练集。未稽查用户里可能隐藏着大量窃电用户模型学了这些「假负样本」自然会漏掉真实窃电者。解决只把「明确稽查过且判定正常」的用户作为负样本未稽查用户要么剔除要么放入待预测集合。如果业务上不能提供明确判定至少要在特征里加入「最近一次稽查时间」字段让模型知道哪些用户是被查过没问题的。5.3 特征窗口太短导致小额窃电漏检现象模型对剧烈窃电行为识别很好但对慢速窃电几乎无效。原因有些窃电手法是缓慢调低电表系数电量曲线不会出现断崖而是持续缓慢下降。窗口设置为 14 天时这种平缓衰减在短窗口内被当作正常波动。解决同时构造 14 天、30 天、90 天多个窗口的特征让模型同时捕捉短期突变和长期趋势。我实际项目中会专门加一个「过去 90 天电量斜率」特征通过线性回归拟合电量序列的斜率慢速窃电在这个特征上会有稳定负值。5.4 节假日和季节性用电波动干扰现象每年春节、夏季高温期模型误报率明显上升。原因窃漏电特征是「电量下降」但节假日工厂停产也会造成电量下降两者在特征上非常相似。解决特征工程中加入日历特征比如是否为法定节假日、是否为周末、未来一周是否高温预警。更复杂的做法是计算「同期环比」拿今年某日电量与去年同一时期电量做比值消除季节性影响。这个项目数据集里如果不含日期跨度超过一年的数据至少要把节假日标志加进去。5.5 评估指标只看 AUC 导致现场吃不消现象模型报告 AUC 超过 0.9但稽查队伍反馈每天白跑一半路。原因AUC 对类别不平衡不敏感它衡量的是排序能力而不是绝对正确率。在正样本占比只有 8% 的数据上AUC 0.9 可能对应的精确率只有 30%。解决报告模型效果时同时给出 top 50、top 100、top 200 名单内的精确率这个指标直接反映业务收益。我一般在建模完成后输出一张表格名单规模、预期查实户数、预期漏网户数让业务方按照处理能力选择每天的稽查名单长度。6. 进阶落地把模型输出变成可复查的嫌疑名单与效果反馈闭环模型训练完成后真正的挑战不是调参而是让业务方信任并持续使用。我见过太多项目止步于 Jupyter Notebook 里的高 AUC却从未真正帮助稽查人员省过一天时间。这个项目的进阶方向是把预测结果接回业务流。常见做法是写一个定时任务脚本每天读取前一天的数据重新构造特征调用已保存的模型预测概率然后生成当天的稽查推荐名单。名单格式要尽量贴近稽查人员已有的工作习惯包含用户编号、地址、预测概率、嫌疑特征摘要摘要直接写「近 30 天电量下降 47%线损率持续抬升」这样稽查人员不用打开模型报告就能判断是否值得去现场。名单输出代码可以写成这样def generate_daily_suspects(feature_df, model, top_n50): X feature_df[feature_cols].values proba model.predict_proba(X)[:, 1] result feature_df[[user_id]].copy() result[suspicion_score] proba result result.sort_values(suspicion_score, ascendingFalse).head(top_n) result.to_csv(daily_suspects.csv, indexFalse, encodingutf-8-sig) return result这里的 top_n 不是固定值。我一般让业务方在 20 到 100 之间调整并根据前一天的查实率反馈动态增减。比如周一查实率只有 20%周二可以把入场名单缩到 30 个如果能查实 5 个就扩大到 80 个。这个反馈闭环比任何参数调优都重要因为它让模型的效果直接与业务指标挂钩。最后说一个我自己的习惯每隔两周用新增的稽查结果重新评估一次模型把确认窃电的用户加入训练集把确认正常的用户从嫌疑名单里移除。这样才能保证模型随窃电手法的变化而演进。希望这个项目的每一步都能在你手里跑通少走我当初折腾了两周才绕开的弯路也希望你的模型真正被业务用起来而不只是停在代码仓库里吃灰。本文还有配套的精品资源点击获取