← Back to Projects

A/B Testing & Experimentation Analysis (R)

A complete A/B testing pipeline in R – from data generation and randomization QC through hypothesis testing, bootstrap/permutation inference, regression adjustment, and power analysis – built on simulated SaaS onboarding experiment data with 10,000 users.


Project Overview

Overview

This project builds a 7-script experimentation pipeline in R simulating a SaaS A/B test comparing a new onboarding flow (treatment) against the current experience (control) across 10,000 users. The pipeline covers every stage of a rigorous experiment analysis – from data generation through power assessment.

The analysis follows industry methodology used at companies like Microsoft, Booking.com, and Netflix: verify randomization integrity (SRM) first, then analyze the primary metric, check guardrail metrics, validate with distribution-free inference (bootstrap/permutation), refine estimates with covariate adjustment, and assess statistical power for future experiment design.

Business Context

A/B testing is the gold standard for measuring the causal impact of product changes. This project simulates a realistic scenario: a SaaS company redesigns its onboarding flow, expecting to lift conversion rates and reduce time-to-complete. The pipeline demonstrates how to move from raw experiment data to a defensible ship/no-ship decision with quantified uncertainty.

Objectives

  • Generate a realistic simulated dataset with known ground-truth effects for pipeline validation
  • Verify randomization integrity via Sample Ratio Mismatch (SRM) and covariate balance tests
  • Measure the primary conversion metric with confidence intervals and effect sizes
  • Check guardrail metrics (time-to-complete) to ensure no unintended regressions
  • Validate results with bootstrap CIs and permutation tests (distribution-free inference)
  • Refine treatment effect estimates using regression adjustment with robust standard errors
  • Calculate minimum detectable effect (MDE) and required sample sizes for future experiments

Dataset Overview

AttributeValue
TypeSimulated (known ground truth)
Sample Size10,000 users
VariantsControl / Treatment (50/50 split)
Primary MetricConversion rate (binary)
Secondary MetricTime-to-complete (continuous, minutes)
Covariatepre_sessions_7d (Poisson, λ=3)
Ground Truth+1.5pp conversion lift, −0.5 min time reduction

Tools & Packages

PackagePurpose
tidyverse / dplyrData manipulation, piping, summarization
ggplot2Publication-quality figures
inferTidy permutation and bootstrap framework
broomTidy model outputs (tidy, glance, augment)
sandwichHC3 robust standard errors
scalesFormatting (percent, comma)
janitorclean_names, data cleaning
readrCSV I/O

Key Metrics Defined

MetricDefinition
Conversion RateProportion of users who completed the target action (binary: 0/1)
Time-to-CompleteMinutes from onboarding start to completion (continuous)
Absolute LiftTreatment rate − Control rate (percentage points)
Relative Lift(Treatment rate / Control rate) − 1 (percent)
Cohen's hStandardized effect size for comparing two proportions
MDEMinimum detectable effect at 80% power and α=0.05
Experiment Design & Data Generation

Approach

Using simulated data with a known ground truth is the most rigorous way to validate an analysis pipeline. If the analysis recovers the planted 1.5pp conversion lift and 0.5-minute time reduction, the methodology is confirmed to work correctly. This is standard practice in experimentation platform development – teams at Microsoft and Netflix use simulation to verify their analysis code before deploying it on real experiments.

The simulation generates 10,000 users with 50/50 random assignment to control or treatment. Control baseline conversion is 10%, treatment adds a 1.5 percentage-point lift (11.5% true rate). Time-to-complete is normally distributed – control centered at 8.0 minutes, treatment at 7.5 minutes (0.5 min faster), both with SD=3. A pre-experiment covariate (pre_sessions_7d, Poisson λ=3) is included to enable regression adjustment (CUPED) in later scripts.

Key Code

# Simulation parameters
n_users <- 10000
baseline_conversion <- 0.10
treatment_lift <- 0.015  # +1.5 percentage points

