| Title: | Outcome-Adaptive Lasso Propensity Scores |
|---|---|
| Description: | Estimates propensity scores by the outcome-adaptive lasso of Shortreed and Ertefaie (2017) <doi:10.1111/biom.12679> and the generalized outcome-adaptive lasso (GOAL) of Balde, Yang and Lefebvre (2023) <doi:10.1111/biom.13683>, using 'glmnet' with an exact penalty-scale correction so that the published objectives and tuning grids are reproduced. Tuning is by the weighted absolute mean difference balance criterion. The resulting score is designed to be supplied directly to the matchit() function of 'MatchIt' as a distance measure, to the weightit() function of 'WeightIt' as a propensity score, or to the psave() function of 'psAve' as an appended candidate. |
| Authors: | Daijiro Kabata [aut, cre, cph] |
| Maintainer: | Daijiro Kabata <[email protected]> |
| License: | GPL (>= 2) |
| Version: | 1.0.0 |
| Built: | 2026-07-22 08:19:33 UTC |
| Source: | https://github.com/kabajiro/oalasso |
A method for cobalt::bal.tab(): assesses balance on the (original-scale)
covariates of an oal() fit under the inverse-probability weights implied
by the outcome-adaptive lasso propensity score, which is supplied as a
distance measure.
## S3 method for class 'oal' bal.tab(x, ...)## S3 method for class 'oal' bal.tab(x, ...)
x |
An |
... |
Further arguments passed on to |
The call delegates to the default cobalt machinery as
cobalt::bal.tab(<covariates>, treat = x$treat, weights = x$weights, s.d.denom = <by estimand>, distance = data.frame(ps = x$ps), ...), so all
the usual cobalt arguments (un, stats, thresholds, ...) are
available and display conventions are cobalt's own. The selection
criterion inside oal() is the papers' wAMD instead, computed natively on
the standardized covariates; see oal_wamd().
A bal.tab object; see cobalt::bal.tab().
Shortreed SM, Ertefaie A (2017). Outcome-adaptive lasso: variable selection for causal inference. Biometrics, 73(4), 1111-1122. doi:10.1111/biom.12679
oal(), cobalt::bal.tab(), plot.oal()
data("lalonde", package = "MatchIt") fit <- oal(treat ~ age + educ + married + re74, data = lalonde, outcome = ~ re78) cobalt::bal.tab(fit, un = TRUE)data("lalonde", package = "MatchIt") fit <- oal(treat ~ age + educ + married + re74, data = lalonde, outcome = ~ re78) cobalt::bal.tab(fit, un = TRUE)
Returns the selected outcome-adaptive lasso propensity score coefficients
(intercept first), either on the original covariate scale (default; the
scale used by predict.oal()) or on the standardized scale that the
penalized objective was actually optimized on (the scale of the adaptive
weights and of the wAMD – methods-paper material).
## S3 method for class 'oal' coef(object, scale = c("original", "standardized"), ...)## S3 method for class 'oal' coef(object, scale = c("original", "standardized"), ...)
object |
An |
scale |
|
... |
Ignored. |
A named numeric vector of length p + 1.
oal() estimates a propensity score by the outcome-adaptive lasso (OAL) of
Shortreed and Ertefaie (2017) or its generalized elastic-net form (GOAL) of
Balde, Yang and Lefebvre (2023): an adaptive lasso on the treatment
(propensity score) log-likelihood whose penalty weights come from an
outcome regression, so that covariates unrelated to the outcome –
instruments and noise variables – are excluded from the propensity score
model. Tuning parameters are selected by the papers' weighted absolute mean
difference (wAMD) balance criterion. The result is deliberately modest: a
numeric score vector designed to be handed to MatchIt::matchit() as
distance, to WeightIt::weightit() as ps, or to psAve::psave() as an
appended candidate.
oal( formula, data, outcome, method = c("oal", "goal"), estimand = c("ATE", "ATT"), family = gaussian(), lambda = c(-10, -5, -2, -1, -0.75, -0.5, -0.25, 0.25, 0.49), gamma = NULL, gamma.factor = 2, lambda2 = c(0, 10^c(-2, -1.5, -1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 1)), outcome.coef = NULL, refit = FALSE, clip = c(0.01, 0.99), keep.path = TRUE, keep.fits = FALSE, verbose = FALSE, ... )oal( formula, data, outcome, method = c("oal", "goal"), estimand = c("ATE", "ATT"), family = gaussian(), lambda = c(-10, -5, -2, -1, -0.75, -0.5, -0.25, 0.25, 0.49), gamma = NULL, gamma.factor = 2, lambda2 = c(0, 10^c(-2, -1.5, -1, -0.75, -0.5, -0.25, 0, 0.25, 0.5, 1)), outcome.coef = NULL, refit = FALSE, clip = c(0.01, 0.99), keep.path = TRUE, keep.fits = FALSE, verbose = FALSE, ... )
formula |
A two-sided formula |
data |
A data frame containing the variables in |
outcome |
A one-sided formula |
method |
|
estimand |
|
family |
The outcome (weight) model family: |
lambda |
Numeric vector of EXPONENTS |
gamma |
|
gamma.factor |
The convergence factor |
lambda2 |
Numeric grid of elastic-net ridge constants for
|
outcome.coef |
The extension hook: a NAMED numeric vector of outcome
coefficients |
refit |
|
clip |
Length-2 numeric: propensity scores are clipped to
|
keep.path |
If |
keep.fits |
If |
verbose |
If |
... |
Reserved for future use; supplying unused arguments triggers a warning. |
OAL solves, on the TOTAL log-likelihood scale,
where is the binomial log-likelihood of treatment on the
standardized covariates and are the outcome-model coefficients.
glmnet minimizes the per-observation loss and internally rescales
penalty factors to sum to the number of variables, so each grid point is
evaluated exactly at
via coef(fit, s = s, exact = TRUE, x = , y = , penalty.factor = ), which
re-solves at that penalty (the KKT-verified recipe of the
Shortreed/Ertefaie rejoinder code). When outcome.coef contains zeros,
some : glmnet converts those to exclusions and
internally resets their factor to 1 before the sum-to-nvars rescale, so
the constant generalizes to
– identical to
when all factors are finite.
This correction is always on; it is what makes the grid
carry its published meaning.
The model matrix X <- model.matrix(formula, data)[, -1] (default
treatment contrasts) is standardized ONCE, Xs <- scale(X, TRUE, TRUE),
all columns including dummies (the rejoinder convention). The outcome model
AND glmnet are both fit on this SAME Xs with
glmnet(standardize = FALSE). Per-subset scaling is structurally
impossible. Coefficients are back-transformed for output:
and
, with
centers and scales stored in info.
By default comes from lm(y ~ A + Xs) (or
glm(y ~ A + Xs, binomial)) on the FULL sample, both arms pooled – the
Shortreed-Ertefaie/rejoinder convention (not control-arm-only). Only the
covariate coefficients are used; the treatment coefficient is discarded and
the treatment is never penalized anywhere. Any NA or exactly zero
internal coefficient is an ERROR naming the columns: OLS betas are almost
surely nonzero, so an exact zero or NA signals rank deficiency or
aliasing that would silently corrupt the penalty-scale correction. Remove
the collinear columns or supply outcome.coef.
pairinglambda supplies exponents with
, n = nrow(model.matrix) always. With
gamma = NULL each is paired with
(default
), the formula derivable from
; there are no
cross-products in paired mode. A scalar
gamma is crossed with all (Schnitzer's
is reachable this way; convert their raw grid via
).
For the elastic-net term is solved by the Zou-Hastie
augmentation:
with pseudo-responses 0, followed by rescaling ALL coefficients
INCLUDING the intercept by (Balde 2025 supplement:
);
the propensity score uses the original rows only. On augmented
grid points the raw penalty constant is
with augmentation rows
(Balde's adaptive.lasso(lambda = n.q^(il)), n.q = n + q), and the
penalty-scale constant uses in place of , i.e.
.
grid points run the plain-OAL code path (no
augmentation, ), so method = "goal" with
lambda2 = 0 nests method = "oal" exactly. (Balde's script literally
augments with zero rows and the base even at
; the zero rows still shift the intercept, so exact
nesting is deliberately preferred here.) is selected
jointly with by the wAMD: the flat first-minimum
over the -outer -inner grid,
which is equivalent to Balde's nested rule (per- minima
over , then the first minimum over
).
Every grid point is scored by oal_wamd(): propensity scores clipped to
clip first (note that Balde's reference implementation is unclipped;
see the clip argument), IPW weights at estimand, per-column weighted
absolute mean differences on the standardized matrix summed with weights
over ALL columns (including excluded ones). The selected grid point is the
FIRST minimum (relative tolerance 1e-9) in the grid order
ascending (0 first) outer
ascending inner – ties therefore prefer plain OAL and then the smallest
penalty. The wAMD is a balance heuristic: no published proof guarantees it
lands inside the window required by the theory
(,
, ), and it
inherits the outcome model's misspecification vulnerability.
Covariates that predict treatment but not outcome should be excluded from a propensity score model: they increase the variance of the effect estimate without decreasing bias (Brookhart et al. 2006) and amplify the bias from any unmeasured confounding (Myers et al. 2011). OAL's penalty implements exactly this exclusion.
print() and summary() always print a provenance line first:
"OAL (Shortreed & Ertefaie 2017, doi:10.1111/biom.12679)" or
"GOAL (Balde, Yang & Lefebvre 2023, doi:10.1111/biom.13683)" with a
clause stating whether the lambda2 grid is the author's published grid
(Balde 2025 supplement) or user-specified. Supplying outcome.coef appends
"user-supplied outcome coefficients — screening use only; no oracle property"; family = binomial() and refit = TRUE append their own
flags.
$ps is a numeric vector of length n, named by rownames(data),
strictly inside (0, 1) after clipping – it satisfies
psAve::psave(ps.append = ), MatchIt::matchit(distance = ), and
WeightIt::weightit(ps = ) by construction.
There is no seed argument: the whole pipeline is deterministic.
An object of class "oal": a list with components
psnumeric(n), named by rownames(data): the propensity score,
clipped to clip, strictly in (0, 1) – the deliverable.
weightsnumeric(n): the IPW weights at estimand implied by
ps.
coefficientsnamed numeric(p+1): selected propensity score coefficients on the ORIGINAL covariate scale (intercept first).
coefficients.stdthe same on the standardized scale actually optimized.
selecteddata frame, one row per model-matrix column: term,
outcome.coef (signed , standardized scale),
penalty.factor, coef (original scale), selected (logical), and
role ("retained"/"excluded").
lambdanamed numeric: delta, lambda.n (;
on GOAL grid points with ),
gamma, lambda2 (NA for method = "oal") at the selected point.
criterion, criterion.value
"wamd" and its value at the
selection.
pathdata frame (or NULL without keep.path), one row per
grid point in grid order: lambda2, delta, lambda.n, gamma, s
(the exact glmnet s used), wamd, n.selected, refit.converged
(NA unless refit), selected.
setslist (or NULL): the selected variable set at every grid
point.
coef.path(p+1) x G matrix (or NULL): propensity score
coefficients across the grid, original scale.
outcome.coefnamed numeric(p): the actually used
(internal or user), standardized scale.
outcome.modelcompact list: coefficient table, family, and a
label ("lm(y ~ A + X)", "glm(y ~ A + X, binomial)", or
"user-supplied"); the full fit object only under keep.fits.
balancedata frame per ORIGINAL covariate column: smd.un,
smd.wt, ks.un, ks.wt via cobalt – identical layout to
psAve::psave()$balance.
provenancethe fixed provenance label, printed first by
print()/summary().
treatinteger(n) 0/1 treatment as coerced.
covsn x p numeric original-scale model matrix with
attr(, "bin.vars").
estimand, method, refit, clip
as resolved.
formula, data, terms, xlevels
stored to power
oal_match(), oal_weight() and predict.oal().
fitslist (or NULL): glmnet (one path per
), outcome, refit – iff keep.fits.
infolist: n, n.treated, p, grid.size, family,
gamma.mode ("paired"/"fixed"/"user-coef"), center, scale,
contrasts, glmnet.version, oalasso.version.
callthe matched call.
Shortreed SM, Ertefaie A (2017). Outcome-adaptive lasso: variable selection for causal inference. Biometrics, 73(4), 1111-1122. doi:10.1111/biom.12679
Balde I, Yang Y, Lefebvre G (2023). Reader reaction to "Outcome-adaptive lasso: variable selection for causal inference" by Shortreed and Ertefaie (2017). Biometrics, 79(1), 514-520. doi:10.1111/biom.13683
Jones B, Ertefaie A, Shortreed SM (2023). Rejoinder to reader reaction "On the use of the outcome-adaptive lasso for propensity score estimation". Biometrics, 79(1), 521-525. doi:10.1111/biom.13681
Balde I (2025). Oracle properties of the generalized outcome-adaptive lasso. Statistics & Probability Letters. doi:10.1016/j.spl.2025.110379
Zou H (2006). The adaptive lasso and its oracle properties. Journal of the American Statistical Association, 101(476), 1418-1429. doi:10.1198/016214506000000735
Brookhart MA, Schneeweiss S, Rothman KJ, Glynn RJ, Avorn J, Sturmer T (2006). Variable selection for propensity score models. American Journal of Epidemiology, 163(12), 1149-1156. doi:10.1093/aje/kwj149
Myers JA, Rassen JA, Gagne JJ, Huybrechts KF, Schneeweiss S, Rothman KJ, Joffe MM, Glynn RJ (2011). Effects of adjusting for instrumental variables on bias and precision of effect estimates. American Journal of Epidemiology, 174(11), 1213-1222. doi:10.1093/aje/kwr364
Schnitzer ME, Talbot D, Liu Y, Berger C, Wang G, O'Loughlin J, Sylvestre M-P, Ertefaie A (2025). Outcome-adaptive LASSO for longitudinal data. Statistics in Medicine (arXiv:2410.08283).
oal_match(), oal_weight(), oal_wamd(), predict.oal(),
plot.oal(), bal.tab.oal()
data("lalonde", package = "MatchIt") ## 1) Matching fit <- oal(treat ~ age + educ + race + married + nodegree + re74 + re75, data = lalonde, outcome = ~ re78) fit m <- oal_match(fit) # = MatchIt::matchit(f, lalonde, distance = fit$ps) ## 2) Weighting (requires WeightIt) # w <- oal_weight(fit) # = WeightIt::weightit(f, lalonde, ps = fit$ps, "ATE") ## 3) psAve composition (requires psAve); cbind() labels the candidate ## while keeping the row names aligned with rownames(data) # ma <- psAve::psave(treat ~ age + educ + race + married + nodegree + # re74 + re75, # data = lalonde, outcome = ~ re78, # ps.append = cbind(oal = fit$ps))data("lalonde", package = "MatchIt") ## 1) Matching fit <- oal(treat ~ age + educ + race + married + nodegree + re74 + re75, data = lalonde, outcome = ~ re78) fit m <- oal_match(fit) # = MatchIt::matchit(f, lalonde, distance = fit$ps) ## 2) Weighting (requires WeightIt) # w <- oal_weight(fit) # = WeightIt::weightit(f, lalonde, ps = fit$ps, "ATE") ## 3) psAve composition (requires psAve); cbind() labels the candidate ## while keeping the row names aligned with rownames(data) # ma <- psAve::psave(treat ~ age + educ + race + married + nodegree + # re74 + re75, # data = lalonde, outcome = ~ re78, # ps.append = cbind(oal = fit$ps))
Convenience pass-through to MatchIt::matchit(): matches on the propensity
score of an oal() fit, reusing the formula and data stored in the object.
Equivalent to the canonical explicit call
MatchIt::matchit(<formula>, data = <data>, distance = fit$ps, ...)
but with no opportunity for row misalignment between the two steps. All
... arguments are forwarded verbatim; the return value is an ordinary
matchit object, so the full MatchIt/cobalt toolkit applies.
oal_match(object, ...)oal_match(object, ...)
object |
An |
... |
Arguments forwarded verbatim to |
A matchit object; see MatchIt::matchit().
oal(), oal_weight(), MatchIt::matchit()
data("lalonde", package = "MatchIt") fit <- oal(treat ~ age + educ + married + re74, data = lalonde, outcome = ~ re78) m <- oal_match(fit, method = "nearest", caliper = 0.2)data("lalonde", package = "MatchIt") fit <- oal(treat ~ age + educ + married + re74, data = lalonde, outcome = ~ re78) m <- oal_match(fit, method = "nearest", caliper = 0.2)
Scores an arbitrary candidate propensity score on the exact wAMD balance
criterion used by oal() to select its tuning parameters. This function is
the package's single source of truth for the criterion: the internal
grid search calls it verbatim, and exporting it lets any candidate score –
from any package – be scored on the same yardstick (the mirror of
psAve::psave_criteria()).
oal_wamd( ps, treat, covs, coef, estimand = c("ATE", "ATT"), clip = c(0.01, 0.99) )oal_wamd( ps, treat, covs, coef, estimand = c("ATE", "ATT"), clip = c(0.01, 0.99) )
ps |
Numeric vector of propensity scores, strictly inside (0, 1) before clipping. |
treat |
Treatment vector: numeric 0/1, logical, or a two-level
factor/character (second level = treated), as in |
covs |
Numeric matrix of covariates, one row per unit; no missing values. |
coef |
Numeric vector of outcome-model coefficients, one per column of
|
estimand |
|
clip |
Length-2 numeric; the scores are clipped to
|
The propensity scores are first clipped to clip; the inverse-probability
weights at estimand are then (with the clipped score)
For each covariate column the weighted absolute mean difference is
and the criterion is , summed
over all columns of covs (including any the propensity score model
excluded), with the outcome-model coefficients supplied in
coef (their absolute values are taken internally).
Inside oal(), covs is the standardized model matrix and coef the
outcome coefficients on that same standardized scale (the wAMD, like the
adaptive weights, is scale-dependent). The formula is implemented natively
– never delegated to cobalt – so that it reproduces the published
criterion exactly.
A list with elements
totalthe wAMD, ;
by.covariatenamed numeric vector of the per-covariate
contributions (they sum to total).
Shortreed SM, Ertefaie A (2017). Outcome-adaptive lasso: variable selection for causal inference. Biometrics, 73(4), 1111-1122. doi:10.1111/biom.12679
set.seed(7) X <- matrix(rnorm(200), 100, 2, dimnames = list(NULL, c("x1", "x2"))) A <- rbinom(100, 1, plogis(X[, 1])) ps <- plogis(0.8 * X[, 1]) oal_wamd(ps, A, X, coef = c(x1 = 0.5, x2 = 0.1), estimand = "ATE")set.seed(7) X <- matrix(rnorm(200), 100, 2, dimnames = list(NULL, c("x1", "x2"))) A <- rbinom(100, 1, plogis(X[, 1])) ps <- plogis(0.8 * X[, 1]) oal_wamd(ps, A, X, coef = c(x1 = 0.5, x2 = 0.1), estimand = "ATE")
Convenience pass-through to WeightIt::weightit(): constructs balancing
weights from the propensity score of an oal() fit, reusing the stored
formula and data. Equivalent to the canonical explicit call
WeightIt::weightit(<formula>, data = <data>, ps = fit$ps, estimand = <estimand>, ...)
All ... arguments are forwarded verbatim; the return value is an ordinary
weightit object.
oal_weight(object, estimand = object$estimand, ...)oal_weight(object, estimand = object$estimand, ...)
object |
An |
estimand |
The estimand passed to |
... |
Arguments forwarded verbatim to |
A weightit object; see WeightIt::weightit().
oal(), oal_match(), WeightIt::weightit(),
WeightIt::get_w_from_ps()
data("lalonde", package = "MatchIt") fit <- oal(treat ~ age + educ + married + re74, data = lalonde, outcome = ~ re78) w <- oal_weight(fit)data("lalonde", package = "MatchIt") fit <- oal(treat ~ age + educ + married + re74, data = lalonde, outcome = ~ re78) w <- oal_weight(fit)
Three diagnostic displays for an oal() fit:
"wamd"the wAMD selection criterion against the exponent
(lambda), one curve per lambda2 value for
method = "goal"; the selected point is marked. Requires
keep.path = TRUE.
"coef"the original-scale propensity score coefficient paths
against at the selected lambda2, one line per covariate;
covariates driven to zero (instruments, noise) are visible as lines
absorbed into the axis. Requires keep.path = TRUE.
"balance"a Love plot of covariate balance before/after
weighting, via cobalt::love.plot() (dispatched through
bal.tab.oal(); cobalt is an Import, so always available).
## S3 method for class 'oal' plot(x, type = c("wamd", "coef", "balance"), ...)## S3 method for class 'oal' plot(x, type = c("wamd", "coef", "balance"), ...)
x |
An |
type |
One of |
... |
For |
For "balance", the ggplot object from cobalt::love.plot()
(invisibly, after printing); otherwise x, invisibly.
oal(), bal.tab.oal(), cobalt::love.plot()
Computes propensity scores (or the linear predictor) for new observations
from the stored terms, factor levels, and ORIGINAL-scale coefficients of an
oal() fit. Works without keep.fits = TRUE and for both refit modes;
the training standardization is never re-estimated (the original-scale
coefficients already absorb it). Missing values in newdata are an error.
## S3 method for class 'oal' predict(object, newdata = NULL, type = c("ps", "link"), ...)## S3 method for class 'oal' predict(object, newdata = NULL, type = c("ps", "link"), ...)
object |
An |
newdata |
A data frame containing the variables of the propensity
score formula, or |
type |
|
... |
Ignored. |
A numeric vector with one value per row, named by rownames. For
newdata = NULL and type = "ps" this is exactly object$ps.
Prints a one-screen teaching summary of a fitted oal() object: the
provenance label first (which method, which tuning provenance), the sample,
the selected tuning point and its wAMD, the retained
(outcome-related) versus excluded (outcome-unrelated) covariates with their
outcome-coefficient magnitudes, the fixed instrument-exclusion rationale,
and then the literal next call – echoing the formula and data name
from your own oal() call – that hands the score to
MatchIt::matchit() or WeightIt::weightit(). A near-positivity
diagnostic is appended when more than 5\
clipping bound.
## S3 method for class 'oal' print(x, digits = 3, ...)## S3 method for class 'oal' print(x, digits = 3, ...)
x |
An |
digits |
Number of significant digits to print. Default 3. |
... |
Ignored. |
x, invisibly.
Produces (a) the full per-covariate selection table (selected), (b) a
path summary (the best grid row per lambda2 value, when the path was
kept), (c) the outcome (weight) model coefficient table, (d) the full
balance table (unweighted vs. weighted SMD and KS, with a * marker at
weighted SMD > 0.1), and (e) the near-positivity clip diagnostic.
## S3 method for class 'oal' summary(object, ...) ## S3 method for class 'summary.oal' print(x, digits = 3, ...)## S3 method for class 'oal' summary(object, ...) ## S3 method for class 'summary.oal' print(x, digits = 3, ...)
object |
An |
... |
Ignored. |
x |
A |
digits |
Number of significant digits to print. Default 3. |
For summary.oal(), an object of class "summary.oal": a list
with elements call, provenance, method, estimand, refit,
lambda, criterion, criterion.value, selected, path.summary,
outcome.model, balance, clip, clip.share, and nn.
print.summary.oal() returns x invisibly.
oal(), print.oal(), bal.tab.oal()
Returns object$weights: the IPW weights at the fitted estimand implied
by the clipped propensity score (ATE:
; ATT: ). For other
estimands use WeightIt::get_w_from_ps(object$ps, object$treat, estimand = ...) or oal_weight().
## S3 method for class 'oal' weights(object, ...)## S3 method for class 'oal' weights(object, ...)
object |
An |
... |
Ignored. |
The numeric vector object$weights, named by rownames(data).
oal(), oal_weight(), oal_wamd()