diagnostics(fit) is the friendlier "is my fit okay?" companion to
glance(). It returns a structured object with PASS / WARN /
FAIL flags for the control/treated ESS, the worst post-weighting
standardized difference, convergence, and (for trimmed objects)
trim feasibility. Print method renders each check on its own line.
Thresholds (ess_warn, ratio_warn, std_diff_warn) are
user-configurable per call.ebalance() now emits a warning() when the resulting fit is
unhealthy: converged = FALSE, ESS below 30% of the relevant
side's n, or max/mean weight ratio above 10. Suppressible via
options(ebal.warn_weak_fit = FALSE). The warning explains which
diagnostic tripped and points at ?diagnostics /
ebalance.trim().autoplot(), plot(), glance(),
summary(), tidy(), as.data.frame()) now route through the
exported balance_table() instead of the older internal
.balance_table(). The canonical column names
(mean_treated_pre, mean_treated_post, mean_control_pre,
mean_control_post, diff_pre, diff_post, std_diff_pre,
std_diff_post, pct_reduction) propagate everywhere.balance_table(fit) returns a tidy per-covariate balance table with
columns variable, mean_treated_pre, mean_treated_post,
mean_control_pre, mean_control_post, diff_pre, diff_post,
std_diff_pre, std_diff_post, pct_reduction. Carries
attr(out, "estimand"). The table is the canonical balance
representation for the package; summary(), tidy(), plot(),
and autoplot() all read from it.plot(fit, type = "weights") and autoplot(fit, type = "weights")
show histograms of the per-unit weights with the Kish ESS and
max-weight ratio in the subtitle. type = "balance" (default) is
the original Love plot.glance(fit) now reports per-side ESS / max-weight diagnostics:
ess_control, ess_treated, max_weight_control,
max_weight_treated, max_weight_ratio_control,
max_weight_ratio_treated. Also reports
max_abs_std_diff_pre and max_abs_std_diff_post so the row is a
one-glance "is this fit usable?" summary..active_group(fit) resolves the per-estimand side
semantics in one place; weights(), print(), glance(), and the
plot/autoplot/balance-table functions all route through it instead
of re-implementing the estimand branch.?ebalance documents what $w, weights(fit), $target.margins,
$norm.constant, and $base.weight mean per estimand.?ebalance adds a section of formula-interface examples
(I(age^2), interactions, factor(region)).vignette("estimands", package = "ebal") walks through ATT / ATC /
ATE side by side: what gets reweighted, how weights(fit) shape
changes, how to read the diagnostics.vignette("outcome-models", package = "ebal") shows the standard
downstream-regression workflow: weighted lm(), robust SEs via
sandwich/lmtest (in Suggests), survey-style inference, and when
to add regression adjustment for a doubly-robust estimator.ebalance() gains an estimand argument: "ATT" (default; original
behavior), "ATC" (treated reweighted to match controls; symmetric to
ATT), "ATE" (both groups reweighted to match the overall sample)."ATE" the returned object carries per-side solves under
$control_solve and $treated_solve. weights(fit) returns a
length-n vector with both groups carrying their estimated weights;
drop straight into lm(..., weights = w) for the population ATE.glance(fit) now reports the estimand. summary(fit), plot(fit),
autoplot(fit), tidy(fit), and as.data.frame(fit) all work
unchanged across estimands; the underlying .balance_table() helper
consumes a length-n weight vector so all three estimands route through
the same display path.ebalance.trim() does not yet support "ATE" and refuses with a
clear message; trim each side separately if needed.ebalance() gains a method argument: "newton" (default; the
classical Newton-Raphson solver, behavior unchanged from earlier
releases) and "autodiff" (a torch-based solver that uses BFGS on
gradients computed by automatic differentiation)."autodiff" path is contributed by Apoorva Lal, ported from his
ebal fork at https://github.com/apoorvalal/ebal. It is more
stable when the optimization landscape is poorly conditioned and
scales better at large covariate counts. Newton remains the default
because it is faster on the small problems that dominate everyday
use; users opt into autodiff with method = "autodiff".torch is in Suggests:, not Imports:. Users who do not use the
autodiff path see no change to their installation footprint. The
first call to a torch function in a session may require
torch::install_torch() to download libtorch.aut on the package for this contribution.tidy(fit) returns a per-covariate balance table (means, raw and
weighted differences, standardized differences) as a data.frame,
ready for dplyr / kable / gt consumption. Methods registered
against the generics package generics, so library(broom) makes
them discoverable.glance(fit) returns a one-row summary: n_treated, n_control,
n_moments, sum_weights, ess_kish (Kish effective sample size),
max_weight, max_weight_ratio, maxdiff, converged. For
ebalance.trim objects the row also carries trim_feasible.augment(fit, data) joins per-unit weights back to the original
data frame as .weight (treated units get 1; controls get the
ebalance weight). Drop-in for lm(..., weights = .weight).as.data.frame(fit) returns the balance table directly (alias for
tidy()'s output).autoplot(fit) returns a ggplot object showing the standardized
difference of each covariate before vs. after weighting, with
reference lines at 0 and ±0.1. Discoverable via library(ggplot2);
ggplot2 is in Suggests:.vignette("ebal-quickstart", package = "ebal") walks through the
Lalonde PSID example end-to-end with both solver methods, the
weighted regression follow-up, and the new tidy/autoplot output.ebalance() is no longer an S3 generic. The formula vs. matrix
dispatch is now handled inside the function via
inherits(Treatment, "formula"). The user-facing API is unchanged:
ebalance(treat ~ x1 + x2, data = df) and
ebalance(Treatment = t, X = X) both work exactly as before.
This change was made to satisfy CRAN's auto-checker, which flagged
the long-standing top-level function ebalance.trim() as an
"apparent method" of the ebalance generic and refused the 0.2.0
submission.Authors@R field replaces the older Author / Maintainer pair
(current CRAN style).Depends: methods moved to Imports: graphics, methods, stats so
the package no longer attaches its dependencies onto the user's
search path.tests/testthat/ suite with 54 assertions covering the
matrix and formula interfaces, the ebalance.trim graceful-fallback
behavior, the new S3 methods, and the helper functions
(matrixmaker, getsquares).README.md with quick-start examples for both the matrix and
formula interfaces and a section on trimming.R-CMD-check.yaml) runs against R
release on macOS / Windows / Ubuntu, R devel on Ubuntu, and one
prior R release on Ubuntu.Formula interface. ebalance(treat ~ x1 + x2, data = df) now
works, in addition to the original ebalance(Treatment = t, X = X)
matrix interface. Both produce identical numerical results. The
formula interface uses model.frame() and model.matrix() and
drops the (Intercept) column automatically.
weights() method. weights(fit) returns a length-\eqn{n}
vector aligned to the original Treatment: treated units get
weight 1, control units get their entropy-balancing weight. Drop-in
for lm(..., weights = w) or survey::svyglm().
print() and summary() methods. print(fit) shows a
one-screen overview (counts, moments, convergence status, and for
trimmed objects whether the trim target was feasible). summary(fit)
returns a balance table comparing treated and control means
(and standardized differences) before and after weighting.
plot() method. Base-graphics Love plot of standardized
differences before vs. after weighting, one row per covariate.
No ggplot2 or other graphics dependency.
New return fields on ebalance and ebalance.trim objects:
Treatment (the original treatment indicator) and X (the original
covariate matrix). Existing fields are unchanged. These enable the
summary(), plot(), and weights() methods without requiring the
user to pass the original data back in.
ebalance is now an S3 generic with ebalance.default (the
workhorse with the matrix interface) and ebalance.formula
(formula method) registered. ebalance.trim is intentionally NOT
registered as a method; it remains a top-level function callable as
ebalance.trim(fit) exactly as before. R CMD check emits an
informational NOTE about the legacy name and we accept it.ebalance() and ebalance.trim() no longer crash with
"missing value where TRUE/FALSE needed" when the optimizer is
pushed into a regime where exp(co.x %*% coefs) would overflow IEEE
double precision. Both call sites now route through an internal
.safe_exp() helper that caps the linear predictor at 700 before
exponentiating. The cap is inactive (no observable effect) for
well-conditioned problems; it activates when an aggressive
max.weight target in ebalance.trim() forces the algorithm to
explore very large coefficients, returning a large-but-finite weight
so the line search and gradient check can keep navigating.
ebalance.trim() now fails gracefully when the inner Newton solve
becomes numerically singular during the explicit-max.weight branch
(for example, because the requested target is infeasible). It emits
a clear warning, returns the most recent feasible fit, and sets the
new trim.feasible field to FALSE. Previously it crashed with
"system is computationally singular" (or, before the overflow
guard, the older NaN error). The automatic-minimization branch
was already wrapped in try() and is unchanged.
New return field trim.feasible (logical) on ebalance.trim
objects. TRUE when an explicit target was met or when automatic
minimization completed; FALSE when an explicit target proved
infeasible. Existing fields are unchanged; reading any other field
by name continues to work as before.
ebalance() uses colSums() instead of apply(X, 2, sum) to
compute treatment-group target margins. Output is identical;
trivially faster and avoids an unnecessary as.matrix() wrap.
H = X' diag(w) X is symmetric positive semidefinite,
so a Cholesky-based solve would be the textbook choice. We
considered switching from solve(H, gradient) to chol2inv(chol(H))
but kept solve() because (a) the Hessian is small in this package
(covariates × covariates, typically < 50 × 50), so the speed
difference is negligible, and (b) Cholesky and LU agree only at
machine precision, which would introduce last-bit drift versus the
0.1-8 baseline that we have no compelling reason to accept.ebalance.trim() now correctly populates the target.margins field of
the returned object. Previously it referenced a non-existent
ebalanceobj$tr.total and silently returned NULL.ebalance() now uses drop = FALSE when subsetting the control rows
of X, eliminating a latent dimension-drop issue that was masked by
cbind() in the next line.print.level = 0, neither ebalance() nor
ebalance.trim() emits per-iteration progress messages or a
"Converged within tolerance" line. Set print.level = 1 to restore
the previous chatter; 2 and 3 give increasing detail.eb(): removed a redundant call to solve(hessian, gradient) after
the line search; the Newton direction is now reused.getsquares(): replaced dim(table(x)) > 2 with
length(unique(x)) > 2; same predicate, faster on large vectors.matrixmaker(): dropped an unused "dummy" column that was allocated
and then discarded before return.zzz.r: removed unused this.year computation; switched startup
message URL to https.ebalance.trim.Rd: \seealso now correctly cross-references
ebalance instead of itself.print.level documentation in ebalance.Rd and ebalance.trim.Rd
now lists all four levels (0, 1, 2, 3) the code actually supports.