learn · experimentation engine
Readout and statistics
Sizing, win rules, peeking, small-account math.
In Experimentation engine
- Hypothesis registryOne row per test, everything set before launch.
- Assignment and randomizationUnits, arms, holdouts and logged odds.
- Exposure log and unitsWhich unit saw which arm, joined to outcomes.
- Metrics and guardrailsPrimary metrics and what must not get worse.
- Readout and statisticsSizing, win rules, peeking, small-account math.
- Offline replay and shadow testsTesting listen-side changes on stored data.
- Off-policy evaluationJudging a new policy on logged decisions.
- Learnings baseEvery readout, win or lose, and the priors it sets.
The readout turns a test's raw counts into a verdict: ship, revert, extend or kill, or void and underpowered when the test itself failed. It does that with math chosen before launch, so the answer can't drift toward whatever the founder hoped. This page covers sample size for small accounts, why peeking breaks a test, Bayesian readouts with priors from past tests, how often a pre-set win rule would declare a winner by luck alone, and the decision table.
hypothesis → assignment → exposure → metrics → readout · the math and the verdict → learning
what it is, in industry terms
Statistical inference with a decision rule attached.
Experimentation platforms call this the analysis or readout step. It answers three questions in order. Did the test run the way it was designed (enough units, the right split)? Is the difference real or noise? Is it big enough to act on? Every number it uses comes from the experiment record, which was written before launch.
Significance level — α · false-positive rate · How often you'd declare a win when nothing changed. 5% is the usual line.
Power — 1 − β · How often you'd catch a real effect of the size you planned for. 80% is the usual line.
MDE — minimum detectable effect · The smallest difference the test can reliably see at its sample size.
Confidence interval — The range of effects consistent with the data. Report it, not just "won".
Fixed horizon — Decide the sample size first, look once at the end.
Sequential test — group sequential · always-valid · A test designed to be looked at many times, at the cost of stricter lines.
Posterior — Bayesian · What you believe about each arm after the data, starting from a prior.
SRM — sample ratio mismatch · The arms got a different split than planned. Usually a bug; it voids the test.
how it works
Seven checks, always in the same order.
The readout runs on the judge-by date whether the test looks interesting or not. A failed early check stops the readout before the primary metric is ever looked at, so a broken test can't be read as a win.
- Freeze — the snapshot on the judge-by date, never later
- Sample — reached? units per arm vs min_per_arm
- Split — as planned? SRM check: χ² on assigned counts
- Exposure — clean? assigned but never shown is excluded
- Guardrails — any breach stops the readout
- Primary — vs the rule effect + interval, method set at launch
- Verdict — + learning ship · revert · extend · kill · void A failed check at steps 2 to 4 ends the readout as void or underpowered, before the primary metric is computed.
Freeze — Copy every unit, its arm, its exposure time and its outcomes into a readout snapshot at the judge-by date. Late events after that date don't count.
Sample reached? — Compare units per arm to min_per_arm. If short, the primary metric isn't read: extend once, as pre-planned, or close as underpowered.
Split as planned? — A chi-square test on assigned counts against the planned shares. p < 0.001 means sample ratio mismatch, usually a bug in assignment or logging. The test is void.
Exposure clean? — Only units that actually saw their arm count. A lead assigned a DM that was never sent is excluded from both the count and the outcome.
Guardrails — Stop requests, spam reports, founder minutes, tool spend. A breach ends the test as revert, whatever the primary metric says.
Primary vs the rule — Compute the effect and its interval with the method named in the record, then apply the win rule exactly as written.
Verdict and learning — Write the verdict, the effect with its interval, and the confidence label. Then write one learnings row, win or lose.
sizing for small accounts
How many units each arm needs, before launch.
For a yes or no metric, the rule of thumb for 80% power at a 5% two-sided significance level is n per arm ≈ 16 · p(1 − p) / d², where p is the average rate across the two arms and d is the difference you want to detect. The exact two-proportion formula is below; the rule of thumb approximates it.
n ≈ 16 · p̄(1 − p̄) / d² rule of thumb (80% power, α = 0.05) n = [ z₁₋α/₂ · √(2p̄(1 − p̄)) + z₁₋β · √(p₀(1 − p₀) + p₁(1 − p₁)) ]² / d² exact, two proportions n ≈ 16 · σ² / d² for an average (use log impressions, not raw)
worked example
Control 5%, variant 15%, 80% power, 20 units per arm a week: 141 units per arm by the exact formula, 144 by the rule of thumb, 282 in total, about 7 weeks. Too slow for most small accounts. With the same 5% control, the smallest variant rate you could detect is about 36% with 25 units per arm, 25% with 50, 17% with 100, 13% with 200 and 10% with 400. Units are what gets an arm: posts, leads or visitors. A thousand impressions on one post is still one unit.
| Worked case | Control | Variant | Rule of thumb | What it means for a small account |
|---|---|---|---|---|
| Replied within 7 days | 5% | 10% | 444 leads | A doubled reply rate still needs hundreds of leads per arm. |
| Replied within 7 days | 5% | 15% | 144 leads | Tripling it is visible in weeks of daily outreach. |
| Cleared a threshold at 24h | 30% | 50% | 96 posts | |
| Replied (any) | 10% | 20% | 204 units | A common size for post-level reply tests. |
| Waitlist join per visitor | 2% | 4% | 1,164 visitors | Page tests need real traffic. Use replay or a proxy metric first. |
rule of thumb for small accounts
If the sizing says more units than you'll have in 6 weeks, don't run that test live. Pick a metric with a higher base rate (replies or profile clicks before signups), test a bigger change, replay it offline if it's a listen-side change, or pool it across accounts later.
why peeking breaks a test
Look every day and stop at the first "win", and you'll win by luck far more than 5% of the time.
A fixed-horizon test promises a 5% false-positive rate only if you look once, at the end. Each extra look is another chance for noise to cross the line, and you stop on the lucky one. A run of 2,000 A/A tests, where both arms are identical so every "win" is false, shows how often each habit declares a winner.
2,000 A/A tests
True rate 10% in both arms, 200 units per arm, a two-proportion z-test at α = 0.05 on each look. Looking once at the end declared a false winner 5.3% of the time. Stopping at the first of 10 looks that crossed p < 0.05 did it 19.1% of the time. Small samples and low rates make the z-test a little conservative, so "once" can land just under or over 5%.
Ways to look safely
- Don't look at the primary metric. Watch guardrails only (stop requests, spam reports, credits). Stopping for harm is always allowed.
- Plan the looks. A group-sequential design (O'Brien-Fleming boundaries) spends the 5% across, say, 3 planned looks, with a very strict line early and close to the normal line at the end.
- Use an always-valid test. A mixture sequential probability ratio test (mSPRT) gives p-values you can check any time, at the cost of needing a bit more data.
- Read a Bayesian posterior without a stop rule. Report it on the judge-by date only. Bayesian numbers don't make peeking free if you still stop on the first good-looking day.
A sensible default
Fixed horizon, block-randomized, one readout on the judge-by date. Guardrails are checked daily and can stop a test for harm. The primary metric isn't computed until judge-by. That's the cheapest rule to follow and the easiest to audit.
Bayesian readouts
"How likely is the variant better, and by how much?" with priors from past tests.
For a yes or no metric, each arm's rate gets a Beta prior. Data updates it: successes add to α and failures add to β. The posterior answers the founder's real question directly: the chance the variant is better, a range for the difference, and the expected loss if you ship it and you're wrong. Small samples give wide answers, honestly.
prior: rate ~ Beta(α₀, β₀) α₀ = m · s, β₀ = (1 − m) · s (m = prior mean, s = prior strength in pseudo-units) posterior: rate ~ Beta(α₀ + successes, β₀ + failures) report: P(variant > control), 90% interval for (variant − control), expected loss = E[max(control − variant, 0)]
worked example
Control 0 of 2, variant 2 of 2, a flat prior (mean 50%, strength 2 pseudo-units). Posterior means: control 25%, variant 75%. Chance the variant is better: 95.0%. 90% interval for the difference: 0 to +88 points. Expected loss if you ship it: 0.7 points. Four units give a confident-sounding number with a very wide interval, which is the honest answer.
priors from the learnings base
A prior is a past learning, discounted. If replies-per-post averaged around 10% across earlier tests on the same account and channel, start the control at a mean of 10% with a strength of 20 to 50 pseudo-units, not the full count of past posts. Old results from other channels or account sizes get a weaker strength, or a flat prior. The prior and its source are written in the experiment record before launch.
pre-set win rules
A win rule is a test too. Know its false-win rate.
Hand-run tests on a small account often use simple rules instead of p-values: "both posts clear 150 impressions at 24 hours and the other version's don't", or "beats both other arms in 2 of 3 blocks". Those rules are good: they're written in advance and easy to read. But each one has a chance of declaring a winner when nothing is different, and that chance should be known before launch. The table works it out for the rules in use, assuming the units are independent and every arm truly performs the same.
threshold rule, 2 units per version
Rule: one version clears the line with both its posts, and the other version clears with neither. With no real difference, that happens with probability 2 · p² · (1 − p)², where p is the chance any one post clears by itself. It peaks at 12.5% when p = 50%. Requiring the same version to win on two independent replications drops it to 0.78%.
| Rule | Used in | Declares a winner by luck alone | Stronger version |
|---|---|---|---|
| Threshold rule, 2 units per version | Hand-run post tests | at most 12.5% (when each post clears half the time) | 3 or 4 units per version, or require the same winner in a second pair |
| Block majority, 3 arms, 2 of 3 blocks | Block-randomized tests | about 78% (21 of 27 block outcomes produce a winner) | 3 of 3 blocks (about 11%), or more blocks with a margin over the runner-up |
| Rank top lanes | Channel or topic rankings | not a pass or fail test; any lane is in the top 4 by chance 2 times in 3 | treat it as a ranking to follow up, then confirm the winner with a 2-arm test |
| Two-proportion z-test, one look | Default for yes or no metrics | 5%, by design | fine as is; add a guardrail-only interim |
| Same test, 10 looks, stop at first win | What happens without a rule | roughly 15 to 20% (see the A/A runs above) | a planned sequential design or one look |
finding
With no real difference, the 2-of-3 block majority still names a winner about 78% of the time. The only way it doesn't is when each block has a different winner, which happens 6 times out of 27. Read its result as a direction, not a finding. A stricter rule (3 of 3 blocks, or a margin over the runner-up) brings luck down to about 11%, at the cost of more "inconclusive" results.
the decision table
Every readout ends in one of six verdicts.
The verdict depends on three things, read in order: did the test run cleanly, did a guardrail break, and where does the interval for the primary metric sit compared with the smallest effect worth acting on? That smallest effect is set before launch, not after.
| Test ran cleanly? | Guardrails | Primary metric | Verdict | What happens next |
|---|---|---|---|---|
| yes | none broken | interval above zero and the win rule passes | ship | Roll out the arm, bump the version string, write the learning with its effect and interval. |
| yes | none broken | interval below zero | revert | Keep the control. Log the loss; it's as useful as a win. |
| yes | none broken | interval spans zero and still includes an effect worth acting on | extend | Extend once, to a size set in the record before launch. If still unclear, close it as inconclusive. |
| yes | none broken | interval spans zero and excludes any effect worth acting on | kill | The knob doesn't matter at this size. Keep whichever arm is cheaper and move to the next constraint. |
| any | a guardrail broke | not read | revert | Stop now, even mid-test. Guardrails are the one allowed early look. |
| no: sample ratio mismatch or broken exposure | any | not read | void | Fix the assignment or logging bug, then rerun from scratch. Never repair a void test with math. |
| no: sample not reached by judge-by | none broken | not read | underpowered | Extend once if the plan allows; otherwise close it as underpowered. Not a loss, not a win. |
the readout object
One frozen snapshot per readout.
The readout is written once, at the judge-by date, and never edited. Recomputing it later from live tables would let late events and fixed bugs quietly change history. A re-analysis is a new readout row that points at the old one.
| Field | Type | What it holds |
|---|---|---|
readout_id | string | ro.<experiment_id>.<n>. A re-analysis is a new row with supersedes set. |
experiment_id | string | The test it reads. |
frozen_at | timestamp | The judge-by moment. Events after it are ignored. |
n_assigned | per arm | Units drawn into each arm. |
n_exposed | per arm | Units that actually saw their arm. The denominator for the primary metric. |
srm_p | number | Chi-square p-value of assigned counts against planned shares. Below 0.001 voids the test. |
guardrails | list | Each guardrail per arm, its limit, and whether it broke. |
primary | object | Metric id, value per arm, the difference, its interval, and the method used. |
posterior | object, optional | For Bayesian readouts: the prior used, P(variant better), expected loss. |
rule | object | The win rule as written at launch, and its result: pass, fail, or not read. |
looks | integer | How many times the primary metric was computed. Should be 1 unless the design planned more. |
verdict | ship | revert | extend | kill | void | underpowered | From the decision table. |
confidence | strong | suggestive | inconclusive | Copied into the learning. |
caveats | list | Confounds, timing problems, anything a reader should know. |
supersedes | readout id, optional | For a re-analysis. |
{
"readout_id": "ro.exp.post.opener.01.1",
"experiment_id": "exp.post.opener.01",
"frozen_at": "2026-05-12T21:00:00Z",
"n_assigned": {
"proof": 2,
"plain": 2
},
"n_exposed": {
"proof": 2,
"plain": 2
},
"srm_p": null,
"guardrails": [
{
"metric": "negative_feedback",
"proof": 0,
"plain": 0,
"limit": 0,
"broke": false
}
],
"primary": {
"metric": "impressions_24h >= 150",
"value": {
"proof": "2 of 2",
"plain": "0 of 2"
},
"raw": {
"proof": [
310,
188
],
"plain": [
74,
51
]
},
"method": "threshold rule, set before launch"
},
"secondary": {
"replies": {
"proof": 14,
"plain": 3
}
},
"rule": {
"text": "both posts in a version clear 150 impressions at 24h and the other version's don't",
"result": "pass: proof"
},
"looks": 1,
"verdict": "ship",
"confidence": "suggestive",
"caveats": [
"metrics pulled between 20 and 30 hours after posting, not exactly 24h",
"other posts went out the same week and shared the audience",
"the post bodies differed, so the opener isn't perfectly isolated",
"rule's false-win rate under no difference is at most 12.5%"
],
"supersedes": null
}Illustrative values, invented to show the shape.
catalog of methods
Which method for which metric.
Pick one before launch and write it into the experiment record. Small-account fit says how well each one copes with a few dozen units.
yes or no rate — small-account fit: fair
Two-proportion z-test
- when
- Yes or no metrics with at least about 5 successes per arm.
- how
- z = (p̂₁ − p̂₀) / √(p̄(1 − p̄)(2/n))
- watch
- Too optimistic with tiny counts; switch to Fisher.
yes or no rate — small-account fit: good
Fisher's exact test
- when
- Yes or no metrics with tiny counts, such as 2 of 12 against 0 of 12.
- how
- Exact hypergeometric probability of a split this lopsided.
- watch
- Conservative: it rarely declares a win at very small n, which is honest.
skewed count — small-account fit: good
Mann-Whitney U
- when
- Skewed counts like impressions, where one viral post would swamp an average.
- how
- Ranks all units together and asks whether one arm's ranks run higher.
- watch
- Answers 'tends to be higher', not 'by how much'. Pair it with a bootstrap interval.
skewed count — small-account fit: fair
t-test on log(1 + x)
- when
- Impressions and engagement counts when you want an effect size.
- how
- Compare means of log(1 + impressions). exp(difference) is the ratio of geometric means.
- watch
- Zeros and huge outliers still matter; check with Mann-Whitney.
any metric — small-account fit: fair
Bootstrap interval
- when
- Any metric when the formula is unclear: medians, ratios, reply rate per post.
- how
- Resample units with replacement 10,000 times; take the 5th and 95th percentiles of the difference.
- watch
- Resample units (posts, leads), never impressions inside a post.
yes or no rate — small-account fit: good
Threshold rule (sign test)
- when
- Small-account post tests: does each post clear a line set before launch?
- how
- Turns a noisy count into yes or no per unit, then compares counts of yes.
- watch
- Know its false-win rate (see the table above); pick the line before launch.
yes or no rate — small-account fit: fair
Block majority rule
- when
- Block-randomized tests where each arm runs once per block of days.
- how
- Count blocks each arm wins; the rule says how many it needs.
- watch
- With 3 arms and 3 blocks, 2 of 3 picks a winner by luck about 78% of the time.
any metric — small-account fit: good
Paired or matched design
- when
- Same topic or same lead type in both arms, so topic doesn't masquerade as the effect.
- how
- Analyze differences within each pair rather than between groups.
- watch
- Pairs must be formed before seeing outcomes.
yes or no rate — small-account fit: good
Beta-Binomial Bayesian
- when
- Small samples where a prior from past tests is honest and helpful.
- how
- Posterior Beta(α₀ + s, β₀ + f) per arm; report P(better) and expected loss.
- watch
- Write the prior and its source before launch; don't tune it after.
yes or no rate — small-account fit: fair
O'Brien-Fleming group sequential
- when
- Tests you genuinely need to look at, 2 to 5 times, at planned points.
- how
- Very strict boundary at early looks, close to 1.96 at the last; total α stays 5%.
- watch
- Looks must be planned; an unplanned extra look breaks it.
yes or no rate — small-account fit: weak
mSPRT, always-valid p-values
- when
- Continuous monitoring, such as a live holdout read monthly.
- how
- A mixture likelihood ratio you can check at any time without inflating α.
- watch
- Needs more data than a fixed test to reach the same certainty.
any metric — small-account fit: fair
Holm-Bonferroni correction
- when
- More than one primary comparison: three arms, or several metrics treated as primary.
- how
- Sort p-values; compare the k-th smallest to α / (m − k + 1).
- watch
- Better: name one primary metric and treat the rest as exploratory.
any metric — small-account fit: weak
CUPED covariate adjustment
- when
- Units with a pre-period baseline, such as accounts or returning leads.
- how
- Subtract θ · (pre-period value − its mean) from each unit's outcome before comparing.
- watch
- Needs a pre-period per unit; posts don't have one.
any metric — small-account fit: good
Sample ratio mismatch check
- when
- Every test, before anything else.
- how
- Chi-square of assigned counts against planned shares; p < 0.001 voids the test.
- watch
- Catches assignment and logging bugs that would otherwise look like effects.
failure modes
How readouts go wrong, and how to catch it.
Peeking at the primary metric — Checking daily and stopping on the first good day inflates false wins well past 5%. · Readout code computes the primary only at judge-by; the looks field must equal the plan.
Counting impressions as units — Treating 2,000 impressions as 2,000 trials makes any gap look significant. · The unit field in the record decides the denominator: posts, leads or visitors.
Moving the goalposts — Changing the metric, the window or the threshold after seeing data. · The win rule is copied from the record into the readout, word for word, and compared.
Sample ratio mismatch — A bug sends 70% of leads to one arm; the 'effect' is the bug. · SRM check first; p < 0.001 voids the test before the metric is read.
Winner's curse — A winning test's effect is overestimated, so the shipped change disappoints. · Shrink toward the prior (Bayesian), and recheck the learning on a later date.
Confounded calendar — Another viral post or a holiday lands in one arm's days. · Block-randomize so each arm runs in each block; note confounds in caveats.
Too many metrics — Ten metrics checked, one 'wins' by chance. · One primary metric per test; everything else is labeled exploratory.
Reading a rule's win as proof — A rule with a high false-win rate is treated as strong evidence. · Store the rule's false-win rate in the record; above 5% caps confidence at suggestive.
data to capture
What the readout records, and what that trains.
Record — every readout as a frozen snapshot: units assigned and exposed per arm, SRM p-value, guardrails, the primary effect with its interval and method, the rule result, number of looks, verdict and caveats
Label — the verdict (ship, revert, extend, kill, void, underpowered) and the confidence (strong, suggestive, inconclusive), set by rule, not judgment
Trains — priors for the next test on the same knob; a calibration check of how often 'suggestive' wins replicate; later, an agent that proposes and sizes tests
Test — the readout itself is tested by A/A runs: an A/A test that 'wins' more than 5% of the time over many runs means the pipeline is broken