R/decision2S_boundary.R

Defines functions decision2S_boundary.gammaMix decision2S_boundary.normMix solve_boundary2S_normMix decision2S_boundary.betaMix decision2S_boundary.default decision2S_boundary

Documented in decision2S_boundary decision2S_boundary.betaMix decision2S_boundary.gammaMix decision2S_boundary.normMix

#' Decision Boundary for 2 Sample Designs
#'
#' The \code{decision2S_boundary} function defines a 2 sample design
#' (priors, sample sizes, decision function) for the calculation of
#' the decision boundary. A function is returned which calculates the
#' critical value of the first sample \eqn{y_{1,c}} as a function of
#' the outcome in the second sample \eqn{y_2}. At the decision
#' boundary, the decision function will change between 0 (failure) and
#' 1 (success) for the respective outcomes.
#'
#' @template args-boundary2S
#'
#' @details For a 2 sample design the specification of the priors, the
#' sample sizes and the decision function, \eqn{D(y_1,y_2)}, uniquely
#' defines the decision boundary
#'
#' \deqn{D_1(y_2) = \max_{y_1}\{D(y_1,y_2) = 1\},}{D_1(y_2) = max_{y_1}{D(y_1,y_2) = 1},}
#'
#' which is the critical value of \eqn{y_{1,c}} conditional on the
#' value of \eqn{y_2} whenever the decision \eqn{D(y_1,y_2)} function
#' changes its value from 0 to 1 for a decision function with
#' \code{lower.tail=TRUE} (otherwise the definition is \eqn{D_1(y_2) =
#' \max_{y_1}\{D(y_1,y_2) = 0\}}{D_1(y_2) = max_{y_1}{D(y_1,y_2) =
#' 0}}). The decision function may change at most at a single critical
#' value for given \eqn{y_{2}} as only one-sided decision functions
#' are supported. Here, \eqn{y_2} is defined for binary and Poisson
#' endpoints as the sufficient statistic \eqn{y_2 = \sum_{i=1}^{n_2}
#' y_{2,i}} and for the normal case as the mean \eqn{\bar{y}_2 = 1/n_2
#' \sum_{i=1}^{n_2} y_{2,i}}.
#'
#' @return Returns a function with a single argument. This function
#' calculates in dependence of the outcome \eqn{y_2} in sample 2 the
#' critical value \eqn{y_{1,c}} for which the defined design will
#' change the decision from 0 to 1 (or vice versa, depending on the
#' decision function).
#'
#' @family design2S
#'
#' @examples
#'
#' # see ?decision2S for details of example
#' priorT <- mixnorm(c(1,   0, 0.001), sigma=88, param="mn")
#' priorP <- mixnorm(c(1, -49, 20   ), sigma=88, param="mn")
#' # the success criteria is for delta which are larger than some
#' # threshold value which is why we set lower.tail=FALSE
#' successCrit  <- decision2S(c(0.95, 0.5), c(0, 50), FALSE)
#' # the futility criterion acts in the opposite direction
#' futilityCrit <- decision2S(c(0.90)     , c(40),    TRUE)
#'
#' # success criterion boundary
#' successBoundary <- decision2S_boundary(priorP, priorT, 10, 20, successCrit)
#'
#' # futility criterion boundary
#' futilityBoundary <- decision2S_boundary(priorP, priorT, 10, 20, futilityCrit)
#'
#' curve(successBoundary(x), -25:25 - 49, xlab="y2", ylab="critical y1")
#' curve(futilityBoundary(x), lty=2, add=TRUE)
#'
#' # hence, for mean in sample 2 of 10, the critical value for y1 is
#' y1c <- futilityBoundary(-10)
#'
#' # around the critical value the decision for futility changes
#' futilityCrit(postmix(priorP, m=y1c+1E-3, n=10), postmix(priorT, m=-10, n=20))
#' futilityCrit(postmix(priorP, m=y1c-1E-3, n=10), postmix(priorT, m=-10, n=20))
#'
#' @export
decision2S_boundary <- function(prior1, prior2, n1, n2, decision, ...) UseMethod("decision2S_boundary")
#' @export
decision2S_boundary.default <- function(prior1, prior2, n1, n2, decision, ...) "Unknown density"

