ebalance() reweights
one or both groups to match a target moment distribution. Which target
you pick determines the estimand — the average
treatment effect on a particular subpopulation. As of 0.3-0,
ebalance(..., estimand = "ATT" / "ATE" / "ATC")
selects:
estimand |
reweighted | target moments | answers |
|---|---|---|---|
"ATT" (default) |
controls | treated group means | “what was the effect on those who actually got treatment?” |
"ATC" |
treated | control group means | “what would the effect have been on the control population if it had been treated?” |
"ATE" |
both | overall sample means | “what is the average effect across the whole population?” |
This vignette builds the same toy panel and shows what changes across the three estimands.
library(ebal)
set.seed(20260505)
n0 <- 200; n1 <- 100
X <- rbind(
replicate(3, rnorm(n0, mean = 0)), # controls
replicate(3, rnorm(n1, mean = 0.5)) # treated, shifted
)
colnames(X) <- c("x1", "x2", "x3")
treatment <- c(rep(0, n0), rep(1, n1))
c(treated_mean = colMeans(X[treatment == 1, ])[1],
control_mean = colMeans(X[treatment == 0, ])[1],
overall_mean = mean(X[, 1]))
#> treated_mean.x1 control_mean.x1 overall_mean
#> 0.51489188 0.06796959 0.21694369The treated group means are above zero; the controls are at zero; the overall mean is somewhere between.
fit_att <- ebalance(Treatment = treatment, X = X, estimand = "ATT")
fit_ate <- ebalance(Treatment = treatment, X = X, estimand = "ATE")
fit_atc <- ebalance(Treatment = treatment, X = X, estimand = "ATC")weights(fit) returns a length-n vector
aligned to the original treatment indicator. The shape changes with the
estimand:
table(treatment, sign(weights(fit_att))) # ATT: treated = 1, controls reweighted
#>
#> treatment 1
#> 0 200
#> 1 100
table(treatment, sign(weights(fit_ate))) # ATE: both reweighted
#>
#> treatment 1
#> 0 200
#> 1 100
table(treatment, sign(weights(fit_atc))) # ATC: treated reweighted, controls = 1
#>
#> treatment 1
#> 0 200
#> 1 100For ATT, the controls are reweighted toward the treated mean (so their weighted mean equals the treated mean):
weighted.mean(X[treatment == 0, 1], w = weights(fit_att)[treatment == 0])
#> [1] 0.5151681
mean(X[treatment == 1, 1])
#> [1] 0.5148919For ATE, both groups are reweighted toward the overall mean:
weighted.mean(X[treatment == 0, 1], w = weights(fit_ate)[treatment == 0])
#> [1] 0.2169915
weighted.mean(X[treatment == 1, 1], w = weights(fit_ate)[treatment == 1])
#> [1] 0.2152382
mean(X[, 1])
#> [1] 0.2169437For ATC, the treated are reweighted toward the control mean:
glance() returns a one-row “is this fit usable?”
summary, with per-side ESS / max-weight ratios and the worst pre/post
standardized difference:
library(generics)
do.call(rbind, lapply(list(ATT = fit_att, ATE = fit_ate, ATC = fit_atc),
glance))[, c("estimand", "ess_control", "ess_treated",
"max_weight_ratio_control",
"max_weight_ratio_treated",
"max_abs_std_diff_post")]
#> estimand ess_control ess_treated max_weight_ratio_control
#> ATT ATT 115.0302 100.00000 8.805591
#> ATE ATE 188.8541 79.50143 2.244101
#> ATC ATC 200.0000 59.85559 1.000000
#> max_weight_ratio_treated max_abs_std_diff_post
#> ATT 1.000000 0.0002689209
#> ATE 2.924031 0.0030481091
#> ATC 4.900253 0.0004691477Read these top-down: ATT keeps the treated side trivial (ESS = 100, ratio = 1) and concentrates weight on a subset of controls. ATE reweights both sides, so both ESS values fall below their group sizes. ATC mirrors ATT with roles swapped.
autoplot(fit, type = "balance") is the Love plot of
standardized differences before vs. after weighting.
autoplot(fit, type = "weights") is a histogram of the
per-unit weights with the Kish ESS and max-weight ratio in the
subtitle:
Identical interfaces are available in base graphics via
plot(fit, type = ...).
A short rule of thumb:
ebalance() doesn’t make this choice for you — it just
gives you the weights once you’ve made it. The associated standard error
/ inference question is independent (see
vignette("outcome-models", package = "ebal")).