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_timeof 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 callingget_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 callingget()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 callingget()anytime.$get()retrieve intermediate results saved by calling functionssave_custom_data()orbind().$get_output()retrieve intermediate results saved by calling functionsave().
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 aregimenobject 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.
Value
an R6Class generator object; use trial() to create a trial.
Methods
Method new()
initialize a trial
Usage
Trials$new(
name,
n_patients,
description = name,
seed = NULL,
enroller = StaggeredRecruiter,
dropout = NULL,
stratification_factors = NULL,
silent = FALSE,
...
)Arguments
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 theseedcolumn in$get_output(). Setting it to beNULLis recommended. For debugging, set it to a specific integer.enrollerenrollment-time generator. Must be
StaggeredRecruiter(the default); any other value is rejected. Supply itsaccrual_ratevia.... See?TrialSimulator::StaggeredRecruiter. Kept (rather than dropped) for backward compatibility, so existing code that passesenroller = StaggeredRecruiterexplicitly 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. Usuallyrexpif dropout rate is set at a single time point, orrweibullif 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()withreadout = 0. As a natural assumption for randomized trial,TrialSimulatorassumes that the baseline characteristics share the same distribution across arms, but endpoints can have same or different distributions given baseline characteristics.NULLby default, i.e., unstratified permuted block randomization is executed.silentlogical.
TRUEto mute messages. However, warning message is still displayed....(optional) arguments of
enrolleranddropout.
Method 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.
Method 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.
Method 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.
Arguments
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.
Method update_generator()
update endpoint generator in an arm
Arguments
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
generatorofendpoint()....optional arguments for
generator.
Method 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.
Arguments
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 afterTrials$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 runTrials$add_arm(sample_ratio1, arm1)followed byTrials$add_arm(sample_ratio2, arm2). We would expected similar result withTrials$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.
Method 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)).
Arguments
whata function selecting which eligible patients crossover and to what
new_treatment(NA= no crossover). Seeregimen(). Values ofnew_treatmentmust not contain'@'or';', which are reserved for encodingregimen_trajectory.howa function returning the modified post-switch endpoint values.
when(optional) a function returning
switch_timefrom enrollment. IfNULL(default), patients switch atT(switch_time = earliest_crossover_time_from_enrollment).delaynumeric. Time after the milestone before crossover opens;
T = get_current_time() + delay. Default0....(optional) named arguments routed to
what,when, and/orhow.
Method 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.
Arguments
...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.
Method 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.
Method 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.
Arguments
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|. IfNULL, the triggering condition is left unchanged.action(optional) new action function. See
actionofmilestone(). IfNULL, the action is left unchanged....(optional) named arguments of the new
action. Only allowed whenactionis provided. The new action is executed with exactly the arguments supplied here: fixed arguments of the previous action are never carried over.
Method 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.
Method 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.
Arguments
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
valueis a scalar. Ifvalueis a data frame,namewill be the prefix pasted with the column name ofvaluein trial's output. If user want to usevalue's column name as is in trial's output, setnameto be''as default. Otherwise, column name would be, e.g.,"{<name>}_<{colnames(value)}>".overwritelogic.
TRUEif overwriting existing entries with warning, otherwise, throwing an error and stop.
Method 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.
Arguments
colscolumns to be returned from
Trial$output. IfNULL, all columns are returned.simplifylogical. Return value rather than a data frame of one column when
length(col) == 1andsimplify == TRUE.tidylogical.
TrialSimulatorautomatically records a set of standard outputs at milestones, even whendoNothingis 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 (seevignette('actionFunctions')). This usually mean a large number of columns in outputs. If users have no intent to summarize a trial on these columns, settingtidy = TRUEcan eliminate these columns fromget_output(). Note that currently we use regex"^n_events_<.*?>_<.*?>$"and"^milestone_time_<.*?>$"to match columns to be eliminated. If users plan to usetidy = TRUE, caution is needed when naming custom outputs insave(). DefaultFALSE.
Method 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.
Method 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.
Method get_custom_data()
return custom data saved by calling Trials$save_custom_data()
or Trials$bind() with its name.
Method get()
alias of function get_custom_data to make it short and cool.
Method 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)
Method dunnettTest()
carry out closed test based on Dunnett method under group sequential design.
Usage
Trials$dunnettTest(
formula,
placebo,
treatments,
milestones,
alternative,
planned_info,
...
)Arguments
formulaAn object of class
formulathat can be used withsurvival::coxph. Must consistarmand endpoint indata. No covariate is allowed. Stratification variables are supported and can be added usingstrata(...).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
milestonesdoes 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.,
milestonesare row names ofplanned_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_infocan also be a character"default"so thatplanned_infoare 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 pickplanned_infois recommended in formal simulation study. Another issue withplanned_infoset to be"default"is that it is possible patient recruitment is done before a specific stage, as a result,planned_infois zero which can crash the program....subset condition that is compatible with
dplyr::filter.survdiffwill 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.
Details
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.
Returns
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.
Method closedTest()
perform closed test based on Dunnett test
Usage
Trials$closedTest(
dunnett_test,
treatments,
milestones,
alpha,
alpha_spending = c("asP", "asOF")
)Arguments
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
milestonesdoes 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.
Returns
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.
Examples
\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')
}
Method 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.
Arguments
milestonecharacter. Name of a triggered milestone at which the interim results are observed.
formulaan object of class
formulaas infitLogrank(), e.g.,Surv(pfs, pfs_event) ~ arm. Stratification viastrata(...)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. SeefitLogrank().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 withqnorm(alpha). Under a group sequential design it is implied by the alpha spending function, e.g.,1 - pnorm(c)for a final critical valuecon 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; otherwisealphamust be a named vector using treatment arm names, matching the names ofD. Entries are matched toDby 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
Dmust 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.Dandalphamust be of the same length and, when named, use the identical set of arm names. An error is raised if the observed number of eventsdof a comparison already reachesD.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 tofitLogrank().
Returns
a data frame with one row per treatment-vs-placebo
comparison, with columns arm, placebo, z,
d, D, info_fraction, alpha,
effect and cp.
Examples
\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)
}
Method 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).
Usage
Trials$eventNumberReestimationFromConditionalPower(
milestone,
formula,
placebo,
alternative,
alpha,
target_cp,
effect,
...,
D_cap = NULL
)Arguments
milestonecharacter. Name of a triggered milestone at which the interim results are observed.
formulaan object of class
formulaas infitLogrank(), e.g.,Surv(pfs, pfs_event) ~ arm. Stratification viastrata(...)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. SeefitLogrank().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; otherwisealphamust be a named vector using treatment arm names, matching the names oftarget_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).
alphaandtarget_cpmust be of the same length and, when named, use the identical set of arm names. Entries are matched toalphaby 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 tofitLogrank().D_capNULLor 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 observedd, orInffor an unbounded search. The defaultNULLis converted internally to scalarInffor one comparison or a named vector ofInffor multiple comparisons. An explicit scalarInfapplies to all comparisons; a finite unnamed scalar is accepted when a single treatment arm is compared with placebo; otherwiseD_capmust be a named vector over the same treatment arms asalphaandtarget_cp, in which individual entries may beInf. Placed after..., so it must always be passed by name.
Details
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
Dis returned with its conditional power andtarget_reached = TRUE;if no finite solution exists in the requested range,
Dandachieved_cpareNA, whiletarget_reached = FALSE.D_capcontinues 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.
Returns
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.
Examples
\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)
}
Method 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.
Arguments
regimenan object created by
regimen().
Method 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.
Method 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.
Method 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).
Usage
Trials$get_data_lock_time_by_event_number(
endpoints,
arms,
target_n_events,
type = c("all", "any"),
...
)Arguments
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.typeallif all target number of events are reached.anyif 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.
Method 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.
Usage
Trials$get_data_lock_time_by_enrollment(
arms,
target_n_patients,
min_treatment_duration,
...
)Arguments
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
...andarms. If positive, it means that milestone is triggered when a specific number of enrolled patients have received treatment for at leastmin_treatment_durationduration. It is users' responsibility to assure that the unit ofmin_treatment_durationare 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.
Method has_arm()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
check if the trial has any arm. Return TRUE or FALSE.
Method event_plot()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
plot of cumulative number of events/samples over calendar time.
Method mute()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
mute all messages (not including warnings)
Method 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.
Method 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.
Method make_arms_snapshot()
INTERNAL MACHINERY: DO NOT CALL THIS METHOD DIRECTLY.
make a snapshot of arms
Method 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.