#' @templateVar fun decision2S_boundary
#' @template design2S-binomial
# If the \code{eps} argument is specificed, then the
# returned function will use the additional \code{lim2}
# argument to limit the search for the critical value.
#' @export
decision2S_boundary.betaMix <- function(prior1, prior2, n1, n2, decision, eps, ...) {
    ## only n2=0 is supported
    assert_number(n1, lower=1, finite=TRUE)
    assert_number(n2, lower=0, finite=TRUE)

    if(!missing(eps))
        assert_number(eps, lower=0, upper=0.1, finite=TRUE)

    cond_decisionDist <- function(post2cond) {
        fn <- function(m1) {
            ## Note: Subtracting from the decision 0.25 leads to
            ## negative decisions being at -0.25 while positives are
            ## at 0.75; since uniroot_int *always* returns the x which
            ## has lowest absolute value we are guaranteed that y2crit
            ## is just before the jump
            ##decision(post1cond, post2[[m2+1]]) - 0.25
            decision(postmix(prior1, r=m1, n=n1), post2cond) - 0.25
            ##decision(postmix(prior2, r=m2, n=n2), post1cond) - 0.25
        }
        Vectorize(fn)
    }

    ## saves the decision boundary conditional on the outcome of the
    ## second variable
    clim1 <- c(Inf, -Inf)
    clim2 <- c(Inf, -Inf)
    boundary <- c()
    full_boundary <- missing(eps)

    lower.tail <- attr(decision, "lower.tail")

    update_boundary <- function(lim1, lim2) {
        boundary <<- rep(NA, diff(lim2) + 1)
        clim2 <<- lim2
        clim1 <<- lim1
        for(y2 in lim2[1]:lim2[2]) {
            ## find decision point
            if(n2 == 0) {
                decFun <- cond_decisionDist(prior2)
            } else {
                decFun <- cond_decisionDist(postmix(prior2, r=y2, n=n2))
            }
            ind_llim <- decFun(lim1[1])
            ind_ulim <- decFun(lim1[2])
            y2ind <- y2 - lim2[1] + 1
            if(ind_llim < 0 & ind_ulim < 0) {
                ## then the decision is never 1
                boundary[y2ind] <<- -1
                next
            }
            if(ind_llim > 0 & ind_ulim > 0) {
                ## then the decision is always 1
                boundary[y2ind] <<- n1+1
                next
            }
            ## find boundary
            boundary[y2ind] <<- uniroot_int(decFun, lim1,
                                            f.lower=ind_llim,
                                            f.upper=ind_ulim)
        }
        if(lower.tail) {
            ## if lower.tail==TRUE, then the condition becomes true when
            ## going from large to small values, hence we need to integrate from
            ## 0 to boundary
            boundary <<- pmax(boundary - 1, -1)
        }
        return()
    }

    if(full_boundary)
        update_boundary(c(0, n1), c(0, n2))

    decision_boundary <- function(y2, lim1) {
        ## check if we need to recalculate the decision grid for the
        ## case of enabled approximate method
        assert_integerish(y2, lower=0, upper=n2, any.missing=FALSE)

        if(!full_boundary) {
            if(missing(lim1)) {
                ## if not hint is given we search the full sample
                ## space which should be OK, as the complexity is
                ## log(N)
                lim1 <- c(0, n1)
            } else {
                assert_integerish(lim1, lower=0, upper=n1, any.missing=FALSE)
            }
            lim2 <- c(min(y2), max(y2))
            ## check if the decision grid needs to be recomputed
            if(lim1[1] < clim1[1] | lim1[2] > clim1[2] |
               lim2[1] < clim2[1] | lim2[2] > clim2[2]) {
                ## ensure that lim1 never shrinks
                lim1[1] <- min(lim1[1], clim1[1])
                lim1[2] <- max(lim1[2], clim1[2])
                update_boundary(lim1, lim2)
            }
        }

        ## make sure y2 is an integer which is the value of
        ## the second read-out for which we return the decision
        ## boundary
        ## TODO: handle case with eps with care
        crit <- boundary[(y2 - clim2[1]) + 1]
        if(!full_boundary) {
            ## in case the lower boundary of the searched grid is not
            ## zero, then we cannot say anything about cases when the
            ## decision is always negative
            if(!lower.tail) {
                ## in this case the decision changes from negative to
                ## positive when going from small to large
                ## values. Hence, if the decision is always negative,
                ## then we can be sure of that we can never be sure,
                ## but should the decision be negative at all values,
                ## it can change at larger values.
                crit[crit==n1+1] <- NA
            } else {
                ## now the decision changes from positive to negative
                ## when going from small to large => should the
                ## decision not change in the clim1 domain then we do
                ## not know if it happens later
                if(clim1[1] > 0)
                    crit[crit==-1] <- NA
                ## however, if crit==Inf then we can be sure that the
                ## decision is indeed always positive
            }
        }
        return(crit)
    }
    decision_boundary
}


