Propensity score matching with causatr

Propensity score matching estimates causal effects by pairing treated and control individuals with similar propensity scores, then comparing outcomes within matched sets. causatr delegates match construction to MatchIt and fits the outcome model on the matched sample. Inference runs through the unified influence-function variance engine (variance_if()): IFs are computed on the matched sample and aggregated cluster-robustly on the matched-pair subclass — the IF analogue of sandwich::vcovCL(), derived in vignettes/variance-theory.qmd §4.3.

This vignette demonstrates propensity score matching in causatr using the NHEFS dataset from Hernán & Robins (2025), covering every supported combination of treatment type, outcome type, contrast scale, and inference method for time-fixed treatments.

Note: Matching supports only static() interventions and binary point treatments. MatchIt itself is binary-only, so categorical (k > 2 levels) and continuous treatments are rejected up front with a clear error — use estimator = "gcomp" or estimator = "ipw" for those. Longitudinal matching is also not supported. For modified treatment policies (shift / scale / threshold / dynamic / MTP), use g-computation (see vignette("gcomp")).

Data: NHEFS

All examples use the NHEFS dataset (Hernán & Robins, 2025). The assumed causal structure is:

  • Treatment (\(A\)): qsmk — quit smoking between 1971 and 1982 (binary: 0/1).
  • Outcome (\(Y\)): wt82_71 — weight change in kg (continuous), or gained_weight (\(\mathbf{1}\{Y > 0\}\), binary).
  • Confounders (\(L\)): sex, age, race, education, smoking intensity, years smoked, exercise, physical activity, and baseline weight — common causes of both \(A\) and \(Y\).

Propensity score matching estimates \(\mathbb{E}[Y^{a=1}] - \mathbb{E}[Y^{a=0}]\) by constructing a matched sample where the distribution of \(L\) is (approximately) balanced across treatment groups, then comparing outcomes within matched sets.

Code
library(causatr)
library(tinytable)
library(tinyplot)

data("nhefs")

nhefs_complete <- nhefs[complete.cases(nhefs), ]

nhefs_complete$gained_weight <- as.integer(nhefs_complete$wt82_71 > 0)

nhefs_complete$sex <- factor(
  nhefs_complete$sex,
  levels = 0:1,
  labels = c("Male", "Female")
)

Binary treatment, continuous outcome

Matching estimate of the effect of quitting smoking on weight change, following the approach in Chapter 15 of Hernán & Robins.

ATT with sandwich SE

Matching naturally targets the ATT (effect on the treated). This is the default for nearest-neighbour matching.

Code
fit_m <- causat(
  nhefs_complete,
  outcome = "wt82_71",
  treatment = "qsmk",
  confounders = ~ sex + age + I(age^2) + race + factor(education) +
    smokeintensity + I(smokeintensity^2) + smokeyrs + I(smokeyrs^2) +
    factor(exercise) + factor(active) + wt71 + I(wt71^2),
  estimator = "matching",
  estimand = "ATT"
)

res_att_sw <- contrast(
  fit_m,
  interventions = list(quit = static(1), continue = static(0)),
  reference = "continue",
  type = "difference",
  ci_method = "sandwich"
)
res_att_sw

Estimator: matching  ·  Estimand: ATT  ·  Contrast: difference  ·  CI method: sandwich  ·  N: 403

Intervention means
intervention estimate se ci_lower ci_upper
quit 4.525 0.435 3.672 5.378
continue 1.184 0.383 0.433 1.935
Contrasts
comparison estimate se ci_lower ci_upper
quit vs continue 3.341 0.559 2.246 4.436

ATT with bootstrap SE

The bootstrap resamples individuals, re-matches, and refits the outcome model on each bootstrap sample, fully accounting for matching uncertainty.

Code
res_att_bs <- contrast(
  fit_m,
  interventions = list(quit = static(1), continue = static(0)),
  reference = "continue",
  type = "difference",
  ci_method = "bootstrap",
  n_boot = 50L
)
res_att_bs

Estimator: matching  ·  Estimand: ATT  ·  Contrast: difference  ·  CI method: bootstrap  ·  N: 403

