SPB Git

spb/wp29_uqo

Public

# F3-conf-03: Rolling-origin ridge forecasts of US industrial production growth

# Question

Does a ridge regression with a fixed penalty (λ = 50, key lambda) on 8 standardised monthly predictors (key n_features) forecast next month's US industrial production growth (100 × Δln INDPRO) better than two baselines: the naive (last value) forecast and the expanding historical mean? The evaluation uses 60 rolling origins (n_origins) from first_origin = 2021-09-01 to last_origin = 2026-08-01, at horizon h = 1.

# Data and exclusions

  • The input is data/ip_predictors.csv: 417 data rows (n_input_rows), 1992-01 to 2026-09. Its SHA-256 (input_sha256) matches data/README.md. There are no gaps and no duplicate months. The only missing cells are neworder_chg for 1992-01 and 1992-02. The full audit is in out/state/data_audit.md.
  • Exclusions (out/exclusions.csv):
    • 2 rows dropped because they come before the specified training start of 1992-03.
    • 2 rows dropped because their targets fall after the last origin.
    • This leaves 413 training rows at the last origin (n_analysed).
  • Data issues (data_issues):
    1. The README says the 1992-01 values of every change series are empty, but the file has values for ip_growth, oil_chg and permit_chg. I used the values as given. Only ip_growth for 1992-01 enters the analysis, as a lag-2 feature, which the specification requires.
    2. The extreme COVID-19 observations of 2020 are kept in every training window.
    3. The data are the current vintage, not real-time.

# Method

For each origin t = 2021-09, …, 2026-08, the target is ip_growth of month t+1. Only data dated ≤ t are used.

  • Naive forecast: ip_growth of month t.
  • Expanding mean: the mean of ip_growth over 1992-04 to t.
  • Ridge regression:
    • Training rows are s = 1992-03, …, t−1, with target y_{s+1}.
    • The features are z_s = (ip_s, ip_{s−1}, ip_{s−2}, oil_chg_s, fedfunds_s, permit_chg_s, neworder_chg_s, tcu_s).
    • Each feature is standardised with the mean and sample SD (ddof = 1) of that origin's training rows; this is recomputed at every origin (normalisation_window = "expanding"). The target is centred on its training mean ȳ.
    • Coefficients: b = (Z′Z + 50·I)⁻¹ Z′(y − ȳ).
    • Forecast: ȳ + ((z_t − μ)/σ)·b.
  • The method is deterministic (random_seed = null).
  • Forecast errors are actual − forecast. RMSE and MAE are computed over the 60 targets, 2021-10 to 2026-09.
  • Implementation: out/analysis.py. Forecasts: out/forecasts.csv (180 rows).

# Results

model RMSE (%/month) MAE (%/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's RMSE is 36.5% lower than the naive forecast's (rel_improvement_vs_naive = 0.365).
  • Ridge vs expanding mean: ridge does not beat the expanding mean. Its RMSE is 3.1% higher (rel_improvement_vs_mean = −0.031), and its MAE is also higher (mae_ridge vs mae_mean).

Answer: over these 60 origins, ridge is better than the naive baseline but slightly worse than the expanding historical mean.

# Uncertainty

I ran Diebold–Mariano tests on squared-error loss (h = 1, Harvey–Leybourne–Newbold small-sample correction, t(59) reference distribution; see dm_note):

  • Ridge vs naive: statistic 3.36 (dm_stat_ridge_vs_naive), p = 0.0014 (dm_pvalue_ridge_vs_naive). Ridge's advantage over the naive forecast is statistically distinguishable from zero.
  • Ridge vs mean: statistic −0.54 (dm_stat_ridge_vs_mean), p = 0.59 (dm_pvalue_ridge_vs_mean). The small RMSE difference between ridge and the expanding mean is not statistically distinguishable from zero.

Caveats on these tests:

  • 60 targets is a short sample.
  • The DM test treats the loss differential as stationary. With h = 1, it does not adjust for autocorrelation in the loss differential.
  • The test ignores that the models are re-estimated on expanding windows, i.e. it does not account for parameter-estimation effects.

# Limitations and open issues

  • Not a real-time evaluation. All series are current-vintage (revised) data, and publication lags are ignored. INDPRO, TCU and NEWORDER are revised substantially, so these errors do not show historical real-time performance.
  • Specification taken as given. λ = 50 is fixed by the task and not tuned. Other penalties, or outlier handling for the 2020 observations that sit in every training window, could change the ridge-vs-mean ranking. I did not explore these, because the task does not require it.
  • Minor data discrepancy. The README and the data file disagree about which 1992-01 values are present. This does not materially affect the results: only one 1992 value enters the analysis, as a lag feature.
  • Status. All required outputs were produced (feasibility = "complete").
Files · 4f59ed568641ceda6f39552d5cc3a441aa812088
Menu

Theme