## returns a function object which is the decision boundary. That is
## the function finds at a regular grid between llim1 and ulim1 the
## roots of the decision function and returns an interpolation
## function object
solve_boundary2S_normMix <- function(decision, mix1, mix2, n1, n2, lim1, lim2, delta2) {
    grid <- seq(lim2[1], lim2[2], length=diff(lim2)/delta2)

    sigma1 <- sigma(mix1)
    sigma2 <- sigma(mix2)

    sem1 <- sigma1/sqrt(n1)
    scale1 <- sigma1/(n1^0.25)

    cond_decisionStep <- function(post2) {
        fn <- function(m1) {
            decision(postmix(mix1, m=m1, se=sem1), post2) - 0.75
        }
        Vectorize(fn)
    }

    Neval <- length(grid)
    #cat("Calculating boundary from", lim2[1], "to", lim2[2], "with", Neval, "points\n")
    tol <- min(delta2/100, .Machine$double.eps^0.25)
    ##cat("Using tolerance", tol, "\n")
    crit <- rep(NA, times=Neval)
    for(i in 1:Neval) {
        if(n2 == 0) {
            post2  <- mix2
        } else {
            post2 <- postmix(mix2, m=grid[i], se=sigma2/sqrt(n2))
        }
        ind_fun <- cond_decisionStep(post2)
        dec_bounds <- ind_fun(lim1)
        ## if decision function is not different at boundaries, lim1
        ## is too narrow and we then enlarge
        while(prod(dec_bounds) > 0) {
            w <- diff(lim1)
            lim1 <- c(lim1[1] - w/2, lim1[2] + w/2)
            dec_bounds <- ind_fun(lim1)
        }
        y1c <- uniroot(ind_fun, lim1,
                       f.lower=dec_bounds[1], f.upper=dec_bounds[2], tol=tol)$root
        crit[i] <- y1c
        ## set lim1 tightly around the current critical value and use the
        ## last boundary limits to not shrink too fast
        lim1 <- c(mean(lim1[1], y1c - 2*scale1), mean(y1c + 2*scale1, lim1[2]))
    }

    cbind(grid, crit)
}

