R/as_gt.R

Defines functions as_gt.simtrial_gs_wlr gsd_parts gsd_footnote_row gsd_footnote_nb gsd_footnote gsd_columns as_gt.gs_design fd_footnote fd_title fd_method get_method as_gt.fixed_design as_gt

Documented in as_gt as_gt.fixed_design as_gt.gs_design

#  Copyright (c) 2024 Merck & Co., Inc., Rahway, NJ, USA and its affiliates.
#  All rights reserved.
#
#  This file is part of the gsDesign2 program.
#
#  gsDesign2 is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program.  If not, see <http://www.gnu.org/licenses/>.

#' Convert summary table of a fixed or group sequential design object to a gt object
#'
#' @param x A summary object of a fixed or group sequential design.
#' @param ... Additional arguments (not used).
#'
#' @return A `gt_tbl` object.
#'
#' @export
as_gt <- function(x, ...) {
  UseMethod("as_gt", x)
}

#' @rdname as_gt
#'
#' @export
#'
#' @examplesIf interactive() && !identical(Sys.getenv("IN_PKGDOWN"), "true")
#' library(dplyr)
#'
#' # Enrollment rate
#' enroll_rate <- define_enroll_rate(
#'   duration = 18,
#'   rate = 20
#' )
#'
#' # Failure rates
#' fail_rate <- define_fail_rate(
#'   duration = c(4, 100),
#'   fail_rate = log(2) / 12,
#'   dropout_rate = .001,
#'   hr = c(1, .6)
#' )
#'
#' # Study duration in months
#' study_duration <- 36
#'
#' # Experimental / Control randomization ratio
#' ratio <- 1
#'
#' # 1-sided Type I error
#' alpha <- 0.025
#'
#' # Type II error (1 - power)
#' beta <- 0.1
#'
#' # Example 1 ----
#' fixed_design_ahr(
#'   alpha = alpha, power = 1 - beta,
#'   enroll_rate = enroll_rate, fail_rate = fail_rate,
#'   study_duration = study_duration, ratio = ratio
#' ) %>%
#'   summary() %>%
#'   as_gt()
#'
#' # Example 2 ----
#' fixed_design_fh(
#'   alpha = alpha, power = 1 - beta,
#'   enroll_rate = enroll_rate, fail_rate = fail_rate,
#'   study_duration = study_duration, ratio = ratio
#' ) %>%
#'   summary() %>%
#'   as_gt()
as_gt.fixed_design <- function(x, title = NULL, footnote = NULL, ...) {
  method <- fd_method(x)
  ans <- gt::gt(x) %>%
    gt::tab_header(title = title %||% fd_title(method)) %>%
    gt::tab_footnote(
      footnote = footnote %||% fd_footnote(x, method),
      locations = gt::cells_title(group = "title")
    )
  return(ans)
}

get_method <- function(x, methods) intersect(methods, class(x))[1]

# get the fixed design method
fd_method <- function(x) {
  get_method(x, c("ahr", "fh", "mb", "lf", "rd", "maxcombo", "milestone", "rmst"))
}

# get the default title
fd_title <- function(method) {
  sprintf("Fixed Design %s Method", switch(
    method,
    ahr = "under AHR", fh = "under Fleming-Harrington", mb = "under Magirr-Burman",
    lf = "under Lachin and Foulkes", maxcombo = "under MaxCombo",
    milestone = "under Milestone", rmst = "under Restricted Mean Survival Time",
    rd = "of Risk Difference under Farrington-Manning"
  ))
}

# get the default footnote
fd_footnote <- function(x, method) {
  switch(
    method,
    ahr = "Power computed with average hazard ratio method.",
    fh = paste(
      "Power for Fleming-Harrington test", substring(x$Design, 19),
      "using method of Yung and Liu."
    ),
    lf = paste(
      "Power using Lachin and Foulkes method applied using expected",
      "average hazard ratio (AHR) at time of planned analysis."
    ),
    rd = paste(
      "Risk difference power without continuity correction using method of",
      "Farrington and Manning."
    ),
    maxcombo = paste0(
      "Power for MaxCombo test with Fleming-Harrington tests ",
      substring(x$Design, 9), "."
    ),
    # for mb, milestone, and rmst
    paste("Power for", x$Design, "computed with method of Yung and Liu.")
  )
}

