Nothing
# pROC: Tools Receiver operating characteristic (ROC curves) with
# (partial) area under the curve, confidence intervals and comparison.
# Copyright (C) 2011-2014 Xavier Robin, Alexandre Hainard, Natacha Turck,
# Natalia Tiberti, Frédérique Lisacek, Jean-Charles Sanchez
# and Markus Müller
#
# This program 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/>.
power.roc.test <- function(...) {
UseMethod("power.roc.test")
}
power.roc.test.roc <- function(roc1, roc2, sig.level = 0.05, power = NULL, kappa = NULL,
alternative = c("two.sided", "one.sided"),
reuse.auc = TRUE, method = c("delong", "bootstrap", "obuchowski"),
...) {
# Basic sanity checks
if (!is.null(power) && (power < 0 || power > 1)) {
stop("'power' must range from 0 to 1")
}
if (!is.null(sig.level) && (sig.level < 0 || sig.level > 1)) {
stop("'sig.level' must range from 0 to 1")
}
# check that the AUC of roc1 was computed, or do it now
if (is.null(roc1$auc) | !reuse.auc) {
roc1$auc <- auc(roc1, ...)
}
if (!is.null(attr(roc1$auc, "partial.auc.correct")) && attr(roc1$auc, "partial.auc.correct")) {
stop("Cannot compute power with corrected partial AUCs")
}
roc1 <- roc_utils_unpercent(roc1)
if (!missing(roc2) && !is.null(roc2)) {
alternative <- match.arg(alternative)
if (!is.null(sig.level) && alternative == "two.sided") {
sig.level <- sig.level / 2
}
if ("roc" %in% class(roc2)) {
# check that the AUC of roc2 was computed, or do it now
if (is.null(roc2$auc) | !reuse.auc) {
roc2$auc <- auc(roc2, ...)
}
if (!is.null(attr(roc2$auc, "partial.auc.correct")) && attr(roc2$auc, "partial.auc.correct")) {
stop("Cannot compute power with corrected partial AUCs")
}
roc2 <- roc_utils_unpercent(roc2)
# Make sure the ROC curves are paired
rocs.are.paired <- are.paired(roc1, roc2)
if (!rocs.are.paired) {
stop("The sample size for a difference in AUC cannot be applied to unpaired ROC curves yet.")
}
# Make sure the AUC specifications are identical
attr1 <- attributes(roc1$auc)
attr1$roc <- NULL
attr2 <- attributes(roc2$auc)
attr2$roc <- NULL
if (!identical(attr1, attr2)) {
stop("Different AUC specifications in the ROC curves.")
}
# check that the same region was requested in auc. Otherwise, issue a warning
if (!identical(attributes(roc1$auc)[names(attributes(roc1$auc)) != "roc"], attributes(roc2$auc)[names(attributes(roc2$auc)) != "roc"])) {
warning("Different AUC specifications in the ROC curves. Enforcing the inconsistency, but unexpected results may be produced.")
}
ncontrols <- length(roc1$controls)
ncases <- length(roc1$cases)
if (is.null(kappa)) {
kappa <- ncontrols / ncases
}
# Power test
if (is.null(power)) {
if (is.null(sig.level)) {
stop("'sig.level' or 'power' must be provided.")
}
zalpha <- qnorm(1 - sig.level)
zbeta <- zbeta.obuchowski(roc1, roc2, zalpha, method = method, ...)
power <- pnorm(zbeta)
}
# sig.level
else if (is.null(sig.level)) {
zbeta <- qnorm(power)
zalpha <- zalpha.obuchowski(roc1, roc2, zbeta, method = method, ...)
sig.level <- 1 - pnorm(zalpha)
}
# Sample size
else {
zalpha <- qnorm(1 - sig.level)
zbeta <- qnorm(power)
ncases <- ncases.obuchowski(roc1, roc2, zalpha, zbeta, method = method, ...)
ncontrols <- kappa * ncases
}
# Restore sig.level if two.sided
if (alternative == "two.sided") {
sig.level <- sig.level * 2
}
return(structure(list(ncases = ncases, ncontrols = ncontrols, auc1 = roc1$auc, auc2 = roc2$auc, sig.level = sig.level, power = power, alternative = alternative, method = "Two ROC curves power calculation"), class = "power.htest"))
} else {
stop("'roc2' must be an object of class 'roc'.")
}
} else {
ncontrols <- length(roc1$controls)
ncases <- length(roc1$cases)
if (!is.null(sig.level) && !is.null(power)) {
if (is.null(kappa)) {
kappa <- ncontrols / ncases
}
ncontrols <- ncases <- NULL
}
auc <- auc(roc1)
# TODO: implement this with var() and cov() for the given ROC curve
return(power.roc.test.numeric(ncontrols = ncontrols, ncases = ncases, auc = auc, sig.level = sig.level, power = power, alternative = alternative, kappa = kappa, ...))
}
}
power.roc.test.numeric <- function(auc = NULL, ncontrols = NULL, ncases = NULL, sig.level = 0.05, power = NULL, kappa = 1, alternative = c("two.sided", "one.sided"), ...) {
# basic sanity checks
if (!is.null(ncases) && ncases < 0) {
stop("'ncases' must be positive")
}
if (!is.null(ncontrols) && ncontrols < 0) {
stop("'ncontrols' must be positive")
}
if (!is.null(kappa) && kappa < 0) {
stop("'kappa' must be positive")
}
if (!is.null(power) && (power < 0 || power > 1)) {
stop("'power' must range from 0 to 1")
}
if (!is.null(sig.level) && (sig.level < 0 || sig.level > 1)) {
stop("'sig.level' must range from 0 to 1")
}
# Complete ncontrols and ncases with kappa
if (is.null(ncontrols) && !is.null(ncases) && !is.null(kappa)) {
ncontrols <- kappa * ncases
} else if (is.null(ncases) && !is.null(ncontrols) && !is.null(kappa)) {
ncases <- ncontrols / kappa
}
alternative <- match.arg(alternative)
if (alternative == "two.sided" && !is.null(sig.level)) {
sig.level <- sig.level / 2
}
# determine AUC
if (is.null(auc)) {
if (is.null(ncontrols) || is.null(ncases)) {
stop("'ncontrols' and 'ncases' (or one of these with 'kappa') or 'auc' must be provided.")
} else if (is.null(power)) {
stop("'power' or 'auc' must be provided.")
} else if (is.null(sig.level)) {
stop("'sig.level' or 'auc' must be provided.")
}
kappa <- ncontrols / ncases
zalpha <- qnorm(1 - sig.level)
zbeta <- qnorm(power)
tryCatch(
root <- uniroot(power_roc_test_optimize_auc_function, interval = c(0.5, 1 - 1e-16), ncontrols = ncontrols, ncases = ncases, zalpha = zalpha, zbeta = zbeta),
error = function(e) {
stop(sprintf("AUC could not be solved:\n%s", e))
}
)
auc <- root$root
}
# Determine number of patients (sample size)
else if (is.null(ncases) && is.null(ncontrols)) {
if (is.null(power)) {
stop("'ncontrols' and 'ncases' (or one of these with 'kappa') or 'auc' must be provided.")
} else if (is.null(kappa)) {
stop("'kappa' must be provided.")
} else if (is.null(auc)) {
stop("'auc' or 'ncases' and 'ncontrols' must be provided.")
} else if (is.null(sig.level)) {
stop("'sig.level' or 'ncases' and 'ncontrols' must be provided.")
}
theta <- as.numeric(auc)
Vtheta <- var_theta_obuchowski(theta, kappa)
ncases <- solve.nd(
zalpha = qnorm(1 - sig.level),
zbeta = qnorm(power),
v0 = 0.0792 * (1 + 1 / kappa),
va = Vtheta,
delta = theta - 0.5
)
ncontrols <- kappa * ncases
}
# Determine power
else if (is.null(power)) {
if (is.null(ncontrols) || is.null(ncases)) {
stop("'ncontrols' and 'ncases' (or one of these with 'kappa') or 'auc' must be provided.")
} else if (is.null(auc)) {
stop("'auc' or 'power' must be provided.")
} else if (is.null(sig.level)) {
stop("'sig.level' or 'power' must be provided.")
}
kappa <- ncontrols / ncases
theta <- as.numeric(auc)
Vtheta <- var_theta_obuchowski(theta, kappa)
zbeta <- solve.zbeta(
nd = ncases,
zalpha = qnorm(1 - sig.level),
v0 = 0.0792 * (1 + 1 / kappa),
va = Vtheta,
delta = theta - 0.5
)
power <- pnorm(zbeta)
}
# Determine sig.level
else if (is.null(sig.level)) {
if (is.null(ncontrols) || is.null(ncases)) {
stop("'ncontrols' and 'ncases' (or one of these with 'kappa') or 'auc' must be provided.")
} else if (is.null(auc)) {
stop("'auc' or 'sig.level' must be provided.")
} else if (is.null(power)) {
stop("'power' or 'sig.level' must be provided.")
}
kappa <- ncontrols / ncases
theta <- as.numeric(auc)
Vtheta <- var_theta_obuchowski(theta, kappa)
zalpha <- solve.zalpha(
nd = ncases,
zbeta = qnorm(power),
v0 = 0.0792 * (1 + 1 / kappa),
va = Vtheta,
delta = theta - 0.5
)
sig.level <- 1 - pnorm(zalpha)
} else {
stop("One of 'power', 'sig.level', 'auc', or both 'ncases' and 'ncontrols' must be NULL.")
}
# Restore sig.level if two.sided
if (alternative == "two.sided") {
sig.level <- sig.level * 2
}
return(structure(list(ncases = ncases, ncontrols = ncontrols, auc = auc, sig.level = sig.level, power = power, method = "One ROC curve power calculation"), class = "power.htest"))
}
power.roc.test.list <- function(parslist, ncontrols = NULL, ncases = NULL, sig.level = 0.05, power = NULL, kappa = 1, alternative = c("two.sided", "one.sided"), ...) {
# basic sanity checks
if (!is.null(ncases) && ncases < 0) {
stop("'ncases' must be positive")
}
if (!is.null(ncontrols) && ncontrols < 0) {
stop("'ncontrols' must be positive")
}
if (!is.null(kappa) && kappa < 0) {
stop("'kappa' must be positive")
}
if (!is.null(power) && (power < 0 || power > 1)) {
stop("'power' must range from 0 to 1")
}
if (!is.null(sig.level) && (sig.level < 0 || sig.level > 1)) {
stop("'sig.level' must range from 0 to 1")
}
# Complete ncontrols and ncases with kappa
if (is.null(ncontrols) && !is.null(ncases) && !is.null(kappa)) {
ncontrols <- kappa * ncases
} else if (is.null(ncases) && !is.null(ncontrols) && !is.null(kappa)) {
ncases <- ncontrols / kappa
}
# Warn if anything is passed with ...
if (length(list(...)) > 0) {
warning(paste("The following arguments were ignored:", paste(names(list(...)), collapse = ", ")))
}
alternative <- match.arg(alternative)
if (alternative == "two.sided" && !is.null(sig.level)) {
sig.level <- sig.level / 2
}
# Check required elements of parslist
required <- c("A1", "B1", "A2", "B2", "rn", "ra", "delta")
if (any(!required %in% names(parslist))) {
stop(paste("Missing parameter(s):", paste(required[!required %in% names(parslist)], collapse = ", ")))
}
# Determine number of patients (sample size)
if (is.null(ncases) && is.null(ncontrols)) {
if (is.null(power)) {
stop("'ncontrols' and 'ncases' (or one of these with 'kappa') or 'auc' must be provided.")
} else if (is.null(kappa)) {
stop("'kappa' must be provided.")
} else if (is.null(sig.level)) {
stop("'sig.level' or 'ncases' and 'ncontrols' must be provided.")
}
zalpha <- qnorm(1 - sig.level)
zbeta <- qnorm(power)
ncases <- ncases.obuchowski.params(parslist, zalpha, zbeta, kappa)
ncontrols <- kappa * ncases
}
# Determine power
else if (is.null(power)) {
if (is.null(ncontrols) || is.null(ncases)) {
stop("'ncontrols' and 'ncases' (or one of these with 'kappa') or 'auc' must be provided.")
} else if (is.null(sig.level)) {
stop("'sig.level' or 'power' must be provided.")
}
kappa <- ncontrols / ncases
zalpha <- qnorm(1 - sig.level)
zbeta <- zbeta.obuchowski.params(parslist, zalpha, ncases, kappa)
power <- pnorm(zbeta)
}
# Determine sig.level
else if (is.null(sig.level)) {
if (is.null(ncontrols) || is.null(ncases)) {
stop("'ncontrols' and 'ncases' (or one of these with 'kappa') or 'auc' must be provided.")
} else if (is.null(power)) {
stop("'power' or 'sig.level' must be provided.")
}
kappa <- ncontrols / ncases
zbeta <- qnorm(power)
zalpha <- zalpha.obuchowski.params(parslist, zbeta, ncases, kappa)
sig.level <- 1 - pnorm(zalpha)
} else {
stop("One of 'power', 'sig.level', 'auc', or both 'ncases' and 'ncontrols' must be NULL.")
}
# Restore sig.level if two.sided
if (alternative == "two.sided") {
sig.level <- sig.level * 2
}
return(structure(list(ncases = ncases, ncontrols = ncontrols, sig.level = sig.level, power = power, method = "Two ROC curves power calculation"), class = "power.htest"))
}
#### HIDDEN FUNCTIONS ####
# A function to 'optimize' auc
power_roc_test_optimize_auc_function <- function(x, ncontrols, ncases, zalpha, zbeta) {
kappa <- ncontrols / ncases
Vtheta <- var_theta_obuchowski(x, kappa)
(zalpha * sqrt(0.0792 * (1 + 1 / kappa)) + zbeta * sqrt(Vtheta))^2 / (x - 0.5)^2 - ncases
}
# Compute variance of a delta from a 'covvar' list (see 'covvar' below)
var_delta_covvar <- function(covvar) {
covvar$var1 + covvar$var2 - 2 * covvar$cov12
}
# Compute variance of a delta from a 'covvar' list (see 'covvar' below)
# under the null hypothesis
# roc1 taken as reference.
var0.delta.covvar <- function(covvar) {
2 * covvar$var1 - 2 * covvar$cov12
}
# Compute the number of cases with Obuchowski formula and var(... method=method)
ncases.obuchowski <- function(roc1, roc2, zalpha, zbeta, method, ...) {
delta <- roc1$auc - roc2$auc
covvar <- covvar(roc1, roc2, method, ...)
v0 <- var0.delta.covvar(covvar)
va <- var_delta_covvar(covvar)
nd <- solve.nd(
zalpha = zalpha,
zbeta = zbeta,
v0 = v0, va = va,
delta = delta
)
return(nd)
}
# Compute the number of cases with Obuchowski formula from params
ncases.obuchowski.params <- function(parslist, zalpha, zbeta, kappa) {
covvar <- list(
var1 = var_params_obuchowski(parslist$A1, parslist$B1, kappa, parslist$FPR11, parslist$FPR12),
var2 = var_params_obuchowski(parslist$A2, parslist$B2, kappa, parslist$FPR21, parslist$FPR22),
cov12 = cov_params_obuchowski(parslist$A1, parslist$B1, parslist$A2, parslist$B2, parslist$rn, parslist$ra, kappa, parslist$FPR11, parslist$FPR12, parslist$FPR21, parslist$FPR22)
)
v0 <- var0.delta.covvar(covvar)
va <- var_delta_covvar(covvar)
nd <- solve.nd(
zalpha = zalpha,
zbeta = zbeta,
v0 = v0, va = va,
delta = parslist$delta
)
return(nd)
}
# Compute the z alpha with Obuchowski formula and var(... method=method)
zalpha.obuchowski <- function(roc1, roc2, zbeta, method, ...) {
delta <- roc1$auc - roc2$auc
ncases <- length(roc1$cases)
covvar <- covvar(roc1, roc2, method, ...)
v0 <- var0.delta.covvar(covvar)
va <- var_delta_covvar(covvar)
zalpha <- solve.zalpha(
nd = ncases,
zbeta = zbeta,
v0 = v0, va = va,
delta = delta
)
return(zalpha)
}
# Compute the z alpha with Obuchowski formula from params
zalpha.obuchowski.params <- function(parslist, zbeta, ncases, kappa) {
covvar <- list(
var1 = var_params_obuchowski(parslist$A1, parslist$B1, kappa, parslist$FPR11, parslist$FPR12),
var2 = var_params_obuchowski(parslist$A2, parslist$B2, kappa, parslist$FPR21, parslist$FPR22),
cov12 = cov_params_obuchowski(parslist$A1, parslist$B1, parslist$A2, parslist$B2, parslist$rn, parslist$ra, kappa, parslist$FPR11, parslist$FPR12, parslist$FPR21, parslist$FPR22)
)
v0 <- var0.delta.covvar(covvar)
va <- var_delta_covvar(covvar)
zalpha <- solve.zalpha(
nd = ncases,
zbeta = zbeta,
v0 = v0, va = va,
delta = parslist$delta
)
return(zalpha)
}
# Compute the z beta with Obuchowski formula and var(... method=method)
zbeta.obuchowski <- function(roc1, roc2, zalpha, method, ...) {
delta <- roc1$auc - roc2$auc
ncases <- length(roc1$cases)
covvar <- covvar(roc1, roc2, method, ...)
v0 <- var0.delta.covvar(covvar)
va <- var_delta_covvar(covvar)
zbeta <- solve.zbeta(
nd = ncases,
zalpha = zalpha,
v0 = v0, va = va,
delta = delta
)
return(zbeta)
}
# Compute the z beta with Obuchowski formula from params
zbeta.obuchowski.params <- function(parslist, zalpha, ncases, kappa) {
covvar <- list(
var1 = var_params_obuchowski(parslist$A1, parslist$B1, kappa, parslist$FPR11, parslist$FPR12),
var2 = var_params_obuchowski(parslist$A2, parslist$B2, kappa, parslist$FPR21, parslist$FPR22),
cov12 = cov_params_obuchowski(parslist$A1, parslist$B1, parslist$A2, parslist$B2, parslist$rn, parslist$ra, kappa, parslist$FPR11, parslist$FPR12, parslist$FPR21, parslist$FPR22)
)
v0 <- var0.delta.covvar(covvar)
va <- var_delta_covvar(covvar)
a <- va
zbeta <- solve.zbeta(
nd = ncases,
zalpha = zalpha,
v0 = v0, va = va,
delta = parslist$delta
)
return(zbeta)
}
solve.zbeta <- function(nd, zalpha, v0, va, delta) {
# Solve for z_\beta in Obuchowski formula:
# See formula 2 in Obuchowsk & McClish 1997 (2 ROC curves)
# or formula 2 in Obuchowski et al 2004 (1 ROC curve)
# The formula is of the form:
# nd = (z_alpha * sqrt(v0) - z_beta * sqrt(va)) / delta ^ 2
# Re-organized:
# z_beta = (sqrt(nd * delta ^ 2) - z_alpha * sqrt(v0)) / sqrt(va)
# @param nd: number of diseased patients (or abornmal, N_A in Obuchowsk & McClish 1997)
# @param zalpha: upper \alpha (sig.level) percentile of the standard normal distribution
# @param v0 the null variance associated with z_alpha
# @param va: the alternative variance associated with z_beta
# @param delta: the difference in AUC
return((sqrt(nd * delta^2) - zalpha * sqrt(v0)) / sqrt(va))
}
solve.nd <- function(zalpha, zbeta, v0, va, delta) {
# Solve for number of diseased (abnormal) patients in Obuchowski formula:
# See formula 2 in Obuchowsk & McClish 1997 (2 ROC curves)
# or formula 2 in Obuchowski et al 2004 (1 ROC curve)
# nd = (z_alpha * sqrt(v0) - z_beta * sqrt(va)) / delta ^ 2
# @param zalpha: upper \alpha (sig.level) percentile of the standard normal distribution
# @param zbeta: upper \beta (power) percentile of the standard normal distribution
# @param v0 the null variance associated with z_alpha
# @param va: the alternative variance associated with z_beta
# @param delta: the difference in AUC
return((zalpha * sqrt(v0) + zbeta * sqrt(va))^2 / delta^2)
}
solve.zalpha <- function(nd, zbeta, v0, va, delta) {
# Solve for z_\alpha in Obuchowski formula:
# See formula 2 in Obuchowsk & McClish 1997 (2 ROC curves)
# or formula 2 in Obuchowski et al 2004 (1 ROC curve)
# The formula is of the form:
# nd = (z_alpha * sqrt(v0) - z_beta * sqrt(va)) / delta ^ 2
# Re-organized:
# z_alpha = (sqrt(nd * delta ^ 2) - z_beta * sqrt(va)) / sqrt(v0)
# @param nd: number of diseased patients (or abornmal, N_A in Obuchowsk & McClish 1997)
# @param zbeta: upper \beta (power) percentile of the standard normal distribution
# @param v0 the null variance associated with z_alpha
# @param va: the alternative variance associated with z_beta
# @param delta: the difference in AUC
return((sqrt(nd * delta^2) - zbeta * sqrt(va)) / sqrt(v0))
}
# Compute var and cov of two ROC curves by bootstrap in a single bootstrap run
covvar <- function(roc1, roc2, method, ...) {
cov12 <- cov(roc1, roc2, boot.return = TRUE, method = method, ...)
if (!is.null(attr(cov12, "resampled.values"))) {
var1 <- var(attr(cov12, "resampled.values")[, 1])
var2 <- var(attr(cov12, "resampled.values")[, 2])
attr(cov12, "resampled.values") <- NULL
} else {
var1 <- var(roc1, method = method, ...)
var2 <- var(roc2, method = method, ...)
}
ncases <- length(roc1$cases)
return(list(var1 = var1 * ncases, var2 = var2 * ncases, cov12 = cov12 * ncases))
}
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.