Causal Inference Treatment Effect Estimation Confounding
# Causal Inference: Treatment Effect Estimation & Confounding
## Introduction & Motivation
Causal inference estimates effect of intervention (treatment) on outcome. Observational data confounded; confounder C influences both T and Y. Propensity score: P(T|C) balances treated/control. Instrumental variables handle unmeasured confounding. Critical for policy decisions, A/B testing, medical trials.
Motivation: Correlation ≠ causation. Confounding biases naive estimators. Causal methods isolate treatment effect.
Applications: Policy evaluation, personalized medicine, marketing effectiveness, economic analysis.
---
## Core Concepts & Theory
### Confounder
Variable affecting both treatment and outcome; creates spurious correlation.
### Propensity Score
P(T=1|C). Match on propensity score to balance confounders.
### Average Treatment Effect (ATE)
E[Y(1) - Y(0)]; expected effect if everyone treated vs. untreated.
---
## Mathematical Formulation
ATE via stratification:
$$ ext{ATE} = \sum_c P(C=c)[E[Y|T=1,C=c] - E[Y|T=0,C=c]]$$
Propensity score matching:
$$E[Y(1) - Y(0)] \approx E[Y|T=1, P(T|C)] - E[Y|T=0, P(T|C)]$$
Doubly robust estimation:
$$\hat{ ext{ATE}} = \mathbb{E}[\hat{m}_1(X) - \hat{m}_0(X) + \frac{T(Y - \hat{m}_1(X))}{\hat{e}(X)} - \frac{(1-T)(Y - \hat{m}_0(X))}{1 - \hat{e}(X)}]$$
---
## Advanced Theory & Extensions
### Heterogeneous Treatment Effects (HTE)
Causal forest: treatment effect varies by subgroup.
### Instrumental Variables
Confounder unobserved; use instrument Z (affects T, not Y directly).
### Difference-in-Differences
Temporal: E[Y_post - Y_pre | T=1] - E[Y_post - Y_pre | T=0].
---
## Computational Considerations
Propensity scoring: O(n) logistic regression + O(n) matching.
Causal forests: O(n log n) tree training.
Cross-fitting: O(folds × estimation time).
---
## Practical Implementation Strategies
### Overlap Check
Ensure 0 < P(T|C) < 1; common support for treated/control.
### Balance Assessment
Check covariate balance after matching/weighting.
### Sensitivity Analysis
Assess robustness to unmeasured confounding.
---
## Benchmark Datasets & Evaluation
IHDP: Infant health; 747 samples, 25 covariates.
Jobs: Job training program evaluation.
Synthetic: Known ground truth treatment effect.
Metrics: ATE bias, variance, MSE vs. true effect.
---
## Key Challenges & Limitations
### Unmeasured Confounding
Confounders unobserved; sensitivity analysis required.
### High-Dimensional Confounders
Many covariates; model selection crucial.
### Overlap Violation
Some T=1, C values have no T=0 match (or vice versa).
---
## Hyperparameter Tuning
Caliper (matching distance): 0.01-0.1 of propensity range.
Number of matches: 1-5 per unit.
Regularization (doubly robust): Ridge parameter λ.
---
## Real-World Applications & Case Studies
A/B Testing: Estimate causal lift via propensity weighting.
Medical Trials: Adjust for baseline confounders via AIPW.
Economics: Job training effect on earnings via IV.
---
## Integration with Other Methods
Causal Inference + ML → targeted maximum likelihood, TMLE.
Causal Inference + RL → policy learning with causal discovery.
---
## Summary & Key Takeaways
Causal inference estimates treatment effects while accounting for confounding via propensity scores, matching, weighting, or instrumental variables.
Principles:
1. Identify confounders; account for selection bias.
2. Propensity score balances treated/control on observables.
3. ATE = average of stratum-specific treatment effects.
4. Doubly robust combines outcome + propensity models.
5. Overlap and sensitivity analysis assess validity.
---
---
## Appendix: Practical Labs
### Lab 1: Propensity Score Estimation
import numpy as np
from sklearn.linear_model import LogisticRegression
def estimate_propensity_scores(X, T):
"""Estimate P(T=1|X)"""
model = LogisticRegression(max_iter=200)
model.fit(X, T)
propensity_scores = model.predict_proba(X)[:, 1]
return propensity_scores
# Data
np.random.seed(42)
n = 200
X = np.random.randn(n, 5)
T = (0.5 + X[:, 0] + np.random.randn(n) * 0.5 > 0).astype(int)
ps = estimate_propensity_scores(X, T)
print(f"Propensity score range: [{ps.min():.3f}, {ps.max():.3f}]")
assert 0 <= ps.min() and ps.max() <= 1, "Scores should be in [0,1]"
assert (ps > 0).sum() > 0, "Should have some positive"
print("✓ Propensity score estimation working")
if __name__ == "__main__":
print("Lab 1: Propensity - PASSED")### Lab 2: ATE via Stratification
import numpy as np
def ate_via_stratification(Y, T, strata, n_strata=5):
"""Estimate ATE by stratifying on confounder"""
ate = 0
for s in range(n_strata):
mask = strata == s
Y_s = Y[mask]
T_s = T[mask]
if (T_s == 1).sum() > 0 and (T_s == 0).sum() > 0:
ate_s = Y_s[T_s == 1].mean() - Y_s[T_s == 0].mean()
weight = mask.sum() / len(Y)
ate += weight * ate_s
return ate
# Data
np.random.seed(42)
n = 300
C = np.random.randint(0, 5, n) # Confounder
T = (np.random.rand(n) > 0.4 * (C + 1) / 5).astype(int)
Y = T * 2 + C * 0.5 + np.random.randn(n)
ate = ate_via_stratification(Y, T, C, n_strata=5)
print(f"Estimated ATE: {ate:.3f}")
assert isinstance(ate, (int, float, np.number)), "Should be scalar"
print("✓ ATE stratification working")
if __name__ == "__main__":
print("Lab 2: ATE - PASSED")### Lab 3: Propensity Score Weighting
import numpy as np
def ate_via_weighting(Y, T, propensity_scores):
"""Estimate ATE via inverse propensity weighting (IPW)"""
ps = propensity_scores
# IPW estimator
ipw_t1 = (T * Y / ps).sum() / (T / ps).sum()
ipw_t0 = ((1 - T) * Y / (1 - ps)).sum() / ((1 - T) / (1 - ps)).sum()
ate = ipw_t1 - ipw_t0
return ate
# Data
np.random.seed(42)
n = 200
X = np.random.randn(n, 3)
T = (X[:, 0] > 0).astype(int)
ps = 1 / (1 + np.exp(-X[:, 0])) # True propensity
Y = T * 3 + np.random.randn(n)
ate_weighted = ate_via_weighting(Y, T, ps)
print(f"IPW ATE: {ate_weighted:.3f}")
assert isinstance(ate_weighted, (int, float, np.number)), "Should be scalar"
print("✓ IPW weighting working")
if __name__ == "__main__":
print("Lab 3: IPW - PASSED")### Lab 4: Overlap Check
import numpy as np
import matplotlib.pyplot as plt
def check_overlap(T, propensity_scores, caliper=0.1):
"""Check common support: overlap of PS distributions"""
ps_treated = propensity_scores[T == 1]
ps_control = propensity_scores[T == 0]
# Common support range
min_ps_treated = ps_treated.min()
max_ps_treated = ps_treated.max()
min_ps_control = ps_control.min()
max_ps_control = ps_control.max()
overlap_min = max(min_ps_treated, min_ps_control)
overlap_max = min(max_ps_treated, max_ps_control)
n_treated_overlap = (ps_treated >= overlap_min) & (ps_treated <= overlap_max)
n_control_overlap = (ps_control >= overlap_min) & (ps_control <= overlap_max)
print(f"Treated with overlap: {n_treated_overlap.sum()}/{len(ps_treated)}")
print(f"Control with overlap: {n_control_overlap.sum()}/{len(ps_control)}")
return overlap_min, overlap_max
# Data
np.random.seed(42)
T = np.concatenate([np.ones(100), np.zeros(100)])
ps = np.concatenate([np.random.beta(5, 2, 100), np.random.beta(2, 5, 100)])
overlap_min, overlap_max = check_overlap(T, ps)
print(f"Common support: [{overlap_min:.3f}, {overlap_max:.3f}]")
assert overlap_min < overlap_max, "Should have overlap"
print("✓ Overlap check working")
if __name__ == "__main__":
print("Lab 4: Overlap - PASSED")