#' @templateVar fun decision2S_boundary
#' @template design2S-normal
#' @export
decision2S_boundary.normMix <- function(prior1, prior2, n1, n2, decision, sigma1, sigma2, eps=1e-6, Ngrid=10, ...) {
    ## distributions of the means of the data generating distributions
    ## for now we assume that the underlying standard deviation
    ## matches the respective reference scales
    if(missing(sigma1)) {
        sigma1 <- RBesT::sigma(prior1)
        message("Using default prior 1 reference scale ", sigma1)
    }
    if(missing(sigma2)) {
        sigma2 <- RBesT::sigma(prior2)
        message("Using default prior 2 reference scale ", sigma2)
    }
    assert_number(sigma1, lower=0)
    assert_number(sigma2, lower=0)

    sem1 <- sigma1 / sqrt(n1)
    sem2 <- sigma2 / sqrt(n2)

    sigma(prior1) <- sigma1
    sigma(prior2) <- sigma2

    ## only n2 can be zero
    assert_that(n1 >  0)
    assert_that(n2 >= 0)

    if(n2 == 0) sem2 <- sigma(prior2) / sqrt(1E-1)

    ## change the reference scale of the prior such that the prior
    ## represents the distribution of the respective means
    mean_prior1 <- prior1
    sigma(mean_prior1) <- sem1
    ##mean_prior2 <- prior2
    ##sigma(mean_prior2) <- sem2

    ## discretization step-size
    delta2 <- sem2/Ngrid

    ## for the case of mix1 and mix2 having just 1 component, then one
    ## can prove that the decision boundary is a linear function.
    ## Hence we only calculate a very rough grid and apply linear
    ## interpolation.

    linear_boundary <- FALSE
    if(ncol(prior1) == 1 && ncol(prior2) == 1) {
        linear_boundary <- TRUE
        ## we could relax this even further
        delta2 <- sigma2/Ngrid
    }

    ## the boundary function depends only on the samples sizes n1, n2,
    ## the priors and the decision, but not the assumed truths

    clim2 <- c(Inf, -Inf)

    ## the boundary function which gives conditional on the second
    ## variable the critical value where the decision changes
    boundary <- NA
    boundary_discrete <- matrix(NA, nrow=0, ncol=2)

    ## check where the decision is 1, i.e. left or right
    lower.tail <- attr(decision, "lower.tail")

    decision_boundary <- function(y2, lim1) {

        lim2 <- range(y2)

        ## check if boundary function must be recomputed
        if(lim2[1] < clim2[1] | lim2[2] > clim2[2]) {
            new_lim2 <- clim2
            ## note: the <<- assignment is needed to set the variable in the enclosure
            if(missing(lim1))
                lim1 <- qmix(mean_prior1, c(eps/2, 1-eps/2))
            if(nrow(boundary_discrete) == 0) {
                ## boundary hasn't been calculated before, do it all
                boundary_discrete <<- solve_boundary2S_normMix(decision, prior1, prior2, n1, n2, lim1, lim2, delta2)
                new_lim2 <- lim2
            } else {
                if(lim2[1] < clim2[1]) {
                    ## the lower bound is not low enough... only add the region which is missing
                    new_left_lim2 <- min(lim2[1], clim2[1]-2*delta2)
                    boundary_extra <- solve_boundary2S_normMix(decision, prior1, prior2, n1, n2, lim1, c(new_left_lim2, clim2[1]-delta2), delta2)
                    new_lim2[1] <- new_left_lim2
                    boundary_discrete <<- rbind(boundary_extra, boundary_discrete)
                }
                if(lim2[2] > clim2[2]) {
                    ## the upper bound is not large enough.. again only add whats missing
                    new_right_lim2 <- max(lim2[2], clim2[2]+2*delta2)
                    boundary_extra <- solve_boundary2S_normMix(decision, prior1, prior2, n1, n2, lim1, c(clim2[2]+delta2, new_right_lim2), delta2)
                    new_lim2[2] <- new_right_lim2
                    boundary_discrete <<- rbind(boundary_discrete, boundary_extra)
                }
            }
            ## only for debugging
            ##assert_that(all(order(boundary_discrete[,1]) == 1:nrow(boundary_discrete)), msg="x grid must stay ordered!")
            if(linear_boundary) {
                boundary <<- approxfun(boundary_discrete[,1], boundary_discrete[,2], rule=2)
            } else {
                boundary <<- splinefun(boundary_discrete[,1], boundary_discrete[,2])
            }
            clim2 <<- new_lim2
        }

        return(boundary(y2))
    }

    decision_boundary
}


