Attaching package: 'MatchIt'
The following object is masked from 'package:cobalt':
lalonde
To perform matching, we’ll use the MatchIt
package, which provides an interface to many forms of matching and allows for specification of many different options to customize the matching. The MatchIt
documentation includes extensive examples and vignettes that should be used to supplement the example here. Much of the information here is simply lifted from this documentation.
Attaching package: 'MatchIt'
The following object is masked from 'package:cobalt':
lalonde
For matching, we’ll focus on the ATT, though it is possible for some matching methods to target the ATE as well. The simplest method of matching is 1:1 nearest neighbor propensity score matching, which is the default using matchit()
. For more details on this procedure, including effect estimation, see the MatchIt
documentation and vignettes.
#1:1 NN propensity score matching w/o replacement
m1 <- matchit(RHC ~ aps1 + meanbp1 + pafi1 + crea1 + hema1 +
paco21 + surv2md1 + resp1 + card + edu +
age + race + sex, data = rhc)
m1
A matchit object
- method: 1:1 nearest neighbor matching without replacement
- distance: Propensity score
- estimated with logistic regression
- number of obs.: 5735 (original), 4368 (matched)
- target estimand: ATT
- covariates: aps1, meanbp1, pafi1, crea1, hema1, paco21, surv2md1, resp1, card, edu, age, race, sex
We can use summary()
in MatchIt
to assess balance, but we’ll stick with cobalt
. We can just supply the matchit
object to bal.tab()
, which contains the treatment and covariate information.
bal.tab(m1, stats = c("m", "ks"), binary = "std")
Balance Measures
Type Diff.Adj KS.Adj
distance Distance 0.3765 0.2060
aps1 Contin. 0.2109 0.1103
meanbp1 Contin. -0.1447 0.0728
pafi1 Contin. -0.1650 0.0907
crea1 Contin. 0.1296 0.1282
hema1 Contin. -0.0494 0.0614
paco21 Contin. -0.0699 0.0453
surv2md1 Contin. -0.1087 0.0710
resp1 Contin. -0.1003 0.0755
card_Yes Binary 0.1270 0.0627
edu Contin. 0.0307 0.0343
age Contin. -0.0291 0.0682
race_white Binary 0.0000 0.0000
race_black Binary 0.0038 0.0014
race_other Binary -0.0056 0.0014
sex_Male Binary 0.0362 0.0179
Sample sizes
Control Treated
All 3551 2184
Matched 2184 2184
Unmatched 1367 0
Although balanced improved, we still have covariates with unacceptable imbalance, and it is possible to do much better than simple 1:1 matching. Connors et al. (1996) used matching with a caliper on the propensity score; here we’ll do so as well, setting a caliper of .2 standard deviations of the logit of the propensity score, which is an arbitrary but often used caliper width:
m2 <- matchit(RHC ~ aps1 + meanbp1 + pafi1 + crea1 + hema1 +
paco21 + surv2md1 + resp1 + card + edu +
age + race + sex, data = rhc,
link = "linear.logit", caliper = .2)
m2
A matchit object
- method: 1:1 nearest neighbor matching without replacement
- distance: Propensity score [caliper]
- estimated with logistic regression and linearized
- caliper: <distance> (0.2)
- number of obs.: 5735 (original), 3868 (matched)
- target estimand: ATT
- covariates: aps1, meanbp1, pafi1, crea1, hema1, paco21, surv2md1, resp1, card, edu, age, race, sex
bal.tab(m1, stats = c("m", "ks"), binary = "std")
Balance Measures
Type Diff.Adj KS.Adj
distance Distance 0.3765 0.2060
aps1 Contin. 0.2109 0.1103
meanbp1 Contin. -0.1447 0.0728
pafi1 Contin. -0.1650 0.0907
crea1 Contin. 0.1296 0.1282
hema1 Contin. -0.0494 0.0614
paco21 Contin. -0.0699 0.0453
surv2md1 Contin. -0.1087 0.0710
resp1 Contin. -0.1003 0.0755
card_Yes Binary 0.1270 0.0627
edu Contin. 0.0307 0.0343
age Contin. -0.0291 0.0682
race_white Binary 0.0000 0.0000
race_black Binary 0.0038 0.0014
race_other Binary -0.0056 0.0014
sex_Male Binary 0.0362 0.0179
Sample sizes
Control Treated
All 3551 2184
Matched 2184 2184
Unmatched 1367 0
We can see that several treated units were discarded, which changes the estimand, though we do see major improvements in balance. There are many methods we can try to improve balance while retaining the estimand, but we’ll use generalized full matching (Sävje, Higgins, and Sekhon 2021) by setting method = "quick"
, which is fast and tends to perform well in a variety of settings1.
m3 <- matchit(RHC ~ aps1 + meanbp1 + pafi1 + crea1 + hema1 +
paco21 + surv2md1 + resp1 + card + edu +
age + race + sex, data = rhc,
method = "quick")
m3
A matchit object
- method: Generalized full matching
- distance: Propensity score
- estimated with logistic regression
- number of obs.: 5735 (original), 5735 (matched)
- target estimand: ATT
- covariates: aps1, meanbp1, pafi1, crea1, hema1, paco21, surv2md1, resp1, card, edu, age, race, sex
bal.tab(m3, stats = c("m", "ks"), binary = "std")
Balance Measures
Type Diff.Adj KS.Adj
distance Distance -0.0001 0.0050
aps1 Contin. -0.0260 0.0192
meanbp1 Contin. -0.0124 0.0294
pafi1 Contin. -0.0309 0.0355
crea1 Contin. -0.0505 0.0576
hema1 Contin. 0.0203 0.0626
paco21 Contin. 0.0044 0.0231
surv2md1 Contin. 0.0341 0.0403
resp1 Contin. -0.0392 0.0684
card_Yes Binary -0.0516 0.0255
edu Contin. -0.0154 0.0239
age Contin. -0.0347 0.0652
race_white Binary 0.0287 0.0118
race_black Binary 0.0017 0.0006
race_other Binary -0.0506 0.0125
sex_Male Binary 0.0178 0.0088
Sample sizes
Control Treated
All 3551. 2184
Matched (ESS) 1017.28 2184
Matched (Unweighted) 3551. 2184
Balance is good and we retained the target estimand, but generalized full matching took a toll on the effective sample size (ESS) of our control group, which is now around 1000 (from around 3500). Even though generalized full matching retains the entire sample (i.e., not a single unit is dropped), the matching weights resulting from it have variability such that the ESS is much lower than the original sample size. There are ways to manage the balance-ESS trade-off that are specific to each matching method.
Although balance isn’t perfect and could be improved with additional fine-tuning, we’ll move forward with this matched sample to demonstrate effect estimation and reporting. First, we need to extract the matched sample from the matchit
object using match.data()
. This adds columns to the original dataset called "distance"
, "weights"
, and "subclass"
containing the propensity score, matching weights, and matched strata (i.e., pair membership). When units are dropped in the matching (e.g., when using 1:1 matching), the output will only contain the units remaining in the matched sample2.
md <- match.data(m3)
# Names of new dataset; note the three new variables at the end
names(md)
[1] "aps1" "meanbp1" "pafi1" "crea1" "hema1" "paco21"
[7] "surv2md1" "resp1" "card" "edu" "age" "race"
[13] "sex" "RHC" "death" "distance" "weights" "subclass"
To estimate the treatment effect, we need to proceed in two steps. First, we fit the outcome model to the matched sample including the matching weights. Second, we compute the treatment effect using g-computation. Our estimand will be the marginal risk ratio for the treated units (i.e., the ATT on the risk ratio scale). We will fit a logistic regression for the outcome, including covariates and their interactions with treatment in the model.
fit <- glm(death ~ RHC * (aps1 + meanbp1 + pafi1 + crea1 + hema1 +
paco21 + surv2md1 + resp1 + card + edu +
age + race + sex),
data = md,
weights = weights,
family = quasibinomial)
There is no value in examining this outcome model; the coefficients are uninterpretable and provide no information about the effects of the included predictors on the outcome (Westreich and Greenland 2013). This model can be arbitrarily complicated and is not designed to be a useful predictive model for the outcome. Its sole purpose is to increase the precision of the resulting effect estimate. We will compute the marginal risks under treatment for each group and compute the risk ratio using g-computation. Here we specify arguments to avg_predictions()
to use cluster-robust SEs that account for the matching and restrict the data to the treated units because we are estimating the ATT.
library("marginaleffects")
avg_predictions(fit,
variables = "RHC",
vcov = ~subclass,
newdata = subset(RHC == 1))
RHC Estimate Std. Error z Pr(>|z|) S 2.5 % 97.5 %
0 0.656 0.01355 48.4 <0.001 Inf 0.630 0.683
1 0.680 0.00915 74.4 <0.001 Inf 0.662 0.698
Type: response
Columns: RHC, estimate, std.error, statistic, p.value, s.value, conf.low, conf.high
We find marginal risks of .69 and .713 for the treated units under control and treatment, respectively. We can compute the risk ratio using the following:
avg_comparisons(fit,
variables = "RHC",
vcov = ~subclass,
newdata = subset(RHC == 1),
comparison = "lnratioavg",
transform = "exp")
Estimate Pr(>|z|) S 2.5 % 97.5 %
1.04 0.138 2.9 0.989 1.09
Term: RHC
Type: response
Comparison: ln(mean(1) / mean(0))
Columns: term, contrast, estimate, p.value, s.value, conf.low, conf.high, predicted_lo, predicted_hi, predicted
From this we find a risk ratio of 1.04, indicating that the risk of death is 4% higher for those receiving RHC than had they not received it. The confidence interval for the risk ratio is (.989, 1.09), and the p-value for the test that the log risk ratio is equal to 0 (i.e., that the risk ratio is equal to 1) is .138, indicating no evidence for an effect of RHC in either direction.
To report balance, we could include the final balance table above, a visual representation of it, or a summary of balance statistics (or combinations thereof). A clean visual representation of balance is in a Love plot, which can be requested as follows:
love.plot(m3, stats = c("m", "ks"), binary = "std",
drop.distance = TRUE, abs = TRUE)
See the cobalt
documentation for more information on using love.plot()
to make publication-ready plots.