R/gsMethods.R

Defines functions summary.spendfn sfprint gsLegendText print.gsBoundSummary xprint gsBoundSummary print.nSurvival print.gsDesign summary.gsDesign print.gsProbability

Documented in gsBoundSummary print.gsBoundSummary print.gsDesign print.gsProbability print.nSurvival summary.gsDesign summary.spendfn xprint

# print.gsProbability roxy [sinew] ----
#' @export
#' @rdname gsProbability
#' @importFrom stats pnorm
# print.gsProbability function [sinew] ----
print.gsProbability <- function(x, ...) {
  ntxt <- "N "
  nval <- ceiling(x$n.I)
  nspace <- log10(x$n.I[x$k])
  for (i in 1:nspace)
  {
    cat(" ")
  }

  cat("            ")
  if (min(x$lower$bound) < 0) {
    cat(" ")
  }
  if (max(class(x) == "gsBinomialExact") == 1) {
    cat("Bounds")
    y <- cbind(1:x$k, nval, x$lower$bound, round(x$upper$bound, 2))
    colnames(y) <- c("Analysis", "  N", "  a", "  b")
  }
  else {
    cat("Lower bounds   Upper bounds")
    y <- cbind(
      1:x$k, nval, round(x$lower$bound, 2), round(stats::pnorm(x$lower$bound), 4),
      round(x$upper$bound, 2), round(stats::pnorm(-x$upper$bound), 4)
    )
    colnames(y) <- c("Analysis", ntxt, "Z  ", "Nominal p", "Z  ", "Nominal p")
  }
  rownames(y) <- rep(" ", x$k)
  cat("\n")
  print(y)
  cat("\nBoundary crossing probabilities and expected sample size assume\n")
  cat("any cross stops the trial\n\n")
  j <- length(x$theta)
  sump <- 1:j
  for (m in 1:j)
  {
    sump[m] <- sum(x$upper$prob[, m])
  }
  y <- round(cbind(x$theta, t(x$upper$prob), sump, x$en), 4)
  y[, x$k + 3] <- round(y[, x$k + 3], 1)
  rownames(y) <- rep(" ", j)
  colnames(y) <- c("Theta", 1:x$k, "Total", "E{N}")
  cat("Upper boundary\n")
  cat("          Analysis\n")
  print(y)

  for (m in 1:j)
  {
    sump[m] <- sum(x$lower$prob[, m])
  }

  y <- round(cbind(x$theta, t(x$lower$prob), sump), 4)
  rownames(y) <- rep(" ", j)
  colnames(y) <- c("Theta", 1:x$k, "Total")
  cat("\nLower boundary\n")
  cat("          Analysis\n")
  print(y)
  invisible(x)
}


# summary.gsDesign roxy [sinew] ----
#' @export
#' @rdname gsBoundSummary
# summary.gsDesign function [sinew] ----
summary.gsDesign <- function(object, information = FALSE, timeunit = "months", ...) {
  out <- NULL
  if (object$test.type == 1) {
    out <- paste(out, "One-sided group sequential design with ", sep = "")
  } else if (object$test.type == 2) {
    out <- paste(out, "Symmetric two-sided group sequential design with ", sep = "")
  } else {
    out <- paste(out, "Asymmetric two-sided group sequential design with ", sep = "")
    if (object$test.type %in% c(2, 3, 5)) {
      out <- paste(out, "binding futility bound, ", sep = "")
    } else {
      out <- paste(out, "non-binding futility bound, ", sep = "")
    }
  }
  out <- paste(out, object$k, " analyses, ", sep = "")
  if (object$nFixSurv > 0) {
    out <- paste(out, "time-to-event outcome with sample size ", ceiling(object$nSurv),
      " and ", ceiling(object$n.I[object$k]), " events required, ",
      sep = ""
    )
  } else if ("gsSurv" %in% class(object)) {
    out <- paste(out, "time-to-event outcome with sample size ",
      ifelse(object$ratio == 1, 2 * ceiling(rowSums(object$eNE))[object$k],
        (ceiling(rowSums(object$eNE)) + ceiling(rowSums(object$eNC)))[object$k]
      ),
      " and ", ceiling(object$n.I[object$k]), " events required, ",
      sep = ""
    )
  } else if (information) {
    out <- paste(out, " total information ", round(object$n.I[object$k], 2), ", ", sep = "")
  } else {
    out <- paste(out, "sample size ", ceiling(object$n.I[object$k]), ", ", sep = "")
  }
  out <- paste(out, 100 * (1 - object$beta), " percent power, ", 100 * object$alpha, " percent (1-sided) Type I error", sep = "")
  if ("gsSurv" %in% class(object)) {
    out <- paste(out, " to detect a hazard ratio of ", round(object$hr, 2), sep = "")
    if (object$hr0 != 1) out <- paste(out, " with a null hypothesis hazard ratio of ", round(object$hr0, 2), sep = "")
    out <- paste(out, ". Enrollment and total study durations are assumed to be ", round(sum(object$R), 1),
      " and ", round(max(object$T), 1), " ", timeunit, ", respectively",
      sep = ""
    )
  }
  if (object$test.type == 2) {
    out <- paste(out, ". Bounds derived using a ", sep = "")
  } else {
    out <- paste(out, ". Efficacy bounds derived using a", sep = "")
  }
  out <- paste(out, " ", summary(object$upper), ".", sep = "")
  if (object$test.type > 2) out <- paste(out, " Futility bounds derived using a ", summary(object$lower), ".", sep = "")
  return(out)
}

