Fist of all, we will upload the required libraries:
library(twang)
library(magrittr)
library(tidyverse)
library(gtsummary)
library(stddiff)
library(ggplot2)
library(data.table)
library(boot)
library(splines)
library(PSAgraphics)
library(Matching)
library(sandwich)
library(survey)
library(rms)
We will use a dataset coming from an observational study of 996 patients receiving an initial Percutaneous Coronary Intervention (PCI) at Ohio Heart Health, Christ Hospital, Cincinnati in 1997 and followed for at least 6 months by the staff of the Lindner Center.
The patients thought to be more severely diseased were assigned to treatment with abciximab (an expensive, high-molecular-weight IIb/IIIa cascade blocker); in fact, only 298 (29.9 percent) of patients received usual-care-alone with their initial PCI.
Our research question aims at estimating the treatment effect of abciximab+PCI (abcix) vs the standard care on the probability of of being deceased at 6 months.
In this practical, we will apply the methods based on the propensity score.
Measured pre-treatment characteristics that could confound the treatment-outcome relationship are:
data("lindner",package="twang")
set.seed(123)
summary(lindner)
## lifepres cardbill abcix stent
## Min. : 0.0 Min. : 2216 Min. :0.0000 Min. :0.0000
## 1st Qu.:11.6 1st Qu.: 10219 1st Qu.:0.0000 1st Qu.:0.0000
## Median :11.6 Median : 12458 Median :1.0000 Median :1.0000
## Mean :11.3 Mean : 15674 Mean :0.7008 Mean :0.6687
## 3rd Qu.:11.6 3rd Qu.: 16660 3rd Qu.:1.0000 3rd Qu.:1.0000
## Max. :11.6 Max. :178534 Max. :1.0000 Max. :1.0000
## height female diabetic acutemi
## Min. :108.0 Min. :0.0000 Min. :0.0000 Min. :0.0000
## 1st Qu.:165.0 1st Qu.:0.0000 1st Qu.:0.0000 1st Qu.:0.0000
## Median :173.0 Median :0.0000 Median :0.0000 Median :0.0000
## Mean :171.4 Mean :0.3474 Mean :0.2239 Mean :0.1436
## 3rd Qu.:178.0 3rd Qu.:1.0000 3rd Qu.:0.0000 3rd Qu.:0.0000
## Max. :196.0 Max. :1.0000 Max. :1.0000 Max. :1.0000
## ejecfrac ves1proc sixMonthSurvive
## Min. : 0.00 Min. :0.000 Mode :logical
## 1st Qu.:45.00 1st Qu.:1.000 FALSE:26
## Median :55.00 Median :1.000 TRUE :970
## Mean :50.97 Mean :1.386
## 3rd Qu.:56.00 3rd Qu.:2.000
## Max. :90.00 Max. :5.000
How is the treatment variable distributed in the population?
# Exposure
lindner %>%
dplyr::select(abcix) %>%
tbl_summary()
Characteristic | N = 9961 |
---|---|
abcix | 698 (70%) |
1 n (%) |
ggplot(lindner)+
geom_bar(aes(x=abcix,fill=as.factor(abcix)),stat="count")+
scale_fill_discrete("Treatment",labels = c("PCI", "PCI+abciximab")) +
theme_classic()
Is the outcome rare?
# Outcome
lindner$sixMonthDeath <- 1-lindner$sixMonthSurvive
lindner %>%
dplyr::select(sixMonthDeath) %>%
tbl_summary()
Characteristic | N = 9961 |
---|---|
sixMonthDeath | 26 (2.6%) |
1 n (%) |
What is the crude odds ratio for mortality?
# Crude OR
fit.crude <- glm(sixMonthDeath~abcix,family = binomial,data=lindner)
tbl_regression(fit.crude,exponentiate=T)
Characteristic | OR1 | 95% CI1 | p-value |
---|---|---|---|
abcix | 0.30 | 0.13, 0.66 | 0.003 |
1 OR = Odds Ratio, CI = Confidence Interval |
Let’s now summarize the confounding variables by treatment group and by outcome, to have a general idea about the observed associations:
# Possible confounders
lindner %>%
dplyr::select(acutemi,
ejecfrac,
ves1proc,
stent,
diabetic,
female,
height) %>%
tbl_summary()
Characteristic | N = 9961 |
---|---|
acutemi | 143 (14%) |
ejecfrac | 55 (45, 56) |
ves1proc | |
0 | 4 (0.4%) |
1 | 680 (68%) |
2 | 252 (25%) |
3 | 45 (4.5%) |
4 | 14 (1.4%) |
5 | 1 (0.1%) |
stent | 666 (67%) |
diabetic | 223 (22%) |
female | 346 (35%) |
height | 173 (165, 178) |
1 n (%); Median (IQR) |
# Descriptive statistics of patients'characteristics by treatment group
lindner %>%
dplyr::select(acutemi,
ejecfrac,
ves1proc,
stent,
diabetic,
female,
height,
abcix) %>%
tbl_summary(by=abcix) %>%
add_overall() %>%
add_p()
Characteristic | Overall, N = 9961 | 0, N = 2981 | 1, N = 6981 | p-value2 |
---|---|---|---|---|
acutemi | 143 (14%) | 18 (6.0%) | 125 (18%) | <0.001 |
ejecfrac | 55 (45, 56) | 55 (50, 60) | 55 (45, 56) | 0.004 |
ves1proc | <0.001 | |||
0 | 4 (0.4%) | 1 (0.3%) | 3 (0.4%) | |
1 | 680 (68%) | 243 (82%) | 437 (63%) | |
2 | 252 (25%) | 47 (16%) | 205 (29%) | |
3 | 45 (4.5%) | 6 (2.0%) | 39 (5.6%) | |
4 | 14 (1.4%) | 1 (0.3%) | 13 (1.9%) | |
5 | 1 (0.1%) | 0 (0%) | 1 (0.1%) | |
stent | 666 (67%) | 174 (58%) | 492 (70%) | <0.001 |
diabetic | 223 (22%) | 80 (27%) | 143 (20%) | 0.027 |
female | 346 (35%) | 115 (39%) | 231 (33%) | 0.10 |
height | 173 (165, 178) | 173 (165, 178) | 173 (165, 180) | 0.8 |
1 n (%); Median (IQR) | ||||
2 Pearson’s Chi-squared test; Wilcoxon rank sum test; Fisher’s exact test |
# Descriptive statistics of patients'characteristics by outcome
lindner %>%
dplyr::select(acutemi,
ejecfrac,
ves1proc,
stent,
diabetic,
female,
height,
sixMonthDeath) %>%
tbl_summary(by=sixMonthDeath) %>%
add_overall() %>%
add_p()
Characteristic | Overall, N = 9961 | 0, N = 9701 | 1, N = 261 | p-value2 |
---|---|---|---|---|
acutemi | 143 (14%) | 137 (14%) | 6 (23%) | 0.2 |
ejecfrac | 55 (45, 56) | 55 (45, 56) | 50 (33, 54) | 0.002 |
ves1proc | 0.10 | |||
0 | 4 (0.4%) | 4 (0.4%) | 0 (0%) | |
1 | 680 (68%) | 661 (68%) | 19 (73%) | |
2 | 252 (25%) | 249 (26%) | 3 (12%) | |
3 | 45 (4.5%) | 41 (4.2%) | 4 (15%) | |
4 | 14 (1.4%) | 14 (1.4%) | 0 (0%) | |
5 | 1 (0.1%) | 1 (0.1%) | 0 (0%) | |
stent | 666 (67%) | 649 (67%) | 17 (65%) | 0.9 |
diabetic | 223 (22%) | 212 (22%) | 11 (42%) | 0.014 |
female | 346 (35%) | 335 (35%) | 11 (42%) | 0.4 |
height | 173 (165, 178) | 173 (165, 178) | 168 (163, 177) | 0.10 |
1 n (%); Median (IQR) | ||||
2 Fisher’s exact test; Wilcoxon rank sum test; Pearson’s Chi-squared test |
We can now calculate the Standardized Difference,which can be use as a measure of balance in the treatment groups. It is a measure of difference between groups that is independent from statistical testing (remember that p values always depend on sample size !!). It is very similar to the definition of effect size that we discussed in Block 2.
It can be defined for a continuous covariate as:
\(SD_{c}=\frac{\overline{x_{1}}-\overline{x_{0}}} {\sqrt{\frac{s_1^2+s_0^2}{2}}}\)
and for a dichotomous covariate as:
\(SD_{d}=\frac{\overline{p_{1}}-\overline{p_{0}}} {\sqrt{\frac{\overline{p_{1}}(1-\overline{p_{1})}+\overline{p_{0}}(1-\overline{p_{0}})}{2}}}\)
The rough interpretation is that imbalance is present if the standardized difference is greater than 0.1 or 0.2.
s1 <- stddiff.numeric(vcol="height",gcol="abcix",data=lindner)
s2 <- stddiff.numeric(vcol="ejecfrac",gcol="abcix",data=lindner)
s3 <- stddiff.numeric(vcol="ves1proc",gcol="abcix",data=lindner)
s4 <- stddiff.binary(vcol="stent",gcol="abcix",data=lindner)
s5 <- stddiff.binary(vcol="female",gcol="abcix",data=lindner)
s6 <- stddiff.binary(vcol="diabetic",gcol="abcix",data=lindner)
s7 <- stddiff.binary(vcol="acutemi",gcol="abcix",data=lindner)
cont.var <- as.data.frame(rbind(s1,s2,s3))
rownames(cont.var) <- c("height", "ejecfrac", "ves1proc")
cont.var
## mean.c sd.c mean.t sd.t missing.c missing.t stddiff stddiff.l
## height 171.446 10.589 171.443 10.695 0 0 0.000 -0.135
## ejecfrac 52.289 10.297 50.403 10.419 0 0 0.182 0.046
## ves1proc 1.205 0.480 1.463 0.706 0 0 0.427 0.290
## stddiff.u
## height 0.136
## ejecfrac 0.318
## ves1proc 0.564
bin.var <- as.data.frame(rbind(s4,s5,s6, s7))
rownames(bin.var) <- c("stent", "female", "diabetic", "acutemi")
bin.var
## p.c p.t missing.c missing.t stddiff stddiff.l stddiff.u
## stent 0.584 0.705 0 0 0.255 0.119 0.391
## female 0.386 0.331 0 0 0.115 -0.021 0.251
## diabetic 0.268 0.205 0 0 0.150 0.014 0.286
## acutemi 0.060 0.179 0 0 0.372 0.235 0.508
Fit now a propensity score model — a logistic regression model with abciximab+PCI (vs. PCI) as the outcome, and the confounders listed in the table above included as covariates. We exclude from the list the variable height, since there was not a relevant difference between the groups.
# Fit a propensity score model
fit.ps<- glm(abcix~
acutemi+
ejecfrac+
ves1proc+
stent+
diabetic+
female,
data=lindner,family = binomial)
summary(fit.ps)
##
## Call:
## glm(formula = abcix ~ acutemi + ejecfrac + ves1proc + stent +
## diabetic + female, family = binomial, data = lindner)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -2.5479 -1.2311 0.6309 0.8799 1.6053
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 0.272481 0.440711 0.618 0.536394
## acutemi 1.178158 0.269479 4.372 1.23e-05 ***
## ejecfrac -0.015076 0.007391 -2.040 0.041374 *
## ves1proc 0.757365 0.138279 5.477 4.32e-08 ***
## stent 0.571165 0.150208 3.803 0.000143 ***
## diabetic -0.409191 0.170371 -2.402 0.016316 *
## female -0.133346 0.151377 -0.881 0.378379
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 1215.5 on 995 degrees of freedom
## Residual deviance: 1127.0 on 989 degrees of freedom
## AIC: 1141
##
## Number of Fisher Scoring iterations: 4
# Save the estimated propensity score
lindner$ps <- fitted(fit.ps)
# Plot estimated ps
ggplot(lindner) +
geom_boxplot(aes(y = ps,group = as.factor(abcix),col = as.factor(abcix))) +
scale_y_continuous("Estimated PS") +
scale_color_discrete("Treatment",labels = c("PCI", "PCI+abciximab")) +
theme_classic()
ggplot(lindner) +
geom_histogram(aes(x = ps,group = as.factor(abcix),fill = as.factor(abcix))) +
scale_y_continuous("Estimated PS") +
facet_grid(cols=vars(abcix))+
scale_fill_discrete("Treatment",labels = c("PCI", "PCI+abciximab")) +
theme_classic()
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
Assess whether there are non-overlapping scores (positivity violation) in the two exposure groups:
lindner %>%
dplyr::select(ps,abcix) %>%
tbl_summary(type = list(ps~"continuous2"),by=abcix,
statistic = all_continuous2() ~c(
"{median} ({p25}, {p75})",
"{min}, {max}"))
Characteristic | 0, N = 298 | 1, N = 698 |
---|---|---|
ps | ||
Median (IQR) | 0.65 (0.55, 0.70) | 0.71 (0.65, 0.82) |
Range | 0.25, 0.96 | 0.28, 0.98 |
Investigate overlap:
lindner %<>% mutate(overlap=ifelse(ps>=min(ps[abcix==1]) &
ps<=max(ps[abcix==0]),1,0))
# non-overlap: treatment group have higher ps than any non-abciximab user and
# control group have smaller ps than any abciximab user
with(lindner,table(overlap,abcix))
## abcix
## overlap 0 1
## 0 1 8
## 1 297 690
with(lindner,prop.table(table(overlap,abcix)),2)
## abcix
## overlap 0 1
## 0 0.001004016 0.008032129
## 1 0.298192771 0.692771084
In the successive steps, we remove subjects that does not overlap. This step reduce the original sample size, but we should respect the assumption of positivity in order to estimate a reasonable causal effect. It makes no sense including subjects that have “near-zero” probability to receive the treatment or to have a “match” in the successive analyses.
We can use the estimated PS as a covariate in a logistic regression model for the outcome:
# Model 1: Linear relationship between ps and outcome
fit.out<- glm(sixMonthDeath~abcix+ps,
data=lindner,
family = binomial,
subset=overlap==1)
# Model summary
summary(fit.out)
##
## Call:
## glm(formula = sixMonthDeath ~ abcix + ps, family = binomial,
## data = lindner, subset = overlap == 1)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -0.4229 -0.2821 -0.1833 -0.1596 3.1184
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -4.211 1.165 -3.616 0.000300 ***
## abcix -1.445 0.439 -3.293 0.000992 ***
## ps 1.947 1.698 1.147 0.251522
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 233.15 on 986 degrees of freedom
## Residual deviance: 222.01 on 984 degrees of freedom
## AIC: 228.01
##
## Number of Fisher Scoring iterations: 7
The second step is to save the predicted probabilities for the treated and the untreated and estimate the causal effects of interest in the population:
# fitted values (probabilities)
lindner$predY0<-fit.out$family$linkinv(coef(fit.out)[1]+coef(fit.out)[3]*lindner$ps) # PCI subjects
lindner$predY1<-fit.out$family$linkinv(coef(fit.out)[1]+coef(fit.out)[2]+coef(fit.out)[3]*lindner$ps) # PCI+abciximab subjects
# ATE effect
Y1<-mean(lindner$predY1)
Y0<-mean(lindner$predY0)
# ATT effect
Y1_1<-mean(lindner$predY1[lindner$abcix==1])
Y0_1<-mean(lindner$predY0[lindner$abcix==1])
# ATE effect
Y1-Y0
## [1] -0.04246731
# ATT effect
Y1_1-Y0_1
## [1] -0.04436082
# Estimate odds ratios related to the "ATE" and the "ATT"
(Y1/(1-Y1))/(Y0/(1-Y0))
## [1] 0.2363187
(Y1_1/(1-Y1_1))/(Y0_1/(1-Y0_1))
## [1] 0.236305
The ATE effect is quite similar to the ATT effect, indicating that there is a protective effect of the PCI+abciximab vs PCI alone.
To obtain the corresponding confidence intervals we can use the bootstrap approach.
We do not here outline this procedure, see at the end of this practical the supplementary code.
This model relies on two additional assumptions: no interaction between propensity score and treatment, and a linear relationship between the propensity score and treatment.
Do these assumptions appear reasonable here?
We can try to fit different models, and then compare the AIC:
# Model 2: Non-linear relationship between ps and outcome
fit.out2<- glm(sixMonthDeath~abcix+ps+I(ps^2),
data=lindner,
family = binomial,
subset=overlap==1)
summary(fit.out2)
##
## Call:
## glm(formula = sixMonthDeath ~ abcix + ps + I(ps^2), family = binomial,
## data = lindner, subset = overlap == 1)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -0.5751 -0.2881 -0.1730 -0.1407 3.0416
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 2.3787 3.8937 0.611 0.541255
## abcix -1.5088 0.4493 -3.358 0.000784 ***
## ps -17.7559 11.5272 -1.540 0.123475
## I(ps^2) 14.1968 8.3131 1.708 0.087681 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 233.15 on 986 degrees of freedom
## Residual deviance: 219.46 on 983 degrees of freedom
## AIC: 227.46
##
## Number of Fisher Scoring iterations: 7
# Model 3: Non-linear relationship between ps and outcome and interaction between ps and treatment
fit.out3<- glm(sixMonthDeath~abcix*ps+I(ps^2),
data=lindner,
family = binomial,
subset=overlap==1)
summary(fit.out3)
##
## Call:
## glm(formula = sixMonthDeath ~ abcix * ps + I(ps^2), family = binomial,
## data = lindner, subset = overlap == 1)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -0.6659 -0.2798 -0.1697 -0.1458 3.0262
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 2.361196 3.786408 0.624 0.5329
## abcix 0.009567 2.075020 0.005 0.9963
## ps -18.641792 11.183815 -1.667 0.0955 .
## I(ps^2) 15.513326 8.167099 1.899 0.0575 .
## abcix:ps -2.130967 2.862505 -0.744 0.4566
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 233.15 on 986 degrees of freedom
## Residual deviance: 218.93 on 982 degrees of freedom
## AIC: 228.93
##
## Number of Fisher Scoring iterations: 7
It seems that the relationship could be partially non-linear, but there is no a statistical significance very strong, as well as for the interaction. So probably the best parsimonious model to keep is Model 1.
Create propensity score strata: this could be an iterative process, since we should verify if we have enough subjects/event in each stratum.
lindner %<>% mutate(strata=cut(ps,quantile(ps,c(0,0.25,0.5,0.75,1)),include.lowest=T,labels=c(1:4)))
#Check they have been created correctly
summary(lindner$strata)
## 1 2 3 4
## 252 246 257 241
tapply(lindner$ps,lindner$strata,summary)
## $`1`
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.2499 0.5168 0.5463 0.5321 0.5713 0.6051
##
## $`2`
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.6078 0.6505 0.6674 0.6605 0.6807 0.6846
##
## $`3`
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.6877 0.7036 0.7307 0.7406 0.7728 0.8106
##
## $`4`
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.8106 0.8305 0.8706 0.8759 0.9102 0.9765
#Look at numbers of events and patients in each strata/exposure group
table(lindner$sixMonthDeath,lindner$strata,lindner$abcix)
## , , = 0
##
##
## 1 2 3 4
## 0 105 89 64 25
## 1 7 1 4 3
##
## , , = 1
##
##
## 1 2 3 4
## 0 138 156 185 208
## 1 2 0 4 5
These strata seem quite “sparse” as number of events. Another possibility is :
#Create propensity score strata
lindner %<>% mutate(strata=cut(ps,quantile(ps,c(0,0.33,0.66,1)),include.lowest=T,labels=c(1:3)))
#Check they have been created correctly
summary(lindner$strata)
## 1 2 3
## 356 302 338
#Look at numbers of events and patients in each strata/exposure group
table(lindner$sixMonthDeath,lindner$strata,lindner$abcix)
## , , = 0
##
##
## 1 2 3
## 0 146 96 41
## 1 7 4 4
##
## , , = 1
##
##
## 1 2 3
## 0 201 201 285
## 1 2 1 8
Remind : we should also check for the balance of the confounders in each strata ! See the supplementary material for that. For now, let’s just estimate the OR in each stratum:
beta.treat<-numeric(3)
nstrata<-table(lindner$strata)
treated.strata<-table(lindner$strata,lindner$abcix)[,2]
for (i in 1:3){
ms<-glm(sixMonthDeath~abcix,data=lindner,subset = strata==i,family="binomial")
beta.treat[i]<-coef(ms)[2]
print(summary(ms))
}
##
## Call:
## glm(formula = sixMonthDeath ~ abcix, family = "binomial", data = lindner,
## subset = strata == i)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -0.3060 -0.3060 -0.1407 -0.1407 3.0398
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -3.0377 0.3869 -7.851 4.13e-15 ***
## abcix -1.5725 0.8091 -1.943 0.052 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 83.969 on 355 degrees of freedom
## Residual deviance: 79.319 on 354 degrees of freedom
## AIC: 83.319
##
## Number of Fisher Scoring iterations: 7
##
##
## Call:
## glm(formula = sixMonthDeath ~ abcix, family = "binomial", data = lindner,
## subset = strata == i)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -0.2857 -0.2857 -0.0996 -0.0996 3.2583
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -3.1781 0.5103 -6.228 4.73e-10 ***
## abcix -2.1253 1.1249 -1.889 0.0589 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 50.927 on 301 degrees of freedom
## Residual deviance: 46.200 on 300 degrees of freedom
## AIC: 50.2
##
## Number of Fisher Scoring iterations: 8
##
##
## Call:
## glm(formula = sixMonthDeath ~ abcix, family = "binomial", data = lindner,
## subset = strata == i)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -0.4315 -0.2353 -0.2353 -0.2353 2.6835
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -2.3273 0.5238 -4.443 8.88e-06 ***
## abcix -1.2458 0.6347 -1.963 0.0497 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 103.68 on 337 degrees of freedom
## Residual deviance: 100.39 on 336 degrees of freedom
## AIC: 104.39
##
## Number of Fisher Scoring iterations: 6
And, finally, let’s estimate the weighted OR related to the ATE and the ATT as a weighted average of the ORs in the various strata:
exp(sum(beta.treat*nstrata)/nrow(lindner))
## [1] 0.1960846
exp(sum(beta.treat*treated.strata)/sum(treated.strata))
## [1] 0.2028472
Also here, we should use a bootstrap approach to estimate the corresponding confidence intervals.
Here we should create a reduced dataset retaining only patients with the overlap:
lindner.overlap <- lindner %>% filter(overlap==1)
Now we proceed with the matching algorithm: there is plenty of different algorithms in R that produce matching, here we use one from the library(Matching).
match <- Match(Y=lindner.overlap$sixMonthDeath,
Tr=lindner.overlap$abcix,
X=lindner.overlap$ps,
caliper=0.2,# all matches not equal to or within 0.2 standard deviations of ps are dropped
M=1,
ties=F,
replace=TRUE # 1:1
)
# Number of pairs
nn <- length(match$index.treated)
# Create matched dataset
lindnerMatched <- cbind(rbind(lindner.overlap[match$index.treated,],
lindner.overlap[match$index.control,]),
pair=c(1:nn,1:nn))
table(lindner.overlap$abcix)
##
## 0 1
## 297 690
#Check number of treated patients
table(lindnerMatched$abcix)
##
## 0 1
## 689 689
#Look at people being used multiple times in the matched sample
summary(as.factor(table(match$index.treated)))
## 1
## 689
summary(as.factor(table(match$index.control)))
## 1 2 3 4 5 6 7 8 9 10 12 13 15 17 19
## 69 45 30 20 11 9 9 5 2 2 1 1 2 1 2
#Look at the propensity score distribution in the matched dataset
ggplot(lindnerMatched) +
geom_boxplot(aes(y = ps,group = as.factor(abcix),col = as.factor(abcix))) +
scale_y_continuous("Estimated PS") +
scale_color_discrete("Treatment",labels = c("PCI", "PCI+abciximab")) +
theme_classic()
ggplot(lindnerMatched) +
geom_histogram(aes(x = ps,group = as.factor(abcix),fill = as.factor(abcix))) +
scale_y_continuous("Estimated PS") +
facet_grid(cols=vars(abcix))+
scale_fill_discrete("Treatment",labels = c("PCI", "PCI+abciximab")) +
theme_classic()
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
Let’s check now the balance:
# Balance Diagnostics before and after matching
bal <- MatchBalance(abcix~
stent+
female+
diabetic+
acutemi+
ejecfrac+
ves1proc,
data=lindner.overlap,
match.out = match)
##
## ***** (V1) stent *****
## Before Matching After Matching
## mean treatment........ 0.70435 0.70537
## mean control.......... 0.58586 0.65602
## std mean diff......... 25.947 10.817
##
## mean raw eQQ diff..... 0.11785 0.049347
## med raw eQQ diff..... 0 0
## max raw eQQ diff..... 1 1
##
## mean eCDF diff........ 0.059245 0.024673
## med eCDF diff........ 0.059245 0.024673
## max eCDF diff........ 0.11849 0.049347
##
## var ratio (Tr/Co)..... 0.85663 0.92097
## T-test p-value........ 0.0004398 0.0057027
##
##
## ***** (V2) female *****
## Before Matching After Matching
## mean treatment........ 0.33333 0.33382
## mean control.......... 0.38384 0.30479
## std mean diff......... -10.706 6.151
##
## mean raw eQQ diff..... 0.050505 0.029028
## med raw eQQ diff..... 0 0
## max raw eQQ diff..... 1 1
##
## mean eCDF diff........ 0.025253 0.014514
## med eCDF diff........ 0.025253 0.014514
## max eCDF diff........ 0.050505 0.029028
##
## var ratio (Tr/Co)..... 0.9378 1.0495
## T-test p-value........ 0.13211 0.09767
##
##
## ***** (V3) diabetic *****
## Before Matching After Matching
## mean treatment........ 0.20725 0.2061
## mean control.......... 0.26599 0.21771
## std mean diff......... -14.483 -2.8684
##
## mean raw eQQ diff..... 0.057239 0.011611
## med raw eQQ diff..... 0 0
## max raw eQQ diff..... 1 1
##
## mean eCDF diff........ 0.029373 0.0058055
## med eCDF diff........ 0.029373 0.0058055
## max eCDF diff........ 0.058747 0.011611
##
## var ratio (Tr/Co)..... 0.83988 0.96072
## T-test p-value........ 0.050489 0.47958
##
##
## ***** (V4) acutemi *****
## Before Matching After Matching
## mean treatment........ 0.17101 0.17126
## mean control.......... 0.060606 0.16255
## std mean diff......... 29.302 2.3098
##
## mean raw eQQ diff..... 0.11111 0.0087083
## med raw eQQ diff..... 0 0
## max raw eQQ diff..... 1 1
##
## mean eCDF diff........ 0.055204 0.0043541
## med eCDF diff........ 0.055204 0.0043541
## max eCDF diff........ 0.11041 0.0087083
##
## var ratio (Tr/Co)..... 2.4853 1.0426
## T-test p-value........ 4.1797e-08 0.5361
##
##
## ***** (V5) ejecfrac *****
## Before Matching After Matching
## mean treatment........ 50.559 50.553
## mean control.......... 52.279 51.209
## std mean diff......... -16.899 -6.4414
##
## mean raw eQQ diff..... 1.8687 0.80406
## med raw eQQ diff..... 0 0
## max raw eQQ diff..... 20 15
##
## mean eCDF diff........ 0.033253 0.013763
## med eCDF diff........ 0.0087396 0.0043541
## max eCDF diff........ 0.10771 0.046444
##
## var ratio (Tr/Co)..... 0.97403 1.0808
## T-test p-value........ 0.016162 0.13002
## KS Bootstrap p-value.. 0.002 0.216
## KS Naive p-value...... 0.016165 0.44722
## KS Statistic.......... 0.10771 0.046444
##
##
## ***** (V6) ves1proc *****
## Before Matching After Matching
## mean treatment........ 1.4435 1.4456
## mean control.......... 1.2088 1.5109
## std mean diff......... 34.534 -9.6339
##
## mean raw eQQ diff..... 0.24242 0.065312
## med raw eQQ diff..... 0 0
## max raw eQQ diff..... 1 1
##
## mean eCDF diff........ 0.048684 0.013062
## med eCDF diff........ 0.014024 0.0043541
## max eCDF diff........ 0.1805 0.049347
##
## var ratio (Tr/Co)..... 2.0392 0.93506
## T-test p-value........ 9.0068e-10 0.0050611
## KS Bootstrap p-value.. < 2.22e-16 0.062
## KS Naive p-value...... 2.6627e-06 0.37114
## KS Statistic.......... 0.1805 0.049347
##
##
## Before Matching Minimum p.value: < 2.22e-16
## Variable Name(s): ves1proc Number(s): 6
##
## After Matching Minimum p.value: 0.0050611
## Variable Name(s): ves1proc Number(s): 6
lindnerMatched %>%
dplyr::select(stent,female,diabetic,acutemi,ejecfrac,ves1proc,abcix) %>%
tbl_summary(by=abcix) %>%
add_overall() %>%
add_p()
Characteristic | Overall, N = 1,3781 | 0, N = 6891 | 1, N = 6891 | p-value2 |
---|---|---|---|---|
stent | 938 (68%) | 452 (66%) | 486 (71%) | 0.049 |
female | 440 (32%) | 210 (30%) | 230 (33%) | 0.2 |
diabetic | 292 (21%) | 150 (22%) | 142 (21%) | 0.6 |
acutemi | 230 (17%) | 112 (16%) | 118 (17%) | 0.7 |
ejecfrac | 55 (45, 56) | 55 (45, 60) | 55 (45, 56) | 0.3 |
ves1proc | 0.3 | |||
0 | 2 (0.1%) | 0 (0%) | 2 (0.3%) | |
1 | 842 (61%) | 405 (59%) | 437 (63%) | |
2 | 434 (31%) | 231 (34%) | 203 (29%) | |
3 | 73 (5.3%) | 38 (5.5%) | 35 (5.1%) | |
4 | 27 (2.0%) | 15 (2.2%) | 12 (1.7%) | |
1 n (%); Median (IQR) | ||||
2 Pearson’s Chi-squared test; Wilcoxon rank sum test; Fisher’s exact test |
Note that the number of stent has not been well balanced after the matching procedure.
For this reason, we use this covariate in the regression model for the outcome.
Now we estimate the causal effect:
fit.out4<-glm(sixMonthDeath~abcix+stent, data=lindnerMatched,family=binomial)
summary(fit.out4)
##
## Call:
## glm(formula = sixMonthDeath ~ abcix + stent, family = binomial,
## data = lindnerMatched)
##
## Deviance Residuals:
## Min 1Q Median 3Q Max
## -0.4039 -0.4039 -0.1888 -0.1888 3.1524
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -3.4089 0.3429 -9.941 < 2e-16 ***
## abcix -1.5529 0.3562 -4.359 1.3e-05 ***
## stent 0.9438 0.3727 2.532 0.0113 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 449.29 on 1377 degrees of freedom
## Residual deviance: 418.68 on 1375 degrees of freedom
## AIC: 424.68
##
## Number of Fisher Scoring iterations: 7
We can see that the number of stent is statistically significant in the model,so it has been a good idea to control for it, since it was not well balanced in the matching procedure. As we already have discussed, sometimes also covariates that are not confounders for the effect of the treatment on the outcome could be included in the final model in order to obtain more accurate estimates of the effect.
Definition of the weights:
# Definition of weights for ATE
lindner.overlap %<>%mutate(w_ATE=case_when(abcix==1~1/ps,
abcix==0~1/(1-ps)))
#Definition of weights for ATT
lindner.overlap %<>%mutate(w_ATT=case_when(abcix==1~1,
abcix==0~ps/(1-ps)))
Check the extreme weights: sometimes it is useful to use truncated or stabilized weights, in order to reduce the variance of the final estimates, but we do not cover here this aspect.
#Check extremes
quantile(lindner.overlap$w_ATE[lindner.overlap$abcix==1],c(0,0.01,0.05,0.95,0.99,1))
## 0% 1% 5% 95% 99% 100%
## 1.041578 1.052785 1.076734 1.882227 2.339352 3.627077
quantile(lindner.overlap$w_ATE[lindner.overlap$abcix==0],c(0,0.01,0.05,0.95,0.99,1))
## 0% 1% 5% 95% 99% 100%
## 1.566665 1.658869 1.791224 6.137759 14.832595 25.687628
quantile(lindner.overlap$w_ATT[lindner.overlap$abcix==1],c(0,0.01,0.05,0.95,0.99,1))
## 0% 1% 5% 95% 99% 100%
## 1 1 1 1 1 1
quantile(lindner.overlap$w_ATT[lindner.overlap$abcix==0],c(0,0.01,0.05,0.95,0.99,1))
## 0% 1% 5% 95% 99% 100%
## 0.5666647 0.6588687 0.7912238 5.1377591 13.8325951 24.6876276
# Balance diagnostics
#ATE
bal_IPTW_ATE <- dx.wts(x=lindner.overlap$w_ATE,
data=lindner.overlap,
vars=colnames(lindner.overlap)[4:10],
treat.var = colnames(lindner.overlap)[3],
estimand = "ATE")
#ATT
bal_IPTW_ATT <- dx.wts(x=lindner.overlap$w_ATT,
data=lindner.overlap,
x.as.weights = T,
vars=colnames(lindner.overlap)[4:10],
treat.var = colnames(lindner.overlap)[3],
estimand = "ATT")
bal.table(bal_IPTW_ATE)
## $unw
## tx.mn tx.sd ct.mn ct.sd std.eff.sz stat p ks ks.pval
## stent 0.704 0.457 0.586 0.493 0.252 3.541 0.000 0.118 0.005
## height 171.419 10.728 171.458 10.605 -0.004 -0.053 0.958 0.024 1.000
## female 0.333 0.472 0.384 0.487 -0.106 -1.509 0.132 0.051 0.641
## diabetic 0.207 0.406 0.266 0.443 -0.141 -1.962 0.050 0.059 0.450
## acutemi 0.171 0.377 0.061 0.239 0.320 5.537 0.000 0.110 0.012
## ejecfrac 50.559 10.179 52.279 10.313 -0.168 -2.415 0.016 0.108 0.015
## ves1proc 1.443 0.680 1.209 0.476 0.370 6.207 0.000 0.181 0.000
##
## [[2]]
## tx.mn tx.sd ct.mn ct.sd std.eff.sz stat p ks ks.pval
## stent 0.669 0.471 0.668 0.472 0.000 0.005 0.996 0.000 1.000
## height 171.123 10.734 172.398 10.965 -0.119 -1.351 0.177 0.064 0.532
## female 0.347 0.476 0.330 0.471 0.035 0.468 0.640 0.017 1.000
## diabetic 0.225 0.418 0.242 0.429 -0.040 -0.461 0.645 0.017 1.000
## acutemi 0.137 0.344 0.149 0.357 -0.035 -0.307 0.759 0.012 1.000
## ejecfrac 51.077 9.910 51.093 10.182 -0.002 -0.020 0.984 0.044 0.909
## ves1proc 1.368 0.641 1.427 0.690 -0.093 -0.783 0.434 0.032 0.996
bal.table(bal_IPTW_ATT)
## $unw
## tx.mn tx.sd ct.mn ct.sd std.eff.sz stat p ks ks.pval
## stent 0.704 0.457 0.586 0.493 0.259 3.541 0.000 0.118 0.005
## height 171.419 10.728 171.458 10.605 -0.004 -0.053 0.958 0.024 1.000
## female 0.333 0.472 0.384 0.487 -0.107 -1.509 0.132 0.051 0.641
## diabetic 0.207 0.406 0.266 0.443 -0.145 -1.962 0.050 0.059 0.450
## acutemi 0.171 0.377 0.061 0.239 0.293 5.537 0.000 0.110 0.012
## ejecfrac 50.559 10.179 52.279 10.313 -0.169 -2.415 0.016 0.108 0.015
## ves1proc 1.443 0.680 1.209 0.476 0.345 6.207 0.000 0.181 0.000
##
## [[2]]
## tx.mn tx.sd ct.mn ct.sd std.eff.sz stat p ks ks.pval
## stent 0.704 0.457 0.703 0.458 0.003 0.034 0.973 0.001 1.000
## height 171.419 10.728 172.793 11.090 -0.128 -1.264 0.206 0.068 0.597
## female 0.333 0.472 0.307 0.462 0.055 0.683 0.495 0.026 1.000
## diabetic 0.207 0.406 0.232 0.423 -0.061 -0.598 0.550 0.025 1.000
## acutemi 0.171 0.377 0.186 0.390 -0.041 -0.313 0.754 0.015 1.000
## ejecfrac 50.559 10.179 50.594 10.085 -0.003 -0.041 0.968 0.035 0.997
## ves1proc 1.443 0.680 1.519 0.743 -0.111 -0.827 0.409 0.043 0.966
Finally, let’s estimate the ATE causal effect on the weighted dataset:
# Estimate ATE
design.lindnerATE <- svydesign(ids=~1,
weights = ~w_ATE,
data=lindner.overlap)
fit_itpw_ATE <- svyglm(sixMonthDeath~abcix,
family=binomial,
design=design.lindnerATE)
tbl_regression(fit_itpw_ATE,exponentiate = T)
Characteristic | OR1 | 95% CI1 | p-value |
---|---|---|---|
abcix | 0.17 | 0.06, 0.49 | <0.001 |
1 OR = Odds Ratio, CI = Confidence Interval |
And the ATT:
design.lindnerATT <- svydesign(ids=~1,
weights = ~w_ATT,
data=lindner.overlap)
fit_iptw_ATT <- svyglm(sixMonthDeath~abcix,
family=binomial,
design=design.lindnerATT)
## Warning in eval(family$initialize): #successi non interi in un binomial glm!
tbl_regression(fit_iptw_ATT,exponentiate = T)
Characteristic | OR1 | 95% CI1 | p-value |
---|---|---|---|
abcix | 0.15 | 0.05, 0.46 | <0.001 |
1 OR = Odds Ratio, CI = Confidence Interval |
Also here it is possible to estimate the 95% CI using boostrap methods (that are in general more robust).
results <- data.frame(ATE=rep(NA,4),ATT=rep(NA,4))
f_PSadj <- function(data, indices,outcome.formula) {
d <- data[indices,] # allows boot to select sample
# estimation of ps
m1<-glm(abcix~
acutemi+
ejecfrac+
ves1proc+
stent+
diabetic+
female,
data=d,
family = binomial)
d$ps<-fitted.values(m1)
# overlap
d %<>% mutate(overlap=ifelse(ps>=min(ps[abcix==1]) &
ps<=max(ps[abcix==0]),1,0))
# outcome model
m2<-glm(outcome.formula,data=d,family="binomial",subset = overlap==1)
if(!m2$converged) print("Model did not converged")
d$predY0<-m2$family$linkinv(coef(m2)[1]+coef(m2)[3]*d$ps)
d$predY1<-m2$family$linkinv(coef(m2)[1]+coef(m2)[2]+coef(m2)[3]*d$ps)
Y1<-mean(d$predY1)
Y0<-mean(d$predY0)
Y1_1<-mean(d$predY1[lindner$abcix==1])
Y0_1<-mean(d$predY0[lindner$abcix==1])
ATE_PSadj<-(Y1/(1-Y1))/(Y0/(1-Y0))
ATT_PSadj<-(Y1_1/(1-Y1_1))/(Y0_1/(1-Y0_1))
return(c(ATE_PSadj,ATT_PSadj))
}
res_boot <- function(obj.boot,type="percent",digits=3){
suppressWarnings({
orig <- round(obj.boot$t0,digits)
ciATE <- paste(round(boot.ci(obj.boot,index=1)[[type]][4:5],digits),collapse = "-")
ciATT <- paste(round(boot.ci(obj.boot,index=2)[[type]][4:5],digits),collapse = "-")
})
res <- paste(orig,rbind(ciATE,ciATT),sep="(")
res.u <- paste(res,rep(")",2),sep = "")
return(res.u)
}
boot.out <- boot(data=lindner, statistic=f_PSadj,R=1000,outcome.formula=fit.out$formula)
print(boot.out)
##
## ORDINARY NONPARAMETRIC BOOTSTRAP
##
##
## Call:
## boot(data = lindner, statistic = f_PSadj, R = 1000, outcome.formula = fit.out$formula)
##
##
## Bootstrap Statistics :
## original bias std. error
## t1* 0.2363187 0.02393070 0.1318934
## t2* 0.2363050 0.02394347 0.1318932
# Get 95% confidence interval
results$ATE[1]<-res_boot(boot.out,digits=4)[1]
results$ATT[1]<-res_boot(boot.out,digits=4)[2]
results
## ATE ATT
## 1 0.2363(0.0929-0.5899) 0.2363(0.0929-0.5899)
## 2 <NA> <NA>
## 3 <NA> <NA>
## 4 <NA> <NA>
Very often with the stratification method there are many problems of convergence of the regression algorithm, since in the strata we have very few events !
f_PSstrat <- function(data, indices) {
d <- data[indices,] # allows boot to select sample
m1<-glm(abcix~
acutemi+
ejecfrac+
ves1proc+
stent+
diabetic+
female,
data=d,family = binomial)
d$ps<-fitted.values(m1)
quart_PS<-quantile(d$ps,c(0,0.33,0.66,1))
d$strata<-cut(d$ps, quart_PS, labels=c(1:3))
for (i in 1:3){
ms<-glm(sixMonthDeath~abcix,data=d,subset = strata==i,family="binomial")
beta.treat[i]<-coef(ms)[2]
}
ATE <- exp(sum(beta.treat*nstrata)/nrow(lindner))
ATT <- exp(sum(beta.treat*treated.strata)/sum(treated.strata))
return(c(ATE,ATT))
}
boot.out4 <- boot(data=lindner, statistic=f_PSstrat,R=1000)
# Get 95% confidence interval
results$ATE[2]<-res_boot(boot.out4,digits=3)[1]
results$ATT[2]<-res_boot(boot.out4,digits=3)[2]
We now estimate the robust standard errors related to the estimate on the matched dataset:
cov <- vcovHC(fit.out4, type = "HC0")
std.err <- sqrt(diag(cov))
q.val <- qnorm(0.975)
r <- cbind(
Estimate = coef(fit.out4)
, "Robust SE" = std.err
, z = (coef(fit.out4)/std.err)
, "Pr(>|z|) "= 2 * pnorm(abs(coef(fit.out4)/std.err), lower.tail = FALSE)
, LL = coef(fit.out4) - q.val * std.err
, UL = coef(fit.out4) + q.val * std.err
)
#Exponential to get the OR
results$ATT[3]<- paste0(round(exp(r[2,1]),4),"(",round(exp(r[2,5]),4),"-",round(exp(r[2,6]),4),")")
f_IPTW <- function(data, indices) {
d <- data[indices,] # allows boot to select sample
# estimation of ps
m1<-glm(abcix~
acutemi+
ejecfrac+
ves1proc+
stent+
diabetic+
female,
data=d,
family = binomial)
d$ps<-fitted.values(m1)
# overlap
d %<>% mutate(overlap=ifelse(ps>=min(ps[abcix==1]) &
ps<=max(ps[abcix==0]),1,0)) %>%
filter(overlap==1)
# Definition of weights for ATE
d %<>%mutate(w_ATE=case_when(abcix==1~1/ps,
abcix==0~1/(1-ps)))
#Definition of weights for ATT
d %<>%mutate(w_ATT=case_when(abcix==1~1,
abcix==0~ps/(1-ps)))
# Estimate ATE
design.lindnerATE <- svydesign(ids=~1,
weights = ~w_ATE,
data=d)
fit_itpw_ATE <- svyglm(sixMonthDeath~abcix,
family=binomial,
design=design.lindnerATE)
# Estimate ATT
design.lindnerATT <- svydesign(ids=~1,
weights = ~w_ATT,
data=d)
fit_iptw_ATT <- svyglm(sixMonthDeath~abcix,
family=binomial,
design=design.lindnerATT)
ATE <- exp(fit_itpw_ATE$coefficients[2])
ATT <- exp(fit_iptw_ATT$coefficients[2])
return(c(ATE,ATT))
}
boot.out5 <- boot(data=lindner, statistic=f_IPTW,R=1000)
results$ATE[4]<-res_boot(boot.out5,digits=4)[1]
results$ATT[4]<-res_boot(boot.out5,digits=4)[2]
# recall the crude OR from the original dataset
tbl_regression(fit.crude,exponentiate=T)
Characteristic | OR1 | 95% CI1 | p-value |
---|---|---|---|
abcix | 0.30 | 0.13, 0.66 | 0.003 |
1 OR = Odds Ratio, CI = Confidence Interval |
# PS results
row.names(results) <- c("PS Adj Linear","Stratification","Matching","IPTW")
results
## ATE ATT
## PS Adj Linear 0.2363(0.0929-0.5899) 0.2363(0.0929-0.5899)
## Stratification 0.196(0-43.913) 0.202(0.001-35.486)
## Matching <NA> 0.2116(0.1046-0.4283)
## IPTW 0.1749(0.0592-0.6322) 0.1525(0.0474-0.6406)
We can observe that using the propensity score based methods we obtain a stronger estimate of the treatment effect with respect to the crude odds ratio. We can also observe that stratification produce very unstable results due to the low sample size in the different strata. In conclusion, adjusting for confounders was important in this context !
# Function for graphical diagnostic to check balance
balance.Strat <- function(x){
if(length(unique(lindner[,x]))<10){
cat.psa(categorical = lindner[,x],
treatment = lindner$abcix,
strata = lindner$strata,
ylab="Relative Frequency")
title(x)
}
else{
box.psa(continuous = lindner[,x],
treatment = lindner$abcix,
strata = lindner$strata)
title(x)
}
}
var <- lindner %>%dplyr::select(stent,
female,
diabetic,
acutemi,
ejecfrac,
ves1proc) %>% colnames()
balance.Strat(var[1])
balance.Strat(var[2])
balance.Strat(var[3])
balance.Strat(var[4])
balance.Strat(var[5])
## Warning in xy.coords(x, y): NA introdotti per coercizione
## Warning in xy.coords(x, y): NA introdotti per coercizione
balance.Strat(var[6])
There are many R libraries that can be used to estimate weights see for example: https://cran.r-project.org/web/packages/PSweight/index.html This library enables the estimation and inference of average causal effects with binary and multiple treatments using overlap weights (ATO), inverse probability of treatment weights (ATE), average treatment effect among the treated weights (ATT), matching weights (ATM) and entropy weights (ATEN), with and without propensity score trimming. Reference: https://arxiv.org/pdf/2010.08893