df <- tibble(
  user_id = 1:n_users,
  variant = sample(c("control", "treatment"), size = n_users, replace = TRUE),
  pre_sessions_7d = rpois(n_users, lambda = 3)
) %>%
  mutate(
    conversion_prob = case_when(
      variant == "control"   ~ baseline_conversion,
      variant == "treatment" ~ baseline_conversion + treatment_lift
    ),
    converted = rbinom(n_users, 1, conversion_prob),
    time_to_complete = rnorm(n_users,
      mean = ifelse(variant == "treatment", 7.5, 8.0), sd = 3)
  )

Key Points

  • Reproducible via set.seed(123) – every run produces identical data
  • Ground truth is known: +1.5pp conversion lift, −0.5 min time-to-complete
  • Pre-experiment covariate (pre_sessions_7d) included for regression adjustment
  • Clean CSV output (data/ab_test_data.csv) feeds all downstream scripts
Quality Control & SRM Detection

Approach

Before analyzing any experiment results, the first step is verifying randomization integrity. Sample Ratio Mismatch (SRM) is the single most critical quality check – if the observed 50/50 split deviates significantly from expectation, something is wrong with assignment, logging, or data processing. SRM detection is standard practice at Microsoft, Booking.com, and other companies running experiments at scale.

The pipeline applies a chi-square goodness-of-fit test against the expected 50/50 proportions and a Welch two-sample t-test to verify that the pre-experiment covariate (pre_sessions_7d) is balanced across variants. If SRM is detected (p < 0.01), the pipeline halts – analyzing results from a compromised randomization is worse than analyzing no results at all.

Key Code

# SRM test (expected 50/50 split)
counts <- df %>% count(variant)
chisq <- chisq.test(x = counts$n, p = c(0.5, 0.5))

# Baseline covariate balance
tt <- t.test(pre_sessions_7d ~ variant, data = df)

# Fail loudly if SRM detected
if (chisq$p.value < 0.01) {
  stop("SRM detected – investigate randomization before proceeding.")
}

Results

Group Counts

VariantnExpected PropObserved Prop
Control5,0170.5000.502
Treatment4,9830.5000.498

SRM Test Result

Chi-square Statisticdfp-value
0.11610.734

Baseline Covariate Balance

VariantMean pre_sessions_7dSD
Control3.0061.725
Treatment2.9611.713

Welch t-test for covariate balance: p = 0.188

Key Findings

  • No SRM detected (chi-square p = 0.734, well above the 0.01 threshold) – randomization is intact
  • Baseline covariate pre_sessions_7d is balanced across variants (t-test p = 0.188) – no pre-existing group differences
  • Pipeline cleared to proceed with primary and guardrail metric analysis

Business Recommendations

  • Always run SRM before analyzing any experiment – it is the single most important integrity check
  • Automate SRM checks into experiment platforms to catch issues before analysts examine results
  • If SRM is detected, do NOT analyze results – investigate the root cause (logging bugs, bot traffic, assignment errors) first
Primary Metric – Conversion Rate

Approach

Conversion rate is the primary decision metric for this experiment. The analysis uses a two-proportion z-test (prop.test) comparing control vs. treatment conversion rates. This is the standard frequentist approach for binary outcome experiments.

The analysis reports absolute lift (percentage points), relative lift (%), a 95% confidence interval for the difference, and Cohen's h as a standardized effect size. The distinction between statistical significance and practical significance matters – the width of the confidence interval tells us about the range of plausible effects, which is more informative than a binary p-value threshold.

Key Code

# Two-proportion z-test
pt <- prop.test(
  x = c(x_control, x_treat),
  n = c(n_control, n_treat),
  correct = FALSE
)

# Effect size (Cohen's h)
cohens_h <- 2 * asin(sqrt(p_treat)) - 2 * asin(sqrt(p_control))

# Absolute and relative lift
abs_lift <- p_treat - p_control
rel_lift <- (p_treat / p_control) - 1

Results

