This document is the main appendix (S6 appendix) covers the illustrative example in the paper
Sensitivity analyses for effect modifiers not observed in the target population when generalizing treatment effects from a randomized controlled trial: Assumptions, models, effect scales, data scenarios and implementation details
by Trang Quynh Nguyen, Ben Ackerman, Ian Schmid, Stephen R. Cole, and Elizabeth A. Stuart.
Two data files are used for the illustration: one for the trial (synthetictrial.csv, provided in the S8 appendix), and one for the target population (us.csv, provided in the S9 appendix).
All code for data manipulation and analysis in R are included in the current document. Readers can skip straight to section 4. Synthetic trial data analysis. Sections 2 and 3 are included only for completeness, so interested readers can see how the synthetic trial data were created.
This document mentions the real trial data, only as the basis of the creation of the synthetic data used in the illustrative example. The real data were NOT part of the example and of this paper, and thus are not shared.
In addition to the R code in this document, we attach Stata code in the file StataFits.do (S7 appendix). This is used to obtain correct variance estimates when fitting mixed effects models with probability weights. The R package lme4 used in this document was not built to handle probability weights, therefore it is only good for the point estimates when models are fit with weights that are probability weights.
library(tidyverse)
library(lme4)
library(lmerTest)
library(survey)
Input two data files that are available from the trial: the main dataset without CD4 count (one observation per individual) and the CD4 count dataset (multiple counts, thus multiple observations, per person).
main <- read.table(here::here("data", "actg320.27feb15.dat"), header=FALSE,
col.names = c("id", "new", "event", "event.time",
"age", "fem", "nwhite", "entry"))
main <- main %>% select(id, entry, new, age, fem, nwhite)
cd4 <- read.table(here::here("data", "actg320cd4.27feb15.dat"),
header=FALSE, col.names = c("id", "cd4", "date"))
Extract baseline and post-treatment CD4 counts and baseline status of severe immune suppression (sis), and restrict dataset to those with baseline and post-treatment CD4 counts.
cd4 <- cd4 %>% inner_join(main %>% select(id, entry), by = "id")
cd4.base <- cd4 %>%
filter(date >= entry - 10 & date <= entry + 10) %>%
group_by(id) %>%
summarize(sis = as.integer(min(cd4) <= 50),
cd4.base = mean(cd4))
cd4.post <- cd4 %>%
filter(date >= (entry + 61) - 10 & date <= (entry + 61) + 10) %>%
group_by(id) %>%
summarize(cd4.post = mean(cd4))
dat <- main %>%
inner_join(cd4.base, by = "id") %>%
inner_join(cd4.post, by = "id") %>%
select(id, new, cd4.base, cd4.post, age, fem, nwhite, sis)
rm(cd4, cd4.base, cd4.post)
names(dat); dim(dat)
## [1] "id" "new" "cd4.base" "cd4.post" "age" "fem" "nwhite" "sis"
## [1] 933 8
Reshape to long data for multilevel modeling later.
long <- dat %>%
gather("post", "cd4",
-c(id, new, age, fem, nwhite, sis)) %>%
mutate(post = as.integer(post == "cd4.post"))
names(long); dim(long)
## [1] "id" "new" "age" "fem" "nwhite" "sis" "post" "cd4"
## [1] 1866 8
cross.dummy.race.sis <- function(data) {
data %>%
mutate(nwhite.sis = interaction(nwhite, sis)) %>%
fastDummies::dummy_cols(select_columns = "nwhite.sis") %>%
rename(white.nosis = "nwhite.sis_0.0",
white.sis = "nwhite.sis_0.1",
nonwhite.nosis = "nwhite.sis_1.0",
nonwhite.sis = "nwhite.sis_1.1") %>%
mutate(nwhite.sis.plot = factor(nwhite.sis,
levels = c("0.0", "1.0", "0.1", "1.1"),
labels = c("white.nosis",
"nonwhite.nosis",
"white.sis",
"nonwhite.sis")))
}
construct.newdata <- function(dat) {
newdat <- data.frame(
nwhite = c(0, 0, 1, 1),
sis = c(0, 1, 0, 1),
cd4.base =
c(dat %>% filter(nwhite==0, sis==0) %>% pull(cd4.base) %>% mean(),
dat %>% filter(nwhite==0, sis==1) %>% pull(cd4.base) %>% mean(),
dat %>% filter(nwhite==1, sis==0) %>% pull(cd4.base) %>% mean(),
dat %>% filter(nwhite==1, sis==1) %>% pull(cd4.base) %>% mean()),
age =
c(dat %>% filter(nwhite==0, sis==0) %>% pull(age) %>% mean(),
dat %>% filter(nwhite==0, sis==1) %>% pull(age) %>% mean(),
dat %>% filter(nwhite==1, sis==0) %>% pull(age) %>% mean(),
dat %>% filter(nwhite==1, sis==1) %>% pull(age) %>% mean()),
fem =
c(dat %>% filter(nwhite==0, sis==0) %>% pull(fem) %>% mean(),
dat %>% filter(nwhite==0, sis==1) %>% pull(fem) %>% mean(),
dat %>% filter(nwhite==1, sis==0) %>% pull(fem) %>% mean(),
dat %>% filter(nwhite==1, sis==1) %>% pull(fem) %>% mean())
)
newdat <- cross.dummy.race.sis(newdat)
newdat <- bind_rows(newdat %>% mutate(new = 0),
newdat %>% mutate(new = 1))
newlong <- bind_rows(newdat %>% select(-cd4.base) %>% mutate(post = 0),
newdat %>% select(-cd4.base) %>% mutate(post = 1))
out <- list(newdat = newdat,
newlong = newlong)
}
Nothing particularly worrisome jumps out of the plots above.
Let’s compare treatment arms with regards to proportions in sex-by-race categories.
## new fem nwhite F.nonwhite F.white M.nonwhite M.white
## 1 0 0.1428571 0.4681319 0.06687598 0.07598116 0.4012559 0.4558870
## 2 1 0.1736402 0.4728033 0.08209765 0.09154252 0.3907057 0.4356541
There are more females in the new treatment arm than in the old treatment arm in the restricted sample. Let’s compare these proportions to the original sample below.
## new fem nwhite F.nonwhite F.white M.nonwhite M.white
## 1 0 0.1623489 0.4905009 0.07963226 0.08271661 0.4108686 0.4267825
## 2 1 0.1837088 0.4748700 0.08723782 0.09647102 0.3876322 0.4286590
This gender imbalance exists in the original data too. When weighting to the trial population, we might want to weight each group to the population separately.
The plot above shows that there is an average treatment effect.
This plot, stratified by SIS status shows treatment effect more clearly. The effect is more pronounced for those with baseline SIS, but seems to also exist for those without SIS at baseline.
Before searching for effect modification, we need to pick an effect scale. Since CD4 count is a non-negative variable, there is the question of whether we should consider CD4 change on an additive scale or on a multiplicative scale. The literature on CD4 count change as a result of antiretroviral therapy uses additive change, which is the convention we follow. While we have not seen a discussion or justification for this choice in the literature, it seems to make sense because there is generally a normal (i.e., no immune suppression) CD4 range, which puts a bound on CD4 increase on both the multiplicative and additive scales, but much more so on the multiplicative scale.
For data that include both baseline and post-treatment measures of the outcome, there are two modeling options: treating the baseline measure as a covariate to be controlled for, and modeling the combination of baseline and post-treatment measures using a two-level model. (We don’t consider the third, which is hard-coding the difference as the outcome, because it is equivalent to forcing the baseline measure to a have coefficient of 1 in a model for the post-treatment outcome.) With two measures of the outcome, these two modeling strategies are essentially equivalent [wish I knew of a cite somewhere for this], but the hierarchical model works for situations with more than one post-treatment measurement.
The analysis of real data is interesting and informative, but the ultimate purpose here is to use the results to generate a synthetic dataset with similar distribution. To generate such dataset, we can use either a two-level or a one-level as the basis. Therefore, we run both options in parallel below. The h. and f. in model names mean hierarchical (random intercepts) models and flat (one level) models, respectively.
No covariates.
h.0 <- lmer(cd4 ~ new*post + (1|id), data = long)
Covariates having equal influences on CD4 count regardless of baseline or post-treatment, and regardless of treatment condition.
h.1 <- lmer(cd4 ~ age + fem + nwhite + sis + new*post + (1|id), data = long)
Covariates influencing CD4 count differently for the two treatment conditions.
h.1.new <- lmer(cd4 ~ age + fem + nwhite + sis +
age*new + fem*new + nwhite*new + sis*new +
new*post + (1|id), data = long)
Covariates infuencing pre-to-post CD4 count change the same way for both conditions.
h.1.post <- lmer(cd4 ~ age + fem + nwhite + sis +
age*post + fem*post + nwhite*post + sis*post +
new*post + (1|id), data = long)
Covariates influencing baseline CD4 count differently for the two treatment conditions, and influencing pre-to-post CD4 count change the same way for both conditions.
h.1.new.post <- lmer(cd4 ~ age + fem + nwhite + sis +
age*new + fem*new + nwhite*new + sis*new +
age*post + fem*post + nwhite*post + sis*post +
new*post + (1|id), data = long)
h.sate.ests <- rbind(
contest(h.0, matrix(c(rep(0, 3), 1), nrow = 1), joint = FALSE),
contest(h.1, matrix(c(rep(0, 7), 1), nrow = 1), joint = FALSE),
contest(h.1.new, matrix(c(rep(0, 11), 1), nrow = 1), joint = FALSE),
contest(h.1.post, matrix(c(rep(0, 11), 1), nrow = 1), joint = FALSE),
contest(h.1.new.post, matrix(c(rep(0, 15), 1), nrow = 1), joint = FALSE)
)
rownames(h.sate.ests) <- c("0", "1", "2.new", "2.post", "2.new.post")
h.sate.ests
## Estimate Std. Error df t value lower upper Pr(>|t|)
## 0 36.60212 4.415954 931 8.288610 27.93574 45.26849 3.973998e-16
## 1 36.60212 4.415954 931 8.288610 27.93574 45.26849 3.973999e-16
## 2.new 36.60212 4.415954 931 8.288610 27.93574 45.26849 3.973998e-16
## 2.post 35.93868 4.406401 927 8.156017 27.29100 44.58636 1.119636e-15
## 2.new.post 35.93868 4.406401 927 8.156017 27.29100 44.58636 1.119635e-15
f.0 <- lm(cd4.post ~ new, data = dat)
f.1.base <- lm(cd4.post ~ cd4.base + new, data = dat)
f.1.all <- lm(cd4.post ~ cd4.base + age + fem + nwhite + sis + new, data = dat)
f.sate.ests <- rbind(
summary(f.0)$coef["new",],
summary(f.1.base)$coef["new",],
summary(f.1.all)$coef["new",]
)
rownames(f.sate.ests) <- c("0", "1.base", "1.all")
f.sate.ests
## Estimate Std. Error t value Pr(>|t|)
## 0 43.19200 6.371486 6.778951 2.146816e-11
## 1.base 36.42762 4.421874 8.238050 5.905006e-16
## 1.all 36.29295 4.394502 8.258718 5.051788e-16
h.2.age <- lmer(cd4 ~ age + fem + nwhite + sis +
age*new + fem*new + nwhite*new + sis*new +
age*post + fem*post + nwhite*post + sis*post +
new*post + age*new*post + (1|id), data = long)
summary(h.2.age)$coef[16:17,]
## Estimate Std. Error df t value Pr(>|t|)
## new:post 38.89877368 19.9770611 926 1.9471720 0.05181611
## age:new:post -0.07485031 0.4926945 926 -0.1519203 0.87928285
h.2.sex <- lmer(cd4 ~ age + fem + nwhite + sis +
age*new + fem*new + nwhite*new + sis*new +
age*post + fem*post + nwhite*post + sis*post +
new*post + fem*new*post + (1|id), data = long)
summary(h.2.sex)$coef[16:17,]
## Estimate Std. Error df t value Pr(>|t|)
## new:post 36.638788 4.800149 926 7.6328437 5.704432e-14
## fem:new:post -4.481333 12.156607 926 -0.3686335 7.124852e-01
h.2.race <- lmer(cd4 ~ age + fem + nwhite + sis +
age*new + fem*new + nwhite*new + sis*new +
age*post + fem*post + nwhite*post + sis*post +
new*post + nwhite*new*post + (1|id), data = long)
summary(h.2.race)$coef[16:17,]
## Estimate Std. Error df t value Pr(>|t|)
## new:post 30.06656 6.054431 926 4.966042 8.134296e-07
## nwhite:new:post 12.51004 8.850994 926 1.413405 1.578728e-01
h.2.sis <- lmer(cd4 ~ age + fem + nwhite + sis +
age*new + fem*new + nwhite*new + sis*new +
age*post + fem*post + nwhite*post + sis*post +
new*post + sis*new*post + (1|id), data = long)
summary(h.2.sis)$coef[16:17,]
## Estimate Std. Error df t value Pr(>|t|)
## new:post 30.01259 5.974867 926 5.023140 6.098415e-07
## sis:new:post 12.97436 8.841021 926 1.467519 1.425746e-01
It looks like race and SIS status are potential effect modifiers (with substantial coefficients, about 12, albeit not statistically significant). Let’s cross-classify these two variables and interact with treatment, to see if we get a stronger signal.
h.2.race.sis <- lmer(cd4 ~ age + fem + interaction(nwhite, sis) +
age*new + fem*new + new*interaction(nwhite, sis) +
age*post + fem*post + post*interaction(nwhite, sis) +
new*post + new*post*interaction(nwhite, sis) + (1|id),
data = long)
summary(h.2.race.sis)$coef[19:22,]
## Estimate Std. Error df t value Pr(>|t|)
## new:post 21.32591 7.951362 923 2.682045 0.007448076
## interaction(nwhite, sis)1.0:new:post 20.01050 12.102804 923 1.653378 0.098594089
## interaction(nwhite, sis)0.1:new:post 20.67795 12.266800 923 1.685684 0.092194686
## interaction(nwhite, sis)1.1:new:post 22.51718 12.084005 923 1.863387 0.062725234
The signal is clearer when we collapse the three groups that are not white without SIS.
h.2.race.sis.b <- lmer(cd4 ~ age + fem + interaction(nwhite, sis) +
age*new + fem*new + new*interaction(nwhite, sis) +
age*post + fem*post + post*interaction(nwhite, sis) +
new*post + new*post*I(!(nwhite==0 & sis==0)) +
(1|id), data = long)
summary(h.2.race.sis.b)$coef[19:20,]
## Estimate Std. Error df t value Pr(>|t|)
## new:post 21.32348 7.942893 925 2.684599 0.007391571
## new:post:I(!(nwhite == 0 & sis == 0))TRUE 21.07678 9.552352 925 2.206449 0.027597620
f.2.age <- lm(cd4.post ~ cd4.base + age + fem + nwhite + sis + age*new, data = dat)
summary(f.2.age)$coef[7:8,]
## Estimate Std. Error t value Pr(>|t|)
## new 39.97995831 19.9178569 2.0072420 0.04501442
## age:new -0.09322224 0.4911793 -0.1897927 0.84951322
f.2.sex <- lm(cd4.post ~ cd4.base + age + fem + nwhite + sis + fem*new, data = dat)
summary(f.2.sex)$coef[7:8,]
## Estimate Std. Error t value Pr(>|t|)
## new 37.103506 4.788064 7.7491673 2.429227e-14
## fem:new -5.180234 12.120674 -0.4273883 6.691960e-01
f.2.race <- lm(cd4.post ~ cd4.base + age + fem + nwhite + sis + nwhite*new, data = dat)
summary(f.2.race)$coef[7:8,]
## Estimate Std. Error t value Pr(>|t|)
## new 30.33759 6.035877 5.026210 6.005363e-07
## nwhite:new 12.69058 8.822859 1.438375 1.506661e-01
f.2.sis <- lm(cd4.post ~ cd4.base + age + fem + nwhite + sis + sis*new, data = dat)
summary(f.2.sis)$coef[7:8,]
## Estimate Std. Error t value Pr(>|t|)
## new 30.77581 5.964222 5.160071 3.021130e-07
## sis:new 12.06281 8.821463 1.367438 1.718201e-01
f.2.race.sis <- lm(cd4.post ~ cd4.base + age + fem + interaction(nwhite, sis) + new + new*interaction(nwhite, sis), data = dat)
summary(f.2.race.sis)$coef[8:11,]
## Estimate Std. Error t value Pr(>|t|)
## new 22.21259 7.935238 2.799234 0.005229323
## interaction(nwhite, sis)1.0:new 19.73532 12.067190 1.635453 0.102295426
## interaction(nwhite, sis)0.1:new 19.25027 12.242971 1.572353 0.116211791
## interaction(nwhite, sis)1.1:new 21.88873 12.050476 1.816420 0.069630524
f.2.race.sis.b <- lm(cd4.post ~ cd4.base + age + fem + interaction(nwhite, sis) + new + new*I(!(nwhite==0 & sis==0)), data = dat)
summary(f.2.race.sis.b)$coef[8:9,]
## Estimate Std. Error t value Pr(>|t|)
## new 22.21134 7.926833 2.802044 0.00518414
## new:I(!(nwhite == 0 & sis == 0))TRUE 20.30959 9.528612 2.131432 0.03331693
Here we use synthetic observations with group-specific mean baseline covariates.
newlong <- construct.newdata(dat)$newlong
newdat <- construct.newdata(dat)$newdat
p1 <- newlong %>%
bind_cols(cd4.pred = predict(h.2.race.sis, newdata = newlong, re.form = NA)) %>%
mutate(new = factor(new, labels = c("old", "new")),
post = factor(post, labels = c("pre", "post"))) %>%
ggplot(aes(x = nwhite.sis.plot,
y = cd4.pred,
color = new,
shape = post)) +
geom_point() +
labs(x = "",
y = "Predicted mean CD4 count",
color = "Treatment",
shape = "Time",
title = "Predictions from model h.2.race.sis") +
theme_bw() +
ylim(0, 250)
p2 <- newdat %>%
bind_cols(cd4.post.pred = predict(f.2.race.sis, newdata = newdat)) %>%
mutate(new = factor(new, labels = c("old", "new"))) %>%
ggplot(aes(x = nwhite.sis.plot,
y = cd4.post.pred,
color = new)) +
geom_point() +
labs(x = "",
y = "Predicted mean post-treatment CD4 count",
color = "Treatment",
title = "Predictions from model f.2.race.sis") +
theme_bw() +
ylim(0, 250)
gridExtra::grid.arrange(p1, p2, ncol = 2)
These predicted mean CD4 counts are consistent between h and f models.
In making synthetic data, we will need to
With the baseline covariates, it is safe to borrow the categorical variables (fem, nwhite, sis). These form 8 patterns with plenty of observations in each, so that does not confer any risk of identifying any individual.
We do not want to borrow the continuous covariate age. Instead, we want to generate a new variable age that only preserves the range of age in the data.
To avoid causing substantial distortion to the CD4 count distributions, let’s consider the role of variable age in the models we have fit.
summary(h.2.race.sis)$coef["age",]; summary(f.2.race.sis)$coef["age",]
## Estimate Std. Error df t value Pr(>|t|)
## 9.541396e-02 2.980936e-01 1.294321e+03 3.200805e-01 7.489589e-01
## Estimate Std. Error t value Pr(>|t|)
## -0.1437725 0.2484484 -0.5786814 0.5629455
Age has a very statistically non-sig coefficient in these models. So let’s refit these models without age. And for simplicity, let’s also leave out any associations with CD4 count with sex.
h.3 <- lmer(cd4 ~ interaction(nwhite, sis) +
new*interaction(nwhite, sis) +
post*interaction(nwhite, sis) +
new*post + new*post*interaction(nwhite, sis) + (1|id),
data = long)
f.3 <- lm(cd4.post ~ cd4.base + interaction(nwhite, sis) +
new + new*interaction(nwhite, sis), data = dat)
summary(h.3)$coef; summary(f.3)$coef
## Estimate Std. Error df t value Pr(>|t|)
## (Intercept) 128.703008 5.317539 1593.132 24.2034901 1.808421e-110
## interaction(nwhite, sis)1.0 4.551709 7.984665 1593.132 0.5700564 5.687198e-01
## interaction(nwhite, sis)0.1 -100.384965 7.923282 1593.132 -12.6696185 4.048465e-35
## interaction(nwhite, sis)1.1 -106.137587 7.963875 1593.132 -13.3273799 1.701035e-38
## new 6.649806 7.259242 1593.132 0.9160469 3.597809e-01
## post 44.342105 5.817590 925.000 7.6220744 6.176508e-14
## interaction(nwhite, sis)1.0:new -2.417567 10.994422 1593.132 -0.2198903 8.259848e-01
## interaction(nwhite, sis)0.1:new -10.637917 11.205965 1593.132 -0.9493084 3.426077e-01
## interaction(nwhite, sis)1.1:new -4.788800 11.032869 1593.132 -0.4340485 6.643120e-01
## interaction(nwhite, sis)1.0:post -8.747766 8.735528 925.000 -1.0014010 3.168949e-01
## interaction(nwhite, sis)0.1:post -25.329873 8.668372 925.000 -2.9221026 3.561386e-03
## interaction(nwhite, sis)1.1:post -22.477619 8.712782 925.000 -2.5798441 1.003776e-02
## new:post 21.038847 7.941887 925.000 2.6490992 8.208256e-03
## interaction(nwhite, sis)1.0:new:post 21.271161 12.028316 925.000 1.7684238 7.731963e-02
## interaction(nwhite, sis)0.1:new:post 20.710825 12.259752 925.000 1.6893348 9.149229e-02
## interaction(nwhite, sis)1.1:new:post 22.931502 12.070378 925.000 1.8998163 5.776801e-02
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 61.7503641 8.91748749 6.9246370 8.177780e-12
## cd4.base 0.8647409 0.05262936 16.4307689 2.040327e-53
## interaction(nwhite, sis)1.0 -8.1321053 8.71247462 -0.9333864 3.508643e-01
## interaction(nwhite, sis)0.1 -38.9078574 10.12917907 -3.8411659 1.308096e-04
## interaction(nwhite, sis)1.1 -36.8336986 10.32754679 -3.5665487 3.802797e-04
## new 21.9382942 7.92566461 2.7680069 5.752812e-03
## interaction(nwhite, sis)1.0:new 20.9441631 11.99271299 1.7464074 8.107248e-02
## interaction(nwhite, sis)0.1:new 19.2719496 12.23559149 1.5750730 1.155819e-01
## interaction(nwhite, sis)1.1:new 22.2837727 12.03661222 1.8513326 6.444063e-02
There is little change when leaving out these two variables. Let’s look at the predicted means.
Regarding the last point (generation of random elements), theoretically we could generate these from normal distributions with model-estimated variances. But the normality assumption seldom holds. Let’s look at the distribution of residuals.
Let’s look at the residuals.
long <- long %>%
bind_cols(cd4.pred = predict(h.3, re.form = NULL)) %>%
mutate(residual = cd4 - cd4.pred)
dat <- dat %>%
bind_cols(cd4.post.pred = predict(f.3)) %>%
mutate(residual = cd4.post - cd4.post.pred)
The distribution of residuals varies by group and is not normal. This suggets we may do better drawing from existing residuals than generating normal errors.
The residuals in cd4.post from the h model have slightly smaller variance than those from the f model, but they are actually quite close.
Before moving on, let’s take stock of what we have.
names(dat)
## [1] "id" "new" "cd4.base" "cd4.post" "age" "fem"
## [7] "nwhite" "sis" "cd4.post.pred" "residual"
names(long)
## [1] "id" "new" "age" "fem" "nwhite" "sis" "post" "cd4"
## [9] "cd4.pred" "residual"
The predicted values and residuals in the datasets above are from models h.3 and f.3, respectively.
3 steps for making synthetic data based on model h.3
4 steps for making synthetic data based on model f.3
Two functions for making synthetic trial data based on models h.3 and f.3 for a given set of seeds.
get.hmodel.synth <- function(seed, real.long = long) {
set.seed(seed)
# step 1
synth <- real.long %>%
select(id, post, new, sis, fem, nwhite, cd4.pred) %>%
spread(post, cd4.pred) %>%
select(-id) %>%
rename(mean.base = "0", mean.post = "1") %>%
sample_frac(1) %>%
mutate(id = 1:n()) %>%
arrange(new, sis)
# step 2
resids <- real.long %>%
select(id, post, new, sis, residual) %>%
spread(post, residual) %>%
select(-id) %>%
rename(resid.base = "0", resid.post = "1") %>%
group_by(new, sis) %>%
sample_frac(1) %>%
arrange(new, sis)
synth <- synth %>%
bind_cols(resids %>% select(new, sis, resid.base, resid.post)) %>%
mutate(cd4.base = round(mean.base + resid.base),
cd4.post = round(mean.post + resid.post),
cd4.base = ifelse(cd4.base < 0, 0, cd4.base),
cd4.post = ifelse(cd4.post < 0, 0, cd4.post)) %>%
select(id, fem, nwhite, sis, new, cd4.base, cd4.post)
# step 3
synth <- synth %>%
bind_cols(age = round(rnorm(n = nrow(dat),
mean = sample(dat$age, replace = TRUE),
sd = density(dat$age)$bw)))
synth
}
get.fmodel.synth <- function(seed, real.dat = dat) {
set.seed(seed)
# step 1
synth <- dat %>%
select(new, sis, fem, nwhite, cd4.base) %>%
sample_frac(1) %>%
mutate(id = 1:n(),
cd4.base = ifelse(sis == 1,
round(jitter(cd4.base, amount = 4)),
round(jitter(cd4.base, amount = 8))),
cd4.base = ifelse(cd4.base < 0, 0, cd4.base)) %>%
arrange(new, sis)
# step 2
synth <- synth %>% bind_cols(mean.post = predict(f.3, newdata = synth))
# step 3
resids <- dat %>%
select(new, sis, residual) %>%
group_by(new, sis) %>%
sample_frac(1) %>%
arrange(new, sis)
synth <- synth %>%
bind_cols(resid.post = resids$residual) %>%
mutate(cd4.post = round(mean.post + resid.post),
cd4.post = ifelse(cd4.post < 0, 0, cd4.post)) %>%
select(id, fem, nwhite, sis, new, cd4.base, cd4.post)
# step 4
synth <- synth %>%
bind_cols(age = round(rnorm(n = nrow(dat),
mean = sample(dat$age, replace = TRUE),
sd = density(dat$age)$bw)))
synth
}
The seeds in h.seed and f.seed are hidden. Results vary a lot by seeds. What are shown here are from one of infinitely many sets of seeds.
sdat.h <- get.hmodel.synth(seed = h.seed)
sdat.f <- get.fmodel.synth(seed = f.seed)
Let’s compare these two synthetic datasets and the original real data with respect to the distributions of CD4 counts.
## # A tibble: 3 x 7
## data cd4.base.mean cd4.post.mean cd4.base.sd cd4.post.sd cd4.base.zeros cd4.post.zeros
## <chr> <dbl> <dbl> <dbl> <dbl> <int> <int>
## 1 dat 84.1 134. 68.4 99.6 18 14
## 2 sdat.f 84.0 134. 68.5 101. 21 7
## 3 sdat.h 85.1 134. 65.7 87.1 56 2
The dataset based on the h model has a lot of zeros in baseline CD4, because we have a truncate the CD4 distribution at zero. This is not desirable.
Let’s have a quick peak at modelfit to these datasets. (This is just a quick peak; we will do a proper analysis later.)
slong.h <- sdat.h %>%
gather("post", "cd4", -c(id, new, age, fem, nwhite, sis)) %>%
mutate(post = as.integer(post == "cd4.post"))
slong.f <- sdat.f %>%
gather("post", "cd4", -c(id, new, age, fem, nwhite, sis)) %>%
mutate(post = as.integer(post == "cd4.post"))
h.h <- lmer(cd4 ~ age + fem + interaction(nwhite, sis) +
age*new + fem*new + new*interaction(nwhite, sis) +
age*post + fem*post + post*interaction(nwhite, sis) +
new*post + new*post*interaction(nwhite, sis) + (1|id),
data = slong.h)
h.f <- lmer(cd4 ~ age + fem + interaction(nwhite, sis) +
age*new + fem*new + new*interaction(nwhite, sis) +
age*post + fem*post + post*interaction(nwhite, sis) +
new*post + new*post*interaction(nwhite, sis) + (1|id),
data = slong.f)
summary(h.h)$coef[19:22,]; summary(h.f)$coef[19:22,]
## Estimate Std. Error df t value Pr(>|t|)
## new:post 27.546028 7.59346 923 3.6275990 0.0003017532
## interaction(nwhite, sis)1.0:new:post 5.402486 11.55941 923 0.4673669 0.6403477486
## interaction(nwhite, sis)0.1:new:post 7.460379 11.71010 923 0.6370892 0.5242247192
## interaction(nwhite, sis)1.1:new:post 17.268562 11.53578 923 1.4969564 0.1347464601
## Estimate Std. Error df t value Pr(>|t|)
## new:post 19.72150 7.862622 923 2.508260 0.01230331
## interaction(nwhite, sis)1.0:new:post 23.44112 11.979436 923 1.956780 0.05067447
## interaction(nwhite, sis)0.1:new:post 24.93563 12.135934 923 2.054694 0.04018965
## interaction(nwhite, sis)1.1:new:post 21.43789 11.956149 923 1.793043 0.07329338
We pick the f dataset above. We’ll write the data out to submit with the paper.
sdat <- get.fmodel.synth(seed = f.seed)
slong <- sdat %>%
gather("post", "cd4", -c(id, new, age, fem, nwhite, sis)) %>%
mutate(post = as.integer(post == "cd4.post"))
glimpse(slong, width = 60)
## Observations: 1,866
## Variables: 8
## $ id <int> 1, 4, 12, 13, 17, 21, 22, 26, 30, 37, 40...
## $ fem <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0...
## $ nwhite <int> 1, 1, 1, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0...
## $ sis <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0...
## $ new <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0...
## $ age <dbl> 40, 48, 37, 38, 40, 55, 41, 34, 49, 27, ...
## $ post <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0...
## $ cd4 <dbl> 112, 98, 124, 59, 147, 180, 108, 87, 91,...
write_csv(slong, here::here("data", "synthetictrial.csv"))
The data file slong.rds can be read using function readRDS().
No covariates
s.0 <- lmer(cd4 ~ new*post + (1|id), data = slong)
Covariates having equal influences on CD4 count regardless of baseline or post-treatment, and regardless of treatment condition.
s.1 <- lmer(cd4 ~ age + fem + nwhite + sis + new*post + (1|id), data = slong)
Covariates influencing CD4 count differently for the two treatment conditions.
s.1.new <- lmer(cd4 ~ age + fem + nwhite + sis +
age*new + fem*new + nwhite*new + sis*new +
new*post + (1|id), data = slong)
Covariates infuencing pre-to-post CD4 count change the same way for both conditions.
s.1.post <- lmer(cd4 ~ age + fem + nwhite + sis +
age*post + fem*post + nwhite*post + sis*post +
new*post + (1|id), data = slong)
Covariates influencing baseline CD4 count differently for the two treatment conditions, and influencing pre-to-post CD4 count change the same way for both conditions.
s.1.new.post <- lmer(cd4 ~ age + fem + nwhite + sis +
age*new + fem*new + nwhite*new + sis*new +
age*post + fem*post + nwhite*post + sis*post +
new*post + (1|id), data = slong)
sate.ests <- rbind(
contest(s.0, matrix(c(rep(0, 3), 1), nrow = 1), joint = FALSE),
contest(s.1, matrix(c(rep(0, 7), 1), nrow = 1), joint = FALSE),
contest(s.1.new, matrix(c(rep(0, 11), 1), nrow = 1), joint = FALSE),
contest(s.1.post, matrix(c(rep(0, 11), 1), nrow = 1), joint = FALSE),
contest(s.1.new.post, matrix(c(rep(0, 15), 1), nrow = 1), joint = FALSE)
)
rownames(sate.ests) <- c("0", "1", "1.new", "1.post", "1.new.post")
sate.ests
## Estimate Std. Error df t value lower upper Pr(>|t|)
## 0 36.55986 4.383802 931 8.339761 27.95658 45.16314 2.660009e-16
## 1 36.55986 4.383802 931 8.339761 27.95658 45.16314 2.660009e-16
## 1.new 36.55986 4.383802 931 8.339761 27.95658 45.16314 2.660013e-16
## 1.post 35.88346 4.361957 927 8.226460 27.32301 44.44392 6.486639e-16
## 1.new.post 35.88346 4.361957 927 8.226460 27.32301 44.44392 6.486635e-16
m.sate <- s.1.new.post
SATE <- sate.ests["1.new.post",]
SATE
## Estimate Std. Error df t value lower upper Pr(>|t|)
## 1.new.post 35.88346 4.361957 927 8.22646 27.32301 44.44392 6.486635e-16
Interaction with one covariate at a time.
s.2.age <- lmer(cd4 ~ age + fem + nwhite + sis +
age*new + fem*new + nwhite*new + sis*new +
age*post + fem*post + nwhite*post + sis*post +
new*post + age*new*post + (1|id), data = slong)
summary(s.2.age)$coef[16:17,]
## Estimate Std. Error df t value Pr(>|t|)
## new:post 66.2106057 19.8738125 926 3.331550 0.0008978053
## age:new:post -0.7676332 0.4907944 926 -1.564063 0.1181445437
s.2.sex <- lmer(cd4 ~ age + fem + nwhite + sis +
age*new + fem*new + nwhite*new + sis*new +
age*post + fem*post + nwhite*post + sis*post +
new*post + fem*new*post + (1|id), data = slong)
summary(s.2.sex)$coef[16:17,]
## Estimate Std. Error df t value Pr(>|t|)
## new:post 34.324406 4.750698 926 7.2251284 1.045487e-12
## fem:new:post 9.973967 12.029992 926 0.8290918 4.072661e-01
s.2.race <- lmer(cd4 ~ age + fem + nwhite + sis +
age*new + fem*new + nwhite*new + sis*new +
age*post + fem*post + nwhite*post + sis*post +
new*post + nwhite*new*post + (1|id), data = slong)
summary(s.2.race)$coef[16:17,]
## Estimate Std. Error df t value Pr(>|t|)
## new:post 30.35849 5.991056 926 5.067302 4.870728e-07
## nwhite:new:post 11.77988 8.760551 926 1.344650 1.790675e-01
s.2.sis <- lmer(cd4 ~ age + fem + nwhite + sis +
age*new + fem*new + nwhite*new + sis*new +
age*post + fem*post + nwhite*post + sis*post +
new*post + sis*new*post + (1|id), data = slong)
summary(s.2.sis)$coef[16:17,]
## Estimate Std. Error df t value Pr(>|t|)
## new:post 29.89819 5.921342 926 5.049225 5.341286e-07
## sis:new:post 13.09494 8.768058 926 1.493482 1.356517e-01
Interaction with cross-classifications
s.3.sex.race <- lmer(cd4 ~ age + sis + interaction(fem, nwhite) +
new + age*new + sis*new + new*interaction(fem, nwhite) +
post + age*post + sis*post + post*interaction(fem, nwhite) +
new*post + new*post*interaction(fem, nwhite) + (1|id),
data = slong)
summary(s.3.sex.race)$coef[19:22,]
## Estimate Std. Error df t value Pr(>|t|)
## new:post 27.48889 6.279561 923 4.3775175 1.337542e-05
## interaction(fem, nwhite)1.0:new:post 34.19427 21.228083 923 1.6108037 1.075644e-01
## interaction(fem, nwhite)0.1:new:post 16.09933 9.621761 923 1.6732211 9.462263e-02
## interaction(fem, nwhite)1.1:new:post 11.04217 14.793243 923 0.7464332 4.555958e-01
s.3.sex.sis <- lmer(cd4 ~ age + nwhite + interaction(fem, sis) +
new + age*new + nwhite*new + new*interaction(fem, sis) +
post + age*post + nwhite*post + post*interaction(fem, sis) +
new*post + new*post*interaction(fem, sis) + (1|id),
data = slong)
summary(s.3.sex.sis)$coef[19:22,]
## Estimate Std. Error df t value Pr(>|t|)
## new:post 28.527571 6.454427 923 4.4198457 1.105001e-05
## interaction(fem, sis)1.0:new:post 8.873224 16.488192 923 0.5381563 5.905989e-01
## interaction(fem, sis)0.1:new:post 12.677687 9.557645 923 1.3264446 1.850206e-01
## interaction(fem, sis)1.1:new:post 24.066481 17.522574 923 1.3734558 1.699442e-01
s.3.race.sis <- lmer(cd4 ~ age + fem + interaction(nwhite, sis) +
new + age*new + fem*new + new*interaction(nwhite, sis) +
post + age*post + fem*post + post*interaction(nwhite, sis) +
new*post + new*post*interaction(nwhite, sis) + (1|id),
data = slong)
summary(s.3.race.sis)$coef[19:22,]
## Estimate Std. Error df t value Pr(>|t|)
## new:post 19.72150 7.862622 923 2.508260 0.01230331
## interaction(nwhite, sis)1.0:new:post 23.44112 11.979436 923 1.956780 0.05067447
## interaction(nwhite, sis)0.1:new:post 24.93563 12.135934 923 2.054694 0.04018965
## interaction(nwhite, sis)1.1:new:post 21.43789 11.956149 923 1.793043 0.07329338
Let’s visualize the results.
snewlong <- construct.newdata(sdat)$newlong
snewlong %>%
bind_cols(cd4.pred = predict(s.3.race.sis,
newdata = snewlong, re.form = NA)) %>%
mutate(new = factor(new, labels = c("old", "new")),
post = factor(post, labels = c("pre", "post"))) %>%
ggplot(aes(x = nwhite.sis.plot,
y = cd4.pred,
color = new,
shape = post)) +
geom_point() +
labs(x = "",
y = "Predicted mean CD4 count",
color = "Treatment",
shape = "Time",
title = "Predictions from s.3.race.sis model") +
theme_bw() +
ylim(0, 250)
s.3.race.sis.effs <-
contest(s.3.race.sis, cbind(matrix(0, ncol = 18, nrow = 4),
matrix(c(1, 0, 0, 0,
1, 1, 0, 0,
1, 0, 1, 0,
1, 0, 0, 1), byrow = TRUE, ncol = 4)),
joint = FALSE, check_estimability = TRUE)
rownames(s.3.race.sis.effs) <-
c("white.nosis", "nonwhite.nosis", "white.sis", "nonwhite.sis")
s.3.race.sis.effs
## Estimate Std. Error df t value lower upper Pr(>|t|)
## white.nosis 19.72150 7.862622 923 2.508260 4.290806 35.15219 1.230331e-02
## nonwhite.nosis 43.16262 9.016177 923 4.787242 25.468037 60.85721 1.968485e-06
## white.sis 44.65712 9.246912 923 4.829409 26.509712 62.80453 1.602462e-06
## nonwhite.sis 41.15939 9.003218 923 4.571631 23.490234 58.82854 5.497825e-06
m.effmod <- s.3.race.sis
png(filename = here::here("Model.png"),
width = 8.5, height = 4, units = "in", res = 2400)
p1 <- snewlong %>%
bind_cols(cd4.pred = predict(m.effmod,
newdata = snewlong, re.form = NA)) %>%
mutate(new = factor(new, labels = c("old", "new")),
post = factor(post, labels = c("pre", "post"))) %>%
ggplot(aes(x = nwhite.sis.plot,
y = cd4.pred,
color = new,
shape = post)) +
geom_point() +
labs(x = "",
y = "Predicted mean CD4 count",
color = "Treatment",
shape = "Time",
title = "Model-predicted mean CD4 counts") +
theme_bw() +
ylim(0, 250)
p2 <- s.3.race.sis.effs %>%
as_tibble(rownames = "group") %>%
mutate(group = factor(group, levels = c("white.nosis",
"nonwhite.nosis",
"white.sis",
"nonwhite.sis"))) %>%
ggplot(aes(x = group, y = Estimate, min = lower, max = upper)) +
geom_point(shape = 15) +
geom_errorbar(width = 0) +
labs(x = "",
y = "Treatment effect",
title = "Model-estimated treatment effects") +
theme_bw()
gridExtra::grid.arrange(gridExtra::arrangeGrob(p1, p2, ncol = 2, widths = c(1.3, 1)))
dev.off()
## png
## 2
Input the summary target population dataset that captures the joint distribution several characteristics in the form of frequencies of patterns of characteristics.
pop <- read_csv(here::here("data", "us.csv"), col_types = cols())
pop <- pop %>%
mutate(fem = as.integer(1 - male),
nwhite = as.integer(1 - white)) %>%
select(agegp, fem, nwhite, n) %>%
uncount(weights = n)
glimpse(pop)
## Observations: 54,220
## Variables: 3
## $ agegp <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,...
## $ fem <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,...
## $ nwhite <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,...
table(pop$agegp)
##
## 1 2 3 4
## 18500 16740 13370 5610
sdat <- sdat %>%
mutate(agegp = ifelse(age <= 29, 1,
ifelse(age <= 39, 2,
ifelse(age <= 49, 3, 4))))
stack <-
bind_rows(pop %>%
mutate(id = NA,
cd4.base = NA,
cd4.post = NA,
age = NA,
sis = NA,
new = NA,
sample = 0) %>%
select(id, cd4.base, cd4.post,
age, agegp, fem, nwhite, sis, new, sample),
sdat %>%
mutate(sample = 1) %>%
select(id, cd4.base, cd4.post,
age, agegp, fem, nwhite, sis, new, sample)) %>%
mutate(agegp = factor(agegp))
glimpse(stack)
## Observations: 55,153
## Variables: 10
## $ id <int> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA...
## $ cd4.base <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA...
## $ cd4.post <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA...
## $ age <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA...
## $ agegp <fct> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ...
## $ fem <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ...
## $ nwhite <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ...
## $ sis <int> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA...
## $ new <int> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA...
## $ sample <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...
stack %>%
group_by(sample) %>%
summarize(age = mean(age),
agegp.1 = mean(agegp == 1),
agegp.2 = mean(agegp == 2),
agegp.3 = mean(agegp == 3),
agegp.4 = mean(agegp == 4),
female = mean(fem),
male = 1 - female,
nwhite = mean(nwhite),
white = 1 - nwhite,
sis = mean(sis))
## # A tibble: 2 x 11
## sample age agegp.1 agegp.2 agegp.3 agegp.4 female male nwhite white sis
## <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 0 NA 0.341 0.309 0.247 0.103 0.266 0.734 0.639 0.361 NA
## 2 1 39.5 0.107 0.421 0.348 0.123 0.159 0.841 0.471 0.529 0.456
For sensitivity analysis, we need a plausible range for the proportion of people in the target population who have SIS. We specify this to be a wide range of (.20, .60).
pop.mean.nwhite <- mean(pop$nwhite)
pop.mean.sis <- c(.2, .6)
TATE <- contest(m.effmod, cbind(rbind(c(rep(0, 18), 1),
c(rep(0, 18), 1)),
pop.mean.nwhite * (1 - pop.mean.sis),
(1 - pop.mean.nwhite) * pop.mean.sis,
pop.mean.nwhite * pop.mean.sis),
joint = FALSE, check_estimability = TRUE)
TATE <- cbind(pop.mean.sis = pop.mean.sis, TATE)
TATE
## pop.mean.sis Estimate Std. Error df t value lower upper Pr(>|t|)
## 1 0.2 36.24252 5.299374 923 6.839019 25.84230 46.64274 1.449615e-11
## 2 0.6 39.33251 4.741525 923 8.295329 30.02709 48.63793 3.809404e-16
Under 6.1 above, we see that the target population and the trial sample differ substantially with respect to the distribution of the variables that are observed in both.
Note that there are also differences between the new and old treatment arms, especially in sex.
sdat %>%
group_by(new) %>%
summarize(age = mean(age),
agegp.1 = mean(agegp == 1),
agegp.2 = mean(agegp == 2),
agegp.3 = mean(agegp == 3),
agegp.4 = mean(agegp == 4),
female = mean(fem),
male = 1 - female,
nwhite = mean(nwhite),
white = 1 - nwhite,
sis = mean(sis))
## # A tibble: 2 x 11
## new age agegp.1 agegp.2 agegp.3 agegp.4 female male nwhite white sis
## <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 0 39.5 0.105 0.426 0.341 0.127 0.143 0.857 0.468 0.532 0.475
## 2 1 39.4 0.109 0.416 0.356 0.119 0.174 0.826 0.473 0.527 0.437
One option is to reweight each arm of the trial to the target population based on covariates observed in both the trial and the target population. Since not all covariates we have used in the outcome model in the trial data analysis are observed in the target population, however, there is the risk that separate weighting of each trial arm may further distort between arms balance of such variables.
Therefore we use a two step weighting procedure: first reweighting the trial sample to obtain better balance between the two treatment arms, and second to weight this reweighted trial sample to the target population.
new.mod <- glm(new ~ splines::ns(age, 9)*fem*nwhite*sis, family = binomial,
data = sdat)
sdat <- sdat %>% bind_cols(new.ps = predict(new.mod, type = "response")) %>%
mutate(new.wt = ifelse(new == 1, 1/new.ps, 1/(1-new.ps)),
new.wt = new.wt / mean(new.wt)) %>%
select(-new.ps)
sdat %>%
group_by(new) %>%
summarize(age = weighted.mean(age, new.wt),
agegp.1 = weighted.mean(1 * (agegp == 1), new.wt),
agegp.2 = weighted.mean(1 * (agegp == 2), new.wt),
agegp.3 = weighted.mean(1 * (agegp == 3), new.wt),
agegp.4 = weighted.mean(1 * (agegp == 4), new.wt),
female = weighted.mean(fem, new.wt),
male = 1 - female,
nwhite = weighted.mean(nwhite, new.wt),
white = 1 - nwhite,
sis = weighted.mean(sis, new.wt),
tot.wt = sum(new.wt))
## # A tibble: 2 x 12
## new age agegp.1 agegp.2 agegp.3 agegp.4 female male nwhite white sis tot.wt
## <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 0 39.5 0.101 0.431 0.343 0.125 0.143 0.857 0.472 0.528 0.455 465.
## 2 1 39.4 0.108 0.420 0.354 0.118 0.146 0.854 0.478 0.522 0.458 468.
The between arms balance looks good. Let’s look at these weights.
sdat %>%
mutate(new = factor(new, labels = c("old", "new"))) %>%
ggplot(aes(x = new.wt, group = new, color = new)) +
geom_density(alpha = .1) +
theme_bw()
They are not extreme, which is a good thing.
Note that this reweighted trial sample with better balance is perfectly suited to estimate treatment effects in the trial. We’ll fit the SATE model and the effect modification model to this dataset below. Note that we will pay attention to the point estimate, not the estimated variance; because the weights argument in package lme4 was not designed for probability weights, the point estimates are correct, but the estimated variances and degrees of freedom are not. Where we need those estimates, we will run the model in Stata, which deals with probability weights appropriately.
slong <- sdat %>%
gather(post, cd4, -c(id, age, agegp, fem, nwhite, sis, new, new.wt)) %>%
mutate(post = 1 * (post == "cd4.post"))
m.sate.adjusted <-
lmer(cd4 ~ age + fem + interaction(nwhite, sis) +
new + age*new + fem*new + new*interaction(nwhite, sis) +
post + age*post + fem*post + post*interaction(nwhite, sis) +
new*post + (1|id),
data = slong, weights = new.wt)
summary(m.sate.adjusted)$coef["new:post",]
## Estimate Std. Error df t value Pr(>|t|)
## 3.386079e+01 4.244422e+00 9.387142e+02 7.977715e+00 4.323802e-15
This SATE estimate is slightly smaller than that before reweighting.
m.effmod.adjusted <-
lmer(cd4 ~ age + fem + interaction(nwhite, sis) +
new + age*new + fem*new + new*interaction(nwhite, sis) +
post + age*post + fem*post + post*interaction(nwhite, sis) +
new*post + new*post*interaction(nwhite, sis) + (1|id),
data = slong, weights = new.wt)
summary(m.effmod.adjusted)$coef[19:22,]
## Estimate Std. Error df t value Pr(>|t|)
## new:post 19.00278 7.688133 935.5915 2.471703 0.01362415
## interaction(nwhite, sis)1.0:new:post 21.41792 11.582468 935.5915 1.849167 0.06474901
## interaction(nwhite, sis)0.1:new:post 22.35354 11.853326 935.5915 1.885846 0.05962535
## interaction(nwhite, sis)1.1:new:post 20.33846 11.634496 935.5915 1.748117 0.08077186
The effect modification part of this model is very similar to that before reweighting. After all, the balance of the trial data was not perfect but was not bad before this reweighting adjustment.
stack <- bind_rows(
sdat %>%
select(id, age, agegp, fem, nwhite, sis, new,
cd4.base, cd4.post, new.wt) %>%
mutate(sample = 1),
pop %>%
mutate(id = NA,
age = NA,
sis = NA,
new = NA,
cd4.base = NA,
cd4.post = NA,
new.wt = 1) %>%
select(id, age, agegp, fem, nwhite, sis, new,
cd4.base, cd4.post, new.wt) %>%
mutate(sample = 0)
)
sample.mod <-
svyglm(sample ~ factor(agegp)*fem*nwhite, family = quasibinomial,
design = svydesign(ids=~1, weights = ~ new.wt, data = stack))
stack <- stack %>%
bind_cols(ps = predict(sample.mod, type = "response")) %>%
mutate(sample.wt = ifelse(sample == 0, 1, (1-ps)/ps),
wt = new.wt * sample.wt) %>%
select(-ps) %>%
group_by(sample) %>%
mutate(wt = wt / mean(wt))
stack %>%
group_by(sample, new) %>%
summarize(age = weighted.mean(age, wt),
agegp.1 = weighted.mean(1 * (agegp == 1), wt),
agegp.2 = weighted.mean(1 * (agegp == 2), wt),
agegp.3 = weighted.mean(1 * (agegp == 3), wt),
agegp.4 = weighted.mean(1 * (agegp == 4), wt),
female = weighted.mean(fem, wt),
male = 1 - female,
nwhite = weighted.mean(nwhite, wt),
white = 1 - nwhite,
sis = weighted.mean(sis, wt),
tot.wt = sum(wt))
## # A tibble: 3 x 13
## # Groups: sample [?]
## sample new age agegp.1 agegp.2 agegp.3 agegp.4 female male nwhite white sis tot.wt
## <dbl> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 0 NA NA 0.341 0.309 0.247 0.103 0.266 0.734 0.639 0.361 NA 54220
## 2 1 0 36.1 0.333 0.313 0.246 0.107 0.260 0.740 0.636 0.364 0.522 463.
## 3 1 1 35.8 0.349 0.304 0.247 0.0995 0.272 0.728 0.642 0.358 0.515 470.
This looks good! Let’s look at the weights.
stack %>%
filter(sample == 1) %>%
mutate(new = factor(new, labels = c("old", "new"))) %>%
ggplot(aes(x = wt, group = new, color = new)) +
geom_density(alpha = .1) +
theme_bw()
Now we grab the weighted trial data
sdat <- stack %>% filter(sample == 1)
slong <- sdat %>%
gather(post, cd4,
-c(id, age, agegp, fem, nwhite, sis, new, sample,
new.wt, sample.wt, wt)) %>%
mutate(post = 1 * (post == "cd4.post"))
Now we refit the effect modification model to this trial dataset that has been weighted to the target population. Again we are ignoring estimated variace here; we will get the correct estimates using Stata later.
m.effmod.wtd <-
lmer(cd4 ~ age + fem + interaction(nwhite, sis) +
new + age*new + fem*new + new*interaction(nwhite, sis) +
post + age*post + fem*post + post*interaction(nwhite, sis) +
new*post + new*post*interaction(nwhite, sis) + (1|id),
data = slong, weights = wt)
summary(m.effmod.wtd)$coef[19:22,]
## Estimate Std. Error df t value Pr(>|t|)
## new:post 32.903852 8.918195 994.89 3.6895191 0.0002368559
## interaction(nwhite, sis)1.0:new:post 9.335134 11.745935 994.89 0.7947544 0.4269459063
## interaction(nwhite, sis)0.1:new:post 4.593148 13.509615 994.89 0.3399910 0.7339350510
## interaction(nwhite, sis)1.1:new:post 3.294465 11.154916 994.89 0.2953375 0.7677976585
Note that after the weighting to population, the fitted effect modification model has changed a lot. The coefficients of the interaction terms above are much smaller than before, and smaller than their under-estimated standard errors.
This is an indicator that the model was misspecified. This is not a cause for concern, because we can almost always be sure that the model is misspecified. Also, from the beginning, we sort of knew that modeling CD4 count change either on an additive or on a multiplicative scale is not right.
Anyway, this fitted model suggests we might be able to estimate TATE using a non-interaction moel
m.xzate <-
lmer(cd4 ~ age + fem + interaction(nwhite, sis) +
new + age*new + fem*new + new*interaction(nwhite, sis) +
post + age*post + fem*post + post*interaction(nwhite, sis) +
new*post + (1|id),
data = slong, weights = wt)
summary(m.xzate)$coef["new:post",]
## Estimate Std. Error df t value Pr(>|t|)
## 3.740766e+01 4.021817e+00 9.974117e+02 9.301185e+00 8.532915e-20
But we could also argue that a decision to abandon an effect modification model at this point is ad hoc, at least because the weighted model does not have power to reject effect modification. To use the weighted effect modification model is appropriate still because the TATE range it estimates carries with it all the uncertainty that results from increased variance due to weighting.
TATE.wtd <- contest(m.effmod.wtd, cbind(rbind(c(rep(0, 18), 1),
c(rep(0, 18), 1)),
pop.mean.nwhite * (1 - pop.mean.sis),
(1 - pop.mean.nwhite) * pop.mean.sis,
pop.mean.nwhite * pop.mean.sis),
joint = FALSE, check_estimability = TRUE)
TATE.wtd <- cbind(pop.mean.sis = pop.mean.sis, TATE.wtd)
TATE.wtd
## pop.mean.sis Estimate Std. Error df t value lower upper Pr(>|t|)
## 1 0.2 38.42776 4.813294 994.89 7.983671 28.98238 47.87313 3.900362e-15
## 2 0.6 37.54753 4.111727 994.89 9.131814 29.47887 45.61618 3.670858e-19
OK, there is a problem here. The number of degrees of freedom reported from the weighted model is larger than the sample size (933). The problem is that the weights argument in the lme4 package is not for probability weights. We wil fit the same model in Stata.
First, we write the data out as a Stata data file.
foreign::write.dta(slong, file = here::here("data","synthetictrial.dta"), version = 10)
Then in Stata, we run the code StataFits.do (to be placed in the same folder with file synthetictrial.dta).
Now to input by hand results from Stata.
sate.naive <- c(estimate = 36.55986,
lower = 27.96777,
upper = 45.15195)
sate.raw <- c(estimate = 35.80648,
lower = 27.25114,
upper = 44.36182)
tate.method1.raw <- cbind(pop.mean.sis = pop.mean.sis,
estimate = c(36.24252, 39.33251),
lower = c(25.85594, 30.03929),
upper = c(46.62911, 48.62573))
sate.adjusted <- c(estimate = 33.86079,
lower = 25.4003,
upper = 42.32128)
tate.method1.adjusted <- cbind(pop.mean.sis = pop.mean.sis,
estimate = c(34.16277, 37.11585),
lower = c(23.50098, 28.54771),
upper = c(44.82456, 45.68399))
xzate <- c(estimate = 37.40766,
lower = 27.04435,
upper = 47.77097)
tate.method2 <- cbind(pop.mean.sis = pop.mean.sis,
estimate = c(38.42776, 37.54753),
lower = c(25.98259, 27.38064),
upper = c(50.87292, 47.71441))
## png
## 2
par(mar = c(4.5, 5, 1, 1))
plot(NA, NA, xlim = c(-.1, .6), ylim = c(10, 69),
cex.main = .9, cex.lab = .85, cex.axis = .8, xaxt = "n",
ylab = "increase in CD4 count (cells/ml) \ndue to new (vs. old) treatment",
xlab = "")
segments(-.03, sate.adjusted["lower"], -.03, sate.adjusted["upper"])
points(-.03, sate.adjusted["estimate"], pch=16)
segments(.07, xzate["lower"], .07, xzate["upper"], col = "gray40")
points(.07, xzate["estimate"], pch=16, col = "gray40")
axis(1,
at = seq(pop.mean.sis[1], pop.mean.sis[2], .1),
labels = seq(pop.mean.sis[1], pop.mean.sis[2], .1)*100,
tick = TRUE, cex.axis = .8)
axis(1,
at = .4, tick = FALSE, cex.axis = .8, pos = 0,
labels = "sensitivity parameter: percentage of target population\nwith severe immune suppression")
segments(pop.mean.sis[1], tate.method1.adjusted[1, "estimate"],
pop.mean.sis[2], tate.method1.adjusted[2, "estimate"], col="blue", lwd=2)
segments(pop.mean.sis[1], tate.method1.adjusted[1, "lower" ],
pop.mean.sis[2], tate.method1.adjusted[2, "lower" ], col="blue", lty=2)
segments(pop.mean.sis[1], tate.method1.adjusted[1, "upper" ],
pop.mean.sis[2], tate.method1.adjusted[2, "upper" ], col="blue", lty=2)
segments(pop.mean.sis[1], tate.method2[1, "estimate"],
pop.mean.sis[2], tate.method2[2, "estimate"], col="red", lwd=2)
segments(pop.mean.sis[1], tate.method2[1, "lower" ],
pop.mean.sis[2], tate.method2[2, "lower" ], col="red", lty=2)
segments(pop.mean.sis[1], tate.method2[1, "upper" ],
pop.mean.sis[2], tate.method2[2, "upper" ], col="red", lty=2)
text(c(-.03, .07, .02, .4), c(20, 21, 15, 60), adj = c(.5, .5, .5), cex = .8,
labels = c("SATE", "(X,Z)-adjusted\nATE", "(95% confidence intervals)", "range of TATE (95% confidence bounds)\nblue: outcome-model-based method\nred: weighted-outcome-model-based method"))