Skill 51 · Diagnosing Experiment Health
Subchapter 51.4
references/interpretation.mdMarkdown30 KBView on GitHub
How to read PostHog experiment results without falling into common interpretation pitfalls.
Read the statistics settings before any result (Step 1 of SKILL.md): the method, the confidence level, sequential testing, CUPED and the baseline variant.
C5 to C7 cover the method and the confidence level. C13 covers sequential testing, CUPED and the baseline variant.
Watching results live and ending the experiment the moment it looks significant inflates false positives — you’re giving randomness more chances to look significant.
The exception: sequential testing. A Frequentist experiment with sequential testing on keeps its false-positive rate at the chosen level however often the result is read (C13). The peeking warning does not apply to it. Bayesian experiments have no sequential mode.
Evidence. All of it is in experiment-get and the stored results:
start_date against now.running_time_calculation.recommended_sample_size.
That target is a total for all variants, not a number per variant.running_time_calculation.recommended_running_time.
On a launched experiment it is the number of days still missing at the current exposure rate, as of the last time someone opened the experiment, and 0 once the target is reached.
It is not the planned length of the run.
Three cases hold the whole estimated length instead: an estimate from numbers the user typed (exposure_estimate_config.conversionRateInputType is manual), a value written when the experiment was created that no page load has replaced since, and a value saved by a page load in the first day of the run or before 100 exposures, when no later page load has replaced it (C12).
Judge progress by the sample, not by this number.stats_config.method, and stats_config.frequentist.sequential_testing_enabled.
A missing method reads as Bayesian.
When the experiment does not set sequential_testing_enabled, the project default applies (C13).Say it unprompted.
MUST state the experiment’s age and its share of the target sample before any reading of a result, when the experiment is below its target sample and sequential testing is off. One day into a run, the honest first sentence is that the result is not readable yet. When the experiment has no target, state the age and the exposures per variant, and say that no target was set.
MUST NOT withhold the numbers or refuse the question. Give the current estimate, labeled as early, and say when it becomes readable.
In the early days of the experiment, significance can flip back and forth a lot: the floor of 50 exposures per variant is where analysis starts, not where it becomes stable.
Recommend:
Symptom: few hundred or fewer exposures per variant; the visible split looks badly off (a roughly 2-to-1 skew at a few dozen exposures can be chance alone: 30 against 16 gives a p-value near 0.04, far from the 0.001 that the product flags).
Mechanism: With low samples per variant (rule of thumb: under a thousand), the visible split can
swing widely from the configured ratio — deterministic-hash variance is large at small samples.
PostHog’s calculations account for this; the visible ratio is not a bug.
Whether a split is a real mismatch is the sample ratio test’s call, not the eye’s (A2 in bias-and-skew.md).
Validity gates [HIGH]. A variant gets no result, and the metric reads “Not enough data yet”, until:
A Bayesian funnel result additionally needs at least 5 people who converted and 5 who did not, in each variant.
So a variant where almost everyone converted reports nothing either.
The same check applies to a Bayesian mean metric with a threshold (5 people who reached it and 5 who did not), and not to a funnel that CUPED adjusts.
The stored results name the failed gate in validation_failures (table in diagnostic-snapshot.md).
Some checks give no result and leave validation_failures empty: the Bayesian 5-and-5 check above, a baseline mean below zero under Bayesian, and a metric whose values do not vary.
The variant then has no interval, no significance and no entry that says why.
The fix is the same for all: more exposures, or accept that the result isn’t ready.
Recommend: wait. Run longer or increase rollout. The gates are where analysis starts. The sample the experiment needs is the running-time calculator’s target (C12).
A/A tests should rarely show significance. If the user reports their A/A test is showing a significant difference, work through:
bias-and-skew.md).multiple_variant_handling = "exclude", people in more than one variant leave the smaller arm in a larger share (A1 in bias-and-skew.md).
On an even split they leave both arms alike.ExperimentDataWarehouseNode with events_join_key: $group_<n>
on the exposure side and data_warehouse_join_key on a group-keyed metric table). The join
gives each exposed user all rows of that user’s group, so a sum
metric over-counts proportional to per-group user count. If user counts are balanced but per-group
user counts aren’t, the sum can swing widely even on a true A/A — and the test reads that as
significant. Detect: the metric’s definition in experiment-get: a warehouse source whose join key is a group key, on an experiment that is aggregated by person.
Sanity check: re-aggregate the warehouse table by
org/group once (deduped) and compare to the per-user sum; a large gap confirms repeated-row
inflation.mid-run-changes.md).
Migrate it before reading an A/A result.Recommend: if chance and exposure do not explain the result, investigate instrumentation rather than assuming the methodology is wrong.
PostHog does not apply multiple-comparisons correction:
So with many metrics or many variants, the chance of some spurious significance grows.
(Confidence level is configurable — see C6.)
Recommend:
mid-run-changes.md.PostHog defaults to Bayesian, unless the project’s default method says otherwise (C7). Common misreads:
goal: "decrease" on the metric).
The page shows one minus the stored value, so that a high chance to win always means “better”.
The stored chance_to_win is not inverted: on such a metric a stored value below 5% is a significant win.is_legacy in experiment-get) no longer show results.
Migrate them with experiment-migrate. See E13 in mid-run-changes.md.PostHog has Frequentist support. Set in stats_config.method. Quick rules:
default_experiment_confidence_level.
It is copied into a new experiment at creation, as stats_config.frequentist.alpha and stats_config.bayesian.ci_level, so a later change of the default does not move existing experiments.
The interval column’s title reads “Credible interval (95%)” or “Confidence interval (95%)” at every level, and the “How to read” tooltip says 95% too.
The interval itself is computed at the experiment’s level.
If a user reports a p-value of 0.07 as “significant”, they’re
likely on the 90% setting; check before debugging the math.A frequent source of confusion:
stats_config.method of the experiment. Do not assume Bayesian.Shipping an inconclusive result can be defensible when all of these hold:
Do not ship if the timeseries chart shows a sustained regression — point-in-time significance can flip, but a sustained downward trend on the timeseries is a stronger signal than a snapshot reading.
Recommend the user open the experiment’s timeseries view (per metric) — point-in-time significance can
flip, but a sustained trend is a stronger signal than a snapshot reading. The agent can also pull
this directly via experiment-timeseries-results, with the metric’s uuid and fingerprint from experiment-get (a shared metric carries both in saved_metrics[].query).
In the series, look for a single day that carries the whole effect: one unusual day can make a flat experiment look significant.
For the qualitative part (replays / intuition), invoke the
posthog:analyzing-experiment-session-replays skill — it surfaces variant-level replay patterns and
is the right tool when the call is “primary metric is up, no guardrail regression, do we ship?”
PostHog can mark a metric as significant and send a notification well before the experiment has accumulated enough data for the result to be stable. The verdict can revert as the sample grows, and no notification is sent when it does. Treat the notification as a prompt to review, not an instruction to ship.
The notification is an opt-in destination on the experiment’s Settings tab.
It is sent from the scheduled calculation, each time a variant turns from not significant to significant against the calculation before.
A result that reverts and comes back is announced again.
Its only gates are the validity gates of C2: no minimum number of days, no check against the planned sample.
The scheduled calculation covers only experiments with status running that started in the last 60 days, and no legacy metric: outside that, no notification is sent.
Before acting on a significance notification, check all of:
$multiple share. If a bias or skew diagnostic (bias-and-skew.md) is in play, the
significance verdict is suspect regardless of how large the gap looks.When a previously-significant result reverts to not-significant, that’s not a bug — it’s the same analysis updated with more exposures. Explain the difference between signal seen so far and result confirmed.
The End-experiment modal opens with the “Variant to keep” selector empty, so ending the experiment rolls out nothing unless the user picks a variant. Nothing behind that pick reads the results: there is no significance check, no primary-metric direction check, and no guardrail check. The “End experiment” button is gated by selecting a conclusion (won / lost / inconclusive / stopped early / invalid), not by the variant selector. The modal keeps a picked variant while the page stays open, so a user who picked one earlier and comes back ships that variant, unexamined.
The modal also asks how to release the chosen variant, with two radio options:
The release-mode choice doesn’t read metrics either; the safer “experiment population” option is the default.
Recommend: before clicking “End experiment”, do three things:
If any guardrail is trending negative, or the primary isn’t actually significant, the safe move is to keep control rather than ship the test variant. This matters most for sophisticated users who set guardrails for a reason.
A common case: conversion counts from the experiment page get pasted into an online A/B calculator, or another AI assistant is asked for the numbers, and the answer differs from PostHog’s (“not significant” vs “significant”, a different count, a different winner).
Three questions to ask before debugging stats:
stats_config.method and re-read (see C6).$multiple excluded, test accounts filtered, exposure-bounded date range, per-user aggregation
for means, the conversion window applied. An online calculator gets none of that — if
the user typed in raw event counts they grabbed from SQL, the calculator and PostHog are
computing on different populations, and disagreement is expected.
An assistant that counts a metric’s events without the experiment’s scope answers a different question too (D1 in numbers-vs-sql.md).After confirming both methodology and inputs match, if the disagreement persists, treat it as a real anomaly worth investigating with the experiment URL.
Symptom (in the user’s vocabulary): “the experiment says complete but nothing is significant”, “it finished within days and we had planned for weeks”, “it reads ‘Target reached’ while every metric still says ‘Not enough data yet’”.
Behavior [HIGH]. The duration display next to the experiment’s status is an estimate of sample size.
It reads “~N days left” while the exposures are below the target, and “Target reached” once they are at or above it.
When the target is more than four weeks away, it reads “Can detect X%+ changes within 4 weeks” with a warning icon instead.
X is the smallest change that the experiment can detect within four weeks at its current traffic, not the minimum detectable effect.
Above 100% it reads “Can detect only changes over 100% within 4 weeks”.
Older versions read “Complete”.
On an ended experiment “Complete” is the status tag: every ended experiment carries it, and the duration display is hidden.
Read status first when the user says “complete”.
Evidence to gather. running_time_calculation in experiment-get:
recommended_sample_size — the target, a total across all variants.recommended_running_time — the days still missing at the current exposure rate, and 0 once the target is reached.
A value far beyond the time the team will run the experiment (many months, years) means that the target is out of reach at this traffic: the experiment cannot detect an effect of that size in a useful time (C8).exposure_estimate_config.conversionRateInputType — manual means that the user typed the baseline and the daily exposures.
Both numbers above then come from that input: recommended_running_time is the whole estimated length, not the days left, and neither number is updated during the run.
The same holds for numbers that were written when the experiment was created, through the API or by an agent, until someone opens the launched experiment.
A recommended_running_time above 0 on an experiment whose exposures have passed the target is one of these cases, or a value saved by a page load before the target was reached.minimum_detectable_effect — the experiment’s own value, or the project default that was copied in when the experiment was created.
When it is absent the project default applies.
experiment-setup-context returns that default (team_defaults) where the tool is available. Otherwise ask the user.In automatic mode the product estimates the target again from the experiment’s own data whenever someone opens the page of a launched experiment, and saves it. So the target moves during the run, and the label a user saw last week can differ from today’s stored numbers. Compare the minimum detectable effect with the effect the user hopes for.
Co-occurs with: C1 (the same people read results early), C2.
Recommendation.
Three settings change what the numbers on the page mean.
Each is set on the experiment.
Sequential testing and CUPED fall back to the project default where the experiment does not set them. The baseline falls back to control.
Sequential testing (stats_config.frequentist.sequential_testing_enabled). Frequentist only.
sequential_tuning_parameter, default 5,000 or the project’s default), and larger far from it.
An experiment with a much larger sample and the default value has a wider interval than it needs.[MEDIUM]: the value stays at 1 until the evidence crosses a threshold.CUPED (stats_config.cuped.enabled).
numbers-vs-sql.md).threshold, a mean metric on a session property (such as $session_duration) and a funnel with a data-warehouse step are not adjusted either.Baseline variant (stats_config.baseline_variant_key).
control, or the flag’s first variant when it has no control, unless the experiment names another variant.Evidence to gather. stats_config in experiment-get, and the experiment’s change history when a result moved without new data: a setting changed on the page recalculates the whole run (D8 in numbers-vs-sql.md).
What a missing key means depends on the key:
method reads as Bayesian, and a missing confidence level as 95%.
The project’s defaults for both are copied into an experiment when it is created.frequentist.sequential_testing_enabled and cuped fall back to the project’s current default.experiment-setup-context returns the project’s defaults (team_defaults) where that tool is available.
Otherwise ask the user to read the project’s experiment settings.
A p-value of exactly 1.000 on a metric of a Frequentist experiment is a sign of sequential testing. A p-value below 1 does not rule it out.