demo/robustMAP.R

#' ---
#' title: "Using RBesT to reproduce Schmidli et al. \"Robust MAP Priors\""
#' author: "Sebastian Weber"
#' date: "`r Sys.Date()`"
#' output: rmarkdown::html_vignette
#' vignette: >
#'   %\VignetteIndexEntry{Using RBesT to reproduce Schmidli et al. "Robust MAP Priors"}
#'   %\VignetteEngine{knitr::rmarkdown}
#'   %\VignetteDepends{foreach}
#'   %\VignetteDepends{reshape2}
#' ---
#'
#' The following script demonstrates the ` RBesT` library to reproduce the
#' main results from Schmidli et al., Biometrics 70, 1024, 2014.
#'
#' The two main ideas of the paper are
#'
#' 1. use mixture priors to approximate accuratley numerical MCMC MAP
#'    priors
#'
#' 2. robustify informative MAP priors by adding a suitable
#'    non-informative component to the informative MAP prior
#'
#' As example an adaptive design for a binomial endpoint is considered:
#'
#' - Stage 1: mI in test treatment and nI in control (e.g., mI = 20, nI = 15);
#' - Stage 2: (m - mI ) in test treatment and max(n - ESSI, nmin) in control (e.g., nmin = 5).
#'
#' To render a report with all the figures, please run:
#'
#' - ` rmarkdown::render("robustMAP.R")`
#'
#' - ` rmarkdown::render("robustMAP.R", "pdf_document")` (for a pdf version)
#'

library(foreach)
library(ggplot2)
library(dplyr)
library(tidyr)
library(RBesT)
library(knitr)
theme_set(theme_bw())
knitr::opts_chunk$set(
  fig.width = 7,
  fig.height = 4
)

#'
#' ## Operating Characteristics, Table 1
#'

#' the different priors
map <- list()
map$beta <- mixbeta(c(1.0, 4, 16))
map$mix90 <- mixbeta(c(0.9, 4, 16), c(0.1, 1, 1))
## map$mix70 <- mixbeta(c(0.7, 4, 16), c(0.3, 1, 1))
map$mix50 <- mixbeta(c(0.5, 4, 16), c(0.5, 1, 1))
## map$mix30 <- mixbeta(c(0.3, 4, 16), c(0.7, 1, 1))
## map$mix10 <- mixbeta(c(0.1, 4, 16), c(0.9, 1, 1))
map$unif <- mixbeta(c(1.0, 1, 1))

unif <- map$unif

## for the adaptive design the calculation is a bit involved as we
## have to calculate all possible ctl sample sizes which is determined
## by the ESS at the intermediate

OC_adaptBinary2S <- function(
  N1,
  Ntarget,
  Nmin,
  M,
  ctl.prior,
  treat.prior,
  pc,
  pt,
  decision
) {
  ## calculate the different possible ESS values which we get after
  ## stage1
  r1 <- 0:N1
  ESSstage1 <- vector("double", N1 + 1)
  for (r in r1) {
    ESSstage1[r + 1] <- round(ess(
      postmix(ctl.prior, r = r, n = N1),
      method = "morita"
    ))
  }

  ## number of patients enrolled in stage 2
  N2 <- pmax(Ntarget - ESSstage1, Nmin)

  ## total number of patients enrolled
  N <- N1 + N2

  P <- try(data.frame(pc = pc, pt = pt))
  if (inherits(P, "try-error")) {
    stop("pc and pt need to be of same size")
  }

  ## calculate for each scenario and sample size of the control the
  ## power
  power_all <- matrix(0, N1 + 1, nrow(P))
  for (r in r1) {
    ## power_all[r+1,] <- OC_binary2S(N[r+1], M, ctl.prior, treat.prior, P$pc, P$pt, crit)
    design_calc <- oc2S(treat.prior, ctl.prior, M, N[r + 1], decision)
    power_all[r + 1, ] <- design_calc(P$pt, P$pc)
  }

  ## finally take the mean with the respective weight which corresponds
  ## to the weight how the respective sample size occur
  w <- sapply(P$pc, function(p) dbinom(r1, N1, p))

  data.frame(power = colSums(power_all * w), samp = colSums(w * N))
}


## minimum of patients enrolled in stage 2
Nmin <- 5
## number of patients enrolled to control in stage 1
N1 <- 15
## target number of patients overall
Ntarget <- 40
## target number of patients in treatment group
M <- 40

