## Statistical Code

The program is written in R.

View Code


function(input_type, nsim, n, lambda1, hratio, hratio_alt, accrual, follow,
event_frac, int_frac, int_follow, alpha, alpha_interim, frac_control) {

eventv1 = eventv2 = int.sev = int.eventv1 = int.eventv2 = int.betav = betav = testv = rep(NA, nsim)  # storage

lambda2 = lambda1 / hratio                # hazard rate for the experimental
int_accrual = min(int_frac, 1) * accrual  # int_frac must be smaller or equal to 1
sample.size = n
n1a = round(frac_control * n)
n2a = sample.size - n1a

# If provided, convert event fraction to accrual and follow-up time
if (input_type == "Total") {
follow.v = seq(from = 0, to = follow, by = follow / 100)
accrual.v = seq(from = 0, to = accrual, by = accrual / 100)

prob1.arm1 = (accrual.v - 1 / lambda1 + (1 / lambda1) * exp(-lambda1 * accrual.v)) / accrual             #arm 1 prob event
prob2.arm1 = 1 - exp(-lambda1 * follow.v) + (exp(-lambda1 * (accrual + follow.v)) / lambda1 +
(accrual - 1 / lambda1) * exp(-lambda1 * follow.v)) / accrual

prob1.arm2 = (accrual.v - 1 / lambda2 + (1 / lambda2) * exp(-lambda2 * accrual.v)) / accrual             #arm 2 prob event
prob2.arm2 = 1 - exp(-lambda2 * follow.v) + (exp(-lambda2 * (accrual + follow.v)) / lambda2 +
(accrual - 1 / lambda2) * exp(-lambda2 * follow.v)) / accrual

probv.arm1 = c(prob1.arm1, prob2.arm1)
probv.arm2 = c(prob1.arm2, prob2.arm2)

probv = frac_control * probv.arm1 + (1 - frac_control) * probv.arm2                    # sum events accross arms weighted by arm sample size

times = c(accrual.v, follow.v)
accstat = c(rep(1, 101), rep(2, 101))

ind1 = probv >= event_frac
indpt = min((1:202)[ind1])

if (accstat[indpt] == 1) {
int_accrual = times[indpt]
} else if (accstat[indpt] == 2) {
int_accrual = accrual
}

int_frac = min(int_accrual / accrual, 1)
}

# This is the simple case
for (jj in 1:nsim) {
# Generate full analysis data unobservables
tt1 = -log(runif(n1a)) / lambda1
tt2 = -log(runif(n2a)) / lambda2
cc1 = follow + runif(n1a) * accrual
cc2 = follow + runif(n2a) * accrual
tt = c(tt1, tt2)
cc = c(cc1, cc2)
# Generate times corresponding to the first interim

int.n1 = round(int_frac * n1a)   #no pts in arm 1 at interim
int.n2 = round(int_frac * n2a)

oo1 = rev(order(cc1))    #ordering for accrual time
int.patients1 = oo1[1:int.n1]

oo2 = rev(order(cc2))
int.patients2 = oo2[1:int.n2]

int.cc = c(cc1[int.patients1], cc2[int.patients2]) - (follow - int_follow)  #create true censoring times
int.tt = c(tt1[int.patients1], tt2[int.patients2]) #create true survival times

int.cc = int.cc - min(int.cc) + .01      #??? Why wouldn't I do the same for true survival times

int.status = 1 * (int.tt < int.cc)
int.group = c(rep(0, int.n1), rep(1, int.n2))
int.times = int.status * int.tt + (1 - int.status) * int.cc
int.bb = survival::coxph(survival::Surv(int.times, int.status) ~ int.group)   # So we can store all the parameters we need from here
int.betav[jj] = int.bb$coef int.sev[jj] = summary(int.bb)$coef[3]
int.eventv1[jj] = sum(int.status[int.group == 0])
int.eventv2[jj] = sum(int.status[int.group == 1])

# final analysis
status = 1 * (tt < cc)
group = c(rep(0, n1a), rep(1, n2a))
times = status * tt + (1 - status) * cc
bb = survival::coxph(survival::Surv(times, status) ~ group)
testv[jj] = bb$score betav[jj] = bb$coef
eventv1[jj] = sum(status[group == 0])
eventv2[jj] = sum(status[group == 1])
}

# mean number of events in control/experimental arm at time of each analysis
events.int = c(mean(int.eventv1), mean(int.eventv2))
events.final = c(mean(eventv1), mean(eventv2))

event_mat = rbind(c(events.int, sum(events.int)), c(events.final, sum(events.final)))
rownames(event_mat) = c("interim", "final")
colnames(event_mat) = c("Control", "Expt", "Total")
vv = dim(event_mat)

# alpha = .2        # Testing Null
# alpha_interim = .05  # Testing Alternative

# Calulate Futility Stopping Probablities

# Using HR = 1 Rule
stop.betav =! (int.betav < 0)
stop.HR1 = mean(stop.betav)

# Using Test with alpha_interim
tt2 = (-((-log(hratio_alt) - int.betav) / int.sev)) < qnorm(1 - alpha_interim)
stop.testv =! tt2
stop.test = mean(stop.testv)

# Thresholds Using Test
stop.test.thresh = -(log(hratio_alt) - qnorm(1 - alpha_interim) * int.sev)

# Power with No Interim Analysis
rejectv = (testv * (betav < 0)) > qchisq(1 - alpha, 1)
power.no = mean(rejectv)

# Power with Interim Analysis - using HR1 rule
power.HR1 = mean(rejectv * (!stop.betav))

# Power with Interim Analysis - using Interim Test
power.test = mean(rejectv * (!stop.testv))

# OUTPUT
output =
list(n_per_arm = n,
lambda1 = lambda1,
hratio = hratio,
accrual = accrual,
int_frac = int_frac,
int_accrual = int_accrual,
alpha_interim = alpha_interim,
alpha = alpha,
total_events = event_mat[vv[1], vv[2]],
event_mat = as.data.frame(event_mat),
n_interim_analysis = int.n1,
p_stop_int_hr = stop.HR1,
p_stop_int_test = stop.test,
p_stop_int_test_minus_beta = mean(stop.test.thresh),
power_no_minus_interim = power.no,
power_hr1_minus_interim = power.HR1,
power_test_minus_interim = power.test)
return(jsonlite::toJSON(output, pretty = TRUE))
}