knitr::opts_chunk$set( collapse = TRUE, cache = TRUE, cache.path = 'cache/defineNonTimeToEventEndpoints/', comment = '#>', dpi = 300, out.width = '100%' )
library(dplyr) library(TrialSimulator) set.seed(12345)
TrialSimulator provides a flexible framework for defining and simulating a variety of clinical trial endpoints by specifying the type parameter in endpoint. This vignette covers non-time-to-event (non-TTE) endpoints, demonstrating how they can be defined, integrated into trial arms, and analyzed at pre-specified milestones. For time-to-event endpoints, please refer to the vignette Define Time-to-Event Endpoints in Clinical Trials. For longitudinal endpoints, please refer to the vignette Define Longitudinal Endpoints in Clinical Trials
This vignette demonstrates how to use the following key functions to define non-TTE endpoints. For the sake of completeness, we also demonstrates how to define arms and trial with the created endpoints
endpoint: Creates one or more endpoints. It can also be used to define covariates, bio-markers, sub-group indicators, etc. arm: Creates one or more armsadd_endpoints: Add one or more endpoints to an armmilestone: Defines one or more milestones when data snapshots are needed for analysisSimilar to time-to-event endpoints, non-TTE endpoints can be defined using any univariate random number generator that takes n (number of observations) as its first argument. The stats package provides a set of random number generators that can be assigned to generator in endpoints, where additional arguments required by generator can be passed through .... When creating non-TTE endpoints, the argument type should be set to "non-tte", and the argument readout should be specified as a named numeric vector, indicating the time required for the endpoint to be available for analysis after patient enrollment.
In the example below, we define two types of endpoints:
Continuous endpoint: Tumor size change from baseline (cfb), available after 6 months, assuming a normal distribution (generator = rnorm) with custom mean and sd.
Binary endpoint: Objective response rate (orr), available after 2 months, assuming a binomial distribution (generator = rbinom) with size = 1 and custom prob.
## endpoints in placebo arm tumor_cfb_pbo <- endpoint(name = 'cfb', type = 'non-tte', readout = c(cfb = 6), generator = rnorm, mean = .8, sd = 3.2) orr_pbo <- endpoint(name = 'orr', type = 'non-tte', readout = c(orr = 2), generator = rbinom, size = 1, prob = .1) ## define the placebo arm pbo <- arm(name = 'placebo') pbo$add_endpoints(tumor_cfb_pbo, orr_pbo) ## endpoints in treatment arm tumor_cfb_trt <- endpoint(name = 'cfb', type = 'non-tte', readout = c(cfb = 6), generator = rnorm, mean = -2.3, sd = 1.5) orr_trt <- endpoint(name = 'orr', type = 'non-tte', readout = c(orr = 2), generator = rbinom, size = 1, prob = .25) ## define the treatment arm trt <- arm(name = 'treatment') trt$add_endpoints(tumor_cfb_trt, orr_trt)
With the treatment arms defined, we can proceed to create a trial. Patients are recruited at a piecewise constant rate, with an accrual pattern as follows:
We also specify a dropout process with a Weibull distribution. The dropout rates are set as follows:
These constraints are resolved using the Weibull dropout function:
$$ \begin{split} 0.15 & = & 1 - e^{-(12/\lambda)^k} \ 0.30 & = & 1 - e^{-(18/\lambda)^k} \end{split} $$
dropout_pars <- weibullDropout(c(12, 18), c(.15, .30)) dropout_pars
Using the computed scale parameter $\lambda=$ r round(dropout_pars['scale'], 3) and shape parameter $k=$ r round(dropout_pars['shape'], 3), we specify the trial setup:
accrual_rate <- data.frame(end_time = c(6, Inf), piecewise_rate = c(10, 20)) trial <- trial( name = 'Trial-31415', description = 'Example Clinical Trial', n_patients = 420, duration = 30, enroller = StaggeredRecruiter, accrual_rate = accrual_rate, dropout = rweibull, scale = 30.636, shape = 1.939 ) ## add arms to the trial trial$add_arms(sample_ratio = c(1, 1), trt, pbo) trial
Here accrual_rate is an argument of TrialSimulator::StaggeredRecruiter controlling how patients are recruited into the trial. Similarly, scale and shape are arguments of rweibull. All these arguments are passed through ... of trial().
TrialSimulator allows defining trial milestones at specific time points when data is locked for analysis. Here, we define three key milestones:
orr has been observed for 60 patients.cfb has been observed for at least 100 patients, orr has been observed for at least 180 patients. interim <- milestone(name = 'interim', when = eventNumber(endpoint = 'orr', n = 60), action = doNothing) random <- milestone(name = 'random', when = calendarTime(time = 10) & (eventNumber(endpoint = 'cfb', n = 100) | eventNumber(endpoint = 'orr', n = 180) ), action = doNothing) final <- milestone(name = 'final', when = calendarTime(time = 30), action = doNothing)
Here action = doNothing in milestone means we don't expect any action at the time of triggered milestones. In practice, instead of doNothing, custom action function can be adopted to add or remove arms (e.g., dose selection), adjust sample ratio per arm, or carry out statistical analysis based on locked data. These advanced setups are covered in other vignettes.
Next, we register the milestones with a listener and create a controller to monitor and execute the trial.
## register milestones to the listener listener <- listener() listener$add_milestones(interim, random, final) ## run the trial controller <- controller(trial, listener) controller$run()
We can inspect the dataset locked at different milestone by calling member function get_locked_data with milestone names. Ideally, this should be done within custom action function, where decision is made based on data locked at the time of a milestone.
interim_data <- trial$get_locked_data(milestone_name = 'interim') random_data <- trial$get_locked_data(milestone_name = 'random') final_data <- trial$get_locked_data(milestone_name = 'final') head(interim_data)
Since cfb has a 6-month readout time, at interim analysis, some patients’ cfb values are still unavailable, appearing as NA in interim_data. However, these values become available in random_data collected at a later time point. This demonstrates how TrialSimulator properly and automatically handles endpoint availability at different milestones
not_ready_at_interim <- interim_data %>% dplyr::filter(is.na(cfb) & is.na(orr) & enroll_time + 6 < dropout_time) %>% head() %>% print() random_data %>% dplyr::filter(patient_id %in% not_ready_at_interim$patient_id) %>% print()
In this example, we simulate tumor size change from baseline (cfb). However, in many trials, it is more appropriate to simulate tumor size at both baseline and follow-up separately to allow for more complex modeling, such as longitudinal or repeated measures analysis. This will be covered in another vignette.
With this flexible setup, TrialSimulator enables efficient endpoint definition, adaptive trial execution, and data monitoring—allowing users to design and simulate clinical trials tailored to specific research needs.
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.