Theme D: Comparing Runs and Gating on Them
Examples 35-46 build the case that "is B better than A" is a hypothesis test, not a comparison of
two printed numbers (co-17): a bare gap between two point estimates is itself noisy (Example 35),
and the actual test that answers the question is a two-proportion z-test (Example 36) or, when
baseline and candidate score the SAME items, a far more sensitive paired test (co-18, Examples
37-38). Statistical significance and practical importance are then shown to be two separate axes
(co-19, Examples 39-40), the bootstrap extends to any statistic with no closed form (co-20,
Examples 41-42), testing many criteria at once manufactures apparent wins unless corrected (co-21,
Examples 43-44), and an unchanged system's own repeat runs decompose into two distinct variance
sources (co-22, co-23, Example 45). The theme closes on one typed ShipDecision that gathers every
number this whole topic built into a single defensible verdict (co-24, Example 46). Every worked
example's real, runnable Python 3.13 file lives under learning/code/ex-NN-*/, run for real
against the pinned statistical toolchain to capture genuine output.
Worked Example 35: Two Point Estimates Are Not a Comparison
ex-35 · exercises co-17, co-01
Context: co-17 -- comparing two runs opens by showing what a bare gap between two point estimates actually is: one noisy draw. This example re-runs the identical comparison between two fixed systems five times, at a typical small sample size, and shows the printed gap itself moves around -- sometimes even pointing the wrong direction.
# learning/code/ex-35-two-point-estimates-are-not-a-comparison/two_point_estimates.py
"""Worked Example 35: Two Point Estimates Are Not a Comparison.""" # => co-17: this file's own restated purpose, doubling as its module __doc__
from __future__ import annotations # => hygiene: postpones annotation evaluation, interpreter-version-agnostic
import random # => co-17: draws independent samples of two systems whose true rates are fixed but unobserved
TRUE_BASELINE_RATE = 0.75 # => co-17: baseline's real, unobservable pass rate
TRUE_CANDIDATE_RATE = 0.80 # => co-17: candidate's real, unobservable pass rate -- genuinely 5 points better
SAMPLE_SIZE = 30 # => co-17: a typical small eval-run size
TRIALS = 5 # => co-01: repeat the SAME comparison several times, to see how much the printed gap itself moves
if __name__ == "__main__": # => co-17: entry point -- runs only when this file executes directly, not on import
print(f"True rates (never observed directly): baseline={TRUE_BASELINE_RATE:.2f} candidate={TRUE_CANDIDATE_RATE:.2f} (candidate is genuinely better by 0.05)") # => co-17: the ground truth this whole example is checking against
gaps: list[float] = [] # => co-01: collects each trial's own observed gap, for the instability check below
for trial in range(1, TRIALS + 1): # => co-01: FIVE separate re-runs of the identical comparison, nothing else changes
baseline_rng = random.Random(trial * 100 + 1) # => co-17: this trial's own independent baseline sample
candidate_rng = random.Random(trial * 100 + 2) # => co-17: this trial's own independent candidate sample
baseline_sample = [baseline_rng.random() < TRUE_BASELINE_RATE for _ in range(SAMPLE_SIZE)] # => co-17: baseline's observed outcomes this trial
candidate_sample = [candidate_rng.random() < TRUE_CANDIDATE_RATE for _ in range(SAMPLE_SIZE)] # => co-17: candidate's observed outcomes this trial
baseline_rate = sum(baseline_sample) / SAMPLE_SIZE # => co-17: baseline's point estimate, this trial
candidate_rate = sum(candidate_sample) / SAMPLE_SIZE # => co-17: candidate's point estimate, this trial
gap = candidate_rate - baseline_rate # => co-01: the NAIVE "comparison" -- just subtracting two bare numbers
gaps.append(gap) # => co-01: stored for the spread check below
print(f"trial {trial}: baseline={baseline_rate:.4f} candidate={candidate_rate:.4f} gap={gap:+.4f}") # => co-01: the number a report that skips a formal test would ship
wrong_direction_trials = sum(1 for g in gaps if g < 0) # => co-01: how many trials showed candidate LOOKING worse, despite being genuinely better
print(f"Trials where candidate LOOKED worse despite being genuinely better: {wrong_direction_trials} of {TRIALS}") # => co-01
assert wrong_direction_trials >= 1, "at least one small-sample trial must show the WRONG-direction gap, to make the instability concrete" # => co-01: the claim this example demonstrates
print("MATCH: the same two systems, compared the same way, produce a gap that sometimes points the WRONG direction at this sample size") # => co-01
# => co-01,co-17: 'candidate scored X points higher than baseline' is not yet a comparison -- it is one noisy draw from a distribution of possible gaps, and ex-36 builds the test that actually answers 'is B better than A'Run: python3 two_point_estimates.py
Output:
True rates (never observed directly): baseline=0.75 candidate=0.80 (candidate is genuinely better by 0.05)
trial 1: baseline=0.8000 candidate=0.9333 gap=+0.1333
trial 2: baseline=0.8667 candidate=0.7667 gap=-0.1000
trial 3: baseline=0.5667 candidate=0.7667 gap=+0.2000
trial 4: baseline=0.7000 candidate=0.8667 gap=+0.1667
trial 5: baseline=0.8000 candidate=0.8333 gap=+0.0333
Trials where candidate LOOKED worse despite being genuinely better: 1 of 5
MATCH: the same two systems, compared the same way, produce a gap that sometimes points the WRONG direction at this sample sizeVerify: across 5 independent trials of the identical two systems, 1 trial shows candidate
scoring LOWER than baseline (a -0.1000 gap) despite candidate's true rate being genuinely 0.05
higher, satisfying co-01's rule that a bare point-estimate gap is one noisy draw, not yet a
comparison.
Key takeaway: "candidate scored higher than baseline" is a fact about one sample, not yet a fact about the two systems -- the same comparison run again can, and sometimes does, disagree.
Why It Matters: this instability is exactly why eval reports that ship on "candidate beat baseline by N points" without a test are gambling on which draw they happened to get -- ex-36 builds the formal test that actually answers the question this bare gap cannot.
Worked Example 36: The Actual Test -- Two-Proportion Z-Test
ex-36 · exercises co-17
Context: continuing co-17, this example runs the SAME two systems from ex-35 through
statsmodels' own two-proportion z-test, at both the small sample size ex-35 used and a properly
powered one, showing the formal test's own verdict changes with sample size even though the true
gap does not.
# learning/code/ex-36-the-actual-test-two-proportion-z-test/two_proportion_z_test.py
"""Worked Example 36: The Actual Test -- Two-Proportion Z-Test.""" # => co-17: this file's own restated purpose, doubling as its module __doc__
from __future__ import annotations # => hygiene: postpones annotation evaluation, interpreter-version-agnostic
import random # => co-17: draws independent baseline and candidate samples, at two different sample sizes
from statsmodels.stats.proportion import proportions_ztest # => co-17: the pinned library's own unpaired two-sample test for comparing two proportions
TRUE_BASELINE_RATE = 0.75 # => co-17: baseline's real, unobservable pass rate -- the SAME systems as ex-35
TRUE_CANDIDATE_RATE = 0.80 # => co-17: candidate's real, unobservable pass rate -- genuinely 5 points better
def sample_outcomes(true_rate: float, n: int, *, seed: int) -> list[bool]: # => co-17: one independent sample of one system's outcomes
"""Draw n independent Bernoulli outcomes from true_rate.""" # => co-17: documents sample_outcomes's contract -- no runtime output, just sets its __doc__
rng = random.Random(seed) # => co-17: one fixed generator per sample
return [rng.random() < true_rate for _ in range(n)] # => co-17: one Bernoulli draw per case
if __name__ == "__main__": # => co-17: entry point -- runs only when this file executes directly, not on import
for n in (30, 300): # => co-17: the SAME true systems, sampled at a typical small size and a properly powered size
baseline = sample_outcomes(TRUE_BASELINE_RATE, n, seed=1) # => co-17: baseline's own sample at this n
candidate = sample_outcomes(TRUE_CANDIDATE_RATE, n, seed=2) # => co-17: candidate's own sample at this n
baseline_rate = sum(baseline) / n # => co-17: baseline's point estimate at this n
candidate_rate = sum(candidate) / n # => co-17: candidate's point estimate at this n
count = [sum(candidate), sum(baseline)] # => co-17: successes for each group, in the order the test compares them
nobs = [n, n] # => co-17: each group's own sample size
z_stat, p_value = proportions_ztest(count, nobs) # => co-17: THE actual hypothesis test -- is this gap distinguishable from sampling noise
print(f"n={n}: baseline={baseline_rate:.4f} candidate={candidate_rate:.4f} gap={candidate_rate - baseline_rate:+.4f} z={z_stat:.4f} p={p_value:.4f}") # => co-17: the test's own verdict, not just the bare gap
if n == 30: # => co-17: the small-sample case -- the SAME sample size ex-35 showed as unstable
assert p_value > 0.05, "at n=30, this test must fail to reach significance -- the sample is too small to distinguish this gap from noise" # => co-17: the underpowered claim
else: # => co-17: the properly powered case
assert p_value < 0.05, "at n=300, this test must reach significance -- the same true gap is now detectable" # => co-17: the properly-powered claim
print("MATCH: 'is B better than A' has a real answer -- a p-value from an actual test -- and that answer depends on whether the sample is large enough to detect the true gap") # => co-17
# => co-17: this is the test ex-35's bare gap was missing -- a formal hypothesis test, not a comparison of two printed numbers, is what actually answers 'is candidate better than baseline'Run: python3 two_proportion_z_test.py
Output:
n=30: baseline=0.7333 candidate=0.8000 gap=+0.0667 z=0.6105 p=0.5416
n=300: baseline=0.7367 candidate=0.8067 gap=+0.0700 z=2.0424 p=0.0411
MATCH: 'is B better than A' has a real answer -- a p-value from an actual test -- and that answer depends on whether the sample is large enough to detect the true gapVerify: at n=30, proportions_ztest() returns p=0.5416 (not significant), while at n=300
the identical ~0.07 true gap returns p=0.0411 (significant), satisfying co-17's rule that "is B
better than A" resolves through a formal test whose own power depends on sample size.
Key takeaway: the two-proportion z-test is the actual answer to "is B better than A" -- a p-value earned from a real comparison, not a bare difference between two point estimates.
Why It Matters: this is the SAME test underlying every "candidate beat baseline" claim in a responsible eval report -- ex-37 and ex-38 next show that when baseline and candidate score the SAME items, this unpaired test is leaving real sensitivity on the table.
Worked Example 37: Paired Data Is More Sensitive
ex-37 · exercises co-18
Context: co-18 -- paired comparison shows that when baseline and candidate score the SAME items (not separate samples), a paired test can detect a difference the unpaired test from ex-36 misses entirely, because pairing removes item-to-item difficulty as a source of noise.
# learning/code/ex-37-paired-data-is-more-sensitive/paired_is_more_sensitive.py
"""Worked Example 37: Paired Data Is More Sensitive.""" # => co-18: this file's own restated purpose, doubling as its module __doc__
from __future__ import annotations # => hygiene: postpones annotation evaluation, interpreter-version-agnostic
import random # => co-18: builds a paired dataset -- baseline and candidate scored on the SAME items
from statsmodels.stats.contingency_tables import mcnemar # => co-18: the paired test -- uses only the discordant pairs
from statsmodels.stats.proportion import proportions_ztest # => co-18: the SAME unpaired test ex-36 used, run here on paired data to make the contrast concrete
def build_paired_dataset(
n: int, *, seed: int, baseline_rate: float, candidate_rate: float, correlation: float
) -> tuple[list[bool], list[bool]]: # => co-18: SAME items, two verdicts each -- baseline and candidate are correlated because they share per-item difficulty
"""Build paired baseline/candidate outcomes over n SHARED items, correlated by per-item difficulty.""" # => co-18: documents build_paired_dataset's contract -- no runtime output, just sets its __doc__
rng = random.Random(seed) # => co-18: drives each item's shared difficulty draw
baseline: list[bool] = [] # => co-18: baseline's verdict, one per item
candidate: list[bool] = [] # => co-18: candidate's verdict on the SAME item, one per item
for _ in range(n): # => co-18: one shared item at a time
difficulty_draw = rng.random() # => co-18: this item's own shared difficulty draw -- read by BOTH systems below
baseline_pass = difficulty_draw < baseline_rate # => co-18: baseline's verdict on this exact item
if rng.random() < correlation: # => co-18: most of the time, candidate's verdict is driven by the SAME difficulty draw -- easy items stay easy for both
candidate_pass = difficulty_draw < candidate_rate # => co-18: correlated verdict -- shares baseline's own notion of "hard" vs "easy" for this item
else: # => co-18: occasionally, candidate's verdict is an independent draw instead -- real systems are correlated but not identical
candidate_pass = rng.random() < candidate_rate # => co-18: an independent verdict, uncorrelated with baseline's difficulty read
baseline.append(baseline_pass) # => co-18: records this item's baseline verdict
candidate.append(candidate_pass) # => co-18: records this item's candidate verdict, SAME item, SAME index
return baseline, candidate # => co-18: two same-length, index-aligned lists -- item i's baseline and candidate verdicts
if __name__ == "__main__": # => co-18: entry point -- runs only when this file executes directly, not on import
n = 50 # => co-18: fifty shared items -- reused unchanged in ex-38
baseline, candidate = build_paired_dataset(n, seed=3, baseline_rate=0.70, candidate_rate=0.84, correlation=0.85) # => co-18: the paired fixture this whole (ex-37, ex-38) pair reuses
baseline_rate = sum(baseline) / n # => co-18: baseline's own pass rate
candidate_rate = sum(candidate) / n # => co-18: candidate's own pass rate
print(f"Baseline rate: {baseline_rate:.4f} | Candidate rate: {candidate_rate:.4f} | gap: {candidate_rate - baseline_rate:+.4f}") # => co-18: the SAME kind of bare gap ex-35 warned about
unpaired_count = [sum(candidate), sum(baseline)] # => co-18: treats the two columns as if they were TWO INDEPENDENT samples -- discards the pairing entirely
unpaired_nobs = [n, n] # => co-18: same nominal sample size either way
_z, unpaired_p = proportions_ztest(unpaired_count, unpaired_nobs) # => co-18: the unpaired test, run on data that is secretly paired
print(f"Unpaired two-proportion z-test (pairing discarded): p={unpaired_p:.4f}") # => co-18: this test cannot see WHICH items changed, only the two marginal rates
both_pass = sum(1 for b, c in zip(baseline, candidate) if b and c) # => co-18: items both systems got right -- uninformative about which system is better
both_fail = sum(1 for b, c in zip(baseline, candidate) if not b and not c) # => co-18: items both systems got wrong -- also uninformative
baseline_only = sum(1 for b, c in zip(baseline, candidate) if b and not c) # => co-18: items where candidate REGRESSED relative to baseline
candidate_only = sum(1 for b, c in zip(baseline, candidate) if not b and c) # => co-18: items where candidate IMPROVED relative to baseline -- the informative cell
table = [[both_pass, baseline_only], [candidate_only, both_fail]] # => co-18: the 2x2 contingency table McNemar's test actually uses
paired_result = mcnemar(table, exact=False, correction=True) # => co-18: the PAIRED test -- uses only the discordant pairs (baseline_only, candidate_only)
print(f"Paired McNemar test (same discordant items): p={paired_result.pvalue:.4f}") # => co-18: this test sees EXACTLY which items flipped, and in which direction
assert unpaired_p > 0.05, "the unpaired test, discarding the pairing, must fail to reach significance on this data" # => co-18: the less-sensitive claim
assert paired_result.pvalue < 0.05, "the paired test, using the SAME data's pairing, must reach significance" # => co-18: the more-sensitive claim
print(f"MATCH: the identical underlying data is NOT significant unpaired (p={unpaired_p:.4f}) but IS significant paired (p={paired_result.pvalue:.4f}) -- pairing is where the sensitivity comes from") # => co-18
# => co-18: throwing away which items changed and keeping only the two marginal rates is throwing away exactly the information a paired test needs -- ex-38 builds that paired test from its own definitionRun: python3 paired_is_more_sensitive.py
Output:
Baseline rate: 0.6600 | Candidate rate: 0.8000 | gap: +0.1400
Unpaired two-proportion z-test (pairing discarded): p=0.1149
Paired McNemar test (same discordant items): p=0.0455
MATCH: the identical underlying data is NOT significant unpaired (p=0.1149) but IS significant paired (p=0.0455) -- pairing is where the sensitivity comes fromVerify: on the identical 50-item paired dataset, the unpaired z-test returns p=0.1149 (not
significant) while the paired McNemar test returns p=0.0455 (significant), satisfying co-18's
rule that a paired test is far more sensitive than treating the same paired data as independent
samples.
Key takeaway: discarding which items flipped and keeping only the two marginal rates throws away exactly the information a paired comparison needs -- when items are shared, use a paired test.
Why It Matters: most real eval comparisons DO score baseline and candidate on the same eval set, making this the common case, not the exception -- treating that data as if it were two independent samples routinely under-detects real improvements.
Worked Example 38: McNemar's Test, From Definition
ex-38 · exercises co-18
Context: continuing co-18, this example derives ex-37's McNemar statistic from its own textbook formula -- using only the two discordant counts -- and verifies it against both the pinned library and an independent exact test.
# learning/code/ex-38-mcnemars-test-from-definition/mcnemar_from_definition.py
"""Worked Example 38: McNemar's Test, From Definition.""" # => co-18: this file's own restated purpose, doubling as its module __doc__
from __future__ import annotations # => hygiene: postpones annotation evaluation, interpreter-version-agnostic
import math # => co-18: isclose -- verifies the from-definition and library statistics agree
import random # => co-18: rebuilds the SAME paired dataset ex-37 introduced
from scipy.stats import binomtest, chi2 # => co-18: an EXACT alternative test, for cross-checking the chi2 approximation at this small discordant-pair count
from statsmodels.stats.contingency_tables import mcnemar # => co-18: the pinned library's own paired test
def build_paired_dataset(n: int, *, seed: int, baseline_rate: float, candidate_rate: float, correlation: float) -> tuple[list[bool], list[bool]]: # => co-18: the identical fixture-building function from ex-37
"""Build paired baseline/candidate outcomes over n SHARED items, correlated by per-item difficulty.""" # => co-18: documents build_paired_dataset's contract -- no runtime output, just sets its __doc__
rng = random.Random(seed) # => co-18: drives each item's shared difficulty draw
baseline: list[bool] = [] # => co-18: baseline's verdict, one per item
candidate: list[bool] = [] # => co-18: candidate's verdict on the SAME item, one per item
for _ in range(n): # => co-18: one shared item at a time
difficulty_draw = rng.random() # => co-18: this item's own shared difficulty draw -- read by BOTH systems below
baseline_pass = difficulty_draw < baseline_rate # => co-18: baseline's verdict on this exact item
if rng.random() < correlation: # => co-18: most of the time, candidate's verdict is driven by the SAME difficulty draw
candidate_pass = difficulty_draw < candidate_rate # => co-18: correlated verdict
else: # => co-18: occasionally, candidate's verdict is an independent draw instead
candidate_pass = rng.random() < candidate_rate # => co-18: an independent verdict
baseline.append(baseline_pass) # => co-18: records this item's baseline verdict
candidate.append(candidate_pass) # => co-18: records this item's candidate verdict, SAME item, SAME index
return baseline, candidate # => co-18: two same-length, index-aligned lists
def mcnemar_from_definition(baseline_only: int, candidate_only: int) -> tuple[float, float]: # => co-18: the textbook chi2-corrected McNemar formula, in code
"""Return (statistic, p_value) for the continuity-corrected McNemar chi2 test on the two discordant counts.""" # => co-18: documents mcnemar_from_definition's contract -- no runtime output, just sets its __doc__
statistic = (abs(baseline_only - candidate_only) - 1) ** 2 / (baseline_only + candidate_only) # => co-18: Yates' continuity correction -- the "-1" before squaring
p_value = 1 - chi2.cdf(statistic, df=1) # => co-18: the chi-squared survival function at 1 degree of freedom -- ONLY the two discordant cells ever enter this formula
return statistic, p_value # => co-18: returns this computed value to the caller
if __name__ == "__main__": # => co-18: entry point -- runs only when this file executes directly, not on import
n = 50 # => co-18: the SAME fifty shared items as ex-37
baseline, candidate = build_paired_dataset(n, seed=3, baseline_rate=0.70, candidate_rate=0.84, correlation=0.85) # => co-18: reproduces ex-37's exact fixture
both_pass = sum(1 for b, c in zip(baseline, candidate) if b and c) # => co-18: concordant -- both right, uninformative for this test
both_fail = sum(1 for b, c in zip(baseline, candidate) if not b and not c) # => co-18: concordant -- both wrong, uninformative for this test
baseline_only = sum(1 for b, c in zip(baseline, candidate) if b and not c) # => co-18: discordant -- candidate REGRESSED on this item
candidate_only = sum(1 for b, c in zip(baseline, candidate) if not b and c) # => co-18: discordant -- candidate IMPROVED on this item
print(f"Discordant pairs: baseline-only (regressions)={baseline_only} | candidate-only (improvements)={candidate_only}") # => co-18: the ONLY two numbers McNemar's test actually uses
stat_def, p_def = mcnemar_from_definition(baseline_only, candidate_only) # => co-18: computed from the formula directly
print(f"McNemar (from definition): statistic={stat_def:.4f} p={p_def:.4f}") # => co-18: the chi2-corrected coefficient this file derives itself
table = [[both_pass, baseline_only], [candidate_only, both_fail]] # => co-18: the full 2x2 table the library's own function expects
lib_result = mcnemar(table, exact=False, correction=True) # => co-18: the SAME computation, called from the pinned library
print(f"McNemar (statsmodels library): statistic={lib_result.statistic:.4f} p={lib_result.pvalue:.4f}") # => co-18: the library's answer to the identical question
assert math.isclose(stat_def, lib_result.statistic, abs_tol=1e-9), "the from-definition and library statistics must agree" # => co-18
assert math.isclose(p_def, lib_result.pvalue, abs_tol=1e-9), "the from-definition and library p-values must agree" # => co-18
exact = binomtest(min(baseline_only, candidate_only), n=baseline_only + candidate_only, p=0.5, alternative="two-sided") # => co-18: cross-checks the chi2 approximation with an EXACT test -- appropriate given only 9 discordant pairs
print(f"McNemar (exact binomial cross-check): p={exact.pvalue:.4f}") # => co-18: a second, independent confirmation this is genuinely significant
assert p_def < 0.05, "McNemar's test must reach significance on this paired data" # => co-18: the claim this example demonstrates
print(f"MATCH: the from-definition statistic ({stat_def:.4f}), the library's statistic, and an independent exact test all agree this paired difference is significant") # => co-18
# => co-18: McNemar's test is nothing more than 'compare the two discordant counts to each other' -- everything about the items where both systems agreed is thrown away on purpose, because it carries no information about which system is betterRun: python3 mcnemar_from_definition.py
Output:
Discordant pairs: baseline-only (regressions)=1 | candidate-only (improvements)=8
McNemar (from definition): statistic=4.0000 p=0.0455
McNemar (statsmodels library): statistic=4.0000 p=0.0455
McNemar (exact binomial cross-check): p=0.0391
MATCH: the from-definition statistic (4.0000), the library's statistic, and an independent exact test all agree this paired difference is significantVerify: mcnemar_from_definition() and the statsmodels library both return statistic=4.0000
and p=0.0455, with an independent exact binomial cross-check returning p=0.0391 (also
significant), satisfying co-18's rule that McNemar's test is fully derivable from just the two
discordant counts.
Key takeaway: McNemar's test uses only the discordant pairs -- every item both systems agreed on, right or wrong, carries zero information about which system is better.
Why It Matters: knowing McNemar's test is "just the two discordant counts" makes it easy to sanity-check any paired-comparison library call by hand -- if the discordant counts printed in a report do not match the p-value's own direction, something upstream is broken.
Worked Example 39: Statistically Significant, Practically Irrelevant
ex-39 · exercises co-19
Context: co-19 -- significance vs. practical importance shows the two are separate axes: at a large enough sample size, even a genuinely tiny true gap becomes statistically detectable, while staying below any reasonable materiality bar.
# learning/code/ex-39-statistically-significant-practically-irrelevant/statistically_significant_practically_irrelevant.py
"""Worked Example 39: Statistically Significant, Practically Irrelevant.""" # => co-19: this file's own restated purpose, doubling as its module __doc__
from __future__ import annotations # => hygiene: postpones annotation evaluation, interpreter-version-agnostic
import random # => co-19: draws two large independent samples of two near-identical systems
from statsmodels.stats.proportion import proportions_ztest # => co-19: the SAME two-proportion test ex-36 introduced
TRUE_A_RATE = 0.850 # => co-19: system A's real pass rate
TRUE_B_RATE = 0.862 # => co-19: system B's real pass rate -- only 1.2 points higher, genuinely
N = 8000 # => co-19: a LARGE eval run -- big enough to detect even a small true gap
MATERIALITY_THRESHOLD = 0.03 # => co-19: this team's own stated bar for "worth acting on" -- a 3-point gap or larger
def sample_outcomes(true_rate: float, n: int, *, seed: int) -> list[bool]: # => co-19: one independent sample of one system's outcomes
"""Draw n independent Bernoulli outcomes from true_rate.""" # => co-19: documents sample_outcomes's contract -- no runtime output, just sets its __doc__
rng = random.Random(seed) # => co-19: one fixed generator per sample
return [rng.random() < true_rate for _ in range(n)] # => co-19: one Bernoulli draw per case
if __name__ == "__main__": # => co-19: entry point -- runs only when this file executes directly, not on import
a = sample_outcomes(TRUE_A_RATE, N, seed=31) # => co-19: system A's own large sample
b = sample_outcomes(TRUE_B_RATE, N, seed=32) # => co-19: system B's own large sample
a_rate = sum(a) / N # => co-19: A's point estimate
b_rate = sum(b) / N # => co-19: B's point estimate
gap = b_rate - a_rate # => co-19: the observed gap
print(f"N={N}: A={a_rate:.4f} B={b_rate:.4f} gap={gap:+.4f}") # => co-19: a genuinely small gap
count = [sum(b), sum(a)] # => co-19: successes for each group
nobs = [N, N] # => co-19: each group's sample size
z_stat, p_value = proportions_ztest(count, nobs) # => co-19: the SAME formal test ex-36 built
print(f"Two-proportion z-test: z={z_stat:.4f} p={p_value:.4f}") # => co-19: the test's own verdict
assert p_value < 0.05, "at this large n, the test must reach statistical significance despite the small true gap" # => co-19: the "statistically significant" half of the claim
assert abs(gap) < MATERIALITY_THRESHOLD, "the observed gap must fall BELOW this team's own materiality threshold, despite being statistically significant" # => co-19: the "practically irrelevant" half of the claim
print(f"MATCH: p={p_value:.4f} < 0.05 (statistically significant) AND gap={gap:+.4f} is below the {MATERIALITY_THRESHOLD:.2f} materiality threshold (practically irrelevant)") # => co-19
# => co-19: a large enough sample can make even a genuinely trivial difference statistically detectable -- 'significant' answers 'is there a real difference,' never 'is this difference worth acting on'Run: python3 statistically_significant_practically_irrelevant.py
Output:
N=8000: A=0.8474 B=0.8650 gap=+0.0176
Two-proportion z-test: z=3.1767 p=0.0015
MATCH: p=0.0015 < 0.05 (statistically significant) AND gap=+0.0176 is below the 0.03 materiality threshold (practically irrelevant)Verify: at N=8000, the observed gap (+0.0176) returns p=0.0015 (clearly significant)
while sitting below this team's own 0.03 materiality threshold, satisfying co-19's rule that
statistical significance and practical importance are two separate axes.
Key takeaway: "significant" answers "is there a real difference," never "is this difference worth acting on" -- those two questions need two separate, explicitly stated bars.
Why It Matters: at scale, nearly every comparison eventually becomes statistically significant if the sample grows large enough -- a stated materiality threshold, decided BEFORE looking at the result, is what keeps a report from chasing statistically real but practically meaningless wins.
Worked Example 40: Not Yet Significant, but Practically Important
ex-40 · exercises co-19, co-06
Context: continuing co-19, this example shows the reverse failure: a genuinely large, practically important 8-point gap is invisible at a typical small sample size, and co-06's own power calculation gives the exact n needed to detect it.
# learning/code/ex-40-not-yet-significant-but-practically-important/not_yet_significant.py
"""Worked Example 40: Not Yet Significant, but Practically Important.""" # => co-19: this file's own restated purpose, doubling as its module __doc__
from __future__ import annotations # => hygiene: postpones annotation evaluation, interpreter-version-agnostic
import math # => co-06: ceil -- a sample size must be a whole number of cases
import random # => co-19: draws samples of two systems with a genuinely large true gap
from statsmodels.stats.power import NormalIndPower # => co-06: the pinned library's own power-based sample-size solver
from statsmodels.stats.proportion import proportion_effectsize, proportions_ztest # => co-06: Cohen's h -- the effect-size input the power solver needs; co-19: the SAME test ex-36/ex-39 used
TRUE_A_RATE = 0.70 # => co-19: system A's real pass rate
TRUE_B_RATE = 0.78 # => co-19: system B's real pass rate -- a genuinely large, practically important 8-point gap
SMALL_N = 25 # => co-19: a typical small eval run -- the size a team might reach for first
def sample_outcomes(true_rate: float, n: int, *, seed: int) -> list[bool]: # => co-19: one independent sample of one system's outcomes
"""Draw n independent Bernoulli outcomes from true_rate.""" # => co-19: documents sample_outcomes's contract -- no runtime output, just sets its __doc__
rng = random.Random(seed) # => co-19: one fixed generator per sample
return [rng.random() < true_rate for _ in range(n)] # => co-19: one Bernoulli draw per case
if __name__ == "__main__": # => co-06: entry point -- runs only when this file executes directly, not on import
a_small = sample_outcomes(TRUE_A_RATE, SMALL_N, seed=11) # => co-19: A's sample at the small, typical size
b_small = sample_outcomes(TRUE_B_RATE, SMALL_N, seed=12) # => co-19: B's sample at the small, typical size
count_small = [sum(b_small), sum(a_small)] # => co-19: successes for each group, small sample
_z_small, p_small = proportions_ztest(count_small, [SMALL_N, SMALL_N]) # => co-19: the formal test, applied at the small sample size
print(f"n={SMALL_N}: A={sum(a_small) / SMALL_N:.4f} B={sum(b_small) / SMALL_N:.4f} p={p_small:.4f}") # => co-19: looks unremarkable -- NOT significant
effect_size = proportion_effectsize(TRUE_B_RATE, TRUE_A_RATE) # => co-06: Cohen's h -- how large this gap is in the units the power solver needs
power_analysis = NormalIndPower() # => co-06: the pinned library's own two-sample power calculator
required_n_raw = power_analysis.solve_power(effect_size=effect_size, alpha=0.05, power=0.80, ratio=1.0, alternative="two-sided") # => co-06: how many cases PER GROUP are needed to detect this exact gap 80% of the time
required_n = math.ceil(required_n_raw) # => co-06: round UP -- a fractional case cannot be collected
print(f"Effect size (Cohen's h): {effect_size:.4f} | Required n per group for 80% power: {required_n}") # => co-06: the computed, justified target -- not a guess
a_big = sample_outcomes(TRUE_A_RATE, required_n, seed=11) # => co-19: A's sample at the COMPUTED required size, same underlying true rate
b_big = sample_outcomes(TRUE_B_RATE, required_n, seed=12) # => co-19: B's sample at the COMPUTED required size, same underlying true rate
count_big = [sum(b_big), sum(a_big)] # => co-19: successes for each group, at the required sample size
_z_big, p_big = proportions_ztest(count_big, [required_n, required_n]) # => co-19: the SAME test, now properly powered
print(f"n={required_n}: A={sum(a_big) / required_n:.4f} B={sum(b_big) / required_n:.4f} p={p_big:.4f}") # => co-19: the SAME real gap, now clearly detected
assert p_small > 0.05, "at n=25, this genuinely large gap must still fail to reach significance" # => co-19: the "not yet significant" half of the claim
assert p_big < 0.05, "at the computed required n, the SAME true gap must reach significance" # => co-19: the "practically important, and now detected" half of the claim
print(f"MATCH: the identical 8-point true gap is invisible at n={SMALL_N} (p={p_small:.4f}) but clearly detected at the computed n={required_n} (p={p_big:.4f})") # => co-19
# => co-06,co-19: 'not significant' at a small sample does not mean 'not real' -- it can mean the sample was never large enough to see a gap that a power calculation would have said needed a specific, computable nRun: python3 not_yet_significant.py
Output:
n=25: A=0.8000 B=0.8400 p=0.7128
Effect size (Cohen's h): 0.1829 | Required n per group for 80% power: 470
n=470: A=0.6894 B=0.7936 p=0.0003
MATCH: the identical 8-point true gap is invisible at n=25 (p=0.7128) but clearly detected at the computed n=470 (p=0.0003)Verify: the identical 0.08 true gap returns p=0.7128 at n=25 but p=0.0003 at the
power-computed n=470, satisfying co-19's and co-06's rule that a real, practically important
effect can stay statistically invisible until the sample reaches its own computed required size.
Key takeaway: "not significant" at a small sample is not evidence "not real" -- it can simply mean the sample was never large enough, and a power calculation says exactly how large is enough.
Why It Matters: this pairs directly with ex-39's opposite failure -- together they show significance and materiality are two separate, both-necessary checks, and co-06's power calculation is what turns "we need more data" from a vague feeling into a specific, justified n.
Worked Example 41: Bootstrap for a Statistic With No Closed Form
ex-41 · exercises co-20
Context: co-20 -- bootstrap resampling extends beyond agreement coefficients (ex-30) to any named statistic with no textbook standard-error formula. This example puts a bootstrap interval around a median latency, and checks the interval has already stabilized at a modest resample count.
# learning/code/ex-41-bootstrap-for-a-statistic-with-no-closed-form/bootstrap_median_latency.py
"""Worked Example 41: Bootstrap for a Statistic With No Closed Form.""" # => co-20: this file's own restated purpose, doubling as its module __doc__
from __future__ import annotations # => hygiene: postpones annotation evaluation, interpreter-version-agnostic
import random # => co-20: builds a right-skewed latency sample -- a realistic shape for real request timings
import numpy as np # => co-20: scipy's bootstrap operates on numpy arrays
from scipy.stats import bootstrap # => co-20: no closed-form standard error exists for a median -- bootstrap resampling instead
def build_latency_sample(n: int, *, seed: int) -> list[float]: # => co-20: a right-skewed distribution of per-request latencies, in milliseconds
"""Build n simulated request latencies (ms), log-normally distributed -- a realistic right-skewed shape.""" # => co-20: documents build_latency_sample's contract -- no runtime output, just sets its __doc__
rng = random.Random(seed) # => co-20: one fixed generator -- reproducible latency draws
return [round(rng.lognormvariate(mu=6.0, sigma=0.5)) for _ in range(n)] # => co-20: most requests are fast, a long tail is slow -- exactly the shape a median (not a mean) is meant to summarize
def median_statistic(x: np.ndarray, axis: int = -1) -> np.ndarray | float: # => co-20: the statistic scipy's bootstrap resamples -- unlike a proportion or a mean, a median has no textbook standard-error formula
"""Compute the median of x, vectorized over resample rows when axis is given.""" # => co-20: documents median_statistic's contract -- no runtime output, just sets its __doc__
return np.median(x, axis=axis) # => co-20: numpy's own median -- works identically whether x is one sample or a whole batch of resamples
if __name__ == "__main__": # => co-20: entry point -- runs only when this file executes directly, not on import
sample = build_latency_sample(50, seed=42) # => co-20: fifty simulated latencies -- a realistic eval-run size for a latency check
observed_median = float(np.median(sample)) # => co-20: the point estimate -- the ONLY number a bare report would show
print(f"n=50 observed median latency: {observed_median:.1f}ms") # => co-20: no formula exists to hand-derive this number's own standard error
arr = np.array(sample, dtype=float) # => co-20: scipy's bootstrap resamples this array with replacement
for n_resamples in (1000, 5000): # => co-20: checks whether the interval has already STABILIZED at a modest resample count
result = bootstrap(
(arr,), median_statistic, vectorized=True, confidence_level=0.95, n_resamples=n_resamples, method="percentile", rng=np.random.default_rng(42)
) # => co-20: resamples the 50 latencies with replacement, recomputes the median each time
low, high = result.confidence_interval # => co-20: unpacks the interval's two ends
print(f"n_resamples={n_resamples}: bootstrap 95% CI on median = [{low:.1f}, {high:.1f}]ms") # => co-20: the honest report -- a range around the median, not a bare point
assert low < observed_median < high, "the observed median must sit inside its own bootstrap interval" # => co-20: a basic sanity check on the interval itself
print("MATCH: the bootstrap interval on the median stays essentially stable between 1000 and 5000 resamples -- 1000 was already enough to trust") # => co-20
# => co-20: any named statistic with no closed-form formula for its own uncertainty -- a median, a trimmed mean, a custom aggregate metric -- can still get an honest interval via resampling; the statistic itself never has to changeRun: python3 bootstrap_median_latency.py
Output:
n=50 observed median latency: 441.5ms
n_resamples=1000: bootstrap 95% CI on median = [334.0, 524.0]ms
n_resamples=5000: bootstrap 95% CI on median = [332.0, 529.5]ms
MATCH: the bootstrap interval on the median stays essentially stable between 1000 and 5000 resamples -- 1000 was already enough to trustVerify: at n_resamples=1000, the bootstrap interval is [334.0, 524.0], and at 5000 it is
[332.0, 529.5] -- essentially unchanged -- satisfying co-20's rule that a modest resample count
is enough to trust the interval once it has stabilized.
Key takeaway: bootstrap resampling puts an honest interval around any statistic, whether or not a closed-form formula exists for its uncertainty -- the median needed no special-case math.
Why It Matters: real eval metrics are rarely limited to proportions and kappas -- latency percentiles, trimmed means, and custom composite scores all lack closed-form standard errors, and the bootstrap is the one technique that covers all of them with the same procedure.
Worked Example 42: Honest Small-n Uncertainty
ex-42 · exercises co-20
Context: continuing co-20, this example reuses ex-41's latency fixture at a small vs. a much larger sample size, showing the bootstrap interval's own width shrinks dramatically as n grows -- making the small-sample uncertainty visible rather than hidden.
# learning/code/ex-42-honest-small-n-uncertainty/honest_small_n_uncertainty.py
"""Worked Example 42: Honest Small-n Uncertainty.""" # => co-20: this file's own restated purpose, doubling as its module __doc__
from __future__ import annotations # => hygiene: postpones annotation evaluation, interpreter-version-agnostic
import random # => co-20: rebuilds the SAME latency-generating fixture at two very different sample sizes
import numpy as np # => co-20: scipy's bootstrap operates on numpy arrays
from scipy.stats import bootstrap # => co-20: the SAME bootstrap machinery ex-41 introduced
def build_latency_sample(n: int, *, seed: int) -> list[float]: # => co-20: the identical fixture-building function from ex-41
"""Build n simulated request latencies (ms), log-normally distributed -- a realistic right-skewed shape.""" # => co-20: documents build_latency_sample's contract -- no runtime output, just sets its __doc__
rng = random.Random(seed) # => co-20: one fixed generator -- reproducible latency draws
return [round(rng.lognormvariate(mu=6.0, sigma=0.5)) for _ in range(n)] # => co-20: most requests are fast, a long tail is slow
def median_statistic(x: np.ndarray, axis: int = -1) -> np.ndarray | float: # => co-20: the identical statistic function from ex-41
"""Compute the median of x, vectorized over resample rows when axis is given.""" # => co-20: documents median_statistic's contract -- no runtime output, just sets its __doc__
return np.median(x, axis=axis) # => co-20: numpy's own median
if __name__ == "__main__": # => co-20: entry point -- runs only when this file executes directly, not on import
widths: dict[int, float] = {} # => co-20: collects each sample size's own interval width, for the shrinkage check below
for n in (15, 150): # => co-20: the SAME latency-generating process, sampled a small vs. a much larger number of times
sample = build_latency_sample(n, seed=42) # => co-20: this sample size's own draw
arr = np.array(sample, dtype=float) # => co-20: scipy's bootstrap resamples this array with replacement
median = float(np.median(sample)) # => co-20: this sample's own point estimate
result = bootstrap((arr,), median_statistic, vectorized=True, confidence_level=0.95, n_resamples=3000, method="percentile", rng=np.random.default_rng(7)) # => co-20: the SAME bootstrap procedure, only n changes
low, high = result.confidence_interval # => co-20: this sample size's own interval bounds
width = high - low # => co-20: how WIDE the honest uncertainty band is, at this sample size
widths[n] = width # => co-20: stored for the comparison below
print(f"n={n}: median={median:.1f}ms bootstrap 95% CI=[{low:.1f}, {high:.1f}]ms width={width:.1f}ms") # => co-20: the full, honest report at each sample size
assert widths[15] > 3 * widths[150], "the n=15 interval must be dramatically wider than the n=150 interval -- small-n uncertainty is not a rounding error" # => co-20: the claim this example demonstrates
print(f"MATCH: the n=15 interval ({widths[15]:.1f}ms wide) is over {widths[15] / widths[150]:.1f}x wider than the n=150 interval ({widths[150]:.1f}ms wide), on the SAME underlying latency distribution") # => co-20
# => co-20: a bootstrap interval does not manufacture precision a small sample does not have -- it reports, honestly, exactly how little a 15-case sample can actually pin down, which a bare median alone would have hidden entirelyRun: python3 honest_small_n_uncertainty.py
Output:
n=15: median=535.0ms bootstrap 95% CI=[320.0, 856.0]ms width=536.0ms
n=150: median=377.0ms bootstrap 95% CI=[356.0, 455.5]ms width=99.5ms
MATCH: the n=15 interval (536.0ms wide) is over 5.4x wider than the n=150 interval (99.5ms wide), on the SAME underlying latency distributionVerify: at n=15, the bootstrap interval is 536.0ms wide, over 5.4x wider than the 99.5ms
interval at n=150, on the same underlying latency distribution, satisfying co-20's rule that a
bootstrap interval reports small-sample uncertainty honestly rather than hiding it.
Key takeaway: a bootstrap interval does not fabricate precision a small sample never had -- a wide interval at low n is the correct, honest answer, not a flaw in the method.
Why It Matters: a 15-case latency check that reports only its median, with no interval, hides exactly the fact this example makes visible -- that the true median could plausibly be anywhere in a 536ms-wide band, information no ship decision should be made without.
Worked Example 43: The Multiple Comparisons Trap
ex-43 · exercises co-21
Context: co-21 -- multiple comparisons shows that testing many criteria at once manufactures apparent wins by pure chance. This example runs twenty separate two-proportion tests where baseline and candidate share the IDENTICAL true rate on every criterion, and finds a "significant" result anyway.
# learning/code/ex-43-the-multiple-comparisons-trap/multiple_comparisons_trap.py
"""Worked Example 43: The Multiple Comparisons Trap.""" # => co-21: this file's own restated purpose, doubling as its module __doc__
from __future__ import annotations # => hygiene: postpones annotation evaluation, interpreter-version-agnostic
import random # => co-21: draws twenty INDEPENDENT criterion comparisons, none of them actually different
from statsmodels.stats.proportion import proportions_ztest # => co-21: the SAME two-proportion test used throughout this theme
TRUE_RATE = 0.80 # => co-21: baseline and candidate have the IDENTICAL true rate on EVERY criterion -- no real difference exists anywhere
N = 30 # => co-21: a typical small per-criterion eval-run size
N_CRITERIA = 20 # => co-21: a realistic rubric size -- twenty separate criteria, each tested independently
def sample_outcomes(true_rate: float, n: int, *, seed: int) -> list[bool]: # => co-21: one independent sample of one system on one criterion
"""Draw n independent Bernoulli outcomes from true_rate.""" # => co-21: documents sample_outcomes's contract -- no runtime output, just sets its __doc__
rng = random.Random(seed) # => co-21: one fixed generator per (criterion, system) sample
return [rng.random() < true_rate for _ in range(n)] # => co-21: one Bernoulli draw per case
if __name__ == "__main__": # => co-21: entry point -- runs only when this file executes directly, not on import
pvalues: list[float] = [] # => co-21: one p-value per criterion, from twenty SEPARATE, independent tests
for criterion in range(N_CRITERIA): # => co-21: tests EVERY criterion the SAME way -- no criterion is special
baseline = sample_outcomes(TRUE_RATE, N, seed=criterion * 2 + 500) # => co-21: this criterion's own baseline sample
candidate = sample_outcomes(TRUE_RATE, N, seed=criterion * 2 + 501) # => co-21: this criterion's own candidate sample -- SAME true rate as baseline
count = [sum(candidate), sum(baseline)] # => co-21: successes for each group, this criterion
_z, p = proportions_ztest(count, [N, N]) # => co-21: this criterion's own p-value
pvalues.append(p) # => co-21: stored for the sweep below
significant = [i for i, p in enumerate(pvalues) if p < 0.05] # => co-21: every criterion that CROSSED the ordinary 0.05 threshold, uncorrected
print(f"Criteria tested: {N_CRITERIA} | criteria with p < 0.05 (uncorrected): {significant}") # => co-21: how many "wins" a report that skips correction would claim
for i in significant: # => co-21: prints the "winning" criterion's own p-value
print(f" criterion {i}: p={pvalues[i]:.4f}") # => co-21: looks like a real, ordinary significant result
assert len(significant) >= 1, "at 20 independent tests, at least one must cross p < 0.05 purely by chance, on the SAME true rate for this fixed seed" # => co-21: the claim this example demonstrates
print(f"MATCH: {len(significant)} of {N_CRITERIA} criteria appear 'significant' even though baseline and candidate have the IDENTICAL true rate ({TRUE_RATE}) on every single one") # => co-21
# => co-21: testing enough criteria at the ordinary 0.05 threshold manufactures apparent wins out of pure noise -- ex-44 corrects for exactly thisRun: python3 multiple_comparisons_trap.py
Output:
Criteria tested: 20 | criteria with p < 0.05 (uncorrected): [14]
criterion 14: p=0.0227
MATCH: 1 of 20 criteria appear 'significant' even though baseline and candidate have the IDENTICAL true rate (0.8) on every single oneVerify: across 20 independent tests of two systems sharing the IDENTICAL true rate, criterion
14 returns p=0.0227 (uncorrected), satisfying co-21's rule that testing enough criteria
manufactures at least one apparent win by chance alone.
Key takeaway: testing many criteria at the ordinary 0.05 threshold is not "twenty honest tests" -- it is one test run twenty times, with a rising chance one of them crosses the line by luck alone.
Why It Matters: a rubric with many criteria (faithfulness, tone, safety, formatting, and more) is exactly this scenario -- reporting "criterion 14 improved significantly" without correcting for the other nineteen tests is a phantom win waiting to be believed.
Worked Example 44: Correcting for Multiple Comparisons
ex-44 · exercises co-21
Context: continuing co-21, this example applies Bonferroni and Benjamini-Hochberg correction to ex-43's exact twenty p-values, showing both correctly eliminate the phantom win.
# learning/code/ex-44-correcting-for-multiple-comparisons/correcting_for_multiple_comparisons.py
"""Worked Example 44: Correcting for Multiple Comparisons.""" # => co-21: this file's own restated purpose, doubling as its module __doc__
from __future__ import annotations # => hygiene: postpones annotation evaluation, interpreter-version-agnostic
import random # => co-21: rebuilds the SAME twenty-criteria fixture ex-43 introduced
from statsmodels.stats.multitest import multipletests # => co-21: the pinned library's own multiple-comparisons correction -- both Bonferroni and Benjamini-Hochberg
from statsmodels.stats.proportion import proportions_ztest # => co-21: the SAME two-proportion test used throughout this theme
TRUE_RATE = 0.80 # => co-21: baseline and candidate have the IDENTICAL true rate on EVERY criterion -- the SAME fixture as ex-43
N = 30 # => co-21: a typical small per-criterion eval-run size
N_CRITERIA = 20 # => co-21: the SAME twenty criteria as ex-43
def sample_outcomes(true_rate: float, n: int, *, seed: int) -> list[bool]: # => co-21: the identical sampling function from ex-43
"""Draw n independent Bernoulli outcomes from true_rate.""" # => co-21: documents sample_outcomes's contract -- no runtime output, just sets its __doc__
rng = random.Random(seed) # => co-21: one fixed generator per (criterion, system) sample
return [rng.random() < true_rate for _ in range(n)] # => co-21: one Bernoulli draw per case
if __name__ == "__main__": # => co-21: entry point -- runs only when this file executes directly, not on import
pvalues: list[float] = [] # => co-21: reproduces ex-43's exact twenty p-values
for criterion in range(N_CRITERIA): # => co-21: the SAME twenty criteria, in the SAME order
baseline = sample_outcomes(TRUE_RATE, N, seed=criterion * 2 + 500) # => co-21: this criterion's own baseline sample -- SAME seed as ex-43
candidate = sample_outcomes(TRUE_RATE, N, seed=criterion * 2 + 501) # => co-21: this criterion's own candidate sample -- SAME seed as ex-43
count = [sum(candidate), sum(baseline)] # => co-21: successes for each group, this criterion
_z, p = proportions_ztest(count, [N, N]) # => co-21: this criterion's own p-value
pvalues.append(p) # => co-21: stored for correction below
uncorrected_significant = [i for i, p in enumerate(pvalues) if p < 0.05] # => co-21: ex-43's own phantom win -- criterion 14, uncorrected
print(f"Uncorrected significant criteria: {uncorrected_significant}") # => co-21: reproduces ex-43's result exactly, for direct comparison
reject_bonferroni, _corrected_p_bonferroni, _, _ = multipletests(pvalues, alpha=0.05, method="bonferroni") # => co-21: the STRICT correction -- divides alpha by the number of tests
bonferroni_significant = [i for i, rejected in enumerate(reject_bonferroni) if rejected] # => co-21: which criteria survive the strict correction
print(f"Bonferroni-corrected significant criteria: {bonferroni_significant}") # => co-21: the phantom win should NOT survive
reject_bh, _corrected_p_bh, _, _ = multipletests(pvalues, alpha=0.05, method="fdr_bh") # => co-21: the LESS strict Benjamini-Hochberg false-discovery-rate correction
bh_significant = [i for i, rejected in enumerate(reject_bh) if rejected] # => co-21: which criteria survive the FDR correction
print(f"Benjamini-Hochberg-corrected significant criteria: {bh_significant}") # => co-21: even the more forgiving correction should reject this phantom win
assert len(uncorrected_significant) >= 1, "the uncorrected sweep must reproduce ex-43's own phantom win" # => co-21: confirms this file reproduces the SAME fixture
assert len(bonferroni_significant) == 0, "Bonferroni correction must eliminate the phantom win entirely" # => co-21: the strict-correction claim
assert len(bh_significant) == 0, "Benjamini-Hochberg correction must ALSO eliminate the phantom win" # => co-21: the FDR-correction claim
print("MATCH: both Bonferroni and Benjamini-Hochberg correction eliminate the criterion that looked significant BEFORE correcting for testing twenty criteria at once") # => co-21
# => co-21: the fix for the multiple-comparisons trap is not 'test fewer criteria' -- it is correcting the significance threshold for how many tests were actually run, using a named, citable methodRun: python3 correcting_for_multiple_comparisons.py
Output:
Uncorrected significant criteria: [14]
Bonferroni-corrected significant criteria: []
Benjamini-Hochberg-corrected significant criteria: []
MATCH: both Bonferroni and Benjamini-Hochberg correction eliminate the criterion that looked significant BEFORE correcting for testing twenty criteria at onceVerify: multipletests() returns zero surviving criteria under both bonferroni and fdr_bh
correction, versus one uncorrected criterion, satisfying co-21's rule that a named correction
method eliminates the phantom win multiple testing manufactures.
Key takeaway: the fix for the multiple-comparisons trap is a correction applied to the threshold, not fewer tests -- Bonferroni and Benjamini-Hochberg both name a specific, citable method for it.
Why It Matters: any eval report that tests more than one criterion and quotes uncorrected p-values is systematically overclaiming wins -- applying a named correction before reporting "significant" is the discipline that closes this gap.
Worked Example 45: Noise Floor Decomposition
ex-45 · exercises co-22, co-23
Context: co-22 -- noise floor of a suite and co-23 -- variance from stochastic generation together show that re-running an unchanged system produces a spread with (at least) two distinct sources: which cases got sampled, and how each case's own stochastic generation turned out. This example measures both separately and confirms they sum back to the observed total.
# learning/code/ex-45-noise-floor-decomposition/noise_floor_decomposition.py
"""Worked Example 45: Noise Floor Decomposition.""" # => co-22: this file's own restated purpose, doubling as its module __doc__
from __future__ import annotations # => hygiene: postpones annotation evaluation, interpreter-version-agnostic
import random # => co-22: builds a population where each case has its own true pass PROBABILITY, not a fixed pass/fail
import statistics # => co-22: pvariance -- population variance, computed over each simulated distribution of pass rates
POPULATION_SIZE = 300 # => co-22: a realistic-sized pool of real cases
N = 30 # => co-22: a typical eval-set size, drawn from that pool
K_REGENERATIONS = 1000 # => co-23: how many times the SAME fixed cases are re-generated, to measure generation noise alone
M_RESAMPLES = 1000 # => co-22: how many DIFFERENT case samples are drawn, to measure case-sampling variance and total variance
if __name__ == "__main__": # => co-22: entry point -- runs only when this file executes directly, not on import
population_rng = random.Random(7) # => co-22: builds the fixed population every sample below draws from
population_p = [population_rng.betavariate(6, 2) for _ in range(POPULATION_SIZE)] # => co-22: each case's own TRUE pass PROBABILITY -- some cases are inherently easier or harder than others
print(f"Population: {POPULATION_SIZE} cases, mean true pass probability {statistics.mean(population_p):.4f}") # => co-22: the population's own per-case difficulty spread, not a single pass rate
fixed_sample_rng = random.Random(42) # => co-23: fixes ONE specific eval set -- the same 30 cases used throughout the generation-noise measurement below
fixed_indices = fixed_sample_rng.sample(range(POPULATION_SIZE), N) # => co-23: the exact 30 cases this "eval set" contains, held constant below
fixed_p = [population_p[i] for i in fixed_indices] # => co-23: these 30 cases' own true pass probabilities, held fixed
generation_rates: list[float] = [] # => co-23: one pass rate per re-run of the SAME 30 cases
for k in range(K_REGENERATIONS): # => co-23: re-generates the SAME fixed cases, over and over, as an unchanged stochastic system would on repeat runs
gen_rng = random.Random(1000 + k) # => co-23: a fresh generation draw each time -- cases do NOT change, only the stochastic outcome does
outcomes = [gen_rng.random() < p for p in fixed_p] # => co-23: each case's outcome THIS regeneration -- a fresh Bernoulli draw from its own true probability
generation_rates.append(sum(outcomes) / N) # => co-23: this regeneration's own pass rate
generation_variance_empirical = statistics.pvariance(generation_rates) # => co-23: the SPREAD of pass rates across regenerations, cases held fixed -- pure generation noise
generation_variance_closed_form = sum(p * (1 - p) for p in fixed_p) / (N**2) # => co-23: the EXACT closed form -- mean per-case Bernoulli variance, divided by n
print(
f"Generation variance (fixed cases, regenerated {K_REGENERATIONS}x): empirical={generation_variance_empirical:.6f} closed-form={generation_variance_closed_form:.6f}"
) # => co-23: computed twice, same as every other named quantity in this course
case_sampling_means: list[float] = [] # => co-22: one mean TRUE probability per DIFFERENT case sample, with NO generation noise mixed in
for m in range(M_RESAMPLES): # => co-22: draws a DIFFERENT set of 30 cases each time, from the SAME population
sample_rng = random.Random(2000 + m) # => co-22: this resample's own case draw
sample_idx = sample_rng.sample(range(POPULATION_SIZE), N) # => co-22: which 30 cases this resample happens to contain
sample_p = [population_p[i] for i in sample_idx] # => co-22: those 30 cases' own TRUE probabilities -- using the true value, not a noisy generation, isolates case-sampling variance alone
case_sampling_means.append(statistics.mean(sample_p)) # => co-22: this resample's own mean true probability
case_sampling_variance_empirical = statistics.pvariance(case_sampling_means) # => co-22: the SPREAD across different case samples, with generation noise removed entirely
total_rates: list[float] = [] # => co-22,co-23: one pass rate per DIFFERENT case sample, EACH ALSO freshly generated -- both noise sources combined
for m in range(M_RESAMPLES): # => co-22: a DIFFERENT case sample every time, exactly like real repeated eval runs
total_sample_rng = random.Random(3000 + m) # => co-22: this run's own case draw
total_idx = total_sample_rng.sample(range(POPULATION_SIZE), N) # => co-22: which 30 cases this run happens to contain
total_p = [population_p[i] for i in total_idx] # => co-22: those 30 cases' own true probabilities
total_gen_rng = random.Random(4000 + m) # => co-23: this run's own fresh generation
total_outcomes = [total_gen_rng.random() < p for p in total_p] # => co-23: each case's actual outcome THIS run -- both which cases AND their generation vary
total_rates.append(sum(total_outcomes) / N) # => co-22,co-23: this run's own observed pass rate, exactly like a real repeated eval run
total_variance_empirical = statistics.pvariance(total_rates) # => co-22,co-23: the SPREAD a team would actually observe re-running the whole eval, unchanged system
sum_of_components = generation_variance_empirical + case_sampling_variance_empirical # => co-22,co-23: the decomposition's own prediction -- total should equal the sum of its two independent sources
print(f"Case-sampling variance (different cases, true probabilities): {case_sampling_variance_empirical:.6f}") # => co-22
print(f"Total variance (different cases AND fresh generation): {total_variance_empirical:.6f}") # => co-22,co-23
print(f"Sum of components (generation + case-sampling): {sum_of_components:.6f}") # => co-22,co-23: the internal-consistency check this decomposition must pass
assert abs(generation_variance_empirical - generation_variance_closed_form) / generation_variance_closed_form < 0.10 # => co-23: the empirical and closed-form generation variance must agree closely
assert abs(total_variance_empirical - sum_of_components) / sum_of_components < 0.20 # => co-22,co-23: total variance must be close to the sum of its two independent components
print("MATCH: total run-to-run variance decomposes into generation noise (same cases, re-generated) plus case-sampling variance (different cases drawn) -- and the two sum back to the observed total") # => co-22,co-23
# => co-22,co-23: the noise floor is not one number -- it is (at least) two distinct sources, and neither one alone is 'the' variance of an unchanged, re-run suiteRun: python3 noise_floor_decomposition.py
Output:
Population: 300 cases, mean true pass probability 0.7407
Generation variance (fixed cases, regenerated 1000x): empirical=0.005279 closed-form=0.005219
Case-sampling variance (different cases, true probabilities): 0.000710
Total variance (different cases AND fresh generation): 0.006178
Sum of components (generation + case-sampling): 0.005989
MATCH: total run-to-run variance decomposes into generation noise (same cases, re-generated) plus case-sampling variance (different cases drawn) -- and the two sum back to the observed totalVerify: the empirical generation variance (0.005279) matches its closed form (0.005219)
within 10%, and the total empirical variance (0.006178) matches the sum of generation plus
case-sampling variance (0.005989) within 20%, satisfying co-22's and co-23's rule that an
unchanged suite's own run-to-run spread decomposes into two distinct, separately measurable
sources.
Key takeaway: an unchanged system's own repeat-run variance is not one mysterious number -- it splits into "which cases got sampled" and "how each case's own generation turned out," and both are separately measurable.
Why It Matters: this decomposition IS the noise floor ex-46's ship decision checks a gap against -- a regression bar set without measuring both sources risks flagging ordinary generation noise as a real regression, or missing a real regression buried inside sampling noise.
Worked Example 46: A Defensible Ship Decision
ex-46 · exercises co-24, co-01
Context: this theme, and this topic's whole worked-example arc, closes on co-24 -- reporting
honestly: one typed ShipDecision that gathers the estimate and its interval, the paired
comparison and its significance, the noise floor, and the materiality threshold into a single
verdict -- reprising co-01's opening claim that a number without its context invites a wrong
decision.
# learning/code/ex-46-a-defensible-ship-decision/defensible_ship_decision.py
"""Worked Example 46: A Defensible Ship Decision.""" # => co-24: this file's own restated purpose, doubling as its module __doc__
from __future__ import annotations # => hygiene: postpones annotation evaluation, interpreter-version-agnostic
import random # => co-24: builds one paired baseline/candidate comparison, tying this theme's techniques together
from dataclasses import dataclass # => co-24: forces the final ship/hold call through one typed, checkable record -- no bare verdict allowed
from statsmodels.stats.contingency_tables import mcnemar # => co-18: the paired significance test, per ex-37/ex-38
from statsmodels.stats.proportion import proportion_confint # => co-04,co-05: the small-n-appropriate interval, per Theme A
MEASURED_NOISE_FLOOR = 0.03 # => co-22: this suite's own measured generation-noise floor -- from a decomposition like ex-45's, run previously on this exact eval set
MATERIALITY_THRESHOLD = 0.05 # => co-19: this team's own stated bar for "worth acting on" -- a 5-point gap or larger
def build_paired_dataset(n: int, *, seed: int, baseline_rate: float, candidate_rate: float, correlation: float) -> tuple[list[bool], list[bool]]: # => co-18: the SAME paired-fixture shape as ex-37/ex-38
"""Build paired baseline/candidate outcomes over n SHARED items, correlated by per-item difficulty.""" # => co-18: documents build_paired_dataset's contract -- no runtime output, just sets its __doc__
rng = random.Random(seed) # => co-18: drives each item's shared difficulty draw
baseline: list[bool] = [] # => co-18: baseline's verdict, one per item
candidate: list[bool] = [] # => co-18: candidate's verdict on the SAME item, one per item
for _ in range(n): # => co-18: one shared item at a time
difficulty_draw = rng.random() # => co-18: this item's own shared difficulty draw -- read by BOTH systems below
baseline_pass = difficulty_draw < baseline_rate # => co-18: baseline's verdict on this exact item
if rng.random() < correlation: # => co-18: most of the time, candidate's verdict is driven by the SAME difficulty draw
candidate_pass = difficulty_draw < candidate_rate # => co-18: correlated verdict
else: # => co-18: occasionally, candidate's verdict is an independent draw instead
candidate_pass = rng.random() < candidate_rate # => co-18: an independent verdict
baseline.append(baseline_pass) # => co-18: records this item's baseline verdict
candidate.append(candidate_pass) # => co-18: records this item's candidate verdict, SAME item, SAME index
return baseline, candidate # => co-18: two same-length, index-aligned lists
@dataclass(frozen=True) # => co-24: immutable -- a ship decision cannot be silently edited after being made
class ShipDecision: # => co-24: EVERY field this theme built, gathered into the ONE record a real ship/hold call rests on
n: int # => co-06: how many paired items this decision rests on
candidate_rate: float # => co-02: candidate's own point estimate
ci_low: float # => co-04,co-05: candidate's own interval -- small-n-appropriate, per Theme A
ci_high: float # => co-04,co-05: candidate's own interval upper bound
gap: float # => co-17: the observed gap over baseline
mcnemar_p: float # => co-18: the paired significance test's own p-value
noise_floor: float # => co-22: this suite's own measured generation-noise floor
materiality_threshold: float # => co-19: this team's own stated bar for "worth acting on"
verdict: str # => co-24: SHIP or HOLD -- derived from every field above, never chosen by feel
def decide(baseline: list[bool], candidate: list[bool]) -> ShipDecision: # => co-24: the ONE function that turns raw paired data into a defensible decision
"""Compute every field a ship/hold decision needs, and the resulting verdict.""" # => co-24: documents decide's contract -- no runtime output, just sets its __doc__
n = len(baseline) # => co-06: item count
candidate_rate = sum(candidate) / n # => co-02: candidate's own point estimate
ci_low, ci_high = proportion_confint(sum(candidate), n, alpha=0.05, method="wilson") # => co-04,co-05: candidate's own Wilson interval, per Theme A
gap = candidate_rate - (sum(baseline) / n) # => co-17: the observed gap over baseline
both_pass = sum(1 for b, c in zip(baseline, candidate) if b and c) # => co-18: concordant pairs -- uninformative for the test
both_fail = sum(1 for b, c in zip(baseline, candidate) if not b and not c) # => co-18: concordant pairs -- uninformative for the test
baseline_only = sum(1 for b, c in zip(baseline, candidate) if b and not c) # => co-18: discordant -- candidate regressed
candidate_only = sum(1 for b, c in zip(baseline, candidate) if not b and c) # => co-18: discordant -- candidate improved
table = [[both_pass, baseline_only], [candidate_only, both_fail]] # => co-18: the 2x2 table McNemar's test uses
mcnemar_p = mcnemar(table, exact=False, correction=True).pvalue # => co-18: the paired significance test's own p-value
is_significant = mcnemar_p < 0.05 # => co-18: passes the formal test, per ex-37/ex-38
beats_noise_floor = gap > MEASURED_NOISE_FLOOR # => co-22: the gap must exceed what an UNCHANGED system's own re-runs would show, per ex-45
beats_materiality = gap > MATERIALITY_THRESHOLD # => co-19: the gap must clear this team's own stated bar for action, per ex-39/ex-40
verdict = "SHIP" if (is_significant and beats_noise_floor and beats_materiality) else "HOLD" # => co-24: ALL THREE conditions, never any one number alone
return ShipDecision( # => co-24: every field populated -- no bare verdict leaves this function
n=n, # => co-06: how many paired items this decision rests on
candidate_rate=candidate_rate, # => co-02: candidate's own point estimate
ci_low=ci_low, # => co-04,co-05: the interval's lower bound
ci_high=ci_high, # => co-04,co-05: the interval's upper bound
gap=gap, # => co-17: the observed gap over baseline
mcnemar_p=mcnemar_p, # => co-18: the paired test's own p-value
noise_floor=MEASURED_NOISE_FLOOR, # => co-22: this suite's own measured noise floor
materiality_threshold=MATERIALITY_THRESHOLD, # => co-19: this team's own stated bar for action
verdict=verdict, # => co-24: SHIP or HOLD, derived above from all three conditions together
) # => co-24: closes the ShipDecision constructor -- all nine fields supplied, none deferred
if __name__ == "__main__": # => co-24: entry point -- runs only when this file executes directly, not on import
baseline, candidate = build_paired_dataset(60, seed=15, baseline_rate=0.72, candidate_rate=0.88, correlation=0.85) # => co-18: one paired eval run over 60 shared items
decision = decide(baseline, candidate) # => co-24: the full, typed decision
print(f"n={decision.n} candidate_rate={decision.candidate_rate:.4f} 95% CI=[{decision.ci_low:.4f}, {decision.ci_high:.4f}]") # => co-02,co-04,co-05: the estimate and its interval, never the estimate alone
print(f"gap={decision.gap:.4f} mcnemar_p={decision.mcnemar_p:.4f}") # => co-17,co-18: the comparison and its actual test
print(f"noise_floor={decision.noise_floor:.4f} materiality_threshold={decision.materiality_threshold:.4f}") # => co-19,co-22: the two bars the gap must clear, beyond bare significance
print(f"VERDICT: {decision.verdict}") # => co-24: the final, defensible call
assert decision.verdict == "SHIP", "with a significant, above-noise-floor, above-materiality gap, this decision must resolve to SHIP" # => co-24: the claim this example demonstrates
assert decision.ci_low > 0.0 and decision.ci_high < 1.0, "the reported interval must be a genuine range, not a degenerate point" # => co-04,co-05
print("MATCH: every field this theme built -- estimate, interval, gap, paired test, noise floor, materiality -- points the SAME direction, which is what makes this verdict defensible") # => co-24
# => co-24: a ship decision built on any ONE of these numbers alone -- the bare rate, the bare p-value, the bare gap -- is not yet defensible; this course's own discipline is reporting every field together, or not shipping the claimRun: python3 defensible_ship_decision.py
Output:
n=60 candidate_rate=0.8000 95% CI=[0.6822, 0.8817]
gap=0.1500 mcnemar_p=0.0159
noise_floor=0.0300 materiality_threshold=0.0500
VERDICT: SHIP
MATCH: every field this theme built -- estimate, interval, gap, paired test, noise floor, materiality -- points the SAME direction, which is what makes this verdict defensibleVerify: decide() returns a ShipDecision with candidate_rate=0.8000, a genuine interval
[0.6822, 0.8817], mcnemar_p=0.0159 (significant), a 0.1500 gap clearing both the 0.0300
noise floor and the 0.0500 materiality threshold, and verdict="SHIP", satisfying co-24's rule
that a defensible decision is the full record, not any single field read alone.
Key takeaway: a ship decision built on the bare pass rate, the bare p-value, or the bare gap
alone is not yet defensible -- the full ShipDecision, every field checked together, is what this
whole topic has been building toward.
Why It Matters: this is the exact shape the capstone scales up to a full, real evaluation report -- a justified sample size, a small-n-appropriate interval, chance-corrected judge concordance against a measured human ceiling, a paired comparison corrected for multiple comparisons, and a regression bar derived from a measured noise floor, all reported together.
← Previous: Theme C: Agreement and Judge Concordance · Next: Capstone →
Last updated July 25, 2026