#' @rdname as_gt
#'
#' @param title A string to specify the title of the gt table.
#' @param subtitle A string to specify the subtitle of the gt table.
#' @param colname_spanner A string to specify the spanner of the gt table.
#' @param colname_spannersub A vector of strings to specify the spanner details of the gt table.
#' @param footnote A list containing `content`, `location`, and `attr`.
#'   `content` is a vector of string to specify the footnote text;
#'   `location` is a vector of string to specify the locations to put the
#'   superscript of the footnote index;
#'   `attr` is a vector of string to specify the attributes of the footnotes,
#'   for example, `c("colname", "title", "subtitle", "analysis", "spanner")`;
#'   users can use the functions in the `gt` package to customize the table.
#' @param display_bound A vector of strings specifying the label of the bounds.
#'   The default is `c("Efficacy", "Futility")`.
#' @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.
#'
#' @export
#'
#' @examplesIf interactive() && !identical(Sys.getenv("IN_PKGDOWN"), "true")
#' library(dplyr)
#' # Example 1 ----
#' # The default output
#'
#' gs_design_ahr() %>%
#'   summary() %>%
#'   as_gt()
#'
#' gs_power_ahr() %>%
#'   summary() %>%
#'   as_gt()
#'
#' gs_design_wlr() %>%
#'   summary() %>%
#'   as_gt()
#'
#' gs_power_wlr() %>%
#'   summary() %>%
#'   as_gt()
#'
#' gs_power_combo() %>%
#'   summary() %>%
#'   as_gt()
#'
#' gs_design_rd() %>%
#'   summary() %>%
#'   as_gt()
#'
#' gs_power_rd() %>%
#'   summary() %>%
#'   as_gt()
#'
#' # Example 2 ----
#' # Usage of title = ..., subtitle = ...
#' # to edit the title/subtitle
#' gs_power_wlr() %>%
#'   summary() %>%
#'   as_gt(
#'     title = "Bound Summary",
#'     subtitle = "from gs_power_wlr"
#'   )
#'
#' # Example 3 ----
#' # Usage of colname_spanner = ..., colname_spannersub = ...
#' # to edit the spanner and its sub-spanner
#' gs_power_wlr() %>%
#'   summary() %>%
#'   as_gt(
#'     colname_spanner = "Cumulative probability to cross boundaries",
#'     colname_spannersub = c("under H1", "under H0")
#'   )
#'
#' # Example 4 ----
#' # Usage of footnote = ...
#' # to edit the footnote
#' gs_power_wlr() %>%
#'   summary() %>%
#'   as_gt(
#'     footnote = list(
#'       content = c(
#'         "approximate weighted hazard ratio to cross bound.",
#'         "wAHR is the weighted AHR.",
#'         "the crossing probability.",
#'         "this table is generated by gs_power_wlr."
#'       ),
#'       location = c("~wHR at bound", NA, NA, NA),
#'       attr = c("colname", "analysis", "spanner", "title")
#'     )
#'   )
#'
#' # Example 5 ----
#' # Usage of display_bound = ...
#' # to either show efficacy bound or futility bound, or both(default)
#' gs_power_wlr() %>%
#'   summary() %>%
#'   as_gt(display_bound = "Efficacy")
#'
#' # Example 6 ----
#' # Usage of display_columns = ...
#' # to select the columns to display in the summary table
#' gs_power_wlr() %>%
#'   summary() %>%
#'   as_gt(display_columns = c("Analysis", "Bound", "Nominal p", "Z", "Probability"))
#'
as_gt.gs_design <- function(
    x,
    title = NULL,
    subtitle = NULL,
    colname_spanner = "Cumulative boundary crossing probability",
    colname_spannersub = c("Alternate hypothesis", "Null hypothesis"),
    footnote = NULL,
    display_bound = c("Efficacy", "Futility"),
    display_columns = NULL,
    display_inf_bound = FALSE,
    ...) {

  x_old <- x
  parts <- gsd_parts(
    x, title, subtitle, colname_spannersub, footnote,
    display_bound, display_columns, display_inf_bound
  )

  x <- parts$x %>%
    group_by(Analysis) %>%
    gt::gt() %>%
    gt::tab_spanner(
      columns = all_of(colname_spannersub),
      label = colname_spanner
    ) %>%
    gt::tab_header(title = parts$title, subtitle = parts$subtitle)

  # Add footnotes ----
  footnote <- parts$footnote
  for (i in seq_along(footnote$content)) {
    att <- footnote$attr[i]
    loc <- if (att == "colname") {
      # footnotes are added on the colnames
      gt::cells_column_labels(columns = footnote$location[i])
    } else if (att %in% c("title", "subtitle")) {
      # on the title/subtitle
      gt::cells_title(group = att)
    } else if (att == "analysis") {
      # on the analysis summary row, which is a grouping variable, i.e., Analysis
      gt::cells_row_groups(groups = dplyr::starts_with("Analysis"))
    } else if (att == "spanner") {
      # on the column spanner
      gt::cells_column_spanners(spanners = colname_spanner)
    }
    if (!is.null(loc))
      x <- gt::tab_footnote(x, footnote = footnote$content[i], locations = loc)
  }

  # add footnote for non-binding design
  footnote_nb <- gsd_footnote_nb(x_old, parts$alpha)
  if (!is.null(footnote_nb)) x <- gt::tab_footnote(
    x,
    footnote = footnote_nb,
    locations = gt::cells_body(
      columns = colname_spannersub[2],
      rows = gsd_footnote_row(parts$x, display_bound[1])
    )
  )

  return(x)
}

