This vignette documents the method
implemented by psave() at the level of formulas, records
the small number of places where the package deliberately deviates from
the paper’s reference code (with justification), and situates
psAve among related software. Throughout, “the paper” is
Kabata, Stuart & Shintani (2024).
For subjects \(i = 1, \dots, n\): covariates \(X_i\) (a \(p\)-column numeric matrix after full dummy expansion of factors), binary treatment \(A_i \in \{0, 1\}\), and outcome \(Y_i\). The propensity score is \(e(X_i) = \Pr(A_i = 1 \mid X_i)\). The prognostic score is \(g(0, X_i)\), the predicted outcome under the untreated condition, estimated from untreated units only (Hansen 2008).
psave() fits \(M\)
candidate propensity score models \(\hat e_1,
\dots, \hat e_M\) (on all \(n\)
units) and \(K\) candidate prognostic
models \(\hat g_1, \dots, \hat g_K\)
(on the \(n_0\) untreated units,
predicted for all \(n\)), then selects
convex mixing weights \(\gamma\)
(prognostic) and \(\lambda\)
(propensity) on a simplex grid.
Both \(\gamma\) and \(\lambda\) range over the probability
simplex discretized with increment step (default 0.05). The
exported function simplex_grid(M, step) enumerates the grid
in integer arithmetic: with \(S = 1/\text{step}\) steps, it lists every
integer composition \((c_1, \dots,
c_M)\) with \(\sum_m c_m = S\)
and \(c_m \ge 0\), returned as \(c/S\). The number of grid points is exactly
\(\binom{S + M - 1}{M - 1}\):
library(psAve)
nrow(simplex_grid(2, step = 0.05)) # choose(21, 1) = 21
#> [1] 21
nrow(simplex_grid(3, step = 0.05)) # choose(22, 2) = 231
#> [1] 231
nrow(simplex_grid(4, step = 0.05)) # choose(23, 3) = 1771
#> [1] 1771The enumeration order is a documented part of the method because it defines the tie-breaking rule. The grid is generated by recursive descent, with \(c_1\) running from \(S\) down to 0, then \(c_2\) descending over the remainder, and so on:
head(simplex_grid(3, step = 0.25))
#> [,1] [,2] [,3]
#> [1,] 1.00 0.00 0.00
#> [2,] 0.75 0.25 0.00
#> [3,] 0.75 0.00 0.25
#> [4,] 0.50 0.50 0.00
#> [5,] 0.50 0.25 0.25
#> [6,] 0.50 0.00 0.50
tail(simplex_grid(3, step = 0.25), 3)
#> [,1] [,2] [,3]
#> [13,] 0 0.50 0.50
#> [14,] 0 0.25 0.75
#> [15,] 0 0.00 1.00The first row puts all weight on the first candidate; the last row
puts all weight on the last. Ties in any grid search are
resolved by taking the first row attaining the minimum, within a 1e-9
relative tolerance of the minimum, so ties favor learners
listed earlier in ps.methods / prog.methods.
This makes “first minimum” a reproducible formula rather than an
accident of grid construction. The tolerance is deliberate: the
criterion values are computed with floating-point matrix algebra whose
lowest-order bits can differ across BLAS implementations, so an exact
bitwise which.min() would not be reproducible across
machines, whereas the tolerant first-minimum rule is. (The reference
code used expand.grid() order with
which.min(); the package’s order differs but is equally
deterministic and is fixed by simplex_grid() by
construction.)
The grid size grows quickly with \(M\): with step = 0.05, \(M = 6\) already gives 53,130 points.
psave() warns when the grid exceeds \(10^5\) rows and suggests a coarser
step; in that case the full criterion path is also not
stored.
Candidate propensity score models are fit on all \(n\) units and predict \(\Pr(A = 1 \mid X)\) in-sample. The built-in
engines are pinned to fixed hyperparameters (chosen to mirror the
SuperLearner wrapper defaults used in the paper, so the
direct and SL.* routes agree); they can be overridden per
learner via control =, and the resolved values are stored
in fit$info$learners. Any "SL.*" label is
passed through to SuperLearner verbatim, which is the
exact-replication path for the paper. Alternatively, a user-supplied
matrix of candidate scores can be given via ps.matrix.
Each candidate column is clipped to clip = c(0.01, 0.99)
before averaging (as in the paper). No re-clipping is
applied after averaging: a convex combination of values inside the
clipping interval cannot leave it.
Candidate prognostic models use the same learner menu in regression
mode (or probability mode for family = binomial()), fit
only on untreated units and predicted for all \(n\) units.
\(\gamma\) minimizes the unweighted mean squared prediction error among untreated units:
\[ \hat\gamma = \arg\min_{\gamma \in \mathcal{S}_K} \; \frac{1}{n_0} \sum_{i : A_i = 0} \Bigl( Y_i - \sum_{k=1}^{K} \gamma_k \, \hat g_k(X_i) \Bigr)^2, \]
where \(\mathcal{S}_K\) is the
simplex grid. The model-averaged prognostic score is \(\bar g(X) = \sum_k \hat\gamma_k \hat
g_k(X)\). For family = binomial() the same formula
is the Brier score; note the paper’s simulations validated continuous
outcomes (see Limitations). With a single prognostic candidate, \(\gamma = 1\) and the grid search is
skipped.
For each grid point \(\lambda\), the averaged score is \(\bar e_\lambda(X_i) = \sum_m \lambda_m \hat e_m(X_i)\), and the implied inverse-probability weights are
\[ \text{ATT:}\quad W_i = \begin{cases} 1 & A_i = 1 \\ \dfrac{\bar e_\lambda(X_i)}{1 - \bar e_\lambda(X_i)} & A_i = 0 \end{cases} \qquad\qquad \text{ATE:}\quad W_i = \begin{cases} \dfrac{1}{\bar e_\lambda(X_i)} & A_i = 1 \\ \dfrac{1}{1 - \bar e_\lambda(X_i)} & A_i = 0. \end{cases} \]
\(\lambda\) is selected to minimize one of four criteria, evaluated at every grid point.
criterion = "logloss"The negative Bernoulli log-likelihood of treatment (prediction accuracy, the Xie et al. 2019 lineage):
\[ \mathrm{LL}(\lambda) = -\frac{1}{n} \sum_{i=1}^{n} \Bigl[ A_i \log \bar e_\lambda(X_i) + (1 - A_i) \log\bigl(1 - \bar e_\lambda(X_i)\bigr) \Bigr], \]
finite by clipping. This is the only criterion that does not require
an outcome.
criterion = "smd"The mean over covariate columns \(j\) of the weighted absolute standardized mean difference:
\[ \mathrm{wASMD}_j(\lambda) = \frac{\Bigl| \dfrac{\sum_i A_i W_i X_{ij}}{\sum_i A_i W_i} - \dfrac{\sum_i (1 - A_i) W_i X_{ij}}{\sum_i (1 - A_i) W_i} \Bigr|}{s_j}, \qquad s_j = \mathrm{sd}(X_j \mid A = 1), \]
where \(s_j\) is the plain (unweighted, \(n - 1\) denominator) sample standard deviation in the treated group — for both the ATT and the ATE, per the paper’s supplement (not the pooled SD). Under the ATT, \(W_i = 1\) for treated units, so the first term is the raw treated mean.
criterion = "ks"The mean over covariates of the weighted Kolmogorov–Smirnov statistic, using the proper weighted empirical CDF in each arm:
\[ F^w_a(x) = \frac{\sum_{i : A_i = a} W_i \, \mathbf{1}(X_{ij} \le x)}{\sum_{i : A_i = a} W_i}, \qquad \mathrm{wKS}_j(\lambda) = \max_{x} \bigl| F^w_1(x) - F^w_0(x) \bigr| \]
over the observed values of \(X_j\). For a binary covariate this reduces to the absolute difference in weighted proportions.
criterion = "prog" (the default; the paper’s “Prog
(Ave)”)The wASMD formula above applied to a single column:
the model-averaged prognostic score \(\bar
g\) (when prog.target = "average", the headline
recommendation) or a single candidate \(\hat
g_k\) (when prog.target names a learner — the
paper’s “Prog (\(g_k\))” variants). The
denominator is \(\mathrm{sd}(\bar g \mid A =
1)\), again unweighted and for both estimands.
average = FALSE runs the identical machinery on the
\(M\) vertices of the simplex only,
i.e., it selects the single best candidate propensity score by the
chosen criterion (the “best single learner” variants computed alongside
Table 1 of the paper’s supplement).
One subtlety deserves explicit documentation. The paper (and its
reference implementation) standardizes every covariate —
including binary ones — by the plain sample SD in the treated group.
cobalt, by contrast, standardizes binary covariates by
\(\sqrt{p_1 (1 - p_1)}\) (the
population formula) when it standardizes them at all, and its
bal.tab() display leaves binary covariates as raw
differences in proportions by default.
psAve resolves this as follows:
smd and prog criteria use uniform sample-SD
standardization for all columns (implemented by passing
bin.vars = FALSE for every column to
cobalt::col_w_smd()), so the selected \(\lambda\) and the reported
criterion.value match the published method exactly. For the
ks criterion the two conventions coincide for binary
variables, so auto-detection is harmless there.balance field of the fitted object, summary(),
plot(type = "balance"), and bal.tab() use
cobalt’s native conventions, so the numbers you see agree with what
cobalt::bal.tab() reports for the same matched or weighted
analysis elsewhere in your workflow.For criterion = "prog" the denominator is a single
positive constant across the whole \(\lambda\) grid, so the argmin —
and therefore the selected score — is invariant to this choice; only the
reported criterion value depends on it.
The published algorithm is implemented exactly; the published
reference code contained implementation artifacts that this
package fixes. These are documented so that anyone comparing
psAve output against the original scripts understands where
and why small discrepancies arise.
expand.grid() over \(\{0, 0.05, \dots, 1\}^M\) and kept rows
with rowSums(gr) == 1 — an exact floating-point equality
test. With \(M = 4\) and step 0.05,
this silently dropped 187 of the 1,771 valid simplex points (about
10.6%), including potentially optimal mixtures.
simplex_grid() enumerates integer compositions, so every
valid grid point is present by construction.Fks computed ks.test() on the covariate values
multiplied by the weights within each arm, which is not the
paper’s weighted-eCDF definition (multiplying values by weights changes
the distribution’s support, not its weighting). The package uses the
paper’s definition via cobalt::col_w_ks().binomial() family for binary
responses. The reference code fit binary models inside
SuperLearner with gaussian(link = "logit").
The package uses binomial() throughout for treatment models
(and for binary-outcome prognostic models).scale().
The reference code applied scale() separately to fitting
and prediction sets, standardizing them by different means and SDs. The
package passes raw covariates to all engines (the pinned engines are
either scale-invariant or fit and predict on identical rows).Fasmd applied na.omit() to a covariate vector
while indexing full-length treatment and weight vectors, silently
misaligning rows in the presence of missing data. psave()
refuses incomplete data outright: any NA in the treatment,
the covariates, or the outcome (when used) is an error, never a silent
drop.A sixth, minor note: candidate scores are clipped before averaging
and never re-clipped afterward (see Step 1); the reference code’s
clipping constants 0.01/0.99 are exposed as the clip
argument.
No other R package implements propensity score model averaging over a
mixing-weight simplex (Xie et al. 2019 published no software). The
closest relatives, and how psAve differs:
WeightIt::method_super with
SL.method = "method.balance" (the Balance Super
Learner; Pirracchio & Carone 2018). Also builds a convex combination
of candidate propensity models with weights chosen for balance rather
than prediction. Differences: its criterion is covariate
balance (a user-chosen cobalt-computed statistic), whereas
psAve’s default criterion is prognostic-score
balance, which targets the covariate directions that matter for outcome
bias; the Balance Super Learner optimizes over the continuous simplex
via SuperLearner’s machinery with cross-validated
predictions, whereas psAve uses the paper’s fixed grid with
in-sample candidate predictions and a documented tie-break; and
method_super lives inside a weighting workflow, whereas
psAve returns a bare score vector equally usable for
matching (MatchIt::matchit(distance = )) and
weighting.twang (McCaffrey, Ridgeway &
Morral 2004) tunes a single model class — the number of
boosting iterations of a GBM — against covariate balance. It selects
within one learner; psAve averages across heterogeneous
learners.PSweight provides a broad
estimand/weighting framework (including overlap weights) with a single
user-specified propensity model per analysis; it does model
use, not model selection or averaging.cobalt is not a competitor but the
substrate: the balance statistics that define psAve’s
criteria are computed by cobalt::col_w_smd() and
cobalt::col_w_ks(), and bal.tab() works on
psave objects directly.Honesty about scope, matching the paper’s evidence base:
family = binomial() is
permitted — the \(\gamma\) criterion is
then the Brier score, an unchanged formula — but its finite-sample
performance was not studied in the paper.diagnostics table shows every candidate under every
criterion, and plot(type = "distribution") makes overfit
candidates visible. Balance-targeted selection (unlike log-loss
selection) is not rewarded for overfit treatment prediction,
which is part of the method’s rationale.Hansen, B. B. (2008). The prognostic analogue of the propensity score. Biometrika, 95(2), 481–488. doi:10.1093/biomet/asn004
Kabata, D., Stuart, E. A., & Shintani, A. (2024). Prognostic score-based model averaging approach for propensity score estimation. BMC Medical Research Methodology, 24, 228. doi:10.1186/s12874-024-02350-y
McCaffrey, D. F., Ridgeway, G., & Morral, A. R. (2004). Propensity score estimation with boosted regression for evaluating causal effects in observational studies. Psychological Methods, 9(4), 403–425. doi:10.1037/1082-989X.9.4.403
Pirracchio, R., & Carone, M. (2018). The Balance Super Learner: A robust adaptation of the Super Learner to improve estimation of the average treatment effect in the treated based on propensity score matching. Statistical Methods in Medical Research, 27(8), 2504–2518. doi:10.1177/0962280216682055
Stuart, E. A., Lee, B. K., & Leacy, F. P. (2013). Prognostic score-based balance measures can be a useful diagnostic for propensity score methods in comparative effectiveness research. Journal of Clinical Epidemiology, 66(8 Suppl), S84–S90. doi:10.1016/j.jclinepi.2013.01.013
Xie, Y., Zhu, Y., Cotton, C. A., & Wu, P. (2019). A model averaging approach for estimating propensity scores by optimizing balance. Statistical Methods in Medical Research, 28(1), 84–101. doi:10.1177/0962280217715487