| Title: | Clinical Trial Simulator |
|---|---|
| Description: | Simulate phase II and/or phase III clinical trials. It supports various types of endpoints and adaptive strategies. Tools for carrying out graphical testing procedure and combination test under group sequential design are also provided. |
| Authors: | Han Zhang [cre, aut] |
| Maintainer: | Han Zhang <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 1.39.0 |
| Built: | 2026-10-16 07:40:32 UTC |
| Source: | https://github.com/zhangh12/trialsimulator |
add one or more arms to a trial. This function can be used in two
scenarios: (1) adding arms right after a trial is created
(i.e., trial(...)). sample_ratio and arms added through
... must be of same length; (2) adding arms to an existing trial in
action functions of milestones.
This is a user-friendly wrapper of the member function of trial, i.e.,
Trials$add_arms(), which is used in vignettes. Users who are not
familiar with the concept of classes may consider using this wrapper
directly.
The trial captures an independent deep copy of every arm it registers.
Subsequent changes to the original arm object do not affect the trial,
and adaptations within the trial (e.g., update_generator()) do
not modify the original arm. Complete the configuration of an arm before
registering it. The copy covers the arm and endpoint objects themselves;
a mutable environment or R6 object captured by a generator function is
shared by design of R closures and is not isolated.
add_arms(trial, sample_ratio, ...)add_arms(trial, sample_ratio, ...)
trial |
a trial object returned by |
sample_ratio |
integer vector. Sample ratio for permuted block randomization. It will be appended to existing sample ratio in the trial. |
... |
one or more objects returned from |
no return value, called for its side effect of updating trial.
Define an arm in a trial. This is a user-friendly wrapper for
the class constructor Arms$new(). Users who are not familiar with
the concept of classes may consider using this wrapper directly.
arm(name, ...)arm(name, ...)
name |
character. Name of arm, which is the arm's label in generated
trial data, i.e., the one retrieved by calling |
... |
subset condition that is compatible with |
an object of R6 class Arms, representing an arm of a trial.
risk <- data.frame( end_time = c(1, 10, 26.0, 52.0), piecewise_risk = c(1, 1.01, 0.381, 0.150) * exp(-3.01) ) pfs <- endpoint(name = 'pfs', type='tte', generator = PiecewiseConstantExponentialRNG, risk = risk, endpoint_name = 'pfs') orr <- endpoint( name = 'orr', type = 'non-tte', readout = c(orr = 2), generator = rbinom, size = 1, prob = .4) placebo <- arm(name = 'pbo') placebo$add_endpoints(pfs, orr) ## try to generate some data from the arm ## it is NOT a recommended way to use the package in simulation head(placebo$get_endpoints()[[1]]$get_generator()(n = 1e3)) ## get name of endpoints in the arm ## for illustration only, NOT recommended placebo$get_endpoints()[[2]]$get_name() ## run it in console to get summary report ## It is the recommended way to view an arm placeborisk <- data.frame( end_time = c(1, 10, 26.0, 52.0), piecewise_risk = c(1, 1.01, 0.381, 0.150) * exp(-3.01) ) pfs <- endpoint(name = 'pfs', type='tte', generator = PiecewiseConstantExponentialRNG, risk = risk, endpoint_name = 'pfs') orr <- endpoint( name = 'orr', type = 'non-tte', readout = c(orr = 2), generator = rbinom, size = 1, prob = .4) placebo <- arm(name = 'pbo') placebo$add_endpoints(pfs, orr) ## try to generate some data from the arm ## it is NOT a recommended way to use the package in simulation head(placebo$get_endpoints()[[1]]$get_generator()(n = 1e3)) ## get name of endpoints in the arm ## for illustration only, NOT recommended placebo$get_endpoints()[[2]]$get_name() ## run it in console to get summary report ## It is the recommended way to view an arm placebo
Create a class of arm.
Public methods in this R6 class are used in developing this package. Thus, we have to export the whole R6 class which exposures all public methods. However, only the public methods in the list below are useful to end users.
$add_endpoints() add one or multiple endpoints to the arm.
$print() print a summary report of the arm based on
example data of its endpoints.
In addition, $generate_data() and $get_endpoints() are for
exploratory purpose only. They appear in examples and vignettes to help
users understand how this class works, but they are not needed at all in
formal simulation once users know how to use this package.
Internal machinery. The remaining public methods
($get_name(), $get_endpoints_name(), $has_endpoint()
and $update_endpoint_generator()) are public only because they are
invoked on an arm object by other components of the package (trials),
which R6 cannot grant through private members. Users should not call them
directly.
an R6Class generator object; use arm() to create an arm.
new()
initialize an arm
Arms$new(name, ...)
namename of arm, which is the arm's label in generated data.
It must not contain '@' or ';', which are reserved for
encoding the treatment switching history (regimen_trajectory)
of trials with a regimen.
...subset condition that is compatible with dplyr::filter.
This can be used to specify inclusion criteria of an arm.
By default it is not specified, i.e. all data generated by the generator
will be used as trial data. More than one conditions can be
specified in ....
add_endpoints()
add one or multiple endpoints to the arm.
Arms$add_endpoints(...)
...one or more objects returned from endpoint().
a <- arm(name = 'trt')
x <- endpoint(name = 'x', type = 'tte',
generator = rexp) # median = log(2)/1 = 0.7
y <- endpoint(name = 'y', type = 'non-tte', readout = c(y = 0),
generator = rnorm, sd = 1.4, mean = 0.7)
a$add_endpoints(y, x)
## run it in console to see the summary report
a
print(a) # use the print method
get_name()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
return name of arm.
Arms$get_name()
has_endpoint()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
check if the arm has any endpoint. Return TRUE or FALSE.
Arms$has_endpoint()
get_endpoints()
return a list of endpoints in the arm.
Arms$get_endpoints()
get_endpoints_name()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
return name of endpoints registered to the arm.
Arms$get_endpoints_name()
update_endpoint_generator()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
update generator of an endpoint object
Arms$update_endpoint_generator(endpoint_name, generator, ...)
endpoint_namecharacter. A vector of endpoint names whose generator is updated.
generatora random number generation (RNG) function.
See generator of endpoint().
...optional arguments for generator.
generate_data()
generate arm data.
Arms$generate_data(n_patients_in_arm)
n_patients_in_arminteger. Number of patients randomized to the arm.
print()
print an arm.
Arms$print(categorical_vars = NULL)
categorical_varscharacter vector of categorical variables. This can be used to specify variables with limited distinct (numeric) values as categorical variables in summary report.
clone()
The objects of this class are cloneable with this method.
Arms$clone(deep = FALSE)
deepWhether to make a deep clone.
# Instead of using Arms$new(), please use arm(), a user-friendly # wrapper. See examples in ?arm ## ------------------------------------------------ ## Method `Arms$add_endpoints` ## ------------------------------------------------ a <- arm(name = 'trt') x <- endpoint(name = 'x', type = 'tte', generator = rexp) # median = log(2)/1 = 0.7 y <- endpoint(name = 'y', type = 'non-tte', readout = c(y = 0), generator = rnorm, sd = 1.4, mean = 0.7) a$add_endpoints(y, x) ## run it in console to see the summary report a print(a) # use the print method# Instead of using Arms$new(), please use arm(), a user-friendly # wrapper. See examples in ?arm ## ------------------------------------------------ ## Method `Arms$add_endpoints` ## ------------------------------------------------ a <- arm(name = 'trt') x <- endpoint(name = 'x', type = 'tte', generator = rexp) # median = log(2)/1 = 0.7 y <- endpoint(name = 'y', type = 'non-tte', readout = c(y = 0), generator = rnorm, sd = 1.4, mean = 0.7) a$add_endpoints(y, x) ## run it in console to see the summary report a print(a) # use the print method
Define a condition to trigger trial milestone by calendar time. The milestone will be triggered when a trial has been running for at least the specified duration since the first patient is enrolled. It can be used combined with conditions specified by enrollment and eventNumber.
Refer to the
vignette
to learn how to define milestones when performing simulation using
TrialSimulator.
calendarTime(time)calendarTime(time)
time |
numeric. Calendar time to trigger a milestone of a trial. |
an object of class 'Condition'
milestone(name = 'end of trial', when = calendarTime(time = 12))milestone(name = 'end of trial', when = calendarTime(time = 12))
Define a controller of a trial. This is a user-friendly wrapper for
the class constructor Controller$new(). Users who are not familiar with
the concept of classes may consider using this wrapper directly.
TrialSimulator uses a controller to coordinate a trial object
and a listener object to run simulations, in which the trial object defines
endpoints, arms, and other settings of a trial, while the listener object
monitors trials to triggered pre-defined milestones and execute action
functions. See vignettes of this package for more examples.
controller(trial, listener)controller(trial, listener)
trial |
an object returned from |
listener |
an object returned from |
an object of R6 class Controllers, executing simulation of a trial.
# a minimum, meaningful, and executable example, # where a randomized trial with two arms is simulated and analyzed. control <- arm(name = 'control arm') active <- arm(name = 'active arm') pfs_in_control <- endpoint(name = 'PFS', type = 'tte', generator = rexp, rate = log(2) / 5) control$add_endpoints(pfs_in_control) pfs_in_active <- endpoint(name = 'PFS', type = 'tte', generator = rexp, rate = log(2) / 6) active$add_endpoints(pfs_in_active) accrual_rate <- data.frame(end_time = c(10, Inf), piecewise_rate = c(30, 50)) trial <- trial(name = 'trial', n_patients = 1000, enroller = StaggeredRecruiter, accrual_rate = accrual_rate, dropout = rweibull, shape = 2, scale = 38) trial$add_arms(sample_ratio = c(1, 1), control, active) action_at_final <- function(trial){ locked_data <- trial$get_locked_data('final analysis') fitLogrank(Surv(PFS, PFS_event) ~ arm, placebo = 'control arm', data = locked_data, alternative = 'less') invisible(NULL) } final <- milestone(name = 'final analysis', action = action_at_final, when = calendarTime(time = 40)) listener <- listener() listener$add_milestones(final) controller <- controller(trial, listener) controller$run(n = 1)# a minimum, meaningful, and executable example, # where a randomized trial with two arms is simulated and analyzed. control <- arm(name = 'control arm') active <- arm(name = 'active arm') pfs_in_control <- endpoint(name = 'PFS', type = 'tte', generator = rexp, rate = log(2) / 5) control$add_endpoints(pfs_in_control) pfs_in_active <- endpoint(name = 'PFS', type = 'tte', generator = rexp, rate = log(2) / 6) active$add_endpoints(pfs_in_active) accrual_rate <- data.frame(end_time = c(10, Inf), piecewise_rate = c(30, 50)) trial <- trial(name = 'trial', n_patients = 1000, enroller = StaggeredRecruiter, accrual_rate = accrual_rate, dropout = rweibull, shape = 2, scale = 38) trial$add_arms(sample_ratio = c(1, 1), control, active) action_at_final <- function(trial){ locked_data <- trial$get_locked_data('final analysis') fitLogrank(Surv(PFS, PFS_event) ~ arm, placebo = 'control arm', data = locked_data, alternative = 'less') invisible(NULL) } final <- milestone(name = 'final analysis', action = action_at_final, when = calendarTime(time = 40)) listener <- listener() listener$add_milestones(final) controller <- controller(trial, listener) controller$run(n = 1)
Create a class of controller to run a trial.
Public methods in this R6 class are used in developing this package. Thus, we have to export the whole R6 class which exposures all public methods. However, only the public methods in the list below are useful to end users.
$run() run trial simulation, sequentially or in parallel.
It cannot be called twice on a controller unless $reset() is
called in between.
$get_output() return a data frame of all outputs saved
during simulation.
$reset() reset the trial and listener registered to the
controller before starting a new simulation with $run().
an R6Class generator object; use controller() to create a controller.
new()
initialize a controller of the trial
Controllers$new(trial, listener)
triala trial object returned from trial().
listenera listener object returned from listener().
reset()
reset the trial and listener registered to the controller so that a
new simulation can be started with controller$run(). The trial
and the milestones are restored to their as-designed version.
Controllers$reset()
get_output()
return a data frame of all current outputs saved by calling save().
Controllers$get_output(cols = NULL, simplify = TRUE, tidy = FALSE)
colscharacter vector. Columns to be returned from the data frame
of simulation outputs. If NULL, all columns are returned.
simplifylogical. Return vector rather than a data frame of one
column when length(cols) == 1 and simplify == TRUE.
tidylogical. TrialSimulator automatically records a set
of standard outputs at milestones, even when doNothing is used
as action functions. These includes time of triggering milestones,
number of observed events for time-to-event endpoints, and number of
non-missing readouts for non-TTE endpoints
(see vignette('actionFunctions')). This usually mean a large
number of columns in outputs. If users have no intent to summarize a
trial on these columns, setting tidy = TRUE can eliminate these
columns from get_output(). This is useful to reduced the size of
output data frame when a large number of replicates are done for
simulation. Note that currently we use regex
"^n_events_<.*?>_<.*?>$" and
"^milestone_time_<.*?>$" to match columns to be eliminated.
If users plan to use tidy = TRUE, caution is needed when naming
custom outputs in save(). Default FALSE.
run()
run trial simulation. It cannot be called again on the same
controller unless reset() is called first.
Controllers$run( n = 1, n_workers = 1, plot_event = TRUE, silent = FALSE, tidy = FALSE )
ninteger. Number of replicates of simulation.
n = 1 by default. Simulation results can be accessed by
controller$get_output().
n_workersinteger. Number of parallel workers. When
n_workers = 1 (default), replicates are run sequentially.
When n_workers > 1, replicates are distributed across
parallel workers using the mirai package, which must be
installed separately. Each worker receives a serialized copy of
the trial and listener objects and runs its share of replicates
independently. If any replicate encounters an error, execution
stops and already-collected results are preserved in
$get_output(). To debug, manually set seed in
trial() and n_workers = 1 in run() for reproduced
results. Note that optimal n_workers may not be
parallel::detectCores(). For example, Macbook with M1/M2/M3 chips
may have performance cores and efficiency cores. To achieve the best
parallel performance, one may want to use the performance cores only.
For a M1 laptop with 4 performance cores, n_workers = 3 may give
the best performance.
plot_eventlogical. Create event plot if TRUE. Forced to
FALSE when n > 1 or n_workers > 1.
silentlogical. TRUE if muting all messages during a
trial. Note that warning messages are still displayed. When
silent = TRUE and replicates are run sequentially
(n_workers = 1), a progress bar is displayed automatically
if the simulation is expected to take more than 1 minute.
tidylogical. If TRUE, the per-arm event count table
(output column n_events_<milestone>_<arms>) is not saved at
milestones; the per-endpoint totals and milestone times are still
saved. Saving that table is the most expensive part of the standard
outputs, so tidy = TRUE is recommended for a large number of
replicates unless the per-arm counts are needed in the summary. This
differs from tidy in $get_output(), which removes all
standard columns from the returned data frame after the fact.
Default FALSE.
clone()
The objects of this class are cloneable with this method.
Controllers$clone(deep = FALSE)
deepWhether to make a deep clone.
####
Apply a milestone-triggered crossover to patients who are still in the trial.
Unlike a regimen registered via add_regimen() (which is applied once,
at enrollment), crossover() is meant to be called inside a
milestone's action function. At the earliest crossover (calendar) time
T = trial$get_current_time() + delay, eligible patients may switch to a
new treatment, and only their post-switch endpoint values are altered;
already-observed data is left intact. The crossover triplet is stacked onto
the trial's regimen, so it is also re-applied to patients enrolled later.
Eligibility (the pool passed to what()) consists of patients with at
least one endpoint still "open" (unobserved, dropout-/duration-aware) at
T; fully-observed patients are excluded. when() must return a
switch time with enroll_time + switch_time >= T (a crossover cannot
predate its opening) or an error is raised. how() may only change
post-switch outcomes; returning a changed value for a pre-switch/locked cell
raises an error.
Two helper columns are injected into patient_data for the triplet
functions: earliest_crossover_calendar_time (= T) and
earliest_crossover_time_from_enrollment (= max(T - enroll_time, 0)),
so when() can write e.g.
switch_time = pmax(pfs, earliest_crossover_time_from_enrollment).
Note that this function should only be called within action functions of
milestones. It is users' responsibility to ensure that and
TrialSimulator has no way to track it.
This is a user-friendly wrapper of the member function of trial, i.e.,
Trials$crossover(). Users who are not familiar with the concept of
classes may consider using this wrapper directly.
crossover(trial, what, how, when = NULL, delay = 0, ...)crossover(trial, what, how, when = NULL, delay = 0, ...)
trial |
a trial object returned by |
what |
a function selecting which eligible patients crossover, returning
one row per crossing-over patient with their |
how |
a function returning the modified post-switch endpoint values for
crossing-over patients. See |
when |
(optional) a function returning |
delay |
numeric. Time after the milestone before the crossover opens, so
|
... |
(optional) named arguments routed to |
no return value, called for its side effect of updating trial.
This is an action function that does nothing when the corresponding milestone
is triggered. When the listener is monitoring a trial and determining the
time to trigger a milestone, data is automatically locked with other necessary
data manipulations (censoring, truncation, etc.) are executed.
If the users have no intent to modify the
trial adaptively at the milestone, e.g., adding (add_arms()) or
removing (remove_arms()) arm(s),
changing sampling ratio(s) (update_sample_ratio()),
stopping follow-up of a cohort (stop_followup()), carrying out statistical
testing, or saving intermediate results (save(), etc.),
then this function
can be used to set the argument action when creating a new milestone.
Note that the triggering time and number of observations/events of endpoints
at a milestone with action = doNothing is still recorded in output
automatically.
doNothing(trial, ...)doNothing(trial, ...)
trial |
an object returned from |
... |
(optional) arguments. This is for capturing redundant arguments
in |
This function returns NULL. Actually, nothing is done in this function.
This function may be useful to advanced users of TrialSimulator. It
creates a wrapper function of a random number generator, while fixing a
subset or all of arguments. This function is design to prevent inadvertent
changing to arguments of random number generator. See examples below.
DynamicRNGFunction(fn, ...)DynamicRNGFunction(fn, ...)
fn |
random number generator, e.g., |
... |
arguments for |
a function to generate random number based on fn and arguments in
.... Specified arguments will be fixed and cannot be changed when invoking
DynamicRNGFunction(fn, ...)(). For example,
if foo <- DynamicRNGFunction(rnorm, sd = 2),
then foo(n = 100) will always generate data from normal distribution of
variance 4. foo(n = 100, sd = 1) will trigger an error. However,
if an argument is not specified in DynamicRNGFunction, then it can be specified
later. For example, foo(n = 100, mean = -1) will generate data from N(-1, 4).
# example code dfunc <- DynamicRNGFunction(rnorm, sd = 3.2) x <- dfunc(1e3) # mean 0 and sd 3.2 hist(x) y <- dfunc(1e3, mean = 3.5) # mean can be changed mean(y) try(z <- dfunc(1e3, sd = 1)) # error because sd is fixed in dfunc# example code dfunc <- DynamicRNGFunction(rnorm, sd = 3.2) x <- dfunc(1e3) # mean 0 and sd 3.2 hist(x) y <- dfunc(1e3, mean = 3.5) # mean can be changed mean(y) try(z <- dfunc(1e3, sd = 1)) # error because sd is fixed in dfunc
Define one or multiple endpoints. This is a user-friendly wrapper for
the class constructor Endpoints$new. Users who are not familiar with
the concept of classes may consider using this wrapper directly.
Note that it is users' responsibility to assure that the units of readout of non-tte endpoints, dropout time, and trial duration are consistent.
endpoint(name, type, readout = NULL, generator, ...)endpoint(name, type, readout = NULL, generator, ...)
name |
character vector. Name(s) of endpoint(s) |
type |
character vector. Type(s) of endpoint(s) in |
readout |
numeric vector named by non-tte endpoint(s).
|
generator |
a RNG function. Its first argument must be |
... |
(optional) arguments of |
an object of R6 class Endpoints, representing one or more endpoints.
set.seed(12345) ## Example 1. Generate a time-to-event endpoint. ## Two columns are returned, one for time, one for event (1/0, 0 for ## A built-in RNG function is used to handle piecewise constant exponential ## distribution risk <- data.frame( end_time = c(1, 10, 26.0, 52.0), piecewise_risk = c(1, 1.01, 0.381, 0.150) * exp(-3.01) ) pfs <- endpoint(name = 'pfs', type='tte', generator = PiecewiseConstantExponentialRNG, risk = risk, endpoint_name = 'pfs') # run it in R console to display a summary report # event indicator takes values 0/1 pfs ## Example 2. Generate continuous and binary endpoints using R's built-in ## RNG functions, e.g. rnorm, rexp, rbinom, etc. ep1 <- endpoint( name = 'cd4', type = 'non-tte', generator = rnorm, readout = c(cd4=1), mean = 1.2) ep2 <- endpoint( name = 'resp_time', type = 'non-tte', generator = rexp, readout = c(resp_time=0), rate = 4.5) ep3 <- endpoint( name = 'orr', type = 'non-tte', readout = c(orr=3), generator = rbinom, size = 1, prob = .4) ep1 # run it in R console. Mean and sd should be comparable to (1.2, 1.0) ep2 # run it in R console. Median should be comparable to log(2)/4.5 = 0.154 ep3 # run it in R console. Mean and sd should be comparable to 0.4 and 0.49 ## Example3: delayed effect ## Use piecewise constant exponential random number generator ## Baseline hazards are piecewise constant ## Hazard ratios are piecewise constant, resulting a delayed effect. ## Note that this example is for explaining the concept of "endpoint". ## Generating endpoint data manually is not the recommended way to use this package. run <- TRUE if (!requireNamespace("survminer", quietly = TRUE)) { run <- FALSE message("Please install 'survminer' to run this example.") } if (!requireNamespace("survival", quietly = TRUE)) { run <- FALSE message("Please install 'survival' to run this example.") } if(run){ risk1 <- risk ep1 <- endpoint( name = 'pfs', type='tte', generator = PiecewiseConstantExponentialRNG, risk=risk1, endpoint_name = 'pfs') risk2 <- risk1 risk2$hazard_ratio <- c(1, 1, .6, .4) ep2 <- endpoint( name = 'pfs', type='tte', generator = PiecewiseConstantExponentialRNG, risk=risk2, endpoint_name = 'pfs') n <- 1000 tte <- rbind(ep1$get_generator()(n), ep2$get_generator()(n)) arm <- rep(0:1, each = n) dat <- data.frame(tte, arm) sfit <- survival::survfit( survival::Surv(time = pfs, event = pfs_event) ~ arm, dat) survminer::ggsurvplot(sfit, data = dat, pval = TRUE, # Show p-value conf.int = TRUE, # Show confidence intervals risk.table = TRUE, # Add risk table palette = c("blue", "red")) ## print summary reports for endpoint objects in console ep1 ep2 } ## Example 4: generate correlated pfs and os ## See vignette('simulatePfsAndOsIdm') and vignette('simulatePfsAndOsGumbel')set.seed(12345) ## Example 1. Generate a time-to-event endpoint. ## Two columns are returned, one for time, one for event (1/0, 0 for ## A built-in RNG function is used to handle piecewise constant exponential ## distribution risk <- data.frame( end_time = c(1, 10, 26.0, 52.0), piecewise_risk = c(1, 1.01, 0.381, 0.150) * exp(-3.01) ) pfs <- endpoint(name = 'pfs', type='tte', generator = PiecewiseConstantExponentialRNG, risk = risk, endpoint_name = 'pfs') # run it in R console to display a summary report # event indicator takes values 0/1 pfs ## Example 2. Generate continuous and binary endpoints using R's built-in ## RNG functions, e.g. rnorm, rexp, rbinom, etc. ep1 <- endpoint( name = 'cd4', type = 'non-tte', generator = rnorm, readout = c(cd4=1), mean = 1.2) ep2 <- endpoint( name = 'resp_time', type = 'non-tte', generator = rexp, readout = c(resp_time=0), rate = 4.5) ep3 <- endpoint( name = 'orr', type = 'non-tte', readout = c(orr=3), generator = rbinom, size = 1, prob = .4) ep1 # run it in R console. Mean and sd should be comparable to (1.2, 1.0) ep2 # run it in R console. Median should be comparable to log(2)/4.5 = 0.154 ep3 # run it in R console. Mean and sd should be comparable to 0.4 and 0.49 ## Example3: delayed effect ## Use piecewise constant exponential random number generator ## Baseline hazards are piecewise constant ## Hazard ratios are piecewise constant, resulting a delayed effect. ## Note that this example is for explaining the concept of "endpoint". ## Generating endpoint data manually is not the recommended way to use this package. run <- TRUE if (!requireNamespace("survminer", quietly = TRUE)) { run <- FALSE message("Please install 'survminer' to run this example.") } if (!requireNamespace("survival", quietly = TRUE)) { run <- FALSE message("Please install 'survival' to run this example.") } if(run){ risk1 <- risk ep1 <- endpoint( name = 'pfs', type='tte', generator = PiecewiseConstantExponentialRNG, risk=risk1, endpoint_name = 'pfs') risk2 <- risk1 risk2$hazard_ratio <- c(1, 1, .6, .4) ep2 <- endpoint( name = 'pfs', type='tte', generator = PiecewiseConstantExponentialRNG, risk=risk2, endpoint_name = 'pfs') n <- 1000 tte <- rbind(ep1$get_generator()(n), ep2$get_generator()(n)) arm <- rep(0:1, each = n) dat <- data.frame(tte, arm) sfit <- survival::survfit( survival::Surv(time = pfs, event = pfs_event) ~ arm, dat) survminer::ggsurvplot(sfit, data = dat, pval = TRUE, # Show p-value conf.int = TRUE, # Show confidence intervals risk.table = TRUE, # Add risk table palette = c("blue", "red")) ## print summary reports for endpoint objects in console ep1 ep2 } ## Example 4: generate correlated pfs and os ## See vignette('simulatePfsAndOsIdm') and vignette('simulatePfsAndOsGumbel')
Create a class of endpoint to specify its name, type, readout time (optional) and assign a random number generator.
Public methods in this R6 class are used in developing this package. Thus, I have to export the whole R6 class which exposures all public methods. However, none of the public methods is useful to end users except for the one below.
$print() print a summary report of endpoint(s) based on
example data from the generator.
In addition, $test_generator(), $get_generator() and
$get_name() are for exploratory purpose only. They appear in
examples and vignettes to help users understand how this class works,
but they are not needed at all in formal simulation once users know how
to use this package.
Internal machinery. The remaining public methods
($get_uid() and $update_generator()) are public only because
they are invoked on an endpoint object by other components of the package
(arms), which R6 cannot grant through private members. Users should not
call them directly.
an R6Class generator object; use endpoint() to create endpoints.
new()
initialize an endpoint.
Endpoints$new(name, type, readout = NULL, generator, ...)
namecharacter vector. Name(s) of endpoint(s)
typecharacter vector. Type(s) of endpoint(s). It supports
"tte" for time-to-event endpoints, and "non-tte" for
all other types of endpoints (e.g., continous, binary, categorical,
or repeated measurement. TrialSimulator will do some verification if
an endpoint is of type "tte". However, no special
manipulation is done for non-tte endpoints.
Note: Endpoints$new() only recognizes "tte" and
"non-tte". The "baseline" type accepted by endpoint()
is a non-tte endpoint observed at randomization; endpoint()
converts it to "non-tte" with readout 0 before constructing
the object, so it never reaches here.
readouta named numeric vector with name to be non-tte endpoint(s).
readout must be specified for every non-tte endpoint. For
example, c(endpoint1 = 6, endpoint2 = 3), which means that it
takes 6 and 3 unit time to get readout of
endpoint1 and endpoint2 of a patient since being
randomized. Error message would be prompted if readout is not
named or readout is not specified for some non-tte endpoint.
If all endpoints are tte, readout should be NULL as default.
generatora random number generation (RNG) function.
It supports all built-in random number generators in stats, e.g.,
stats::rnorm, stats::rexp, etc. that with n as the
argument for number of observations and returns a vector.
A custom RNG function is also supported.
generator could be any functions as long as
(1) its first argument is n; and (2) it returns a vector of
length n (univariate endpoint) or a data frame of n rows
(multiple endpoints), i.e., custom RNG can return data of more than one
endpoint. This is useful when users need to simulate correlated endpoints
or longitudinal data. The column names of returned data frame should
match to name exactly, although order of columns does not matter.
If an endpoint is of type "tte", the custom generator
should also return a column as its event indicator. For example,
if "pfs" is "tte",
then custom generator should return at least two columns
"pfs" and "pfs_event". Usually pfs_event can be
all 1s if no censoring. Some RNG functions, e.g.,
TrialSimulator::PiecewiseConstantExponentialRNG() and
TrialSimulator::CorrelatedPfsAndOs4(), simulate TTE endpoint
data with censoring simultaneously, thus 0 exists in the columns of
event indicators. Users can implement censorship in their own RNG.
Censoring can also be specified later when
defining a trial object through argument dropout.
See ?trial.
Note that if covariates, e.g., biomarker, subgroup, are needed in
generating and analyzing trial data, they can and should be defined as
endpoints in endpoint() as well.
...optional arguments for generator.
test_generator()
test random number generator of the endpoints. It returns an example
dataset of an endpoint object. Note that users of TrialSimulator
does not need to call this function to generate trial data; instead,
the package will call this function at milestone automatically.
Users may see example in vignette where this function is called.
However, it is for illustration purpose only. In practice, this function
may be used for debugging if users suspect some issues in custom
generator, otherwise, this function should never been called in formal
simulation.
Endpoints$test_generator(n = 1000)
ninteger. Number of random numbers generated from the generator.
get_generator()
return random number generator of an endpoint
Endpoints$get_generator()
update_generator()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
update endpoint generator
Endpoints$update_generator(generator, ...)
generatora random number generation (RNG) function.
See generator of endpoint().
...optional arguments for generator.
get_uid()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
return uid
Endpoints$get_uid()
get_name()
return endpoints' name
Endpoints$get_name()
print()
print an endpoint object
Endpoints$print(categorical_vars = NULL)
categorical_varsa character vector of endpoints. This can
be used to force variables with limited distinct values as categorical
variables in summary report. For example, a numeric endpoint may take
integer values 0, 1, 2. Instead of computing mean and standard derivation
in the summary report, put this endpoint in categorical_vars can
force it be a categorical variable and a barplot is generated in summary
report instead.
rng <- function(n){
data.frame(x = sample(1:3, n, replace = TRUE),
y = sample(1:3, n, replace = TRUE)
)
}
ep <- endpoint(name = c('x', 'y'),
type = c('non-tte', 'non-tte'),
readout = c(x = 0, y = 0),
generator = rng)
## x and y as continuous endpoints, thus mean and sd are reported
ep
## force y to be categorical to create barplot of it
print(ep, categorical_vars = 'y')
clone()
The objects of this class are cloneable with this method.
Endpoints$clone(deep = FALSE)
deepWhether to make a deep clone.
# Instead of using Endpoints$new(), please use endpoint(), a user-friendly # wrapper to define endpoints. See examples in ?endpoint. ## ------------------------------------------------ ## Method `Endpoints$print` ## ------------------------------------------------ rng <- function(n){ data.frame(x = sample(1:3, n, replace = TRUE), y = sample(1:3, n, replace = TRUE) ) } ep <- endpoint(name = c('x', 'y'), type = c('non-tte', 'non-tte'), readout = c(x = 0, y = 0), generator = rng) ## x and y as continuous endpoints, thus mean and sd are reported ep ## force y to be categorical to create barplot of it print(ep, categorical_vars = 'y')# Instead of using Endpoints$new(), please use endpoint(), a user-friendly # wrapper to define endpoints. See examples in ?endpoint. ## ------------------------------------------------ ## Method `Endpoints$print` ## ------------------------------------------------ rng <- function(n){ data.frame(x = sample(1:3, n, replace = TRUE), y = sample(1:3, n, replace = TRUE) ) } ep <- endpoint(name = c('x', 'y'), type = c('non-tte', 'non-tte'), readout = c(x = 0, y = 0), generator = rng) ## x and y as continuous endpoints, thus mean and sd are reported ep ## force y to be categorical to create barplot of it print(ep, categorical_vars = 'y')
Define a condition to trigger trial milestone by the number of randomized patients. The milestone will be triggered when a trial has enrolled at least the specified number of patients. It can be used combined with conditions specified by calendarTime and eventNumber.
Refer to the
vignette
to learn how to define milestones when performing simulation using
TrialSimulator.
enrollment(n, ..., arms = NULL, min_treatment_duration = 0)enrollment(n, ..., arms = NULL, min_treatment_duration = 0)
n |
integer. Number of randomized patients. |
... |
subset conditions compatible with |
arms |
vector of character. Name of arms on which the number of patients
is counted. If |
min_treatment_duration |
numeric. Zero or positive value.
minimum treatment duration of enrolled patients.
Default is 0, i.e., looking for triggering time based on number of enrolled
patients in population specified by |
an object of class 'Condition'
## ensure sufficient sample size of whole trial enrollment(n = 100) ## ensure sufficient sample size in sub-group of interest enrollment(n = 100, biomarker1 == 'positive' & biomarker2 == 'high') ## ensure sufficient sample size in high dose + placebo enrollment(n = 1000, arms = c('high dose', 'placebo')) ## ensure sufficient treatment duration enrollment(n = 500, min_treatment_duration = 2)## ensure sufficient sample size of whole trial enrollment(n = 100) ## ensure sufficient sample size in sub-group of interest enrollment(n = 100, biomarker1 == 'positive' & biomarker2 == 'high') ## ensure sufficient sample size in high dose + placebo enrollment(n = 1000, arms = c('high dose', 'placebo')) ## ensure sufficient treatment duration enrollment(n = 500, min_treatment_duration = 2)
Define a condition to trigger trial milestone by the number of events of a time-to-event endpoint or the number of non-missing observations of a non-time-to-event endpoint. The milestone will be triggered when a trial has observed at least the specified number of endpoint events (or non-missing observations). It can be used combined with conditions specified by calendarTime and enrollment.
Number of events for a time-to-event endpoint can vary at different milestones as more patients are randomized into a trial, or more events onset over time.
Number of non-missing observations for a non-time-to-event endpoint can vary
at different milestones as more patients are randomized into a trial, or more
patients have been treated until their readout time (thus, NA turns
to a value).
Both numbers are affected by dropout.
Refer to the
vignette
to learn how to define milestones when performing simulation using
TrialSimulator.
eventNumber(endpoint, n, ..., arms = NULL)eventNumber(endpoint, n, ..., arms = NULL)
endpoint |
character. Name of an endpoint. It should be something that
is specified in the argument |
n |
integer. Targeted number of events or non-missing obervations, depending on the type of endpoint. |
... |
subset conditions compatible with |
arms |
vector of character. Name of arms on which the number of
events/observations is counted. If |
an object of class 'Condition'
Expands the compact regimen_trajectory column in locked data
(returned by trial$get_locked_data()) into a long-format data frame
with one row per regimen segment per patient.
The regimen_trajectory column stores each patient's treatment history
as a semicolon-separated string of "name\@time" entries, e.g.
"placebo\@0;low dose\@8.5". expandRegimen parses this into
two additional columns:
regimen — name of the treatment regimen
switch_time_from_enrollment — time from enrollment at which
the patient switched to this regimen
The regimen_trajectory column is dropped from the result.
expandRegimen(data)expandRegimen(data)
data |
a data frame returned by |
a long-format data frame: one row per regimen segment per patient,
with regimen and switch_time_from_enrollment appended and
regimen_trajectory removed.
## Not run: locked <- trial$get_locked_data('final') long <- expandRegimen(locked) ## End(Not run)## Not run: locked <- trial$get_locked_data('final') long <- expandRegimen(locked) ## End(Not run)
Fit Cox proportional hazards model on an time-to-event endpoint.
Refer to this vignette for more information and examples.
fitCoxph(formula, placebo, data, alternative, scale, ..., tidy = TRUE)fitCoxph(formula, placebo, data, alternative, scale, ..., tidy = TRUE)
formula |
An object of class |
placebo |
Character. String indicating the placebo in |
data |
Data frame. Usually it is a data snapshot locked at a milestone. |
alternative |
a character string specifying the alternative hypothesis,
must be one of |
scale |
character. The type of estimate in the output. Must be one
of |
... |
(optional) subset conditions compatible with |
tidy |
logical. |
a data frame with columns:
armname of the treatment arm.
placeboname of the placebo arm.
estimateestimate of main effect of arm, depending on scale.
pone-sided p-value for log hazard ratio (treated vs placebo).
infothe number of events of the endpoint in the subset.
zthe z statistics of log hazard ratios.
Test rate difference by comparing it to a pre-specified value using the Farrington-Manning test.
Refer to this vignette for more information and examples.
fitFarringtonManning(endpoint, placebo, data, alternative, ..., delta = 0)fitFarringtonManning(endpoint, placebo, data, alternative, ..., delta = 0)
endpoint |
Character. Name of the endpoint in |
placebo |
Character. String indicating the placebo in |
data |
Data frame. Usually it is a locked data set. |
alternative |
a character string specifying the alternative hypothesis,
must be one of |
... |
Subset conditions compatible with |
delta |
the rate difference between a treatment arm and placebo under the null. 0 by default. |
a data frame with columns:
armname of the treatment arm.
placeboname of the placebo arm.
estimateestimate of rate difference.
pone-sided p-value for log odds ratio (treated vs placebo).
infosample size in the subset with NA being removed.
zthe z statistics of log odds ratio (treated vs placebo).
Farrington, Conor P., and Godfrey Manning. "Test statistics and sample size formulae for comparative binomial trials with null hypothesis of non-zero risk difference or non-unity relative risk." Statistics in medicine 9.12 (1990): 1447-1454.
Fit linear regression model on a continuous endpoint.
Refer to this vignette for more information and examples.
fitLinear(formula, placebo, data, alternative, ...)fitLinear(formula, placebo, data, alternative, ...)
formula |
an object of class |
placebo |
Character. String indicating the placebo arm in |
data |
Data frame. Usually it is a locked data set. |
alternative |
a character string specifying the alternative hypothesis,
must be one of |
... |
Subset conditions compatible with |
a data frame with columns:
armname of the treatment arm.
placeboname of the placebo arm.
estimateestimate of average treatment effect of arm.
pone-sided p-value for between-arm difference (treated vs placebo).
infosample size used in model with NA being removed.
zz statistics of between-arm difference (treated vs placebo).
Fit logistic regression model on an binary endpoint.
Refer to this vignette for more information and examples.
fitLogistic(formula, placebo, data, alternative, scale, ...)fitLogistic(formula, placebo, data, alternative, scale, ...)
formula |
An object of class |
placebo |
Character. String indicating the placebo in |
data |
Data frame. Usually it is a locked data set. |
alternative |
a character string specifying the alternative hypothesis,
must be one of |
scale |
character. The type of estimate in the output. Must be one
of |
... |
Subset conditions compatible with |
a data frame with columns:
armname of the treatment arm.
placeboname of the placebo arm.
estimateestimate depending on scale.
pone-sided p-value for log odds ratio (treated vs placebo).
infosample size used in model with NA being removed.
zz statistics of log odds ratio (treated vs placebo).
Compute log rank test statistic on an endpoint.
Refer to this vignette for more information and examples.
fitLogrank(formula, placebo, data, alternative, ..., tidy = TRUE)fitLogrank(formula, placebo, data, alternative, ..., tidy = TRUE)
formula |
An object of class |
placebo |
character. String of placebo in |
data |
data frame. Usually it is a locked data. |
alternative |
a character string specifying the alternative hypothesis,
must be one of |
... |
subset condition that is compatible with |
tidy |
logical. |
a data frame with columns:
armname of the treatment arm.
placeboname of the placebo arm.
pone-sided p-value for log-rank test (treated vs placebo).
infothe number of events of the endpoint in the subset.
zthe z statistic of the log rank test, with the sign of the log hazard ratio of treatment vs placebo.
If the statistic is undefined because its variance is zero (e.g., no
informative event comparison in the subset), a simulation placeholder
z = 0 with the corresponding p = 0.5 is returned with a
warning rather than an error, so that a few such replicates in a large
simulation do not require error handling in action functions. The
placeholder carries no evidence either way and is not a standardized
normal statistic.
Internal function that retrieves precomputed simulation results. Not meant for use by package users.
getAdaptiveDesignOutput()getAdaptiveDesignOutput()
A data frame containing simulation results of 1000 replicates.
Internal function that retrieves precomputed simulation results. Not meant for use by package users.
getConditionalPowerOutput()getConditionalPowerOutput()
A data frame containing simulation results of 1000 replicates.
Internal function that retrieves precomputed simulation results. Not meant for use by package users.
getDoseRangingOutput()getDoseRangingOutput()
A data frame containing simulation results of 10 replicates.
Internal function that retrieves precomputed simulation results. Not meant for use by package users.
getEnrichmentDesignOutput()getEnrichmentDesignOutput()
A data frame containing simulation results of 10000 replicates
under the alternative and 10000 replicates under the global null,
distinguished by the column scenario.
Internal function that retrieves precomputed simulation results. Not meant for use by package users.
getFixedDesignOutput()getFixedDesignOutput()
A data frame containing simulation results of 1000 replicates.
Perform graphical testing under group sequential design for one or multiple endpoints. See Maurer & Bretz (2013).
new()
Initialize an object for graphical testing procedure. Group sequential design is also supported.
GraphicalTesting$new( alpha, transition, alpha_spending, planned_max_info, hypotheses = NULL, silent = FALSE )
alphainitial alpha allocated to each of the hypotheses.
transitionmatrix of transition weights. Its diagonals should be all 0s. The row sums should be 1s (for better power) or 0s (if no outbound edge from a node).
alpha_spendingcharacter vector of same length of alpha.
Currently it supports 'asP', 'asOF', and 'asUser'.
planned_max_infovector of integers. Maximum numbers of events (tte endpoints) or patients (non-tte endpoints) at the final analysis of each hypothesis when planning a trial. The actual numbers could be different, which can be specified elsewhere.
hypothesesvector of characters. Names of hypotheses.
silentTRUE if muting all messages and not generating
plots.
reset()
reset an object of class GraphicalTesting to original status
so that it can be reused.
GraphicalTesting$reset()
is_valid_hid()
determine if index of a hypothesis is valid
GraphicalTesting$is_valid_hid(hid)
hidan integer
get_hypothesis_name()
get name of a hypothesis given its index.
GraphicalTesting$get_hypothesis_name(hid)
hidan integer
get_weight()
return weight between two nodes
GraphicalTesting$get_weight(hid1, hid2)
hid1an integer
hid2an integer
set_weight()
update weight between two nodes
GraphicalTesting$set_weight(hid1, hid2, value)
hid1an integer
hid2an integer
valuenumeric value to be set as a weight two nodes
get_alpha()
return alpha allocated to a hypothesis when calling this function.
Note that a function can be called several time with the graph is
updated dynamically. Thus, returned alpha can be different even for
the same hid.
GraphicalTesting$get_alpha(hid)
hidan integer
set_alpha()
update alpha of a hypothesis
GraphicalTesting$set_alpha(hid, value)
hidinteger. Index of a hypothesis
valuenumeric value to be allocated
get_hypotheses_ids()
return all valid hid
GraphicalTesting$get_hypotheses_ids()
get_number_hypotheses()
return number of hypotheses, including those been rejected.
GraphicalTesting$get_number_hypotheses()
get_hids_not_in_graph()
return index of hypotheses that are rejected.
GraphicalTesting$get_hids_not_in_graph()
get_testable_hypotheses()
return index of hypotheses with non-zero alphas, thus can be tested at the current stage.
GraphicalTesting$get_testable_hypotheses()
has_testable_hypotheses()
determine whether at least one hypothesis is testable.
If return FALSE, the testing procedure is completed.
GraphicalTesting$has_testable_hypotheses()
is_in_graph()
determine whether a hypothesis is not yet rejected (in graph).
GraphicalTesting$is_in_graph(hid)
hidinteger. Index of a hypothesis
is_testable()
determine whether a hypothesis has a non-zero alpha allocated.
GraphicalTesting$is_testable(hid)
hidinteger. Index of a hypothesis
get_hid()
convert hypothesis's name into (unique) index.
GraphicalTesting$get_hid(hypothesis)
hypothesischaracter. Name of a hypothesis. It is different from
hid, which is an index.
reject_a_hypothesis()
remove a node from graph when a hypothesis is rejected
GraphicalTesting$reject_a_hypothesis(hypothesis)
hypothesisname of a hypothesis. It is different from
hid, which is an index.
set_trajectory()
save new testing results at current stage
GraphicalTesting$set_trajectory(result)
resulta data frame of specific columns.
get_trajectory()
return testing results by the time this function is called. Note that graphical test is carried out in a sequential manner. Users may want to review the results anytime. Value returned by this function can possibly vary over time.
GraphicalTesting$get_trajectory()
test_hypotheses()
test hypotheses using p-values (and other information in stats)
base on the current graph. All rows should have the same order
number.
GraphicalTesting$test_hypotheses(stats)
statsa data frame. It must contain the following columns:
orderinteger. P-values (among others) of hypotheses that
can be tested at the same time (e.g., an interim, or final analysis)
should be labeled with the same order number.
If a hypothesis is not tested at a stage,
simply don't put it in stats with that order number.
hypothesescharacter. Name of hypotheses to be tested. They
should be identical to those when calling GraphicalTesting$new.
pnominal p-values.
infoobserved number of events or samples at test. These will be used to compute information fractions in group sequential design.
max_infointegers. Maximum information at test. At interim,
max_info should be equal to planned_max_info when
calling GraphicalTesting$new. At the final stage of a
hypothesis, one can update it with observed numbers.
test()
test hypotheses using p-values (and other information in stats)
base on the current graph. Users can call this function multiple times.
P-values of the same order should be passed through stats
together. P-values of multiple orders can be passed together as well.
For example, if users only have p-values at current stage, they can call
this function and update the graph accordingly. In this case, column
order in stats is a constant. They can call this
function again when p-values of next stage is available, where
order is another integer. In simulation, if p-values of all
stages are on hand, users can call this function to
test them all in a single pass. In this case, column order in
stats can have different values.
GraphicalTesting$test(stats)
statsa data frame. It must contain the following columns:
orderinteger. P-values (among others) of hypotheses that
can be tested at the same time (e.g., an interim, or final analysis)
should be labeled with the same order number.
If a hypothesis is not tested at a stage,
simply don't put it in stats with that order number.
If all p-values in stats are tested at the same stage, order
can be absent.
hypothesescharacter. Name of hypotheses to be tested. They
should be identical to those when calling
GraphicalTesting$new.
pnominal p-values.
infoobserved number of events or samples at test. These will be used to compute information fractions in group sequential design.
max_infointegers. Maximum information at test. At interim,
max_info should be equal to planned_max_info when
calling GraphicalTesting$new. At the final stage of a
hypothesis, one can update it with observed numbers.
alpha_spentaccumulative proportion of allocated alpha
to be spent if alpha_spending = "asUser". Set it to
NA_real_ otherwise. If no hypothesis uses "asUser" in
stats, this column could be ignored.
a data frame returned by get_current_testing_results.
It contains details of each of the testing steps.
get_current_testing_results()
return testing results with details by the time this function
is called. This function can be called by users by multiple
times, thus the returned value varies over time.
This function is called by GraphicalTesting::test,
and returns a data frame consisting of columns
hypothesisname of hypotheses.
obs_p_valueobserved p-values.
max_allocated_alphamaximum allocated alpha for the hypothesis.
decision'reject' or 'accept' the hypotheses.
stagesstage of a hypothesis.
orderorder number that this hypothesis is tested for the last time.
It is different from stages.
typeOfDesignname of alpha spending functions.
GraphicalTesting$get_current_testing_results()
get_current_decision()
get current decisions for all hypotheses. Returned value could changes over time because it depends on the stages being tested already.
GraphicalTesting$get_current_decision()
a named vector of values "accept" or "reject".
Note that if a hypothesis is not yet tested when calling this function,
the decision for that hypothesis would be "accept".
print()
generic function for print
GraphicalTesting$print(graph = TRUE, trajectory = TRUE, ...)
graphlogic. TRUE if visualizing the current graph,
which can vary over time.
trajectorylogic. TRUE if print the current data frame of
trajectory, which can vary over time.
...other arguments supported in gMCPLite::hGraph,
e.g., trhw and trhh to control the size of transition box,
and trdigits to control the digits displayed for transition
weights.
clone()
The objects of this class are cloneable with this method.
GraphicalTesting$clone(deep = FALSE)
deepWhether to make a deep clone.
## Example 1 ## dry-run to study the behavior of a graph ## without group sequential design if(interactive()){ eps <- .01 alpha <- c(.01, .04, 0, 0, 0) transition <- matrix(c( 0, 0, 0, 0, 1, 0, 0, .75, 0, .25, 0, 1/2-eps/2, 0, eps, 1/2-eps/2, 0, 0, 0, 0, 0, 0, 1/2, 1/2, 0, 0 ), nrow = 5, byrow = TRUE) ## dummy can be anything, we don't actually use it asf <- rep('asOF', 5) ## dummy can be anything, we don't actually use it max_info <- c(300, 1100, 1100, 1100, 500) hs <- c('H1: UPCR IgA', 'H2: eGFR GN', 'H3: eGFR GN 10wk', 'H5: 2nd Endpoints', 'H4: eGFR IgA') ## initialize an object gt <- GraphicalTesting$new(alpha, transition, asf, max_info, hs) print(gt) ## reject hypotheses based on customized order ## to understand the behavior of a testing strategy ## Any other rejection order is possible gt$reject_a_hypothesis('H1: UPCR IgA') print(gt) gt$reject_a_hypothesis('H2: eGFR GN') print(gt) gt$reject_a_hypothesis('H4: eGFR IgA') print(gt) gt$reject_a_hypothesis('H3: eGFR GN 10wk') print(gt) gt$reset() } ## Example 2 ## Example modified from vignettes in gMCPLite: ## Graphical testing for group sequential design if(interactive()){ ## initial alpha split to each of the hypotheses alpha <- c(.01, .01, .004, .0, .0005, .0005) ## transition matrix of the initial graph transition <- matrix(c( 0, 1, 0, 0, 0, 0, 0, 0, .5, .5, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, .5, .5, 0, 0, 0, 0, 0, 1, .5, .5, 0, 0, 0, 0 ), nrow = 6, byrow = TRUE) ## alpha spending functions per hypothesis asf <- c('asUser', 'asOF', 'asUser', 'asOF', 'asOF', 'asOF') ## planned maximum number of events per hypothesis max_info <- c(295, 800, 310, 750, 500, 1100) ## name of hypotheses hs <- c('H1: OS sub', 'H2: OS all', 'H3: PFS sub', 'H4: PFS all', 'H5: ORR sub', 'H6: ORR all') gt <- GraphicalTesting$new(alpha, transition, asf, max_info, hs) ## print initial graph gt ## nominal p-values at each stage ## Note: p-values with same order are calculated with the same locked data ## Note: alpha_spent is only specified for hypotheses using custom alpha ## spending function "asUser" stats <- data.frame( order = c(1:3, 1:3, 1:2, 1:2, 1, 1), hypotheses = c(rep('H1: OS sub', 3), rep('H2: OS all', 3), rep('H3: PFS sub', 2), rep('H4: PFS all', 2), 'H5: ORR sub', 'H6: ORR all'), p = c(.03, .0001, .000001, .2, .15, .1, .2, .001, .3, .2, .00001, .1), info = c(185, 245, 295, 529, 700, 800, 265, 310, 675, 750, 490, 990), is_final = c(F, F, T, F, F, T, F, T, F, T, T, T), max_info = c(rep(295, 3), rep(800, 3), rep(310, 2), rep(750, 2), 490, 990), alpha_spent = c(c(.1, .4, 1), rep(NA, 3), c(.3, 1), rep(NA, 2), NA, NA) ) ## test the p-values from the first analysis, plot the updated graph gt$test(stats %>% dplyr::filter(order==1)) ## test the p-values from the second analysis, plot the updated graph gt$test(stats %>% dplyr::filter(order==2)) ## test the p-values from the third analysis, plot the updated graph ## because no futher test would be done, displayed results are final gt$test(stats %>% dplyr::filter(order==3)) ## plot the final status of the graph print(gt, trajectory = FALSE) ## you can get final testing results as follow gt$get_current_testing_results() ## if you want to see step-by-step details print(gt$get_trajectory()) ## equivalently, you can call gt$test(stats) for only once to get same results. gt$reset() gt$test(stats) ## if you only want to get the final testing results gt$get_current_decision() }## Example 1 ## dry-run to study the behavior of a graph ## without group sequential design if(interactive()){ eps <- .01 alpha <- c(.01, .04, 0, 0, 0) transition <- matrix(c( 0, 0, 0, 0, 1, 0, 0, .75, 0, .25, 0, 1/2-eps/2, 0, eps, 1/2-eps/2, 0, 0, 0, 0, 0, 0, 1/2, 1/2, 0, 0 ), nrow = 5, byrow = TRUE) ## dummy can be anything, we don't actually use it asf <- rep('asOF', 5) ## dummy can be anything, we don't actually use it max_info <- c(300, 1100, 1100, 1100, 500) hs <- c('H1: UPCR IgA', 'H2: eGFR GN', 'H3: eGFR GN 10wk', 'H5: 2nd Endpoints', 'H4: eGFR IgA') ## initialize an object gt <- GraphicalTesting$new(alpha, transition, asf, max_info, hs) print(gt) ## reject hypotheses based on customized order ## to understand the behavior of a testing strategy ## Any other rejection order is possible gt$reject_a_hypothesis('H1: UPCR IgA') print(gt) gt$reject_a_hypothesis('H2: eGFR GN') print(gt) gt$reject_a_hypothesis('H4: eGFR IgA') print(gt) gt$reject_a_hypothesis('H3: eGFR GN 10wk') print(gt) gt$reset() } ## Example 2 ## Example modified from vignettes in gMCPLite: ## Graphical testing for group sequential design if(interactive()){ ## initial alpha split to each of the hypotheses alpha <- c(.01, .01, .004, .0, .0005, .0005) ## transition matrix of the initial graph transition <- matrix(c( 0, 1, 0, 0, 0, 0, 0, 0, .5, .5, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, .5, .5, 0, 0, 0, 0, 0, 1, .5, .5, 0, 0, 0, 0 ), nrow = 6, byrow = TRUE) ## alpha spending functions per hypothesis asf <- c('asUser', 'asOF', 'asUser', 'asOF', 'asOF', 'asOF') ## planned maximum number of events per hypothesis max_info <- c(295, 800, 310, 750, 500, 1100) ## name of hypotheses hs <- c('H1: OS sub', 'H2: OS all', 'H3: PFS sub', 'H4: PFS all', 'H5: ORR sub', 'H6: ORR all') gt <- GraphicalTesting$new(alpha, transition, asf, max_info, hs) ## print initial graph gt ## nominal p-values at each stage ## Note: p-values with same order are calculated with the same locked data ## Note: alpha_spent is only specified for hypotheses using custom alpha ## spending function "asUser" stats <- data.frame( order = c(1:3, 1:3, 1:2, 1:2, 1, 1), hypotheses = c(rep('H1: OS sub', 3), rep('H2: OS all', 3), rep('H3: PFS sub', 2), rep('H4: PFS all', 2), 'H5: ORR sub', 'H6: ORR all'), p = c(.03, .0001, .000001, .2, .15, .1, .2, .001, .3, .2, .00001, .1), info = c(185, 245, 295, 529, 700, 800, 265, 310, 675, 750, 490, 990), is_final = c(F, F, T, F, F, T, F, T, F, T, T, T), max_info = c(rep(295, 3), rep(800, 3), rep(310, 2), rep(750, 2), 490, 990), alpha_spent = c(c(.1, .4, 1), rep(NA, 3), c(.3, 1), rep(NA, 2), NA, NA) ) ## test the p-values from the first analysis, plot the updated graph gt$test(stats %>% dplyr::filter(order==1)) ## test the p-values from the second analysis, plot the updated graph gt$test(stats %>% dplyr::filter(order==2)) ## test the p-values from the third analysis, plot the updated graph ## because no futher test would be done, displayed results are final gt$test(stats %>% dplyr::filter(order==3)) ## plot the final status of the graph print(gt, trajectory = FALSE) ## you can get final testing results as follow gt$get_current_testing_results() ## if you want to see step-by-step details print(gt$get_trajectory()) ## equivalently, you can call gt$test(stats) for only once to get same results. gt$reset() gt$test(stats) ## if you only want to get the final testing results gt$get_current_decision() }
Perform group sequential test for a single endpoint based on sequential one-sided p-values at each stages. Selected alpha spending functions, including user-defined functions, are supported. Boundaries are calculated with 'rpact'. At the final analysis, adjustment can be applied for over-running or under-running trial where observed final information is greater or lower than the planned maximum information. See Wassmer & Brannath, 2016, p78f. The test is based on p-values not z statistics because it is easier to not handling direction of alternative hypothesis in current implementation. In addition, only one-sided test is supported which should be sufficient for common use in clinical design.
an R6Class generator object; create a test with GroupSequentialTest$new().
new()
initialize a group sequential test. Now only support one-sided test based on p-values.
GroupSequentialTest$new(
alpha = 0.025,
alpha_spending = c("asP", "asOF", "asUser"),
planned_max_info,
name = "H0",
silent = TRUE
)alphafamilywise error rate
alpha_spendingalpha spending function. Use "asUser"
if custom alpha spending schedule is used.
planned_max_infointeger. Planned maximum number of patients for non-tte endpoints or number of events for tte endpoints
namecharacter. Name of the hypothesis, e.g. endpoint, subgroup, etc. Optional.
silentTRUE if muting all messages.
get_name()
get name of hypothesis
GroupSequentialTest$get_name()
get_alpha()
get overall alpha
GroupSequentialTest$get_alpha()
set_alpha_spending()
set alpha spending function. This is useful when set 'asUser' at the final stage to adjust for an under- or over-running trial.
GroupSequentialTest$set_alpha_spending(asf)
asfcharacter of alpha spending function.
get_alpha_spending()
return character of alpha spending function
GroupSequentialTest$get_alpha_spending()
get_max_info()
return planned maximum information
GroupSequentialTest$get_max_info()
set_max_info()
set planned maximum information. This is used at the final stage to adjust for an under- or over-running trial.
GroupSequentialTest$set_max_info(obs_max_info)
obs_max_infointeger. Maximum information, which could be observed number of patients or events at the final stage.
get_stage()
get current stage.
GroupSequentialTest$get_stage()
reset()
an object of class GroupSequentialTest is designed to be used
sequentially by calling GroupSequentialTest$test. When all
planned tests are performed, no further analysis could be done. In that
case keep calling GroupSequentialTest$test will trigger an error.
To reuse the object for a new set of staged p-values, call this function
to reset the status to stage 1. See examples. This implementation can
prevent the error that more than the planned number of stages are tested.
GroupSequentialTest$reset()
set_trajectory()
save testing result at current stage
GroupSequentialTest$set_trajectory(result, is_final = FALSE)
resulta data frame storing testing result at a stage.
is_finallogical. TRUE if final test for the hypothesis,
FALSE otherwise.
get_trajectory()
return testing trajectory until current stage. This function can be called at any stage. See examples.
GroupSequentialTest$get_trajectory()
get_stage_level()
compute boundaries given current (potentially updated) settings. It returns different values if settings are changed over time.
GroupSequentialTest$get_stage_level()
test_one()
test a hypothesis with the given p-value at current stage
GroupSequentialTest$test_one( p_value, is_final, observed_info, alpha_spent = NA_real_ )
p_valuenumeric. A p-value.
is_finallogical. TRUE if this test is carried out for
the final analysis.
observed_infointeger. Observed information at current stage. It
can be the number of samples (non-tte) or number of events (tte) at test.
If the current stage is final, observed_info will be used to update
planned_max_info, the alpha spending function (typeOfDesign
in rpact) will be updated to 'asUser', and the argument
userAlphaSpending will be used when calling
rpact::getDesignGroupSequential.
alpha_spentnumeric if alpha_spending = "asUser". It must
be between 0 and alpha, the overall alpha of the test.
NA_real_ for other alpha spending functions "asOF" and
"asP".
test()
Carry out test based on group sequential design. If p_values
is NULL, dummy values will be use and boundaries are calculated
for users to review.
GroupSequentialTest$test( observed_info, is_final, p_values = NULL, alpha_spent = NULL )
observed_infoa vector of integers, observed information at stages.
is_finallogical vector. TRUE if the test is for the final
analysis.
p_valuesa vector of p-values. If specified, its length should
equal to the length of observed_info.
alpha_spentaccumulative alpha spent at observed information.
It is a numeric vector of values between 0 and 1, and of length that
equals length(observed_info) if alpha-spending
function is "asUser". Otherwise NULL.
print()
generic function for print
GroupSequentialTest$print()
clone()
The objects of this class are cloneable with this method.
GroupSequentialTest$clone(deep = FALSE)
deepWhether to make a deep clone.
## Note: examples showed here replicate the results from ## https://www.rpact.org/vignettes/planning/rpact_boundary_update_example/ ## Example 1. Generate boundaries for a pre-fix group sequential design gst <- GroupSequentialTest$new( alpha = .025, alpha_spending = 'asOF', planned_max_info = 387) ## without giving p-values, boundaries are returned without actual testing gst$test(observed_info = c(205, 285, 393), is_final = c(FALSE, FALSE, TRUE)) gst ## Example 2. Calculate boundaries with observed information at stages ## No p-values are provided ## get an error without resetting an used object try( gst$test(observed_info = 500, is_final = FALSE) ) ## reset the object for re-use gst$reset() gst$test(observed_info = c(205, 285, 393), is_final = c(FALSE, FALSE, TRUE)) gst ## Example 3. Test stagewise p-values sequentially gst$reset() gst$test(observed_info = 205, is_final = FALSE, p_values = .09) gst$test(285, FALSE, .006) ## print testing trajectory by now gst gst$test(393, TRUE, .002) ## print all testing trajectory gst ## you can also test all stages at once ## the result is the same as calling test() for each of the stages gst$reset() gst$test(c(205, 285, 393), c(FALSE, FALSE, TRUE), c(.09, .006, .002)) gst ## Example 4. use user-define alpha spending gst <- GroupSequentialTest$new( alpha = .025, alpha_spending = 'asUser', planned_max_info = 387) gst$test( observed_info = c(205, 285, 393), is_final = c(FALSE, FALSE, TRUE), alpha_spent = c(.005, .0125, .025)) gst## Note: examples showed here replicate the results from ## https://www.rpact.org/vignettes/planning/rpact_boundary_update_example/ ## Example 1. Generate boundaries for a pre-fix group sequential design gst <- GroupSequentialTest$new( alpha = .025, alpha_spending = 'asOF', planned_max_info = 387) ## without giving p-values, boundaries are returned without actual testing gst$test(observed_info = c(205, 285, 393), is_final = c(FALSE, FALSE, TRUE)) gst ## Example 2. Calculate boundaries with observed information at stages ## No p-values are provided ## get an error without resetting an used object try( gst$test(observed_info = 500, is_final = FALSE) ) ## reset the object for re-use gst$reset() gst$test(observed_info = c(205, 285, 393), is_final = c(FALSE, FALSE, TRUE)) gst ## Example 3. Test stagewise p-values sequentially gst$reset() gst$test(observed_info = 205, is_final = FALSE, p_values = .09) gst$test(285, FALSE, .006) ## print testing trajectory by now gst gst$test(393, TRUE, .002) ## print all testing trajectory gst ## you can also test all stages at once ## the result is the same as calling test() for each of the stages gst$reset() gst$test(c(205, 285, 393), c(FALSE, FALSE, TRUE), c(.09, .006, .002)) gst ## Example 4. use user-define alpha spending gst <- GroupSequentialTest$new( alpha = .025, alpha_spending = 'asUser', planned_max_info = 387) gst$test( observed_info = c(205, 285, 393), is_final = c(FALSE, FALSE, TRUE), alpha_spent = c(.005, .0125, .025)) gst
Define a listener. This is a user-friendly wrapper for
the class constructor Listener$new(). Users who are not familiar with
the concept of classes may consider using this wrapper directly.
Listener is an important concept of TrialSimulator. Used with a
trial object in a controller, a listener can monitor a running trial to
execute user-defined actions when it determine condition of triggering a
milestone is met. This mechanism allows the package users to focus on
the development of action functions in a simulation.
listener(silent = FALSE)listener(silent = FALSE)
silent |
logical. |
an object of R6 class Listeners, monitoring and triggering milestones of a trial.
listener <- listener()listener <- listener()
Create a class of listener. A listener monitors the trial while checking condition of pre-defined milestones. Actions are triggered and executed automatically.
Public methods in this R6 class are used in developing this package. Thus, we have to export the whole R6 class which exposures all public methods. However, only the public methods in the list below are useful to end users.
$add_milestones() register milestone(s) with the listener.
$get_milestone_names() return names of registered milestones.
Internal machinery. The remaining public methods
($monitor(), $mute() and $reset()) are public only
because they are invoked on a listener object by other components of the
package (controllers), which R6 cannot grant through private members.
Users should not call them directly.
an R6Class generator object; use listener() to create a listener.
new()
initialize a listener
Listeners$new(silent = FALSE)
silentlogical. TRUE to mute messages.
add_milestones()
register milestones with listener. Order in ... matter
as they are scanned and triggered in that order. It is users'
responsibility to use reasonable order when calling this function,
otherwise, the result of Listeners$monitor() can be problematic.
Listeners$add_milestones(...)
...one or more objects returned from milestone().
listener <- listener()
interim <- milestone(name = 'interim',
when = eventNumber('endpoint', n = 100)
)
final <- milestone(name = 'final',
when = calendarTime(time = 24)
)
listener$add_milestones(interim, final)
get_milestone_names()
return names of registered milestones
Listeners$get_milestone_names()
monitor()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
scan, check, and trigger registered milestones.
Milestones are triggered in the order when calling
Listener$add_milestones.
Listeners$monitor(trial)
triala Trial object.
mute()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
mute all messages (not including warnings)
Listeners$mute(silent)
silentlogical.
reset()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
reset all milestones registered to the listener. Usually, this is called before a controller can run additional replicates of simulation.
Listeners$reset()
clone()
The objects of this class are cloneable with this method.
Listeners$clone(deep = FALSE)
deepWhether to make a deep clone.
## ## ------------------------------------------------ ## Method `Listeners$add_milestones` ## ------------------------------------------------ listener <- listener() interim <- milestone(name = 'interim', when = eventNumber('endpoint', n = 100) ) final <- milestone(name = 'final', when = calendarTime(time = 24) ) listener$add_milestones(interim, final)## ## ------------------------------------------------ ## Method `Listeners$add_milestones` ## ------------------------------------------------ listener <- listener() interim <- milestone(name = 'interim', when = eventNumber('endpoint', n = 100) ) final <- milestone(name = 'final', when = calendarTime(time = 24) ) listener$add_milestones(interim, final)
Define a milestone of a trial. This is a user-friendly wrapper for
the class constructor Milestones$new(). Users who are not familiar with
the concept of classes may consider using this wrapper directly.
A milestone means the time point to take an action, e.g., carrying out (futility, interim, final) analysis for adding/removing arms, or stopping a trial early. It can also be any more general time point where trial data is used in decision making or adaptation. For example, one can define a milestone for changing randomization scheme, sample size re-assessment, trial duration extension etc.
Refer to the
vignette
to learn how to define milestones when performing simulation using
TrialSimulator.
milestone(name, when, action = doNothing, ...)milestone(name, when, action = doNothing, ...)
name |
character. Name of milestone. |
when |
condition to check if this milestone should be
triggered. It taks value returned from functions |
action |
function to execute when the milestone triggers.
If no action to be executed but simply need to record triggering time and
number of events/non-missing observations of endpoints at
a milestone, |
... |
(optional) arguments of |
an object of R6 class Milestones, representing a milestone of a trial.
## See vignette('conditionSystem')## See vignette('conditionSystem')
Create a class of milestone. A milestone means the time point to take an action, e.g., carrying out (futility, interim, final) analysis for adding/removing arms, or stopping a trial early. It can also be any more general time point where trial data is used in decision making or adaptation. For example, one can define a milestone for changing randomization scheme, sample size re-assessment, trial duration extension etc.
Public methods in this R6 class are used in developing
this package. Thus, we have to export the whole R6 class which exposures all
public methods. However, none of the public methods on this page is
useful to end users. Instead, refer to the
vignette
to learn how to define milestones when performing simulation using
TrialSimulator.
an R6Class generator object; use milestone() to create a milestone.
new()
initialize milestone
Milestones$new(name, type = name, trigger_condition, action = doNothing, ...)
namecharacter. Name of milestone.
typecharacter vector. Milestone type(s) (futility, interim, final), a milestone can be of multiple types. This is for information purpose so can be any string.
trigger_conditionfunction to check if this milestone should
trigger. See vignette Condition System for Triggering Milestones in a Trial.
actionfunction to execute when the milestone triggers.
...(optional) arguments of action.
get_name()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
return name of milestone
Milestones$get_name()
set_trigger_condition()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
replace the trigger condition of a not-yet-triggered milestone. This
is invoked by the listener when it applies an update requested through
Trials$update_milestone() within an action function. The
as-designed condition, frozen at construction, is restored by
reset() between simulation replicates.
Milestones$set_trigger_condition(trigger_condition)
trigger_conditionan object of class Condition. See
trigger_condition of milestone().
set_action_function()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
replace the action function (and its fixed arguments) of a
not-yet-triggered milestone. This is invoked by the listener when it
applies an update requested through Trials$update_milestone()
within an action function. The action in effect at construction is
restored by reset() between simulation replicates.
Milestones$set_action_function(action, action_args = list())
actionfunction to execute when the milestone triggers. See
action of milestone().
action_argsnamed list. Fixed arguments of action,
corresponding to ... of milestone().
get_trigger_status()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
return trigger status
Milestones$get_trigger_status()
reset()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
reset an milestone so that it can be triggered again. Usually, this is called
before the controller of a trial can run additional replicates
of simulation. If the trigger condition or the action was replaced
in-run through Trials$update_milestone(), the as-designed
version is restored so that every replicate starts from the original
design.
Milestones$reset()
trigger_milestone()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
trigger an milestone (always TRUE) and execute action accordingly. It calls Trial$get_data_lock_time() to lock data based on conditions implemented in Milestones$trigger_condition. If time that meets the condition cannot be found, Trial$get_data_lock_time() will throw an error and stop the program. This means that user needs to adjust their trigger_condition (e.g., target number of events (target_n_events) is impossible to reach).
Milestones$trigger_milestone(trial)
triala Trial object.
mute()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
mute all messages (not including warnings)
Milestones$mute(silent)
silentlogical.
clone()
The objects of this class are cloneable with this method.
Milestones$clone(deep = FALSE)
deepWhether to make a deep clone.
This function can be used as generator to define endpoint. Implementation is based on this algorithm. This distribution can be used to simulate delayed treatment effect.
PiecewiseConstantExponentialRNG(n, risk, endpoint_name)PiecewiseConstantExponentialRNG(n, risk, endpoint_name)
n |
integer. Number of random numbers |
risk |
a data frame of columns
|
endpoint_name |
character. Name of endpoint. This should be the same as
the |
a data frame of n rows and two columns
<endpoint_name>name of endpoint specified by users in
endpoint_name.
event indicator with 0/1 as censoring and
event, respectively. Note that due to the nature of the algorithm to
generate data from this distribution, it is possible to have the endpoint
being censoring at the last end_time unless it is set to Inf.
# example code # In this example, absolute risk in [0, 1) and [26, 52] are 0.0181 and # 0.0027, respectively. risk <- data.frame( end_time = c(1, 4.33, 26.0, 52.0), piecewise_risk = c(1, 1.01, 0.381, 0.150) * exp(-4.01) ) PiecewiseConstantExponentialRNG(10, risk, 'PFS')# example code # In this example, absolute risk in [0, 1) and [26, 52] are 0.0181 and # 0.0027, respectively. risk <- data.frame( end_time = c(1, 4.33, 26.0, 52.0), piecewise_risk = c(1, 1.01, 0.381, 0.150) * exp(-4.01) ) PiecewiseConstantExponentialRNG(10, risk, 'PFS')
Plot Triggering Time of Milestones in Simulated Trials
## S3 method for class 'milestone_time_summary' plot(x, ...)## S3 method for class 'milestone_time_summary' plot(x, ...)
x |
an object returned by |
... |
currently not supported. |
no return value, called to display a boxplot of milestone triggering times.
Plot result of three-state illness-death model
## S3 method for class 'three_state_model' plot(x, ...)## S3 method for class 'three_state_model' plot(x, ...)
x |
an object returned by |
... |
currently not supported. |
no return value, called to print the calibration table and display the corresponding plot.
To generate endpoint that follows a piecewise exponential distribution and
is correlated with other endpoints, the copula method is commonly used. The
quantile function needs to be specified (e.g., in the simdata package).
If a piecewise exponential distributed endpoint is independent to other
endpoints, one can simply use
TrialSimulator::PiecewiseConstantExponentialRNG()
to specify the generator argument in endpoint().
There are many R packages implementing the quantile function of the
piecewise exponential distributed random variable. Why do I implement it
again? The reason is that this function is extremely important for simulating
time-to-event endpoint in clinical trial simulation, thus the speed matters.
qPiecewiseExponential() is implemented purely in R for
code transparency, and is much faster than other packages.
qPiecewiseExponential(p, times, piecewise_risk)qPiecewiseExponential(p, times, piecewise_risk)
p |
numeric. A vector of probabilities. |
times |
numeric. A vector of time points where risk (event
rates) change. 0 shouldn't be in |
piecewise_risk |
numeric. A vector of constant risk (event rates) in
a time window. |
A vector of quantiles.
## This code snippet can take > 10s to execute if(interactive()){ library(TrialSimulator) run <- TRUE if(!requireNamespace("rpact", quietly = TRUE)){ run <- FALSE message("Please install 'rpact' to run this example.") } if(!requireNamespace("PWEXP", quietly = TRUE)){ run <- FALSE message("Please install 'PWEXP' to run this example.") } if(!requireNamespace("PWEALL", quietly = TRUE)){ run <- FALSE message("Please install 'PWEALL' to run this example.") } if(run){ x <- solvePiecewiseConstantExponentialDistribution( surv_prob = c(.9, .75, .64, .42, .28), times = c(.4, 1.2, 4, 5.5, 9) ) p <- stats::runif(1e5) ## fast, and only five lines of R codes message('TrialSimulator::qPiecewiseExponential(): ') system.time( a <- qPiecewiseExponential( p, times = x$end_time, piecewise_risk = c(x$piecewise_risk, .1) ) ) |> print() ## > 10s message('rpact::getPiecewiseExponentialQuantile(): ') system.time( b <- rpact::getPiecewiseExponentialQuantile( p, piecewiseSurvivalTime = c(0, x$end_time), piecewiseLambda=c(x$piecewise_risk, .1) ) ) |> print() ## equally fast, but implemented in Fortran message('PWEALL::qpwe(): ') system.time( c <- PWEALL::qpwe(p, rate = c(x$piecewise_risk, .1), tchange = c(0, x$end_time))$q ) |> print() ## equally fast, long codes in R (maybe more versatile?) message('PWEXP::qpwexp(): ') system.time( d <- PWEXP::qpwexp(p, rate = c(x$piecewise_risk, .1), breakpoint = x$end_time) ) |> print() message('a == b: ') all.equal(a, b) |> print() message('a == c: ') all.equal(a, c) |> print() message('a == d: ') all.equal(a, d) |> print() } }## This code snippet can take > 10s to execute if(interactive()){ library(TrialSimulator) run <- TRUE if(!requireNamespace("rpact", quietly = TRUE)){ run <- FALSE message("Please install 'rpact' to run this example.") } if(!requireNamespace("PWEXP", quietly = TRUE)){ run <- FALSE message("Please install 'PWEXP' to run this example.") } if(!requireNamespace("PWEALL", quietly = TRUE)){ run <- FALSE message("Please install 'PWEALL' to run this example.") } if(run){ x <- solvePiecewiseConstantExponentialDistribution( surv_prob = c(.9, .75, .64, .42, .28), times = c(.4, 1.2, 4, 5.5, 9) ) p <- stats::runif(1e5) ## fast, and only five lines of R codes message('TrialSimulator::qPiecewiseExponential(): ') system.time( a <- qPiecewiseExponential( p, times = x$end_time, piecewise_risk = c(x$piecewise_risk, .1) ) ) |> print() ## > 10s message('rpact::getPiecewiseExponentialQuantile(): ') system.time( b <- rpact::getPiecewiseExponentialQuantile( p, piecewiseSurvivalTime = c(0, x$end_time), piecewiseLambda=c(x$piecewise_risk, .1) ) ) |> print() ## equally fast, but implemented in Fortran message('PWEALL::qpwe(): ') system.time( c <- PWEALL::qpwe(p, rate = c(x$piecewise_risk, .1), tchange = c(0, x$end_time))$q ) |> print() ## equally fast, long codes in R (maybe more versatile?) message('PWEXP::qpwexp(): ') system.time( d <- PWEXP::qpwexp(p, rate = c(x$piecewise_risk, .1), breakpoint = x$end_time) ) |> print() message('a == b: ') all.equal(a, b) |> print() message('a == c: ') all.equal(a, c) |> print() message('a == d: ') all.equal(a, d) |> print() } }
A random number generator returning only a constant. This can be used to
set dropout time. Currently it is the default value of dropout time, with
value = Inf.
This function can also be used as a generator of endpoint() if a
constant endpoint is needed.
rconst(n, value)rconst(n, value)
n |
integer. Number of observations. |
value |
scalar. Value of constant observations. |
a numeric vector of length n with all entries equal to value.
Define a regimen of a trial. This is a user-friendly wrapper for
the class constructor Regimens$new(). Users who are not familiar with
the concept of classes may consider using this wrapper directly.
A regimen defines the rules to select patients who switch treatments, to determine the time of switching, and to update patients' endpoint data.
regimen(what, when, how, ...)regimen(what, when, how, ...)
what |
a function determining whether patients' data would be
updated due to switching treatment. It takes |
when |
a function determining the time at which a patient switches
to another treatment regimen, measured from the time of enrollment.
It takes |
how |
a function updating patients' data after treatment switching.
Only modified columns and |
... |
(optional) named arguments to be passed to one or more of
|
an object of R6 class Regimens, representing a treatment regimen.
Create a class of regimen. A regimen defines the rules to select treatments for patients switch, to determine the time of switching, and to update patients' endpoint data.
Public methods in this R6 class are used in developing
this package. Thus, we have to export the whole R6 class which exposures all
public methods. However, none of the public methods on this page is
useful to end users. Instead, use regimen() to create a regimen and
register it with a trial through trial$add_regimen() or
trial$crossover().
an R6Class generator object; use regimen() to create a regimen.
new()
initialize regimen
Regimens$new(what, when, how, ..., earliest_crossover_calendar_time = 0)
whata function determining whether patients' data would be
updated due to switching treatment. It takes patient_data,
a data frame as argument, and returns a data frame of two columns
patient_id and new_treatment, with one row per switching
patient. The number of rows in the returned data frame may be smaller
than the number of patients in the input data frame; patients that are
left out are simply not switched. Values of new_treatment must
not contain '@' or ';', which are reserved for encoding
regimen_trajectory.
Note that the returned object will be passed into function 'how()', which
is also provide by users. This argument can also be a
list of functions that will be executed sequentially. No default value.
whena function determining the time at which a patient switches
to another treatment regimen, measured from the time of enrollment.
It takes patient_data, a data frame as
argument, and returns a data frame of two columns patient_id and
switch_time (from enroll_time). The number of rows in the
returned data frame must equal the number of rows in patient_data,
i.e., a switching time must be specified for every patient (missing
values are not allowed).
Note that the returned object will be passed into function 'how()', which
is also provided by users. This argument can also be a
list of functions that will be executed sequentially. No default value.
howa function updating patients' data after treatment switching.
Only modified columns and patient_id are returned. For a cell that
should not change, return its original value. Only post-switch
outcomes may be changed:
returning a value that differs from the original for an endpoint whose
readout/event is at or before switch_time raises an error. This
argument can also be a list of functions that will be executed
sequentially. No default value.
...(optional) named arguments routed to one or more of
what, when, and how.
earliest_crossover_calendar_timenumeric. The earliest calendar
time at which the triplet(s) may take effect. 0 (default) is the
classic enrollment-time regimen, applied from the first enrollment. A
positive value marks the triplet(s) as a milestone-triggered crossover
(eligibility filtering, switch-time validation and the post-switch data
mask). This is set internally by trial$crossover(); it is not a
user argument of regimen().
get_number_treatment_allocator()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
return number of treatment allocators for regimen
Regimens$get_number_treatment_allocator()
get_treatment_allocator()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
return user-defined new treatment for a patient
Regimens$get_treatment_allocator(index = NULL)
indexinteger. Index of allocator. Return all allocators if NULL.
get_time_selector()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
return user-defined time selector
Regimens$get_time_selector(index = NULL)
indexinteger. Index of selector. Return all selectors if NULL.
get_data_modifier()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
return user-defined endpoint data modifier
Regimens$get_data_modifier(index = NULL)
indexinteger. Index of selector. Return all modifiers if NULL.
get_treatment_allocator_args()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
return pre-bound arguments for the i-th treatment allocator
Regimens$get_treatment_allocator_args(index)
indexinteger.
get_time_selector_args()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
return pre-bound arguments for the i-th time selector
Regimens$get_time_selector_args(index)
indexinteger.
get_data_modifier_args()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
return pre-bound arguments for the i-th data modifier
Regimens$get_data_modifier_args(index)
indexinteger.
get_earliest_crossover_calendar_time()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
return the earliest crossover calendar time of triplet(s)
Regimens$get_earliest_crossover_calendar_time(index = NULL)
indexinteger. Index of triplet. Return all if NULL.
append_triplet()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
append one more triplet to the regimen. Used by milestone-triggered
crossover to stack a new what/when/how (with its own
earliest_crossover_calendar_time) onto an existing regimen without
overwriting earlier triplets. Triplets are executed in append order.
Regimens$append_triplet( what, when, how, ..., earliest_crossover_calendar_time = 0 )
what, when, howsee regimen().
...(optional) named arguments routed to what, when,
and/or how.
earliest_crossover_calendar_timenumeric. Earliest calendar time for the appended triplet. A positive value marks it as a crossover.
clone()
The objects of this class are cloneable with this method.
Regimens$clone(deep = FALSE)
deepWhether to make a deep clone.
remove arms from a trial. The application of this function includes, but is not limited to, dose selection, enrichment analysis (select sub-population).
Note that this function should only be called within action functions of
milestones. It is users' responsibility to ensure that and
TrialSimulator has no way to track it.
Removing an arm has three effects. No patient is randomized to it
afterwards; unenrolled patients are randomized again among the remaining
arms. It leaves the set of arms in the trial, e.g., arms = NULL in
eventNumber() and enrollment() no longer counts it, and
dunnettTest() stops testing it at later milestones. Its data are
censored at the time of removal, or additional_followup later,
but stay in the trial data, so they are present in the locked data of
later milestones.
additional_followup has no default, so that the decision is
visible in the action function. With 0, follow-up of the patients in a
removed arm stops at the milestone where the arm is removed, e.g., a dose
dropped for futility. A positive value keeps following them for a fixed
time beyond the milestone, and Inf for the rest of the trial,
e.g., to collect overall survival or safety data of a dose that stops
enrolling.
The same value applies to all arms removed in one call; to grant
different times to different arms, call this function once per arm.
Events of a removed arm within its follow-up are counted by a later
milestone only if the arm is listed in arms of its triggering
condition; see the next section.
remove_arms(trial, arms_name, additional_followup)remove_arms(trial, arms_name, additional_followup)
trial |
a trial object returned by |
arms_name |
character vector. Name of arms to be removed. |
additional_followup |
numeric. Extra follow-up time granted to the
patients of the removed arms after the current milestone, shared by all
arms in |
no return value, called for its side effect of updating trial.
Which arms are removed, and how many, are usually decided from the data and thus unknown when milestones are defined. Three patterns cover the triggering conditions of later milestones:
Count on the arms still in the trial: leave arms = NULL in
eventNumber() or enrollment(). It is resolved when the
milestone is evaluated, so any arm removed by then is excluded.
Count on all arms of the design, including removed ones: list every
arm of the design in arms of eventNumber() or
enrollment(). All names are known when the design is
written; a removed arm passes the check because it was once in the trial,
and a warning is raised unless the trial is silent. This is how the total
sample size of a trial is counted after a dose is dropped.
Count on a subset that depends on which arm was removed, e.g., the
control arm and the removed arm only: the subset is unknown when the
later milestone is defined. Call update_milestone() in the same
action function as remove_arms() to replace the triggering
condition of the later milestone, with arms built from the arm
just removed.
See the examples.
This is a user-friendly wrapper of the member function of trial, i.e.,
Trials$remove_arms(), which is used in vignettes. Users who are not
familiar with the concept of classes may consider using this wrapper
directly.
## Not run: ## Within an action function: drop a dose and stop following its patients ## at this milestone. additional_followup has no default. remove_arms(trial, 'low dose', additional_followup = 0) ## Drop a dose but follow its patients for another 12 months, e.g., for ## overall survival. remove_arms(trial, 'low dose', additional_followup = 12) ## Drop a dose and follow its patients until the end of the trial. remove_arms(trial, 'low dose', additional_followup = Inf) ## Different follow-up for different arms: one call per arm. remove_arms(trial, 'low dose', additional_followup = 12) remove_arms(trial, 'mid dose', additional_followup = Inf) ## A design with placebo and two doses. At dose selection, one dose may be ## dropped; which one, if any, is decided from the data. ## Pattern 1: the final analysis counts events in the arms still in the ## trial. arms = NULL is resolved when `final` is evaluated, so the dropped ## dose is excluded automatically. final <- milestone(name = 'final', when = eventNumber(endpoint = 'os', n = 300), action = action_at_final) # final is then registered with the listener ## Pattern 2: the final analysis counts events in all arms of the design, ## including a dropped dose (events up to its removal). List every arm; ## a dropped arm is accepted, with a warning unless the trial is silent. ## In this example, final analysis is triggered when in total 1000 patients ## are enrolled, including those from the removed arm. final <- milestone(name = 'final', when = enrollment(n = 1000, arms = c('placebo', 'low dose', 'high dose')), action = action_at_final) # final is then registered with the listener ## Pattern 3: the final analysis counts events in placebo and the dropped ## dose only. The pair is unknown when `final` is defined, so `final` is ## registered with a placeholder condition, and the action of dose ## selection replaces it after the removal. The placeholder is never ## evaluated: milestones are triggered in registration order, and the ## update takes effect before `final` is checked. final <- milestone(name = 'final', when = eventNumber(endpoint = 'os', n = 90), # placeholder action = action_at_final) action_at_dose_selection <- function(trial){ locked_data <- trial$get_locked_data('dose selection') dropped <- ... # the dose to drop, selected from locked_data, or NULL if(!is.null(dropped)){ trial$remove_arms(dropped, additional_followup = 0) trial$update_milestone(name = 'final', when = eventNumber(endpoint = 'os', n = 300, arms = c('placebo', dropped))) } } dose_selection <- milestone(name = 'dose selection', when = eventNumber(endpoint = 'pfs', n = 150), action = action_at_dose_selection) # dose_selection and final are then registered with the listener, in this # order, so that the update in dose_selection is in effect before final # is checked listener <- listener() listener$add_milestones(dose_selection, final) ## End(Not run)## Not run: ## Within an action function: drop a dose and stop following its patients ## at this milestone. additional_followup has no default. remove_arms(trial, 'low dose', additional_followup = 0) ## Drop a dose but follow its patients for another 12 months, e.g., for ## overall survival. remove_arms(trial, 'low dose', additional_followup = 12) ## Drop a dose and follow its patients until the end of the trial. remove_arms(trial, 'low dose', additional_followup = Inf) ## Different follow-up for different arms: one call per arm. remove_arms(trial, 'low dose', additional_followup = 12) remove_arms(trial, 'mid dose', additional_followup = Inf) ## A design with placebo and two doses. At dose selection, one dose may be ## dropped; which one, if any, is decided from the data. ## Pattern 1: the final analysis counts events in the arms still in the ## trial. arms = NULL is resolved when `final` is evaluated, so the dropped ## dose is excluded automatically. final <- milestone(name = 'final', when = eventNumber(endpoint = 'os', n = 300), action = action_at_final) # final is then registered with the listener ## Pattern 2: the final analysis counts events in all arms of the design, ## including a dropped dose (events up to its removal). List every arm; ## a dropped arm is accepted, with a warning unless the trial is silent. ## In this example, final analysis is triggered when in total 1000 patients ## are enrolled, including those from the removed arm. final <- milestone(name = 'final', when = enrollment(n = 1000, arms = c('placebo', 'low dose', 'high dose')), action = action_at_final) # final is then registered with the listener ## Pattern 3: the final analysis counts events in placebo and the dropped ## dose only. The pair is unknown when `final` is defined, so `final` is ## registered with a placeholder condition, and the action of dose ## selection replaces it after the removal. The placeholder is never ## evaluated: milestones are triggered in registration order, and the ## update takes effect before `final` is checked. final <- milestone(name = 'final', when = eventNumber(endpoint = 'os', n = 90), # placeholder action = action_at_final) action_at_dose_selection <- function(trial){ locked_data <- trial$get_locked_data('dose selection') dropped <- ... # the dose to drop, selected from locked_data, or NULL if(!is.null(dropped)){ trial$remove_arms(dropped, additional_followup = 0) trial$update_milestone(name = 'final', when = eventNumber(endpoint = 'os', n = 300, arms = c('placebo', dropped))) } } dose_selection <- milestone(name = 'dose selection', when = eventNumber(endpoint = 'pfs', n = 150), action = action_at_dose_selection) # dose_selection and final are then registered with the listener, in this # order, so that the update in dose_selection is in effect before final # is checked listener <- listener() listener$add_milestones(dose_selection, final) ## End(Not run)
resize a trial with a greater sample size. This function is used to update
the maximum sample size adaptively after sample size reassessment. Note that
this function should be called within action functions. It is users'
responsibility to ensure it and TrialSimulator has no way to track this.
This is a user-friendly wrapper of the member function of trial, i.e.,
Trials$resize(), which is used in vignettes. Users who are not
familiar with the concept of classes may consider using this wrapper
directly.
resize(trial, n_patients)resize(trial, n_patients)
trial |
a trial object returned by |
n_patients |
integer. Number of maximum sample size of a trial. |
no return value, called for its side effect of updating trial.
This is a helper function to explore parameters for endpoint generator, likely in an enrichment design.
Assume that the overall population in an arm is a mixture of two exponential
distributions with medians median1 () and
median2 (). Given the proportion of the first component
() and the overall median , we have
This function computes or given and .
These parameters can be used in custom random number generator to define
exponential distributed endpoints.
Note that the math formula above may not be displayed correctly on a
html page. You can read it with better format by running
?solveMixtureExponentialDistribution.
solveMixtureExponentialDistribution( weight1, median1, median2 = NULL, overall_median = NULL )solveMixtureExponentialDistribution( weight1, median1, median2 = NULL, overall_median = NULL )
weight1 |
numeric. The proportion of the first component. |
median1 |
numeric. Median of the first component. |
median2 |
numeric. Median of the second component. If |
overall_median |
numeric. Median of the overall population. If
|
a named vector of median2 or overall_median.
library(dplyr) median2 <- solveMixtureExponentialDistribution( weight1 = .3, median1 = 10, overall_median = 8) median2 n <- 1e6 ifelse( runif(n) < .3, rexp(n, rate=log(2)/10), rexp(n, rate=log(2)/median2)) %>% median() ## should be close to 8 overall_median <- solveMixtureExponentialDistribution( weight1 = .4, median1 = 12, median2 = 4) overall_median ifelse( runif(n) < .4, rexp(n, rate=log(2)/12), rexp(n, rate=log(2)/4)) %>% median() ## should be close to overall_medianlibrary(dplyr) median2 <- solveMixtureExponentialDistribution( weight1 = .3, median1 = 10, overall_median = 8) median2 n <- 1e6 ifelse( runif(n) < .3, rexp(n, rate=log(2)/10), rexp(n, rate=log(2)/median2)) %>% median() ## should be close to 8 overall_median <- solveMixtureExponentialDistribution( weight1 = .4, median1 = 12, median2 = 4) overall_median ifelse( runif(n) < .4, rexp(n, rate=log(2)/12), rexp(n, rate=log(2)/4)) %>% median() ## should be close to overall_median
This function computes the rate parameters lambda in each of the time
windows. lambda are natural parameters of piecewise exponential
distribution, but in practice, users may define the distribution by
specifying the survival probabilities at time points where event rates
change.
This function returns a data frame, which can be used as input of the
argument risk of the data generator PiecewiseConstantExponentialRNG.
solvePiecewiseConstantExponentialDistribution(surv_prob, times)solvePiecewiseConstantExponentialDistribution(surv_prob, times)
surv_prob |
numeric. A vector of survival probabilities at |
times |
numeric. A vector of time points where event rates change.
|
a data frame of two columns
end_timeEnd time for a constant event rate. The start time of the first time window is 0.
A constant event rate in the time window ending with
end_time on the same row.
solvePiecewiseConstantExponentialDistribution( surv_prob = c(.9, .75, .64, .42, .28), times = c(.4, 1.2, 4, 5.5, 9) )solvePiecewiseConstantExponentialDistribution( surv_prob = c(.9, .75, .64, .42, .28), times = c(.4, 1.2, 4, 5.5, 9) )
The illness-death model consists of three states, initial, progression,
and death. It can be used to model the progression-free survival (PFS)
and overall survival (OS) in clinical trial simulation. It models the
correlation between PFS and OS without assumptions on latent status and copula.
Also, it does not assume PFS and OS satisfy the proportional hazard assumption
simultaneously. The three-state illness-death model ensures a nice property that
PFS <= OS with probability one. However, it requires three hazard parameters
under the homogeneous Markov assumption. In practice, hazard parameters are
hard to specify intuitively especially when no trial data is available at
the planning stage.
This function reparametrizes the illness-death model in term of three parameters,
i.e. median of PFS, median of OS, and correlation between PFS and OS. The
output of this function, which consists of the three hazard parameters, can
be used to generate PFS and OS with desired property. It can be used with
the built-in data generator CorrelatedPfsAndOs3() when defining
endpoints in TrialSimulator.
For more information, refer to this vignette.
solveThreeStateModel( median_pfs, median_os, corr, h12 = seq(0.05, 0.2, length.out = 50) )solveThreeStateModel( median_pfs, median_os, corr, h12 = seq(0.05, 0.2, length.out = 50) )
median_pfs |
numeric. Median of PFS. |
median_os |
numeric. Median of OS. |
corr |
numeric vector. Pearson correlation coefficients between PFS and OS. |
h12 |
numeric vector. A set of hazard from progression to
death that may induce the target correlation |
a data frame with columns:
corrtarget Peason's correlation coefficients.
h01hazard from stable to progression.
h02hazard from stable to death.
h12hazard from progression to death.
errorabsolute error between target correlation and correlation
derived from h01, h02, and h12.
dat <- CorrelatedPfsAndOs3(1e6, h01 = .1, h02 = .05, h12 = .12) cor(dat$pfs, dat$os) ## 0.65 median(dat$pfs) ## 4.62 median(dat$os) ## 9.61 ## find h01, h02, h12 that can match to median_pfs, median_os and corr ## should be close to h01 = 0.10, h02 = 0.05, h12 = 0.12 when corr = 0.65 ret <- solveThreeStateModel(median_pfs = 4.6, median_os = 9.6, corr = seq(.5, .7, length.out=5)) retdat <- CorrelatedPfsAndOs3(1e6, h01 = .1, h02 = .05, h12 = .12) cor(dat$pfs, dat$os) ## 0.65 median(dat$pfs) ## 4.62 median(dat$os) ## 9.61 ## find h01, h02, h12 that can match to median_pfs, median_os and corr ## should be close to h01 = 0.10, h02 = 0.05, h12 = 0.12 when corr = 0.65 ret <- solveThreeStateModel(median_pfs = 4.6, median_os = 9.6, corr = seq(.5, .7, length.out=5)) ret
It assumes a uniform enrollment with constant rate in each of the time
windows. This function can be used as the enroller when calling
trial() to define a trial.
StaggeredRecruiter(n, accrual_rate)StaggeredRecruiter(n, accrual_rate)
n |
integer. Number of enrollment times to generate. |
accrual_rate |
a data frame of columns
|
StaggeredRecruiter is the only enroller accepted by trial():
a piecewise constant accrual rate is flexible enough to approximate
realistic recruitment in practice, e.g., site ramp-up, steady accrual,
and temporary pauses.
The returned enrollment times are deterministic, not random. Within a
window of positive rate, patients enroll one by one with spacing
1/piecewise_rate; under a constant rate r, the k-th
patient enrolls exactly at k / r. In particular, the first patient
enrolls at 1/piecewise_rate rather than at time 0, and a
milestone triggered by enrollment(n = n) occurs exactly at the time
the planned cumulative accrual reaches n.
A window with piecewise_rate = 0 models a recruitment pause (a hold
for safety review, a site not yet activated, a seasonal gap, etc.): no
patient is enrolled in that window, and enrollment resumes after its
end_time. Pauses may occur in the first window or span several
consecutive windows; a leading pause defers the first enrollment
accordingly.
A valid accrual_rate must satisfy all of the following:
it is a data frame with columns end_time and
piecewise_rate;
end_time is positive and strictly increasing, and the last
entry is Inf with a positive rate, so that the schedule can supply
any number of patients (TrialSimulator may internally request more
than the planned sample size, e.g., for adaptive resizing via
resize());
rates are non-negative and finite;
a finite window with a positive rate must expect at least one
patient, i.e., window length x piecewise_rate >= 1. A tiny positive
rate meant as a pause is rejected with an error; use
piecewise_rate = 0 for a true pause.
a sorted numeric vector of n enrollment times.
## constant accrual of 25 patients/month: patient k enrolls at k / 25 accrual_rate <- data.frame(end_time = Inf, piecewise_rate = 25) StaggeredRecruiter(30, accrual_rate) ## recruitment pause: 30/mo through month 12, paused during months 12-18, ## then 30/mo again. Monthly counts show months 13-17 are empty and ## enrollment resumes at the end of the pause (month 18). accrual_rate <- data.frame( end_time = c(12, 18, Inf), piecewise_rate = c(30, 0, 30) ) enroll_time <- StaggeredRecruiter(400, accrual_rate) table(ceiling(enroll_time)) ## leading pause (first rate is 0): enrollment opens 3 months after study ## start, e.g., the first site is activated with a delay, then 30/mo accrual_rate <- data.frame( end_time = c(3, Inf), piecewise_rate = c(0, 30) ) StaggeredRecruiter(30, accrual_rate) ## approximate a linear ramp-up by monthly steps: accrual grows by 5/mo ## each month, from 5/mo up to 30/mo, then stays steady at 30/mo accrual_rate <- data.frame( end_time = c(1:6, Inf), piecewise_rate = c(seq(5, 30, by = 5), 30) ) enroll_time <- StaggeredRecruiter(200, accrual_rate) ## monthly enrolled counts show the ramp (5, 10, ..., 30) and the plateau (30) table(ceiling(enroll_time))## constant accrual of 25 patients/month: patient k enrolls at k / 25 accrual_rate <- data.frame(end_time = Inf, piecewise_rate = 25) StaggeredRecruiter(30, accrual_rate) ## recruitment pause: 30/mo through month 12, paused during months 12-18, ## then 30/mo again. Monthly counts show months 13-17 are empty and ## enrollment resumes at the end of the pause (month 18). accrual_rate <- data.frame( end_time = c(12, 18, Inf), piecewise_rate = c(30, 0, 30) ) enroll_time <- StaggeredRecruiter(400, accrual_rate) table(ceiling(enroll_time)) ## leading pause (first rate is 0): enrollment opens 3 months after study ## start, e.g., the first site is activated with a delay, then 30/mo accrual_rate <- data.frame( end_time = c(3, Inf), piecewise_rate = c(0, 30) ) StaggeredRecruiter(30, accrual_rate) ## approximate a linear ramp-up by monthly steps: accrual grows by 5/mo ## each month, from 5/mo up to 30/mo, then stays steady at 30/mo accrual_rate <- data.frame( end_time = c(1:6, Inf), piecewise_rate = c(seq(5, 30, by = 5), 30) ) enroll_time <- StaggeredRecruiter(200, accrual_rate) ## monthly enrolled counts show the ramp (5, 10, ..., 30) and the plateau (30) table(ceiling(enroll_time))
stop follow-up of a subset of enrolled patients at a specified time at or
after the current milestone. Data of affected patients are censored
(time-to-event endpoints) or set to missing (non-time-to-event endpoints
with readout after the stopping time), as if those patients were no longer
followed since then. This function can be used in adaptive designs; its
application includes, but is not limited to, treatment discontinuation,
early termination of follow-up for a sub-population, or enrichment design
where follow-up of a de-selected sub-population is stopped after an
interim analysis. It can also be called at a pre-specified milestone that
splits a trial into cohorts, e.g., a milestone marking the last patient
of the first cohort and the first patient of the second cohort. Such a
milestone is usually event driven, so its time is unknown until the trial
is simulated. Stopping follow-up of the earlier cohort at that milestone,
or after a pre-specified, fixed additional_followup beyond it,
makes statistics computed from the two cohorts independent, which
facilitates tests requiring independence, e.g., combination tests.
Only patients who are enrolled by the time this function is called and
satisfy the subset conditions in ... (if any) are affected.
Patients enrolled afterwards are followed as usual.
Note that this function should only be called within action functions of
milestones. It is users' responsibility to ensure that and
TrialSimulator has no way to track it. Calling it before any
milestone has been triggered is an error.
This is a user-friendly wrapper of the member function of trial, i.e.,
Trials$stop_followup(), which is used in vignettes. Users who are not
familiar with the concept of classes may consider using this wrapper
directly.
stop_followup(trial, ..., additional_followup)stop_followup(trial, ..., additional_followup)
trial |
a trial object returned by |
... |
subset conditions compatible with |
additional_followup |
numeric. Extra follow-up time granted to the selected patients after the current milestone. No default: 0 stops follow-up at the milestone itself. |
no return value, called for its side effect of updating trial.
A minimum alternative to summarytools::dfSummary to avoid package
dependency. This function is used to generate summary reports of endpoints
and arms. No meant to be used by end users. However, users may
find it helpful in their own applications if the interface is okay with them.
summarizeDataFrame( data, exclude_vars = NULL, tte_vars = NULL, event_vars = NULL, categorical_vars = NULL, title = "Summary", sub_title = "" )summarizeDataFrame( data, exclude_vars = NULL, tte_vars = NULL, event_vars = NULL, categorical_vars = NULL, title = "Summary", sub_title = "" )
data |
a data frame. |
exclude_vars |
columns to be excluded from summary. |
tte_vars |
character. Vector of time-to-event variables. |
event_vars |
character. Vector of event indicators. Every time-to-event variable should be corresponding to an event indicator. |
categorical_vars |
character. Vector of categorical variables. This can be used to specify variables with limited distinct values as categorical variables in summary. |
title |
character. Title of the summary report. |
sub_title |
character. Sub-title. |
a data frame of summary
set.seed(123) n <- 1000 data <- data.frame( age = rnorm(n, 65, 10), gender = sample(c('M', 'F', NA), n, replace = TRUE, prob = c(.4, .4, .2)), time_to_death = rexp(n, .01), death = rbinom(n, 1, .6), type = sample(LETTERS[1:8], n, replace = TRUE) ) summarizeDataFrame(data, tte_vars = 'time_to_death', event_vars = 'death')set.seed(123) n <- 1000 data <- data.frame( age = rnorm(n, 65, 10), gender = sample(c('M', 'F', NA), n, replace = TRUE, prob = c(.4, .4, .2)), time_to_death = rexp(n, .01), death = rbinom(n, 1, .6), type = sample(LETTERS[1:8], n, replace = TRUE) ) summarizeDataFrame(data, tte_vars = 'time_to_death', event_vars = 'death')
Summary of Milestone Time from Simulated Trials
summarizeMilestoneTime(output)summarizeMilestoneTime(output)
output |
a data frame.
It assumes that triggering time of milestones
are store in columns |
A data frame of class milestone_time_summary. It comes with a
plot method for visualization.
# a minimum, meaningful, and executable example, # where a randomized trial with two arms is simulated and analyzed. control <- arm(name = 'control arm') active <- arm(name = 'active arm') pfs_in_control <- endpoint(name = 'PFS', type = 'tte', generator = rexp, rate = log(2) / 5) control$add_endpoints(pfs_in_control) pfs_in_active <- endpoint(name = 'PFS', type = 'tte', generator = rexp, rate = log(2) / 6) active$add_endpoints(pfs_in_active) accrual_rate <- data.frame(end_time = c(10, Inf), piecewise_rate = c(30, 50)) trial <- trial(name = 'trial', n_patients = 1000, enroller = StaggeredRecruiter, accrual_rate = accrual_rate, dropout = rweibull, shape = 2, scale = 38, silent = TRUE) trial$add_arms(sample_ratio = c(1, 1), control, active) action_at_final <- function(trial){ locked_data <- trial$get_locked_data('final analysis') fitLogrank(Surv(PFS, PFS_event) ~ arm, placebo = 'control arm', data = locked_data, alternative = 'less') invisible(NULL) } final <- milestone(name = 'final analysis', action = action_at_final, when = eventNumber(endpoint = 'PFS', n = 300)) listener <- listener(silent = TRUE) listener$add_milestones(final) controller <- controller(trial, listener) controller$run(n = 10, plot_event = FALSE, silent = TRUE) output <- controller$get_output() time <- summarizeMilestoneTime(output) time plot(time)# a minimum, meaningful, and executable example, # where a randomized trial with two arms is simulated and analyzed. control <- arm(name = 'control arm') active <- arm(name = 'active arm') pfs_in_control <- endpoint(name = 'PFS', type = 'tte', generator = rexp, rate = log(2) / 5) control$add_endpoints(pfs_in_control) pfs_in_active <- endpoint(name = 'PFS', type = 'tte', generator = rexp, rate = log(2) / 6) active$add_endpoints(pfs_in_active) accrual_rate <- data.frame(end_time = c(10, Inf), piecewise_rate = c(30, 50)) trial <- trial(name = 'trial', n_patients = 1000, enroller = StaggeredRecruiter, accrual_rate = accrual_rate, dropout = rweibull, shape = 2, scale = 38, silent = TRUE) trial$add_arms(sample_ratio = c(1, 1), control, active) action_at_final <- function(trial){ locked_data <- trial$get_locked_data('final analysis') fitLogrank(Surv(PFS, PFS_event) ~ arm, placebo = 'control arm', data = locked_data, alternative = 'less') invisible(NULL) } final <- milestone(name = 'final analysis', action = action_at_final, when = eventNumber(endpoint = 'PFS', n = 300)) listener <- listener(silent = TRUE) listener$add_milestones(final) controller <- controller(trial, listener) controller$run(n = 10, plot_event = FALSE, silent = TRUE) output <- controller$get_output() time <- summarizeMilestoneTime(output) time plot(time)
Define a trial. This is a user-friendly wrapper for
the class constructor Trial$new(). Users who are not familiar with
the concept of classes may consider using this wrapper directly.
Trial's name, planned size, enrollment plan, dropout mechanism and seeding are specified in this function. Note that many of these parameters can be altered adaptively during a trial.
A trial has no pre-specified duration. It ends when its last milestone
(usually the final analysis) is triggered, and the timing of a milestone
is determined by its triggering condition, e.g., a calendar time, a
number of events, or a number of enrolled patients. In particular, the
duration of a trial with event-driven milestones is random and varies
across simulated replicates. It can be read from the milestone time
columns of $get_output().
Note that it is users' responsibility to assure that the units of dropout time, calendar time in milestones' triggering conditions, and readout of non-tte endpoints are consistent.
trial( name, n_patients, description = name, seed = NULL, enroller = StaggeredRecruiter, dropout = NULL, stratification_factors = NULL, global_data = list(), silent = FALSE, ... )trial( name, n_patients, description = name, seed = NULL, enroller = StaggeredRecruiter, dropout = NULL, stratification_factors = NULL, global_data = list(), silent = FALSE, ... )
name |
character. Name of trial. Usually, hmm..., useless. |
n_patients |
integer. Maximum (and initial) number of patients could be enrolled when planning the trial. It can be altered adaptively during a trial. |
description |
character. Optional for description of the trial. By
default it is set to be trial's |
seed |
random seed. If |
enroller |
enrollment-time generator. Must be |
dropout |
a function returning a vector of dropout time for patients.
It can be any random number generator with first argument |
stratification_factors |
character. Names of baseline characteristics
to define stratums in stratified permuted block randomization.
Stratification factors must be defined in |
global_data |
a list with named components, e.g., design parameters
or a template object of a testing procedure, to be shared by all
replicates of a simulation. It is read-only and can be accessed in
action functions by calling |
silent |
logical. |
... |
(optional) arguments of |
an object of R6 class Trials, representing a trial.
risk1 <- data.frame( end_time = c(1, 10, 26.0, 52.0), piecewise_risk = c(1, 1.01, 0.381, 0.150) * exp(-3.01) ) pfs1 <- endpoint(name = 'pfs', type='tte', generator = PiecewiseConstantExponentialRNG, risk = risk1, endpoint_name = 'pfs') orr1 <- endpoint( name = 'orr', type = 'non-tte', readout = c(orr=1), generator = rbinom, size = 1, prob = .4) placebo <- arm(name = 'pbo') placebo$add_endpoints(pfs1, orr1) risk2 <- risk1 risk2$hazard_ratio <- .8 pfs2 <- endpoint(name = 'pfs', type='tte', generator = PiecewiseConstantExponentialRNG, risk = risk2, endpoint_name = 'pfs') orr2 <- endpoint( name = 'orr', type = 'non-tte', generator = rbinom, readout = c(orr=3), size = 1, prob = .6) active <- arm(name = 'ac') active$add_endpoints(pfs2, orr2) ## Plan a trial, Trial-3415, of up to 100 patients. ## Enrollment accrues at a constant 10 patients per time unit. trial <- trial( name = 'Trial-3415', n_patients = 100, seed = 31415926, enroller = StaggeredRecruiter, accrual_rate = data.frame(end_time = Inf, piecewise_rate = 10)) trial trial$add_arms(sample_ratio = c(1, 2), placebo, active) ## updated information after arms are registered trialrisk1 <- data.frame( end_time = c(1, 10, 26.0, 52.0), piecewise_risk = c(1, 1.01, 0.381, 0.150) * exp(-3.01) ) pfs1 <- endpoint(name = 'pfs', type='tte', generator = PiecewiseConstantExponentialRNG, risk = risk1, endpoint_name = 'pfs') orr1 <- endpoint( name = 'orr', type = 'non-tte', readout = c(orr=1), generator = rbinom, size = 1, prob = .4) placebo <- arm(name = 'pbo') placebo$add_endpoints(pfs1, orr1) risk2 <- risk1 risk2$hazard_ratio <- .8 pfs2 <- endpoint(name = 'pfs', type='tte', generator = PiecewiseConstantExponentialRNG, risk = risk2, endpoint_name = 'pfs') orr2 <- endpoint( name = 'orr', type = 'non-tte', generator = rbinom, readout = c(orr=3), size = 1, prob = .6) active <- arm(name = 'ac') active$add_endpoints(pfs2, orr2) ## Plan a trial, Trial-3415, of up to 100 patients. ## Enrollment accrues at a constant 10 patients per time unit. trial <- trial( name = 'Trial-3415', n_patients = 100, seed = 31415926, enroller = StaggeredRecruiter, accrual_rate = data.frame(end_time = Inf, piecewise_rate = 10)) trial trial$add_arms(sample_ratio = c(1, 2), placebo, active) ## updated information after arms are registered trial
Create a class of trial.
Public methods in this R6 class are used in developing this package. Thus, we have to export the whole R6 class which exposures all public methods. However, only the public methods in the sections below are useful to end users, and users are encouraged to restrict themselves to them. The remaining public methods are internal machinery of the package and should not be called directly (see the last section).
Adaptation methods. The following methods adapt an ongoing trial
and should be called within action functions of milestones. Each of them
has a user-friendly wrapper of the same name, e.g., resize(trial, ...)
for trial$resize(...).
$resize() set maximum sample size of a trial. This function can
be used to increase sample size under adaptive designs (e.g., sample size
reassessment).
$remove_arms() drop arms from a trial. This function can be
used in adaptive designs, e.g., dose selection, enrichment design, etc.
$update_sample_ratio() change sample ratio of arm. This function
can be used under adaptive designs, e.g., response-adaptive design, etc.
$update_generator() change endpoint generator of arm. This
function can be used in enrichment design.
$add_arms() add arms to a trial. This function is used to add
arms to a newly defined trial, or add arms under adaptive design, e.g.,
dose-ranging, etc.
$crossover() apply a milestone-triggered crossover to eligible
patients in the trial. Called inside a milestone action; only alters patients'
post-switch endpoint values and leaves already-observed data intact.
$stop_followup() stop follow-up of a subset of enrolled
patients at or after a milestone. Their data are censored (time-to-event
endpoints) or set to missing (non-time-to-event endpoints) accordingly.
This function can be used in adaptive designs, e.g., to simulate
treatment discontinuation, early termination of follow-up for a
sub-population, or enrichment design where follow-up of a de-selected
sub-population is stopped after an interim analysis. It can also stop
follow-up of an earlier cohort at a pre-specified, event-driven milestone
(e.g., the last patient of the first cohort), optionally with a fixed
additional follow-up beyond it, making statistics of the cohorts
independent to facilitate, e.g., combination tests.
$update_accrual_rate() update the accrual rate of the
recruitment curve at a milestone, e.g., to revise or pause recruitment
after dose selection or enrichment. end_time of the new accrual
rate is measured from the milestone; patients not yet enrolled are
re-planned and re-randomized under the new schedule.
$update_milestone() update the trigger condition and/or the
action of a not-yet-triggered milestone. The update takes effect right
after the current action function returns.
Methods callable within action functions. In addition to the adaptation methods above, users can call the following methods within action functions to access and manipulate data, to query the current status of a trial, and to carry out statistical testing.
Data access and manipulation:
$get_locked_data() request for data snapshot at a milestone.
Calling this function is recommended as the first action in any action
function as long as trial data is needed in statistical analysis or decision
making.
$save() save intermediate result for simulation summary.
Results across multiple replicates of simulation are saved, which can be
retrieved by calling get_output() anytime.
$bind() row bind and save intermediate results across
milestones if those results are data frames of similar formats. The life
cycle of the save results is within a single replicate of simulation and
is reset to NULL in next simulated trial. Saved results
can be retrieved by calling get() anytime.
$save_custom_data() save intermediate results of any format.
The life cycle of the saved result is within a single replicate of simulation
and is cleared before the next simulated trial. Saved results can be retrieved
by calling get() anytime.
$get() retrieve intermediate results saved by calling functions
save_custom_data() or bind().
$get_global_data() retrieve read-only data registered through
the global_data argument of trial(), e.g., design
parameters or a template object of a testing procedure, which is
available in every replicate of a simulation. R6 objects are returned as
independent deep clones so the registered template is never changed.
$get_output() retrieve intermediate results saved by calling
function save().
Trial status queries:
$get_current_time() return the triggering time of the
milestone that the calling action function is attached to.
$get_milestone_time() return milestone time when triggering a
given milestone.
$get_sample_ratio() return current sample ratios of arms.
$get_arms_name() return names of the arms in the trial at
the time of calling, i.e., arms that have been added and not yet removed
by $remove_arms(). Note that this can differ from the arms present
in locked data, where data of removed arms remain available (censored at
the time of removal, or later if additional_followup was granted).
Statistical testing:
$dunnettTest() perform Dunnett's test.
$closedTest() perform combination test based on Dunnett's test.
$conditionalPower() compute conditional power at a triggered
interim milestone for each treatment-vs-placebo comparison of a
time-to-event endpoint, under a design with one interim and one final
analysis and a constant allocation ratio.
$eventNumberReestimationFromConditionalPower() re-estimate
the number of events at the final analysis: the smallest whole number of
events at which conditional power reaches a target, given the
observations at a triggered interim milestone, optionally bounded by a
practical cap.
Trial setup.
$add_regimen() register a regimen object to a trial.
Must be called before $add_arms(). Applied at enrollment. Unlike
the adaptation methods above, it belongs to the setup stage of a trial
and must not be called within action functions.
Internal machinery. The remaining public methods
($lock_data(), $get_data_lock_time_by_calendar_time(),
$get_data_lock_time_by_event_number(),
$get_data_lock_time_by_enrollment(), $has_arm(),
$event_plot(), $mute(), $tidy_output(), $reset(),
$make_arms_snapshot(), $has_been_run() and
$pop_milestone_updates()) are
public only because they are invoked on
a trial object by other components of the package (milestones, listeners,
controllers and triggering conditions), which R6 cannot grant through
private members. Users should not call them directly. Note that
$save() and $get_output() are invoked by those components
too, but they are part of the user-facing API above at the same time.
an R6Class generator object; use trial() to create a trial.
new()
initialize a trial
Trials$new( name, n_patients, description = name, seed = NULL, enroller = StaggeredRecruiter, dropout = NULL, stratification_factors = NULL, global_data = list(), silent = FALSE, ... )
namecharacter. Name of trial. Usually, hmm..., useless.
n_patientsinteger. Maximum (and initial) number of patients could be enrolled when planning the trial. It can be altered adaptively during a trial.
descriptioncharacter. Optional for description of the trial. By
default it is set to be trial's name. Usually useless.
seedrandom seed. If NULL, seed is set for each simulated
trial automatically and saved in output. It can be retrieved in the
seed column in $get_output(). Setting it to be NULL
is recommended. For debugging, set it to a specific integer.
enrollerenrollment-time generator. Must be
StaggeredRecruiter (the default); any other value is rejected.
Supply its accrual_rate via .... See
?TrialSimulator::StaggeredRecruiter. Kept (rather than dropped) for
backward compatibility, so existing code that passes
enroller = StaggeredRecruiter explicitly keeps working unchanged.
dropouta function returning a vector of dropout time for patients.
It can be any random number generator with first argument n,
the number of enrolled patients. Usually rexp if dropout rate
is set at a single time point, or rweibull if dropout rates are
set at two time points. See ?TrialSimulator::weibullDropout.
stratification_factorscharacter. Names of baseline characteristics
to define stratums in stratified permuted block randomization.
Stratification factors must be defined in endpoint() with
readout = 0. As a natural assumption for randomized trial,
TrialSimulator assumes that the baseline
characteristics share the same distribution across arms, but endpoints
can have same or different distributions given baseline characteristics.
NULL by default, i.e., unstratified permuted block randomization is
executed.
global_dataa list with named components, e.g., design
parameters or a template object of a testing procedure, to be shared
by all replicates of a simulation. It is read-only and can be
accessed in action functions by calling
Trials$get_global_data(). For temporary results passed
between action functions within a single replicate, use
Trials$save_custom_data() instead.
silentlogical. TRUE to mute messages. However, warning
message is still displayed.
...(optional) arguments of enroller and dropout.
resize()
resize a trial with a greater sample size. This function is used to
update the maximum sample size adaptively after sample size reassessment.
Note that this function should be called within action functions.
It is users' responsibility to ensure it and TrialSimulator has
no way to track this.
Trials$resize(n_patients)
n_patientsinteger. Number of maximum sample size of a trial.
remove_arms()
remove arms from a trial. enroll_patients() will be called
at the end of this function to enroll all remaining patients after
Trials$get_current_time(), i.e. no more unenrolled patients
could be randomized to removed arms. This function may be used with
futility analysis, dose selection, enrichment analysis (sub-population)
or interim analysis (early stop for efficacy).
Note that this function should only be called within action functions.
It is users' responsibility to ensure it and TrialSimulator has
no way to track this.
In addition, data of the removed arms are censored or truncated at
the time of arm removal, or additional_followup later, but
stay in the trial data and thus in the locked data of later
milestones. A removed arm is no longer in the set of arms of the
trial: arms = NULL in eventNumber() and
enrollment() excludes it, listing it in arms includes
it, and a subset that depends on which arm was removed requires
$update_milestone() in the same action function.
See ?remove_arms for the three patterns and examples.
Trials$remove_arms(arms_name, additional_followup)
arms_namecharacter vector. Name of arms to be removed.
additional_followupnumeric. Extra follow-up time granted to
the patients of the removed arms after the current milestone, shared
by all arms in arms_name. No default: 0 stops their follow-up
at the milestone itself. Inf keeps following them for
the rest of the trial. To grant different times to different arms,
call this function once per arm.
update_sample_ratio()
update sample ratios of arms. This could happen after an arm is added or removed. Note that we may update sample ratios of unaffected arms as well. Once sample ratio is updated, trial data should be rolled back with updated randomization queue. Data of unenrolled patients are re-sampled as well.
Trials$update_sample_ratio(arm_names, sample_ratios)
arm_namescharacter vector. Name of arms.
sample_ratiosnumeric vector. New sample ratios of arms. If
sample ratio is a whole number, the permuted block randomization is
adopted; otherwise, sample() will be used instead, which can
cause imbalance between arms by chance. However, this is fine for
simulation.
update_generator()
update endpoint generator in an arm
Trials$update_generator(arm_name, endpoint_name, generator, ...)
arm_namecharacter. Name of an arm.
endpoint_namecharacter. A vector of endpoint names whose
generator is updated. It must cover all names registered together in
the corresponding endpoint() call, but their order does not
matter.
generatora random number generation (RNG) function.
See generator of endpoint().
...optional arguments for generator.
add_arms()
add one or more arms to the trial. enroll_patients() will be
called at the end to enroll all remaining patients in
private$randomization_queue. This function can be used in two
scenarios:
(1) add arms right after a trial is created (i.e., Trials$new(...)).
sample_ratio and arms added through ... should be of same
length;
(2) add arms to a trial already with arm(s).
Note that this function should only be called within action functions.
It is users' responsibility to ensure it and TrialSimulator has
no way to track this.
The trial captures an independent deep copy of every arm it registers.
Subsequent changes to the original arm object or its endpoint objects
do not affect the trial, and adaptations within the trial (e.g.,
update_generator()) do not modify the original arm. Complete
the configuration of an arm before registering it; after registration,
change it only through the trial's adaptation methods. The copy covers
the arm and endpoint objects themselves; a mutable environment or R6
object captured by a generator function is shared by design of R
closures and is not isolated, so generators should not carry external
mutable state.
Trials$add_arms(sample_ratio, ...)
sample_ratiointeger vector. Sample ratio for permuted block randomization. It will be appended to existing sample ratio in the trial.
...one or more objects returned from arm().
Randomization is carried out with updated
sample ratio of newly added arm. It rolls back all patients after
Trials$get_current_time(), i.e. redo randomization for those
patients. This can be useful to add arms one by one when creating a trial.
Note that we can run Trials$add_arm(sample_ratio1, arm1) followed
by Trials$add_arm(sample_ratio2, arm2).
We would expected similar result with
Trials$add_arms(c(sample_ratio1, sample_ratio2), arm1, arm2). Note
that these two method won't return exactly the same trial because
randomization_queue were generated twice in the first approach but only
once in the second approach. But statistically, they are equivalent and
of the same distribution.
crossover()
Apply a milestone-triggered crossover to eligible patients in the trial.
Unlike a regimen registered via add_regimen() (applied at
enrollment), crossover() is meant to be called inside a milestone's
action function. At the earliest crossover (calendar) time
T = get_current_time() + delay, eligible patients may switch to a
new treatment, and only their post-switch endpoint values are
altered. The triplet is stacked onto the trial's regimen (so it is also
re-applied to patients enrolled later), and applied immediately, in place,
to all currently-eligible patients.
Eligibility (the pool passed to what()) = patients with at least
one endpoint still "open" (unobserved, dropout-aware) at
T; fully-observed patients are excluded. when() must return
a switch time with enroll_time + switch_time >= T (a crossover
cannot predate its opening), otherwise an error is raised. how()
may only change post-switch outcomes; returning a changed value for a
pre-switch/locked cell raises an error.
Two helper columns are injected into patient_data for the triplet
functions: earliest_crossover_calendar_time (= T) and
earliest_crossover_time_from_enrollment (= max(T - enroll_time, 0)).
Trials$crossover(what, how, when = NULL, delay = 0, ...)
whata function selecting which eligible patients crossover and to
what new_treatment (NA = no crossover). See regimen().
Values of new_treatment must not contain '@' or
';', which are reserved for encoding regimen_trajectory.
howa function returning the modified post-switch endpoint values.
when(optional) a function returning switch_time from
enrollment. If NULL (default), patients switch at T
(switch_time = earliest_crossover_time_from_enrollment).
delaynumeric. Time after the milestone before crossover opens;
T = get_current_time() + delay. Default 0.
...(optional) named arguments routed to what, when,
and/or how.
stop_followup()
stop follow-up of a subset of patients at a specified time at or after
the current milestone. Data of affected patients are censored
(time-to-event endpoints) or set to missing (non-time-to-event endpoints
with readout after the stopping time), as if those patients were no
longer followed since then. This function can be used in adaptive
designs, e.g., to simulate treatment discontinuation, early
termination of follow-up for a sub-population, or enrichment design
where follow-up of a de-selected sub-population is stopped after an
interim analysis. It can also be called at a pre-specified milestone
that splits a trial into cohorts, e.g., a milestone marking the last
patient of the first cohort and the first patient of the second
cohort. Such a milestone is usually event driven, so its time is
unknown until the trial is simulated. Stopping follow-up of the
earlier cohort at that milestone, or after a pre-specified, fixed
additional_followup beyond it, makes statistics computed from
the two cohorts independent, which facilitates tests requiring
independence, e.g., combination tests.
Only patients who are enrolled by the time this function is called and
satisfy the subset conditions in ... (if any) are affected.
Patients enrolled afterwards are followed as usual.
Note that this function should only be called within action functions.
It is users' responsibility to ensure it and TrialSimulator has
no way to track this. Calling it before any milestone has been
triggered is an error.
Trials$stop_followup(..., additional_followup)
...subset conditions compatible with dplyr::filter.
Follow-up is stopped for selected patients only. If no condition is
provided, follow-up is stopped for all patients enrolled by the time
this function is called.
additional_followupnumeric. Extra follow-up time granted to the selected patients after the current milestone. No default: 0 stops follow-up at the milestone itself.
update_accrual_rate()
update the accrual rate of the recruitment curve at a milestone. The
enroller of a trial is always StaggeredRecruiter; this function
replaces its accrual_rate for patients not yet enrolled, while
enrolled patients are left unchanged. It can be used in adaptive
designs, e.g., to revise recruitment after dose selection or
enrichment, or to pause recruitment for a period after an interim
decision.
end_time in accrual_rate is measured from the time this
function is called (i.e., the current milestone), not from the start
of the trial. A milestone is usually event driven, so its calendar
time is unknown until the trial is simulated, and a schedule on the
calendar time scale could not be pre-specified. Measuring
end_time from the milestone also lets users state the new plan
simply as "from now on": e.g.,
data.frame(end_time = c(3, Inf), piecewise_rate = c(20, 35))
means 20 patients per month for the 3 months following the milestone
and 35 per month thereafter, whenever the milestone occurs. Following
the convention of StaggeredRecruiter, the first re-planned
patient is enrolled 1 / piecewise_rate after the milestone; a
leading window with piecewise_rate = 0 defers enrollment
further. As with other adaptations, patients not yet enrolled are
re-randomized and their data are regenerated under the new schedule.
Note that this function should only be called within action functions. Calling it before any milestone has been triggered is an error.
Trials$update_accrual_rate(accrual_rate)
accrual_ratea data frame of columns end_time and
piecewise_rate as in StaggeredRecruiter, with
end_time measured from the current milestone. The last
end_time must be Inf with a positive rate.
update_milestone()
update the trigger condition and/or the action of a not-yet-triggered milestone at a milestone. The milestone to be updated is identified by its name, which cannot be changed. This function can be used in adaptive designs, e.g., when conditional power at an interim analysis is lower than expected, the final analysis can be postponed by increasing the target number of events in its triggering condition, or its triggering condition can be switched from a calendar time to an event count entirely.
The update is not applied immediately: it is queued and takes effect right after the current action function returns, before the next milestone is evaluated. The new trigger condition and action replace the old ones as a whole. Between simulation replicates the milestone is restored to its as-designed trigger condition and action, so every replicate starts from the original design. A milestone that has already been triggered cannot be updated.
Note that this function should only be called within action functions. Calling it before any milestone has been triggered is an error. Also note that milestones must trigger in their registration order: an updated triggering condition that makes a later-registered milestone fire before an earlier one stops the simulation with an error.
Trials$update_milestone(name, when = NULL, action = NULL, ...)
namecharacter. Name of the milestone to be updated. It must be registered with the listener and not yet triggered.
when(optional) new triggering condition, an object returned by
calendarTime(), enrollment(), eventNumber() or
their combinations using & and |. If NULL, the
triggering condition is left unchanged.
action(optional) new action function. See action of
milestone(). If NULL, the action is left unchanged.
...(optional) named arguments of the new action. Only
allowed when action is provided.
The new action is executed with exactly the arguments supplied
here: fixed arguments of the previous action are never carried over.
get_locked_data()
return locked data, i.e. snapshot at a milestone. TTE data is censored and non-TTE data is truncated accounting for readout time and dropout time simultaneously by the triggering time of milestone.
Trials$get_locked_data(milestone_name)
milestone_namecharacter. Milestone name of which the locked data to be extracted.
save()
save a single value or a one-row data frame to trial's output for further analysis/summary later. Results saved by calling this function have a life cycle of the whole simulation. This means that all results are accumulated across multiple simulated trial and can be used for summary later.
Trials$save(value, name = "", overwrite = FALSE)
valuevalue to be saved. It can be a scalar (vector of length 1) or a data frame (of one row).
namecharacter to name the saved object. It will be used to
name a column in trial's output if value is a scalar.
If value is a data frame, name will be the prefix pasted
with the column name of value in trial's output.
If user want to use
value's column name as is in trial's output, set name
to be '' as default. Otherwise, column name would be, e.g.,
"{<name>}_<{colnames(value)}>".
overwritelogic. TRUE if overwriting existing entries
with warning, otherwise, throwing an error and stop.
get_output()
return a data frame of all current outputs saved by calling
Trials$save(). Usually this function is call at the end of
simulation for summary.
Trials$get_output(cols = NULL, simplify = TRUE, tidy = FALSE)
colscolumns to be returned from Trial$output. If
NULL, all columns are returned.
simplifylogical. Return value rather than a data frame of one
column when length(col) == 1 and simplify == TRUE.
tidylogical. TrialSimulator automatically records a set
of standard outputs at milestones, even when doNothing is used
as action functions. These includes time of triggering milestones,
number of observed events for time-to-event endpoints, and number of
non-missing readouts for non-TTE endpoints
(see vignette('actionFunctions')). This usually mean a large
number of columns in outputs. If users have no intent to summarize a
trial on these columns, setting tidy = TRUE can eliminate these
columns from get_output(). Note that currently we use regex
"^n_events_<.*?>_<.*?>$" and
"^milestone_time_<.*?>$" to match columns to be eliminated.
If users plan to use tidy = TRUE, caution is needed when naming
custom outputs in save(). Default FALSE.
bind()
row bind a data frame to existing data frame. If a data frame name
is not existing in a trial, then it is equivalent to
calling Trials$save_custom_data().
Extra columns in value are ignored. Columns in
Trials$custom_data[[name]] but not in value are filled
with NA.
This function can be used to save results across multiple milestones.
For example, p-values and effect estimates of endpoints may be computed
at multiple milestones. Users may want to bind them into a data frame
for combination test or graphical test. In this case, this function
can be called repeatedly in milestones. Once the data frame is fully
conducted, statistical test can be performed on its final version
retrieved by calling Trials$get().
Note that data saved by calling this function has a short life cycle
within a single simulated trial. It will be reset to NULL before
simulated another trial. Thus, it cannot be used to save results that
are used for summarizing the simulation.
Trials$bind(value, name)
valuea data frame to be saved. It can consist of one or multiple rows.
namecharacter. Name of object to be saved.
save_custom_data()
save temporary data into a trial to pass intermediate results between
action functions of milestones within a single simulated trial, e.g.,
a testing procedure object updated at every interim analysis. The
saved data live only within the current replicate: the storage is
cleared before every replicate, so nothing saved here can leak into
the next replicate or be used to summarize a simulation (use
Trials$save() for outputs, and the global_data argument
of trial() for read-only data that must be available in all
replicates). After run() returns, the storage still holds the
data of the last replicate, which can be inspected like the rest of
the trial's final state; the next run() clears it when it
starts.
Trials$save_custom_data(value, name, overwrite = FALSE)
valuevalue to be saved. Any type except NULL.
namecharacter. Name of the value to be accessed later.
overwritelogic. TRUE if overwriting existing entries
with warning, otherwise, throwing an error and stop.
get_custom_data()
return custom data saved by calling Trials$save_custom_data()
or Trials$bind() with its name.
Trials$get_custom_data(name)
namecharacter. Name of custom data to be accessed.
get()
alias of function get_custom_data to make it short and cool.
Trials$get(name)
namecharacter. Name of custom data to be accessed.
get_global_data()
return global data registered through the global_data argument
of trial() with its name. Global data is read-only and
available in every replicate of a simulation: an R6 object is
returned as an independent deep clone and a data.table as a
copy (also when nested inside plain lists), so updating the returned
object never changes the registered template. To carry an updated
object across milestones within a replicate, save the returned object
with Trials$save_custom_data().
Trials$get_global_data(name)
namecharacter. Name of global data to be accessed.
get_current_time()
return current time of a trial
Trials$get_current_time()
get_milestone_time()
return milestone time when triggering a given milestone
Trials$get_milestone_time(milestone_name = NULL)
milestone_namecharacter. Name of milestone. If NULL,
time of all triggered milestones are returned.
get_sample_ratio()
return current sample ratio of the trial. The ratio can probably change during the trial (e.g., arm is removed or added)
Trials$get_sample_ratio(arm_names = NULL)
arm_namescharacter vector of arms.
get_arms_name()
return arms' name of trial
Trials$get_arms_name()
dunnettTest()
carry out closed test based on Dunnett method under group sequential design.
Trials$dunnettTest( formula, placebo, treatments, milestones, alternative, planned_info, ... )
formulaAn object of class formula that can be used with
survival::coxph. Must consist arm and endpoint in data.
No covariate is allowed. Stratification variables are supported and can be
added using strata(...).
placebocharacter. Name of placebo arm.
treatmentscharacter vector. Name of treatment arms to be used in comparison.
milestonescharacter vector. Names of triggered milestones at which either
adaptation is applied or statistical testing for endpoint is performed.
Milestones in milestones does not need to be sorted by their triggering time.
alternativea character string specifying the alternative hypothesis,
must be one of "greater" or "less". No default value.
"greater" means superiority of treatment over placebo is established
by an hazard ratio greater than 1 when a log-rank test is used.
planned_infoa data frame of planned number of events of
time-to-event endpoint in each stage and each arm. Milestone names, i.e.,
milestones are row names of planned_info, and arm names, i.e.,
c(placebo, treatments) are column names.
Note that it is not the accumulative but stage-wise event numbers.
It is usually not easy to determine these numbers in practice, simulation
may be used to get estimates.
Note: planned_info can also be a character
"default" so that planned_info are set to be number
of newly randomized patients in the control arm in each of the stages.
This assumes that
event rate do not change over time and/or sample ratio between placebo
and a treatment arm does not change as well, which may not be true.
It is for the purpose of debugging or rapid implementation
only. Using simulation to pick planned_info is recommended in
formal simulation study. Another issue with planned_info set to
be "default" is that it is possible patient recruitment is done
before a specific stage, as a result, planned_info is zero which
can crash the program.
...subset condition that is compatible with dplyr::filter.
survdiff will be fitted on this subset only to compute one-sided
logrank statistics. It could be useful when comparison is made on a
subset of treatment arms. By default it is not specified,
all data (placebo plus one treatment arm at a time) in the locked data
are used to fit the model.
This function computes stage-wise p-values for each of the intersection
hypotheses based on Dunnett test. If only one treatment arm is present,
it is equivalent to compute the stage-wise p-values of elemental
hypotheses. This function also computes inverse normal combination
test statistics at each of the stages.
The choice of planned_info can affect the calculation of
stage-wise p-values. Specifically, it is used to compute
the columns observed_info and p_inverse_normal in returned
data frame, which will be used in Trial$closedTest().
The choice of planned_info can affect the result of
Trial$closedTest() so user should chose it with caution.
Note that in Trial$closedTest(),
observed_info, which is derived from planned_info, will
lead to the same closed testing results up to a constant. This is because
the closed test uses information fraction
observed_info/sum(observed_info). As a result, setting
planned_info to, e.g., 10 * planned_info should give same
closed test results.
Based on numerical study, setting planned_info = "default" leads
to a much higher power (roughly 10%) than setting planned_info to
median of event numbers at stages, which can be determined by simulation.
I am not sure if regulator would support such practice. For example,
if a milestone (e.g., interim analysis) is triggered at a pre-specified
calendar time, the number of randomized patients is random and is unknown
when planning the trial. If I understand it correctly, regulator may want
the information fraction in closed test (combined with Dunnett test) to
be pre-fixed. In addition, this choice for planned_info assumes
that the event rates does not change over time which is obviously not
true. It is recommended to always use pre-fixed planned_info for
restrict control of family-wise error rate. It should be pointed out
that the choice of pre-fixed planned_info can affect statistical
power significantly so fine-tuning may be required.
a list with element names like arm_name,
arm1_name|arm2_name, arm1_name|arm2_name|arm3_name, etc.,
i.e., all possible combination of treatment arms in comparison. Each
element is a data frame, with its column names self-explained. Specifically,
the columns p_inverse_normal, observed_info,
is_final can be used with GroupSequentialTest to perform
significance test.
\dontrun{
trial$dunnettTest(Surv(pfs, pfs_event) ~ arm, 'pbo', c('high dose', 'low dose'),
listener$get_milestone_names(), 'default')
}
closedTest()
perform closed test based on Dunnett test
Trials$closedTest(
dunnett_test,
treatments,
milestones,
alpha,
alpha_spending = c("asP", "asOF")
)dunnett_testobject returned by Trial$dunnettTest().
treatmentscharacter vector. Name of treatment arms to be used in comparison.
milestonescharacter vector. Names of triggered milestones at which
significance testing for endpoint is performed in closed test.
Milestones in milestones does not need to be sorted by their triggering time.
alphanumeric. Allocated alpha.
alpha_spendingalpha spending function. It can be "asP" or
"asOF". Note that theoretically it can be "asUser", but
it is not tested. It may be supported in the future.
a data frame of columns arm, decision
(final decision on a hypothesis at the end of trial, "accept" or "reject"),
milestone_at_reject, and reject_time.
If a hypothesis is accepted at then end of a trial,
milestone_at_reject is NA, and reject_time is Inf.
Note that if a hypothesis is tested at multiple milestones, the final
decision will be "accept" if it is accepted at at least
one milestone. The decision is "reject" only if the hypothesis
is rejected at all milestones.
\dontrun{
dt <- trial$dunnettTest(
Surv(pfs, pfs_event) ~ arm,
placebo = 'pbo',
treatments = c('high dose', 'low dose'),
milestones = c('dose selection', 'interim', 'final'),
data.frame(pbo = c(100, 160, 80),
low = c(100, 160, 80),
high = c(100, 160, 80),
row.names = c('dose selection', 'interim', 'final'))
trial$closedTest(dt, treatments = c('high dose', 'low dose'),
milestones = c('interim', 'final'),
alpha = 0.025, alpha_spending = 'asOF')
}
conditionalPower()
compute conditional power at a triggered interim milestone for each
treatment-vs-placebo comparison of a time-to-event endpoint, under a
group sequential design with one interim and one final analysis.
Locked data of the milestone is pulled automatically;
fitLogrank() is called internally to obtain, for every
treatment arm vs placebo, the observed z statistic and the
observed number of events d on the two arms of that
comparison (after applying subset conditions in ..., if
any). Conditional power is then
under alternative = 'less' (mirrored for 'greater'),
where is the log hazard ratio at which conditional
power is evaluated and with the
allocation ratio of the pair recorded when the milestone's data was
locked. Like and , is an interim quantity:
the result depends on the requested milestone only, not on
adaptations applied after it.
The calculation assumes the trial continues as designed: the
allocation ratio of the compared arms is constant from the start of
enrollment through the final analysis, and the final analysis tests
the planned statistic at the planned boundary. A data-dependent
design change (e.g., updating the sample ratio based on interim
results) alters both the final test statistic and its boundary;
such adaptations require a combination-test analysis instead (see
$dunnettTest() and $closedTest()). It is users'
responsibility to call this function only when the calculation is
legitimate – in particular, the allocation ratio of the compared
arms has not been updated before the milestone, the compared arms
are enrolled concurrently with placebo, and subset conditions in
... are independent of randomization –
TrialSimulator has no way to track this.
Conditional power can be requested for an arm that has been removed
from the trial: its z and d are well-defined historical
quantities, although no further event will accrue on it. With a
numeric effect, however, an error is raised for an arm
removed before the milestone, as no allocation ratio of the pair is
recorded at the milestone.
Trials$conditionalPower( milestone, formula, placebo, alternative, alpha, D, effect, ... )
milestonecharacter. Name of a triggered milestone at which the interim results are observed.
formulaan object of class formula as in
fitLogrank(), e.g., Surv(pfs, pfs_event) ~ arm.
Stratification via strata(...) is supported; no covariate is
allowed.
placebocharacter. Name of the placebo arm.
alternativea character string specifying the alternative
hypothesis, must be one of "greater" or "less". No
default value. "greater" means superiority of treatment over
placebo is established by a hazard ratio greater than 1. See
fitLogrank().
alphanumeric. The one-sided nominal significance level(s)
corresponding to the planned final critical boundary, in (0, 1):
under alternative = 'less' the final z statistic is compared
with qnorm(alpha). Under a group sequential design it is
implied by the alpha spending function, e.g., 1 - pnorm(c)
for a final critical value c on the upper scale; in general
it differs from both the total design alpha and the alpha spent,
cumulatively or incrementally, at the final look. If a single
treatment arm is compared with placebo, an unnamed scalar is
accepted; otherwise alpha must be a named vector using
treatment arm names, matching the names of D. Entries are
matched to D by name, so the order of components does not
matter.
Dnumeric. Planned number of events at the final analysis for
each comparison, counted on the two arms of that comparison (placebo
plus one treatment arm). If a single treatment arm is compared with
placebo, an unnamed scalar is accepted; otherwise D must be a
named vector using treatment arm names. A subset of the treatment
arms can be specified, in which case conditional power is computed
for that subset of comparisons only. D and alpha must
be of the same length and, when named, use the identical set of arm
names. An error is raised if the observed number of events d
of a comparison already reaches D.
effectthe treatment effect at which conditional power is
evaluated. No default value. 'trend' extrapolates the effect
observed at the interim; 'null' assumes no effect for the
remaining events (conditional type I error); a single positive
numeric value is interpreted as a hazard ratio (e.g.,
effect = 0.75), which is converted internally using the
allocation ratio of each pair recorded at the milestone.
...subset conditions compatible with dplyr::filter,
passed to fitLogrank().
a data frame with one row per treatment-vs-placebo
comparison, with columns arm, placebo, z,
d, D, info_fraction, alpha,
effect and cp.
\donttest{
## a two-arm trial with a calendar-time interim
pbo <- arm(name = 'pbo')
pbo$add_endpoints(endpoint(name = 'pfs', type = 'tte',
generator = rexp, rate = log(2) / 10))
trt <- arm(name = 'trt')
trt$add_endpoints(endpoint(name = 'pfs', type = 'tte',
generator = rexp, rate = log(2) / 14))
accrual <- data.frame(end_time = Inf, piecewise_rate = 30)
tr <- trial(name = 'ex', n_patients = 400,
seed = 31416, enroller = StaggeredRecruiter,
accrual_rate = accrual, silent = TRUE)
add_arms(tr, sample_ratio = c(1, 1), pbo, trt)
lstn <- listener(silent = TRUE)
lstn$add_milestones(
milestone(name = 'interim', when = calendarTime(time = 15)),
milestone(name = 'final', when = calendarTime(time = 40))
)
controller(tr, lstn)$run(n = 1, silent = TRUE, plot_event = FALSE)
## conditional power at the interim trend, with 300 events planned
## at the final analysis and a final boundary at nominal level 0.022
tr$conditionalPower('interim', Surv(pfs, pfs_event) ~ arm,
placebo = 'pbo', alternative = 'less',
alpha = 0.022, D = 300, effect = 'trend')
## under an assumed hazard ratio instead of the interim trend
tr$conditionalPower('interim', Surv(pfs, pfs_event) ~ arm,
placebo = 'pbo', alternative = 'less',
alpha = 0.022, D = 300, effect = 0.75)
}
eventNumberReestimationFromConditionalPower()
re-estimate the number of events at the final analysis for each
treatment-vs-placebo comparison of a time-to-event endpoint: the
smallest whole number of events D, strictly greater than the
event number observed at the interim, at which the conditional power
computed by $conditionalPower() reaches a target, under a group
sequential design with one interim and one final analysis. Locked
data of the milestone is pulled automatically; fitLogrank()
is called internally to obtain, for every treatment arm vs
placebo, the observed z statistic and the observed number of
events d on the two arms of that comparison (after applying
subset conditions in ..., if any).
Trials$eventNumberReestimationFromConditionalPower( milestone, formula, placebo, alternative, alpha, target_cp, effect, ..., D_cap = NULL )
milestonecharacter. Name of a triggered milestone at which the interim results are observed.
formulaan object of class formula as in
fitLogrank(), e.g., Surv(pfs, pfs_event) ~ arm.
Stratification via strata(...) is supported; no covariate is
allowed.
placebocharacter. Name of the placebo arm.
alternativea character string specifying the alternative
hypothesis, must be one of "greater" or "less". No
default value. "greater" means superiority of treatment over
placebo is established by a hazard ratio greater than 1. See
fitLogrank().
alphanumeric. The one-sided nominal significance level(s)
corresponding to the planned final critical boundary, in (0, 1).
See $conditionalPower(). If a single treatment arm is
compared with placebo, an unnamed scalar is accepted; otherwise
alpha must be a named vector using treatment arm names,
matching the names of target_cp. A subset of the treatment
arms can be specified, in which case the event number is
re-estimated for that subset of comparisons only.
target_cpnumeric. Target conditional power(s) in (0, 1).
alpha and target_cp must be of the same length and,
when named, use the identical set of arm names. Entries are matched
to alpha by name, so the order of components does not
matter.
effectthe treatment effect at which conditional power is
evaluated. No default value. 'trend' extrapolates the effect
observed at the interim; a single positive numeric value is
interpreted as a hazard ratio (e.g., effect = 0.75), which
is converted internally using the allocation ratio of each pair
recorded at the milestone. 'null' is not supported (see
Details).
...subset conditions compatible with dplyr::filter,
passed to fitLogrank().
D_capNULL or numeric. Practical upper bound(s) of the
re-estimated event number, counted on the two arms of each comparison.
Whole number(s) greater than the observed d, or Inf for
an unbounded search. The default NULL is converted internally to
scalar Inf for one comparison or a named vector of Inf
for multiple comparisons. An explicit scalar Inf applies to all
comparisons; a finite unnamed scalar is accepted when a single
treatment arm is compared with placebo; otherwise D_cap must
be a named vector over the same treatment arms as alpha and
target_cp, in which individual entries may be Inf.
Placed after ..., so it must always be passed by name.
The method returns the smallest whole number D > d satisfying
where is target_cp and is the
conditional power of $conditionalPower() at the same
milestone, alpha and effect. The strict inequality
requires a genuine future final analysis. It also applies when the
interim z statistic already reaches the final boundary, because future
observations can dilute the interim result and reduce conditional
power below target_cp.
Conditional power need not be monotone in D, particularly when
a numeric effect differs from the interim trend. The method finds the
stationary points of conditional power by solving a cubic equation,
partitions the integer search range into monotone intervals, and
searches them from left to right. Integer bisection within the first
interval that reaches target_cp therefore returns the earliest
crossing required by the rule above, even if conditional power later
falls below the target and recovers.
D_cap bounds the search by the largest event number considered
practical. The complete integer range through the cap is searched:
if a finite solution exists in the requested range, the smallest
such D is returned with its conditional power and
target_reached = TRUE;
if no finite solution exists in the requested range,
D and achieved_cp are NA, while
target_reached = FALSE. D_cap continues to report the
requested search cap.
effect = 'null' is not supported; use
$conditionalPower() to compute the conditional type I error
at a given D instead.
Like $conditionalPower(), the calculation assumes the trial
continues as designed: constant allocation ratio of the compared
arms, planned final statistic, and the planned final boundary held
fixed at nominal level alpha while D varies.
Increasing the event number based on a promising interim while
keeping the boundary unchanged is the practice studied in the
sample size re-estimation literature; whether it is legitimate for
the design at hand is users' responsibility, as
TrialSimulator has no way to track this.
a data frame with one row per treatment-vs-placebo
comparison, with columns arm, placebo, z,
d, D, D_cap, alpha, effect,
target_cp, achieved_cp and target_reached.
D is the smallest solution found in the requested range and
achieved_cp is the conditional power at that D. Both are
NA when no solution is found. target_cp always records
the requested target, D_cap always records the requested search
cap (with the default NULL represented as Inf), and
target_reached indicates whether a solution was found.
\donttest{
## a two-arm trial with a calendar-time interim
pbo <- arm(name = 'pbo')
pbo$add_endpoints(endpoint(name = 'pfs', type = 'tte',
generator = rexp, rate = log(2) / 10))
trt <- arm(name = 'trt')
trt$add_endpoints(endpoint(name = 'pfs', type = 'tte',
generator = rexp, rate = log(2) / 14))
accrual <- data.frame(end_time = Inf, piecewise_rate = 30)
tr <- trial(name = 'ex', n_patients = 400,
seed = 31416, enroller = StaggeredRecruiter,
accrual_rate = accrual, silent = TRUE)
add_arms(tr, sample_ratio = c(1, 1), pbo, trt)
lstn <- listener(silent = TRUE)
lstn$add_milestones(
milestone(name = 'interim', when = calendarTime(time = 15)),
milestone(name = 'final', when = calendarTime(time = 40))
)
controller(tr, lstn)$run(n = 1, silent = TRUE, plot_event = FALSE)
## smallest number of final events reaching conditional power 0.9
## under an assumed hazard ratio
tr$eventNumberReestimationFromConditionalPower(
'interim', Surv(pfs, pfs_event) ~ arm,
placebo = 'pbo', alternative = 'less',
alpha = 0.022, target_cp = 0.9, effect = 0.75)
## with a practical cap: when no event number through the cap reaches
## the target, D and achieved_cp are NA and target_reached is FALSE
tr$eventNumberReestimationFromConditionalPower(
'interim', Surv(pfs, pfs_event) ~ arm,
placebo = 'pbo', alternative = 'less',
alpha = 0.022, target_cp = 0.9, effect = 'trend', D_cap = 500)
}
add_regimen()
register regimen to a trial. The regimen consists of three functions to determine the patients who may switch to other treatment during a a trial, to determine the switching time and how to update patients' endpoint data accordingly.
The trial captures an independent deep copy of the regimen: triplets
appended in-run by crossover() do not modify the caller's
regimen object. The copy covers the regimen object itself; a mutable
environment or R6 object captured by a what/when/how
function is shared by design of R closures and is not isolated.
Trials$add_regimen(regimen)
regimenan object created by regimen().
lock_data()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
lock data at specific calendar time.
For time-to-event endpoints, their event indicator *_event should be
updated accordingly. Locked data should be stored separately.
DO NOT OVERWRITE/UPDATE private$trial_data! which can lose actual
time-to-event information. For example, a patient may be censored at
the first data lock. However, he may have event being observed in a
later data lock.
Trials$lock_data(at_calendar_time, milestone_name)
at_calendar_timetime point to lock trial data
milestone_nameassign milestone name as the name of locked data for future reference.
get_data_lock_time_by_calendar_time()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
given the calendar time to lock the data, return it with event counts of each of the endpoints.
Trials$get_data_lock_time_by_calendar_time(calendar_time)
calendar_timenumeric. Calendar time to lock the data
data lock time
get_data_lock_time_by_event_number()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
given a set of endpoints and target number of events, determine the data lock time for a milestone (futility, interim, final, etc.). This function does not change trial object (e.g. rolling back not yet randomized patients after the found data lock time).
Trials$get_data_lock_time_by_event_number(
endpoints,
arms,
target_n_events,
type = c("all", "any"),
...
)endpointscharacter vector. Data lock time is determined by a set of endpoints.
armsa vector of arms' name on which number of events will be
counted. If NULL, arms in the trial when this function is
called. Otherwise, every name must be an arm ever added to the trial
(an error otherwise), and arms removed before this call are counted
as specified, with a warning unless the trial is silent.
target_n_eventstarget number of events for each of the
endpoints.
typeall if all target number of events are reached.
any if the any target number of events is reached.
...subset conditions compatible with dplyr::filter. Number
Time of milestone is based on event counts on the subset of trial data.
data lock time
get_data_lock_time_by_enrollment()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
given a target number of enrolled patients, determine the data lock time for a milestone (futility, interim, final, etc.). This function does not change trial object (e.g. rolling back not yet randomized patients after the found data lock time). It is similar to get_data_lock_time_by_event_number but only focus on patient_id.
Trials$get_data_lock_time_by_enrollment( arms, target_n_patients, min_treatment_duration, ... )
armsa vector of arms' name on which number of patients will be
counted. If NULL, arms in the trial when this function is
called. Otherwise, every name must be an arm ever added to the trial
(an error otherwise), and arms removed before this call are counted
as specified, with a warning unless the trial is silent.
target_n_patientstarget number of enrolled patients.
min_treatment_durationnumeric. Zero or positive value.
minimum treatment duration of enrolled patients.
If 0, it looks for triggering time based on number of enrolled
patients in population specified by ... and arms. If positive,
it means that milestone is triggered when a specific number of enrolled
patients have received treatment for at least min_treatment_duration
duration. It is users' responsibility to assure that the unit of
min_treatment_duration are consistent with
readout of non-tte endpoints, dropout time, and calendar time of milestones.
...subset conditions compatible with dplyr::filter. Number
Time of milestone is based on event counts on the subset of trial data.
data lock time
has_arm()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
check if the trial has any arm. Return TRUE or FALSE.
Trials$has_arm()
event_plot()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
plot of cumulative number of events/samples over calendar time.
Trials$event_plot()
mute()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
mute all messages (not including warnings)
Trials$mute(silent)
silentlogical.
tidy_output()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
control whether the per-arm event count table is saved in trial
output at every milestone. It is set by controller$run()
through its argument tidy.
Trials$tidy_output(tidy)
tidylogical. If TRUE, the per-arm event count table
(output column n_events_<milestone>_<arms>) is not saved in
trial output; the per-endpoint totals and milestone times are still
saved. The table remains available in the attributes of locked data,
so event_plot() is unaffected.
reset()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
reset a trial to its snapshot taken before it was executed. Seed will be reassigned with a new one. Enrollment time are re-generated. If the trial already have arms when this function is called, they are added back to recruit patients again.
Trials$reset()
make_arms_snapshot()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
make a snapshot of arms
Trials$make_arms_snapshot()
has_been_run()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
report whether the trial has left its as-designed state, i.e., at
least one replicate has been (partially) executed on it. The
controller refuses to start run() on such a trial: wrapping an
already-run trial in a new controller would otherwise bypass the
run-twice guard and simulate from a dirty state.
Trials$has_been_run()
pop_milestone_updates()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
return and clear the queue of milestone update requests scheduled by
update_milestone() within the current action function. It is
called by the listener right after each action function returns, to
apply the requested updates to its registered milestones.
Trials$pop_milestone_updates()
print()
print a trial
Trials$print()
clone()
The objects of this class are cloneable with this method.
Trials$clone(deep = FALSE)
deepWhether to make a deep clone.
# Instead of using Trials$new, please use trial(), a user-friendly # wrapper. See examples in ?trial. ## ------------------------------------------------ ## Method `Trials$dunnettTest` ## ------------------------------------------------ ## Not run: trial$dunnettTest(Surv(pfs, pfs_event) ~ arm, 'pbo', c('high dose', 'low dose'), listener$get_milestone_names(), 'default') ## End(Not run) ## ------------------------------------------------ ## Method `Trials$closedTest` ## ------------------------------------------------ ## Not run: dt <- trial$dunnettTest( Surv(pfs, pfs_event) ~ arm, placebo = 'pbo', treatments = c('high dose', 'low dose'), milestones = c('dose selection', 'interim', 'final'), data.frame(pbo = c(100, 160, 80), low = c(100, 160, 80), high = c(100, 160, 80), row.names = c('dose selection', 'interim', 'final')) trial$closedTest(dt, treatments = c('high dose', 'low dose'), milestones = c('interim', 'final'), alpha = 0.025, alpha_spending = 'asOF') ## End(Not run) ## ------------------------------------------------ ## Method `Trials$conditionalPower` ## ------------------------------------------------ ## a two-arm trial with a calendar-time interim pbo <- arm(name = 'pbo') pbo$add_endpoints(endpoint(name = 'pfs', type = 'tte', generator = rexp, rate = log(2) / 10)) trt <- arm(name = 'trt') trt$add_endpoints(endpoint(name = 'pfs', type = 'tte', generator = rexp, rate = log(2) / 14)) accrual <- data.frame(end_time = Inf, piecewise_rate = 30) tr <- trial(name = 'ex', n_patients = 400, seed = 31416, enroller = StaggeredRecruiter, accrual_rate = accrual, silent = TRUE) add_arms(tr, sample_ratio = c(1, 1), pbo, trt) lstn <- listener(silent = TRUE) lstn$add_milestones( milestone(name = 'interim', when = calendarTime(time = 15)), milestone(name = 'final', when = calendarTime(time = 40)) ) controller(tr, lstn)$run(n = 1, silent = TRUE, plot_event = FALSE) ## conditional power at the interim trend, with 300 events planned ## at the final analysis and a final boundary at nominal level 0.022 tr$conditionalPower('interim', Surv(pfs, pfs_event) ~ arm, placebo = 'pbo', alternative = 'less', alpha = 0.022, D = 300, effect = 'trend') ## under an assumed hazard ratio instead of the interim trend tr$conditionalPower('interim', Surv(pfs, pfs_event) ~ arm, placebo = 'pbo', alternative = 'less', alpha = 0.022, D = 300, effect = 0.75) ## ------------------------------------------------ ## Method `Trials$eventNumberReestimationFromConditionalPower` ## ------------------------------------------------ ## a two-arm trial with a calendar-time interim pbo <- arm(name = 'pbo') pbo$add_endpoints(endpoint(name = 'pfs', type = 'tte', generator = rexp, rate = log(2) / 10)) trt <- arm(name = 'trt') trt$add_endpoints(endpoint(name = 'pfs', type = 'tte', generator = rexp, rate = log(2) / 14)) accrual <- data.frame(end_time = Inf, piecewise_rate = 30) tr <- trial(name = 'ex', n_patients = 400, seed = 31416, enroller = StaggeredRecruiter, accrual_rate = accrual, silent = TRUE) add_arms(tr, sample_ratio = c(1, 1), pbo, trt) lstn <- listener(silent = TRUE) lstn$add_milestones( milestone(name = 'interim', when = calendarTime(time = 15)), milestone(name = 'final', when = calendarTime(time = 40)) ) controller(tr, lstn)$run(n = 1, silent = TRUE, plot_event = FALSE) ## smallest number of final events reaching conditional power 0.9 ## under an assumed hazard ratio tr$eventNumberReestimationFromConditionalPower( 'interim', Surv(pfs, pfs_event) ~ arm, placebo = 'pbo', alternative = 'less', alpha = 0.022, target_cp = 0.9, effect = 0.75) ## with a practical cap: when no event number through the cap reaches ## the target, D and achieved_cp are NA and target_reached is FALSE tr$eventNumberReestimationFromConditionalPower( 'interim', Surv(pfs, pfs_event) ~ arm, placebo = 'pbo', alternative = 'less', alpha = 0.022, target_cp = 0.9, effect = 'trend', D_cap = 500)# Instead of using Trials$new, please use trial(), a user-friendly # wrapper. See examples in ?trial. ## ------------------------------------------------ ## Method `Trials$dunnettTest` ## ------------------------------------------------ ## Not run: trial$dunnettTest(Surv(pfs, pfs_event) ~ arm, 'pbo', c('high dose', 'low dose'), listener$get_milestone_names(), 'default') ## End(Not run) ## ------------------------------------------------ ## Method `Trials$closedTest` ## ------------------------------------------------ ## Not run: dt <- trial$dunnettTest( Surv(pfs, pfs_event) ~ arm, placebo = 'pbo', treatments = c('high dose', 'low dose'), milestones = c('dose selection', 'interim', 'final'), data.frame(pbo = c(100, 160, 80), low = c(100, 160, 80), high = c(100, 160, 80), row.names = c('dose selection', 'interim', 'final')) trial$closedTest(dt, treatments = c('high dose', 'low dose'), milestones = c('interim', 'final'), alpha = 0.025, alpha_spending = 'asOF') ## End(Not run) ## ------------------------------------------------ ## Method `Trials$conditionalPower` ## ------------------------------------------------ ## a two-arm trial with a calendar-time interim pbo <- arm(name = 'pbo') pbo$add_endpoints(endpoint(name = 'pfs', type = 'tte', generator = rexp, rate = log(2) / 10)) trt <- arm(name = 'trt') trt$add_endpoints(endpoint(name = 'pfs', type = 'tte', generator = rexp, rate = log(2) / 14)) accrual <- data.frame(end_time = Inf, piecewise_rate = 30) tr <- trial(name = 'ex', n_patients = 400, seed = 31416, enroller = StaggeredRecruiter, accrual_rate = accrual, silent = TRUE) add_arms(tr, sample_ratio = c(1, 1), pbo, trt) lstn <- listener(silent = TRUE) lstn$add_milestones( milestone(name = 'interim', when = calendarTime(time = 15)), milestone(name = 'final', when = calendarTime(time = 40)) ) controller(tr, lstn)$run(n = 1, silent = TRUE, plot_event = FALSE) ## conditional power at the interim trend, with 300 events planned ## at the final analysis and a final boundary at nominal level 0.022 tr$conditionalPower('interim', Surv(pfs, pfs_event) ~ arm, placebo = 'pbo', alternative = 'less', alpha = 0.022, D = 300, effect = 'trend') ## under an assumed hazard ratio instead of the interim trend tr$conditionalPower('interim', Surv(pfs, pfs_event) ~ arm, placebo = 'pbo', alternative = 'less', alpha = 0.022, D = 300, effect = 0.75) ## ------------------------------------------------ ## Method `Trials$eventNumberReestimationFromConditionalPower` ## ------------------------------------------------ ## a two-arm trial with a calendar-time interim pbo <- arm(name = 'pbo') pbo$add_endpoints(endpoint(name = 'pfs', type = 'tte', generator = rexp, rate = log(2) / 10)) trt <- arm(name = 'trt') trt$add_endpoints(endpoint(name = 'pfs', type = 'tte', generator = rexp, rate = log(2) / 14)) accrual <- data.frame(end_time = Inf, piecewise_rate = 30) tr <- trial(name = 'ex', n_patients = 400, seed = 31416, enroller = StaggeredRecruiter, accrual_rate = accrual, silent = TRUE) add_arms(tr, sample_ratio = c(1, 1), pbo, trt) lstn <- listener(silent = TRUE) lstn$add_milestones( milestone(name = 'interim', when = calendarTime(time = 15)), milestone(name = 'final', when = calendarTime(time = 40)) ) controller(tr, lstn)$run(n = 1, silent = TRUE, plot_event = FALSE) ## smallest number of final events reaching conditional power 0.9 ## under an assumed hazard ratio tr$eventNumberReestimationFromConditionalPower( 'interim', Surv(pfs, pfs_event) ~ arm, placebo = 'pbo', alternative = 'less', alpha = 0.022, target_cp = 0.9, effect = 0.75) ## with a practical cap: when no event number through the cap reaches ## the target, D and achieved_cp are NA and target_reached is FALSE tr$eventNumberReestimationFromConditionalPower( 'interim', Surv(pfs, pfs_event) ~ arm, placebo = 'pbo', alternative = 'less', alpha = 0.022, target_cp = 0.9, effect = 'trend', D_cap = 500)
update the accrual rate of the recruitment curve at a milestone. The
enroller of a trial is always StaggeredRecruiter; this function
replaces its accrual_rate for patients not yet enrolled, while
enrolled patients are left unchanged. This function can be used in
adaptive designs; its application includes, but is not limited to,
revising recruitment after dose selection or enrichment, or pausing
recruitment for a period after an interim decision.
end_time in accrual_rate is measured from the time this
function is called (i.e., the current milestone), not from the start of
the trial. A milestone is usually event driven, so its calendar time is
unknown until the trial is simulated, and a schedule on the calendar time
scale could not be pre-specified. Measuring end_time from the
milestone also lets users state the new plan simply as "from now on":
e.g., data.frame(end_time = c(3, Inf), piecewise_rate = c(20, 35))
means 20 patients per month for the 3 months following the milestone and
35 per month thereafter, whenever the milestone occurs. Following the
convention of StaggeredRecruiter, the first re-planned patient is
enrolled 1/piecewise_rate after the milestone; a leading window
with piecewise_rate = 0 defers enrollment further. As with other
adaptations, patients not yet enrolled are re-randomized and their data
are regenerated under the new schedule.
Note that this function should only be called within action functions of
milestones. It is users' responsibility to ensure that and
TrialSimulator has no way to track it. Calling it before any
milestone has been triggered is an error.
This is a user-friendly wrapper of the member function of trial, i.e.,
Trials$update_accrual_rate(), which is used in vignettes. Users
who are not familiar with the concept of classes may consider using this
wrapper directly.
update_accrual_rate(trial, accrual_rate)update_accrual_rate(trial, accrual_rate)
trial |
a trial object returned by |
accrual_rate |
a data frame of columns |
no return value, called for its side effect of updating trial.
update endpoint generator in an arm. This function can be useful in, e.g., enrichment design where generator is updated so that patients are enrolled from sub-population afterwards. This function can also be used when data model changes over time, i.e., generating data in a new way after a milestone. This function can be called multiple times to update generators of endpoints one by one.
Note that this function should only be called within action functions of
milestones. It is users' responsibility to ensure that and
TrialSimulator has no way to track it.
This is a user-friendly wrapper of the member function of trial, i.e.,
Trials$update_generator(), which is used in vignettes. Users who are
not familiar with the concept of classes may consider using this wrapper
directly.
update_generator(trial, arm_name, endpoint_name, generator, ...)update_generator(trial, arm_name, endpoint_name, generator, ...)
trial |
a trial object returned by |
arm_name |
character. Name of an arm. |
endpoint_name |
character. A vector of endpoint names whose generator is updated. |
generator |
a random number generation (RNG) function.
See generator of |
... |
optional arguments for generator. |
no return value, called for its side effect of updating trial.
update the trigger condition and/or the action of a not-yet-triggered milestone. The milestone to be updated is identified by its name, which cannot be changed. This function can be used in adaptive designs, e.g., when conditional power at an interim analysis is lower than expected, the final analysis can be postponed by increasing the target number of events in its triggering condition, or its triggering condition can be switched from a calendar time to an event count entirely.
The update is not applied immediately: it is queued and takes effect right after the current action function returns, before the next milestone is evaluated. The new trigger condition and action replace the old ones as a whole. Between simulation replicates the milestone is restored to its as-designed trigger condition and action, so every replicate starts from the original design. A milestone that has already been triggered cannot be updated.
Note that this function should only be called within action functions of
milestones. It is users' responsibility to ensure that and
TrialSimulator has no way to track it. Calling it before any
milestone has been triggered is an error. Also note that milestones must
trigger in their registration order: an updated triggering condition that
makes a later-registered milestone fire before an earlier one stops the
simulation with an error.
This is a user-friendly wrapper of the member function of trial, i.e.,
Trials$update_milestone(), which is used in vignettes. Users who
are not familiar with the concept of classes may consider using this
wrapper directly.
update_milestone(trial, name, when = NULL, action = NULL, ...)update_milestone(trial, name, when = NULL, action = NULL, ...)
trial |
a trial object returned by |
name |
character. Name of the milestone to be updated. It must be registered with the listener and not yet triggered. |
when |
(optional) new triggering condition, an object returned by
|
action |
(optional) new action function. See |
... |
(optional) named arguments of the new |
no return value, called for its side effect of updating trial.
update sample ratios of arms. This could be called after an arm is added or removed. Sample ratios can be updated for any existing arms.
This is a user-friendly wrapper of the member function of trial, i.e.,
Trials$update_sample_ratio(), which is used in vignettes.
Users who are not familiar with the concept of classes may consider using
this wrapper directly.
update_sample_ratio(trial, arm_names, sample_ratios)update_sample_ratio(trial, arm_names, sample_ratios)
trial |
a trial object returned by |
arm_names |
character vector. Name of arms. |
sample_ratios |
numeric vector. New sample ratios of arms. If sample
ratio is a whole number, the permuted block randomization is adopted;
otherwise, |
no return value, called for its side effect of updating trial.
Fit scale and shape parameters of the Weibull distribution to match dropout rates at two specified time points. Weibull distribution can be used as a dropout distribution because it has two parameters.
Note that It is users' responsibility to assure that the units of dropout time, readout of non-tte endpoints, and trial duration are consistent.
weibullDropout(time, dropout_rate)weibullDropout(time, dropout_rate)
time |
a numeric vector of two time points at which dropout rates are specified. |
dropout_rate |
a numeric vector of dropout rates at |
a named vector for scale and shape parameters.
## dropout rates are 8% and 18% at time 12 and 18. weibullDropout(time = c(12, 18), dropout_rate = c(.08, .18))## dropout rates are 8% and 18% at time 12 and 18. weibullDropout(time = c(12, 18), dropout_rate = c(.08, .18))