Method details and design decisions
Daijiro Kabata
Source:vignettes/method-details.Rmd
method-details.RmdThis 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).
Notation
For subjects : covariates (a -column numeric matrix after full dummy expansion of factors), binary treatment , and outcome . The propensity score is . The prognostic score is , the predicted outcome under the untreated condition, estimated from untreated units only (Hansen 2008).
psave() fits
candidate propensity score models
(on all
units) and
candidate prognostic models
(on the
untreated units, predicted for all
),
then selects convex mixing weights
(prognostic) and
(propensity) on a simplex grid.
The simplex grid and tie-breaking
Both
and
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
steps, it lists every integer composition
with
and
,
returned as
.
The number of grid points is exactly
:
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 running from down to 0, then 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
:
with step = 0.05,
already gives 53,130 points. psave() warns when the grid
exceeds
rows and suggests a coarser step; in that case the full
criterion path is also not stored.
Step 1: Candidate models
Candidate propensity score models are fit on all
units and predict
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
units.
Step 2: — the model-averaged prognostic score
minimizes the unweighted mean squared prediction error among untreated units:
where
is the simplex grid. The model-averaged prognostic score is
.
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,
and the grid search is skipped.
Step 3: — the model-averaged propensity score
For each grid point , the averaged score is , and the implied inverse-probability weights are
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):
finite by clipping. This is the only criterion that does not require
an outcome.
criterion = "smd"
The mean over covariate columns of the weighted absolute standardized mean difference:
where is the plain (unweighted, 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, 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:
over the observed values of . 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
(when prog.target = "average", the headline recommendation)
or a single candidate
(when prog.target names a learner — the paper’s “Prog
()”
variants). The denominator is
,
again unweighted and for both estimands.
Criterion vs. display conventions for the SMD denominator
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
(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:
-
The selection criteria are paper-faithful. The
smdandprogcriteria use uniform sample-SD standardization for all columns (implemented by passingbin.vars = FALSEfor every column tocobalt::col_w_smd()), so the selected and the reportedcriterion.valuematch the published method exactly. For thekscriterion the two conventions coincide for binary variables, so auto-detection is harmless there. -
The display follows cobalt. The
balancefield of the fitted object,summary(),plot(type = "balance"), andbal.tab()use cobalt’s native conventions, so the numbers you see agree with whatcobalt::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
grid, so the argmin — and therefore the selected score — is
invariant to this choice; only the reported criterion value depends on
it.
Five documented fixes relative to the paper’s reference code
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.
-
Integer simplex grid. The reference code built the
grid with
expand.grid()over and kept rows withrowSums(gr) == 1— an exact floating-point equality test. With 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. -
Proper weighted-eCDF KS. The reference
Fkscomputedks.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 viacobalt::col_w_ks(). -
binomial()family for binary responses. The reference code fit binary models insideSuperLearnerwithgaussian(link = "logit"). The package usesbinomial()throughout for treatment models (and for binary-outcome prognostic models). -
No train/test-inconsistent
scale(). The reference code appliedscale()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). -
Strict complete-case handling. The reference
Fasmdappliedna.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: anyNAin 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.
Relation to other software
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_superwithSL.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-chosencobalt-computed statistic), whereaspsAve’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 viaSuperLearner’s machinery with cross-validated predictions, whereaspsAveuses the paper’s fixed grid with in-sample candidate predictions and a documented tie-break; andmethod_superlives inside a weighting workflow, whereaspsAvereturns 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;psAveaverages across heterogeneous learners. -
PSweightprovides 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. -
cobaltis not a competitor but the substrate: the balance statistics that definepsAve’s criteria are computed bycobalt::col_w_smd()andcobalt::col_w_ks(), andbal.tab()works onpsaveobjects directly.
Limitations
Honesty about scope, matching the paper’s evidence base:
- Binary treatment only. The method is defined, and was evaluated, for a two-arm comparison. Multi-category and continuous treatments are out of scope.
- ATT and ATE only. The supplement’s criterion formulas are validated for these two estimands; other estimands (ATO, ATM, …) are refused with an error rather than extrapolated.
-
Continuous outcomes in the validating simulations.
The paper’s simulations used continuous
.
family = binomial()is permitted — the criterion is then the Brier score, an unchanged formula — but its finite-sample performance was not studied in the paper. -
In-sample candidate predictions. As in the paper’s
worked example, candidate models predict on their own training data (no
cross-fitting in v1). Flexible learners can therefore produce
optimistically extreme candidate scores; clipping bounds the damage, the
diagnosticstable shows every candidate under every criterion, andplot(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. - Design-based standard errors downstream do not reflect score estimation. See the weighting vignette; bootstrap the full pipeline if that matters for your application.
References
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