Viewing files at commit f86d1109f5dfCommit detailsView latest

Browse files
tests/test_stats_upgrades.py 171 lines · 7.7 KB · python
1
"""Tests for the statistical-upgrades slice of oakbench.stats (additive only)."""
2
3
from __future__ import annotations
4
5
import sys
6
import unittest
7
from pathlib import Path
8
9
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
10
11
from oakbench.stats import (
12
    adaptive_rep_plan,
13
    benjamini_hochberg_detail,
14
    mad_outliers,
15
    two_stage_confirmation,
16
)
17
18
19
class MadOutliersTest(unittest.TestCase):
20
    def test_clean_data_flags_nothing(self) -> None:
21
        result = mad_outliers([10.0, 10.1, 9.9, 10.05, 9.95, 10.0, 10.02])
22
        self.assertEqual(result["outlier_indices"], [])
23
        self.assertFalse(result["first_run_warmup_hint"])
24
        self.assertEqual(result["policy"], "flag_never_delete")
25
        self.assertEqual(len(result["modified_z"]), 7)
26
27
    def test_single_huge_value_flagged_with_exact_index(self) -> None:
28
        values = [10.0, 10.1, 9.9, 500.0, 10.05, 9.95, 10.0, 10.1, 9.9, 10.0]
29
        result = mad_outliers(values)
30
        self.assertEqual(result["outlier_indices"], [3])
31
        self.assertGreater(abs(result["modified_z"][3]), 14.826)
32
        self.assertFalse(result["first_run_warmup_hint"])
33
34
    def test_slow_first_run_sets_warmup_hint(self) -> None:
35
        values = [500.0, 10.0, 10.1, 9.9, 10.05, 9.95, 10.0, 10.1, 9.9, 10.0]
36
        result = mad_outliers(values)
37
        self.assertEqual(result["outlier_indices"], [0])
38
        self.assertTrue(result["first_run_warmup_hint"])
39
40
    def test_fast_sole_first_outlier_is_not_warmup_hint(self) -> None:
41
        values = [0.001, 10.0, 10.1, 9.9, 10.05, 9.95, 10.0, 10.1, 9.9, 10.0]
42
        result = mad_outliers(values)
43
        self.assertEqual(result["outlier_indices"], [0])
44
        self.assertFalse(result["first_run_warmup_hint"])
45
46
    def test_mad_zero_constant_data_flags_nothing(self) -> None:
47
        result = mad_outliers([5.0] * 8)
48
        self.assertEqual(result["outlier_indices"], [])
49
        self.assertEqual(result["modified_z"], [0.0] * 8)
50
        self.assertFalse(result["first_run_warmup_hint"])
51
52
    def test_empty_input(self) -> None:
53
        result = mad_outliers([])
54
        self.assertEqual(result["outlier_indices"], [])
55
        self.assertEqual(result["modified_z"], [])
56
        self.assertFalse(result["first_run_warmup_hint"])
57
58
    def test_threshold_respected(self) -> None:
59
        # max |modified z| here is ~25.5 (|x-med|/MAD = 94.5/2.5 = 37.8,
60
        # divided by 1.4826): above the 14.826 default, below 30.
61
        values = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 100.0]
62
        self.assertEqual(mad_outliers(values)["outlier_indices"], [9])
63
        self.assertEqual(mad_outliers(values, threshold=30.0)["outlier_indices"], [])
64
65
66
class AdaptiveRepPlanTest(unittest.TestCase):
67
    def test_below_min_runs_tops_up_to_floor_within_budget(self) -> None:
68
        plan = adaptive_rep_plan([1.0] * 4, time_budget_s=100.0, per_run_s_estimate=1.0)
69
        self.assertFalse(plan["satisfied"])
70
        self.assertEqual(plan["reason"], "below_min_runs")
71
        self.assertEqual(plan["additional_runs"], 6)
72
        self.assertEqual(plan["cap"], 100)
73
74
    def test_high_stddev_triggers_top_up(self) -> None:
75
        samples = [10.0, 20.0] * 5  # %stddev ~35
76
        plan = adaptive_rep_plan(samples, time_budget_s=100.0, per_run_s_estimate=1.0)
77
        self.assertFalse(plan["satisfied"])
78
        self.assertEqual(plan["reason"], "stddev_above_threshold")
79
        self.assertEqual(plan["additional_runs"], 10)
80
81
    def test_sigma_escapee_triggers_top_up(self) -> None:
82
        # %stddev ~1.1 (below the 5% threshold) but 10.5 is >3 sigma out.
83
        samples = [10.0] * 20 + [10.5]
84
        plan = adaptive_rep_plan(samples, time_budget_s=1000.0, per_run_s_estimate=1.0)
85
        self.assertFalse(plan["satisfied"])
86
        self.assertEqual(plan["reason"], "sigma_escapee")
