Nothing
# Copyright (c) 2022 Merck Sharp & Dohme Corp. a subsidiary of Merck & Co., Inc., Rahway, NJ, USA.
#
# 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/>.
#' @importFrom tibble tibble
#' @importFrom stats qnorm uniroot
NULL
#' Group sequential design computation with non-constant effect and information
#'
#' \code{gs_design_npe()} derives group sequential design size, bounds and boundary crossing probabilities based on proportionate
#' information and effect size at analyses.
#' It allows a non-constant treatment effect over time, but also can be applied for the usual homogeneous effect size designs.
#' It requires treatment effect and proportionate statistical information at each analysis as well as a method of deriving bounds, such as spending.
#' The routine enables two things not available in the gsDesign package: 1) non-constant effect, 2) more flexibility in boundary selection.
#' For many applications, the non-proportional-hazards design function \code{gs_design_nph()} will be used; it calls this function.
#' Initial bound types supported are 1) spending bounds, 2) fixed bounds, and 3) Haybittle-Peto-like bounds.
#' The requirement is to have a boundary update method that can each bound without knowledge of future bounds.
#' As an example, bounds based on conditional power that require knowledge of all future bounds are not supported by this routine;
#' a more limited conditional power method will be demonstrated.
#' Boundary family designs Wang-Tsiatis designs including the original (non-spending-function-based) O'Brien-Fleming and Pocock designs
#' are not supported by \code{gs_power_npe()}.
#' @param theta natural parameter for group sequential design representing expected incremental drift at all analyses;
#' used for power calculation
#' @param theta1 natural parameter used for lower bound spending; if \code{NULL}, this will be set to \code{theta}
#' which yields the usual beta-spending. If set to 0, spending is 2-sided under null hypothesis.
#' @param info proportionate statistical information at all analyses for input \code{theta}
#' @param info0 proportionate statistical information under null hypothesis, if different than alternative;
#' impacts null hypothesis bound calculation
#' @param info1 proportionate statistical information under alternate hypothesis;
#' impacts null hypothesis bound calculation
#' @param alpha One-sided Type I error
#' @param beta Type II error
#' @param binding indicator of whether futility bound is binding; default of FALSE is recommended
#' @param upper function to compute upper bound
#' @param lower function to compare lower bound
#' @param upar parameter to pass to function provided in \code{upper}
#' @param lpar Parameter passed to function provided in \code{lower}
#' @param test_upper indicator of which analyses should include an upper (efficacy) bound; single value of 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
#' @param test_lower indicator of which analyses should include an lower bound; single value of TRUE (default) indicates all analyses;
#' single value 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
#' @param r Integer, at least 2; default of 18 recommended by Jennison and Turnbull
#' @param tol Tolerance parameter for boundary convergence (on Z-scale)
#' @section Specification:
#' \if{latex}{
#' \itemize{
#' \item Validate if input info is a numeric vector or NULL, if non-NULL validate if it
#' is strictly increasing and positive.
#' \item Validate if input info0 is a numeric vector or NULL, if non-NULL validate if it
#' is strictly increasing and positive.
#' \item Validate if input info1 is a numeric vector or NULL, if non-NULL validate if it
#' is strictly increasing and positive.
#' \item Validate if input theta is a real vector and has the same length as info.
#' \item Validate if input theta1 is a real vector and has the same length as info.
#' \item Validate if input test_upper and test_lower are logical and have the same length as info.
#' \item Validate if input test_upper value is TRUE.
#' \item Validate if input alpha and beta are positive and of length one.
#' \item Validate if input alpha and beta are from the unit interval and alpha is smaller than beta.
#' \item Initialize bounds, numerical integration grids, boundary crossing probabilities.
#' \item Compute fixed sample size for desired power and Type I error.
#' \item Find an interval for information inflation to give correct power using \code{gs_power_npe()}.
#' \item
#' \item If there is no interim analysis, return a tibble including Analysis time, upper bound, Z-value,
#' Probability of crossing bound, theta, info0 and info1.
#' \item If the desing is a group sequential design, return a tibble of Analysis,
#' Bound, Z, Probability, theta, info, info0.
#' }
#' }
#' \if{html}{The contents of this section are shown in PDF user manual only.}
#'
#' @return a \code{tibble} with columns Analysis, Bound, Z, Probability, theta, info, info0
#' @details The inputs \code{info} and \code{info0} should be vectors of the same length with increasing positive numbers.
#' The design returned will change these by some constant scale factor to ensure the design has power \code{1 - beta}.
#' The bound specifications in \code{upper, lower, upar, lpar} will be used to ensure Type I error and other boundary properties are as specified.
#' @author Keaven Anderson \email{keaven_anderson@@merck.com}
#'
#' @noRd
#'
#' @examples
#'
#' library(gsDesign)
#' library(gsDesign2)
#' library(dplyr)
#'
#' # Single analysis
#' # Lachin book p 71 difference of proportions example
#' pc <- .28 # Control response rate
#' pe <- .40 # Experimental response rate
#' p0 <- (pc + pe) / 2 # Ave response rate under H0
#' # Information per increment of 1 in sample size
#' info0 <- 1 / (p0 * (1 - p0) * 4)
#' info1 <- 1 / (pc * (1 - pc) * 2 + pe * (1 - pe) * 2)
#' # Result should round up to next even number = 652
#' # Divide information needed under H1 by information per patient added
#' gsDesign2:::gs_design_npe_(theta = pe - pc, info = info1, info0 = info0)$info[1] / info1
#'
#' # Fixed bound
#' design <- gsDesign2:::gs_design_npe_(
#' theta = c(.1, .2, .3), info = (1:3) * 80,
#' info0 = (1:3) * 80, info1 = (1:3) * 80,
#' upper = gs_b, upar = gsDesign::gsDesign(k = 3, sfu = gsDesign::sfLDOF)$upper$bound,
#' lower = gs_b, lpar = c(-1, 0, 0)
#' )
#' design
#'
#' # Same fixed bounds, null hypothesis
#' gsDesign2:::gs_design_npe_(
#' theta = rep(0, 3), info = design$info0[1:3],
#' upar = design$Z[1:3], lpar = design$Z[4:6]
#' )
#'
#' # Same upper bound; this represents non-binding Type I error and will total 0.025
#' gsDesign2:::gs_design_npe_(
#' theta = rep(0, 3), info = design$info0[1:3],
#' upar = design$Z[1:3], lpar = rep(-Inf, 3)
#' ) %>%
#' dplyr::filter(Bound == "Upper")
#'
#' # Spending bound examples
#'
#' # Design with futility only at analysis 1; efficacy only at analyses 2, 3
#' # Spending bound for efficacy; fixed bound for futility
#' # NOTE: test_upper and test_lower DO NOT WORK with gs_b; must explicitly make bounds infinite
#' # test_upper and test_lower DO WORK with gs_spending_bound
#' design <- gsDesign2:::gs_design_npe_(
#' theta = c(.1, .2, .3), info = (1:3) * 40, info0 = (1:3) * 40,
#' upper = gs_spending_bound,
#' upar = list(
#' sf = gsDesign::sfLDOF, total_spend = 0.025,
#' param = NULL, timing = NULL
#' ),
#' lower = gs_b, lpar = c(-1, -Inf, -Inf),
#' test_upper = c(FALSE, TRUE, TRUE)
#' )
#' design
#'
#' # Spending function bounds
#' # 2-sided asymmetric bounds
#' # Lower spending based on non-zero effect
#' gsDesign2:::gs_design_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 = -1, timing = NULL
#' )
#' )
#'
#' # Two-sided symmetric spend, O'Brien-Fleming spending
#' # Typically, 2-sided bounds are binding
#' xx <- gsDesign2:::gs_design_npe_(
#' theta = c(.1, .2, .3), theta1 = rep(0, 3), info = (1:3) * 40,
#' binding = TRUE,
#' 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::sfLDOF, total_spend = 0.025,
#' param = NULL, timing = NULL
#' )
#' )
#' xx
#'
#' # Re-use these bounds under alternate hypothesis
#' # Always use binding = TRUE for power calculations
#' upar <- (xx %>% dplyr::filter(Bound == "Upper"))$Z
#' gsDesign2:::gs_design_npe_(
#' theta = c(.1, .2, .3), info = (1:3) * 40,
#' binding = TRUE,
#' upar = upar,
#' lpar = -upar
#' )
#'
gs_design_npe_ <- function(theta = .1, theta1 = NULL, info = 1, info0 = NULL, info1 = NULL,
alpha = 0.025, beta = .1, binding = FALSE,
upper = gs_b, lower = gs_b, upar = qnorm(.975), lpar = -Inf,
test_upper = TRUE, test_lower = TRUE,
r = 18, tol = 1e-6) {
#######################################################################################
# WRITE INPUT CHECK TESTS AND RETURN APPROPRIATE ERROR MESSAGES
# info should be a scalar or vector of positive increasing values
# info0, info1 should be NULL or of the same form as info
# theta should be a scalar or vector of real values; if vector, same length as info
# theta0, theta1 should be NULL or same form and length as theta
# test_upper and test_lower should be logical scalar or vector; if vector same length as info
# alpha and beta should be scalars with 0 < alpha < 1 - beta < 1
# CHECK STATISTICAL INFORMATION PARAMETERS: info, info0, info1
if (!is.vector(info, mode = "numeric")) stop("gs_design_npe(): info must be specified numeric vector")
K <- length(info)
if (is.null(info0)) info0 <- info
if (is.null(info1)) info1 <- info
if (!is.vector(info0, mode = "numeric")) stop("gs_design_npe(): info0 must be specified numeric vector or NULL")
if (!is.vector(info1, mode = "numeric")) stop("gs_design_npe(): info1 must be specified numeric vector or NULL")
if (length(info1) != length(info) || length(info0) != length(info)) stop("gs_design_npe(): length of info, info0, info1 must be the same")
if (min(info - dplyr::lag(info, default = 0) <= 0)) stop("gs_design_npe(): info much be strictly increasing and positive")
if (min(info0 - dplyr::lag(info0, default = 0) <= 0)) stop("gs_design_npe(): info0 much be NULL or strictly increasing and positive")
if (min(info1 - dplyr::lag(info1, default = 0) <= 0)) stop("gs_design_npe(): info1 much be NULL or strictly increasing and positive")
# CHECK TREATMENT EFFECT PARAMETERS: theta, theta0, theta1
if (!is.vector(theta, mode = "numeric")) stop("gs_design_npe(): theta must be a real vector")
if (length(theta) == 1 && K > 1) theta <- rep(theta, K)
if (length(theta) != K) stop("gs_design_npe(): if length(theta) > 1, must be same as info")
if (theta[K] <= 0) stop("gs_design_npe(): final effect size must be > 0")
if (is.null(theta1)) {
theta1 <- theta
} else if (length(theta1) == 1) theta1 <- rep(theta1, K)
if (!is.vector(theta1, mode = "numeric")) stop("gs_design_npe(): theta1 must be a real vector")
if (length(theta1) != K) stop("gs_design_npe(): if length(theta1) > 1, must be same as info")
# CHECK CORRECT SPEC OF test_upper and test_lower
if (length(test_upper) == 1 && K > 1) test_upper <- rep(test_upper, K)
if (length(test_lower) == 1 && K > 1) test_lower <- rep(test_lower, K)
## Check test_upper and test_lower are logical and correct length
if (!is.vector(test_upper, mode = "logical") || !is.vector(test_lower, mode = "logical")) {
stop("gs_design_npe(): test_upper and test_lower must be logical")
}
if (!(length(test_upper) == 1 || length(test_upper) == K)) {
stop("gs_design_npe(): test_upper must be length 1 or same length as info")
}
if (!(length(test_lower) == 1 || length(test_lower) == K)) {
stop("gs_design_npe(): test_lower must be length 1 or same length as info")
}
## Check that final test_upper value is TRUE
if (!dplyr::last(test_upper)) stop("gs_design_npe(): last value of test_upper must be TRUE")
## Check alpha and beta numeric, scalar, 0 < alpha < 1 - beta
if (!is.numeric(alpha)) stop("gs_design_npe(): alpha must be numeric")
if (!is.numeric(beta)) stop("gs_design_npe(): beta must be numeric")
if (length(alpha) != 1 || length(beta) != 1) stop("gs_design_npe(): alpha and beta must be length 1")
if (alpha <= 0 || 1 - beta <= alpha || beta <= 0) stop("gs_design_npe(): must have 0 < alpha < 1 - beta < 1")
## END OF INPUT CHECKS ############################################################################
# Initialize bounds, numerical integration grids, boundary crossing probabilities
a <- rep(-Inf, K)
b <- rep(Inf, K)
hgm1_0 <- NULL
hgm1_1 <- NULL
upperProb <- rep(NA, K)
lowerProb <- rep(NA, K)
## Compute fixed sample size for desired power and Type I error.
minx <- ((qnorm(alpha) / sqrt(info0[K]) + qnorm(beta) / sqrt(info[K])) / theta[K])^2
## For a fixed design, this is all you need.
if (K == 1) {
return(tibble::tibble(
Analysis = 1,
Bound = "Upper",
Z = qnorm(1 - alpha),
Probability = 1 - beta,
theta = theta,
info = info * minx,
info0 = info0 * minx
))
}
## Find an interval for information inflation to give correct power
minpwr <- gs_power_npe_(
theta = theta, theta1 = theta1,
info = info * minx, info1 = info * minx, info0 = info0 * minx,
binding = binding,
upper = upper, lower = lower, upar = upar, lpar = lpar,
test_upper = test_upper, test_lower = test_lower,
r = r, tol = tol
)$Probability[K]
##### FOLLOWING IS PAINFUL AND SHOULD NEVER BE NEEDED
##### BUT IF IT IS NEEDED, IT TELLS YOU WHAT WENT WRONG!
##### NEED TO BRACKET TARGETED POWER BEFORE ROOT FINDING
## Ensure minx gives power < 1 - beta and maxx gives power > 1 - beta
if (minpwr < 1 - beta) {
maxx <- 1.05 * minx
## Ensure maxx is sufficient information inflation to overpower
err <- 1
for (i in 1:10) {
maxpwr <- gs_power_npe_(
theta = theta, theta1 = theta1,
info = info * maxx, info1 = info * maxx, info0 = info0 * maxx,
binding = binding,
upper = upper, lower = lower, upar = upar, lpar = lpar,
test_upper = test_upper, test_lower = test_lower,
r = r, tol = tol
)$Probability[K]
if (1 - beta > maxpwr) {
minx <- maxx
maxx <- 1.05 * maxx
} else {
err <- 0
break
}
}
if (err) stop("gs_design_npe: could not inflate information to bracket power before root finding")
} else {
maxx <- minx
minx <- maxx / 1.05
err <- 1
for (i in 1:10) {
if (1 - beta < gs_power_npe_(
theta = theta, theta1 = theta1,
info = info * minx, info1 = info1 * minx, info0 = info0 * minx,
binding = binding,
upper = upper, lower = lower, upar = upar, lpar = lpar,
test_upper = test_upper, test_lower = test_lower,
r = r, tol = tol
)$Probability[K]
) {
maxx <- minx
minx <- minx / 1.05
} else {
err <- 0
break
}
}
if (err) stop("gs_design_npe: could not deflate information to bracket targeted power before root finding")
}
#### EITHER TARGETED POWER NOW BRACKETED OR ERROR MESSAGE HAS BEEN RETURNED
#### AND WE CAN ACTUALLY GO ON TO FIND THE ROOT
## Use root finding with the above function to find needed sample size inflation
# Now we can solve for the inflation factor for the enrollment rate to achieve the desired power
res <- try(
uniroot(errbeta_,
lower = minx, upper = maxx,
theta = theta, theta1 = theta1, K = K, beta = beta,
info = info, info1 = info1, info0 = info0, binding = binding,
Zupper = upper, Zlower = lower, upar = upar, lpar = lpar,
test_upper = test_upper, test_lower = test_lower,
r = r, tol = tol
)
)
if (inherits(res, "try-error")) {
stop("gs_design_npe: Sample size solution not found")
}
## Update targeted info, info0 based on inflation factor and return a tibble with
## bounds, targeted information, and boundary crossing probabilities at each analysis
return(gs_power_npe_(
theta = theta, theta1 = theta1,
info = info * res$root, info1 = info1 * res$root, info0 = info0 * res$root,
binding = binding,
upper = upper, lower = lower, upar = upar, lpar = lpar,
test_upper = test_upper, test_lower = test_lower,
r = r, tol = tol
))
}
## Create a function that uses gs_power_npe to compute difference from targeted power
## for a given sample size inflation factor
errbeta_ <- function(x = 1, K = 1, beta = .1, theta = .1, theta1 = .1, info = 1, info1 = 1, info0 = 1, binding = FALSE,
Zupper = gs_b, Zlower = gs_b, upar = qnorm(.975), lpar = -Inf,
test_upper = TRUE, test_lower = TRUE,
r = 18, tol = 1e-6) {
return(1 - beta -
gs_power_npe_(
theta = theta, theta1 = theta1,
info = info * x, info1 = info1 * x, info0 = info0 * x, binding = binding,
upper = Zupper, lower = Zlower, upar = upar, lpar = lpar,
test_upper = test_upper, test_lower = test_lower,
r = r, tol = tol
)$Probability[K])
}
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.