CoolFace
Datasetpublic

arikw/intervention-learning-framework

intervention-learning-framework (v1, milestone 1) A recursive intervention-learning framework for a real-time sales-call assistant, built simulator-first: every estimator is validated by recovering known ground truth from the generative simulator in intervene/sim/. No production data exists yet; nothing in this repo claims a result from real data, and no estimate is reported without an uncertainty interval. Milestone 1 scope: simulator + detection + offline effect estimation… See the full description on the dataset page: https://huggingface.co/datasets/arikw/intervention-learning-framework.

sourceHugging Faceupdated 16d agoView on Hugging Face
0likes91downloads
run_experiment.py186 linesDownload Raw Back to scripts
1"""Reproducible milestone-1 experiment: simulate -> detect -> estimate -> gate.2 3Runs the full milestone-1 loop on simulated data and prints:4  * simulator parameter recovery summary,5  * per-detector report (AUC, ECE, cost-optimal threshold, P/R),6  * per-moment effect table (pooled / stratified / IPW with 95% CIs, true ATE,7    confounding warnings),8  * a power/runnability gate for the headline effect.9 10Usage:11    python scripts/run_experiment.py --n-calls 25000 --seed 20260910 [--out results.json]12"""13 14from __future__ import annotations15 16import argparse17import json18from pathlib import Path19 20import numpy as np21from sklearn.metrics import roc_auc_score22 23from intervene.detect import (24    MomentDetector,25    expected_calibration_error,26    select_threshold_by_cost,27)28from intervene.effects import moment_effect_table29from intervene.planning import plan30from intervene.sim import ArmSpec, CallSimulator, DetectionSpec, MomentSpec, SimulatorConfig31 32FEATURES = [f"f{d}" for d in range(8)]33 34 35def build_config(seed: int, n_calls: int) -> SimulatorConfig:36    return SimulatorConfig(37        seed=seed,38        n_calls=n_calls,39        moments=(40            MomentSpec(41                id="pricing_objection",42                name="Pricing objection",43                base_logodds=-1.35,44                rep_coef=0.3,45                quality_coef=0.5,46                arms=(47                    ArmSpec(id="control", is_control=True),48                    ArmSpec(49                        id="discount_offer",50                        effect_logodds=0.0,51                        policy_intercept=-0.35,52                        policy_quality_coef=1.3,53                    ),54                ),55                detector=DetectionSpec(beta_pos=(9.0, 2.0), beta_neg=(1.5, 8.0)),56            ),57            MomentSpec(58                id="value_reframe",59                name="Value reframe",60                base_logodds=-1.9,61                rep_coef=0.2,62                quality_coef=0.3,63                arms=(64                    ArmSpec(id="control", is_control=True),65                    ArmSpec(id="reframe", effect_logodds=0.25),66                ),67                detector=DetectionSpec(beta_pos=(7.0, 2.5), beta_neg=(1.2, 7.0)),68            ),69        ),70    )71 72 73def main() -> None:74    ap = argparse.ArgumentParser(description=__doc__)75    ap.add_argument("--seed", type=int, default=20260910)76    ap.add_argument("--n-calls", type=int, default=25000)77    ap.add_argument("--out", type=Path, default=Path("results.json"))78    args = ap.parse_args()79 80    cfg = build_config(args.seed, args.n_calls)81    data = CallSimulator(cfg).generate()82    gt = data.ground_truth83 84    print("=" * 78)85    print(f"MILESTONE 1 EXPERIMENT  seed={args.seed}  n_calls={args.n_calls}")86    print(f"decisions logged: {data.meta['decided_calls']}  proxy-censored dropped: {data.n_proxy_censored}")87    print("=" * 78)88 89    # --- parameter recovery -------------------------------------------------90    print("\n[1] Simulator parameter recovery (empirical vs analytic quadrature)")91    for spec in cfg.moments:92        emp = data.calls_df["moment_id"].eq(spec.id).mean()93        true = gt.true_marginal_rate(spec.id)94        print(95            f"  {spec.id:22s} base rate: empirical {emp:.4f}  true {true:.4f}  "96            f"diff {emp - true:+.4f}"97        )98 99    # --- detection ----------------------------------------------------------100    print("\n[2] Detection (isotonic-calibrated logistic regression, 30/70 split, cost 3:1)")101    detector_report = {}102    for spec in cfg.moments:103        df = data.detector_frame[data.detector_frame["moment_id"] == spec.id].sample(104            frac=1.0, random_state=args.seed105        )106        n_train = int(0.3 * len(df))107        train, ev = df.iloc[:n_train], df.iloc[n_train:]108        det = MomentDetector(spec.id, calibration="isotonic", seed=args.seed).fit(109            train[FEATURES].to_numpy(), train["label"].to_numpy()110        )111        proba = det.predict_proba(ev[FEATURES].to_numpy())112        y = ev["label"].to_numpy()113        sel = select_threshold_by_cost(y, proba, c_fp=3.0, c_fn=1.0)114        k = int(np.argmin(np.abs(sel.curve.thresholds - sel.threshold)))115        detector_report[spec.id] = {116            "auc": float(roc_auc_score(y, proba)),117            "ece": expected_calibration_error(y, proba),118            "threshold": sel.threshold,119            "expected_cost": sel.expected_cost,120            "precision": float(sel.curve.precision[k]),121            "recall": float(sel.curve.recall[k]),122            "n_eval": int(len(y)),123        }124        r = detector_report[spec.id]125        print(126            f"  {spec.id:22s} AUC {r['auc']:.3f}  ECE {r['ece']:.4f}  "127            f"tau* {sel.threshold:.3f}  P {r['precision']:.3f}  R {r['recall']:.3f}  "128            f"cost {r['expected_cost']:.4f} (n_eval={r['n_eval']})"129        )130 131    # --- effects ------------------------------------------------------------132    print("\n[3] Offline effect estimation (95% Wald intervals; true ATE from quadrature)")133    all_rows = []134    for spec in cfg.moments:135        rows = moment_effect_table(data.decisions_df, spec.id, ground_truth=gt)136        all_rows.extend(rows)137        print(f"  moment: {spec.id}")138        for arm in sorted({r["arm"] for r in rows}):139            for est in ("pooled", "stratified", "ipw"):140                r = next(x for x in rows if x["arm"] == arm and x["estimator"] == est)141                star = " <-- covers true ATE" if r["estimate"].covers(r["true_ate"]) else ""142                print(143                    f"    arm {r['arm']:16s} {est:10s} "144                    f"{r['value']:+.4f} [{r['ci_low']:+.4f}, {r['ci_high']:+.4f}] "145                    f"(n={r['n']}, true ATE {r['true_ate']:+.4f}){star}"146                )147            warn = next(x for x in rows if x["estimator"] == "ipw")["confounding_warning"]148            if warn:149                print(f"    !! {warn}")150 151    # --- power gate ---------------------------------------------------------152    print("\n[4] Planning gate: detecting +3pt proxy lift at p1=0.24")153    gate = plan(154        p1=0.24,155        p2=0.27,156        eligible_rate=0.2,157        calls_per_day=500.0,158        objective_resolution_lag_days=5.0,159    )160    print(161        f"  n/arm {gate['n_per_arm']}  proxy {gate['proxy_days']:.1f} days  "162        f"objective {gate['objective_days']:.1f} days (+ objective resolution lag)"163    )164 165    out = {166        "seed": args.seed,167        "n_calls": args.n_calls,168        "decisions": data.meta["decided_calls"],169        "proxy_censored": data.n_proxy_censored,170        "recovery": {171            s.id: {172                "base_rate_empirical": float(data.calls_df["moment_id"].eq(s.id).mean()),173                "base_rate_true": gt.true_marginal_rate(s.id),174            }175            for s in cfg.moments176        },177        "detectors": detector_report,178        "effects": [{k: v for k, v in r.items() if k != "estimate"} for r in all_rows],179        "planning_gate": gate,180    }181    args.out.write_text(json.dumps(out, indent=2, default=str))182    print(f"\nwrote {args.out}")183 184 185if __name__ == "__main__":186    main()