# F3-conf-03 — Rolling-origin ridge forecasts of US industrial production growth with standardised predictors ## Scientific question Does a fixed-penalty ridge regression on eight standardised monthly predictors forecast next month's US industrial production growth better than two simple baselines — the naive (last value) forecast and the expanding historical mean — over 60 rolling origins from 2021-09 to 2026-08? The estimands are the out-of-sample RMSE and MAE of the three forecasts (naive, expanding mean, ridge) at horizon h = 1 month, and the relative improvements of the ridge forecast over each baseline, 1 − RMSE_ridge / RMSE_baseline. ## Inputs - `data/ip_predictors.csv` — observed data derived from six FRED series (INDPRO, MCOILWTICO, FEDFUNDS, PERMIT, NEWORDER, TCU; retrieved 2026-10-30 UTC; current vintage; see `data/README.md` for URLs, retrieval times, transformations, SHA-256 and licence). Columns: `month` (YYYY-MM-01), `ip_growth` (target: 100 × Δln of the industrial production index, percent per month), `oil_chg` (100 × Δln WTI monthly price), `fedfunds` (effective federal funds rate, percent, level), `permit_chg` (100 × Δln building permits), `neworder_chg` (100 × Δln core capital goods orders), `tcu` (capacity utilisation, percent of capacity, level). 417 monthly rows, 1992-01 to 2026-09; the first one or two rows of the change columns are empty. ## Information-availability rules - Forecast origins: the 60 months t = 2021-09-01, …, 2026-08-01 (the `month` value). Horizon h = 1: the target of origin t is `ip_growth` of month t + 1. - At origin t, every value dated ≤ t is treated as available (publication lags are ignored in this exercise, as stated in `data/README.md`); no value dated > t may be used in any way. In particular, **every statistic used to transform or scale the data (means, standard deviations, target mean) must be computed from the training window of that origin only**, and recomputed at every origin. - Baselines (both required): naive = `ip_growth` of month t; expanding mean = the arithmetic mean of `ip_growth` over the months 1992-04-01 to t inclusive. - The ridge model (fully specified; implement exactly this): - training rows at origin t: all months s with 1992-03-01 ≤ s and s + 1 ≤ t, with target y_{s+1} = `ip_growth` of month s + 1 and feature vector z_s = (ip_growth_s, ip_growth_{s−1}, ip_growth_{s−2}, oil_chg_s, fedfunds_s, permit_chg_s, neworder_chg_s, tcu_s) (8 features); - standardise each feature as (z − μ_j) / σ_j, with μ_j the mean and σ_j the sample standard deviation (denominator n − 1) over the training rows of that origin; centre the target with its training mean ȳ; - coefficients b = (Z′Z + λ I)⁻¹ Z′ (y − ȳ) with λ = 50, Z the standardised training feature matrix (no intercept column; the intercept is ȳ and is not penalised); - forecast = ȳ + ((z_t − μ) / σ) · b, where z_t is the feature vector at the origin month t, standardised with the same μ, σ. - Errors are computed on the current-vintage data as given; do not claim real-time validity. ## Required outputs - `out/forecasts.csv` — one row per (origin, model), columns `origin_date`, `target_date`, `horizon` (always 1), `forecast`, `actual`, `model` ∈ {`naive`, `mean`, `ridge`}. Expected: 60 × 3 = 180 rows. - `out/results.json` with the keys: | key | type | unit | meaning | |---|---|---|---| | `task_id` | string | — | "F3-conf-03" | | `n_origins` | integer | — | 60 | | `first_origin`, `last_origin` | string | date | first and last origin dates | | `lambda` | number | — | ridge penalty used (50) | | `normalisation_window` | string | — | how the standardisation statistics were computed: "expanding" (training window of each origin), "full_sample", or "none" | | `n_features` | integer | — | number of ridge features (8) | | `rmse_naive`, `mae_naive` | number | percent per month | naive-forecast errors | | `rmse_mean`, `mae_mean` | number | percent per month | expanding-mean errors | | `rmse_ridge`, `mae_ridge` | number | percent per month | ridge-forecast errors | | `rel_improvement_vs_naive` | number | fraction | 1 − rmse_ridge / rmse_naive | | `rel_improvement_vs_mean` | number | fraction | 1 − rmse_ridge / rmse_mean | | `feasibility` | string | — | "complete", "partial" or "infeasible" | | `feasibility_reason` | string or null | — | explanation if not complete | | `data_issues` | array of {"issue","handling"} | — | data problems detected and how handled (empty list if none) | | `n_input_rows` | object | — | {"ip_predictors.csv": number of data rows read, excluding the header} | | `n_analysed` | integer | — | training rows at the last origin | | `random_seed` | integer or null | — | seed used, if any | - `out/report.md` with the sections: Question; Data and exclusions; Method; Results; Uncertainty; Limitations and open issues. - `out/analysis.py` — running `~/wp29/venv/bin/python out/analysis.py` from the workspace root must regenerate `out/results.json` and `out/forecasts.csv` from `data/` only, deterministically. - `out/claims.json` — a list of the central claims of the report: [{"id": "C1", "text": "...", "results_keys": ["..."], "status": "supported"|"unsupported"|"not_evaluated", "caveat": "..."}]. ## Constraints - No network access. Python: `~/wp29/venv/bin/python` (numpy, pandas, scipy, statsmodels available). Do not modify `data/`. - This task is designed to be completed in well under 20 minutes of work.