# print.gsDesign roxy [sinew] ----
#' @export
#' @rdname gsBoundSummary
# print.gsDesign function [sinew] ----
print.gsDesign <- function(x, ...) {
  if (x$nFixSurv > 0) {
    cat("Group sequential design sample size for time-to-event outcome\n",
      "with sample size ", x$nSurv, ". The analysis plan below shows events\n",
      "at each analysis.\n",
      sep = ""
    )
  }

  if (x$test.type == 1) {
    cat("One-sided")
  }
  else if (x$test.type == 2) {
    cat("Symmetric two-sided")
  }
  else {
    cat("Asymmetric two-sided")
  }

  cat(" group sequential design with\n")
  cat(100 * (1 - x$beta), "% power and", 100 * x$alpha, "% Type I Error.\n")
  if (x$test.type > 1) {
    if (x$test.type == 4 || x$test.type == 6) {
      cat("Upper bound spending computations assume\ntrial continues if lower bound is crossed.\n\n")
    }
    else {
      cat("Spending computations assume trial stops\nif a bound is crossed.\n\n")
    }
  }
  if (x$n.fix != 1) {
    ntxt <- "N "
    nval <- ceiling(x$n.I)
    nspace <- log10(x$n.I[x$k])
    for (i in 1:nspace)
    {
      cat(" ")
    }
    cat("            ")
  }
  else {
    ntxt <- "Ratio*"
    nval <- round(x$n.I, 3)
    cat("           Sample\n")
    cat("            Size ")
  }
  if (x$test.type > 2) {
    if (min(x$lower$bound) < 0) {
      cat(" ")
    }
    cat("  ----Lower bounds----  ----Upper bounds-----")
    y <- cbind(
      1:x$k, nval, round(x$lower$bound, 2), round(stats::pnorm(x$lower$bound), 4),
      round(x$lower$spend, 4), round(x$upper$bound, 2), round(stats::pnorm(-x$upper$bound), 4),
      round(x$upper$spend, 4)
    )
    colnames(y) <- c("Analysis", ntxt, "Z  ", "Nominal p", "Spend+", "Z  ", "Nominal p", "Spend++")
  }
  else {
    y <- cbind(
      1:x$k, nval, round(x$upper$bound, 2), round(stats::pnorm(-x$upper$bound), 4),
      round(x$upper$spend, 4)
    )
    colnames(y) <- c("Analysis", ntxt, "Z  ", "Nominal p", "Spend")
  }
  rownames(y) <- rep(" ", x$k)
  cat("\n")
  print(y)
  cat("     Total")
  if (x$n.fix != 1) {
    for (i in 1:nspace)
    {
      cat(" ")
    }
  }
  else {
    cat("     ")
  }
  cat("                  ")

  if (x$test.type > 2) {
    if (min(x$lower$bound) < 0) {
      cat(" ")
    }
    cat(format(round(sum(x$lower$spend), 4), nsmall = 4), "                ")
  }

  cat(format(round(sum(x$upper$spend), 4), nsmall = 4), "\n")

  if (x$test.type > 4) {
    cat("+ lower bound spending (under H0):\n ")
  }
  else if (x$test.type > 2) {
    cat("+ lower bound beta spending (under H1):\n ")
  }

  if (x$test.type > 2) {
    cat(summary(x$lower), ".", sep = "")
  }

  cat("\n++ alpha spending:\n ")
  cat(summary(x$upper), ".\n", sep = "")

  if (x$n.fix == 1) {
    cat("* Sample size ratio compared to fixed design with no interim\n")
  }

  cat("\nBoundary crossing probabilities and expected sample size\nassume any cross stops the trial\n\n")
  j <- length(x$theta)
  sump <- 1:j

  for (m in 1:j)
  {
    sump[m] <- sum(x$upper$prob[, m])
  }

  y <- round(cbind(x$theta, t(x$upper$prob), sump, x$en), 4)
  if (x$n.fix != 1) {
    y[, x$k + 3] <- round(y[, x$k + 3], 1)
  }
  rownames(y) <- rep(" ", j)
  colnames(y) <- c("Theta", 1:x$k, "Total", "E{N}")
  cat("Upper boundary (power or Type I Error)\n")
  cat("          Analysis\n")
  print(y)
  if (x$test.type > 1) {
    for (m in 1:j)
    {
      sump[m] <- sum(x$lower$prob[, m])
    }
    y <- round(cbind(x$theta, t(x$lower$prob), sump), 4)
    rownames(y) <- rep(" ", j)
    colnames(y) <- c("Theta", 1:x$k, "Total")
    cat("\nLower boundary (futility or Type II Error)\n")
    cat("          Analysis\n")
    print(y)
  }
  invisible(x)
}

