In this vignette, we walk through the simulation of a two-stage
adaptive enrichment design with TrialSimulator. The idea
behind the design is simple. A baseline biomarker splits the patients
into a biomarker-positive subgroup and the rest, and we suspect that the
treatment works mainly in the subgroup. At an interim analysis, we
compute the conditional power for the full population and for the
subgroup, and the pair of numbers tells us what to do next: stop for
futility, carry on as planned, enroll biomarker-positive patients only
from then on (enrichment), or enlarge the trial (promising zone). The
two stages are analyzed separately and combined with the inverse normal
method, and a closed test with Simes’ test for the intersection
hypothesis keeps the family-wise error rate under control across the
full-population and subgroup hypotheses.
Most of the building blocks appear in other vignettes. Conditional power and event number reassessment are explained in Conditional power and event number reassessment, and the adaptation functions in Action function. Here we put them together.
pbo) and a treatment arm
(trt), randomized 1:1.biomarker is measured at
randomization, with a prevalence of 40%. Patients with
biomarker = 1 form the subgroup of interest.PFS). It is exponential, with a median that depends on the
arm and on the biomarker:
PFS
events have been observed in the whole trial and the stage 1 cohort has
reached 65 events. Keep in mind that this is only the plan; the
condition is replaced when the trial is adapted at the interim, as
described next.PFS events have
been observed in the stage 1 cohort. Let \(CP_{full}\) be the conditional power for
the full population given the planned 240 events, and \(CP_{sub}\) the conditional power for the
subgroup given 150 events in the subgroup. Both are computed under the
trend observed at the interim, at the one-sided level \(\alpha = 0.025\) of the final test. The
five decision zones are
PFS events
have been observed in the stage 1 cohort. Follow-up of that cohort stops
there, which makes the stage 2 data independent of the stage 1 data. We
compute one-sided logrank p-values for the full population (\(p_{F,1}\)) and for the subgroup (\(p_{S,1}\)), and Simes’ p-value \(p_{FS,1} = \min\{2\min(p_{F,1}, p_{S,1}),
\max(p_{F,1}, p_{S,1})\}\) for the intersection hypothesis.Since the distribution of PFS depends on the biomarker,
we generate the two together with a custom generator. A generator
returns a data frame with one column per endpoint. For a time-to-event
endpoint, an extra column with the suffix _event holds the
event indicator. It is always 1 here because the generator produces
event times; censoring is handled by TrialSimulator at data
locks, at adaptations and through dropout. The biomarker is declared
with type = 'baseline', a non-time-to-event endpoint
observed at randomization, so it needs no readout time.
rng <- function(n, median0, median1, prevalence){
biomarker <- rbinom(n, size = 1, prob = prevalence)
pfs <- rexp(n, rate = log(2) / ifelse(biomarker == 1, median1, median0))
data.frame(biomarker = biomarker, pfs = pfs, pfs_event = 1)
}One call of endpoint() gives each arm both of its
endpoints. We will meet these generators again at enrichment, when they
are swapped for versions that only produce biomarker-positive
patients.
pbo_endpoints <- endpoint(name = c('biomarker', 'pfs'),
type = c('baseline', 'tte'),
generator = rng, median0 = 6, median1 = 6,
prevalence = .4)
pbo <- arm(name = 'pbo')
pbo$add_endpoints(pbo_endpoints)
trt_endpoints <- endpoint(name = c('biomarker', 'pfs'),
type = c('baseline', 'tte'),
generator = rng, median0 = 6.5, median1 = 10,
prevalence = .4)
trt <- arm(name = 'trt')
trt$add_endpoints(trt_endpoints)The trial starts with 320 planned patients and a piecewise constant
accrual rate. Note that there is no end date here: the trial ends when
its last milestone is triggered, and the milestones are defined below.
We fix a seed so that the single run shown later can be reproduced. In a
real simulation study, leave seed = NULL and let
TrialSimulator pick and record a seed for every
replicate.
accrual_rate <- data.frame(end_time = c(6, Inf), piecewise_rate = c(6, 12))
trial <- trial(name = 'enrichment', n_patients = 320, seed = 97,
enroller = StaggeredRecruiter, accrual_rate = accrual_rate,
dropout = rexp, rate = -log(1 - .05) / 12, ## 5% by month 12
silent = TRUE)
trial$add_arms(sample_ratio = c(1, 1), pbo, trt)The trial has three milestones, named interim,
stage 1 analysis and final, and all three are
event-driven. The conditions of interim and
stage 1 analysis count events in the stage 1 cohort only,
through the subset condition patient_id <= 80.
Triggering conditions like these, and the way they can be filtered and
combined with & and |, are described in
the vignette Condition System for
Triggering Milestones in a Trial.
The condition of final deserves a closer look. It asks
for 240 events in the whole trial, as planned, but also for 65 events in
the stage 1 cohort, which is the condition of
stage 1 analysis. The second part looks redundant, since 65
events in a cohort of 80 patients will normally be reached long before
240 events in the whole trial. It is there for a different reason:
TrialSimulator triggers milestones in the order in which
they are registered, and a milestone whose condition is met before an
earlier one raises an error. Repeating the stage 1 condition inside
final guarantees that final can never fire
before stage 1 analysis, whatever the accrual pattern in a
particular replicate. We keep this part in every condition the interim
action installs later, for the same reason. The 240 events, on the other
hand, are only the plan; the action function of interim
replaces that part whenever the trial is adapted.
Every milestone comes with an action function, which we write in the following subsections; see the vignette Action function for what an action function can do. The milestone calls below are only displayed at this point; they are run once the action functions exist.
interim <- milestone(name = 'interim', action = interim_action,
when = eventNumber(endpoint = 'pfs', n = 45, patient_id <= 80))
stage1 <- milestone(name = 'stage 1 analysis', action = stage1_action,
when = eventNumber(endpoint = 'pfs', n = 65, patient_id <= 80))
final <- milestone(name = 'final', action = final_action,
when = eventNumber(endpoint = 'pfs', n = 240) &
eventNumber(endpoint = 'pfs', n = 65, patient_id <= 80))
listener <- listener(silent = TRUE)
listener$add_milestones(interim, stage1, final)The action function of interim starts by computing the
two conditional powers with trial$conditionalPower(). The
subset condition patient_id <= 80 restricts the
calculation to the stage 1 cohort, and biomarker == 1
narrows it further to the subgroup. The argument D is the
number of events we expect at the final analysis, alpha is
the one-sided level of the final test, and effect = 'trend'
means the hazard ratio observed at the interim is carried forward.
What happens next depends on the zone. In the futility zone the trial
stops. TrialSimulator has no dedicated method for stopping
a trial early, because a trial simply ends when its last milestone is
triggered. So we stop the trial with
trial$update_milestone(), moving the two remaining
milestones to the current calendar time. Both are then triggered right
after the interim, enrollment stops there, and their action functions,
written below, skip the analyses when they see a futility decision.
In the enrichment zone we make four adaptations:
trial$eventNumberReestimationFromConditionalPower()
finds the smallest number of events in the subgroup that reaches the
target conditional power, subject to the cap D_cap;trial$resize() raises the maximum sample size so that
280 biomarker-positive patients are enrolled in total;trial$update_accrual_rate() slows accrual down, because
screening for the biomarker takes time;trial$update_generator() replaces the generators of
both arms by the same generator with prevalence = 1, so
that every patient enrolled from now on is biomarker-positive.Finally, trial$update_milestone() rewrites the
triggering condition of the final analysis around the re-estimated
number of events in the subgroup. In the promising zone, only the sample
size and the total number of events change. Either way, the new
condition keeps asking for 65 events in the stage 1 cohort, which
guarantees that the stage 1 analysis always comes before the final
analysis.
interim_action <- function(trial){
locked_data <- trial$get_locked_data('interim')
cp_full <- trial$conditionalPower(
milestone = 'interim', Surv(pfs, pfs_event) ~ arm, placebo = 'pbo',
alternative = 'less', alpha = .025, D = 240, effect = 'trend',
patient_id <= 80)$cp
cp_sub <- trial$conditionalPower(
milestone = 'interim', Surv(pfs, pfs_event) ~ arm, placebo = 'pbo',
alternative = 'less', alpha = .025, D = 150, effect = 'trend',
patient_id <= 80 & biomarker == 1)$cp
decision <- if(cp_full < .01 && cp_sub < .01){
'futility'
}else if(cp_full < .2 && cp_sub < .6){
'unfavorable'
}else if(cp_full < .2){
'enrichment'
}else if(cp_full < .9){
'promising'
}else{
'favorable'
}
if(decision == 'futility'){
## stop the trial now: both remaining milestones are triggered at the
## current time, and their action functions skip the analyses
now <- trial$get_current_time()
trial$update_milestone(name = 'stage 1 analysis',
when = calendarTime(time = now))
trial$update_milestone(name = 'final',
when = calendarTime(time = now))
}else if(decision == 'enrichment'){
en <- trial$eventNumberReestimationFromConditionalPower(
milestone = 'interim', Surv(pfs, pfs_event) ~ arm, placebo = 'pbo',
alternative = 'less', alpha = .025, target_cp = .95, effect = 'trend',
patient_id <= 80 & biomarker == 1, D_cap = 200)
max_events_in_subgroup <- ifelse(en$target_reached, en$D, 200)
max_patients_in_subgroup <- 280
enrolled_in_subgroup <- sum(locked_data$biomarker)
trial$resize(n_patients = nrow(locked_data) +
max_patients_in_subgroup - enrolled_in_subgroup)
trial$update_accrual_rate(
accrual_rate = data.frame(end_time = Inf, piecewise_rate = 5))
trial$update_generator(
arm_name = 'pbo', endpoint_name = c('pfs', 'biomarker'),
generator = rng, median0 = 6, median1 = 6, prevalence = 1.0)
trial$update_generator(
arm_name = 'trt', endpoint_name = c('pfs', 'biomarker'),
generator = rng, median0 = 6.5, median1 = 10, prevalence = 1.0)
trial$update_milestone(
name = 'final',
when = eventNumber(endpoint = 'pfs', n = max(max_events_in_subgroup, 50),
biomarker == 1) &
eventNumber(endpoint = 'pfs', n = 65, patient_id <= 80))
}else if(decision == 'promising'){
en <- trial$eventNumberReestimationFromConditionalPower(
milestone = 'interim', Surv(pfs, pfs_event) ~ arm, placebo = 'pbo',
alternative = 'less', alpha = .025, target_cp = .95, effect = 'trend',
patient_id <= 80, D_cap = 400)
max_events <- ifelse(en$target_reached, en$D, 400)
trial$resize(n_patients = 480)
trial$update_milestone(
name = 'final',
when = eventNumber(endpoint = 'pfs', n = max_events) &
eventNumber(endpoint = 'pfs', n = 65, patient_id <= 80))
}
## 'unfavorable' and 'favorable': no adaptation
trial$save(value = decision, name = 'decision')
trial$save(value = cp_full, name = 'cp_full')
trial$save(value = cp_sub, name = 'cp_subgroup')
}The first thing the action function of stage 1 analysis
does is to call
trial$stop_followup(patient_id <= 80, additional_followup = 0),
which freezes the stage 1 cohort at the milestone time. From now on
these patients look the same in every data lock, and that is exactly
what makes the stage 2 p-values independent of the stage 1 p-values. The
three stage 1 p-values are then saved with trial$save(),
ready to be picked up at the final analysis. Note that
fitLogrank() accepts subset conditions such as
patient_id <= 80 too.
If the trial has already stopped for futility, the action simply
returns without saving anything. There is no need to save
NA placeholders to keep the output rectangular —
this is a feature of TrialSimulator working for you: when
the outputs of the replicates are combined, a column that a replicate
never saved is filled with NA in that replicate’s row
automatically. An action function only records what its replicate
actually computed, and the summary still sees the same columns in every
row.
stage1_action <- function(trial){
if(trial$get_output('decision') == 'futility'){
return(invisible(NULL))
}
trial$stop_followup(patient_id <= 80, additional_followup = 0)
locked_data <- trial$get_locked_data('stage 1 analysis')
pf1 <- fitLogrank(Surv(pfs, pfs_event) ~ arm, placebo = 'pbo',
data = locked_data, alternative = 'less',
patient_id <= 80)$p
ps1 <- fitLogrank(Surv(pfs, pfs_event) ~ arm, placebo = 'pbo',
data = locked_data, alternative = 'less',
patient_id <= 80 & biomarker == 1)$p
trial$save(value = pf1, name = 'pf1')
trial$save(value = ps1, name = 'ps1')
trial$save(value = min(2 * min(pf1, ps1), max(pf1, ps1)), name = 'simes1')
}At the final analysis, we fetch the values saved earlier in the same
replicate with trial$get_output(), and compute the stage 2
p-values from the stage 2 cohort only (patient_id > 80).
After that, the combination and the closed test are a few lines of
arithmetic.
Under futility the action again returns early without saving
NA placeholders for the stage 2 p-values — missing columns
are filled with NA automatically, as explained at the stage
1 analysis. The two rejection indicators are a different
matter: a trial stopped for futility rejects nothing, so
FALSE is their true value, not a placeholder. Saving
FALSE explicitly is what keeps
mean(reject_full) in the summary an unconditional
rejection rate. Had these two columns been left NA, the
rates would silently have been computed among continuing trials
only.
final_action <- function(trial){
locked_data <- trial$get_locked_data('final')
decision <- trial$get_output('decision')
trial$save(value = sum(locked_data$biomarker), name = 'n_positive')
if(decision == 'futility'){
trial$save(value = FALSE, name = 'reject_full')
trial$save(value = FALSE, name = 'reject_subgroup')
return(invisible(NULL))
}
pf1 <- trial$get_output('pf1')
ps1 <- trial$get_output('ps1')
simes1 <- trial$get_output('simes1')
if(decision == 'enrichment'){
ps2 <- fitLogrank(Surv(pfs, pfs_event) ~ arm, placebo = 'pbo',
data = locked_data, alternative = 'less',
patient_id > 80 & biomarker == 1)$p
pf2 <- 1
simes2 <- ps2
}else{
pf2 <- fitLogrank(Surv(pfs, pfs_event) ~ arm, placebo = 'pbo',
data = locked_data, alternative = 'less',
patient_id > 80)$p
ps2 <- fitLogrank(Surv(pfs, pfs_event) ~ arm, placebo = 'pbo',
data = locked_data, alternative = 'less',
patient_id > 80 & biomarker == 1)$p
simes2 <- min(2 * min(pf2, ps2), max(pf2, ps2))
}
trial$save(value = pf2, name = 'pf2')
trial$save(value = ps2, name = 'ps2')
trial$save(value = simes2, name = 'simes2')
w1 <- sqrt(65 / 240)
w2 <- sqrt(175 / 240)
combine <- function(p1, p2){
1 - pnorm(w1 * qnorm(1 - p1) + w2 * qnorm(1 - p2))
}
pf <- combine(pf1, pf2)
ps <- combine(ps1, ps2)
simes <- combine(simes1, simes2)
trial$save(value = (simes <= .025 && pf <= .025), name = 'reject_full')
trial$save(value = (simes <= .025 && ps <= .025), name = 'reject_subgroup')
}Now that the three action functions exist, we run the milestone calls shown at the beginning of this section and register the milestones with a listener.
Let’s run the trial once. With the seed set above, the interim lands
in the enrichment zone. The event plot shows the cumulative counts by
arm over calendar time, with the three milestones as dashed lines. The
biomarker panel is really the enrollment curve, since a
baseline endpoint is observed as soon as a patient is randomized. It
bends at the interim, where accrual slows down to biomarker-positive
patients only. In this particular replicate the re-estimated number of
subgroup events is reached while enrollment is still going on, so the
final analysis fires before the enlarged sample size is used up. That is
by design: the final condition is a number of events, and the sample
size is only an upper bound.
The output of the replicate collects the milestone times, the number of patients and events at each milestone, and everything we saved in the action functions. Here the subgroup hypothesis is rejected at the final analysis and the full-population hypothesis is not, which is the only possible outcome under enrichment.
controller$get_output() %>%
kable(escape = FALSE) %>%
kable_styling(bootstrap_options = "striped",
full_width = FALSE,
position = "left") %>%
scroll_box(width = "100%")| trial | seed | milestone_time_<interim> | n_events_<interim>_<pfs> | n_events_<interim>_<biomarker> | n_events_<interim>_<patient_id> | n_events_<interim>_<arms> | decision | cp_full | cp_subgroup | milestone_time_<stage 1 analysis> | n_events_<stage 1 analysis>_<pfs> | n_events_<stage 1 analysis>_<biomarker> | n_events_<stage 1 analysis>_<patient_id> | n_events_<stage 1 analysis>_<arms> | pf1 | ps1 | simes1 | milestone_time_<final> | n_events_<final>_<pfs> | n_events_<final>_<biomarker> | n_events_<final>_<patient_id> | n_events_<final>_<arms> | n_positive | pf2 | ps2 | simes2 | reject_full | reject_subgroup | error_message |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| enrichment | 97 | 13.33963 | 54 | 124 | 124 | c(“pbo”,…. | enrichment | 0.0104861 | 0.9936771 | 20.41128 | 100 | 159 | 159 | c(“pbo”,…. | 0.3237104 | 0.014804 | 0.0296079 | 33.6292 | 159 | 225 | 225 | c(“pbo”,…. | 150 | 1 | 0.0230686 | 0.0230686 | FALSE | TRUE |
To study the operating characteristics of the design, we run many
replicates. The code below was run with 10000 replicates under the
alternative described in the settings, and with another 10000 replicates
under the global null, where the median of PFS in the
treatment arm equals the placebo median of 6 months in both biomarker
strata. The results are stored in the package, so the vignette does not
have to rerun them.
## reset a controller if $run has been executed before
controller$reset()
controller$run(n = 10000, plot_event = FALSE, silent = TRUE, tidy = TRUE)
output <- controller$get_output()output %>%
filter(scenario == 'alternative') %>%
select(-scenario) %>%
head(5) %>%
kable(escape = FALSE) %>%
kable_styling(bootstrap_options = "striped",
full_width = FALSE,
position = "left") %>%
scroll_box(width = "100%")| seed | milestone_time_<interim> | n_events_<interim>_<pfs> | n_events_<interim>_<biomarker> | n_events_<interim>_<patient_id> | decision | cp_full | cp_subgroup | milestone_time_<stage 1 analysis> | n_events_<stage 1 analysis>_<pfs> | n_events_<stage 1 analysis>_<biomarker> | n_events_<stage 1 analysis>_<patient_id> | pf1 | ps1 | simes1 | milestone_time_<final> | n_events_<final>_<pfs> | n_events_<final>_<biomarker> | n_events_<final>_<patient_id> | n_positive | pf2 | ps2 | simes2 | reject_full | reject_subgroup |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 1716968512 | 13.24 | 48 | 122 | 122 | promising | 0.3478 | 0.0086 | 22.90 | 133 | 238 | 238 | 0.25950 | 0.146400 | 0.259500 | 49.61 | 400 | 480 | 480 | 198 | 0.5680000 | 0.0066820 | 0.0133600 | FALSE | TRUE |
| 1151366066 | 14.92 | 67 | 143 | 143 | promising | 0.7659 | 0.9915 | 20.01 | 117 | 204 | 204 | 0.01271 | 0.003335 | 0.006669 | 48.36 | 400 | 480 | 480 | 191 | 0.0001214 | 0.0018620 | 0.0002429 | TRUE | TRUE |
| 473618302 | 15.09 | 57 | 145 | 145 | promising | 0.4032 | 0.0200 | 23.51 | 128 | 246 | 246 | 0.37890 | 0.464100 | 0.464100 | 49.38 | 400 | 480 | 480 | 196 | 0.0000168 | 0.0000095 | 0.0000168 | TRUE | TRUE |
| 1521899696 | 13.80 | 55 | 129 | 129 | favorable | 0.9999 | 1.0000 | 28.75 | 186 | 308 | 308 | 0.04332 | 0.042350 | 0.043320 | 35.38 | 240 | 320 | 320 | 121 | 0.0066500 | 0.0185700 | 0.0133000 | TRUE | TRUE |
| 1786717175 | 12.28 | 48 | 111 | 111 | promising | 0.8714 | 0.9987 | 24.80 | 148 | 261 | 261 | 0.08308 | 0.022370 | 0.044750 | 42.15 | 329 | 469 | 469 | 171 | 0.0545000 | 0.0006147 | 0.0012290 | TRUE | TRUE |
Under the alternative, here is how often each zone is reached, and how the adaptations change the duration and the size of the trial. In the futility zone the trial ends at the interim, which is why the three milestone times coincide there.
output %>%
filter(scenario == 'alternative') %>%
group_by(decision) %>%
summarise(
zone = n() / 100,
time_interim = mean(`milestone_time_<interim>`),
time_stage1 = mean(`milestone_time_<stage 1 analysis>`),
time_final = mean(`milestone_time_<final>`),
n_final = mean(`n_events_<final>_<patient_id>`),
n_positive_final = mean(n_positive),
events_final = mean(`n_events_<final>_<pfs>`)
) %>%
kable(col.names = c('Zone', 'Zone (%)', 'Interim', 'Stage 1', 'Final',
'Patients', 'Positive Patients', 'PFS Events'),
digits = 1, align = 'r',
caption = 'Decision Zones, Milestone Times, and Trial Size at the Final Analysis') %>%
add_header_above(c(' ' = 2, 'Time (Months)' = 3, 'At Final Analysis' = 3)) %>%
kable_styling(full_width = TRUE)| Zone | Zone (%) | Interim | Stage 1 | Final | Patients | Positive Patients | PFS Events |
|---|---|---|---|---|---|---|---|
| futility | 7.5 | 14.2 | 14.2 | 14.2 | 134.3 | 53.4 | 56.4 |
| unfavorable | 17.3 | 14.3 | 23.5 | 34.9 | 320.0 | 127.7 | 240.0 |
| enrichment | 14.0 | 14.3 | 23.7 | 43.3 | 279.5 | 197.7 | 206.8 |
| promising | 31.6 | 14.3 | 23.8 | 49.1 | 477.5 | 191.0 | 389.5 |
| favorable | 29.6 | 14.4 | 24.1 | 34.9 | 320.0 | 128.3 | 240.0 |
Power is summarized by zone and overall. The three columns “Full Only”, “Subgroup Only” and “Both” are mutually exclusive outcomes, and “Any”, their sum, is the probability of rejecting at least one of the two hypotheses.
power_table <- function(dat){
dat %>%
summarise(
zone = n() / 100,
reject_full_only = 100 * mean(reject_full & !reject_subgroup),
reject_subgroup_only = 100 * mean(!reject_full & reject_subgroup),
reject_both = 100 * mean(reject_full & reject_subgroup),
reject_any = 100 * mean(reject_full | reject_subgroup)
)
}
alt <- output %>% filter(scenario == 'alternative')
bind_rows(
alt %>% group_by(decision) %>% power_table(),
alt %>% power_table() %>% mutate(decision = 'overall')
) %>%
kable(col.names = c('Zone', 'Zone (%)', 'Full Only', 'Subgroup Only',
'Both', 'Any'),
digits = 1, align = 'r',
caption = 'Power (%) under the Alternative') %>%
add_header_above(c(' ' = 2, 'Rejected Hypotheses (%)' = 4)) %>%
kable_styling(full_width = TRUE)| Zone | Zone (%) | Full Only | Subgroup Only | Both | Any |
|---|---|---|---|---|---|
| futility | 7.5 | 0.0 | 0.0 | 0.0 | 0.0 |
| unfavorable | 17.3 | 3.3 | 15.5 | 24.8 | 43.6 |
| enrichment | 14.0 | 0.0 | 77.8 | 0.0 | 77.8 |
| promising | 31.6 | 2.2 | 13.3 | 70.5 | 86.0 |
| favorable | 29.6 | 4.6 | 11.8 | 60.6 | 77.0 |
| overall | 100.0 | 2.6 | 21.2 | 44.5 | 68.4 |
Under the global null, the probability of rejecting at least one hypothesis is the family-wise error rate. The closed test with the inverse normal combination keeps it at the one-sided level 0.025 in the strong sense, whatever we decide at the interim. The rates within a zone, however, are conditional on the interim decision and are not controlled one by one. They are large in the favorable zone precisely because that zone is reached when the interim data happen to look good. Only the overall rate matters. With 10000 replicates, the Monte Carlo standard error of the overall rate is about 0.16 percentage points.
null <- output %>% filter(scenario == 'null')
bind_rows(
null %>% group_by(decision) %>% power_table(),
null %>% power_table() %>% mutate(decision = 'overall')
) %>%
kable(col.names = c('Zone', 'Zone (%)', 'Full Only', 'Subgroup Only',
'Both', 'Any'),
digits = 1, align = 'r',
caption = 'Type I Error (%) under the Global Null') %>%
add_header_above(c(' ' = 2, 'Rejected Hypotheses (%)' = 4)) %>%
kable_styling(full_width = TRUE)| Zone | Zone (%) | Full Only | Subgroup Only | Both | Any |
|---|---|---|---|---|---|
| futility | 33.3 | 0.0 | 0.0 | 0.0 | 0.0 |
| unfavorable | 27.8 | 0.4 | 0.5 | 0.3 | 1.2 |
| enrichment | 8.7 | 0.0 | 3.5 | 0.0 | 3.5 |
| promising | 21.0 | 1.1 | 1.3 | 1.3 | 3.7 |
| favorable | 9.2 | 2.4 | 2.2 | 2.8 | 7.4 |
| overall | 100.0 | 0.6 | 0.9 | 0.6 | 2.1 |