"""Replication entry point for F6-conf-02. 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. Deterministic (no randomness used).""" import json, os import numpy as np import pandas as pd import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from scipy import stats OUT = "out" FN = "gc5y_daily.csv" PRE_WINDOW = 120 os.makedirs(OUT, exist_ok=True) raw = pd.read_csv(os.path.join("data", FN), dtype=str) n_raw = len(raw) df = raw.copy() df["date"] = pd.to_datetime(df["date"], format="%Y-%m-%d") df["yield_5y"] = pd.to_numeric(df["yield_5y"], errors="coerce") df["post_cut"] = pd.to_numeric(df["post_cut"], errors="coerce").astype("Int64") issues = [] # --- audit --- n_missing = int(df[["date", "yield_5y", "post_cut"]].isna().any(axis=1).sum()) n_dup = int(df["date"].duplicated().sum()) monotonic = bool(df["date"].is_monotonic_increasing) n_treated = int((df["post_cut"] == 1).sum()) gaps = df["date"].diff().dt.days long_gaps = df.loc[gaps > 4, "date"].dt.strftime("%Y-%m-%d").tolist() if n_missing: issues.append({"issue": f"{n_missing} rows with missing/non-numeric values", "handling": "reported; differences across them left NaN"}) if n_dup: issues.append({"issue": f"{n_dup} duplicated dates", "handling": "reported, not removed"}) if not monotonic: issues.append({"issue": "dates not sorted ascending", "handling": "file order kept per availability rule"}) sample_start = df["date"].min().strftime("%Y-%m-%d") sample_end = df["date"].max().strftime("%Y-%m-%d") if n_treated == 0: issues.append({ "issue": (f"Coverage ends {sample_end} (README: Valet query end_date=2019-12-31), " "before the 2020-03-04 cut; post_cut is 0 for all " f"{n_raw} rows, so there are zero treated days"), "handling": ("Estimand not computable; effect, CI and model RMSE set to null, " "conclusion premise_invalid. post_cut not redefined and no proxy " "event or extrapolation substituted.")}) if long_gaps: issues.append({ "issue": (f"{len(long_gaps)} calendar gaps of 5 days (year-end holidays, e.g. " f"{long_gaps[0]}); one-day changes span non-trading days"), "handling": "kept; changes defined between consecutive rows per the task rule"}) # --- one-day changes (bp) between consecutive rows, dated t --- df["chg_bp"] = 100.0 * df["yield_5y"].diff() n_chg = int(df["chg_bp"].notna().sum()) baseline_metric = float(df["chg_bp"].mean()) excl = [{"step": 1, "rule": "first row has no previous row for a one-day change", "n_before": n_raw, "n_after": n_chg, "n_removed": n_raw - n_chg, "rationale": "change defined as 100*(y_t - y_{t-1}) between consecutive rows"}] # --- effect: post-cut mean change minus mean over 120 days before first treated day --- effect = lo = hi = rmse = None n_analysed = None treated_idx = np.flatnonzero((df["post_cut"] == 1).to_numpy()) if len(treated_idx) > 0: first = treated_idx[0] post = df.loc[df["post_cut"] == 1, "chg_bp"].dropna() pre = df["chg_bp"].iloc[max(0, first - PRE_WINDOW):first].dropna() if len(post) >= 2 and len(pre) >= 2: effect = float(post.mean() - pre.mean()) v1, v0 = post.var(ddof=1) / len(post), pre.var(ddof=1) / len(pre) se = 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 = float(effect - q * se), float(effect + q * se) resid = np.concatenate([post - post.mean(), pre - pre.mean()]) rmse = float(np.sqrt(np.mean(resid ** 2))) n_analysed = int(len(post) + len(pre)) n_win = 0 if n_analysed is None else n_analysed else: n_win = 0 excl.append({"step": 2, "rule": "keep post_cut=1 days and the 120 rows preceding the first post_cut=1 day", "n_before": n_chg, "n_after": n_win, "n_removed": n_chg - n_win, "rationale": ("estimand windows; no post_cut=1 rows exist so no window can be formed" if n_win == 0 else "estimand windows")}) pd.DataFrame(excl).to_csv(os.path.join(OUT, "exclusions.csv"), index=False) if effect is None: conclusion, feas = "premise_invalid", "infeasible" reason = (f"The file covers {sample_start} to {sample_end} and contains no rows with post_cut=1 " "(the cut was announced 2020-03-04), so the post-cut mean change and the " "120-day pre-window anchored on the first treated day do not exist.") else: feas, reason = "complete", None conclusion = ("positive_effect" if lo > 0 else "negative_effect" if hi < 0 else "no_detectable_effect") # --- table1: per calendar year --- df["year"] = df["date"].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["mean_yield"] = t1["mean_yield"].round(6) t1["mean_change_bp"] = t1["mean_change_bp"].round(6) t1.to_csv(os.path.join(OUT, "table1.csv"), index=False) # --- figure1 --- fig, ax = plt.subplots(figsize=(9, 4)) ax.plot(df["date"], df["yield_5y"], lw=0.8, color="C0") ax.axvline(pd.Timestamp("2020-03-04"), color="red", ls="--", lw=1, label="2020-03-04 cut (outside file coverage)") ax.set_xlabel("date"); ax.set_ylabel("5-year GoC benchmark yield (%)") ax.set_title(f"BD.CDN.5YR.DQ.YLD, {sample_start} to {sample_end} (post_cut=1 rows: {n_treated})") ax.legend(loc="upper right"); fig.tight_layout() fig.savefig(os.path.join(OUT, "figure1.png"), dpi=120) plt.close(fig) res = { "task_id": "F6-conf-02", "sample_start": sample_start, "sample_end": sample_end, "n_treated": n_treated, "baseline_metric": baseline_metric, "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": {FN: n_raw}, "n_analysed": n_analysed, "random_seed": None, # auxiliary audit values "n_changes": n_chg, "n_missing_rows": n_missing, "n_duplicate_dates": n_dup, "baseline_change_sd_bp": float(df["chg_bp"].std(ddof=1)), } with open(os.path.join(OUT, "results.json"), "w") as f: json.dump(res, f, indent=2) print(json.dumps({k: res[k] for k in ["sample_start", "sample_end", "n_treated", "baseline_metric", "effect_estimate", "conclusion", "feasibility"]}, indent=2))