# print.nSurvival roxy [sinew] ----
#' @rdname nSurvival
#' @export
# print.nSurvival function [sinew] ----
print.nSurvival <- function(x, ...) {
  if (!inherits(x, "nSurvival")) stop("print.nSurvival: primary argument must have class nSurvival")
  cat("Fixed design, two-arm trial with time-to-event\n")
  cat("outcome (Lachin and Foulkes, 1986).\n")
  cat("Study duration (fixed):          Ts=", x$Ts, "\n", sep = "")
  cat("Accrual duration (fixed):        Tr=", x$Tr, "\n", sep = "")
  if (x$entry == "unif") {
    cat('Uniform accrual:              entry="unif"\n')
  } else {
    cat('Exponential accrual:          entry="expo"\n')
    cat("Accrual shape parameter:      gamma=", round(x$gamma, 3), "\n", sep = "")
  }
  cat("Control median:      log(2)/lambda1=", round(log(2) / x$lambda1, 1), "\n", sep = "")
  cat("Experimental median: log(2)/lambda2=", round(log(2) / x$lambda2, 1), "\n", sep = "")
  if (x$eta == 0) {
    cat("Censoring only at study end (eta=0)\n")
  } else {
    cat("Censoring median:        log(2)/eta=", round(log(2) / x$eta, 1), "\n", sep = "")
  }
  cat("Control failure rate:       lambda1=", round(x$lambda1, 3), "\n", sep = "")
  cat("Experimental failure rate:  lambda2=", round(x$lambda2, 3), "\n", sep = "")
  cat("Censoring rate:                 eta=", round(x$eta, 3), "\n", sep = "")
  cat("Power:                 100*(1-beta)=", (1 - x$beta) * 100, "%\n", sep = "")
  cat("Type I error (", x$sided, "-sided):   100*alpha=", 100 * x$alpha, "%\n", sep = "")
  if (x$ratio == 1) {
    cat("Equal randomization:          ratio=1\n")
  } else {
    cat("Randomization (Exp/Control):  ratio=", x$ratio, "\n", sep = "")
  }
  if (x$type == "rr") {
    cat("Sample size based on hazard ratio=", round(x$lambda2 / x$lambda1, 3), ' (type="rr")\n', sep = "")
  } else {
    cat("Sample size based on risk difference=", round(x$lambda1 - x$lambda2, 3), ' (type="rd")\n', sep = "")
    if (x$approx) {
      cat("Sample size based on H1 variance only:  approx=TRUE\n")
    } else {
      cat("Sample size based on H0 and H1 variance: approx=FALSE\n")
    }
  }
  cat("Sample size (computed):           n=", 2 * ceiling(x$n / 2), "\n", sep = "")
  cat("Events required (computed): nEvents=", ceiling(x$nEvents), "\n", sep = "")
  invisible(x)
}