#' @templateVar fun decision2S_boundary
#' @template design2S-poisson
#' @export
decision2S_boundary.gammaMix <- function(prior1, prior2, n1, n2, decision, eps=1e-6, ...) {
    assert_that(likelihood(prior1) == "poisson")
    assert_that(likelihood(prior2) == "poisson")

    # only the second n2 argument may be 0
    assert_that(n1 >  0)
    assert_that(n2 >= 0)

    cond_decisionStep <- function(post2) {
        fn <- function(m1) {
            decision(postmix(prior1, n=n1, m=m1/n1), post2) - 0.25
        }
        Vectorize(fn)
    }

    clim1 <- c(Inf, -Inf)
    clim2 <- c(Inf, -Inf)
    boundary <- NA
    grid <- NA
    lower.tail <- attr(decision, "lower.tail")

    decision_boundary <- function(y2, lim1) {

        if(missing(lim1)) {
            lambda1 <- summary(prior1, probs=c())["mean"] * n1
            lim1 <- qpois(c(eps/2, 1-eps/2), lambda1)
        }

        lim2 <- range(y2)

        assert_number(lim1[1], lower=0, finite=TRUE)
        assert_number(lim1[2], lower=0, finite=TRUE)
        assert_number(lim2[1], lower=0, finite=TRUE)
        assert_number(lim2[2], lower=0, finite=TRUE)

        ## check if the boundary needs to be recomputed
        if(lim1[1] < clim1[1] | lim1[2] > clim1[2] |
           lim2[1] < clim2[1] | lim2[2] > clim2[2]) {
            ## ensure that lim1 never shrinks in size
            lim1[1] <- min(lim1[1], clim1[1])
            lim1[2] <- max(lim1[2], clim1[2])
            grid <<- lim2[1]:lim2[2]
            Neval <- length(grid)
            boundary <<- rep(NA, Neval)
            for(i in 1:Neval) {
                if(n2 == 0) {
                    cond_dec <- cond_decisionStep(prior2)
                } else {
                    cond_dec <- cond_decisionStep(postmix(prior2, n=n2, m=grid[i]/n2))
                }
                low  <- cond_dec(lim1[1])
                high <- cond_dec(lim1[2])
                if(low < 0 & high < 0) {
                    boundary[i] <<- -1
                    next;
                }
                if(low > 0 & high > 0) {
                    boundary[i] <<- Inf
                    next;
                }
                boundary[i] <<- uniroot_int(cond_dec, lim1,
                                            f.lower=low,
                                            f.upper=high)
            }

            if(lower.tail) {
                ## if lower.tail==TRUE, then the condition becomes
                ## true when going from large to small values, hence
                ## we need to integrate from 0 to the boundary
                boundary <<- pmax(boundary - 1, -1)
            }

            ## save limits of new grid
            clim1 <<- lim1
            clim2 <<- lim2
        }

        assert_numeric(y2, lower=0, finite=TRUE, any.missing=FALSE)
        crit <- boundary[y2 - clim2[1] + 1]
        ## in case the lower boundary of the searched grid is not
        ## zero, then we cannot say anything about cases when the
        ## decision is always negative
        if(!lower.tail) {
            ## in this case the decision changes from negative to
            ## positive when going from small to large values. Hence,
            ## if the decision is always negative, then we can be sure
            ## that the decision changes past lim1[2]. We set the
            ## boundary to lim1[2]+1 if lim1 has been given; otherwise
            ## to NA.
            ## Should the decision be negative at all values,
            ## it can change at larger values.
            if(missing(lim1)) {
                crit[crit==-1] <- NA
                crit[crit==Inf] <- NA
            } else {
                crit[crit==-1] <- lim1[2]+1
                crit[crit==Inf] <- lim1[1]-1
            }
        } else {
            ## now the decision changes from positive to negative
            ## when going from small to large => should the
            ## decision not change in the clim1 domain then we do
            ## not know if it happens later
            if(clim1[1] > 0)
                crit[crit==-1] <- NA
            ## however, if crit==Inf then we can be sure that the
            ## decision is indeed always positive
        }
        return(crit)
    }
    decision_boundary
}

Try the RBesT package in your browser

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

RBesT documentation built on Aug. 22, 2023, 1:08 a.m.