diff --git a/R/boundsPostprob.R b/R/boundsPostprob.R index 09822eda..a35fef8d 100644 --- a/R/boundsPostprob.R +++ b/R/boundsPostprob.R @@ -1,72 +1,78 @@ #' Decision cutpoints for boundary (based on posterior probability) #' #' This function is used to identify the efficacy and futility -#' boundaries based on posterior probabilities, i.e.: -#' Efficacy boundary: find minimum x (xU) where Pr(P>p0|x,n,a,b) >= tU and -#' Futility boundary: find maximum x (xL) where Pr(P>p1|x,n,a,b) <= tL +#' boundaries based on the following rules: +#' Efficacy boundary: find minimum x (xU) where Pr(RR > p1 |x, n, a, b) >= tU and +#' Futility boundary: find maximum x (xL) where Pr(RR < p0 | x, n, a, b) >= tL #' -#' @param nvec a vector of number of patients -#' @param p0 the efficacy threshold parameter in the postprob function -#' @param p1 the futility threshold parameter in the postprob function -#' (default = p0) -#' @param tL futility boundary probability threshold -#' @param tU efficacy boundary probability threshold -#' @param a the alpha parameter of the beta prior of treatment group -#' @param b the beta parameter of the beta prior of treatment group -#' @return A matrix where for each sample size in \code{nvec}, this function -#' returns the maximum number of responses that meet the futility -#' threshold (xL), its corresponding response rate (pL), posterior probability -#' (postL), upper bound of one sided 95% CI for the response rate based on an -#' exact binomial test (UciL), and the same boundary parameters for efficacy: -#' the minimal number of responses that meet the efficacy threshold (xU), -#' the corresponding response rate (pU), posterior probability (postU) and -#' the lower bound of one sided 95% CI for the response rate based on exact -#' binomial test (LciU). -#' -#' @importFrom stats binom.test +#' @inheritParams postprob +#' @inheritParams ocPostprob +#' @typed looks : numeric +#' A vector of number of patients in each look. +#' @return A matrix for each same size in `looks`. For each sample size, the following is returned: +#' - `xL` : the maximum number of responses that meet the futility threshold. +#' - `pL` : response rate corresponding to `xL`. +#' - `postL`: posterior probability corresponding to `xL`, i.e. Pr(RR < p0 | xL, n, a, b). +#' - `pL_upper_ci` : upper bound of one sided 95% CI for the response rate `pL` based on an +#' exact binomial test. +#' - `xU` : the minimal number of responses that meet the efficacy threshold. +#' - `pU` : response rate corresponding to `xU`. +#' - `postU` : posterior probability corresponding to `xU`, i.e. Pr(RR > p1 |xU, n, a, b). +#' - `pU_lower_ci` : lower bound of one sided 95% CI for the response rate `pU` based on exact +#' binomial test. #' #' @example examples/boundsPostprob.R #' @export -#' @keywords graphics -boundsPostprob <- function(nvec, p0, p1 = p0, tL, tU, a, b) { - z <- matrix(NA, length(nvec), 6) - dimnames(z) <- list(nvec, c( - "xL", "pL", "postL", - "xU", "pU", "postU" - )) +boundsPostprob <- function(looks, p0, p1 = p0, tL, tU, parE = c(1, 1), weights) { + assert_numeric(looks) + assert_number(p0, lower = 0, upper = 1) + assert_number(p1, lower = 0, upper = 1) + assert_number(tL, lower = 0, upper = 1) + assert_number(tU, lower = 0, upper = 1) + assert_numeric(parE, min.len = 2, any.missing = FALSE) + z <- matrix(NA, nrow = length(looks), ncol = 8) znames <- c( - "xL", "pL", "postL", "UciL", - "xU", "pU", "postU", "LciU" + "xL", "pL", "postL", "pL_upper_ci", + "xU", "pU", "postU", "pU_lower_ci" ) - z <- matrix(NA, length(nvec), length(znames)) - dimnames(z) <- list(nvec, znames) + dimnames(z) <- list(looks, znames) k <- 0 - for (n in nvec) { + parE <- t(parE) + if (missing(weights)) { + weights <- rep(1, nrow(parE)) + } + assert_numeric(weights, min.len = 0, len = nrow(par), finite = TRUE) + for (n in looks) { k <- k + 1 # initialize so will return NA if 0 or n in "continue" region xL <- NA xU <- NA for (x in 0:n) { - postp <- postprob(x, n, p1, parE = c(a, b)) - if (postp <= tL) { + postp_fut <- 1 - postprob(x, n, p0, parE, weights) # futility look + if (postp_fut >= tL) { # Rule is P(RR < p0) > tL + postL <- postp_fut xL <- x } - if (p0 != p1) { - postp <- postprob(x, n, p0, parE = c(a, b)) - } - if (postp >= tU) { + postp_eff <- postprob(x, n, p1, parE, weights) # efficacy look + if (postp_eff >= tU) { # Rule is P(RR > p1) > tU + postU <- postp_eff xU <- x - # done: leave innermost for loop break } } - # calculate posterior probabilities at boundaries - postL <- postprob(xL, n, p1, parE = c(a, b)) - postU <- postprob(xU, n, p0, parE = c(a, b)) # calculate lower CI at boundaries - UciL <- ifelse(!is.na(xL), stats::binom.test(xL, n, alt = "less")$conf.int[2], NA) - LciU <- ifelse(!is.na(xU), stats::binom.test(xU, n, alt = "greater")$conf.int[1], NA) - z[k, ] <- c(xL, xL / n, postL, UciL, xU, xU / n, postU, LciU) + pL_upper_ci <- ifelse(!is.na(xL), stats::binom.test(xL, n, alt = "less")$conf.int[2], NA) + pU_lower_ci <- ifelse(!is.na(xU), stats::binom.test(xU, n, alt = "greater")$conf.int[1], NA) + z[k, ] <- c( + xL, + xL / n, + postL, + pL_upper_ci, + xU, + xU / n, + postU, + pU_lower_ci + ) } - return(round(data.frame(nvec, z), 4)) + round(data.frame(looks, z), 4) } diff --git a/R/ocPostprob.R b/R/ocPostprob.R index 060a0b1c..f7dcd334 100644 --- a/R/ocPostprob.R +++ b/R/ocPostprob.R @@ -187,11 +187,11 @@ h_get_oc <- function(all_sizes, Nmax, decision) { #' #' Stop criteria for Efficacy : #' -#' `Pr(truep > p1) > tU` +#' `Pr(RR > p1) > tU` #' #' Stop criteria for Futility : #' -#' `Pr(truep < p0) > tL` +#' `Pr(RR < p0) > tL` #' #' Resulting operating characteristics include the following: #' diff --git a/examples/boundsPostprob.R b/examples/boundsPostprob.R index 97491a9f..694942d0 100644 --- a/examples/boundsPostprob.R +++ b/examples/boundsPostprob.R @@ -1,10 +1,26 @@ -## 40 pts trial with interim looks after each 10 pts., -## efficacy decision if more than 90% probability to be above 20% ORR, -## futility decision if less than 10% probability to be above 20% ORR, -## with uniform prior (i.e. beta(1, 1)) on the ORR: +# 40 pts trial with interim looks after each 10 pts., +# Efficacy decision if more than 80% probability to be above 20% ORR, +# Futility decision if more than 60% probability to be below 15% ORR, +# with uniform prior (i.e. beta(1, 1)) on the ORR: boundsPostprob( - nvec = c(10, 20, 30, 40), p0 = 0.20, - tL = 0.10, tU = 0.90, a = 1, b = 1 + looks = c(10, 20, 30, 40), + p0 = 0.15, + p1 = 0.20, + tL = 0.60, + tU = 0.80, + parE = c(1, 1) +) + +# 40 pts trial with interim looks at 7 and 20 pts. +# Efficacy decision if more than 80% probability to be above 20% ORR, +# Futility decision if more than 60% probability to be below 15% ORR, +# with mixed prior and weights: +boundsPostprob( + looks = c(7, 20, 40), + p0 = 0.15, + p1 = 0.20, + tL = 0.60, + tU = 0.80, + parE = rbind(c(1, 19), c(2, 10)), + weights = c(0.2, 0.8) ) -## From this we see e.g. that at the third IA at 30 pts, we would stop for futility -## if 5 or less patients responded, and for efficacy if 9 or more pts responded. diff --git a/examples/ocPredprob.r b/examples/ocPredprob.R similarity index 100% rename from examples/ocPredprob.r rename to examples/ocPredprob.R diff --git a/examples/ocPredprobDist.R b/examples/ocPredprobDist.R index e925a68a..4bc2715e 100644 --- a/examples/ocPredprobDist.R +++ b/examples/ocPredprobDist.R @@ -74,10 +74,14 @@ result$oc # True response rate or truep of the treatment group = 40% # Desired difference to Standard of Care for Efficacy and Futility = 50% # Delta calculation is absolute case. The following are the Final Stop rules respectively : -# - Final look for Efficacy: Pr( response rate + deltaE > 25% ) > 60% or P(response rate + deltaE > p0) > tT -# - Final look for Futility: Pr( response rate + deltaF < 25% ) < 60% or P(response rate + deltaF > p0) < tT -# - Interim look for Efficacy: Pr( success at final ) > 80% or P(success at final) > phiU -# - Interim look for Futility: Pr( failure at final ) < 20% or P(success at final) < phiL +# - Final look for Efficacy: Pr( response rate + deltaE > 25% ) > 60% or +# P(response rate + deltaE > p0) > tT +# - Final look for Futility: Pr( response rate + deltaF < 25% ) < 60% or +# P(response rate + deltaF > p0) < tT +# - Interim look for Efficacy: Pr( success at final ) > 80% or +# P(success at final) > phiU +# - Interim look for Futility: Pr( failure at final ) < 20% or +# P(success at final) < phiL # We assume a prior of treatment arm parE = Beta(1,1), unless otherwise indicated. set.seed(20) diff --git a/examples/plotBounds.R b/examples/plotBounds.R index f4ba8525..c28479ea 100644 --- a/examples/plotBounds.R +++ b/examples/plotBounds.R @@ -7,10 +7,3 @@ plotBounds(boundsPredprob( nvec = c(10, 20, 30, 40), p = 0.20, tT = 0.80, phiL = 0.10, phiU = 0.90, a = 1, b = 1 ), yt = "p") -plotBounds( - boundsPostprob( - nvec = c(10, 20, 30, 40), p0 = 0.20, - tL = 0.10, tU = 0.90, a = 1, b = 1 - ), - yt = "p", add = TRUE -) diff --git a/man/boundsPostprob.Rd b/man/boundsPostprob.Rd index 1cb2ae60..b21240cd 100644 --- a/man/boundsPostprob.Rd +++ b/man/boundsPostprob.Rd @@ -4,51 +4,70 @@ \alias{boundsPostprob} \title{Decision cutpoints for boundary (based on posterior probability)} \usage{ -boundsPostprob(nvec, p0, p1 = p0, tL, tU, a, b) +boundsPostprob(looks, p0, p1 = p0, tL, tU, parE = c(1, 1), weights) } \arguments{ -\item{nvec}{a vector of number of patients} +\item{looks}{(\code{numeric}):\cr A vector of number of patients in each look.} -\item{p0}{the efficacy threshold parameter in the postprob function} +\item{p0}{(\code{number}):\cr lower Futility threshold of response rate.} -\item{p1}{the futility threshold parameter in the postprob function -(default = p0)} +\item{p1}{(\code{number}):\cr upper Efficacy threshold of response rate.} -\item{tL}{futility boundary probability threshold} +\item{tL}{(\code{number}):\cr posterior probability threshold for being below \code{p0}.} -\item{tU}{efficacy boundary probability threshold} +\item{tU}{(\code{number}):\cr posterior probability threshold for being above \code{p1}.} -\item{a}{the alpha parameter of the beta prior of treatment group} +\item{parE}{(\code{matrix}):\cr the beta parameters matrix, with \code{K} rows and 2 columns, +corresponding to the beta parameters of the \code{K} components.} -\item{b}{the beta parameter of the beta prior of treatment group} +\item{weights}{(\code{vector}):\cr The mixture weights of the beta mixture prior.} } \value{ -A matrix where for each sample size in \code{nvec}, this function -returns the maximum number of responses that meet the futility -threshold (xL), its corresponding response rate (pL), posterior probability -(postL), upper bound of one sided 95\% CI for the response rate based on an -exact binomial test (UciL), and the same boundary parameters for efficacy: -the minimal number of responses that meet the efficacy threshold (xU), -the corresponding response rate (pU), posterior probability (postU) and -the lower bound of one sided 95\% CI for the response rate based on exact -binomial test (LciU). +A matrix for each same size in \code{looks}. For each sample size, the following is returned: +\itemize{ +\item \code{xL} : the maximum number of responses that meet the futility threshold. +\item \code{pL} : response rate corresponding to \code{xL}. +\item \code{postL}: posterior probability corresponding to \code{xL}, i.e. Pr(RR < p0 | xL, n, a, b). +\item \code{pL_upper_ci} : upper bound of one sided 95\% CI for the response rate \code{pL} based on an +exact binomial test. +\item \code{xU} : the minimal number of responses that meet the efficacy threshold. +\item \code{pU} : response rate corresponding to \code{xU}. +\item \code{postU} : posterior probability corresponding to \code{xU}, i.e. Pr(RR > p1 |xU, n, a, b). +\item \code{pU_lower_ci} : lower bound of one sided 95\% CI for the response rate \code{pU} based on exact +binomial test. +} } \description{ This function is used to identify the efficacy and futility -boundaries based on posterior probabilities, i.e.: -Efficacy boundary: find minimum x (xU) where Pr(P>p0|x,n,a,b) >= tU and -Futility boundary: find maximum x (xL) where Pr(P>p1|x,n,a,b) <= tL +boundaries based on the following rules: +Efficacy boundary: find minimum x (xU) where Pr(RR > p1 |x, n, a, b) >= tU and +Futility boundary: find maximum x (xL) where Pr(RR < p0 | x, n, a, b) >= tL } \examples{ -## 40 pts trial with interim looks after each 10 pts., -## efficacy decision if more than 90\% probability to be above 20\% ORR, -## futility decision if less than 10\% probability to be above 20\% ORR, -## with uniform prior (i.e. beta(1, 1)) on the ORR: +# 40 pts trial with interim looks after each 10 pts., +# Efficacy decision if more than 80\% probability to be above 20\% ORR, +# Futility decision if more than 60\% probability to be below 15\% ORR, +# with uniform prior (i.e. beta(1, 1)) on the ORR: +boundsPostprob( + looks = c(10, 20, 30, 40), + p0 = 0.15, + p1 = 0.20, + tL = 0.60, + tU = 0.80, + parE = c(1, 1) +) + +# 40 pts trial with interim looks at 7 and 20 pts. +# Efficacy decision if more than 80\% probability to be above 20\% ORR, +# Futility decision if more than 60\% probability to be below 15\% ORR, +# with mixed prior and weights: boundsPostprob( - nvec = c(10, 20, 30, 40), p0 = 0.20, - tL = 0.10, tU = 0.90, a = 1, b = 1 + looks = c(7, 20, 40), + p0 = 0.15, + p1 = 0.20, + tL = 0.60, + tU = 0.80, + parE = rbind(c(1, 19), c(2, 10)), + weights = c(0.2, 0.8) ) -## From this we see e.g. that at the third IA at 30 pts, we would stop for futility -## if 5 or less patients responded, and for efficacy if 9 or more pts responded. } -\keyword{graphics} diff --git a/man/ocPostprob.Rd b/man/ocPostprob.Rd index 23095a77..950cde0a 100644 --- a/man/ocPostprob.Rd +++ b/man/ocPostprob.Rd @@ -63,11 +63,11 @@ probability to be below \code{p0} is larger than \code{tL}: Stop criteria for Efficacy : -\code{Pr(truep > p1) > tU} +\code{Pr(RR > p1) > tU} Stop criteria for Futility : -\code{Pr(truep < p0) > tL} +\code{Pr(RR < p0) > tL} Resulting operating characteristics include the following: \itemize{ diff --git a/man/ocPredprob.Rd b/man/ocPredprob.Rd index 25c8d9e2..63e17a09 100644 --- a/man/ocPredprob.Rd +++ b/man/ocPredprob.Rd @@ -117,3 +117,124 @@ The criteria for Decision 2 for Futility looks are : } } } +\examples{ +# Here we illustrate an example for Decision 1 with the following assumptions : +# True response rate or truep of the treatment group = 40\% +# The following are the Final Stop rules respectively : +# - Final look for Efficacy: Pr( response rate > 25\% ) > 60\% or P(response rate > p0) > tT +# - Final look for Futility: Pr( response rate < 25\% ) < 60\% or P(response rate > p0) < tT +# - Interim look for Efficacy: Pr( success at final ) > 80\% or P(success at final) > phiU +# - Interim look for Futility: Pr( failure at final ) < 20\% or P(success at final) < phiL +# We assume a prior of treatment arm parE = Beta(1,1), unless otherwise indicated. + +# Decision 1 with no wiggle. +set.seed(20) +result <- ocPredprob( + nnE = c(10, 20), + truep = 0.4, + p0 = 0.25, + tT = 0.6, + phiL = 0.2, + phiU = 0.8, + parE = c(1, 1), + sim = 50, + wiggle = FALSE, + decision1 = TRUE +) +result$oc + +# Decision 1 with wiggle. +result <- ocPredprob( + nnE = c(10, 20), + truep = 0.4, + p0 = 0.25, + tT = 0.6, + phiL = 0.2, + phiU = 0.8, + parE = c(1, 1), + sim = 50, + wiggle = TRUE, + nnF = c(10, 20), + decision1 = TRUE +) +result$oc + +# Decision 1 with separate Futility and Efficacy looks at interim and final without wiggle. +result <- ocPredprob( + nnE = c(10, 25, 30), + truep = 0.4, + p0 = 0.25, + p1 = 0.2, + tT = 0.6, + phiL = 0.2, + phiU = 0.8, + parE = c(1, 1), + sim = 50, + wiggle = FALSE, + nnF = c(10, 15, 20), + decision1 = TRUE +) +result$oc + +# Decision 1 with separate Futility and Efficacy looks at interim and final with wiggle. +result <- ocPredprob( + nnE = c(10, 25, 30), + truep = 0.4, + p0 = 0.25, + p1 = 0.2, + tT = 0.6, + phiL = 0.2, + phiU = 0.8, + parE = c(1, 1), + sim = 50, + wiggle = TRUE, + nnF = c(10, 15, 20), + decision1 = TRUE +) +result$oc + +# Here we illustrate an example for Decision 2 with the following assumptions : +# True response rate or truep of the treatment group = 60\% +# The following are the Final Stop rules respectively : +# - Final look for Efficacy: Pr( response rate > 25\% ) > 60\% or P(response rate > p0) > tT +# - Final look for Futility: Pr( response rate < 25\% ) < 60\% or P(response rate < p1) > tF +# - Interim look for Efficacy: Pr( success at final ) > 80\% or P(success at final) > phiU +# - Interim look for Futility: Pr( failure at final ) > 80\% or P(failure at final) > phiFu +# We assume a prior of treatment arm parE = Beta(1,1), unless otherwise indicated. + +# Decision 2 without wiggle. +result <- ocPredprob( + nnE = c(10, 20), + truep = 0.6, + p0 = 0.25, + p1 = 0.25, + tT = 0.6, + tF = 0.6, + phiU = 0.8, + phiFu = 0.8, + parE = c(1, 1), + sim = 50, + wiggle = FALSE, + nnF = c(10, 20), + decision1 = FALSE +) +result$oc + +# Decision 2 with wiggle and with Futility only at final with non-uniform beta prior parE. +result <- ocPredprob( + nnE = c(10, 25, 30), + truep = 0.6, + p0 = 0.25, + p1 = 0.25, + tT = 0.6, + tF = 0.6, + phiL = 0.8, + phiU = 0.8, + parE = c(11, 19), + sim = 50, + wiggle = TRUE, + nnF = 30, + decision1 = FALSE +) +result$oc +} diff --git a/man/ocPredprobDist.Rd b/man/ocPredprobDist.Rd index 2c1d66fc..3a570cfa 100644 --- a/man/ocPredprobDist.Rd +++ b/man/ocPredprobDist.Rd @@ -208,10 +208,14 @@ result$oc # True response rate or truep of the treatment group = 40\% # Desired difference to Standard of Care for Efficacy and Futility = 50\% # Delta calculation is absolute case. The following are the Final Stop rules respectively : -# - Final look for Efficacy: Pr( response rate + deltaE > 25\% ) > 60\% or P(response rate + deltaE > p0) > tT -# - Final look for Futility: Pr( response rate + deltaF < 25\% ) < 60\% or P(response rate + deltaF > p0) < tT -# - Interim look for Efficacy: Pr( success at final ) > 80\% or P(success at final) > phiU -# - Interim look for Futility: Pr( failure at final ) < 20\% or P(success at final) < phiL +# - Final look for Efficacy: Pr( response rate + deltaE > 25\% ) > 60\% or +# P(response rate + deltaE > p0) > tT +# - Final look for Futility: Pr( response rate + deltaF < 25\% ) < 60\% or +# P(response rate + deltaF > p0) < tT +# - Interim look for Efficacy: Pr( success at final ) > 80\% or +# P(success at final) > phiU +# - Interim look for Futility: Pr( failure at final ) < 20\% or +# P(success at final) < phiL # We assume a prior of treatment arm parE = Beta(1,1), unless otherwise indicated. set.seed(20) diff --git a/man/plotBounds.Rd b/man/plotBounds.Rd index 5d24d8c6..0ee0034d 100644 --- a/man/plotBounds.Rd +++ b/man/plotBounds.Rd @@ -64,12 +64,5 @@ plotBounds(boundsPredprob( nvec = c(10, 20, 30, 40), p = 0.20, tT = 0.80, phiL = 0.10, phiU = 0.90, a = 1, b = 1 ), yt = "p") -plotBounds( - boundsPostprob( - nvec = c(10, 20, 30, 40), p0 = 0.20, - tL = 0.10, tU = 0.90, a = 1, b = 1 - ), - yt = "p", add = TRUE -) } \keyword{graphics} diff --git a/tests/testthat/test-boundsPostProb.R b/tests/testthat/test-boundsPostProb.R new file mode 100644 index 00000000..225f7441 --- /dev/null +++ b/tests/testthat/test-boundsPostProb.R @@ -0,0 +1,61 @@ +# boundsPostProb ---- +test_that("boundsPostProb gives correct result", { + result <- boundsPostprob( + looks = c(10, 20, 30, 40), + p0 = 0.15, + p1 = 0.2, + tL = 0.70, + tU = 0.60, + parE = c(1, 1) + ) + expected <- data.frame( + looks = c(10, 20, 30, 40), + xL = c(0, 1, 3, 4), + pL = c(0, 0.05, 0.1, 0.1), + postL = c(0.8327, 0.8450, 0.7039, 0.7567), + pL_upper_ci = c(0.2589, 0.2161, 0.2386, 0.2144), + xU = c(2, 5, 7, 9), + pU = c(0.2000, 0.2500, 0.2333, 0.2250), + postU = c(0.6174, 0.7693, 0.7300, 0.7040), + pU_lower_ci = c(0.0368, 0.1041, 0.1150, 0.1227) + ) + expect_equal(result$xL, expected$xL) + expect_equal(result$pL, expected$pL) + expect_equal(result$postL, expected$postL) + expect_equal(result$pL_upper_ci, expected$pL_upper_ci) + expect_equal(result$xU, expected$xU) + expect_equal(result$pU, expected$pU) + expect_equal(result$postU, expected$postU) + expect_equal(result$pU_lower_ci, expected$pU_lower_ci) +}) + +test_that("boundsPostProb for beta mixture gives correct result", { + result <- boundsPostprob( + looks = c(7, 20, 40), + p0 = 0.15, + p1 = 0.20, + tL = 0.60, + tU = 0.80, + parE = rbind(c(1, 19), c(2, 10)), + weights = c(0.2, 0.8) + ) + expected <- data.frame( + looks = c(7, 20, 40), + xL = c(0, 2, 5), + pL = c(0.000, 0.100, 0.125), + postL = c(0.7456, 0.6601, 0.6173), + pL_upper_ci = c(0.3482, 0.2826, 0.2450), + xU = c(2, 6, 10), + pU = c(0.2857, 0.3000, 0.2500), + postU = c(0.8890, 0.9054, 0.8012), + pU_lower_ci = c(0.0534, 0.1396, 0.1424) + ) + expect_equal(result$xL, expected$xL) + expect_equal(result$pL, expected$pL) + expect_equal(result$postL, expected$postL) + expect_equal(result$pL_upper_ci, expected$pL_upper_ci) + expect_equal(result$xU, expected$xU) + expect_equal(result$pU, expected$pU) + expect_equal(result$postU, expected$postU) + expect_equal(result$pU_lower_ci, expected$pU_lower_ci) +})