Intervention means
intervention estimate se ci_lower ci_upper
quit 4.525 0.446 3.65 5.4
continue 1.184 0.376 0.446 1.922
Contrasts
comparison estimate se ci_lower ci_upper
quit vs continue 3.341 0.512 2.338 4.344

ATE estimand

For the ATE, MatchIt performs full matching (each unit gets a weight) rather than 1:1 nearest-neighbour matching.

Code
fit_m_ate <- causat(
  nhefs_complete,
  outcome = "wt82_71",
  treatment = "qsmk",
  confounders = ~ sex + age + I(age^2) + race + factor(education) +
    smokeintensity + I(smokeintensity^2) + smokeyrs + I(smokeyrs^2) +
    factor(exercise) + factor(active) + wt71 + I(wt71^2),
  estimator = "matching",
  estimand = "ATE"
)

res_ate_sw <- contrast(
  fit_m_ate,
  interventions = list(quit = static(1), continue = static(0)),
  reference = "continue",
  type = "difference",
  ci_method = "sandwich"
)
res_ate_sw

Estimator: matching  ·  Estimand: ATE  ·  Contrast: difference  ·  CI method: sandwich  ·  N: 1566

Intervention means
intervention estimate se ci_lower ci_upper
quit 5.442 0.55 4.364 6.519
continue 1.831 0.241 1.359 2.303
Contrasts
comparison estimate se ci_lower ci_upper
quit vs continue 3.611 0.578 2.479 4.743

ATE with bootstrap SE

Code
res_ate_bs <- contrast(
  fit_m_ate,
  interventions = list(quit = static(1), continue = static(0)),
  reference = "continue",
  type = "difference",
  ci_method = "bootstrap",
  n_boot = 50L
)
res_ate_bs

Estimator: matching  ·  Estimand: ATE  ·  Contrast: difference  ·  CI method: bootstrap  ·  N: 1566

Intervention means
intervention estimate se ci_lower ci_upper
quit 5.442 0.541 4.382 6.502
continue 1.831 0.215 1.41 2.251
Contrasts
comparison estimate se ci_lower ci_upper
quit vs continue 3.611 0.535 2.563 4.659

ATT with external survey weights

Pre-computed survey weights are passed via weights = to causat() (either as a numeric vector or as a survey::svydesign object — the latter is unpacked automatically) and validated upfront by check_weights(). Internally causatr multiplies the survey weights by the match weights from MatchIt; the combined weight drives the outcome-model fit on the matched sample and enters the cluster-robust IF variance via prior.weights. The row-alignment between the matched sample and the user-supplied weights is handled by combine_match_and_external_weights() (defined in R/matching.R) so the invariant is asserted in exactly one place.

NoteCluster argument not available for matching

causat(cluster = ...) and contrast(cluster = ...) are rejected with a causatr_bad_cluster abort under estimator = "matching". The matched sample is already aggregated cluster-robustly on subclass (pair or stratum), and a user-supplied design cluster would either conflict with or double-count the within-subclass rows. For a design cluster outside the matched structure (site, household, PSU), use estimator = "gcomp" or "ipw" instead — both expose the same sum-within-cluster-then-square IF aggregation via cluster =.

Similarly, passing a survey::svydesign object with non-trivial PSUs to weights = under matching aborts: the design’s first-stage cluster would land in the cluster slot and hit the same rejection. Use svydesign(ids = ~1, weights = ~pw, data = d) to declare a sampling-weight-only design (no clustering), or supply the weights as a numeric vector.

Code
set.seed(42)
# Synthetic survey weights unrelated to the outcome — demonstrates the
# plumbing without introducing real bias.
svy_w <- runif(nrow(nhefs_complete), 0.5, 2.0)

fit_m_sv <- causat(
  nhefs_complete,
  outcome = "wt82_71",
  treatment = "qsmk",
  confounders = ~ sex + age + I(age^2) + race + factor(education) +
    smokeintensity + I(smokeintensity^2) + smokeyrs + I(smokeyrs^2) +
    factor(exercise) + factor(active) + wt71 + I(wt71^2),
  estimator = "matching",
  estimand = "ATT",
  weights = svy_w
)

res_m_sv <- contrast(
  fit_m_sv,
  interventions = list(quit = static(1), continue = static(0)),
  reference = "continue",
  type = "difference",
  ci_method = "sandwich"
)
res_m_sv

