| Trials | R Documentation |
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., set_duration(trial, ...)
for trial$set_duration(...).
$set_duration() set duration of a trial. This function can be
used to extend duration under adaptive designs.
$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 reset to NULL in 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_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).
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() 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, duration, description = name, seed = NULL, enroller = StaggeredRecruiter, dropout = NULL, stratification_factors = NULL, 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.
durationNumeric. Trial duration. 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.
silentlogical. TRUE to mute messages. However, warning
message is still displayed.
...(optional) arguments of enroller and dropout.
set_duration()set trial duration in an adaptive designed trial. All patients enrolled before resetting the duration are truncated (non-tte endpoints) or censored (tte endpoints) at the original duration. Remaining patients are re-randomized. New duration must be longer than the old one.
Trials$set_duration(duration)
durationnew duration of a trial. It must be greater than the current duration.
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 by
the time of arm removal.
Trials$remove_arms(arms_name)
arms_namecharacter vector. Name of arms to be removed.
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-/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), 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 = 0)
...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. If 0 (default), follow-up stops 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 arbitrary (number of) objects into a trial so that users can use those to control the workflow. Most common use case is to store simulation parameters to be used in action functions.
Trials$save_custom_data(value, name, overwrite = FALSE)
valuevalue to be saved. Any type.
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_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
CP = \Phi\left(\frac{\Phi^{-1}(\alpha) - \sqrt{d/D}\,z -
\theta\sqrt{\omega D}\,(1 - d/D)}{\sqrt{1 - d/D}}\right)
under alternative = 'less' (mirrored for 'greater'),
where \theta is the log hazard ratio at which conditional
power is evaluated and \omega = r/(1+r)^2 with r the
allocation ratio of the pair recorded when the milestone's data was
locked. Like z and d, r 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, duration = 40,
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
CP(D) \ge \gamma,
where \gamma is target_cp and CP(D) 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, duration = 40,
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.
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 events will be counted.
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 trial duration.
...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()
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, duration = 40,
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, duration = 40,
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 the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.