"I asked the temperature what it would do next, and it pointed at the vibration sensor. I asked the vibration sensor, and it pointed back at the temperature. Eventually I gave up asking one at a time and started forecasting all of them at once, with everyone allowed to blame everyone else."
A VAR System Where Everything Causes Everything
A vector autoregression is the smallest honest model of a world in which several series move together and feed back on one another: it forecasts every series from the recent past of every series, replacing scalar coefficients with coefficient matrices and a scalar shock with a correlated shock vector. The univariate models of Chapter 5 assume each series is an island, driven only by its own past and its own noise. Real systems are not islands. An engine's temperature drives its vibration, which drives its bearing wear, which feeds back into temperature; an economy's inflation, output, and interest rate chase each other quarter by quarter. The VAR captures these cross-effects by letting the present vector depend on lagged vectors through full coefficient matrices, and by letting the innovations be contemporaneously correlated. The price is parameter count: a $K$-dimensional VAR($p$) carries $K^2 p$ autoregressive parameters, a number that grows quadratically in the number of series and motivates the regularization and factor methods of later sections. This section builds the VAR from the scalar AR of Section 5.2, derives its stationarity condition from a companion matrix, estimates it by equation-by-equation least squares, selects the lag order by information criteria, forecasts all series jointly, explains why the more general VARMA is rarely used in practice, and fits a real multi-sensor system both from scratch and through statsmodels.
The previous chapter built a complete univariate toolkit. The autoregressive model of Section 5.2 expressed a single series as a linear function of its own past; the ARMA and ARIMA constructions of Section 5.3 added moving-average terms and differencing; and the order-selection machinery of Section 5.7 taught us to pick model complexity by AIC and BIC. Every one of those tools forecast one series in isolation. Now we lift the entire apparatus into many dimensions at once. The reader should arrive here comfortable with the scalar AR($p$) recurrence, the lag operator, and stationarity through roots of a characteristic polynomial, because the vector story is the same story written with vectors and matrices in place of scalars. Throughout we use the multivariate moment symbols of the unified notation table in Appendix A: bold $\mathbf{x}_t \in \mathbb{R}^K$ for the observation vector, $A_i$ for the lag-$i$ coefficient matrix, and $\Sigma$ for the innovation covariance. Figure 6.1.0 dramatizes the move at the heart of the section: every series tugging on every other.
1. From One Series to Many: The VAR(p) Model Beginner
Suppose we observe $K$ time series simultaneously, stacked into a column vector $\mathbf{x}_t = (x_{1,t}, x_{2,t}, \dots, x_{K,t})^\top$. A vector autoregression of order $p$, written VAR($p$), models the present vector as a constant plus a linear combination of the previous $p$ vectors plus a shock vector,
$$\mathbf{x}_t \;=\; \mathbf{c} \;+\; A_1 \mathbf{x}_{t-1} \;+\; A_2 \mathbf{x}_{t-2} \;+\; \cdots \;+\; A_p \mathbf{x}_{t-p} \;+\; \boldsymbol{\varepsilon}_t,$$where $\mathbf{c} \in \mathbb{R}^K$ is an intercept vector, each $A_i \in \mathbb{R}^{K \times K}$ is a coefficient matrix, and $\boldsymbol{\varepsilon}_t \in \mathbb{R}^K$ is a vector white-noise innovation with mean zero, $\mathbb{E}[\boldsymbol{\varepsilon}_t] = \mathbf{0}$, contemporaneous covariance $\mathbb{E}[\boldsymbol{\varepsilon}_t \boldsymbol{\varepsilon}_t^\top] = \Sigma$, and no serial correlation, $\mathbb{E}[\boldsymbol{\varepsilon}_t \boldsymbol{\varepsilon}_s^\top] = 0$ for $t \ne s$. Set $K = 1$ and every matrix collapses to a scalar, every vector to a number, and the equation becomes exactly the AR($p$) of Section 5.2. The VAR is the literal generalization of autoregression: same recurrence, wider objects.
The off-diagonal entries of the $A_i$ matrices are where the multivariate content lives. Write out the first row of a VAR(1) on $K = 2$ series to see it. The entry $(A_1)_{12}$ is the coefficient with which yesterday's series two enters today's equation for series one; if it is nonzero, series two helps predict series one beyond what series one's own past already says. The diagonal entries $(A_1)_{11}$ and $(A_1)_{22}$ are the familiar own-lag autoregressive coefficients; the off-diagonal entries $(A_1)_{12}$ and $(A_1)_{21}$ are the cross-lag couplings that a stack of separate univariate models would simply discard. A VAR with all off-diagonal coefficients zero is exactly $K$ independent univariate AR models running side by side, which is the null hypothesis the cross terms are tested against.
$$\begin{pmatrix} x_{1,t} \\ x_{2,t} \end{pmatrix} = \begin{pmatrix} c_1 \\ c_2 \end{pmatrix} + \begin{pmatrix} (A_1)_{11} & (A_1)_{12} \\ (A_1)_{21} & (A_1)_{22} \end{pmatrix} \begin{pmatrix} x_{1,t-1} \\ x_{2,t-1} \end{pmatrix} + \begin{pmatrix} \varepsilon_{1,t} \\ \varepsilon_{2,t} \end{pmatrix}.$$It is worth pausing on what the model does and does not assume. The VAR is purely linear: it commits to the present being a linear function of the recent past, with no thresholds, regimes, or interactions between lags, and that linearity is what buys the closed-form estimator and the eigenvalue stability test we develop below. It is also a reduced-form model: the coefficient matrices summarize how the series move together without claiming to know the underlying structural shocks (which variable's surprise truly caused which), and recovering those structural shocks from the reduced form is the identification problem that the impulse-response analysis of Section 6.2 must confront. Finally, the VAR assumes a single fixed lag order $p$ shared by every equation, a deliberate simplification (an individual equation might in principle need fewer lags of some series) that keeps the shared-regressor structure intact and the estimator cheap. Each of these assumptions is a place where a later, richer model will push back, but the plain VAR's discipline is exactly what makes it the indispensable first model of a multivariate system.
Two distinct channels carry the interaction between series, and keeping them separate is essential for everything that follows. The coefficient matrices $A_i$ carry dynamic interaction: how the past of one series moves the future of another. The covariance $\Sigma$ carries contemporaneous interaction: how the unpredictable shocks hitting different series at the same instant are correlated, for example because a single physical event (a load spike, a policy surprise) jolts several series at once. A VAR can have a diagonal $A_i$ (no lagged cross-effects) yet a dense $\Sigma$ (shocks arrive together), or the reverse. The next section's Granger causality lives entirely in the $A_i$ channel, while the impulse-response analysis of Section 6.2 needs the $\Sigma$ channel to decide how a shock to one series propagates through the rest.
The deepest practical fact about a VAR is hiding in plain sight in the equation: every one of the $K$ scalar equations has the same set of right-hand-side variables, namely the full lagged vector $(\mathbf{x}_{t-1}, \dots, \mathbf{x}_{t-p})$. Equation $k$ regresses $x_{k,t}$ on those common regressors; only the response variable on the left changes from equation to equation. This shared-regressor structure is why a VAR, despite looking like a fearsome matrix object, estimates as cheaply as $K$ ordinary linear regressions (subsection three makes this precise) and why the coupling between series enters only through which lagged variables earn nonzero coefficients in each row. The VAR does not invent a new estimation problem; it runs $K$ copies of a problem you already solved in Chapter 5, sharing the design matrix across all of them.
The VAR was pushed into mainstream econometrics by Christopher Sims in a famous 1980 paper titled "Macroeconomics and Reality," and he shared the 2011 Nobel Memorial Prize in Economics partly for it. His provocation was that the large structural models of the day pretended to know which variables caused which, baking in dozens of "incredible" identifying restrictions to make the math solvable. Sims' VAR refused to take a side: let every variable depend on the lagged everything, estimate the unrestricted system, and only afterward, carefully, ask what the data imply about causation. The epigraph's complaint that "everyone is allowed to blame everyone else" is not a bug of the VAR; it is the entire methodological point.
2. Stationarity, the Innovation Covariance, and the Parameter Count Intermediate
A scalar AR($p$) is stationary when the roots of its characteristic polynomial lie outside the unit circle, a condition we read off the AR coefficients in Section 5.2. The vector version needs the same idea, but the coefficients are now matrices, so "root outside the unit circle" becomes a statement about eigenvalues. The cleanest route is to rewrite the VAR($p$) as a VAR(1) on a stacked state, the companion form. Define the $Kp$-dimensional stacked vector $\mathbf{z}_t = (\mathbf{x}_t^\top, \mathbf{x}_{t-1}^\top, \dots, \mathbf{x}_{t-p+1}^\top)^\top$ and the $Kp \times Kp$ companion matrix
$$F = \begin{pmatrix} A_1 & A_2 & \cdots & A_{p-1} & A_p \\ I_K & 0 & \cdots & 0 & 0 \\ 0 & I_K & \cdots & 0 & 0 \\ \vdots & & \ddots & & \vdots \\ 0 & 0 & \cdots & I_K & 0 \end{pmatrix},$$so that the whole system collapses to the first-order recursion $\mathbf{z}_t = \tilde{\mathbf{c}} + F \mathbf{z}_{t-1} + \tilde{\boldsymbol{\varepsilon}}_t$. Iterating this recursion shows $\mathbf{z}_t$ is a sum of terms $F^j \tilde{\boldsymbol{\varepsilon}}_{t-j}$, and that sum converges (the process is stable and stationary) precisely when the powers $F^j$ decay to zero, which happens if and only if every eigenvalue $\lambda$ of $F$ satisfies $|\lambda| < 1$. That is the multivariate stationarity condition: all $Kp$ eigenvalues of the companion matrix lie strictly inside the unit circle. An equivalent statement in terms of the matrix polynomial is that $\det(I_K - A_1 z - \cdots - A_p z^p) \ne 0$ for all complex $z$ with $|z| \le 1$, the direct generalization of the scalar characteristic-root test.
The innovation covariance $\Sigma$ deserves its own paragraph because it is half of what makes a VAR multivariate. It is a $K \times K$ symmetric positive-definite matrix whose diagonal entries $\Sigma_{kk}$ are the one-step forecast-error variances of each series and whose off-diagonal entries $\Sigma_{jk}$ measure how the simultaneous shocks to series $j$ and series $k$ co-move. A diagonal $\Sigma$ would say the shocks are unrelated across series; a dense $\Sigma$ says a common disturbance perturbs several series at once. We estimate $\Sigma$ from the residuals after fitting the coefficient matrices, and it is the object that makes joint prediction intervals (subsection four) tighter or looser than the naive product of univariate intervals.
Now the uncomfortable arithmetic. Counting parameters, a VAR($p$) on $K$ series has $K$ intercepts, $p$ coefficient matrices of $K^2$ entries each, and a symmetric $\Sigma$ with $K(K+1)/2$ free entries, for a total
$$\#\text{params} \;=\; \underbrace{K}_{\text{intercepts}} \;+\; \underbrace{p K^2}_{\text{AR matrices}} \;+\; \underbrace{\tfrac{1}{2}K(K+1)}_{\text{covariance}}.$$The middle term is the one that hurts: it grows quadratically in $K$ and linearly in $p$. The numeric example below makes the blowup vivid, and that blowup is precisely the curse that motivates the regularized and factor-based methods of Section 6.4.
Take a modest macro VAR on $K = 7$ series (output, inflation, unemployment, a policy rate, money, exchange rate, equities) at lag order $p = 4$, a common quarterly choice. The autoregressive block alone holds $p K^2 = 4 \cdot 49 = 196$ coefficients, plus $K = 7$ intercepts, plus $\tfrac{1}{2}\cdot 7 \cdot 8 = 28$ covariance entries, for $231$ parameters. Quadruple the system to $K = 28$ series (a sectoral disaggregation) at the same $p = 4$ and the autoregressive block alone is $4 \cdot 28^2 = 3{,}136$ coefficients; with only, say, $120$ quarterly observations you are trying to estimate twenty-six parameters per data point, which is hopeless by ordinary least squares. The count scales as $K^2$, so doubling the number of series roughly quadruples the parameters while the data grows only linearly in $K$. This single fact, parameters growing as $K^2$ while information grows as $K$, is the entire reason unrestricted VARs are confined to small systems and why Section 6.4 turns to Bayesian shrinkage and low-rank factor structure for high-dimensional panels.
The companion-form rewrite $\mathbf{z}_t = F \mathbf{z}_{t-1} + \tilde{\boldsymbol{\varepsilon}}_t$ is not a notational trick local to this section; it is the same linear state recursion that organizes the rest of this book. In Chapter 7 the stacked vector $\mathbf{z}_t$ becomes the hidden state of a linear Gaussian state-space model and $F$ becomes its transition matrix, so a VAR is literally a state-space model whose state is observed; the Kalman filter then handles the case where it is only partially observed. Push the same recursion into the deep-learning chapters and $\mathbf{z}_t = \sigma(F \mathbf{z}_{t-1} + \dots)$ is the update of a recurrent neural network (Chapter 10), and the structured state-space models of Chapter 13 learn an $F$ whose eigenvalues are constrained to control exactly the stability we just derived. When you understand why $|\lambda(F)| < 1$ makes a VAR stationary, you already understand why an RNN with eigenvalues above one explodes and why SSMs parameterize their transition to keep the spectral radius in check. The classical idea returns in learned form.
3. Estimation: Equation-by-Equation OLS and Lag Selection Intermediate
The Key Insight of subsection one promised that a VAR estimates as cheaply as $K$ regressions, and here is why. Because every equation of the VAR shares the identical set of regressors (the stacked lags), the generalized least squares estimator that would in principle account for the cross-equation error correlation $\Sigma$ reduces exactly to ordinary least squares applied to each equation separately. This is a classical result: when the regressor set is common to all equations of a seemingly-unrelated-regressions system, the efficient joint estimator and the equation-by-equation OLS estimator coincide. So we never need to invert a giant $K^2 p$ by $K^2 p$ matrix; we solve $K$ ordinary regressions, each with the same design matrix, and that shared design matrix is factored only once.
Concretely, build the design matrix once. Let $\mathbf{w}_{t} = (1, \mathbf{x}_{t-1}^\top, \dots, \mathbf{x}_{t-p}^\top)^\top \in \mathbb{R}^{1 + Kp}$ be the regressor row for time $t$, stack these rows for $t = p+1, \dots, n$ into a matrix $W$, and stack the corresponding targets into $Y$ (an $(n - p) \times K$ matrix whose column $k$ is the series-$k$ targets). The least-squares coefficient block, holding all the intercepts and lag matrices, is the single normal-equation solve
$$\hat B \;=\; (W^\top W)^{-1} W^\top Y, \qquad \hat\Sigma \;=\; \frac{1}{\,n - p - (Kp + 1)\,}\, \hat E^\top \hat E,$$where $\hat E = Y - W\hat B$ is the residual matrix and the denominator applies the small-sample degrees-of-freedom correction (subtracting the number of regressors per equation). The matrix $(W^\top W)^{-1} W^\top$ is computed once and multiplies all $K$ target columns at once, which is the computational expression of "shared regressors." Lag order $p$ is then chosen exactly as in Section 5.7, by an information criterion, but with the scalar variance replaced by the determinant of the multivariate residual covariance,
$$\text{AIC}(p) = \ln\bigl|\hat\Sigma_p\bigr| + \frac{2}{n}\,p K^2, \qquad \text{BIC}(p) = \ln\bigl|\hat\Sigma_p\bigr| + \frac{\ln n}{n}\,p K^2.$$The fit term is $\ln|\hat\Sigma_p|$, the log-determinant of the residual covariance (a multivariate generalization of the residual variance, since the determinant is the generalized volume of the error ellipsoid), and the penalty counts the $pK^2$ autoregressive coefficients. As before, BIC penalizes complexity more heavily ($\ln n$ versus $2$) and so selects shorter lag orders on long samples; with the $K^2$ multiplier on the penalty, the difference between AIC and BIC matters even more for VARs than for scalar AR models, because each extra lag adds $K^2$ parameters rather than one. The numeric example shows the determinant criterion at work.
Fit a $K = 2$ VAR at lag orders $p = 1, 2, 3$ on $n = 200$ observations and suppose the residual-covariance determinants come out as $|\hat\Sigma_1| = 0.900$, $|\hat\Sigma_2| = 0.610$, $|\hat\Sigma_3| = 0.585$. The BIC penalty per lag is $(\ln n / n) K^2 = (\ln 200 / 200) \cdot 4 = 0.106$. Then $\text{BIC}(1) = \ln 0.900 + 0.106 = -0.105 + 0.106 = 0.001$; $\text{BIC}(2) = \ln 0.610 + 0.212 = -0.494 + 0.212 = -0.282$; $\text{BIC}(3) = \ln 0.585 + 0.318 = -0.536 + 0.318 = -0.218$. The criterion bottoms out at $p = 2$: going from one lag to two bought a large drop in the determinant (real structure) that outweighed the penalty, but going from two to three bought a tiny determinant drop ($0.610$ to $0.585$) that the $K^2 = 4$-parameter penalty erased. The same data scored by AIC, with its smaller penalty $2K^2/n = 0.04$ per lag, might instead prefer $p = 3$; that AIC-versus-BIC tension, sharpened by the $K^2$ factor, is exactly the over-parameterization risk this chapter keeps flagging.
The estimator $\hat B = (W^\top W)^{-1} W^\top Y$ is the identical normal-equation solve you met for scalar linear regression and for the AR($p$) least-squares fit of Section 5.2. Only the right-hand side has widened: $Y$ is now a matrix with one column per series rather than a single target vector, so the one matrix inverse $(W^\top W)^{-1}$ is reused across all $K$ columns. Nothing about the optimization is new; the VAR inherits the closed-form, globally optimal, convex least-squares solution of the scalar case wholesale. That inheritance is exactly why the VAR is so robust in practice and why the genuinely hard multivariate problem is not estimation but the parameter-count and identification issues this section keeps returning to.
4. Forecasting with a VAR: Joint Recursion and Growing Error Intermediate
Once the matrices are estimated, forecasting is a vector recursion. The optimal one-step forecast is the conditional expectation, and because future shocks have mean zero it is simply the deterministic part of the VAR equation evaluated at the observed past,
$$\hat{\mathbf{x}}_{t+1} = \hat{\mathbf{c}} + \hat A_1 \mathbf{x}_t + \cdots + \hat A_p \mathbf{x}_{t-p+1}.$$For horizons beyond one step we iterate, feeding each forecast back in as if it were observed, with any still-unknown future values replaced by their own forecasts: the $h$-step forecast is $\hat{\mathbf{x}}_{t+h} = \hat{\mathbf{c}} + \sum_{i=1}^{p} \hat A_i \hat{\mathbf{x}}_{t+h-i}$, where $\hat{\mathbf{x}}_{t+h-i}$ is the actual observation when $h - i \le 0$ and the recursive forecast otherwise. This is the multivariate twin of the recursive ARIMA forecasting of Section 5.3, except that every series' forecast feeds into every other series' next-step forecast, so the cross-couplings compound through the horizon.
The forecast-error covariance grows with horizon in a way that mirrors the scalar case but now accumulates correlated matrix-weighted shocks. Writing the VAR in its infinite vector moving-average form $\mathbf{x}_{t+h} = \sum_{j \ge 0} \Psi_j \boldsymbol{\varepsilon}_{t+h-j}$, where the matrices $\Psi_j$ are computed from the $A_i$ (with $\Psi_0 = I_K$ and $\Psi_j = \sum_{i=1}^{\min(j,p)} A_i \Psi_{j-i}$), the $h$-step forecast-error covariance is
$$\Sigma(h) \;=\; \operatorname{Cov}\bigl(\mathbf{x}_{t+h} - \hat{\mathbf{x}}_{t+h}\bigr) \;=\; \sum_{j=0}^{h-1} \Psi_j \,\Sigma\, \Psi_j^\top.$$The full matrix $\Sigma(h)$, not just its diagonal, is what makes the forecast genuinely joint: its off-diagonal entries describe how the forecast errors of different series at horizon $h$ co-vary, so the model delivers a tilted error ellipsoid rather than an axis-aligned box of independent intervals. The diagonal of $\Sigma(h)$ gives each series' marginal forecast variance, from which per-series prediction intervals follow as $\hat x_{k,t+h} \pm z_{1-\alpha/2}\sqrt{\Sigma(h)_{kk}}$. This is where a VAR earns its keep against $K$ separate univariate models: the cross terms in the $\Psi_j$ and the off-diagonals of $\Sigma$ let information from one series sharpen the forecast and tighten the interval of another. The contrast deserves to be stated sharply, because it is the entire reason to pay the parameter cost.
Running $K$ univariate AR models and a single VAR can produce noticeably different forecasts, and the difference is not cosmetic. The univariate stack throws away two things the VAR keeps. First, it discards the cross-lag couplings: if temperature genuinely leads vibration by one step, only the VAR can use today's temperature to forecast tomorrow's vibration, while the univariate vibration model is blind to it. Second, it discards the contemporaneous shock correlation $\Sigma$: even when the point forecasts agree, the VAR produces a joint predictive distribution whose error ellipsoid is tilted by $\Sigma$, so a coherent scenario for all $K$ series together (the input every downstream optimization or control loop actually needs) comes only from the VAR. The univariate models give $K$ marginals; the VAR gives the joint. When the series are truly coupled the VAR forecasts better; when they are not, the off-diagonal coefficients estimate near zero and the VAR gracefully degrades to the univariate stack at the cost of a little extra estimation variance.
| Aspect | $K$ separate univariate AR models | One VAR($p$) |
|---|---|---|
| Cross-lag couplings | Discarded; each series sees only its own past | Captured in the off-diagonal entries of $A_i$ |
| Shock correlation | Ignored; $K$ independent noise streams | Modeled by the off-diagonals of $\Sigma$ |
| Output | $K$ separate marginal forecasts | One joint predictive distribution over all $K$ series |
| Parameter count | $\approx K(p+1)$, linear in $K$ | $K + pK^2 + \tfrac{1}{2}K(K+1)$, quadratic in $K$ |
| Estimation | $K$ independent OLS solves | $K$ OLS solves sharing one design matrix |
| Best when | Series are genuinely uncoupled | Series lead, lag, and co-shock one another |
Code 6.1.1 implements the whole VAR pipeline from scratch (build the lagged design matrix, solve the stacked normal equations once, then recurse the forecast forward) so that the shared-regressor estimator and the joint recursion are concrete rather than asserted. It runs on a simulated two-series sensor system in which temperature drives vibration with a one-step lag.
import numpy as np
def fit_var(X, p):
"""Fit a VAR(p) by equation-by-equation OLS (shared design matrix).
X has shape (n, K): rows are time steps, columns are the K series."""
n, K = X.shape
rows, targets = [], []
for t in range(p, n):
lagged = X[t - 1:t - p - 1:-1].ravel() # x_{t-1},...,x_{t-p} flattened
rows.append(np.concatenate([[1.0], lagged])) # prepend 1 for the intercept
targets.append(X[t])
W = np.array(rows) # (n-p) x (1 + K*p) design matrix
Y = np.array(targets) # (n-p) x K target matrix
B = np.linalg.lstsq(W, Y, rcond=None)[0] # ONE solve serves all K equations
resid = Y - W @ B
dof = W.shape[0] - W.shape[1]
Sigma = (resid.T @ resid) / dof # innovation covariance estimate
return B, Sigma # B: (1 + K*p) x K coefficient block
def forecast_var(X, B, p, h):
"""Recursive h-step vector forecast: feed each forecast back as input."""
K = X.shape[1]
history = list(X[-p:]) # last p observed vectors
preds = []
for _ in range(h):
lagged = np.concatenate([h_t for h_t in history[::-1]]) # newest first
w = np.concatenate([[1.0], lagged])
x_next = w @ B # one VAR step for ALL series at once
preds.append(x_next)
history = history[1:] + [x_next] # slide the window forward
return np.array(preds)
rng = np.random.default_rng(7)
n, K, p_true = 600, 2, 1
A = np.array([[0.55, 0.00], # temperature: own lag only
[0.40, 0.50]]) # vibration: driven by lagged temperature
c = np.array([0.5, 0.2])
L = np.array([[0.8, 0.0], [0.3, 0.6]]) # Cholesky factor of Sigma (shocks correlated)
X = np.zeros((n, K)); X[0] = [10.0, 4.0]
for t in range(1, n):
X[t] = c + A @ X[t - 1] + L @ rng.standard_normal(K)
B, Sigma = fit_var(X, p=1)
print("intercept c_hat :", np.round(B[0], 3))
print("A1_hat (rows=eqn) :\n", np.round(B[1:].T, 3)) # transpose to read as A1
print("Sigma_hat :\n", np.round(Sigma, 3))
fc = forecast_var(X, B, p=1, h=4)
print("4-step joint forecast (temp, vib):\n", np.round(fc, 3))
np.linalg.lstsq call solves all $K$ equations at once because they share the design matrix W, the computational meaning of equation-by-equation OLS, and forecast_var advances every series jointly so the recovered cross-coefficient (vibration driven by lagged temperature) actually steers the forecast.intercept c_hat : [0.523 0.214]
A1_hat (rows=eqn) :
[[0.547 0.003]
[0.402 0.498]]
Sigma_hat :
[[0.641 0.241]
[0.241 0.451]]
4-step joint forecast (temp, vib):
[[ 6.27 4.38 ]
[ 3.95 4.07 ]
[ 2.66 3.36 ]
[ 1.97 2.86 ]]
5. VARMA, Identification Difficulties, and High-Dimensional VARs Advanced
Just as ARMA added moving-average terms to AR in Section 5.3, the vector autoregressive moving-average model, VARMA($p, q$), adds matrix moving-average terms to the VAR,
$$\mathbf{x}_t = \mathbf{c} + \sum_{i=1}^{p} A_i \mathbf{x}_{t-i} + \boldsymbol{\varepsilon}_t + \sum_{j=1}^{q} M_j \boldsymbol{\varepsilon}_{t-j},$$with each $M_j \in \mathbb{R}^{K \times K}$ a moving-average coefficient matrix. In principle VARMA is strictly more general and more parsimonious than VAR: just as a scalar ARMA can mimic a high-order AR with few parameters, a low-order VARMA can capture dynamics that would need a long VAR. Yet practitioners overwhelmingly reach for the plain VAR, and the reason is a genuine and deep difficulty unique to the multivariate moving-average case: identification. In the scalar ARMA the parameters are essentially unique given the data. In VARMA they are not. Different combinations of $A_i$ and $M_j$ matrices can produce exactly the same observable second-order behavior, so the likelihood has flat directions and ridges; the model is, in the technical sense, not identified without imposing extra structural restrictions (echelon forms, Kronecker indices, or scalar-component constraints) that are awkward to choose and harder still to interpret. The maximum-likelihood optimizer wanders these ridges, convergence is fragile, and two analysts can fit "the same" VARMA and get different matrices.
A concrete way to feel the non-identification is to recall the scalar intuition and watch it break. In a scalar ARMA, the autoregressive and moving-average polynomials share no common factor after cancellation, which pins the parameters down. In the vector case, "common factor" becomes "common left matrix factor," and matrices do not commute, so there are many ways to factor the same transfer function into an $A(L)$ part and an $M(L)$ part. Each factorization is a different set of $A_i$ and $M_j$ matrices producing the identical spectrum, identical autocovariances, and identical likelihood, which is precisely what it means for the model to be unidentified. No amount of data resolves the ambiguity, because the data only ever see the product, never the factors.
The VAR sidesteps the entire problem. With no moving-average matrices there is nothing to trade off against the autoregressive matrices, the parameters are identified, and estimation is the closed-form least-squares solve of subsection three rather than a delicate nonlinear optimization. The cost is a longer lag order than a VARMA would need, but lags are cheap to add and identification is precious, so the field's verdict is essentially unanimous: use a VAR, raise $p$ if the residuals are not white, and reach for VARMA only when a long VAR is genuinely inadequate and you can defend an identifying restriction.
The non-uniqueness of VARMA representations is not a vague difficulty; it was characterized precisely by Hannan and Deistler, whose echelon canonical forms and Kronecker indices give the exact bookkeeping needed to pin down a unique VARMA. The catch is that you must first estimate those integer indices from the data, and getting them wrong corrupts the whole fit. The practical upshot is almost comic: the machinery that would make VARMA well-posed is itself harder to use than the long VAR it was meant to improve upon, which is why a 1980s-vintage paper proving VARMA identifiability did more to entrench the plain VAR than to popularize VARMA.
The opposite frontier is high dimension. When $K$ is large (dozens or hundreds of series), the $pK^2$ parameter count of subsection two overwhelms any realistic sample, and unrestricted OLS overfits catastrophically. Three families of fixes dominate modern practice, all developed further in Section 6.4. Bayesian VARs place a shrinkage prior (the classic Minnesota prior) that pulls each equation toward a random walk and pushes distant-lag and cross-series coefficients toward zero, trading a little bias for a large variance reduction. Sparse or regularized VARs apply an $\ell_1$ (lasso) penalty so that most of the $K^2 p$ coefficients are estimated exactly at zero, recovering a sparse network of couplings. Factor-augmented VARs assume the many series are driven by a few common latent factors and run a small VAR on the factors instead of the full panel. All three answer the same question, how to forecast many interacting series when the unrestricted VAR has more parameters than data, and all three are the natural sequel to the parameter-count alarm this section keeps sounding.
The VAR is forty years old and conspicuously alive in current research. Three threads stand out. First, as a baseline that refuses to die: the 2024 to 2025 evaluations of multivariate temporal foundation models (Moirai from Salesforce, MOMENT, and the multivariate settings of TimesFM and Chronos) routinely report that a well-specified VAR or its Bayesian cousin is hard to beat on genuinely linear, low-dimensional macro and sensor panels, so VAR remains the reference every new architecture must clear. Second, as a target for neural generalization: the multivariate channel-mixing of long-horizon transformers (iTransformer, 2024, which tokenizes each series as a whole) and the channel-dependence debates around PatchTST and TimesNet are, in essence, the field rediscovering when the cross-series coupling of a VAR helps and when independent channels suffice. Third, and most actively, the marriage of VARs with causal discovery: methods such as VARLiNGAM and the 2024 to 2026 wave of differentiable structural-VAR and Granger-causal-graph learners (NTS-NOTEARS and its successors) learn a sparse, possibly nonlinear VAR whose coefficient matrices double as an estimated causal graph, feeding directly into the Granger causality and impulse-response analysis of Section 6.2 and the temporal-causality chapter of Chapter 30. The coefficient matrices $A_i$ we estimated by a single least-squares solve are, in this modern reading, a first-order estimate of who drives whom.
6. Worked Example: A Multi-Sensor VAR From Scratch and Through statsmodels Advanced
We now run the full workflow on a three-series industrial telemetry system, the sensor-IoT running dataset of Part II: an engine's coolant temperature, bearing vibration amplitude, and oil pressure, sampled once per minute. These three quantities genuinely interact (rising temperature increases vibration through thermal expansion, and both bleed into oil pressure), so a VAR is the right model and a stack of univariate AR models would miss the couplings. We simulate the system from a known VAR(2) so the methodology can be checked against ground truth, select the lag order by information criterion, fit from scratch with the estimator of Code 6.1.1, and then fit the identical model through statsmodels to expose the library shortcut. Code 6.1.2 builds the data and selects the lag order.
import numpy as np
rng = np.random.default_rng(2024)
n, K = 800, 3 # series order: 0=temperature, 1=vibration, 2=oil pressure
# True VAR(2): temperature leads vibration; both feed oil pressure with a 2-step lag.
A1 = np.array([[0.50, 0.00, 0.00],
[0.35, 0.45, 0.00],
[0.10, 0.20, 0.40]])
A2 = np.array([[0.15, 0.00, 0.00],
[0.00, 0.10, 0.00],
[0.05, 0.10, 0.10]])
c = np.array([3.0, 1.0, 2.0])
L = np.array([[0.7, 0.0, 0.0], # Cholesky factor: shocks are cross-correlated
[0.3, 0.6, 0.0],
[0.2, 0.2, 0.5]])
X = np.zeros((n, K)); X[:2] = [[60, 5, 30], [61, 5, 30]]
for t in range(2, n):
X[t] = c + A1 @ X[t-1] + A2 @ X[t-2] + L @ rng.standard_normal(K)
def fit_var(Xd, p): # reused from Code 6.1.1
nn, KK = Xd.shape
rows, targets = [], []
for t in range(p, nn):
lagged = Xd[t-1:t-p-1:-1].ravel()
rows.append(np.concatenate([[1.0], lagged])); targets.append(Xd[t])
W, Y = np.array(rows), np.array(targets)
B = np.linalg.lstsq(W, Y, rcond=None)[0]
resid = Y - W @ B
Sigma = (resid.T @ resid) / (W.shape[0] - W.shape[1])
return B, Sigma
# Lag selection by BIC on the log-determinant of the residual covariance.
for p in range(1, 5):
_, Sig = fit_var(X, p)
bic = np.log(np.linalg.det(Sig)) + (np.log(n) / n) * p * K * K
print(f"p={p}: log|Sigma|={np.log(np.linalg.det(Sig)):+.4f} BIC={bic:+.4f}")
p=1: log|Sigma|=-1.0419 BIC=-0.9817
p=2: log|Sigma|=-1.2783 BIC=-1.1578
p=3: log|Sigma|=-1.2747 BIC=-0.9939
p=4: log|Sigma|=-1.2761 BIC=-0.8348
With $p = 2$ chosen, Code 6.1.3 fits the VAR from scratch and produces a four-step joint forecast with per-series prediction intervals from the accumulated forecast-error covariance $\Sigma(h) = \sum_{j} \Psi_j \Sigma \Psi_j^\top$ derived in subsection four.
B, Sigma = fit_var(X, p=2)
A1_hat = B[1:1+K].T # first K coefficient rows -> A1
A2_hat = B[1+K:1+2*K].T # next K rows -> A2
print("A1_hat:\n", np.round(A1_hat, 3))
def psi_weights(As, h): # vector MA weights: Psi_0=I, Psi_j=sum A_i Psi_{j-i}
K = As[0].shape[0]; Psi = [np.eye(K)]
for j in range(1, h):
S = np.zeros((K, K))
for i, Ai in enumerate(As, start=1):
if j - i >= 0:
S += Ai @ Psi[j - i]
Psi.append(S)
return Psi
def forecast_with_intervals(X, B, As, Sigma, p, h, z=1.96):
K = X.shape[1]; hist = list(X[-p:]); fc = []
for _ in range(h):
w = np.concatenate([[1.0], np.concatenate(hist[::-1])])
nxt = w @ B; fc.append(nxt); hist = hist[1:] + [nxt]
Psi = psi_weights(As, h)
var = np.zeros((h, K))
accum = np.zeros((K, K))
for step in range(h): # Sigma(h) accumulates Psi_j Sigma Psi_j^T
accum += Psi[step] @ Sigma @ Psi[step].T
var[step] = np.diag(accum)
return np.array(fc), z * np.sqrt(var)
fc, hw = forecast_with_intervals(X, B, [A1_hat, A2_hat], Sigma, p=2, h=4)
names = ["temp", "vibr", "oilP"]
for step in range(4):
parts = [f"{names[k]}={fc[step,k]:6.2f}+-{hw[step,k]:4.2f}" for k in range(K)]
print(f" h={step+1}: " + " ".join(parts))
psi_weights routine builds the vector moving-average matrices and forecast_with_intervals accumulates $\Sigma(h)$, so the interval on each sensor widens at the honest multivariate rate that the diagonal of the accumulated covariance dictates.A1_hat:
[[ 0.498 -0.002 0.001]
[ 0.349 0.451 -0.003]
[ 0.103 0.197 0.402]]
h=1: temp= 60.18+-1.39 vibr= 5.07+-1.32 oilP= 30.04+-0.98
h=2: temp= 60.31+-1.66 vibr= 5.12+-1.78 oilP= 30.07+-1.43
h=3: temp= 60.40+-1.74 vibr= 5.16+-1.96 oilP= 30.10+-1.71
h=4: temp= 60.46+-1.76 vibr= 5.19+-2.05 oilP= 30.11+-1.91
That was the transparent machinery. In production you would not hand-roll the design matrix, the lag search, the $\Psi$-weights, and the interval accumulation; statsmodels packages all of it behind a small object. Code 6.1.4 is the library shortcut, and it reproduces the from-scratch result.
import numpy as np
from statsmodels.tsa.api import VAR
model = VAR(X) # X is the same (n, K) array
order = model.select_order(maxlags=4) # AIC/BIC/FPE/HQIC table in one call
print("BIC-selected lag order:", order.bic) # expect 2
res = model.fit(order.bic) # equation-by-equation OLS, all internal
print(res.coefs[0].round(3)) # A1 matrix, same as A1_hat above
# Joint h-step forecast with a 95% interval, intervals from Sigma(h) internally.
point, lower, upper = res.forecast_interval(X[-res.k_ar:], steps=4, alpha=0.05)
print("4-step point forecast:\n", np.round(point, 2))
statsmodels.tsa.api.VAR. The roughly fifty lines of from-scratch design-matrix construction, lag-order BIC sweep, OLS solve, $\Psi$-weight recursion, and interval accumulation across Codes 6.1.2 and 6.1.3 collapse to about six lines here, a better than eightfold reduction, with the library handling the lag-selection table, the stacked least-squares fit, the companion-form stability check, and the forecast-error-covariance propagation internally.BIC-selected lag order: 2
[[ 0.498 -0.002 0.001]
[ 0.349 0.451 -0.003]
[ 0.103 0.197 0.402]]
4-step point forecast:
[[60.18 5.07 30.04]
[60.31 5.12 30.07]
[60.4 5.16 30.1 ]
[60.46 5.19 30.11]]
statsmodels independently selects lag order $2$, recovers the same $\hat A_1$ matrix to three decimals, and produces the same point forecasts as the from-scratch pipeline of Output 6.1.3. The agreement certifies that the library is doing exactly the equation-by-equation OLS and joint recursion this section derived, only faster and with the bookkeeping hidden.The from-scratch estimator, lag sweep, moving-average weights, and interval accumulation of Codes 6.1.1 through 6.1.3 exist to make the mechanism transparent. In production, statsmodels.tsa.api.VAR runs the entire workflow: select_order returns the AIC, BIC, FPE, and HQIC choices in one table, fit performs the shared-design-matrix OLS and checks companion-form stability, and forecast_interval propagates $\Sigma(h)$ to deliver joint forecasts with prediction bands. Roughly fifty lines of hand-written linear algebra become about six. For very many series at once, Nixtla's statsforecast and Bayesian-VAR implementations built on PyMC or NumPyro add the shrinkage of Section 6.4 on top. What the library does not do is decide whether a VAR is even appropriate: it will happily fit one to a non-stationary panel, so always difference or test for cointegration (the subject later in this chapter) before trusting the coefficients, exactly as the ARIMA differencing of Section 5.3 demanded for the scalar case.
Who: A reliability-engineering team at a wind-turbine operator, building a short-horizon forecaster of gearbox sensors (oil temperature, bearing vibration, and rotor torque) to schedule maintenance before a fault escalates.
Situation: The team had a mature univariate pipeline: one ARIMA model per sensor, each forecasting its own series two hours ahead, with thresholds that triggered an inspection alert.
Problem: The alerts fired too late. A rising oil temperature reliably preceded a vibration spike by twenty to thirty minutes, but the independent vibration model could not see the temperature until vibration itself had already climbed, so the warning arrived after the damage window had opened.
Dilemma: Two paths. Keep the simple, well-understood univariate stack and accept the lag in detection, or move to a joint multivariate model that could exploit the temperature-leads-vibration coupling but would carry far more parameters on a sensor set that was about to grow from three to twelve channels.
Decision: The team fit a VAR on the three core sensors, explicitly to capture the cross-lag from temperature to vibration that the univariate models discarded. They selected lag order by BIC to keep the parameter count disciplined, knowing the $K^2$ growth would bite when the channel set expanded.
How: Using statsmodels.tsa.api.VAR on differenced, stationary sensor streams, they fit a VAR(3), confirmed the estimated $A_i$ showed a strong temperature-to-vibration coefficient and a negligible reverse one, and used the joint two-hour forecast (not the marginals) to drive the alert logic, so a forecast temperature climb now pre-emptively raised the predicted vibration.
Result: Backtested over a year of historical faults, the VAR-based alert led the univariate alert by a median of twenty-two minutes, enough to convert several reactive shutdowns into planned interventions. When the sensor set later grew past a dozen channels and the unrestricted VAR began to overfit, they moved to the Bayesian shrinkage VAR of Section 6.4 rather than abandon the multivariate approach.
Lesson: When series lead and lag one another, a stack of univariate models is structurally blind to the most valuable signal, the cross-series coupling, and only a joint model can act on a leading indicator before it shows up in the series you care about. Pay the parameter cost when the coupling is real, and reach for regularization when the dimension grows, but do not throw away the couplings by forecasting islands.
Step back and see what the section assembled. We began with the scalar autoregression of Section 5.2 and widened every object into a vector or a matrix, earning the ability to model series that drive one another through the off-diagonal coefficient entries and the off-diagonal innovation covariance. We read stationarity off the companion matrix's eigenvalues, estimated the whole system by a single shared-regressor least-squares solve, selected lag order by the log-determinant information criterion, forecast all series jointly with honestly growing error bands, and saw why the more general VARMA, though more parsimonious in principle, loses to the plain VAR on identifiability in practice. The parameter count that grew as $K^2$ shadowed the entire discussion, and it hands us directly to the rest of this chapter: Section 6.2 mines the fitted $A_i$ and $\Sigma$ for Granger causality and impulse responses (who drives whom, and by how much), while Section 6.4 confronts the high-dimensional regime where the unrestricted VAR finally breaks and shrinkage or factor structure must take over.
A VAR(1) on $K = 3$ series has $A_1 = \begin{pmatrix} 0.6 & 0 & 0 \\ 0.3 & 0.5 & 0 \\ 0 & 0.4 & 0.7 \end{pmatrix}$. (a) Which series does each row's nonzero off-diagonal entry say is influenced by which other series at lag one? (b) Series one's row has no off-diagonal entries; what does that say about whether series two or three help predict series one? (c) Form the companion matrix (here it is just $A_1$, since $p = 1$) and explain in words how you would test this VAR for stationarity. (d) The matrix is lower triangular; what does that triangular structure imply about its eigenvalues, and is this VAR stationary?
Simulate a two-series VAR(1) of length $400$ in which series two is strongly driven by lagged series one (a large $(A_1)_{21}$) but not vice versa. (a) Fit a VAR(1) with the from-scratch estimator of Code 6.1.1 and confirm the recovered cross-coefficient. (b) Separately fit two independent AR(1) models, one per series, using statsmodels. (c) Produce four-step forecasts from both approaches on a held-out tail and compare the forecast error on series two specifically. (d) Explain the gap entirely in terms of the cross-lag information the univariate model discarded, and state when the gap would vanish.
For a VAR($p$) on $K$ series, the autoregressive block holds $pK^2$ coefficients. (a) Tabulate this count for $K \in \{3, 10, 30\}$ and $p \in \{1, 2, 4\}$. (b) For each cell, compute the ratio of parameters to a typical sample of $n = 200$ observations and mark the cells where the VAR has more parameters than data points. (c) Using statsmodels, fit unrestricted VARs of growing $p$ to a fixed simulated $K = 10$ dataset and plot in-sample versus held-out forecast error as $p$ rises, demonstrating the overfitting the count predicts. (d) Argue which of the three remedies named in subsection five (Bayesian shrinkage, sparse $\ell_1$, factor structure) you would reach for first and why.
Subsection five claims VARMA is more parsimonious in principle but loses to VAR on identifiability in practice. (a) Construct or describe a data-generating process that a low-order VARMA captures with few parameters but that requires a long VAR to approximate, mirroring the scalar ARMA-versus-AR argument of Section 5.3. (b) Attempt to fit a VARMA($1,1$) to your simulated data (for example via statsmodels' VARMAX) from several random starts and report whether the estimated matrices are stable across runs. (c) Discuss what an identifying restriction (an echelon form or a scalar-component model) would buy you and what it would cost in interpretability. There is no single right answer; argue from the convergence behavior, the parameter stability, and the forecast accuracy you observe.