
简介面向电力系统短期负荷预测场景的CatBoost算法应用研究文档适合电网调度、能源互联网方向的研究生及相关工程技术人员。文档针对传统时间序列法仅依赖时间因素、神经网络与灰色模型等超参数多、对算力要求高的痛点系统梳理了CatBoost基于GBDT机器学习框架的实现原理包括Boosting集成思想、梯度提升算法中损失函数梯度下降与基学习器加权组合的数学推导过程并给出了短期负荷预测中历史负荷、日期类型、天气数据等典型特征选取思路。全文从引言、短期负荷预测概述、CatBoost预测算法分析到实验验证逐步推进不仅解释了负荷变化作为非平稳随机过程的特性还通过预测实验验证算法精度说明其能提升电网调度可靠性与经济性。资源包共1个docx文件大小约276KB结构完整可直接阅读引用。已有115人次学习适合作为短期负荷预测课题、算法对比或课程论文的参考资料。1. CatBoost做电力短期负荷预测从 4% 误差压到 3% 以内差在哪CatBoost做电力短期负荷预测第一眼看起来就是换个梯度提升库的事但实际走一遍你会发现同一份数据XGBoost和LightGBM不调参MAPE在4%上下CatBoost把类别特征和时间特征喂对了之后能压到3%以内差距全在处理方式上。这套流程解决的是电力调度里最常见的场景用历史负荷、温度、节假日信息预测未来24小时逐点负荷适合电网调度、售电公司、园区微电网这几类角色。跟深度学习方案比它的落地成本低很多——不需要GPU、不需要构造长序列一份带时间戳的表格数据就能跑而且能解释每个时段预测依据。2. 负荷数据预处理与特征工程先把时间特征拆对再谈算法CatBoost这类梯度提升算法对特征工程的要求不算苛刻但电力负荷数据有自己的脾气强周期性、强自相关、温度敏感、节假日突变。数据不进特征工程之前模型再强也是空转。这一章我按实际处理顺序拆开讲每一步都是踩过坑之后留下来比较稳的做法。2.1 数据清洗异常尖峰与缺失值怎么处理负荷数据最常见的脏数据有两种一是采集终端故障导致的连续零值或恒定值二是检修、限电造成的单点突变。前者直接污染滞后特征后者会让模型学到错误的尖峰模式。我处理的第一步是先把时间列转成标准格式并按时间排序然后对负荷列做分位数筛查超出正常范围的标记为缺失值。import pandas as pd import numpy as np df pd.read_csv(load_data.csv, parse_dates[time]) df df.sort_values(time).reset_index(dropTrue) # 负荷值合理性筛查超出 [Q1 - 4*IQR, Q3 4*IQR] 视为异常 q1, q3 df[load].quantile(0.25), df[load].quantile(0.75) iqr q3 - q1 lower, upper q1 - 4 * iqr, q3 4 * iqr df.loc[df[load] lower, load] np.nan df.loc[df[load] upper, load] np.nan # 缺失值与异常值用前后 24 点中位数填补 df[load] df[load].fillna( df[load].rolling(24, centerTrue, min_periods1).median() ) df df.dropna().reset_index(dropTrue)这段代码核心是两个操作先用 4 倍 IQR 做粗筛把明显不合理的点置空再用窗口 24 的中位数插补。为什么用中位数不是均值负荷曲线在早晚高峰处变化快均值容易把尖峰磨平中位数对局部突变更稳。rolling(24, centerTrue)表示取当前点前后各 12 个点的中位数min_periods1保证边界处也有输出。这里要提醒如果一段连续缺失超过 2 小时直接丢弃比插补更好连续插补出来的数据会让模型误以为那段曲线是真实存在的规律。2.2 滞后特征与滚动统计24 小时周期和 168 小时周期怎么用满电力负荷的自相关性极强今天上午 10 点的负荷和昨天上午 10 点的负荷高度相关和上周同一天同一时刻也强相关。这就是滞后特征的根本逻辑。我常用的滞后项是 lag_1、lag_2、lag_24、lag_168分别对应上一小时、前两小时、昨天同一时刻、上周同一时刻。df[lag_1] df[load].shift(1) df[lag_2] df[load].shift(2) df[lag_24] df[load].shift(24) df[lag_168] df[load].shift(168) # 滚动统计用过去 24 小时均值、过去 3 小时均值刻画趋势 df[rolling_mean_24] df[load].shift(1).rolling(24).mean() df[rolling_mean_3] df[load].shift(1).rolling(3).mean() df[rolling_std_24] df[load].shift(1).rolling(24).std() df df.dropna().reset_index(dropTrue)这里的shift(1)很关键预测 t 时刻时滞后特征只能用 t 之前的数据shift(1)保证不会把当前时刻的负荷泄露给模型。滚动统计同样要用shift(1)后再 rolling我见过不少人在这一步翻车——直接对当前行做 rolling训练时验证集表现异常好线上预测立刻露馅。rolling_mean_24刻画的是过去一天的整体用电水平rolling_mean_3刻画短时趋势两个窗口配合能让模型区分「温度上升导致的负荷爬坡」和「短时设备启停导致的波动」。2.3 cat_features 的正确传法时刻、星期、节假日为什么要走原生类别特征CatBoost 名字里的 Cat 就是 categorical它原生支持类别特征不需要手动 one-hot。电力负荷数据里hour、weekday、month 这类时间成分从数值上看是 0 到 23、0 到 6但本质是类别直接用数值喂给模型会让模型错误地认为 23 点距离 0 点很远、周三距离周二很近。CatBoost 内置的 ordered target statistics 会按类别出现的顺序做统计编码比 one-hot 更省维度也比普通 target encoding 抗过拟合。cat_cols [hour, weekday, month, is_holiday] for col in cat_cols: df[col] df[col].astype(str) # 构造完成后需要保留类别特征列名 print(df[cat_cols].dtypes)类别特征建议先转成字符串再传给 CatBoost原因是整数类型容易被当成数值特征处理转成字符串可以避免这个歧义。is_holiday 这种 0/1 特征本身可以走数值但我习惯把它也放进 cat_features因为节假日对负荷的影响不是线性的类别化处理更符合业务直觉。温度特征保持数值型不要放进 cat_features否则模型会把每个温度值都当成独立类别特征维度爆炸且无泛化能力。3. CatBoost 参数配置与时序交叉验证从默认参数到可复现特征工程做完模型选择的优势才开始显现。CatBoost 在中小规模表格数据上表现稳定它的对称树结构和 ordered boosting 机制能减少预测偏移对带噪声的负荷数据有天然的抗过拟合能力。这一章讲参数怎么定、验证怎么做、模型怎么存给一组可以直接套用的基准配置。3.1 核心参数详解与一组能用的基准配置CatBoost 参数多但电力负荷预测场景里真正需要手工调的核心参数不超过七个。迭代轮数决定模型上限深度和学习率决定收敛效率L2 正则和早停决定过拟合程度随机种子决定可复现性。参数作用常用范围备注iterations最大训练轮数500 ~ 2000配合早停使用不是越大越好depth对称树深度4 ~ 8电力数据 6 以下比较稳learning_rate学习率0.03 ~ 0.1小学习率配大迭代轮数l2_leaf_reg叶子节点 L2 正则1 ~ 10控制过拟合最有效的旋钮od_type早停方式Iter验证集连续无提升即停od_wait早停容忍轮数80 ~ 150防止验证集抖动导致提前停random_seed随机种子固定值保证每次实验可复现eval_metric验证指标MAPE与业务误差口径一致这里重点说 depth 和剪枝的关系。CatBoost 的树是对称树每层所有叶子做相同分裂模型表达能力比 XGBoost 的同深度树弱一些但泛化更好。我见过有人把 depth 调到 10训练集 MAPE 降到 0.5%验证集却升到 5%典型的过拟合。电力负荷数据虽然样本量大但周期性规律并不需要极深树来捕捉depth 控制在 4 到 6配合 l2_leaf_reg 更实用。3.2 时序交叉验证TimeSeriesSplit 为什么不能偷懒用 K 折不少人在负荷预测里直接套用 sklearn 的 KFold这是最容易犯又最难发现的错误。K 折随机打乱样本会把 1 月的测试样本混进 6 月的训练集里模型在时间上「穿越」了——它见过未来数据验证集分数自然漂亮上线后立刻原形毕露。时序数据必须用 TimeSeriesSplit严格按时间顺序切分训练集永远在验证集之前。from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits5, gap24) for fold, (train_idx, val_idx) in enumerate(tscv.split(df)): train_fold df.iloc[train_idx] val_fold df.iloc[val_idx] print(fFold {fold 1}: train {train_fold[time].min()} ~ {train_fold[time].max()}, fval {val_fold[time].min()} ~ {val_fold[time].max()})gap24这个参数容易被忽略。TimeSeriesSplit 默认 gap0意味着上一折最后一条训练样本和下一折第一条验证样本是相邻时刻而滞后特征用的是 shift(1) 之后的数据相邻时刻的负荷值高度相关验证集被严重污染。我统一设 gap24也就是在训练集和验证集之间留出 24 小时的空窗类似工业上的「等待真实值落地」逻辑。如果你的数据粒度是 15 分钟gap 就得按一天 96 个采样点来设。3.3 训练与模型保存一份能直接跑的 CatBoost 训练代码参数和验证框架定下来后训练代码本身很薄。CatBoostRegressor 的接口和 sklearn 兼容但有几个细节必须注意类别特征列表要在训练和验证时都传入Pool 对象能缓存特征统计信息模型保存格式用 cbm。训练过程中我最看重的是 validate 集合上 MAPE 的收敛曲线它会明确告诉你模型是欠拟合还是过拟合。from catboost import CatBoostRegressor X df.drop(columns[load, time]) y df[load] model CatBoostRegressor( iterations2000, depth6, learning_rate0.05, l2_leaf_reg5, loss_functionRMSE, eval_metricMAPE, od_typeIter, od_wait100, random_seed2024, verbose100 ) model.fit( X.iloc[train_idx], y.iloc[train_idx], eval_set(X.iloc[val_idx], y.iloc[val_idx]), cat_featurescat_cols, use_best_modelTrue ) model.save_model(catboost_load_forecast.cbm)use_best_modelTrue意味着最终保存的是验证集上 MAPE 最优的那棵树而不是最后一棵树这个开关必须开。损失函数选 RMSE 而不是 MAPE 的原因MAPE 在负荷接近零点时会产生极端值用 RMSE 做训练目标更平稳但验证指标用 MAPE符合业务汇报习惯。eval_set 里传入的是时序验证集的真实数据CatBoost 会在每一轮迭代后计算一次 MAPEod_wait100 表示连续 100 轮没有改善就停止训练实际训练中常在 600 到 900 轮收敛。4. 基于 CatBoost 的短期负荷预测实战完整流程与误差分析前面两章把特征和参数分别讲透了这一章把它们串成一条完整可跑的链路。数据流、切分方案、训练预测、误差拆解照着跑就能得出第一版结果。重点看两个东西一是 MAPE 按小时拆分后哪里差二是误差的分布形状是否健康。4.1 数据准备与训练/验证切分训练集和验证集的比例我通常按时间切前 70% 训练、后 30% 验证不再额外抽测试集。原因很简单负荷预测的评估目的是看模型在未知未来上的表现按时间先后切出的验证集最接近线上真实场景。如果数据横跨一年以上验证集最好包含完整四季这样温度变化对负荷的影响才能被充分评估。split_idx int(len(df) * 0.7) train_df df.iloc[:split_idx].reset_index(dropTrue) val_df df.iloc[split_idx:].reset_index(dropTrue) X_train train_df.drop(columns[load, time]) y_train train_df[load] X_val val_df.drop(columns[load, time]) y_val val_df[load] print(fTrain: {train_df[time].min()} - {train_df[time].max()}, rows{len(train_df)}) print(fVal: {val_df[time].min()} - {val_df[time].max()}, rows{len(val_df)})这个切分看起来简单核心问题在于滞后特征在切分边界处的连续性。如果 val_df 从 t0 开始lag_24 需要用到 t0 之前 24 小时的负荷值而 train_df 的最后一条记录只到 t0 前 1 小时特征已经存在所以不需要额外拼接历史数据。真正要注意的是滚动统计的窗口rolling_mean_24在数据边界处会因 min_periods 默认值出现 NaN切分后记得检查验证集前几行是否有空值。4.2 模型训练与预测模型训练复用上一章的参数配置训练完成后直接对验证集做预测同时输出预测值和真实值的对比表方便后续误差分析。model.fit( X_train, y_train, eval_set(X_val, y_val), cat_featurescat_cols, use_best_modelTrue ) val_df[pred] model.predict(X_val) # 计算逐点相对误差并落表 val_df[abs_err] np.abs(val_df[pred] - val_df[load]) val_df[rel_err] val_df[abs_err] / val_df[load] * 100 val_df.to_csv(val_predictions.csv, indexFalse)我的习惯是在验证集里保留 time 列predict 之后马上落表。这样后续做误差分析不需要回源查数据直接把 val_predictions.csv 拖进任何分析工具都能看。预测完成后第一件事不是看整体 MAPE而是看误差有没有集中在某个时段——如果早高峰 8 点到 10 点的误差明显偏高模型大概率没学好温度变化对负荷的拉升作用这时优先检查特征而不是调参。4.3 误差评估与结果可视化整体 MAPE 只是第一层指标电力负荷预测必须做按时段拆解的误差评估。我的做法是把一天按 4 个时段拆分深谷时段 0 到 6 点、早高峰 7 到 10 点、平段 11 到 17 点、晚高峰 18 到 23 点分别计算 MAPE。这样做的好处是能立刻看出模型在哪类时段表现差跟业务方聊的时候也更有针对性。def calc_mape(y_true, y_pred): return np.mean(np.abs((y_true - y_pred) / y_true)) * 100 val_df[period] pd.cut( val_df[time].dt.hour, bins[0, 7, 11, 18, 24], labels[deep_night, morning_peak, flat, evening_peak], rightFalse ) summary val_df.groupby(period, observedTrue).apply( lambda g: pd.Series({ mape: calc_mape(g[load], g[pred]), mean_load: g[load].mean(), max_err: g[rel_err].max() }) ) print(summary.round(2))这个时段拆分逻辑来自电网调度习惯深谷时段负荷低绝对误差虽小但相对误差容易被放大早晚高峰负荷高误差直接影响调度决策。pd.cut的bins要覆盖全天 24 小时rightFalse保证 7 点归入 morning_peak 而不是 flat。同一组数据跑完我期望的结果是整体 MAPE 在 3% 以内早高峰和晚高峰的时段 MAPE 不低于整体 MAPE 太多——如果晚高峰 MAPE 超过整体两倍就要查温度特征和滞后窗口的匹配度。5. 避坑与常见问题特征泄漏、时序穿越与调参翻车实录这一章是从实际运行里沉淀出来的踩坑记录。每条都是「现象 → 原因 → 解决」的完整链路按严重程度排序越靠前的越隐蔽也越致命。5.1 验证集 MAPE 低到离谱线上却高出 4 倍现象验证集 MAPE 只有 1.2%模型上线后实际预测误差飙到 5% 以上业务方直接质疑数据造假。原因滞后特征构造时用了shift(0)而不是shift(1)模型在训练时看到了当前时刻的真实负荷值。这类泄漏在验证集上表现极好因为验证集同样包含被泄漏的特征只有到了线上没有当前时刻真实值才暴露。解决所有滞后特征和滚动统计一律先shift(1)再做训练前打印训练集和验证集各自的特征列核对特征里是否包含load本身或由load直接派生且未经 shift 的列。5.2 随机 K 折验证的 MAPE 比时序验证好看得多现象同一份数据、同一个模型KFold 验证 MAPE 2.0%TimeSeriesSplit 验证 MAPE 3.5%两者相差很大。原因随机 K 折把未来样本混进了训练集模型见过验证时段附近的负荷走势时间上穿越了。解决切换到带gap24的 TimeSeriesSplit以时序验证结果为准。我在项目里发现一个规律如果 KFold 和 TimeSeriesSplit 的 MAPE 差超过 1 个百分点基本可以断定建模流程里存在时间泄漏需要逐特征排查。5.3 depth10 训练集 MAPE 0.4%验证集 MAPE 5%现象调大 depth 后训练集误差快速下降验证集误差反而升高训练曲线和验证曲线开始分叉。原因对称树深度过大模型把训练集里的噪声当成规律硬记下来。电力负荷数据受温度、偶发事件影响本身含噪过深的树很容易把这些随机波动学进叶子节点。解决depth 回退到 6同时把l2_leaf_reg从 3 调到 5。先把 depth 降下来看验证集 MAPE 是否回落再用 l2_leaf_reg 微调。记住一个判断标准训练集和验证集 MAPE 差距大于 1.5 个百分点先砍树深再谈其他。5.4 类别特征传错列把温度放进 cat_features 导致模型完全失效现象训练完成后特征重要性里温度排第一但验证集 MAPE 异常高预测曲线几乎是一条平线。原因温度是连续数值放进 cat_features 后 CatBoost 把每个温度值都看作独立类别模型看到的特征维度急剧膨胀且毫无泛化意义相当于把回归问题退化成查表。解决检查cat_features列表确保只包含 hour、weekday、month、is_holiday 这类离散特征。有个简单的自检方法打印model.get_feature_importance()如果温度的重要性异常高超过总重要性的 50%第一反应是先查类别特征配置而不是怀疑温度本身。5.5 节假日处理不当长假期间预测误差整体抬升现象整体 MAPE 在 3% 以内但春节、国庆期间时段 MAPE 逼近 10%模型在节假日完全失灵。原因is_holiday 只有 0/1 两态模型无法区分春节这种连续 7 天的长假和普通周末。长假期间负荷曲线整体下移且逐日变化规律与日常工作周完全不同单一节假日标签不足以刻画这种模式。解决把 is_holiday 扩展成 holiday_type正常工作日编码为 0、周末编码为 1、长假第一天编码为 2、长假中间日编码为 3、长假最后一天编码为 4、节前一天编码为 5。这个特征作为类别型传入后模型能区分长假开始前的负荷爬坡和结束后的恢复性反弹。6. 进阶SHAP 解释与多模型对比让预测结果真正可用模型跑通只是第一步电力负荷预测交付给调度侧时必须回答「为什么预测这个值」。这一章用 SHAP 解释模型决策同时给出与深度学习模型的对比方法帮你在方案评审时有据可依。SHAP 值能把 CatBoost 的每次预测拆解成特征贡献度定位具体时段高预测的原因。用 shap 库的 TreeExplainer 配合训练好的模型直接对验证集计算解释值。import shap explainer shap.TreeExplainer(model) shap_values explainer.shap_values(X_val) shap.summary_plot(shap_values, X_val, max_display15)max_display15控制图中显示的特征数量电力负荷场景下我只看重要性排序前 10 的特征。summary_plot 中横轴是 SHAP 值正值表示该特征把预测值往上推。实际使用中我发现lag_168 在多数时段贡献最高这印证了周周期在负荷预测里的核心作用在温度异常的夏季午后温度特征的 SHAP 值明显放大说明模型正确捕捉到了空调负荷的驱动关系。除了 summary_plot我建议加一张按小时拆分的 SHAP 热力图可以更直观地看到同一特征在不同时段的方向性差异。跟深度学习方案对比也是避不开的环节尤其当业务方提出「为什么不用 LSTM」时。我的对比方式是LSTM 用过去 168 小时负荷序列预测未来 1 小时CatBoost 用本文的特征工程方案两者在同一份验证集上评估。# LSTM 侧对比要点 # 输入: X_seq shape (n_samples, 168, 1) # 模型: LSTM(64) - Dense(1) # 评估: 同样计算整体 MAPE 和分时段 MAPE对比结果通常是 CatBoost 在整体 MAPE 上略优或打平但训练时间差一个数量级LSTM 需要调序列长度和归一化方式而 CatBoost 无需对特征做标准化。这背后的原因是时序场景里滞后特征的表达能力已经很强序列模型的优势只有在数据量极大、非线性模式复杂时才能体现出来。从那以后我每次做负荷预测实验都强制走一遍完整流程先检查特征是否含未来信息再确认验证集严格按时间切分最后才看参数和模型对比这套习惯帮我挡掉了不少线上翻车的可能性。希望帮到你。本文还有配套的精品资源点击获取