F3-conf-03 — Rolling-origin ridge forecasts of US industrial production growth
Question
Does a fixed-penalty ridge regression (λ = 50, key lambda) on eight standardised monthly predictors (n_features = 8) forecast next month's US industrial production growth (100 × Δln INDPRO, percent per month) better than the naive (last-value) forecast and the expanding historical mean? There are n_origins = 60 rolling origins, from first_origin = 2021-09-01 to last_origin = 2026-08-01, all at horizon h = 1.
Data and exclusions
- Input:
data/ip_predictors.csv, 417 data rows (n_input_rows), 1992-01 to 2026-09, contiguous months, no duplicates. The SHA-256 matches the README (sha256_input). - Missing values: only
neworder_chgfor 1992-01 and 1992-02. These months come before the training window, so nothing was imputed. - Exclusions (
out/exclusions.csv): two steps take 417 rows down to 413 training rows at the last origin (n_analysed= 413). Step 1 drops months before 1992-03-01, as the spec requires (2 rows). Step 2 drops feature months whose target would be dated after the last origin (2 rows). - Data issues are listed in
data_issues:- The README says 1992-01 is empty for every change series. In fact
ip_growth,oil_chgandpermit_chghave values for that month. I used them as given; onlyip_growthfor 1992-01 enters, as the 2-month lag for s = 1992-03. - The training windows contain extreme values (2020-04 = −14.1, 2008-09 = −4.5). I kept them because the specification is fixed.
- The data are current vintage and publication lags are ignored.
- The README says 1992-01 is empty for every change series. In fact
Method
All computations are in out/analysis.py, which is deterministic and uses no randomness (random_seed = null).
Baselines.
- Naive: the forecast is
ip_growthof month t. - Expanding mean: the forecast is the mean of
ip_growthover 1992-04..t.
Ridge model.
- Training rows: months s with 1992-03 ≤ s ≤ t−1, target
ip_growth_{s+1}. - Features: ip_growth at s, s−1 and s−2, plus oil_chg, fedfunds, permit_chg, neworder_chg and tcu at s.
- At every origin the features are standardised using the training-window mean and sample SD (ddof = 1), and the target is centred on its training mean ȳ (
normalisation_window= "expanding"). - Coefficients: b = (Z′Z + 50 I)⁻¹ Z′(y − ȳ). Forecast: ȳ + z̃_t·b.
- ȳ is the same quantity as the expanding-mean forecast. The ridge forecast is therefore the mean forecast plus a shrunken, predictor-based adjustment.
Evaluation. RMSE and MAE over the 60 forecasts. Actuals are ip_growth at t+1 in the current vintage.
Results
| model | RMSE (pp/month) | MAE (pp/month) |
|---|---|---|
| naive | 0.8831 (rmse_naive) |
0.6990 (mae_naive) |
| expanding mean | 0.5439 (rmse_mean) |
0.4293 (mae_mean) |
| ridge | 0.5608 (rmse_ridge) |
0.4514 (mae_ridge) |
- Ridge vs naive: ridge improves RMSE by 36.5% (
rel_improvement_vs_naive= 0.3649). - Ridge vs expanding mean: ridge is 3.1% worse in RMSE (
rel_improvement_vs_mean= −0.0310) and also worse in MAE. - Overall: ridge clearly beats the naive forecast but does not beat the expanding historical mean. The stronger of the two baselines is the mean (
baseline_best_rmse_model).
Uncertainty
I ran Diebold–Mariano tests on the squared-error differential, with the Harvey–Leybourne–Newbold small-sample correction and a t₅₉ reference distribution. For h = 1 no HAC lags are used.
- Naive vs ridge: DM = 3.36 (
dm_stat_naive_vs_ridge), p = 0.0014 (dm_pvalue_naive_vs_ridge). Ridge is significantly more accurate than naive. - Mean vs ridge: DM = −0.54 (
dm_stat_mean_vs_ridge), p = 0.59 (dm_pvalue_mean_vs_ridge). There is no significant difference; the mean's point advantage is within sampling noise.
With only 60 forecasts, the RMSE differences are imprecise. Ridge and the mean share the same intercept, so the comparison is nested-like, and a standard DM test can be conservative in that setting.
Limitations and open issues
- Data vintage: the data are the current (revised) vintage and publication lags are ignored. The results describe how predictable the revised series is under stylised timing, not real-time forecast performance.
- Fixed specification: λ = 50 and the feature set are fixed by the task and were not tuned. A different λ could change the ranking against the mean; I did not explore this because the task does not ask for it.
- Outliers: the COVID-era outliers (2020) in the training windows are not down-weighted, and they may distort the ridge coefficients.
- Sample size: the evaluation sample is short (60 months) and covers a single regime (2021–2026).
- README discrepancy: the README does not match the data on which 1992-01 cells are empty (see Data and exclusions). The effect is negligible, since only one lag value enters.
- Verification steps are recorded in
out/state/checks.md. They include an independent re-implementation run with all future data corrupted, which reproduced every forecast and so rules out look-ahead.