SPB Git

spb/wp29_uqo

Public
5.8 KB · 122 lines python
1"""F6-conf-01: weekday effects in daily changes of the 10y Treasury yield (DGS10), 2000-2019.2Run from workspace root: ~/wp29/venv/bin/python out/analysis.py3Reads only data/DGS10.csv. No randomness is used (random_seed = null)."""4import json, os5import numpy as np, pandas as pd6from scipy import stats7import matplotlib8matplotlib.use("Agg")9import matplotlib.pyplot as plt1011OUT = "out"; os.makedirs(OUT, exist_ok=True)12DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]1314raw = pd.read_csv("data/DGS10.csv", dtype=str, keep_default_na=False)15n_raw = len(raw)16raw["date"] = pd.to_datetime(raw["observation_date"], format="%Y-%m-%d")17val = raw["DGS10"].str.strip()18missing = val == ""19n_missing = int(missing.sum())20obs = raw.loc[~missing, ["date"]].copy()21obs["y"] = val[~missing].astype(float).values22obs = obs.sort_values("date").reset_index(drop=True)23obs["dy"] = 100.0 * obs["y"].diff()24ch = obs.dropna(subset=["dy"]).copy()25ch["dy"] = ch["dy"].round(10)  # remove float noise from percent->bp conversion26ch["weekday"] = ch["date"].dt.dayofweek.map(dict(enumerate(DAYS)))27assert ch["weekday"].notna().all()2829# exclusion ledger30excl = pd.DataFrame([31    dict(step=1, rule="drop rows with empty DGS10 field (no published yield)", n_before=n_raw,32         n_after=n_raw - n_missing, n_removed=n_missing, rationale="TASK rule: holidays/no publication"),33    dict(step=2, rule="first available observation has no prior value -> no change", n_before=n_raw - n_missing,34         n_after=len(ch), n_removed=n_raw - n_missing - len(ch), rationale="TASK rule: change undefined"),35])36excl.to_csv(f"{OUT}/exclusions.csv", index=False)3738cut = pd.Timestamp("2009-12-31")39train = ch[ch["date"] <= cut]; test = ch[ch["date"] >= pd.Timestamp("2010-01-01")]40assert len(train) + len(test) == len(ch)4142# (i) classical one-way ANOVA, all changes43groups = [ch.loc[ch.weekday == d, "dy"].values for d in DAYS]44F, p = stats.f_oneway(*groups)4546# (ii) Monday contrast, Welch two-sample t interval47mon = ch.loc[ch.weekday == "Monday", "dy"].values48oth = ch.loc[ch.weekday != "Monday", "dy"].values49eff = mon.mean() - oth.mean()50v1, v2 = mon.var(ddof=1) / len(mon), oth.var(ddof=1) / len(oth)51se = np.sqrt(v1 + v2)52dfw = (v1 + v2) ** 2 / (v1 ** 2 / (len(mon) - 1) + v2 ** 2 / (len(oth) - 1))53tq = stats.t.ppf(0.975, dfw)54lo, hi = eff - tq * se, eff + tq * se55welch_p = stats.ttest_ind(mon, oth, equal_var=False).pvalue5657# (iii) out-of-sample: baseline zero change vs train weekday means58wk_means_train = train.groupby("weekday")["dy"].mean()59pred = test["weekday"].map(wk_means_train)60assert pred.notna().all()61rmse_base = float(np.sqrt(np.mean(test["dy"].values ** 2)))62rmse_model = float(np.sqrt(np.mean((test["dy"].values - pred.values) ** 2)))63# supplementary: unconditional train mean forecast and DM-type test of model vs zero64rmse_trainmean = float(np.sqrt(np.mean((test["dy"].values - train["dy"].mean()) ** 2)))65dloss = (test["dy"].values - pred.values) ** 2 - test["dy"].values ** 266dm_stat = dloss.mean() / (dloss.std(ddof=1) / np.sqrt(len(dloss)))67dm_p = 2 * stats.norm.sf(abs(dm_stat))6869# conclusion70if p >= 0.05:71    conclusion = "no_detectable_effect"72elif lo > 0:73    conclusion = "positive_effect"74elif hi < 0:75    conclusion = "negative_effect"76else:77    conclusion = "no_detectable_effect"7879# table + figure80tab = ch.groupby("weekday")["dy"].agg(n="size", mean_bp="mean", sd="std").reindex(DAYS)81tab["se_bp"] = tab["sd"] / np.sqrt(tab["n"])82tab = tab.reset_index()[["weekday", "n", "mean_bp", "se_bp"]]83tab.to_csv(f"{OUT}/table1.csv", index=False, float_format="%.6f")84crit = stats.t.ppf(0.975, tab["n"] - 1)85fig, ax = plt.subplots(figsize=(6.5, 4))86ax.errorbar(range(5), tab["mean_bp"], yerr=crit * tab["se_bp"], fmt="o", capsize=5, color="C0")87ax.axhline(0, color="grey", lw=0.8, ls="--")88ax.set_xticks(range(5)); ax.set_xticklabels([d[:3] for d in DAYS])89ax.set_ylabel("Mean one-day change in DGS10 (bp)")90ax.set_title(f"10y Treasury yield change by weekday, 2000-2019 (n={len(ch)})\n"91             f"95% CIs; ANOVA F={F:.2f}, p={p:.3f}", fontsize=9)92fig.tight_layout(); fig.savefig(f"{OUT}/figure1.png", dpi=150); plt.close(fig)9394issues = [95    {"issue": f"{n_missing} rows with empty DGS10 field (US market holidays / closures)",96     "handling": "dropped before computing changes, per TASK rule (see out/exclusions.csv)"},97    {"issue": "changes following a holiday span more than one business day (e.g. Tuesday after a Monday holiday)",98     "handling": "kept and attributed to the weekday of t, per TASK definition; noted as a limitation"},99    {"issue": "large outlier change of -51 bp on 2009-03-18 (FOMC QE announcement)",100     "handling": "genuine market move; kept (no outlier removal)"},101]102res = dict(103    task_id="F6-conf-01", feasibility="complete", feasibility_reason=None, data_issues=issues,104    n_input_rows={"DGS10.csv": int(n_raw)}, n_analysed=int(len(ch)), random_seed=None,105    n_missing_rows=n_missing, n_train=int(len(train)), n_test=int(len(test)),106    anova_f_stat=float(F), anova_p_value=float(p),107    effect_estimate=float(eff), effect_ci_low=float(lo), effect_ci_high=float(hi),108    baseline_metric=rmse_base, model_metric=rmse_model, conclusion=conclusion,109    # supplementary keys110    effect_se=float(se), effect_welch_df=float(dfw), effect_welch_p_value=float(welch_p),111    effect_ci_method="Welch two-sample t interval (Monday vs Tue-Fri, all changes)",112    oos_rmse_ratio_model_to_baseline=rmse_model / rmse_base,113    oos_rmse_train_unconditional_mean=rmse_trainmean,114    oos_dm_stat_model_vs_zero=float(dm_stat), oos_dm_p_value=float(dm_p),115    train_weekday_means_bp={d: float(wk_means_train[d]) for d in DAYS},116    n_changes_total=int(len(ch)),117)118with open(f"{OUT}/results.json", "w") as f:119    json.dump(res, f, indent=2)120print(json.dumps({k: res[k] for k in ["n_train", "n_test", "anova_f_stat", "anova_p_value", "effect_estimate",121      "effect_ci_low", "effect_ci_high", "baseline_metric", "model_metric", "conclusion"]}, indent=1))122
Files · main
Menu

Theme