Conversion Summary

VariantnConversionsRate
Control5,01751110.19%
Treatment4,98357411.52%

Test Results

Absolute Lift (pp)Relative Lift (%)95% CI Low95% CI Highp-valueCohen's h
+1.33+13.1%0.11pp2.55pp0.0320.043

Key Findings

  • Treatment conversion rate (11.52%) is 1.33pp higher than control (10.19%) – close to the planted 1.5pp effect
  • The 95% confidence interval [0.11pp, 2.55pp] excludes zero, confirming statistical significance at α=0.05
  • Relative lift of 13.1% over a 10% baseline – a meaningful improvement for SaaS onboarding
  • Cohen's h = 0.043 indicates a small but real effect size, consistent with typical product A/B tests

Business Recommendations

  • A 1.33pp lift on a 10% baseline represents a ~13% relative improvement – meaningful for SaaS onboarding funnels where even small gains compound across thousands of users
  • Always report confidence intervals alongside p-values – the range of plausible effects (0.11pp to 2.55pp) is more useful than binary significance
  • Estimate business impact: at 100K annual signups, a 1.33pp lift translates to ~1,330 additional conversions per year
Secondary Metric – Time-to-Complete Guardrail

Approach

Guardrail metrics ensure the treatment does not cause unintended harm. Even if conversion improves, if the onboarding process takes significantly longer, the user experience may suffer – leading to downstream churn or support costs that offset the conversion gain.

The analysis compares time-to-complete using three complementary methods: a Welch t-test (parametric), Wilcoxon rank-sum test (non-parametric), and bootstrap CI (distribution-free). Using multiple approaches validates robustness, especially when the underlying distribution may be skewed or heavy-tailed.

Key Code

# Parametric + nonparametric tests
tt <- t.test(time_to_complete ~ variant, data = df)
wt <- wilcox.test(time_to_complete ~ variant, data = df, exact = FALSE)

# Bootstrap CI for mean difference
boot_diffs <- replicate(2000, {
  boot_df <- df %>% group_by(variant) %>%
    slice_sample(prop = 1, replace = TRUE)
  mean(boot_df$time_to_complete[boot_df$variant == "treatment"]) -
    mean(boot_df$time_to_complete[boot_df$variant == "control"])
})
boot_ci <- quantile(boot_diffs, probs = c(0.025, 0.975))

Visualization

Box plot of onboarding time-to-complete by variant
Figure: Onboarding time-to-complete by variant. Treatment group completed onboarding faster on average.

Results

Time-to-Complete Summary

VariantnMean (min)Median (min)SDp90
Control5,0177.877.873.0311.80
Treatment4,9837.537.492.9711.40

Test Results

Mean Diff (min)Bootstrap CI LowBootstrap CI Hight-test p-valueWilcoxon p-value
−0.34−0.46−0.22< 0.001< 0.001

Key Findings

  • Treatment group completed onboarding 0.34 minutes faster on average – consistent with the planted 0.5-min effect (sampling variability accounts for the difference)
  • All three testing approaches (t-test, Wilcoxon, bootstrap) agree: the difference is highly significant (p < 0.001)
  • Bootstrap CI [−0.46, −0.22] is entirely below zero – the guardrail metric shows improvement, not regression
  • The guardrail is clear: the new onboarding flow is both more effective (higher conversion) and faster (lower time-to-complete)

Business Recommendations

  • Even a 0.34-minute reduction compounds across thousands of users – calculate total time saved per month to quantify UX improvement
  • Use multiple testing approaches when the metric distribution may be non-normal – agreement across methods strengthens confidence
  • Always check guardrail metrics before shipping – a conversion lift that degrades the user experience may not be worth it
Bootstrap & Permutation Inference

Approach

This section validates the primary conversion results using computation-based inference that does not rely on distributional assumptions. These methods are increasingly preferred in industry experimentation because they provide intuitive uncertainty quantification without requiring normality or large-sample approximations.