87
        self.assertEqual(plan["additional_runs"], 10)
88
89
    def test_satisfied(self) -> None:
90
        samples = [10.0] * 6 + [10.1] * 6  # %stddev ~0.5, no 3-sigma escapee
91
        plan = adaptive_rep_plan(samples, time_budget_s=100.0, per_run_s_estimate=1.0)
92
        self.assertTrue(plan["satisfied"])
93
        self.assertEqual(plan["reason"], "satisfied")
94
        self.assertEqual(plan["additional_runs"], 0)
95
96
    def test_budget_exhausted_when_per_run_too_slow(self) -> None:
97
        plan = adaptive_rep_plan([], time_budget_s=5.0, per_run_s_estimate=10.0)
98
        self.assertFalse(plan["satisfied"])
99
        self.assertEqual(plan["reason"], "budget_exhausted")
100
        self.assertEqual(plan["additional_runs"], 0)
101
        self.assertEqual(plan["cap"], 0)
102
103
    def test_budget_exhausted_when_cap_reached_with_high_variance(self) -> None:
104
        samples = [10.0, 20.0] * 10  # 20 noisy samples, cap of 20
105
        plan = adaptive_rep_plan(
106
            samples, time_budget_s=20.0, per_run_s_estimate=1.0, cap_multiplier=2
107
        )
108
        self.assertFalse(plan["satisfied"])
109
        self.assertEqual(plan["reason"], "budget_exhausted")
110
        self.assertEqual(plan["additional_runs"], 0)
111
112
    def test_nonpositive_per_run_estimate_rejected(self) -> None:
113
        with self.assertRaises(ValueError):
114
            adaptive_rep_plan([1.0], time_budget_s=10.0, per_run_s_estimate=0.0)
115
116
117
class BenjaminiHochbergDetailTest(unittest.TestCase):
118
    def test_canonical_hand_computed_example(self) -> None:
119
        # m=5, fdr=0.05: ranks 1..4 satisfy p_(k) <= k/5 * 0.05; 0.2 > 0.05.
120
        result = benjamini_hochberg_detail([0.01, 0.02, 0.03, 0.04, 0.2], fdr=0.05)
121
        self.assertEqual(result["rejected"], [True, True, True, True, False])
122
        self.assertEqual(result["critical_index"], 3)
123
        self.assertAlmostEqual(result["threshold"], 0.04)
124
125
    def test_all_high_rejects_nothing(self) -> None:
126
        result = benjamini_hochberg_detail([0.4, 0.6, 0.9], fdr=0.05)
127
        self.assertEqual(result["rejected"], [False, False, False])
128
        self.assertIsNone(result["critical_index"])
129
        self.assertIsNone(result["threshold"])
130
131
    def test_empty_input(self) -> None:
132
        result = benjamini_hochberg_detail([], fdr=0.05)
133
        self.assertEqual(result["rejected"], [])
134
        self.assertIsNone(result["critical_index"])
135
        self.assertIsNone(result["threshold"])
136
137
    def test_ties_at_critical_value_all_rejected(self) -> None:
138
        # 0.02 appears twice (input order shuffled); both tie at the critical
139
        # value and both must reject, regardless of sort placement.
140
        result = benjamini_hochberg_detail([0.02, 0.5, 0.02, 0.001], fdr=0.05)
141
        self.assertEqual(result["rejected"], [True, False, True, True])
142
        self.assertAlmostEqual(result["threshold"], 0.02)
143
144
145
class TwoStageConfirmationTest(unittest.TestCase):
146
    def test_both_above_threshold_confirmed(self) -> None:
147
        result = two_stage_confirmation(20.0, 18.0, threshold_pct=10.0)
148
        self.assertTrue(result["confirmed"])
149
        self.assertFalse(result["borderline"])
150
        self.assertTrue(result["first_exceeds"])
151
        self.assertTrue(result["second_exceeds"])
152
153
    def test_first_borderline_second_below_not_confirmed(self) -> None:
154
        # threshold 10 -> borderline band is 7.5..12.5; first=11 is borderline.
155
        result = two_stage_confirmation(11.0, 5.0, threshold_pct=10.0)
156
        self.assertFalse(result["confirmed"])
157
        self.assertTrue(result["borderline"])
158
        self.assertTrue(result["first_exceeds"])
159
        self.assertFalse(result["second_exceeds"])
160
161
    def test_borderline_below_threshold_never_confirms(self) -> None:
162
        # first=9 is borderline but does not exceed: not confirmed even if
163
        # the second stage clears.
164
        result = two_stage_confirmation(9.0, 30.0, threshold_pct=10.0)
165
        self.assertFalse(result["confirmed"])
166
        self.assertTrue(result["borderline"])
167
        self.assertFalse(result["first_exceeds"])
168
169
170
if __name__ == "__main__":
171
    unittest.main()