# gsBoundSummary roxy [sinew] ----
#' @title Bound Summary and Z-transformations
#' @description  A tabular summary of a group sequential design's bounds and their properties
#' are often useful. The 'vintage' \code{print.gsDesign()} function provides a
#' complete but minimally formatted summary of a group sequential design
#' derived by \code{gsDesign()}. A brief description of the overall design can
#' also be useful (\code{summary.gsDesign()}.  A tabular summary of boundary
#' characteristics oriented only towards LaTeX output is produced by
#' \code{\link{xtable.gsSurv}}. More flexibility is provided by
#' \code{gsBoundSummary()} which produces a tabular summary of a
#' user-specifiable set of package-provided boundary properties in a data
#' frame.  This can also be used to along with functions such as
#' \code{\link{print.data.frame}()}, \code{\link{write.table}()},
#' \code{\link{write.csv}()}, \code{\link{write.csv2}()} or, from the RTF
#' package, \code{addTable.RTF()} (from the rtf package) to produce console or
#' R Markdown output or output to a variety of file types. \code{xprint()} is
#' provided for LaTeX output by setting default options for
#' \code{\link{print.xtable}()} when producing tables summarizing design
#' bounds.
#'
#' Individual transformation of z-value test statistics for interim and final
#' analyses are obtained from \code{gsBValue()}, \code{gsDelta()},
#' \code{gsHR()} and \code{gsCPz()} for B-values, approximate treatment effect
#' (see details), approximate hazard ratio and conditional power, respectively.
#'
#' The \code{print.gsDesign} function is intended to provide an easier output
#' to review than is available from a simple list of all the output components.
#' The \code{gsBoundSummary} function is intended to provide a summary of
#' boundary characteristics that is often useful for evaluating boundary
#' selection; this outputs an extension of the \code{data.frame} class that
#' sets up default printing without row names using
#' \code{print.gsBoundSummary}. \code{summary.gsDesign}, on the other hand,
#' provides a summary of the overall design at a higher level; this provides
#' characteristics not included in the \code{gsBoundSummary} summary and no
#' detail concerning interim analysis bounds.
#'
#' In brief, the computed descriptions of group sequential design bounds are as
#' follows: \code{Z:} Standardized normal test statistic at design bound.
#'
#' \code{p (1-sided):} 1-sided p-value for \code{Z}. This will be computed as
#' the probability of a greater EXCEPT for lower bound when a 2-sided design is
#' being summarized.
#'
#' \code{delta at bound:} Approximate value of the natural parameter at the
#' bound. The approximate standardized effect size at the bound is generally
#' computed as \code{Z/sqrt(n)}. Calling this \code{theta}, this is translated
#' to the \code{delta} using the values \code{delta0} and \code{delta1} from
#' the input \code{x} by the formula \code{delta0 +
#' (delta1-delta0)/theta1*theta} where \code{theta1} is the alternate
#' hypothesis value of the standardized parameter. Note that this value will be
#' exponentiated in the case of relative risks, hazard ratios or when the user
#' specifies \code{logdelta=TRUE}. In the case of hazard ratios, the value is
#' computed instead by \code{gsHR()} to be consistent with
#' \code{plot.gsDesign()}. Similarly, the value is computed by \code{gsRR()}
#' when the relative risk is the natural parameter.
#'
#' \code{Spending: }Incremental error spending at each given analysis. For
#' asymmetric designs, futility bound will have beta-spending summarized.
#' Efficacy bound always has alpha-spending summarized.
#'
#' \code{B-value: }\code{sqrt(t)*Z} where \code{t} is the proportion of
#' information at the analysis divided by the final analysis planned
#' information. The expected value for B-values is directly proportional to
#' \code{t}.
#'
#' \code{CP: }Conditional power under the estimated treatment difference
#' assuming the interim Z-statistic is at the study bound
#'
#' \code{CP H1: }Conditional power under the alternate hypothesis treatment
#' effect assuming the interim test statistic is at the study bound.
#'
#' \code{PP: }Predictive power assuming the interim test statistic is at the
#' study bound and the input prior distribution for the standardized effect
#' size. This is the conditional power averaged across the posterior
#' distribution for the treatment effect given the interim test statistic
#' value. \code{P{Cross if delta=xx}: }For each of the parameter values in
#' \code{x}, the probability of crossing either bound given that treatment
#' effect is computed. This value is cumulative for each bound. For example,
#' the probability of crossing the efficacy bound at or before the analysis of
#' interest.
#'
#' @param x An item of class \code{gsDesign} or \code{gsSurv}, except for
#' \code{print.gsBoundSummary()} where \code{x} is an object created by
#' \code{gsBoundSummary()} and \code{xprint()} which is used with \code{xtable}
#' (see examples)
#' @param object An item of class \code{gsDesign} or \code{gsSurv}
#' @param information indicator of whether \code{n.I} in \code{object}
#' represents statistical information rather than sample size or event counts.
#' @param timeunit Text string with time units used for time-to-event designs
#' created with \code{gsSurv()}
#' @param deltaname Natural parameter name. If default \code{NULL} is used,
#' routine will default to \code{"HR"} when class is \code{gsSurv} or if
#' \code{nFixSurv} was input when creating \code{x} with \code{gsDesign()}.
#' @param logdelta Indicates whether natural parameter is the natural logarithm
#' of the actual parameter. For example, the relative risk or odds-ratio would
#' be put on the logarithmic scale since the asymptotic behavior is 'more
#' normal' than a non-transformed value. As with \code{deltaname}, the default
#' will be changed to true if \code{x} has class \code{gsDesign} or if
#' \code{nFixSurv>0} was input when \code{x} was created by \code{gsDesign()};
#' that is, the natural parameter for a time-to-event endpoint will be on the
#' logarithmic scale.
#' @param Nname This will normally be changed to \code{"N"} or, if a
#' time-to-event endpoint is used, \code{"Events"}. Other immediate possibility
#' are \code{"Deaths"} or \code{"Information"}.
#' @param digits Number of digits past the decimal to be printed in the body of
#' the table.
#' @param ddigits Number of digits past the decimal to be printed for the
#' natural parameter delta.
#' @param tdigits Number of digits past the decimal point to be shown for
#' estimated timing of each analysis.
#' @param timename Text string indicating time unit.
#' @param prior A prior distribution for the standardized effect size. Must be
#' of the format produced by \code{normalGrid()}, but can reflect an arbitrary
#' prior distribution. The default reflects a normal prior centered half-way
#' between the null and alternate hypothesis with the variance being equivalent
#' to the treatment effect estimate if 1 percent of the sample size for a fixed
#' design were sampled. The prior is intended to be relatively uninformative.
#' This input will only be applied if \code{POS=TRUE} is input.
#' @param ratio Sample size ratio assumed for experimental to control treatment
#' group sample sizes. This only matters when \code{x} for a binomial or
#' time-to-event endpoint where \code{gsRR} or \code{gsHR} are used for
#' approximating the treatment effect if a test statistic falls on a study
#' bound.
#' @param exclude A list of test statistics to be excluded from design boundary
#' summary produced; see details or examples for a list of all possible output
#' values. A value of \code{NULL} produces all available summaries.
#' @param POS This is an indicator of whether or not probability of success
#' (POS) should be estimated at baseline or at each interim based on the prior
#' distribution input in \code{prior}. The prior probability of success before
#' the trial starts is the power of the study averaged over the prior
#' distribution for the standardized effect size. The POS after an interim
#' analysis assumes the interim test statistic is an unknown value between the
#' futility and efficacy bounds. Based on this, a posterior distribution for
#' the standardized parameter is computed and the conditional power of the
#' trial is averaged over this posterior distribution.
#' @param r See \code{\link{gsDesign}}. This is an integer used to control the
#' degree of accuracy of group sequential calculations which will normally not
#' be changed.
#' @param row.names indicator of whether or not to print row names
#' @param include.rownames indicator of whether or not to include row names in
#' output.
#' @param hline.after table lines after which horizontal separation lines
#' should be set; default is to put lines between each analysis as well as at
#' the top and bottom of the table.
#' @param z A vector of z-statistics
#' @param i A vector containing the analysis for each element in \code{z}; each
#' element must be in 1 to \code{x$k}, inclusive
#' @param theta A scalar value representing the standardized effect size used
#' for conditional power calculations; see \code{gsDesign}; if NULL,
#' conditional power is computed at the estimated interim treatment effect
#' based on \code{z}
#' @param ylab Used when functions are passed to \code{plot.gsDesign} to
#' establish default y-axis labels
#' @param ... This allows many optional arguments that are standard when
#' calling \code{plot} for \code{gsBValue}, \code{gsDelta}, \code{gsHR},
#' \code{gsRR} and \code{gsCPz}
#' @return \code{gsBValue()}, \code{gsDelta()}, \code{gsHR()} and
#' \code{gsCPz()} each returns a vector containing the B-values, approximate
#' treatment effect (see details), approximate hazard ratio and conditional
#' power, respectively, for each value specified by the interim test statistics
#' in \code{z} at interim analyses specified in \code{i}.
#'
#' \code{summary} returns a text string summarizing the design at a high level.
#' This may be used with \code{gsBoundSummary} for a nicely formatted, concise
#' group sequential design description.
#'
#' \code{gsBoundSummary} returns a table in a data frame providing a variety of
#' boundary characteristics. The tabular format makes formatting particularly
#' amenable to place in documents either through direct creation of readable by
#' Word (see the \code{rtf} package) or to a csv format readable by spreadsheet
#' software using \code{write.csv}.
#'
#' \code{print.gsDesign} prints an overall summary a group sequential design.
#' While the design description is complete, the format is not as `document
#' friendly' as \code{gsBoundSummary}.
#'
#' \code{print.gsBoundSummary} is a simple extension of \code{print.data.frame}
#' intended for objects created with \code{gsBoundSummary}. The only extension
#' is to make the default to not print row names. This is probably `not good R
#' style' but may be helpful for many lazy R programmers like the author.
#' @examples
#' library(ggplot2)
#' # survival endpoint using gsSurv
#' # generally preferred over nSurv since time computations are shown
#' xgs <- gsSurv(lambdaC = .2, hr = .5, eta = .1, T = 2, minfup = 1.5)
#' gsBoundSummary(xgs, timename = "Year", tdigits = 1)
#' summary(xgs)
#' 
#' # survival endpoint using nSurvival
#' # NOTE: generally recommend gsSurv above for this!
#' ss <- nSurvival(
#'   lambda1 = .2, lambda2 = .1, eta = .1, Ts = 2, Tr = .5,
#'   sided = 1, alpha = .025, ratio = 2
#' )
#' xs <- gsDesign(nFixSurv = ss$n, n.fix = ss$nEvents, delta1 = log(ss$lambda2 / ss$lambda1))
#' gsBoundSummary(xs, logdelta = TRUE, ratio = ss$ratio)
#' # generate some of the above summary statistics for the upper bound
#' z <- xs$upper$bound
#' # B-values
#' gsBValue(z = z, i = 1:3, x = xs)
#' # hazard ratio
#' gsHR(z = z, i = 1:3, x = xs)
#' # conditional power at observed treatment effect
#' gsCPz(z = z[1:2], i = 1:2, x = xs)
#' # conditional power at H1 treatment effect
#' gsCPz(z = z[1:2], i = 1:2, x = xs, theta = xs$delta)
#' 
#' # information-based design
#' xinfo <- gsDesign(delta = .3, delta1 = .3)
#' gsBoundSummary(xinfo, Nname = "Information")
#' 
#' # show all available boundary descriptions
#' gsBoundSummary(xinfo, Nname = "Information", exclude = NULL)
#' 
#' # add intermediate parameter value
#' xinfo <- gsProbability(d = xinfo, theta = c(0, .15, .3))
#' class(xinfo) # note this is still as gsDesign class object
#' gsBoundSummary(xinfo, Nname = "Information")
#' 
#' # now look at a binomial endpoint; specify H0 treatment difference as p1-p2=.05
#' # now treatment effect at bound (say, thetahat) is transformed to
#' # xp$delta0 + xp$delta1*(thetahat-xp$delta0)/xp$delta
#' np <- nBinomial(p1 = .15, p2 = .10)
#' xp <- gsDesign(n.fix = np, endpoint = "Binomial", delta1 = .05)
#' summary(xp)
#' gsBoundSummary(xp, deltaname = "p[C]-p[E]")
#' # estimate treatment effect at lower bound
#' # by setting delta0=0 (default) and delta1 above in gsDesign
#' # treatment effect at bounds is scaled to these differences
#' # in this case, this is the difference in event rates
#' gsDelta(z = xp$lower$bound, i = 1:3, xp)
#' 
#' # binomial endpoint with risk ratio estimates
#' n.fix <- nBinomial(p1 = .3, p2 = .15, scale = "RR")
#' xrr <- gsDesign(k = 2, n.fix = n.fix, delta1 = log(.15 / .3), endpoint = "Binomial")
#' gsBoundSummary(xrr, deltaname = "RR", logdelta = TRUE)
#' gsRR(z = xp$lower$bound, i = 1:3, xrr)
#' plot(xrr, plottype = "RR")
#' 
#' # delta is odds-ratio: sample size slightly smaller than for relative risk or risk difference
#' n.fix <- nBinomial(p1 = .3, p2 = .15, scale = "OR")
#' xOR <- gsDesign(k = 2, n.fix = n.fix, delta1 = log(.15 / .3 / .85 * .7), endpoint = "Binomial")
#' gsBoundSummary(xOR, deltaname = "OR", logdelta = TRUE)
#' 
#' # for nice LaTeX table output, use xprint
#' xprint(xtable::xtable(gsBoundSummary(xOR, deltaname = "OR", logdelta = TRUE), 
#'                                           caption = "Table caption."))
#' @note The gsDesign technical manual is available at
#'   \url{https://keaven.github.io/gsd-tech-manual/}.
#' @author Keaven Anderson \email{keaven_anderson@@merck.com}
#' @seealso \link{gsDesign}, \link{plot.gsDesign},
#' \code{\link{gsProbability}}, \code{\link{xtable.gsSurv}}
#' @references Jennison C and Turnbull BW (2000), \emph{Group Sequential
#' Methods with Applications to Clinical Trials}. Boca Raton: Chapman and Hall.
#' @keywords design
#' @aliases print.gsDesign summary.gsDesign
#' @rdname gsBoundSummary
#' @export
#' @importFrom stats pnorm
# gsBoundSummary function [sinew] ----
gsBoundSummary <- function(x, deltaname = NULL, logdelta = FALSE, Nname = NULL, digits = 4, ddigits = 2, tdigits = 0, timename = "Month",
                           prior = normalGrid(mu = x$delta / 2, sigma = 10 / sqrt(x$n.fix)),
                           POS = FALSE, ratio = NULL, exclude = c("B-value", "Spending", "CP", "CP H1", "PP"), r = 18, ...) {
  k <- x$k
  if (is.null(Nname)) {
    if (x$n.fix == 1) {
      Nname <- "N/Fixed design N"
    } else {
      Nname <- "N"
    }
  }
  if(is.null(deltaname)){
    if ("gsSurv" %in% class(x) || x$nFixSurv>0){deltaname="HR"}else{deltaname="delta"}
  }
  # delta values corresponding to x$theta
  delta <- x$delta0 + (x$delta1 - x$delta0) * x$theta / x$delta
  if (logdelta || "gsSurv" %in% class(x)) delta <- exp(delta)
  # ratio is only used for RR and HR calculations at boundaries
  if ("gsSurv" %in% class(x)) {
    ratio <- x$ratio
  } else if (is.null(ratio)) ratio <- 1
  # delta values at bounds
  # note that RR and HR are treated specially
  if (x$test.type > 1) {
    if (x$nFixSurv > 0 || "gsSurv" %in% class(x) || toupper(deltaname) == "HR") {
      deltafutility <- gsHR(x = x, i = 1:x$k, z = x$lower$bound[1:x$k], ratio = ratio)
    } else if (tolower(deltaname) == "rr") {
      deltafutility <- gsRR(x = x, i = 1:x$k, z = x$lower$bound[1:x$k], ratio = ratio)
    } else {
      deltafutility <- gsDelta(x = x, i = 1:x$k, z = x$lower$bound[1:x$k])
      if (logdelta==TRUE) deltafutility <- exp(deltafutility)
    }
  }
  if (x$nFixSurv > 0 || "gsSurv" %in% class(x) || toupper(deltaname) == "HR") {
    deltaefficacy <- gsHR(x = x, i = 1:x$k, z = x$upper$bound[1:x$k], ratio = ratio)
  } else if (tolower(deltaname) == "rr") {
    deltaefficacy <- gsRR(x = x, i = 1:x$k, z = x$upper$bound[1:x$k], ratio = ratio)
  } else {
    deltaefficacy <- gsDelta(x = x, i = 1:x$k, z = x$upper$bound[1:x$k])
    if (logdelta==TRUE) deltaefficacy <- exp(deltaefficacy)
  }
  # create delta names for boundary crossing probabilities
  deltanames <- paste("P(Cross) if ", deltaname, "=", round(delta, ddigits), sep = "")
  pframe <- NULL
  for (i in 1:length(x$theta)) pframe <- rbind(pframe, data.frame("Value" = deltanames[i], "Efficacy" = cumsum(x$upper$prob[, i]), i = 1:x$k))
  if (x$test.type > 1) {
    pframe2 <- NULL
    for (i in 1:length(x$theta)) pframe2 <- rbind(pframe2, data.frame("Futility" = cumsum(x$lower$prob[, i])))
    pframe <- data.frame(cbind("Value" = pframe[, 1], pframe2, pframe[, -1]))
  }
  # conditional power at bound, theta=hat(theta)
  cp <- data.frame(gsBoundCP(x, r = r))
  # conditional power at bound, theta=theta[1]
  cp1 <- data.frame(gsBoundCP(x, theta = x$delta, r = r))
  if (x$test.type > 1) {
    colnames(cp) <- c("Futility", "Efficacy")
    colnames(cp1) <- c("Futility", "Efficacy")
  } else {
    colnames(cp) <- "Efficacy"
    colnames(cp1) <- "Efficacy"
  }
  cp <- data.frame(cp, "Value" = "CP", i = 1:(x$k - 1))
  cp1 <- data.frame(cp1, "Value" = "CP H1", i = 1:(x$k - 1))
  if ("PP" %in% exclude) {
    pp <- NULL
  } else {
    # predictive probability
    Efficacy <- as.vector(1:(x$k - 1))
    for (i in 1:(x$k - 1)) Efficacy[i] <- gsPP(x = x, i = i, zi = x$upper$bound[i], theta = prior$z, wgts = prior$wgts, r = r, total = TRUE)
    if (x$test.type > 1) {
      Futility <- Efficacy
      for (i in 1:(x$k - 1)) Futility[i] <- gsPP(x = x, i = i, zi = x$lower$bound[i], theta = prior$z, wgts = prior$wgts, r = r, total = TRUE)
    } else {
      Futility <- NULL
    }
    pp <- data.frame(cbind(Efficacy, Futility, i = 1:(x$k - 1)))
    pp$Value <- "PP"
  }
  # start a frame for other statistics
  # z at bounds
  statframe <- data.frame("Value" = "Z", "Efficacy" = x$upper$bound, i = 1:x$k)
  if (x$test.type > 1) statframe <- data.frame(cbind(statframe, "Futility" = x$lower$bound))
  # add nominal p-values at each bound
  tem <- data.frame("Value" = "p (1-sided)", "Efficacy" = stats::pnorm(x$upper$bound, lower.tail = FALSE), i = 1:x$k)
  if (x$test.type == 2) tem <- data.frame(cbind(tem, "Futility" = stats::pnorm(x$lower$bound, lower.tail = TRUE)))
  if (x$test.type > 2) tem <- data.frame(cbind(tem, "Futility" = stats::pnorm(x$lower$bound, lower.tail = FALSE)))
  statframe <- rbind(statframe, tem)
  # delta values at bounds
  tem <- data.frame("Value" = paste("~",deltaname, " at bound", sep = ""), "Efficacy" = deltaefficacy, i = 1:x$k)
  if (x$test.type > 1) tem$Futility <- deltafutility
  statframe <- rbind(statframe, tem)

  # spending
  tem <- data.frame("Value" = "Spending", i = 1:x$k, "Efficacy" = x$upper$spend)
  if (x$test.type > 1) tem$Futility <- x$lower$spend
  statframe <- rbind(statframe, tem)
  # B-values
  tem <- data.frame("Value" = "B-value", i = 1:x$k, "Efficacy" = gsBValue(x = x, z = x$upper$bound, i = 1:x$k))
  if (x$test.type > 1) tem$Futility <- gsBValue(x = x, i = 1:x$k, z = x$lower$bound)
  statframe <- rbind(statframe, tem)
  # put it all together
  statframe <- rbind(statframe, cp, cp1, pp, pframe)
  # exclude rows not wanted
  statframe <- statframe[!(statframe$Value %in% exclude), ]
  # sort by analysis
  statframe <- statframe[order(statframe$i), ]
  # add analysis and timing
  statframe$Analysis <- ""
  aname <- paste("IA ", 1:x$k, ": ", round(100 * x$timing, 0), "%", sep = "")
  aname[x$k] <- "Final"
  statframe[statframe$Value == statframe$Value[1], ]$Analysis <- aname
  # sample size, events or information at analyses
  if (!("gsSurv" %in% class(x))) {
    if (x$n.fix > 1) N <- ceiling(x$n.I) else N <- round(x$n.I, 2)
    if (Nname == "Information") N <- round(x$n.I, 2)
    nstat <- 2
  } else {
    nstat <- 4
    statframe[statframe$Value == statframe$Value[3], ]$Analysis <- paste("Events:", ceiling(x$n.I))
    if (x$ratio == 1) N <- 2 * ceiling(rowSums(x$eNE)) else N <- ceiling(rowSums(x$eNE)) + ceiling(rowSums(x$eNC))
    Time <- round(x$T, tdigits)
    statframe[statframe$Value == statframe$Value[4], ]$Analysis <- paste(timename, ": ", as.character(Time), sep = "")
  }
  statframe[statframe$Value == statframe$Value[2], ]$Analysis <- paste(Nname, ": ", N, sep = "")
  # add POS and predicitive POS, if requested
  if (POS) {
    ppos <- rep("", x$k)
    for (i in 1:(x$k - 1)) ppos[i] <- paste("Post IA POS: ", as.character(round(100 * gsCPOS(i = i, x = x, theta = prior$z, wgts = prior$wgts), 1)), "%", sep = "")
    statframe[statframe$Value == statframe$Value[nstat + 1], ]$Analysis <- ppos
    statframe[nstat + 2, ]$Analysis <- ppos[1]
    statframe[nstat + 1, ]$Analysis <- paste("Trial POS: ", as.character(round(100 * gsPOS(x = x, theta = prior$z, wgts = prior$wgts), 1)), "%", sep = "")
  }
  # add futility column to data frame
  scol <- c(1, 2, if (x$test.type > 1) {
    4
  } else {
    NULL
  })
  rval <- statframe[c(ncol(statframe), scol)]
  rval$Efficacy <- round(rval$Efficacy, digits)
  if (x$test.type > 1) rval$Futility <- round(rval$Futility, digits)
  class(rval) <- c("gsBoundSummary", "data.frame")
  return(rval)
}

