简介:一个关于电动汽车充电负荷预测的MATLAB项目实例,融合SARIMA与XGBoost混合模型,采用‘SARIMA提取线性季节结构、XGBoost学习残差非线性关系’的两阶段融合策略,以应对充电负荷的强季节性与强波动性。适合具备MATLAB编程基础、熟悉时间序列分析与机器学习的研发人员、数据分析师、电力系统工程师及高校研究生,可应用于公共快充站调度、配电网负荷管理、储能协调和动态电价设计等场景。资源包含1个docx文档,大小118KB,内容涵盖项目背景、目标与意义、挑战及解决方案、五层模型架构、完整MATLAB代码示例、GUI界面设计以及系统部署建议。文档详细展示了数据读取与整理、季节性差分与SARIMA建模、特征工程与XGBoost训练数据构造、树集成近似残差修正、融合预测与性能评估、未来时段滚动预测等关键环节的代码实现与调参思路,并给出多场景应用方向和未来改进路径。目前已有53人学习,适合作为教学案例或科研原型,通过调整参数、引入新特征或扩展应用场景,深化对混合预测模型设计与优化方法的认识。
1. 为什么EV充电负荷需要SARIMA-XGBoost混合框架
城市里公共快充站的负荷曲线从来不是一条平滑的线,工作日早高峰和晚高峰各有一个尖峰,周末峰形整体后移,节假日直接变成另一个形态。如果只用单一时间序列模型,ARIMA能抓住线性趋势,但面对温度骤降导致的同期充电量跳变,拟合残差会明显放大;反过来让XGBoost直接拟合负荷值,树模型对长期周期结构的记忆能力又不如统计模型稳定。把两者的边界划清楚:SARIMA负责把日周期、周周期这些可重复的季节骨架建出来,XGBoost负责学习SARIMA剩下那些与天气、日期特征、滞后变量相关的残差成分。这个组合在EV充电负荷预测场景里已经是被验证过的实用路线,尤其适合15分钟到1小时粒度的短期预测,既保留了统计模型的可解释性,又能吸收机器学习处理高维非线性特征的优势。本文基于MATLAB环境,给出从数据准备、SARIMA建模、XGBoost残差修正、融合评估到GUI工程化的完整实现路径,适合有MATLAB基础、想把混合预测模型落到实际充电站或配电网项目的研发人员。
2. SARIMA建模:先把季节骨架立起来
2.1 从ARIMA到SARIMA的参数语义
EV充电负荷是非平稳序列,一天24小时的负荷形状高度重复,但均值水平和峰值幅度会随星期几、月份漂移。ARIMA(p,d,q)通过d阶差分消除趋势,对周期信息却无能为力。SARIMA在此基础上增加季节性差分和季节自回归、季节滑动平均项,完整记法为ARIMA(p,d,q)(P,D,Q)s,其中s是季节周期长度。对小时粒度数据,s=24表示日内周期;对日粒度数据,s=7表示周周期。在充电负荷场景里,通常先做s=24的季节差分,观察ACF/PACF在滞后24、48、72处的截尾或拖尾特征,用来初定P和Q。d和D一般取0或1,过高的差分阶数会丢失水平信息,反而让后续XGBoost残差修正更难学。
2.2 MATLAB中的建模流程与数据准备
在MATLAB中建模前,先把原始充电功率序列整理成timetable格式,保证时间戳连续,缺失值用线性插值或前一天同时刻均值填充。对于毛刺尖峰,用中位数绝对偏差(MAD)识别并替换。实际项目中,我一般会额外生成小时、星期、节假日三列特征,但SARIMA阶段只吃负荷序列本身,其余特征留给XGBoost。使用Econometrics Toolbox时,先对序列做季节差分,再用arima对象指定季节项,调用estimate估计参数。一个常用配置是SARIMA(2,1,1)(1,1,1)24:
% 读取并清洗负荷数据,形成连续timetable loadData = readtimetable('charging_load.csv'); loadData.Power = fillmissing(loadData.Power, 'linear'); % 构造SARIMA模型对象:ARIMA(2,1,1)(1,1,1)24 Mdl = arima('ARLags', 1:2, 'D', 1, 'MALags', 1, ... 'Seasonality', 24, ... 'SARLags', 24, 'SMA', 1, ... 'SMALags', 24, 'Constant', 0); % 估计参数 EstMdl = estimate(Mdl, loadData.Power, 'Display', 'off'); % 对训练集内样本做一步预测,提取残差 [resid, ~] = infer(EstMdl, loadData.Power);参数说明:ARLags指定非季节自回归滞后阶数,这里取1和2对应AR(2);D=1表示一阶非季节差分;MALags是滑动平均滞后;Seasonality=24告诉模型存在周期24的季节结构,SARLags=24表示季节自回归项作用于滞后24的位置,SMALags=24同理。设置Constant=0可避免差分后常量项与漂移混淆。infer返回的是基于历史数据计算的残差序列,这个残差就是下一步XGBoost的监督目标。
2.3 季节周期选择与参数网格
并不是所有场景都固定用s=24。如果数据是15分钟粒度,日内周期就是96;如果是周末服务区快充站,周周期效应可能比日周期更显著。一个可行的做法是分别用s=24和s=168(小时粒度下的一周)建两个SARIMA模型,比较AIC或BIC后再决定主模型。注意SARIMA参数不宜设得过大,p和q超过3、P和Q超过1时,参数估计耗时显著增加,且容易出现过拟合。下面是一张参考配置表:
| 数据粒度 | 常用s | 推荐p,d,q | 推荐P,D,Q | 适用场景 |
|---|---|---|---|---|
| 15分钟 | 96 | (1-2, 1, 0-2) | (0-1, 1, 1) | 快充站日内负荷 |
| 1小时 | 24 | (2, 1, 1) | (1, 1, 1) | 城市充电桩聚合负荷 |
| 1小时 | 168 | (1, 1, 0) | (1, 1, 0) | 含周规律的总站负荷 |
| 1天 | 7 | (0, 1, 1) | (1, 1, 1) | 中长期规划 |
参数初定后,用fmincon或简单网格遍历多个组合,取AIC最小者。但AIC最低的模型不一定残差白噪声性最好,还要对残差执行Ljung-Box检验,p值大于0.05说明残差中不再有明显自相关,SARIMA部分就可以收工了。多数情况下SARIMA只能解释负荷里60%~80%的方差,剩余残差里藏着温度、降雨、电价突变带来的非线性信息,这就是第三阶段XGBoost要处理的内容。
3. XGBoost残差修正:非线性部分交给树模型
3.1 为什么要对残差学习
SARIMA输出的是条件均值预测,它对历史负荷的线性依赖结构建模很好,但对“下雨天通勤时段充电量额外上浮”“电价低谷前两小时用户集中插枪”这类条件非线性关系无能为力。把这些因素放入SARIMA的额外回归项,一方面会让模型结构变得臃肿,另一方面统计模型的参数估计对共线性敏感,多个天气变量同时进入容易引发数值问题。更干净的做法是让SARIMA先预测,然后把真实值减预测值得到残差序列,再用XGBoost去拟合“当前时刻的外部和滞后特征是什么,残差就会偏向什么方向”。这里有个关键点:XGBoost学习目标不是负荷本身,而是SARIMA的误差。这样做的好处是两者的任务边界清晰,SARIMA负责低频周期,XGBoost负责高频非线性扰动,融合时不会互相抢占解释方差。
3.2 特征工程与训练样本构造
构造训练样本时,每个时间戳形成一个样本,特征分为三类:
- 时间编码:小时、星期、是否周末、是否节假日、是否为峰谷电价时段。用正弦/余弦编码小时可以避免0点和23点之间的距离被模型当成23。
- 滞后与滚动特征:残差的前1、2、3、24、48小时值,负荷的前24小时均值、前7天同时刻均值、最近3小时滑动标准差。
- 外部变量:温度、湿度、降雨量、空气质量指数,以及它们的滞后1小时值。
注意滞后特征只能使用模型预测时已经确定的量,比如预测t时刻,只能使用t-1之前的真实值或SARIMA预测值,不能把t时刻的真实残差放进去,否则会泄漏。下面是一段构建特征矩阵的MATLAB代码:
% resid是SARIMA残差序列,tbl包含时间相关外部变量 % 构造滞后残差特征 lagMat = lagmatrix(resid, [1 2 3 24 48]); lagMat = array2timetable(lagMat, 'RowTimes', timeVector); featTbl = [tbl, lagMat]; % 生成小时正弦余弦编码 hourOfDay = hour(timeVector); featTbl.HourSin = sin(2*pi*hourOfDay/24); featTbl.HourCos = cos(2*pi*hourOfDay/24); % 去除包含NaN的起始行 validIdx = all(~ismissing(featTbl), 2); X = featTbl(validIdx, :); Y = resid(validIdx); % 监督目标为残差逻辑说明:lagmatrix一次性生成多个滞后列,但会引入前48行的NaN,因此用all(~ismissing(featTbl),2)过滤掉不完整样本。小时的正弦余弦编码把“深夜”和“凌晨”映射到相近坐标,树模型也能更好地利用周期连续性。这里的Y是SARIMA残差,模型训练完成后,对测试集的预测输出就是修正量。
3.3 MATLAB中训练XGBoost风格残差模型的两种路径
MATLAB从R2021a开始提供fitcensemble和fitrensemble,但不直接支持XGBoost原生算法。工程上常见两种做法:
- 路径A:调用MATLAB的
fitrensemble,基学习器选择tree,方法选择LSBoost,训练梯度提升树模型。该实现接近XGBoost的回归思路,支持学习率和子采样,适合纯MATLAB环境。 - 路径B:使用Python引擎调用
xgboost库,在MATLAB脚本中通过py.xgboost.XGBRegressor完成训练。该路径能使用完整XGBoost功能,但需要配置Python环境,部署复杂度高。
对于多数项目,路径A已经足够。训练代码如下:
% 使用LSBoost训练提升树回归模型,学习率设为0.1 rng(42); residModel = fitrensemble(X, Y, ... 'Method', 'LSBoost', ... 'NumLearningCycles', 300, ... 'Learners', templateTree('MaxNumSplits', 16), ... 'LearnRate', 0.1, ... 'PredictorNames', X.Properties.VariableNames); % 输出特征重要性 imp = predictorImportance(residModel); [~, idx] = sort(imp, 'descend'); disp(X.Properties.VariableNames(idx(1:5)));参数说明:MaxNumSplits=16限制单棵树深度,避免单棵过拟合;NumLearningCycles=300是最大迭代轮数,配合早停避免无效训练。predictorImportance输出基于分裂增益的特征重要性,通常会发现滞后24小时残差和前7天同时刻负荷均值排在前列,这与充电负荷的周记忆性一致。LSBoost和XGBoost的共同点是逐步减小残差、用负梯度方向拟合新树,区别在正则项和近似分裂算法,但对本场景最终精度差异不大。如果你手头有现成的XGBoost模型文件,也可以在MATLAB中用importONNXNetwork导入ONNX格式的模型,但需要先用Python导出。
4. 融合预测与评估:从训练到滚动预测的完整闭环
4.1 两阶段融合策略的落地顺序
融合预测不是把SARIMA的预测值直接加上XGBoost的残差修正值那么简单。实际操作时要分两条线走:在训练集内部,SARIMA的预测残差会被XGBoost拟合,所以SARIMA必须用严格的样本外预测方式产生残差。最稳妥的做法是使用infer得到基于历史数据的残差序列,再用这些残差训练XGBoost。到了测试阶段,SARIMA先按时序预测出未来一段的负荷均值,XGBoost再根据测试时可得的滞后特征输出残差修正量,两者相加得到最终预测:
% sarimaForecast是SARIMA对未来24小时的点预测 sarimaForecast = forecast(EstMdl, 24, 'Y0', loadData.Power(end-336:end)); % 构造测试阶段XGBoost所需特征矩阵 % 注意:滞后残差需要回填预测残差或SARIMA残差近似 testX = buildTestFeatures(timeVector, weather, sarimaForecast, resid); % XGBoost残差修正 residFix = predict(residModel, testX); % 融合输出 finalForecast = sarimaForecast + residFix';需要留意的是,XGBoost的训练特征里包含残差滞后项,多步预测时后续时刻的滞后残差无法从真实值得到,我一般用SARIMA残差的期望值0代替,或者用上一步的预测残差滚动代入。两种方式各有取舍,滚动代入精度通常更高,但偏差会累积,因此只适合短周期预测。也可以退一步,训练XGBoost时不使用残差滞后项,只采用外部特征和负荷滞后特征,这样多步预测时不用考虑残差回填问题,工程实现更简单。
这里给出一段典型的评估代码:
% 计算MAE、RMSE、MAPE mae = mean(abs(yTrue - yPred)); rmse = sqrt(mean((yTrue - yPred).^2)); mape = mean(abs((yTrue - yPred)./yTrue)) * 100; % 打印结果 fprintf('MAE: %.3f kW\nRMSE: %.3f kW\nMAPE: %.2f%%\n', mae, rmse, mape);4.2 评估指标与阈值判断
充电负荷预测不能只看整体误差,峰期误差对调度意义更大。因此除MAE、RMSE、MAPE外,我习惯补充两个业务指标:
- 峰值时段RMSE:只统计每日负荷最大的6个小时。
- 误差超限率:预测相对误差超过15%的点占总点数比例,这个指标直接反映模型对异常时段的把控能力。
测试集上如果MAPE低于10%、峰值RMSE低于20%,基本可以满足充电站排班和配电网容量评估的需求。SARIMA单独建模时MAPE通常在15%到25%之间,叠加XGBoost修正后往往能下降3到8个百分点。如果融合后MAPE反而上升,优先检查XGBoost是否用到了未来信息,或者SARIMA残差序列中仍含有强烈自相关,说明SARIMA阶数设置偏低。
4.3 滚动预测与模型更新
实际部署时不可能每天手动重训一次模型。我采用的方案是每天凌晨用过去60天数据重估SARIMA参数,XGBoost每周重训一次,预测时使用滚动窗口:每次预测未来24小时,每15分钟或每小时用新的真实负荷值更新滞后特征。这样既保证了模型对季节变化的适应,又限制了训练频度,避免过度消耗计算资源。下面是一个滚动预测的核心片段:
for t = 1:24 % 更新滚动特征,将真实负荷或上一步预测值写入历史表 updateHistory(historyTbl, observedOrPred(t)); % 重新预测下一点 nextResid = predict(residModel, buildOneSample(historyTbl, t)); pred(t+1) = sarimaOneStep(t+1) + nextResid; end这里每一步只预测一个点,observedOrPred在前几步是真实观测值,后续则用预测值填充,形成自回归式循环。注意滚动窗口里的历史长度必须与训练时的滞后特征定义一致,否则预测结果会系统性偏移。
5. 参数调优与过拟合控制:让模型在工程中站得住
5.1 树模型超参数与早停
XGBoost风格的提升树模型最影响泛化能力的参数是NumLearningCycles、LearnRate和MaxNumSplits。学习率越低,每棵树贡献越小,需要的树数量越多,但整体泛化更好。我通常先固定LearnRate=0.05,用交叉验证观察验证集误差随树数变化的曲线,找到拐点位置,然后用早停机制截断训练。在MATLAB的fitrensemble中,fitrensemble本身不直接支持早停,但可以通过先训练再比较验证集误差手工截断,或者使用classreg.learning.regr.FullRegressionModel的相关接口。一种简单做法是划分训练集和验证集,迭代增加树数并记录验证集损失,选择损失最小的树数重训全量模型。下面是网格搜索学习率和树数的小例子:
lrList = [0.01, 0.05, 0.1]; numTreeList = [100, 300, 500]; bestLoss = inf; for lr = lrList for nt = numTreeList mdl = fitrensemble(Xtr, Ytr, 'Method', 'LSBoost', ... 'NumLearningCycles', nt, 'LearnRate', lr, ... 'Learners', templateTree('MaxNumSplits', 8)); lossVal = loss(mdl, Xva, Yva); if lossVal < bestLoss bestLoss = lossVal; bestParams = [lr, nt]; end end end这段代码通过网格搜索比较9组超参数组合。每次训练生成的模型只在验证集上评估,最终保留验证损失最小的参数,再用全量训练数据重训一次,防止验证集信息泄漏到最终模型。
5.2 子采样与特征降维
XGBoost原生算法支持行采样和列采样,MATLAB的fitrensemble对应的是以树为单位的随机子采样。子采样比例设为0.7到0.9,能有效降低树之间的相关性,缓解过拟合。特征层面,如果外部变量过多,比如同时加入降雨、湿度、风速、风向、空气质量等十几个连续变量,树模型容易在某些无关特征上过早分裂。处理方式有两个:一是靠特征重要性排序,只保留累计贡献前80%的特征;二是对连续变量做分箱,把温度按5度间隔分成离散档,降低噪声敏感度。在实际项目中,我发现“是否节假日”这个二值特征的重要性常常排进前三,而“空气质量指数”贡献很小,可以剔除。
5.3 排错清单和常见坑
很多人在复现时会遇到融合预测结果比单模型更差的情况,常见原因如下:
- 时序错位:训练XGBoost时使用了t时刻的真实负荷作为特征去预测t时刻的残差,这等于把答案泄漏给了模型。必须保证特征时间都早于预测目标时间。
- 周期长度不匹配:数据粒度不是严格小时,但SARIMA固定用s=24,导致季节项匹配错误。先检查时间戳是否连续、是否存在跨时区问题。
- SARIMA残差不平稳:如果残差序列还带有明显趋势,XGBoost训练时会被趋势误导,此时应先增大SARIMA的d或D。
- 数据归一化不一致:LSBoost对单棵树的尺度不敏感,但如果使用路径B调用XGBoost,默认对特征不做缩放,树模型不受影响,但特征类别编码若包含字符串,需要先做哑变量处理。
排错时我建议先画三张图:SARIMA预测与真实值的对比图、残差自相关图、XGBoost修正后残差分布图。如果第二张图在滞后24处仍有明显尖峰,说明SARIMA季节捕捉不完整;如果第三张图修正后的残差方差大于修正前,说明XGBoost过拟合,需要降低树复杂度或增加子采样比例。
6. GUI与工程化:把预测能力变成可交付的工具
6.1 界面结构与数据流
项目最终交付时,纯命令行脚本很难让运营人员直接使用,因此配套一个MATLAB App。GUI采用上下分区:顶部是数据加载、模型训练、预测执行、结果保存四个功能按钮;左侧是参数设置面板,包含SARIMA阶数输入框、季节周期选择下拉框、XGBoost学习率和树数编辑框、预测步数滑块;右侧是信息显示面板,用于展示评估指标、特征重要性和日志信息;中间大区域用来绘制训练集拟合曲线、测试集预测曲线和残差分布图。数据流按“加载数据 → 清洗与特征工程 → SARIMA训练 → 残差提取 → XGBoost训练 → 融合预测 → 评估与保存”的顺序推进,每一步都会在状态栏更新当前进度。
6.2 训练与预测回调的关键实现
按钮回调函数中,最关键的是把训练好的模型对象保存到App的属性中,供预测按钮使用。下面是一个精简版训练回调:
function trainButtonPushed(app, event) % 从输入框读取参数 p = app.POrderEdit.Value; d = app.DOrderEdit.Value; q = app.QOrderEdit.Value; s = app.SeasonalityDropDown.Value; lr = app.LearnRateEdit.Value; nt = app.NumTreesEdit.Value; % 数据预处理 [dataClean, timeVec, extTbl] = preprocessData(app.data); % SARIMA训练 Mdl = arima('ARLags', 1:p, 'D', d, 'MALags', 1:q, ... 'Seasonality', s, 'SARLags', s, 'SMA', 1, 'SMALags', s, 'Constant', 0); app.EstMdl = estimate(Mdl, dataClean, 'Display', 'off'); [app.resid, ~] = infer(app.EstMdl, dataClean); % XGBoost风格残差模型 X = buildFeatureTable(timeVec, extTbl, app.resid); Y = app.resid(~any(ismissing(X), 2)); X = X(~any(ismissing(X), 2), :); app.residModel = fitrensemble(X, Y, 'Method', 'LSBoost', ... 'NumLearningCycles', nt, 'LearnRate', lr, ... 'Learners', templateTree('MaxNumSplits', 8)); % 更新界面日志 app.LogTextArea.Value = sprintf('训练完成。SARIMA残差方差:%.3f;修正后方差:%.3f', ... var(app.resid), var(app.resid - predict(app.residModel, X))); end预测回调中,从app.EstMdl调用forecast生成未来基点负荷,再从app.residModel预测残差修正量,相加后绘制曲线,并将预测结果写入表格。需要注意UI线程中不要执行过重的训练任务,否则界面会卡死。对于大规模数据,把训练放在parfeval异步任务中,完成后通过afterEach更新UI,这样可以在训练过程保留响应性。
最后一个实用技巧:模型保存时不要只保存预测结果,还要保存SARIMA模型对象、XGBoost特征列顺序、数据预处理参数(缺失值阈值、异常值替换率)和时间戳对齐规则。部署到新站点时,只需修改数据加载路径和参数配置,就能复用同一套工程模板。这样做的好处是,任何新增的充电站数据都能快速接入,而不需要重新搭建整个预测流程。
本文还有配套的精品资源,点击获取