| 1 | """The printed MDE holds its stated power on resampled REAL A/A rows (QA R2-1).
|
| 2 |
|
| 3 | Cells are drawn from the committed identical-binary rows
|
| 4 | (tests/fixtures/devloop_real_rows). For each trial the printed MDE X is taken
|
| 5 | from a null draw; a fresh draw with a true +X% shift in one cell of the family
|
| 6 | must then be confirmed. The stated power (cellstats.MDE_POWER, 80%) must hold.
|
| 7 | Mann-Whitney p uses the normal approximation for n >= 10 here purely for test
|
| 8 | speed (it closely tracks the exact/Monte-Carlo p at n = 20).
|
| 9 | """
|
| 10 | from __future__ import annotations
|
| 11 |
|
| 12 | import gzip
|
| 13 | import json
|
| 14 | import math
|
| 15 | import random
|
| 16 | import sys
|
| 17 | import unittest
|
| 18 | from pathlib import Path
|
| 19 | from unittest import mock
|
| 20 |
|
| 21 | ROOT = Path(__file__).resolve().parents[1]
|
| 22 | sys.path.insert(0, str(ROOT / "scripts"))
|
| 23 |
|
| 24 | from oakbench import cellstats as C # noqa: E402
|
| 25 |
|
| 26 | FIXTURES = ROOT / "tests" / "fixtures" / "devloop_real_rows"
|
| 27 | EXACT = C.permutation_p_value
|
| 28 |
|
| 29 |
|
| 30 | def fast_p(baseline, target, **kwargs):
|
| 31 | if len(baseline) < 10:
|
| 32 | return EXACT(baseline, target, **kwargs)
|
| 33 | n1, n2 = len(baseline), len(target)
|
| 34 | sd = math.sqrt(n1 * n2 * (n1 + n2 + 1) / 12)
|
| 35 | z = (abs(C._mann_whitney_u(baseline, target) - n1 * n2 / 2) - 0.5) / sd
|
| 36 | return max(0.0, min(1.0, math.erfc(z / math.sqrt(2))))
|
| 37 |
|
| 38 |
|
| 39 | def real_pool() -> dict[tuple[str, str], list[float]]:
|
| 40 | pool: dict[tuple[str, str], list[float]] = {}
|
| 41 | for path in FIXTURES.glob("*.jsonl.gz"):
|
| 42 | with gzip.open(path, "rt") as handle:
|
| 43 | for line in handle:
|
| 44 | row = json.loads(line)
|
| 45 | if (row.get("skipped") or row.get("returncode") or not isinstance(row.get("elapsed_ms"), (int, float))
|
| 46 | or row["subject"] not in ("oak_installed", "oak_installed_aa", "oak_local")
|
| 47 | or row["operation"].endswith((".add", ".commit")) or row["operation"].startswith("remote")):
|
| 48 | continue
|
| 49 | pool.setdefault((f"{path.name.split('-')[1]}:{row['scenario']}", row["operation"]), []).append(row["elapsed_ms"])
|
| 50 | return {key: values for key, values in pool.items() if len(values) >= 30}
|
| 51 |
|
| 52 |
|
| 53 | POOL = real_pool()
|
| 54 | KEYS = sorted(POOL)
|
| 55 |
|
| 56 |
|
| 57 | def draw(rng, n, cells, shift=None, shifted=None):
|
| 58 | rows = []
|
| 59 | for key in cells:
|
| 60 | for run in range(n):
|
| 61 | for subject in ("oak_installed", "oak_installed_aa", "oak_local"):
|
| 62 | value = rng.choice(POOL[key])
|
| 63 | if shift and subject == "oak_local" and key == shifted:
|
| 64 | value *= 1 + shift / 100
|
| 65 | rows.append({"subject": subject, "scenario": key[0], "operation": key[1], "run": run,
|
| 66 | "elapsed_ms": value, "returncode": 0})
|
| 67 | return rows
|
| 68 |
|
| 69 |
|
| 70 | def classify(rows, cells=None):
|
| 71 | floors = C.cell_floors(rows, "oak_installed", "oak_installed_aa", lambda r: r["elapsed_ms"], samples=400)
|
| 72 | return C.classify_cells(rows, "oak_local", "oak_installed", floors, cells=cells, bootstrap_samples=400, seed=3)
|
| 73 |
|
| 74 |
|
| 75 | @mock.patch.object(C, "permutation_p_value", fast_p)
|
| 76 | class StatedPowerTest(unittest.TestCase):
|
| 77 | def test_pool_is_real_and_large_enough(self):
|
| 78 | self.assertGreaterEqual(len(KEYS), 20)
|
| 79 |
|
| 80 | def test_confirmation_stage_mde_is_detected_at_stated_power(self):
|
| 81 | rng = random.Random(20260927)
|
| 82 | hits = trials = 0
|
| 83 | for _ in range(30):
|
| 84 | cells = rng.sample(KEYS, 10)
|
| 85 | null = classify(draw(rng, 20, cells))[cells[0]]
|
| 86 | if null.get("mde_pct") is None:
|
| 87 | continue
|
| 88 | trials += 1
|
| 89 | shifted = classify(draw(rng, 20, cells, shift=null["mde_pct"], shifted=cells[0]))[cells[0]]
|
| 90 | hits += shifted["verdict"] == "regression"
|
| 91 | self.assertGreaterEqual(trials, 25)
|
| 92 | self.assertGreaterEqual(hits / trials, C.MDE_POWER, f"{hits}/{trials}")
|
| 93 |
|
| 94 | def test_screen_stage_mde_is_detected_by_the_two_stage_pipeline(self):
|
| 95 | rng = random.Random(7)
|
| 96 | hits = trials = 0
|
| 97 | for _ in range(20):
|
| 98 | cells = rng.sample(KEYS, 14)
|
| 99 | null = classify(draw(rng, 5, cells))[cells[0]]
|
| 100 | if null.get("mde_pct") is None:
|
| 101 | continue
|
| 102 | trials += 1
|
| 103 | x = null["mde_pct"]
|
| 104 | flagged = C.screen_candidates(classify(draw(rng, 5, cells, shift=x, shifted=cells[0])), target_mde_pct=20.0)
|
| 105 | if cells[0] not in flagged:
|
| 106 | continue
|
| 107 | confirmed = classify(draw(rng, 20, flagged, shift=x, shifted=cells[0]), cells=flagged)
|
| 108 | hits += confirmed[cells[0]]["verdict"] == "regression"
|
| 109 | self.assertGreaterEqual(trials, 15)
|
| 110 | self.assertGreaterEqual(hits / trials, C.MDE_POWER, f"{hits}/{trials}")
|
| 111 |
|
| 112 | def test_old_half_power_point_is_not_what_is_printed(self):
|
| 113 | item = {"floor_pct": 8.0, "ci_low_pct": -5.0, "ci_high_pct": 5.0}
|
| 114 | self.assertEqual(C.minimum_detectable_effect_pct(item), 8.0 + C.MDE_POWER_MULTIPLIER * 5.0)
|
| 115 | self.assertGreater(C.minimum_detectable_effect_pct(item), max(8.0, 5.0))
|
| 116 |
|
| 117 |
|
| 118 | if __name__ == "__main__":
|
| 119 | unittest.main()
|