# xprint roxy [sinew] ----
#' @export
#' @rdname gsBoundSummary
# xprint function [sinew] ----
xprint <- function(x, include.rownames = FALSE, hline.after = c(-1, which(x$Value == x[1, ]$Value) - 1, nrow(x)), ...) {
  xtable::print.xtable(x, hline.after = hline.after, include.rownames = include.rownames, ...)
}

# print.gsBoundSummary roxy [sinew] ----
#' @export
#' @rdname gsBoundSummary
# print.gsBoundSummary function [sinew] ----
print.gsBoundSummary <- function(x, row.names = FALSE, digits = 4, ...) {
  print.data.frame(x, row.names = row.names, digits = digits, ...)
}

# gsLegendText function [sinew] ----
gsLegendText <- function(test.type) {
  switch(as.character(test.type),
    "1" = c(expression(paste("Reject ", H[0])), "Continue"),
    "2" = c(expression(paste("Reject ", H[0])), "Continue", expression(paste("Reject ", H[0]))),
    c(expression(paste("Reject ", H[0])), "Continue", expression(paste("Reject ", H[1])))
  )
}

# sfprint function [sinew] ----
sfprint <- function(x) {
  # print spending function information
  if (x$name == "OF") {
    cat("O'Brien-Fleming boundary")
  }
  else if (x$name == "Pocock") {
    cat("Pocock boundary")
  }
  else if (x$name == "WT") {
    cat("Wang-Tsiatis boundary with Delta =", x$param)
  }
  else if (x$name == "Truncated") {
    cat(x$param$name, " spending function compressed to ", x$param$trange[1], ", ", x$param$trange[2], sep = "")
    if (!is.null(x$param$parname)) {
      cat(" with", x$param$parname, "=", x$param$param)
    }
  }
  else if (x$name == "Trimmed") {
    cat(x$param$name, " spending function trimmed at ", x$param$trange[1], ", ", x$param$trange[2], sep = "")
    if (!is.null(x$param$parname)) {
      cat(" with", x$param$parname, "=", x$param$param)
    }
  }
  else {
    cat(x$name, "spending function")
    if (!is.null(x$parname) && !is.null(x$param)) {
      cat(" with", x$parname, "=", x$param)
    }
  }
  cat("\n")
}

