Paired bootstrap test: is there a significant difference between two variants? Both variants answer the same set of questions, so we use a *paired* test: compute per-question differences diff[i] = A[i] - B[i] (each is +1, -1, or 0), then bootstrap the mean of that difference vector. Th
(
results_a: dict[int, bool],
results_b: dict[int, bool],
name_a: str,
name_b: str,
rng: np.random.Generator,
)
| 102 | |
| 103 | |
| 104 | def paired_bootstrap( |
| 105 | results_a: dict[int, bool], |
| 106 | results_b: dict[int, bool], |
| 107 | name_a: str, |
| 108 | name_b: str, |
| 109 | rng: np.random.Generator, |
| 110 | ): |
| 111 | """Paired bootstrap test: is there a significant difference between two variants? |
| 112 | |
| 113 | Both variants answer the same set of questions, so we use a *paired* test: |
| 114 | compute per-question differences diff[i] = A[i] - B[i] (each is +1, -1, or 0), |
| 115 | then bootstrap the mean of that difference vector. This cancels out shared |
| 116 | question difficulty — if both variants get the same 150 hard questions wrong |
| 117 | and only differ on 3 borderline ones, the paired test sees the tiny signal |
| 118 | clearly, while an unpaired test would be swamped by shared noise. |
| 119 | |
| 120 | Concretely: |
| 121 | 1. Align on shared doc_ids → two boolean arrays of length n. |
| 122 | 2. diff = A - B. Observed accuracy gap = mean(diff). |
| 123 | 3. Draw BOOTSTRAP_N resamples (with replacement) of size n from diff, |
| 124 | compute the mean of each → distribution of plausible gaps. |
| 125 | 4. 95% CI = 2.5th / 97.5th percentiles of bootstrap means. |
| 126 | 5. Two-sided p-value = 2 * fraction of bootstrap means with opposite sign |
| 127 | to the observed gap (clamped to 1.0). |
| 128 | """ |
| 129 | shared_ids = sorted(set(results_a) & set(results_b)) |
| 130 | if not shared_ids: |
| 131 | print(f"No shared doc_ids between {name_a} and {name_b}") |
| 132 | return |
| 133 | |
| 134 | arr_a = np.array([results_a[d] for d in shared_ids], dtype=np.float64) |
| 135 | arr_b = np.array([results_b[d] for d in shared_ids], dtype=np.float64) |
| 136 | diff = arr_a - arr_b # per-question difference |
| 137 | |
| 138 | observed_diff = diff.mean() |
| 139 | n = len(diff) |
| 140 | idx = rng.integers(0, n, size=(BOOTSTRAP_N, n)) |
| 141 | boot_diffs = diff[idx].mean(axis=1) |
| 142 | lo, hi = np.percentile(boot_diffs, [2.5, 97.5]) |
| 143 | |
| 144 | # Two-sided p-value: fraction of bootstrap samples where sign flips |
| 145 | if observed_diff >= 0: |
| 146 | p_value = (boot_diffs <= 0).mean() |
| 147 | else: |
| 148 | p_value = (boot_diffs >= 0).mean() |
| 149 | p_value = min(2 * p_value, 1.0) # two-sided |
| 150 | |
| 151 | sig = "" if p_value < 0.05 else " (not significant)" |
| 152 | print( |
| 153 | f"{name_a} vs {name_b}: " |
| 154 | f"diff = {100 * observed_diff:+.1f}pp, " |
| 155 | f"95% CI [{100 * lo:+.1f}pp, {100 * hi:+.1f}pp], " |
| 156 | f"p = {p_value:.3f}{sig}" |
| 157 | ) |
| 158 | print(f" ({len(shared_ids)} shared questions, {BOOTSTRAP_N:,} bootstrap samples)") |
| 159 | |
| 160 | |
| 161 | async def judge_dir(eval_dir: str, client: AsyncOpenAI, sem: asyncio.Semaphore) -> dict[int, bool]: |