| Title: | Stable Balancing Weights for Covariate Adjustment in Trials |
| Version: | 0.3.0 |
| Description: | Covariate adjustment for randomized trials using stable balancing weights (SBW), as proposed by Irish, Zubizarreta, and Luedtke (2026) <doi:10.48550/arXiv.2609.01638>. sbw_weights() fits exact-balance SBW for a two-arm study by a closed-form solve with a nonnegative quadratic-programming fallback; sbw_estimate() computes a closed menu of treatment-effect estimands (average treatment effect, relative risk, survival ratio, Mann-Whitney, quantile contrasts) from the fitted weights, with bootstrap confidence intervals. |
| License: | MIT + file LICENSE |
| URL: | https://github.com/kaylairish/sbwadjust |
| BugReports: | https://github.com/kaylairish/sbwadjust/issues |
| Encoding: | UTF-8 |
| Language: | en-US |
| RoxygenNote: | 7.2.3 |
| Imports: | graphics, quadprog, stats, survival |
| Suggests: | testthat (≥ 3.0.0) |
| Config/testthat/edition: | 3 |
| NeedsCompilation: | no |
| Packaged: | 2026-09-29 13:01:59 UTC; kaylairish |
| Author: | Kayla Irish [aut, cre] |
| Maintainer: | Kayla Irish <kayla.a.irish@gmail.com> |
| Repository: | CRAN |
| Date/Publication: | 2026-10-10 10:20:02 UTC |
sbwadjust: Stable Balancing Weights for Covariate Adjustment in Trials
Description
Covariate adjustment for randomized trials using stable balancing weights (SBW), as proposed by Irish, Zubizarreta, and Luedtke (2026) doi:10.48550/arXiv.2609.01638. sbw_weights() fits exact-balance SBW for a two-arm study by a closed-form solve with a nonnegative quadratic-programming fallback; sbw_estimate() computes a closed menu of treatment-effect estimands (average treatment effect, relative risk, survival ratio, Mann-Whitney, quantile contrasts) from the fitted weights, with bootstrap confidence intervals.
Author(s)
Maintainer: Kayla Irish kayla.a.irish@gmail.com
See Also
Useful links:
Report bugs at https://github.com/kaylairish/sbwadjust/issues
Build the balance design matrix from a one-sided formula
Description
Build the balance design matrix from a one-sided formula
Usage
.build_design(balance, data)
Arguments
balance |
One-sided formula naming the covariates to balance. |
data |
Data frame containing those covariates. |
Value
A data frame of numeric balance columns (factors dummy-coded, intercept dropped).
Clip small/negative numerical noise out of a QP weight solution
Description
Clip small/negative numerical noise out of a QP weight solution
Usage
.clip_weights(w)
Arguments
w |
Numeric vector of weights (possibly containing negative floating-point dust from the QP solve). |
Value
A list with w (clipped weights), n_clipped (how many entries
were negative before clipping), and max_abs_clipped (largest
magnitude among the clipped entries, 0 if none).
Extract survival + SE at a fixed time from a survfit object, by arm
Description
Extract survival + SE at a fixed time from a survfit object, by arm
Usage
.extract_surv_at_t0(fit, t0)
Arguments
fit |
A |
t0 |
Time at which to extract survival estimates. |
Value
A list with S0, S1 (survival estimates for A=0/A=1) and se0,
se1 (their standard errors).
Count units with (numerically) zero SBW weight in each arm
Description
Counted directly from the weights rather than from n_clipped, which
counts QP rounding noise (not units dropped) and varies by platform.
Usage
.n_zero_by_arm(w, A)
Arguments
w |
Numeric vector of SBW weights. |
A |
0/1 treatment vector. |
Value
Named integer vector c(treated = , control = ).
Recode a raw treatment vector to 0/1, recording the level map used
Description
Recode a raw treatment vector to 0/1, recording the level map used
Usage
.recode_treatment(raw)
Arguments
raw |
Raw treatment vector: numeric 0/1, logical, or a two-level factor/character vector. |
Value
A list with A (integer 0/1 vector) and levels (named integer
vector mapping each original level/label to 0 or 1; for numeric input the
names are "0" and "1", and for logical input "FALSE" and "TRUE").
Resolve the outcome formula against the analysis-stage data
Description
Checks that data lines up row-for-row with the baseline data the weights
were fit on (same row count; any column the two share must agree), and
makes Surv() resolve without the user attaching survival.
Usage
.resolve_outcome(outcome, data, object)
Arguments
outcome |
Outcome formula, as passed to |
data |
Analysis-stage data frame, or |
object |
The |
Value
The model response: a numeric vector, or a Surv matrix.
Resolve the treatment argument (column name or vector) against data
Description
Resolve the treatment argument (column name or vector) against data
Usage
.resolve_treatment(treatment_expr, data, env)
Arguments
treatment_expr |
The unevaluated |
data |
Data frame to resolve a column name against. |
env |
Environment to evaluate |
Value
A list with name (character, a label for reporting) and raw
(the resolved vector).
Weighted mean
Description
Weighted mean
Usage
.weighted_mean(x, w)
Weighted generalized-inverse (step-function) quantile
Description
q_hat(u) = inf{y : F_w(y) >= u} for the weighted empirical CDF F_w,
the natural SBW-weighted analogue of stats::quantile(type = 1).
Usage
.weighted_quantile(y, w, probs)
Arguments
y |
Numeric outcome vector for one arm. |
w |
Nonnegative weights, same length as |
probs |
Vector of probabilities in (0, 1). |
Details
Normalizes by sum(w), not length(y). By construction (the balancing
QP's intercept equality constraint), SBW weights for an arm sum to that
arm's size – but only up to solver precision, since the nonneg-QP
fallback's clipping of negative numerical dust can nudge the sum a hair
away from it. Dividing by sum(w) keeps F_w a valid CDF (F_w(Inf) = 1
exactly) regardless, and avoids having to track which arm's size applies
to whatever w subset was passed in.
Value
Numeric vector of estimated quantiles, one per element of probs.
Weighted Mann-Whitney / win-probability estimator
Description
P(Y1 > Y0) + 0.5 P(Y1 = Y0), weighted by SBW weights within each arm.
Finite/uncensored outcomes only.
Usage
.weighted_win_prob(y1, w1, y0, w0)
Arguments
y1, w1 |
Outcome and weights for the treated arm. |
y0, w0 |
Outcome and weights for the control arm. |
Value
Scalar win-probability estimate.
Phrase zero-weight counts, e.g. "6 of 8 treated and 2 of 40 control"
Description
Phrase zero-weight counts, e.g. "6 of 8 treated and 2 of 40 control"
Usage
.zero_weight_phrase(n_zero, n_arm)
Arguments
n_zero, n_arm |
Named integer vectors |
Value
A character string covering the arms with any zero weights.
Bootstrap CI for an SBW-weighted KM survival ratio
Description
Bootstraps the standard error of km_ratio_greenwood()'s
log-ratio, then builds either a Wald CI (log scale) or a percentile CI
(log scale). If the point estimate or bootstrap SE come out non-finite,
falls back to the unadjusted KM ratio and sets MC_fail = TRUE; if even
the unadjusted ratio is undefined (a double-degenerate case with no valid
fallback), throws an error rather than returning a nonsense finite
estimate. If the SBW fit or weighted KM ratio fails on an individual
bootstrap resample, the unadjusted KM ratio is used for that resample;
boot_fail_rate reports the fraction of resamples where this happened.
Usage
boot_km_ratio(
time,
status,
A,
X_subset,
t0,
B = 1500,
alpha = 0.05,
ci_method = c("wald", "percentile"),
seed = NULL
)
Arguments
time |
Event/censoring time. |
status |
Event indicator (1 = event, 0 = censored). |
A |
Treatment indicator (0/1). |
X_subset |
Covariate data frame for the full study sample, used to fit the SBW weights. |
t0 |
Time at which to evaluate the survival ratio. |
B |
Number of bootstrap replicates. |
alpha |
Significance level for the confidence interval (default 0.05). |
ci_method |
Either |
seed |
Optional seed set at the start of the bootstrap. |
Value
A list with MC_fail, log_est, est, se_log, ci_log, ci,
boot_fail_rate, boot_n_finite_reps, and SBW clipping diagnostics
(sbw_n_clipped_full, sbw_max_abs_clipped_full,
sbw_n_clipped_boot_mean, sbw_n_clipped_boot_max,
sbw_max_abs_clipped_boot_max).
Study-level SBW weights: closed-form first, QP fallback if needed
Description
Tries the unconstrained closed-form solve first for each arm
(get_weights_for_group_neg()); if the result is nonnegative within
floating-point tolerance, zeroes out negligible negative dust and returns
it directly. Otherwise falls back to the nonnegative-QP solve
(get_weights_for_group_nonneg()).
Usage
get_sbws_for_study(X_subset, A)
Arguments
X_subset |
Covariate data frame for the full study sample. |
A |
Treatment indicator (0/1) for the full study sample. |
Value
A list with w (numeric vector of weights, one per row of
X_subset), n_clipped, and max_abs_clipped (both 0 if the
closed-form solve was used).
Closed-form SBW group weights (negative weights allowed)
Description
Solves for weights on one treatment-arm's covariate data that exactly
balance its mean to the target X_n, minimizing sum of squared weights,
via the closed-form solution (no nonnegativity constraint).
Usage
get_weights_for_group_neg(data, X_n)
Arguments
data |
Covariate data frame for the group (one treatment arm). |
X_n |
Target covariate mean to balance to. |
Value
Numeric vector of weights, one per row of data.
Nonnegative-QP SBW group weights, with clipping diagnostics
Description
Solves the same balance objective as get_weights_for_group_neg(), but
constrained to nonnegative weights via quadratic programming.
Usage
get_weights_for_group_nonneg(data, X_n)
Arguments
data |
Covariate data frame for the group (one treatment arm). |
X_n |
Target covariate mean to balance to. |
Value
A list with w (numeric vector of weights), n_clipped, and
max_abs_clipped (see .clip_weights()).
Weighted KM survival ratio S1(t0)/S0(t0) with a Greenwood-based CI
Description
Wald CI on the log scale, with se(log S) = se(S) / S for each arm and
se(S) from Greenwood's formula.
Usage
km_ratio_greenwood(time, status, A, t0, alpha = 0.05, weights = NULL)
Arguments
time |
Event/censoring time. |
status |
Event indicator (1 = event, 0 = censored). |
A |
Treatment indicator (0/1; numeric, logical, character, or factor). |
t0 |
Time at which to evaluate the survival ratio. |
alpha |
Significance level for the confidence interval (default 0.05). |
weights |
Optional case weights (e.g. SBW), passed to
|
Value
A list with S0, S1, ratio (= S1(t0)/S0(t0)), log_ratio,
se_log, ci_ratio, and ci_log.
Plot covariate balance before and after SBW weighting
Description
Standardized mean differences (treated vs. control) for each balance covariate, unweighted and SBW-weighted.
Usage
## S3 method for class 'sbw_fit'
plot(x, ...)
Arguments
x |
An |
... |
Passed on to the underlying |
Value
x, invisibly.
Estimate a treatment effect from a fitted sbw_fit object
Description
Estimate a treatment effect from a fitted sbw_fit object
Usage
sbw_estimate(object, outcome, estimand, ...)
## S3 method for class 'sbw_fit'
sbw_estimate(
object,
outcome,
estimand,
data = NULL,
probs = 0.5,
horizon = NULL,
B = 1500,
alpha = 0.05,
ci_method = c("wald", "percentile"),
seed = NULL,
...
)
Arguments
object |
An |
outcome |
A one-sided-response formula naming the outcome, e.g.
|
estimand |
One of |
... |
Passed to methods. |
data |
Optional data frame holding the outcome, for when the weights
were fit on baseline data before outcomes were available. Its rows must
correspond one-to-one, in the same order, to the data passed to
|
probs |
Vector of probabilities in (0, 1), used when |
horizon |
Time |
B |
Number of bootstrap replicates. |
alpha |
Significance level for the confidence interval. |
ci_method |
Either |
seed |
Optional seed set at the start of the bootstrap. |
Details
The bootstrap resamples participants independently (a nonparametric row bootstrap), which is valid under simple randomization. It does not account for stratified or covariate-adaptive randomization (permuted blocks, minimization, biased coin); under those designs the confidence intervals may be miscalibrated.
For estimand = "survival_ratio", if the SBW point estimate or its
bootstrap standard error is non-finite, the unadjusted Kaplan-Meier
ratio is returned instead, with a warning and mc_fail = TRUE.
Value
An object of class sbw_estimate: a list with estimand;
estimate (named by estimand, or q<p> per quantile); se, the
bootstrap standard error (on the log scale when scale = "log"); ci,
a matrix with one row per estimate (lower, upper) on the estimate's own
scale; scale ("log" for "RR", "quantile_ratio", and
"survival_ratio", otherwise "identity"); alpha; and B. Most
estimands also return boot_fail_rate, the fraction of bootstrap
resamples that failed. "survival_ratio" instead returns horizon,
mc_fail, and detail (the full boot_km_ratio() result, which
includes its own boot_fail_rate).
Examples
set.seed(1)
n = 100
trial_baseline = data.frame(
age = rnorm(n, 50, 10),
region = sample(c("N", "S"), n, replace = TRUE),
arm = rbinom(n, 1, 0.5)
)
# Design stage: fit weights before any outcomes exist.
sbw = sbw_weights(~ age + region, data = trial_baseline, treatment = arm)
# Analysis stage: outcomes arrive later, one row per participant, same order.
trial_outcomes = data.frame(Y = rbinom(n, 1, plogis(-1 + 0.02 * trial_baseline$age)))
sbw_estimate(sbw, Y ~ 1, estimand = "RR", data = trial_outcomes, B = 200, seed = 1)
Fit stable balancing weights for a two-arm study (design stage)
Description
Formula-based front end to get_sbws_for_study(): balances the covariates
named in balance (factors dummy-coded automatically) to the pooled-arm
mean, exactly (imbalance tolerance fixed at zero), matching the paper's
SBW specification. Intended to be called before unblinding, using only
baseline covariates and the randomization assignment.
Usage
sbw_weights(balance, data, treatment)
Arguments
balance |
One-sided formula naming the covariates to balance, e.g.
|
data |
Data frame containing the balance covariates and treatment. |
treatment |
Treatment assignment: either a column name in |
Value
An object of class sbw_fit with elements weights, n_clipped,
max_abs_clipped (see get_sbws_for_study()), balance (the formula),
treatment_name, treatment_levels, treatment (recoded 0/1),
X (the balance design matrix), data, n, and call.
Examples
set.seed(1)
n = 100
trial_baseline = data.frame(
age = rnorm(n, 50, 10),
region = sample(c("N", "S"), n, replace = TRUE),
arm = rbinom(n, 1, 0.5)
)
sbw = sbw_weights(~ age + region, data = trial_baseline, treatment = arm)
sbw
summary(sbw)
Summarize an sbw_fit: balance table and weight diagnostics
Description
Summarize an sbw_fit: balance table and weight diagnostics
Usage
## S3 method for class 'sbw_fit'
summary(object, ...)
Arguments
object |
An |
... |
Currently unused. |
Value
An object of class summary.sbw_fit, printed by
print.summary.sbw_fit(), with elements balance (unweighted and
SBW-weighted arm means of each balance column), n_treated, n_control,
and n_zero (units with weight 0, by arm).