Causal DAG, Driver Selection and Model Results
The causal structure behind the Petro4Cast demand models, the drivers each model uses, the scenario effects, and the accuracy of the models against the univariate benchmark.
Production, inventory and trade nodes are folded into the refinery box here; the full graph is in Appendix A.
Note: The crude recovery of 2009 to 2011, the recovery of 2020 to 2022, the rate tightening of 2022 to 2023 and the PMI decoupling of 2022 to 2025 are macro regimes rather than dated shocks, and are not flagged.
Every price-on-demand effect conditions on macro activity, season, the demand-shock flags and lagged inventories, and never on same-week inventories, refinery output or CPI, which sit downstream of demand.
Weather and consumer mobility are both observed. DoWhy's unobserved-confounder test ran for all nine estimands and every effect kept its sign.
Prices enter at a lag of zero or more weeks and macro variables at one or more weeks, with no look-ahead on the weekly grid.
Effects are estimated on log year-on-year changes, with breaks dated at September 2001, September 2008, March 2020, February 2021 and March 2026.
All four scenario shocks lie inside the observed range: a 25% WTI rise against 59% observed in 2026, a 10-point utilization fall observed 11 times, a 5% industrial production fall observed in 2015 to 2016.
Placebo-treatment, random-common-cause and data-subset refuters ran for all nine estimands, and every estimate held.
Product supplied is an accounting identity, not a consumption survey, so supply nodes reach the demand models only through prices and the event flags.
The causal DAG produced the candidate drivers; however, this alone did not guarantee any of them a spot in the final model.
So we had to test even further:
To make the final model, a driver had to:
Oil Shock and Slowdown apply the DoWhy backdoor-adjusted effect to the baseline forecast, phased in over the lag of the estimand, because the regression coefficients on price and macro activity are confounded. Refinery Down runs through the fitted model: the utilization shock moves the crack spread, the crack spread moves retail prices, and the supply-disruption flag is set for the shock weeks. Every effect passed the placebo and data-subset refuters and was identical across every valid adjustment set DoWhy found.
| Scenario | Shock | Gasoline | Distillate | Jet fuel |
|---|---|---|---|---|
| Oil shock | WTI rises 25% | −0.36% demand (p = 0.003) | +0.24% demand, not significant | +0.32% demand, not significant |
| Refinery down | Utilization falls 10 points for 2 weeks | −3.4% output (p < 0.001) | −9.3% output (p < 0.001) | −14.5% output (p < 0.001) |
| Slowdown | Industrial production falls 5% | +0.89% demand, not significant | −3.18% demand (p = 0.012) | −2.72% demand |
| Counterfactual | Removing a flagged event to see what would have happened instead are on the Counterfactual page → | |||
MASE is the forecast error divided by the error of a seasonal-naive forecast, so 1.0 equals seasonal naive and lower is better. Held mode extrapolates the future driver values and never sees them. The benchmark is the better of univariate SARIMAX and ETS in each of 10 training-window and horizon combinations per fuel.
Gasoline and jet fuel beat the benchmark, both on average and in 7 of the 10 windows, but distillate did not. Each of its drivers held up individually in Section 4, yet combined, they underperform: giving the model their true future values scores worse than giving it guessed values, in every window. That means distillate's forecast relies on its own seasonal pattern and recent history rather than on its drivers.
Even so, the benchmark produces only a single forecast number, while the causal model also breaks down what is driving each forecast, applies backdoor-adjusted effects with refutation checks, and runs all four scenario branches.
Variable names match the companion Python module, which the forecasting notebook imports for the DoWhy identification step.
digraph { season; weather; cold_snap; ev_supply; ev_demand; geo; spr; rate; macro; mobility; cpi; wti; crack; p_gas; p_dsl; p_jet; crude_prod; crude_stk; util; prod_gas; prod_dst; prod_jet; trade_gas; trade_dst; trade_jet; stk_gas; stk_dst; stk_jet; d_gas; d_dst; d_jet; cold_snap -> ev_supply; weather -> d_dst; season -> util; season -> mobility; season -> d_gas; season -> d_dst; season -> d_jet; ev_supply -> util; ev_supply -> crude_prod; ev_demand -> macro; ev_demand -> mobility; geo -> wti; spr -> wti; spr -> crude_stk; rate -> macro; macro -> d_dst; macro -> d_jet; macro -> d_gas; macro -> mobility; mobility -> d_gas; mobility -> d_jet; macro -> wti; crude_prod -> wti; crude_prod -> crude_stk; wti -> p_gas; wti -> p_dsl; wti -> p_jet; crack -> p_gas; crack -> p_dsl; crack -> p_jet; util -> crack; p_gas -> d_gas; p_dsl -> d_dst; p_jet -> d_jet; p_gas -> cpi; p_dsl -> cpi; crude_stk -> util; util -> prod_gas; util -> prod_dst; util -> prod_jet; prod_gas -> stk_gas; prod_dst -> stk_dst; prod_jet -> stk_jet; trade_gas -> stk_gas; trade_dst -> stk_dst; trade_jet -> stk_jet; crack -> trade_gas; crack -> trade_dst; crack -> trade_jet; prod_gas -> d_gas; prod_dst -> d_dst; prod_jet -> d_jet; }
Four feedback relationships enter at a one-week lag to keep the graph acyclic and are not drawn: product stocks to the crack spread, the crack spread to utilization, product stocks to trade, and gasoline and distillate demand to utilization.