Skip to content

Sensitivity Analysis

Quantify how robust your conclusion is to unmeasured confounding.

Methods Overview

Method Treatment Type Output Intuition
Rosenbaum bounds Binary Critical Γ "How much hidden bias needed to flip result?"
Cinelli-Hazlett Continuous Robustness Value (RV) "Min confounding strength to change significance"
E-value Any (risk ratio) E-value, E-value CI "Min risk ratio of confounder to explain away effect"
TIPS curves Continuous Contour plot "Visualize estimate across confounding space"

Rosenbaum Bounds

For matched/stratified binary treatment studies.

from causal_toolkit.analysis import SensitivityAnalyzer

analyzer = SensitivityAnalyzer(model)
result = analyzer.rosenbaum_bounds(estimate, gamma_range=(1.0, 3.0))

print(f"Critical Γ = {result.gamma:.2f}")
# Γ = 1.0: no hidden bias
# Γ = 1.5: moderate hidden bias
# Γ = 2.0: strong hidden bias

if result.conclusion_reversed:
    print("⚠ Conclusion flips at Γ = {:.2f}".format(result.gamma))

Interpretation: Γ = 1.8 means an unobserved confounder that makes treatment 1.8x more likely for similar units could explain away the effect.

Cinelli-Hazlett Robustness Value

For linear models with continuous treatment.

result = analyzer.cinelli_hazlett(
    estimate,
    benchmark_covariate="income"  # Compare to observed confounder
)

print(f"RV = {result.robustness_value:.4f}")
print(f"Benchmark RV (income) = {result.benchmark_r2_yz:.4f}")

if result.conclusion_reversed:
    print("⚠ Benchmark covariate explains away effect")

Interpretation: RV = 0.12 means a confounder explaining 12% of residual variance in both treatment and outcome could change significance.

E-Value

From VanderWeele & Ding (2017). For risk ratios / odds ratios.

result = analyzer.e_value(estimate)

print(f"E-value = {result.e_value:.2f}")
print(f"E-value (CI) = {result.e_value_ci:.2f}")

# Rule of thumb:
# E-value > 2: moderately robust
# E-value > 5: highly robust
# E-value < 1.25: fragile

Interpretation: An unmeasured confounder would need a risk ratio of X with both treatment AND outcome to explain away the effect.

TIPS Curves

Visualize how estimate changes across confounding space.

data = analyzer.tip_curve(
    estimate,
    r2_yz_range=(0, 0.3),
    r2_zd_range=(0, 0.3),
    n_points=50
)

# data contains:
# - r2_yz: outcome confounding strength
# - r2_zd: treatment confounding strength
# - adjusted_estimates: 2D array of estimates
# - significant: 2D boolean array for significance

Full Suite

from causal_toolkit.analysis import run_sensitivity_suite

analyzer = run_sensitivity_suite(
    estimate=estimate,
    model=model,
    benchmark_covariates=["income", "education"]
)

print(analyzer.summarize())

Output:

Sensitivity Analysis Summary
========================================
Rosenbaum bounds: Γ=1.84, conclusion_reversed=True
Cinelli-Hazlett: RV=0.12, R²_yz=0.15, R²_zd=0.08
E-value: 2.45 (CI: 1.67)
  Benchmark 'income': R²_yz=0.08, R²_zd=0.05
  Benchmark 'education': R²_yz=0.03, R²_zd=0.02

Choosing a Method

Scenario Recommended
Matched/stratified binary T Rosenbaum
Linear regression, continuous T Cinelli-Hazlett
Risk ratios / case-control E-value
Want visual exploration TIPS curves
Compare to observed confounders Cinelli-Hazlett with benchmarks
Full robustness report run_sensitivity_suite()

Reporting Template

"The estimated ATE was X (95% CI: [L, U]). Sensitivity analysis using Cinelli-Hazlett yielded a Robustness Value of RV=0.15, meaning an unobserved confounder explaining at least 15% of residual variance in both treatment and outcome would be needed to reverse the conclusion. The E-value for the point estimate was 2.3 (CI: 1.6), indicating moderate robustness. Results were compared against observed confounders 'income' (RV=0.08) and 'education' (RV=0.03), neither of which individually explain the effect."