
Statistical Testing Guide
- 40 installs
- 1 repo stars
- Updated July 31, 2026
- jurgendn/agent-skills
Helps with testing & qa tasks.
About
statistical-testing-guide is a Claude Code skill for testing & qa. It helps solo builders move faster with AI-assisted development.
- statistical-testing-guide
- Testing & QA
- AI-coding skill
Statistical Testing Guide by the numbers
- 40 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,285 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/jurgendn/agent-skills --skill statistical-testing-guideAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 40 |
|---|---|
| repo stars | ★ 1 |
| Last updated | July 31, 2026 |
| Repository | jurgendn/agent-skills ↗ |
What it does
Helps with testing & qa tasks.
Files
Statistical Testing Guide
Statistical validity is not bureaucracy. It is the mechanism by which the reader can trust that a reported improvement is real rather than a lucky seed.
The most common mistake in ML papers is not misapplying a test — it is skipping the test entirely and reporting a mean over 3 seeds as if it were a fact.
---
Compute it with the bundled script
Once you know the comparison structure (next section), don't hand-derive the scipy calls — run scripts/stats.py, which encodes the test-selection logic, effect sizes, and reporting template below so the numbers are correct and reproducible.
# Compare two methods across seeds (auto-selects t-test / Wilcoxon / Mann-Whitney),
# with effect size, bootstrap CI, permutation cross-check, and a copy-paste sentence:
python scripts/stats.py compare --a 0.843 0.851 0.838 0.847 0.840 \
--b 0.812 0.805 0.820 0.808 0.815 --paired --metric F1
python scripts/stats.py correct --pvalues 0.01 0.04 0.03 0.20 --method holm # multiple comparisons
python scripts/stats.py power --d 0.5 --n 5 # seeds / detectable effect
python scripts/stats.py selftest # verify the installUse the workflow below to decide what to compute and how to read it; use the script to actually compute it. The script's report mirrors the "Output format" template at the end of this skill.
---
Workflow
1. Decide what you are comparing
Before choosing a test, be explicit about the comparison structure:
- Two methods, multiple seeds: e.g., method A vs. method B, each run with seeds {42, 123, 456}
- One method, multiple datasets: e.g., does the improvement hold across 5 benchmarks?
- One method, multiple hyperparameter settings: tuning curve, not a significance test
- Two methods, multiple tasks: a multi-task comparison
The unit of observation matters. If you run 5 seeds × 3 datasets, you have 15 observations — but they are not independent, and pooling them inflates your effective sample size.
---
2. Choose the right test
For comparing two methods with multiple seeds (most common ML case)
| Situation | Recommended test |
|---|---|
| ≥10 seeds, metric approximately normal | Paired t-test |
| <10 seeds, or non-normal distribution | Wilcoxon signed-rank test |
| Want to avoid distributional assumptions | Bootstrap permutation test |
| Comparing across multiple datasets | Average-and-test, or mixed-effects model |
Paired means each seed is shared between both methods (both run on seed 42, both on seed 123, etc.). Paired tests are more powerful and more appropriate when both methods see the same data/initialization.
When not to use a t-test: fewer than 5 seeds, or the metric is bounded (e.g., accuracy near 0 or 1), or the metric is a rank (e.g., position on a leaderboard).
For comparing more than two methods
Run pairwise tests, then apply multiple comparison correction (see Step 4). Do not run a single omnibus ANOVA and claim all methods differ — test the specific pairs you care about.
For comparing on multiple benchmarks
Do not pool results across benchmarks. Instead:
- Report results per benchmark.
- Note the number of benchmarks where the difference is significant.
- Compute win rate (fraction of benchmarks where method A beats B) as a summary statistic.
Non-parametric tests to know
- Wilcoxon signed-rank: paired, non-parametric. Use with few seeds or ordinal metrics.
- Mann-Whitney U: unpaired, non-parametric. Use when seeds cannot be matched.
- Bootstrap: resample the observed runs to estimate the distribution of the difference. Extremely flexible; works well with any metric.
- Permutation test: randomly permute labels between methods to build the null distribution. Gold standard for small-n comparisons.
---
3. Compute and report effect sizes
Statistical significance answers "is the difference probably not zero?" Effect size answers "is the difference worth caring about?" Both are required.
| Effect size measure | When to use | Interpretation |
|---|---|---|
| Cohen's d | Continuous metric, approximately normal | Small: 0.2, Medium: 0.5, Large: 0.8 |
| Cliff's delta | Ordinal or non-normal | -1 to 1; ~0 means no difference |
| Mean difference ± CI | Always report alongside the above | Most interpretable for readers |
| Win rate | Multiple datasets | Fraction of tasks where A > B |
Always report the actual numbers, not just p < 0.05. A difference of 0.1% F1 that is statistically significant (large n) is not practically significant.
---
4. Handle multiple comparisons
If you run k tests at significance level α, you expect k × α false positives by chance. With 10 tests at α = 0.05, you expect 0.5 false discoveries — significant even if everything is null.
When to correct:
- You are testing the same hypothesis across multiple metrics.
- You are comparing many method pairs in an ablation table.
- You ran many hyperparameter configs and selected the best.
How to correct:
- Bonferroni: multiply each p-value by k (or set threshold to α/k). Conservative but simple.
- Holm-Bonferroni: step-down version of Bonferroni. Less conservative.
- Benjamini-Hochberg (FDR): controls false discovery rate rather than family-wise error. Preferred for exploratory analysis with many tests.
When not to correct:
- Pre-specified primary comparison (one method, one metric, one dataset). Correction is unnecessary if the hypothesis was stated before seeing data.
- Exploratory analysis that will feed a follow-up confirmatory study.
Report correction choices explicitly: "We apply Bonferroni correction across 8 ablation variants (α = 0.05/8 = 0.006)."
---
5. Plan the number of seeds before running
Power analysis determines how many seeds you need to detect a given effect size with a given probability.
Practical guidance for ML experiments:
| Expected effect size | Minimum seeds for 80% power |
|---|---|
| Large (d > 0.8, e.g., 5+ point improvement) | 3–5 seeds |
| Medium (d ≈ 0.5, e.g., 2–3 point improvement) | 8–12 seeds |
| Small (d < 0.2, e.g., <1 point improvement) | 30+ seeds (often infeasible) |
If a claimed improvement is <1 point and the paper runs only 3 seeds, the result is not interpretable. Say so — and either run more seeds or bound the detectable effect size.
Rule of thumb for most ML papers: 5 seeds is the practical minimum; 10 seeds gives reasonable power for medium effects; report variance always.
---
6. Report correctly
Minimum reporting requirements for any comparison in a paper:
- Mean (or median for non-normal distributions).
- Standard deviation or standard error (be explicit which).
- Number of seeds / trials.
- Whether seeds were fixed before or after seeing results.
- Test used, p-value, and effect size (when claiming significance).
Standard deviation vs. standard error:
- Standard deviation: spread of individual runs. Use for communicating variability.
- Standard error (SD/√n): precision of the mean estimate. Use for confidence intervals on the mean.
- Never report only SE without saying it's SE — it looks like SD and makes results seem more stable than they are.
Confidence interval format: "84.3 ± 1.2" should state explicitly "mean ± SD over 5 seeds" or "mean ± 95% CI".
---
7. Common mistakes in ML papers
Reporting only the best seed: If you ran 10 seeds and reported the maximum, your result is biased. Report mean ± std; mention if you also report max for reference.
Choosing seeds post-hoc: Picking seeds after seeing which ones favor your method inflates results. Pre-register seeds (or use all seeds from a fixed range).
Significance without effect size: p < 0.05 with d = 0.05 is a meaningless result. Always pair with effect size.
Comparing means without variance: Saying "84.3 vs. 83.1" without variance is not evidence. If the SD is 2.0, the difference is invisible.
Conflating statistical and practical significance: A 0.1% improvement that is statistically significant at large n is not a contribution if the downstream impact is negligible.
Testing after adding data: If you added more seeds after seeing non-significant results, you need to correct for sequential testing (or acknowledge the limitation).
---
Reporting template for a paper comparison
Method A achieved [mean] ± [SD] on [metric] over [n] seeds,
compared to Method B's [mean] ± [SD] ([n] seeds).
The difference ([A−B]) is statistically significant
(Wilcoxon signed-rank, p = [value]; Cohen's d = [value]).If not significant:
The observed difference of [A−B] was not statistically significant
(p = [value]; 95% CI [low, high]), with insufficient power to detect
effects smaller than [minimum detectable effect] given [n] seeds.---
Output format
# Statistical Analysis Plan / Report
## Comparison structure
- Unit of observation:
- Number of conditions:
- Pairing:
## Recommended test
- Test: [name and justification]
- Correction for multiple comparisons: [yes/no; method]
## Effect size
- Measure: [Cohen's d / Cliff's delta / mean difference + CI]
- Observed / estimated value:
## Power / seeds
- Effect size target: [what you're trying to detect]
- Seeds required for 80% power: [estimate]
- Seeds you have: [n]
- Verdict: [adequately powered / underpowered / unknown]
## Results
[Formatted comparison with mean ± SD, test statistic, p-value, effect size]
## Reporting text (copy-paste ready)
[Draft sentence for the paper]
## Caveats
[Known limitations of the analysis]#!/usr/bin/env python3
"""Statistical comparison helper for ML experiments.
This is the deterministic companion to the statistical-testing-guide skill. The
skill explains *which* test to use and *why*; this script computes it correctly
so the numbers that go into a paper or rebuttal are not re-derived (and possibly
mis-derived) by hand each time.
It covers the common ML case — comparing two methods across seeds — and the two
follow-ups that always come up: effect size and "did I run enough seeds?".
Dependencies: numpy, scipy. Both are already present in any ML environment.
Usage
-----
Compare two methods (each a list of per-seed scores), auto-selecting the test:
python stats.py compare --a 0.843 0.851 0.838 0.847 0.840 \\
--b 0.812 0.805 0.820 0.808 0.815 \\
--paired --metric F1
Read the same from JSON ({"A": [...], "B": [...], "paired": true}):
python stats.py compare --json comparison.json
Correct a family of p-values for multiple comparisons:
python stats.py correct --pvalues 0.01 0.04 0.03 0.20 --method holm
Plan seeds (how many for 80% power at a target effect size) or, given the seeds
you have, the smallest effect you can detect:
python stats.py power --d 0.5 # -> required seeds
python stats.py power --d 0.5 --n 5 # -> required seeds + MDE at n=5
Sanity-check the implementation:
python stats.py selftest
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass, field
import numpy as np
from scipy import stats
# --------------------------------------------------------------------------- #
# Effect sizes
# --------------------------------------------------------------------------- #
def cohens_d(a: np.ndarray, b: np.ndarray, paired: bool) -> float:
"""Standardised mean difference.
Paired: mean of differences / SD of differences (the quantity that actually
drives a paired test's power). Unpaired: difference of means / pooled SD.
"""
a, b = np.asarray(a, float), np.asarray(b, float)
if paired:
diff = a - b
sd = diff.std(ddof=1)
return float(diff.mean() / sd) if sd > 0 else 0.0
na, nb = len(a), len(b)
pooled = np.sqrt(((na - 1) * a.var(ddof=1) + (nb - 1) * b.var(ddof=1)) / (na + nb - 2))
return float((a.mean() - b.mean()) / pooled) if pooled > 0 else 0.0
def cliffs_delta(a: np.ndarray, b: np.ndarray) -> float:
"""Non-parametric effect size in [-1, 1]; ~0 means stochastically equal.
delta = P(a > b) - P(a < b). Robust to non-normal / ordinal metrics.
"""
a, b = np.asarray(a, float), np.asarray(b, float)
gt = sum((x > y) for x in a for y in b)
lt = sum((x < y) for x in a for y in b)
return float((gt - lt) / (len(a) * len(b)))
def interpret_d(d: float) -> str:
ad = abs(d)
if ad < 0.2:
return "negligible"
if ad < 0.5:
return "small"
if ad < 0.8:
return "medium"
return "large"
# --------------------------------------------------------------------------- #
# Confidence interval on the difference (bootstrap — distribution-free)
# --------------------------------------------------------------------------- #
def bootstrap_diff_ci(
a: np.ndarray, b: np.ndarray, paired: bool, n_boot: int = 10000,
alpha: float = 0.05, seed: int = 0,
) -> tuple[float, float, float]:
"""Percentile bootstrap CI for (mean A - mean B). Returns (mean_diff, lo, hi)."""
rng = np.random.default_rng(seed)
a, b = np.asarray(a, float), np.asarray(b, float)
boot = np.empty(n_boot)
if paired:
diff = a - b
n = len(diff)
for i in range(n_boot):
boot[i] = diff[rng.integers(0, n, n)].mean()
point = float(diff.mean())
else:
na, nb = len(a), len(b)
for i in range(n_boot):
boot[i] = a[rng.integers(0, na, na)].mean() - b[rng.integers(0, nb, nb)].mean()
point = float(a.mean() - b.mean())
lo, hi = np.percentile(boot, [100 * alpha / 2, 100 * (1 - alpha / 2)])
return point, float(lo), float(hi)
def permutation_pvalue(
a: np.ndarray, b: np.ndarray, paired: bool, n_perm: int = 10000, seed: int = 0,
) -> float:
"""Two-sided permutation p-value for the difference in means.
Gold standard for small n: makes no distributional assumption. Paired version
flips the sign of each difference; unpaired version shuffles group labels.
"""
rng = np.random.default_rng(seed)
a, b = np.asarray(a, float), np.asarray(b, float)
if paired:
diff = a - b
obs = abs(diff.mean())
n = len(diff)
count = 0
for _ in range(n_perm):
signs = rng.choice([-1.0, 1.0], n)
if abs((diff * signs).mean()) >= obs - 1e-12:
count += 1
return (count + 1) / (n_perm + 1)
obs = abs(a.mean() - b.mean())
pool = np.concatenate([a, b])
na = len(a)
count = 0
for _ in range(n_perm):
rng.shuffle(pool)
if abs(pool[:na].mean() - pool[na:].mean()) >= obs - 1e-12:
count += 1
return (count + 1) / (n_perm + 1)
# --------------------------------------------------------------------------- #
# Test selection + comparison
# --------------------------------------------------------------------------- #
@dataclass
class ComparisonResult:
metric: str
paired: bool
n_a: int
n_b: int
mean_a: float
mean_b: float
sd_a: float
sd_b: float
test_name: str
test_reason: str
p_value: float
perm_p: float
effect_name: str
effect_value: float
effect_interp: str
mean_diff: float
ci_lo: float
ci_hi: float
win_rate: float | None
alpha: float
notes: list[str] = field(default_factory=list)
def _is_normal(x: np.ndarray) -> bool:
"""Shapiro-Wilk normality screen. Needs n >= 3; conservative default True at tiny n."""
x = np.asarray(x, float)
if len(x) < 3:
return True
try:
return stats.shapiro(x).pvalue > 0.05
except Exception:
return True
def compare(
a, b, paired: bool, metric: str = "metric", alpha: float = 0.05,
) -> ComparisonResult:
a = np.asarray(a, float)
b = np.asarray(b, float)
notes: list[str] = []
if paired and len(a) != len(b):
raise ValueError("paired comparison requires equal-length, aligned samples")
if min(len(a), len(b)) < 2:
raise ValueError("need at least 2 observations per method")
small = min(len(a), len(b)) < 10
if paired:
normal = _is_normal(a - b)
if not small and normal:
test_name = "Paired t-test"
reason = "paired, >=10 seeds, differences approximately normal"
p = float(stats.ttest_rel(a, b).pvalue)
else:
test_name = "Wilcoxon signed-rank"
reason = ("paired; " + ("few seeds (<10)" if small else "non-normal differences")
+ " -> non-parametric")
try:
p = float(stats.wilcoxon(a, b).pvalue)
except ValueError:
p = float("nan")
notes.append("Wilcoxon undefined (all differences zero); see permutation p.")
effect_name, effect_value = "Cohen's d (paired)", cohens_d(a, b, True)
else:
normal = _is_normal(a) and _is_normal(b)
if not small and normal:
test_name = "Welch's t-test (unpaired)"
reason = ">=10 per group, approximately normal, unequal variance not assumed"
p = float(stats.ttest_ind(a, b, equal_var=False).pvalue)
effect_name, effect_value = "Cohen's d", cohens_d(a, b, False)
else:
test_name = "Mann-Whitney U"
reason = ("unpaired; " + ("small n (<10)" if small else "non-normal") + " -> non-parametric")
p = float(stats.mannwhitneyu(a, b, alternative="two-sided").pvalue)
effect_name, effect_value = "Cliff's delta", cliffs_delta(a, b)
if small:
notes.append("Small sample: the permutation p-value is the more trustworthy figure.")
if test_name.startswith(("Welch", "Paired")) and effect_name.startswith("Cohen") and abs(effect_value) < 0.2:
notes.append("Effect size is negligible — statistical significance here would not be practically meaningful.")
perm_p = permutation_pvalue(a, b, paired)
mean_diff, lo, hi = bootstrap_diff_ci(a, b, paired, alpha=alpha)
win_rate = float(np.mean(a > b)) if paired else None
return ComparisonResult(
metric=metric, paired=paired, n_a=len(a), n_b=len(b),
mean_a=float(a.mean()), mean_b=float(b.mean()),
sd_a=float(a.std(ddof=1)), sd_b=float(b.std(ddof=1)),
test_name=test_name, test_reason=reason, p_value=p, perm_p=perm_p,
effect_name=effect_name, effect_value=effect_value,
effect_interp=interpret_d(effect_value) if effect_name.startswith("Cohen") else f"{effect_value:+.2f}",
mean_diff=mean_diff, ci_lo=lo, ci_hi=hi, win_rate=win_rate,
alpha=alpha, notes=notes,
)
# --------------------------------------------------------------------------- #
# Multiple-comparison correction
# --------------------------------------------------------------------------- #
def correct_pvalues(pvalues, method: str = "holm"):
"""Return corrected p-values. method in {bonferroni, holm, bh}."""
p = np.asarray(pvalues, float)
k = len(p)
method = method.lower()
if method == "bonferroni":
return np.minimum(p * k, 1.0)
order = np.argsort(p)
out = np.empty(k)
if method == "holm":
running = 0.0
for rank, idx in enumerate(order):
val = (k - rank) * p[idx]
running = max(running, val)
out[idx] = min(running, 1.0)
return out
if method in ("bh", "fdr", "benjamini-hochberg"):
running = 1.0
for rank in range(k - 1, -1, -1):
idx = order[rank]
val = p[idx] * k / (rank + 1)
running = min(running, val)
out[idx] = min(running, 1.0)
return out
raise ValueError(f"unknown correction method: {method}")
# --------------------------------------------------------------------------- #
# Power / seed planning (paired or one-sample on the differences)
# --------------------------------------------------------------------------- #
def required_seeds(d: float, power: float = 0.8, alpha: float = 0.05) -> int:
"""Approximate seeds needed to detect standardized effect d at given power.
Uses the normal approximation n = ((z_{1-a/2} + z_{1-power}) / d)^2, then a
small-sample bump. Matches the skill's rule of thumb (large d ~ 3-5 seeds,
medium ~ 8-12, small ~ 30+).
"""
if d <= 0:
return 10**9
z_a = stats.norm.ppf(1 - alpha / 2)
z_b = stats.norm.ppf(power)
n = ((z_a + z_b) / d) ** 2
return max(2, int(np.ceil(n)) + 1)
def detectable_effect(n: int, power: float = 0.8, alpha: float = 0.05) -> float:
"""Smallest standardized effect detectable with n seeds at given power (MDE)."""
z_a = stats.norm.ppf(1 - alpha / 2)
z_b = stats.norm.ppf(power)
return float((z_a + z_b) / np.sqrt(n))
# --------------------------------------------------------------------------- #
# Reporting (matches the skill's "Output format" template)
# --------------------------------------------------------------------------- #
def format_report(r: ComparisonResult) -> str:
sig = (r.p_value < r.alpha) if r.p_value == r.p_value else (r.perm_p < r.alpha)
verdict = "statistically significant" if sig else "not statistically significant"
n_for_power = required_seeds(abs(r.effect_value)) if r.effect_name.startswith("Cohen") else None
powered = (n_for_power is not None and min(r.n_a, r.n_b) >= n_for_power)
lines = [
"# Statistical Analysis Report",
"",
"## Comparison structure",
f"- Metric: {r.metric}",
f"- Pairing: {'paired (same seeds)' if r.paired else 'unpaired'}",
f"- Method A: mean {r.mean_a:.4f} ± {r.sd_a:.4f} SD over {r.n_a} runs",
f"- Method B: mean {r.mean_b:.4f} ± {r.sd_b:.4f} SD over {r.n_b} runs",
"",
"## Recommended test",
f"- Test: {r.test_name}",
f"- Why: {r.test_reason}",
f"- p-value: {r.p_value:.4g}",
f"- Permutation cross-check p: {r.perm_p:.4g}",
"",
"## Effect size",
f"- {r.effect_name}: {r.effect_value:+.3f} ({r.effect_interp})",
f"- Mean difference (A−B): {r.mean_diff:+.4f} [95% CI {r.ci_lo:+.4f}, {r.ci_hi:+.4f}]",
]
if r.win_rate is not None:
lines.append(f"- Win rate (A>B across paired runs): {r.win_rate:.0%}")
lines += [
"",
"## Power / seeds",
]
if n_for_power is not None:
lines += [
f"- Seeds for 80% power at the observed effect: ~{n_for_power}",
f"- Seeds available: {min(r.n_a, r.n_b)}",
f"- Verdict: {'adequately powered' if powered else 'UNDERPOWERED — treat a null result as inconclusive'}",
]
else:
lines.append("- Non-parametric effect; plan seeds from a Cohen's d target via `power` subcommand.")
lines += [
"",
"## Bottom line",
f"- The difference is **{verdict}** at α={r.alpha} "
f"({r.test_name}, p={r.p_value:.4g}; {r.effect_name}={r.effect_value:+.3f}).",
]
if r.notes:
lines += ["", "## Caveats"] + [f"- {n}" for n in r.notes]
lines += [
"",
"## Copy-paste sentence",
]
if sig:
lines.append(
f"Method A achieved {r.mean_a:.3f} ± {r.sd_a:.3f} on {r.metric} over {r.n_a} seeds, "
f"compared to Method B's {r.mean_b:.3f} ± {r.sd_b:.3f} ({r.n_b} seeds). The difference "
f"({r.mean_diff:+.3f}) is statistically significant ({r.test_name}, p={r.p_value:.3g}; "
f"{r.effect_name}={r.effect_value:+.2f})."
)
else:
lines.append(
f"The observed difference of {r.mean_diff:+.3f} on {r.metric} was not statistically "
f"significant ({r.test_name}, p={r.p_value:.3g}; 95% CI [{r.ci_lo:+.3f}, {r.ci_hi:+.3f}]), "
f"with limited power to detect small effects given {min(r.n_a, r.n_b)} seeds."
)
return "\n".join(lines)
# --------------------------------------------------------------------------- #
# CLI
# --------------------------------------------------------------------------- #
def _cmd_compare(args) -> int:
if args.json:
cfg = json.load(open(args.json))
a, b = cfg["A"], cfg["B"]
paired = cfg.get("paired", args.paired)
metric = cfg.get("metric", args.metric)
else:
if args.a is None or args.b is None:
print("error: provide --a and --b (or --json)", file=sys.stderr)
return 2
a, b, paired, metric = args.a, args.b, args.paired, args.metric
print(format_report(compare(a, b, paired=paired, metric=metric, alpha=args.alpha)))
return 0
def _cmd_correct(args) -> int:
corrected = correct_pvalues(args.pvalues, method=args.method)
print(f"# Multiple-comparison correction ({args.method}, k={len(args.pvalues)})\n")
print(f"{'raw p':>10} {'corrected':>10} significant@" + str(args.alpha))
for raw, cor in zip(args.pvalues, corrected):
print(f"{raw:>10.4g} {cor:>10.4g} {'yes' if cor < args.alpha else 'no'}")
return 0
def _cmd_power(args) -> int:
n = required_seeds(args.d, power=args.power, alpha=args.alpha)
print(f"Target effect (Cohen's d): {args.d}")
print(f"Seeds for {args.power:.0%} power (α={args.alpha}): ~{n}")
if args.n:
mde = detectable_effect(args.n, power=args.power, alpha=args.alpha)
print(f"With n={args.n} seeds, smallest detectable effect (MDE): d≈{mde:.2f} "
f"({interpret_d(mde)})")
return 0
def _cmd_selftest(_args) -> int:
rng = np.random.default_rng(0)
# Clear separation, paired.
a = 0.85 + rng.normal(0, 0.005, 12)
b = 0.81 + rng.normal(0, 0.005, 12)
r = compare(a, b, paired=True, metric="F1")
assert r.p_value < 0.05 and r.effect_value > 0.8, r
# No real difference.
c = rng.normal(0.8, 0.02, 8)
d = rng.normal(0.8, 0.02, 8)
r2 = compare(c, d, paired=False, metric="acc")
assert r2.perm_p > 0.05, r2
# Corrections monotone and >= raw.
raw = [0.01, 0.04, 0.03, 0.20]
for m in ("bonferroni", "holm", "bh"):
cor = correct_pvalues(raw, m)
assert np.all(cor >= np.array(raw) - 1e-9), (m, cor)
# Power monotonicity.
assert required_seeds(0.8) < required_seeds(0.5) < required_seeds(0.2)
print("selftest OK")
return 0
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = p.add_subparsers(dest="cmd", required=True)
c = sub.add_parser("compare", help="compare two methods, auto-selecting the test")
c.add_argument("--a", type=float, nargs="+", help="per-seed scores for method A")
c.add_argument("--b", type=float, nargs="+", help="per-seed scores for method B")
c.add_argument("--json", help='JSON file {"A": [...], "B": [...], "paired": bool}')
c.add_argument("--paired", action="store_true", help="seeds are shared/aligned between A and B")
c.add_argument("--metric", default="metric", help="metric name for the report")
c.add_argument("--alpha", type=float, default=0.05)
c.set_defaults(func=_cmd_compare)
r = sub.add_parser("correct", help="correct a family of p-values")
r.add_argument("--pvalues", type=float, nargs="+", required=True)
r.add_argument("--method", default="holm", choices=["bonferroni", "holm", "bh"])
r.add_argument("--alpha", type=float, default=0.05)
r.set_defaults(func=_cmd_correct)
w = sub.add_parser("power", help="plan seeds from an effect-size target")
w.add_argument("--d", type=float, required=True, help="target Cohen's d")
w.add_argument("--n", type=int, help="seeds you have (reports MDE)")
w.add_argument("--power", type=float, default=0.8)
w.add_argument("--alpha", type=float, default=0.05)
w.set_defaults(func=_cmd_power)
s = sub.add_parser("selftest", help="run internal sanity checks")
s.set_defaults(func=_cmd_selftest)
return p
def main(argv=None) -> int:
args = build_parser().parse_args(argv)
return args.func(args)
if __name__ == "__main__":
raise SystemExit(main())