# F3-conf-03 — Rolling-origin ridge forecasts of US industrial production growth ## Question Does a fixed-penalty ridge regression (λ = 50; `lambda`) on eight standardised predictors forecast next-month US industrial production growth (100 × Δln INDPRO) better than the naive (last value) forecast and the expanding historical mean? I evaluated this over 60 rolling origins (`n_origins`), from `first_origin` = 2021-09-01 to `last_origin` = 2026-08-01, at horizon h = 1. The estimands are each model's out-of-sample RMSE and MAE, and the ridge model's relative RMSE improvement over each baseline. ## Data and exclusions The input is `data/ip_predictors.csv`: 417 rows (`n_input_rows`), monthly from 1992-01 to 2026-09. It is current-vintage FRED data, and its SHA-256 matches the README. There are no duplicate or missing months. The only empty cells are `neworder_chg` for 1992-01 and 1992-02. All data problems and how I handled them are listed in `data_issues`: - The README says the 1992-01 cells of every change series are empty. In the file, three of them (`ip_growth`, `oil_chg`, `permit_chg`) have values. I used the values as given. - The extreme COVID-19 months (2020-03 to 2020-06) are kept in every training window. - The data are revised (current vintage), and publication lags are ignored. Exclusion ledger (`out/exclusions.csv`), counted at the last origin: start with 417 rows. Dropping months before 1992-03-01 (the training start set in the specification) leaves 415. Dropping months whose target would fall after the origin leaves 413 training rows (`n_analysed`). ## Method - **Naive forecast:** `ip_growth` at the origin t. - **Mean forecast:** the arithmetic mean of `ip_growth` from 1992-04 to t. - **Ridge model:** trained on rows s from 1992-03 to t−1, with target `ip_growth` at s+1. - Features (`n_features` = 8): `ip_growth` at s, s−1 and s−2, plus `oil_chg`, `fedfunds`, `permit_chg`, `neworder_chg` and `tcu`. - Each feature is standardised with the mean and the sample standard deviation (ddof = 1) of that origin's training rows. These statistics are recomputed at every origin (`normalisation_window` = "expanding"). - The target is centred on its training mean ȳ, and b = (Z′Z + 50 I)⁻¹ Z′(y − ȳ). The intercept is not penalised. - Forecast = ȳ + z̃_t·b, where z̃_t is the origin's feature vector standardised with the same statistics. - **Evaluation:** errors are forecast − actual over the 60 target months, 2021-10 to 2026-09. - No randomness is involved (`random_seed` = null). ## Results | model | RMSE (pp/month) | MAE (pp/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 reduces RMSE by 36.5% (`rel_improvement_vs_naive` = 0.365). - **Ridge vs expanding mean:** ridge does *not* beat the mean. Its RMSE is 3.1% higher (`rel_improvement_vs_mean` = −0.031), and its MAE is also higher (`mae_ridge` vs `mae_mean`). The individual forecasts are in `out/forecasts.csv`: 180 rows, 60 per model. ## Uncertainty I ran Diebold–Mariano tests on squared-error loss for h = 1, with the Harvey–Leybourne–Newbold correction and a t(59) reference distribution: - **Ridge vs naive:** DM = 3.36, p = 0.0014 (`dm_stat_vs_naive`, `dm_pvalue_vs_naive`). The ridge model's advantage over the naive forecast is statistically significant. - **Ridge vs mean:** DM = −0.54, p = 0.59 (`dm_stat_vs_mean`, `dm_pvalue_vs_mean`). The two forecasts' accuracy cannot be told apart statistically. With only 60 forecast errors, the RMSE estimates are imprecise. I did not compute confidence intervals for the RMSEs themselves. ## Limitations and open issues - **Not real-time:** the data are current-vintage (revised) INDPRO, TCU and NEWORDER, and publication lags are ignored. These are pseudo-out-of-sample results, not real-time forecast performance. - **Fixed specification:** λ is fixed at 50 and not tuned, and the model is linear. Every training window includes the COVID-19 extremes, which may distort the coefficients. The specification defines the windows without exclusions, so I kept them. - **Narrow evaluation:** the window is short (60 months) and covers a single period, 2021–2026. - **README discrepancy:** the README's statement about empty 1992-01 cells does not match the file (see `data_issues`). - **Feasibility:** the task was completed in full (`feasibility` = "complete").