PFS Go/Nogo design
PFS_Gonogo.Rmd
Note: This vignette, including functions for TTE Go/Nogo design, are still under development. Hence they are subject to possible change in the future.
library(gonogo)
library(dplyr)
#>
#> Attaching package: 'dplyr'
#> The following objects are masked from 'package:stats':
#>
#> filter, lag
#> The following objects are masked from 'package:base':
#>
#> intersect, setdiff, setequal, union
library(tidyr)
library(ggplot2)
library(ggrepel)
# require(cowplot)
# require(survival)
library(future.apply)
#> Loading required package: future
library(progressr)
# number of parallel cores,
# please adjust according to your machine's capability
# current setting is to leave (at most) 2 cores not used, since the function
# is guaranteed to return at least 1 core for later parallel computation.
worker_num <- parallelly::availableCores(omit = 2)
plan(cluster, workers = worker_num)
# progress info report settings
handlers("cli")
progress_interval <- 50
# fix the seed for random number generation
# Note that later the RNG kind will be set to a parallel safe one
random_seed <- 1024
Basic Settings
In this article, an example of Go/Nogo design based on PFS is demonstrated. First we need some settings about this trial:
basic_settings <- list(
max_subj_num = 40,
# accrual_rate = 40 / 5,
accrual_time = 5,
n1 = 20, # number of subjects in stage1
wait_after_n1 = 0, # time to wait after `n1` subjects being enrolled
# cut_date = 9 + 2 + 5, # cutoff date = actrual accrual_time + wait time + followup time
cut_evt_num = NULL, # event number target to determine cutoff date. If `cut_date` is NULL, `cut_evt_num` will be used.
rate_diff_at = 6,
followup_time = 6
)
(basic_settings <- gonogo:::Get_Settings(basic_settings, type = 1))
#> $max_subj_num
#> [1] 40
#>
#> $accrual_time
#> [1] 5
#>
#> $n1
#> [1] 20
#>
#> $wait_after_n1
#> [1] 0
#>
#> $cut_evt_num
#> NULL
#>
#> $rate_diff_at
#> [1] 6
#>
#> $followup_time
#> [1] 6
#>
#> $accrual_rate
#> [1] 8
#>
#> $cut_date
#> [1] 11
Secondly, we need some settings about this hypothetical drug:
pfs_settings <- list(
m_ref = 7, # median reference PFS time
m_trt = 9.5, # pfs_m_ctrl,
trt_dropout = 0.1, # annual pfs dropout rate
piecewise_trt = FALSE
)
# basic_settings <- Get_Settings(basic_settings, type = 1)
(pfs_settings <- gonogo:::Get_Settings(pfs_settings, type = 2, prior_shape = 0.001, prior_rate = 0.001))
#> $m_ref
#> [1] 7
#>
#> $m_trt
#> [1] 9.5
#>
#> $trt_dropout
#> [1] 0.1
#>
#> $piecewise_trt
#> [1] FALSE
#>
#> $hazard
#> [1] 0.07296286
#>
#> $ref_hazard
#> [1] 0.09902103
#>
#> $dropout_lambda
#> [1] 0.008780043
#>
#> $prior_shape
#> [1] 0.001
#>
#> $prior_rate
#> [1] 0.001
Thirdly, we present the rules for develop this Go/Nogo strategy
# General idea:
# Declare Go if Pr(lambda <= lam_eff_cut) >= go_prob_target
# Declare Nogo if Pr(lambda > lam_fut_cut) >= nogo_prob_target
# Additionaly, for this case, we set `use_evt_cut = TRUE`
# Directly declare go when number of events at analysis <= go_evt_target
# Directly declare nogo when number of events at analysis >= nogo_evt_target
#
# Besides the Bayesian perspective, we also set a frequenter's perspective. The
# rule is based on 6M landmark pfs rate and the detailed rule is based on
# confidence interval of 6M PFS rate.
go_nogo_settings <- list(
lam_eff_cut = pfs_settings$ref_hazard,
lam_fut_cut = pfs_settings$ref_hazard,
go_prob_target = 0.67,
nogo_prob_target = 0.1,
use_evt_cut = TRUE,
go_evt_target = 15,
nogo_evt_target = 25,
pfs_rate_diff_at = 6,
pfs_rate_eff_cut = 2 ^ (-6 / pfs_settings$m_ref),
pfs_rate_fut_cut = 2 ^ (-6 / pfs_settings$m_ref),
pfs_rate_go_prob_target = 0.67,
pfs_rate_nogo_prob_target = 0.1,
pfs_rate_use_evt_cut = TRUE,
pfs_rate_go_evt_target = 15,
pfs_rate_nogo_evt_target = 25
)
This design strategy (not necessarily the underlying survival
distribution) is based on exponential distribution of survival time and
Gamma prior of the hazard parameter. Please refer to
vignette("tte_bayes_parametric", package = "gonogo")
for
more details. Also our function provides the capability to develop
Go/Nogo strategy based on landmark PFS rate (a frequentist’s
perspective, not Bayesian). One can check the function documentation for
more details.
General scope of this trial
This section will demonstrate how this trial scope would vary under different scenarios via some simulations. To reduce the computation burden, the number of simulation is set to a quite small number. Please increase it in actual usage
sim_num <- 100 # number of simulation
mpfs_vec <- sort(c(seq(from = 7, to = 11, by = 1), 9.5)) # underlying mPFS
followup_time_vec <- c(6, 9, 12) # follow-up time since LPI
settings_df <- expand.grid(
mpfs_trt = mpfs_vec,
followup_time = followup_time_vec)
set.seed(random_seed, kind = "L'Ecuyer-CMRG")
settings_res <- apply(settings_df, 1, function(setting, basic_settings, pfs_settings, go_nogo_settings){
pfs_settings$m_trt <- as.numeric(setting["mpfs_trt"])
# pfs_settings$m_ref <- as.numeric(setting["mpfs_ref"])
basic_settings$followup_time <- as.numeric(setting["followup_time"])
# basic_settings <- Get_Settings(basic_settings, type = 1)
pfs_settings <- gonogo:::Get_Settings(pfs_settings, type = 2)
basic_settings <- gonogo:::Get_Settings(basic_settings, type = 1)
with_progress({
p <- progressr::progressor(steps = ceiling(sim_num / progress_interval))
full_res <- future.apply::future_lapply(
seq(sim_num),
function(idx, basic_settings, pfs_settings, go_nogo_settings, p = NULL, progress_interval = NULL){
res <- TTESimulation::Surv_Simulation_1sample_Gonogo_Atom(basic_settings,
pfs_settings,
go_nogo_settings,
sig.lvl = 0.1) # 2-sided alpha!
if(!is.null(p)){
if(idx %% progress_interval == 0){
p(message = paste0("idx = ", idx, " finished!"))
}
}
return(res$analysis_res)
},
basic_settings = basic_settings,
pfs_settings = pfs_settings,
go_nogo_settings = go_nogo_settings,
p = p, progress_interval = progress_interval,
future.seed = TRUE
) })
full_res_summary <- do.call(rbind, full_res) %>%
as_tibble() %>%
mutate(pfs_sum_obs_time = pfs_sum_obs_time / basic_settings$max_subj_num) %>% # 将总观察时间转化为平均观察时间,便于直观理解
summarise(pfs_surv_q10 = quantile(pfs_surv, probs = 0.1, names = FALSE),
pfs_surv_q90 = quantile(pfs_surv, probs = 0.9, names = FALSE),
pfs_evt_num_q10 = quantile(pfs_evt_num, probs = 0.025, names = FALSE),
pfs_evt_num_q90 = quantile(pfs_evt_num, probs = 0.975, names = FALSE),
pfs_sum_obs_time_q10 = quantile(pfs_sum_obs_time, probs = 0.025, names = FALSE),
pfs_sum_obs_time_q90 = quantile(pfs_sum_obs_time, probs = 0.975, names = FALSE),
pfs_evt_num_q_go_l = quantile(pfs_evt_num, probs = 1 - 0.67, names = FALSE),
pfs_evt_num_q_go_r = quantile(pfs_evt_num, probs = 0.67, names = FALSE),
pfs_evt_num_q_nogo_l = quantile(pfs_evt_num, probs = 0.1, names = FALSE),
pfs_evt_num_q_nogo_r = quantile(pfs_evt_num, probs = 1 - 0.1, names = FALSE),
across(everything(), mean)
) %>%
relocate(matches("q*0$"), .after = everything()) %>%
mutate(across(everything(), \(x) round(x, digits = 3)))
return(c(setting, unlist(full_res_summary)))
},
basic_settings = basic_settings,
pfs_settings = pfs_settings,
go_nogo_settings = go_nogo_settings)
settings_res <- t(settings_res)
settings_res %>%
as.data.frame() %>%
select(followup_time, mpfs_trt, pfs_cut_date,
pfs_evt_num, pfs_evt_num_q10, pfs_evt_num_q90,
pfs_sum_obs_time, pfs_sum_obs_time_q10, pfs_sum_obs_time_q90,
pfs_evt_num_q_go_l, pfs_evt_num_q_go_r,
pfs_evt_num_q_nogo_l, pfs_evt_num_q_nogo_r) %>%
knitr::kable()
followup_time | mpfs_trt | pfs_cut_date | pfs_evt_num | pfs_evt_num_q10 | pfs_evt_num_q90 | pfs_sum_obs_time | pfs_sum_obs_time_q10 | pfs_sum_obs_time_q90 | pfs_evt_num_q_go_l | pfs_evt_num_q_go_r | pfs_evt_num_q_nogo_l | pfs_evt_num_q_nogo_r |
---|---|---|---|---|---|---|---|---|---|---|---|---|
6 | 7.0 | 11 | 22.41 | 18.000 | 27.525 | 5.492 | 4.588 | 6.293 | 21.00 | 24.00 | 19.0 | 26.0 |
6 | 8.0 | 11 | 20.44 | 15.475 | 25.000 | 5.779 | 4.856 | 6.642 | 19.00 | 21.00 | 17.0 | 24.0 |
6 | 9.0 | 11 | 18.75 | 13.475 | 23.000 | 5.997 | 5.043 | 6.875 | 18.00 | 20.00 | 15.0 | 22.0 |
6 | 9.5 | 11 | 17.82 | 13.000 | 22.525 | 6.100 | 5.128 | 6.925 | 17.00 | 19.00 | 14.0 | 21.0 |
6 | 10.0 | 11 | 17.21 | 11.000 | 22.525 | 6.186 | 5.221 | 6.994 | 16.00 | 18.33 | 14.0 | 21.0 |
6 | 11.0 | 11 | 16.06 | 11.000 | 22.000 | 6.358 | 5.503 | 7.282 | 15.00 | 17.00 | 12.0 | 20.0 |
9 | 7.0 | 14 | 25.94 | 20.000 | 32.000 | 6.575 | 5.468 | 7.674 | 24.67 | 27.00 | 22.0 | 30.0 |
9 | 8.0 | 14 | 24.17 | 18.475 | 30.000 | 6.973 | 5.816 | 8.233 | 22.67 | 26.00 | 20.0 | 28.0 |
9 | 9.0 | 14 | 22.74 | 16.000 | 28.525 | 7.282 | 6.137 | 8.534 | 21.67 | 24.00 | 19.0 | 27.0 |
9 | 9.5 | 14 | 21.91 | 16.000 | 27.525 | 7.422 | 6.271 | 8.609 | 21.00 | 23.00 | 18.0 | 26.0 |
9 | 10.0 | 14 | 21.02 | 15.000 | 26.525 | 7.559 | 6.436 | 8.748 | 20.00 | 22.00 | 17.0 | 25.0 |
9 | 11.0 | 14 | 19.80 | 14.000 | 24.525 | 7.786 | 6.673 | 8.866 | 19.00 | 21.00 | 16.0 | 23.1 |
12 | 7.0 | 17 | 28.83 | 24.000 | 34.000 | 7.320 | 5.949 | 8.913 | 27.00 | 30.00 | 25.0 | 33.0 |
12 | 8.0 | 17 | 26.86 | 22.000 | 32.000 | 7.900 | 6.477 | 9.501 | 25.00 | 28.00 | 23.0 | 31.0 |
12 | 9.0 | 17 | 25.25 | 19.475 | 31.000 | 8.334 | 6.887 | 9.875 | 24.00 | 26.33 | 21.0 | 29.0 |
12 | 9.5 | 17 | 24.59 | 18.475 | 30.525 | 8.556 | 7.000 | 10.120 | 23.00 | 26.00 | 21.0 | 29.0 |
12 | 10.0 | 17 | 23.75 | 17.000 | 30.525 | 8.769 | 7.157 | 10.205 | 22.00 | 25.00 | 19.9 | 28.1 |
12 | 11.0 | 17 | 22.25 | 15.000 | 28.525 | 9.110 | 7.462 | 10.542 | 21.00 | 24.00 | 17.9 | 26.1 |
Find the rule
In this section, we compute the detailed Go/Nogo rule and visualize it. Given the observed number of events, declare go if total obs time is long engough, declare nogo if total obs time is short enough.
Note: currently evt_num = 0
is not
included in the rule because the function is a little bit unstable at
this corner case and future development is necessary. After some
calculation I discovered that this prior setting of
prior_shape = 0.001
and prior_rate = 0.001
,
though “nearly uninformative” from the perspective of posterior
distribution, will lead to some unexpected results when computing
go/nogo rules when evt_num = 0
. Under this scenario, the
rule would be to declare go and not declare nogo no matter what.
pfs_rules <- PFS_Find_Cut(basic_settings, pfs_settings, go_nogo_settings,
start_evt_num = 1)
We can print the rules as follow:
knitr::kable(pfs_rules$rules_tb)
evt_num | sum_obs_time_Declare Go | sum_obs_time_Declare Nogo | pprob_Declare Go | pprob_Declare Nogo |
---|---|---|---|---|
40 | 430 | 487 | 0.6742397 | 0.1016475 |
39 | 419 | 476 | 0.6712958 | 0.1013377 |
38 | 409 | 465 | 0.6738597 | 0.1009883 |
37 | 398 | 454 | 0.6708880 | 0.1005965 |
36 | 388 | 443 | 0.6735720 | 0.1001596 |
35 | 377 | 431 | 0.6705750 | 0.1022650 |
34 | 367 | 420 | 0.6733933 | 0.1017509 |
33 | 356 | 409 | 0.6703746 | 0.1011819 |
32 | 346 | 398 | 0.6733443 | 0.1005539 |
31 | 335 | 386 | 0.6703089 | 0.1026000 |
30 | 325 | 375 | 0.6734509 | 0.1018642 |
29 | 314 | 364 | 0.6704058 | 0.1010553 |
28 | 304 | 353 | 0.6737451 | 0.1001677 |
27 | 293 | 341 | 0.6707001 | 0.1020881 |
26 | 283 | 330 | 0.6742682 | 0.1010471 |
25 | 272 | 318 | 0.6712370 | 0.1029149 |
24 | 262 | 307 | 0.6750734 | 0.1016894 |
23 | 251 | 296 | 0.6720753 | 0.1003455 |
22 | 241 | 284 | 0.6762308 | 0.1020262 |
21 | 230 | 273 | 0.6732935 | 0.1004346 |
20 | 219 | 261 | 0.6702959 | 0.1019678 |
19 | 209 | 250 | 0.6749986 | 0.1000703 |
18 | 198 | 238 | 0.6721148 | 0.1013941 |
17 | 188 | 226 | 0.6773402 | 0.1026417 |
16 | 177 | 215 | 0.6746409 | 0.1001421 |
15 | 166 | 203 | 0.6719226 | 0.1010408 |
14 | 156 | 191 | 0.6781312 | 0.1017772 |
13 | 145 | 179 | 0.6757605 | 0.1023101 |
12 | 134 | 167 | 0.6734446 | 0.1025873 |
11 | 123 | 155 | 0.6712136 | 0.1025420 |
10 | 113 | 143 | 0.6795806 | 0.1020872 |
9 | 102 | 131 | 0.6782348 | 0.1011083 |
8 | 91 | 118 | 0.6772646 | 0.1042777 |
7 | 80 | 106 | 0.6768435 | 0.1018848 |
6 | 69 | 93 | 0.6772497 | 0.1036424 |
5 | 58 | 80 | 0.6789581 | 0.1042758 |
4 | 47 | 67 | 0.6828508 | 0.1030077 |
3 | 35 | 53 | 0.6725743 | 0.1053436 |
2 | 24 | 39 | 0.6861691 | 0.1023550 |
1 | 12 | 23 | 0.6948869 | 0.1027089 |
We can also visualize the rules at different number of events
cowplot::plot_grid(plotlist = pfs_rules$gplot_list,
ncol = 4)
Detail performance of different PFS and Go/Nogo settings
We use the exponential distribution, not piece-wise exponential distribution for the underlying PFS distribution.
pfs_settings <- list(
m_ref = 7.0, # median reference PFS time
m_trt = 9.5, # pfs_m_ctrl,
trt_dropout = 0.1, # annual pfs dropout rate
piecewise_trt = FALSE # NOT use piecewise treatment survival time
)
And again the setup of trial and go/nogo setting
basic_settings <- list(
max_subj_num = 40,
# accrual_rate = 40 / 5,
accrual_time = 5,
n1 = 20, # number of subjects in stage1
wait_after_n1 = 0, # time to wait after `n1` subjects being enrolled
# cut_date = 9 + 2 + 5, # cutoff date = actrual accrual_time + wait time + followup time
cut_evt_num = NULL, # event number target to determine cutoff date. If `cut_date` is NULL, `cut_evt_num` will be used.
rate_diff_at = 6,
followup_time = 6 # placeholder
)
# basic_settings <- Get_Settings(basic_settings, type = 1)
# Declare Go if Pr(lambda <= lam_eff_cut) >= go_prob_target
# Declare Nogo if Pr(lambda > lam_fut_cut) >= nogo_prob_target
go_nogo_settings <- list(
lam_eff_cut = log(2) / 9.5, # placeholder, update in later code
lam_fut_cut = log(2) / 7.0, # placeholder, update in later code
go_prob_target = 0.67,
nogo_prob_target = 0.1,
use_evt_cut = TRUE,
go_evt_target = 13,
nogo_evt_target = 27,
pfs_rate_diff_at = 6,
pfs_rate_eff_cut = 2 ^ (-6 / pfs_settings$m_ref), # placeholder, update in later code
pfs_rate_fut_cut = 2 ^ (-6 / pfs_settings$m_ref), # placeholder, update in later code
pfs_rate_go_prob_target = 0.67,
pfs_rate_nogo_prob_target = 0.1,
pfs_rate_use_evt_cut = TRUE,
pfs_rate_go_evt_target = 13,
pfs_rate_nogo_evt_target = 27
)
Then we can test the performance of this gonogo rule under different underlying PFS assumption and data cutoff rule
mpfs_vec <- c(7, 9.5, 11)
followup_time_vec <- c(NA, 6) # either data cut at X month after LPI, or at a pre-specified number of observed events
settings_df <- expand.grid(
mpfs_trt = mpfs_vec,
followup_time = followup_time_vec
) %>%
mutate(
mpfs_ref = 7,
cut_evt_num = if_else(is.na(followup_time), 20, NA),
lam_eff_cut = log(2) / 9.5,
lam_fut_cut = log(2) / 7,
pfs_rate_eff_cut = 2 ^ (-6 / 9.5),
pfs_rate_fut_cut = 2 ^ (-6 / 7))
Then we perform the simulation
sim_num <- 100 # number of simulations
set.seed(random_seed, kind = "L'Ecuyer-CMRG")
settings_res_list <- apply(settings_df, 1, function(setting, basic_settings, pfs_settings, go_nogo_settings){
# --- Update settings ---
pfs_settings$m_trt <- as.numeric(setting["mpfs_trt"])
pfs_settings$m_ref <- as.numeric(setting["mpfs_ref"])
pfs_settings <- gonogo:::Get_Settings(pfs_settings, type = 2, prior_shape = 0.001, prior_rate = 0.001)
# pfs_settings <- Get_Settings(pfs_settings, type = 2, prior_shape = 1.05, prior_rate = 0.59)
if(!is.na(setting["cut_evt_num"])){
basic_settings$cut_evt_num <- as.numeric(setting["cut_evt_num"])
basic_settings$cut_date <- NULL
}else{
basic_settings$cut_evt_num <- NULL
basic_settings$followup_time <- as.numeric(setting["followup_time"])
}
basic_settings <- gonogo:::Get_Settings(basic_settings, type = 1)
go_nogo_settings$lam_eff_cut <- as.numeric(setting["lam_eff_cut"])
go_nogo_settings$lam_fut_cut <- as.numeric(setting["lam_fut_cut"])
go_nogo_settings$pfs_rate_eff_cut <- as.numeric(setting["pfs_rate_eff_cut"])
go_nogo_settings$pfs_rate_fut_cut <- as.numeric(setting["pfs_rate_fut_cut"])
# --- Perform simulation ---
with_progress({
p <- progressr::progressor(steps = ceiling(sim_num / progress_interval))
full_res <- future.apply::future_lapply(
seq(sim_num),
function(idx, basic_settings, pfs_settings, go_nogo_settings, p = NULL, progress_interval = NULL){
res <- TTESimulation::Surv_Simulation_1sample_Gonogo_Atom(basic_settings,
pfs_settings,
go_nogo_settings,
sig.lvl = 0.05) # 2-sided alpha!
if(!is.null(p)){
if(idx %% progress_interval == 0){
p(message = paste0("idx = ", idx, " finished!"))
}
}
return(res$analysis_res)
},
basic_settings = basic_settings,
pfs_settings = pfs_settings,
go_nogo_settings = go_nogo_settings,
p = p, progress_interval = progress_interval,
future.seed = TRUE
) })
# --- Summary the results ---
detail_res <- do.call(rbind, full_res) %>%
as_tibble() %>%
select(pfs_cut_date, pfs_evt_num, pfs_sum_obs_time,
pfs_surv, pfs_surv_theory, pfs_surv_sig,
pfs_lam_mle, pfs_lam_sig,
pfs_pprob_go, pfs_pprob_nogo,
pfs_declare_go, pfs_declare_nogo,
pfs_rate_prob_go, pfs_rate_prob_nogo,
pfs_rate_declare_go, pfs_rate_declare_nogo)
summary_res <- detail_res %>%
summarise(
across(
.cols = c(pfs_cut_date, pfs_evt_num, pfs_sum_obs_time,
pfs_pprob_go, pfs_pprob_nogo,
pfs_rate_prob_go, pfs_rate_prob_nogo),
.fns = list(q05 = function(x) quantile(x, probs = 0.025, names = FALSE),
q95 = function(x) quantile(x, probs = 0.975, names = FALSE))),
across(everything(), function(x) mean(x, na.rm = TRUE))) %>%
mutate(
ave_cut_date = paste0(
sprintf("%.1f", pfs_cut_date),
" (", sprintf("%.1f", pfs_cut_date_q05), ", ", sprintf("%.1f", pfs_cut_date_q95), ")"
),
ave_evt_num = paste0(
sprintf("%.1f", pfs_evt_num),
" (", sprintf("%.0f", pfs_evt_num_q05), ", ", sprintf("%.0f", pfs_evt_num_q95), ")"
),
sum_obs_time = paste0(
sprintf("%.1f", pfs_sum_obs_time),
" (", sprintf("%.1f", pfs_sum_obs_time_q05), ", ", sprintf("%.1f", pfs_sum_obs_time_q95), ")"
),
pprob_go = paste0(
sprintf("%.1f", pfs_pprob_go),
" (", sprintf("%.1f", pfs_pprob_go_q05), ", ", sprintf("%.1f", pfs_pprob_go_q95), ")"
),
pprob_nogo = paste0(
sprintf("%.1f", pfs_pprob_nogo),
" (", sprintf("%.1f", pfs_pprob_nogo_q05), ", ", sprintf("%.1f", pfs_pprob_nogo_q95), ")"
),
fprob_go = paste0(
sprintf("%.1f", pfs_rate_prob_go),
" (", sprintf("%.1f", pfs_rate_prob_go_q05), ", ", sprintf("%.1f", pfs_rate_prob_go_q95), ")"
),
fprob_nogo = paste0(
sprintf("%.1f", pfs_rate_prob_nogo),
" (", sprintf("%.1f", pfs_rate_prob_nogo_q05), ", ", sprintf("%.1f", pfs_rate_prob_nogo_q95), ")"
),
bayes_declare_go = sprintf("%.1f%%", pfs_declare_go * 100),
bayes_declare_nogo = sprintf("%.1f%%", pfs_declare_nogo * 100),
freq_declare_go = sprintf("%.1f%%", pfs_rate_declare_go * 100),
freq_declare_nogo = sprintf("%.1f%%", pfs_rate_declare_nogo * 100),
surv_rate_sig = sprintf("%.1f%%", pfs_surv_sig * 100),
lam_sig = sprintf("%.1f%%", pfs_lam_sig * 100),
hazard_theory = sprintf("%.3f", pfs_settings$hazard)
) %>%
select(hazard_theory,
ave_cut_date, ave_evt_num, sum_obs_time,
pprob_go, pprob_nogo, fprob_go, fprob_nogo,
bayes_declare_go, bayes_declare_nogo,
freq_declare_go, freq_declare_nogo,
surv_rate_sig, lam_sig)
detail_res <- cbind(t(setting), detail_res)
summary_res <- cbind(t(setting), summary_res)
res <- list(detail_res = detail_res,
summary_res = summary_res)
return(res)
},
basic_settings = basic_settings,
pfs_settings = pfs_settings,
go_nogo_settings = go_nogo_settings)
summary_res_list <- list()
for(idx in seq(length(settings_res_list))){
summary_res_list <- append(summary_res_list,
list(settings_res_list[[idx]]$summary_res))
}
do.call(rbind, summary_res_list) %>%
knitr::kable()
mpfs_trt | followup_time | mpfs_ref | cut_evt_num | lam_eff_cut | lam_fut_cut | pfs_rate_eff_cut | pfs_rate_fut_cut | hazard_theory | ave_cut_date | ave_evt_num | sum_obs_time | pprob_go | pprob_nogo | fprob_go | fprob_nogo | bayes_declare_go | bayes_declare_nogo | freq_declare_go | freq_declare_nogo | surv_rate_sig | lam_sig |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
7.0 | NA | 7 | 20 | 0.0729629 | 0.099021 | 0.6454696 | 0.5520448 | 0.099 | 9.6 (6.5, 12.5) | 20.0 (20, 20) | 197.1 (116.4, 280.5) | 0.1 (0.0, 0.6) | 0.5 (0.1, 1.0) | 0.2 (0.0, 0.6) | 0.5 (0.1, 1.0) | 1.0% | 96.0% | 3.0% | 96.0% | 7.0% | 6.0% |
9.5 | NA | 7 | 20 | 0.0729629 | 0.099021 | 0.6454696 | 0.5520448 | 0.073 | 12.1 (8.1, 16.5) | 20.0 (20, 20) | 266.8 (156.3, 384.5) | 0.5 (0.0, 1.0) | 0.2 (0.0, 0.8) | 0.5 (0.0, 1.0) | 0.2 (0.0, 0.9) | 27.0% | 48.0% | 29.0% | 52.0% | 14.0% | 20.0% |
11.0 | NA | 7 | 20 | 0.0729629 | 0.099021 | 0.6454696 | 0.5520448 | 0.063 | 13.9 (8.9, 20.5) | 20.0 (20, 20) | 312.2 (181.6, 443.7) | 0.7 (0.1, 1.0) | 0.1 (0.0, 0.7) | 0.6 (0.1, 1.0) | 0.1 (0.0, 0.7) | 52.0% | 20.0% | 48.0% | 31.0% | 31.0% | 47.0% |
7.0 | 6 | 7 | NA | 0.0729629 | 0.099021 | 0.6454696 | 0.5520448 | 0.099 | 11.0 (11.0, 11.0) | 22.3 (17, 29) | 221.0 (184.5, 258.0) | 0.2 (0.0, 0.6) | 0.5 (0.0, 1.0) | 0.2 (0.0, 0.7) | 0.5 (0.1, 1.0) | 2.0% | 94.0% | 3.0% | 94.0% | 3.0% | 5.0% |
9.5 | 6 | 7 | NA | 0.0729629 | 0.099021 | 0.6454696 | 0.5520448 | 0.073 | 11.0 (11.0, 11.0) | 17.9 (12, 23) | 244.0 (205.5, 276.5) | 0.5 (0.0, 1.0) | 0.1 (0.0, 0.6) | 0.5 (0.1, 1.0) | 0.2 (0.0, 0.7) | 34.0% | 43.0% | 32.0% | 53.0% | 14.0% | 19.0% |
11.0 | 6 | 7 | NA | 0.0729629 | 0.099021 | 0.6454696 | 0.5520448 | 0.063 | 11.0 (11.0, 11.0) | 16.1 (11, 22) | 254.3 (220.1, 291.3) | 0.7 (0.1, 1.0) | 0.1 (0.0, 0.4) | 0.7 (0.1, 1.0) | 0.1 (0.0, 0.5) | 64.0% | 24.0% | 57.0% | 32.0% | 31.0% | 41.0% |
OC curves
First some basic settings
Note: Remember to adjust the
oc_params_sim$sim_num
in your real usage. For this
vignette, it is set to a quite small value to ease the computation
burden.
mpfs_pick <- c(7.0, 9.5, 11.0) # m-PFS shown in the figures
tpp_tb <- tribble( # additional of some picked mPFS
~mpfs, ~tpp,
7.0, "SOC",
9.5, "TPP base",
11.0, "TPP best"
)
oc_params <- list(
mpfs_vec = seq(from = 1, to = 30, length.out = 250),
sim_num = 0
)
oc_params_sim <- oc_params
oc_params_sim$sim_num <- 250
oc_params_sim$mpfs_vec <- seq(from = 5, to = 15, length.out = 25)
plot_params <- list(
contour_vec = seq(from = 0.25, to = 1, by = 0.25),
use_smooth = FALSE)
pfs_settings <- list(
m_ref = 7.0, # median reference PFS time
m_trt = 9.5, # placeholder, pfs_m_ctrl,
trt_dropout = 0.1, # annual pfs dropout rate
piecewise_trt = FALSE
)
# pfs_settings <- Get_Settings(pfs_settings, type = 2, prior_shape = 0.001, prior_rate = 0.001)
basic_settings <- list(
max_subj_num = 40,
# accrual_rate = 40 / 5,
accrual_time = 5,
n1 = 20, # number of subjects in stage1
wait_after_n1 = 0, # time to wait after `n1` subjects being enrolled
# cut_date = 9 + 2 + 5, # cutoff date = actrual accrual_time + wait time + followup time
cut_evt_num = NULL, # event number target to determine cutoff date. If `cut_date` is NULL, `cut_evt_num` will be used.
rate_diff_at = 6,
followup_time = 6
)
# basic_settings <- Get_Settings(basic_settings, type = 1)
# Declare Go if Pr(lambda <= lam_eff_cut) >= go_prob_target
# Declare Nogo if Pr(lambda > lam_fut_cut) >= nogo_prob_target
go_nogo_settings <- list(
lam_eff_cut = log(2) / 9.5,
lam_fut_cut = log(2) / 7.0,
go_prob_target = 0.67,
nogo_prob_target = 0.1,
use_evt_cut = TRUE,
go_evt_target = 13,
nogo_evt_target = 27,
pfs_rate_diff_at = 6,
pfs_rate_eff_cut = 2 ^ (-6 / 9.5),
pfs_rate_fut_cut = 2 ^ (-6 / 7.0),
pfs_rate_go_prob_target = 0.67,
pfs_rate_nogo_prob_target = 0.1,
pfs_rate_use_evt_cut = TRUE,
pfs_rate_go_evt_target = 13,
pfs_rate_nogo_evt_target = 27
)
Then we setup some rules
followup_time_vec <- c(6, 9, 12, NA)
cut_evt_num_vec <- c(20, 24, 28, NA)
rules_df1 <- expand.grid(
followup_time = followup_time_vec,
cut_evt_num = cut_evt_num_vec
) %>%
filter((is.na(followup_time) & !is.na(cut_evt_num)) | (is.na(cut_evt_num) & !is.na(followup_time))) %>%
mutate(kickoff = if_else(is.na(followup_time),
paste0("event num at ", cut_evt_num),
paste0(followup_time, "m after LPI")),
idx = row_number(),
idx = c(1 : (length(followup_time_vec) - 1), 1 : (length(cut_evt_num_vec) - 1))
) %>%
mutate(go_evt_target = c(rep(0, 3), 13, 17, 20),
nogo_evt_target = c(rep(40, 3), 27, 31, 34))
rules_df2 <- tribble(
~idx, ~lam_eff_cut, ~lam_fut_cut, ~go_prob_target, ~nogo_prob_target,
1, log(2) / 10.0, log(2) / 7.0, 0.5, 0.5,
2, log(2) / 9.5, log(2) / 7.0, 0.67, 0.1,
3, log(2) / 9.5, log(2) / 7.0, 0.5, 0.15,
4, log(2) / 7.0, log(2) / 7.0, 0.9, 0.1, # BOP2: `lam_eff_cut` and `lam_fut_cut` are the same, `go_prob_target` + `nogo_prob_target` = 1
# 5, log(2) / 10.3, log(2) / 8.2, 0.5, 0.2
) %>%
mutate(
mpfs_eff_cut = log(2) / lam_eff_cut,
mpfs_fut_cut = log(2) / lam_fut_cut,
rules = paste0("Go if Pr(mPFS > ",
sprintf("%.1f", mpfs_eff_cut),
") > ",
sprintf("%.2f", go_prob_target),
".\nNogo if Pr(mPFS > ",
sprintf("%.1f", mpfs_fut_cut),
") < ",
sprintf("%.2f", 1 - nogo_prob_target), "\n"),
rules = if_else(idx == 4,
paste0("BOP2: Go if Pr(mPFS > ",
sprintf("%.1f", mpfs_eff_cut),
") > ",
sprintf("%.2f", go_prob_target),
".\n Nogo if Pr(mPFS > ",
sprintf("%.1f", mpfs_fut_cut),
") < ",
sprintf("%.2f", 1 - nogo_prob_target), "\n"),
rules)) %>%
mutate(go_evt_target = 0,
nogo_evt_target = 40)
rules_df <- bind_rows(rules_df1, rules_df2)
followup_time | cut_evt_num | kickoff | idx | go_evt_target | nogo_evt_target | lam_eff_cut | lam_fut_cut | go_prob_target | nogo_prob_target | mpfs_eff_cut | mpfs_fut_cut | rules |
---|---|---|---|---|---|---|---|---|---|---|---|---|
NA | 20 | event num at 20 | 1 | 0 | 40 | NA | NA | NA | NA | NA | NA | NA |
NA | 24 | event num at 24 | 2 | 0 | 40 | NA | NA | NA | NA | NA | NA | NA |
NA | 28 | event num at 28 | 3 | 0 | 40 | NA | NA | NA | NA | NA | NA | NA |
6 | NA | 6m after LPI | 1 | 13 | 27 | NA | NA | NA | NA | NA | NA | NA |
9 | NA | 9m after LPI | 2 | 17 | 31 | NA | NA | NA | NA | NA | NA | NA |
12 | NA | 12m after LPI | 3 | 20 | 34 | NA | NA | NA | NA | NA | NA | NA |
NA | NA | NA | 1 | 0 | 40 | 0.069 | 0.099 | 0.50 | 0.50 | 10.0 | 7 | Go if Pr(mPFS > 10.0) > 0.50. Nogo if Pr(mPFS > 7.0) < 0.50 |
NA | NA | NA | 2 | 0 | 40 | 0.073 | 0.099 | 0.67 | 0.10 | 9.5 | 7 | Go if Pr(mPFS > 9.5) > 0.67. Nogo if Pr(mPFS > 7.0) < 0.90 |
NA | NA | NA | 3 | 0 | 40 | 0.073 | 0.099 | 0.50 | 0.15 | 9.5 | 7 | Go if Pr(mPFS > 9.5) > 0.50. Nogo if Pr(mPFS > 7.0) < 0.85 |
NA | NA | NA | 4 | 0 | 40 | 0.099 | 0.099 | 0.90 | 0.10 | 7.0 | 7 | BOP2: Go if Pr(mPFS > 7.0) > 0.90. Nogo if Pr(mPFS > 7.0) < 0.90 |
We can draw the theoretical OC curve
oc_fig_theory <- gonogo:::PFS_OC_Curve_Diff_Rules(rules_df[-(1 : nrow(rules_df1)), ],
mpfs_pick = mpfs_pick,
basic_settings = basic_settings,
pfs_settings = pfs_settings,
go_nogo_settings = go_nogo_settings,
tpp_tb = tpp_tb,
oc_params = oc_params,
plot_params = plot_params)
#> followup_time
#> NA
#> cut_evt_num
#> NA
#> kickoff
#> NA
#> idx
#> "1"
#> go_evt_target
#> "0"
#> nogo_evt_target
#> "40"
#> lam_eff_cut
#> "0.06931472"
#> lam_fut_cut
#> "0.09902103"
#> go_prob_target
#> "0.50"
#> nogo_prob_target
#> "0.50"
#> mpfs_eff_cut
#> "10.0"
#> mpfs_fut_cut
#> "7"
#> rules
#> "Go if Pr(mPFS > 10.0) > 0.50.\nNogo if Pr(mPFS > 7.0) < 0.50\n"
#> followup_time
#> NA
#> cut_evt_num
#> NA
#> kickoff
#> NA
#> idx
#> "2"
#> go_evt_target
#> "0"
#> nogo_evt_target
#> "40"
#> lam_eff_cut
#> "0.07296286"
#> lam_fut_cut
#> "0.09902103"
#> go_prob_target
#> "0.67"
#> nogo_prob_target
#> "0.10"
#> mpfs_eff_cut
#> " 9.5"
#> mpfs_fut_cut
#> "7"
#> rules
#> "Go if Pr(mPFS > 9.5) > 0.67.\nNogo if Pr(mPFS > 7.0) < 0.90\n"
#> followup_time
#> NA
#> cut_evt_num
#> NA
#> kickoff
#> NA
#> idx
#> "3"
#> go_evt_target
#> "0"
#> nogo_evt_target
#> "40"
#> lam_eff_cut
#> "0.07296286"
#> lam_fut_cut
#> "0.09902103"
#> go_prob_target
#> "0.50"
#> nogo_prob_target
#> "0.15"
#> mpfs_eff_cut
#> " 9.5"
#> mpfs_fut_cut
#> "7"
#> rules
#> "Go if Pr(mPFS > 9.5) > 0.50.\nNogo if Pr(mPFS > 7.0) < 0.85\n"
#> followup_time
#> NA
#> cut_evt_num
#> NA
#> kickoff
#> NA
#> idx
#> "4"
#> go_evt_target
#> "0"
#> nogo_evt_target
#> "40"
#> lam_eff_cut
#> "0.09902103"
#> lam_fut_cut
#> "0.09902103"
#> go_prob_target
#> "0.90"
#> nogo_prob_target
#> "0.10"
#> mpfs_eff_cut
#> " 7.0"
#> mpfs_fut_cut
#> "7"
#> rules
#> "BOP2: Go if Pr(mPFS > 7.0) > 0.90.\n Nogo if Pr(mPFS > 7.0) < 0.90\n"
oc_fig_theory$gplot
We can draw the OC curve under different data cutoff rules.
First is data cut at different number of observed events:
set.seed(random_seed, kind = "L'Ecuyer-CMRG")
oc_fig <- gonogo:::PFS_OC_Curve_Diff_Rules(rules_df[1 : (length(cut_evt_num_vec) - 1), ],
mpfs_pick = mpfs_pick,
basic_settings = basic_settings,
pfs_settings = pfs_settings,
go_nogo_settings = go_nogo_settings,
tpp_tb = tpp_tb,
oc_params = oc_params_sim,
plot_params = plot_params)
#> followup_time cut_evt_num kickoff idx
#> NA "20" "event num at 20" "1"
#> go_evt_target nogo_evt_target lam_eff_cut lam_fut_cut
#> "0" "40" NA NA
#> go_prob_target nogo_prob_target mpfs_eff_cut mpfs_fut_cut
#> NA NA NA NA
#> rules
#> NA
#> followup_time cut_evt_num kickoff idx
#> NA "24" "event num at 24" "2"
#> go_evt_target nogo_evt_target lam_eff_cut lam_fut_cut
#> "0" "40" NA NA
#> go_prob_target nogo_prob_target mpfs_eff_cut mpfs_fut_cut
#> NA NA NA NA
#> rules
#> NA
#> followup_time cut_evt_num kickoff idx
#> NA "28" "event num at 28" "3"
#> go_evt_target nogo_evt_target lam_eff_cut lam_fut_cut
#> "0" "40" NA NA
#> go_prob_target nogo_prob_target mpfs_eff_cut mpfs_fut_cut
#> NA NA NA NA
#> rules
#> NA
plot_params2 <- oc_fig$plot_params
plot_params2$use_smooth <- TRUE
oc_fig_smooth <- gonogo:::PFS_OC_Curve_Draw(oc_fig$detail_data,
tpp_tb = tpp_tb,
plot_params = plot_params2)
oc_fig$gplot
oc_fig_smooth
#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'
Second is data cut at different time after LPI:
set.seed(random_seed, kind = "L'Ecuyer-CMRG")
oc_fig <- gonogo:::PFS_OC_Curve_Diff_Rules(rules_df[(1 : (length(followup_time_vec) - 1)) + (length(cut_evt_num_vec) - 1), ],
mpfs_pick = mpfs_pick,
basic_settings = basic_settings,
pfs_settings = pfs_settings,
go_nogo_settings = go_nogo_settings,
tpp_tb = tpp_tb,
oc_params = oc_params_sim,
plot_params = plot_params)
#> followup_time cut_evt_num kickoff idx
#> " 6" NA "6m after LPI" "1"
#> go_evt_target nogo_evt_target lam_eff_cut lam_fut_cut
#> "13" "27" NA NA
#> go_prob_target nogo_prob_target mpfs_eff_cut mpfs_fut_cut
#> NA NA NA NA
#> rules
#> NA
#> followup_time cut_evt_num kickoff idx
#> " 9" NA "9m after LPI" "2"
#> go_evt_target nogo_evt_target lam_eff_cut lam_fut_cut
#> "17" "31" NA NA
#> go_prob_target nogo_prob_target mpfs_eff_cut mpfs_fut_cut
#> NA NA NA NA
#> rules
#> NA
#> followup_time cut_evt_num kickoff idx
#> "12" NA "12m after LPI" "3"
#> go_evt_target nogo_evt_target lam_eff_cut lam_fut_cut
#> "20" "34" NA NA
#> go_prob_target nogo_prob_target mpfs_eff_cut mpfs_fut_cut
#> NA NA NA NA
#> rules
#> NA
plot_params2 <- oc_fig$plot_params
plot_params2$use_smooth <- TRUE
oc_fig_smooth <- gonogo:::PFS_OC_Curve_Draw(oc_fig$detail_data,
tpp_tb = tpp_tb,
plot_params = plot_params2)
oc_fig$gplot
oc_fig_smooth
#> `geom_smooth()` using method = 'loess' and formula = 'y ~ x'
Appendix
Session info
#> ─ Session info ───────────────────────────────────────────────────────────────
#> setting value
#> version R version 4.4.1 (2024-06-14)
#> os Ubuntu 22.04.4 LTS
#> system x86_64, linux-gnu
#> ui X11
#> language en
#> collate C.UTF-8
#> ctype C.UTF-8
#> tz UTC
#> date 2024-06-24
#> pandoc 3.1.11 @ /opt/hostedtoolcache/pandoc/3.1.11/x64/ (via rmarkdown)
#>
#> ─ Packages ───────────────────────────────────────────────────────────────────
#> package * version date (UTC) lib source
#> bslib 0.7.0 2024-03-29 [1] RSPM
#> cachem 1.1.0 2024-05-16 [1] RSPM
#> cli 3.6.3 2024-06-21 [1] RSPM
#> codetools 0.2-20 2024-03-31 [3] CRAN (R 4.4.1)
#> colorspace 2.1-0 2023-01-23 [1] RSPM
#> cowplot 1.1.3 2024-01-22 [1] RSPM
#> desc 1.4.3 2023-12-10 [1] RSPM
#> digest 0.6.35 2024-03-11 [1] RSPM
#> dplyr * 1.1.4 2023-11-17 [1] RSPM
#> evaluate 0.24.0 2024-06-10 [1] RSPM
#> fansi 1.0.6 2023-12-08 [1] RSPM
#> farver 2.1.2 2024-05-13 [1] RSPM
#> fastmap 1.2.0 2024-05-15 [1] RSPM
#> fs 1.6.4 2024-04-25 [1] RSPM
#> future * 1.33.2 2024-03-26 [1] RSPM
#> future.apply * 1.11.2 2024-03-28 [1] RSPM
#> generics 0.1.3 2022-07-05 [1] RSPM
#> ggplot2 * 3.5.1 2024-04-23 [1] RSPM
#> ggrepel * 0.9.5 2024-01-10 [1] RSPM
#> globals 0.16.3 2024-03-08 [1] RSPM
#> glue 1.7.0 2024-01-09 [1] RSPM
#> gonogo * 0.0.0.9024 2024-06-24 [1] local
#> gtable 0.3.5 2024-04-22 [1] RSPM
#> highr 0.11 2024-05-26 [1] RSPM
#> htmltools 0.5.8.1 2024-04-04 [1] RSPM
#> jquerylib 0.1.4 2021-04-26 [1] RSPM
#> jsonlite 1.8.8 2023-12-04 [1] RSPM
#> knitr 1.47 2024-05-29 [1] RSPM
#> labeling 0.4.3 2023-08-29 [1] RSPM
#> lattice 0.22-6 2024-03-20 [3] CRAN (R 4.4.1)
#> lifecycle 1.0.4 2023-11-07 [1] RSPM
#> listenv 0.9.1 2024-01-29 [1] RSPM
#> magrittr 2.0.3 2022-03-30 [1] RSPM
#> Matrix 1.7-0 2024-04-26 [3] CRAN (R 4.4.1)
#> memoise 2.0.1 2021-11-26 [1] RSPM
#> mgcv 1.9-1 2023-12-21 [3] CRAN (R 4.4.1)
#> munsell 0.5.1 2024-04-01 [1] RSPM
#> nlme 3.1-164 2023-11-27 [3] CRAN (R 4.4.1)
#> parallelly 1.37.1 2024-02-29 [1] RSPM
#> pillar 1.9.0 2023-03-22 [1] RSPM
#> pkgconfig 2.0.3 2019-09-22 [1] RSPM
#> pkgdown 2.0.9 2024-04-18 [1] any (@2.0.9)
#> progressr * 0.14.0 2023-08-10 [1] RSPM
#> purrr 1.0.2 2023-08-10 [1] RSPM
#> R6 2.5.1 2021-08-19 [1] RSPM
#> ragg 1.3.2 2024-05-15 [1] RSPM
#> Rcpp 1.0.12 2024-01-09 [1] RSPM
#> rlang 1.1.4 2024-06-04 [1] RSPM
#> rmarkdown 2.27 2024-05-17 [1] RSPM
#> sass 0.4.9 2024-03-15 [1] RSPM
#> scales 1.3.0 2023-11-28 [1] RSPM
#> sessioninfo 1.2.2 2021-12-06 [1] RSPM
#> systemfonts 1.1.0 2024-05-15 [1] RSPM
#> textshaping 0.4.0 2024-05-24 [1] RSPM
#> tibble 3.2.1 2023-03-20 [1] RSPM
#> tidyr * 1.3.1 2024-01-24 [1] RSPM
#> tidyselect 1.2.1 2024-03-11 [1] RSPM
#> utf8 1.2.4 2023-10-22 [1] RSPM
#> vctrs 0.6.5 2023-12-01 [1] RSPM
#> withr 3.0.0 2024-01-16 [1] RSPM
#> xfun 0.45 2024-06-16 [1] RSPM
#> yaml 2.3.8 2023-12-11 [1] RSPM
#>
#> [1] /home/runner/work/_temp/Library
#> [2] /opt/R/4.4.1/lib/R/site-library
#> [3] /opt/R/4.4.1/lib/R/library
#>
#> ──────────────────────────────────────────────────────────────────────────────