R/TwoSample.Boundary.TTFE.R

Defines functions TwoSample.Boundary.TTFE

Documented in TwoSample.Boundary.TTFE

#' @title Function to calculate the sequential rejection boundaries for the time-to-first-event (TTFE) method.
#' @description
#' Constructs two-sided group sequential critical boundaries for the two-sample TTFE log-rank Z-statistics at prespecified
#' calendar times. The function converts stage-wise variance estimates into information fractions, allocates incremental Type
#' I error using a spending function, and solves for the corresponding multivariate normal critical values under the
#' canonical joint distribution assumption. It also returns stage-wise rejection indicators (with early stopping enforced).
#'
#'
#' @param result A list returned by \code{TwoSample.Z.Var.Estimator.Sequential.TTFE()}.
#' @param spend A Type I error spending function. This should be a function of a
#'   single argument \code{t} (information fraction) returning cumulative alpha
#'   spent by information fraction \code{t} (e.g., \code{OBF}).
#' @param calendars Numeric vector of interim analysis calendar times (in years).
#' @param alpha Overall two-sided Type I error rate. Default typically \code{0.05}.
#' @param planned.n Planned total sample size used to define the information scale.
#' @param Iunit Information per subject at the final analysis.
#'
#' @returns
#' A list containing:
#' \itemize{
#'   \item \code{Z.stats}: Stage-wise TTFE Z-statistics.
#'   \item \code{vars}: Stage-wise variance estimates.
#'   \item \code{raw.information}: Stage-wise information fractions.
#'   \item \code{TTFE.bdry}: Two-sided critical boundaries at each analysis stage.
#'   \item \code{TTFE.reject}: Stage-wise rejection indicators (1 = reject at that
#'   stage, 0 otherwise), with later stages set to 0 after the first rejection.
#'   \item \code{nu}: Stage-wise information fractions after handling edge cases
#'   (e.g., reaching 100\% information early).
#'   \item \code{pi}: Incremental Type I error allocated to each stage.
#'   \item \code{total.ns}: Total sample size contributing at each analysis stage
#'   (copied from \code{result}).
#' }
#'
#' @export
#' @importFrom stats qnorm
#'
TwoSample.Boundary.TTFE <- function(result, spend, calendars, alpha, planned.n, Iunit){

  # Extract the results from the input object
  Z.stats <- result$Z.stats
  vars <- result$vars
  total.ns <- result$total.ns

  nu <- c()
  pi <- c()
  TTFE.bdry <- c()

  # Step 1a: Calculate the information fraction for this simulated data
  for (j in 1:(length(calendars))){
    nu[j] = vars[j]/(planned.n*Iunit)
    nu[length(calendars)] = 1
  }

  # Step 1b: Calculate the correlation matrix (We already know that the LogRank test does have)
  raw.information <- nu
  canonical.corr <- diag(3)
  for (p in 1:(3-1)){
    for (q in (p+1):3) {
      canonical.corr[p,q] <- sqrt(raw.information[p]/raw.information[q])
      canonical.corr[q,p] <- canonical.corr[p,q]
    }
  }

  # Step 2: Calculate the allocation of type I error and rejection boundaries
  if (nu[1] >= 1){
    # Reached 100% information at stage 1, in that case, treat the first analysis as the final one.
    # Carried the 100% information fraction forward and set the stages 2 and 3 boundaries to 99
    nu = c(1, 1, 1)
    pi = c(alpha, 0, 0)
    TTFE.bdry = c(qnorm(p = alpha/2, lower.tail = FALSE), 99, 99)

  } else if (nu[2] >= 1){
    # Reached 100% information at stage 2, in that case, treat the 2nd interim analysis as the final one.
    # Carried the 100% information fraction forward and set the stage 3 boundary to 99
    nu = c(nu[1], 1, 1)
    pi = c(spend(nu[1]), alpha - spend(nu[1]), 0)

    TTFE.bdry[1] = qnorm(p = pi[1]/2, lower.tail = FALSE)
    TTFE.bdry[2] = find.c(previous.c = TTFE.bdry[1], pi = pi[2], corr = canonical.corr[c(1:2),c(1:2)])
    TTFE.bdry[3] = 99

  } else if (nu[2]< nu[1]){
    # Observed a smaller information fraction at a later stage
    # in that case, skip stage 2 and move on to the final stage
    # set the stage 2 boundary to 88
    nu = c(nu[1], nu[1], 1)
    pi = c(spend(nu[1]), 0, alpha - spend(nu[1]))

    TTFE.bdry[1] = qnorm(p = pi[1]/2, lower.tail = FALSE)
    TTFE.bdry[2] = 88
    TTFE.bdry[3] = find.c(previous.c = TTFE.bdry[1], pi = pi[3], corr = canonical.corr[c(1,3),c(1,3)])

  } else {
    # Trial proceed as planned
    # Calculate the allocation of Type I errors
    pi[1] <- spend(nu[1])
    for (j in 2:(length(calendars) - 1)){
      pi[j] <- spend(nu[j]) - spend(nu[j-1])
    }
    pi[length(calendars)] <- alpha - spend(nu[length(calendars) - 1])

    #  Calculate the rejection boundary
    TTFE.bdry[1] = qnorm(p = pi[1]/2, lower.tail = FALSE)
    for (j in 2:length(calendars)){
      TTFE.bdry[j] <- find.c(previous.c = TTFE.bdry[1:j-1], pi = pi[j], corr = canonical.corr[1:j,1:j])
    }

  }

  # Step 4: Compare the Z statistics and the rejection boundary

  TTFE.reject <- 1*(Z.stats >= TTFE.bdry|Z.stats <= -TTFE.bdry)


  # Step 5: find the first reject and set the later values to zero
  if (any(TTFE.reject == 1)) {
    first <- which(TTFE.reject == 1)[1]
    if (first < length(TTFE.reject)) {
      TTFE.reject[(first + 1):length(TTFE.reject)] <- 0
    }
  }


  return(list(Z.stats = Z.stats,
              vars = vars,
              raw.information = raw.information,
              TTFE.bdry = TTFE.bdry,
              TTFE.reject = TTFE.reject,
              nu = nu,
              pi = pi,
              total.ns = total.ns))
}

Try the gsMeanFreq package in your browser

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

gsMeanFreq documentation built on Feb. 17, 2026, 1:07 a.m.