--- title: "From a fitted model to a sensitivity report" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{From a fitted model to a sensitivity report} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} has_sm <- requireNamespace("sensemakr", quietly = TRUE) has_kf <- requireNamespace("konfound", quietly = TRUE) has_ev <- requireNamespace("EValue", quietly = TRUE) knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.width = 6.5, fig.height = 4, eval = has_sm) ``` ```{r, eval = !has_sm, echo = FALSE, results = "asis"} cat("*This vignette needs the 'sensemakr' package for the example data;", "install it to see the output.*") ``` This vignette walks through the complete workflow: ``` fitted model -> sensitivity path -> confoundsens object -> plots -> report ``` using a real, publicly available dataset. ## Data and model The `darfur` data (Hazlett, 2020), distributed with the **sensemakr** package, come from a survey of Darfurian refugees in eastern Chad. The question is whether being directly harmed in the violence (`directlyharmed`) changed attitudes toward peace (`peacefactor`). Following Cinelli and Hazlett (2020), we adjust for age, occupation, past voting, household size, sex, and village fixed effects. ```{r model} library(confoundvis) data("darfur", package = "sensemakr") fit <- lm(peacefactor ~ directlyharmed + age + farmer_dar + herder_dar + pastvoted + hhsize_darfur + female + village, data = darfur) coef(summary(fit))["directlyharmed", ] ``` ## Step 1: compute sensitivity paths A *sensitivity path* is the treatment estimate as a function of the strength $\lambda$ of a hypothetical omitted confounder. `confoundvis` computes paths for two regression frameworks directly from the fitted model. **Partial $R^2$ (Cinelli and Hazlett, 2020).** $\lambda$ is the partial $R^2$ of the confounder with both treatment and outcome: ```{r path-r2} p_r2 <- sens_path_lm(fit, treatment = "directlyharmed", lambda = seq(0, 0.3, by = 0.001)) p_r2 ``` **Impact threshold (Frank, 2000).** $\lambda$ is the impact $k = r_{DU}\,r_{YU}$; the path is the adjusted partial correlation: ```{r path-itcv} it <- itcv_lm(fit, treatment = "directlyharmed", lambda = seq(0, 0.3, by = 0.001)) c(r = it$r, r_crit = it$r_crit, ITCV = it$itcv, ITCV_unconditional = it$itcv_unconditional) ``` Both results are `confoundsens` objects, so everything downstream is the same regardless of framework. ## Step 2: plot the paths ```{r curve-r2} plot_robustness_curve(p_r2, points = FALSE) ``` The dotted line is the null. The band is a pointwise 95% interval for the bias-adjusted estimate. ```{r curve-itcv} plot_robustness_curve(it$path, points = FALSE) ``` On the ITCV path the dashed line marks the critical correlation; the path crosses it exactly at the ITCV. ## Step 3: benchmark against observed covariates A threshold alone does not say whether an omitted confounder that strong is plausible. The usual aid is to compare it with observed covariates. `covariate_impacts()` computes, for every covariate term, its partial associations with treatment and outcome. ```{r impacts} imp <- covariate_impacts(fit, "directlyharmed", metric = "partial_r2") imp[order(-imp$impact), c("covariate", "term_df", "r2dz.x", "r2yz.dx", "bias_index")] ``` The sensitivity Love plot places each covariate on the chosen metric and draws the threshold. With `metric = "partial_r2"`, a covariate to the right of the line would, if an omitted confounder were equally strong, bring the point estimate to zero. ```{r love} plot_sensitivity_love(imp) ``` The ITCV metric uses signed products of partial correlations and is defined for single-column terms; the multi-column `village` term is dropped with a message. ```{r love-itcv} imp_it <- covariate_impacts(fit, "directlyharmed", metric = "itcv") plot_sensitivity_love(imp_it) ``` The same object supplies benchmarks for the ITCV contour: ```{r contour} plot_sensitivity_contour(attr(imp_it, "threshold"), benchmarks = imp_it[!is.na(imp_it$r_yu), ]) ``` ## Step 4: a plain-language report ```{r report} sens_report(p_r2) sens_report(it$path) ``` `robustness_points()` returns the same numbers as a data frame for tables. On the partial $R^2$ path, the null crossing is the robustness value and the significance crossing is the robustness value at $\alpha = 0.05$ reported by **sensemakr**. ## Using results from other packages If an analysis was run with another package, convert its result instead of recomputing it. ```{r sensemakr} s <- sensemakr::sensemakr(fit, treatment = "directlyharmed", benchmark_covariates = "female", kd = 1:3) p_sm <- from_sensemakr(s, lambda = seq(0, 0.3, by = 0.001)) robustness_points(p_sm) unlist(s$sensitivity_stats[c("rv_q", "rv_qa")]) ``` ```{r konfound, eval = has_sm && has_kf} k <- suppressWarnings(suppressMessages( konfound::konfound(fit, directlyharmed, to_return = "raw_output"))) p_kf <- from_konfound(k, lambda = seq(0, 0.3, by = 0.001)) c(confoundvis = it$itcv, konfound_itcvGz = k$itcvGz) ``` For binary or ratio-scale outcomes, E-value results convert to a path on the log risk-ratio scale that crosses the null at the E-value: ```{r evalue, eval = has_ev} e <- EValue::evalues.RR(est = 1.8, lo = 1.3, hi = 2.5) p_ev <- from_evalue(e, lambda = seq(1, 4, by = 0.01)) plot_robustness_curve(p_ev, points = FALSE) ``` ## What this package does and does not do `confoundvis` is a presentation layer. Its computations reproduce the published formulas of each framework (the test suite checks them against **sensemakr**, **konfound**, and **EValue**), and its contribution is a shared object and a shared set of displays. It inherits every assumption of the framework that generated a path. In particular: * A sensitivity path answers "how strong would confounding have to be?", not "is there such a confounder?" Only substantive knowledge can judge that. * Covariate benchmarks describe observed variables. An omitted confounder can be stronger than every measured covariate, and benchmarks can themselves be distorted when the omitted confounder is correlated with them. * The partial $R^2$ and ITCV paths are for linear regression coefficients. The E-value path concerns ratio measures; its bias factor is a bound, not an exact adjustment. * No display repairs a flawed identification strategy (for example, bad controls, selection, or measurement error in the treatment). * The geometric illustrations (`plot_reversal_cone()`, `plot_taylor_panels()`) are conceptual tools built on stylized models and do not analyse data. ## References Cinelli, C., & Hazlett, C. (2020). Making sense of sensitivity: Extending omitted variable bias. *Journal of the Royal Statistical Society: Series B*, 82(1), 39–67. Frank, K. A. (2000). Impact of a confounding variable on a regression coefficient. *Sociological Methods & Research*, 29(2), 147–194. Frank, K. A., Maroulis, S. J., Duong, M. Q., & Kelcey, B. M. (2013). What would it take to change an inference? *Educational Evaluation and Policy Analysis*, 35(4), 437–460. Hazlett, C. (2020). Angry or weary? How violence impacts attitudes toward peace among Darfurian refugees. *Journal of Conflict Resolution*, 64(5), 844–870. VanderWeele, T. J., & Ding, P. (2017). Sensitivity analysis in observational research: Introducing the E-value. *Annals of Internal Medicine*, 167(4), 268–274.