diff --git a/R/fixed_design_lf.R b/R/fixed_design_lf.R index 301ab984..ede0fce8 100644 --- a/R/fixed_design_lf.R +++ b/R/fixed_design_lf.R @@ -46,7 +46,7 @@ #' x %>% summary() #' #' # Example 2: given sample size and compute power -#' x <- fixed_design_fh( +#' x <- fixed_design_lf( #' alpha = .025, #' enroll_rate = define_enroll_rate(duration = 18, rate = 20), #' fail_rate = define_fail_rate( diff --git a/R/globals.R b/R/globals.R index 25996aaf..4f314986 100644 --- a/R/globals.R +++ b/R/globals.R @@ -57,7 +57,7 @@ utils::globalVariables( # From `gs_design_wlr()` c( "IF", "time", "event", "info", "info0", "theta", "bound", - "z", "n", "rate" + "z", "n", "rate", "delta", "sigma2" ), # From `gs_info_ahr()` c("analysis", "time", "theta", "info", "info0"), diff --git a/R/gs_design_wlr.R b/R/gs_design_wlr.R index 16cf587b..a038f3e2 100644 --- a/R/gs_design_wlr.R +++ b/R/gs_design_wlr.R @@ -172,9 +172,18 @@ gs_design_wlr <- function( interval = interval) %>% dplyr::select(-c(n, delta, sigma2)) + # get the FA events given the FA analysis time final_event <- y$event[nrow(y)] - final_info <- max(y$info) - info_frac_by_time <- y$info / max(y$info) + + # calculate the FA info according to different info_scale + # calculate the info_frac of planned analysis time provided by `analysis_time` + if(info_scale %in% c("h0_info", "h0_h1_info")) { + final_info <- max(y$info0) + info_frac_by_time <- y$info0 / final_info + } else { + final_info <- max(y$info) + info_frac_by_time <- y$info / final_info + } # if it is info frac driven group sequential design # relabel the analysis to FA, and back calculate IAs from FA @@ -206,7 +215,8 @@ gs_design_wlr <- function( enroll_rate = enroll_rate, fail_rate = fail_rate, ratio = ratio, weight = weight, approx = approx, final_info = final_info, next_time = next_time, - input_info_frac = info_frac[n_analysis - i])$root + input_info_frac = info_frac[n_analysis - i], + info_scale = info_scale)$root y_ia <- gs_info_wlr(enroll_rate, fail_rate, ratio = ratio, event = NULL, analysis_time = ia_time, @@ -224,7 +234,8 @@ gs_design_wlr <- function( enroll_rate = enroll_rate, fail_rate = fail_rate, ratio = ratio, weight = weight, approx = approx, final_info = final_info, next_time = next_time, - input_info_frac = info_frac[n_analysis - i])$root + input_info_frac = info_frac[n_analysis - i], + info_scale = info_scale)$root y_ia <- gs_info_wlr(enroll_rate, fail_rate, ratio = ratio, event = ia_event, analysis_time = NULL, @@ -304,6 +315,7 @@ gs_design_wlr <- function( # analysis table analysis <- allout %>% select(analysis, time, n, event, ahr, theta, info, info0, info_frac) %>% + mutate(info_frac0 = info0 / max(info0)) %>% unique() %>% arrange(analysis) @@ -335,25 +347,37 @@ gs_design_wlr <- function( # utility function to find the analysis time to get the planned/input info_frac find_time_by_info_frac <- function(x, enroll_rate, fail_rate, ratio, weight, approx, final_info, next_time, - input_info_frac){ + input_info_frac, + info_scale){ + ia_info <- gs_info_wlr(analysis_time = x, event = NULL, enroll_rate = enroll_rate, fail_rate = fail_rate, weight = weight, approx = approx, ratio = ratio, interval = c(.01, next_time)) - ia_info_frac <- ia_info$info / final_info + if (info_scale %in% c("h0_info", "h0_h1_info")) { + ia_info_frac <- ia_info$info0 / final_info + } else { + ia_info_frac <- ia_info$info / final_info + } + return(ia_info_frac - input_info_frac) } # utility function to find the event to get the planned/input info_frac find_event_by_info_frac <- function(x, enroll_rate, fail_rate, ratio, weight, approx, final_info, next_time, - input_info_frac){ + input_info_frac, + info_scale){ ia_info <- gs_info_wlr(analysis_time = NULL, event = x, enroll_rate = enroll_rate, fail_rate = fail_rate, weight = weight, approx = approx, ratio = ratio, interval = c(.01, next_time)) + if (info_scale %in% c("h0_info", "h0_h1_info")) { + ia_info_frac <- ia_info$info0 / final_info + } else { + ia_info_frac <- ia_info$info / final_info + } - ia_info_frac <- ia_info$info / final_info return(ia_info_frac - input_info_frac) } diff --git a/R/summary.R b/R/summary.R index 08a17d98..fa36f867 100644 --- a/R/summary.R +++ b/R/summary.R @@ -291,7 +291,7 @@ summary.gs_design <- function(object, # (2) decimals to be displayed for the analysis variables in (3) default_vars <- if (method == "rd") c("n", "rd", "info_frac") else c( "time", "n", "event", "ahr", - switch(method, ahr = "info_frac0", wlr = "info_frac", combo = "event_frac") + switch(method, ahr = "info_frac0", wlr = "info_frac0", combo = "event_frac") ) default_decimals <- if (method == "rd") c(1, 4, 2) else c(1, 1, 1, 2, 2) diff --git a/R/to_integer.R b/R/to_integer.R index decce0ea..fc20180b 100644 --- a/R/to_integer.R +++ b/R/to_integer.R @@ -16,10 +16,38 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -#' Rounds sample size to an even number for equal design +#' Round sample size and events #' #' @param x An object returned by fixed_design_xxx() and gs_design_xxx(). #' @param ... Additional parameters (not used). +#' @details +#' For the sample size of the fixed design: +#' - When `ratio` is a positive integer, the sample size is rounded up to a multiple of `ratio + 1` +#' if `round_up_final = TRUE`, and just rounded to a multiple of `ratio + 1` if `round_up_final = FALSE`. +#' - When `ratio` is a positive non-integer, the sample size is rounded up if `round_up_final = TRUE`, +#' (may not be a multiple of `ratio + 1`), and just rounded if `round_up_final = FALSE` (may not be a multiple of `ratio + 1`). +#' Note the default `ratio` is taken from `x$input$ratio`. +#' +#' For the number of events of the fixed design: +#' - If the continuous event is very close to an integer within 0.01 differences, say 100.001 or 99.999, then the integer events is 100. +#' - Otherwise, round up if `round_up_final = TRUE` and round if `round_up_final = FALSE`. +#' +#' For the sample size of group sequential designs: +#' - When `ratio` is a positive integer, the final sample size is rounded to a multiple of `ratio + 1`. +#' + For 1:1 randomization (experimental:control), set `ratio = 1` to round to an even sample size. +#' + For 2:1 randomization, set `ratio = 2` to round to a multiple of 3. +#' + For 3:2 randomization, set `ratio = 4` to round to a multiple of 5. +#' + Note that for the final analysis, the sample size is rounded up to the nearest multiple of `ratio + 1` if `round_up_final = TRUE`. +#' If `round_up_final = FALSE`, the final sample size is rounded to the nearest multiple of `ratio + 1`. +#' - When `ratio` is positive non-integer, the final sample size MAY NOT be rounded to a multiple of `ratio + 1`. +#' + The final sample size is rounded up if `round_up_final = TRUE`. +#' + Otherwise, it is just rounded. +#' +#' For the events of group sequential designs: +#' - For events at interim analysis, it is rounded. +#' - For events at final analysis: +#' + If the continuous event is very close to an integer within 0.01 differences, say 100.001 or 99.999, then the integer events is 100. +#' + Otherwise, final events is rounded up if `round_up_final = TRUE` and rounded if `round_up_final = FALSE`. #' #' @return A list similar to the output of fixed_design_xxx() and gs_design_xxx(), #' except the sample size is an integer. @@ -30,10 +58,6 @@ to_integer <- function(x, ...) { } #' @rdname to_integer -#' -#' @param sample_size Logical, indicting if ceiling -#' sample size to an even integer. -#' #' @export #' #' @examples @@ -89,22 +113,55 @@ to_integer <- function(x, ...) { #' to_integer() |> #' summary() #' } -to_integer.fixed_design <- function(x, sample_size = TRUE, ...) { +to_integer.fixed_design <- function(x, round_up_final = TRUE, ratio = x$input$ratio, ...) { + + if (ratio < 0) { + stop("The ratio must be non-negative.") + } + + if (!is_wholenumber(ratio)) { + message("The output sample size is just rounded, may not a multiple of (ratio + 1).") + } + output_n <- x$analysis$n input_n <- expected_accrual(time = x$analysis$time, enroll_rate = x$input$enroll_rate) - multiply_factor <- x$input$ratio + 1 - enroll_rate_new <- x$enroll_rate %>% - mutate(rate = rate * ceiling(output_n / multiply_factor) * multiply_factor / output_n) + multiply_factor <- ratio + 1 + ss <- output_n / multiply_factor + if (is_wholenumber(ratio)) { + if (round_up_final) { + sample_size_new <- ceiling(ss) * multiply_factor + } else { + sample_size_new <- round(ss, 0) * multiply_factor + } + } else { + if (round_up_final) { + sample_size_new <- ceiling(output_n) + } else { + sample_size_new <- round(output_n, 0) + } + } - # Round up the FA events - event_ceiling <- ceiling(x$analysis$event) + enroll_rate_new <- x$enroll_rate %>% + mutate(rate = rate * sample_size_new / output_n) + + # Round events + # if events is very close to an integer, set it as this integer + if (abs(x$analysis$event - round(x$analysis$event)) < 0.01) { + event_new <- round(x$analysis$event) + # ceiling the FA events as default + } else if (round_up_final) { + event_new <- ceiling(x$analysis$event) + # otherwise, round the FA events + } else{ + event_new <- round(x$analysis$event, 0) + } if ((x$design == "ahr") && (input_n != output_n)) { x_new <- gs_power_ahr( enroll_rate = enroll_rate_new, fail_rate = x$input$fail_rate, - event = event_ceiling, + event = event_new, analysis_time = NULL, ratio = x$input$ratio, upper = gs_b, lower = gs_b, @@ -131,7 +188,7 @@ to_integer.fixed_design <- function(x, sample_size = TRUE, ...) { x_new <- gs_power_wlr( enroll_rate = enroll_rate_new, fail_rate = x$input$fail_rate, - event = event_ceiling, + event = event_new, analysis_time = NULL, ratio = x$input$ratio, upper = gs_b, lower = gs_b, @@ -164,7 +221,7 @@ to_integer.fixed_design <- function(x, sample_size = TRUE, ...) { x_new <- gs_power_wlr( enroll_rate = enroll_rate_new, fail_rate = x$input$fail_rate, - event = event_ceiling, + event = event_new, analysis_time = NULL, ratio = x$input$ratio, weight = function(s, arm0, arm1) { @@ -208,6 +265,12 @@ to_integer.fixed_design <- function(x, sample_size = TRUE, ...) { } #' @rdname to_integer +#' @param round_up_final Events at final analysis is rounded up if `TRUE`; +#' otherwise, just rounded, unless it is very close to an integer. +#' @param ratio Positive integer for randomization ratio (experimental:control). +#' A positive integer will result in rounded sample size, which is a multiple of (ratio + 1). +#' A positive non-integer will result in round sample size, which may not be a multiple of (ratio + 1). +#' A negative number will result in an error. #' #' @export #' @@ -264,7 +327,7 @@ to_integer.fixed_design <- function(x, sample_size = TRUE, ...) { #' x$bound$`nominal p`[1] #' gsDesign::sfLDOF(alpha = 0.025, t = 18 / 30)$spend #' } -to_integer.gs_design <- function(x, sample_size = TRUE, ...) { +to_integer.gs_design <- function(x, round_up_final = TRUE, ratio = x$input$ratio, ...) { is_ahr <- inherits(x, "ahr") is_wlr <- inherits(x, "wlr") is_rd <- inherits(x, "rd") @@ -273,8 +336,16 @@ to_integer.gs_design <- function(x, sample_size = TRUE, ...) { return(x) } + if (ratio < 0) { + stop("The ratio must be non-negative.") + } + + if (!is_wholenumber(ratio)) { + message("The output sample size is just rounded, may not a multiple of (ratio + 1).") + } + n_analysis <- length(x$analysis$analysis) - multiply_factor <- x$input$ratio + 1 + multiply_factor <- ratio + 1 if (!is_rd) { # Updated events to integer @@ -282,11 +353,44 @@ to_integer.gs_design <- function(x, sample_size = TRUE, ...) { if (n_analysis == 1) { event_new <- ceiling(event) } else { - event_new <- c(floor(event[1:(n_analysis - 1)]), ceiling(event[n_analysis])) + # round IA events to the closest integer + event_ia_new <- round(event[1:(n_analysis - 1)]) + + # if the FA events is very close to an integer, set it as this integer + if (abs(event[n_analysis] - round(event[n_analysis])) < 0.01) { + event_fa_new <- round(event[n_analysis]) + # ceiling the FA events as default + } else if (round_up_final) { + event_fa_new <- ceiling(event[n_analysis]) + # otherwise, round the FA events + } else{ + event_fa_new <- round(event[n_analysis], 0) + } + + event_new <- c(event_ia_new, event_fa_new) } # Updated sample size to integer and enroll rates - sample_size_new <- (ceiling(x$analysis$n[n_analysis] / multiply_factor) * multiply_factor) %>% as.integer() + # if the randomization ratio is a whole number, round the sample size as a multiplier of ratio + 1 + if(is_wholenumber(ratio)) { + ss <- x$analysis$n[n_analysis] / multiply_factor + + if (round_up_final) { + sample_size_new <- ceiling(ss) * multiply_factor + } else { + sample_size_new <- round(ss, 0) * multiply_factor + } + # if the randomization ratio is NOT a whole number, just round it + } else { + if (round_up_final) { + sample_size_new <- ceiling(x$analysis$n[n_analysis]) + } else { + sample_size_new <- round(x$analysis$n[n_analysis], 0) + } + } + + sample_size_new <- as.integer(sample_size_new) + enroll_rate <- x$enroll_rate enroll_rate_new <- enroll_rate %>% mutate(rate = rate * sample_size_new / x$analysis$n[n_analysis]) @@ -300,17 +404,28 @@ to_integer.gs_design <- function(x, sample_size = TRUE, ...) { } else if (identical(x$input$upper, gs_spending_bound)) { upar_new <- x$input$upar if (!("timing" %in% names(x$input$upar))) { - info_with_new_event <- gs_info_ahr( - enroll_rate = enroll_rate_new, - fail_rate = x$input$fail_rate, - ratio = x$input$ratio, - event = event_new, - analysis_time = NULL - ) + if (is_ahr) { + info_with_new_event <- gs_info_ahr( + enroll_rate = enroll_rate_new, + fail_rate = x$input$fail_rate, + ratio = ratio, + event = event_new, + analysis_time = NULL + ) + } else if (is_wlr) { + info_with_new_event <- gs_info_wlr( + enroll_rate = enroll_rate_new, + fail_rate = x$input$fail_rate, + ratio = ratio, + event = event_new, + analysis_time = NULL, + weight = x$input$weight + ) + } # ensure info0 is based on integer sample size calculation # as as they become a slight different number due to the `enroll_rate` - q_e <- x$input$ratio / (1 + x$input$ratio) + q_e <- ratio / (1 + ratio) q_c <- 1 - q_e info_with_new_event$info0 <- event_new * q_e * q_c @@ -343,7 +458,7 @@ to_integer.gs_design <- function(x, sample_size = TRUE, ...) { fail_rate = x$input$fail_rate, event = event_new, analysis_time = NULL, - ratio = x$input$ratio, + ratio = ratio, upper = x$input$upper, upar = upar_new, lower = x$input$lower, lpar = lpar_new, test_upper = x$input$test_upper, @@ -359,12 +474,24 @@ to_integer.gs_design <- function(x, sample_size = TRUE, ...) { n_stratum <- length(x$input$p_c$stratum) # Update unstratified sample size to integer + sample_size_new_ia <- round(x$analysis$n[1:(n_analysis - 1)], 0) + if (round_up_final) { + if (is_wholenumber(ratio)) { + sample_size_new_fa <- ceiling(x$analysis$n[n_analysis] / multiply_factor) * multiply_factor + } else { + sample_size_new_fa <- ceiling(x$analysis$n[n_analysis]) + } + } else { + if (is_wholenumber(ratio)) { + sample_size_new_fa <- round(x$analysis$n[n_analysis] / multiply_factor, 0) * multiply_factor + } else { + sample_size_new_fa <- round(x$analysis$n[n_analysis], 0) + } + } + sample_size_new <- tibble( analysis = 1:n_analysis, - n = c( - floor(x$analysis$n[1:(n_analysis - 1)] / multiply_factor), - ceiling(x$analysis$n[n_analysis] / multiply_factor) - ) * multiply_factor + n = c(sample_size_new_ia, sample_size_new_fa) ) # Update sample size per stratum @@ -399,7 +526,7 @@ to_integer.gs_design <- function(x, sample_size = TRUE, ...) { p_e = x$input$p_e, n = tbl_n, rd0 = x$input$rd, - ratio = x$input$ratio, + ratio = ratio, weight = x$input$weight ) @@ -423,7 +550,7 @@ to_integer.gs_design <- function(x, sample_size = TRUE, ...) { p_e = x$input$p_e, n = tbl_n, rd0 = x$input$rd0, - ratio = x$input$ratio, + ratio = ratio, weight = x$input$weight, upper = x$input$upper, lower = x$input$lower, diff --git a/R/utils.R b/R/utils.R index 901c9281..5257379b 100644 --- a/R/utils.R +++ b/R/utils.R @@ -40,3 +40,9 @@ replace_names <- function(x, ...) { round2 <- function(x, digits, ...) { if (is.numeric(x) && !is.na(digits)) round(x, digits, ...) else x } + +# test if it is whole number +is_wholenumber <- function (x, tol = .Machine$double.eps^0.5) { + abs(x - round(x)) < tol +} + diff --git a/man/fixed_design.Rd b/man/fixed_design.Rd index 113dd28c..afc3d7f6 100644 --- a/man/fixed_design.Rd +++ b/man/fixed_design.Rd @@ -232,7 +232,7 @@ x <- fixed_design_lf( x \%>\% summary() # Example 2: given sample size and compute power -x <- fixed_design_fh( +x <- fixed_design_lf( alpha = .025, enroll_rate = define_enroll_rate(duration = 18, rate = 20), fail_rate = define_fail_rate( diff --git a/man/to_integer.Rd b/man/to_integer.Rd index 2086dc38..9e61c6a7 100644 --- a/man/to_integer.Rd +++ b/man/to_integer.Rd @@ -4,28 +4,76 @@ \alias{to_integer} \alias{to_integer.fixed_design} \alias{to_integer.gs_design} -\title{Rounds sample size to an even number for equal design} +\title{Round sample size and events} \usage{ to_integer(x, ...) -\method{to_integer}{fixed_design}(x, sample_size = TRUE, ...) +\method{to_integer}{fixed_design}(x, round_up_final = TRUE, ratio = x$input$ratio, ...) -\method{to_integer}{gs_design}(x, sample_size = TRUE, ...) +\method{to_integer}{gs_design}(x, round_up_final = TRUE, ratio = x$input$ratio, ...) } \arguments{ \item{x}{An object returned by fixed_design_xxx() and gs_design_xxx().} \item{...}{Additional parameters (not used).} -\item{sample_size}{Logical, indicting if ceiling -sample size to an even integer.} +\item{round_up_final}{Events at final analysis is rounded up if \code{TRUE}; +otherwise, just rounded, unless it is very close to an integer.} + +\item{ratio}{Positive integer for randomization ratio (experimental:control). +A positive integer will result in rounded sample size, which is a multiple of (ratio + 1). +A positive non-integer will result in round sample size, which may not be a multiple of (ratio + 1). +A negative number will result in an error.} } \value{ A list similar to the output of fixed_design_xxx() and gs_design_xxx(), except the sample size is an integer. } \description{ -Rounds sample size to an even number for equal design +Round sample size and events +} +\details{ +For the sample size of the fixed design: +\itemize{ +\item When \code{ratio} is a positive integer, the sample size is rounded up to a multiple of \code{ratio + 1} +if \code{round_up_final = TRUE}, and just rounded to a multiple of \code{ratio + 1} if \code{round_up_final = FALSE}. +\item When \code{ratio} is a positive non-integer, the sample size is rounded up if \code{round_up_final = TRUE}, +(may not be a multiple of \code{ratio + 1}), and just rounded if \code{round_up_final = FALSE} (may not be a multiple of \code{ratio + 1}). +Note the default \code{ratio} is taken from \code{x$input$ratio}. +} + +For the number of events of the fixed design: +\itemize{ +\item If the continuous event is very close to an integer within 0.01 differences, say 100.001 or 99.999, then the integer events is 100. +\item Otherwise, round up if \code{round_up_final = TRUE} and round if \code{round_up_final = FALSE}. +} + +For the sample size of group sequential designs: +\itemize{ +\item When \code{ratio} is a positive integer, the final sample size is rounded to a multiple of \code{ratio + 1}. +\itemize{ +\item For 1:1 randomization (experimental:control), set \code{ratio = 1} to round to an even sample size. +\item For 2:1 randomization, set \code{ratio = 2} to round to a multiple of 3. +\item For 3:2 randomization, set \code{ratio = 4} to round to a multiple of 5. +\item Note that for the final analysis, the sample size is rounded up to the nearest multiple of \code{ratio + 1} if \code{round_up_final = TRUE}. +If \code{round_up_final = FALSE}, the final sample size is rounded to the nearest multiple of \code{ratio + 1}. +} +\item When \code{ratio} is positive non-integer, the final sample size MAY NOT be rounded to a multiple of \code{ratio + 1}. +\itemize{ +\item The final sample size is rounded up if \code{round_up_final = TRUE}. +\item Otherwise, it is just rounded. +} +} + +For the events of group sequential designs: +\itemize{ +\item For events at interim analysis, it is rounded. +\item For events at final analysis: +\itemize{ +\item If the continuous event is very close to an integer within 0.01 differences, say 100.001 or 99.999, then the integer events is 100. +\item Otherwise, final events is rounded up if \code{round_up_final = TRUE} and rounded if \code{round_up_final = FALSE}. +} +} } \examples{ library(dplyr) diff --git a/tests/testthat/test-developer-gs_design_wlr.R b/tests/testthat/test-developer-gs_design_wlr.R index 58e8e944..e2c4a96e 100644 --- a/tests/testthat/test-developer-gs_design_wlr.R +++ b/tests/testthat/test-developer-gs_design_wlr.R @@ -21,7 +21,7 @@ test_that("Validate info-frac driven design with a known study duration",{ # validate the info frac - expect_equal(x$analysis$info_frac, c(0.3, 0.7, 1), tolerance = 1e-6) + expect_equal(x$analysis$info_frac0, c(0.3, 0.7, 1), tolerance = 5e-5) # validate the final analysis time expect_equal(max(x$analysis$time), 36) }) @@ -108,4 +108,112 @@ test_that("Validate calendar-time and info-frac driven design -- C",{ expect_equal(x$analysis$time[idx_driven_by_time], analysis_time[idx_driven_by_time]) expect_equal(x$analysis$info_frac[idx_driven_by_info_frac], info_frac[idx_driven_by_info_frac], tolerance = 1e-5) -}) \ No newline at end of file +}) + +test_that("Validate if the output info-frac match the planned info-frac, when the design is only driven by info frac", { + x1 <- gs_design_wlr( + alpha = 0.025, + beta = 0.9, + enroll_rate = enroll_rate, + fail_rate = fail_rate, + ratio = ratio, + info_frac = c(0.75, 1), + analysis_time = 36, + upper = upper, + upar = upar, + lower = lower, + lpar = lpar, + info_scale = "h0_info", + weight = function(x, arm0, arm1) {wlr_weight_fh(x, arm0, arm1, rho = 0, gamma = 0)} + ) + + x2 <- gs_design_wlr( + alpha = 0.025, + beta = 0.9, + enroll_rate = enroll_rate, + fail_rate = fail_rate, + ratio = ratio, + info_frac = c(0.75, 1), + analysis_time = 36, + upper = upper, + upar = upar, + lower = lower, + lpar = lpar, + info_scale = "h0_h1_info", + weight = function(x, arm0, arm1) {wlr_weight_fh(x, arm0, arm1, rho = 0, gamma = 0)} + ) + + x3 <- gs_design_wlr( + alpha = 0.025, + beta = 0.9, + enroll_rate = enroll_rate, + fail_rate = fail_rate, + ratio = ratio, + info_frac = c(0.75, 1), + analysis_time = 36, + upper = upper, + upar = upar, + lower = lower, + lpar = lpar, + info_scale = "h1_info", + weight = function(x, arm0, arm1) {wlr_weight_fh(x, arm0, arm1, rho = 0, gamma = 0)} + ) + + expect_equal(x1$analysis$info_frac[1], 0.75, tolerance = 1e-6) + expect_equal(x2$analysis$info_frac0[1], 0.75, tolerance = 1e-6) + expect_equal(x3$analysis$info_frac[1], 0.75, tolerance = 1e-6) +}) + +test_that("Validate if the output info-frac match the planned info-frac, when the design is driven by both info frac and analysis time", { + x1 <- gs_design_wlr( + alpha = 0.025, + beta = 0.9, + enroll_rate = enroll_rate, + fail_rate = fail_rate, + ratio = ratio, + info_frac = c(0.75, 1), + analysis_time = c(1, 36), + upper = upper, + upar = upar, + lower = lower, + lpar = lpar, + info_scale = "h0_info", + weight = function(x, arm0, arm1) {wlr_weight_fh(x, arm0, arm1, rho = 0, gamma = 0)} + ) + + x2 <- gs_design_wlr( + alpha = 0.025, + beta = 0.9, + enroll_rate = enroll_rate, + fail_rate = fail_rate, + ratio = ratio, + info_frac = c(0.75, 1), + analysis_time = c(1, 36), + upper = upper, + upar = upar, + lower = lower, + lpar = lpar, + info_scale = "h0_h1_info", + weight = function(x, arm0, arm1) {wlr_weight_fh(x, arm0, arm1, rho = 0, gamma = 0)} + ) + + x3 <- gs_design_wlr( + alpha = 0.025, + beta = 0.9, + enroll_rate = enroll_rate, + fail_rate = fail_rate, + ratio = ratio, + info_frac = c(0.75, 1), + analysis_time = c(1, 36), + upper = upper, + upar = upar, + lower = lower, + lpar = lpar, + info_scale = "h1_info", + weight = function(x, arm0, arm1) {wlr_weight_fh(x, arm0, arm1, rho = 0, gamma = 0)} + ) + + expect_equal(x1$analysis$info_frac[1], 0.75, tolerance = 5e-6) + expect_equal(x2$analysis$info_frac0[1], 0.75, tolerance = 5e-6) + expect_equal(x3$analysis$info_frac[1], 0.75, tolerance = 5e-6) +}) diff --git a/tests/testthat/test-developer-to_integer.R b/tests/testthat/test-developer-to_integer.R index 700530a6..0bfc7cef 100644 --- a/tests/testthat/test-developer-to_integer.R +++ b/tests/testthat/test-developer-to_integer.R @@ -51,3 +51,200 @@ test_that("The statistcial information under null equals to event/4 udner equal expect_true(all(x$analysis$info0 - x$analysis$event / 4 == 0)) }) + +test_that("Validate the sample size rounding under equal randomization (1:1) for TTE endpoint. -- GSD", { + + x <- gs_design_ahr(analysis_time = c(24, 36)) + + # ----------------------- # + # round_up_final = TRUE # + # ----------------------- # + y1 <- x |> to_integer(round_up_final = TRUE) + # test the sample size at FA is rounded up and multiple of 2 + expect_equal(ceiling(x$analysis$n[2] / 2) * 2, y1$analysis$n[2]) + # test the event at FA is rounded up + expect_equal(ceiling(x$analysis$event[2]), y1$analysis$event[2]) + # test the event at IA is rounded + expect_equal(round(x$analysis$event[1], 0), y1$analysis$event[1]) + + + # ----------------------- # + # round_up_final = FALSE # + # ----------------------- # + y2 <- x |> to_integer(round_up_final = FALSE) + # test the sample size at FA is rounded up and multiple of 2 + expect_equal(round(x$analysis$n[2] / 2, 0) * 2, y2$analysis$n[2]) + # test the event at FA is rounded + expect_equal(round(x$analysis$event[2], 0), y2$analysis$event[2]) + # test the event at IA is rounded + expect_equal(round(x$analysis$event[1], 0), y2$analysis$event[1]) + + expect_error(x |> to_integer(ratio = -2)) +}) + +test_that("Validate the sample size rounding under unequal randomization (3:2) for TTE endpoint. -- GSD", { + + x <- gs_design_ahr(analysis_time = c(24, 36), ratio = 1.5, + alpha = 0.025, + upper = gs_spending_bound, + upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025)) + + # ---------------------------------------------- # + # round_up_final = TRUE & ratio is NOT integer # + # ---------------------------------------------- # + y1 <- x |> to_integer(round_up_final = TRUE) + # test the FA sample size is rounded up, but may not be a multiplier of 5 + expect_equal(ceiling(x$analysis$n[2]), y1$analysis$n[2]) + # test the FA events is rounded up + expect_equal(ceiling(x$analysis$event[2]), y1$analysis$event[2]) + # test the IA events is rounded + expect_equal(round(x$analysis$event[1], 0), y1$analysis$event[1]) + + # ---------------------------------------------- # + # round_up_final = TRUE & ratio is integer # + # ---------------------------------------------- # + y2 <- x |> to_integer(round_up_final = TRUE, ratio = 4) + # test the FA sample size is round up, and is a multiplier of 5 + expect_equal(ceiling(x$analysis$n[2] / 5) * 5, y2$analysis$n[2]) + # test the FA events is rounded up + expect_equal(ceiling(x$analysis$event[2]), y2$analysis$event[2]) + # test the IA events is rounded + expect_equal(round(x$analysis$event[1], 0), y2$analysis$event[1]) + + # ---------------------------------------------- # + # round_up_final = FALSE & ratio is NOT integer # + # ---------------------------------------------- # + y3 <- x |> to_integer(round_up_final = FALSE) + # test the sample size at FA is rounded, but may not a multiplier of 5 + expect_equal(round(x$analysis$n[2]), y3$analysis$n[2]) + # test the FA events is rounded + expect_equal(round(x$analysis$event[2], 0), y2$analysis$event[2]) + # test the IA events is rounded + expect_equal(round(x$analysis$event[1], 0), y2$analysis$event[1]) + + # ---------------------------------------------- # + # round_up_final = FALSE & ratio is integer # + # ---------------------------------------------- # + y4 <- x |> to_integer(round_up_final = FALSE, ratio = 4) + # test the FA sample size is rounded, but may not is a multiplier of 5 + expect_equal(round(x$analysis$n[2] / 5, 0) * 5, y4$analysis$n[2]) + # test the FA events is rounded + expect_equal(round(x$analysis$event[2], 0), y2$analysis$event[2]) + # test the IA events is rounded + expect_equal(round(x$analysis$event[1], 0), y2$analysis$event[1]) + + # error when ratio is negative + expect_error(x |> to_integer(ratio = -2)) +}) + +test_that("Validate the sample size rounding under equal randomization (1:1) for binary endpoint. -- GSD", { + + x <- gs_design_rd(ratio = 1, + alpha = 0.025, + info_frac = 1:3/3, + upper = gs_spending_bound, + upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025)) + + y1 <- x |> to_integer(round_up_final = TRUE) + y2 <- x |> to_integer(round_up_final = FALSE) + + expect_equal(c(round(x$analysis$n[1:2], 0), ceiling(x$analysis$n[3] / 2) * 2), y1$analysis$n) + expect_equal(c(round(x$analysis$n[1:2], 0), round(x$analysis$n[3] / 2, 0) * 2), y2$analysis$n) + + expect_error(x |> to_integer(ratio = -2)) +}) + +test_that("Validate the sample size rounding under unequal randomization (3:2) for binary endpoint. -- GSD", { + + x <- gs_design_rd(ratio = 1.5, + alpha = 0.025, + info_frac = 1:3/3, + upper = gs_spending_bound, + upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025)) + + # ceiling the sample size at FA, but may not be a multiplier of 5 + y1 <- x |> to_integer(round_up_final = TRUE) + expect_equal(ceiling(x$analysis$n[3]) , y1$analysis$n[3]) + + # ceiling the sample size at FA, and is a multiplier of 5 + y2 <- x |> to_integer(round_up_final = TRUE, ratio = 4) + expect_equal(ceiling(x$analysis$n[3] / 4) * 4, y2$analysis$n[3]) + + # round the sample size at FA, but may not a multiplier of 5 + y3 <- x |> to_integer(round_up_final = FALSE) + expect_equal(round(x$analysis$n[3], 0), y3$analysis$n[3]) + + # round the sample size at FA, and is a multiplier of 5 + y4 <- x |> to_integer(round_up_final = FALSE, ratio = 4) + expect_equal(round(x$analysis$n[3] / 5, 0) * 5, y4$analysis$n[3]) + + # error when ratio is negative + expect_error(x |> to_integer(ratio = -2)) +}) + +test_that("Validate the sample size rounding under equal randomization (1:1) for TTE endpoint -- fixed design.", { + + x <- fixed_design_ahr(alpha = .025, power = .9, ratio = 1, + enroll_rate = define_enroll_rate(duration = 18, rate = 1), + fail_rate = define_fail_rate(duration = c(4, 100), + fail_rate = log(2) / 10, hr = c(1, .6), + dropout_rate = .001), + study_duration = 36) + + y1 <- x |> to_integer(round_up_final = TRUE) + y2 <- x |> to_integer(round_up_final = FALSE) + + expect_equal(ceiling(x$analysis$n / 2) * 2, y1$analysis$n) + expect_equal(round(x$analysis$n / 2, 0) * 2, y2$analysis$n) + + expect_error(x |> to_integer(ratio = -2)) +}) + +test_that("Validate the sample size rounding under unequal randomization (3:2) for TTE endpoint.", { + + x <- fixed_design_ahr(alpha = .025, power = .9, ratio = 1.5, + enroll_rate = define_enroll_rate(duration = 18, rate = 1), + fail_rate = define_fail_rate(duration = c(4, 100), + fail_rate = log(2) / 10, hr = c(1, .6), + dropout_rate = .001), + study_duration = 36) + + # ---------------------------------------------- # + # round_up_final = TRUE & ratio is NOT integer # + # ---------------------------------------------- # + y1 <- x |> to_integer(round_up_final = TRUE) + # test the sample size is rounded up, but may not be a multiplier of 5 + expect_equal(ceiling(x$analysis$n), y1$analysis$n) + # test the event is rounded up + expect_equal(ceiling(x$analysis$event), y1$analysis$event) + + # ---------------------------------------------- # + # round_up_final = TRUE & ratio is integer # + # ---------------------------------------------- # + y2 <- x |> to_integer(round_up_final = TRUE, ratio = 4) + # test the sample size is rounded up, and is a multiplier of 5 + expect_equal(ceiling(x$analysis$n / 5) * 5, y2$analysis$n) + # test the event is rounded up + expect_equal(ceiling(x$analysis$event), y2$analysis$event) + + # ---------------------------------------------- # + # round_up_final = FALSE & ratio is NOT integer # + # ---------------------------------------------- # + y3 <- x |> to_integer(round_up_final = FALSE) + # test the sample size is rounded, but may not a multiplier of 5 + expect_equal(round(x$analysis$n), y3$analysis$n) + # test the event is rounded + expect_equal(round(x$analysis$event, 0), y3$analysis$event) + + # ---------------------------------------------- # + # round_up_final = FALSE & ratio is integer # + # ---------------------------------------------- # + y4 <- x |> to_integer(round_up_final = FALSE, ratio = 4) + # test the sample size is rounded, and is a multiplier of 5 + expect_equal(round(x$analysis$n / 5, 0) * 5, y4$analysis$n) + # test the event is rounded + expect_equal(ceiling(x$analysis$event), y4$analysis$event) + + # error when ratio is negative + expect_error(x |> to_integer(ratio = -2)) +})