/
README.Rmd
995 lines (719 loc) · 34.9 KB
/
README.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
---
output: github_document
bibliography: vignettes/library.bib
---
<!-- README.md is generated from README.Rmd. Please edit that file -->
# escalation <img src="man/figures/logo.png" align="right" height=140/>
<!-- badges: start -->
[![R-CMD-check](https://github.com/brockk/escalation/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/brockk/escalation/actions/workflows/R-CMD-check.yaml)
[![cran
version](http://www.r-pkg.org/badges/version/escalation)](https://cran.r-project.org/package=escalation)
![](https://cranlogs.r-pkg.org/badges/escalation)
![](https://cranlogs.r-pkg.org/badges/grand-total/escalation)
<!-- badges: end -->
by Kristian Brock.
Documentation is hosted at https://brockk.github.io/escalation/
## Overview
`escalation` provides a grammar for dose-finding clinical trials.
Here is a trivial example.
Everyone knows that a 3+3 design will escalate after it sees no toxicities in a cohort of three:
```{r, message=FALSE}
library(escalation)
get_three_plus_three(num_doses = 5) %>%
fit("1NNN") %>%
recommended_dose()
```
but that it will stick when it sees one-out-of-three toxicities in a cohort:
```{r}
x <- get_three_plus_three(num_doses = 5) %>%
fit("1NNN 2NNN 3NNT")
recommended_dose(x)
continue(x)
```
Having escalated through doses 1 and 2, here the design advocates treating another cohort at dose 3.
```{r}
x <- get_three_plus_three(num_doses = 5) %>%
fit("1NNN 2NNN 3NNT 3NTN")
recommended_dose(x)
continue(x)
```
When it sees two-out-of-six toxicities at dose 3, it concludes that dose 3 is too toxic, dose 2 is the MTD, and the trial should stop.
`escalation` provides functions to use common dose-escalation methodologies like the continual reassessment method (CRM), the Bayesian optimal interval design (BOIN), the TPI suite of designs, efficacy-toxicity designs like EffTox or Wages & Tait, and (as we have seen) the perennial 3+3:
* `get_dfcrm()`
* `get_trialr_crm()`
* `get_trialr_nbg()`
* `get_tpi()`
* `get_mtpi()`
* `get_mtpi2()`
* `get_boin()`
* `get_trialr_efftox()`
* `get_wages_and_tait()`
* `get_three_plus_three()`
These functions create model fitting objects.
Where possible, technical implementations are imported from existing R packages like `dfcrm`, `trialr`, and `BOIN`.
Where no external implementations is available however, methods are implemented natively in `escalation`.
These dose-finding approaches can then be augmented with extra behaviours to specialise the dose selection process.
For example, we can add behaviours to prevent skipping doses, or to stop when we reach a certain sample size.
`escalation` supports the following behaviours:
* `dont_skip_doses()`
* `stop_at_n()`
* `stop_when_n_at_dose()`
* `stop_when_too_toxic()`
* `stop_when_tox_ci_covered()`
* `demand_n_at_dose()`
* `try_rescue_dose()`
* `follow_path()`
* `select_dose_by_cibp()`
Each of these functions overrides the way doses are selected or when a design decides to stop the trial.
The behaviours can be flexibly combined using the `%>%` operator from the `tidyverse` suite.
These models are then fit to trial outcomes to produce dose recommendations.
No matter how the dose selection behaviours were combined, the resulting model fits supports a standard interface.
The two most important methods are `recommended_dose()` to get the current dose recommendation, and `continue()` to learn whether the design advocates continuing patient recruitment.
Having defined this nomenclature for combining dose selection behaviours and providing a standard interface for the resulting analyses, it is simple to run simulations or calculate dose-pathways for future cohorts of patients.
`escalation` provides an object-oriented approach to dose-escalation clinical trials in R.
See [Usage](#usage)
# Usage
## Describing outcomes in dose-finding trials
`escalation` uses a succinct syntax for describing dose-finding outcomes, described in @Brock2019 for the phase I setting and in @Brock2017a for the phase I/II setting.
In a phase I trial, we use the letters:
* `T` to show that toxicity occurred in a patient;
* `N` to show that toxicity did not occur in a patient.
In a joint phase I/II trial, like those supported by EffTox, where we have coincident efficacy and toxicity outcomes, those relevant letters are:
* `T` to show that toxicity without efficacy occurred in a patient;
* `E` to show that efficacy without toxicity occurred in a patient;
* `N` to show that neither occurred;
* `B` to show that both occurred.
These outcome letters are strewn behind integer dose-levels to show the outcomes of patients in cohorts.
To show that a cohort a three patients was given dose 2, that the first two patients were without toxicity, but the third patient experienced toxicity, we would use the outcome string:
```{r}
outcomes <- '2NNT'
```
If that cohort was followed by another cohort of three, all of which were without toxicity, the overall outcome string would be:
```{r}
outcomes <- '2NNT 2NNN'
```
And so on.
These strings are used in the `escalate` package to make it easy to fit models to observed outcomes.
There are many examples below.
## Dose selectors
A core class in the `escalation` package is the `selector`.
It encapsulates the notion that a general dose-escalation design is able to recommend doses, keep track of how many patients have been treated at what doses, what toxicity outcomes have been seen, and whether a trial should continue.
This general interface is true of model-based methods like the CRM and rule-based methods like the 3+3.
Irrespective the particular approach used, the interface is consistent.
In this tutorial, we will demonstrate each of the types of `selector` implemented in the package and how they can be combined to tailor behaviour.
To begin, let us load `escalation`
```{r}
library(escalation)
```
At the core of the dose selection process is an algorithm or a model that selects doses in responses to outcomes.
The classes capable of performing this core role are:
* `get_dfcrm()`, using the model-fitting code from [`dfcrm`](https://cran.r-project.org/package=dfcrm)
* `get_trialr_crm()` using the model-fitting code from [`trialr`](https://cran.r-project.org/package=trialr)
* `get_trialr_nbg()` using the model-fitting code from [`trialr`](https://cran.r-project.org/package=trialr)
* `get_boin()` using the model-fitting code from [`BOIN`](https://cran.r-project.org/package=BOIN)
* `get_tpi()`
* `get_mtpi()`
* `get_mtpi2()`
* `get_trialr_efftox()` using the model-fitting code from [`trialr`](https://cran.r-project.org/package=trialr)
* `get_wages_and_tait()`
* `get_three_plus_three()`
* and `follow_path()`
Where indicated these methods rely on external packages.
Otherwise, methods are implemented natively in `escalation`.
We look at several of these below.
### get_dfcrm
The continual reassessment method [@OQuigley1990] (CRM) is implemented in the [`dfcrm`](https://CRAN.R-project.org/package=dfcrm) package by @dfcrm.
The very least information we need to provide is a dose-toxicity skeleton, and our target toxicity level.
The skeleton represents our prior beliefs on the probabilities of toxicity at each of the doses under investigation.
The model iteratively seeks a dose with toxicity probability close to the target.
For illustration, let us say we have
```{r}
skeleton <- c(0.05, 0.1, 0.25, 0.4, 0.6)
target <- 0.25
```
We create a dose-selection model using:
```{r}
model <- get_dfcrm(skeleton = skeleton, target = target)
```
and we can fit this to outcomes using code like:
```{r}
fit <- model %>% fit('2NNN')
```
The `fit` object will tell you the dose recommended by the CRM model to be administered next.
Depending on your preference for classic R or tidyverse R, you might run:
```{r}
recommended_dose(fit)
```
or
```{r}
fit %>% recommended_dose()
```
Either way, you get the same answer.
The model advocates skipping straight to dose 4.
Clinicians are unlikely to feel comfortable with this.
We can respecify the model to expressly not skip doses in escalation.
We will do that later on.
For now, let us return to our model fit.
We can ask whether the trial should keep going:
```{r}
fit %>% continue()
```
Naturally it wants to continue because `dfcrm` does not implement any stopping rules.
Again, we will add various stopping behaviours in sections below.
The CRM-fitting function in `dfcrm` accepts many arguments to customise the model form and these are passed onwards by `get_dfcrm` function via the `...` parameter.
For example, to use the one-parameter logit model in `dfcrm` (rather than the default empiric model) with the intercept term fixed to take the value 4, we can specify:
```{r}
fit <- get_dfcrm(skeleton = skeleton, target = target,
intcpt = 4, model = 'logistic') %>%
fit('2NNN 3TNN')
fit %>% recommended_dose()
```
`intcpt` and `logistic` are the parameter names chosen by the authors of `dfcrm`.
### get_trialr_crm
We could instead fit the CRM models above using the [`trialr`](https://cran.r-project.org/package=trialr) package by [@Brock2019; @trialr].
Reusing the `skeleton` and `target` variables defined above, we fit the same empiric model
```{r, message=FALSE}
model <- get_trialr_crm(skeleton = skeleton, target = target, model = 'empiric',
beta_sd = sqrt(1.34))
fit <- model %>% fit('2NNN')
```
The `dfcrm` package, unless told otherwise, assumes that you want an empiric model where the prior variance for $\beta$ is 1.34.
In the `trialr` package, no such assumptions are made so we had to specify those variables.
All we have changed is the method of inference.
`dfcrm` uses numerical integration to calculate posterior statistics and plugs those into the dose-toxicity function.
In contrast, `trialr` fits the model using Hamiltonian MCMC sampling via `Stan`.
Thankfully, the two models agree on the desired next dose:
```{r}
fit %>% recommended_dose()
```
and that the trial should continue:
```{r}
fit %>% continue()
```
The added bonus we get from the `trialr` fit, however, is those samples from the posterior distribution:
```{r}
fit %>% prob_tox_samples() %>% head(10)
```
That facilitates really flexible inference.
For example, what is the probability that toxicity at dose 3 is at least 5% greater than that at dose 2?
Simple to answer using the posterior samples:
```{r, message=FALSE}
library(dplyr)
fit %>% prob_tox_samples() %>%
summarise(prob = mean(`3` > `2` + 0.05))
```
'More likely than not', is the answer.
See the Continual Reassessment Method vignette for more details.
### get_trialr_nbg
The two-parameter logistic dose-escalation method of @Neuenschwander2008 (NBG) is implemented in the [`trialr`](https://CRAN.R-project.org/package=trialr) package by @trialr.
The very least information we need to provide is a vector of the doses under investigation, a reference dose-level $d^*$, our target toxicity level, and priors on the logit model intercept, $\alpha$, and dose gradient, $\beta$.
For illustration, let us reproduce the notorious example in Figure 1 of @Neuenschwander2008 with 15 doses:
```{r}
dose <- c(1, 2.5, 5, 10, 15, 20, 25, 30, 40, 50, 75, 100, 150, 200, 250)
outcomes <- '1NNN 2NNNN 3NNNN 4NNNN 7TT'
fit <- get_trialr_nbg(real_doses = dose, d_star = 250, target = 0.3,
alpha_mean = 2.15, alpha_sd = 0.84,
beta_mean = 0.52, beta_sd = 0.8, seed = 2020) %>%
fit(outcomes)
```
Sticking at dose 7 is the recommendation:
```{r}
fit %>% recommended_dose()
```
However, we see that it is a close call as to which dose is closest to the target toxicity level:
```{r}
fit %>% mean_prob_tox()
```
This is perhaps unsurprising in a situation with so many doses.
See the Neuenschwander, Branson & Gsponer vignette for more details.
### get_tpi
The Toxicity Probability Interval (TPI) method was introduced by @Ji2007.
The model requires a few parameters:
```{r}
model <- get_tpi(num_doses = 5, target = 0.25, k1 = 1, k2 = 1.5,
exclusion_certainty = 0.95)
```
The model can be fit to outcomes in the usual way:
```{r}
fit <- model %>% fit('1NNT')
```
and the returned model fit obeys the same interface as the other classes described here.
For instance, the dose recommended for the next cohort is:
```{r}
fit %>% recommended_dose()
```
See the Toxicity Probability Interval Design vignette for more information.
### get_mtpi
The Modified Toxicity Probability Interval (mTPI) method was introduced by @Ji2010.
It is generally simpler to implement than TPI because the $\epsilon1$ and $\epsilon2$ parameters have the intuitive interpretation of forming the bounds of the interval that we regard as containing doses equivalent to the target dose.
For instance, if we target a dose with toxicity probability equal to 25%, but would judge doses in the region (20%, 30%) to be satisfactorily toxic, we run:
```{r}
model <- get_mtpi(num_doses = 5, target = 0.25,
epsilon1 = 0.05, epsilon2 = 0.05, exclusion_certainty = 0.95)
```
In this parameterisation, we exclude doses if we are 95% a-posteriori sure that the associated toxicity rate exceeds the target.
We fit the model to outcomes:
```{r}
fit <- model %>% fit('1NNT')
```
and learn that the recommended next dose is
```{r}
fit %>% recommended_dose()
```
dose 1, in accordance with Figure 2 of @Ji2010.
See the Modified Toxicity Probability Interval Design vignette for more information.
### get_mtpi2
mTPI was further updated by @guo2017bayesian to produce mTPI2.
Its parameterisation is similar to mTPI.
```{r}
model <- get_mtpi2(num_doses = 5, target = 0.25,
epsilon1 = 0.05, epsilon2 = 0.05,
exclusion_certainty = 0.95)
fit <- model %>% fit('1NNT')
fit %>% recommended_dose()
```
Once again, see the Modified Toxicity Probability Interval Design vignette for more information.
### get_boin
`escalate` also implements the Bayesian Optimal Interval (BOIN) dose-finding design by @liu_bayesian_2015 via the [`BOIN`](https://CRAN.R-project.org/package=BOIN) package [@BOIN].
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:
```{r}
target <- 0.25
model <- get_boin(num_doses = 5, target = target)
```
As before, we can fit the model to some observed outcomes:
```{r}
fit <- model %>% fit('2NNN')
```
and ask the recommended dose:
```{r}
fit %>% recommended_dose()
```
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:
```{r}
fit <- model %>% fit('2NTN 1TTT')
fit %>% continue()
```
Notice in this scenario that the recommended dose is `NA`:
```{r}
fit %>% recommended_dose()
```
This clarifies that no dose should be recommended for further study.
In this setting, this is because all doses are considered too toxic.
This is distinct from scenarios where a design advocates stopping a trial and recommending a dose for further study.
We will encounter situations like that below.
Since `escalation` provides many flexible options for stopping, we have made it possible to suppress BOIN's native stopping rule via `use_stopping_rule = FALSE`.
In this instance, the user may want to add their own stopping rule, e.g. using `stop_when_too_toxic`.
Extra parameters are passed to the `get.boundary` function in the `BOIN` package to customise the escalation procedure.
For instance, the boundaries that guide changes in dose are set to be 60% and 140% of the target toxicity rate, by default.
To instead use 30% and 170%, we could run:
```{r}
get_boin(num_doses = 5, target = target,
p.saf = 0.3 * target, p.tox = 1.7 * target) %>%
fit('1NNN 2NNT') %>%
recommended_dose()
```
To observe the effect of the change, note that the default values suppress escalation in this scenario:
```{r}
get_boin(num_doses = 5, target = target) %>%
fit('1NNN 2NNT') %>%
recommended_dose()
```
The parameter names `p.saf` and `p.tox` were chosen by the authors of the `BOIN` package.
See the Bayesian Optimal Interval Design vignette for more information.
### get_boin12
`escalation` also supports designs that choose doses according to co-primary efficacy and toxicity outcomes, such as the BOIN12 design [@lin2020boin12].
We provide target toxicity and efficacy thresholds via `phi_t` and `phi_e`, the utility of 'no efficacy and no toxicity' via `u2`, and the utility of 'efficacy with toxicity' via `u3` on a (0, 100) scale:
```{r}
# Examples in Lin et al.
model <- get_boin12(num_doses = 5,
phi_t = 0.35, phi_e = 0.25,
u2 = 40, u3 = 60)
```
In contrast to the examples above, outcomes now include efficacy as well as toxicity. `E` reflects efficacy only, and `B` reflects both efficacy and toxicity.
The model-fitting process is largely the same though:
```{r}
fit <- model %>% fit('1NNN 2ENT 3ETT 2EEN')
```
We have the usual interface:
```{r}
fit %>% recommended_dose()
```
```{r}
fit %>% continue()
```
`escalation` also supports other so-called phase I/II designs that select doses by efficacy and toxicity like @wages2015seamless and @thall2004dose.
See the help pages for more information.
### get_three_plus_three
The 3+3 method is an old method for dose-escalation that uses fixed cohorts of three and pre-specified rules to govern dose-selection [@korn1994; @LeTourneau2009].
To create a 3+3 design, we need no more information than the number of doses under investigation:
```{r}
model <- get_three_plus_three(num_doses = 5)
```
As usual, we can fit the model to some outcomes and learn the recommended dose:
```{r}
model %>% fit('2NTN') %>% recommended_dose()
```
@korn1994 described a variant of 3+3 that permits deescalation to ensure that six patients are treated at a dose before it is recommended.
To use that option in our model, we could have run:
```{r}
model <- get_three_plus_three(num_doses = 5, allow_deescalate = TRUE)
```
The model would then advocate deescalation if at least two toxicities are seen at a dose and the dose below has fewer than 6 treated patients:
```{r}
model %>% fit('2NTT') %>% recommended_dose()
```
### follow_path
The final dose selector in this section is not really a model at all, so much as a pre-specified path to follow.
Let us say that we would like to escalate through the doses in the absence of toxicity, treating two patients at each of the first two doses, and three at the other doses.
We can specify such a path in `escalation` using:
```{r}
model <- follow_path('1NN 2NN 3NNN 4NNN 5NNN')
```
When fit to data, the method just returns whatever comes next in the sequence:
```{r}
model %>% fit('1NN 2N') %>% recommended_dose()
```
```{r}
model %>% fit('1NN 2NN') %>% recommended_dose()
```
When the outcomes diverge from the pre-specified path, however, this selector does not know what to do:
```{r}
model %>% fit('1NN 2NT') %>% recommended_dose()
```
That rather seems to limit its value.
The point of this class is that we sometimes want to specify what is occasionally referred to as _an initial escalation plan_.
When trial outcomes diverge from the initial plan, another method takes over.
This is a perfect opportunity to show how different selectors can be joined together.
Let us say that we wish to follow the initial plan described above, but when the first toxicity event is seen, we want a CRM model to take over.
We simply join the functions together using the pipe operator from `magrittr`:
```{r}
model <- follow_path('1NN 2NN 3NNN 4NNN 5NNN') %>%
get_dfcrm(skeleton = skeleton, target = target)
```
Now, when trial outcomes diverge from the path, the CRM model analyses all of the outcomes and recommends the next dose:
```{r}
model %>% fit('1NN 2NT') %>% recommended_dose()
```
This concludes our look at the core dose-selecting classes.
We now turn our attention to the ways in which these methods can be adapted using extra behaviours.
### dont_skip_doses
We saw in the CRM example above that the design undesirably wanted to skip straight to a high dose, without trying some of the lower doses.
A simple and very common constraint to impose in dose-finding trials is to avoid skipping untested doses.
Resuming our CRM example, we suppress the skipping of untested doses in escalation with:
```{r}
model <- get_dfcrm(skeleton = skeleton, target = target) %>%
dont_skip_doses(when_escalating = TRUE)
```
We then fit the model as before:
```{r}
fit <- model %>% fit('2NNN')
fit %>% recommended_dose()
```
This time, however, the model advocates dose 3.
Previously, it wanted to go straight to dose 4.
We prevented skipping dose in escalation.
We could have prevented skipping doses in deescalation with:
```{r}
model <- get_dfcrm(skeleton = skeleton, target = target) %>%
dont_skip_doses(when_deescalating = TRUE)
```
or both with:
```{r}
model <- get_dfcrm(skeleton = skeleton, target = target) %>%
dont_skip_doses(when_escalating = TRUE, when_deescalating = TRUE)
```
### stop_at_n
Let us now investigate some methods that facilitate stopping.
The simplest condition on which to stop is when the total sample size reaches some pre-specified level.
For instance, we might want to treat a maximum of 15 patients and then stop.
To do this, we call the `stop_at_n` function and append it onto the end of a core dose selector, like this:
```{r}
model <- get_dfcrm(skeleton = skeleton, target = target) %>%
stop_at_n(n = 15)
```
When this design has seen fewer than 15 patients, it will select doses and advocate that the trial continues.
For instance:
```{r}
fit <- model %>% fit('1NNN 2TNN 2NNN 3NNN')
fit %>% continue()
```
The design advocates continuing at dose:
```{r}
fit %>% recommended_dose()
```
In contrast, once 15 patients are seen,
```{r}
fit <- model %>% fit('1NNN 2TNN 2NNN 3NNN 3NTN')
fit %>% continue()
```
the design advocates stopping.
It is important to note that, even though the design has stopped, it still recommends that a dose be studied at the next trial phase:
```{r}
fit %>% recommended_dose()
```
This is in contrast to the scenario where a trial is stopped because all doses are inappropriate.
In this scenario, the dose recommendation would be `NA`.
We will encounter this in examples below.
### stop_when_n_at_dose
Another common approach is to stop a dose-finding experiment when a given number of patients have been treated at a particular dose.
Continuing with our CRM model, to stop when nine patients have been treated at the dose that is about to be recommended again, we use:
```{r}
model <- get_dfcrm(skeleton = skeleton, target = target) %>%
stop_when_n_at_dose(n = 9, dose = 'recommended')
```
We can observe how this alters the dose-selection model.
Here we see six patients treated at dose 2:
```{r}
fit <- model %>% fit('1NNN 2TNN 2NTN')
```
The model recommends that dose 2 should be given to more patients:
```{r}
fit %>% continue()
fit %>% recommended_dose()
```
If the next cohort results in dose 2 being recommended yet again, i.e. to bring the total number of patients at dose 2 to nine or more, the model stops:
```{r}
fit <- model %>% fit('1NNN 2TNN 2NTN 2NNN')
fit %>% continue()
fit %>% recommended_dose()
```
In this scenario, dose 2 is the final recommended dose and the trial stops gracefully at a pre-specified stopping rule.
This behaviour can also be configured to stop when any dose has been given _n_ times:
```{r}
model <- get_dfcrm(skeleton = skeleton, target = target) %>%
stop_when_n_at_dose(n = 9, dose = 'any')
```
or when a particular dose-level has been given _n_ times:
```{r}
model <- get_dfcrm(skeleton = skeleton, target = target) %>%
stop_when_n_at_dose(n = 9, dose = 3)
```
Naturally, you can combine this behaviour with other behaviours.
The following model stops the trial when nine patients have been evaluated at the recommended dose or when 21 patients have been treated in total, whichever occurs first:
```{r}
model <- get_dfcrm(skeleton = skeleton, target = target) %>%
stop_when_n_at_dose(n = 9, dose = 'recommended') %>%
stop_at_n(n = 21)
```
### stop_when_ci_covered
The two stopping mechanisms above scrutinise the number of patients treated.
In many situations, this will be valuable.
However, in other situations, we might want to stop when a threshold amount of statistical information is obtained.
One way to achieve this is to stop when the confidence interval or credible interval for the probability of toxicity at a dose is covered by a specified range.
For instance, we know that the BOIN design seeks a target toxicity level, and we have used a target of 25% in our examples.
We might say that we are sure enough about the recommended dose when the associated 90% credible interval (because BOIN is a Bayesian design) of the toxicity probability falls in the region 10% - 40%.
```{r}
model <- get_boin(target = target, num_doses = 5) %>%
stop_when_tox_ci_covered(dose = 'recommended', lower = 0.10, upper = 0.4)
```
Say that we observe the following trial path:
```{r}
fit <- model %>%
fit('1NNN 2NTN 2TNN 2NNN 2NNT 2NTN 2NNN 2TNN')
```
The design recommends dose 2 and it also advocates stopping:
```{r}
fit %>% recommended_dose()
fit %>% continue()
```
This is because the lower bound of the 90% interval for the probability of toxicity at dose 2 is at least 10%:
```{r}
fit %>% prob_tox_quantile(p = 0.05)
```
and the upper bound is no more than 40%:
```{r}
fit %>% prob_tox_quantile(p = 0.95)
```
It may be intersting to note that our CRM model would not stop in this scenario:
```{r}
model <- get_dfcrm(skeleton = skeleton, target = target) %>%
stop_when_tox_ci_covered(dose = 'recommended', lower = 0.10, upper = 0.4)
fit <- model %>%
fit('1NNN 2NTN 2TNN 2NNN 2NNT 2NTN 2NNN 2TNN')
fit %>% continue()
fit %>% recommended_dose()
```
This is because the lower bound of the 90% CI falls slightly outside the sought range:
```{r}
fit %>% prob_tox_quantile(p = 0.05)
```
As before, we can specify `dose = 'recommended'`, `dose = 'any'`, or a particular numerical dose-level with `dose = 3`, for example.
It should be appreciated that this approach only works when the underlying model extends a way of calculating quantiles and uncertainty intervals.
The 3+3 lacks a statistical foundation and does not offer quantiles:
```{r}
get_three_plus_three(num_doses = 5) %>%
fit('1NNN 2NTN') %>%
prob_tox_quantile(p = 0.05)
```
### stop_when_too_toxic
The stopping rules considered so far stop a trial and recommend a dose once some critical threshold of information is obtained.
We will naturaly want to stop if all doses are too toxic.
We saw above that some model-based dose-finding approaches can calculate quantiles.
We can take this idea further and advocate stopping when there is sufficient evidence that the toxicity probability at some dose exceeds a critical threshold.
In such circumstances, no dose will be recommended because all doses of the treatment will be deemed to be excessively toxic.
Let us set up a rule to stop and recommend no dose if the probability of toxicity at the lowest dose is too high:
```{r}
model <- get_dfcrm(skeleton = skeleton, target = target) %>%
stop_when_too_toxic(dose = 1, tox_threshold = 0.35, confidence = 0.7)
```
The above examples stops when 70% of the probability mass or posterior distribution of the probability of toxicity at dose 1 exceeds 35%.
With an isolated toxicity incidence at dose 1, the model advocates continuing at dose 1:
```{r}
fit <- model %>% fit('1NTN')
fit %>% continue()
fit %>% recommended_dose()
```
This is because the probability that the toxicity rate exceeds 35% is less than 70%:
```{r}
fit %>% prob_tox_exceeds(0.35) %>% round(2)
```
However, with material additional toxicity at dose 1, the design now advocates stopping:
```{r}
fit <- model %>% fit('1NTN 1TTT')
fit %>% continue()
```
Furthermore, no dose is recommended:
```{r}
fit %>% recommended_dose()
```
This is because we are now at least 70% sure that the lowest dose is too toxic:
```{r}
fit %>% prob_tox_exceeds(0.35) %>% round(2)
```
Once again, we can specify `dose = 'recommended'`, `dose = 'any'`, or a particular numerical dose-level with `dose = 3`, for example.
We also require that the underlying model supports the calculation of quantiles.
BOIN supports this fucntionality:
```{r}
model <- get_boin(target = target, num_doses = 5) %>%
stop_when_too_toxic(dose = 1, tox_threshold = 0.35, confidence = 0.7)
fit <- model %>% fit('1NTN 1TTT')
fit %>% continue()
fit %>% recommended_dose()
fit %>% prob_tox_exceeds(0.35) %>% round(2)
```
but a non-statistical method like 3+3 does not.
### demand_n_at_dose
We have looked at many behaviours that provide stopping.
We can also look at some behaviours that delay stopping.
We might want to guarantee that we treat at least _n_ patients at a dose before we permit a dose-finding trial to stop.
For instance, we might not feel comfortable recommending a dose for the next phase of study if it has only been evaluated in a small number of patients.
It makes sense for this behaviour to be used with a design that would otherwise stop.
Let us say that we would normally like to stop after 18 patients have been treated.
However, we will also demand that at least 6 patients be treated at the recommended dose before stopping is allowed, irrespective the overall sample size.
We specify:
```{r}
model <- get_boin(target = target, num_doses = 5) %>%
stop_at_n(n = 18) %>%
demand_n_at_dose(n = 6, dose = 'recommended')
```
In the following situation:
```{r}
fit <- model %>% fit('1NNN 2NNT 3NTN 3NNN 4TTN 3NTT')
fit %>% continue()
fit %>% recommended_dose()
```
the design advocates continuing at dose 2 even though 18 patients have been evaluated.
This is because the `demand_n_at_dose` function is overriding the stopping behaviour of `stop_at_n`.
It is requesting that the trial continue at dose 2 instead of stopping with only three patients treated at the nominal recommended dose.
It is important to recognise that the order of the functions matters.
If we flip the order of the constraints in the example above, the outcome is different:
```{r}
model <- get_boin(target = target, num_doses = 5) %>%
demand_n_at_dose(n = 6, dose = 'recommended') %>%
stop_at_n(n = 18)
fit <- model %>% fit('1NNN 2NNT 3NTN 3NNN 4TTN 3NTT')
fit %>% continue()
fit %>% recommended_dose()
```
Now the `stop_at_n` constraint overrides the action of `demand_n_at_dose` to halt the trial when _n=18_, even though only three patients have been evaluated at dose 2.
It overrides because it comes later in the decision chain.
Users should be aware that commands that come later take precedence.
Once again, we can specify `dose = 'recommended'`, `dose = 'any'`, or a particular numerical dose-level with `dose = 3`, for example.
In summary, the `demand_n_at_dose` function delays stopping in a scenario when a dose is being selected.
### try_rescue_dose
In contrast to `demand_n_at_dose`, the `try_rescue_dose` function delays stopping in a scenario where no dose is going to be selected.
It overrides a decision to stop and recommend no dose when fewer than _n_ patients have been evaluated at a given dose.
Thus, it provides a facility to ensure that some "rescue" dose has been tried before stopping is allowed.
This is another function where effective demonstration requires a design that would normally stop.
Let us say that we will stop if we are 80% sure that the toxicity rate at the lowest dose exceeds 35%.
But before we stop, we want to ensure that at least two patients have been evaluated at the lowest dose.
We write:
```{r}
model <- get_dfcrm(skeleton = skeleton, target = target) %>%
stop_when_too_toxic(dose = 1, tox_threshold = 0.35, confidence = 0.8) %>%
try_rescue_dose(dose = 1, n = 2)
```
Then, even when this design sees some major toxicity at dose 2:
```{r}
fit <- model %>% fit('2TTT')
fit %>% continue()
fit %>% recommended_dose()
```
the design will not advocate stopping, even though the posterior confidence that the tox rate at dose 1 exceeds 35% is greater than 80%:
```{r}
fit %>% prob_tox_exceeds(0.35)
```
Once two patients are seen at dose 1, stopping can be countenanced.
If those two patients tolerate treatment at dose 1:
```{r}
fit <- model %>% fit('2TTT 1NN')
fit %>% continue()
fit %>% recommended_dose()
```
then stopping is not advocated because the posterior belief is now that dose 1 is not excessively toxic:
```{r}
fit %>% prob_tox_exceeds(0.35)
```
However, if even one of those patients at dose 1 experiences toxicity:
```{r}
fit <- model %>% fit('2TTT 1NT')
fit %>% continue()
fit %>% recommended_dose()
```
Then the trial stops and no dose is recommended.
The `try_rescue_dose` function allows researchers to rescue situations where otherwise sensible stopping criteria may prove too sensitive to chance events in very small sample sizes.
### select_dose_by_cibp
This function implements the convex infinite bounds penalisation (CIBP) criterion of @mozgunov_improving_2020 that adjusts the way doses are selected in CRM trials.
Their method is mindful of the uncertainty in the estimates of the probability of toxicity and uses an asymmetry parameter, 0 < _a_ < 2, to penalise escalation to risky doses.
The method alters the way doses are selected but not when the trial should stop.
For _a < 1_, the criterion penalises toxic doses more heavily, making escalation decisions more conservative.
To add the behaviour to a dose-finding design, we run:
```{r}
model <- get_dfcrm(skeleton = skeleton, target = target) %>%
select_dose_by_cibp(a = 0.3)
```
The model is then fit to outcomes in the usual way:
```{r}
model %>%
fit('1NTN') %>%
recommended_dose()
```
## Simulation and dose-paths
We have described at length above the flexible methods that `escalation` provides to specify dose-escalation designs and tailor trial behaviour.
Once designs are specified, we can investigate their operating characteristics by simulation using the `simulate_trials` function, and efficiently compare designs using @sweeting2023's method in `simulate_compare`.
We can also exhaustively calculate dose recommendations for future cohorts using the `get_dose_paths` function.
Both of these topics are the topics of full vignettes so please check them out.
# Installation
```{r, eval=FALSE}
# Install the latest official version from CRAN with:
install.packages("escalation")
# Alternatively, install the latest code from GitHub:
devtools::install_github("brockk/escalation")
```
# Future Plans
I plan to add model-fitting functions for EWOC via [ewoc](https://cran.r-project.org/package=ewoc), further methods for phase I/II designs, and perhaps also methods for dual agents.
I want to investigate adding some further stopping functions like those researched by @zohar_continual_2001.
Finally, I will investigate adding time-to-event versions of the designs presented here, the so-called TITE designs.
These will require a different approach to simulation because cohorts no longer apply.
## Getting help
This package is still in active development.
There are thousands of unit tests run each time the package code is updated.
However, that certainly does not mean that the code is bug free.
You should always be on the defensive.
This software is offered with no guarantee at all.
If you have found a bug, please drop me a line and also log it here:
https://github.com/brockk/escalation/issues
If you want help using the package, feel free to contact me by email.
## References