Viewing files at commit 3354584a5d91Commit detailsView latest

Browse files
tests/test_cellstats_power.py 119 lines · 4.9 KB · python
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()