F3-conf-03: Rolling-origin ridge forecasts of US industrial production growth
Question
Does a ridge regression with a fixed penalty (λ = 50, key lambda) on 8 standardised monthly predictors (key n_features) forecast next month's US industrial production growth (100 × Δln INDPRO) better than two baselines: the naive (last value) forecast and the expanding historical mean? The evaluation uses 60 rolling origins (n_origins) from first_origin = 2021-09-01 to last_origin = 2026-08-01, at horizon h = 1.
Data and exclusions
- The input is
data/ip_predictors.csv: 417 data rows (n_input_rows), 1992-01 to 2026-09. Its SHA-256 (input_sha256) matchesdata/README.md. There are no gaps and no duplicate months. The only missing cells areneworder_chgfor 1992-01 and 1992-02. The full audit is inout/state/data_audit.md. - Exclusions (
out/exclusions.csv):- 2 rows dropped because they come before the specified training start of 1992-03.
- 2 rows dropped because their targets fall after the last origin.
- This leaves 413 training rows at the last origin (
n_analysed).
- Data issues (
data_issues):- The README says the 1992-01 values of every change series are empty, but the file has values for
ip_growth,oil_chgandpermit_chg. I used the values as given. Onlyip_growthfor 1992-01 enters the analysis, as a lag-2 feature, which the specification requires. - The extreme COVID-19 observations of 2020 are kept in every training window.
- The data are the current vintage, not real-time.
- The README says the 1992-01 values of every change series are empty, but the file has values for
Method
For each origin t = 2021-09, …, 2026-08, the target is ip_growth of month t+1. Only data dated ≤ t are used.
- Naive forecast:
ip_growthof month t. - Expanding mean: the mean of
ip_growthover 1992-04 to t. - Ridge regression:
- Training rows are s = 1992-03, …, t−1, with target y_{s+1}.
- The features are z_s = (ip_s, ip_{s−1}, ip_{s−2}, oil_chg_s, fedfunds_s, permit_chg_s, neworder_chg_s, tcu_s).
- Each feature is standardised with the mean and sample SD (ddof = 1) of that origin's training rows; this is recomputed at every origin (
normalisation_window= "expanding"). The target is centred on its training mean ȳ. - Coefficients: b = (Z′Z + 50·I)⁻¹ Z′(y − ȳ).
- Forecast: ȳ + ((z_t − μ)/σ)·b.
- The method is deterministic (
random_seed= null). - Forecast errors are actual − forecast. RMSE and MAE are computed over the 60 targets, 2021-10 to 2026-09.
- Implementation:
out/analysis.py. Forecasts:out/forecasts.csv(180 rows).
Results
| model | RMSE (%/month) | MAE (%/month) |
|---|---|---|
| naive | 0.883 (rmse_naive) |
0.699 (mae_naive) |
| expanding mean | 0.544 (rmse_mean) |
0.429 (mae_mean) |
| ridge (λ=50) | 0.561 (rmse_ridge) |
0.451 (mae_ridge) |
- Ridge vs naive: ridge's RMSE is 36.5% lower than the naive forecast's (
rel_improvement_vs_naive= 0.365). - Ridge vs expanding mean: ridge does not beat the expanding mean. Its RMSE is 3.1% higher (
rel_improvement_vs_mean= −0.031), and its MAE is also higher (mae_ridgevsmae_mean).
Answer: over these 60 origins, ridge is better than the naive baseline but slightly worse than the expanding historical mean.
Uncertainty
I ran Diebold–Mariano tests on squared-error loss (h = 1, Harvey–Leybourne–Newbold small-sample correction, t(59) reference distribution; see dm_note):
- Ridge vs naive: statistic 3.36 (
dm_stat_ridge_vs_naive), p = 0.0014 (dm_pvalue_ridge_vs_naive). Ridge's advantage over the naive forecast is statistically distinguishable from zero. - Ridge vs mean: statistic −0.54 (
dm_stat_ridge_vs_mean), p = 0.59 (dm_pvalue_ridge_vs_mean). The small RMSE difference between ridge and the expanding mean is not statistically distinguishable from zero.
Caveats on these tests:
- 60 targets is a short sample.
- The DM test treats the loss differential as stationary. With h = 1, it does not adjust for autocorrelation in the loss differential.
- The test ignores that the models are re-estimated on expanding windows, i.e. it does not account for parameter-estimation effects.
Limitations and open issues
- Not a real-time evaluation. All series are current-vintage (revised) data, and publication lags are ignored. INDPRO, TCU and NEWORDER are revised substantially, so these errors do not show historical real-time performance.
- Specification taken as given. λ = 50 is fixed by the task and not tuned. Other penalties, or outlier handling for the 2020 observations that sit in every training window, could change the ridge-vs-mean ranking. I did not explore these, because the task does not require it.
- Minor data discrepancy. The README and the data file disagree about which 1992-01 values are present. This does not materially affect the results: only one 1992 value enters the analysis, as a lag feature.
- Status. All required outputs were produced (
feasibility= "complete").