by Ian White and Nicholas Latimer
adaptation for R by Paul Schneider
# Function to install and load all the packages used in the course
install_n_load <- function(packages){
for(package in packages){
if(eval(parse(text=paste("require(",package,")")))==0) {
install.packages(package)
}
eval(parse(text=paste("require(",package,")")))
}
}
required_packages = c("haven",
"psych",
"survival",
"flexsurv",
"ggplot2",
"cowplot",
"survminer",
"geepack",
"Hmisc",
"eha",
"rpsftm",
"rms")
# # clear work space from previous results
rm(list = setdiff(ls(),c("install_n_load","required_packages")))
# # Load required packages
install_n_load(required_packages)
options(scipen = 99999)
# # read data
novCEA = as.data.frame(read_dta("NovCEAevents.dta"))
# create survival outcome
novCEA.surv = Surv(novCEA$deathtime,novCEA$deathind)
# create var for proportion time on treatment
novCEA$rx_prop = ifelse(novCEA$trtrand==1,1,0)
novCEA$rx_prop[novCEA$xoind==1] = with(novCEA[novCEA$xoind==1,], 1 - (xotime/deathtime))
# fit RPSFTM
rpsftm.fit = rpsftm(novCEA.surv ~ rand(arm = trtrand, rx= rx_prop),data = novCEA, test = survdiff)
rpsftm.fit
## arm rx.Min. rx.1st Qu. rx.Median rx.Mean rx.3rd Qu. rx.Max.
## 1 0 0.0000000 0.0000000 0.0000000 0.2485265 0.5377358 0.8242678
## 2 1 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000
## Call:
## rpsftm(formula = novCEA.surv ~ rand(arm = trtrand, rx = rx_prop),
## data = novCEA, test = survdiff)
##
## N Observed Expected (O-E)^2/E (O-E)^2/V
## .arm=0 108 103 103 0.000425 0.000754
## .arm=1 192 154 154 0.000285 0.000754
##
## Chisq= 0 on 1 degrees of freedom, p= 1
##
## psi: -0.5439109
## exp(psi): 0.5804736
# psi and 95% CI (again, differences in the 4th digit)
rpsftm.fit$psi
## [1] -0.5439109
rpsftm.fit$CI
## [1] -0.7492706 -0.3245246
rpsftm.fit2 = rpsftm(novCEA.surv ~ rand(arm = trtrand, rx= rx_prop),data = novCEA, test = survdiff, censor_time=admin)
rpsftm.fit2
## arm rx.Min. rx.1st Qu. rx.Median rx.Mean rx.3rd Qu. rx.Max.
## 1 0 0.0000000 0.0000000 0.0000000 0.2485265 0.5377358 0.8242678
## 2 1 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000
## Call:
## rpsftm(formula = novCEA.surv ~ rand(arm = trtrand, rx = rx_prop),
## data = novCEA, censor_time = admin, test = survdiff)
##
## N Observed Expected (O-E)^2/E (O-E)^2/V
## .arm=0 108 94 94.2 0.000466 0.000754
## .arm=1 192 154 153.8 0.000285 0.000754
##
## Chisq= 0 on 1 degrees of freedom, p= 1
##
## psi: -0.5439109
## exp(psi): 0.5804736
# psi and 95% CI (again, differences in the 4th digit)
rpsftm.fit2$psi
## [1] -0.5439109
rpsftm.fit2$CI
## [1] -0.7492706 -0.3245246
# Plot the graph of Z(psi) against psi
plot(rpsftm.fit2$eval_z, type="s",main="Ever-treated RPSFTM",ylab="log Z rank")
abline(h = c(1.96,-1.96), lty=2)
abline(h = 0,v = 0, lty=1)
# construct a function to plot counter factual survivial times (also for later use)
plot.cf = function(mfit = rpsftm.fit2$fit,title=""){
rpsftm.df = data.frame(time = mfit$time, survival = mfit$surv, upper = mfit$upper, lower = mfit$lower)
rpsftm.df$group = factor( rep(1:2, mfit$strata), labels=c("Control","Treatment"))
rpsftm.df$model = "rpsftm: treatment free"
km.fit = survfit(novCEA.surv~novCEA$trtrand)
km.df = data.frame(time = km.fit$time, survival = km.fit$surv, upper = km.fit$upper, lower = km.fit$lower)
km.df$group = factor( rep(1:2, km.fit$strata), labels=c("Control","Treatment"))
km.df$model = "KM observed"
ggplot()+
geom_step(data= rpsftm.df,aes(x = time, y = survival, group = group,lty=model,col=group)) +
geom_step(data= km.df,aes(x = time, y = survival, group = group,lty=model,col=group))+ ggtitle(title)
}
# plot
plot.cf(mfit = rpsftm.fit2$fit,title="")
NOTE: This paper reproduces the RPSFTM for the Concorde Trial, mentioned in the course-PPT, in R: https://journal.r-project.org/archive/2017/RJ-2017-068/RJ-2017-068.pdf
# counterfactual untreated failure times.
S_psi = rpsftm.fit2$Sstar
# index treated group
Stest <- novCEA$trtrand==1
# replace counter factual with observed for the fully treated group
S_psi[Stest,] <- with(novCEA,Surv(deathtime,deathind))[Stest]
# fit UNadjusted cox
cph_fit <- coxph(novCEA.surv ~ novCEA$trtrand)
cph_fit
## Call:
## coxph(formula = novCEA.surv ~ novCEA$trtrand)
##
## coef exp(coef) se(coef) z p
## novCEA$trtrand -0.5236 0.5924 0.1286 -4.071 0.0000469
##
## Likelihood ratio test=15.88 on 1 df, p=0.00006735
## n= 300, number of events= 257
# And compare with adjusted Hazard Ratio
adj_cph_fit <- coxph(S_psi ~ novCEA$trtrand)
adj_cph_fit
## Call:
## coxph(formula = S_psi ~ novCEA$trtrand)
##
## coef exp(coef) se(coef) z p
## novCEA$trtrand -0.8014 0.4487 0.1420 -5.642 0.0000000169
##
## Likelihood ratio test=30.47 on 1 df, p=0.00000003397
## n= 300, number of events= 248
# unadjusted 95% CI of the adjusted HR
data.frame(
HR=exp(adj_cph_fit$coefficients),
lower=exp(confint(adj_cph_fit)[1]),
upper=exp(confint(adj_cph_fit)[2]))
## HR lower upper
## novCEA$trtrand 0.4487177 0.3396743 0.5927665
# adjusted 95% CI of the adjusted HR
# exp(ln(HR) +/- 1.96*(ln(HR)/Zstat)), with z= se of unadjusted HR
z <- coef(cph_fit)/sqrt(diag(vcov(cph_fit)))
data.frame(
HR=exp(adj_cph_fit$coefficients),
lower=exp(adj_cph_fit$coefficients - 1.96*adj_cph_fit$coefficients/z),
upper=exp(adj_cph_fit$coefficients + 1.96*adj_cph_fit$coefficients/z))
## HR lower upper
## novCEA$trtrand 0.4487177 0.3050698 0.6600048
Bootstrapping adjusted CI
# bootstrapping
boot_rpfstm_ci = function(data=novCEA,iterations){
n.rows = length(data[,1])
boot.est = rep(NA,times=iterations)
for(i in 1:iterations){
cat("\r ",i)
data.b = data[sample(1:n.rows,size=n.rows,replace = T),]
novCEA.surv.t = Surv(data.b$deathtime,data.b$deathind)
rpsftm.fit.t = rpsftm(novCEA.surv.t ~ rand(arm = trtrand, rx= rx_prop),
data = data.b,
test = survdiff,
censor_time = admin)
S_psi = rpsftm.fit.t$Sstar
Stest <- data.b$trtrand==1
S_psi[Stest,] <- with(data.b,Surv(deathtime,deathind))[Stest]
adj_cph_fit <- coxph(S_psi ~ data.b$trtrand)
boot.est[i] = adj_cph_fit$coefficients
}
ci = quantile(boot.est,c(0.05,0.95))
est = mean(boot.est)
res.df = data.frame(est= est,
lower = ci[1],
upper = ci[2])
return(res.df)
}
boot.res = boot_rpfstm_ci(iterations = 10) # more iterations: more accurate, but slow!
##
1
2
3
4
5
6
7
8
9
10
boot.res
## est lower upper
## 5% -0.739682 -0.8434899 -0.6095468
rpsftm.fit.alt1 = rpsftm(novCEA.surv ~ rand(arm = trtrand, rx= rx_prop),
data = novCEA,censor_time = admin, test = survreg)
rpsftm.fit.alt1
## arm rx.Min. rx.1st Qu. rx.Median rx.Mean rx.3rd Qu. rx.Max.
## 1 0 0.0000000 0.0000000 0.0000000 0.2485265 0.5377358 0.8242678
## 2 1 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000
##
## Coefficients:
## (Intercept)
## 5.37699
##
## Scale= 0.6159772
## n= 300
##
## psi: -0.5466217
## exp(psi): 0.5789022
#adj cox for alt1
S_psi.alt1 = rpsftm.fit.alt1$Sstar
S_psi.alt1[Stest,] <- with(novCEA,Surv(deathtime,deathind))[Stest]
adj_cph_fit.alt1 <- coxph(S_psi.alt1 ~ novCEA$trtrand)
adj_cph_fit.alt1
## Call:
## coxph(formula = S_psi.alt1 ~ novCEA$trtrand)
##
## coef exp(coef) se(coef) z p
## novCEA$trtrand -0.8020 0.4484 0.1421 -5.646 0.0000000165
##
## Likelihood ratio test=30.51 on 1 df, p=0.00000003323
## n= 300, number of events= 248
rpsftm.fit.alt2 = rpsftm(novCEA.surv ~ rand(arm = trtrand, rx= rx_prop),
data = novCEA,censor_time = admin ,test = coxph)
rpsftm.fit.alt2
## arm rx.Min. rx.1st Qu. rx.Median rx.Mean rx.3rd Qu. rx.Max.
## 1 0 0.0000000 0.0000000 0.0000000 0.2485265 0.5377358 0.8242678
## 2 1 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000
## n= 300, number of events= 248
##
## psi: -0.5440459
## exp(psi): 0.5803953
#adj cox for alt2
S_psi.alt2 = rpsftm.fit.alt2$Sstar
S_psi.alt2[Stest,] <- with(novCEA,Surv(deathtime,deathind))[Stest]
adj_cph_fit.alt2 <- coxph(S_psi.alt2 ~ novCEA$trtrand)
adj_cph_fit.alt2
## Call:
## coxph(formula = S_psi.alt2 ~ novCEA$trtrand)
##
## coef exp(coef) se(coef) z p
## novCEA$trtrand -0.8014 0.4487 0.1420 -5.642 0.0000000169
##
## Likelihood ratio test=30.47 on 1 df, p=0.00000003397
## n= 300, number of events= 248
# note we need to include the covariate in both rpsftm() and the subsequent Cox model
# with covariate
rpsftm.fit.covar = rpsftm(novCEA.surv ~ rand(arm = trtrand, rx= rx_prop)+
prognosisb,data = novCEA,censor_time = admin ,test = coxph)
rpsftm.fit.covar
## arm rx.Min. rx.1st Qu. rx.Median rx.Mean rx.3rd Qu. rx.Max.
## 1 0 0.0000000 0.0000000 0.0000000 0.2485265 0.5377358 0.8242678
## 2 1 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000
## coef exp(coef) se(coef) z p
## prognosisb 0.780 2.182 0.131 5.94 0.0000000028
## n= 300, number of events= 247
##
## psi: -0.5469189
## exp(psi): 0.5787302
#adj cox for alt2
S_psi.covar = rpsftm.fit.covar$Sstar
S_psi.covar[Stest,] <- with(novCEA,Surv(deathtime,deathind))[Stest]
adj_cph_fit.covar <- coxph(S_psi.covar ~ novCEA$trtrand + novCEA$prognosisb)
adj_cph_fit.covar
## Call:
## coxph(formula = S_psi.covar ~ novCEA$trtrand + novCEA$prognosisb)
##
## coef exp(coef) se(coef) z p
## novCEA$trtrand -0.8836 0.4133 0.1438 -6.144 0.000000000805
## novCEA$prognosisb 0.7548 2.1272 0.1311 5.758 0.000000008507
##
## Likelihood ratio test=63.19 on 2 df, p=0.00000000000001895
## n= 300, number of events= 247
novCEA$k = ifelse(novCEA$trtrand==1,1, 0.5) # effect modifier
rpsftm.fit.k05 = rpsftm(novCEA.surv ~ rand(arm = trtrand, rx= rx_prop),data = novCEA,censor_time = admin ,test = survdiff, treat_modifier = k)
rpsftm.fit.k05
## arm rx.Min. rx.1st Qu. rx.Median rx.Mean rx.3rd Qu. rx.Max.
## 1 0 0.0000000 0.0000000 0.0000000 0.2485265 0.5377358 0.8242678
## 2 1 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000 1.0000000
## Call:
## rpsftm(formula = novCEA.surv ~ rand(arm = trtrand, rx = rx_prop),
## data = novCEA, censor_time = admin, test = survdiff, treat_modifier = k)
##
## N Observed Expected (O-E)^2/E (O-E)^2/V
## .arm=0 108 102 102 0.0000001106 0.000000195
## .arm=1 192 154 154 0.0000000732 0.000000195
##
## Chisq= 0 on 1 degrees of freedom, p= 1
##
## psi: -0.4437057
## exp(psi): 0.6416543
plot.cf(mfit = rpsftm.fit.k05$fit,title="Ever treated - effect after switching k = 0.5")
–> on-treated analysis
# weird observations!
novCEA[novCEA$trtrand ==0 & !is.na(novCEA$disconexp) & novCEA$xoind==0,]
## [1] BASELINE id prognosisb CEAb HRQLb trtrand
## [7] PROGRESSION progtime progind CEAprog EVENTS xotime
## [13] xoind disconexp deathtime deathind admin rx_prop
## [19] k
## <0 rows> (or 0-length row.names)
# create proportion of time on treatment var
novCEA$tnowon = NA
ind1 = novCEA$trtrand ==1 & !is.na(novCEA$disconexp)
novCEA$tnowon[ind1] = novCEA$disconexp[ind1]
ind2 = novCEA$trtrand ==0 & novCEA$xoind==0
novCEA$tnowon[ind2] = 0
ind3 = novCEA$trtrand ==0 & !is.na(novCEA$disconexp) & novCEA$xoind==1
novCEA$tnowon[ind3] = novCEA$disconexp[ind3] - novCEA$xotime[ind3]
novCEA$ton_prop = novCEA$tnowon / novCEA$deathtime
# fit rpsftm
rpsftm.ton.fit = rpsftm(novCEA.surv ~ rand(arm = trtrand, rx= ton_prop),data = novCEA, test = survdiff,censor_time = admin,low_psi=-2,hi_psi = 1)
## Warning in root(qnorm(1 - alpha/2)): Multiple Roots found
rpsftm.ton.fit
## arm rx.Min. rx.1st Qu. rx.Median rx.Mean rx.3rd Qu. rx.Max.
## 1 0 0.0000000 0.0000000 0.0000000 0.1785152 0.3589891 0.7029289
## 2 1 0.2937063 0.5165921 0.6153846 0.6551200 0.7304348 1.0000000
## Call:
## rpsftm(formula = novCEA.surv ~ rand(arm = trtrand, rx = ton_prop),
## data = novCEA, censor_time = admin, test = survdiff, low_psi = -2,
## hi_psi = 1)
##
## N Observed Expected (O-E)^2/E (O-E)^2/V
## .arm=0 108 61 61 0.000000978 0.00000157
## .arm=1 192 101 101 0.000000590 0.00000157
##
## Chisq= 0 on 1 degrees of freedom, p= 1
##
## psi: -1.084247
## exp(psi): 0.3381563
### plot
plot.cf(mfit = rpsftm.ton.fit$fit,title="On-Treatment analysis")
exp(rpsftm.ton.fit$psi)
## [1] 0.3381563
rpsftm.ton.fit$CI
## [1] -1.5315857 -0.5432385
Stest <- novCEA$trtrand==1
S_psi.ton = rpsftm.ton.fit$Sstar
S_psi.ton[Stest,] <- with(novCEA,Surv(deathtime,deathind))[Stest]
adj_cph_fit.ton <- coxph(S_psi.ton ~ novCEA$trtrand)
exp(adj_cph_fit.ton$coefficients)
## novCEA$trtrand
## 0.4542838