# summary.spendfn roxy [sinew] ----
#' @export
#' @rdname spendingFunction
#' @aliases summary.spendfn
# summary.spendfn function [sinew] ----
summary.spendfn <- function(object, ...) {
  # print spending function information
  if (object$name == "OF") {
    s <- "O'Brien-Fleming boundary"
  }
  else if (object$name == "Pocock") {
    s <- "Pocock boundary"
  }
  else if (object$name == "WT") {
    s <- paste("Wang-Tsiatis boundary with Delta =", object$param)
  }
  else if (object$name == "Truncated") {
    s <- paste(object$param$name, " spending function compressed to ", object$param$trange[1], ", ", object$param$trange[2], sep = "")
    if (!is.null(object$param$parname)) {
      s <- paste(s, " with", paste(object$param$parname, collapse = " "), "=", paste(object$param$param, collapse = " "))
    }
  }
  else if (object$name == "Trimmed") {
    s <- paste(object$param$name, " spending function trimmed at ", object$param$trange[1], ", ", object$param$trange[2], sep = "")
    if (!is.null(object$param$parname)) {
      s <- paste(s, " with", paste(object$param$parname, collapse = " "), "=", paste(object$param$param, collapse = " "))
    }
  }
  else if (object$name == "Gapped") {
    s <- paste(object$param$name, " spending function no spending in ", object$param$trange[1], ", ", object$param$trange[2], sep = "")
    if (!is.null(object$param$parname)) {
      s <- paste(s, " with", paste(object$param$parname, collapse = " "), "=", paste(object$param$param, collapse = " "))
    }
  } else {
    s <- paste(object$name, "spending function")
    if (!is.null(object$parname) && !is.null(object$param)) {
      s <- paste(s, "with", paste(object$parname, collapse = " "), "=", paste(object$param, collapse = " "))
    }
  }
  return(s)
}

Try the gsDesign package in your browser

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

gsDesign documentation built on Nov. 12, 2023, 9:06 a.m.