## decision function: P(x1 - x2 > 0) > 0.975
dec <- decision2S(0.975, 0, lower.tail = FALSE)

cases <- expand.grid(
  prior = names(map),
  pc = seq(0.1, 0.6, by = 0.1),
  delta = c(0, 0.3)
)

## the mixture cases have a varying ess at the interim and need the
## adaptive function ...
cases.mix <- grep("mix", names(map), value = TRUE)
cases.fix <- grep("mix", names(map), value = TRUE, invert = TRUE)

resMix <- foreach(i = cases.mix, .combine = rbind) %do%
  {
    design <- subset(cases, prior == i)
    cbind(
      design,
      OC_adaptBinary2S(
        N1,
        Ntarget,
        Nmin,
        M,
        map[[i]],
        unif,
        design$pc,
        design$pc + design$delta,
        dec
      )
    )
  }

## ... for the non-mixture priors the ESS is fixed at the intermediate
## step such that the much faster oc2S can be used directly
resFix <- foreach(i = cases.fix, .combine = rbind) %do%
  {
    design <- subset(cases, prior == i)
    prior <- map[[i]]
    Nc <- Ntarget - round(ess(prior, method = "morita"))
    design_calc <- oc2S(unif, prior, M, Nc, dec)
    cbind(
      design,
      power = design_calc(design$pc + design$delta, design$pc),
      samp = Nc
    )
  }

powerTable <- rbind(resMix, resFix)

P <- expand.grid(pc = c(0.2, 0.3, 0.4, 0.5), pt = seq(0.05, 0.95, by = 0.025))

powerMix <- foreach(i = cases.mix, .combine = rbind) %do%
  {
    cbind(
      P,
      prior = i,
      OC_adaptBinary2S(N1, Ntarget, Nmin, M, map[[i]], unif, P$pc, P$pt, dec)
    )
  }

powerFix <- foreach(i = cases.fix, .combine = rbind) %do%
  {
    prior <- map[[i]]
    Nc <- Ntarget - round(ess(prior, method = "morita"))
    design_calc <- oc2S(unif, prior, M, Nc, dec)
    cbind(P, prior = i, power = design_calc(P$pt, P$pc), samp = Nc)
    ## cbind(P, prior=i, power=OC_binary2S(Nc, M, prior, unif, P$pc, P$pt, dec), samp=Nc)
  }

power <- rbind(powerMix, powerFix)


ocAdapt <- powerTable[, -ncol(powerTable)] %>%
  unite(case, delta, prior) %>%
  transform(power = 100 * power) %>%
  spread(case, power)
ocAdaptSamp <- powerTable[, -(ncol(powerTable) - 1)] %>%
  unite(case, delta, prior) %>%
  spread(case, samp)

kable(ocAdapt, digits = 1, caption = "Type I error and power")

kable(ocAdaptSamp, digits = 1, caption = "Sample size")

#'
#' ## Additional power Figure under varying pc
#'

ggplot(power, aes(pt - pc, power, colour = prior)) +
  geom_line() +
  facet_wrap(~pc) +
  scale_y_continuous(breaks = seq(0, 1, by = 0.2)) +
  scale_x_continuous(breaks = seq(-0.8, 0.8, by = 0.2)) +
  coord_cartesian(xlim = c(-0.15, 0.5)) +
  geom_hline(yintercept = 0.025, linetype = 2) +
  geom_hline(yintercept = 0.8, linetype = 2) +
  ggtitle("Prob. for alternative for different pc")

#'
#' ## Bias and rMSE, Figure 1
#'
#' Reproduction of Fig. 1 in Robust MAP Prior paper.
#'

plot(map$beta, prob = 1)
plot(map$mix50)

#' The bias and rMSE calculations are slightly involved as the sample
#' size depends on the first stage.

