The oil market SVAR — I. Not all oil price shocks are alike (Kilian)¶
Vector autoregressions, Part 8a: identifying oil supply and demand¶
For decades the oil price was treated as an exogenous supply shock: price up $\Rightarrow$ recession (Hamilton 1983). But the oil price is a price — set by supply and demand — so "an oil price increase" is not one event. A rise because OPEC cut output is economically different from a rise because a global boom is pulling up demand for all industrial commodities, which is different again from a rise driven by precautionary fears of future shortfalls. Kilian (2009) disentangles them inside a structural VAR, and the punchline reshaped the field: the sources of an oil price change matter more than its size, and most large price movements have been demand-driven, not supply disruptions.
This is Part 8 of the VAR series and its first genuinely non-monetary structural application. It reuses the identification ideas of Part 2 (recursive SVAR, IRFs) and adds two tools not yet seen here — the forecast-error variance decomposition and the historical decomposition — in a new self-contained module oilsvar.py. A companion notebook takes up the identification debate the recursive scheme provoked.
The model and the recursive identification¶
A monthly VAR($p{=}24$) in three variables (1974–2019): $$y_t = \big(\underbrace{\Delta\text{oil production}}_{\text{\% change}},\ \underbrace{\text{global real activity}}_{\text{Kilian index}},\ \underbrace{\text{real price of oil}}_{\log}\big)'.$$ The Kilian index of global real economic activity is built from dry-cargo ocean shipping freight rates — a measure of the worldwide industrial-commodity business cycle. The real oil price is the refiners' acquisition cost of imported crude, deflated by US CPI.
Identification is recursive (Cholesky) with this ordering, and each zero is a defensible within-the-month timing restriction:
- Oil supply shock — crude production is essentially vertical in the short run (adjusting output is slow and costly; OPEC meets infrequently), so oil production does not respond to demand within the month — only its own shock moves it on impact.
- Aggregate demand shock — global real activity reacts to an oil-supply shock within the month, but not to oil-specific demand (broad industrial activity does not jump on oil-inventory news within a month).
- Oil-specific (precautionary) demand shock — the real oil price is the fast, forward-looking variable and absorbs all three shocks on impact, including precautionary/speculative demand for oil in anticipation of future supply shortfalls.
The three structural shocks are therefore a supply disruption, an aggregate-demand expansion, and an oil-specific (precautionary) demand shock. We estimate the reduced form by OLS and draw the structural impulse responses over the diffuse Normal-inverse-Wishart posterior for Bayesian bands; each shock is sign-normalised to raise the real oil price so the three are comparable.
import numpy as np, pandas as pd, matplotlib.pyplot as plt
import oilsvar as O
d = pd.read_csv("oilsvar_data.csv", index_col=0, parse_dates=True)
Y = d[["prod_growth", "rea", "rpo"]].values; dates = d.index; p = 24; H = 18
fit = O.fit_var(Y, p) # reduced-form VAR(24), OLS
shocklab = ["Oil supply disruption", "Aggregate demand", "Oil-specific demand"]
# structural IRFs over the Normal-IW posterior; sign-normalise each shock to raise the real oil price (var 2)
draws = O.posterior_chol_irf(fit, H, ndraw=1000, seed=1) # (1000, H+1, 3, 3)
draws *= np.sign(draws[:, 0, 2, :])[:, None, None, :]
lo, med, hi = O.bands(draws)
print("Response of REAL OIL PRICE (impact / 12m):")
for j in range(3): print(" %-22s %5.2f / %5.2f" % (shocklab[j], med[0, 2, j], med[12, 2, j]))
# figure: 3x3 IRF grid (rows = responses, cols = shocks)
vlab = ["Oil production growth", "Global real activity", "Real oil price"]
hz = np.arange(H + 1)
fig, ax = plt.subplots(3, 3, figsize=(13, 9), sharex=True)
for r in range(3):
for sh in range(3):
a = ax[r, sh]
a.fill_between(hz, lo[:, r, sh], hi[:, r, sh], color="firebrick", alpha=.15)
a.plot(hz, med[:, r, sh], color="firebrick", lw=1.6)
a.axhline(0, color="k", lw=.5); a.grid(alpha=.25)
if r == 0: a.set_title(shocklab[sh], fontsize=10)
if sh == 0: a.set_ylabel(vlab[r], fontsize=9)
for sh in range(3): ax[2, sh].set_xlabel("months")
fig.suptitle("Structural impulse responses of the oil market (Kilian recursive SVAR, 68% posterior bands)", y=1.005)
fig.tight_layout(); plt.show()
Response of REAL OIL PRICE (impact / 12m): Oil supply disruption 0.52 / 1.13 Aggregate demand 1.05 / 3.96 Oil-specific demand 6.50 / 7.24
Reading the impulse responses¶
The three columns are genuinely different shocks:
- A supply disruption (production falls) raises the real oil price only modestly and gradually (impact +0.5) — in the short run demand is inelastic but the price effect of a production shortfall is small and slow.
- An aggregate-demand expansion raises the price with a hump shape that builds over a year (+1.0 on impact, ~+4 by 12 months), as a booming world economy pulls oil demand up and drags production with it.
- An oil-specific (precautionary) demand shock produces an immediate, large jump (+6.5 on impact) that persists — this is the forward-looking, inventory/speculative response to expected future scarcity.
Same-sized "oil price increase," three completely different dynamic signatures and, as the next panels show, three different weights in the data.
# Forecast-error variance decomposition: how much of each variable's variance each shock explains.
fe = O.fevd(fit, H) # (H+1, 3, 3): [horizon, variable, shock]
print("FEVD of real oil price at h=18:", dict(zip(["supply", "aggdem", "oilspec"], [float(x) for x in np.round(fe[18, 2], 2)])))
# stacked FEVD of the real oil price and of global real activity, by structural shock
import matplotlib.pyplot as plt
h = np.arange(H + 1)
cols = ["firebrick", "navy", "seagreen"]
fig, axes = plt.subplots(1, 2, figsize=(10, 4), sharey=True)
for ax, vi, name in zip(axes, [2, 1], ["Real oil price", "Global real activity"]):
ax.stackplot(h, fe[:, vi, 0], fe[:, vi, 1], fe[:, vi, 2], labels=shocklab, colors=cols, alpha=0.85)
ax.set_title(name); ax.set_xlabel("horizon (months)")
ax.set_xlim(0, H); ax.set_ylim(0, 1); ax.grid(alpha=.25)
axes[0].set_ylabel("share of forecast-error variance")
axes[0].legend(loc="lower left", fontsize=8, framealpha=.9)
fig.suptitle("Forecast-error variance decomposition", y=1.02)
fig.tight_layout(); plt.show()
FEVD of real oil price at h=18: {'supply': 0.02, 'aggdem': 0.17, 'oilspec': 0.81}
Reading the variance decomposition¶
Supply shocks explain almost none of the real oil price — about 2% at a 18-month horizon. The price is driven overwhelmingly by demand: ~17% aggregate demand and ~81% oil-specific (precautionary) demand. This is the result that overturned the old "oil = exogenous supply shock" view. The activity panel is the mirror image: global real activity is (by construction and in the data) driven by its own aggregate-demand shock, not by oil-market disturbances.
# Historical decomposition: attribute the realised path of the real oil price to the three shocks.
hd = O.historical_decomp(fit) # (Teff, 3, 3): [t, variable, shock]
contrib = hd[:, 2, :] # contributions to the real oil price
runup = contrib[(dates[p:] >= "2003-01") & (dates[p:] <= "2008-06")].sum(0)
print("2003-2008 run-up: cumulative contribution by shock:", dict(zip(["supply", "aggdem", "oilspec"], [float(x) for x in np.round(runup, 0)])))
# stacked contribution of the three structural shocks to the real oil price, 1976-2019
import matplotlib.pyplot as plt
t = dates[p:]
cols = ["firebrick", "navy", "seagreen"]
fig, ax = plt.subplots(figsize=(11, 4.5))
pos = np.zeros(len(t)); neg = np.zeros(len(t))
for j, (lab, c) in enumerate(zip(shocklab, cols)):
v = contrib[:, j]
base = np.where(v >= 0, pos, neg)
ax.bar(t, v, width=28, bottom=base, color=c, label=lab, linewidth=0)
pos = pos + np.where(v >= 0, v, 0.0)
neg = neg + np.where(v < 0, v, 0.0)
ax.plot(t, contrib.sum(1), color="k", lw=1.0, label="Real oil price (dev. from base)")
ax.axhline(0, color="k", lw=.5)
ax.axvspan(pd.Timestamp("2003-01-01"), pd.Timestamp("2008-06-01"), color="grey", alpha=.12)
ax.text(pd.Timestamp("2005-06-01"), ax.get_ylim()[1] * 0.92, "2003-08 run-up",
ha="center", fontsize=8, color="dimgray")
ax.set_title("Historical decomposition of the real oil price into the three structural shocks")
ax.set_ylabel("contribution (100 x log real oil price, dev.)")
ax.grid(alpha=.25); ax.margins(x=.01)
ax.legend(loc="upper left", fontsize=8, ncol=2, framealpha=.9)
fig.tight_layout(); plt.show()
2003-2008 run-up: cumulative contribution by shock: {'supply': -161.0, 'aggdem': 432.0, 'oilspec': 887.0}
Results — the big oil episodes, decomposed¶
- The 2003–2008 super-spike was a demand event. Over the run-up, aggregate demand (+432) and oil-specific/precautionary demand (+887) account for essentially the entire rise in the real oil price, while supply subtracted from it (−161) — production was, if anything, growing. The China-led global boom and precautionary demand, not an OPEC squeeze, drove oil to $140. That is Kilian's headline, reproduced here on updated data.
- Different episodes, different mixes. The 1970s spikes (Iranian revolution, Gulf War) carry a genuine supply-disruption component; the 1986 and 2014 collapses show supply/demand interplay — the historical decomposition separates them, where a univariate "oil price" cannot.
- Why it matters for the macro. Because these shocks have different origins, they have different consequences for output and inflation: a demand-driven oil price rise coincides with a strong economy, a supply-driven one does not — so the macro response to "an oil price increase" is only well-defined once its source is known.
This recursive scheme is powerful but its within-month timing zeros are debatable. The companion notebook (Part 8b) relaxes them — set-identifying the same shocks with sign restrictions and short-run elasticity bounds (Kilian-Murphy) and a Bayesian prior on the elasticities (Baumeister-Hamilton) — and asks how much the conclusions move. The R cross-check reproduces the IRFs and FEVD with vars.
References¶
- Kilian, L. (2009). Not all oil price shocks are alike: disentangling demand and supply shocks in the crude oil market. American Economic Review 99, 1053–1069.
- Kilian, L. & Zhou, X. (2018). Modeling fluctuations in the global real price of oil. J. Applied Econometrics.
- Hamilton, J. D. (1983). Oil and the macroeconomy since World War II. J. Political Economy 91, 228–248.
Data: oilsvar_data.csv — world oil production (EIA), the Kilian real-activity index (Dallas Fed), and the real refiners' acquisition cost of imported crude (EIA, deflated by BLS CPI); monthly 1974–2019.
Next: oilsvar_R.ipynb — the R cross-check (vars).