Estimator: matching  ·  Estimand: ATT  ·  Contrast: difference  ·  CI method: sandwich  ·  N: 403

Intervention means
intervention estimate se ci_lower ci_upper
quit 4.576 0.456 3.683 5.469
continue 1.424 0.386 0.667 2.181
Contrasts
comparison estimate se ci_lower ci_upper
quit vs continue 3.152 0.593 1.99 4.315

ATC estimand

The ATC targets the effect among controls (those who continued smoking).

Code
fit_m_atc <- causat(
  nhefs_complete,
  outcome = "wt82_71",
  treatment = "qsmk",
  confounders = ~ sex + age + I(age^2) + race + factor(education) +
    smokeintensity + I(smokeintensity^2) + smokeyrs + I(smokeyrs^2) +
    factor(exercise) + factor(active) + wt71 + I(wt71^2),
  estimator = "matching",
  estimand = "ATC"
)
#> Fewer treated units than control units; not all control units will get
#> a match.

res_atc <- contrast(
  fit_m_atc,
  interventions = list(quit = static(1), continue = static(0)),
  reference = "continue",
  type = "difference",
  ci_method = "sandwich"
)
res_atc

Estimator: matching  ·  Estimand: ATC  ·  Contrast: difference  ·  CI method: sandwich  ·  N: 1163

Intervention means
intervention estimate se ci_lower ci_upper
quit 4.525 0.435 3.672 5.378
continue 3.203 0.355 2.507 3.899
Contrasts
comparison estimate se ci_lower ci_upper
quit vs continue 1.322 0.579 0.187 2.457

Extracting results programmatically

Code
coef(res_att_sw)
#>     quit continue 
#> 4.525079 1.184069
confint(res_att_sw)
#>              lower    upper
#> quit     3.6720225 5.378136
#> continue 0.4328705 1.935267
vcov(res_att_sw)
#>                quit   continue
#> quit     0.18943466 0.01213288
#> continue 0.01213288 0.14689705
tidy(res_att_sw)
#>               term estimate std.error     type conf.low conf.high
#> 1 quit vs continue  3.34101 0.5586286 contrast 2.246118  4.435902
glance(res_att_sw)
#>   estimator estimand contrast_type ci_method   n n_interventions
#> 1  matching      ATT    difference  sandwich 403               2

Binary treatment, binary outcome

Using the gained_weight indicator as a binary outcome. The outcome model on the matched sample is still Y ~ A (linear probability model), so contrast() computes marginal risks from the weighted predictions.

Risk difference (sandwich)

Code
fit_m_bin <- causat(
  nhefs_complete,
  outcome = "gained_weight",
  treatment = "qsmk",
  confounders = ~ sex + age + I(age^2) + race + factor(education) +
    smokeintensity + I(smokeintensity^2) + smokeyrs + I(smokeyrs^2) +
    factor(exercise) + factor(active) + wt71 + I(wt71^2),
  estimator = "matching",
  estimand = "ATT"
)

res_rd <- contrast(
  fit_m_bin,
  interventions = list(quit = static(1), continue = static(0)),
  reference = "continue",
  type = "difference",
  ci_method = "sandwich"
)
res_rd

Estimator: matching  ·  Estimand: ATT  ·  Contrast: difference  ·  CI method: sandwich  ·  N: 403

Intervention means
intervention estimate se ci_lower ci_upper
quit 0.742 0.022 0.699 0.785
continue 0.593 0.024 0.545 0.641
Contrasts
comparison estimate se ci_lower ci_upper
quit vs continue 0.149 0.032 0.087 0.211

Risk difference (bootstrap)

Code
res_rd_bs <- contrast(
  fit_m_bin,
  interventions = list(quit = static(1), continue = static(0)),
  reference = "continue",
  type = "difference",
  ci_method = "bootstrap",
  n_boot = 50L
)
res_rd_bs

Estimator: matching  ·  Estimand: ATT  ·  Contrast: difference  ·  CI method: bootstrap  ·  N: 403

Intervention means
intervention estimate se ci_lower ci_upper
quit 0.742 0.022 0.698 0.786
continue 0.593 0.025 0.544 0.642
Contrasts
comparison estimate se ci_lower ci_upper
quit vs continue 0.149 0.03 0.09 0.208

