Package {sbwadjust}


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:


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 survival::survfit object fit on Surv(time, status) ~ 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 sbw_estimate().

data

Analysis-stage data frame, or NULL to use object$data.

object

The sbw_fit.

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 treatment argument, from substitute(treatment).

data

Data frame to resolve a column name against.

env

Environment to evaluate treatment_expr in if it isn't a column of data (e.g. a vector supplied directly).

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 y.

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 c(treated = , control = ).

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 "wald" (default) or "percentile".

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 survival::survfit(). The weights are treated as fixed and known, so with estimated weights such as SBW, se_log and the CI ignore the uncertainty from estimating them; use boot_km_ratio() for inference.

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 sbw_fit from sbw_weights().

...

Passed on to the underlying graphics::plot() call; these override the defaults (e.g. main = "My trial", xlim = c(-1, 1)).

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 sbw_fit from sbw_weights().

outcome

A one-sided-response formula naming the outcome, e.g. Y ~ 1 (or Surv(time, status) ~ 1 for estimand = "survival_ratio"; Surv() resolves without attaching the survival package).

estimand

One of "ATE", "RR" (relative risk, i.e. ratio of weighted arm means), "survival_ratio", "mann_whitney", "quantile_diff", or "quantile_ratio". No user-supplied functional is accepted; an uncovered estimand means: take weights(object) and run (and bootstrap) your own estimator.

...

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 sbw_weights(); any column the two share is checked for agreement. Defaults to the data the weights were fit on.

probs

Vector of probabilities in (0, 1), used when estimand is "quantile_diff" or "quantile_ratio". Default 0.5 (the median).

horizon

Time t0 at which to evaluate the survival ratio; required for estimand = "survival_ratio".

B

Number of bootstrap replicates.

alpha

Significance level for the confidence interval.

ci_method

Either "wald" (default) or "percentile".

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. ~ age + sex + bmi + region.

data

Data frame containing the balance covariates and treatment.

treatment

Treatment assignment: either a column name in data, unquoted (treatment = arm) or quoted (treatment = "arm"), or a vector. Accepts numeric 0/1, logical, or a two-level factor/character vector. For a factor, the second level is treated (coded 1); for a character vector, the second in alphabetical order. Use a factor with explicit levels to control this.

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 sbw_fit from sbw_weights().

...

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).