--- title: "Get started with balancing" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Get started with balancing} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) options(balancing.quiet = TRUE) set.seed(2024) ``` Balancing weights target covariate balance directly. Instead of fitting a model for the probability of exposure and deriving weights from it, a balancing method poses covariate balance as a convex optimization problem and solves for the weights that achieve it. This vignette walks through the core workflow for a binary exposure: fitting weights with `balance()`, reading the balance they achieve, checking that balance more broadly with halfmoon, and estimating a causal effect. ```{r setup, message = FALSE} # halfmoon provides the balance-assessment tools used throughout this # vignette. library(halfmoon) library(balancing) ``` ## A confounded example We simulate a study with two confounders, `age` and `score`, a binary `exposure`, and a continuous `outcome`. Both confounders drive the exposure and the outcome, so a naive comparison of exposed and unexposed units is biased. ```{r data} n <- 800 age <- rnorm(n) score <- rnorm(n) exposure <- rbinom(n, 1, plogis(0.5 * age - 0.6 * score)) outcome <- 1 + 0.8 * exposure + 0.7 * age - 0.5 * score + rnorm(n) study <- data.frame(exposure, age, score, outcome) ``` Before weighting, the exposed and unexposed groups differ on both confounders. halfmoon's `check_balance()` summarizes that imbalance; the `observed` rows are the unweighted sample, where the standardized mean differences on `age` and `score` run to about half a standard deviation in opposite directions. The same call appears again later with the weights attached. ```{r imbalance} check_balance(study, c(age, score), exposure) ``` ## Fitting balancing weights `balance()` takes the data frame, the exposure and covariate columns selected with tidyselect, a method specification, and an estimand. We start with entropy balancing for the average treatment effect on the treated (ATT), which reweights the unexposed group to match the covariate means of the exposed group. ```{r fit} fit <- balance( study, exposure, c(age, score), method = bw_entropy(), estimand = "att" ) fit ``` The print method summarizes the fit. It reports the method and the exposure type detected from the data, the estimand and its focal level, the number of observations, whether the solver converged, and the largest imbalance the weights leave on the constraint terms. Here the largest standardized mean difference is effectively zero: entropy balancing achieves exact mean balance by construction. ## Checking balance with halfmoon The balance table reports balance on the terms the weights were asked to balance. To assess balance more broadly, including statistics beyond the mean and variables the weights did not target, extract the weights with `weights()` and pass them to the [halfmoon](https://r-causal.github.io/halfmoon/) package. ```{r halfmoon-weights} study$w <- weights(fit) ``` Passing the weights to the same `check_balance()` call compares the weighting scheme against the unweighted sample across a set of covariates. ```{r halfmoon} balance_check <- check_balance( study, c(age, score), exposure, .weights = w ) balance_check ``` `plot_balance()` renders the same comparison as a love plot. ```{r love-plot} plot_balance(balance_check) ``` halfmoon owns the general balance-assessment workflow, so its love plots, mirrored histograms, and balance summaries are the place to look when you want a fuller picture than the constraint terms provide. It also reports the effective sample size, the amount of each group that survives weighting, through `check_ess()`. ## Estimating a causal effect The weights are a `bw` vector. `weights()` returns the balancing weights already multiplied by any sampling weights, so its result goes straight into a weighted outcome model; there is no need to compose the sampling weights yourself. Manual composition is only called for when you have weights from a separate source that the fit does not carry. With a marginal outcome model, the exposure coefficient is the weighted effect estimate. ```{r outcome} outcome_mod <- lm(outcome ~ exposure, data = study, weights = w) coef(outcome_mod)[["exposure"]] ``` The estimate recovers the simulated effect of 0.8, which the unweighted comparison would have missed. ## Standard errors with `ipw()` A weighted outcome model's usual standard errors treat the weights as fixed, but the weights were themselves estimated. For the estimating-equation methods (entropy balancing, inverse probability tilting, and the just-identified covariate balancing propensity score) with a binary or categorical exposure, and for entropy balancing with a continuous one, `ipw()` returns effect estimates with standard errors that account for the estimation of the weights. ```{r ipw} binary_outcome <- rbinom(n, 1, plogis(-0.3 + 0.6 * exposure + 0.4 * age)) study$event <- binary_outcome ate_fit <- balance( study, exposure, c(age, score), method = bw_entropy(), estimand = "ate" ) study$w_ate <- weights(ate_fit) event_mod <- glm( event ~ exposure, data = study, family = quasibinomial(), weights = w_ate ) ipw(ate_fit, event_mod) ``` The `vignette("inference")` article explains why these standard errors differ from the naive ones and how to obtain valid intervals for the methods that do not carry estimating equations. ## Where to go next - `vignette("choosing-a-method")` compares the six methods, the exposures and estimands each supports, and the constraint options exposed through `balance_terms()`. - `vignette("inference")` covers valid standard errors after balancing, through `ipw()` for the estimating-equation methods and a bootstrap recipe for the quadratic-program methods.