diff --git a/.claude/skills/write-tests.md b/.claude/skills/write-tests.md index 98dfc01c1..01a9f3c20 100644 --- a/.claude/skills/write-tests.md +++ b/.claude/skills/write-tests.md @@ -16,11 +16,11 @@ This project uses **testit** for testing. testit assertions are plain R expressi tests/ ├── test-all.R # Runner: library(testit); test_pkg("gsDesign2") └── testit/ - ├── helper.R # Shared setup (sourced before test files) - ├── helper-*.R # Additional helpers + ├── helper.R # Shared setup (auto-sourced before test files) + ├── helper-*.R # Additional helpers (also auto-sourced) ├── fixtures/ # Test data (.Rdata, .rds, etc.) ├── test-*.R # Test files - └── test-*.md # Snapshot files (paired with .R files) + └── test-*.md # Snapshot files (standalone, no .R file needed) ``` ## Core Pattern @@ -131,7 +131,7 @@ assert("output structure is correct", { ## Snapshot Tests -Create a `.md` file alongside the `.R` file (same base name). Format: +A `.md` snapshot file is a standalone test — it does NOT require a paired `.R` file. The `.md` file contains both the code and the expected output. Format: ````markdown ## `function_name()` description @@ -154,7 +154,7 @@ testit runs the R code block and compares output to the text block. To initializ 3. **Use `all.equal(..., tolerance = t)` with the tightest tolerance that passes** — don't use overly loose tolerances. 4. **Group related assertions in one `assert()` block** — each block should test one logical concept. 5. **Use descriptive assert messages** — they appear in failure output. -6. **Shared setup goes in `helper.R`** — it's sourced before all test files. +6. **Shared setup goes in `helper*.R` files** — testit auto-sources all `helper*.R` files before test files. Never `source()` them manually. 7. **Load fixture data with `load("fixtures/file.Rdata")`** — paths are relative to `tests/testit/`. 8. **Use `all.equal()` only when exact comparison fails in CI** — typically macOS produces slightly different floating-point results while `identical()` works fine on Windows/Linux. diff --git a/DESCRIPTION b/DESCRIPTION index 0f18853b1..70e0b3c17 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -68,4 +68,4 @@ VignetteBuilder: LinkingTo: Rcpp Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.3 +Config/roxygen2/version: 8.0.0 diff --git a/R/as_gt.R b/R/as_gt.R index c7a2b8bb0..858005369 100644 --- a/R/as_gt.R +++ b/R/as_gt.R @@ -111,7 +111,7 @@ as_gt.fixed_design_summary <- function(x, title = NULL, footnote = NULL, ...) { #' "spanner")`; users can use the functions in the `gt` package to customize #' the table. To disable footnotes, use `footnote = FALSE`. #' @param display_bound A vector of strings specifying the label of the bounds. -#' The default is `c("Efficacy", "Futility")`. +#' The default is `c("Efficacy", "Futility", "Harm")`. #' @param display_columns A vector of strings specifying the variables to be #' displayed in the summary table. #' @param display_inf_bound Logical, whether to display the +/-inf bound. @@ -128,7 +128,22 @@ as_gt.fixed_design_summary <- function(x, title = NULL, footnote = NULL, ...) { #' gs_design_ahr() |> #' summary() |> #' as_gt() -#' +#' +#' gs_design_ahr( +#' analysis_time = c(12, 24, 36), +#' upper = gs_spending_bound, +#' upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025), +#' test_upper = c(FALSE, TRUE, TRUE), +#' lower = gs_spending_bound, +#' lpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -2), +#' test_lower = c(TRUE, TRUE, FALSE), +#' harm = gs_spending_bound, +#' hpar = list(sf = gsDesign::sfHSD, total_spend = 0.2, param = -4), +#' test_harm = c(TRUE, TRUE, FALSE) +#' ) |> +#' summary() |> +#' as_gt() +#' #' gs_power_ahr(lpar = list(sf = gsDesign::sfLDOF, total_spend = 0.1)) |> #' summary() |> #' as_gt() @@ -193,7 +208,7 @@ as_gt.fixed_design_summary <- function(x, title = NULL, footnote = NULL, ...) { #' #' # Example 5 ---- #' # Usage of display_bound = ... -#' # to either show efficacy bound or futility bound, or both(default) +#' # to show selected bounds #' gs_power_wlr(lpar = list(sf = gsDesign::sfLDOF, total_spend = 0.1)) |> #' summary() |> #' as_gt(display_bound = "Efficacy") @@ -212,7 +227,7 @@ as_gt.gs_design_summary <- function( colname_spanner = "Cumulative boundary crossing probability", colname_spannersub = c("Alternate hypothesis", "Null hypothesis"), footnote = NULL, - display_bound = c("Efficacy", "Futility"), + display_bound = c("Efficacy", "Futility", "Harm"), display_columns = NULL, display_inf_bound = FALSE, ...) { @@ -364,6 +379,7 @@ gsd_parts <- function( x2 <- x2[, columns] x2 <- subset(x2, !is.na(`Alternate hypothesis`) & !is.na(`Null hypothesis`)) x2 <- subset(x2, Bound %in% bound) + x2$Bound <- factor(x2$Bound, levels = bound) i <- match(c("Alternate hypothesis", "Null hypothesis"), names(x2)) names(x2)[i] <- spannersub @@ -382,10 +398,10 @@ gsd_parts <- function( ) list( - x = arrange(x2, Analysis), + x = arrange(x2, Analysis, Bound), title = title, subtitle = subtitle, footnote = if (!isFALSE(footnote)) footnote %||% gsd_footnote(method, columns), - alpha = max(filter(x, Bound == bound[1])[["Null hypothesis"]]) + alpha = max(filter(x, Bound == "Efficacy")[["Null hypothesis"]]) ) } diff --git a/R/gs_bound_summary.R b/R/gs_bound_summary.R index f2625b58a..35d333e2e 100644 --- a/R/gs_bound_summary.R +++ b/R/gs_bound_summary.R @@ -1,6 +1,6 @@ #' Bound summary table #' -#' Summarizes the efficacy and futility bounds for each analysis. +#' Summarizes the efficacy, futility, and harm bounds for each analysis. #' #' @param x Design object. #' @param alpha Vector of alpha values to compute additional efficacy columns. @@ -52,14 +52,25 @@ gs_bound_summary <- function(x, digits = 4, ddigits = 2, tdigits = 0, timename = } } out <- Reduce(cbind, outlist) - # Use of union() allows placement of column "Futility" at the far right, but - # only if it is returned by gs_bound_summary_single(). This is because - # one-sided designs do not produce a Futility column. + # Use of union() allows placement of columns "Futility" and "Harm" at the far + # right, but only if they are returned by gs_bound_summary_single(). This is + # because one-sided designs do not produce a Futility column, and designs + # without harm bounds do not produce a Harm column. column_order <- union(c("Analysis", "Value", col_efficacy_name), colnames(out)) out <- out[, column_order] return(out) } +gs_bound_summary_values <- function(bound, analysis, bound_name, columns) { + row_bound <- bound[ + bound$analysis == analysis & bound$bound == bound_name, + columns, + drop = FALSE + ] + if (nrow(row_bound) == 0) return(rep(NA_real_, length(columns))) + as.numeric(unlist(row_bound[1, columns], use.names = FALSE)) +} + gs_bound_summary_single <- function(x, col_efficacy_name = "Efficacy", digits, ddigits, tdigits, timename) { # Input @@ -72,6 +83,8 @@ gs_bound_summary_single <- function(x, col_efficacy_name = "Efficacy", digits, col_value <- character() col_efficacy <- numeric() col_futility <- numeric() + col_harm <- numeric() + bound_columns <- c("z", "nominal p", "~hr at bound", "probability0", "probability") for (i in seq_len(nrow(analysis))) { @@ -113,33 +126,31 @@ gs_bound_summary_single <- function(x, col_efficacy_name = "Efficacy", digits, ) # Efficacy column - row_efficacy <- bound[ - bound$analysis == i & bound$bound == "upper", - c("z", "nominal p", "~hr at bound", "probability0", "probability") - ] - col_efficacy <- c(col_efficacy, as.numeric(row_efficacy)) + col_efficacy <- c(col_efficacy, gs_bound_summary_values(bound, i, "upper", bound_columns)) # Futility column - row_futility <- bound[ - bound$analysis == i & bound$bound == "lower", - c("z", "nominal p", "~hr at bound", "probability0", "probability") - ] - col_futility <- c(col_futility, as.numeric(row_futility)) + col_futility <- c(col_futility, gs_bound_summary_values(bound, i, "lower", bound_columns)) + + # Harm column + col_harm <- c(col_harm, gs_bound_summary_values(bound, i, "harm", bound_columns)) } col_efficacy <- round(col_efficacy, digits) col_futility <- round(col_futility, digits) + col_harm <- round(col_harm, digits) out <- data.frame( Analysis = col_analysis, Value = col_value, Efficacy = col_efficacy, - Futility = col_futility + Futility = col_futility, + Harm = col_harm ) colnames(out)[3] <- col_efficacy_name # One-sided design should not include Futility column if (all(is.na(out[["Futility"]]))) out[["Futility"]] <- NULL + if (all(is.na(out[["Harm"]]))) out[["Harm"]] <- NULL return(out) } diff --git a/R/gs_design_ahr.R b/R/gs_design_ahr.R index ea1f7843e..65f044e5a 100644 --- a/R/gs_design_ahr.R +++ b/R/gs_design_ahr.R @@ -98,7 +98,7 @@ #' # Example 2 ---- #' # Single analysis #' gs_design_ahr(analysis_time = 40) -#' +#' #' # Example 3 ---- #' # Multiple analysis_time #' gs_design_ahr(analysis_time = c(12, 24, 36)) @@ -168,6 +168,22 @@ #' lpar = rep(-Inf, 3) #' ) #' } +#' +#' # Example 8 ---- +#' # Design with an additional harm bound +#' \donttest{ +#' gs_design_ahr( +#' analysis_time = c(12, 24, 36), +#' upper = gs_spending_bound, +#' upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025, param = NULL, timing = NULL), +#' lower = gs_spending_bound, +#' lpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -2, timing = NULL), +#' test_lower = c(TRUE, TRUE, FALSE), +#' harm = gs_spending_bound, +#' hpar = list(sf = gsDesign::sfHSD, total_spend = 0.2, param = -4, timing = NULL), +#' test_harm = c(TRUE, TRUE, FALSE) +#' ) +#' } gs_design_ahr <- function( enroll_rate = define_enroll_rate( duration = c(2, 2, 10), @@ -186,9 +202,12 @@ gs_design_ahr <- function( upar = list(sf = gsDesign::sfLDOF, total_spend = alpha), lower = gs_spending_bound, lpar = list(sf = gsDesign::sfLDOF, total_spend = beta), + harm = gs_b, + hpar = -Inf, h1_spending = TRUE, test_upper = TRUE, test_lower = TRUE, + test_harm = FALSE, info_scale = c("h0_h1_info", "h0_info", "h1_info"), r = 18, tol = 1e-6, @@ -200,6 +219,10 @@ gs_design_ahr <- function( info_scale <- match.arg(info_scale) upper <- match.fun(upper) lower <- match.fun(lower) + harm <- match.fun(harm) + + # Number of analyses (including final analysis) + n_analysis <- max(length(analysis_time), length(info_frac)) # Check inputs ---- check_analysis_time(analysis_time) @@ -235,9 +258,6 @@ gs_design_ahr <- function( final_event <- y$event[nrow(y)] if_alt <- y$event / final_event - # Number of analyses (including final analysis) - n_analysis <- max(length(analysis_time), length(info_frac)) - # Initialize the next_time as the study duration next_time <- max(analysis_time) @@ -308,6 +328,7 @@ gs_design_ahr <- function( alpha = alpha, beta = beta, binding = binding, upper = upper, upar = upar, test_upper = test_upper, lower = lower, lpar = lpar, test_lower = test_lower, + harm = harm, hpar = hpar, test_harm = test_harm, r = r, tol = tol ) ) @@ -359,7 +380,6 @@ gs_design_ahr <- function( spending_time_upper <- info0 / info0_final } - bound$spending_time[which(bound$bound == "upper")] <- spending_time_upper } @@ -375,6 +395,16 @@ gs_design_ahr <- function( bound$spending_time[which(bound$bound == "lower")] <- spending_time_lower } + if (identical(harm, gs_spending_bound)) { + if (!is.null(hpar$timing)) { + spending_time_harm <- hpar$timing + } else { + spending_time_harm <- info0 / info0_final + } + + bound$spending_time[which(bound$bound == "harm")] <- spending_time_harm + } + if (all(is.na(bound$spending_time))){ bound$spending_time <- NULL } @@ -393,7 +423,8 @@ gs_design_ahr <- function( info_scale = info_scale, upper = upper, upar = upar, lower = lower, lpar = lpar, - test_upper = test_upper, test_lower = test_lower, + harm = harm, hpar = hpar, + test_upper = test_upper, test_lower = test_lower, test_harm = test_harm, h1_spending = h1_spending, binding = binding, info_scale = info_scale, r = r, tol = tol ) diff --git a/R/gs_design_npe.R b/R/gs_design_npe.R index 3ee9bba6c..0055cf332 100644 --- a/R/gs_design_npe.R +++ b/R/gs_design_npe.R @@ -21,7 +21,7 @@ #' @param beta Type II error. #' @details -#' The bound specifications (`upper`, `lower`, `upar`, `lpar`) of \code{gs_design_npe()} +#' The bound specifications (`upper`, `lower`, `harm`, `upar`, `lpar`, `hpar`) of \code{gs_design_npe()} #' will be used to ensure Type I error and other boundary properties are as specified. #' See the help file of \code{gs_spending_bound()} for details on spending function. #' @@ -130,7 +130,7 @@ #' test_upper = c(FALSE, TRUE, TRUE) #' ) #' -#' # Example 4 ---- +#' # Example 4a ---- #' # gs_design_npe with spending function bounds #' # 2-sided asymmetric bounds #' # Lower spending based on non-zero effect @@ -144,6 +144,21 @@ #' lpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -1, timing = NULL) #' ) #' +#' # Example 4b ---- +#' # gs_design_npe with an additional harm bound under the null hypothesis +#' gs_design_npe( +#' theta = c(.1, .2, .3), +#' info = (1:3) * 40, +#' info0 = (1:3) * 30, +#' upper = gs_spending_bound, +#' upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025, param = NULL, timing = NULL), +#' lower = gs_spending_bound, +#' lpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -2, timing = NULL), +#' harm = gs_spending_bound, +#' hpar = list(sf = gsDesign::sfHSD, total_spend = 0.2, param = -4, timing = NULL), +#' test_harm = c(TRUE, FALSE, TRUE) +#' ) +#' #' # Example 5 ---- #' # gs_design_npe with two-sided symmetric spend, O'Brien-Fleming spending #' # Typically, 2-sided bounds are binding @@ -177,6 +192,7 @@ gs_design_npe <- function( upper = gs_b, upar = qnorm(.975), lower = gs_b, lpar = -Inf, test_upper = TRUE, test_lower = TRUE, binding = FALSE, + harm = gs_b, hpar = -Inf, test_harm = FALSE, r = 18, tol = 1e-6) { # Check & set up parameters ---- n_analysis <- length(info) @@ -191,9 +207,15 @@ gs_design_npe <- function( upper <- match.fun(upper) lower <- match.fun(lower) + harm <- match.fun(harm) # check test_upper & test_lower test_upper <- check_test_upper(test_upper, n_analysis) test_lower <- check_test_lower(test_lower, n_analysis) + test_harm <- check_test_ul(test_harm, n_analysis, "test_harm") + + if (identical(harm, gs_b) && length(hpar) == 1 && n_analysis > 1) { + hpar <- rep(hpar, n_analysis) + } # Set up info ---- if (is.null(info0)) { @@ -248,6 +270,41 @@ gs_design_npe <- function( min_x <- ((qnorm(alpha) / sqrt(info0[n_analysis]) + qnorm(beta) / sqrt(info[n_analysis])) / theta[n_analysis])^2 # for a fixed design, this is all you need. if (n_analysis == 1) { + if (any(test_harm)) { + ans_h1 <- gs_power_npe( + theta = theta, theta0 = theta0, theta1 = theta1, + info = info * min_x, info0 = info0 * min_x, info1 = info1 * min_x, + info_scale = info_scale, + upper = gs_b, upar = qnorm(1 - alpha), test_upper = test_upper, + lower = if (two_sided) lower else gs_b, + lpar = if (two_sided) lpar else rep(-Inf, n_analysis), + test_lower = test_lower, binding = binding, + harm = harm, hpar = hpar, test_harm = test_harm, + r = r, tol = tol + ) + ans_h0 <- gs_power_npe( + theta = 0, theta0 = theta0, theta1 = theta1, + info = info0 * min_x, info0 = info0 * min_x, info1 = info1 * min_x, + info_scale = info_scale, + upper = gs_b, upar = qnorm(1 - alpha), test_upper = test_upper, + lower = if (two_sided) lower else gs_b, + lpar = if (two_sided) lpar else rep(-Inf, n_analysis), + test_lower = test_lower, binding = binding, + harm = harm, hpar = hpar, test_harm = test_harm, + r = r, tol = tol + ) + suppressMessages( + ans <- ans_h1 |> + full_join( + ans_h0 |> + select(analysis, bound, probability) |> + rename(probability0 = probability) + ) + ) + ans <- ans |> select(analysis, bound, z, probability, probability0, theta, info_frac, info, info0, info1) + return(ans) + } + ans <- tibble( analysis = 1, bound = "upper", z = qnorm(1 - alpha), probability = 1 - beta, probability0 = alpha, theta = theta, @@ -265,7 +322,9 @@ gs_design_npe <- function( info = info * min_x, info0 = info0 * min_x, info1 = info * min_x, info_scale = info_scale, upper = upper, upar = upar, test_upper = test_upper, lower = lower, lpar = lpar, test_lower = test_lower, - binding = binding, r = r, tol = tol + binding = binding, + harm = harm, hpar = hpar, test_harm = test_harm, + r = r, tol = tol ) min_power <- (min_temp[min_temp$bound == "upper" & min_temp$analysis == n_analysis, ])$probability @@ -283,7 +342,9 @@ gs_design_npe <- function( info = info * max_x, info0 = info0 * max_x, info1 = info * max_x, info_scale = info_scale, upper = upper, upar = upar, test_upper = test_upper, lower = lower, lpar = lpar, test_lower = test_lower, - binding = binding, r = r, tol = tol + binding = binding, + harm = harm, hpar = hpar, test_harm = test_harm, + r = r, tol = tol ) max_power <- (max_temp[max_temp$bound == "upper" & max_temp$analysis == n_analysis, ])$probability @@ -308,7 +369,9 @@ gs_design_npe <- function( info = info * micro_x, info0 = info0 * micro_x, info1 = info * micro_x, info_scale = info_scale, upper = upper, upar = upar, test_upper = test_upper, lower = lower, lpar = lpar, test_lower = test_lower, - binding = binding, r = r, tol = tol + binding = binding, + harm = harm, hpar = hpar, test_harm = test_harm, + r = r, tol = tol ) micro_power <- (micro_temp[micro_temp$bound == "upper" & micro_temp$analysis == n_analysis, ])$probability @@ -333,8 +396,15 @@ gs_design_npe <- function( errbeta <- function(x) { # calculate the probability under H1 ans_h1 <<- cache_fun( - gs_power_npe, theta, theta0, theta1, info * x, info0 * x, info1 * x, - info_scale, upper, upar, lower, lpar, test_upper, test_lower, binding, r, tol + gs_power_npe, + theta = theta, theta0 = theta0, theta1 = theta1, + info = info * x, info0 = info0 * x, info1 = info1 * x, + info_scale = info_scale, + upper = upper, upar = upar, test_upper = test_upper, + lower = lower, lpar = lpar, test_lower = test_lower, + binding = binding, + harm = harm, hpar = hpar, test_harm = test_harm, + r = r, tol = tol ) power <- subset(ans_h1, bound == "upper" & analysis == n_analysis)$probability 1 - beta - power @@ -357,7 +427,9 @@ gs_design_npe <- function( lower = if (two_sided) lower else gs_b, lpar = if (two_sided) lpar else rep(-Inf, n_analysis), test_upper = test_upper, test_lower = test_lower, - binding = binding, r = r, tol = tol + binding = binding, + harm = harm, hpar = hpar, test_harm = test_harm, + r = r, tol = tol ) # combine probability under H0 and H1 via direct merge on analysis+bound diff --git a/R/gs_power_ahr.R b/R/gs_power_ahr.R index 16e371589..971a517e8 100644 --- a/R/gs_power_ahr.R +++ b/R/gs_power_ahr.R @@ -130,6 +130,29 @@ #' lpar = list(sf = gsDesign::sfLDOF, total_spend = 0.025) #' ) #' } +#' # Example 5 ---- +#' # 2-sided asymmetric O'Brien-Fleming spending bound with harm bound +#' # driven by both `event` and `analysis_time`, i.e., +#' # both `event` and `analysis_time` are not `NULL`, +#' # then the analysis will driven by the maximal one, i.e., +#' # Time = max(analysis_time, calculated Time for targeted event) +#' # Events = max(events, calculated events for targeted analysis_time) +#' \donttest{ +#' gs_power_ahr( +#' analysis_time = c(12, 24, 36), +#' event = c(30, 40, 50), h1_spending = FALSE, +#' binding = TRUE, +#' upper = gs_spending_bound, +#' upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025), +#' test_upper = c(TRUE, TRUE, TRUE), +#' lower = gs_spending_bound, +#' lpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -2), +#' test_lower = c(TRUE, TRUE, TRUE), +#' harm = gs_spending_bound, +#' hpar = list(sf = gsDesign::sfHSD, total_spend = 0.2, param = -4), +#' test_harm = c(TRUE, TRUE, TRUE) +#' ) +#' } gs_power_ahr <- function( enroll_rate = define_enroll_rate( duration = c(2, 2, 10), @@ -147,8 +170,11 @@ gs_power_ahr <- function( upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025), lower = gs_spending_bound, lpar = list(sf = gsDesign::sfLDOF, total_spend = NULL), + harm = gs_b, + hpar = -Inf, test_lower = TRUE, test_upper = TRUE, + test_harm = FALSE, ratio = 1, binding = FALSE, h1_spending = TRUE, @@ -165,6 +191,7 @@ gs_power_ahr <- function( upper <- match.fun(upper) lower <- match.fun(lower) + harm <- match.fun(harm) # Check if it is two-sided design or not if ((identical(lower, gs_b) && (!is.list(lpar))) || all(!test_lower)) { @@ -190,6 +217,16 @@ gs_power_ahr <- function( } } + if (identical(harm, gs_spending_bound)) { + if (is.null(hpar$total_spend) && any(test_harm)) { + stop("gs_power_ahr: please input the total_spend to the harm spending function.") + } + } + + if (n_analysis == 1 && test_harm) { + stop("gs_power_ahr() harm bound cannot be tested if there is only one analysis.") + } + # Calculate the asymptotic variance and statistical information ---- x <- gs_info_ahr( enroll_rate = enroll_rate, fail_rate = fail_rate, @@ -224,6 +261,7 @@ gs_power_ahr <- function( info = x$info, info0 = x$info0, info1 = info1, info_scale = info_scale, upper = upper, upar = upar, test_upper = test_upper, lower = lower, lpar = lpar, test_lower = test_lower, + harm = harm, hpar = hpar, test_harm = test_harm, binding = binding, r = r, tol = tol ) @@ -242,6 +280,7 @@ gs_power_ahr <- function( lpar }, test_lower = test_lower, + harm = harm, hpar = hpar, test_harm = test_harm, binding = binding, r = r, tol = tol ) @@ -286,7 +325,8 @@ gs_power_ahr <- function( alpha = if (identical(upper, gs_spending_bound)) {upar$total_spend} else {NULL}, upper = upper, upar = upar, lower = lower, lpar = lpar, - test_lower = test_lower, test_upper = test_upper, + harm = harm, hpar = hpar, + test_lower = test_lower, test_upper = test_upper, test_harm = test_harm, ratio = ratio, binding = binding, h1_spending = h1_spending, info_scale = info_scale, r = r, tol = tol ) diff --git a/R/gs_power_npe.R b/R/gs_power_npe.R index 7cafe7171..e05d4881f 100644 --- a/R/gs_power_npe.R +++ b/R/gs_power_npe.R @@ -73,6 +73,7 @@ #' - \code{gs_b()}: fixed efficacy bounds. #' @param lower Function to compute lower bound, which can be set up similarly as `upper`. #' See [this vignette](https://merck.github.io/gsDesign2/articles/story-seven-test-types.html). +#' @param harm Function to compute harm bound, which can be set up similarly as `lower`. #' @param upar Parameters passed to `upper`. #' - If `upper = gs_b`, then `upar` is a numerical vector specifying the fixed efficacy bounds per analysis. #' - If `upper = gs_spending_bound`, then `upar` is a list including @@ -81,6 +82,7 @@ #' - `param` for the parameter of the spending function. #' - `timing` specifies spending time if different from information-based spending; see details. #' @param lpar Parameters passed to `lower`, which can be set up similarly as `upar.` +#' @param hpar Parameters passed to `harm`, which can be set up similarly as `lpar.` #' @param test_upper Indicator of which analyses should include #' an upper (efficacy) bound; #' single value of `TRUE` (default) indicates all analyses; otherwise, @@ -91,6 +93,13 @@ #' single value of `FALSE` indicated no lower bound; otherwise, #' a logical vector of the same length as `info` should #' indicate which analyses will have a lower bound. +#' @param test_harm Indicator of which analyses should include a harm bound; +#' single value of `TRUE` (default) indicates all analyses; +#' single value of `FALSE` indicates no harm bound; otherwise, +#' a logical vector of the same length as `info` should +#' indicate which analyses will have a harm bound. +#' For fixed designs, the harm bound is typically not included. +#' For group sequential designs, the harm bound is always smaller than the lower bound (if any). #' @param r Integer value controlling grid for numerical integration as in #' Jennison and Turnbull (2000); default is 18, range is 1 to 80. #' Larger values provide larger number of grid points and greater accuracy. @@ -99,7 +108,7 @@ #' #' @return A tibble with columns of #' - `analysis`: analysis index. -#' - `bound`: either of value `"upper"` or `"lower"`, indicating the upper and lower bound. +#' - `bound`: one of value `"upper"`, `"lower"`, or `"harm"`, indicating the upper, lower, and harm bound. #' - `z`: the Z-score bounds. #' - `probability`: cumulative probability of crossing the bound at or before the analysis. #' - `theta`: same as the input. @@ -188,7 +197,7 @@ #' lpar = c(qnorm(.1), -Inf, -Inf) #' ) #' -#' # Example 9 ---- +#' # Example 9a ---- #' # gs_power_npe with spending function bounds #' # Lower spending based on non-zero effect #' gs_power_npe( @@ -210,6 +219,21 @@ #' lpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -1, timing = NULL) #' ) #' +#' # Example 9b ---- +#' # gs_power_npe with an additional harm bound under the null hypothesis +#' gs_power_npe( +#' theta = c(.1, .2, .3), +#' info = (1:3) * 40, +#' upper = gs_spending_bound, +#' upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025, param = NULL, timing = NULL), +#' lower = gs_spending_bound, +#' lpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -2, timing = NULL), +#' test_lower = c(TRUE, TRUE, TRUE), +#' harm = gs_spending_bound, +#' hpar = list(sf = gsDesign::sfHSD, total_spend = 0.2, param = -4, timing = NULL), +#' test_harm = c(TRUE, TRUE, TRUE) +#' ) +#' #' # Example 10 ---- #' # gs_power_npe with two-sided symmetric spend, O'Brien-Fleming spending #' # Typically, 2-sided bounds are binding @@ -262,16 +286,28 @@ gs_power_npe <- function(theta = .1, theta0 = 0, theta1 = theta, # 3 theta upper = gs_b, upar = qnorm(.975), lower = gs_b, lpar = -Inf, test_upper = TRUE, test_lower = TRUE, binding = FALSE, - r = 18, tol = 1e-6) { + harm = gs_b, hpar = -Inf, test_harm = FALSE, + r = 18, tol = 1e-6 + ) { # Check & set up parameters ---- n_analysis <- length(info) + if (n_analysis == 1 && test_harm) { + stop("gs_power_npe() harm bound cannot be tested if there is only one analysis.") + } + theta <- check_theta(theta, n_analysis) theta0 <- check_theta(theta0, n_analysis) theta1 <- check_theta(theta1, n_analysis) upper <- match.fun(upper) lower <- match.fun(lower) + harm <- match.fun(harm) test_upper <- check_test_upper(test_upper, n_analysis) test_lower <- check_test_lower(test_lower, n_analysis) + test_harm <- check_test_ul(test_harm, n_analysis, "test_harm") + + if (identical(harm, gs_b) && length(hpar) == 1 && n_analysis > 1) { + hpar <- rep(hpar, n_analysis) + } # Set up info ---- # impute info @@ -305,24 +341,39 @@ gs_power_npe <- function(theta = .1, theta0 = 0, theta1 = theta, # 3 theta # Initialization ---- a <- rep(-Inf, n_analysis) b <- rep(Inf, n_analysis) + harm_z <- rep(-Inf, n_analysis) hgm1_0 <- NULL hgm1_1 <- NULL hgm1 <- NULL + hgm1_harm0 <- NULL # harm-bound grid under theta0 + hgm1_harm <- NULL # harm-bound grid under the input theta upper_prob <- rep(NA, n_analysis) lower_prob <- rep(NA, n_analysis) + harm_prob <- rep(NA, n_analysis) # Calculate crossing prob under H1 ---- for (k in 1:n_analysis) { - # compute/update lower/upper bound + # compute/update lower/upper/harm bound a[k] <- lower( k = k, par = lpar, hgm1 = hgm1_1, info = info1, r = r, tol = tol, test_bound = test_lower, theta = theta1, efficacy = FALSE ) b[k] <- upper(k = k, par = upar, hgm1 = hgm1_0, info = info0, r = r, tol = tol, test_bound = test_upper) + harm_z[k] <- harm( + k = k, par = hpar, hgm1 = hgm1_harm0, info = info0, r = r, tol = tol, test_bound = test_harm, + theta = theta0, efficacy = FALSE + ) + if (!test_harm[k]) { + harm_z[k] <- -Inf + } + # Cap harm bound: if harm > futility, set to futility + if (!is.na(a[k]) && !is.na(harm_z[k]) && harm_z[k] > a[k]) { + harm_z[k] <- a[k] + } # if it is the first analysis if (k == 1) { - # compute the probability to cross upper/lower bound + # compute the probability to cross upper/lower/harm bound upper_prob[1] <- if (b[1] < Inf) { pnorm(sqrt(info[1]) * (theta[1] - b[1] / sqrt(info0[1]))) } else { @@ -333,6 +384,11 @@ gs_power_npe <- function(theta = .1, theta0 = 0, theta1 = theta, # 3 theta } else { 0 } + harm_prob[1] <- if (harm_z[1] > -Inf) { + pnorm(-sqrt(info[1]) * (theta[1] - harm_z[1] / sqrt(info0[1]))) + } else { + 0 + } # update the grids hgm1_0 <- h1(r = r, theta = theta0[1], info = info0[1], a = if (binding) { a[1] @@ -341,6 +397,8 @@ gs_power_npe <- function(theta = .1, theta0 = 0, theta1 = theta, # 3 theta }, b = b[1]) hgm1_1 <- h1(r = r, theta = theta1[1], info = info1[1], a = a[1], b = b[1]) hgm1 <- h1(r = r, theta = theta[1], info = info[1], a = a[1], b = b[1]) + hgm1_harm0 <- h1(r = r, theta = theta0[1], info = info0[1], a = harm_z[1], b = b[1]) + hgm1_harm <- h1(r = r, theta = theta[1], info = info[1], a = harm_z[1], b = b[1]) } else { # compute the probability to cross upper bound upper_prob[k] <- if (b[k] < Inf) { @@ -362,6 +420,16 @@ gs_power_npe <- function(theta = .1, theta0 = 0, theta1 = theta, # 3 theta } else { 0 } + # compute the probability to cross harm bound + harm_prob[k] <- if (harm_z[k] > -Inf) { + sum(hupdate( + theta = theta[k], thetam1 = theta[k - 1], + info = info[k], im1 = info[k - 1], + a = -Inf, b = harm_z[k], gm1 = hgm1_harm, r = r + )$h) + } else { + 0 + } # update the grids if (k < n_analysis) { @@ -380,22 +448,52 @@ gs_power_npe <- function(theta = .1, theta0 = 0, theta1 = theta, # 3 theta a = a[k], b = b[k], thetam1 = theta[k - 1], im1 = info[k - 1], gm1 = hgm1 ) + hgm1_harm0 <- hupdate( + r = r, theta = theta0[k], info = info0[k], + a = harm_z[k], b = b[k], thetam1 = theta0[k - 1], + im1 = info0[k - 1], gm1 = hgm1_harm0 + ) + hgm1_harm <- hupdate( + r = r, theta = theta[k], info = info[k], + a = harm_z[k], b = b[k], thetam1 = theta[k - 1], + im1 = info[k - 1], gm1 = hgm1_harm + ) } } } - ans <- data.frame( - analysis = rep(1:n_analysis, 2), - bound = c(rep("upper", n_analysis), rep("lower", n_analysis)), - z = c(b, a), - probability = c(cumsum(upper_prob), cumsum(lower_prob)), - theta = rep(theta, 2), - theta1 = rep(theta1, 2), - info_frac = rep(info / max(info), 2), - info = rep(info, 2), - info0 = rep(info0, 2), - info1 = rep(info1, 2) - ) + if (all(!test_harm)) { + ans <- data.frame( + analysis = rep(1:n_analysis, 2), + bound = c(rep("upper", n_analysis), rep("lower", n_analysis)), + z = c(b, a), + probability = c(cumsum(upper_prob), cumsum(lower_prob)), + theta = rep(theta, 2), + theta1 = rep(theta1, 2), + info_frac = rep(info / max(info), 2), + info = rep(info, 2), + info0 = rep(info0, 2), + info1 = rep(info1, 2) + ) + } else { + # harm bound is only provided when futility is tested + inactive_lower <- is.infinite(a) + harm_z[inactive_lower] <- -Inf + harm_prob[inactive_lower] <- 0 + + ans <- data.frame( + analysis = rep(1:n_analysis, 3), + bound = c(rep("upper", n_analysis), rep("lower", n_analysis), rep("harm", n_analysis)), + z = c(b, a, harm_z), + probability = c(cumsum(upper_prob), cumsum(lower_prob), cumsum(harm_prob)), + theta = rep(theta, 3), + theta1 = rep(theta1, 3), + info_frac = rep(info / max(info), 3), + info = rep(info, 3), + info0 = rep(info0, 3), + info1 = rep(info1, 3) + ) + } return(ans) } diff --git a/R/summary.R b/R/summary.R index 821944c9d..6c0717c51 100644 --- a/R/summary.R +++ b/R/summary.R @@ -103,7 +103,9 @@ summary.fixed_design <- function(object, ...) { #' If the vector is unnamed, it must match the length of `col_vars`. If the #' vector is named, you only have to specify the number of digits for the #' columns you want to be displayed differently than the defaults. -#' @param bound_names Names for bounds; default is `c("Efficacy", "Futility")`. +#' @param bound_names Names for bounds; default is `c("Efficacy", "Futility", "Harm")`. +#' The first two values label upper and lower bounds. If a third value is provided, +#' it labels harm bounds; otherwise harm bounds are labeled `"Harm"`. #' @param display_spending_time A logical value (TRUE/FALSE) indicating if the spending time #' is summarized in the table. #' @@ -257,7 +259,7 @@ summary.gs_design <- function(object, analysis_decimals = NULL, col_vars = NULL, col_decimals = NULL, - bound_names = c("Efficacy", "Futility"), + bound_names = c("Efficacy", "Futility", "Harm"), display_spending_time = FALSE, ...) { x <- object @@ -295,7 +297,8 @@ summary.gs_design <- function(object, xy <- x_bound # change Upper -> bound_names[1], e.g., Efficacy # change Lower -> bound_names[2], e.g., Futility - xy$bound <- replace_values(xy$bound, c(upper = bound_names[1], lower = bound_names[2])) + # change Harm -> bound_names[3], e.g., Harm + xy$bound <- replace_values(xy$bound, c(upper = bound_names[1], lower = bound_names[2], harm = bound_names[3])) if (!"probability0" %in% names(xy)) xy$probability0 <- "-" xy <- xy |> arrange(analysis, desc(bound)) diff --git a/_pkgdown.yml b/_pkgdown.yml index 732aad044..4e5bc10b5 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -167,6 +167,7 @@ articles: contents: - articles/story-nph-futility - articles/story-seven-test-types + - articles/story-harm-bound-schoenfeld - title: "Designs by AHR" desc: > @@ -179,6 +180,7 @@ articles: - articles/story-spending-time-example - articles/story-update-boundary - articles/story-cp + - articles/story-harm-bound-schoenfeld - title: "Designs with binary endpoints" desc: > diff --git a/man/as_gt.Rd b/man/as_gt.Rd index 4912d73de..4b0c7d893 100644 --- a/man/as_gt.Rd +++ b/man/as_gt.Rd @@ -17,7 +17,7 @@ as_gt(x, ...) colname_spanner = "Cumulative boundary crossing probability", colname_spannersub = c("Alternate hypothesis", "Null hypothesis"), footnote = NULL, - display_bound = c("Efficacy", "Futility"), + display_bound = c("Efficacy", "Futility", "Harm"), display_columns = NULL, display_inf_bound = FALSE, ... @@ -45,7 +45,7 @@ the table. To disable footnotes, use \code{footnote = FALSE}.} of the gt table.} \item{display_bound}{A vector of strings specifying the label of the bounds. -The default is \code{c("Efficacy", "Futility")}.} +The default is \code{c("Efficacy", "Futility", "Harm")}.} \item{display_columns}{A vector of strings specifying the variables to be displayed in the summary table.} @@ -114,6 +114,21 @@ gs_design_ahr() |> summary() |> as_gt() +gs_design_ahr( + analysis_time = c(12, 24, 36), + upper = gs_spending_bound, + upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025), + test_upper = c(FALSE, TRUE, TRUE), + lower = gs_spending_bound, + lpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -2), + test_lower = c(TRUE, TRUE, FALSE), + harm = gs_spending_bound, + hpar = list(sf = gsDesign::sfHSD, total_spend = 0.2, param = -4), + test_harm = c(TRUE, TRUE, FALSE) + ) |> + summary() |> + as_gt() + gs_power_ahr(lpar = list(sf = gsDesign::sfLDOF, total_spend = 0.1)) |> summary() |> as_gt() @@ -178,7 +193,7 @@ gs_power_wlr(lpar = list(sf = gsDesign::sfLDOF, total_spend = 0.1)) |> # Example 5 ---- # Usage of display_bound = ... -# to either show efficacy bound or futility bound, or both(default) +# to show selected bounds gs_power_wlr(lpar = list(sf = gsDesign::sfLDOF, total_spend = 0.1)) |> summary() |> as_gt(display_bound = "Efficacy") diff --git a/man/gsDesign2-package.Rd b/man/gsDesign2-package.Rd index c31fdc74c..e5a286eeb 100644 --- a/man/gsDesign2-package.Rd +++ b/man/gsDesign2-package.Rd @@ -24,6 +24,7 @@ Useful links: Authors: \itemize{ + \item Yujie Zhao \email{yujie.zhao@merck.com} \item Keaven Anderson \email{keaven_anderson@merck.com} \item Yilong Zhang \email{elong0527@gmail.com} \item John Blischak \email{jdblischak@gmail.com} diff --git a/man/gs_bound_summary.Rd b/man/gs_bound_summary.Rd index e4fe3fc8f..91ff48ad3 100644 --- a/man/gs_bound_summary.Rd +++ b/man/gs_bound_summary.Rd @@ -33,7 +33,7 @@ estimated timing of each analysis.} A data frame } \description{ -Summarizes the efficacy and futility bounds for each analysis. +Summarizes the efficacy, futility, and harm bounds for each analysis. } \examples{ library(gsDesign2) diff --git a/man/gs_design_ahr.Rd b/man/gs_design_ahr.Rd index de4fc18c4..52d6f7950 100644 --- a/man/gs_design_ahr.Rd +++ b/man/gs_design_ahr.Rd @@ -18,9 +18,12 @@ gs_design_ahr( upar = list(sf = gsDesign::sfLDOF, total_spend = alpha), lower = gs_spending_bound, lpar = list(sf = gsDesign::sfLDOF, total_spend = beta), + harm = gs_b, + hpar = -Inf, h1_spending = TRUE, test_upper = TRUE, test_lower = TRUE, + test_harm = FALSE, info_scale = c("h0_h1_info", "h0_info", "h1_info"), r = 18, tol = 1e-06, @@ -70,6 +73,10 @@ See \href{https://merck.github.io/gsDesign2/articles/story-seven-test-types.html \item{lpar}{Parameters passed to \code{lower}, which can be set up similarly as \code{upar.}} +\item{harm}{Function to compute harm bound, which can be set up similarly as \code{lower}.} + +\item{hpar}{Parameters passed to \code{harm}, which can be set up similarly as \code{lpar.}} + \item{h1_spending}{Indicator that lower bound to be set by spending under alternate hypothesis (input \code{fail_rate}) if spending is used for lower bound. @@ -89,6 +96,14 @@ single value of \code{FALSE} indicated no lower bound; otherwise, a logical vector of the same length as \code{info} should indicate which analyses will have a lower bound.} +\item{test_harm}{Indicator of which analyses should include a harm bound; +single value of \code{TRUE} (default) indicates all analyses; +single value of \code{FALSE} indicates no harm bound; otherwise, +a logical vector of the same length as \code{info} should +indicate which analyses will have a harm bound. +For fixed designs, the harm bound is typically not included. +For group sequential designs, the harm bound is always smaller than the lower bound (if any).} + \item{info_scale}{Information scale for calculation. Options are: \itemize{ \item \code{"h0_h1_info"} (default): variance under both null and alternative hypotheses is used. @@ -246,4 +261,20 @@ gs_design_ahr( lpar = rep(-Inf, 3) ) } + +# Example 8 ---- +# Design with an additional harm bound +\donttest{ +gs_design_ahr( + analysis_time = c(12, 24, 36), + upper = gs_spending_bound, + upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025, param = NULL, timing = NULL), + lower = gs_spending_bound, + lpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -2, timing = NULL), + test_lower = c(TRUE, TRUE, FALSE), + harm = gs_spending_bound, + hpar = list(sf = gsDesign::sfHSD, total_spend = 0.2, param = -4, timing = NULL), + test_harm = c(TRUE, TRUE, FALSE) +) +} } diff --git a/man/gs_power_ahr.Rd b/man/gs_power_ahr.Rd index eed8978a8..c9193476d 100644 --- a/man/gs_power_ahr.Rd +++ b/man/gs_power_ahr.Rd @@ -15,8 +15,11 @@ gs_power_ahr( upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025), lower = gs_spending_bound, lpar = list(sf = gsDesign::sfLDOF, total_spend = NULL), + harm = gs_b, + hpar = -Inf, test_lower = TRUE, test_upper = TRUE, + test_harm = FALSE, ratio = 1, binding = FALSE, h1_spending = TRUE, @@ -61,6 +64,10 @@ See \href{https://merck.github.io/gsDesign2/articles/story-seven-test-types.html \item{lpar}{Parameters passed to \code{lower}, which can be set up similarly as \code{upar.}} +\item{harm}{Function to compute harm bound, which can be set up similarly as \code{lower}.} + +\item{hpar}{Parameters passed to \code{harm}, which can be set up similarly as \code{lpar.}} + \item{test_lower}{Indicator of which analyses should include a lower bound; single value of \code{TRUE} (default) indicates all analyses; single value of \code{FALSE} indicated no lower bound; otherwise, @@ -73,6 +80,14 @@ single value of \code{TRUE} (default) indicates all analyses; otherwise, a logical vector of the same length as \code{info} should indicate which analyses will have an efficacy bound.} +\item{test_harm}{Indicator of which analyses should include a harm bound; +single value of \code{TRUE} (default) indicates all analyses; +single value of \code{FALSE} indicates no harm bound; otherwise, +a logical vector of the same length as \code{info} should +indicate which analyses will have a harm bound. +For fixed designs, the harm bound is typically not included. +For group sequential designs, the harm bound is always smaller than the lower bound (if any).} + \item{ratio}{Experimental:Control randomization ratio.} \item{binding}{Indicator of whether futility bound is binding; @@ -218,4 +233,27 @@ gs_power_ahr( lpar = list(sf = gsDesign::sfLDOF, total_spend = 0.025) ) } +# Example 5 ---- +# 2-sided asymmetric O'Brien-Fleming spending bound with harm bound +# driven by both `event` and `analysis_time`, i.e., +# both `event` and `analysis_time` are not `NULL`, +# then the analysis will driven by the maximal one, i.e., +# Time = max(analysis_time, calculated Time for targeted event) +# Events = max(events, calculated events for targeted analysis_time) +\donttest{ +gs_power_ahr( + analysis_time = c(12, 24, 36), + event = c(30, 40, 50), h1_spending = FALSE, + binding = TRUE, + upper = gs_spending_bound, + upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025), + test_upper = c(TRUE, TRUE, TRUE), + lower = gs_spending_bound, + lpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -2), + test_lower = c(TRUE, TRUE, TRUE), + harm = gs_spending_bound, + hpar = list(sf = gsDesign::sfHSD, total_spend = 0.2, param = -4), + test_harm = c(TRUE, TRUE, TRUE) +) +} } diff --git a/man/gs_power_design_npe.Rd b/man/gs_power_design_npe.Rd index f80bb25f3..b8137f122 100644 --- a/man/gs_power_design_npe.Rd +++ b/man/gs_power_design_npe.Rd @@ -23,6 +23,9 @@ gs_design_npe( test_upper = TRUE, test_lower = TRUE, binding = FALSE, + harm = gs_b, + hpar = -Inf, + test_harm = FALSE, r = 18, tol = 1e-06 ) @@ -42,6 +45,9 @@ gs_power_npe( test_upper = TRUE, test_lower = TRUE, binding = FALSE, + harm = gs_b, + hpar = -Inf, + test_harm = FALSE, r = 18, tol = 1e-06 ) @@ -126,6 +132,18 @@ indicate which analyses will have a lower bound.} \item{binding}{Indicator of whether futility bound is binding; default of \code{FALSE} is recommended.} +\item{harm}{Function to compute harm bound, which can be set up similarly as \code{lower}.} + +\item{hpar}{Parameters passed to \code{harm}, which can be set up similarly as \code{lpar.}} + +\item{test_harm}{Indicator of which analyses should include a harm bound; +single value of \code{TRUE} (default) indicates all analyses; +single value of \code{FALSE} indicates no harm bound; otherwise, +a logical vector of the same length as \code{info} should +indicate which analyses will have a harm bound. +For fixed designs, the harm bound is typically not included. +For group sequential designs, the harm bound is always smaller than the lower bound (if any).} + \item{r}{Integer value controlling grid for numerical integration as in Jennison and Turnbull (2000); default is 18, range is 1 to 80. Larger values provide larger number of grid points and greater accuracy. @@ -137,7 +155,7 @@ Normally, \code{r} will not be changed by the user.} A tibble with columns of \itemize{ \item \code{analysis}: analysis index. -\item \code{bound}: either of value \code{"upper"} or \code{"lower"}, indicating the upper and lower bound. +\item \code{bound}: one of value \code{"upper"}, \code{"lower"}, or \code{"harm"}, indicating the upper, lower, and harm bound. \item \code{z}: the Z-score bounds. \item \code{probability}: cumulative probability of crossing the bound at or before the analysis. \item \code{theta}: same as the input. @@ -175,7 +193,7 @@ The only differences in arguments between the two functions are the \code{alpha} parameters used in the \code{gs_design_npe()}. } \details{ -The bound specifications (\code{upper}, \code{lower}, \code{upar}, \code{lpar}) of \code{gs_design_npe()} +The bound specifications (\code{upper}, \code{lower}, \code{harm}, \code{upar}, \code{lpar}, \code{hpar}) of \code{gs_design_npe()} will be used to ensure Type I error and other boundary properties are as specified. See the help file of \code{gs_spending_bound()} for details on spending function. } @@ -316,7 +334,7 @@ gs_design_npe( test_upper = c(FALSE, TRUE, TRUE) ) -# Example 4 ---- +# Example 4a ---- # gs_design_npe with spending function bounds # 2-sided asymmetric bounds # Lower spending based on non-zero effect @@ -330,6 +348,21 @@ gs_design_npe( lpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -1, timing = NULL) ) +# Example 4b ---- +# gs_design_npe with an additional harm bound under the null hypothesis +gs_design_npe( + theta = c(.1, .2, .3), + info = (1:3) * 40, + info0 = (1:3) * 30, + upper = gs_spending_bound, + upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025, param = NULL, timing = NULL), + lower = gs_spending_bound, + lpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -2, timing = NULL), + harm = gs_spending_bound, + hpar = list(sf = gsDesign::sfHSD, total_spend = 0.2, param = -4, timing = NULL), + test_harm = c(TRUE, FALSE, TRUE) +) + # Example 5 ---- # gs_design_npe with two-sided symmetric spend, O'Brien-Fleming spending # Typically, 2-sided bounds are binding @@ -391,7 +424,7 @@ gs_power_npe( lpar = c(qnorm(.1), -Inf, -Inf) ) -# Example 9 ---- +# Example 9a ---- # gs_power_npe with spending function bounds # Lower spending based on non-zero effect gs_power_npe( @@ -413,6 +446,21 @@ gs_power_npe( lpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -1, timing = NULL) ) +# Example 9b ---- +# gs_power_npe with an additional harm bound under the null hypothesis +gs_power_npe( + theta = c(.1, .2, .3), + info = (1:3) * 40, + upper = gs_spending_bound, + upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025, param = NULL, timing = NULL), + lower = gs_spending_bound, + lpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -2, timing = NULL), + test_lower = c(TRUE, TRUE, TRUE), + harm = gs_spending_bound, + hpar = list(sf = gsDesign::sfHSD, total_spend = 0.2, param = -4, timing = NULL), + test_harm = c(TRUE, TRUE, TRUE) +) + # Example 10 ---- # gs_power_npe with two-sided symmetric spend, O'Brien-Fleming spending # Typically, 2-sided bounds are binding diff --git a/man/summary.Rd b/man/summary.Rd index 4cdb08d84..fda3cc608 100644 --- a/man/summary.Rd +++ b/man/summary.Rd @@ -13,7 +13,7 @@ analysis_decimals = NULL, col_vars = NULL, col_decimals = NULL, - bound_names = c("Efficacy", "Futility"), + bound_names = c("Efficacy", "Futility", "Harm"), display_spending_time = FALSE, ... ) @@ -37,7 +37,9 @@ If the vector is unnamed, it must match the length of \code{col_vars}. If the vector is named, you only have to specify the number of digits for the columns you want to be displayed differently than the defaults.} -\item{bound_names}{Names for bounds; default is \code{c("Efficacy", "Futility")}.} +\item{bound_names}{Names for bounds; default is \code{c("Efficacy", "Futility", "Harm")}. +The first two values label upper and lower bounds. If a third value is provided, +it labels harm bounds; otherwise harm bounds are labeled \code{"Harm"}.} \item{display_spending_time}{A logical value (TRUE/FALSE) indicating if the spending time is summarized in the table.} diff --git a/tests/testit/test-developer-gs_design_ahr.R b/tests/testit/test-developer-gs_design_ahr.R index fa4249b21..cd1c14b72 100644 --- a/tests/testit/test-developer-gs_design_ahr.R +++ b/tests/testit/test-developer-gs_design_ahr.R @@ -271,3 +271,53 @@ assert("Spending time when some analyses are skipped", { expected <- x$analysis$info[2:3] / max(x$analysis$info) (filter(x$bound, bound == "lower")$spending_time %==% expected) }) + +assert("Harm bound is not provided when it is a fixed design", { + x1 <- gs_power_npe(theta = 0.5, theta1 = 0.5, theta0 = 0, + info = 10, info1 = 10, info0 = 11, + upper = gs_b, upar = qnorm(1 - 0.025), test_upper = TRUE, + lower = gs_b, lpar = -Inf, test_lower = FALSE) + x2 <- gs_design_npe(theta = 0.5, theta1 = 0.5, theta0 = 0, + info = 10, info1 = 10, info0 = 11, + upper = gs_b, upar = qnorm(1 - 0.025), test_upper = TRUE, + lower = gs_b, lpar = -Inf, test_lower = FALSE) + x3 <- gs_design_ahr(analysis_time = 36, info_frac = NULL, + upper = gs_b, upar = qnorm(1 - 0.025), test_upper = TRUE, + lower = gs_b, lpar = -Inf, test_lower = FALSE) + x4 <- gs_power_ahr(analysis_time = 36, event = NULL, + upper = gs_b, upar = qnorm(1 - 0.025), test_upper = TRUE, + lower = gs_b, lpar = -Inf, test_lower = FALSE) + + (x1$bound[abs(x1$z) != Inf] == "upper") + (x2$bound[abs(x2$z) != Inf] == "upper") + (x3$bound$bound[abs(x3$bound$z) != Inf] == "upper") + (x4$bound$bound[abs(x4$bound$z) != Inf] == "upper") + (has_error(gs_design_ahr(analysis_time = 40, test_harm = TRUE))) + (has_error(gs_design_ahr(analysis_time = 40, hpar = -2, test_harm = TRUE))) +}) + +assert("Harm bound is always below the lower bound in a group sequential design", { + x1 <- gs_design_ahr(analysis_time = c(12, 24, 36), info_frac = NULL, + upper = gs_spending_bound, + upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025), + test_upper = TRUE, + lower = gs_spending_bound, + lpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -2), + test_lower = TRUE, + harm = gs_spending_bound, + hpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -4), + test_harm = TRUE) + x2 <- gs_power_ahr(analysis_time = NULL, event = c(10, 50, 70), + upper = gs_spending_bound, + upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025), + test_upper = TRUE, + lower = gs_spending_bound, + lpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -2), + test_lower = TRUE, + harm = gs_spending_bound, + hpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -4), + test_harm = TRUE) + + (all(x1$bound$z[x1$bound$bound == "lower"] - x1$bound$z[x1$bound$bound == "harm"] >= 0)) + (all(x2$bound$z[x2$bound$bound == "lower"] - x2$bound$z[x2$bound$bound == "harm"] >= 0)) +}) \ No newline at end of file diff --git a/tests/testit/test-developer-gs_design_npe.R b/tests/testit/test-developer-gs_design_npe.R index 619c0b84d..561959c15 100644 --- a/tests/testit/test-developer-gs_design_npe.R +++ b/tests/testit/test-developer-gs_design_npe.R @@ -268,3 +268,13 @@ assert("2-sided symmetric spend", { dplyr::arrange(analysis, bound) (as.data.frame(x1_c) %==% as.data.frame(x2)) }) + +assert("Harm bound is not provided for fixed designs", { + (has_error( + gs_design_npe( + theta = 0.1, info = 40, + upper = gs_b, upar = -qnorm(0.025), test_upper = TRUE, + lower = gs_b, lpar = -Inf, test_lower = FALSE, + harm = gs_b, hpar = -2, test_harm = TRUE) + )) +}) \ No newline at end of file diff --git a/tests/testit/test-developer-gs_power_npe.R b/tests/testit/test-developer-gs_power_npe.R index acb9f08b6..a7e85589e 100644 --- a/tests/testit/test-developer-gs_power_npe.R +++ b/tests/testit/test-developer-gs_power_npe.R @@ -320,3 +320,49 @@ assert("Expect equal with gsDesign::gsProbability outcome for efficacy bounds", (all.equal(x$z[x$bound == "upper"], z$upper$bound, tolerance = 2e-6)) (all.equal(x$probability[x$bound == "upper"], cumsum(z$upper$prob), tolerance = 6e-6)) }) + +assert("Harm bound is not provided for fixed designs", { + (has_error( + gs_power_npe( + theta = 0.1, info = 40, + upper = gs_b, upar = -qnorm(0.025), test_upper = TRUE, + lower = gs_b, lpar = -Inf, test_lower = FALSE, + harm = gs_b, hpar = -2, test_harm = TRUE) + )) +}) + +assert("Harm bound - Cap harm bound at futility bound", { + x <- gs_power_npe( + theta = c(.1, .2, .3), + info = (1:3) * 40, + upper = gs_spending_bound, + upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025, param = NULL, timing = NULL), + lower = gs_spending_bound, + lpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -2, timing = NULL), + test_lower = c(TRUE, TRUE, TRUE), + harm = gs_spending_bound, + hpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -4, timing = NULL), + test_harm = c(TRUE, TRUE, TRUE) + ) + harm_bound <- x |> dplyr::filter(bound == "harm") |> dplyr::pull(z) + futility_bound <- x |> dplyr::filter(bound == "lower") |> dplyr::pull(z) + (harm_bound <= futility_bound) + + x <- gs_power_npe( + theta = c(.1, .2, .3), + info = (1:3) * 40, + upper = gs_spending_bound, + upar = list(sf = gsDesign::sfLDOF, total_spend = 0.025, param = NULL, timing = NULL), + lower = gs_spending_bound, + lpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -2, timing = NULL), + test_lower = c(TRUE, TRUE, FALSE), + harm = gs_spending_bound, + hpar = list(sf = gsDesign::sfHSD, total_spend = 0.1, param = -4, timing = NULL), + test_harm = c(TRUE, TRUE, TRUE) + ) + harm_analysis <- x |> dplyr::filter(bound == "harm", !is.infinite(z)) |> dplyr::pull(analysis) + harm_bound <- x |> dplyr::filter(bound == "harm") |> dplyr::pull(z) + futility_bound <- x |> dplyr::filter(bound == "lower", analysis %in% harm_analysis) |> dplyr::pull(z) + (harm_bound <= futility_bound) + (harm_analysis %==% 1:2) +}) diff --git a/tests/testit/test-independent-as_gt.md b/tests/testit/test-independent-as_gt.md index c47fb1e89..1da54ef05 100644 --- a/tests/testit/test-independent-as_gt.md +++ b/tests/testit/test-independent-as_gt.md @@ -147,7 +147,7 @@ gt_to_latex(output) {\fontsize{14}{17}\selectfont AHR approximations of \textasciitilde{}HR at bound\fontsize{12}{15}\selectfont } } \fontsize{12.0pt}{14.0pt}\selectfont -\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}lrrrrr} +\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}crrrrr} \toprule & & & & \multicolumn{2}{c}{{Cumulative boundary crossing probability}} \\ \cmidrule(lr){5-6} @@ -182,7 +182,7 @@ gt_to_latex(output) {\fontsize{14}{17}\selectfont AHR approximations of \textasciitilde{}HR at bound\fontsize{12}{15}\selectfont } } \fontsize{12.0pt}{14.0pt}\selectfont -\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}lrrrrr} +\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}crrrrr} \toprule & & & & \multicolumn{2}{c}{{Cumulative boundary crossing probability}} \\ \cmidrule(lr){5-6} @@ -190,18 +190,18 @@ Bound & Z & Nominal p\textsuperscript{\textit{1}} & \textasciitilde{}HR at bound \midrule\addlinespace[2.5pt] \multicolumn{6}{l}{Analysis: 1 Time: 14.9 N: 108 Events: 30 AHR: 0.79 Information fraction: 0.6} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & -1.17 & 0.8792 & 1.5336 & 0.0349 & 0.1208 \\ Efficacy & 2.67 & 0.0038 & 0.3774 & 0.0231 & 0.0038 \\ +Futility & -1.17 & 0.8792 & 1.5336 & 0.0349 & 0.1208 \\ \midrule\addlinespace[2.5pt] \multicolumn{6}{l}{Analysis: 2 Time: 19.2 N: 108 Events: 40 AHR: 0.74 Information fraction: 0.8} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & -0.66 & 0.7462 & 1.2331 & 0.0668 & 0.2655 \\ Efficacy & 2.29 & 0.0110 & 0.4849 & 0.0897 & 0.0122 \\ +Futility & -0.66 & 0.7462 & 1.2331 & 0.0668 & 0.2655 \\ \midrule\addlinespace[2.5pt] \multicolumn{6}{l}{Analysis: 3 Time: 24.5 N: 108 Events: 50 AHR: 0.71 Information fraction: 1} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & -0.23 & 0.5897 & 1.0662 & 0.1008 & 0.4303 \\ Efficacy & 2.03 & 0.0211 & 0.5631 & 0.2070 & 0.0250 \\ +Futility & -0.23 & 0.5897 & 1.0662 & 0.1008 & 0.4303 \\ \bottomrule \end{tabular*} \begin{minipage}{\linewidth} @@ -228,7 +228,7 @@ gt_to_latex(output) {\fontsize{14}{17}\selectfont WLR approximation of \textasciitilde{}wHR at bound\fontsize{12}{15}\selectfont } } \fontsize{12.0pt}{14.0pt}\selectfont -\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}lrrrrr} +\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}crrrrr} \toprule & & & & \multicolumn{2}{c}{{Cumulative boundary crossing probability}} \\ \cmidrule(lr){5-6} @@ -275,7 +275,7 @@ gt_to_latex(output) {\fontsize{14}{17}\selectfont WLR approximation of \textasciitilde{}wHR at bound\fontsize{12}{15}\selectfont } } \fontsize{12.0pt}{14.0pt}\selectfont -\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}lrrrrr} +\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}crrrrr} \toprule & & & & \multicolumn{2}{c}{{Cumulative boundary crossing probability}}\textsuperscript{\textit{2}} \\ \cmidrule(lr){5-6} @@ -283,18 +283,18 @@ Bound & Z & Nominal p & \textasciitilde{}wHR at bound\textsuperscript{\textit{3} \midrule\addlinespace[2.5pt] \multicolumn{6}{l}{Analysis: 1 Time: 14.9 N: 108 Events: 30 AHR: 0.79 Information fraction: 0.6\textsuperscript{\textit{4}}} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & -1.17 & 0.8798 & 1.5353 & 0.0341 & 0.1202 \\ Efficacy & 2.68 & 0.0037 & 0.3765 & 0.0217 & 0.0037 \\ +Futility & -1.17 & 0.8798 & 1.5353 & 0.0341 & 0.1202 \\ \midrule\addlinespace[2.5pt] \multicolumn{6}{l}{Analysis: 2 Time: 19.2 N: 108 Events: 40 AHR: 0.75 Information fraction: 0.8\textsuperscript{\textit{4}}} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & -0.66 & 0.7452 & 1.2319 & 0.0664 & 0.2664 \\ Efficacy & 2.29 & 0.0110 & 0.4846 & 0.0886 & 0.0121 \\ +Futility & -0.66 & 0.7452 & 1.2319 & 0.0664 & 0.2664 \\ \midrule\addlinespace[2.5pt] \multicolumn{6}{l}{Analysis: 3 Time: 24.5 N: 108 Events: 50 AHR: 0.71 Information fraction: 1\textsuperscript{\textit{4}}} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & -0.22 & 0.5881 & 1.0650 & 0.1002 & 0.4319 \\ Efficacy & 2.03 & 0.0212 & 0.5631 & 0.2071 & 0.0250 \\ +Futility & -0.22 & 0.5881 & 1.0650 & 0.1002 & 0.4319 \\ \bottomrule \end{tabular*} \begin{minipage}{\linewidth} @@ -337,7 +337,7 @@ gt_to_latex(output) {\fontsize{14}{17}\selectfont MaxCombo approximation\fontsize{12}{15}\selectfont } } \fontsize{12.0pt}{14.0pt}\selectfont -\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}lrrrr} +\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}crrrr} \toprule & & & \multicolumn{2}{c}{{Cumulative boundary crossing probability}} \\ \cmidrule(lr){4-5} @@ -345,18 +345,18 @@ Bound & Z & Nominal p\textsuperscript{\textit{1}} & Alternate hypothesis & Null \midrule\addlinespace[2.5pt] \multicolumn{5}{l}{Analysis: 1 Time: 12 N: 500 Events: 107.4 AHR: 0.84 Event fraction: 0.32\textsuperscript{\textit{2}}} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & -1 & 0.8413 & 0.0293 & 0.0000 \\ Efficacy & 3 & 0.0013 & 0.0175 & 0.0013 \\ +Futility & -1 & 0.8413 & 0.0293 & 0.0000 \\ \midrule\addlinespace[2.5pt] \multicolumn{5}{l}{Analysis: 2 Time: 24 N: 500 Events: 246.3 AHR: 0.72 Event fraction: 0.74\textsuperscript{\textit{2}}} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & 0 & 0.5000 & 0.0314 & 0.0000 \\ Efficacy & 2 & 0.0228 & 0.7261 & 0.0233 \\ +Futility & 0 & 0.5000 & 0.0314 & 0.0000 \\ \midrule\addlinespace[2.5pt] \multicolumn{5}{l}{Analysis: 3 Time: 36 N: 500 Events: 331.3 AHR: 0.68 Event fraction: 1\textsuperscript{\textit{2}}} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & 1 & 0.1587 & 0.0326 & 0.0000 \\ Efficacy & 1 & 0.1587 & 0.9674 & 0.1956 \\ +Futility & 1 & 0.1587 & 0.0326 & 0.0000 \\ \bottomrule \end{tabular*} \begin{minipage}{\linewidth} @@ -383,7 +383,7 @@ gt_to_latex(output) {\fontsize{14}{17}\selectfont measured by risk difference\fontsize{12}{15}\selectfont } } \fontsize{12.0pt}{14.0pt}\selectfont -\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}lrrrrr} +\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}crrrrr} \toprule & & & & \multicolumn{2}{c}{{Cumulative boundary crossing probability}} \\ \cmidrule(lr){5-6} @@ -417,7 +417,7 @@ gt_to_latex(output) {\fontsize{14}{17}\selectfont measured by risk difference\fontsize{12}{15}\selectfont } } \fontsize{12.0pt}{14.0pt}\selectfont -\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}lrrrrr} +\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}crrrrr} \toprule & & & & \multicolumn{2}{c}{{Cumulative boundary crossing probability}} \\ \cmidrule(lr){5-6} @@ -425,8 +425,8 @@ Bound & Z & Nominal p\textsuperscript{\textit{1}} & \textasciitilde{}Risk differ \midrule\addlinespace[2.5pt] \multicolumn{6}{l}{Analysis: 1 N: 40 Risk difference: 0.05 Information fraction: 0.67} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & -1.28 & 0.9000 & -0.1537 & 0.0444 & 0.1000 \\ Efficacy & 3.71 & 0.0001 & 0.4448 & 0.0005 & 0.0001 \\ +Futility & -1.28 & 0.9000 & -0.1537 & 0.0444 & 0.1000 \\ \midrule\addlinespace[2.5pt] \multicolumn{6}{l}{Analysis: 2 N: 50 Risk difference: 0.05 Information fraction: 0.83} \\[2.5pt] \midrule\addlinespace[2.5pt] @@ -461,7 +461,7 @@ gt_to_latex(output) {\fontsize{14}{17}\selectfont from gs\_power\_wlr\fontsize{12}{15}\selectfont } } \fontsize{12.0pt}{14.0pt}\selectfont -\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}lrrrrr} +\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}crrrrr} \toprule & & & & \multicolumn{2}{c}{{Cumulative boundary crossing probability}} \\ \cmidrule(lr){5-6} @@ -469,18 +469,18 @@ Bound & Z & Nominal p\textsuperscript{\textit{1}} & \textasciitilde{}wHR at boun \midrule\addlinespace[2.5pt] \multicolumn{6}{l}{Analysis: 1 Time: 14.9 N: 108 Events: 30 AHR: 0.79 Information fraction: 0.6\textsuperscript{\textit{3}}} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & -1.17 & 0.8798 & 1.5353 & 0.0341 & 0.1202 \\ Efficacy & 2.68 & 0.0037 & 0.3765 & 0.0217 & 0.0037 \\ +Futility & -1.17 & 0.8798 & 1.5353 & 0.0341 & 0.1202 \\ \midrule\addlinespace[2.5pt] \multicolumn{6}{l}{Analysis: 2 Time: 19.2 N: 108 Events: 40 AHR: 0.75 Information fraction: 0.8\textsuperscript{\textit{3}}} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & -0.66 & 0.7452 & 1.2319 & 0.0664 & 0.2664 \\ Efficacy & 2.29 & 0.0110 & 0.4846 & 0.0886 & 0.0121 \\ +Futility & -0.66 & 0.7452 & 1.2319 & 0.0664 & 0.2664 \\ \midrule\addlinespace[2.5pt] \multicolumn{6}{l}{Analysis: 3 Time: 24.5 N: 108 Events: 50 AHR: 0.71 Information fraction: 1\textsuperscript{\textit{3}}} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & -0.22 & 0.5881 & 1.0650 & 0.1002 & 0.4319 \\ Efficacy & 2.03 & 0.0212 & 0.5631 & 0.2071 & 0.0250 \\ +Futility & -0.22 & 0.5881 & 1.0650 & 0.1002 & 0.4319 \\ \bottomrule \end{tabular*} \begin{minipage}{\linewidth} @@ -511,7 +511,7 @@ gt_to_latex(output) {\fontsize{14}{17}\selectfont WLR approximation of \textasciitilde{}wHR at bound\fontsize{12}{15}\selectfont } } \fontsize{12.0pt}{14.0pt}\selectfont -\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}lrrrrr} +\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}crrrrr} \toprule & & & & \multicolumn{2}{c}{{Cumulative probability to cross boundaries}} \\ \cmidrule(lr){5-6} @@ -519,18 +519,18 @@ Bound & Z & Nominal p\textsuperscript{\textit{1}} & \textasciitilde{}wHR at boun \midrule\addlinespace[2.5pt] \multicolumn{6}{l}{Analysis: 1 Time: 14.9 N: 108 Events: 30 AHR: 0.79 Information fraction: 0.6\textsuperscript{\textit{3}}} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & -1.17 & 0.8798 & 1.5353 & 0.0341 & 0.1202 \\ Efficacy & 2.68 & 0.0037 & 0.3765 & 0.0217 & 0.0037 \\ +Futility & -1.17 & 0.8798 & 1.5353 & 0.0341 & 0.1202 \\ \midrule\addlinespace[2.5pt] \multicolumn{6}{l}{Analysis: 2 Time: 19.2 N: 108 Events: 40 AHR: 0.75 Information fraction: 0.8\textsuperscript{\textit{3}}} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & -0.66 & 0.7452 & 1.2319 & 0.0664 & 0.2664 \\ Efficacy & 2.29 & 0.0110 & 0.4846 & 0.0886 & 0.0121 \\ +Futility & -0.66 & 0.7452 & 1.2319 & 0.0664 & 0.2664 \\ \midrule\addlinespace[2.5pt] \multicolumn{6}{l}{Analysis: 3 Time: 24.5 N: 108 Events: 50 AHR: 0.71 Information fraction: 1\textsuperscript{\textit{3}}} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & -0.22 & 0.5881 & 1.0650 & 0.1002 & 0.4319 \\ Efficacy & 2.03 & 0.0212 & 0.5631 & 0.2071 & 0.0250 \\ +Futility & -0.22 & 0.5881 & 1.0650 & 0.1002 & 0.4319 \\ \bottomrule \end{tabular*} \begin{minipage}{\linewidth} @@ -569,7 +569,7 @@ gt_to_latex(output) {\fontsize{14}{17}\selectfont WLR approximation of \textasciitilde{}wHR at bound\fontsize{12}{15}\selectfont } } \fontsize{12.0pt}{14.0pt}\selectfont -\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}lrrrrr} +\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}crrrrr} \toprule & & & & \multicolumn{2}{c}{{Cumulative boundary crossing probability}}\textsuperscript{\textit{2}} \\ \cmidrule(lr){5-6} @@ -577,18 +577,18 @@ Bound & Z & Nominal p & \textasciitilde{}wHR at bound\textsuperscript{\textit{3} \midrule\addlinespace[2.5pt] \multicolumn{6}{l}{Analysis: 1 Time: 14.9 N: 108 Events: 30 AHR: 0.79 Information fraction: 0.6\textsuperscript{\textit{4}}} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & -1.17 & 0.8798 & 1.5353 & 0.0341 & 0.1202 \\ Efficacy & 2.68 & 0.0037 & 0.3765 & 0.0217 & 0.0037 \\ +Futility & -1.17 & 0.8798 & 1.5353 & 0.0341 & 0.1202 \\ \midrule\addlinespace[2.5pt] \multicolumn{6}{l}{Analysis: 2 Time: 19.2 N: 108 Events: 40 AHR: 0.75 Information fraction: 0.8\textsuperscript{\textit{4}}} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & -0.66 & 0.7452 & 1.2319 & 0.0664 & 0.2664 \\ Efficacy & 2.29 & 0.0110 & 0.4846 & 0.0886 & 0.0121 \\ +Futility & -0.66 & 0.7452 & 1.2319 & 0.0664 & 0.2664 \\ \midrule\addlinespace[2.5pt] \multicolumn{6}{l}{Analysis: 3 Time: 24.5 N: 108 Events: 50 AHR: 0.71 Information fraction: 1\textsuperscript{\textit{4}}} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & -0.22 & 0.5881 & 1.0650 & 0.1002 & 0.4319 \\ Efficacy & 2.03 & 0.0212 & 0.5631 & 0.2071 & 0.0250 \\ +Futility & -0.22 & 0.5881 & 1.0650 & 0.1002 & 0.4319 \\ \bottomrule \end{tabular*} \begin{minipage}{\linewidth} @@ -617,7 +617,7 @@ gt_to_latex(output) {\fontsize{14}{17}\selectfont WLR approximation of \textasciitilde{}wHR at bound\fontsize{12}{15}\selectfont } } \fontsize{12.0pt}{14.0pt}\selectfont -\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}lrrrrr} +\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}crrrrr} \toprule & & & & \multicolumn{2}{c}{{Cumulative boundary crossing probability}} \\ \cmidrule(lr){5-6} @@ -661,7 +661,7 @@ gt_to_latex(output) {\fontsize{14}{17}\selectfont WLR approximation of \textasciitilde{}wHR at bound\fontsize{12}{15}\selectfont } } \fontsize{12.0pt}{14.0pt}\selectfont -\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}lrrrr} +\begin{tabular*}{\linewidth}{@{\extracolsep{\fill}}crrrr} \toprule & & & \multicolumn{2}{c}{{Cumulative boundary crossing probability}} \\ \cmidrule(lr){4-5} @@ -669,18 +669,18 @@ Bound & Nominal p\textsuperscript{\textit{1}} & Z & Alternate hypothesis & Null \midrule\addlinespace[2.5pt] \multicolumn{5}{l}{Analysis: 1 Time: 14.9 N: 108 Events: 30 AHR: 0.79 Information fraction: 0.6\textsuperscript{\textit{2}}} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & 0.8798 & -1.17 & 0.0341 & 0.1202 \\ Efficacy & 0.0037 & 2.68 & 0.0217 & 0.0037 \\ +Futility & 0.8798 & -1.17 & 0.0341 & 0.1202 \\ \midrule\addlinespace[2.5pt] \multicolumn{5}{l}{Analysis: 2 Time: 19.2 N: 108 Events: 40 AHR: 0.75 Information fraction: 0.8\textsuperscript{\textit{2}}} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & 0.7452 & -0.66 & 0.0664 & 0.2664 \\ Efficacy & 0.0110 & 2.29 & 0.0886 & 0.0121 \\ +Futility & 0.7452 & -0.66 & 0.0664 & 0.2664 \\ \midrule\addlinespace[2.5pt] \multicolumn{5}{l}{Analysis: 3 Time: 24.5 N: 108 Events: 50 AHR: 0.71 Information fraction: 1\textsuperscript{\textit{2}}} \\[2.5pt] \midrule\addlinespace[2.5pt] -Futility & 0.5881 & -0.22 & 0.1002 & 0.4319 \\ Efficacy & 0.0212 & 2.03 & 0.2071 & 0.0250 \\ +Futility & 0.5881 & -0.22 & 0.1002 & 0.4319 \\ \bottomrule \end{tabular*} \begin{minipage}{\linewidth} diff --git a/tests/testit/test-independent-as_rtf.md b/tests/testit/test-independent-as_rtf.md index e08267cd6..c14df378e 100644 --- a/tests/testit/test-independent-as_rtf.md +++ b/tests/testit/test-independent-as_rtf.md @@ -462,20 +462,6 @@ cat(readLines(path), sep = "\n") \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx6000 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -1.17}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.8798}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.535}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0341}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1202}\cell -\intbl\row\pard -\trowd\trgaph108\trleft0\trqc -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3000 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx4500 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx6000 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Efficacy}\cell \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 2.68}\cell \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0037}\cell @@ -484,10 +470,6 @@ cat(readLines(path), sep = "\n") \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0037}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 2 Time: 19.2 N: 108 Events: 40 AHR: 0.75 Information fraction: 0.8 {\super c}}\cell -\intbl\row\pard -\trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3000 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx4500 @@ -495,11 +477,15 @@ cat(readLines(path), sep = "\n") \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.66}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.7452}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.232}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0664}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.2664}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -1.17}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.8798}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.535}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0341}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1202}\cell +\intbl\row\pard +\trowd\trgaph108\trleft0\trqc +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 2 Time: 19.2 N: 108 Events: 40 AHR: 0.75 Information fraction: 0.8 {\super c}}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 @@ -516,10 +502,6 @@ cat(readLines(path), sep = "\n") \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0121}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 3 Time: 24.5 N: 108 Events: 50 AHR: 0.71 Information fraction: 1 {\super c}}\cell -\intbl\row\pard -\trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3000 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx4500 @@ -527,11 +509,15 @@ cat(readLines(path), sep = "\n") \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.22}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.5881}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.065}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1002}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.4319}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.66}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.7452}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.232}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0664}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.2664}\cell +\intbl\row\pard +\trowd\trgaph108\trleft0\trqc +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 3 Time: 24.5 N: 108 Events: 50 AHR: 0.71 Information fraction: 1 {\super c}}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 @@ -548,6 +534,20 @@ cat(readLines(path), sep = "\n") \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.025}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3000 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx4500 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx6000 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.22}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.5881}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.065}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1002}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.4319}\cell +\intbl\row\pard +\trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrdb\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 {\super a} One-sided p-value for experimental vs control treatment. Value < 0.5 favors experimental, > 0.5 favors control.\line{\super b} Approximate hazard ratio to cross bound.\line{\super c} wAHR is the weighted AHR.}\cell \intbl\row\pard @@ -704,20 +704,6 @@ cat(readLines(path), sep = "\n") \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx6000 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -1.17}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.8798}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.535}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0341}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1202}\cell -\intbl\row\pard -\trowd\trgaph108\trleft0\trqc -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3000 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx4500 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx6000 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Efficacy}\cell \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 2.68}\cell \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0037}\cell @@ -726,10 +712,6 @@ cat(readLines(path), sep = "\n") \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0037}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 2 Time: 19.2 N: 108 Events: 40 AHR: 0.75 Information fraction: 0.8 {\super c}}\cell -\intbl\row\pard -\trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3000 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx4500 @@ -737,11 +719,15 @@ cat(readLines(path), sep = "\n") \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.66}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.7452}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.232}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0664}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.2664}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -1.17}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.8798}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.535}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0341}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1202}\cell +\intbl\row\pard +\trowd\trgaph108\trleft0\trqc +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 2 Time: 19.2 N: 108 Events: 40 AHR: 0.75 Information fraction: 0.8 {\super c}}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 @@ -758,10 +744,6 @@ cat(readLines(path), sep = "\n") \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0121}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 3 Time: 24.5 N: 108 Events: 50 AHR: 0.71 Information fraction: 1 {\super c}}\cell -\intbl\row\pard -\trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3000 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx4500 @@ -769,11 +751,15 @@ cat(readLines(path), sep = "\n") \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.22}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.5881}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.065}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1002}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.4319}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.66}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.7452}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.232}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0664}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.2664}\cell +\intbl\row\pard +\trowd\trgaph108\trleft0\trqc +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 3 Time: 24.5 N: 108 Events: 50 AHR: 0.71 Information fraction: 1 {\super c}}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 @@ -790,6 +776,20 @@ cat(readLines(path), sep = "\n") \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.025}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3000 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx4500 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx6000 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.22}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.5881}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.065}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1002}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.4319}\cell +\intbl\row\pard +\trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrdb\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 {\super a} One-sided p-value for experimental vs control treatment. Value < 0.5 favors experimental, > 0.5 favors control.\line{\super b} Approximate hazard ratio to cross bound.\line{\super c} wAHR is the weighted AHR.}\cell \intbl\row\pard @@ -866,20 +866,6 @@ cat(readLines(path), sep = "\n") \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx6000 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -1.17}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.8798}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.535}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0341}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1202}\cell -\intbl\row\pard -\trowd\trgaph108\trleft0\trqc -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3000 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx4500 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx6000 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Efficacy}\cell \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 2.68}\cell \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0037}\cell @@ -888,10 +874,6 @@ cat(readLines(path), sep = "\n") \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0037}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 2 Time: 19.2 N: 108 Events: 40 AHR: 0.75 Information fraction: 0.8 {\super c}}\cell -\intbl\row\pard -\trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3000 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx4500 @@ -899,11 +881,15 @@ cat(readLines(path), sep = "\n") \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.66}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.7452}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.232}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0664}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.2664}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -1.17}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.8798}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.535}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0341}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1202}\cell +\intbl\row\pard +\trowd\trgaph108\trleft0\trqc +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 2 Time: 19.2 N: 108 Events: 40 AHR: 0.75 Information fraction: 0.8 {\super c}}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 @@ -920,10 +906,6 @@ cat(readLines(path), sep = "\n") \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0121}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 3 Time: 24.5 N: 108 Events: 50 AHR: 0.71 Information fraction: 1 {\super c}}\cell -\intbl\row\pard -\trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3000 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx4500 @@ -931,11 +913,15 @@ cat(readLines(path), sep = "\n") \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.22}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.5881}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.065}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1002}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.4319}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.66}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.7452}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.232}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0664}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.2664}\cell +\intbl\row\pard +\trowd\trgaph108\trleft0\trqc +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 3 Time: 24.5 N: 108 Events: 50 AHR: 0.71 Information fraction: 1 {\super c}}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 @@ -952,6 +938,20 @@ cat(readLines(path), sep = "\n") \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.025}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3000 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx4500 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx6000 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.22}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.5881}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.065}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1002}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.4319}\cell +\intbl\row\pard +\trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrdb\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 {\super a} One-sided p-value for experimental vs control treatment. Value < 0.5 favors experimental, > 0.5 favors control.\line{\super b} Approximate hazard ratio to cross bound.\line{\super c} wAHR is the weighted AHR.}\cell \intbl\row\pard @@ -1036,20 +1036,6 @@ cat(readLines(path), sep = "\n") \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx6000 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -1.17}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.8798}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.535}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0341}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1202}\cell -\intbl\row\pard -\trowd\trgaph108\trleft0\trqc -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3000 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx4500 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx6000 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Efficacy}\cell \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 2.68}\cell \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0037}\cell @@ -1058,10 +1044,6 @@ cat(readLines(path), sep = "\n") \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0037}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 2 Time: 19.2 N: 108 Events: 40 AHR: 0.75 Information fraction: 0.8 {\super b}}\cell -\intbl\row\pard -\trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3000 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx4500 @@ -1069,11 +1051,15 @@ cat(readLines(path), sep = "\n") \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.66}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.7452}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.232}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0664}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.2664}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -1.17}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.8798}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.535}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0341}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1202}\cell +\intbl\row\pard +\trowd\trgaph108\trleft0\trqc +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 2 Time: 19.2 N: 108 Events: 40 AHR: 0.75 Information fraction: 0.8 {\super b}}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 @@ -1090,10 +1076,6 @@ cat(readLines(path), sep = "\n") \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0121}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 3 Time: 24.5 N: 108 Events: 50 AHR: 0.71 Information fraction: 1 {\super b}}\cell -\intbl\row\pard -\trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3000 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx4500 @@ -1101,11 +1083,15 @@ cat(readLines(path), sep = "\n") \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.22}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.5881}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.065}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1002}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.4319}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.66}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.7452}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.232}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0664}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.2664}\cell +\intbl\row\pard +\trowd\trgaph108\trleft0\trqc +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 3 Time: 24.5 N: 108 Events: 50 AHR: 0.71 Information fraction: 1 {\super b}}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 @@ -1122,6 +1108,20 @@ cat(readLines(path), sep = "\n") \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.025}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1500 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3000 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx4500 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx6000 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7500 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.22}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.5881}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 1.065}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1002}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.4319}\cell +\intbl\row\pard +\trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrdb\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 {\super a} approximate weighted hazard ratio to cross bound.\line{\super b} wAHR is the weighted AHR.\line{\super c} the crossing probability.\line{\super d} this table is generated by gs_power_wlr.}\cell \intbl\row\pard @@ -1194,18 +1194,6 @@ cat(readLines(path), sep = "\n") \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx5400 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7200 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.8798}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -1.17}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0341}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1202}\cell -\intbl\row\pard -\trowd\trgaph108\trleft0\trqc -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1800 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3600 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx5400 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7200 -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Efficacy}\cell \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0037}\cell \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 2.68}\cell @@ -1213,20 +1201,20 @@ cat(readLines(path), sep = "\n") \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0037}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 2 Time: 19.2 N: 108 Events: 40 AHR: 0.75 Information fraction: 0.8 {\super b}}\cell -\intbl\row\pard -\trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1800 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3600 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx5400 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7200 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.7452}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.66}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0664}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.2664}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.8798}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -1.17}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0341}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1202}\cell +\intbl\row\pard +\trowd\trgaph108\trleft0\trqc +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 2 Time: 19.2 N: 108 Events: 40 AHR: 0.75 Information fraction: 0.8 {\super b}}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1800 @@ -1241,20 +1229,20 @@ cat(readLines(path), sep = "\n") \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0121}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc -\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 3 Time: 24.5 N: 108 Events: 50 AHR: 0.71 Information fraction: 1 {\super b}}\cell -\intbl\row\pard -\trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1800 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3600 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx5400 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7200 \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.5881}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.22}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1002}\cell -\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.4319}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.7452}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.66}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.0664}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.2664}\cell +\intbl\row\pard +\trowd\trgaph108\trleft0\trqc +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Analysis: 3 Time: 24.5 N: 108 Events: 50 AHR: 0.71 Information fraction: 1 {\super b}}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1800 @@ -1269,6 +1257,18 @@ cat(readLines(path), sep = "\n") \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.025}\cell \intbl\row\pard \trowd\trgaph108\trleft0\trqc +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx1800 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx3600 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx5400 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx7200 +\clbrdrl\brdrs\brdrw15\clbrdrt\brdrs\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrs\brdrw15\clvertalt\cellx9000 +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 Futility}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.5881}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 -0.22}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.1002}\cell +\pard\hyphpar0\sb15\sa15\fi0\li0\ri0\qc\fs18{\f0 0.4319}\cell +\intbl\row\pard +\trowd\trgaph108\trleft0\trqc \clbrdrl\brdrs\brdrw15\clbrdrt\brdrw15\clbrdrr\brdrs\brdrw15\clbrdrb\brdrdb\brdrw15\clvertalt\cellx9000 \pard\hyphpar0\sb15\sa15\fi0\li0\ri0\ql\fs18{\f0 {\super a} One-sided p-value for experimental vs control treatment. Value < 0.5 favors experimental, > 0.5 favors control.\line{\super b} wAHR is the weighted AHR.}\cell \intbl\row\pard diff --git a/vignettes/articles/story-harm-bound-schoenfeld.Rmd b/vignettes/articles/story-harm-bound-schoenfeld.Rmd new file mode 100644 index 000000000..9eff5f2d2 --- /dev/null +++ b/vignettes/articles/story-harm-bound-schoenfeld.Rmd @@ -0,0 +1,546 @@ +--- +title: "Reproducing gsSurv Schoenfeld bounds with harm bounds" +author: "Keaven Anderson" +output: + rmarkdown::html_document: + toc: true + toc_float: true + toc_depth: 2 + number_sections: true + highlight: "textmate" + css: "custom.css" +vignette: > + %\VignetteIndexEntry{Reproducing gsSurv Schoenfeld bounds with harm bounds} + %\VignetteEngine{knitr::rmarkdown} + %\VignetteEncoding{UTF-8} +--- + +```{r, include=FALSE} +knitr::opts_chunk$set( + collapse = TRUE, + comment = "#>" +) +``` + +```{r, message=FALSE, warning=FALSE} +library(gsDesign2) +library(dplyr) +library(gt) +``` + +# Overview + +This vignette shows that `gs_design_ahr()` can reproduce the Z-boundaries from +`gsDesign::gsSurv(method = "Schoenfeld")` for a proportional hazards design +with a single hazard ratio. +The Schoenfeld approximation uses the null hypothesis variance, so the +corresponding `gs_design_ahr()` calls use `info_scale = "h0_info"`. + +The comparison covers `gsSurv()` `test.type` values 1 through 8. +Types 7 and 8 include the additional harm bound. +We use the same spending functions, spending times, and numerical integration +settings in both packages. + +# Design assumptions + +We use a 3-analysis design with information fractions 35%, 70%, and 100%. +Enrollment is uniform for 16 months, the total trial duration is 36 months, and +the control arm has exponential failure with a 12-month median. +The experimental-to-control hazard ratio is constant at 0.7. + +```{r} +trial_duration <- 36 +info_frac <- c(.35, .7, 1) + +enroll_rate <- define_enroll_rate(duration = 16, rate = 1) +minfup <- trial_duration - sum(enroll_rate$duration) + +fail_rate <- define_fail_rate( + duration = Inf, + fail_rate = log(2) / 12, + hr = .7, + dropout_rate = -log(.99) / 12 +) + +alpha <- 0.025 +beta <- 0.15 +astar_candidates <- c(.10, .15, .20) +astar <- .20 +ratio <- 1 +r <- 32 +tol <- 1e-8 +``` + +The `astar` argument is used for null-spending lower bounds in test types 5 and +6 and for harm bounds in test types 7 and 8. +For harm bounds, `astar` controls the cumulative null probability of crossing a +lower-tail boundary, representing evidence that experimental treatment is worse +than control. +We compare `astar` values of 0.10, 0.15, and 0.20 and select `astar = 0.20`. +This choice is intentionally somewhat more liberal than `astar = 0.15` because +the harm bound is meant to warn of a potentially important trend in the wrong +direction, not to require definitive evidence of harm before raising concern. +It still keeps all harm Z-bounds below 0. + +```{r} +candidate_harm_bounds <- lapply(astar_candidates, function(astar_candidate) { + gs_harm <- gsDesign::gsSurv( + k = length(info_frac), + test.type = 7, + alpha = alpha, + beta = beta, + astar = astar_candidate, + timing = info_frac, + T = trial_duration, + minfup = minfup, + lambdaC = fail_rate$fail_rate, + eta = fail_rate$dropout_rate, + hr = fail_rate$hr, + ratio = ratio, + sfu = gsDesign::sfHSD, + sfupar = -4, + sfl = gsDesign::sfHSD, + sflpar = -2, + sfharm = gsDesign::sfHSD, + sfharmparam = -2, + r = r, + tol = tol, + method = "Schoenfeld" + ) + + data.frame( + astar = astar_candidate, + analysis = seq_along(gs_harm$harm$bound), + harm_z = as.numeric(gs_harm$harm$bound), + harm_p_lower_tail = pnorm(as.numeric(gs_harm$harm$bound)) + ) +}) |> + do.call(what = rbind) |> + mutate( + analysis = factor( + analysis, + levels = seq_along(info_frac), + labels = c(paste("IA", seq_len(length(info_frac) - 1)), "Final") + ) + ) + +candidate_harm_bounds |> + gt() |> + fmt_number( + columns = c("astar", "harm_z", "harm_p_lower_tail"), + decimals = 3 + ) +``` + +# Boundary specifications + +We use the same Hwang-Shih-DeCani spending functions in both packages. +The upper boundary is O'Brien-Fleming-like with `param = -4`. +The lower futility and harm boundaries use `param = -2`. + +```{r} +upper_par <- list( + sf = gsDesign::sfHSD, + total_spend = alpha, + param = -4, + timing = info_frac +) + +lower_beta_par <- list( + sf = gsDesign::sfHSD, + total_spend = beta, + param = -2, + timing = info_frac +) + +lower_null_par <- list( + sf = gsDesign::sfHSD, + total_spend = astar, + param = -2, + timing = info_frac +) + +harm_par <- lower_null_par +``` + +# Design constructors + +The `gsSurv()` constructor is direct. +For `gs_design_ahr()`, the mapping is: + +- `test.type = 1`: one-sided efficacy only. +- `test.type = 2`: symmetric two-sided design; use the upper spending rule for + the lower bound and set `h1_spending = FALSE`. +- `test.type = 3` and `4`: beta-spending futility, with binding and + non-binding lower bounds, respectively. +- `test.type = 5` and `6`: null-spending futility, with binding and + non-binding lower bounds, respectively. +- `test.type = 7` and `8`: beta-spending futility plus null-spending harm, + with binding and non-binding lower/harm bounds, respectively. + +```{r} +make_gs_surv <- function(test_type) { + gsDesign::gsSurv( + k = length(info_frac), + test.type = test_type, + alpha = alpha, + beta = beta, + astar = astar, + timing = info_frac, + T = trial_duration, + minfup = minfup, + lambdaC = fail_rate$fail_rate, + eta = fail_rate$dropout_rate, + hr = fail_rate$hr, + ratio = ratio, + sfu = gsDesign::sfHSD, + sfupar = -4, + sfl = gsDesign::sfHSD, + sflpar = -2, + sfharm = gsDesign::sfHSD, + sfharmparam = -2, + r = r, + tol = tol, + method = "Schoenfeld" + ) +} + +make_gs_design_ahr <- function(test_type) { + args <- list( + enroll_rate = enroll_rate, + fail_rate = fail_rate, + alpha = alpha, + beta = beta, + ratio = ratio, + info_frac = info_frac, + analysis_time = trial_duration, + r = r, + tol = tol, + info_scale = "h0_info", + upper = gs_spending_bound, + upar = upper_par, + lower = gs_b, + lpar = rep(-Inf, length(info_frac)), + test_lower = FALSE, + harm = gs_b, + hpar = rep(-Inf, length(info_frac)), + test_harm = FALSE, + binding = FALSE, + h1_spending = TRUE + ) + + if (test_type == 2) { + args$lower <- gs_spending_bound + args$lpar <- upper_par + args$test_lower <- TRUE + args$binding <- TRUE + args$h1_spending <- FALSE + } + + if (test_type %in% 3:4) { + args$lower <- gs_spending_bound + args$lpar <- lower_beta_par + args$test_lower <- TRUE + args$binding <- test_type == 3 + args$h1_spending <- TRUE + } + + if (test_type %in% 5:6) { + args$lower <- gs_spending_bound + args$lpar <- lower_null_par + args$test_lower <- TRUE + args$binding <- test_type == 5 + args$h1_spending <- FALSE + } + + if (test_type %in% 7:8) { + args$lower <- gs_spending_bound + args$lpar <- lower_beta_par + args$test_lower <- TRUE + args$binding <- test_type == 7 + args$h1_spending <- TRUE + args$harm <- gs_spending_bound + args$hpar <- harm_par + args$test_harm <- TRUE + } + + do.call(gs_design_ahr, args) +} +``` + +# Bound comparison + +We extract upper, lower, and harm Z-boundaries from both packages and compare +them analysis by analysis. +Infinite bounds are not included. + +```{r} +extract_gs_surv_bounds <- function(x, test_type) { + add_bound <- function(bound, z) { + if (is.null(z)) { + return(NULL) + } + + z <- as.numeric(z) + keep <- is.finite(z) & abs(z) < 20 + + data.frame( + test_type = test_type, + analysis = seq_along(z)[keep], + bound = bound, + gsSurv = z[keep] + ) + } + + do.call( + rbind, + Filter( + Negate(is.null), + list( + add_bound("upper", x$upper$bound), + if (test_type != 1) add_bound("lower", x$lower$bound), + if (test_type %in% 7:8) add_bound("harm", x$harm$bound) + ) + ) + ) +} + +extract_gs_design_ahr_bounds <- function(x, test_type) { + x$bound |> + transmute( + test_type = test_type, + analysis, + bound, + gs_design_ahr = z + ) +} + +comparison <- lapply(1:8, function(test_type) { + gs_surv <- make_gs_surv(test_type) + gs_ahr <- make_gs_design_ahr(test_type) + + merge( + extract_gs_surv_bounds(gs_surv, test_type), + extract_gs_design_ahr_bounds(gs_ahr, test_type), + by = c("test_type", "analysis", "bound"), + all = TRUE + ) +}) |> + do.call(what = rbind) |> + mutate( + difference = gs_design_ahr - gsSurv, + abs_difference = abs(difference), + bound = factor(bound, levels = c("upper", "lower", "harm")) + ) |> + arrange(test_type, analysis, bound) + +stopifnot(max(comparison$abs_difference, na.rm = TRUE) < 1e-5) +stopifnot(all(comparison$gs_design_ahr[comparison$bound == "harm"] < 0)) +``` + +The maximum absolute Z-boundary difference is below `1e-5` for every test type. +All harm bounds in the selected design are negative. + +```{r} +test_type_labels <- data.frame( + test_type = 1:8, + description = c( + "One-sided efficacy", + "Two-sided symmetric", + "Beta-spending futility, binding", + "Beta-spending futility, non-binding", + "Null-spending futility, binding", + "Null-spending futility, non-binding", + "Binding futility and harm", + "Non-binding futility and harm" + ) +) + +comparison |> + group_by(test_type) |> + summarize(max_abs_z_difference = max(abs_difference), .groups = "drop") |> + left_join(test_type_labels, by = "test_type") |> + select(test_type, description, max_abs_z_difference) |> + gt() |> + fmt_scientific(columns = max_abs_z_difference, decimals = 2) +``` + +The detailed Z-boundary comparison is shown below. + +```{r} +comparison |> + left_join(test_type_labels, by = "test_type") |> + mutate( + test_type = paste0("test.type ", test_type, ": ", description), + bound = as.character(bound) + ) |> + select(-description) |> + gt(groupname_col = "test_type") |> + fmt_number(columns = c(gsSurv, gs_design_ahr), decimals = 6) |> + fmt_scientific(columns = c(difference, abs_difference), decimals = 2) +``` + +# Sample size and event counts + +Test types 7 and 8 add a harm bound to the corresponding beta-spending futility +designs in test types 3 and 4. +With the current specification, the harm bound is below the futility bound at +each analysis, and adding the harm bound does not change the efficacy bound, +futility bound, sample size, or event count. + +```{r} +harm_effect_comparison <- lapply( + list( + c(futility_only = 3, with_harm = 7), + c(futility_only = 4, with_harm = 8) + ), + function(test_types) { + futility_only <- make_gs_surv(test_types[["futility_only"]]) + with_harm <- make_gs_surv(test_types[["with_harm"]]) + + data.frame( + comparison = paste0( + "test.type ", test_types[["futility_only"]], + " vs test.type ", test_types[["with_harm"]] + ), + max_upper_z_difference = max(abs(with_harm$upper$bound - futility_only$upper$bound)), + max_futility_z_difference = max(abs(with_harm$lower$bound - futility_only$lower$bound)), + final_n_difference = tail(as.numeric(with_harm$eNC + with_harm$eNE), 1) - + tail(as.numeric(futility_only$eNC + futility_only$eNE), 1), + final_events_difference = tail(with_harm$n.I, 1) - tail(futility_only$n.I, 1), + largest_harm_z = max(with_harm$harm$bound) + ) + } +) |> + do.call(what = rbind) + +harm_effect_comparison |> + gt() |> + fmt_number( + columns = c( + max_upper_z_difference, + max_futility_z_difference, + final_n_difference, + final_events_difference, + largest_harm_z + ), + decimals = 6 + ) +``` + +Thus, the sample size and event count changes seen for test types 7 and 8 +relative to some other test types are inherited from their underlying +beta-spending futility designs, not from the addition of the harm bound. + +The Z-boundaries match closely, but the displayed sample sizes and event counts +from `gsSurv()` and `gs_design_ahr()` are not identical. +The table below compares final analysis values from the two packages. + +```{r} +sample_size_comparison <- lapply(1:8, function(test_type) { + gs_surv <- make_gs_surv(test_type) + gs_ahr <- make_gs_design_ahr(test_type) + + data.frame( + test_type = test_type, + analysis = seq_along(info_frac), + n_gsSurv = as.numeric(gs_surv$eNC + gs_surv$eNE), + n_gs_design_ahr = gs_ahr$analysis$n, + events_gsSurv = as.numeric(gs_surv$n.I), + events_gs_design_ahr = gs_ahr$analysis$event, + time_gsSurv = as.numeric(gs_surv$T), + time_gs_design_ahr = gs_ahr$analysis$time + ) +}) |> + do.call(what = rbind) |> + mutate( + n_difference = n_gs_design_ahr - n_gsSurv, + events_difference = events_gs_design_ahr - events_gsSurv, + n_percent_difference = 100 * n_difference / n_gsSurv, + events_percent_difference = 100 * events_difference / events_gsSurv + ) + +sample_size_comparison |> + filter(analysis == length(info_frac)) |> + left_join(test_type_labels, by = "test_type") |> + select( + test_type, + description, + n_gsSurv, + n_gs_design_ahr, + n_difference, + events_gsSurv, + events_gs_design_ahr, + events_difference, + events_percent_difference + ) |> + gt() |> + fmt_number( + columns = c( + n_gsSurv, + n_gs_design_ahr, + n_difference, + events_gsSurv, + events_gs_design_ahr, + events_difference + ), + decimals = 2 + ) |> + fmt_number(columns = events_percent_difference, decimals = 3) +``` + +The differences are a reporting conversion issue rather than a boundary issue. +For this proportional hazards example with 1:1 randomization, the Schoenfeld +null-variance information rate is exactly events / 4. +The AHR calculation also computes information under the alternative hypothesis. +Because the experimental arm has fewer events when the hazard ratio is 0.7, the +AHR information per event under the alternative is slightly smaller. + +```{r} +information_rate <- gs_info_ahr( + enroll_rate = enroll_rate, + fail_rate = fail_rate, + ratio = ratio, + analysis_time = trial_duration +) + +information_rate_comparison <- data.frame( + quantity = c( + "H1 AHR information per event", + "H0/Schoenfeld information per event", + "H0 divided by H1 information per event" + ), + value = c( + information_rate$info / information_rate$event, + information_rate$info0 / information_rate$event, + (information_rate$info0 / information_rate$event) / + (information_rate$info / information_rate$event) + ) +) + +information_rate_comparison |> + gt() |> + fmt_number(columns = value, decimals = 6) +``` + +Thus both packages agree on the required H0 information target for the +Schoenfeld design. +The current `gs_design_ahr()` output converts that H0 information target back to +reported events and sample size using the AHR alternative-hypothesis information +rate. +This makes the displayed `gs_design_ahr()` event counts and sample sizes about +0.71% larger than the corresponding `gsSurv()` values in this example. +Making these reported counts align exactly is a display/conversion issue that +can be addressed separately from the boundary comparison. + +# Interpretation + +For a single hazard ratio, `gs_design_ahr()` with `info_scale = "h0_info"` +matches `gsSurv(method = "Schoenfeld")` boundaries to numerical integration +tolerance. +The harm-bound test types are obtained by using the usual beta-spending lower +bound for futility and an additional null-spending lower-tail bound through the +`harm`, `hpar`, and `test_harm` arguments. +With `astar = 0.20`, the harm boundary represents an interim lower-tail evidence +threshold that experimental treatment may be worse than control, while keeping +the harm Z-boundaries below 0.