Risk ratio

Code
res_rr <- contrast(
  fit_m_bin,
  interventions = list(quit = static(1), continue = static(0)),
  reference = "continue",
  type = "ratio",
  ci_method = "sandwich"
)
res_rr

Estimator: matching  ·  Estimand: ATT  ·  Contrast: ratio  ·  CI method: sandwich  ·  N: 403

Intervention means
intervention estimate se ci_lower ci_upper
quit 0.742 0.022 0.699 0.785
continue 0.593 0.024 0.545 0.641
Contrasts
comparison estimate se ci_lower ci_upper
quit vs continue 1.251 0.061 1.136 1.377

Odds ratio

Code
res_or <- contrast(
  fit_m_bin,
  interventions = list(quit = static(1), continue = static(0)),
  reference = "continue",
  type = "or",
  ci_method = "sandwich"
)
res_or

Estimator: matching  ·  Estimand: ATT  ·  Contrast: or  ·  CI method: sandwich  ·  N: 403

Intervention means
intervention estimate se ci_lower ci_upper
quit 0.742 0.022 0.699 0.785
continue 0.593 0.024 0.545 0.641
Contrasts
comparison estimate se ci_lower ci_upper
quit vs continue 1.973 0.291 1.478 2.634

Risk ratio (bootstrap)

Code
res_rr_bs <- contrast(
  fit_m_bin,
  interventions = list(quit = static(1), continue = static(0)),
  reference = "continue",
  type = "ratio",
  ci_method = "bootstrap",
  n_boot = 50L
)
res_rr_bs

Estimator: matching  ·  Estimand: ATT  ·  Contrast: ratio  ·  CI method: bootstrap  ·  N: 403

Intervention means
intervention estimate se ci_lower ci_upper
quit 0.742 0.021 0.7 0.784
continue 0.593 0.029 0.536 0.65
Contrasts
comparison estimate se ci_lower ci_upper
quit vs continue 1.251 0.064 1.132 1.382

Quasibinomial outcome

When binary outcomes exhibit over-dispersion or you want conservative SEs without the strict binomial variance assumption, use family = "quasibinomial":

Code
fit_m_quasi <- causat(
  nhefs_complete,
  outcome = "gained_weight",
  treatment = "qsmk",
  confounders = ~ sex + age + I(age^2) + race + factor(education) +
    smokeintensity + I(smokeintensity^2) + smokeyrs + I(smokeyrs^2) +
    factor(exercise) + factor(active) + wt71 + I(wt71^2),
  estimator = "matching",
  estimand = "ATT",
  family = "quasibinomial"
)

res_quasi <- contrast(
  fit_m_quasi,
  interventions = list(quit = static(1), continue = static(0)),
  reference = "continue",
  type = "difference",
  ci_method = "sandwich"
)
res_quasi

Estimator: matching  ·  Estimand: ATT  ·  Contrast: difference  ·  CI method: sandwich  ·  N: 403

Intervention means
intervention estimate se ci_lower ci_upper
quit 0.742 0.022 0.699 0.785
continue 0.593 0.024 0.545 0.641
Contrasts
comparison estimate se ci_lower ci_upper
quit vs continue 0.149 0.032 0.087 0.211
Code
tt(tidy(res_quasi), digits = 3)
term estimate std.error type conf.low conf.high
quit vs continue 0.149 0.0317 contrast 0.0868 0.211

Comparing estimands

Code
results_list <- list(
  data.frame(
    estimand = "ATT",
    estimate = res_att_sw$contrasts$estimate[1],
    ci_lower = res_att_sw$contrasts$ci_lower[1],
    ci_upper = res_att_sw$contrasts$ci_upper[1]
  ),
  data.frame(
    estimand = "ATC",
    estimate = res_atc$contrasts$estimate[1],
    ci_lower = res_atc$contrasts$ci_lower[1],
    ci_upper = res_atc$contrasts$ci_upper[1]
  )
)
if (requireNamespace("optmatch", quietly = TRUE)) {
  results_list <- c(list(data.frame(
    estimand = "ATE",
    estimate = res_ate_sw$contrasts$estimate[1],
    ci_lower = res_ate_sw$contrasts$ci_lower[1],
    ci_upper = res_ate_sw$contrasts$ci_upper[1]
  )), results_list)
}
est_df <- do.call(rbind, results_list)

