Introduction to package ‘precautionary’

David C. Norris



In package escalation, you can simulate a 3 + 3 design as follows:

# Design a 3 + 3 trial, with 5 pre-specified doses to escalate through
design <- get_three_plus_three(num_doses = 5)
# Posit a scenario where these 5 doses cause dose-limiting toxicities
# (DLTs) in 12%, 27%, etc. of the population:
scenario <- c(0.12, 0.27, 0.44, 0.53, 0.57)
design %>% simulate_trials( # Feed the design to the simulator ...
  num_sims = 100            # run 100 simulated trials
, true_prob_tox = scenario  # under the chosen scenario,
) -> sims                   # and store the results.

summary(sims) # summarize simulation results

Table 1: Simulation summary as produced by package escalation.

dose tox n true_prob_tox prob_recommend prob_administer
NoDose 0.00 0.00 0.00 0.12 0.0000000
1 0.43 3.81 0.12 0.36 0.3607955
2 1.06 3.90 0.27 0.36 0.3693182
3 0.90 2.10 0.44 0.16 0.1988636
4 0.37 0.75 0.53 0.00 0.0710227
5 0.00 0.00 0.57 0.00 0.0000000

From Table 1, we learn a few things of interest. For example, we see that, with the lowest dose being too toxic for 12% of the population, under this scenario there is a real chance even this dose will be rejected by our trial. Conversely, we also find it is not impossible for our trial to recommend a dose that is toxic to nearly half the population. Note also that, by summing the \(n\) column, we can estimate expected enrollment at 10.56, which perhaps informs us about the expected cost or duration of our trial.1 Any complete simulation study will of course consider multiple scenarios, perhaps weighted according to their varying likelihood.

But simulations such as these cannot answer crucial questions about trial safety, mainly because the simulation machinery recognizes no notion of graded toxicity. The binary (yes/no) toxicities in the simulation machinery of escalation regard Grade 5 (fatal) toxicities no differently from Grade 3.

Introducing realistic pharmacologic thinking

The precautionary package solves this problem by pursuing a realistic approach to pharmacologic thinking. Rather than plucking a sequence of toxicity probabilities out of thin air, this approach derives such probabilities according to how a latent toxicity threshold is distributed in the population.

