
简介面向电力系统调度与能源市场分析的MATLAB短期电力负荷/价格预测完整示例演示如何利用温度预报、假期信息与历史负荷数据构建高度准确的日前预测适合具备一定MATLAB基础的开发者学习和复现。压缩包共73个文件约31.77MB涵盖40个txt格式数据与说明、16个xls表格含历史负荷与Excel前端、14个m源码文件、2个pdf文档及license授权文件目录结构清晰便于对照学习。目前已有369人学习下载。资源实现了神经网络与袋装回归树两种非线性回归模型并基于NEPOOL区域2004—2007年小时级数据训练、2008年样本外验证平均预测误差约1%-2%。除完整MATLAB脚本和数据外还提供发布报告和Excel前端使用者可直接调用由MATLAB部署的DLL构建预测流程既能用于复现论文/案例也可作为电力预测项目起步模板。1. 为什么这套 MATLAB 负荷预测案例值得逐行拆开某省级调度的值班日志里负荷预测误差每抬高 1 个百分点备用容量的购买量就要上调 2-3%。ISO New England 的公开数据里同样存在这个问题。这个案例用 2004-2007 年 NEPOOL 区的小时级负荷、温度与节假日数据在 MATLAB 里建了两套非线性回归模型——神经网络与袋装回归树——并在 2008 年样本外数据上把日前预测平均误差压到 1-2%。对正在做电力市场、微电网能量管理或售电公司负荷预测的工程师来说它最值钱的部分不是最终精度而是整套流程数据怎么清洗、特征怎么生成、模型怎么对比、结果怎么部署成 Excel 能调用的 DLL。这篇拆解按这个顺序还原。2. 数据底板从 EEI 小时记录到预测特征矩阵的完整管线2.1 先看清 NEPOOL 原始数据的结构NEPOOLISO New England 运营的电力池的小时级负荷与价格数据存放在Datasets/System-Loads-in-EEI-format目录。EEI 格式由 Edison Electric Institute 定义文件内按行记录每个小时区间的观测但具体列顺序在不同发布版本里并不一致常见的是日期、小时序号、负荷MW、温度°F四列也可能混入分区字段。导入时最容易出错的就是列映射——把温度列当负荷列读进来后续所有特征全错且训练时不会报错只在误差分析阶段暴露问题。所以案例里的importData.m被设计成集中解析入口后续gen_predictors.m、LoadScriptNN.m都只认它输出的统一格式。function [time, load, temp] importData(filename) % importData: 解析EEI格式的小时级负荷/温度文件 % 输出统一时间戳time(datenum)、系统负荷load(MW)、区域温度temp(°F) raw readtable(filename, PreserveVariableNames, true); % 先打印 raw.Properties.VariableNames 确认列映射 time datenum(raw.Date, yyyy-mm-dd) (raw.Hour - 1) / 24; load raw.Load_MW; temp raw.Temp_F; end这里两处细节值得注意PreserveVariableNames设为true是为了防止 MATLAB 把含空格或括号的原始列名改写成Var1、Var2导致后续按名字索引失效时间戳用datenum而不是datetime因为datenum是纯数值在数组比较、filterDates的区间判断和最终 DLL 编译传参时都比对象类型更不容易出兼容性问题。文件如果是 CSV把readtable换成readmatrix也能跑但列顺序一定要在导入后打印出来人工核对一次。2.2 filterDates.m训练期与测试期的干净切分短期负荷预测有一个容易被新手忽略的前提训练集不能混入测试期样本。filterDates.m用区间比较完成这件事输入是datenum数值向量和两个日期边界输出逻辑索引供后续所有脚本统一裁切矩阵。训练窗口是 2004-01-01 到 2007-12-31测试窗口是 2008-01-01 到 2008-12-31两个窗口不能有重叠。function idx filterDates(hourlyNum, startDate, endDate) % 返回hourlyNum落在[startDate, endDate)区间的逻辑索引 if ischar(startDate), startDate datenum(startDate); end if ischar(endDate), endDate datenum(endDate); end idx hourlyNum startDate hourlyNum endDate; end边界设计上我习惯左闭右开起始日期的 00:00 包含在内结束日期那一整天排除这样连续切分 2004-2007 与 2008 时边界不会重叠也不会漏掉最后一个小时。调用后可以用sum(idx)检查样本数四年加一年应该是 5×365×24 左右闰年另算如果数量对不上多半是日期字符串格式或小时基准偏移的问题优先检查importData.m里(raw.Hour - 1) / 24这一步。2.3 gen_predictors.m特征设计比模型选择更影响精度在短期负荷预测场景里模型算法本身通常不是瓶颈特征才是刷精度的主要杠杆。gen_predictors.m把原始负荷、温度、小时序号和节假日标记组装成预测器矩阵每列对应一个特征列顺序固定因为后续trainAndTestNN.m、TreesInDetail.m都要按同一顺序取列。特征清单如下。特征名类型构造方式预测含义Temp连续值预报温度直接读入温度-负荷非线性关系的主输入TempLag1连续值滞后24小时的温度昨天热今天用电惯性延续HolidayFlag0/1createHolidayDates 生成节假日的负荷水平显著低于工作日HourSin[-1,1]sin(2πh/24)保留昼夜循环的连续性HourCos[-1,1]cos(2πh/24)与 HourSin 互补表达相位LoadLag24连续值t-24h 负荷昨日同时刻负荷是最强单个特征LoadLag168连续值t-168h 负荷同星期几的负荷形态相似function X gen_predictors(load, temp, hour, holiday) % 组装预测器矩阵列顺序固定为7列 % load: 历史负荷列向量; temp: 温度; hour: 0-23小时列向量; holiday: 0/1标记 n length(load); X zeros(n, 7); X(:,1) temp; X(:,2) [NaN; temp(1:end-1)]; X(:,3) holiday; X(:,4) sin(2*pi*hour/24); X(:,5) cos(2*pi*hour/24); X(:,6) [NaN(24,1); load(1:end-24)]; X(:,7) [NaN(168,1); load(1:end-168)]; endhour 为什么不直接用 0-23 整数因为决策树会把整数当成有序离散值神经网络也偏好连续光滑输入数值上 0 和 23 距离 23 个小时但在周期意义上只差 1 小时。HourSin 和 HourCos 把小时映射到单位圆上让两个模型都能学到“深夜 23 点和凌晨 0 点是相邻时段”。LoadLag24用NaN(24,1)填充开头 24 行是因为这些位置没有昨天的数据训练时必须用rmmissing或isnan过滤掉这些行否则fitnet会把 NaN 当成缺失值传播。2.4 createHolidayDates.m节假日日历的自动生成NEPOOL 的节假日负荷曲线和工作日有明显差异元旦、独立日、劳动节、感恩节、圣诞节都会造成负荷低谷部分节假日前一天傍晚还会出现提前下班导致的负荷下降。createHolidayDates.m的作用就是生成训练期和测试期内的全部节假日日期供HolidayFlag特征使用。固定日期好处理浮动日期感恩节是 11 月第 4 个星期四劳动节是 9 月第 1 个星期一需要用weekday函数推算。function holidays createHolidayDates(startYear, endYear) % 生成NEPOOL规则下的节假日datenum列表返回列向量 holidays []; for y startYear:endYear % 固定日期 holidays [holidays; datenum(y,1,1); datenum(y,7,4); datenum(y,12,25)]; % 感恩节: 11月第4个星期四 nov1 datenum(y,11,1); offset mod(11 - weekday(nov1), 7); holidays [holidays; nov1 offset 21]; % 劳动节: 9月第1个星期一 sep1 datenum(y,9,1); offset2 mod(9 - weekday(sep1), 7); holidays [holidays; sep1 offset2]; end holidays unique(holidays); end这里weekday返回 1周日到 7周六mod(11 - weekday(nov1), 7)算出从 11 月 1 日到第一个星期四的天数偏移再加 21 天到第 4 个星期四。注意 MATLAB 的mod结果始终非负所以不用考虑负索引问题。生成后建议对目标年份单独执行datestr(holidays)人工核对这一年的感恩节公历日期因为浮动节日的推算是最容易让特征整体错位的一步。另外Holidays.xls里可能记录了备用节假日或区域特定假日如果做多区域对比可以直接读取该表替代硬编码规则。3. 神经网络与袋装回归树两个非线性预测模型的选型与校准3.1 为什么不用 ARIMA 或多元线性回归ARIMA 擅长单变量同方差序列但负荷预测至少需要温度、节假日、小时三个外部变量ARIMAX 要处理差分阶数选择和回归项平稳性建模成本高且对突发的温度变化响应慢。多元线性回归虽然简单但负荷对温度的响应在 18-24°C 区间近似线性超出这个区间后越热负荷上升越快严寒地区低温段又有拐点线性模型无法表达这种分段非线性关系。案例里选的两类模型——神经网络和袋装回归树——都是纯非线性回归器不需要预先指定拐点位置直接让数据拟合。两者的行为差异在于神经网络擅长捕捉温度、小时和历史负荷的交互作用比如“工作日傍晚叠加高温”而袋装回归树对异常值和噪声的容忍度更高因为多棵树平均天然具备抗离群点能力。3.2 LoadScriptNN.m用 fitnet 搭建负荷预测网络LoadScriptNN.m的核心是神经网络工具箱的fitnet函数。常见做法是先用一层隐含层、10-15 个神经元起步观察验证集误差后再决定是否加深。神经元太少欠拟合太多则把训练集的噪声也背下来。hiddenSize 12; net fitnet(hiddenSize, trainlm); net.divideFcn divideblock; net.divideParam.trainRatio 0.85; net.divideParam.valRatio 0.15; net.divideParam.testRatio 0; net.trainParam.epochs 500;trainlm是 Levenberg-Marquardt 算法属于牛顿法家族的快速二阶方法适合中等规模回归问题收敛快但内存占用比trainscg高样本量超过几十万行时可以换trainscg。最关键的divideFcn设置成divideblock按连续时间块划分训练集和验证集而不是随机抽取。随机抽取在时间序列里等价于偷看未来因为相邻小时的负荷强相关验证集会包含训练样本的“邻居”loss 会严重乐观。trainRatio和valRatio的单位是比例而非具体小时数所以四年数据做训练时验证集会自动拿到约 15% 的连续时段。3.3 trainAndTestNN.m数据布局与训练测试切分神经网络工具箱的数据布局约定是“每列一个样本”和 sklearn 的(n_samples, n_features)正好相反。trainAndTestNN.m里最常踩的坑就是忘了转置导致train函数报维度错误或者更隐蔽地把 7 个特征当成了 7 个样本。[Xtr, ytr] prepareData(2004, 2007); % 训练窗口 [Xte, yte] prepareData(2008, 2008); % 样本外测试窗口 net fitnet(12, trainlm); net.divideFcn divideblock; net.divideParam.trainRatio 0.85; net.divideParam.valRatio 0.15; [net, ~] train(net, Xtr, ytr); % 注意转置为 特征x样本 布局 yhat net(Xte); % 输出为 1x样本数 yhat yhat;prepareData内部做的事情就是按年份调用filterDates裁切时间窗再调用gen_predictors生成特征矩阵最后用rmmissing删除因滞后特征产生的 NaN 行。训练完成后用net(Xte)做前向预测输出形状是1×样本数要转置回列向量才能和yte做误差计算。如果发现测试集预测值整体向右偏移一个时刻检查importData里小时序号是不是从 0 开始的NEPOOL 数据有的版本从 0 开始有的从 1 开始差 1 小时会直接体现在残差自相关上。3.4 TreesInDetail.m袋装回归树的训练与树结构检查袋装回归树部分由LoadScriptTrees.m驱动核心是TreeBagger。它是随机森林的近亲区别在于每棵树的特征子集处理方式NumPredictorsToSample可以设为all或一个整数。特征只有 7 列时我一般设为all不做随机特征抽样因为再抽特征会牺牲单棵树表现袋装只要靠样本扰动就足够降低方差。Mdl TreeBagger(200, Xtr, ytr, Method, regression, ... MinLeafSize, 8, NumPredictorsToSample, all); % 袋外误差曲线用于判断树的数量是否足够 figure; plot(oobError(Mdl)); grid on; xlabel(Number of Grown Trees); ylabel(Out-of-Bag MSE); yhat predict(Mdl, Xte);MinLeafSize是控制树复杂度最直接的参数默认 1 会过拟合案例这种 3 万多个样本的四年小时级数据8-20 之间通常比较稳。oobError返回袋外样本的均方误差画出来后如果曲线在 150 棵树之后还在明显下降说明nTrees应该加大如果 50 棵就平台了再加树只会增加计算时间。TreesInDetail.m里还可以对单棵树执行view(Mdl.Trees{1})查看分支规则确认模型真的在按“温度超过某阈值”“小时在 17-20 点之间”这样的条件切分而不是在拟合噪声。3.5 comparePredictors.m用同一把尺子比较两个模型两个模型单独训练完不能直接拿各自的 MAPE 说谁更好因为特征矩阵、训练窗口和误差口径必须完全一致。comparePredictors.m做的就是统一流程生成同一份特征矩阵分别调用两个模型的预测函数对同一份 2008 年测试数据计算误差并叠加绘制实际负荷、神经网络预测、树模型预测三条曲线。误差指标一般用 MAPE 和 MAE 同时看因为 MAPE 会被凌晨低负荷时段放大单看它容易掩盖高峰时段的偏差。模型关键超参对异常值容忍度调参敏感度神经网络hiddenSize12, epochs500中等尖峰负荷会被平滑高需改结构重跑袋装回归树nTrees200, MinLeafSize8高天然抗离群点低主要调 MinLeafSize这里有一个实际经验树模型在温度骤变的极端日表现通常优于神经网络因为单棵树的硬分割边界能快速翻转神经网络的优势在常规日它把“温度高 傍晚 工作日”这类多特征交互学得更平滑。所以案例的结论——平均误差 1-2%——是全年口径内部按季节或按温度区间拆分误差两个模型各有胜负。4. 训练切分、误差评估与价格预测扩展4.1 时间序列切分的两个原则训练期用 2004-2007测试期用 2008这是严格的样本外测试因为 2008 年完全没参与模型校准。切分时有两点必须坚持第一不能随机打乱样本负荷有强自相关和周期结构随机抽样会让训练集包含测试时段的邻近点评估结果虚高第二验证集要从训练数据尾部切连续块而不是随机抽 15% 的散点。divideblock配合filterDates保证两件事训练/验证/测试三段时间不重叠且验证集始终在训练集之后模拟“用过去预测未来”的真实在线应用形态。4.2 MAPE、MAE 与峰荷时段误差案例报告里说的平均误差约 1-2%通常指 MAPE。计算脚本一般在fit_plot.m附近或测试脚本内误差口径建议统一为下面这个函数。function [mape, mae, rmse] calcError(ytrue, yhat) % 计算日前负荷预测的常用误差指标 % ytrue: 实际负荷列向量; yhat: 预测负荷列向量 resid ytrue - yhat; mape mean(abs(resid) ./ ytrue) * 100; % 百分比 mae mean(abs(resid)); rmse sqrt(mean(resid.^2)); endMAPE 对清晨低负荷特别敏感凌晨 3 点绝对误差 20MW 算下来百分比可能很高但电网调度关心的其实是峰荷时段比如 17-21 点的绝对误差。所以建议单独计算peakIdx hour 17 hour 21时段的 MAPE这个数字比全天 MAPE 更能说明日前预测对备用容量决策的实际价值。另外负荷预测里 RMSE 会被偶尔的极端温度日拉高如果 RMSE 明显大于 MAE 的 1.5 倍说明预测在少数几天出现大偏差去看残差直方图比看均值更有用。4.3 PriceScriptNN.m从负荷模型迁移到价格预测价格预测和负荷预测共享同一套数据管线fetchDBPriceData.m负责从数据库或文件拉取价格序列PriceScriptNN.m复用gen_predictors生成特征再训练另一个神经网络。但价格序列和负荷有两个本质区别一是波动性大现货价格经常出现 5-10 倍的尖峰二是分布重尾直接用原始价格训练会让模型为了拟合尖峰而牺牲常规时段精度。常见做法是先对价格做对数变换再训练预测完成后还原。logPrice log(price 1); % 压缩右尾尖峰1避免零值问题 % 用logPrice替代price进入gen_predictors和train % 测试时还原 priceHat exp(yhat) - 1;注意加 1 的原因是 NEPOOL 某些小时价格可能出现 0 元log(0)是负无穷。价格预测的 MAPE 通常比负荷预测高一倍以上这不是模型调参能解决的是价格序列本身的尖峰和负值特性造成的对比误差时要用同口径指标不能拿负荷的 1-2% 标准去要求价格模型。4.4 fit_plot.m 与 dynamicDateTicks.m看图说话模型训练完不要只看误差数字一定要画预测值和真实值的叠加曲线。fit_plot.m做的就是这件事但小时级数据跨一年画出来横轴日期刻度会挤成一团所以案例里配套了dynamicDateTicks.m在缩放和拖动时自动调整横轴刻度密度。figure; plot(timeTest, yte, k-, LineWidth, 1); hold on; plot(timeTest, yhat, r--, LineWidth, 1); datetick(x, mmm-dd); dynamicDateTicks(); legend({Actual, Forecast}, Location, Best);观察叠加曲线时重点看三处温度骤变的 2-3 天里预测是否跟随拐点节假日次日清晨的负荷恢复是否对齐连续阴雨天温度特征相近是否存在系统性偏移。残差如果呈现出明显的 24 小时周期性说明特征中LoadLag24的权重吸收不够或者温度日较差特征没构建完整。这也是dynamicDateTicks.m存在的意义——不缩放时看全年概貌缩放到单周时能清楚识别每天早晚两个峰值的贴合程度。5. 把训练好的模型封装成 DLL让 Excel 前端直接调用5.1 为什么封装成 DLL 而不是让业务人员装 MATLAB负荷预测模型的消费者通常是调度员或交易员他们的日常工作环境是 Excel不可能要求他们安装 MATLAB 并理解fitnet和TreeBagger的区别。案例的最后一步是把训练好的模型封装成 Windows DLL通过 MATLAB Compiler 生成 COM 组件Excel 的 VBA 用CreateObject直接调用把预测变成一张工作表函数。这也是“可发布的报告以及 Excel 前端”这句描述背后的工程逻辑。5.2 用 deploytool 导出 DLL 的配置要点先写一个入口函数loadForecast.m把所有逻辑收敛成纯函数形式——只有输入参数和输出返回值不依赖工作区变量不弹 figure 窗口。然后在 MATLAB 命令行执行deploytool选择 “Generic COM Component”把loadForecast.m加入打包列表。function forecast loadForecast(startDate, endDate, tempForecast) %#codegen % loadForecast: 供Excel前端调用的负荷预测入口 % startDate/endDate: yyyy-mm-dd 格式的两个日期 % tempForecast: 预测时段逐小时温度列向量长度必须等于两个日期之间的小时数 % forecast: 返回逐小时负荷列向量(MW) persistent mdl; if isempty(mdl) s load(trainedNet.mat); mdl s.net; end [t, Xpred] buildPredictionMatrix(startDate, endDate, tempForecast); forecast mdl(Xpred); endpersistent mdl是关键细节第一次调用时加载训练好的网络之后对同一进程内的后续调用都复用避免每次预测都重新读文件。#codegen注释是给 MATLAB Compiler 的代码生成检查用的它会提示哪些函数不支持编译。buildPredictionMatrix需要在封装前单独测试好确保在无交互环境下能稳定生成与训练时完全一致的特征列顺序。Excel 端的 VBA 调用代码相对简短。假设 DLL 注册后组件的 ProgID 是LoadForecast.Forecast在 VBA 里这样调用Dim fc As Object Set fc CreateObject(LoadForecast.Forecast) Dim tempArr(23) As Double 填充tempArr为未来24小时的预报温度 Dim result As Variant result fc.loadForecast(2024-08-01, 2024-08-02, tempArr)CreateObject会把 COM 组件实例化之后调用其方法返回一个 Variant 数组Excel 的WorksheetFunction可以直接引用数组元素。部署前必须解决的问题是 DLL 运行环境的依赖目标机器需要安装 MATLAB Runtime 对应版本且 COM 注册需要管理员权限。建议先用compiler.runtime.download获取离线的 Runtime 安装包预置到业务部门的模板镜像里。验证部署结果有一个稳定技巧选一段已经预测过的历史日期比如 2008-06-15把该日的真实温度序列作为tempForecast输入比较 DLL 返回值与 MATLAB 环境内直接调用模型的输出两者误差应在 1e-6 MW 量级。如果一致说明 DLL 封装没有引入特征顺序错乱如果不一致优先检查buildPredictionMatrix内部是否引用了全局变量或未初始化状态。这个回归测试用例要固化成 Excel 里的一个固定工作表每次重新部署 DLL 后先跑它再放开给业务使用。本文还有配套的精品资源点击获取