时间序列多元预测:VAR、VARMA、VARMAX 全解析(附 Granger 因果检验与完整代码)

时间序列多元预测:VAR、VARMA、VARMAX 全解析(附 Granger 因果检验与完整代码)

《Complete Time Series Forecasting Bootcamp in Python 2025》第 4 章学习笔记(零基础扩充版)

讲师:Marco(Carlos / Marco Peixeiro)

章节:第 4 章 · 多元预测(4 节)

官方仓库:marcopeix/AppliedTimeSeriesForecastingInPython(本课 10_VAR_VARMA_VARMAX.ipynb

环境:statsmodels(VARMAX / grangercausalitytests)+ sklearn + matplotlib


章节总览

编号 主题 模型 说明
1 多元预测概念 --- 多变量同时预测 + Granger 因果
2 代码:VAR 预测 VAR§ 向量自回归
3 代码:VARMA 预测 VARMA(p,q) VAR + MA
4 代码:VARMAX 预测 VARMAX(p,q) VARMA + 外生变量

本章场景 :同时预测 cows(母牛)calves(小牛) 的价格------两者是同一动物不同年龄,价格高度相关(小牛涨则母牛涨)。数据:barley_cow_calves_price.csv(加拿大统计,1992-2022 月度,含 barley/cows/calves 三列)。

什么是多元预测

  • 到目前为止都是单变量(一次预测一个时间序列)
  • 多元 = 同时预测多个时间序列 ,且利用多个序列的过去值互相预测
  • 为什么:cows 和 calves 价格相关(同一动物的不同年龄)

模型演进

#mermaid-svg-GlwbPLS8t00mFo9X{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GlwbPLS8t00mFo9X .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GlwbPLS8t00mFo9X .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GlwbPLS8t00mFo9X .error-icon{fill:#552222;}#mermaid-svg-GlwbPLS8t00mFo9X .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GlwbPLS8t00mFo9X .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GlwbPLS8t00mFo9X .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GlwbPLS8t00mFo9X .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GlwbPLS8t00mFo9X .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GlwbPLS8t00mFo9X .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GlwbPLS8t00mFo9X .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GlwbPLS8t00mFo9X .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GlwbPLS8t00mFo9X .marker.cross{stroke:#333333;}#mermaid-svg-GlwbPLS8t00mFo9X svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GlwbPLS8t00mFo9X p{margin:0;}#mermaid-svg-GlwbPLS8t00mFo9X .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GlwbPLS8t00mFo9X .cluster-label text{fill:#333;}#mermaid-svg-GlwbPLS8t00mFo9X .cluster-label span{color:#333;}#mermaid-svg-GlwbPLS8t00mFo9X .cluster-label span p{background-color:transparent;}#mermaid-svg-GlwbPLS8t00mFo9X .label text,#mermaid-svg-GlwbPLS8t00mFo9X span{fill:#333;color:#333;}#mermaid-svg-GlwbPLS8t00mFo9X .node rect,#mermaid-svg-GlwbPLS8t00mFo9X .node circle,#mermaid-svg-GlwbPLS8t00mFo9X .node ellipse,#mermaid-svg-GlwbPLS8t00mFo9X .node polygon,#mermaid-svg-GlwbPLS8t00mFo9X .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GlwbPLS8t00mFo9X .rough-node .label text,#mermaid-svg-GlwbPLS8t00mFo9X .node .label text,#mermaid-svg-GlwbPLS8t00mFo9X .image-shape .label,#mermaid-svg-GlwbPLS8t00mFo9X .icon-shape .label{text-anchor:middle;}#mermaid-svg-GlwbPLS8t00mFo9X .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GlwbPLS8t00mFo9X .rough-node .label,#mermaid-svg-GlwbPLS8t00mFo9X .node .label,#mermaid-svg-GlwbPLS8t00mFo9X .image-shape .label,#mermaid-svg-GlwbPLS8t00mFo9X .icon-shape .label{text-align:center;}#mermaid-svg-GlwbPLS8t00mFo9X .node.clickable{cursor:pointer;}#mermaid-svg-GlwbPLS8t00mFo9X .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GlwbPLS8t00mFo9X .arrowheadPath{fill:#333333;}#mermaid-svg-GlwbPLS8t00mFo9X .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GlwbPLS8t00mFo9X .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GlwbPLS8t00mFo9X .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GlwbPLS8t00mFo9X .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GlwbPLS8t00mFo9X .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GlwbPLS8t00mFo9X .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GlwbPLS8t00mFo9X .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GlwbPLS8t00mFo9X .cluster text{fill:#333;}#mermaid-svg-GlwbPLS8t00mFo9X .cluster span{color:#333;}#mermaid-svg-GlwbPLS8t00mFo9X div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GlwbPLS8t00mFo9X .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GlwbPLS8t00mFo9X rect.text{fill:none;stroke-width:0;}#mermaid-svg-GlwbPLS8t00mFo9X .icon-shape,#mermaid-svg-GlwbPLS8t00mFo9X .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GlwbPLS8t00mFo9X .icon-shape p,#mermaid-svg-GlwbPLS8t00mFo9X .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GlwbPLS8t00mFo9X .icon-shape .label rect,#mermaid-svg-GlwbPLS8t00mFo9X .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GlwbPLS8t00mFo9X .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GlwbPLS8t00mFo9X .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GlwbPLS8t00mFo9X :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} AR(p)

单变量
VAR(p)

多变量向量化
MA(q)

单变量
VARMA(p,q)

VAR+MA
VARMAX

+外生变量exog


一、核心概念

零基础速览:这一节在解决什么问题?

问题 :前面学的 AR/ARMA/ARIMA 都是单变量 ------只看一个序列(比如牛奶产量)来预测它自己。但现实中有很多"成对出现"的数据:母牛价格和小牛价格、房价和利率、气温和冰淇淋销量------两个变量互相影响。只预测一个会浪费另一个提供的信息。

  • 多元预测 :同时预测多个时间序列,且用其他序列的过去值帮忙预测当前序列
  • 本例:cows(母牛)和 calves(小牛)是同一动物的不同年龄------小牛涨了,饲料成本/供给变化会传导到母牛价格,所以两者高度相关
  • 为什么用矩阵表示 :单变量 AR 的系数是一个数 (φ),多元 VAR 的系数是一个矩阵------矩阵的每个元素代表"序列 i 的过去值"对"序列 j 的当前值"的影响(比如 a12 表示 calves(t-1) 对 cows(t) 的影响)

Granger 因果检验是什么? 它是这道菜的门槛:先用数据证明"一个序列的过去确实能预测另一个序列",才能用 VAR 模型------否则模型学到的是"假关系"(巧合相关)。检验要跑两次(两个方向),p<0.05 才算通过。

!IMPORTANT

VAR 系列与 ARIMA 最大的区别 :ARIMA 有集成阶数 d (模型内部自动差分),而 VAR 没有 d ------你必须自己先把序列差分到平稳(用 ADF 检验),预测后再手动 cumsum() 还原。

数学形式(VAR§ 向量形式)

复制代码
y(t) = c + A1·y(t-1) + A2·y(t-2) + ... + Ap·y(t-p) + ε(t)

其中 y(t)cε(t)向量A1..Ap矩阵

复制代码
[cows(t)]   = [c1]   + [a11 a12] [cows(t-1)]  + ... + ε(t)
[calves(t)]   [c2]      [a21 a22] [calves(t-1)]
  • 每个序列的当前值 = 自身的过去值 + 其他序列的过去值(矩阵 a12/a21 交叉项)
  • 模型假设 各序列互相影响 → 必须用 Granger 因果检验验证

Granger 因果检验(Granger causality)

  • 统计检验:时间序列 X 的过去值能否帮助预测 Y
  • 要求:序列必须平稳
  • 要跑两次(两个方向:calves→cows 和 cows→calves)
  • 零假设:X 不 Granger 导致 Y(p<0.05 拒绝 → 存在因果)
  • 若检验失败 → 不能用 VAR 模型

通用建模流程(与 SARIMAX 类似)

#mermaid-svg-iLFJlTnwNXXRgGLE{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iLFJlTnwNXXRgGLE .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iLFJlTnwNXXRgGLE .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iLFJlTnwNXXRgGLE .error-icon{fill:#552222;}#mermaid-svg-iLFJlTnwNXXRgGLE .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iLFJlTnwNXXRgGLE .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iLFJlTnwNXXRgGLE .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iLFJlTnwNXXRgGLE .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iLFJlTnwNXXRgGLE .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iLFJlTnwNXXRgGLE .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iLFJlTnwNXXRgGLE .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iLFJlTnwNXXRgGLE .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iLFJlTnwNXXRgGLE .marker.cross{stroke:#333333;}#mermaid-svg-iLFJlTnwNXXRgGLE svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iLFJlTnwNXXRgGLE p{margin:0;}#mermaid-svg-iLFJlTnwNXXRgGLE .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iLFJlTnwNXXRgGLE .cluster-label text{fill:#333;}#mermaid-svg-iLFJlTnwNXXRgGLE .cluster-label span{color:#333;}#mermaid-svg-iLFJlTnwNXXRgGLE .cluster-label span p{background-color:transparent;}#mermaid-svg-iLFJlTnwNXXRgGLE .label text,#mermaid-svg-iLFJlTnwNXXRgGLE span{fill:#333;color:#333;}#mermaid-svg-iLFJlTnwNXXRgGLE .node rect,#mermaid-svg-iLFJlTnwNXXRgGLE .node circle,#mermaid-svg-iLFJlTnwNXXRgGLE .node ellipse,#mermaid-svg-iLFJlTnwNXXRgGLE .node polygon,#mermaid-svg-iLFJlTnwNXXRgGLE .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iLFJlTnwNXXRgGLE .rough-node .label text,#mermaid-svg-iLFJlTnwNXXRgGLE .node .label text,#mermaid-svg-iLFJlTnwNXXRgGLE .image-shape .label,#mermaid-svg-iLFJlTnwNXXRgGLE .icon-shape .label{text-anchor:middle;}#mermaid-svg-iLFJlTnwNXXRgGLE .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iLFJlTnwNXXRgGLE .rough-node .label,#mermaid-svg-iLFJlTnwNXXRgGLE .node .label,#mermaid-svg-iLFJlTnwNXXRgGLE .image-shape .label,#mermaid-svg-iLFJlTnwNXXRgGLE .icon-shape .label{text-align:center;}#mermaid-svg-iLFJlTnwNXXRgGLE .node.clickable{cursor:pointer;}#mermaid-svg-iLFJlTnwNXXRgGLE .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iLFJlTnwNXXRgGLE .arrowheadPath{fill:#333333;}#mermaid-svg-iLFJlTnwNXXRgGLE .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iLFJlTnwNXXRgGLE .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iLFJlTnwNXXRgGLE .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iLFJlTnwNXXRgGLE .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iLFJlTnwNXXRgGLE .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iLFJlTnwNXXRgGLE .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iLFJlTnwNXXRgGLE .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iLFJlTnwNXXRgGLE .cluster text{fill:#333;}#mermaid-svg-iLFJlTnwNXXRgGLE .cluster span{color:#333;}#mermaid-svg-iLFJlTnwNXXRgGLE div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iLFJlTnwNXXRgGLE .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iLFJlTnwNXXRgGLE rect.text{fill:none;stroke-width:0;}#mermaid-svg-iLFJlTnwNXXRgGLE .icon-shape,#mermaid-svg-iLFJlTnwNXXRgGLE .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iLFJlTnwNXXRgGLE .icon-shape p,#mermaid-svg-iLFJlTnwNXXRgGLE .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iLFJlTnwNXXRgGLE .icon-shape .label rect,#mermaid-svg-iLFJlTnwNXXRgGLE .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iLFJlTnwNXXRgGLE .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iLFJlTnwNXXRgGLE .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iLFJlTnwNXXRgGLE :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 检验通过
检验失败
① 使序列平稳

VAR无集成阶数,必须平稳
② 训练/测试切分
③ 网格搜索(p,q)

选AIC最小
④ Granger因果检验

验证序列互相影响
⑤ 拟合模型
❌ 不能用VAR系列模型
⑥ 残差分析

Q-Q图 + Ljung-Box
⑦ 滚动预测 + MAE评估


二、VAR§ 模型

零基础速览:VAR 是"AR 的向量版"

  • VAR§ = p 阶向量自回归:每个序列的当前值 = 常数 + 所有序列过去 p 步的加权 + 误差
  • 单变量 AR(1) 是"昨天自己×系数";VAR(1) 是"昨天自己×系数 + 昨天对方×系数"
  • 本例选 VAR(6)(网格搜索 AIC 最小)------用过去 6 个月的数据预测下个月

为什么必须差分 :VAR 没有内置差分,而 cows/calves 价格明显不平稳(有长期趋势)------ADF 检验 p=0.37/0.85(>0.05 不平稳)→ 一阶差分后 p=0.0019/4.14e-06(平稳)→ 建模用差分数据,预测后 cumsum() 还原。

Granger 检验为什么选 6 滞后:网格搜索选的 VAR 阶数是 6,Granger 检验也要用相同的滞后数(maxlag=6)------保持一致性。

1. 数据

python 复制代码
df = pd.read_csv('data/barley_cow_calves_price.csv')
df.head()

输出

复制代码
    Date  barley   cows  calves
0  Jan-92  66.98  47.79   96.25
1  Feb-92  69.53  51.65   90.11
...

三个序列图(barley/cows/calves)

2. 平稳性检验(VAR 无集成阶数,必须平稳)

python 复制代码
from statsmodels.tsa.stattools import adfuller
# cows 原始
adf = adfuller(df['cows'])
print('cows')
print(f'ADF Statistic: {adf[0]}')
print(f'p-value: {adf[1]}')
# calves 原始
adf = adfuller(df['calves'])
print('calves')
print(f'ADF Statistic: {adf[0]}')
print(f'p-value: {adf[1]}')

输出(原始)

复制代码
cows    ADF Statistic: -1.824673327122963   p-value: 0.3682826880725826
calves  ADF Statistic: -0.6797085372042213  p-value: 0.851935709412259

两个 p 都 > 0.05 → 不平稳,需差分。

python 复制代码
# 一阶差分后
adf = adfuller(df['cows'].diff()[1:])
print('cows')    # p=0.0019 平稳
adf = adfuller(df['calves'].diff()[1:])
print('calves')  # p=4.14e-06 平稳

输出

复制代码
cows    ADF Statistic: -3.9172400148092836  p-value: 0.001911570559956177
calves  ADF Statistic: -5.358388855111447   p-value: 4.139911181465782e-06

3. 训练/测试切分 + VAR 网格搜索

python 复制代码
endog = df[['cows', 'calves']]
endog_diff = df[['cows', 'calves']].diff()[1:]
train = endog_diff[:-48]
test = endog_diff[-48:]

def VAR_gridsearch(endog, min_p, max_p):
    all_p = range(min_p, max_p+1, 1)
    results = []
    print(f'Fitting {all_p} unique models')
    for p in tqdm_notebook(all_p):
        try:
            model = VARMAX(endog, order=(p, 0)).fit(disp=False)
        except:
            continue
        results.append([p, model.aic])
    result_df = pd.DataFrame(results)
    result_df.columns = ['p', 'AIC']
    return result_df.sort_values(by='AIC', ascending=True).reset_index(drop=True)

result_df = VAR_gridsearch(train, 0, 6)
result_df

输出

复制代码
Fitting range(0, 7) unique models
   p          AIC
0  6  3857.399348   ← 最小,选 VAR(6)
1  5  3864.637583
2  4  3865.935543
...

4. Granger 因果检验(双向)

python 复制代码
from statsmodels.tsa.stattools import grangercausalitytests
print('calves Granger-causes cows?')
granger_1 = grangercausalitytests(df[['cows', 'calves']], [6])
print('cows Granger-causes calves?')
granger_2 = grangercausalitytests(df[['calves', 'cows']], [6])

输出

复制代码
calves Granger-causes cows?
Granger Causality
number of lags (no zero) 6
ssr based F test:  F=3.8994  p=0.0009  df_denom=351, df_num=6
...

p=0.0009 < 0.05 → calves 确实 Granger 导致 cows(检验通过,可用 VAR)。

5. 拟合 VAR(6)

python 复制代码
from statsmodels.tsa.statespace.varmax import VARMAX
VAR_model = VARMAX(train, order=(6,0))
VAR_model_fit = VAR_model.fit(disp=False)
print(VAR_model_fit.summary())

6. 残差分析(两个变量)

python 复制代码
# cows 残差
VAR_model_fit.plot_diagnostics(variable=0)
python 复制代码
residuals = VAR_model_fit.resid['cows']
ljung_box_test(residuals, False, None)   # 全部 >0.05,无自相关

输出All values are above 0.05. We fail to reject the null hypothesis. The residuals are uncorrelated

python 复制代码
# calves 残差
VAR_model_fit.plot_diagnostics(variable=1)

输出All values are above 0.05...(无自相关)

7. 滚动预测(last vs VAR)

python 复制代码
def rolling_predictions(df, last_train_value, train_len, horizon, window, method):
    total_len = train_len + horizon
    if method == 'VAR':
        cows_pred_VAR, calves_pred_VAR = [], []
        for i in range(train_len, total_len, window):
            model = VARMAX(df[:i], order=(6,0))
            res = model.fit(disp=False)
            predictions = res.get_prediction(0, i + window - 1)
            oos_pred_cows = predictions.predicted_mean.iloc[-window:]['cows']
            oos_pred_calves = predictions.predicted_mean.iloc[-window:]['calves']
            cows_pred_VAR.extend(oos_pred_cows)
            calves_pred_VAR.extend(oos_pred_calves)
        # 差分预测 → cumsum 还原
        cows_pred_VAR = np.insert(cows_pred_VAR, 0, last_train_value['cows']).cumsum()
        calves_pred_VAR = np.insert(calves_pred_VAR, 0, last_train_value['calves']).cumsum()
        return cows_pred_VAR[:horizon], calves_pred_VAR[:horizon]
    elif method == 'last':
        ...

pred_df = df[['cows', 'calves']][-48:]
TRAIN_LEN, HORIZON = len(train), len(test)
LAST_TRAIN_VALUE = endog.iloc[321]
windows = [1, 4, 6, 12]
for window in windows:
    cows_pred_last, calves_pred_last = rolling_predictions(endog_diff, LAST_TRAIN_VALUE, TRAIN_LEN, HORIZON, window, 'last')
    cows_pred_VAR, calves_pred_VAR = rolling_predictions(endog_diff, LAST_TRAIN_VALUE, TRAIN_LEN, HORIZON, window, 'VAR')
    pred_df[f'cows_pred_last_{window}'] = cows_pred_last
    ...

预测对比图(4 个 horizon)

8. 评估(MAE)

python 复制代码
from sklearn.metrics import mean_absolute_error
mae_cow_last_1 = mean_absolute_error(pred_df['cows'], pred_df['cows_pred_last_1'])
mae_cow_VAR_1 = mean_absolute_error(pred_df['cows'], pred_df['cows_pred_VAR_1'])
...

MAE 对比图(cows/calves × last/VAR × 4 horizon)


三、VARMA(p,q) 模型

零基础速览:VARMA 是"VAR + MA"

  • VARMA(p,q) = 向量自回归 + 向量移动平均:不仅要"对方的过去值",还要"双方的过去误差"
  • 网格搜索 49 个 (p,q) 组合 → VARMA(6,4) AIC 最小
  • Granger 检验不用重做:VAR(6) 和 VARMA(6,4) 的滞后结构一致,因果性只要验证一次
  • ⚠️ 本例中 calves 残差 Ljung-Box 有一个 p<0.05(仍有关联)------模型对 calves 的拟合不够完美,这是真实数据的常态,可加阶或接受

1. VARMA 网格搜索((p,q) 组合)

python 复制代码
from itertools import product
def VARMA_gridsearch(endog, min_p, max_p, min_q, max_q):
    all_p = range(min_p, max_p+1, 1)
    all_q = range(min_q, max_q+1, 1)
    all_orders = list(product(all_p, all_q))
    results = []
    print(f'Fitting {len(all_orders)} unique models')
    for order in tqdm_notebook(all_orders):
        try:
            model = VARMAX(endog, order=order).fit(disp=False)
        except:
            continue
        results.append([order, model.aic])
    result_df = pd.DataFrame(results)
    result_df.columns = ['(p,q)', 'AIC']
    return result_df.sort_values(by='AIC', ascending=True).reset_index(drop=True)

result_VARMA = VARMA_gridsearch(train, 0, 6, 0, 6)
result_VARMA.head()

输出

复制代码
Fitting 49 unique models
   (p,q)          AIC
0  (6, 4)  3773.472122   ← 最小,选 VARMA(6,4)
1  (6, 1)  3774.313326
2  (5, 4)  3777.956500
...

Granger 检验无需重做(p=6 与 VAR 相同)。

2. 拟合 VARMA(6,4)

python 复制代码
VARMA_model = VARMAX(train, order=(6,4))
VARMA_model_fit = VARMA_model.fit(disp=False)
print(VARMA_model_fit.summary())

3. 残差分析

python 复制代码
VARMA_model_fit.plot_diagnostics(variable=0)   # cows

输出All values are above 0.05...(cows 无自相关)

python 复制代码
VARMA_model_fit.plot_diagnostics(variable=1)   # calves

输出At least one p-value is smaller than 0.05(⚠️ calves 残差仍有关联)

4. 滚动预测 + 评估

python 复制代码
# rolling_predictions 与 VAR 相同,只是 model = VARMAX(df[:i], order=(6,4))

预测对比图

MAE 对比图


四、VARMAX(p,q) 模型(加外生变量)

零基础速览:VARMAX 是"VARMA + 外生变量"

  • 外生变量(exog) :除了 cows/calves 互相预测外,再引入大麦价格(barley)------饲料成本影响牛价
  • VARMAX(6,4) = VARMA(6,4) + barley 作为额外输入
  • 索引对齐是最大的坑train = endog_diff[:-48] 切的是差分后的数据(少一行),所以 exog 也必须用 .iloc[1:322] 切同一段------两个序列的索引必须严格对应,否则模型把"1 月的第 1 行"和"2 月的第 1 行"对齐了,结果全错
  • 预测时也要传 exogget_prediction(exog=exog[:i])------外生变量必须是已知的(大麦价格当月已知才能预测当月牛价)

1. VARMAX 网格搜索(带 exog)

python 复制代码
def VARMAX_gridsearch(endog, exog, min_p, max_p, min_q, max_q):
    all_p = range(min_p, max_p+1, 1)
    all_q = range(min_q, max_q+1, 1)
    all_orders = list(product(all_p, all_q))
    results = []
    print(f'Fitting {len(all_orders)} unique models')
    for order in tqdm_notebook(all_orders):
        try:
            model = VARMAX(endog, exog, order=order).fit(disp=False)
        except:
            continue
        results.append([order, model.aic])
    result_df = pd.DataFrame(results)
    result_df.columns = ['(p,q)', 'AIC']
    return result_df.sort_values(by='AIC', ascending=True).reset_index(drop=True)

# 外生变量索引必须与训练集对齐(这里用 barley 预测 cows/calves)
exog = df['barley'].iloc[1:322]
result_VARMAX = VARMAX_gridsearch(train, exog, 0, 6, 0, 6)
result_VARMAX.head()

输出

复制代码
Fitting 49 unique models
   (p,q)          AIC
0  (6, 4)  3841.370903   ← 最小,选 VARMAX(6,4)
1  (6, 1)  3841.706027
2  (6, 5)  3845.765687
...

2. 拟合 VARMAX(6,4)(带外生变量)

python 复制代码
VARMAX_model = VARMAX(train, exog, order=(6,4))
VARMAX_model_fit = VARMAX_model.fit(disp=False)
print(VARMAX_model_fit.summary())

⚠️ 外生变量 exog索引必须与 endog 对齐 (都用 .iloc[1:322] 切分)。

3. 残差分析

python 复制代码
VARMAX_model_fit.plot_diagnostics(variable=0)   # cows

输出All values are above 0.05...(cows 无自相关)

python 复制代码
VARMAX_model_fit.plot_diagnostics(variable=1)   # calves

输出All values are above 0.05...(calves 无自相关)

4. 滚动预测(VARMAX 带 exog)

python 复制代码
def rolling_predictions(df, exog, last_train_value, train_len, horizon, window, method):
    total_len = train_len + horizon
    if method == 'VARMAX':
        cows_pred_VARMAX, calves_pred_VARMAX = [], []
        for i in range(train_len, total_len, window):
            model = VARMAX(df[:i], exog[:i], order=(6,4))
            res = model.fit(disp=False)
            predictions = res.get_prediction(exog=exog[:i])
            oos_pred_cows = predictions.predicted_mean.iloc[-window:]['cows']
            oos_pred_calves = predictions.predicted_mean.iloc[-window:]['calves']
            cows_pred_VARMAX.extend(oos_pred_cows)
            calves_pred_VARMAX.extend(oos_pred_calves)
        cows_pred_VARMAX = np.insert(cows_pred_VARMAX, 0, last_train_value['cows']).cumsum()
        calves_pred_VARMAX = np.insert(calves_pred_VARMAX, 0, last_train_value['calves']).cumsum()
        return cows_pred_VARMAX[:horizon], calves_pred_VARMAX[:horizon]
    elif method == 'last':
        ...

pred_df = df[['cows', 'calves']][-48:]
exog = df['barley'][1:]
TRAIN_LEN, HORIZON = len(train), len(test)
LAST_TRAIN_VALUE = endog.iloc[321]
WINDOW = 1
cows_pred_last, calves_pred_last = rolling_predictions(endog_diff, exog, LAST_TRAIN_VALUE, TRAIN_LEN, HORIZON, WINDOW, 'last')
cows_pred_VARMAX, calves_pred_VARMAX = rolling_predictions(endog_diff, exog, LAST_TRAIN_VALUE, TRAIN_LEN, HORIZON, WINDOW, 'VARMAX')

预测对比图

5. 评估(MAE 柱状图)

python 复制代码
mae_cow_last = mean_absolute_error(pred_df['cows'], pred_df['cows_pred_last'])
mae_cow_VARMAX = mean_absolute_error(pred_df['cows'], pred_df['cows_pred_VARMAX'])
...
fig, ax = plt.subplots()
width = 0.3
labels = ['Cows', 'Calves']
x = np.arange(len(labels))
y_last = [mae_cow_last, mae_calves_last]
y_VARMAX = [mae_cow_VARMAX, mae_calves_VARMAX]
ax.bar(x - width/2, y_last, width, label='Last')
ax.bar(x + width/2, y_VARMAX, width, label='VARMAX')
...

MAE 柱状图(Last vs VARMAX)

VARMAX 通常优于 last 基线(外生变量 barley 提供信息)。


本章小结(核心速查)

模型 定义 特点
VAR§ 向量自回归 p 个滞后,多序列互相预测
VARMA(p,q) VAR + MA 加移动平均部分
VARMAX(p,q) VARMA + 外生变量 用外部变量辅助预测

与 SARIMAX 对比

SARIMAX VAR 系列
目标 单变量 多变量(向量)
平稳性 有集成阶数 d 无 d,必须手动平稳
交叉影响 (矩阵交叉项)
关键检验 ADF + Ljung-Box Granger 因果 + Ljung-Box
外生变量 exog 参数 exog 参数

核心易错点

  1. VAR 系列无集成阶数(d) ------序列必须自己先差分平稳,预测后 cumsum 还原
  2. 必须做 Granger 因果检验(双向),失败则不能用 VAR
  3. 外生变量索引必须与 endog 对齐 (切分方式一致,.iloc[1:322]
  4. 预测函数用 VARMAX(endog, exog, order=(p,q)) ,VARMAX 预测时预测 get_prediction(exog=...)
  5. 差分预测后要 cumsum() 还原原始尺度
  6. 网格搜索用 AIC 最小选阶
  7. 残差分析对每个变量都要做(variable=0/1)
相关推荐
小七-七牛开发者3 个月前
世界杯一粒进球被吹掉,背后可能有多少 AI?
ai·var·世界杯·ai裁判·ai判罚
时序之心3 个月前
ICLR 2026 | Chronos、TimesFM、Moirai等模型在6个数据集上的校准误差对比
人工智能·时间序列
Highcharts.js3 个月前
Highcharts v13 全新时间轴标签边界格式|让时间维度表达更智能
前端·信息可视化·时间序列·图表开发·chart·自定义标签·可视化开发
【建模先锋】3 个月前
独家原创!基于特征—时间双图注意力与BiGRU全局注意力并行融合的高创新预测模型
人工智能·深度学习·注意力机制·预测模型·gat·时间序列预测模型
deardao3 个月前
【NeuriPS-25】检索增广生成的时序零概率预测
人工智能·机器学习·数据挖掘·transformer·时间序列
七夜zippoe3 个月前
DolphinDB时间序列引擎:实时聚合计算
服务器·前端·时间序列·dolphindb·实时聚合
七夜zippoe4 个月前
DolphinDB时间序列查询:时间窗口与聚合
窗口·查询·时间序列·聚合·dolphindb
katheta4 个月前
时间序列模型总体分类
人工智能·分类·数据挖掘·时间序列·时序模型
时序之心4 个月前
时序前沿:CNN结合Transformer,通过通道-头绑定实现多变量时序插补!
人工智能·transformer·时间序列