knitr::opts_chunk$set( collapse = TRUE, cache.path = 'cache/crossoverWashout/', comment = '#>', dpi = 300, out.width = '100%' )
library(TrialSimulator) library(mvtnorm) library(dplyr) library(kableExtra)
The TrialSimulator package can handle three types of crossover design.
This vignette focuses on a trial for studies on symptom reduction, e.g.,
for chronic conditions with short-lived and reversible treatment
effects. A crossover trial is a longitudinal design where participants
receive multiple treatments sequentially, acting as their own control if
the carry-over effect can be minimized by the use of untreated wash-out
period. We can use the similar idea in the
vignette of longitudinal endpoints to implement simulation, where endpoints at
the end of each treatment in the pre-determined and randomized sequence
are defined in function endpoint() with its argument readout is set.
To simulate dynamic treatment switching, please refer to its vignette.
In simulation, we assume a balanced Latin square design of four arms
| Sequence | Treatment 1 | Treatment 2 | Treatment 3 | Treatment 4 | |----------|-------------|-------------|-------------|-------------| | ABCD | A | B | C | D | | BDAC | B | D | A | C | | CADB | C | A | D | B | | DCBA | D | C | B | A |
The figure shows the timeline of the trial
#| out-width: 100% #| echo: false knitr::include_graphics('crossover_readout.png')
Additional settings are
We collect baselines at the beginning of a new treatment (i.e., at the end of wash-out period), and endpoint value when the treatment is completed. To do so, we define four baseline variables and four endpoint variables as below
rng <- function(n, means, vcov = diag(1, 8)){ ret <- as.data.frame(rmvnorm(n, mean = means, sigma = vcov)) colnames(ret) <- c('baseline1', 'ep1', 'baseline2', 'ep2', 'baseline3', 'ep3', 'baseline4', 'ep4') ret }
all_endpoint_name <- c('baseline1', 'ep1', 'baseline2', 'ep2', 'baseline3', 'ep3', 'baseline4', 'ep4') readouts <- c(baseline1 = 0, ep1 = 3, baseline2 = 4, ep2 = 8, baseline3 = 10, ep3 = 15, baseline4 = 17.5, ep4 = 19.5) eps <- endpoint( name = all_endpoint_name, type = rep('non-tte', 8), readout = readouts, generator = rng, means = rep(c(0, .5), 4) ) arm1 <- arm(name = 'ABCD') arm1$add_endpoints(eps) arm1
Here, a custom random number generator, rng, is provided by users to describe how data of the treatment sequence ABCD is generated. For the purpose of illustration, we simply assume an independent structure between baselines and endpoints (vcov = diag(1, 8)). Users can adopt much complex models to simulate carry-over effect or other mechanism. Note that rng uses its default value for vcov, and a custom value for its argument means. We will use different value for means when defining other arms.
Adopting TrialSimulator in development of simulation enjoys the convenience of writing similar codes with good readability. To see that, here is how we define the other three arms
eps <- endpoint( name = all_endpoint_name, type = rep('non-tte', 8), readout = readouts, generator = rng, means = rep(c(0, .6), 4) # diff means ) arm2 <- arm(name = 'BDAC') arm2$add_endpoints(eps) eps <- endpoint( name = all_endpoint_name, type = rep('non-tte', 8), readout = readouts, generator = rng, means = rep(c(0, .2), 4), vcov = diag(1.2, 8) # diff means/vcov ) arm3 <- arm(name = 'CADB') arm3$add_endpoints(eps) eps <- endpoint( name = all_endpoint_name, type = rep('non-tte', 8), readout = readouts, generator = rng, means = rep(c(0, 0), 4) # diff means ) arm4 <- arm(name = 'DCBA') arm4$add_endpoints(eps)
As planned, we recruit 10 patients per week until 60 patients are randomized. A built-in enroller function StaggeredRecruiter is used to enroll patients. Note that the last patient would be randomized at the 6th week and complete all treatments at week 25.5. We can set duration = 25.5 in function trial(), however, we set it to a greater number (28) so that we can illustrate some nice features of TrialSimulator later.
accrual_rate <- data.frame(end_time = c(6, Inf), piecewise_rate = c(10, 10)) trial <- trial(name = 'crossover-trial', n_patients = 60, duration = 28, enroller = StaggeredRecruiter, accrual_rate = accrual_rate, silent = TRUE) trial$add_arms(sample_ratio = c(1, 1, 1, 1), arm1, arm2, arm3, arm4) trial
Next, we define the only milestone of the trial where data is analyzed. In this example, we have three different ways to specify the triggering condition of the milestone equivalently, using the three key functions of the condition system.
action <- function(trial){ locked_data <- trial$get_locked_data('final') ## omit statistical analysis trial$save(value = 'anything', name = 'result') ## save more results for summary of simulation # trial$save(value = ..., name = ...) }
The first one uses calendarTime() where we set the 25.5th weeks as the end of trial. Note that the simulate will stop at that time even if the duration of trial is set to be 28 weeks.
final <- milestone(name = 'final', when = calendarTime(time = 25.5), action = action)
We can also define the milestone time by the number of readouts of ep4, i.e., endpoint when the last treatment is completed. In the code below, we trigger the milestone when all 60 patients have their ep4 readouts.
final <- milestone(name = 'final', when = eventNumber(endpoint = 'ep4', n = 60), action = action)
Equivalently, the milestone can be define as the time point when all 60 patients are enrolled and have been treated for at least 19.5 weeks.
final <- milestone(name = 'final', when = enrollment(n = 60, min_treatment_duration = 19.5), action = action)
The action function, action(), is an essential component when defining a milestone. It takes the trial object as its first argument, and any optional arguments follow. Statistical analysis can be carried out in the function using data locked for milestone. The data cut can be requested by calling the member function get_locked_data(milestone_name), followed by user-defined analysis, decisions, and actions. As the statistical methods for crossover design is out of the scope of the package, we omit it from implementation.
Now we have all pieces to complete the implementation of simulation. Let's register the milestone with a listener, so that it can be monitored automatically when a controller is running a trial.
listener <- listener() listener$add_milestones(final) controller <- controller(trial, listener) controller$run(n = 1, plot_event = TRUE)
Simulation results are saved and can be retrieved by calling the member function get_output() of a controller. In this example, we can see the column result we saved manually in action function, along with many other columns saved by the controller automatically. To know more about the naming convention of auto saved columns, please refer to vignette of action function. To run more replicates of simulation, simply set n to a greater integer in controller$run().
output <- controller$get_output() output %>% kable(escape = FALSE) %>% kable_styling(bootstrap_options = "striped", full_width = FALSE, position = "left") %>% scroll_box(width = "100%")
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.