"""F6-conf-02 replication entry point. Run from workspace root: ~/wp29/venv/bin/python out/analysis.py Reads only data/gc5y_daily.csv; writes out/results.json, out/figure1.png, out/table1.csv, out/exclusions.csv. No randomness is used (random_seed = null).""" import json, os import numpy as np, pandas as pd from scipy import stats import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt DATA = "data/gc5y_daily.csv" OUT = "out" PRE_N = 120 os.makedirs(OUT, exist_ok=True) df = pd.read_csv(DATA, dtype={"date": str}) n_raw = len(df) df["dt"] = pd.to_datetime(df["date"], format="%Y-%m-%d") issues = [] # ---- audit ---- n_missing = int(df[["date", "yield_5y", "post_cut"]].isna().sum().sum()) n_dup = int(df["date"].duplicated().sum()) if n_missing: issues.append({"issue": f"{n_missing} missing values", "handling": "reported; rows kept"}) if n_dup: issues.append({"issue": f"{n_dup} duplicate dates", "handling": "reported"}) if not df["dt"].is_monotonic_increasing: issues.append({"issue": "dates not sorted", "handling": "file order used as required by the change definition"}) bad_flag = int((~df["post_cut"].isin([0, 1])).sum()) if bad_flag: issues.append({"issue": f"{bad_flag} post_cut values not in {{0,1}}", "handling": "reported"}) n_treated = int((df["post_cut"] == 1).sum()) start, end = df["date"].iloc[0], df["date"].iloc[-1] gaps = df["dt"].diff().dt.days if gaps.max() > 5: issues.append({"issue": f"max calendar gap {int(gaps.max())} days", "handling": "reported"}) # ---- one-day changes (bp), between consecutive rows, dated t ---- df["chg_bp"] = 100.0 * df["yield_5y"].diff() chg = df.dropna(subset=["chg_bp"]) excl = [dict(step=1, rule="load file", n_before=n_raw, n_after=n_raw, n_removed=0, rationale="all rows read; no missing values or duplicates"), dict(step=2, rule="first difference (first row has no t-1)", n_before=n_raw, n_after=len(chg), n_removed=n_raw - len(chg), rationale="one-day change undefined for first row; remaining changes form baseline sample")] # ---- baseline: mean one-day change over whole file ---- baseline = float(chg["chg_bp"].mean()) # ---- effect: post (post_cut=1) vs 120 trading days before first flagged day ---- effect = lo = hi = rmse = None n_analysed = None feas, reason, conclusion = "complete", None, None if n_treated == 0: issues.append({"issue": f"no rows with post_cut = 1: the file ends {end}, before the 2020-03-04 cut " "(README: retrieved with end_date=2019-12-31)", "handling": "treated window empty and pre-window undefined; estimand declared not computable " "(premise_invalid); no proxy window or redefined treatment used"}) feas = "infeasible" reason = (f"The file covers {start} to {end} and contains 0 rows with post_cut = 1, so neither the post-cut " "mean change nor the 120-day pre-window (anchored on the first flagged day) can be computed.") conclusion = "premise_invalid" excl.append(dict(step=3, rule="keep post_cut=1 days and the 120 rows before the first flagged day", n_before=len(chg), n_after=0, n_removed=len(chg), rationale="no flagged day exists, so the estimation windows are empty")) else: first = int(np.flatnonzero(df["post_cut"].values == 1)[0]) post = df.loc[df["post_cut"] == 1, "chg_bp"].dropna() pre = df["chg_bp"].iloc[max(first - PRE_N, 1):first].dropna() if len(pre) < PRE_N: issues.append({"issue": f"pre-window has only {len(pre)} changes (<{PRE_N})", "handling": "used as available"}) excl.append(dict(step=3, rule="keep post_cut=1 days and the 120 rows before the first flagged day", n_before=len(chg), n_after=len(pre) + len(post), n_removed=len(chg) - len(pre) - len(post), rationale="estimation windows per TASK")) if len(pre) >= 2 and len(post) >= 2: effect = float(post.mean() - pre.mean()) v1, v0 = post.var(ddof=1) / len(post), pre.var(ddof=1) / len(pre) se = float(np.sqrt(v1 + v0)) dof = (v1 + v0) ** 2 / (v1 ** 2 / (len(post) - 1) + v0 ** 2 / (len(pre) - 1)) q = stats.t.ppf(0.975, dof) lo, hi = effect - q * se, effect + q * se resid = np.concatenate([post - post.mean(), pre - pre.mean()]) rmse = float(np.sqrt(np.mean(resid ** 2))) n_analysed = int(len(pre) + len(post)) conclusion = "positive_effect" if lo > 0 else "negative_effect" if hi < 0 else "no_detectable_effect" else: feas, conclusion = "infeasible", "premise_invalid" reason = "fewer than two observations in a window" results = { "task_id": "F6-conf-02", "sample_start": start, "sample_end": end, "n_treated": n_treated, "baseline_metric": baseline, "model_metric": rmse, "effect_estimate": effect, "effect_ci_low": lo, "effect_ci_high": hi, "conclusion": conclusion, "feasibility": feas, "feasibility_reason": reason, "data_issues": issues, "n_input_rows": {"gc5y_daily.csv": n_raw}, "n_analysed": n_analysed, "random_seed": None, } with open(f"{OUT}/results.json", "w") as f: json.dump(results, f, indent=2) pd.DataFrame(excl).to_csv(f"{OUT}/exclusions.csv", index=False) # ---- table 1 ---- df["year"] = df["dt"].dt.year t1 = df.groupby("year").agg(n_days=("yield_5y", "size"), mean_yield=("yield_5y", "mean"), mean_change_bp=("chg_bp", "mean")).reset_index() t1.round(6).to_csv(f"{OUT}/table1.csv", index=False) # ---- figure 1 ---- fig, ax = plt.subplots(figsize=(9, 4)) ax.plot(df["dt"], df["yield_5y"], lw=0.8) ax.set_title("GoC 5-year benchmark yield (BD.CDN.5YR.DQ.YLD), file sample\n" f"{start} to {end}; rows with post_cut=1: {n_treated}") ax.set_ylabel("Yield (%)"); ax.set_xlabel("Date") fig.tight_layout(); fig.savefig(f"{OUT}/figure1.png", dpi=120) print(json.dumps({k: results[k] for k in ["sample_start", "sample_end", "n_treated", "baseline_metric", "effect_estimate", "conclusion", "feasibility"]}, indent=1))