# get different default columns to display
gsd_columns <- function(columns, method, x) {
  # set different default columns to display
  if (is.null(columns)) columns <- c(
    "Analysis", "Bound", "Z", "Nominal p",
    sprintf("%s at bound", switch(method, ahr = "~HR", wlr = "~wHR", rd = "~Risk difference")),
    "Alternate hypothesis", "Null hypothesis"
  )
  # filter the columns to display as the output: if `Probability` is selected to
  # output, transform it to `c("Alternate hypothesis", "Null hypothesis")`
  if (any(i <- columns == "Probability"))
    columns <- c(columns[!i], "Alternate hypothesis", "Null hypothesis")
  ## check if the `display_columns` are included in `x` output
  if (!all(columns %in% names(x))) stop(
    "not all variable names in 'display_columns' are in the summary_bound object!"
  )
  columns
}

# default footnotes for 'gs_design' tables
gsd_footnote <- function(method, columns) {
  n <- c("Nominal p", "~HR at bound", "~wHR at bound")
  i <- n %in% columns
  res <- if (i[1]) list(
    content = paste(
      "One-sided p-value for experimental vs control treatment.",
      "Value < 0.5 favors experimental, > 0.5 favors control."
    ),
    location = n[1], attr = "colname"
  ) else {
    list(content = NULL, location = NULL, attr = NULL)
  }
  x <- "Approximate hazard ratio to cross bound."
  switch(
    method,
    ahr = res %+% if (i[2]) list(x, n[2], "colname"),
    wlr = res %+% (if (i[3]) list(x, n[3], "colname")) %+%
      list("wAHR is the weighted AHR.", NULL, "analysis"),
    combo = res %+% list(
      "EF is event fraction. AHR is under regular weighted log rank test.",
      NULL, "analysis"
    ),
    rd = res
  )
}

# footnote for non-binding designs
gsd_footnote_nb <- function(x, x_alpha) {
  full_alpha <- attr(x, "full_alpha")
  if (!inherits(x, "non_binding") || x_alpha >= full_alpha) return()
  a1 <- format(x_alpha, scientific = FALSE)
  a2 <- format(full_alpha, scientific = FALSE)
  a3 <- format(full_alpha - x_alpha, scientific = FALSE)
  paste0(
    "Cumulative alpha for final analysis ",
    "(", a1, ") ", "is less than the full alpha ", "(", a2, ") ",
    "when the futility bound is non-binding. ",
    "The smaller value subtracts the probability of crossing a futility bound ",
    "before crossing an efficacy bound at a later analysis ",
    "(", a2, " - ", a3, " = ", a1, ") ", "under the null hypothesis."
  )
}

# where to add the non-binding design footnote
gsd_footnote_row <- function(x, bound) {
  # for a vector of "Analysis: N", get a logical vector `i`, in which `TRUE`
  # indicates the position of the largest `N`
  a <- x$Analysis
  r <- "^Analysis: ([0-9]+).*"
  i <- grepl(r, a)
  k <- as.numeric(sub(r, '\\1', a[i]))
  i[i] <- k == max(k)
  i & x$Bound == bound
}

# a list of information for `as_[gt|rtf].gs_design()` methods: the transformed
# data, title, and footnote, etc.
gsd_parts <- function(
    x, title, subtitle, spannersub, footnote, bound, columns, inf_bound,
    alpha_column = spannersub[2], transform = identity
) {
  method <- intersect(c("ahr", "wlr", "combo", "rd"), class(x))[1]
  if (!inf_bound) x <- filter(x, !is.infinite(Z))
  # `x` needs a custom transformation in as_rtf()
  x2 <- transform(x)

  columns <- gsd_columns(columns, method, x2)
  x2 <- x2[, columns]
  x2 <- subset(x2, !is.na(`Alternate hypothesis`) & !is.na(`Null hypothesis`))
  x2 <- subset(x2, Bound %in% bound)

  i <- match(c("Alternate hypothesis", "Null hypothesis"), names(x2))
  names(x2)[i] <- spannersub

  title <- title %||% paste("Bound summary", switch(
    method,
    ahr = "for AHR design", wlr = "for WLR design",
    combo = "for MaxCombo design", rd = "of Binary Endpoint"
  ))
  subtitle <- subtitle %||% switch(
    method,
    ahr = "AHR approximations of ~HR at bound",
    wlr = "WLR approximation of ~wHR at bound",
    combo = "MaxCombo approximation",
    rd = "measured by risk difference"
  )

  list(
    x = arrange(x2, Analysis),
    title = title, subtitle = subtitle,
    footnote = footnote %||% gsd_footnote(method, columns),
    alpha = max(filter(x, Bound == bound[1])[[alpha_column]])
  )
}

# Only purpose of the method below is to fix S3 redirection when gsDesign2 is
# loaded after simtrial, which masks the as_gt() generic from simtrial

#' @export
as_gt.simtrial_gs_wlr <- function(x, ...) {
  f <- utils::getFromNamespace("as_gt.simtrial_gs_wlr", "simtrial")
  f(x, ...)
}

Try the gsDesign2 package in your browser

Any scripts or data that you put into this service are public.

gsDesign2 documentation built on April 3, 2025, 9:39 p.m.