This vignette demonstrates the sensitivity analysis tools in
seine, using the elec_1968 data on
county-level voting in Southern states in the 1968 U.S. presidential
election. Sensitivity analysis is essential for ecological inference
(EI) because all EI methods rely on an untestable identifying
assumption—here, Conditional Average Representativeness, or CAR—that is
unlikely to hold exactly in practice. The tools in
seine are based on a nonparametric sensitivity
framework developed by Chernozhukov et al. (2024).
We begin by loading the 1968 election data, and defining an
ei_spec object that records the outcome, predictor,
covariate, and total-count columns, following the setup from
vignette("seine"). We use a BART basis expansion for
nonparametric covariate adjustment, which we strongly recommend to avoid
dependence on linearity assumptions.
library(seine)
data(elec_1968)
spec = ei_spec(
elec_1968,
predictors = vap_white:vap_other,
outcome = pres_dem_hum:pres_abs,
total = pres_total,
covariates = c(state, pop_city:pop_rural, farm:educ_coll, inc_00_03k:inc_25_99k),
preproc = function(x) {
x = model.matrix(~ 0 + ., x) # convert factors to dummies
bases::b_bart(x, trees = 250)
}
)We fit the regression model with ei_ridge() and the
Riesz representer with ei_riesz(), then combine them with
ei_est() to estimate vote choice by race using double
machine learning (DML). We focus on the contrast between White
and Black voters, which is a direct measure of racially polarized
voting. See the main vignette (vignette("seine")) for a
full walkthrough of this estimation workflow.
m = ei_ridge(spec)
rr = ei_riesz(spec, penalty = m$penalty)
est = ei_est(m, rr, spec, contrast = list(predictor = c(1, -1, 0)), conf_level = FALSE)
print(est)
#> # A tibble: 4 × 4
#> predictor outcome estimate std.error
#> <chr> <chr> <dbl> <dbl>
#> 1 vap_white - vap_black pres_dem_hum -0.365 0.0487
#> 2 vap_white - vap_black pres_rep_nix 0.504 0.0490
#> 3 vap_white - vap_black pres_ind_wal -0.140 0.0396
#> 4 vap_white - vap_black pres_abs 0.000966 0.000993While we normally do not recommend setting
conf_level = FALSE to suppress confidence intervals, here
we do, so that the output can more easily fit on the screen. If
confidence intervals are present in est, they will be
adjusted by the sensitivity analysis below.
The estimates above rest on the CAR assumption: that, conditional on the observed covariates, individual vote choice is independent of the individual’s race. In practice, this assumption is unlikely to hold exactly, as there may be unobserved confounders. seine provides a number of tools to evaluate how sensitive the results are to violations of that assumption.
The sensitivity framework considers the relationship between an
unobserved confounding variable and (i) the outcome and (ii) the Riesz
representer, measured in terms of partial \(R^2\) values (c_outcome and
c_predictor, respectively). Stronger relationships indicate
more confounding and therefore more potential bias in the original
estimates.
The ei_sens() function provides a simple interface to
this framework. Users provide values for the sensitivity parameters, and
a bound on the absolute bias is returned. In the following example, we
investigate the effect of an omitted confounder that explains 50% of the
residual variation in the outcome and 20% of the variation in the Riesz
representer.
ei_sens(est, c_outcome = 0.5, c_predictor = 0.2)
#> # A tibble: 4 × 7
#> predictor outcome estimate std.error c_outcome c_predictor bias_bound
#> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 vap_white - vap_b… pres_d… -3.65e-1 0.0487 0.5 0.2 0.313
#> 2 vap_white - vap_b… pres_r… 5.04e-1 0.0490 0.5 0.2 0.373
#> 3 vap_white - vap_b… pres_i… -1.40e-1 0.0396 0.5 0.2 0.412
#> 4 vap_white - vap_b… pres_a… 9.66e-4 0.000993 0.5 0.2 0.0138We can also work backwards and ask what one of the sensitivity
parameters would have to be in order to produce a certain amount of
bias. For example, if we assumed a worst-case scenario where the
confounder explains the entire outcome (c_outcome = 1), we
can ask how strongly that confounder would need to be related to the
Riesz representer to produce a bias of up to 5pp.
ei_sens(est, c_outcome = 1, bias_bound = 0.05)
#> # A tibble: 4 × 7
#> predictor outcome estimate std.error c_outcome c_predictor bias_bound
#> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 vap_white - vap_b… pres_d… -3.65e-1 0.0487 1 0.00318 0.05
#> 2 vap_white - vap_b… pres_r… 5.04e-1 0.0490 1 0.00225 0.05
#> 3 vap_white - vap_b… pres_i… -1.40e-1 0.0396 1 0.00184 0.05
#> 4 vap_white - vap_b… pres_a… 9.66e-4 0.000993 1 0.621 0.05For all of the outcomes except pres_abs, whose estimate
is much smaller than 0.05, the answer is not very much!
The c_outcome parameter is relatively easy to
understand, but c_predictor is more difficult to interpret
(though see the methodology paper for more discussion). To help
understand plausible values of these parameters, we can conduct a
benchmarking analysis that treats each of our
observed covariates in turn as a hypothetical
unobserved confounder, and calculates the implied values of the
sensitivity parameters.
bench = ei_bench(spec, contrast = list(predictor = c(1, -1, 0)))
#> ⠙ ETA: 1m Benchmarking state [1/13]
#> ⠹ ETA: 1m Benchmarking pop_city [2/13]
#> ⠸ ETA: 1m Benchmarking pop_urban [3/13]
#> ⠼ ETA: 1m Benchmarking pop_rural [4/13]
#> ⠴ ETA: 1m Benchmarking farm [5/13]
#> ⠦ ETA:49s Benchmarking nonfarm [6/13]
#> ⠧ ETA:42s Benchmarking educ_elem [7/13]
#> ⠇ ETA:35s Benchmarking educ_hsch [8/13]
#> ⠏ ETA:28s Benchmarking educ_coll [9/13]
#> ⠋ ETA:21s Benchmarking inc_00_03k [10/13]
#> ⠙ ETA:14s Benchmarking inc_03_08k [11/13]
#> ⠹ ETA: 7s Benchmarking inc_08_25k [12/13]
#> ⠹ ETA: 0s Benchmarking inc_25_99k [13/13]
subset(bench, outcome == "pres_rep_nix")
#> # A tibble: 13 × 7
#> covariate predictor outcome c_outcome c_predictor confounding est_chg
#> <chr> <chr> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 state vap_white - va… pres_r… 0.227 0.392 -0.156 -0.0723
#> 2 pop_city vap_white - va… pres_r… 0.0140 0.238 -0.727 -0.0693
#> 3 pop_urban vap_white - va… pres_r… 0.0135 0 -1 -0.0545
#> 4 pop_rural vap_white - va… pres_r… 0.0184 0 -1 -0.0871
#> 5 farm vap_white - va… pres_r… 0.00531 0 -1 -0.00878
#> 6 nonfarm vap_white - va… pres_r… 0.0182 0.0986 -0.624 -0.0464
#> 7 educ_elem vap_white - va… pres_r… 0.0222 0.00739 -1 -0.0615
#> 8 educ_hsch vap_white - va… pres_r… 0.0229 0 -1 -0.0893
#> 9 educ_coll vap_white - va… pres_r… 0.0395 0.144 -0.412 -0.0534
#> 10 inc_00_03k vap_white - va… pres_r… 0.0202 0.0349 -1 -0.0574
#> 11 inc_03_08k vap_white - va… pres_r… 0.0182 0.0215 -0.484 -0.0174
#> 12 inc_08_25k vap_white - va… pres_r… 0.00267 0 -1 -0.0373
#> 13 inc_25_99k vap_white - va… pres_r… 0.0286 0.104 -0.803 -0.0766The table above shows the benchmark values for each covariate for the
racially polarized Nixon vote estimand. The confounding
column is an additional component of the sensitivity analysis that is
discussed in the paper; the default value is 1, which is a conservative
worst-case bound. The benchmark values here show that state
is far and away the strongest observed confounder, whose inclusion
changes the estimate by 28pp. If the unobserved confounders were as
strong as state, we might expect a significant amount of
bias, as we will see next.
Rather than perform this sensitivity analysis on a single set of sensitivity parameters, we can run it across all combinations of parameter values, and visualize the results on a bias contour plot. We can further overlay the benchmarking values to help interpret the results.
sens = ei_sens(est) # the default evaluates on a grid of parameters
plot(sens, "pres_rep_nix", bench = bench, bounds = c(-1, 1))#> Warning in graphics::par(oldpar): graphical parameter "cin" cannot be set
#> Warning in graphics::par(oldpar): graphical parameter "cra" cannot be set
#> Warning in graphics::par(oldpar): graphical parameter "csi" cannot be set
#> Warning in graphics::par(oldpar): graphical parameter "cxy" cannot be set
#> Warning in graphics::par(oldpar): graphical parameter "din" cannot be set
#> Warning in graphics::par(oldpar): graphical parameter "page" cannot be set
The contour lines indicate how much bias could result from an unobserved confounder with the specified sensitivity parameters. The blue dashed contours correspond to bias of 1, 2, and 3 standard errors. This is a helpful value to compare against, because bias of that size corresponds to a predictable drop in coverage rates of confidence intervals. For example, bias of 1 standard error means that a confidence interval with 95% nominal coverage will actually have coverage of only around 80%.
The red asterisks indicate the benchmark values for each covariate.
Most are clustered in the lower-left corner and can’t be distinguished.
In contrast, the benchmark for state shows that an
unobserved confounder of that strength could lead to bias of around
40pp, which is substantial compared to the estimate itself, which is
50pp.
Finally, it can be helpful to summarize the sensitivity analysis by a
single number. The ei_sens_rv() function calculates the
robustness value, which measures the minimum strength
of an unobserved confounder that would lead to a bias of a given amount.
Here, we consider bias sufficient to eliminate any evidence of racially
polarized voting, i.e., bias equal to the estimated difference between
White and Black voters.
ei_sens_rv(est, bias_bound = estimate)
#> # A tibble: 4 × 5
#> predictor outcome estimate std.error rv
#> <chr> <chr> <dbl> <dbl> <dbl>
#> 1 vap_white - vap_black pres_dem_hum -0.365 0.0487 0.336
#> 2 vap_white - vap_black pres_rep_nix 0.504 0.0490 0.377
#> 3 vap_white - vap_black pres_ind_wal -0.140 0.0396 0.113
#> 4 vap_white - vap_black pres_abs 0.000966 0.000993 0.0245The robustness value (one for each predictor/outcome combination) is
relatively small for Wallace’s vote share, indicating low robustness
(high sensitivity). In particular, it is far smaller than the amount of
confounding benchmarked by the observed state variable. For
Humphrey and Nixon’s vote shares, however, the robustness values are
larger, indicating more confidence in the finding of racially polarized
voting for those candidates.
As with any single-number summary, it is important to consider sensitivity beyond the single value, by using the contour plot and the benchmarking analysis.
McCartan, C., & Kuriwaki, S. (2025+). Identification and semiparametric estimation of conditional means from aggregate data. Working paper arXiv:2509.20194.
Chernozhukov, V., Cinelli, C., Newey, W., Sharma, A., & Syrgkanis, V. (2024). Long story short: Omitted variable bias in causal machine learning (No. w30302). National Bureau of Economic Research.