Bootstrap confidence intervals (B=4,000 resamples) resample within each group to build a distribution of the observed lift. This is the gold standard for uncertainty quantification in modern experimentation platforms – it directly answers "what range of lifts is consistent with the data?"

The permutation test (B=4,000 shuffles) breaks the variant-outcome relationship by randomly shuffling labels, building a null distribution. The p-value is the proportion of permuted differences as extreme as the observed difference. The analysis also demonstrates the infer package for a cleaner tidy workflow.

Key Code

# Bootstrap CI for absolute lift
boot_diffs <- replicate(4000, {
  p_c <- mean(sample(control_rows$converted_num, replace = TRUE))
  p_t <- mean(sample(treat_rows$converted_num, replace = TRUE))
  p_t - p_c
})
boot_ci <- quantile(boot_diffs, probs = c(0.025, 0.975))

# Permutation test
perm_diffs <- replicate(4000, {
  df_perm <- df %>% mutate(variant_perm = sample(variant))
  # ... compute diff under shuffled labels
})
perm_p_value <- mean(abs(perm_diffs) >= abs(obs_diff))

Visualization

Bootstrap distribution of absolute lift with observed value marked
Figure: Bootstrap distribution of the absolute conversion lift (B=4,000 resamples). Dashed line marks the observed lift.

Results

Bootstrap Results

Observed Lift (pp)95% CI Low (pp)95% CI High (pp)
+1.33+0.12+2.57

Permutation Results

Observed Lift (pp)Permutation p-value
+1.330.036

Method Comparison

Methodp-value
prop.test (z-test)0.032
Manual permutation (B=4,000)0.036
infer permutation (B=4,000)0.033

Key Findings

  • Bootstrap CI [+0.12pp, +2.57pp] closely aligns with the analytical CI from prop.test [+0.11pp, +2.55pp] – the methods converge
  • All three p-values (0.032, 0.036, 0.033) are consistent and significant at α=0.05 – strong evidence the result is not due to chance
  • The bootstrap distribution provides a direct visual of lift uncertainty – more intuitive for stakeholders than a p-value alone

Business Recommendations

  • Bootstrap CIs are increasingly preferred in industry – they are intuitive, assumption-free, and work well with any metric distribution
  • Use the bootstrap distribution as a stakeholder visual: "here is what the lift looks like across 4,000 resamples of the data"
  • Permutation tests directly answer "could this result be due to chance?" – a compelling framing for non-technical decision-makers
Regression-Adjusted Treatment Effects

Approach

Regression adjustment – known as CUPED (Controlled-experiment Using Pre-Experiment Data) in industry – uses pre-experiment covariates to reduce residual variance, producing tighter confidence intervals without requiring additional sample size. This is standard practice at Microsoft, Netflix, and Uber for improving experiment sensitivity.

For the primary metric (conversion), the analysis fits a logistic regression with variant and log1p(pre_sessions_7d) as predictors, then computes an adjusted risk difference via marginal means (G-computation). HC3 robust standard errors from the sandwich package protect against heteroskedasticity. A bootstrap CI (B=1,000) provides distribution-free uncertainty for the adjusted risk difference.

For the guardrail metric (time-to-complete), an OLS regression with the same covariates is used. The treatment coefficient directly estimates the adjusted mean difference in minutes, again with HC3 robust standard errors.

Key Code

# Logistic regression for conversion
m_conv <- glm(converted ~ variant + log1p_pre_sessions_7d,
              data = df, family = binomial())
vc_conv <- sandwich::vcovHC(m_conv, type = "HC3")

# Adjusted risk difference via marginal means
p_control <- predict(m_conv, newdata = df %>% mutate(variant = "control"),
                     type = "response")
p_treat   <- predict(m_conv, newdata = df %>% mutate(variant = "treatment"),
                     type = "response")
adj_rd <- mean(p_treat - p_control)  # Adjusted lift in probability

