A/B Testing & Experiment Design

Shipping a model without A/B testing is like tightrope walking without a safety net — you have no idea whether it's actually better or just lucky.

1. What You'll Learn


2. A Real Story from a Product Manager

(1) The Pain: Not Knowing Whether a New Model Is Actually Better

Alice's SaaS platform launched a new recommendation model, and the dashboard showed conversion rising from 3.2% to 3.5%. But the CEO pushed back: "This could just be natural fluctuation — how do you prove the model caused it?" Alice had no answer — without a controlled experiment, no improvement can be attributed.

(2) The A/B Testing Solution

A/B testing randomly splits users into Group A (old model) and Group B (new model), runs both at the same time, and uses statistics to determine whether the difference is significant.

PYTHON
from scipy import stats

# Group A: old model, Group B: new model
conversions_a, n_a = 320, 10000  # 3.2%
conversions_b, n_b = 350, 10000  # 3.5%

z_stat, p_value = stats.proportions_ztest(
    [conversions_a, conversions_b], [n_a, n_b]
)
print(f"p-value: {p_value:.4f}")
print(f"Significant: {p_value < 0.05}")

(3) The Payoff: A 3-Week Experiment Proved a 2.3% Conversion Lift, Adding 500K USD per Year

Alice used a 3-week A/B test to scientifically validate the new model's improvement — p=0.03, statistically significant. With that confidence she rolled it out to everyone, adding roughly 500 thousand USD in annual revenue.


3. The Statistics Behind A/B Testing

(1) The Hypothesis Testing Framework

100%
sequenceDiagram
    participant Design as Design Experiment
    participant Split as Random Split
    participant Collect as Collect Data
    participant Test as Statistical Test
    participant Decision as Decision

    Design->>Split: Define H0/H1, sample size
    Split->>Collect: Group A (control) vs Group B (treatment)
    Collect->>Test: After experiment period
    Test->>Decision: p-value calculation
    Decision->>Decision: p < α → Reject H0 (B is better)
    Decision->>Decision: p ≥ α → Cannot reject H0

(2) Core Statistical Concepts

▶ Example: Hypothesis Testing Calculation

PYTHON
from scipy import stats
import numpy as np

# Two-proportion z-test
def ab_test_proportions(conversions_a, n_a, conversions_b, n_b, alpha=0.05):
    p_a = conversions_a / n_a
    p_b = conversions_b / n_b
    p_pool = (conversions_a + conversions_b) / (n_a + n_b)

    se = np.sqrt(p_pool * (1 - p_pool) * (1/n_a + 1/n_b))
    z_stat = (p_b - p_a) / se
    p_value = 1 - stats.norm.cdf(z_stat)

    ci_low = (p_b - p_a) - 1.96 * se
    ci_high = (p_b - p_a) + 1.96 * se

    return {
        "conversion_a": f"{p_a:.4f}",
        "conversion_b": f"{p_b:.4f}",
        "lift": f"{(p_b - p_a) / p_a * 100:.2f}%",
        "z_stat": f"{z_stat:.3f}",
        "p_value": f"{p_value:.4f}",
        "ci_95": f"({ci_low:.4f}, {ci_high:.4f})",
        "significant": p_value < alpha,
    }

result = ab_test_proportions(320, 10000, 350, 10000)
for k, v in result.items():
    print(f"{k:15s}: {v}")

Output:

TEXT
# Functions defined successfully
Concept Meaning Typical Threshold
H0 (null hypothesis) A and B are no different
H1 (alternative hypothesis) B is better than A
α (significance level) Probability of a Type I error (false positive) 0.05
p-value Probability of observing the current or a more extreme result under H0 <0.05
Power Probability of correctly rejecting H0 (1-β) 0.8
Confidence interval The plausible range of the true difference 95% CI

4. Experiment Design

(1) Sample Size Calculation

▶ Example: Calculating the Sample Size Needed for an A/B Test

PYTHON
from statsmodels.stats.power import zt_ind_solve_power
from statsmodels.stats.proportion import proportion_effectsize
import numpy as np

def calculate_sample_size(p_baseline, mde, alpha=0.05, power=0.8):
    """Calculate required sample size per group.

    Args:
        p_baseline: Baseline conversion rate
        mde: Minimum Detectable Effect (relative lift)
        alpha: Significance level
        power: Statistical power
    """
    p_new = p_baseline * (1 + mde)
    effect_size = proportion_effectsize(p_new, p_baseline)
    n = zt_ind_solve_power(effect_size=effect_size, alpha=alpha, power=power)
    return int(np.ceil(n))

