Introducing escalation

An R package to unify the interface of dose-escalation models in R.

Photo by Martin Adams on Unsplash

escalation

escalation is a new R package that takes existing dose-finding models and gives them a common interface. To use the language of tidyverse R, escalation provides a grammar for dose-finding by breaking the dose-finding process into chunks. What model should I use? How should I select dose during the trial? How will I know when to stop? Which restrictions should I place on escalation and de-escalation? You can create the dose-finding design you want in escalation by combining elements that perform all of these tasks. This might all sound a little obtuse. Some examples will make it clear.

Examples

CRM

O’Quigley, Pepe, and Fisher (1990) introduced the classic continual reassessment method (CRM) and it has been implemented in many software packages since. One of the most popular R implementations is the dfcrm package by Cheung (2013). Let’s fit a model using dfcrm.

The very least information we need to provide is a dose-toxicity skeleton, and our target toxicity level:

skeleton <- c(0.05, 0.1, 0.25, 0.4, 0.6)
target <- 0.25

We use these to create a model-fitting object:

library(escalation)
model <- get_dfcrm(skeleton = skeleton, target = target)

The model can then be fit to outcomes. The escalation package uses the outcome syntax introduced for phase I trials in Brock (2019) and for seamless phase I/II trials in Brock et al. (2017). Let’s assume we have treated 3 patients at dose-level 2 and none of them experienced toxicity. We represent this using the outcome string 2NNN. Fitting the model to the set of outcomes invokes the dose-selection algorithm:

fit <- model %>% fit('2NNN')

and the fit object will tell you the dose recommended by the CRM model to be administered next:

fit %>% recommended_dose()
## [1] 4

The model advocates skipping straight from dose 2 to dose 4. Clinicians are unlikely to feel comfortable with this. We can respecify the model to expressly not skip doses in escalation:

model <- get_dfcrm(skeleton = skeleton, target = target) %>% 
  dont_skip_doses(when_escalating = TRUE)

We have taken the original dfcrm model and added an extra module to prevent skipping doses in escalation. Refitting the new model to the same outcomes gives:

fit <- model %>% fit('2NNN')
fit %>% recommended_dose()
## [1] 3

We see that the ensemble now decides to select dose 3. We can ask the trial design whether it wants to keep recruting patients:

fit %>% continue()
## [1] TRUE

Naturally it wants to continue because dfcrm does not implement any stopping rules. However, we can easily add some. Let us say that we want to stop once the model has evaluated 18 patients, or at least 9 at the dose being recommended, whichever occurs first. We specify this model by adding more behaviours:

model <- get_dfcrm(skeleton = skeleton, target = target) %>% 
  dont_skip_doses(when_escalating = TRUE) %>% 
  stop_at_n(n = 18) %>% 
  stop_when_n_at_dose(dose = 'recommended', n = 9)

Let’s fit this model to some more patients to see how this trial plays out:

fit <- model %>% fit('2NNN 3TTN')
fit %>% recommended_dose()
## [1] 2
fit %>% continue()
## [1] TRUE

After seeing two-in-three patients in the second cohort experience toxicity at dose 3, the design understandably wants to de-escalate. Let’s do that:

fit <- model %>% fit('2NNN 3TTN 2TNN')
fit %>% recommended_dose()
## [1] 2
fit %>% continue()
## [1] TRUE

The third cohort yielded one-in-three tox at dose 2. The design wants to continue at dose 2 so let’s do that:

fit <- model %>% fit('2NNN 3TTN 2TNN 2NNT')
fit %>% recommended_dose()
## [1] 2
fit %>% continue()
## [1] FALSE

Notice that the call to continue() now returns FALSE. The design wants to stop now and recommend dose 2. It does this because it has seen 9 patients at the recommended dose. We can see the number of patients treated at each of the five doses under investigation:

fit %>% n_at_dose()
## [1] 0 9 3 0 0

