Nothing
## ----settings-knitr, include=FALSE--------------------------------------------
library(ggplot2)
knitr::opts_chunk$set(echo = TRUE, message = FALSE, cache = FALSE,
comment = NA,
dev = "png", dpi = 150, fig.asp = 0.618, fig.width = 7, out.width = "85%", fig.align = "center")
options(rmarkdown.html_vignette.check_title = FALSE)
theme_set(theme_bw())
## ----message = FALSE----------------------------------------------------------
library(DoseFinding)
library(ggplot2)
library(survival)
## set seed and ensure reproducibility across R versions
set.seed(2010, kind = "Mersenne-Twister", sample.kind = "Rejection", normal.kind = "Inversion")
## -----------------------------------------------------------------------------
doses <- c(0, 0.05, 0.2, 0.5, 1) # placebo and 4 treatment groups
lambda0 <- 0.6 # yearly hazard rate for the placebo group
HR1 <- 0.5 # hazard ratio for the treatment effect at the highest dose
lambda1 <- lambda0 * HR1 # hazard rate for the highest dose
alRatio <- c(2, 1, 1, 1, 2) # allocation ratio across the dose groups
## ----fig.width = 8, out.width = '100%'----------------------------------------
mods <- Mods(
emax = c(0.1, 0.5),
sigEmax = c(0.5, 4),
quadratic = -0.9,
exponential = 0.2,
doses = doses,
direction = "decreasing", # decreasing (lower hazard) is beneficial
placEff = log(lambda0), # log hazard rate of the placebo arm
maxEff = log(lambda1) - log(lambda0) # log hazard ratio at the highest dose
)
plotMods(mods, superpose = TRUE, ylab = "log(hazard rate)")
## -----------------------------------------------------------------------------
#' @param mods `Mods` object defining the candidate models to simulate from
#' @param model name or index of the model whose hazards are used to simulate
#' (refers to column names or column indices of getResp(mods))
#' @param alRatio allocation ratio across the dose groups
#' @param n total sample size
#' @param etotal number of events at which event-driven censoring is applied
#' @param staggered logical, whether to use staggered study entry
#' @return data.frame with columns group, time, status
simulate_surv_data <- function(mods, model, alRatio, n, etotal,
staggered = FALSE) {
doses <- attr(mods, "doses")
y <- getResp(mods)
lambda0 <- exp(attr(mods, "placEff"))
## assign each patient to a dose group (log hazard rate) according to alRatio
log_rate_resp <- rep(y[, model], rndDesign(alRatio/sum(alRatio), n))
## entry (calendar) times
entry <- if (staggered) {
runif(n, 0, qexp(etotal / n, lambda0))
} else {
rep(0, n)
}
## event times on the calendar-time scale
event <- entry + rexp(n, exp(log_rate_resp))
## event-driven censoring at the time of the etotal-th event
t_cut <- sort(event)[etotal]
status <- as.numeric(event <= t_cut)
obs_time <- pmin(event, t_cut) - entry # follow-up on the study-time scale
data.frame(
group = factor(names(log_rate_resp), levels = as.character(doses)),
time = obs_time,
status = status
)
}
## -----------------------------------------------------------------------------
n <- 300
dat <- simulate_surv_data(
mods, model = 1, alRatio = alRatio, n = n, etotal = 130,
staggered = TRUE
)
head(dat)
## ----fig.width = 6, out.width = '100%'----------------------------------------
km <- survfit(Surv(time, status) ~ group, data = dat)
plot(km, col = seq_along(doses), lwd = 2,
xlab = "Years", ylab = "Survival probability",
main = "Kaplan-Meier estimator by dose")
legend("topright", legend = paste("dose", doses),
col = seq_along(doses), lwd = 2, bty = "n")
## -----------------------------------------------------------------------------
coxfit <- coxph(Surv(time, status) ~ group, data = dat)
summary(coxfit)
coef_cox <- coef(coxfit) # placebo-adjusted log hazard ratios
S_cox <- vcov(coxfit) # covariance matrix of the estimates
## -----------------------------------------------------------------------------
mct <- MCTtest(
dose = doses[-1], # placebo-adjusted, so drop placebo
resp = coef_cox,
S = S_cox,
models = mods,
type = "general",
alternative = "one.sided",
placAdj = TRUE
)
mct
## -----------------------------------------------------------------------------
one_bootstrap_prediction <- function(mu_hat, S_hat, doses, bounds, dose_seq) {
sim <- drop(mvtnorm::rmvnorm(1, mu_hat, S_hat))
fit <- lapply(c("emax", "sigEmax", "exponential", "quadratic"), function(mod)
fitMod(doses, sim, model = mod, S = S_hat, type = "general", bnds = bounds[[mod]], placAdj = TRUE))
index <- which.min(sapply(fit, gAIC))
pred <- predict(fit[[index]], doseSeq = dose_seq, predType = "effect-curve")
return(pred)
}
## bs_predictions is a doses x replications matrix,
## probs is a 4-element vector of increasing probabilities for the quantiles
summarize_predictions <- function(bs_predictions, probs) {
stopifnot(length(probs) == 4)
med <- apply(bs_predictions, 1, median)
quants <- apply(bs_predictions, 1, quantile, probs = probs)
bs_df <- as.data.frame(cbind(med, t(quants)))
names(bs_df) <- c("median", "low_out", "low_in", "high_in", "high_out")
return(bs_df)
}
predict_and_plot <- function(mu_hat, S_hat, doses, dose_seq, n_rep) {
bs_rep <- replicate(
n_rep, one_bootstrap_prediction(mu_hat, S_hat, doses, defBnds(max(doses)), dose_seq))
bs_summary <- summarize_predictions(bs_rep, probs = c(0.025, 0.25, 0.75, 0.975))
bs_summary <- as.data.frame(exp(bs_summary)) # back to hazard ratio scale
ci_half_width <- qnorm(0.975) * sqrt(diag(S_hat))
cox_summary <- data.frame(dose = doses, mu_hat = exp(mu_hat),
low = exp(mu_hat - ci_half_width),
high = exp(mu_hat + ci_half_width))
gg <- ggplot(cbind(bs_summary, dose_seq = dose_seq)) + geom_line(aes(dose_seq, median)) +
geom_ribbon(aes(x = dose_seq, ymin = low_in, ymax = high_in), alpha = 0.2) +
geom_ribbon(aes(x = dose_seq, ymin = low_out, ymax = high_out), alpha = 0.2) +
geom_point(aes(dose, mu_hat), cox_summary) +
scale_y_continuous(breaks = seq(0.5, 1.5, 0.25)) +
geom_errorbar(aes(dose, ymin = low, ymax = high), cox_summary, width = 0, alpha = 0.5) +
xlab("Dose") + ylab("Hazard ratio") +
labs(title = "Bootstrap estimates for population hazard ratio",
subtitle = "confidence levels 50% and 95%")
return(gg)
}
## -----------------------------------------------------------------------------
predict_and_plot(coef_cox, S_cox, doses[-1], dose_seq = seq(0, 1, 0.01), 1000)
## -----------------------------------------------------------------------------
#' Power of the multiple contrast test for a time-to-event endpoint
#'
#' @param mods Set of dose-response models; an object of class `Mods`.
#' @param alpha One-sided significance level.
#' @param alRatio Positive allocation weights for all treatment groups,
#' including placebo.
#' @param etotal Positive number of events.
#'
#' @return A list containing:
#' - `power_table`: power under each candidate model and the average power;
#' - `power`: unrounded power under each candidate model;
#' - `power_av`: unrounded average power;
#' - `contMat`: matrix of optimal contrasts;
#' - `critV`: critical value for the multiple contrast test.
powMCT_TTE <- function(mods, alpha, alRatio, etotal) {
y <- getResp(mods)
doses <- attr(mods, "doses")
n_group <- nrow(y)
n_model <- ncol(y)
n_active <- n_group - 1
model_names <- colnames(y)
if (is.null(model_names)) {
model_names <- paste0("model", seq_len(n_model))
}
# Placebo-adjusted log hazard ratios.
beta <- sweep(y, 2, y[1, ], FUN = "-")
# Group probabilities evaluated at half the assumed treatment effect.
weighted_hazards <- exp(0.5 * beta) * alRatio
p <- sweep(
weighted_hazards,
2,
colSums(weighted_hazards),
FUN = "/"
)
# Covariance matrices under each candidate alternative.
S <- array(
0,
dim = c(n_active, n_active, n_model),
dimnames = list(
as.character(doses[-1]),
as.character(doses[-1]),
model_names
)
)
for (i in seq_len(n_model)) {
p0 <- p[1, i]
S_i <- matrix(
1 / p0,
nrow = n_active,
ncol = n_active
)
diag(S_i) <- 1 / p[-1, i] + 1 / p0
S[, , i] <- S_i / etotal
}
# Covariance matrix under the null hypothesis.
p_null <- alRatio / sum(alRatio)
p0_null <- p_null[1]
S0 <- matrix(
1 / p0_null,
nrow = n_active,
ncol = n_active
)
diag(S0) <- 1 / p_null[-1] + 1 / p0_null
S0 <- S0 / etotal
# Obtain the optimal contrast for each candidate model using the
# covariance matrix associated with that model.
contMat <- vapply(
seq_len(n_model),
FUN = function(i) {
contrast_i <- optContr(
mods,
doses = doses[-1],
S = S[, , i],
placAdj = TRUE
)[[1]]
contrast_i[, i]
},
FUN.VALUE = numeric(n_active)
)
dimnames(contMat) <- list(
as.character(doses[-1]),
model_names
)
# The null correlation matrix and critical value do not depend on which
# candidate model is treated as the true model.
covMat0 <- crossprod(contMat, S0 %*% contMat)
corMat0 <- cov2cor(covMat0)
integration_control <- mvtnorm.control()
critV <- critVal(
corMat0,
alpha,
df = 0,
alternative = "one.sided",
control = integration_control
)
model_power <- numeric(n_model)
for (i in seq_len(n_model)) {
model_power[i] <- powMCT(
contMat,
alpha = alpha,
altModels = mods,
S = S[, , i],
placAdj = TRUE,
alternative = "one.sided",
df = Inf,
critV = critV,
control = integration_control
)[i]
}
average_power <- mean(model_power)
power_table <- data.frame(
model = c(model_names, "average"),
power = round(c(model_power, average_power), 3),
row.names = NULL
)
list(
power_table = power_table,
power = stats::setNames(model_power, model_names),
power_av = average_power,
contMat = contMat,
critV = critV
)
}
#' Required number of events for a target power
#'
#' @param mods Set of dose-response models; an object of class `Mods`.
#' @param alpha One-sided significance level.
#' @param alRatio Positive allocation weights for all treatment groups,
#' including placebo.
#' @param targPow Target average power.
#' @param lowerE Lower bound for the number of events.
#' @param upperE Upper bound for the number of events.
#'
#' @return A list containing:
#' - `events`: smallest integer event count attaining the target power;
#' - `power`: average power at that event count;
#' - `power_table`: model-specific powers at that event count;
#' - `target_power`: requested average power.
eventsMCT_TTE <- function(mods,
alpha,
alRatio,
targPow,
lowerE = 1,
upperE = 1000) {
# alRatio = 1 makes sampSize() iterate the total number of events directly;
# targFunc still uses the alRatio argument of eventsMCT_TTE (lexical scoping)
events <- sampSize(
upperN = upperE,
lowerN = lowerE,
targFunc = function(e) {
powMCT_TTE(mods = mods, alpha = alpha, alRatio = alRatio, etotal = e)$power_av
},
target = targPow,
alRatio = 1,
Ntype = "total"
)$samp.size
final_result <- powMCT_TTE(
mods = mods,
alpha = alpha,
alRatio = alRatio,
etotal = events
)
list(
events = events,
power = final_result$power_av,
power_table = final_result$power_table,
target_power = targPow
)
}
## -----------------------------------------------------------------------------
powMCT_TTE(mods, alpha = 0.025, alRatio = alRatio, etotal = 120)
## -----------------------------------------------------------------------------
eventsMCT_TTE(mods, alpha = 0.025, alRatio = alRatio, targPow = 0.8, lowerE = 100, upperE = 300)
## -----------------------------------------------------------------------------
#' Required sample size for target average power with a time-to-event endpoint
#'
#' For each candidate model, the expected number of events is calculated from
#' that model's hazards. Model-specific powers are then evaluated at the
#' corresponding expected event counts and averaged across candidate models.
#'
#' @param mods Set of dose-response models; an object of class `Mods`.
#' @param alpha One-sided significance level.
#' @param alRatio Positive allocation ratios for all treatment groups,
#' including placebo.
#' @param targPow Target average power across candidate models.
#' @param end_rec End of the recruitment period, in years.
#' @param end_stu End of the study, in years.
#' @param lowerN Lower bound for the total sample size.
#' @param upperN Upper bound for the total sample size.
#'
#' @return A list containing:
#' - `sampSize`: smallest integer sample size attaining the target average power;
#' - `power`: average power at the returned sample size;
#' - `model_power`: model-specific powers at the returned sample size;
#' - `expected_events`: expected events under each candidate model;
#' - `event_probability`: overall event probability under each candidate model;
#' - `event_probability_by_group`: group-specific event probabilities;
#' - `allocation`: allocation proportions;
#' - `power_table`: model-specific expected events and powers;
#' - `target_power`: requested average power.
sampSizeMCT_TTE <- function(mods,
alpha,
alRatio,
targPow,
end_rec,
end_stu,
lowerN = 1,
upperN = 1000) {
y <- as.matrix(getResp(mods))
n_group <- nrow(y)
n_model <- ncol(y)
model_names <- colnames(y)
if (is.null(model_names)) {
model_names <- paste0("model", seq_len(n_model))
}
allocation <- alRatio / sum(alRatio)
# Each column contains the group-specific hazards under one candidate model.
lambda <- exp(y)
# Group-specific probability of observing an event by the end of the study.
#
# This is equivalent to
#
# 1 - (
# exp(-lambda * (end_stu - end_rec)) -
# exp(-lambda * end_stu)
# ) / (lambda * end_rec)
#
# but expm1() is more stable when lambda * end_rec is small.
event_probability_by_group <-
1 -
exp(-lambda * (end_stu - end_rec)) *
(-expm1(-lambda * end_rec)) /
(lambda * end_rec)
# Overall event probability under each candidate model.
event_probability <- colSums(
sweep(
event_probability_by_group,
MARGIN = 1,
STATS = allocation,
FUN = "*"
)
)
names(event_probability) <- model_names
colnames(event_probability_by_group) <- model_names
# Evaluate average power for a specified integer sample size.
evaluate_sample_size <- function(n) {
expected_events <- n * event_probability
model_power <- vapply(
seq_len(n_model),
FUN = function(j) {
power_result <- powMCT_TTE(
mods = mods,
alpha = alpha,
alRatio = alRatio,
etotal = expected_events[j]
)
unname(power_result$power[j])
},
FUN.VALUE = numeric(1)
)
names(expected_events) <- model_names
names(model_power) <- model_names
list(
expected_events = expected_events,
model_power = model_power,
power_av = mean(model_power)
)
}
# alRatio = 1 makes sampSize() iterate the total sample size directly;
# evaluate_sample_size() still uses the alRatio argument of sampSizeMCT_TTE
required_n <- sampSize(
upperN = upperN,
lowerN = lowerN,
targFunc = function(n) evaluate_sample_size(n)$power_av,
target = targPow,
alRatio = 1,
Ntype = "total"
)$samp.size
final_result <- evaluate_sample_size(required_n)
power_table <- data.frame(
model = c(model_names, "average"),
expected_events = c(
unname(final_result$expected_events),
mean(final_result$expected_events)
),
power = c(
unname(final_result$model_power),
final_result$power_av
),
row.names = NULL
)
list(
sampSize = required_n,
power = final_result$power_av,
model_power = final_result$model_power,
expected_events = final_result$expected_events,
event_probability = event_probability,
event_probability_by_group = event_probability_by_group,
allocation = allocation,
power_table = power_table,
target_power = targPow
)
}
## -----------------------------------------------------------------------------
sampSizeMCT_TTE(mods, alpha = 0.025, alRatio = alRatio, end_rec = 2, end_stu = 4, targPow = 0.8, lowerN = 100, upperN = 300)
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.