Code
library(rpact) # load rpact for futility bounds
<- getDesignGroupSequential(typeOfDesign = "asUser", alpha=0.025, userAlphaSpending = c(0,0,0,0,0.025),
fo_p1 typeBetaSpending = "bsP", # Pocock futility boundaries
bindingFutility = FALSE, kMax = 5, sided=1, beta=0.2)
<- fo_p1$futilityBounds # extract futility boundaries
fo_p1_crit <- matrix( c(fo_p1_crit, -Inf), ncol=5, nrow=5) # create matrix to compare with test statistics in simulation, add -Inf for final comparison at end of trial
fo_p1_crit_mat
# set means (m) per study arm
<- 0
mc <- -0.05
m1 <- 0
m2 <- 0.1
m3 <- 0.35
m4 <- 0.4
m5
# set other parameters
<- s1 <- s2 <- s3 <- s4 <- s5 <- 1 # common variance, could change for other scenarios
sc <- 515 # seed for reproducibility
seed <- 100 # max per arm
nmax <- 5 # total number of stages
nstage <- ceiling( seq(0,100,length.out=6)[-1] ) # number enrolled in each stage (so you can change nmax, nstage, etc. and code still works)
n_perstage <- 1000 # number of simulations
nsim
<- strat2_res <- strat3_res <- matrix(nrow=nsim, ncol=11) # create objects to save simulation results
strat1_res
# simulation
set.seed(seed) # set seed for reproducibility
for( i in 1:nsim ){
# use sapply to create matrix of data with each data set represented by a column
<- sapply( c('c',1:5), function(x) rnorm(mean = get(paste0('m',x)), sd = get(paste0('s',x)), n=nmax) )
simdat
# calculate two-sample t-tests for what the observed test statistic and p-value would be at each stage
# write helper function, paircompare(), to extract this information
<- function(arm_control, arm_trt, n_perstage){
paircompare ### Helper function to calculate test statistic and p-value for two groups given data and sample sizes to use
# arm_control/arm_trt: vector with observed data up to max sample size
# n_perstage: sample size after each stage
<- t(sapply(n_perstage, function(z) t.test(arm_trt[1:z], arm_control[1:z], alternative = 'greater')[c('p.value','statistic')] ))
tres <- sapply(n_perstage, function(z) mean(arm_trt[1:z]) - mean(arm_control[1:z] ) )
eres
return( cbind(tres, eres) )
}
<- sapply( 2:6, function(w) paircompare(arm_control = simdat[,1], arm_trt = simdat[,w], n_perstage = n_perstage) )
res <- as.matrix(res[1:5,]) # extract p-values at each stage for control vs. active arm
pval <- as.matrix(res[6:10,]) # extract t-values at each stage for control vs. active arm
tval <- as.matrix(res[11:15,]) # extract observed effect size (trt - con) at each stage (one-sided goal with trt > con)
diff
### Strategy 1: Pocock Boundaries
<- (tval < fo_p1_crit_mat) # calculate if each arm has any test statistics below the futility boundary
fut_stop
<- sapply( 1:5, function(a) which(fut_stop[,a] == TRUE)[1] )
arm_stop1 is.na(arm_stop1) ] <- 5 # make NA 5 since they never dropped for futility
arm_stop1[
<- n_perstage[arm_stop1] # record sample size for each arm
n_strat1 <- sum(n_strat1) # sum up for total sample size
ntot1
<- arm_stop1==5 # calculate indicator if arm made it to the end
finish1
<- rep(FALSE, 5) # create indicator if significant comparison
sig1 which(arm_stop1==5) ] <- unlist(pval[,5])[ which(arm_stop1==5) ] < 0.025 # estimate if arm is significant at alpha=0.025
sig1[
# save results
<- c(ntot1, finish1, sig1)
strat1_res[i,]
### Strategy 2: Drop smallest treatment effect arm as long as not significant
<- diff # create copy of object to manipulate for decision rule
diff2
<- rep(5,5) # create object to save when arm stops, assume 5 for all to start
arm_stop2
for( k in 1:4 ){
<- which( unlist(diff2[k,]) == min(unlist(diff2[k,])) ) # calc arm with min effect size
armnum
if( pval[k,armnum] >= 0.1 ){
<- k
arm_stop2[armnum] <- Inf # make all observed diffs large to ignore in next stage(s)
diff2[,armnum]
}
}
<- n_perstage[arm_stop2] # record sample size for each arm
n_strat2 <- sum(n_strat2) # sum up for total sample size
ntot2
<- arm_stop2==5 # calculate indicator if arm made it to the end
finish2
<- rep(FALSE, 5) # create indicator if significant comparison
sig2 which(arm_stop2==5) ] <- unlist(pval[,5])[ which(arm_stop2==5) ] < 0.025 # estimate if arm is significant at alpha=0.025
sig2[
# save results
<- c(ntot2, finish2, sig2)
strat2_res[i,]
### Strategy 3: Drop smallest treatment effect arm regardless of significance
<- diff # create copy of object to manipulate for decision rule
diff3
<- rep(5,5) # create object to save when arm stops, assume 5 for all to start
arm_stop3
for( k in 1:4 ){
<- which( unlist(diff3[k,]) == min(unlist(diff3[k,])) ) # calc arm with min effect size
armnum
<- k
arm_stop3[armnum] <- Inf # make all observed diffs large to ignore in next stage(s)
diff3[,armnum]
}
<- n_perstage[arm_stop3] # record sample size for each arm
n_strat3 <- sum(n_strat3) # sum up for total sample size
ntot3
<- arm_stop3==5 # calculate indicator if arm made it to the end
finish3
<- sapply( 1:5, function(u) pval[ arm_stop3[u], u] < 0.025 ) # create indicator if significant comparison, here we will check each arm regardless of stopping point
sig3
# save results
<- c(ntot3, finish3, sig3)
strat3_res[i,]
}
# Format results to display
<- colMeans(strat1_res)
strat1 <- sd( strat1_res[,1] )
s1_sd <- c( paste0( round(strat1[1])," (",round(s1_sd),")"), paste0( strat1[2:11]*100, "%") )
s1res
<- colMeans(strat2_res)
strat2 <- sd( strat2_res[,1] )
s2_sd <- c( paste0( round(strat2[1])," (",round(s2_sd),")"), paste0( strat2[2:11]*100, "%") )
s2res
<- colMeans(strat3_res)
strat3 <- sd( strat3_res[,1] )
s3_sd <- c( paste0( round(strat3[1])," (",round(s3_sd),")"), paste0( strat3[2:11]*100, "%") )
s3res
# Format results
library(kableExtra)
<- rbind('Pocock Futility' = s1res, 'Min(ES) and p>0.1' = s2res, 'Min(ES)' = s3res)
kbl_tab
%>%
kbl_tab kbl(col.names=c('Dropping Rule','ESS (SD)', 'ES=-0.5', 'ES=0', 'ES=0.1', 'ES=0.35','ES=0.4', 'ES=-0.5', 'ES=0', 'ES=0.1', 'ES=0.35','ES=0.4') ) %>%
kable_classic() %>%
add_header_above(c(" "=1, " "=1, "Arm Made to End of Trial"=5, "Arm Rejected Null Hypothesis"=5))
Arm Made to End of Trial
|
Arm Rejected Null Hypothesis
|
||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
Dropping Rule | ESS (SD) | ES=-0.5 | ES=0 | ES=0.1 | ES=0.35 | ES=0.4 | ES=-0.5 | ES=0 | ES=0.1 | ES=0.35 | ES=0.4 |
Pocock Futility | 292 (83) | 2.8% | 4.8% | 15.1% | 64.8% | 73.4% | 1.3% | 3.9% | 13.2% | 51.4% | 68.9% |
Min(ES) and p>0.1 | 327 (28) | 3.3% | 5.7% | 18% | 80.7% | 89.2% | 1.6% | 4.2% | 14.8% | 58.6% | 76.9% |
Min(ES) | 300 (0) | 0.1% | 0% | 1.4% | 37.8% | 60.7% | 1.6% | 2.7% | 7.2% | 61% | 71.4% |