R/get.oc.kb.R

#' Operating Characteristics for Single-agent Trials
#'
#' This function generates the operating characteristics of the Keyboard design for
#' single-agent trials.
#'
#' @details
#' The operating characteristics of the Keyboard design are generated by
#' simulating trials under the pre-specified true toxicity probabilities of the
#' investigational doses.
#'
#' The Keyboard design defines a series of equal-width dosing intervals (or
#' "keys") to present the potential locations of the true toxicity of a dose
#' and guide the dose escalation or de-escalation between cohorts of patients.
#' The Keyboard design starts by specifying a proper dosing interval, referred
#' to as the "target key", based on which it forms a series of equally wide
#' keys that span the remainder of the range from 0 to 1.
#'
#' The Keyboard design relies on the posterior distribution of the toxicity
#' probability to guide dosage. To make the decision of dose escalation or
#' de-escalation, given the observed data at the current dose, we identify the
#' key that has the highest posterior probability, referred to as the
#' "strongest key". This key represents where the true dose-limiting toxicity
#' (DLT) rate of the current dose is most likely located. If the strongest key
#' is to the  left of the target key, then we escalate the dose because
#'  the data suggest that the current dose is most
#' likely to underdose; if the strongest key is  to the
#' right of the target key, then we de-escalate the dose because the observed data
#' suggest that the current dose is likely to overdose; and if the strongest
#' key is the target key, then we retain the current dose because the observed data
#' support the notion that the current dose is most likely to be in the proper dosing
#' interval.
#' Graphically, the strongest key is the one with the largest area under the
#' posterior distribution curve of the DLT rate of the current dose.
#'
#' An attractive feature of the Keyboard design is that its dose escalation and
#' de-escalation rule can be tabulated before the onset of the trial. Thus,
#' when conducting the trial, no calculation or model fitting is needed, and we
#'need to count only the number of DLTs observed at the current dose;
#' the decision to escalate or de-escalate the dose is based on the pre-tabulated
#' decision rules.
#'
#' Given all observed data, the Keyboard design uses an isotonic regression to obtain an efficient statistical estimate of
#' the maximum tolerated dose (MTD) by utilizing the fact that toxicity
#' presumably increases with the dose.
#'
#' For patient safety, the following dose-elimination rule is evaluated after
#' each cohort:
#' if at least 3 patients have been treated at the given dose and
#' the observed data indicate that there is more than a 95\% chance that the
#' current dose is above the maximum tolerated dose (MTD), then we eliminate this dose and beyond from the trial to prevent exposing future patients to these
#' overly toxic doses. The probability threshold for elimination can be
#' specified with \code{cutoff.eli}. When a dose is eliminated, the design
#' recommends the next lower dose for treating the next patient.
#'
#' The Keyboard design has two built-in stopping rules:\cr
#' (1) stop the trial if the lowest dose is eliminated due to toxicity, and no
#'     dose should be selected as the MTD; and \cr
#' (2) stop the trial and select the MTD if the number of patients treated at
#'     the current dose reaches \code{n.earlystop}.
#'
#' The first stopping rule is a safety rule to protect patients from being exposed to overly toxic doses. The rationale for the second stopping
#' rule is that when enough (i.e., \code{n.earlystop})
#' patients are assigned to a dose, then the dose-finding algorithm has
#' approximately converged. Thus, we can stop the trial early and select the
#' MTD to minimize the sample size and  the trial duration.
#'
#' For some applications, investigators may prefer a stricter stopping rule
#' than rule (1) for extra safety when the lowest dose is overly toxic.
#' This can be achieved by setting \code{extrasafe=TRUE}, which imposes the
#' following, stricter, safety stopping rule:\cr
#' Stop the trial if \cr
#' (i) the number of patients treated at the lowest dose \eqn{\ge 3}, and \cr
#' (ii) \deqn{Pr((toxicity rate of the lowest dose > target) | data)
#'            > cutoff.eli - offset}
#' As a tradeoff, the strong stopping rule will decrease the MTD selection
#' percentage when the lowest dose is the true MTD.
#'
#' @param target The target dose-limiting toxicity (DLT) rate.
#' @param p.true A vector containing the true toxicity probabilities of the
#'               investigational dose levels.
#' @param ncohort A scalar specifying the total number of cohorts in the trial.
#' @param cohortsize The number of patients in the cohort.\cr
#'                   The default is 1.
#' @param n.earlystop The early stopping parameter. If the number of patients
#'                    treated at the current dose reaches \code{n.earlystop}, then
#'                    stop the trial and select the MTD based on the observed
#'                    data.\cr
#'                    The default value is 100.
#' @param startdose The starting dose level for the trial.\cr
#'                  The default is 1.
#' @param marginL The difference between the target and the lower limit of the
#'                "target key" (proper dosing interval) to be defined.\cr
#'                The default is 0.05.
#' @param marginR The difference between the target and the upper limit of the
#'                "target key" (proper dosing interval) to be defined.\cr
#'                The default is 0.05.
#' @param cutoff.eli The cutoff value to eliminate an overly toxic dose and all
#'                   higher doses for safety.\cr
#'                   The default value is 0.95.
#' @param extrasafe Set \code{extrasafe=TRUE} to impose a stricter
#'                  stopping rule.\cr
#'                  The default is FALSE.
#' @param offset A small positive number (between 0 and 0.5) to control how
#'               strict the stopping rule is when \code{extrasafe=TRUE}. A
#'               larger value leads to a stricter stopping rule.\cr
#'               The default value of 0.05 generally works well.
#' @param ntrial The total number of trials to be simulated. \cr
#'               The default value is 1000.
#'
#' @return The function returns the operating characteristics of the Keyboard
#'   design (single-agent) as a list, which includes: \cr
#' \enumerate{
#'   \item the selection percentage at each dose level (\code{$selpercent}),\cr
#'   \item the average number of patients treated at each dose level (\code{$npatients}),\cr
#'   \item the average number of toxicities observed at each dose level (\code{$ntox}),\cr
#'   \item the average number of toxicities (\code{$totaltox}),\cr
#'   \item the average number of patients (\code{$totaln}),\cr
#'   \item the percentage of early stopping due to toxicity without selecting the MTD (\code{$percentstop}),\cr
#'   \item the risk of overdosing 60\% or more of patients (\code{$overdose60}),\cr
#'   \item the risk of overdosing 80\% or more of patients (\code{$overdose80}),\cr
#'   \item a data.frame object containing simulation parameters, such as \code{target, p.true,} etc. (\code{$simu.setup}).
#' }
#'
#' @import Rcpp methods graphics stats
#' @export
#'
#' @author Xiaomeng Yuan, Chen Li, Hongying Sun, Li Tang and Haitao Pan
#' @examples
#' \donttest{
#' ### Single-agent trial ###
#'
#' oc <- get.oc.kb(target=0.3, p.true=c(0.05, 0.15, 0.3, 0.45, 0.6),
#'                 ncohort=20, cohortsize=3, ntrial=1000)
#'
#' oc
#' }
#'
#'
#' @section Uses:
#' This function uses \code{\link{get.boundary.kb}} and
#' \code{\link{select.mtd.kb}}.
#'
#' @family single-agent functions
#'
#' @references
#'
#' Yan F, Mandrekar SJ, Yuan Y. Keyboard: A Novel Bayesian Toxicity Probability
#' Interval Design for Phase I Clinical Trials.
#' \emph{Clinical Cancer Research}. 2017; 23:3994-4003.
#' http://clincancerres.aacrjournals.org/content/23/15/3994.full-text.pdf
get.oc.kb <- function (target, p.true, ncohort, cohortsize,
                       n.earlystop = 100, startdose = 1,
                       marginL = 0.05, marginR = 0.05, cutoff.eli = 0.95,
                       extrasafe = FALSE, offset = 0.05, ntrial = 1000) {
    if (offset >= 0.5) {
        warning("Error: the offset is too large! \n")
        return()
    }
    if (n.earlystop <= 6) {
        warning("Warning: the value of n.earlystop is too low to ensure good operating characteristics.",
            "Recommend n.earlystop = 9 to 18 \n")
        return()
    }
    set.seed(6)
    ndose = length(p.true)
    npts = ncohort * cohortsize
    Y = matrix(rep(0, ndose * ntrial), ncol = ndose)
    N = matrix(rep(0, ndose * ntrial), ncol = ndose)
    dselect = rep(0, ntrial)

    temp = get.boundary.kb(target, ncohort, cohortsize)$full_boundary_tab
    b.e = temp[2, ]
    b.d = temp[3, ]
    b.elim = temp[4, ]

    for (trial in 1:ntrial) {
        y <- rep(0, ndose)
        n <- rep(0, ndose)
        earlystop = 0
        d = startdose
        elimi = rep(0, ndose)
        for (i in 1:ncohort) {
            y[d] = y[d] + sum(runif(cohortsize) < p.true[d])
            n[d] = n[d] + cohortsize
            if (n[d] >= n.earlystop) {
                break
            }
            if (!is.na(b.elim[n[d]])) {
                if (n[d]>=3 && y[d] >= b.elim[n[d]]) {
                    elimi[d:ndose] = 1
                    if (d == 1) {
                        earlystop = 1
                        break
                    }
                }
                if (extrasafe) {
                    if (d == 1 && n[1] >= 3) {
                        if (1 - pbeta(target, y[1] + 1, n[1] - y[1] + 1) > cutoff.eli - offset) {
                            earlystop = 1
                            break
                        }
                    }
                }
            }
            if (y[d] <= b.e[n[d]] && d != ndose) {
                if (elimi[d + 1] == 0) {
                    d = d + 1
                }
            }
            else if (y[d] >= b.d[n[d]] && d != 1) {
                d = d - 1
            }
            else {
                d = d
            }
        }
        Y[trial, ] = y
        N[trial, ] = n
        if (earlystop == 1) {
            dselect[trial] = 99
        }
        else {
            dselect[trial] = select.mtd.kb(target, n, y)$MTD
        }
    }

    selpercent = rep(0, ndose)
    nptsdose = apply(N, 2, mean)
    ntoxdose = apply(Y, 2, mean)
    for (i in 1:ndose) {
        selpercent[i] = sum(dselect == i)/ntrial * 100
    }

    if (length(which(p.true == target)) > 0) {
        nmtd=nptsdose[p.true==target]
        pcs=selpercent[p.true==target]
        poor_dosing=mean(N[, p.true == target] < npts/ndose) * 100
        if (which(p.true==target)== ndose-1) {
            overdosing60=mean(N[,p.true>target]>0.6*npts)*100
            overdosing70=mean(N[,p.true>target]>0.7*npts)*100
            overdosing80=mean(N[,p.true>target]>0.8*npts)*100
        }
        else {
            overdosing60=mean(rowSums(N[,p.true>target])>0.6*npts) * 100
            overdosing70=mean(rowSums(N[,p.true>target])>0.7*npts) * 100
            overdosing80=mean(rowSums(N[,p.true>target])>0.8*npts) * 100
        }
        out = list(selpercent=selpercent, npatients=nptsdose, ntox=ntoxdose,
                   totaltox=sum(Y)/ntrial, totaln=sum(N)/ntrial, percentstop=sum(dselect == 99)/ntrial*100,
                   overdose60=overdosing60, overdose80=overdosing80,
                   # poorallocation=mean(N[, p.true==target]<npts/ndose)*100,
                   simu.setup=data.frame(target=target, p.true=p.true, ncohort=ncohort,
                                         cohortsize = cohortsize, startdose = startdose,
                                         marginL = marginL, marginR = marginR,
                                         cutoff.eli = cutoff.eli, extrasafe = extrasafe, offset = offset,
                                         ntrial = ntrial, dose=1:ndose));
    }
    else {
        out = list(selpercent=selpercent, npatients=nptsdose, ntox=ntoxdose,
                   totaltox=sum(Y)/ntrial, totaln=sum(N)/ntrial, percentstop=sum(dselect == 99)/ntrial*100,
                   simu.setup=data.frame(target=target, p.true=p.true, ncohort=ncohort,
                                         cohortsize = cohortsize, startdose = startdose,
                                         marginL = marginL, marginR = marginR,
                                         cutoff.eli = cutoff.eli, extrasafe = extrasafe, offset = offset,
                                         ntrial = ntrial, dose=1:ndose));
    }
    return(out)
}

Try the Keyboard package in your browser

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

Keyboard documentation built on Aug. 11, 2022, 5:08 p.m.