Our stopping criteria have been met. We can verify that dose 2 is indeed the dose with posterior expected toxicity rate closest to our target of 25%:

fit %>% mean_prob_tox()
## [1] 0.1845713 0.2728713 0.4575229 0.5964102 0.7496662

BOIN

escalate also implements the BOIN dose-finding design by Liu and Yuan (2015) via the BOIN R-package (Yuan and Liu 2018).

In contrast to CRM, BOIN does not require a dose-toxicity skeleton. In its simplest case, it requires merely the number of doses under investigation and our target toxicity level:

target <- 0.25
model <- get_boin(num_doses = 5, target = target)

As before, we can fit the model to some observed outcomes:

fit <- model %>% fit('2NNN')
fit %>% recommended_dose()
## [1] 3
fit %>% continue()
## [1] TRUE

The BOIN dose selector natively implements stopping rules, as described by Liu & Yuan. For instance, if the bottom dose is too toxic, the design will advise the trial halts:

fit <- model %>% fit('2NTN 1TTT')
fit %>% recommended_dose()
## [1] NA
fit %>% continue()
## [1] FALSE

Nevertheless, as with the CRM examples above, our BOIN selector can be adorned with various behaviours to tailor stopping and skipping. In fact, we can add the exact same behaviours as before because all escalation objects support exactly the same interface. This makes the package completely modular. We are building the design we want from pieces of Lego:

model <- get_boin(num_doses = 5, target = target) %>% 
  dont_skip_doses(when_escalating = TRUE) %>% 
  stop_at_n(n = 18) %>% 
  stop_when_n_at_dose(dose = 'recommended', n = 9)

fit <- model %>% fit('2NNN 3TTN 2TNN 2NNT')
fit %>% recommended_dose()
## [1] 2
fit %>% continue()
## [1] FALSE
fit %>% mean_prob_tox()
## [1]   NA 0.23 0.66   NA   NA

Notice that the posterior toxicity estimate is similar to the CRM model at dose 2, but ultimately different because the two approaches use different model forms. BOIN does not estimate the tox rate at doses that have not been administered.

Initial escalation plans

Another dose-selection mechanism supported by escalation is the concept of an initial escalation plan. These can be used at the start of a trial to show how escalation should proceed initially. As soon as the realised outcomes diverge from the prespecified path, or the path reaches the end, a secondary model takes over.

For instance, let us assume we would like to treat one patient per dose so long as no toxicity is seen, in order to facilitate fast escalation. When toxicity is seen, or dose five is reached, we would like to hand control to a CRM model. We can write:

model <- follow_path('1N 2N 3N 4N 5N') %>% 
  get_dfcrm(skeleton = skeleton, target = target)

So long as the idealised path is realised, it continues:

model %>% 
  fit('1N') %>% 
  recommended_dose()
## [1] 2
model %>% 
  fit('1N 2N') %>% 
  recommended_dose()
## [1] 3

But if the path is deviated from, the secondary model takes over:

model %>% 
  fit('1N 2N 3T') %>% 
  recommended_dose()
## [1] 2

Also, when the initial path is completed, the secondary model continues:

model %>% 
  fit('1N 2N 3N 4N 5N') %>% 
  recommended_dose()
## [1] 5

Summary

The escalation package takes dose-finding methods that other authors have provided and gives them a consistent interface. This makes it simple to add any behaviour to any method, creating a modular Lego-like approach to creating dose-finding designs. For instance, we can take any dose-selection model and tweak the way it selects the next dose, the way it escalates, de-escalates and stops. I hope eventually that escalation makes it easy to specify, assess and compare all dose-escalation designs.

Roadmap

Version 0.0.1 has been submitted to CRAN (but as of 2020-02-17, not yet accepted) with the following base dose-finding approaches:

  • the CRM model from the dfcrm package
  • the BOIN model from the BOIN package
  • the perennial 3+3 model without dose de-escalation