mtdi_dist <- mtdi_lognormal(CV = 2          # coefficient of variation
                           ,median = 5      # median DLT threshold
                           ,units = "mg/kg" # real doses have units!

Likewise, the pre-specified doses for our dose-escalation design must be specified as actual doses. With package precautionary, this is accomplished by setting a dose_levels option:

options(dose_levels = c(0.5, 1, 2, 4, 6)) # specify actual dosing

A plot of the \(\mathrm{MTD}_i\) distribution makes clear the connection with toxicity probabilities:


probs <- mtdi_dist@dist$cdf(getOption('dose_levels'))
names(probs) <- paste(getOption('dose_levels'), mtdi_dist@units)
t(probs) %>% kable(digits = 4)
0.5 mg/kg 1 mg/kg 2 mg/kg 4 mg/kg 6 mg/kg
0.0348 0.1023 0.2351 0.4302 0.5571

Extending escalation to support the \(\mathrm{MTD}_i\) concept

Package precautionary provides additional methods for escalation functions, so that an mtdi_distribution may be used instead of out-of-thin-air probabilities:

design %>% simulate_trials(
  num_sims = 100
, true_prob_tox = mtdi_dist # pull tox probs from a MODEL, not thin air
) -> SIMS


Table 2: A simulation summary under the MTD\(_i\) regime of package precautionary.

dose dose (mg/kg) tox n true_prob_tox prob_recommend prob_administer
NoDose 0.0 0.00 0.00 0.0000000 0.04 0.0000000
1 0.5 0.20 3.42 0.0347613 0.07 0.2567568
2 1.0 0.28 3.42 0.1022854 0.33 0.2567568
3 2.0 0.98 3.78 0.2350660 0.46 0.2837838
4 4.0 1.12 2.34 0.4301892 0.10 0.1756757
5 6.0 0.27 0.36 0.5571371 0.00 0.0270270

Judging from Table 2, however, introducing the \(\mathrm{MTD}_i\) concept has by itself generated little progress. The only apparent improvement compared with Table 1 is the addition of a column with actual doses. To make further progress while continuing to operate within the dose-escalation paradigm, we need to introduce another concept.

Introducing graded toxicities

When its full implications are allowed to develop, the latent toxicity threshold \(\mathrm{MTD}_i\) has far-reaching consequences for the design of dose-finding trials. Indeed, it forms the conceptual basis for dose-titration designs that abandon cohortwise dose-escalation altogether (Norris 2017a, 2017b).

For present purposes, however, we take it for granted that (for whatever reason) we have chosen to employ a dose-escalation design. That choice effectively discards the core insight of \(\mathrm{MTD}_i\), and relegates the \(\mathrm{MTD}_i\) concept standing alone to the status of a mere formalism. But in conjunction with a dose scaling that links different toxicity grades at the individual level, the \(\mathrm{MTD}_i\) can be rehabilitated as an effective tool, even within the confines of a dose-escalation design. In package precautionary, such scaling functions are called ‘ordinalizers’, and may be applied at the time when simulations are summarized:

tox_threshold_scaling <- function(MTDi, r0) {
  MTDi * r0 ^ c(Gr1=-2, Gr2=-1, Gr3=0, Gr4=1, Gr5=2)
       ,ordinalizer = tox_threshold_scaling
       ,r0 = 2      # supply a value for ordinalizer's r0 parameter
       )$safety %>% # select the 'safety' component of the summary
Expected counts per toxicity grade
None Gr1 Gr2 Gr3 Gr4 Gr5 Total
6 2 2 1.6 0.8 0.5 13

Clearly, we have now made some genuine progress. Of special interest from a safety perspective are the numbers of Grade 4 (severe) and Grade 5 (fatal) toxicities expected in the trial.

A closer look at the ordinalizer

Let us unroll the ordinalizer above, to make it less cryptic:

tox_threshold_scaling <-
  function(MTDi   # An ordinalizer is a function of a dose threshold,
          ,r0 = 2 # and in general has additional parameters as well.
          ) {
    # An ordinalizer assumes we start with a binary toxicity notion,
    # and maps that to a *graded* notion of toxicity by means of a
    # transformation in 'dose-space'.
    # Assuming the default value r0 = 2 provided in its definition,
    # this ordinalizer says that an individual whose dose threshold
    # for the binary toxicity is MTDi has thresholds ...
    c(Gr1 = MTDi / r0^2 # at MTDi/4 for Gr1,
     ,Gr2 = MTDi / r0   # at MTDi/2 for Gr2,
     ,Gr3 = MTDi        # at MTDi for Gr3 ('tox' is defined as Gr3+),
     ,Gr4 = MTDi * r0   # at 2*MTDi for Gr4,
     ,Gr5 = MTDi * r0^2 # at 4*MTDi for Gr5.

In general, an ordinalizer returns a named vector2 The names allow for user-customized labeling of the toxicity levels, which carries forward into summaries, etc. that links the different dose thresholds at which an individual will experience each grade of toxicity. In (Norris 2020), these concepts are laid out in terms of \(\mathrm{MTD}_i^{g}\) with the index \(g\) running over toxicity grades:

\[ \mathrm{MTD}_i^{g}, g \in \{1,...,5\}, \]

and defined as the dose threshold where a grade-\((g-1)\) toxicity would convert to grade-\(g\).3 To preserve the intuition of the term ‘maximum tolerated dose’, you could say \(\mathrm{MTD}_i^{g}\) is the maximum dose that individual \(i\) can tolerate if ‘tolerability’ is defined as toxicity below grade-\(g\). Thus, in the usual case where the binary ‘dose-limiting toxicity’ (DLT) of a dose-escalation design is defined as CTCAE Grade ≥ 3, we identify ‘\(\mathrm{MTD}_i\)’ with \(\mathrm{MTD}_i^{3}\) .

Taking stock of parameter counts

It might seem that the benefits of package precautionary come at the cost of having to pre-specify many additional parameters. Au contraire! In the example above, the CV and median parameters of mtdi_dist replaced the 5 ‘true toxicity probabilities’ that the package-escalation approach requires you to pluck out of thin air. Thus, immediately we have saved 3 parameters. From this savings, we then spent only 1 on the single parameter r0 of our simple ordinalizer. Indeed, if we had wished to spend all of our ‘parameter savings’, we could have specified an ordinalizer such as:4 Note the judicious allocation of parameters. This ordinalizer devotes 2 parameters (r4 and r5) to the safety-critical threshold ratios \(r_4 := \mathrm{MTD}_i^3 : \mathrm{MTD}_i^4\) and \(r_5 := \mathrm{MTD}_i^3 : \mathrm{MTD}_i^4\), but conserves on parameters by sharing the same parameter \(r_{12}\) for the ratios involving low-grade toxicities.

function(MTDi3, r12, r4, r5) {
  c(Gr1 = MTDi3 / r12^2
   ,Gr2 = MTDi3 / r12
   ,Gr3 = MTDi3
   ,Gr4 = MTDi3 * r4
   ,Gr5 = MTDi3 * r4*r5

As we count design parameters, we also ought not overlook the pre-specified dose levels themselves, which are indeed parameters of our design. But package precautionary changes the manner in which these parameters enter into simulation-based trial design. In the usual approach, every new set of pre-specified dose levels requires its own set of ‘true toxicity probabilities’ to be pulled anew out of thin air. But in package precautionary, the linkage between dose levels and probabilities is provided through a model. In theory, this would allow us to obtain our dose levels as results of simulation-based design, instead of providing them as inputs.

Modeling uncertainty

When you ask people to start thinking, sometimes they keep going. No sooner will you have elicited values for the CV and median parametrizing a lognormal \(\mathrm{MTD}_i\) distribution

\[ \begin{align} \log \mathrm{MTD}_i &\sim \mathscr{N}(\mu, \sigma^2) \\ \mu &\equiv \log(\mathrm{median}) \\ \sigma^2 &\equiv \log (1+\mathrm{CV}^2), \end{align} \]

than you will hear about the uncertainty in these values themselves. You can model this uncertainty by introducing hyperparameters \(\sigma_{\mathrm{CV}}\) and \(\sigma_{\mathrm{med}}\):

\[ \begin{align} \mu &\sim \mathscr{N}(\log \mathrm{median}, \sigma_{\mathrm{med}}^2) \\ \mathrm{CV} &\sim \mathscr{R}(\sigma_{\mathrm{CV}}) \end{align} \]

Here, \(\sigma_{\mathrm{med}}\) expresses in relative terms5 For example, setting \(\sigma_{\mathrm{med}} = 0.5\) would express a ±50% uncertainty in our guessed median \(\mathrm{MTD}_i\). our uncertainty about the median of \(\mathrm{MTD}_i\), while \(\sigma_{\mathrm{CV}}\) is the parameter of the Raleigh distribution:

Happily, the Raleigh distribution’s \(\sigma\) parameter coincides with its mode (likeliest value), and its standard deviation is about \(\frac{2}{3} \sigma\), which seems a reasonable degree of relative uncertainty about CV(\(\mathrm{MTD}_i\)).6 The exact figure is \(\sqrt{2-\pi/2} \approx 0.655\). Given how little is known generally—or even acknowledged!—about inter-individual variation in optimal dosing, it seems reasonable to suppose that you will allow a ± of 66% on any value you choose as most likely for \(\mathrm{CV}(\mathrm{MTD}_i)\). We take advantage of this when we implement these features in package precautionary, avoiding the need to ask users to specify a separate uncertainty parameter for CV:

mtdi_gen <- hyper_mtdi_lognormal(CV = 1
                                ,median_mtd = 5
                                ,median_sdlog = 0.5 # this is new
plot(mtdi_gen, n=100, col=adjustcolor("red", alpha=0.25))

Figure 1: Multiple samples from a hyperprior over the distribution of MTD\(_i\). Consider the implications for the customary practice of pulling ‘true toxicity probabilities’ out of thin air!

Multiple samples from a *hyperprior* over the distribution of MTD$_i$. Consider the implications for the customary practice of pulling 'true toxicity probabilities' out of thin air!
design %>% simulate_trials(
  num_sims = 400
, true_prob_tox = mtdi_gen # pull tox probs from MANY models

# As a convenience, package 'precautionary' lets you set the
# ordinalizer as an *option* so that you don't have to keep
# specifying it as an argument to summary(). By providing a
# default setting for the r0 parameter in the ordinalizer
# definition, we avoid having to keep specifying that, too.
options(ordinalizer = function(MTDi, r0 = 1.5) {
  MTDi * r0 ^ c(Gr1=-2, Gr2=-1, Gr3=0, Gr4=1, Gr5=2)

Expected counts per toxicity grade
None Gr1 Gr2 Gr3 Gr4 Gr5 Total
9 1.7 1.6 1.0 0.7 0.7 14

Uncertainty about the ordinalizer

What about our uncertainty over the parameter \(r_0\)? Here, it seems entirely reasonable simply to explore a range of values:

r0 <- c(1.25, 1.5, 1.75, 2.0)
rbind(summary(HYPERSIMS, r0=r0[1])$safety[1,]
     ,summary(HYPERSIMS, r0=r0[2])$safety[1,]
     ,summary(HYPERSIMS, r0=r0[3])$safety[1,]
     ,summary(HYPERSIMS, r0=r0[4])$safety[1,]
     ) -> safety
cbind(data.table(`$r_0$` = r0), safety) %>% kable(digits=2) %>%
  add_header_above(c(" "=1, "Expected counts by toxicity grade"=6, " "=1))
Expected counts by toxicity grade
\(r_0\) None Gr1 Gr2 Gr3 Gr4 Gr5 Total
1.25 10.14 0.88 0.88 0.65 0.50 1.31 14.36
1.50 8.57 1.75 1.58 1.05 0.71 0.70 14.36
1.75 7.24 2.40 2.27 1.39 0.65 0.42 14.36
2.00 6.06 3.01 2.84 1.62 0.56 0.28 14.36

Beyond 3 + 3

One reason that package escalation makes such a suitable basis for these developments is that it unifies several dose-escalation designs under one object-oriented design. Thus, we can repeat the above simulation exercise with a CRM design:

crm_design <- get_dfcrm(skeleton = scenario, target = 0.25) %>%
  stop_at_n(n = 24)
crm_design %>% simulate_trials(
  num_sims = 200
, true_prob_tox = mtdi_gen

Expected counts per toxicity grade
None Gr1 Gr2 Gr3 Gr4 Gr5 Total