Binomial endpoint
binomial.Rmd
Context
This example is motivated by the Personalised Immunotherapy Platform (PIP) trial (Lo et al. 2022), which is a phase II randomised controlled trial that is currently under development that will compare immunotherapy combinations for advanced melanoma patients.
This study aims to assess the effectiveness of different novel drug combinations compared to a control arm in treatment-naive patients that are predicted to be unlikely to respond to standard first line immunotherapy treatments. The primary outcome is the objective response rate (ORR, i.e., the probability of observing a one/success) according to the Response Evaluation Criteria in Solid Tumors (RECIST) (partial or complete response vs stable disease or progressive disease) at 6 months post randomisation.
Design
The chosen design has the following characteristics:
- treatment arms: Our case study has 6 treatment arms: standard of care (arm “A”), five drug combinations (referred to as arms “B”, “C”, “D”, “E” and “F”).
- alternative hypotheses: This is a superiority trial where we are interested in demonstrating efficacy compared to the standard of care arm and intervention arms will not be compared to one another.
- interim analyses and maximum sample size: We will begin the interim analyses at 60 participants completing follow-up, and perform interim analyses every 12 participants completing follow-up thereafter. Our maximum sample size is 216 participants and so a total of 14 looks are planned.
- endpoint conditional distribution: the binary endpoint is assumed to follow a binomial distribution.
-
group allocation: the trial will start with equal
randomisation (1:1:1:1:1:1) and RAR will be used from interim analysis 1
onwards. The randomisation probabilities are calculated using a
variation of Trippa et al. (2012) and
Cellamare et al. (2017) where the
allocation probabilities at look \(j\)
are proportional to:
- control arm: \[
\frac{\text{exp}\left(\text{max}(n_{kj}) -
n_{0j}\right)^{\nu}}{K_j}\] where
- \(n_{kj}\) corresponds to the sample size of arm \(k\) (where \(k=0\) denotes the control arm) at stage \(j\) and \(\text{max}(n_{kj})\) is evaluated across intervention arms only (i.e. for \(k>0\)),
- \(K_j\) is the number of active arms (excluding control) at look \(j\),
- \(\nu=0.1\) is a tuning parameter.
- intervention arms: \[\frac{\text{Prob}(\beta_k>\delta_{r}|y,X)^h}{\sum_{k=1}^K
\text{Prob}(\beta_k>\delta_{r}|y,X)^h}\] where
- \(\beta_k\) denotes the \(k\)th target parameter (on the logit scale),
- \(\delta_{r} = 0\) denotes the (RAR-related) clinically meaningful treatment effect value,
- and \(h = \gamma(\sum_{k=0}^{K} n_{kj} /
N)^{\eta}\) with
- \(n_{kj}\), the sample size of arm \(k\) at stage \(j\),
- \(N\), the maximum sample size,
- \(\gamma=3\) and \(\eta=1.4\), two tuning parameters. Refer to Gotmaker et al. (2019) for details
- control arm: \[
\frac{\text{exp}\left(\text{max}(n_{kj}) -
n_{0j}\right)^{\nu}}{K_j}\] where
- efficacy stopping rule: Stopping arms for efficacy is not permitted at the interim analyses as the main objective of the adaptations is to drop poorly performing arms.
-
end of trial efficacy rule: An intervention may be
declared effective at the end of the trial if the posterior probability
of efficacy is above a certain (high) threshold: \[\text{Prob}(\beta_k > \delta_{\epsilon}|y, X)
> 1-b_{\epsilon}\] where
- \(\delta_{\epsilon} = 0\) denotes the (efficacy-related) clinically meaningful treatment effect value,
- \(b_{\epsilon}=0.045\) is a tuning parameter.
-
futility stopping rule: Early stopping of
intervention arms for futility may occur if there is a low posterior
probability of observing at least a 10% absolute increase (improvement)
in the proportion of responders, i.e., when \[ \text{Prob}(\beta_k > \delta_{f}|y, X) <
b_f\] where
- \(\delta_{f} = log(1.5)\) denotes the (futility-related) clinically meaningful treatment effect values assuming a control ORR of 40%,
- \(b_{f} = 0.1\) is the cut-off value used to declare futility.
- trial stopping rule: The trial will run until a futility decision has been reached for each intervention arm or once the maximum sample size has been reached.
Note that, in the above, the tuning parameter values have been optimised to lead to suitable operating characteristics.
In the following, we define the group allocation, RAR, futility and efficacy functions corresponding to the design described above
RAR
We need to generate a function that defines the allocation probabilities of patients to the different active arms as a function of
- relevant ingredients, i.e.,
-
posterior
, for the posterior probabilities of the target parameter being greater thandelta.RAR = 0
, -
n
andN
, respectively the sample size per arm at the look of interest and the max sample size to define the information fraction, -
ref
, for a vector of logicals indicating which group is the reference one, -
active
, for a vector of logicals indicating which groups are active at the look of interest,
-
- relevant tuning parameters (to be added to
RAR.control
inbatss.glm
), namely \(\gamma\), \(\eta\), and \(\nu\), that we will respectively callgamma
,eta
andnu
# function
prob.trippa = function(posterior,n,N,ref,active,gamma,eta,nu){
g = sum(active)
h = gamma*(sum(n)/N)^eta
prob = rep(NA,g)
# reference/control arm allocation
prob[1] = (exp(max(n[!ref])-n[ref])^nu)/(g-1)
# targets/interventions (that haven't been dropped)
prob[2:g] = (posterior^h)/(sum(posterior^h))
unlist(prob)
}
# test after at the first look with two set of posterior probabilities
# 1/ c(.5,.5,.5,.5,.5)
# 2/ c(.5,.5,.5,.5,.6)
prob.trippa(c(.5,.5,.5,.5,.5),n=c(A=10,B=10,C=10,D=10,E=10,F=10),N=16,
ref = c(TRUE,rep(FALSE,5)), active = rep(TRUE,6),
gamma=3, eta=1.4, nu=0.1)
prob.trippa(c(.5,.5,.5,.5,.6),n=c(A=10,B=10,C=10,D=10,E=10,F=10),N=16,
ref = c(TRUE,rep(FALSE,5)), active = rep(TRUE,6),
gamma=3, eta=1.4, nu=0.1)
Note that this corresponds to the function RAR.trippa
available in the BATSS package.
Group allocation
We need to generate a function that randomises the m
participants of the next look according to the allocation ratios
prob
, where m
and prob
are
ingredients described in the Ingredients
section and where prob
is the output of the
user-defined RAR function.
# function
treatalloc.fun = function(m,prob){
prob = abs(prob)/sum(abs(prob))
m0.g = floor(prob*m)
m0 = sum(m0.g)
factor(rep(names(prob),m0.g+rmultinom(1,m-m0,prob)),
levels=names(prob))
}
# test on m = 60 patients and equal allocation per group
table(treatalloc.fun(m=60,prob=c(A=1,B=1,C=1,D=1,E=1,F=1)))
treatalloc.fun
first allocates the largest possible
number of units to the different groups given their exact target
probabilities and then assigns randomly the remaining units to the
different groups according to multinomial draws.
Note that this corresponds to the function
alloc.balanced
available in the BATSS
package.
Efficacy rule
We need to generate a function that leads to a logical output when used at the last look of the trial and takes as input
- the ingredient
posterior
for the posterior probability of the target parameter being greater thandelta.eff = 0
, - the additional parameter \(b_{\epsilon}\) that we will name
b.eff
(and that needs to be added toeff.arm.control
inbatss.glm
).
# function
efficacy.arm.fun = function(posterior,b.eff){
posterior > 1-b.eff
}
# test for a parameter with a posterior = 0.999
efficacy.arm.fun(0.999, b.eff = 0.045)
# test for a parameter with a posterior = 0.95
efficacy.arm.fun(0.95, b.eff = 0.045)
Arm futility stopping rule
We need to generate a function that leads to a logical output and takes as input
- the ingredient
posterior
for the posterior probability of the target parameter being greater thandelta.fut = log(1.5)
, - the additional parameter \(b_{f}\)
that we will name
b.fut
(and that needs to be added tofut.arm.control
inbatss.glm
)
# function
futility.arm.fun = function(posterior,b.fut){
posterior < b.fut
}
# test
futility.arm.fun(0.9, b.fut=0.1)
futility.arm.fun(0.075, b.fut=0.1)
Note that this corresponds to the function
fut.arm.simple
available in the BATSS
package.
Trial futility stopping rule
We need to generate a function that, based on the ingredient
fut.target
(indicating if futility was declared for each
target parameter at the stage of interest or before) leads to a logical
output equal to TRUE
if all target parameters were declared
futile and FALSE
otherwise.
# function
futility.trial.fun = function(fut.target){
all(fut.target)
}
# test
futility.trial.fun(c(B=TRUE,C=TRUE,D=TRUE,E=TRUE,F=TRUE))
futility.trial.fun(c(B=TRUE,C=TRUE,D=TRUE,E=TRUE,F=FALSE))
Note that this corresponds to the function fut.trial.all
available in the BATSS package and that this behaviour
is the default one of the function batss.glm
when
fut.trial = NULL
.
Monte Carlo Simulations
We consider two scenarios:
- Scenario 1: each arm has an ORR of 0.4 (global null),
-
Scenario 2:
- arms “A”, “B” and “C” have an ORR of 0.4,
- arms “D” has an ORR of 0.5,
- arms “E” and “F” have an ORR of 0.7.
Scenario 1
# number of trials
R = 25
# logit function
logit = function(p){log(p/(1 - p))}
# simulation
scenario1 = batss.glm(
model = y~group,
var = list(y = rbinom,
group = treatalloc.fun),
var.control = list(y = list(size = 1)),
family = "binomial",
link = "logit",
beta = c(logit(0.4), rep(0,5)),
which = c(2:6),
R = R,
alternative = c("greater"),
RAR = prob.trippa,
RAR.control = list("gamma"=3, "eta"=1.4,"nu"=0.1),
delta.RAR = 0,
prob0 = c(A=1,B=1,C=1,D=1,E=1,F=1),
N = 216,
interim = list(recruited=list(m0=60, m=12)),
eff.arm = efficacy.arm.fun,
delta.eff = c(rep(NA,13), 0),
eff.arm.control = list(b.eff = 0.045),
delta.fut = log(1.5),
fut.arm = futility.arm.fun,
fut.arm.control = list(b.fut = 0.1),
computation = "parallel",
mc.cores = 12,
H0 = FALSE,
extended = 1)
You can note that
-
y
is generated via the functionrbinom
and has an ORR of respectively-
expit(logit(0.4)) = 0.4
for the reference group, wherelogit(0.4) = -0.405
corresponds to the log odds of ‘success’ for the control group, -
expit(logit(0.4+0)) = 0.4
for all other groups (as contrasts of type treatment are used for the factortreatment
in the self-defined functiontreatalloc.fun
), where0
corresponds to the log odds ratio value related to groups “B” to “F” (i.e., no effect),
-
- the target parameters (corresponding to the shift in ORR of
treatment arms “B” to “F” compared to the “control” on the logit scale)
are in position 2 to 6 of the fitted coefficients obtained when using
the formula
y~group
, -
prob0
provides- the (equal) allocation probabilities at the start of the trial,
- the names of the groups,
-
eff.arm
andfut.arm
are set to the functions defined above (i.e.,efficacy.arm.fun
andfutility.arm.fun
), -
delta.eff
is set toNA
for all 13 interim analyses and to0
for the last look. This ensures that the trial can’t stop early for efficacy. This aim could be achieved using other strategies, like setting the output of the function indicated undereff.trial
toFALSE
, -
fut.trial
is not specified and therefore equal toNULL
(default) which leads to the behaviour wished in this case, - the values of the additional parameters of
efficacy.arm.fun
andfutility.arm.fun
(i.e.,b.eff
andb.fut
) are specified undereff.arm.control
andfut.arm.control
, -
delta.eff
anddelta.fut
are respectively set to0
andlog(1.5)
.
We chose here a low number of seeds/trials (R=25
) to
save time.
Scenario 2
# number of trials
R = 25
# logit function
logit = function(p){log(p/(1 - p))}
# simulation
scenario1 = batss.glm(
model = y~group,
var = list(y = rbinom,
group = treatalloc.fun),
var.control = list(y = list(size = 1)),
family = "binomial",
link = "logit",
beta = c(logit(0.4), # log odd of reference group
0,0, # log odds ratio of groups B and C
logit(0.5)-logit(0.4), # log odds ratio of group D
rep(logit(0.7)-logit(0.4),2)), # log odds ratio of groups E and F
which = c(2:6),
R = R,
alternative = c("greater"),
RAR = prob.trippa,
RAR.control = list("gamma"=3, "eta"=1.4,"nu"=0.1),
delta.RAR = 0,
prob0 = c(A=1,B=1,C=1,D=1,E=1,F=1),
N = 216,
interim = list(recruited=list(m0=60, m=12)),
eff.arm = efficacy.arm.fun,
delta.eff = c(rep(NA,13), 0),
eff.arm.control = list(b.eff = 0.045),
delta.fut = log(1.5),
fut.arm = futility.arm.fun,
fut.arm.control = list(b.fut = 0.1),
computation = "parallel",
mc.cores = 12,
H0 = FALSE,
extended = 1)
Same as above except for beta
.