EVIKA ACADEMY · DATA ANALYTICS SERIES · CHAPTER 88

A/B Testing Deep Dive for Data Analysts

From hypothesis to decision — sample size, p-values, power, Type I/II errors, multiple testing corrections, novelty effects, and how to run experiments that produce trustworthy results.

⏱ 23 min read🧪 Experimentation · Statistics · Python
← Ch 87: Advanced Google SheetsCh 89: Data Warehouse Fundamentals →

A/B testing is the gold standard for establishing causality in product decisions. Every other analytical technique — cohort analysis, funnel analysis, regression — can identify correlations. Only a well-run experiment can tell you whether a change caused an outcome. That distinction matters enormously when a product team is deciding whether to ship a new feature or roll back a redesign.

The mechanics of running an A/B test are straightforward. The statistics are where most teams go wrong — p-hacking, underpowered experiments, peeking at results early, and running so many tests simultaneously that some will appear significant by chance. This chapter gives you the complete mental model: what the numbers actually mean, what can go wrong, and how to interpret and communicate results responsibly.

The A/B Testing Framework

1. Define the goal metric
One primary metric that the test is designed to move. Multiple metrics make interpretation ambiguous.
2. Write the hypothesis
IF we change X, THEN metric Y will change by Z%, BECAUSE of this mechanism.
3. Calculate required sample size
Before launching. Determines how long the test must run — not how long until you see significance.
4. Set the test duration
Based on sample size ÷ daily traffic. Never stop early because you hit significance.
5. Randomise and launch
Assign users to control and treatment randomly. Verify assignment balance before proceeding.
6. Analyse after duration
Run the statistical test once, after the pre-set duration. Not before.

Writing a Good Hypothesis

A vague hypothesis ("we think this will improve conversion") produces uninterpretable results. A precise hypothesis specifies the change, the expected direction and magnitude, and the mechanism — so if the result surprises you, you learn something either way.

❌ Weak hypothesis
"Adding a progress bar to onboarding will improve completion."
✓ Strong hypothesis
"IF we add a 5-step progress bar to the onboarding flow, THEN the onboarding completion rate (users who complete all 5 steps within 48 hours of sign-up) will increase by at least 8 percentage points (from 42% to 50%), BECAUSE users who can see their progress are less likely to abandon mid-flow — a pattern consistent with completion motivation research."

Sample Size and Statistical Power

The three inputs you must define before running any experiment:

Significance level (α)
Usually 0.05
The probability of falsely concluding there is an effect when there is none (Type I error). α = 0.05 means a 5% false positive rate.
Statistical power (1−β)
Usually 0.80
The probability of detecting a real effect when it exists. Power = 0.80 means a 20% chance of missing a real improvement (Type II error).
Minimum Detectable Effect (MDE)
You decide
The smallest change you actually care about. A 0.5% lift may not justify shipping the feature; a 5% lift might. Smaller MDE = more users needed.
import scipy.stats as stats
import numpy as np

def ab_sample_size(baseline_rate, mde, alpha=0.05, power=0.80):
    """
    Calculate required sample size per variant for a two-proportion z-test.
    baseline_rate : current conversion rate, e.g. 0.42
    mde           : minimum detectable effect (absolute), e.g. 0.08
    """
    treatment_rate = baseline_rate + mde
    effect_size = abs(
        (treatment_rate - baseline_rate) /
        np.sqrt((baseline_rate * (1 - baseline_rate) +
                 treatment_rate * (1 - treatment_rate)) / 2)
    )
    analysis = stats.power.tt_ind_solve_power(
        effect_size=effect_size,
        alpha=alpha,
        power=power,
        alternative='two-sided'
    )
    return int(np.ceil(analysis))

# Example: onboarding test
baseline = 0.42   # 42% current completion
mde      = 0.08   # we want to detect ≥ 8pp improvement

n_per_variant = ab_sample_size(baseline, mde)
print(f"Required per variant: {n_per_variant:,}")
print(f"Total users needed: {n_per_variant * 2:,}")

# If daily traffic = 800 users, split 50/50 → 400 per variant per day
days_needed = n_per_variant / 400
print(f"Test duration: {days_needed:.1f} days")

