Validates the statistical significance of an A/B / feature-flag experiment result - computes per-metric effect size + p-value (chi-square for proportions, Welch's t-test for continuous metrics), applies a multiple-comparison correction (Bonferroni / Benjamini-Hochberg) when N>1 metric, surfaces practical-vs-statistical-significance distinction, and emits a ship/don't-ship verdict per metric. Use when an experiment has finished and someone is about to ship the winning variant off a dashboard readout, when a result rests on a small sample, or when more than one metric was compared - the rigorous version of "the variant looks better in the dashboard."
75
94%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
An experiment toggle A/B test routes each user into a cohort, measures behavior per cohort, and ships the winning variant. A/B tests are "sensitive to variance; they require a large sample size in order to reduce standard error and produce a statistically significant result" (ab-test-wiki; per-cohort toggle routing per feature-toggles). Without proper analysis, teams ship variants that "look better" but aren't actually better. This skill validates the analysis.
The validator needs, per variant:
# experiment-data.yml
experiment_id: checkout-promo-banner-v2
running_since: 2026-04-15
running_until: 2026-05-05 # 21 days
hypothesis: "Promo banner increases checkout completion."
variants:
- name: control
cohort_size: 12450
metrics:
checkout_completion_count: 8523
avg_session_duration_sec: [...samples...] # raw samples for continuous metrics
avg_revenue_per_user: [...samples...]
- name: treatment_a
cohort_size: 12380
metrics:
checkout_completion_count: 8755
avg_session_duration_sec: [...samples...]
avg_revenue_per_user: [...samples...]Per-metric, identify whether it's:
Different statistical tests apply.
For "did the user convert? yes/no":
from scipy.stats import chi2_contingency
def proportion_test(c_success, c_total, t_success, t_total):
"""Returns (p_value, effect_size_pct)."""
table = [[c_success, c_total - c_success],
[t_success, t_total - t_success]]
chi2, p, dof, _ = chi2_contingency(table)
c_rate = c_success / c_total
t_rate = t_success / t_total
effect = (t_rate - c_rate) / c_rate * 100 # relative lift in %
return p, effectFor very small cells (< 5 expected per cell), Fisher's exact is more accurate; chi-square otherwise.
For "what's the average revenue per user?":
from scipy.stats import ttest_ind, mannwhitneyu
def continuous_test(c_samples, t_samples, parametric=True):
"""Welch's t-test (parametric) or Mann-Whitney U (non-parametric)."""
if parametric:
t, p = ttest_ind(c_samples, t_samples, equal_var=False)
else:
u, p = mannwhitneyu(c_samples, t_samples, alternative='two-sided')
c_mean = sum(c_samples) / len(c_samples)
t_mean = sum(t_samples) / len(t_samples)
effect = (t_mean - c_mean) / c_mean * 100
return p, effectUse Mann-Whitney U when the metric isn't normally distributed (revenue per user - heavy right tail; latency - log-normal). Welch's t-test for approximately-normal metrics.
Per ab-test-wiki's "challenges" framing: testing many metrics inflates the false-positive rate. With α=0.05 and 10 independent metrics, P(at least one false positive) ≈ 1 - 0.95^10 = 40%.
Default: Benjamini-Hochberg FDR control - balances false-positive vs false-negative rates; controls the proportion of "wins" that are actually noise. Use Bonferroni when the cost of any false positive is catastrophic (regulatory / safety contexts) and over-conservatism is acceptable.
from statsmodels.stats.multitest import multipletests
reject, p_adj, _, _ = multipletests(p_values, alpha=0.05, method='fdr_bh')
# `reject[i]` is True when metric i is significant after FDR control.adjusted_alpha = alpha / n_metrics # e.g. 0.05 / 10 = 0.005
# Each metric must have p < 0.005 to be significant.Over-conservative - increases false negatives.
For pre-registered single-primary-metric experiments, no correction needed for the primary; correction applies to secondary metrics.
A non-significant result might mean "no effect" or "experiment too small." Compute post-hoc power:
from statsmodels.stats.power import NormalIndPower
def required_sample(effect_size, alpha=0.05, power=0.8):
"""How many users per variant to detect this effect with this power?"""
analysis = NormalIndPower()
return analysis.solve_power(effect_size=effect_size, alpha=alpha, power=power)If the observed effect (e.g., 0.5% relative lift) requires N=50,000 users per variant for 80% power and the experiment had N=12,000, the experiment was under-powered. The verdict shouldn't be "no effect"; it should be "inconclusive - re-run at higher N or accept that we can't detect effects this small."
A 0.1% lift can be statistically significant at N=10M; that doesn't mean the team should ship.
Define minimum detectable effect (MDE) per metric:
# mde.yml
checkout_completion_rate:
mde_relative: 1.0 # 1% relative lift to be worth shipping
mde_absolute: 0.5 # OR a 0.5pp absolute lift
avg_revenue_per_user:
mde_absolute: 0.50 # $0.50/user; below this, ship cost > revenueThe verdict requires both statistical significance AND practical significance (effect ≥ MDE).
Emit a per-metric results table (type, control, treatment, relative effect, raw and adjusted p-value, MDE met, verdict), then a verdict explanation, a ship/pause recommendation, and a power-analysis note. Full worked report: references/output-example.md.
Validate the experiment:
If continuous monitoring is required (e.g. a regression-detection
A/B test), use a sequential testing framework (statsmodels'
sequential probability ratio test) instead of repeated significance
tests.
Seven analysis mistakes and their fixes: references/anti-patterns.md.
feature-flag-test-harness (in the qa-test-environment plugin) - harness that runs the experiment IN test (this skill
validates the experiment IN production).prod-canary-validator -
sibling: same statistical framework, different application
(canary verdict vs experiment verdict).synthetic-monitor-author - sibling: production-side verification, different role.