and the following optional embellishments:

  • don’t skip doses in escalation and/or de-escalation
  • stop when dose(s) are too toxic
  • stop when \(n\) patients have been treated in total
  • stop when \(n\) patients have been treated at a particular dose
  • use an initial fixed dose-escalation plan
  • demand at least \(n\) patients have been treated at a particular dose before stopping is permitted.

In future versions, the following base dose-finding approaches will probably be added:

  • the EWOC model from the EWOC package
  • the CRM and EffTox models from the trialr package
  • the CRM model from the bcrm package
  • the CRM model from the crmPack package

Before I plough into the MCMC methods (trialr, bcrm, crmPack), I want to understand the implications of adding other software to the dependency chain to what at the moment is a very lightweight package.

In addition to those, I plan to add further behaviours that allow:

  • stopping under the conditions investigated by Zohar and Chevret (2001);
  • selecting dose by the CIBP criterion of Mozgunov and Jaki (2020).

Independent to the work described above, I will add functions that run simulations and calculate dose pathways.

How escalation came about

It had bothered me for several years that it always seemed so difficult to tweak behaviour of dose-finging designs to allow flexible stopping (etc). In my trials unit, we frequently started with the dfcrm code and then added custom embellishments to achieve the design and behaviour we wanted in each particular trial. Running simulations or calculating future dose pathways was never as simple as it should have been. The approach we used of tweaking dfcrm implicitly assumed we needed a more complex dose-fitting function. And then one night at about 4am when I was neither asleep nor awake, I realised that we needed to daisy-chain the dose-selecting classes together to augment behaviour. We did not need a more complex dose-fitting function, we needed a flexible way of combining simple functions. Having used dplyr and tidyverse packages for years now, the %>% operator seemed the natural solution.

Installation

# Once it hits CRAN, install the latest official version with:
install.packages("escalation")

# Alternatively, install the latest code at any time from GitHub:
devtools::install_github("brockk/escalation")

References

Brock, Kristian. 2019. “trialr: Bayesian Clinical Trial Designs in R and Stan.” arXiv E-Prints, June, arXiv:1907.00161.

Brock, Kristian, Lucinda Billingham, Mhairi Copland, Shamyla Siddique, Mirjana Sirovica, and Christina Yap. 2017. “Implementing the EffTox Dose-Finding Design in the Matchpoint Trial.” BMC Medical Research Methodology 17 (1): 112. https://doi.org/10.1186/s12874-017-0381-x.

Cheung, Ken. 2013. Dfcrm: Dose-Finding by the Continual Reassessment Method. https://CRAN.R-project.org/package=dfcrm.

Liu, Suyu, and Ying Yuan. 2015. “Bayesian Optimal Interval Designs for Phase I Clinical Trials.” Journal of the Royal Statistical Society: Series C (Applied Statistics) 64 (3): 507–23. https://doi.org/10.1111/rssc.12089.

Mozgunov, Pavel, and Thomas Jaki. 2020. “Improving Safety of the Continual Reassessment Method via a Modified Allocation Rule.” Statistics in Medicine, 1–17. https://doi.org/10.1002/sim.8450.

O’Quigley, J, M Pepe, and L Fisher. 1990. “Continual Reassessment Method: A Practical Design for Phase 1 Clinical Trials in Cancer.” Biometrics 46 (1): 33–48. https://doi.org/10.2307/2531628.

Yuan, Ying, and Suyu Liu. 2018. BOIN: Bayesian Optimal Interval (Boin) Design for Single-Agent and Drug- Combination Phase I Clinical Trials. https://CRAN.R-project.org/package=BOIN.

Zohar, Sarah, and Sylvie Chevret. 2001. “The Continual Reassessment Method: Comparison of Bayesian Stopping Rules for Dose-Ranging Studies.” Statistics in Medicine 20 (19): 2827–43. https://doi.org/10.1002/sim.920.

Avatar
Kristian Brock
Principal Biostatistician

I am an academic early phase clincial trial statistician.