
摘要本文以电力业务为背景系统讲解决策树与随机森林的原理、训练、调参与实战应用。内容涵盖决策树如何通过“层层提问”做分类、深度与过拟合的关系、随机森林如何通过组队投票降低方差、决策树回归的局限不会外推、输出是台阶、如何可视化决策树、特征重要性的解读与“重要≠因果”的辨析最后以反窃电用户识别为例演示从数据划分、模型对比、特征分析到人工复核的完整流程。一、问题引入什么是决策树核心结论决策树就是一种模仿人类“层层提问、逐步排除”来做判断的机器学习模型它长得像一棵倒着的树最上面是起点一路往下问问题走到最底下得到结论什么是随机森林随机森林就是由很多棵决策树组成的——它是“决策树的集体会诊版”。 单棵决策树像一位医生独立下诊断水平不稳定还容易钻牛角尖过拟合随机森林请了几百位医生每位医生只看一部分病历随机抽样、只参考部分检查指标随机选特征最后大家投票取多数意见作为诊断结果个体可以有偏差但集体投票后的结论稳定得多二、如何训练第一棵决策树在这里我们使用sklearn 的方法数据集是官方自带的鸢尾花更多细节用法可以官网查询scikit-learn: machine learning in Python — scikit-learn 1.9.1 documentationsklearn三部曲model DecisionTreeClassifier(max_depth3, random_state42) # 第 1 步创建空壳 model.fit(X_tr, y_tr) # 第 2 步训练真正学习 model.score(X_te, y_te) # 第 3 步使用打分/预测在这里max_depth3表示深度意思是“问三个问题”即这棵树切了三刀它能问三层第一刀分出 setosa第二、三刀用花瓣宽度把剩下两类再切开测试代码展示from sklearn.datasets import load_iris from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier X, y load_iris(return_X_yTrue) X_tr, X_te, y_tr, y_te train_test_split( X, y, test_size0.3, random_state42 ) model DecisionTreeClassifier(max_depth3,random_state42) train_model model.fit(X_tr, y_tr) print(训练集准确率:, train_model.score(X_tr, y_tr)) print(测试集准确率:, train_model.score(X_te, y_te))测试结果为什么深度3测试集准确率为1深度1的时候测试集的正确率就降了许多因为这次测试的是鸢尾花数据集他是三分类标签学名中文名特点0setosa山鸢尾花瓣又短又小长 ~1.5cm、宽 ~0.25cm和其他两种隔得很远1versicolor变色鸢尾花瓣中等长 ~4.3cm、宽 ~1.3cm2virginica维吉尼亚鸢尾花瓣最大长 ~5.5cm、宽 ~2.0cm如果你是深度等于3那么模型就能分的清特征要是等于1等于1也被称为树桩只切一刀剩下特征模型分不清就会误判正确率就会下来那如果切两刀正确率会如何倘若只切两刀还是不能达到最好为什么呢--第一刀的“任务”是注定的。 建树时算法挑信息增益最大的一刀永远先是“花瓣长度 ≤ 2.45”——这一刀把 50 朵山鸢尾完整切出去这片叶子纯净了。此时深度预算用掉 1 层第二步剩下 100 朵混战只剩一刀可问 depth2 时混战区只能再问一个问题比如“花瓣宽度 ≤ 1.75 吗”问题是变色鸢尾和维吉尼亚鸢尾的花瓣宽度在1.0~1.8cm 之间是重叠的——一条阈值线切下去重叠带里的花必然有错判这片叶子永远不纯所以测试集卡在 0.93~0.96 附近上不去值得注意的是不是所有的测试集准确率都是1的之所以这次鸢尾花的数据集可以做到是因为太简单了遇到复杂的业务有噪声、有重叠测试的时候就会出现正确率落到 0.85~0.95 这个“更真实”的区间要是深度设为None结果又会怎么样--在这里由于没有噪声等干扰因素两者都是1训练集是巧合不是规律且这次数据集没有出现过拟合的情况下面会另外介绍。生活化类比一个学生把复习资料里的偏题怪题也全背了过拟合的苗头结果期末卷恰好没出怪题——考了满分你能说他“背怪题”是好策略吗不能只能说这次卷子配合换个爱出怪题的老师噪声大的真实数据。后面介绍再跑 depthNone 就会看到熟悉的剧本训练集逼近 1.0测试集明显往下掉——训练集和测试集的差距泛化间隙才是衡量过拟合的尺子三、深度与过拟合第一测试不同深度不同的结果测试代码from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier X, y make_classification( n_samples600, n_features6, n_informative4, n_redundant1, flip_y0.1, random_state42 ) X_tr, X_te, y_tr, y_te train_test_split( X, y, test_size0.3, random_state42 ) depths [1, 2, 3, 5, 10, None] max_depths [1, 2, 3, 5, 10, None] for max_depth in max_depths: model DecisionTreeClassifier(max_depthmax_depth, random_state42) train_model model.fit(X_tr, y_tr) print(fmax_depth{max_depth} 训练集准确率: {train_model.score(X_tr, y_tr)}) print(fmax_depth{max_depth} 测试集准确率: {train_model.score(X_te, y_te)})部分参数意思n_informative 表示有 4 个特征是有信息的其他 2 个特征是冗余的 flip_y0.1 表示 10% 的标签被随机翻转过——现实中数据永远不干净 n_redundant 表示有 1 个特征是冗余的即与目标变量没有直接关系测试结果哪一个最好呢--选模型的完整逻辑是三步顺序不能乱第一标准测试集或交叉验证分数—— 这是对“未来新数据表现”的直接估计depth10 得 0.800 depth5 得 0.761先赢一局。间隙只是第二位的体检项—— 它的作用是提醒你“这个 0.800 里有背题成分可信度要打折扣换批数据可能没这么漂亮” 它是个风险提示不是淘汰标准。分数接近时才轮到间隙说话—— 如果 depth5 和 depth10 考 0.795 vs 0.800差距在噪声范围内才按“简单优先”选 5。但 0.761 和 0.800 差了 4 个百分点不算接近为什么 depth5 间隙小反而不是好事看它的间隙是怎么来的depth5: 0.895 − 0.761 0.13 ← 训练分低把间隙压小了 depth10: 0.998 − 0.800 0.20 ← 训练分高把间隙撑大了depth5 的间隙小一部分原因是它学得本来就不够多——它没背那么多题但也因此漏掉了一些真实的细分规律。间隙小只保证“没背题”不保证“学得全”。从 5 到 10 多出来的那些分裂一部分确实在背噪声浪费但另一部分学到了真结构的细枝末节赚到了赚的比亏的多测试分就上去了深度为 None 时训练集准确率是1为什么它能这么高ax_depthNone解除了“提问次数”的限制而 sklearn 决策树的默认规则是“只要这片叶子还有杂类就继续切切到每片叶子纯净为止”。 预算无限 目标是切干净 必然把 420 个训练样本切到全对机制拆开看三步默认停止条件DecisionTreeClassifier建树时只有三种情况会停下——叶子纯净了gini0、样本太少min_samples_split兜底、或深度用完。前两个条件不设限的话它就会一直切。连续特征保证了“总能切下去”只要特征是连续数值如花瓣长度 4.2、4.3任何一片混杂的叶子里总能找到一个阈值把“异类样本”单独圈出来。哪怕最后一片叶子只剩 1 个样本它也必然是纯的。极限情况树一路切下去最坏就是每个样本独占一片叶子——相当于把整本习题册逐题背答案。150 或 420 个样本对它来说绰绰有余所以训练集正好 1.0第二测试random_state0的情况X, y make_classification( n_samples600, n_features6, n_informative4, n_redundant1, flip_y0.1, random_state0 ) X_tr, X_te, y_tr, y_te train_test_split( X, y, test_size0.3, random_state0 )改了两处种子结果自然变这次相比较还是深度为10好理由是--深度seed42 测试分seed0 测试分结论50.7610.833波动 ±0.04100.8000.800稳None0.7940.789稳切忌在写报告、文献等地方使用此方法——“挑种子”即反复换种子、只把最好看的那次结果写进汇报。这等于对测试集过拟合是数据造假的温和版。种子的作用是“固定实验条件”不是“美化实验结果”第三和随机森林做对比from sklearn.datasets import make_classification from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier from sklearn.ensemble import RandomForestClassifier X, y make_classification( n_samples600, n_features6, n_informative4, n_redundant1, flip_y0.1, random_state42 ) X_tr, X_te, y_tr, y_te train_test_split( X, y, test_size0.3, random_state42 ) TODO 1: 训练一棵不限制深度的单棵决策树打印测试集准确率作为基准 single_tree DecisionTreeClassifier(max_depthNone, random_state42) single_tree.fit(X_tr, y_tr) print(单棵决策树 测试集准确率:, single_tree.score(X_te, y_te)) TODO 2: n_trees [5, 50, 200] for n_estimators in n_trees: model RandomForestClassifier(n_estimatorsn_estimators, random_state42) model.fit(X_tr, y_tr) print(fn_estimators{n_estimators} 测试集准确率: {model.score(X_te, y_te)})得出的结论1.单棵树效果没有随机森林好2.数量不是越多越好会出现过拟合现象随机森林里的每棵树都用 max_depthNone默认不剪枝单棵时这样会过拟合为什么组队之后反而没那么容易过拟合了--单棵树时代“过拟合”是缺点进了森林“每棵树深到过拟合”反而是设计意图——因为森林要的不是“每棵树都对”而是“每棵树错得不一样”投票才能把错消掉为什么单棵树必须“深”--树的能力分两块偏差学得够不够透和方差换批数据结论抖不抖;深树 偏差低真规律学得干干净净连细枝末节都学到了 方差大噪声也背了不少换个划分结论就变。浅树正好反过来方差小但偏差大——很多真规律它根本没学到。森林的修正能力只能消方差消不了偏差--投票能消掉“背题”消不掉“没学到”--树的数量增加方差被越削越小但削的幅度是边际递减的方差理论上按 1/N 衰减且树与树并非完全独立削不完。所以实战经验50~200 棵后基本躺平树多解决“稳不稳”不解决“准不准”——再想准要靠调特征、调参数或换更强的模型如 XGBoostXGBoost简单介绍XGBoost 是“决策树的另一组建队方式”——随机森林是几百棵树各自独立、同时做题再投票并行集成学名 BaggingXGBoost 是几百棵树排队接力、后一棵专门修正前一棵的错误串行集成学名 Boosting四、决策树回归决策树除了可以直观的画图后面介绍之外也可以去进行预测。决策树回归的作用是“用一串 if-else 阈值直接预测连续数值”最适合有非线性突变、特征是表格、还想要可解释性的场景。 它单独出场不多更多是以“回归树”的身份当零件组成随机森林回归和 XGBoost。它擅长什么关系是“台阶式/突变式”而不是平滑曲线——真实业务里很多规律本来就是分段的ul电价阶梯用电量 0~200 度一个价、200~400 一个价峰谷电价不同时段电价直接跳变变压器负载率超 80% 后温升曲线突然变陡。这些“阈值行为”正是树的天生母语——线性回归拟合它反而要掰弯一条直线。特征混合类型数值电压、温度和类别设备型号、区域混在一张表里树不用做标准化、不用 one-hot 也能切。要给业务方解释输出的不是玄学系数而是“负载率 82% 且环境温度 35℃ → 预计寿命损耗 0.4%/年”一眼能审它的两大死穴不会外推训练数据里负荷最高见过 500MW预测明年夏天 600MW 的场景它只能给你输出 500 附近——树永远“台阶到底”趋势外推要靠线性类模型或做好特征变换。输出是台阶本质是“分段平均值”对平滑连续的物理规律如温度-损耗曲线拟合得粗。台阶到底什么意思树的预测从头到尾都是台阶连最后“越界外推”的地方也是平的永远不给斜线。 看图里橙色线的右端——越过数据边界后它不是顺着绿色曲线的趋势继续爬而是直接水平延长。因为树的输出规则是“每片叶子输出一个固定平均值”它没有“趋势”这个概念只有“档位”所谓“到底”就是从第一段到无穷远处全程都是水平段一段斜率都没有对平滑规律拟合得粗”怎么理解——用数字算一遍最直观假设真实的物理规律是温度每升高 1℃损耗精确增加 0.01这是条平滑直线。线性回归学到损耗 0.01 × 温度——25℃ 答 0.2525.5℃ 答 0.25526℃ 答 0.26每一个点都给不同的、连续的值误差可以趋近于零。回归树只能切成几段比如 25~27℃ 这一段统一答“0.26”段内三个温度的平均。结果26℃正好在段中间答 0.26误差 0 ✔25℃答 0.26多答了 0.01 ✘27℃答 0.26少答了 0.01 ✘段内除了中点两端全有误差——这就是图上红色虚线标的那截。想消除这些误差只有一条路把段切得无限窄 → 树无限深 → 又回到过拟合。所以面对平滑规律树模型的误差是结构性的不是它没学好而是“分段常数”这种表达形式天生描不平一条斜线代码展示import numpy as np from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeRegressor rng np.random.default_rng(42) X np.linspace(0, 10, 200).reshape(-1, 1) # 200 个 x 点 y np.sin(X).ravel() rng.normal(0, 0.1, 200) # sin 曲线 噪声 X_tr, X_te, y_tr, y_te train_test_split( X, y, test_size0.3, random_state42 ) TODO 1: 训练两棵回归树max_depth2 和 max_depth10 model_shallow DecisionTreeRegressor(max_depth2, random_state42) model_shallow.fit(X_tr, y_tr) model_deep DecisionTreeRegressor(max_depth10, random_state42) model_deep.fit(X_tr, y_tr) TODO 2: 均方误差MSE函数np.mean((y_true - y_pred) ** 2) def mse(y_true, y_pred): return np.mean((y_true - y_pred) ** 2) TODO 3: 分别打印两棵树在测试集上的 MSE print(浅树(depth2) 测试集 MSE:, mse(y_te, model_shallow.predict(X_te))) print(深树(depth10) 测试集 MSE:, mse(y_te, model_deep.predict(X_te))) TODO 4: 真实 y 的散点 两棵树的预测曲线 from matplotlib import pyplot as plt plt.rcParams[font.sans-serif] [Microsoft YaHei] # 修中文方框 关键用排好序的密集网格去预测而不是乱序的 X_te x_grid np.linspace(0, 10, 500).reshape(-1, 1) plt.figure(figsize(10, 5)) plt.scatter(X_te, y_te, s15, alpha0.5, label真实 y散点) plt.plot(x_grid, model_shallow.predict(x_grid), linewidth2, label浅树 depth2) plt.plot(x_grid, model_deep.predict(x_grid), linewidth2, label深树 depth10) plt.legend() plt.show()输出结果结论1. 数据的“真身”是正弦波 噪声蓝点沿一条平滑的 sin 曲线上下分布、带随机抖动——这就是“真实物理规律 测量噪声”的标准长相也是我们故意造的数据背景2. 浅树蓝线抓住了骨架丢掉了形状——欠拟合的实锤它只切了 4 大段0.15 / 0.8 / −0.65 / 0.48。你看它的山峰和谷底位置全对x≈1.5 附近高、x≈4.5 附近低但 sin 的圆滑起伏全没了x∈[3, 6] 的整个下坡直接一条水平线糊过。偏差大的模型就是这样大方向对细节全错——对应它 MSE 0.31是深树的近 3 倍3. 深树橙线贴着数据走但连“毛刺”也照抄——方差的实锤用几十级小台阶精确贴合散点但注意 x≈1.3 处它跟着一个孤立噪声点猛地冒尖、x≈9.5 之后跟着最后几个散点一路下坠到 −0.55这些扭动不是“规律”是在背噪声——它追的是每个点不是那条隐约的 sin 曲线4. 阶梯状的疏密 树的“档位细度”蓝线 4 级台阶、橙线几十级——这就是“深度预算”的直接可视化每多一层分裂x 轴被多切几刀台阶变小变密5. 两棵树都不会外推且深树把边缘噪声也背进去了图最左端 x0.2蓝线平在 0.15橙线怪异地翘到 −0.05那里几乎没有训练数据纯靠边缘叶子兜底图最右端 x9.8橙线还跟着最后几个点继续往下冲。训练数据没覆盖的区间树给出的不是预测是“最后一片叶子的复读”不会外推树的预测永远不会超出它见过的最大/最小答案——数据边界之外它只会“复读”不会“顺着趋势推”树在外推区会发生什么--图里面的区间是0~10倘若在10之外如10.5决策树10.5 落进“最右边那片叶子”该片叶子的平均值是 −0.55于是它回答 −0.55——哪怕 sin 曲线的真实趋势到那里应该回升它没有“斜率”“趋势”的概念只有叶子里的平均数所以出了边界就只能把最后一片叶子的值水平地复读下去。线性回归对比学到y 0.6x b这样的公式x10.5 就把 10.5 代进公式顺着趋势往外延伸——可能错趋势未必延续但至少“会推”在电力业务里这是大问题--电力预测最值钱的时刻恰恰是突破历史纪录的时刻极端高温日历史最高气温 39℃明天预报 42℃——负荷预测最需要准的就是这种天而树模型对“42℃”只能复读“39℃ 那片叶子”的值往往系统性低估恰恰是最不能低估的一天低估备电网负荷不足拉闸风险负荷自然增长今年园区新增产线明年用电整体抬升 15%——树模型学的是“历史范围里的形状”整体抬升它感知不到除非你把“用户数、产能”这些能表征增长的特征喂进去新能源场站扩容风电场装机从 100MW 扩到 150MW满发时刻功率早冲破历史值纯靠历史数据训练的树全部失灵把“趋势”翻译成特征喂给树——比如不直接用“气温”改用“气温对历史分位数的超越程度”不直接用负荷绝对值改用“负荷/装机容量”的比值。趋势被装进特征里树就不用自己外推了五、如何画决策树在这里还是使用sklearn方法鸢尾花数据集代码展示from matplotlib import pyplot as plt from sklearn.datasets import load_iris from sklearn.tree import DecisionTreeClassifier from sklearn.tree import plot_tree from sklearn.model_selection import train_test_split#拆分训练 / 测试集 from sklearn.metrics import classification_report#评估模型效果 plt.rcParams[font.family] SimHei plt.rcParams[axes.unicode_minus] False #导入鸢尾花数据集 data load_iris() #print(data) #导入特征值 x data[data] y data[target] #划分数据集 x_train,x_test,y_train,y_test train_test_split(x,y,train_size0.8,random_state42) #训练决策树 dt DecisionTreeClassifier( max_depth3, min_samples_leaf2, min_samples_split5, max_leaf_nodes10 ) dt.fit(x_train,y_train) #评估模型和可视化 y_pre dt.predict(x_test) print(classification_report(y_test,y_pre)) #画决策树 fig plt.figure(figsize(10,6)) plot_tree( dt, feature_namesdata[feature_names], class_namesdata[target_names], filledTrue ) plt.show()输出怎么理解--以根节点为例行内容含义petal width ≤ 0.8提问这一刀的判据False 走右True 走左gini 0.667混乱度0纯净0.667三类最混的极限值samples 120样本量到达此节点的训练样本数value [40, 41, 39]构成三类各多少朵加起来120守恒class versicolor当前结论如果在此强停输出多数类颜色是一门语言色相类别深浅纯度橙setosa、绿versicolor、紫virginica颜色越深、越饱和gini 越接近 0发白的节点如 samples2 那个gini0.5就是“混战区”看 gini 数值不用算对表就知道这个节点“有多混”白色怎么理解--在这里,白色 “没有主导类别”的节点——三种颜色类别掺在一起互相稀释谁也压不住谁最后只剩一片白颜色编码的完整规则两条色调什么颜色由节点的多数类决定——多数是 versicolor 就偏绿多数是 virginica 就偏紫饱和度多深多浅由主导程度决定——value 里某类一家独大 → 颜色浓三类或两类势均力敌 → 颜色被“稀释”成发白。所以白色的准确含义是这个节点里没有哪类样本占明显优势是“混战区”——对应 gini 高、对应“在此强停则预测基本靠蒙”那这部分怎么样是不是不行分到后面还是分不清还有参杂这两个白叶子就是整棵树的“能力死角”——树用完了 3 层提问预算还是切不干净只能带着 50% 的含混率交卷。 但“不行”要分两层说作为“预测可信度”它确实不行作为“模型的诚实表现”它反而是合理的但是它们“闯祸的能力”其实很小这两个叶子只蓄了 10/120 ≈ 8% 的训练样本而且是最边缘的样本。整棵树 92% 的流量都流进了 4 片深色纯叶评估一个坏叶子要同时看它的“含混率”和“接客量”gini0.5 很糟但 samples2/8 很小影响面有限真正危险的是“又大又白”的叶子——那种才是要连夜修实战中怎么处理这种“分不清”?业务上不会硬猜而是让模型说出“我没把握”proba dt.predict_proba([[5.0, 3.5, 4.5, 1.5]]) # 若输出类似 [0, 0.5, 0.5] → 落进白叶子置信度只有 50%规则最大概率低于阈值如 0.7的预测不自动下结论转人工复核;对应到反窃电场景就是模型对 900 个用户自动判定剩下 100 个“两头不像”的标记为“存疑”交给稽查人员现场核查——这比硬给 1000 个答案专业得多六、特征的重要性四层目的目的 1表层掌握feature_importances_这个高频工具它是随机森林/决策树最有实用价值的“附加产出”——训练白送不用额外花钱未来写任何 项目的结题报告“哪些因素最关键”这张排名表几乎是必配图目的 2业务层练习“把模型输出翻译成业务语言”模型吐出0.35 接头温度要能写成“建议巡检优先核查接头测温异常的台区”模型负责算工程师负责“翻译落措施”目的 3思维层“重要” ≠ “因果”重要性衡量的是“模型分类时多用它”不是“改变它能改变结果”--相关但非因果某地区“冰激凌销量”对“溺水人数”预测很重要都是夏天的影子但禁售冰激凌救不了溺水的人电力版假设“运行年限”重要性很高——它可能只是“老化故障”的影子真正该做的也许是换掉老化设备因果而不是盯着年限数字做文章更隐蔽的坑两个高度相关的特征比如“接头温度”和“负载率”——负载高导致温度高模型可能把重要性随机分给其中一个你据此做决策就会打偏目的 4稳健层重要性是“参考”不是“定论”把 200 棵树改成 20 棵排名大概率会抖——因为重要性是“几百次随机划分的平均统计量”样本少、树少时它不稳定业务上对应的安全规范特征重要性只用于“生成核查优先级假设”最终措施要用业务逻辑和实地验证来确认代码展示from sklearn.datasets import make_classification# 生成分类数据集 from sklearn.model_selection import train_test_split# 划分数据集 from sklearn.ensemble import RandomForestClassifier# 随机森林分类器 特征名假设是 6 个电力监测指标 feature_names [电压偏差, 谐波畸变率, 三相不平衡, 接头温度, 负载率, 运行年限] X, y make_classification( n_samples800, n_features6, n_informative4, n_redundant1, weights[0.7, 0.3], flip_y0.08, random_state7 ) X_tr, X_te, y_tr, y_te train_test_split( X, y, test_size0.3, random_state42 ) TODO 1: 训练随机森林n_estimators200, random_state42打印测试集准确率 model RandomForestClassifier(n_estimators200, random_state42) model.fit(X_tr, y_tr) print(测试集准确率:, model.score(X_te, y_te)) TODO 2: 打印每个特征的 importance并按从大到小排序输出 for importance, feature in sorted(zip(model.feature_importances_, feature_names), reverseTrue): print(f{importance:.2f} {feature}) TODO 3: 把 n_estimators 改成 20 再跑一遍 model1 RandomForestClassifier(n_estimators20, random_state42) model1.fit(X_tr, y_tr) print(把 n_estimators 改成 20 再跑一遍;测试集准确率:, model1.score(X_te, y_te))测试结果问题一为什么接头温度排第一而负载率排第三--负载率和接头温度的关系一对“因果孪生指标”;物理因果链负载率↑ → 负荷电流↑ → 发热按 I²R 放大 → 接头温度↑。两者高度相关、信息重叠(n_redundant1,有 1 个特征是有效特征的线性组合比如“接头温度”≈ a×负载率 b它信息重复但和目标是有关系的只是不提供新信息)模拟电力监测里“测了温度就不用再测负载也能猜个八九不离十”的真实情况问题二为什么负载率只排第三0.14还和三相不平衡打平冗余摊薄它和接头温度信息重叠模型“知道负载率了就不太需要温度、知道温度了就不太需要负载”重要性在孪生特征间分配时接头温度信息更纯、可能非线性关系更强拿走了 0.33 的大头负载率只剩残羹。孪生指标注定有一个要“让贤”名字是随机贴的诚实声明合成数据里哪个匿名特征强是随机决定的给特征起名时不掌握真相所以“负载率凭什么是第三”在合成数据里没有物理答案——重要性由数据决定不由名字和直觉决定0.14 的“第三”和 0.11 的“第五”未必是真实差距在 8% 标签噪声 单次划分下中间这几名的排序本来就有抖动别过度解读 0.14 vs 0.11问题三为什么树少20 棵准确率反而低同一套数据、同一个划分200 棵 0.8917 vs 20 棵 0.8792掉了 1.2 个百分点——这就是“树数量影响方差削减”的直接证据一个特征重要是否等于它和结果有因果关系--不等于。“重要”只说明“模型预测时很依赖它”预测功劳“有因果”说明“改变它会改变结果”干预有效——两者是两码事七、综合场景供电局反窃电嫌疑用户识别某供电局营销部门想用机器学习从用电数据里筛出窃电嫌疑用户数据共 1000 个用户5 个特征月均用电量(kWh) —— 窃电户往往异常偏低深夜用电占比(0~1) —— 窃电户常在深夜大功率用电电压合格率(0~1) —— 窃电/私拉电线会导致电压质量下降历史告警次数 —— 计量装置异常告警越多越可疑台区线损关联度(0~1) —— 与所在台区线损异常的相关程度标签0 正常用户1 窃电嫌疑用户代码展示import numpy as np from sklearn.model_selection import train_test_split from sklearn.tree import DecisionTreeClassifier from sklearn.ensemble import RandomForestClassifier feature_names [月均用电量, 深夜用电占比, 电压合格率, 历史告警次数, 线损关联度] rng np.random.default_rng(2026) def make_group(n, usage, night, volt, alarm, loss): data np.column_stack([ rng.normal(usage, usage * 0.3, n), np.clip(rng.normal(night, night * 0.4, n), 0, 1), np.clip(rng.normal(volt, 0.02, n), 0.8, 1.0), np.round(np.clip(rng.normal(alarm, 1.2, n), 0, 12)), np.clip(rng.normal(loss, loss * 0.3, n), 0, 1), ]) return data normal make_group(800, usage320, night0.10, volt0.98, alarm1, loss0.04) thief make_group(200, usage130, night0.45, volt0.90, alarm5, loss0.12) X np.vstack([normal, thief]) y np.array([0] * 800 [1] * 200) -------------------------------------- TODO 1: 划分训练/测试集 X_tr, X_te, y_tr, y_te train_test_split( X, y, test_size0.3, random_state42, stratifyy ) #stratifyy 保证训练集和测试集里正常:嫌疑都保持 4:1避免测试集偶然偏斜比如嫌疑用户全挤进训练集导致评估失真。 TODO 2训练决策树 tree_model DecisionTreeClassifier(max_depth4, random_state42) tree_model.fit(X_tr, y_tr) print(决策树测试集准确率:, tree_model.score(X_te, y_te)) TODO 3: 训练随机森林 forest_model RandomForestClassifier(n_estimators100, random_state42) forest_model.fit(X_tr, y_tr) print(随机森林测试集准确率:, forest_model.score(X_te, y_te)) TODO 4: 写循环让决策树 max_depth 从 1 取到 10 best_depth, best_acc None, 0.0 for depth in range(1, 11): m DecisionTreeClassifier(max_depthdepth, random_state42) m.fit(X_tr, y_tr) acc m.score(X_te, y_te) print(fmax_depth{depth}: 测试集准确率 {acc:.4f}) if acc best_acc: best_acc, best_depth acc, depth print(f\n最优深度: {best_depth}测试集准确率: {best_acc:.4f}) TODO 5: print(\n特征重要性排名从高到低:) for importance, name in sorted(zip(forest_model.feature_importances_, feature_names), reverseTrue): print(f{importance:.2f} {name}) TODO 6: 现有 3 个新用户待判定数据如下顺序与 feature_names 一致 用户 A: [290, 0.12, 0.97, 1, 0.05] 用户 B: [150, 0.52, 0.88, 6, 0.15] 用户 C: [310, 0.48, 0.95, 0, 0.03] (a) 用训练好的随机森林逐一预测 (b) 用户 C 比较特殊用电量正常、电压正常但深夜用电占比很高。 结合特征重要性分析模型为什么这么判作为人工复核者是否同意。 new_users np.array([ [290, 0.12, 0.97, 1, 0.05], [150, 0.52, 0.88, 6, 0.15], [310, 0.48, 0.95, 0, 0.03], ]) preds forest_model.predict(new_users) confidences forest_model.predict_proba(new_users).max(axis1) for name, p, conf in zip([用户A, 用户B, 用户C], preds, confidences): label 窃电嫌疑 if p 1 else 正常 print(f{name}: 判定{label}置信度 {conf:.2f})输出结果通过特征重要性排名可以优先核查什么--电压合格率异常 深夜用电占比异常的用户理由电压合格率是窃电最直接的物理后果——绕表、私拉线路会拉低台区电压质量仪表一测便知深夜占比是行为证据而月均用电量低反而几乎没贡献0.01一是正常用户用电量本身波动很大±30%区分度弱二是被更早分裂的电压/深夜特征抢了功劳掩蔽效应——所以业务直觉里的头号指标未必是模型眼里的关键新用户ABC里面为什么用户C是正常但是置信度55%----是典型的特征打架样本结论1.电压合格率0.37是模型眼里的头号指标月均用电量0.01几乎为零——和业务直觉用电量低窃电相反两个解释正常用户用电量本身波动 ±30%区分度弱且电压/深夜特征先分裂把它的功劳“掩蔽”了。教训业务直觉的关键指标 ≠ 模型的关键特征排名信数据、措施靠机理2.用户 C 是“人工复核”案例置信度 0.55深夜占比 0.48 指向嫌疑但电压、告警、线损全指向正常特征打架。正确业务动作不列嫌疑可能有夜班工厂的正常解释但转现场核实3.整体 99.7%~100% 的准确率说明这是一套“干净的合成数据”真实反窃电稽查的准确率远没这么漂亮嫌疑用户会伪装这段代码的价值在于流程骨架——划分→对比→选型→解释→复核