tinyplot(
  estimate ~ estimand,
  data = est_df,
  type = "pointrange",
  ymin = est_df$ci_lower,
  ymax = est_df$ci_upper,
  xlab = "Estimand",
  ylab = "Effect on weight change (kg)",
  main = "Matching estimates by estimand"
)
abline(h = 0, lty = 2, col = "grey40")

Point estimates and confidence intervals for ATT and ATC (and ATE if optmatch is available) estimated by matching.

Forwarding MatchIt arguments (CEM)

Any extra arguments passed to causat() flow through ... straight into MatchIt::matchit(). Because the causatr argument is now estimator (not method), MatchIt’s own method = "..." is free and you can pick a non-default matching algorithm. The example below swaps the default nearest-neighbor matcher for coarsened exact matching (Iacus, King & Porro, 2012), which is built into MatchIt and needs no extra packages. Other forwardable values include "optimal" (needs optmatch), "genetic" (needs Matching), "cardinality" (needs an LP solver), and the full MatchIt method list.

Code
fit_cem <- causat(
  nhefs_complete,
  outcome = "wt82_71",
  treatment = "qsmk",
  confounders = ~ sex + age + race + factor(education) +
    smokeintensity + smokeyrs + factor(exercise) + factor(active) + wt71,
  estimator = "matching",
  estimand = "ATT",
  method = "cem"  # forwarded to MatchIt::matchit()
)

res_cem <- contrast(
  fit_cem,
  interventions = list(quit = static(1), continue = static(0)),
  reference = "continue",
  type = "difference",
  ci_method = "sandwich"
)
res_cem

Estimator: matching  ·  Estimand: ATT  ·  Contrast: difference  ·  CI method: sandwich  ·  N: 403

Intervention means
intervention estimate se ci_lower ci_upper
quit 4.446 1.496 1.515 7.378
continue 2.894 1.295 0.357 5.431
Contrasts
comparison estimate se ci_lower ci_upper
quit vs continue 1.552 1.422 -1.235 4.339

Any additional MatchIt knobs (ratio, replace, caliper, distance, k2k, …) can be passed the same way.

Effect modification with by

When confounders includes an A:modifier interaction term, matching expands the outcome MSM from Y ~ A to Y ~ A + modifier + A:modifier. The saturated MSM recovers stratum-specific treatment effects on the matched sample. Use by = "modifier" in contrast() to obtain heterogeneous treatment effects.

Here we add a qsmk:sex interaction to examine how the effect of quitting smoking on weight change differs by sex. The propensity score model used for matching automatically strips the interaction term.

Code
fit_m_hte <- causat(
  nhefs_complete,
  outcome = "wt82_71",
  treatment = "qsmk",
  confounders = ~ sex + age + I(age^2) + race + factor(education) +
    smokeintensity + I(smokeintensity^2) + smokeyrs + I(smokeyrs^2) +
    factor(exercise) + factor(active) + wt71 + I(wt71^2) +
    qsmk:sex,
  estimator = "matching"
)

res_m_by_sex <- contrast(
  fit_m_hte,
  interventions = list(quit = static(1), continue = static(0)),
  reference = "continue",
  type = "difference",
  ci_method = "sandwich",
  by = "sex"
)
res_m_by_sex

Estimator: matching  ·  Estimand: ATE  ·  Contrast: difference  ·  CI method: sandwich  ·  N: 1566

Intervention means
intervention estimate se ci_lower ci_upper by n_by
quit 4.995 0.551 3.915 6.076 Male 762
continue 1.938 0.241 1.465 2.411 Male 762
quit 5.906 0.551 4.825 6.987 Female 804
continue 1.727 0.241 1.254 2.201 Female 804
Contrasts
comparison estimate se ci_lower ci_upper by n_by
quit vs continue 3.057 0.579 1.923 4.191 Male 762
quit vs continue 4.178 0.579 3.045 5.312 Female 804
Note

The matching expansion differs from IPW: because matching weights are subclass indicators (not density-ratio weights), the treatment is not absorbed by the weights. The outcome MSM must therefore include A, modifier, and A:modifier — the full saturated model for the treatment-by-modifier interaction. The propensity score model used for match construction strips the A:modifier term automatically.

