balancing provides six methods. They fall into two families that
differ in what they optimize and in the guarantees they offer. This
article describes the families, the exposures and estimands each method
supports, the difference between exact and tolerance-based balance, the
constraint options in balance_terms(), and the handling of
continuous exposures.
Two families of methods
Every method solves a convex optimization problem, but the problems come in two shapes.
The estimating-equation family fits weights that
solve smooth estimating equations. bw_entropy() reweights
to the maximum-entropy solution that matches the covariate targets;
bw_ipt() fits a propensity model through a tilted moment
condition; bw_cbps() chooses propensity parameters to
satisfy covariate-balancing moment conditions. Because these weights
solve estimating equations, their uncertainty can be propagated
analytically into effect standard errors through ipw().
With mean balance and a logit link, all three produce the same ATT
weights for a binary exposure.
The quadratic-program family minimizes a dispersion
or distance objective subject to balance constraints.
bw_energy() minimizes the energy distance between the
reweighted groups; bw_cfd() minimizes a kernel distance;
bw_sbw() minimizes the weight dispersion subject to a
balance tolerance. These methods do not carry estimating equations, so
inference for them uses resampling rather than the M-estimation route
ipw() takes.
The family a method belongs to is reflected in its class hierarchy:
the estimating-equation methods subclass
estimating_equation_method and the quadratic-program
methods subclass quadratic_program_method.
Which exposures and estimands each method supports
The methods differ in the exposure types and estimands they support. The table below is built from the methods themselves, so it always reflects what the installed version accepts.
| method | binary | categorical | continuous |
|---|---|---|---|
| bw_entropy | ate, att, atu | ate, att | ate |
| bw_ipt | ate, att, atu | ate, att | - |
| bw_cbps | ate, att, atu, ato | ate, att | ate |
| bw_energy | ate, att, atu | ate, att | ate |
| bw_cfd | ate, att, atu | ate, att | - |
| bw_sbw | ate, att, atu | ate, att | ate |
A dash marks an unsupported exposure type. The estimand vocabulary
follows propensity: "ate" (average treatment effect),
"att" (average treatment effect on the treated),
"atu" (on the untreated, accepted as "atc"),
and "ato" (the overlap estimand). The overlap estimand is
available only for bw_cbps() with a binary exposure.
Continuous exposures permit only "ate".
Exact versus tolerance-based balance
The families also differ in the balance they guarantee.
The estimating-equation methods achieve exact balance on the requested moments. Their weighted covariate means equal the target means to solver precision, which is why the printed summary reports a largest imbalance at the level of numerical noise.
n <- 600
age <- rnorm(n)
score <- rnorm(n)
exposure <- rbinom(n, 1, plogis(0.5 * age - 0.5 * score))
study <- data.frame(exposure, age, score)
exact <- balance(study, exposure, c(age, score), method = bw_entropy())
exact
#>
#> ── Entropy balancing ───────────────────────────────────────────────────────────
#> Exposure: "exposure" (binary)
#> Estimand: "ate"
#> Observations: 600
#> Solver: converged in 4 iterations
#> Constraints: 2 terms (tolerance 0)
#> Largest imbalance: 0.0000 (standardized mean difference)Exact balance is not always achievable or desirable. When the groups overlap poorly, forcing exact balance can drive the weights to extremes and collapse the effective sample size. A tolerance relaxes the constraints: each weighted statistic is held within a band rather than pinned to a point. For entropy balancing, a positive tolerance selects an inexact problem that trades a little residual imbalance for less variable weights. The printed summary records the tolerance the fit used and the largest imbalance it leaves.
relaxed <- balance(
study,
exposure,
c(age, score),
method = bw_entropy(),
estimand = "att",
constraints = balance_terms(tolerance = 0.05)
)
relaxed
#>
#> ── Entropy balancing ───────────────────────────────────────────────────────────
#> Exposure: "exposure" (binary)
#> Estimand: "att" (focal level "1")
#> Observations: 600
#> Solver: converged in 8 iterations
#> Constraints: 2 terms (tolerance 0.05)
#> Largest imbalance: 0.0500 (standardized mean difference)For bw_sbw() the tolerance is not optional. Stable
balancing weights minimize weight dispersion subject to a balance band,
so the tolerance is the method’s central tuning parameter and a fit
without a positive tolerance is refused.
sbw_fit <- balance(
study,
exposure,
c(age, score),
method = bw_sbw(),
constraints = balance_terms(tolerance = 0.02)
)
sbw_fit
#>
#> ── Stable balancing weights ────────────────────────────────────────────────────
#> Exposure: "exposure" (binary)
#> Estimand: "ate"
#> Observations: 600
#> Solver: converged in 75 iterations
#> Constraints: 2 terms (tolerance 0.02)
#> Largest imbalance: 0.0400 (standardized mean difference)For bw_energy() and bw_cfd(), the objective
itself drives balance, so the tolerance relaxes any moment constraints
you add rather than selecting an inexact solver. A tolerance supplied
without moment constraints has nothing to relax and is ignored with a
warning.
Constraints in depth
balance_terms() records which covariate functions a
method should balance. It has four ingredients.
Moments set the highest power of each numeric covariate to balance. A scalar applies to every covariate; a named integer vector sets powers per covariate.
# Balance means and variances of both covariates
balance_terms(moments = 2)
#> <balancing::balance_terms>
#> @ moments : int 2
#> @ interactions: logi FALSE
#> @ quantiles : NULL
#> @ tolerance : num 0
# Balance the mean of age but the mean and variance of score
balance_terms(moments = c(age = 1, score = 2))
#> <balancing::balance_terms>
#> @ moments : int [1:2] 1 2
#> @ interactions: logi FALSE
#> @ quantiles : NULL
#> @ tolerance : num 0Powers above one are ignored for binary indicator columns, since squaring a zero/one column reproduces it.
Interactions add all pairwise products of the base
columns when set to TRUE, excluding products of two
indicators of the same factor. This balances the joint distribution of
pairs of covariates, not just their marginals, and adds the product
terms to the constraint count the printed summary reports. The word
names a constraint here rather than a causal quantity:
interactions = TRUE expands the set of covariate functions
the weights must balance, equating them across the exposure groups of a
discrete exposure and driving them to zero correlation with a continuous
one, and says nothing about two treatments acting together, which is an
effect ipw() reports for a joint exposure rather than a
constraint requested here.
with_interactions <- balance(
study,
exposure,
c(age, score),
method = bw_entropy(),
constraints = balance_terms(moments = 1, interactions = TRUE)
)
with_interactions
#>
#> ── Entropy balancing ───────────────────────────────────────────────────────────
#> Exposure: "exposure" (binary)
#> Estimand: "ate"
#> Observations: 600
#> Solver: converged in 5 iterations
#> Constraints: 3 terms (tolerance 0)
#> Largest imbalance: 0.0000 (standardized mean difference)Quantiles add an indicator column at each requested probability, so that mean balance on the indicator is quantile balance on the covariate. This targets the shape of the distribution rather than its moments, and each added indicator becomes another constraint term. Quantile constraints apply to discrete exposures only.
with_quantiles <- balance(
study,
exposure,
c(age, score),
method = bw_entropy(),
constraints = balance_terms(
moments = 1,
quantiles = c(0.25, 0.5, 0.75)
)
)
with_quantiles
#>
#> ── Entropy balancing ───────────────────────────────────────────────────────────
#> Exposure: "exposure" (binary)
#> Estimand: "ate"
#> Observations: 600
#> Solver: converged in 4 iterations
#> Constraints: 8 terms (tolerance 0)
#> Largest imbalance: 0.0000 (standardized mean difference)Tolerance sets the per-constraint balance band, as described above. It can be a scalar or a named vector giving a different tolerance per source covariate, with derived columns inheriting their source’s tolerance.
balance_terms(tolerance = c(age = 0.01, score = 0.1))
#> <balancing::balance_terms>
#> @ moments : NULL
#> @ interactions: logi FALSE
#> @ quantiles : NULL
#> @ tolerance : Named num [1:2] 0.01 0.1
#> .. - attr(*, "names")= chr [1:2] "age" "score"When constraints is left at its default, the
estimating-equation methods and stable balancing weights balance first
moments, while energy and characteristic function distance balancing add
no moment constraints and let their objective drive balance.
Continuous exposures
For a continuous exposure there are no groups to balance against one
another. Instead, the target is independence between the exposure and
the covariates: the weighted exposure-covariate correlations should be
zero. bw_entropy(), bw_cbps(),
bw_energy(), and bw_sbw() support continuous
exposures; the estimand is always "ate".
dose <- 0.7 * age - 0.5 * score + rnorm(n)
dose_study <- data.frame(dose, age, score)
dose_fit <- balance(
dose_study,
dose,
c(age, score),
method = bw_entropy()
)
dose_fit
#>
#> ── Entropy balancing ───────────────────────────────────────────────────────────
#> Exposure: "dose" (continuous)
#> Estimand: "ate"
#> Observations: 600
#> Solver: converged in 6 iterations
#> Constraints: 2 terms (tolerance 0)
#> Largest imbalance: 0.0000 (correlation)For a continuous exposure the printed summary reports the largest weighted exposure-covariate correlation rather than a standardized mean difference.
For bw_entropy() and bw_energy(), the
distribution_moments argument on the method constructor
sets how many moments of the exposure and covariate marginals are held
equal to the sample, extending the identifying conditions beyond the
correlation constraints. Both measure that sample under the base
measure: any sampling weights, times the base_weights of
bw_entropy(), which bw_energy() does not
carry. With neither in play the marginals are held equal to the
unweighted sample. It is raised automatically to at least the moments
the constraints require.
dose_moments <- balance(
dose_study,
dose,
c(age, score),
method = bw_entropy(distribution_moments = 2, convergence_tolerance = 1e-8)
)
dose_moments
#>
#> ── Entropy balancing ───────────────────────────────────────────────────────────
#> Exposure: "dose" (continuous)
#> Estimand: "ate"
#> Observations: 600
#> Solver: converged in 5 iterations
#> Constraints: 2 terms (tolerance 0)
#> Largest imbalance: 0.0000 (correlation)The marginal-moment conditions are enforced inside the solver rather
than added to the balance table, so they leave the constraint count in
the printed summary unchanged. They also make the problem stiffer, so
this fit relaxes the convergence_tolerance to a looser
value to reach the requested balance.
A note on solvers
Each method has a default numerical backend: the estimating-equation family uses a damped Newton solver, and the quadratic-program family uses an operator-splitting solver, with stable balancing weights falling back automatically to an interior-point solver when the default certifies a problem infeasible. The backend that produced a fit is recorded on the result. These defaults are set to be reliable across a wide range of problems, so a typical analysis does not need to change them.