Performs 1:1 nearest-neighbour matching without replacement on a
pre-computed propensity score column. Returns a ps_match object whose
$data slot is the full original dataset with a match column set to
1 for matched pairs and 0 for unmatched patients.
Usage
ps_match(
data,
treatment_col = "tavr",
score_col = "prob_t",
id_col = "id",
caliper = NULL,
covariates = NULL,
match_col = "match",
ratio = 1L,
seed = NULL,
treated_level = NULL,
scale = c("probability", "logit"),
caliper_sd = NULL
)Arguments
- data
A data frame. Must contain
treatment_col,score_col, andid_col.- treatment_col
Name of the binary treatment column (0/1 or logical). Default
"tavr".- score_col
Name of the numeric propensity score column, on the scale named by
scale: probabilities in [0, 1] forscale = "probability", or logits (any finite value, such as thelogit_tcolumnps_logistic()appends) forscale = "logit". Default"prob_t".- id_col
Name of the patient identifier column. Default
"id".- caliper
Maximum allowable difference in
score_col, on its own scale, for a match to be accepted.NULL(default) imposes no caliper unlesscaliper_sdis given. Give at most one ofcaliperandcaliper_sd.- covariates
Character vector of covariate columns for SMD diagnostics, used as given. If
NULL(default), every numeric column that varies is used, excepttreatment_col,score_col,id_col,match_coland the fixed columns this package appends (prob_t,logit_t,mt_wt,iptw,pair_id,quintile,decile,bs,cluster).- match_col
Name of the output match-indicator column appended to
$data. Default"match". If the column already exists it is overwritten.- ratio
Matching ratio (controls : treated). Currently only the number
1(1:1) is supported. Default1.- seed
Seed for the random shuffling of treated-patient order that reduces order-dependence.
NULL(default) or one whole number. With a seed, the random number stream is set toabs(seed)immediately before the draws, the result is reproducible whatever ran earlier in the session, the caller's stream is restored afterwards, and the seed is kept in$meta$seed. WithNULLthe draws come from the caller's stream and advance it, and$meta$seedisNA.- treated_level
The value of
treatment_colthat is treated.NULL(default) requires a 0/1 or logical column and takes 1 orTRUE. Give a value, for example"transcatheter", to use a column holding any two values.- scale
The scale
score_colis on, and so the scale distances and the caliper are measured on:"probability"(default) or"logit". The recommended caliper, 0.2 standard deviations of the logit of the propensity score (Austin 2011), needsscale = "logit".- caliper_sd
Caliper in pooled standard-deviation units of
score_col: the caliper applied iscaliper_sd * sqrt((var_t + var_c) / 2), wherevar_tandvar_care the variances ofscore_colamong treated and control patients (missing values dropped).NULL(default) usescaliperinstead.scale = "logit", caliper_sd = 0.2is the Austin (2011) recommendation.
Value
An object of class c("ps_match", "ps_data") with:
$dataThe full input data frame with
match_colandpair_idappended.match_colis1for matched patients and0otherwise.pair_idis an integer identifying which matched pair each patient belongs to (NAfor unmatched patients). Filter tomatch == 1to obtain the matched subset.$metaNamed list:
treatment_col,treated_level(the value oftreatment_coltaken as treated),score_col,id_col,match_col,pair_id_col,scale,caliper(the absolute caliper applied, onscale),caliper_sd,method,seed,n_total,n_excluded(rows with a missing treatment or score),n_matched(the number of matched pairs, so the number of matched treated patients) andn_unmatched(treated patients left without a match; unmatched controls are not counted).$tablesNamed list:
smd_before,smd_after(variable,smd),group_counts_before,group_counts_after(group,treated,n). Group tables name each group by its value intreatment_col(0/1,FALSE/TRUE, or the two values whentreated_levelis given) and carry a logicaltreatedcolumn.
Details
Estimand. Every treated patient looks for a control, so the matched
sample targets the average treatment effect on the treated (ATT). A treated
patient who finds no control within the caliper, or none at all once the
controls run out, is dropped, and the estimand shifts to the treated
patients who could be matched, in practice those inside common support.
$meta$n_unmatched counts them; when it is not zero, say which population
the estimate describes.
The $tables slot contains standardised mean difference (SMD) tables
before and after matching, and group counts. hvtiPlotR::hv_mirror_hist()
reads $data directly. hvtiPlotR::hv_balance() needs the two SMD tables
stacked into long form with variable, group and std_diff (in percent);
the vignette shows the reshape.
A patient with a missing treatment or a missing score cannot be matched.
Those rows stay in $data with match_col set to 0, are left out of the
treated and control pools, the SMD tables and the group counts, and one
warning states how many there are. $meta$n_excluded keeps the count.
References
Austin PC (2011). Optimal caliper widths for propensity-score matching when estimating differences in means and differences in proportions in observational studies. Pharmaceutical Statistics, 10(2), 150–161. doi:10.1002/pst.433
Examples
dta <- sample_ps_data(n = 200, seed = 42)
# 1. Build the matched object
obj <- ps_match(dta)
# 2. Inspect diagnostics
print(obj)
#> <ps_match>
#> N total : 400
#> Treatment : tavr
#> PS column : prob_t
#> Method : nearest-neighbour 1:1
#> Tables : smd_before, smd_after, group_counts_before, group_counts_after
summary(obj)
#> Summary of <ps_match>
#>
#> Smd before:
#> variable smd
#> age age -0.7994
#> female female 0.3989
#> ef ef 0.6139
#> diabetes diabetes -0.0554
#> hypertension hypertension -0.0310
#>
#> Smd after:
#> variable smd
#> age age -0.7994
#> female female 0.3989
#> ef ef 0.6139
#> diabetes diabetes -0.0554
#> hypertension hypertension -0.0310
#>
#> Group counts before:
#> group treated n
#> 1 0 FALSE 200
#> 2 1 TRUE 200
#>
#> Group counts after:
#> group treated n
#> 1 0 FALSE 200
#> 2 1 TRUE 200
#>
# 3. Extract the full data (with match indicator)
head(obj$data)
#> id tavr age female ef diabetes hypertension prob_t match
#> 1 1 0 88.96767 0 51.95379 1 1 0.1112484 1
#> 2 2 0 73.48241 0 59.60242 0 1 0.5270896 1
#> 3 3 0 80.90503 1 52.38991 1 0 0.3951929 1
#> 4 4 0 83.06290 0 59.35072 0 1 0.2836606 1
#> 5 5 0 81.23415 1 50.53527 0 0 0.3751620 1
#> 6 6 0 77.15100 0 51.42113 0 0 0.3148051 1
#> pair_id
#> 1 186
#> 2 45
#> 3 104
#> 4 129
#> 5 3
#> 6 27
table(obj$data$match)
#>
#> 1
#> 400
# 4. Matched subset only
matched <- obj$data[obj$data$match == 1L, ]
nrow(matched)
#> [1] 400
# 5. Match on the logit with a 0.2 SD caliper (Austin 2011)
dta$logit_t <- stats::qlogis(dta$prob_t)
obj_logit <- ps_match(dta, score_col = "logit_t", scale = "logit", caliper_sd = 0.2)
obj_logit$meta$caliper
#> [1] 0.2170102