Understanding p-values (and What They Don't Mean)

The p-value is the probability of observing a difference at least as large as the one you measured, assuming the null hypothesis (no real difference) is true. A p-value of 0.03 means: "if there were truly no difference between control and treatment, we would see a gap this large or larger in only 3% of experiments by random chance."

p < 0.05 does NOT mean
✗The variant is definitely better
✗The effect size is meaningful
✗You should ship the feature
✗There is a 95% chance the variant is better
p < 0.05 DOES mean
✓The observed difference is unlikely to be random noise (at your chosen α)
✓You have enough evidence to reject the null hypothesis
✓Combined with a meaningful effect size — you can consider this a real signal

Statistical vs practical significance

A result can be statistically significant but practically useless. With 2 million users in each variant, you can detect a 0.01% lift with p < 0.05 — but a 0.01% improvement in conversion probably doesn't justify the engineering and maintenance cost of a new feature. Always report effect size alongside the p-value.

# Full A/B test analysis
from scipy import stats
import numpy as np

# Results after test duration
control_visitors   = 4200
control_converts   = 1764   # 42.0%
treatment_visitors = 4200
treatment_converts = 1974   # 47.0%

# Proportions
p_control   = control_converts   / control_visitors
p_treatment = treatment_converts / treatment_visitors

# Two-proportion z-test
count   = np.array([treatment_converts, control_converts])
nobs    = np.array([treatment_visitors, control_visitors])
z_stat, p_value = stats.proportions_ztest(count, nobs)

# Effect size: absolute and relative lift
abs_lift = p_treatment - p_control
rel_lift = abs_lift / p_control

# Confidence interval for the difference
se = np.sqrt(p_control*(1-p_control)/control_visitors +
             p_treatment*(1-p_treatment)/treatment_visitors)
ci_low  = abs_lift - 1.96 * se
ci_high = abs_lift + 1.96 * se

print(f"Control:   {p_control:.1%}")
print(f"Treatment: {p_treatment:.1%}")
print(f"Absolute lift: {abs_lift:+.1%}  ({ci_low:+.1%} to {ci_high:+.1%}, 95% CI)")
print(f"Relative lift: {rel_lift:+.1%}")
print(f"p-value:   {p_value:.4f}")
print(f"Significant: {'Yes' if p_value < 0.05 else 'No'}")

Type I and Type II Errors

H₀ is TRUE (no real effect)H₀ is FALSE (real effect exists)
Test says: SIGNIFICANTType I Error (False Positive)
You ship a feature that doesn't work. Probability = α (0.05).
Correct Decision ✓
You detected a real improvement. Probability = Power (0.80).
Test says: NOT SIGNIFICANTCorrect Decision ✓
You correctly didn't ship. Probability = 1−α (0.95).
Type II Error (False Negative)
You missed a real improvement. Probability = β (0.20).

Multiple Testing Problem and Bonferroni Correction

If you test 20 independent hypotheses simultaneously with α = 0.05, you expect one false positive by chance — even if none of the variants actually work. This is the multiple testing problem. It is why running 10 simultaneous A/B tests and declaring the "winning" one is statistically unreliable.

# Bonferroni correction: divide alpha by number of tests
# Conservative but simple

n_tests     = 5            # running 5 simultaneous variants
alpha_orig  = 0.05
alpha_bonf  = alpha_orig / n_tests   # = 0.01

print(f"Original α: {alpha_orig}")
print(f"Bonferroni-corrected α: {alpha_bonf}")
print("Each individual test must achieve p < 0.01 to be considered significant")

# Benjamini-Hochberg (FDR): less conservative than Bonferroni
from statsmodels.stats.multitest import multipletests

p_values = [0.003, 0.041, 0.072, 0.009, 0.118]  # from 5 tests

reject, p_corrected, _, _ = multipletests(p_values, alpha=0.05, method='fdr_bh')

for i, (p, p_c, r) in enumerate(zip(p_values, p_corrected, reject)):
    print(f"Test {i+1}: raw p={p:.3f}  corrected p={p_c:.3f}  reject={r}")

Common A/B Testing Mistakes

⚠️ Peeking at results and stopping early
Running a significance test before the pre-set duration inflates Type I error dramatically. With daily peeking, the effective false positive rate at α=0.05 becomes closer to 30%. Set the duration in advance and do not look until it ends (or use sequential testing methods designed for early stopping).
⚠️ Testing too many variants simultaneously
Each additional variant increases the multiple testing problem. A/B/C/D/E tests (4 variants vs control) require Bonferroni correction. Prefer head-to-head tests unless you have very high traffic.
⚠️ Ignoring the novelty effect
Users react to anything new — including bad new things. A new UI element often gets inflated engagement for the first 1–2 weeks because it is unfamiliar. Run tests long enough to see stable behaviour, not the novelty spike.
⚠️ Not checking for sample ratio mismatch
If your 50/50 split actually assigns 52% to treatment and 48% to control, something is wrong with your assignment logic. Check the split before analysing results — an imbalanced split invalidates the statistical test.
⚠️ Using the wrong metric
Testing a change to checkout flow and using page views as the primary metric. The right metric is completion rate at the specific step being changed. Choosing a metric that is too broad or too upstream dilutes the signal.

Communicating A/B Test Results to Stakeholders

A test result is not a decision. The analyst's job is to present the evidence clearly so the decision-maker can weigh the statistical signal against business context, implementation cost, and strategic priority.

RESULT SUMMARY TEMPLATE
Test name: Progress bar in onboarding flow (v2)
Duration: 14 days (Oct 7–21, 2026)
Traffic: 4,200 control / 4,200 treatment (balanced ✓)
Primary metric: 48-hour onboarding completion rate
Control: 42.0% | Treatment: 47.0%
Absolute lift: +5.0pp (95% CI: +3.2pp to +6.8pp)
Relative lift: +11.9% | p-value: 0.0003 ✓ Significant
Secondary metrics: No degradation to Day-7 retention or session length
Recommendation: Ship. The lift is statistically and practically significant.
Caveat: Watch novelty — re-check completion at 6 weeks post-launch.
← Ch 87: Advanced Google SheetsCh 89: Data Warehouse Fundamentals →
Learn Experimentation at EVIKA ACADEMY
Sector 51, Noida · Live classes online & offline
Book Free DemoWhatsApp Us
🎓 Free Demo Class — Online & Offline · Noida Sector 51