1#!/usr/bin/env python2"""Phase 3: baseline ladder B0-B5 (base rates, logistic ridge per information set) on the chronological split, every3horizon, primary + secondary targets; walk-forward out-of-fold predictions; firm hold-out variant.4Predictions are saved per (design, h, target, model) so that 07_evaluate.py computes every metric from files."""5from __future__ import annotations67import json8import sys9from pathlib import Path1011import numpy as np12import polars as pl1314sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))15from wp39.models import INFO_SETS, Preprocessor, base_rate_predict, columns_for, fit_logistic, fit_ridge, firm_history_predict, predict_proba # noqa: E40216from wp39.pipeline import Paths, get_logger, load_config # noqa: E40217from wp39.provenance import now_iso, write_json # noqa: E40218from wp39.samples import feature_columns # noqa: E40219from wp39.splits import chronological, firm_holdout_mask, overlap_check, walk_forward # noqa: E4022021KEY = ["entity_id", "symbol", "fiscal_period_end", "h", "cutoff", "A_q", "sic_division", "release_time"]22TARGETS = ["y_adverse_opinc", "y_adverse_netinc", "y_roa_decline", "y_ocf_negative", "y_liquidity_deterioration", "y_abs_surprise"]232425def save_preds(P: Paths, df: pl.DataFrame, p: np.ndarray, tag: str, design: str, h: int, target: str, model: str, part: str) -> None:26 out = df.select(KEY + [target]).with_columns([pl.Series("p", p), pl.lit(model).alias("model"), pl.lit(part).alias("part"),27 pl.lit(tag).alias("tag")])28 d = P.evaluation / "preds"; d.mkdir(exist_ok=True)29 out.write_parquet(d / f"{design}_h{h}_{target}_{model}_{part}_{tag}.parquet")303132def run_split(P, cfg, mcfg, fc, design, h, target, train, valid, test, tag, log, info_sets, record):33 ytr, yva, yte = (x[target].to_numpy().astype(int) for x in (train, valid, test))34 # B035 save_preds(P, test, base_rate_predict(ytr, test.height), tag, design, h, target, "B0", "test")36 save_preds(P, valid, base_rate_predict(ytr, valid.height), tag, design, h, target, "B0", "validation")37 save_preds(P, test, firm_history_predict(train, test, target), tag, design, h, target, "B0b", "test")38 for name in info_sets:39 cols = columns_for(name, fc)40 pp = Preprocessor(cols).fit(train)41 Xtr, Xva, Xte = pp.transform(train), pp.transform(valid), pp.transform(test)42 m, info = fit_logistic(Xtr, ytr, Xva, yva, mcfg["logistic"]["C_grid"], cfg["seed"])43 save_preds(P, valid, predict_proba(m, Xva), tag, design, h, target, name, "validation")44 save_preds(P, test, predict_proba(m, Xte), tag, design, h, target, name, "test")45 record.append({"design": design, "h": h, "target": target, "model": name, "tag": tag, "n_train": train.height,46 "n_valid": valid.height, "n_test": test.height, "n_features": len(cols), **info,47 "coef": dict(zip(cols, m.coef_[0].round(4).tolist()))})48 log.info("%s h=%d %s %s %s: C=%s val_ll=%.4f (p=%d)", design, h, target, name, tag, info["C"], info["val_logloss"], len(cols))495051def main() -> int:52 P = Paths.default()53 log = get_logger("05_baselines", P.logs / "05_baselines.log")54 cfg, mcfg = load_config("research"), load_config("models")55 record = []56 for design in ("event", "calendar"):57 df = pl.read_parquet(P.processed / f"sample_{design}.parquet")58 if design == "calendar":59 df = df.with_columns(pl.lit(0).alias("h")) # calendar: h varies; pooled model60 fc = feature_columns(df)61 write_json(P.evaluation / f"feature_columns_{design}.json", fc)62 horizons = sorted(df["h"].unique().to_list()) if design == "event" else [0]63 for h in horizons:64 d = df.filter(pl.col("h") == h) if design == "event" else df65 targets = TARGETS if (design == "event" and h == cfg["cutoffs"]["primary_horizon_td"]) else ["y_adverse_opinc"]66 for target in targets:67 dd = d.filter(pl.col(target).is_not_null())68 sp = chronological(dd, cfg["splits"])69 chk = overlap_check(sp["train"], sp["test"])70 assert chk["shared_observations"] == 0 and chk["train_outcomes_revealed_in_test"] == 0, chk71 sets = list(INFO_SETS) if target == "y_adverse_opinc" else ["B1", "B2", "B5"]72 run_split(P, cfg, mcfg, fc, design, h, target, sp["train"], sp["validation"], sp["test"], "chrono", log, sets, record)73 if design == "event" and h == cfg["cutoffs"]["primary_horizon_td"] and target == "y_adverse_opinc":74 # continuous target (ridge) on the same split75 for name in ("B2", "B5"):76 cols = columns_for(name, fc); pp = Preprocessor(cols).fit(sp["train"])77 ytr = sp["train"]["S_opinc"].to_numpy(); lo, hi = np.quantile(ytr, [0.01, 0.99]); ytr = np.clip(ytr, lo, hi)78 m, info = fit_ridge(pp.transform(sp["train"]), ytr, pp.transform(sp["validation"]), np.clip(sp["validation"]["S_opinc"].to_numpy(), lo, hi))79 save_preds(P, sp["test"], m.predict(pp.transform(sp["test"])), "chrono_cont", design, h, "S_opinc", name, "test")80 record.append({"design": design, "h": h, "target": "S_opinc", "model": name, "tag": "chrono_cont", "winsor": [float(lo), float(hi)],81 "train_mean": float(ytr.mean()), **info})82 # firm hold-out: train/validate on non-held-out firms, test on both groups separately83 hold = firm_holdout_mask(dd["entity_id"], cfg["splits"]["firm_holdout_fraction"])84 dd2 = dd.with_columns(pl.Series("_hold", hold))85 sp2 = chronological(dd2.filter(~pl.col("_hold")), cfg["splits"])86 te_hold = dd2.filter(pl.col("_hold") & (pl.col("cutoff") >= sp2["test"]["cutoff"].min()))87 run_split(P, cfg, mcfg, fc, design, h, target, sp2["train"], sp2["validation"], te_hold, "firmhold_heldout", log, ["B2", "B5"], record)88 run_split(P, cfg, mcfg, fc, design, h, target, sp2["train"], sp2["validation"], sp2["test"], "firmhold_seen", log, ["B2", "B5"], record)89 # walk-forward out-of-fold predictions (logistic, C fixed at the chronological choice per set)90 for name in ("B1", "B2", "B3", "B4", "B5"):91 cols = columns_for(name, fc)92 C = next(r["C"] for r in record if r["model"] == name and r["tag"] == "chrono" and r["h"] == h and r["target"] == target)93 parts = []94 for y, tr, te in walk_forward(dd, cfg["splits"], 2026):95 pp = Preprocessor(cols).fit(tr)96 from sklearn.linear_model import LogisticRegression97 m = LogisticRegression(C=C, max_iter=2000, random_state=cfg["seed"]).fit(pp.transform(tr), tr[target].to_numpy().astype(int))98 parts.append(te.select(KEY + [target]).with_columns([pl.Series("p", predict_proba(m, pp.transform(te))), pl.lit(y).alias("fold")]))99 oof = pl.concat(parts).with_columns([pl.lit(name).alias("model"), pl.lit("oof").alias("part"), pl.lit("walkforward").alias("tag")])100 oof.write_parquet(P.evaluation / "preds" / f"{design}_h{h}_{target}_{name}_oof_walkforward.parquet")101 log.info("walk-forward %s: %d oof rows", name, oof.height)102 write_json(P.evaluation / "baselines_fit_log.json", {"run_at": now_iso(), "fits": record})103 print(json.dumps({"fits": len(record)}, indent=1))104 return 0105106107if __name__ == "__main__":108 raise SystemExit(main())109