Experiment Design & Causal Inference
Designing rigorous experiments and drawing valid causal conclusions. For a Senior Manager at Capital One, this means knowing when A/B testing is feasible and when to use quasi-experimental methods when randomization is impossible.
Key Takeaways
- Calculate sample size before running — never after seeing the data
- The MDE (Minimum Detectable Effect) is a business decision: what is the smallest effect that would change a decision?
- SUTVA: my treatment must not affect your outcome. Network effects (referrals, social feeds) violate this.
- Novelty effect: run experiments through at least one full business cycle before concluding
- Multiple testing: 20 tests at α=0.05 → 1 expected false positive. Use Bonferroni (conservative) or BH-FDR (less conservative).
- DiD (Difference-in-Differences) is the most practical causal inference method when randomization is impossible
1. A/B Test Design
Stating Hypotheses
- Null Hypothesis (H₀): No difference between control and treatment (effect = 0)
- Alternative Hypothesis (H₁): A meaningful difference exists
- State the MDE before any data collection — what is the smallest effect that would change a business decision?
Sample Size Formula
For comparing two means with equal group sizes:
$$n = \frac{(z_{\alpha/2} + z_{\beta})^2 \cdot 2\sigma^2}{\delta^2}$$Where $z_{\alpha/2} = 1.96$ for $\alpha = 0.05$ (two-tailed), $z_\beta = 0.84$ for 80% power, $\sigma^2$ is the metric variance, and $\delta$ is the MDE.
from scipy import stats
import numpy as np
def sample_size_two_proportion(p1, p2, alpha=0.05, power=0.80):
"""Sample size per group for a two-proportion z-test."""
z_alpha = stats.norm.ppf(1 - alpha / 2)
z_beta = stats.norm.ppf(power)
p_bar = (p1 + p2) / 2
num = (z_alpha * np.sqrt(2 * p_bar * (1 - p_bar)) +
z_beta * np.sqrt(p1*(1-p1) + p2*(1-p2))) ** 2
return int(np.ceil(num / (p1 - p2) ** 2))
# Capital One example: baseline conversion 5%, want to detect lift to 6%
n = sample_size_two_proportion(0.05, 0.06)
print(f"Required n per group: {n}") # ~4,900
# Smaller MDE = much larger sample needed
n_small = sample_size_two_proportion(0.03, 0.035)
print(f"Detecting 0.5pp lift from 3%: {n_small} per group") # ~17,000–20,000
Choosing the Right Test
| Metric Type | Test |
|---|---|
| Proportions (click rate, conversion) | Two-proportion z-test |
| Continuous means (revenue, time on page) | Two-sample t-test |
| Counts or categorical | Chi-square |
| Multiple groups (3+) | ANOVA (then post-hoc) |
2. Multiple Testing
Running $k$ tests at $\alpha = 0.05$ produces $k \times 0.05$ expected false positives. With 20 metrics, you expect 1 false positive even if nothing changed.
| Correction | Method | Use When |
|---|---|---|
| Bonferroni | $\alpha_{adj} = \alpha / k$ | Conservative; false positives are very costly |
| Benjamini-Hochberg (FDR) | Controls false discovery rate | Less conservative; good for exploratory analysis |
from statsmodels.stats.multitest import multipletests
p_values = [0.01, 0.04, 0.03, 0.20, 0.50]
# Bonferroni
reject_bf, p_adj_bf, _, _ = multipletests(p_values, method='bonferroni')
# Benjamini-Hochberg FDR
reject_bh, p_adj_bh, _, _ = multipletests(p_values, method='fdr_bh')
for i, (orig, bf, bh) in enumerate(zip(p_values, p_adj_bf, p_adj_bh)):
print(f"p={orig:.2f} Bonf={bf:.3f} BH={bh:.3f}")
3. Common Experiment Pitfalls
| Pitfall | Description | Mitigation |
|---|---|---|
| Novelty effect | Behavior changes just because something is new | Run ≥2 business cycles; segment new vs. returning users |
| SUTVA violation | Treatment of user A affects user B (referrals, shared network) | Cluster-randomize by geography or household |
| Simpson's Paradox | Aggregate trend reverses within subgroups | Always check segmented results |
| Peeking | Stopping early when p < 0.05 inflates false positive rate | Pre-register stopping rules; use sequential testing |
| Imbalanced randomization | Unequal group sizes reduce power | Stratify on key covariates before randomization |
4. Causal Inference (When A/B Isn't Possible)
Difference-in-Differences (DiD)
Compares the change over time in the treatment group to the change over time in the control group:
$$\hat{\delta}_{DiD} = (\bar{Y}_{T,\text{post}} - \bar{Y}_{T,\text{pre}}) - (\bar{Y}_{C,\text{post}} - \bar{Y}_{C,\text{pre}})$$Parallel trends assumption: Without treatment, both groups would have trended identically. You cannot prove this directly — check that pre-treatment trends were parallel using historical data.
Capital One example: A new collections policy rolls out in 3 states. Compare default rate changes in those states vs. similar untreated states over the same period.
Regression Discontinuity Design (RDD)
Exploits a hard cutoff that determines treatment assignment. Units just above and just below the cutoff are near-random in their characteristics.
Capital One example: Customers with credit score ≥ 700 receive a better offer. Compare outcomes for [690, 700) vs. [700, 710) — the small band around 700 approximates random assignment.
Instrumental Variables (IV)
Uses a variable (instrument) that affects treatment assignment but has no direct effect on the outcome. Valid instruments are hard to find in practice; more common in academic research.
import pandas as pd
import numpy as np
import statsmodels.formula.api as smf
# DiD with OLS regression
# Equation: Y = alpha + beta1*Treated + beta2*Post + delta*(Treated*Post) + error
# delta is the DiD estimator
df['treated_x_post'] = df['treated'] * df['post']
model = smf.ols('outcome ~ treated + post + treated_x_post', data=df).fit()
did_estimate = model.params['treated_x_post']
print(f"DiD estimate: {did_estimate:.4f} (p={model.pvalues['treated_x_post']:.4f})")
Practice Questions
Q1: A colleague says "we got p=0.04 so it's significant — ship it." What questions do you ask before deciding?
- Was the hypothesis pre-registered, or did you look at the data first? Post-hoc hypothesis selection inflates false positive risk dramatically.
- How many metrics did you test? Multiple comparisons inflate false positives — 20 metrics at α=0.05 expects 1 false positive even if nothing changed.
- What is the effect size? Is the difference practically meaningful, or just statistically significant? A 0.01% lift with p=0.04 may not justify the engineering cost.
- Did the experiment run long enough to clear novelty effects and capture a full business cycle (e.g., a full week including weekends, or a full month)?
- Are there SUTVA violations? Did treatment group members interact with control group members (referrals, shared households, social feeds)?
Q2: You want to test a new credit card offer page. Baseline application rate is 3%. You want to detect a 0.5pp lift (to 3.5%) with 80% power and α=0.05. Roughly how many users per group?
Using the two-proportion z-test formula with $p_1 = 0.03$, $p_2 = 0.035$, $z_{\alpha/2} = 1.96$, $z_\beta = 0.84$:
from scipy import stats
import numpy as np
p1, p2 = 0.03, 0.035
z_alpha = stats.norm.ppf(0.975) # 1.96
z_beta = stats.norm.ppf(0.80) # 0.84
p_bar = (p1 + p2) / 2
num = (z_alpha * np.sqrt(2 * p_bar * (1 - p_bar)) +
z_beta * np.sqrt(p1*(1-p1) + p2*(1-p2))) ** 2
n = int(np.ceil(num / (p2 - p1)**2))
print(f"n per group: {n}") # ~17,000–20,000
The small MDE (0.5pp absolute) requires roughly 17,000–20,000 users per group. This is a key insight: small effects require large samples. When traffic is limited, you may need to either widen the MDE (accept only detecting larger lifts) or extend the experiment duration.
Q3: Explain the parallel trends assumption in Difference-in-Differences. How do you test it?
The parallel trends assumption states that, in the absence of treatment, the treatment and control groups would have followed the same trend over time. The DiD estimator is only valid as a causal estimate if this assumption holds.
You cannot directly test it (the counterfactual does not exist), but you can check its plausibility:
- Plot pre-treatment time series for both groups. If their trends were parallel before treatment began, the assumption is more credible.
- Placebo test: Apply the DiD estimator using only pre-treatment periods. Assign a fake "treatment" date in the middle of the pre-period. If DiD ≈ 0, that supports parallel trends.
- Covariate balance check: Verify that treatment and control groups are similar on observable characteristics before the intervention.
Q4: What is the "peeking problem" in A/B testing? How do sequential testing methods address it?
Peeking means checking the p-value repeatedly during the experiment and stopping as soon as p < 0.05. This inflates the false positive rate far above α, because each interim look is an additional opportunity to observe a false positive by chance.
Simulation: with a fixed α = 0.05 and daily peeking over 4 weeks, the true false positive rate can exceed 20–30%.
Sequential testing solutions:
- O'Brien-Fleming alpha spending: Distributes the total α budget across planned interim looks — earlier looks require a more extreme p-value to reject, preserving the overall Type I error.
- Always-valid inference (anytime-valid p-values): Constructs confidence sequences that are valid at any stopping time, making continuous monitoring mathematically sound.
- Pre-registered stopping rules: Decide before launch exactly when and how many interim analyses will occur. Ad hoc stopping after peeking is not correctable post-hoc.
Action Items
- Calculate the required sample size for a conversion rate test from 3.0% to 3.5% (α=0.05, 80% power) using the formula and verify with Python.
- Explain the parallel trends assumption of DiD in plain English to a non-technical stakeholder in two sentences.
- Describe a Capital One scenario where SUTVA would be violated (e.g., referral program) and explain the cluster-randomization fix.
- Sketch a DiD analysis for a new credit limit policy rolled out to a subset of customers — write the regression equation and interpret the interaction coefficient.