简介这是一套基于R语言的煤炭价格多元时序预测资源适合能源市场分析、经济统计及时间序列建模学习者使用。资源围绕VAR模型展开覆盖平稳性检验ADF/PP、Johansen协整检验、滞后阶数选择AIC/BIC/HQIC、模型拟合、脉冲响应分析与未来价格预测等关键环节能够帮助读者掌握从数据预处理到结果解读的完整分析流程。压缩包为rar格式共3个文件包含R语言实现脚本、csv格式的煤炭价格历史数据以及docx格式的研究报告。报告对方法原理、操作步骤和预测结论进行了详细说明代码与数据可直接对应复现。整包大小1.38MB轻量易用。目前已有361人学习下载适合需要快速上手多元时间序列建模并进行价格预测的读者。通过搭建VAR模型并进行动态交互分析读者不仅能理解各变量间的长期均衡关系与短期冲击响应还能依据置信区间评估预测不确定性为煤炭市场研究与投资决策提供定量支持。1. 煤炭价格多元时序预测为什么只看价格历史一定不够如果你手头有一份周度或月度的煤炭价格序列直接用 ARIMA 或者指数平滑去拟合大概率会在 2021 年那波行情里彻底翻车。煤炭价格不是一条挂在时间轴上的曲线它背后跟着港口库存、火电发电量、粗钢产量、运价指数一连串变量这些变量互相咬合才是真正能预测的信息源。煤炭价格多元时序预测要做的事就是把外部变量和价格历史一起喂进模型让模型学它们的关系而不是只盯着一根曲线做外推。这篇文章用 R language 从数据对齐聊到模型落地覆盖 VAR、prophet、xgboost 在这一场景下的选型边界和参数取舍以及我实际跑下来最常遇到的几个坑。新手能照着建出一套基线熟手能直接拿去排查自己的特征泄漏问题。2. 先搭多元数据底子煤炭价格预测的特征构造与口径选择2.1 价格口径与外生变量选择哪些变量真正对煤价有解释力做煤炭价格多元时序预测第一步不是选模型而是定口径。同样叫煤炭价格Q5500 动力煤平仓价、Q5000 混煤价格、焦煤出厂价走势差异很大。我一般用秦皇岛 Q5500 动力煤平仓价作为基准它市场认可度高、历史数据连续而且和电力、库存的联动关系最直接。如果你手里只有焦煤数据那外生变量也要换成焦煤对应的下游指标不能照搬动力煤的逻辑。外生变量选哪些我按对价格的影响路径来排北方港口库存万吨库存是供需缓冲池库存低位时价格弹性大库存高位时价格上行被压制。这个变量滞后意义很强当月的库存变化往往在下月价格里才体现出来。火电发电量亿千瓦时动力煤的最主要下游月度数据公开可得但发布滞后。月中才能看到上月实数做预测时必须把它当成滞后变量处理否则就是未来函数。粗钢产量万吨焦煤和喷吹煤的主要下游对动力煤也有间接拉动尤其在旺季补库阶段。沿海煤炭运价指数CBCFI反映运输端紧张程度运价先行于到岸价可以作为领先指标。这些变量都是月度频率。如果你想把预测频率做到周度问题会复杂很多因为发电量只有月度数据强行拆周会有大量插值噪声。我的经验是先做月度模型把多元数据对齐这件事练熟再考虑周度的事。2.2 从原始表到建模表滞后特征、滚动统计量与日期对齐拿到一张包含价格、库存、发电量、粗钢产量的月度表之后第一步是把原始数据变成建模用的特征表。这个环节我吃过亏最开始直接把当月库存塞进模型预测当月价格训练集 R² 高得吓人一到测试集就崩。原因很简单——预测当期价格的时候当期的库存和发电量还没披露你等于偷看了未来。下面是我目前一直在用的特征构造代码library(tidyverse) library(lubridate) library(slider) coal_raw - read_csv(coal_monthly.csv) # 字段示例 # date 每月月末日期 # price Q5500动力煤平仓价元/吨 # inv 北方四港库存合计万吨 # gen 火电发电量当月值亿千瓦时 # steel 粗钢产量当月值万吨 # cbc 沿海煤炭运价指数CBCFI coal_feat - coal_raw %% arrange(date) %% mutate( month month(date), price_lag1 lag(price, 1), # 上期价格动量效应 price_lag3 lag(price, 3), # 一季度前的价格捕捉中周期 price_lag12 lag(price, 12), # 去年同期价格捕捉年度季节性 mom price / price_lag1 - 1, # 环比变化率 yoy price / price_lag12 - 1, # 同比变化率 inv_lag1 lag(inv, 1), # 库存必须滞后防止未来函数 gen_lag1 lag(gen, 1), # 发电量滞后信息披露规则决定 steel_lag1 lag(steel, 1), # 粗钢产量也滞后 cbc_lag1 lag(cbc, 1), roll_mean3 slide_dbl(price, mean, .before 2, .complete TRUE), roll_sd3 slide_dbl(price, sd, .before 2, .complete TRUE) ) %% filter(!is.na(price_lag12))这段代码的逻辑有几个关键点。第一所有外生变量都取滞后一期原因不只是统计意义上的防泄漏更是实际业务里的信息披露节奏——发电量、粗钢产量这些数据都是次月才公布的月底做预测时你手里只有上月实数。第二price_lag12是年度季节性锚点煤炭价格有明显的冬储夏煤节奏去年同期价格代表了淡旺季的高度。第三roll_mean3和roll_sd3用slider包计算滚动窗口统计量帮助模型识别当前价格处在什么波动状态是高位震荡还是低位爬升。month这个变量是给树模型用的xgboost 和 prophet 都能从月份号里学出季节性但对 VAR 来说单独加月份虚拟变量意义不大VAR 靠滞后项自己捕捉周期。所以后续建模时特征组合要按模型分开处理不能一张表打天下。2.3 平稳性处理VAR 前的差分和树模型的不动点一个常见分歧是特征到底要不要做平稳化。我的处理方式按模型分流VAR 必须平稳。VAR 是线性模型非平稳序列进去会出现伪回归R² 再高也不能信。常用做法是先对价格、库存做 ADF 检验不平稳就一阶差分。xgboost 不需要差分。树模型对趋势和水平不敏感它切的是特征空间里的阈值价格从 400 涨到 800 不改变树的分裂逻辑。prophet 也不需要差分它自己会把趋势项拆出来。所以我在第 4 章的实现里VAR 用差分后的序列xgboost 和 prophet 用原始价格。同一个业务问题三种模型数据预处理层面就分岔了。这一点新手很容易踩——拿差分后的数据喂 xgboost预测出来的是价格增量还得手动累加还原多一道转换就多一个出错点。3. 模型选型VAR、prophet 与 xgboost 回归预测模型的适用边界3.1 VAR内生变量互相咬合时的经典路线向量自回归VAR是把价格、库存、发电量、粗钢产量放在一个方程组里每个变量都由自己和对方的滞后项来解释。它的价值在于承认经济变量之间的双向反馈价格涨了抑制需求需求降了库存回升库存回升又反过来压制价格。单方程 ARIMA 做不到这一点它把库存当外生变量但事实上库存和价格是互相纠缠的。VAR 的代价是参数爆炸。一个 4 变量的 VAR(2) 模型每个方程有1 4×2 截距共 9 个参数4 个方程加起来几十个参数。如果月度数据只有 48 到 60 个观测点每个方程的自由度非常紧张。所以我做 VAR 时只放 4 个核心变量price、inv、gen、steel不放运价指数和同比特征——那些留给 xgboost 用。选择 VAR 的另一个理由是可解释性。irf()函数能画出脉冲响应曲线告诉你库存突增一个标准差价格在之后几个月怎么反应。这种因点击穿的信息在给业务方汇报时非常有用比树模型的特征重要性更容易讲清楚因果链。3.2 prophet 时序预测模型趋势平滑能力强但外部冲击是短板prophet 时序预测模型的强项是把序列拆成趋势、季节性、节假日三项用分段线性趋势拟合长期方向。对趋势稳定、季节规律明确的数据比如零售销量、网站流量prophet 很好用。但煤炭价格有个特点趋势突变非常剧烈2021 年那轮动力煤从 600 涨到 2600 再跌回 800不是渐变趋势是外部供需冲击。prophet 里有一个changepoint.prior.scale参数控制趋势突变点的灵敏度。调大了模型会对历史突变敏感但容易把噪声也当成突变点调小了趋势线过于平滑暴涨暴跌段完全拟合不了。我在煤炭数据上试过不同取值最终建议设到 0.05 到 0.1 之间并且接受 prophet 在极端行情段的预测会偏保守。prophet 仍然值得跑原因有两个一是它天然处理缺失值和异常值作为数据诊断工具很好用二是它自带cross_validation和performance_metrics函数快速给出一套基线误差拿来做模型对比的下限。3.3 xgboost 回归预测模型非线性与特征交互的增量xgboost 回归预测模型在我做煤炭价格的实战里胜率最高原因是它适合特征多、关系非线性、有交互效应的场景。比如库存对价格的影响不是线性的——库存低于 1200 万吨时价格加速上涨高于 1800 万吨时价格钝化这种阈值效应线性模型很难捕捉树模型天然会切出类似的分段。xgboost 的另一个优势是特征灵活性。VAR 不能放太多变量prophet 对外生变量支持有限xgboost 则可以吃进滞后特征、滚动统计量、环比同比、月份号甚至外部事件哑变量。我可以把price_lag1、price_lag3、roll_sd3、mom、gen_lag1这些描述不同时间窗口的变量全部放进特征矩阵让模型自己去挑有用的。代价是 xgboost 不能外推趋势。它学的是特征和目标之间的映射关系如果未来价格区间超出了训练数据范围树模型只能给出训练样本里见过的最近邻值。所以我在业务里用它做 3 到 6 个月的短期预测不指望它预测一年以上的价格水平。3.4 三种模型的选型逻辑小结我对三类模型的定位是配合关系不是淘汰关系VAR 做参照系给出一个只用四个核心变量、纯线性、结构明确的预测同时输出置信区间。prophet 做数据诊断和快速基线跑一遍交叉验证让你知道问题难在哪。xgboost 作为主模型承接所有特征追求最低误差。如果你的目标是做月度价格预测用于采购决策我建议至少同时跑 VAR 和 xgboost 两个模型。如果两者方向一致决策信心会强很多如果矛盾大概率是数据对齐出了问题回到第 2 章排查。4. 在 R language 里跑通煤炭价格预测VAR 到 xgboost 的最小实现4.1 用 vars 包建立多元 VAR 模型单位根检验与滞后阶数选择先跑 VAR。这里用tseries包做 ADF 单位根检验用vars包做模型估计和预测。library(tseries) library(vars) var_data - coal_feat %% dplyr::select(price, inv, gen, steel) %% drop_na() # 注意VAR 用原始值检验后决定是否需要差分 # 1) 对每个变量做 ADF 检验 apply(var_data, 2, function(x) adf.test(x)$p.value) # 如果 p 值大于 0.05不能拒绝单位根假设说明序列非平稳 # 2) 对非平稳序列做一阶差分 var_diff - var_data %% mutate(across(everything(), ~ c(NA, diff(.)))) %% drop_na() # 3) 根据信息准则选滞后阶数 lag_select - VARselect(var_diff, lag.max 6, type const) # AIC、HQ、SC、FPE 会给出各自最优的 p通常取多数派 # 4) 估计 VAR(2) 模型 var_fit - VAR(var_diff, p 2, type const) summary(var_fit) # 5) 预测未来 3 个月差分空间 var_fcst - predict(var_fit, n.ahead 3) plot(var_fcst) # 6) 把差分预测还原成价格水平 last_price - tail(var_data$price, 1) fcst_level - cumsum(var_fcst$fcst$price[, 1]) last_price参数说明lag.max 6给足搜索空间但月度数据样本量有限最终选出来的 p 一般在 1 到 3 之间type const表示截距项月度数据如果有明显趋势可以改成both。差分还原这一步容易被忽略——VAR 在差分空间做预测得到的是增量必须用cumsum()累加回价格水平。第 6 步的last_price是最后一个观测月的实际价格加上未来三个月的预测增量才是真实的预测结果。4.2 用 xgboost 建立外生变量驱动的回归预测参数与早停xgboost 部分我把特征分成三组价格滞后组、外生变量组、滚动统计量组。library(xgboost) library(Matrix) # 特征列与第2章构造的coal_feat对应 features - c(month, price_lag1, price_lag3, price_lag12, mom, yoy, inv_lag1, gen_lag1, steel_lag1, cbc_lag1, roll_mean3, roll_sd3) model_df - coal_feat %% filter(!is.na(price_lag12)) %% dplyr::select(date, price, all_of(features)) # 按时间顺序切分不用随机抽样时序数据必须按时间切 cut_idx - floor(nrow(model_df) * 0.8) train_df - model_df[1:cut_idx, ] test_df - model_df[(cut_idx 1):nrow(model_df), ] # 特征矩阵与标签 dtrain - xgb.DMatrix(data as.matrix(train_df[, features]), label train_df$price) dtest - xgb.DMatrix(data as.matrix(test_df[, features]), label test_df$price) # 参数月度数据样本少树不宜太深 params - list( eta 0.05, # 学习率小了稳但慢不要超过0.1 max_depth 4, # 月度数据4层足够太深容易过拟合 subsample 0.8, # 行采样每棵树只用80%样本 colsample_bytree 0.8, # 列采样特征12个不算多但采样有助稳定 objective reg:squarederror ) # watchlist 同时监控训练误差和测试误差 set.seed(42) xgb_model - xgb.train( params params, data dtrain, nrounds 500, watchlist list(train dtrain, test dtest), early_stopping_rounds 20, print_every_n 50 ) # 训练结束后查看测试集误差 test_pred - predict(xgb_model, dtest) test_rmse - sqrt(mean((test_pred - test_df$price)^2)) test_mape - mean(abs((test_pred - test_df$price) / test_df$price)) * 100参数说明里最容易出错的是max_depth。月度样本只有几十条树深调到 6 以上就开始记忆样本测试集误差不降反升。我试过 4、5、6、8 几档最终 4 最稳。eta 0.05配合nrounds 500和早停是小样本 树模型的安全组合避免一次性把学习率调大导致前期就过拟合。切分方式也要注意时间序列不能随机抽样。我严格按日期切前 80% 训练、后 20% 测试只有这样才能模拟用过去预测未来的真实场景。如果你用sample()随机切训练集里混进来未来的数据点模型会作弊测试集误差会好看很多但这个好看是假的。4.3 用 prophet 加外生回归项做对照prophet 的建模方式是两列起步ds放日期y放目标值。外生变量通过add_regressor加进去但 prophet 要求外生变量在未来预测区间也有值后面第 5 章会专门讲这个坑。先看训练部分library(prophet) prophet_df - coal_feat %% dplyr::select(date, price, inv_lag1, gen_lag1, steel_lag1) %% rename(ds date, y price) # 初始化模型月度数据没有周季节关掉周季节项 p_model - prophet( yearly.seasonality TRUE, weekly.seasonality FALSE, daily.seasonality FALSE, changepoint.prior.scale 0.08 ) p_model - add_regressor(p_model, inv_lag1) p_model - add_regressor(p_model, gen_lag1) p_model - add_regressor(p_model, steel_lag1) p_model - fit.prophet(p_model, prophet_df) # 交叉验证起始36个月预测未来6个月 library(prophet) cv_result - prophet::cross_validation( p_model, initial 36, period 6, horizon 6, units months ) prophet::performance_metrics(cv_result)changepoint.prior.scale 0.08是我在煤炭数据上的折中值。太大0.5 以上会把 2021 年的暴涨当成趋势突变拟合漂亮但未来会更敏感太小0.01趋势线太刚年初的走势完全被平滑掉。交叉验证的initial 36保证第一次训练有三年数据打底horizon 6表示看未来半年的预测能力。5. 煤炭时序预测常见问题排查五个踩坑现场与解法5.1 当月库存预测当月价格隐性时间泄漏现象训练集 R² 高达 0.98测试集 RMSE 突然翻了三倍而且误差集中在价格拐点附近。原因特征表里用了当月的库存、发电量。这些数据在当月底还没有最终值模型训练时看见的是上帝视角实际预测时拿不到当月的这些数一测就露馅。解决所有外生变量用lag(, 1)滞后一期。我在第 2 章的特征构造代码里已经把inv_lag1、gen_lag1写死这是原则性问题不是参数选择问题。自查方法很简单把特征表里每一列的日期和该列数据实际发布时间对齐任何比发布日更早出现的数值都是泄漏。5.2 外生变量未来值给不上模型训练好了预测时没数据现象xgboost 训练一切正常到predict()新数据时报错说特征数量不匹配或者你拿 NA 去填充未来三个月的外生变量产出结果离谱。原因时间监督模型要求训练和预测的特征矩阵同构但未来三个月没有库存、发电量的实数你也没打算预测它们。解决对外生变量单独建模。我一般给每个外生变量配一个snaive季节性朴素法或auto.arima先用历史数据预测未来三个月的值再把这些预测值作为 xgboost 的输入。注意这个过程会引入双层误差外生变量的预测误差会传导到价格预测上。备选方案是场景法——人为设定低、中、高三种库存情景看价格预测的敏感区间这比单点预测更符合采购决策的用法。参考晶体管的双层嵌套计算思路我也常把外生变量的预测集成成多套输入。5.3 非平稳序列直接跑 VAR伪回归让你错得自信现象VAR 拟合的 R² 超过 0.9残差诊断图看起来也很干净但预测结果几乎是一条水平线完全跟不上实际波动。原因价格和库存都是 I(1) 非平稳序列直接建模得到的显著关系可能是共同趋势导致的伪回归不是真实因果关系。解决建模前跑 ADF 检验p 值大于 0.05 就对变量做一阶差分。差分后再检查 VAR 残差的 Ljung-Box 检验确保没有自相关残留。我踩过这个坑之后养成了习惯——VAR 的代码永远从adf.test开始不跳过检验。5.4 结构性断点吞掉模型2021 年之后系数完全失稳现象模型在 2018 到 2020 年训练2021 到 2022 年测试误差大到你怀疑数据是不是标错了。原因2021 年动力煤行情受外部供需冲击影响价格波动幅度是过去五年的数倍市场结构发生改变固定训练集的模型一旦离开历史区就失效。解决改用滚动窗口训练。模型只使用最近 36 个月的数据训练预测下 3 个月每滚动一个月就重新训练一次。这个做法牺牲了一些训练数据量但对结构变化敏感度低很多实战中稳定性比全样本训练高出一截。第 6 章会给出可复用的滚动回测代码。5.5 数据量不够硬上深度学习LSTM 和 transformer 在这里帮倒忙现象读了深度学习的文章把月度数据直接塞进 LSTM预测结果比线性回归还差训练集却好得惊人。原因月度数据只有 60 个点左右LSTM、transformer 这类模型需要大量序列样本才能学会时序依赖小样本上只会把记忆噪声当规律。深度学习时序预测的优势领域是交易高频数据和电力现货交易预测模型这类小时级、分钟级的海量数据月度煤炭数据不在它的射程内。解决月度数据的正确基线是 xgboost 加滞后特征或者 VAR。如果你最后一定要试 LSTM至少把数据换成周度并且用keras的GRU这种参数更少的变体同时做好早停别让训练误差降到 0。6. 进阶验证滚动回测与特征贡献分析的实操技巧滚动回测是评价时序预测模型唯一靠谱的方式它模拟的是每个月站在当时用所有可得数据做预测的真实状态。下面的函数接受特征表和参数输出一份预测结果和实际值的对照表# 滚动回测每次只用截止当前月的数据训练预测未来 h 个月 roll_forecast_xgb - function(data, features, h 3, min_train 36) { total - nrow(data) results - list() for (i in (min_train 1):(total - h)) { train - data[1:i, ] test - data[(i 1):(i h), ] dtrain_i - xgb.DMatrix(as.matrix(train[, features]), label train$price) dtest_i - xgb.DMatrix(as.matrix(test[, features])) model_i - xgb.train( params params, data dtrain_i, nrounds 300, watchlist list(train dtrain_i), verbose 0 ) pred_i - predict(model_i, dtest_i) results[[i]] - data.frame( date test$date, actual test$price, predict pred_i, horizon 1:h ) } bind_rows(results) }跑完滚动回测后不要只算 RMSE。我的习惯是分层看误差把价格按分位数切成低位、中位、高位三组分别算 MAPE。煤炭预测的难点往往不在中位数附近而是价格突然启动的那一两个月也就是极值捕捉能力。如果模型在高位区间的误差远大于低位区间说明特征里缺少领先指标这时候用xgb.importance检查一下哪个特征的 Gain 最高再决定要不要补数据。特征贡献分析的代码很简单imp - xgb.importance(feature_names features, model xgb_model) head(imp, 10) xgb.plot.importance(imp, measure Gain)Gain 代表特征在树分裂时带来的平均损失下降越高说明对预测越重要。我跑完煤炭数据后roll_sd3和price_lag3经常排在前三这个结果有业务含义市场波动率状态对后续价格走势的影响比单纯的库存水平更早显现。另一个实用的验证技巧是残差自相关检查。把滚动回测的残差按时间排列用Box.test(resid, lag 6)看 p 值。如果 p 值小于 0.05说明残差里还有可利用的信息模型没学干净优先去补特征而不是调参数。我的习惯是在模型迭代过程的每版结果里固定画出三张图预测对比曲线、残差 QQ 图、特征重要性条形图三张图看完再决定下一版改什么。希望这套流程能帮你把煤炭价格多元时序预测从纸上谈兵推进到可以落地的状态。本文还有配套的精品资源点击获取
