old <- options(rmarkdown.html_vignette.check_title = FALSE) # suppress un-needed warning knitr::opts_chunk$set(echo = TRUE) knitr::opts_chunk$set(fig.height = 4, fig.width = 6) library(precautionary) library(knitr) library(kableExtra) options(ordinalizer = NULL) # vignette presumes this is not already set # Echo source focused on package functionality by redacting calls to kable() # see https://bookdown.org/yihui/rmarkdown-cookbook/hook-hide.html: local({ hook_source <- knitr::knit_hooks$get('source') knitr::knit_hooks$set(source = function(x, options) { x <- gsub(" -> etc.", "", x) x <- x[!grepl("^etc. %>%", x)] # strip lines starting "etc. %>% ..." x <- x[!grepl("\\(etc.,", x)] # strip lines with fun(etc., ...) hook_source(x, options) }) })

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

library(escalation) # Design a 3 + 3 trial, with 5 prespecified 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) -> etc. # summarize simulation results etc. %>% kable(caption="Simulation summary as produced by package `escalation`.")

From Table \@ref(tab:basic-sim), 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 `r sum(round(summary(sims)$n, digits=2))`

, which perhaps informs us about the expected cost or duration of our trial.^[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.

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.

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

Likewise, the prespecified 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 $\MTDi$ distribution makes clear the connection with toxicity probabilities:

```
plot(mtdi_dist)
```

probs <- mtdi_dist@dist$cdf(getOption('dose_levels')) names(probs) <- paste(getOption('dose_levels'), mtdi_dist@units) t(probs) %>% kable(digits = 4)

`escalation`

to support the $\MTDi$ conceptPackage `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 summary(SIMS) -> etc. etc. %>% kable(caption = "A simulation summary under the MTD$_i$ regime of package `precautionary`.")

Judging from Table \@ref(tab:MTDi-regime), however, introducing the $\MTDi$ concept has by itself generated little progress. The only apparent improvement compared with Table \@ref(tab:basic-sim) 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.

When its full implications are allowed to develop, the latent toxicity threshold $\MTDi$ 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_dose_2017; @norris_precautionary_2017].

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 $\MTDi$, and relegates the $\MTDi$ 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 $\MTDi$ 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) } summary(SIMS ,ordinalizer = tox_threshold_scaling ,r0 = 2 # supply a value for ordinalizer's r0 parameter )$safety %>% # select the 'safety' component of the summary safety_kable()

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.

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 vector^[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_retrospective_2020], these concepts are laid out in terms of $\MTDig$ with the index $g$ running over toxicity grades:

$$ \MTDig, g \in {1,...,5}, $$

and defined as the dose threshold where a grade-$(g-1)$ toxicity would convert to grade-$g$.^[To preserve the intuition of the term 'maximum tolerated dose', you could say $\MTDig$ 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 '$\MTDi$' with $\MTDig[3]$ .

It might seem that the benefits of package `precautionary`

come at the cost of having to prespecify 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:^[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 prespecified 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 prespecified 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*.

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 $\MTDi$ 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 terms*^[For example, setting $\sigma_{\mathrm{med}} = 0.5$ would express a ±50% uncertainty in our guessed median $\MTDi$.] our uncertainty about the median of $\MTDi$, while $\sigma_{\mathrm{CV}}$ is the parameter of the Raleigh distribution:

# Plot the Raleigh density for a suitable range of sigma_CV values sigmas <- c(0.1, 0.25, 0.5, 1) curves <- as.data.table(expand.grid(x = seq(0, 2, 0.01) ,mode = sigmas ) ) curves[, pdf := 2*(x/mode^2)*dchisq((x/mode)^2, df=2), by = mode] plot(pdf ~ x, data = curves[mode == sigmas[1]], type = 'l' , main = "Probability density function of the Rayleigh distribution") for(sigma in sigmas[-1]) lines(pdf ~ x, data = curves[mode == sigma]) text(x=0.3, y=5, expression(sigma == 0.1)) text(x=0.5, y=2.25, expression(sigma == 0.25)) text(x=0.75, y=1.4, expression(sigma == 0.5)) text(x=1.5, y=0.9, expression(sigma == 1))

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($\MTDi$).^[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 ,units="mg/kg" ) plot(mtdi_gen, n=100, col=adjustcolor("red", alpha=0.25))

design %>% simulate_trials( num_sims = 400 , true_prob_tox = mtdi_gen # pull tox probs from MANY models ) -> HYPERSIMS # 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) }) summary(HYPERSIMS)$safety -> etc. etc. %>% safety_kable()

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))

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 ) -> CRM_HYPERSIMS summary(CRM_HYPERSIMS)$safety -> etc. etc. %>% safety_kable()

The BOIN design of [@liu_bayesian_2015] is also supported:

boin_design <- get_boin(num_doses = 5, target = 0.25) %>% stop_at_n(n = 24) boin_design %>% simulate_trials( num_sims = 200 , true_prob_tox = mtdi_gen ) -> BOIN_HYPERSIMS summary(BOIN_HYPERSIMS)$safety -> etc. etc. %>% safety_kable()

`precautionary`

Package `precautionary`

demonstrates in principle that any dose-escalation design can be assessed for safety, given a modicum of realistic pharmacologic thinking during prior elicitation. How truly modest this additional input is, should be apparent from the obvious connections between the ratios in an 'ordinalizer' and the notion of therapeutic index (TI). It is hard to imagine that any trialist would pursue a phase 1 investigation without some notion of a plausible range for TI [@muller_determination_2012]. Yet within the biostatistical literature on dose-finding methods, I know of no attempt to apply a TI concept formally to assess design safety.

options(old) # restore user's original options before finishing, per CRAN

**Any scripts or data that you put into this service are public.**

Embedding an R snippet on your website

Add the following code to your website.

For more information on customizing the embed code, read Embedding Snippets.