Tidy and glance

causatr results work with the broom ecosystem via tidy() and glance():

Code
tidy(res_att_sw)
#>               term estimate std.error     type conf.low conf.high
#> 1 quit vs continue  3.34101 0.5586286 contrast 2.246118  4.435902
glance(res_att_sw)
#>   estimator estimand contrast_type ci_method   n n_interventions
#> 1  matching      ATT    difference  sandwich 403               2

Forest plot

The plot() method produces a forest plot using the forrest package.

Code
plot(res_att_sw)

Forest plot of the matching-estimated ATT of quitting smoking on weight change.

Diagnostics

After fitting a matching model, use diagnose() to assess covariate balance and match quality. Balance diagnostics use cobalt to compare SMDs before and after matching.

Code
diag <- diagnose(fit_m)
diag
#> <causatr_diag>
#>  Estimator:matching
#>  Treatment: binary
#>  Estimand:  ATT
#> 
#> Positivity:
#>       statistic        value
#>          <char>        <num>
#>             min 2.667709e-07
#>             q25 1.739280e-01
#>          median 2.356540e-01
#>             q75 3.232473e-01
#>             max 8.723380e-01
#>   n_below_lower 6.000000e+00
#>   n_above_upper 0.000000e+00
#>    n_violations 6.000000e+00
#>  pct_violations 3.800000e-01
#> 
#> Covariate balance:
#> Balance Measures
#>                          Type Diff.Un V.Ratio.Un Diff.Adj    M.Threshold
#> distance             Distance  0.5697     1.4784   0.0468 Balanced, <0.1
#> sex_Female             Binary -0.1604          .   0.0299 Balanced, <0.1
#> age                   Contin.  0.2771     1.0731  -0.0116 Balanced, <0.1
#> I(age^2)              Contin.  0.2715     1.1632  -0.0123 Balanced, <0.1
#> race                   Binary -0.1993          .   0.0261 Balanced, <0.1
#> factor(education)_0    Binary  0.0394          .  -0.0250 Balanced, <0.1
#> factor(education)_1    Binary -0.0835          .   0.0000 Balanced, <0.1
#> factor(education)_2    Binary  0.0654          .   0.0501 Balanced, <0.1
#> factor(education)_3    Binary  0.0221          .   0.0000 Balanced, <0.1
#> factor(education)_4    Binary -0.0134          .  -0.0289 Balanced, <0.1
#> factor(education)_5    Binary -0.0762          .  -0.0353 Balanced, <0.1
#> factor(education)_6    Binary  0.0461          .   0.0152 Balanced, <0.1
#> factor(education)_7    Binary  0.0025          .   0.0421 Balanced, <0.1
#> factor(education)_8    Binary  0.0386          .  -0.0166 Balanced, <0.1
#> factor(education)_9    Binary  0.0170          .   0.0109 Balanced, <0.1
#> factor(education)_10   Binary -0.0879          .  -0.0184 Balanced, <0.1
#> factor(education)_11   Binary -0.1159          .  -0.0229 Balanced, <0.1
#> factor(education)_12   Binary -0.0475          .  -0.0204 Balanced, <0.1
#> factor(education)_13   Binary  0.0088          .   0.0428 Balanced, <0.1
#> factor(education)_15   Binary -0.0759          .  -0.0410 Balanced, <0.1
#> factor(education)_16   Binary  0.1221          .   0.0088 Balanced, <0.1
#> factor(education)_17   Binary  0.0823          .   0.0298 Balanced, <0.1
#> smokeintensity        Contin. -0.2087     1.1679  -0.0014 Balanced, <0.1
#> I(smokeintensity^2)   Contin. -0.1246     1.1519   0.0148 Balanced, <0.1
#> smokeyrs              Contin.  0.1526     1.1846  -0.0421 Balanced, <0.1
#> I(smokeyrs^2)         Contin.  0.1675     1.3279  -0.0342 Balanced, <0.1
#> factor(exercise)_0     Binary -0.1307          .   0.0068 Balanced, <0.1
#> factor(exercise)_1     Binary  0.0397          .  -0.0851 Balanced, <0.1
#> factor(exercise)_2     Binary  0.0565          .   0.0808 Balanced, <0.1
#> factor(active)_0       Binary -0.0721          .  -0.0251 Balanced, <0.1
#> factor(active)_1       Binary  0.0268          .  -0.0099 Balanced, <0.1
#> factor(active)_2       Binary  0.0706          .   0.0552 Balanced, <0.1
#> wt71                  Contin.  0.1313     1.0606  -0.0126 Balanced, <0.1
#> I(wt71^2)             Contin.  0.1247     1.0876  -0.0139 Balanced, <0.1
#>                      V.Ratio.Adj
#> distance                  1.2040
#> sex_Female                     .
#> age                       0.9930
#> I(age^2)                  1.0026
#> race                           .
#> factor(education)_0            .
#> factor(education)_1            .
#> factor(education)_2            .
#> factor(education)_3            .
#> factor(education)_4            .
#> factor(education)_5            .
#> factor(education)_6            .
#> factor(education)_7            .
#> factor(education)_8            .
#> factor(education)_9            .
#> factor(education)_10           .
#> factor(education)_11           .
#> factor(education)_12           .
#> factor(education)_13           .
#> factor(education)_15           .
#> factor(education)_16           .
#> factor(education)_17           .
#> smokeintensity            1.0721
#> I(smokeintensity^2)       1.0921
#> smokeyrs                  1.0200
#> I(smokeyrs^2)             1.0645
#> factor(exercise)_0             .
#> factor(exercise)_1             .
#> factor(exercise)_2             .
#> factor(active)_0               .
#> factor(active)_1               .
#> factor(active)_2               .
#> wt71                      0.9769
#> I(wt71^2)                 0.9424
#> 
#> Balance tally for mean differences
#>                    count
#> Balanced, <0.1        34
#> Not Balanced, >0.1     0
#> 
#> Variable with the greatest mean difference
#>            Variable Diff.Adj    M.Threshold
#>  factor(exercise)_1  -0.0851 Balanced, <0.1
#> 
#> Sample sizes
#>           Control Treated
#> All          1163     403
#> Matched       403     403
#> Unmatched     760       0
#> 
#> Match quality:
#>     statistic  value
#>        <char>  <num>
#>       n_total 1566.0
#>     n_matched  806.0
#>   n_discarded  760.0
#>  pct_retained   51.5