est <- foreach(case = names(map), .combine = rbind) %do%
  {
    ## prior to consider
    prior <- map[[case]]

    ## calculate the different possible ESS values which we get after
    ## stage1
    r1 <- 0:N1
    ESSstage1 <- c()
    for (r in r1) {
      ESSstage1 <- c(
        ESSstage1,
        round(ess(
          postmix(prior, r = r, n = N1),
          method = "morita",
          loc = "mode"
        ))
      )
    }

    ## number of patients enrolled in stage 2
    N2 <- pmax(Ntarget - ESSstage1, 5)

    ## total number of patients enrolled
    N <- N1 + N2

    ## we need the maximal possible number of patients
    Nmax <- max(N)

    ## calculate for each i = 0 to N1 possible responders in stage one
    ## the posterior when observing 0 to N[i] in total. Calculate for
    ## each scenario outcome E(p) and E(p^2)
    m <- matrix(0, N1 + 1, Nmax + 1)
    m2 <- matrix(0, N1 + 1, Nmax + 1)
    for (i in seq_along(N)) {
      n <- N[i]
      for (r in 0:n) {
        res <- summary(postmix(prior, r = r, n = n))[c("mean", "sd")]
        m[i, r + 1] <- res["mean"]
        m2[i, r + 1] <- res["sd"]^2 + m[i, r + 1]^2
      }
    }

    ## now collect the terms correctly weighted for each assumed true rate
    bias <- rMSE <- c()
    pt <- seq(0, 1, length = 101)
    for (p in pt) {
      ## weight for each possible N at stage 1
      wnp <- dbinom(0:N1, N1, p)

      ## E(p) and E(p^2) for each possible N at stage 1
      Mnm <- rep(0, N1 + 1)
      Mnm2 <- rep(0, N1 + 1)

      ## for a given weight at stage 1....
      for (i in seq(N1 + 1)) {
        n <- N[i]
        ## weights of possible outcomes when having n draws in
        ## stage1, we go up to Nmax+1 to get a vector of correct
        ## length; all entries above n are set to 0 from dbinom as
        ## expected as we can never observe more counts than the
        ## number of trials...
        wp <- dbinom(0:Nmax, n, p)

        Mnm[i] <- sum(m[i, ] * wp)
        Mnm2[i] <- sum(m2[i, ] * wp)
      }

      ## ... which we average over possible outcomes in stage 1
      Mm <- sum(wnp * Mnm)
      Mm2 <- sum(wnp * Mnm2)

      bias <- c(bias, (Mm - p))
      rMSE <- c(rMSE, sqrt(Mm2 - 2 * p * Mm + p^2))
    }
    data.frame(p = pt, bias = bias, rMSE = rMSE, prior = case)
  }


ggplot(est, aes(p, 100 * bias, colour = prior)) +
  geom_line() +
  ggtitle("Bias")
ggplot(est, aes(p, 100 * rMSE, colour = prior)) +
  geom_line() +
  ggtitle("rMSE")


#'
#' ##  Ulcerative colitis example
#'
#' Clinical example to exemplify the methodology.
#'

## set seed to guarantee exact reproducible results
set.seed(25445)

map <- gMAP(
  cbind(r, n - r) ~ 1 | study,
  family = binomial,
  data = colitis,
  tau.dist = "HalfNormal",
  beta.prior = 2,
  tau.prior = 1
)

map_auto <- automixfit(map)

## advanced: look at AIC of all fitted models
sapply(attr(map_auto, "models"), AIC)

print(map_auto)

## use best fitting mixture model as prior
prior <- map_auto

pl <- plot(prior)
pl$mix + ggtitle("MAP prior for ulcerative colitis")


#'
#' Colitis MAPs from paper for further figures.
#'

mapCol <- list(
  one = mixbeta(c(1, 2.3, 16)),
  two = mixbeta(c(0.77, 6.2, 50.8), c(1 - 0.77, 1.0, 4.7)),
  three = mixbeta(c(0.53, 2.5, 19.1), c(0.38, 14.6, 120.2), c(0.08, 0.9, 2.9))
)
mapCol <- c(
  mapCol,
  list(
    twoRob = robustify(mapCol$two, weight = 0.1, mean = 1 / 2),
    threeRob = robustify(mapCol$three, weight = 0.1, mean = 1 / 2)
  )
)

#'
#' Posterior for different remission rates, Figure 3
#'

N <- 20
post <- foreach(prior = names(mapCol), .combine = rbind) %do%
  {
    res <- data.frame(mean = rep(NA, N + 1), sd = 0, r = 0:N)
    for (r in 0:N) {
      res[r + 1, 1:2] <- summary(postmix(mapCol[[prior]], r = r, n = N))[c(
        "mean",
        "sd"
      )]
    }
    res$prior <- prior
    res
  }

ggplot(post, aes(r, mean, colour = prior, shape = prior)) +
  geom_point() +
  geom_abline(slope = 1 / 20)
ggplot(post, aes(r, sd, colour = prior, shape = prior)) +
  geom_point() +
  coord_cartesian(ylim = c(0, 0.17))

sessionInfo()

Try the RBesT package in your browser

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

RBesT documentation built on March 13, 2026, 5:06 p.m.