| Title: | Dose Transition Pathways for Time-to-Event Dose-Finding Designs |
| Version: | 0.1.0 |
| Description: | Implements the Dose Transition Pathways (DTP) look-ahead strategy for time-to-event phase I dose-finding clinical trials with delayed or pending toxicity outcomes. Provides a DTP decorator for the 'escalation' framework, supporting both TITE-CRM (model-based) and TITE-BOIN (model-assisted) base designs. |
| License: | GPL (≥ 3) |
| URL: | https://github.com/ICR-EPAT/dtptite |
| BugReports: | https://github.com/ICR-EPAT/dtptite/issues |
| Depends: | R (≥ 4.1.0) |
| Imports: | BOIN, dplyr, escalation (≥ 0.2.0), stats, tibble |
| Suggests: | dfcrm, ggplot2, testthat (≥ 3.0.0), TITEgBOIN |
| Config/testthat/edition: | 3 |
| Encoding: | UTF-8 |
| RoxygenNote: | 7.3.3 |
| NeedsCompilation: | no |
| Packaged: | 2026-10-09 14:32:32 UTC; xlai |
| Author: | Xiaoran Lai [aut, cre], The Institute of Cancer Research [cph], National Institute for Health and Care Research [fnd] |
| Maintainer: | Xiaoran Lai <xiaoran.lai@icr.ac.uk> |
| Repository: | CRAN |
| Date/Publication: | 2026-10-21 13:10:41 UTC |
dtptite: Dose Transition Pathways for Time-to-Event Dose-Finding Designs
Description
Implements the Dose Transition Pathways (DTP) look-ahead strategy for time-to-event phase I dose-finding clinical trials with delayed or pending toxicity outcomes. Provides a DTP decorator for the 'escalation' framework, supporting both TITE-CRM (model-based) and TITE-BOIN (model-assisted) base designs.
Funding
Xiaoran Lai, NIHR Development and Skills Enhancement Award (DSE), NIHR500663, is funded by the NIHR for this research project. The views expressed in this publication are those of the author(s) and not necessarily those of the NIHR, NHS or the UK Department of Health and Social Care.
Author(s)
Maintainer: Xiaoran Lai xiaoran.lai@icr.ac.uk
Other contributors:
The Institute of Cancer Research [copyright holder]
National Institute for Health and Care Research [funder]
See Also
Useful links:
Apply the DTP look-ahead to a TITE selector factory.
Description
Wraps a parent TITE selector factory with the Dose Transition Pathways
projection engine. At fit time, the decorator checks whether waiting
for pending patients' follow-up (up to t_max) could change the dose
recommendation upward.
Usage
apply_dtp(
parent_selector_factory,
t_max,
obswin,
projection_search = c("binary", "linear"),
verbose = TRUE
)
Arguments
parent_selector_factory |
A |
t_max |
Maximum allowable wait time (numeric >= 0, or |
obswin |
DLT observation window length (e.g. 56 days or 8 weeks).
Defines the time-weight mapping: |
projection_search |
Strategy used to locate the minimum wait time.
|
verbose |
If |
Value
A dtp_selector_factory object.
Examples
library(escalation)
design <- get_dfcrm_tite(skeleton = c(0.124, 0.25, 0.398, 0.542, 0.666),
target = 0.25) |>
apply_dtp(t_max = 35, obswin = 56)
# Four patients at dose 2: one DLT, and the last patient is only 30% of the
# way through the observation window.
outcomes <- data.frame(dose = c(2, 2, 2, 2), tox = c(0, 1, 0, 0),
weight = c(1, 1, 1, 0.3), cohort = 1:4)
x <- fit(design, outcomes)
recommended_dose(x) # de-escalate now...
dtp_should_wait(x) # ...but waiting could change that
dtp_wait_time(x) # days to wait
dtp_wait_time(x, type = "weight") # the same wait as a weight increment
dtp_projected_dose(x) # recommendation after the wait
Projected dose after waiting.
Description
Projected dose after waiting.
Usage
dtp_projected_dose(x, ...)
Arguments
x |
A |
... |
Additional arguments (unused). |
Value
Integer dose level, or NA if waiting has no benefit.
Examples
library(escalation)
design <- get_dfcrm_tite(skeleton = c(0.124, 0.25, 0.398, 0.542, 0.666),
target = 0.25) |>
apply_dtp(t_max = 35, obswin = 56)
# Four patients at dose 2: one DLT, and the last patient is only 30% of the
# way through the observation window.
outcomes <- data.frame(dose = c(2, 2, 2, 2), tox = c(0, 1, 0, 0),
weight = c(1, 1, 1, 0.3), cohort = 1:4)
x <- fit(design, outcomes)
recommended_dose(x) # de-escalate now...
dtp_should_wait(x) # ...but waiting could change that
dtp_wait_time(x) # days to wait
dtp_wait_time(x, type = "weight") # the same wait as a weight increment
dtp_projected_dose(x) # recommendation after the wait
Should the trialist wait before dosing the next cohort?
Description
Returns TRUE if the DTP projection found that waiting could lead to
a higher dose recommendation within t_max.
Usage
dtp_should_wait(x, ...)
Arguments
x |
A |
... |
Additional arguments (unused). |
Value
Logical scalar.
Examples
library(escalation)
design <- get_dfcrm_tite(skeleton = c(0.124, 0.25, 0.398, 0.542, 0.666),
target = 0.25) |>
apply_dtp(t_max = 35, obswin = 56)
# Four patients at dose 2: one DLT, and the last patient is only 30% of the
# way through the observation window.
outcomes <- data.frame(dose = c(2, 2, 2, 2), tox = c(0, 1, 0, 0),
weight = c(1, 1, 1, 0.3), cohort = 1:4)
x <- fit(design, outcomes)
recommended_dose(x) # de-escalate now...
dtp_should_wait(x) # ...but waiting could change that
dtp_wait_time(x) # days to wait
dtp_wait_time(x, type = "weight") # the same wait as a weight increment
dtp_projected_dose(x) # recommendation after the wait
Raw DTP wait event log from simulated trials.
Description
Returns a tidy tibble with one row per DTP wait event fired across all
simulated replicates. This is the single source of truth from which all
DTP wait summaries are derived; users who want metrics not covered by
dtp_wait_summary() can compute them directly from this table.
Usage
dtp_wait_events(x, ...)
Arguments
x |
A |
... |
Unused. |
Details
Replicates with zero waits contribute zero rows (but are still reflected
in dtp_wait_summary() with explicit 0s). Replicates from non-DTP
designs contribute zero rows and are flagged by the design carrying no
event log at all, not by reporting zero waits.
Value
A tibble with columns replicate (and design for a
simulations_collection), cohort_idx, dose_before, time_in,
time_out, wait_duration, projected_dose, dose_after,
ended_by (factor: wait_end/dtp_decision/stopped), dose_delta,
effective, num_extensions, n_held, n_missed.
wait_duration is the total elapsed time between when the wait was
triggered and when it ended, including any extensions caused by
in-wait DLT events: each such event prompts a re-fit, and if the
updated fit still requests a wait the original wait_end is bumped
out. num_extensions records how many times that happened (0 means
the wait ran exactly to its initial projection); as a result
wait_duration can exceed the design's t_max.
n_held is the number of patients held through the wait and dosed at
wait_end, including the cohort opener (the patient the wait is for);
it is at least 1 for any wait that doses and ranges up to the cohort size.
n_missed is the number who arrived while the wait was running to a full
waiting room and were never enrolled. See set_dtp_queue_size() for the
room capacity that bounds n_held and produces n_missed.
Examples
library(escalation)
design <- get_dfcrm_tite(skeleton = c(0.124, 0.25, 0.398, 0.542, 0.666),
target = 0.25) |>
apply_dtp(t_max = 35, obswin = 56, verbose = FALSE) |>
stop_at_n(n = 9)
set.seed(456)
sims <- simulate_trials(
design,
num_sims = 2,
true_prob_tox = c(0.05, 0.15, 0.25, 0.35, 0.60),
sample_patient_arrivals = function(df) {
cohorts_of_n(n = 3, mean_time_delta = 1)
},
max_time = 56
)
dtp_wait_events(sims)
dtp_wait_summary(sims)
dtp_wait_summary(sims, by_dose = TRUE)
Per-trial DTP wait summary from simulated trials.
Description
Aggregates the raw event log returned by dtp_wait_events() into a
curated per-replicate (or per-replicate × starting dose) summary.
Usage
dtp_wait_summary(x, by_dose = FALSE, ...)
Arguments
x |
A |
by_dose |
If |
... |
Unused. |
Details
With the default by_dose = FALSE, one row per replicate with columns:
-
num_waits— how often DTP fired in this trial -
total_wait_time— sum of wait durations -
wait_fraction—total_wait_time / trial_duration, a single comparable "DTP overhead" number. The denominator is the full trial calendar time as returned byescalation::trial_duration(), which includes the post-loop "advance to full follow-up" tail. Use the raw event log if you need a decision-phase-only denominator. -
max_wait_time— longest single wait (for slot / IRB conversations). Can exceedt_maxwhen an in-wait DLT triggers an extension; seedtp_wait_events(). -
num_effective_waits— number of waits where the post-wait recommendation was strictly higher than the pre-wait recommendation (dose_after > dose_before). Captures both outright escalations and waits that recovered from an otherwise unnecessary de-escalation. -
num_missed— per-trial sum of the event-logn_missed: patients who presented during a wait to a full waiting room and were never enrolled. This is DTP's accrual cost, and is zero whenever no wait ran longer than the room capacity allows.
With by_dose = TRUE, one row per (replicate × dose_before).
wait_fraction (needs a whole-trial denominator) is dropped and a
mean_wait_time column is added; max_wait_time and
num_effective_waits are retained per dose level.
Non-DTP designs — those whose simulation did not attach a wait event log
at all — return all-NA metric columns so that users can distinguish
"design did not run under DTP" from "ran under DTP and never waited"
(num_waits = 0).
On a simulations_collection (e.g. the return value of
simulate_compare), both accessors prepend a design column naming
the arm.
Value
A tibble. See Details for columns.
Examples
library(escalation)
design <- get_dfcrm_tite(skeleton = c(0.124, 0.25, 0.398, 0.542, 0.666),
target = 0.25) |>
apply_dtp(t_max = 35, obswin = 56, verbose = FALSE) |>
stop_at_n(n = 9)
set.seed(456)
sims <- simulate_trials(
design,
num_sims = 2,
true_prob_tox = c(0.05, 0.15, 0.25, 0.35, 0.60),
sample_patient_arrivals = function(df) {
cohorts_of_n(n = 3, mean_time_delta = 1)
},
max_time = 56
)
dtp_wait_events(sims)
dtp_wait_summary(sims)
dtp_wait_summary(sims, by_dose = TRUE)
Minimum wait time before the dose recommendation changes.
Description
Minimum wait time before the dose recommendation changes.
Usage
dtp_wait_time(x, type = c("time", "weight"), ...)
Arguments
x |
A |
type |
Character: |
... |
Additional arguments (unused). |
Value
Numeric scalar (0 if no benefit from waiting).
Examples
library(escalation)
design <- get_dfcrm_tite(skeleton = c(0.124, 0.25, 0.398, 0.542, 0.666),
target = 0.25) |>
apply_dtp(t_max = 35, obswin = 56)
# Four patients at dose 2: one DLT, and the last patient is only 30% of the
# way through the observation window.
outcomes <- data.frame(dose = c(2, 2, 2, 2), tox = c(0, 1, 0, 0),
weight = c(1, 1, 1, 0.3), cohort = 1:4)
x <- fit(design, outcomes)
recommended_dose(x) # de-escalate now...
dtp_should_wait(x) # ...but waiting could change that
dtp_wait_time(x) # days to wait
dtp_wait_time(x, type = "weight") # the same wait as a weight increment
dtp_projected_dose(x) # recommendation after the wait
Create a TITE-BOIN dose-finding selector.
Description
Returns a selector factory for the BOIN design with TITE
(time-to-event) support via effective sample sizes. This is a barebone
selector: stopping, elimination, and overdose control are added via
piping (e.g. stop_for_beta_binomial_toxicity, dont_skip_doses).
Usage
get_boin_tite(
num_doses,
target,
p.saf = 0.6 * target,
p.tox = 1.4 * target,
...
)
Arguments
num_doses |
Number of doses under investigation. |
target |
Target toxicity probability. |
p.saf |
Safety boundary parameter (default |
p.tox |
Toxicity boundary parameter (default |
... |
Extra arguments stored for future use. |
Value
A boin_tite_selector_factory object.
Examples
# Barebone — pure dose decision
library(escalation)
get_boin_tite(5, 0.25) %>%
fit(data.frame(dose = c(1,1,1), tox = c(0,0,0),
weight = c(1,1,1), cohort = c(1,1,1)))
Plot the cumulative incidence of reaching the MTD.
Description
Draws, for each design, the proportion of replicates in which a patient had
been dosed at the true MTD by a given point. The curve plateaus at
reached_fraction; replicates that never reached the MTD keep it below 1.
Usage
## S3 method for class 'time_to_first_mtd'
plot(x, axis = c("time", "patients"), ci = FALSE, conf = 0.95, ...)
Arguments
x |
A |
axis |
|
ci |
If |
conf |
Confidence level for the intervals. Default |
... |
Unused. |
Value
A ggplot object.
Examples
library(escalation)
skeleton <- c(0.124, 0.25, 0.398, 0.542, 0.666)
designs <- list(
`TITE-CRM` = get_dfcrm_tite(skeleton = skeleton, target = 0.25) |>
stop_at_n(n = 18),
`DTP + TITE-CRM` = get_dfcrm_tite(skeleton = skeleton, target = 0.25) |>
apply_dtp(t_max = 35, obswin = 56, verbose = FALSE) |>
stop_at_n(n = 18)
)
set.seed(456)
sims <- simulate_compare(
designs,
num_sims = 10,
true_prob_tox = c(0.05, 0.15, 0.25, 0.35, 0.60),
sample_patient_arrivals = function(df) {
cohorts_of_n(n = 3, mean_time_delta = 1)
},
max_time = 56,
i_like_big_trials = TRUE
)
ttm <- time_to_first_mtd(sims)
ttm
summary(ttm)
if (requireNamespace("ggplot2", quietly = TRUE)) {
plot(ttm)
}
True MTD of a simulated scenario.
Description
Identifies the dose whose true toxicity probability lies within tol of the
target. When several doses qualify the lowest is returned. When none
qualifies the scenario has no MTD and NA is returned with a warning.
Usage
scenario_mtd(true_prob_tox, target, tol = 0.05)
Arguments
true_prob_tox |
Numeric vector of true toxicity probabilities, one per dose. |
target |
Target toxicity probability. |
tol |
Half-width of the acceptance band. A dose qualifies when
|
Value
Integer dose index, or NA_integer_ when no dose qualifies.
Examples
scenario_mtd(c(0.05, 0.15, 0.25, 0.35, 0.60), target = 0.25)
# Two doses inside the band: the lowest is the MTD
scenario_mtd(c(0.05, 0.22, 0.27, 0.50), target = 0.25)
Set the waiting-room capacity for DTP simulation.
Description
Recruitment onto the trial is shut for the duration of a DTP wait, but the
pool of available patients keeps accruing. Patients who present during a
wait are held — up to queue_size of them — and dosed at the post-wait
recommendation the moment the wait ends. Patients who present when the room
is already full are not enrolled; they are the accrual cost of waiting,
reported as the n_missed column of dtp_wait_events().
Usage
set_dtp_queue_size(dtp_selector_factory, queue_size)
Arguments
dtp_selector_factory |
A |
queue_size |
Integer >= 0, and no greater than the cohort size.
Default (when not set) equals the cohort size, inferred at simulation time
from the first |
Details
A patient is therefore missed exactly when
wait_duration > queue_size * accrual_gap, which concentrates the cost at
small cohort sizes.
The patient whose arrival opened the cohort is always held — they are the reason the wait is being taken — so they occupy one slot and values below 1 behave as 1. At cohort size 1 the capacity therefore cannot bind: a single-patient cohort has no room for anyone else whatever this is set to. The knob only does work for cohorts of 2 or more.
Value
The modified dtp_selector_factory (invisibly).
Examples
library(escalation)
design <- get_dfcrm_tite(skeleton = c(0.124, 0.25, 0.398, 0.542, 0.666),
target = 0.25) |>
apply_dtp(t_max = 35, obswin = 56)
# Hold at most two patients while a wait runs
set_dtp_queue_size(design, 2)
Stop for excessive toxicity using a Beta-Binomial conjugate posterior.
Description
Creates a stopping decorator that monitors one or more doses and halts
the trial when the posterior probability of the true toxicity rate
exceeding tox_threshold is greater than confidence.
Usage
stop_for_beta_binomial_toxicity(
parent_selector_factory,
dose = 1,
tox_threshold,
confidence = 0.88,
a = NULL,
b = NULL,
n_min = 1
)
Arguments
parent_selector_factory |
A |
dose |
Which dose(s) to monitor. A numeric scalar (e.g. |
tox_threshold |
Toxicity probability threshold. Required. |
confidence |
Posterior exceedance cutoff (default: |
a, b |
Beta prior hyperparameters. When |
n_min |
Minimum number of patients at a dose required before the
exceedance check fires (default: |
Value
A beta_binom_tox_selector_factory object.
Examples
library(escalation)
design <- get_dfcrm_tite(skeleton = c(0.124, 0.25, 0.398, 0.542, 0.666),
target = 0.25) |>
stop_for_beta_binomial_toxicity(dose = 1, tox_threshold = 0.25,
confidence = 0.70)
# Three DLTs in four patients at the lowest dose stops the trial
outcomes <- data.frame(dose = c(1, 1, 1, 1), tox = c(1, 1, 0, 1),
weight = c(1, 1, 1, 1), cohort = 1:4)
x <- fit(design, outcomes)
continue(x)
recommended_dose(x)
Summarise time-to-first-MTD across replicates.
Description
Summarise time-to-first-MTD across replicates.
Usage
## S3 method for class 'time_to_first_mtd'
summary(object, probs = NULL, ...)
Arguments
object |
A |
probs |
Optional numeric vector of probabilities for additional
quantile columns. Must be distinct values between 0 and 1. Default |
... |
Unused. |
Details
Columns:
-
num_sims— replicates simulated -
mtd— dose treated as the true MTD -
num_reached— replicates in which a patient was dosed atmtd -
reached_fraction—num_reached / num_sims;NAwhen the scenario has no MTD -
median_time_to_mtd— median of the cumulative incidence of reaching the MTD.NAunless more than half of replicates reached it, because the quantile is otherwise undefined — including when exactly half did -
median_n_before_mtd— median patients dosed before the MTD, over replicates that reached it
Replicates that never reached the MTD remain in the denominator; they are
not dropped. Any probability supplied in probs adds a
q<pct>_time_to_mtd column, computed the same way and likewise NA beyond
reached_fraction. The percentage is not rounded, so 0.125 gives
q12.5_time_to_mtd; distinct probabilities always get distinct columns.
Value
A tibble with one row per design.
Examples
library(escalation)
skeleton <- c(0.124, 0.25, 0.398, 0.542, 0.666)
designs <- list(
`TITE-CRM` = get_dfcrm_tite(skeleton = skeleton, target = 0.25) |>
stop_at_n(n = 18),
`DTP + TITE-CRM` = get_dfcrm_tite(skeleton = skeleton, target = 0.25) |>
apply_dtp(t_max = 35, obswin = 56, verbose = FALSE) |>
stop_at_n(n = 18)
)
set.seed(456)
sims <- simulate_compare(
designs,
num_sims = 10,
true_prob_tox = c(0.05, 0.15, 0.25, 0.35, 0.60),
sample_patient_arrivals = function(df) {
cohorts_of_n(n = 3, mean_time_delta = 1)
},
max_time = 56,
i_like_big_trials = TRUE
)
ttm <- time_to_first_mtd(sims)
ttm
summary(ttm)
if (requireNamespace("ggplot2", quietly = TRUE)) {
plot(ttm)
}
Time until the first patient is dosed at the true MTD.
Description
Returns one row per simulated replicate recording when, if ever, a patient was first treated at the scenario's true MTD.
Usage
time_to_first_mtd(x, mtd = NULL, tol = 0.05, ...)
Arguments
x |
A |
mtd |
Dose index to treat as the true MTD. |
tol |
Passed to |
... |
Unused. |
Details
Times are dosing times measured from the start of the trial (t = 0). For
designs without DTP a patient is dosed on arrival, so dosing time equals
recruitment time; for patients held in a DTP queue it is the instant the
wait ends and they are dosed.
Columns:
-
replicate— index of the simulated trial (anddesignfor asimulations_collection) -
mtd— the dose treated as the true MTD -
time_to_mtd— dosing time of the first patient givenmtd, orNAif no patient was given it before the trial ended -
reached— whether any patient was dosed atmtd;NAwhen the scenario has no MTD -
n_before_mtd— patients dosed strictly earlier than that patient, orNAif the MTD was never given
When the scenario has no MTD (no dose within tol of the target) every row
carries mtd = NA and reached = NA, distinguishing "no MTD to reach" from
"an MTD existed and was not reached" (reached = FALSE).
The number of replicates is recorded in a num_sims attribute and is used
as the denominator by summary.time_to_first_mtd().
Value
A tibble of class time_to_first_mtd. See Details for columns.
Examples
library(escalation)
skeleton <- c(0.124, 0.25, 0.398, 0.542, 0.666)
designs <- list(
`TITE-CRM` = get_dfcrm_tite(skeleton = skeleton, target = 0.25) |>
stop_at_n(n = 18),
`DTP + TITE-CRM` = get_dfcrm_tite(skeleton = skeleton, target = 0.25) |>
apply_dtp(t_max = 35, obswin = 56, verbose = FALSE) |>
stop_at_n(n = 18)
)
set.seed(456)
sims <- simulate_compare(
designs,
num_sims = 10,
true_prob_tox = c(0.05, 0.15, 0.25, 0.35, 0.60),
sample_patient_arrivals = function(df) {
cohorts_of_n(n = 3, mean_time_delta = 1)
},
max_time = 56,
i_like_big_trials = TRUE
)
ttm <- time_to_first_mtd(sims)
ttm
summary(ttm)
if (requireNamespace("ggplot2", quietly = TRUE)) {
plot(ttm)
}
Create patient samples with realistic TITE DLT timing.
Description
Generates a list of escalation::PatientSample objects whose
tox_time values are drawn from U(0, max_time) instead of the
default U(0, 1). Pass the result to
escalation::simulate_compare() or escalation::simulate_trials()
via patient_samples.
Usage
tite_patient_samples(num_sims, max_time, num_patients = 100)
Arguments
num_sims |
Number of simulation replicates. |
max_time |
DLT observation window (e.g. 56 days). Must match the
|
num_patients |
Number of latent patients per sample (default 100). |
Value
A list of length num_sims, each element a PatientSample.
Examples
samples <- tite_patient_samples(num_sims = 2, max_time = 56)
length(samples)