SPB Git

spb/wp39_uqo

Public
7.6 KB · 109 lines python
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
Files · da12d24917eb7765094341104ca6f7a5cd26a752
Menu

Theme