# 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`) matches `data/README.md`. There are no gaps and no duplicate months. The only missing cells are `neworder_chg` for 1992-01 and 1992-02. The full audit is in `out/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`): 1. The README says the 1992-01 values of every change series are empty, but the file has values for `ip_growth`, `oil_chg` and `permit_chg`. I used the values as given. Only `ip_growth` for 1992-01 enters the analysis, as a lag-2 feature, which the specification requires. 2. The extreme COVID-19 observations of 2020 are kept in every training window. 3. The data are the current vintage, not real-time. ## 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_growth` of month t. - **Expanding mean:** the mean of `ip_growth` over 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_ridge` vs `mae_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").