Nothing
## ----include=FALSE--------------------------------------
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
dev = "svg",
fig.ext = "svg",
fig.width = 7.2916667,
fig.asp = 0.618,
fig.align = "center",
out.width = "80%"
)
options(width = 58)
## ----message=FALSE, warning=FALSE-----------------------
library(gsDesign)
library(gt)
library(tibble)
## -------------------------------------------------------
alpha <- 0.025
beta <- 0.1
ratio <- 3
ve0 <- 0.30
ve1 <- 0.80
hr0 <- 1 - ve0
hr1 <- 1 - ve1
seasonal_event_rate_control <- 0.003
season_length_months <- 6
season_length_years <- season_length_months / 12
dropout_6mo <- 0.10
n_seasons <- 3
stopifnot(n_seasons >= 2)
timing <- seq_len(n_seasons) / n_seasons
enrollment_months <- 2
off_enrollment_months <- 10
annual_cycle_months <- enrollment_months + off_enrollment_months
enroll_pattern <- c(rep(c(1, 0), n_seasons - 1), 1)
enroll_periods <- c(rep(c(enrollment_months, off_enrollment_months), n_seasons - 1), enrollment_months)
calendar_time <- enrollment_months + season_length_months +
annual_cycle_months * (seq_len(n_seasons) - 1)
test_lower <- rep(FALSE, n_seasons)
test_lower[1] <- TRUE
## -------------------------------------------------------
p_event_experimental <- function(ve, randomization_ratio) {
randomization_ratio / (randomization_ratio + 1 / (1 - ve))
}
p0 <- p_event_experimental(ve0, ratio)
p1 <- p_event_experimental(ve1, ratio)
c(p0 = p0, p1 = p1)
## -------------------------------------------------------
lambdaC <- c(-log(1 - seasonal_event_rate_control) / season_length_months, 0)
S <- season_length_months
eta <- -log(1 - dropout_6mo) / season_length_months
# Integer conversion can slightly adjust total enrollment so the final integer
# event target is achievable with the seasonal piecewise event-rate model.
design_calendar <- gsSurvCalendar(
test.type = 4, # Non-binding lower bound framework
alpha = alpha, # One-sided Type I error
beta = beta, # Type II error
calendarTime = calendar_time, # Analysis times in months
spending = "information", # Spending by information fraction
sfu = sfHSD, # Efficacy spending function
sfupar = 1, # Pocock-like efficacy spending
sfl = sfHSD, # Futility spending function
sflpar = -2, # Futility spending parameter
lambdaC = lambdaC, # Control hazard rate per month by period
S = S, # Event-rate period duration in months
hr = hr1, # Alternative hypothesis HR
hr0 = hr0, # Null hypothesis HR
eta = eta, # Dropout hazard rate per month
gamma = enroll_pattern, # Relative enrollment rates by period
R = enroll_periods, # Enrollment period durations in months
minfup = season_length_months, # Minimum follow-up for final enrollees
ratio = ratio, # Experimental:control randomization ratio
testLower = test_lower # Futility only at selected analyses
) |>
toInteger() |>
suppressWarnings()
gsBoundSummary(design_calendar)
planned_final_events <- design_calendar$n.I[design_calendar$k]
planned_counts <- as.integer(round(planned_final_events * timing))
planned_counts[n_seasons] <- planned_final_events
for (j in seq_along(planned_counts)[-1]) {
planned_counts[j] <- max(planned_counts[j], planned_counts[j - 1] + 1L)
}
design_exact <- toBinomialExact(design_calendar, observedEvents = planned_counts)
planned_enrollment_period <- as.numeric(rowSums(as.matrix(design_calendar$gamma))) *
as.numeric(design_calendar$R)
season_id <- rep(seq_len(n_seasons), each = 2, length.out = length(planned_enrollment_period))
planned_enrollment_by_season <- as.integer(round(tapply(planned_enrollment_period, season_id, sum)))
planned_enrollment_control <- as.integer(round(planned_enrollment_by_season / (1 + ratio)))
planned_enrollment_experimental <- planned_enrollment_by_season - planned_enrollment_control
planned_cum_enrollment <- cumsum(planned_enrollment_by_season)
## -------------------------------------------------------
target_alpha_spend <- design_calendar$upper$sf(
alpha = alpha,
t = timing,
param = design_calendar$upper$param
)$spend
achieved_alpha_spend <- cumsum(
gsBinomialExact(
k = design_exact$k, # Number of analyses
theta = design_exact$theta[1], # Null event probability in experimental arm
n.I = design_exact$n.I, # Cumulative events by analysis
a = design_exact$lower$bound, # Efficacy bounds (x <= a crosses)
b = design_exact$n.I + 1 # Non-binding upper bound for alpha-spend check
)$lower$prob[, 1]
)
achieved_power_h1 <- cumsum(
gsBinomialExact(
k = design_exact$k, # Number of analyses
theta = design_exact$theta[2], # Alternative event probability in experimental arm
n.I = design_exact$n.I, # Cumulative events by analysis
a = design_exact$lower$bound, # Efficacy bounds
b = design_exact$n.I + 1 # Non-binding upper bound for cumulative efficacy probability
)$lower$prob[, 1]
)
ve_from_bound <- function(x, n, ratio) {
out <- rep(NA_real_, length(x))
ok <- x >= 0 & x < n
if (any(ok)) {
p <- x[ok] / n[ok]
hr <- p / (ratio * (1 - p))
out[ok] <- 1 - hr
}
out
}
futility_active <- if (!is.null(design_calendar$testLower)) {
tl <- design_calendar$testLower
if (length(tl) == 1) tl <- rep(tl, design_exact$k)
as.logical(tl)
} else {
design_exact$upper$bound <= design_exact$n.I
}
nominal_p_futility <- rep(NA_real_, design_exact$k)
nominal_p_futility[futility_active] <- stats::pbinom(
q = design_exact$upper$bound[futility_active],
size = design_exact$n.I[futility_active],
prob = p0
)
tibble(
Season = seq_len(n_seasons),
`Spending time` = timing,
`Planned total events` = design_exact$n.I,
`Approx cumulative enrollment` = planned_cum_enrollment,
`Exact efficacy bound (x <= a)` = design_exact$lower$bound,
`VE at bound (efficacy)` = ve_from_bound(design_exact$lower$bound, design_exact$n.I, ratio),
`Nominal 1-sided p at bound (efficacy)` = stats::pbinom(design_exact$lower$bound, design_exact$n.I, p0),
`Exact futility bound (x >= b)` = ifelse(futility_active, design_exact$upper$bound, NA_integer_),
`VE at bound (futility)` = ve_from_bound(
ifelse(futility_active, design_exact$upper$bound, -1L),
design_exact$n.I,
ratio
),
`Nominal 1-sided p at bound (futility)` = nominal_p_futility,
`Target cumulative alpha spend` = target_alpha_spend,
`Achieved cumulative alpha spend` = achieved_alpha_spend,
`Cumulative power under H1` = achieved_power_h1
) |>
gt() |>
fmt_number(columns = 2, decimals = 3) |>
fmt_percent(columns = c(`VE at bound (efficacy)`, `VE at bound (futility)`), decimals = 1) |>
fmt_number(
columns = c(
`Nominal 1-sided p at bound (efficacy)`,
`Nominal 1-sided p at bound (futility)`,
`Target cumulative alpha spend`,
`Achieved cumulative alpha spend`,
`Cumulative power under H1`
),
decimals = 4
) |>
tab_spanner(
label = "Efficacy",
columns = c(
`Exact efficacy bound (x <= a)`,
`VE at bound (efficacy)`,
`Nominal 1-sided p at bound (efficacy)`
)
) |>
tab_spanner(
label = "Futility",
columns = c(
`Exact futility bound (x >= b)`,
`VE at bound (futility)`,
`Nominal 1-sided p at bound (futility)`
)
) |>
tab_header(
title = "Planned exact binomial seasonal monitoring",
subtitle = "Super-superiority example with Pocock-like efficacy spending"
) |>
tab_footnote(
footnote = "x denotes cumulative observed events in the experimental arm; efficacy is established when x is at or below the listed efficacy bound.",
locations = cells_column_labels(columns = `Exact efficacy bound (x <= a)`)
) |>
tab_footnote(
footnote = "Blank futility entries indicate no futility stopping boundary at that analysis.",
locations = cells_column_labels(columns = `Exact futility bound (x >= b)`)
)
## -------------------------------------------------------
enrollment_table <- tibble(
Season = as.character(seq_len(n_seasons)),
`Control planned enrollment` = planned_enrollment_control,
`Experimental planned enrollment` = planned_enrollment_experimental
) |>
dplyr::mutate(
`Total planned enrollment` = `Control planned enrollment` + `Experimental planned enrollment`,
`Cumulative planned enrollment` = cumsum(`Total planned enrollment`)
)
dplyr::bind_rows(
enrollment_table,
tibble(
Season = "Overall",
`Control planned enrollment` = sum(enrollment_table$`Control planned enrollment`),
`Experimental planned enrollment` = sum(enrollment_table$`Experimental planned enrollment`),
`Total planned enrollment` = sum(enrollment_table$`Total planned enrollment`),
`Cumulative planned enrollment` = sum(enrollment_table$`Total planned enrollment`)
)
) |>
gt() |>
tab_header(title = "Planned enrollment by season and overall")
## -------------------------------------------------------
x_offset_from_efficacy <- rep(0L, n_seasons)
x_offset_from_efficacy[min(2L, n_seasons)] <- 1L
example_x <- pmax(0L, design_exact$lower$bound + x_offset_from_efficacy)
example_p <- repeatedPValueBinomialExact(
gsD = design_calendar,
n.I = design_exact$n.I,
x = example_x
)
example_p
## -------------------------------------------------------
sequentialPValueBinomialExact(
gsD = design_calendar,
n.I = design_exact$n.I,
x = example_x
)
## -------------------------------------------------------
observed_counts_update <- c(
planned_counts[-n_seasons],
max(planned_counts[n_seasons - 1] + 1L, planned_counts[n_seasons] - 5L)
)
update_exact <- toBinomialExact(design_calendar, observedEvents = observed_counts_update)
update_exact_full <- toBinomialExact(
design_calendar,
observedEvents = observed_counts_update,
maxSpend = TRUE
)
tibble(
Analysis = seq_along(update_exact$n.I),
`Observed total events` = update_exact$n.I,
`Updated efficacy bound (x <= a), default spending` = update_exact$lower$bound,
`Updated efficacy bound (x <= a), maxSpend=TRUE` = update_exact_full$lower$bound,
`Updated futility bound, default spending` = update_exact$upper$bound,
`Updated futility bound, maxSpend=TRUE` = update_exact_full$upper$bound
) |>
gt() |>
tab_header(title = "Updated exact bounds using observedEvents")
## -------------------------------------------------------
ve_scenarios <- c(`H0 (VE=30%)` = ve0, `H1 (VE=80%)` = ve1)
planned_control_event_rates <- rep(seasonal_event_rate_control, length(ve_scenarios))
sim_light <- simBinomialSeasonalExact(
gsD = design_calendar,
ve = ve_scenarios,
nsim = rep(150, length(ve_scenarios)),
control_event_rate = planned_control_event_rates,
season_length = season_length_years,
dropout_rate = dropout_6mo,
planned_counts = planned_counts,
enroll_control_per_look = planned_enrollment_control,
enroll_experimental_per_look = planned_enrollment_experimental,
adaptive = c(FALSE, TRUE),
max_multiplier = 2,
final_full_spending = TRUE,
seed = 101
)
## -------------------------------------------------------
oc <- sim_light$summary |>
dplyr::mutate(
Scenario = ifelse(adaptive, paste0("Adaptive: ", scenario), paste0("Fixed: ", scenario))
) |>
dplyr::select(
Scenario,
`Efficacy crossing probability` = rejection_rate,
`Futility stopping probability` = futility_stop_rate,
`MC SE (efficacy)` = mc_se,
`MC SE (futility)` = futility_mc_se,
`Mean total events` = mean_total_events,
`Mean total enrolled` = mean_total_enrolled,
`Mean looks used` = mean_looks
)
oc |>
gt() |>
fmt_number(columns = 2:5, decimals = 4) |>
fmt_number(columns = 6:8, decimals = 2) |>
tab_header(
title = "Lightweight simulation results",
subtitle = "Exact-binomial monitoring with seasonal analyses"
) |>
tab_footnote(
footnote = "For VE=30% scenarios, efficacy crossing probability is Type I error under the non-binding futility convention (futility crossings do not block later efficacy crossings).",
locations = cells_column_labels(columns = `Efficacy crossing probability`)
)
## -------------------------------------------------------
low_control_event_rates <- planned_control_event_rates / 2
sim_low <- simBinomialSeasonalExact(
gsD = design_calendar,
ve = ve_scenarios,
nsim = rep(300, length(ve_scenarios)),
control_event_rate = low_control_event_rates,
season_length = season_length_years,
dropout_rate = dropout_6mo,
planned_counts = planned_counts,
enroll_control_per_look = planned_enrollment_control,
enroll_experimental_per_look = planned_enrollment_experimental,
adaptive = c(FALSE, TRUE),
max_multiplier = 2,
final_full_spending = TRUE,
seed = 505
)
low <- sim_low$summary
tibble(
Scenario = c(
"Without adaptation: Type I error (VE=30%)",
"With adaptation: Type I error (VE=30%)",
"Without adaptation: Power (VE=80%)",
"With adaptation: Power (VE=80%)"
),
`Efficacy crossing probability` = c(
low$rejection_rate[!low$adaptive & low$scenario == "H0 (VE=30%)"],
low$rejection_rate[low$adaptive & low$scenario == "H0 (VE=30%)"],
low$rejection_rate[!low$adaptive & low$scenario == "H1 (VE=80%)"],
low$rejection_rate[low$adaptive & low$scenario == "H1 (VE=80%)"]
),
`Futility stopping probability` = c(
low$futility_stop_rate[!low$adaptive & low$scenario == "H0 (VE=30%)"],
low$futility_stop_rate[low$adaptive & low$scenario == "H0 (VE=30%)"],
low$futility_stop_rate[!low$adaptive & low$scenario == "H1 (VE=80%)"],
low$futility_stop_rate[low$adaptive & low$scenario == "H1 (VE=80%)"]
),
`Mean total events` = c(
low$mean_total_events[!low$adaptive & low$scenario == "H0 (VE=30%)"],
low$mean_total_events[low$adaptive & low$scenario == "H0 (VE=30%)"],
low$mean_total_events[!low$adaptive & low$scenario == "H1 (VE=80%)"],
low$mean_total_events[low$adaptive & low$scenario == "H1 (VE=80%)"]
),
`Mean total enrolled` = c(
low$mean_total_enrolled[!low$adaptive & low$scenario == "H0 (VE=30%)"],
low$mean_total_enrolled[low$adaptive & low$scenario == "H0 (VE=30%)"],
low$mean_total_enrolled[!low$adaptive & low$scenario == "H1 (VE=80%)"],
low$mean_total_enrolled[low$adaptive & low$scenario == "H1 (VE=80%)"]
),
`Mean looks used` = c(
low$mean_looks[!low$adaptive & low$scenario == "H0 (VE=30%)"],
low$mean_looks[low$adaptive & low$scenario == "H0 (VE=30%)"],
low$mean_looks[!low$adaptive & low$scenario == "H1 (VE=80%)"],
low$mean_looks[low$adaptive & low$scenario == "H1 (VE=80%)"]
)
) |>
gt() |>
fmt_number(columns = 2:3, decimals = 4) |>
fmt_number(columns = 4:6, decimals = 2) |>
tab_header(
title = "Lower-than-planned event rate illustration",
subtitle = "Adaptive approach increases enrollment to recover information"
) |>
tab_footnote(
footnote = "Type I error rows use non-binding futility for efficacy crossing probability; futility stopping probability is shown separately.",
locations = cells_column_labels(columns = `Efficacy crossing probability`)
)
## ----eval=FALSE-----------------------------------------
# # Suggested offline settings
# type1_nsim <- 20000
# power_nsim <- 3500
#
# sim_type1_big <- simBinomialSeasonalExact(
# gsD = design_calendar,
# ve = c(`H0 (VE=30%)` = ve0),
# nsim = type1_nsim,
# control_event_rate = seasonal_event_rate_control,
# season_length = season_length_years,
# dropout_rate = dropout_6mo,
# planned_counts = planned_counts,
# enroll_control_per_look = planned_enrollment_control,
# enroll_experimental_per_look = planned_enrollment_experimental,
# adaptive = c(FALSE, TRUE),
# final_full_spending = TRUE,
# seed = 5001
# )
#
# sim_power_big <- simBinomialSeasonalExact(
# gsD = design_calendar,
# ve = c(`H1 (VE=80%)` = ve1),
# nsim = power_nsim,
# control_event_rate = seasonal_event_rate_control,
# season_length = season_length_years,
# dropout_rate = dropout_6mo,
# planned_counts = planned_counts,
# enroll_control_per_look = planned_enrollment_control,
# enroll_experimental_per_look = planned_enrollment_experimental,
# adaptive = c(FALSE, TRUE),
# final_full_spending = TRUE,
# seed = 6001
# )
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.