简介时间序列预测在能源市场分析中占据核心地位然而单变量模型往往难以捕捉价格与库存、物流、宏观因子之间的动态联动。向量自回归VAR作为多变量时序分析的基础框架通过将多个内生变量纳入同一方程系统能够刻画变量间的双向反馈机制而协整检验则决定了模型应在VAR与向量误差修正VECM之间如何取舍。这一系列方法广泛应用于煤炭、原油等大宗商品的价格研判帮助分析师在复杂产业数据中提取可解释的长期均衡关系与短期波动路径。本文围绕基于R语言的煤炭价格多元预测实践系统梳理了数据清洗、对数差分、ADF平稳性检验、协整检验、模型定阶与残差诊断等关键环节并给出了滚动回测与区间校准的工程化实现为周频、月频的价格预测提供了一套可复现的完整链路。1. 煤炭价格多元时序预测为什么绕不开多变量模型单变量时序的硬边界煤炭价格是我见过最不“讲武德”的时间序列。单变量模型ARIMA、指数平滑拟合历史可以达到很高的决定系数但一到拐点就翻车因为煤价被港口库存、电厂日耗、物流贸易这些外部变量拉扯着走单变量建模等于只盯着后视镜开车。这份基于 R 语言的煤炭价格多元时序预测资源价值在于把预测从“单一变量的孤独拟合”换成“一组变量的系统联动”用 VAR向量自回归把煤价和库存、物流、宏观因子装进同一个模型用协整检验决定该跑 VAR 还是 VECM再从数据清洗一路做到滚动回测。适合需要做周频、月频煤价研判的行业分析师、量化交易者和现货贸易决策者新手可以按章节顺序复现熟手可以直接照抄参数和排坑要点。2. 原始数据准备把不同口径的行业数据整理成能跑 VAR 的时间序列VAR 是典型的“数据进去垃圾出来”能源行业数据又散落在不同口径的周报、月报和宏观库里我一般不会直接把 csv 丢进模型会先花半天做清洗和对齐。这一节的目标是让你拿到任何一套煤价、库存、BDI 数据都能在 R 里整理成可跑的多元时间序列。2.1 多变量选什么主变量与解释变量的搭配逻辑先说结论多元时序预测不是变量越多越好而是每一列都要有可解释的产业逻辑。对煤炭价格而言最常用的结构是“价格 库存 需求 物流/宏观”。我在实际项目中验证过的搭配如下表你可以直接参考这份变量清单不再费劲去试错。变量常见数据口径进入模型形式对煤价的预期作用煤炭价格秦皇岛动力煤平仓价或环渤海价格指数取对数后差分被解释变量港口库存北方港口周度库存合计对数库存库存高价格承压电厂日耗六大发电集团日耗煤量对数日耗日耗上升价格上行物流与贸易BDI 指数或煤炭运价指数对数 BDI运输成本与贸易景气映射这里有一个选型原则每个解释变量都必须回答“它通过什么路径影响煤价”。港口库存代表供给缓冲库存高位时买方有议价权电厂日耗代表下游真实需求夏季和冬季双高峰驱动煤价上行BDI 代表跨区域贸易的冷热程度。如果一股脑把几十个宏观指标塞进模型后续跑出来的相关系数大部分是玄学样本外预测更是无法落地。在数据频率上我通常把 VAR 预测的默认频率定在周频。月度数据虽然平滑但产业传导滞后严重日度数据噪音大且和宏观变量频率难以对齐。周频刚好能兼顾数据的可得性与滞后性这也是煤炭行业里周报最多的原因。2.2 用 R 清洗与变换日期对齐、缺失值插补、对数差分假设你手上是三个独立 CSV煤价、港口库存、BDI。第一步是把它们读进来建一个共同的日期轴。这里我推荐用zoo包而不是手动写 merge 逻辑因为报价日不重合的情况很多手动对齐容易出错。library(zoo) # 处理不规整时间序列 library(lubridate) # 解析日期字符串 # 读取原始数据注意字符串不要自动变成因子 coal - read.csv(coal_price.csv, stringsAsFactors FALSE) inv - read.csv(inventory.csv, stringsAsFactors FALSE) bdi - read.csv(bdi.csv, stringsAsFactors FALSE) # 统一解析成 Date 类型 coal$date - ymd(coal$date) inv$date - ymd(inv$date) bdi$date - ymd(bdi$date) # 用 merge 生成一个对齐的日度面板缺失日期自动补 NA z - merge(coal zoo(coal$price, order.by coal$date), inv zoo(inv$stock, order.by inv$date), bdi zoo(bdi$close, order.by bdi$date)) # 缺失值处理中间缺口用线性插值尾部缺口用 locf 填充 z - na.approx(z, na.rm FALSE) # 中间的 NA 做线性插值 z - na.locf(z, na.rm FALSE) # 结尾的 NA 用最后非缺失值填充这段代码的核心是merge生成齐次面板不同数据源的交易日不重合时R 会自动补齐 NA。插值顺序值得强调na.approx会把中间断点拟合成线性过渡na.locf则处理尾部缺失。煤炭数据里最常出现的是节假日缺数比如春节前后港口库存停发这种情况下用 locf 比线性插值更符合产业实际——库存本身不会在这几天线性增长而更接近上一个发布值。再往下是频率转换和对数差分# 将日度面板压缩成周度序列取每周最后一个观测值 w - apply.weekly(z, tail, 1) # 对数差分将水平值序列变换为对数收益率序列 dlog - diff(log(w)) dlog - na.omit(dlog) # 快速查看转换后的数据分布确认没有极端离群点 summary(dlog)为什么用对数差分而不是普通差分两个原因。第一煤价水平值的方差会随时间放大对数变换能让波动率更稳定第二对数差分的系数可以解释为“弹性”比如“港口库存增加 1% 会导致煤价下降 0.3%”这在汇报里很有说服力。diff(log(w))会产生一个 NA——第一个观测值在 diff 后退场这个遗漏点会直接影响第 4 章预测还原时的基准点我建议你从一开始就把这一期的日期和价格单独存一个变量后面会用到。数据清洗阶段有一个值得注意的习惯看到异常价格跳空时不要马上删掉先确认是不是数据录入错误再确认是否来自真实的产业事件。如果把真实跳空当成异常值剔除模型的预测区间会过于自信回测时捡了大便宜实盘必然吃亏。这里说的不是玄学是偶然实践留下的教训。3. VAR 模型选型与定阶协整检验分水岭残差检验做裁判很多初学者拿到多列数据就直接跑VAR()函数这是最容易翻车的一步。VAR 对平稳性有硬性要求而煤价水平值基本是随机游走直接跑会得到一堆看似显著、实则脆弱的回归系数。更复杂的决策是如果变量之间存在协整关系即水平值的线性组合是平稳的则应使用 VECM向量误差修正模型而不是普通 VAR。3.1 为什么先测平稳性与协整VAR 还是 VECM先理解“VAR 为什么需要平稳性”背后的大前提VAR 的本质是把多个内生变量放在同一个方程系统里每个变量的当前值都由它自己和其它变量的滞后值来解释。它不识别谁是因谁是果因此非常适合煤炭价格这种双向因果频繁的领域——港口库存影响价格价格反过来影响调运节奏这种反馈关系在单方程模型里很难表述清楚。VAR 的统计性质基于弱平稳假设。煤炭价格的对数水平值通常是非平稳的ADF 检验的原假设是“存在单位根”如果 p 值达不到显著性水准就说明序列需要差分处理。我们把差分后的序列再做一次 ADF一般就能拒绝原假设。library(urca) library(vars) # 对对数差分后的价格序列做 ADF 检验 adf_price - ur.df(dlog$coal, type drift, selectlags AIC) summary(adf_price) # 重点看 statistic 和 p-valuep 0.05 则拒绝单位根认为序列平稳 # 对对数水平值做 Johansen 协整检验判断多变量间是否长期联动 vecm_test - ca.jo(data.frame(w$coal, w$inv, w$bdi), type trace, ecdet const, K 2, spec transitory) summary(vecm_test)ADF 检验的参数里type drift表示回归方程包含常数项但不含时间趋势煤炭价格取对数后一般没有明显趋势用 drift 版本比 trend 版本更合适。ca.jo的type trace使用迹统计量ecdet const表示协整方程含常数项K 2是先验地设置滞后期为 2。看到 Johansen 检验 p 值低于 0.05 时通常说明至少存在一个协整向量。这时需要对协整个数做进一步判断如果只有 1 个协整关系可以用单一误差修正项来刻画长期收敛。如果确认存在协整正确做法是从 VAR 切换到 VECM。VECM 的优势在于它把长期均衡关系作为误差修正项带进短期动态方程既能看短期偏离又能看长期回归吸引力。煤炭库存与价格之间的均值回归关系很强——库存过高时价格会逐步回落库存紧张时价格中枢缓慢上行——这正是 VECM 最擅长刻画的“锚定”效应。如果协整不显著则老老实实用差分数据跑普通 VAR不要强迫变量之间出现不存在的长期关系。3.2 用vars包定阶与拟合AIC/BIC 只是参考确定用 VAR 还是 VECM 之后需要选滞后阶数。vars包提供VARselect函数用 AIC、HQ、SC、FPE 四个信息准则给出参考阶数。我通常会把四个准则全部打印出来而不是只盯 AIC如果四个准则给出的阶数差异很大说明样本数据对模型复杂度的反应不稳定需要人工介入。# VARselect 自动计算 AIC、HQ、SC、FPE 四类指标 lag_sel - VARselect(dlog, lag.max 12, type const) print(lag_sel$selection) # 拟合 VAR 模型以 AIC 选出的阶数为例 var_fit - VAR(dlog, p lag_sel$selection[AIC(n)], type const) # 残差三连检是否存在自相关、非正态、条件异方差 serial.test(var_fit, lags.pt 10, type PT.asymptotic) normality.test(var_fit) arch.test(var_fit, lags.multi 5)这里有一个容易被忽视的点信息准则只是数学上的“折中方案”它不关心残差性质是否满足统计推断的要求。我见过大量案例——AIC 选了 2 阶但残差在 lag 4 上仍然显著自相关说明季节性或者周度效应没有被吸收。遇到这种情况有两个选择一是提高阶数到 4 或 6二是加入外生变量比如月度虚拟变量用exogen参数传入模型。# 假设 2 月有春节效应用月度虚拟变量吸收季节性 month_dummy - model.matrix(~ as.factor(month(index(dlog))), data as.data.frame(dlog)) var_fit2 - VAR(dlog, p 2, type const, exogen month_dummy) # 加外生变量后需要重新诊断 serial.test(var_fit2, lags.pt 10, type PT.asymptotic)exogen参数的意义是这些变量只出现在方程右侧不被系统内生决定。煤炭行业的春节效应、夏季用电高峰都能用这种办法吸收掉而不是简单加大滞后阶数。加了虚拟变量后要重新做残差检验确认自相关消失再去谈预测。拟合完之后一个小样本提醒煤炭周度数据往往不超过 300 个观测点VAR 参数很多容易过拟合。这时宁可降一阶也不要无脑追 AIC。我的经验是如果 p3 的模型系数符号开始出现违背产业直觉的异性符号就果断退回 p2因为模型的预测稳定性比拟合优度更重要。这个取舍在滚动回测里会体现得非常明显。4. 从系数矩阵到预测结果多步预测与差分还原的完整闭环很多人在这一阶段掉进“黑匣子”陷阱predict()返回一堆数值却不知道这些数值到底是真实煤价还是对数差分。这一章的目标是让你的预测结果能直接画出一张“真实煤价走势图”而不是停留在模型输出的中间态。4.1 多步预测的误差累积滚动窗口是常态做法VAR 的多步预测本质上是递归的预测 t1 期时要用 t 期的滞后值预测 t2 期时t1 期已经不再是已知值而是上一步的预测值。这样误差会沿着路径不断累积步长越大置信区间越宽。在实盘分析中我会把步长控制在 4 到 12 周之间小于 4 周看不出模型的趋势优势大于 12 周误差累积很快预测区间会宽到失去决策价值。# 用拟合好的 VAR 模型做 12 步预测周度即 12 周 fc - predict(var_fit, n.ahead 12) # 提取“coal”价格路径的预测值这是对数差分形式的序列 fc_dlog - fc$fcst$coal[, 1] # 预测标准误也一起取出来后面算区间要用 fc_sd - fc$fcst$coal[, 2] print(head(fc_dlog))predict(var_fit, n.ahead 12)在内部已经完成了递归推进不需要手写循环。但你要记得$fcst$coal是对数差分形式不是煤价本身。如果要画真实价格的走势就必须做反变换。这里我一般会直接给出两个方案短期4 到 8 周看普通 VAR 的滚动预测长期12 周以上更建议叠加一个 VECM 的长期均衡约束。原因在于差分 VAR 擅长捕捉短期走势但对价格中枢没有长期记忆而 VECM 会在长期路径上给你一个“回归均衡”的引力防止预测线飘到离谱的位置。4.2 反变换与基准点差分还原要盯紧起始值把对数差分还原成对数水平值需要一个累积求和和一个基准水平。最容易出错的点在于基准水平必须是真实观测序列的最后一个已知值不能是差分序列的最后一个值。这是新手最容易错的地方所以我把它单独拎出来。# 封装一个反变换函数把对数差分预测值还原为实际价格 recover_price - function(dlog_pred, base_price, base_log NULL) { start_log - if (is.null(base_log)) log(base_price) else base_log # 在差分序列前拼接一个 0表示第一期的预测增量是相对基准值而言 cum_path - cumsum(c(0, dlog_pred)) # 还原成对数水平再 exp去掉第一个等于基准值的元素 exp(start_log cum_path)[-1] } # 调用示例用最后一个真实煤价作为基准 base_price - as.numeric(tail(w$coal, 1)) price_pred - recover_price(fc_dlog, base_price base_price) # 结合标准误构造近似 95% 区间 upper_pred - recover_price(fc_dlog 1.96 * fc_sd, base_price base_price) lower_pred - recover_price(fc_dlog - 1.96 * fc_sd, base_price base_price)理解反变换的关键是cumsum(c(0, dlog_pred))。diff(log(w))的定义是后一期的对数减去前一期的对数还原时就要把每个差分值累加成对数水平值的偏移。c(0, ...)的作用是将预测路径的起点对齐到当前实际观测值第一期预测应等于exp(log(base_price) dlog_pred[1])。如果你用exp(cumsum(dlog_pred))而漏掉了c(0, ...)第一期的预测价格会被错误地乘上一个没有来源的偏移量整条预测曲线通常都比真实价格下移一截。这个坑我踩过一次之后就把反变换函数写成了独立的 R 脚本并且加入了一个单元测试输入全零差分序列时输出应该正好等于基准价格。这样每次改动数据流程后都能快速确认还原逻辑还保留着。基于模拟的预测区间会比使用 1.96 倍标准误更紧密因为predict.varest()会把参数不确定性也纳入模拟。如果你要发布正式价格预测建议直接用这个模拟区间而不是简单的正态区间。第 4 章的核心动作就是把“模型输出”差分还原成“业务语言”价格水平。从这段代码开始你已经能把 VAR 系数矩阵变成可汇报的真实煤价预测线下一步该做的就是用历史数据验证这条预测线的可靠性。5. 煤炭价格预测实战避坑指南五个必须背下来的血泪教训这一节的内容几乎全部来自实盘踩坑每一条都按“现象 → 原因 → 解决”来写你可以在自己的项目里直接对号入座。5.1 数据频率错配周度煤价配月度宏观相关系数全是虚高现象把周度煤价与月度 CPI、月度 PMI 混在一起未对齐就丢进模型出来的相关系数高得惊人但样本外预测能力很差。原因月度变量在周频数轴上被大量前向填充导致相同数值重复出现人为制造了平滑路径和虚假相关性。简单说前向填充等于把同一个月度值复制了四周模型看到的是“假样本”。解决宏观数据一律降采样到周频或月频且必须保证解释变量的“发布时点”不晚于预测时点。我的做法是如果某个月度指标只能到月底才有那就把它放在月末对齐VAR 里别把它当周频变量用只能做月频模型的解释变量。每次整理数据时我都会记录每条变量的发布滞后天数写进脚本的注释里避免下次忘记。验证修好的方法也很简单重新跑一遍变量相关性矩阵观察是否会因为滞后错位出现完全同步的类似 0.98 的高相关如果高相关消失说明对齐逻辑对了。5.2 对数差分还原翻车预测值比真实价格整体低一截现象还原后的预测曲线形态与真实价格非常像但整体低了几十元/吨。初看还以为是模型偏差但形态如此一致显然不是随机误差。原因反变换时漏掉了基准点把cumsum(dlog_pred)直接当成了对数水平增量少加了一个log(base_price)。换句话说预测序列只是在“某个未知的零水平”上上下波动自然整体偏低。解决反变换必须显式写出基准水平值并验证第一期输出exp(log(base_price) dlog_pred[1])是否在逻辑区间内。我习惯写一个 5 行的单元测试输入全 0 差分序列反变换输出应该刚好等于基准价格序列这样能一眼看出起点有没有错。从那以后我再也没有因为“序列形态对了就发报告”而后悔过。5.3 只看信息准则选阶残差带着自相关硬跑现象VARselect根据 AIC 选了 1 阶但serial.test()显示滞后 4 期显著自相关预测残差存在明显的规律性波动。原因信息准则只评估拟合与复杂度的折中不保证残差是白噪声。残差存在自相关意味着模型还有未吸收的动态结构比如周内季节性、月度补库存周期等直接预测会系统性低估不确定性。解决把 p 强制提高到残差成为白噪声为止或者加入外生虚拟变量吸收季节效应。我自己的流程是每一种滞后期都跑一次serial.test()并记录 p 值最后选一个“残差已白噪声 参数不过多”的模型而不是直接抄 AIC 最小值。这个小动作多花十分钟能避免后续不靠谱的预测区间。5.4 回测数据泄漏所有样本一起拟合等于开卷考试现象回测误差远小于实盘误差甚至出现接近完美的预测线让人一度怀疑模型可以稳定预测价格走势。原因整个样本期间的数据都参与了模型拟合再拿同一段区间做预测比较模型在背书而非考试。这种“未来数据泄漏”是时序建模最常见的虚假成功模式。解决严格使用滚动窗口或扩展窗口训练。每次预测只使用截至当前时刻的信息绝对不把未来的观测值放进训练集。具体的扩展窗口回测函数在第 6 章给出一版可以直接跑的脚本另外在真实生产中还要注意发布延迟——例如库存周报可能是 T2 日发布预测时应使用“公布时点版本”而不是“事件时点版本”。5.5 基准期错位多步预测后的“第一天”落点用错现象预测出的第一期价格比第一个回测值差一个常数量调整基准后又发现整个序列准确只有第一期总是偏移。原因在没有更新的情况下多步预测的第一步本应基于最后真实观测但回测时训练区间不自觉地向后滑动了一步导致模型把已经发布的一期当成“未来未知”来预测。解决回测循环里在第一次预测之前先把训练集的末尾观测值“冻结”为预测基准不要让它参与了模型更新。这个点特别容易在每周一自动重训练的脚本里出现所以我每次都会在代码里写一个日志打印预测当天的基准观测值、训练截止日期、预测期起点三者核对无误后再继续。6. 预测落地的鲁棒性验证滚动回测与置信区间校准前面几章带你从数据清洗走到 VAR 模型构建和预测输出现在的问题是怎么知道这个模型真的能用唯一可信的方法是把它放到历史数据的滚动回测里严格模拟“用过去预测未来”。6.1 扩展窗口滚动回测循环四步走常用做法是“扩展窗口 4 周预测”的回测方案。核心流程固定初始训练长度为 100 周从第 101 周开始滚动预测未来 4 周每一步训练窗口增长一个周期然后计算预测误差。下面这个循环可以直接复用h - 4 n - nrow(dlog) start_t - 100 max_t - n - h error_list - vector(list, max_t - start_t 1) for (t in start_t:max_t) { train_data - dlog[1:t, ] # 训练集截止到 t test_actual - dlog[(t 1):(t h), coal] # 验证集是未来的 h 期 fit_v - VAR(train_data, p 2, type const) pred_v - predict(fit_v, n.ahead h) pred_coal - pred_v$fcst$coal[, 1] error_list[[t - start_t 1]] - pred_coal - test_actual } err_all - unlist(error_list) cat(RMSE:, sqrt(mean(err_all^2)), MAE:, mean(abs(err_all)), \n)这段循环的核心纪律train_data永远截止在 ttest_actual永远从 t1 开始。统计误差时我通常用差分尺度上的 RMSE 和 MAE而不是还原成价格尺度因为还原过程会把 h 期误差累积起来不好定位是第几步开始漂移。如果要看价格尺度多做一个“从最后一期真实价格起跳的累计还原”再做差。6.2 用区间覆盖校准别只看点预测点预测够用但最好看区间。在vars包中fanchart能快速画出不同置信水平下的预测扇面fc - predict(var_fit, n.ahead 12) fanchart(fc, names coal, n.head 4)但更有价值的不是画图而是做“区间覆盖回测”统计实际观测值落入 80% 预测区间的比例。如果模型正确这个比例应该在 80% 左右如果只有 60%说明模型过度自信如果高达 95%说明区间过宽、预测没太多信息量。校准逻辑可以手写in_interval - ifelse(actual lower_bound actual upper_bound, 1, 0) coverage - mean(in_interval) cat(80% 区间实际覆盖率:, coverage, \n)如果覆盖率明显偏离最可能的原因是残差分布不对称或模型在某个极端市场状态下遇到外生冲击。我会把区间校准结果加到每月报告里每个预测图都配上一个“覆盖率检验是否通过”的标签。从那以后我每次做煤炭价格预测都强制走一遍完整流程数据清洗、协整检验、定阶残差体检、滚动回测、区间校准五步缺一不可。曾经偷懒跳过区间校准结果月报里的 80% 置信区间实际覆盖率只有 62%直接丢掉一个月的决策依据。希望帮到你。本文还有配套的精品资源点击获取
