# F3-conf-03 — Rolling-origin ridge forecasts of US industrial production growth ## Question Does a fixed-penalty ridge regression (λ = 50) on eight standardised monthly predictors forecast next month's US industrial production growth (100 × Δln INDPRO, % per month) better than (a) the naive last-value forecast and (b) the expanding historical mean, over 60 rolling origins from 2021-09 to 2026-08 (horizon h = 1)? ## Data and exclusions - `data/ip_predictors.csv`: 417 monthly rows, 1992-01 to 2026-09, current-vintage FRED data (see `data/README.md`). No rows were dropped. - Missing values: only `neworder_chg` for 1992-01 and 1992-02. Neither falls in any training window (training rows start at 1992-03), so no imputation was needed. - Minor discrepancy: the README says the first month of every change series is empty, but `ip_growth`, `oil_chg` and `permit_chg` are populated for 1992-01. I kept the values as given. `ip_growth` 1992-01 enters only as the second lag in the 1992-03 training row, which is what the task specification implies. - The extreme COVID-19 months of 2020 (ip_growth from −14.14 to +6.35 %) stay in every training window, as the specification requires. They inflate the standardisation statistics and affect the coefficients. ## Method - Origins t = 2021-09-01 … 2026-08-01 (60). Each origin's target is `ip_growth` at t+1. - Naive forecast: `ip_growth` at t. - Expanding-mean forecast: the mean of `ip_growth` from 1992-04 to t. - Ridge: the training rows are s = 1992-03 … t−1, with target y_{s+1}. The features are ip_growth_s, ip_growth_{s−1} and ip_growth_{s−2}, plus oil_chg_s, fedfunds_s, permit_chg_s, neworder_chg_s and tcu_s. - At every origin I recompute the feature means and sample SDs (ddof = 1) and the target mean from that origin's training rows only. `normalisation_window = "expanding"`. - b = (Z′Z + 50 I)⁻¹ Z′(y − ȳ), with no penalised intercept. Forecast = ȳ + ((z_t − μ)/σ)·b. - The code asserts that no training target is dated after t. - I scored errors against current-vintage actuals and computed RMSE and MAE over the 60 origins. - `out/analysis.py` regenerates `results.json`, `forecasts.csv` (180 rows) and `uncertainty.json`. ## Results | model | RMSE (%/month) | MAE (%/month) | |---|---|---| | naive | 0.8831 | 0.6990 | | expanding mean | 0.5439 | 0.4293 | | ridge (λ=50) | 0.5608 | 0.4514 | - Relative RMSE improvement of ridge over naive: 1 − 0.5608/0.8831 = **+0.365** (36.5 % lower RMSE). - Relative RMSE improvement of ridge over the expanding mean: **−0.031**. Ridge's RMSE is 3.1 % *higher* than the mean's, and its MAE is also higher. So the ridge model clearly beats the naive forecast but does **not** beat the expanding historical mean over this evaluation window. At the one-month horizon, the eight predictors add no out-of-sample value over the unconditional mean. ## Uncertainty These diagnostics are in `out/uncertainty.json`, produced by the same script. - **Diebold–Mariano tests** (squared loss, with the Harvey–Leybourne–Newbold small-sample correction and a t(59) reference): - ridge vs naive: statistic −3.36, p = 0.0014. Ridge is significantly better. - ridge vs mean: statistic +0.54, p = 0.59. There is no significant difference. - **Moving-block bootstrap** (block 6, B = 5000, seed 12345), 95 % intervals for the relative RMSE improvement: - vs naive: [0.248, 0.426]. - vs mean: [−0.158, 0.056]. - With only 60 evaluation months, a small loss to the mean cannot be told apart from a small gain. ## Limitations and open issues - **Data vintage.** I used current-vintage, revised data and ignored publication lags (INDPRO, TCU and NEWORDER are revised substantially). These results are **not** real-time forecast performance. - **Model choices.** λ = 50 is fixed by the task, not tuned. The level variables (fedfunds, tcu) enter untransformed. The COVID outliers are untreated. Each of these could change the ridge results. - **Evaluation window.** It is one short window (2021-09 to 2026-08) and includes the post-pandemic recovery. The relative ranking may not generalise to other periods. - **Uncertainty method.** The DM test and the bootstrap both assume approximate stationarity of loss differentials. The bootstrap seed does not affect any value in `results.json`.