Buckets:
| """cwm3/eval/power_gold.py — how many clean-test POSITIVES the warning-head-vs-DMS claim needs (roadmap §B3 item 4; CPU, seconds). | |
| Hanley–McNeil (1982) variance of an AUROC A with n_pos positives and n_neg negatives: | |
| Var(A) = [A(1−A) + (n_pos−1)(Q1 − A²) + (n_neg−1)(Q2 − A²)] / (n_pos·n_neg), Q1 = A/(2−A), Q2 = 2A²/(1+A) | |
| and for the PAIRED difference of two AUROCs on the same rows Var(Δ) = Var(A1) + Var(A2) − 2·r·SD(A1)·SD(A2) with r the between-score correlation. | |
| The observed device-cluster paired CI of the head − DMS Δ on the 509-row / 63-positive gold has half-width 0.088 (warning_head_oof.md, pooled; manual | |
| 0.142, engaged 0.115); the empirical resamples include the device clustering that the formula does not, so the table below is CALIBRATED to that anchor: | |
| half_width(n_pos) = 0.088 · sqrt(63 / n_pos) (the Hanley–McNeil positive-limited scaling; n_neg grows in the same proportion and matters little) | |
| and reported next to the uncalibrated formula (r fitted so that the formula reproduces 0.088 at 63 positives, A1 = 0.712 / A2 = 0.637, n_neg = 446). | |
| A paired Δ is declared at the 95 % level when |Δ| > half_width. Extending the gold with round-2 labels: n_pos = 63 + n_new_pos, where round-2 clips | |
| are drawn from the active strata (results/mining/round2_draw_v1.csv), so the warranted share is expected ABOVE the gold's 12.4 % (63 / 509); the table | |
| gives the number of extra labelled clips needed at 12.4 % (gold rate) and at 25 % (active-strata expectation) for each n_pos. | |
| python eval/power_gold.py -> results/tables/power_gold.md (append-only section) + results/power_gold/power_gold_<stamp>.csv | |
| No openpilot DMS field is read. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import math | |
| import sys | |
| import time | |
| from pathlib import Path | |
| import numpy as np | |
| import pandas as pd | |
| CWM3 = Path(__file__).resolve().parents[1] | |
| sys.path.insert(0, str(CWM3 / "common")) | |
| from runmeta import write_run_json # noqa: E402 | |
| TABLE = CWM3 / "results" / "tables" / "power_gold.md" | |
| OUT = CWM3 / "results" / "power_gold" | |
| def hm_var(A, n_pos, n_neg): | |
| Q1, Q2 = A / (2 - A), 2 * A * A / (1 + A) | |
| return (A * (1 - A) + (n_pos - 1) * (Q1 - A * A) + (n_neg - 1) * (Q2 - A * A)) / (n_pos * n_neg) | |
| def paired_half_width(A1, A2, n_pos, n_neg, r): | |
| v = hm_var(A1, n_pos, n_neg) + hm_var(A2, n_pos, n_neg) - 2 * r * math.sqrt(hm_var(A1, n_pos, n_neg) * hm_var(A2, n_pos, n_neg)) | |
| return 1.96 * math.sqrt(max(v, 0.0)) | |
| def fit_r(A1, A2, n_pos, n_neg, target): | |
| """between-score correlation that makes the Hanley–McNeil paired half-width equal the observed one.""" | |
| lo, hi = -0.99, 0.99 | |
| for _ in range(60): | |
| mid = 0.5 * (lo + hi) | |
| if paired_half_width(A1, A2, n_pos, n_neg, mid) > target: | |
| lo = mid | |
| else: | |
| hi = mid | |
| return 0.5 * (lo + hi) | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--n-pos0", type=int, default=63) | |
| ap.add_argument("--n-gold0", type=int, default=509) | |
| ap.add_argument("--hw0", type=float, default=0.088, help="observed pooled paired-Δ CI half-width (device-cluster bootstrap) at n_pos0") | |
| ap.add_argument("--hw-manual", type=float, default=0.142) | |
| ap.add_argument("--hw-engaged", type=float, default=0.115) | |
| ap.add_argument("--n-pos-manual", type=int, default=31) | |
| ap.add_argument("--n-pos-engaged", type=int, default=32) | |
| ap.add_argument("--a-head", type=float, default=0.712) | |
| ap.add_argument("--a-dms", type=float, default=0.637) | |
| ap.add_argument("--n-pos", default="63,75,88,113,133,195,260") | |
| ap.add_argument("--deltas", default="0.10,0.075,0.05,0.03") | |
| ap.add_argument("--pos-rates", default="0.124,0.25", help="expected warranted share among newly labelled clips (gold rate; active-strata expectation)") | |
| a = ap.parse_args() | |
| t0 = time.time() | |
| stamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime()) | |
| OUT.mkdir(parents=True, exist_ok=True) | |
| n_neg0 = a.n_gold0 - a.n_pos0 | |
| r = fit_r(a.a_head, a.a_dms, a.n_pos0, n_neg0, a.hw0) | |
| n_list = [int(x) for x in a.n_pos.split(",")] | |
| deltas = [float(x) for x in a.deltas.split(",")] | |
| rates = [float(x) for x in a.pos_rates.split(",")] | |
| rows = [] | |
| for n_pos in n_list: | |
| n_neg = int(round(n_pos * n_neg0 / a.n_pos0)) | |
| hw_cal = a.hw0 * math.sqrt(a.n_pos0 / n_pos) | |
| hw_hm = paired_half_width(a.a_head, a.a_dms, n_pos, n_neg, r) | |
| rec = dict(n_pos=n_pos, n_neg_prop=n_neg, half_width_calibrated=hw_cal, half_width_hanley_mcneil=hw_hm, hw_manual_scaled=a.hw_manual * math.sqrt(a.n_pos_manual / max(n_pos * a.n_pos_manual / a.n_pos0, 1)), | |
| hw_engaged_scaled=a.hw_engaged * math.sqrt(a.n_pos_engaged / max(n_pos * a.n_pos_engaged / a.n_pos0, 1)), smallest_declarable_delta=hw_cal) | |
| for d in deltas: | |
| rec[f"declares_{d:g}"] = bool(d > hw_cal) | |
| for rate in rates: | |
| extra_pos = max(n_pos - a.n_pos0, 0) | |
| rec[f"new_clips_at_{rate:g}"] = int(math.ceil(extra_pos / rate)) if extra_pos else 0 | |
| rows.append(rec) | |
| for d in deltas: | |
| n_need = int(math.ceil(a.n_pos0 * (a.hw0 / d) ** 2)) | |
| rows.append(dict(n_pos=n_need, n_neg_prop=int(round(n_need * n_neg0 / a.n_pos0)), half_width_calibrated=a.hw0 * math.sqrt(a.n_pos0 / n_need), half_width_hanley_mcneil=paired_half_width(a.a_head, a.a_dms, n_need, int(round(n_need * n_neg0 / a.n_pos0)), r), | |
| hw_manual_scaled=np.nan, hw_engaged_scaled=np.nan, smallest_declarable_delta=d, note=f"n_pos needed to declare Δ = {d:g}", **{f"declares_{dd:g}": bool(dd >= d) for dd in deltas}, | |
| **{f"new_clips_at_{rate:g}": int(math.ceil(max(n_need - a.n_pos0, 0) / rate)) for rate in rates})) | |
| df = pd.DataFrame(rows) | |
| df.to_csv(OUT / f"power_gold_{stamp}.csv", index=False) | |
| L = [f"## Power of the clean-test gold for the head-vs-DMS paired AUROC claim (eval/power_gold.py, run {stamp})", "", | |
| f"Anchor: the observed device-cluster paired-Δ CI half-width on the current gold ({a.n_gold0} rows / {a.n_pos0} positives) is {a.hw0:.3f} pooled " | |
| f"({a.hw_manual:.3f} manual on {a.n_pos_manual} positives, {a.hw_engaged:.3f} engaged on {a.n_pos_engaged}; `warning_head_oof.md`). Hanley–McNeil positive-limited scaling: half-width(n_pos) = " | |
| f"{a.hw0:.3f}·√({a.n_pos0}/n_pos); the uncalibrated formula (A_head {a.a_head}, A_DMS {a.a_dms}, negatives in the gold proportion, between-score r fitted to the anchor = {r:+.3f}) is shown for reference. " | |
| f"A paired Δ is declarable when |Δ| exceeds the half-width. `new clips` = labelled clips needed for the extra positives at a warranted share of {rates[0]:.1%} (gold rate) / {rates[1]:.0%} (active-strata expectation).", "", | |
| "| n_pos | half-width (calibrated) | half-width (Hanley–McNeil) | manual / engaged half-width (scaled) | smallest declarable Δ | " + " | ".join(f"Δ={d:g}" for d in deltas) + f" | new clips @{rates[0]:.1%} | new clips @{rates[1]:.0%} | note |", | |
| "|---|---|---|---|---|" + "---|" * len(deltas) + "---|---|---|"] | |
| for i, r_ in df.iterrows(): | |
| me = f"{r_.hw_manual_scaled:.3f} / {r_.hw_engaged_scaled:.3f}" if np.isfinite(r_.hw_manual_scaled) else "—" | |
| note = r_["note"] if "note" in df and isinstance(r_["note"], str) else "" | |
| L.append(f"| {int(r_.n_pos)} | {r_.half_width_calibrated:.3f} | {r_.half_width_hanley_mcneil:.3f} | {me} | {r_.smallest_declarable_delta:.3f} | " | |
| + " | ".join("yes" if bool(r_[f"declares_{d:g}"]) else "no" for d in deltas) | |
| + f" | {int(r_[f'new_clips_at_{rates[0]:g}'])} | {int(r_[f'new_clips_at_{rates[1]:g}'])} | {note} |") | |
| L += ["", f"Reading: today's pooled +0.075 (HGB current vs DMS att@3s) needs n_pos ≥ {int(math.ceil(a.n_pos0 * (a.hw0 / 0.075) ** 2))} to be declared; +0.05 needs ≥ {int(math.ceil(a.n_pos0 * (a.hw0 / 0.05) ** 2))}. " | |
| f"The engaged stratum alone (32 positives, half-width {a.hw_engaged:.3f}) cannot declare anything below ≈ 0.11 without new engaged positives. The primary claim stays on the 509-row gold; " | |
| "the pooled 509 + round-2 stratum (`clean_test_r2`, eval/gold_extended.py) is pre-registered as secondary.", ""] | |
| md = "\n".join(L) + "\n" | |
| if not TABLE.exists(): | |
| TABLE.write_text("# Gold power for the warning-head claims (eval/power_gold.py; sections appended per run)\n\n") | |
| with open(TABLE, "a") as f: | |
| f.write(md) | |
| print(md) | |
| write_run_json(OUT, "power_gold", [Path(__file__)], [], seed=0, t_start=t0, input_listing=False, | |
| extra={"anchor": dict(n_pos0=a.n_pos0, n_gold0=a.n_gold0, hw0=a.hw0, hw_manual=a.hw_manual, hw_engaged=a.hw_engaged, a_head=a.a_head, a_dms=a.a_dms, r_fitted=r), "rows": rows, "dms_fields_used": []}) | |
| if __name__ == "__main__": | |
| main() | |
Xet Storage Details
- Size:
- 8.89 kB
- Xet hash:
- cada71dc81259f6a0a9c5baf483a61765b191fffc4d3bc751a3882f5f8117106
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.