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; seedata/README.mdfor 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
monthvalue). Horizon h = 1: the target of origin t isip_growthof 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_growthof month t; expanding mean = the arithmetic mean ofip_growthover 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_growthof 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 μ, σ.
- training rows at origin t: all months s with 1992-03-01 ≤ s and s + 1 ≤ t, with target y_{s+1} =
- 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), columnsorigin_date,target_date,horizon(always 1),forecast,actual,model∈ {naive,mean,ridge}. Expected: 60 × 3 = 180 rows.out/results.jsonwith 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.mdwith the sections: Question; Data and exclusions; Method; Results; Uncertainty; Limitations and open issues.out/analysis.py— running~/wp29/venv/bin/python out/analysis.pyfrom the workspace root must regenerateout/results.jsonandout/forecasts.csvfromdata/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 modifydata/. - This task is designed to be completed in well under 20 minutes of work.