# OLS for guardrail metric
m_time <- lm(time_to_complete ~ variant + log1p_pre_sessions_7d, data = df)
vc_time <- sandwich::vcovHC(m_time, type = "HC3")

Results

Logistic Regression Coefficients (Conversion)

TermLog-OddsOdds RatioOR 95% CIp-value
(Intercept)−2.1830.113[0.093, 0.137]< 0.001
variant: treatment+0.1381.148[1.012, 1.303]0.032
log1p(pre_sessions_7d)+0.0051.005[0.879, 1.148]0.943

Standard errors: HC3 robust via sandwich package.

Adjusted Risk Difference (Conversion)

Adjusted Lift (pp)Bootstrap CI Low (pp)Bootstrap CI High (pp)
+1.33+0.25+2.57

OLS Regression Coefficients (Time-to-Complete)

TermEstimate (min)95% CIp-value
(Intercept)7.942[7.765, 8.119]< 0.001
variant: treatment−0.341[−0.459, −0.224]< 0.001
log1p(pre_sessions_7d)−0.054[−0.175, 0.067]0.382

Standard errors: HC3 robust via sandwich package.

Key Findings

  • Adjusted conversion lift (+1.33pp) matches the unadjusted estimate closely – the covariate adjustment confirms rather than changes the conclusion
  • The adjusted bootstrap CI [+0.25pp, +2.57pp] is slightly tighter than the unadjusted CI, demonstrating the variance-reduction benefit of CUPED
  • The pre-experiment covariate (pre_sessions_7d) is not a significant predictor of either outcome (p > 0.38) – expected in a well-randomized simulation, but the adjustment still provides marginal precision gains
  • Adjusted time-to-complete difference (−0.341 min) is consistent with the unadjusted estimate, with robust standard errors protecting against heteroskedasticity

Business Recommendations

  • Regression adjustment should be standard practice – it provides free precision (tighter CIs) without requiring more users or longer experiments
  • Always use robust standard errors (HC3) for experiment analysis – they protect against model misspecification and heteroskedasticity
  • Report the adjusted risk difference in percentage points – stakeholders understand "1.33pp lift" better than "odds ratio of 1.148"
Power Analysis & MDE

Approach

Power analysis answers a critical pre-experiment question: "given our sample size, what is the smallest effect we can reliably detect?" This minimum detectable effect (MDE) determines whether an experiment is worth running. If the MDE is larger than the expected effect, the experiment is underpowered and likely to produce inconclusive results.

The analysis calculates the MDE at 80% power for the current sample size, computes required sample sizes for a range of target lifts (0.5pp, 1.0pp, 1.5pp, 2.0pp), and generates a power curve showing how statistical power changes across effect sizes.

Key Code

# MDE given current sample size
mde_abs <- mde_solver(p0 = baseline_rate, n = n_per_group,
                       alpha = 0.05, power_target = 0.80)

# Required sample sizes for target lifts
power.prop.test(power = 0.80, p1 = baseline_rate,
                p2 = baseline_rate + target_lift, sig.level = 0.05)

# Power curve across effect sizes
power_curve <- tibble(
  lift_pp = seq(0, 3, by = 0.1),
  power = sapply(lift_pp / 100, function(d) {
    power.prop.test(n = n_per_group, p1 = p0,
                    p2 = p0 + d, sig.level = 0.05)$power
  })
)

Visualization

Power curve showing statistical power vs effect size
Figure: Statistical power by effect size. Red dashed line = 80% power target. Green dotted line = MDE at current sample size.

Results

MDE Summary

Baseline Raten per GroupMDE at 80% Power (pp)
10.19%4,9831.77pp

Sample Size Requirements

Target Lift (pp)Required n per GroupTotal n Required
0.558,686117,372
1.014,98129,962
1.56,79413,588
2.03,8987,796

