--- title: "Method details and design decisions" author: "Daijiro Kabata" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Method details and design decisions} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` 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). ## Notation 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. ## The simplex grid and tie-breaking 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}$: ```{r} library(psAve) nrow(simplex_grid(2, step = 0.05)) # choose(21, 1) = 21 nrow(simplex_grid(3, step = 0.05)) # choose(22, 2) = 231 nrow(simplex_grid(4, step = 0.05)) # choose(23, 3) = 1771 ``` The 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: ```{r} head(simplex_grid(3, step = 0.25)) tail(simplex_grid(3, step = 0.25), 3) ``` The 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 `r format(choose(25, 5), big.mark = ",")` 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. ## Step 1: Candidate models 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. ## Step 2: $\gamma$ — the model-averaged prognostic score $\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. ## Step 3: $\lambda$ — the model-averaged propensity score 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. ### Vertex mode `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). ## 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 $\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: * **The selection criteria are paper-faithful.** The `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. * **The display follows cobalt.** The `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. ## 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. 1. **Integer simplex grid.** The reference code built the grid with `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. 2. **Proper weighted-eCDF KS.** The reference `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()`. 3. **`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). 4. **No train/test-inconsistent `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). 5. **Strict complete-case handling.** The reference `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. ## 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_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. ## 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 $Y$. `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. * **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 `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. * **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