Love plot

Code
plot(diag)

Love plot showing covariate balance before and after propensity score matching.

Match quality

Code
diag$match_quality
#>       statistic  value
#>          <char>  <num>
#> 1:      n_total 1566.0
#> 2:    n_matched  806.0
#> 3:  n_discarded  760.0
#> 4: pct_retained   51.5

Summary of covered combinations

Legend. ✅ covered and truth-pinned in tests · 🟡 smoke test only · ⛔ rejected with an informative error.

Treatment Outcome Intervention Estimand Contrast Inference Weights Status
Binary Continuous Static ATT Difference Sandwich none
Binary Continuous Static ATT Difference Bootstrap none
Binary Continuous Static ATT Difference Sandwich survey
Binary Continuous Static ATE Difference Sandwich none
Binary Continuous Static ATE Difference Bootstrap none
Binary Continuous Static ATC Difference Sandwich none
Binary Binary Static ATT Difference Sandwich none
Binary Binary Static ATT Difference Bootstrap none
Binary Binary Static ATT Ratio Sandwich none
Binary Binary Static ATT OR Sandwich none
Binary Binary (quasibinomial) Static ATT Difference Sandwich none
Binary Binary Static ATT Ratio Bootstrap none
Binary Gaussian Static ATE (with A:modifier) Difference Sandwich / Bootstrap none
Categorical (k>2) / Continuous ⛔ binary only
Any Dynamic / Shift / Scale / Threshold / IPSI ⛔ Phase 4

See FEATURE_COVERAGE_MATRIX.md for the authoritative coverage status of every method × treatment × outcome × intervention × variance combination.

References

Hernán MA, Robins JM (2025). Causal Inference: What If. Chapman & Hall/CRC. Chapter 15: Outcome regression and propensity scores.

Ho DE, Imai K, King G, Stuart EA (2011). MatchIt: Nonparametric Preprocessing for Parametric Causal Inference. Journal of Statistical Software 42(8):1-28.