SPB Git

spb/wp29_uqo

Public
6.2 KB · 117 lines python
1"""F3-conf-03: rolling-origin ridge vs naive / expanding-mean forecasts of US IP growth (h=1).2Run from workspace root: ~/wp29/venv/bin/python out/analysis.py3Reads only data/; writes out/results.json, out/forecasts.csv, out/exclusions.csv. No randomness."""4import json, hashlib5import numpy as np6import pandas as pd78DATA = "data/ip_predictors.csv"9LAMBDA = 50.010FIRST_ORIGIN, LAST_ORIGIN = pd.Timestamp("2021-09-01"), pd.Timestamp("2026-08-01")11TRAIN_START = pd.Timestamp("1992-03-01")12MEAN_START = pd.Timestamp("1992-04-01")13FEATS = ["ip_growth", "ip_lag1", "ip_lag2", "oil_chg", "fedfunds", "permit_chg", "neworder_chg", "tcu"]1415raw = pd.read_csv(DATA)16n_raw = len(raw)17sha = hashlib.sha256(open(DATA, "rb").read()).hexdigest()18d = raw.copy()19d["month"] = pd.to_datetime(d["month"])20d = d.sort_values("month").set_index("month")21assert d.index.is_unique and (d.index.to_series().diff().dropna().dt.days.between(28, 31)).all()22# Lags use only past rows (shift forward in time); next-month target kept in a separate column23d["ip_lag1"] = d["ip_growth"].shift(1)24d["ip_lag2"] = d["ip_growth"].shift(2)25d["y_next"] = d["ip_growth"].shift(-1)   # target of row s = ip_growth of s+1 (only used when s+1 <= t)2627origins = pd.date_range(FIRST_ORIGIN, LAST_ORIGIN, freq="MS")28rows, n_train_last = [], None29for t in origins:30    tgt = t + pd.DateOffset(months=1)31    actual = float(d.loc[tgt, "ip_growth"])32    avail = d.loc[:t]                                   # information set: rows dated <= t33    # --- baselines ---34    naive = float(avail.loc[t, "ip_growth"])35    mean_f = float(avail.loc[MEAN_START:t, "ip_growth"].mean())36    # --- ridge ---37    tr = avail.loc[TRAIN_START:t - pd.DateOffset(months=1)]   # s+1 <= t38    X = tr[FEATS].to_numpy(float); y = tr["y_next"].to_numpy(float)39    assert np.isfinite(X).all() and np.isfinite(y).all()40    assert tr.index.max() + pd.DateOffset(months=1) <= t41    mu = X.mean(0); sd = X.std(0, ddof=1); ybar = y.mean()42    Z = (X - mu) / sd43    b = np.linalg.solve(Z.T @ Z + LAMBDA * np.eye(Z.shape[1]), Z.T @ (y - ybar))44    zt = (avail.loc[t, FEATS].to_numpy(float) - mu) / sd45    ridge = float(ybar + zt @ b)46    n_train_last = len(tr)47    for m, f in (("naive", naive), ("mean", mean_f), ("ridge", ridge)):48        rows.append(dict(origin_date=t.strftime("%Y-%m-%d"), target_date=tgt.strftime("%Y-%m-%d"),49                         horizon=1, forecast=f, actual=actual, model=m))5051fc = pd.DataFrame(rows)52fc.to_csv("out/forecasts.csv", index=False, float_format="%.10f")5354def err(m):55    e = fc.loc[fc.model == m, "actual"] - fc.loc[fc.model == m, "forecast"]56    return float(np.sqrt(np.mean(e ** 2))), float(np.mean(np.abs(e)))5758def dm(base):59    """Diebold-Mariano test (squared-error loss, h=1, Harvey et al. small-sample correction).60    d = e_base^2 - e_ridge^2; positive mean => ridge more accurate."""61    from scipy import stats62    e_b = (fc.loc[fc.model == base, "actual"] - fc.loc[fc.model == base, "forecast"]).to_numpy()63    e_r = (fc.loc[fc.model == "ridge", "actual"] - fc.loc[fc.model == "ridge", "forecast"]).to_numpy()64    dd = e_b ** 2 - e_r ** 2; T = len(dd)65    stat = dd.mean() / np.sqrt(dd.var(ddof=0) / T) * np.sqrt((T - 1) / T)   # HLN factor for h=166    return float(stat), float(2 * stats.t.sf(abs(stat), df=T - 1))6768rmse_n, mae_n = err("naive"); rmse_m, mae_m = err("mean"); rmse_r, mae_r = err("ridge")6970# exclusion ledger (row-level, for the training set at the last origin)71last = origins[-1]72ex = []73n0 = n_raw74n1 = int((d.index >= TRAIN_START).sum());          ex.append(("1", "month >= 1992-03-01 (training start per spec)", n0, n1))75n2 = int(((d.index >= TRAIN_START) & (d.index <= last - pd.DateOffset(months=1))).sum())76ex.append(("2", "s+1 <= last origin 2026-08-01 (target observed at origin)", n1, n2))77pd.DataFrame([dict(step=s, rule=r, n_before=a, n_after=b_, n_removed=a - b_,78                   rationale=("rows 1992-01/02 lack full lag/neworder features; spec starts at 1992-03" if s == "1"79                              else "rows dated after 2026-07 have targets after the last origin (information rule)"))80              for s, r, a, b_ in ex]).to_csv("out/exclusions.csv", index=False)81assert n2 == n_train_last8283res = {84    "task_id": "F3-conf-03",85    "n_origins": len(origins),86    "first_origin": origins[0].strftime("%Y-%m-%d"),87    "last_origin": origins[-1].strftime("%Y-%m-%d"),88    "lambda": LAMBDA,89    "normalisation_window": "expanding",90    "n_features": len(FEATS),91    "baseline_note": "naive and mean are the task-required baselines",92    "rmse_naive": rmse_n, "mae_naive": mae_n,93    "rmse_mean": rmse_m, "mae_mean": mae_m,94    "rmse_ridge": rmse_r, "mae_ridge": mae_r,95    "rel_improvement_vs_naive": 1 - rmse_r / rmse_n,96    "rel_improvement_vs_mean": 1 - rmse_r / rmse_m,97    "dm_stat_ridge_vs_naive": dm("naive")[0], "dm_pvalue_ridge_vs_naive": dm("naive")[1],98    "dm_stat_ridge_vs_mean": dm("mean")[0], "dm_pvalue_ridge_vs_mean": dm("mean")[1],99    "dm_note": "Diebold-Mariano, squared-error loss, h=1, HLN correction, t(59) reference; positive stat => ridge more accurate",100    "feasibility": "complete",101    "feasibility_reason": None,102    "data_issues": [103        {"issue": "data/README.md states the first month (1992-01) of every change series is empty, but ip_growth, oil_chg and permit_chg have values at 1992-01; only neworder_chg is missing (1992-01, 1992-02).",104         "handling": "Values used as given. Only ip_growth 1992-01 enters the analysis (lag-2 feature of the s=1992-03 training row, as specified); missing neworder_chg rows precede the training window."},105        {"issue": "Extreme COVID-19 observations (ip_growth 2020-04 = -14.14, 2020-06 = +6.35; oil_chg 2020-03..05) lie inside every training window.",106         "handling": "Kept; the task fully specifies the model without outlier treatment. Noted as a limitation."},107        {"issue": "Current-vintage (revised) data; publication lags ignored.",108         "handling": "Errors computed on the data as given; no real-time validity claimed."},109    ],110    "n_input_rows": {"ip_predictors.csv": n_raw},111    "input_sha256": {"ip_predictors.csv": sha},112    "n_analysed": n_train_last,113    "random_seed": None,114}115json.dump(res, open("out/results.json", "w"), indent=2)116print(json.dumps({k: res[k] for k in res if k.startswith(("rmse", "mae", "rel", "n_"))}, indent=2))117
Files · main
Menu

Theme