# Alice's SaaS: baseline 3.2%, wants to detect 10% relative lift
baseline = 0.032
for mde in [0.05, 0.10, 0.15, 0.20]:
    n = calculate_sample_size(baseline, mde)
    print(f"MDE={mde*100:.0f}%: Need {n:,} users per group "
          f"(total {n*2:,}, ~{n*2/baseline/1000:.0f}k visitors)")

Output:

TEXT
# Functions defined successfully
MDE (Minimum Detectable Effect) Sample Size per Group Total Sample Size Experiment Duration (50k DAU)
5% relative lift 152,000 304,000 6 days
10% relative lift 38,000 76,000 1.5 days
15% relative lift 17,000 34,000 <1 day
20% relative lift 9,600 19,200 <1 day
📌 Key point: The smaller the MDE (the smaller the lift you want to detect), the larger the sample size you need. With a baseline of just 3.2%, detecting a 5% relative lift (i.e., 3.2%→3.36%) requires more than 300,000 users.

(2) Traffic Splitting Strategies

Strategy Method Pros Cons
User-level Hash by user_id Consistent user experience Requires login
Request-level By request_id Simple The same user may see different results
Device-level By device_id Covers logged-out users Multi-device users may conflict

▶ Example: Consistent Hash Splitting

PYTHON
import hashlib

def assign_group(user_id, salt="experiment_1", num_groups=2):
    """Consistent hash assignment for A/B testing."""
    hash_input = f"{salt}_{user_id}".encode()
    hash_val = int(hashlib.md5(hash_input).hexdigest(), 16)
    return hash_val % num_groups  # 0=A, 1=B

# Verify equal split
user_ids = range(100000)
groups = [assign_group(uid) for uid in user_ids]
print(f"Group A: {groups.count(0)} ({groups.count(0)/len(groups)*100:.1f}%)")
print(f"Group B: {groups.count(1)} ({groups.count(1)/len(groups)*100:.1f}%)")

Output:

TEXT
# Functions defined successfully

5. A/B Testing ML Models

(1) Comparing Models Online

▶ Example: Alice's Recommendation Model A/B Test

PYTHON
import numpy as np
from scipy import stats

rng = np.random.default_rng(42)

# Simulate 2-week A/B test for recommendation model
n_users = 20000  # Per group

# Group A: Old model (baseline conversion 3.2%)
conversions_a = rng.binomial(1, 0.032, n_users)
revenue_a = conversions_a * rng.exponential(200, n_users)  # Avg 200 USD per conversion

# Group B: New model (true conversion 3.5%, 9.4% lift)
conversions_b = rng.binomial(1, 0.035, n_users)
revenue_b = conversions_b * rng.exponential(200, n_users)

# Analyze conversion rate difference
conv_rate_a = conversions_a.mean()
conv_rate_b = conversions_b.mean()
z_stat, p_value = stats.proportions_ztest(
    [conversions_a.sum(), conversions_b.sum()],
    [n_users, n_users]
)

# Analyze revenue difference
rev_per_user_a = revenue_a.mean()
rev_per_user_b = revenue_b.mean()
t_stat, t_pvalue = stats.ttest_ind(revenue_a, revenue_b)

print("=" * 50)
print("A/B TEST RESULTS: Recommendation Model")
print("=" * 50)
print(f"Conversion: A={conv_rate_a:.3%} vs B={conv_rate_b:.3%}")
print(f"  Lift: {(conv_rate_b - conv_rate_a) / conv_rate_a * 100:.1f}%")
print(f"  p-value: {p_value:.4f} {'✅ Significant' if p_value < 0.05 else '❌ Not significant'}")

print(f"\nRevenue/User: A={rev_per_user_a:.2f} vs B={rev_per_user_b:.2f} USD")
print(f"  Lift: {(rev_per_user_b - rev_per_user_a) / rev_per_user_a * 100:.1f}%")
print(f"  p-value: {t_pvalue:.4f}")

# Business impact
annual_lift = (rev_per_user_b - rev_per_user_a) * 50000 * 12  # 50k users * 12 months
print(f"\nProjected annual revenue lift: {annual_lift:,.0f} USD")

Output:

TEXT
=
A/B TEST RESULTS: Recommendation Model
=

(2) Common A/B Testing Pitfalls