Key Findings

  • With ~5,000 users per group and a 10.19% baseline, the MDE at 80% power is 1.77pp – the experiment is slightly underpowered for the observed 1.33pp lift (power ~55% at that effect size)
  • The planted 1.5pp effect falls just below the MDE threshold, which explains why the observed p-value (0.032) is significant but not overwhelmingly so
  • Detecting a 0.5pp lift would require ~58,700 per group (roughly 12x the current sample) – small effects demand large experiments
  • The power curve shows diminishing returns: moving from 80% to 95% power roughly doubles the required sample size

Business Recommendations

  • Always calculate MDE before launching an experiment – if the MDE is larger than the expected effect, the experiment is underpowered and likely to waste resources
  • Use power analysis to negotiate experiment duration: "we need X weeks at current traffic to detect a Y% lift with 80% power"
  • For small expected effects, use CUPED/regression adjustment to lower the effective MDE without increasing sample size
R Skills & Techniques Demonstrated

R Packages Used

PackagePurpose
tidyverseData manipulation, piping, ggplot2
dplyrfilter, mutate, group_by, summarise
janitorclean_names, data cleaning
broomTidy model outputs
scalesFormatting (percent, dollar)
inferTidy permutation/bootstrap framework
sandwichRobust (HC3) standard errors
readrCSV I/O
ggplot2Publication-quality figures

Statistical Methods Applied

MethodScriptPurpose
Chi-square goodness-of-fit01SRM detection
Two-sample t-test01, 03Covariate balance, time comparison
Two-proportion z-test02Primary metric comparison
Cohen's h02Standardized effect size
Wilcoxon rank-sum03Non-parametric time comparison
Bootstrap CI03, 04, 05Distribution-free confidence intervals
Permutation test04Null hypothesis simulation
Logistic regression (GLM)05Binary outcome modeling
OLS regression05Continuous outcome modeling
HC3 robust SEs05Heteroskedasticity-consistent inference
Marginal means / Adjusted RD05Causal effect estimation
Power analysis06Experiment design / MDE

Programming Practices

The pipeline follows a modular architecture: each of the 7 scripts (00–06) reads from data/, writes results to tables/ and visualizations to figures/. Defensive coding is used throughout – stopifnot() for column checks, explicit factor levels, and SRM-based early termination. All results are reproducible via set.seed(123), and the code follows a consistent tidyverse style with clear variable naming.

Conclusion & Project Summary

Pipeline Summary

Starting with data generation, the pipeline verified randomization integrity via SRM (chi-square p = 0.734), measured the primary conversion lift (+1.33pp, p = 0.032), confirmed the guardrail metric showed improvement (−0.34 min, p < 0.001), validated results with distribution-free inference (bootstrap CI [+0.12pp, +2.57pp], permutation p = 0.036), refined estimates with covariate adjustment (+1.33pp adjusted, bootstrap CI [+0.25pp, +2.57pp]), and assessed statistical power (MDE = 1.77pp at 80% power with current sample size).

Key Takeaways

  • Treatment increased conversion by +1.33pp (absolute) – a ~13% relative lift over the 10.19% baseline
  • Guardrail metric (time-to-complete) improved by 0.34 minutes – no regressions detected
  • All inference methods (prop.test, bootstrap, permutation) produced consistent results (p-values: 0.032, 0.036, 0.033)
  • Regression adjustment confirmed the unadjusted estimate and tightened the confidence interval
  • The experiment was slightly underpowered (MDE = 1.77pp vs. observed 1.33pp) but still detected a significant effect
  • The pipeline follows industry best practices: SRM → primary metric → guardrails → validation → regression adjustment → power

Limitations

The data is simulated – real experiments involve messier realities including non-compliance, network effects, and novelty/primacy bias. The analysis focuses on a single binary primary metric; real experiments often track multiple correlated metrics requiring multiple comparison correction (e.g., Bonferroni, Benjamini-Hochberg). Future enhancements could include sequential testing for early stopping, Bayesian analysis with informative priors, and heterogeneous treatment effect estimation (CATE) to identify which user segments benefit most.