# 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_chg` for 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_chg` and `permit_chg` have values for that month. I used them as given; only `ip_growth` for 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. ## Method All computations are in `out/analysis.py`, which is deterministic and uses no randomness (`random_seed` = null). **Baselines.** - Naive: the forecast is `ip_growth` of month t. - Expanding mean: the forecast is the mean of `ip_growth` over 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.