Pitfall Description Solution
Peeking Declaring significance before the deadline Fix the experiment duration; only analyze at the end
Multiple testing Testing many metrics / many groups Bonferroni correction
Novelty effect A new UI performs well at first Extend the experiment duration
Contamination Group A users see Group B's content User-level splitting + log verification
Insufficient sample size Statistical power not reached Calculate the sample size in advance

6. Multi-Armed Bandits (MAB)

(1) Balancing Exploration and Exploitation

Traditional A/B testing uses a fixed traffic split (50/50), while MAB adjusts dynamically — better-performing models get more traffic.

▶ Example: Thompson Sampling

PYTHON
import numpy as np

rng = np.random.default_rng(42)

# 3 model variants with true conversion rates
true_rates = [0.030, 0.035, 0.028]  # Model B is best
n_arms = len(true_rates)
n_rounds = 10000

# Thompson Sampling
successes = np.zeros(n_arms)
trials = np.zeros(n_arms)
total_reward = 0

for t in range(n_rounds):
    # Sample from Beta distribution for each arm
    samples = [rng.beta(successes[i] + 1, trials[i] - successes[i] + 1) for i in range(n_arms)]
    chosen = np.argmax(samples)

    # Simulate user interaction
    reward = rng.binomial(1, true_rates[chosen])
    successes[chosen] += reward
    trials[chosen] += 1
    total_reward += reward

print(f"Thompson Sampling after {n_rounds} rounds:")
for i in range(n_arms):
    print(f"  Model {i}: chosen {int(trials[i]):5d} times, "
          f"observed rate={successes[i]/max(trials[i],1):.4f} (true: {true_rates[i]:.4f})")
print(f"Total conversions: {int(total_reward)}")

Output:

TEXT
# Executed successfully
MAB Algorithm Strategy Exploration Method Complexity
ε-Greedy Explore randomly with probability ε Fixed proportion Low
UCB Pick the arm with the highest upper confidence bound Uncertainty-driven Medium
Thompson Sampling Sample from the posterior to pick an arm Probability-driven Medium
Dimension A/B Testing MAB
Traffic allocation Fixed (50/50) Dynamically adjusted
Exploration cost High (50% goes to the worse one) Low (adaptive)
Statistical rigor High (hypothesis testing) Low (asymptotic)
Use case When statistical proof is needed Fast iteration

❓ FAQ

Q How long should an A/B test run?
A At least 2 weeks (to cover full-cycle effects such as weekend differences) and until you reach the planned sample size. Take whichever is longer. Don't stop early (even if p<0.05).
Q Is p-value < 0.05 enough?
A No. You also need — 1) statistical power (power ≥ 0.8); 2) sufficient sample size; 3) business significance (is the lift worth the investment); 4) cycle effects ruled out.
Q What if the A/B test and the B/A test contradict each other?
A Check — 1) whether there's a time-period effect; 2) whether the split is even; 3) whether there's an interaction effect (the new model is better for some users and worse for others). Use stratified analysis to dig deeper.
Q Can MAB replace A/B testing?
A Not entirely. MAB suits fast exploration (e.g., recommendation ranking), while A/B testing suits decisions that need statistical proof (e.g., pricing strategy, model rollout). They complement each other.
Q How do you handle an A/A test (both groups use the old model)?
A An A/A test validates the splitting system — if the A/A test shows a significant difference, the split is broken. It's a good idea to run an A/A validation before every A/B test.
Q What about testing multiple metrics at once?
A Use the Bonferroni correction — with k metrics, use a significance level of α/k. Or pre-designate one primary metric and treat the rest as secondary metrics.

📖 Summary


📝 Exercises

  1. Basic (difficulty ⭐): Use proportions_ztest to test whether two conversion rates (3.0% vs 3.5%, 10k users each) differ significantly, and compute the p-value and 95% confidence interval. Hint: stats.proportions_ztest.
  2. Intermediate (difficulty ⭐⭐): Calculate the sample size for different MDEs and plot an "MDE vs sample size" curve. Hint: zt_ind_solve_power + matplotlib.
  3. Challenge (difficulty ⭐⭐⭐): Build a Thompson Sampling simulator — 3 arms with different true conversion rates — and after 10,000 rounds compare Thompson Sampling with ε-Greedy on total reward and best-arm selection rate. Hint: Beta distribution sampling + comparison against random exploration.

← Previous: Model Deployment | Next: Production Monitoring & Model Drift →

Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