Skip to content

Commit 9d6b139

Browse files
committed
Fixes to MMRE functionality. bmdBoot now works for drmMMRE models.
1 parent 7d29748 commit 9d6b139

11 files changed

Lines changed: 418 additions & 49 deletions

File tree

R/bmd.R

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
#' Details on the implemented definitions and methods can be found in Crump
1212
#' (2002)
1313
#'
14-
#' @param object object of class \code{drc}
14+
#' @param object object of class \code{drc} or \code{drcMMRE}
1515
#' @param bmr numeric value of benchmark response level for which to calculate
1616
#' the benchmark dose
1717
#' @param backgType character string specifying how the background level is
@@ -169,6 +169,16 @@
169169
#' ## BMD using the hybrid method, background risk is 2 SD, hybrid definition using excess risk
170170
#' bmd(ryegrass.m1, 0.05, backg = 2, backgType = "hybridSD", def = "hybridAdd", display = TRUE)
171171
#'
172+
#' ## BMD on meta-analytic random effects model
173+
#' set.seed(1)
174+
#' data0 <- data.frame(x = rep(drcData::ryegrass$conc, 2),
175+
#' y = rep(drcData::ryegrass$rootl, 2) +
176+
#' c(rnorm(n = nrow(drcData::ryegrass), mean = 2, sd = 0.5),
177+
#' rnorm(n = nrow(drcData::ryegrass), mean = 2.7, sd = 0.7)),
178+
#' EXP_ID = rep(as.character(1:2), each = nrow(drcData::ryegrass)))
179+
#'
180+
#' modMMRE <- drmMMRE(y~x, exp_id = EXP_ID, data = data0, fct = LL.4())
181+
#' bmd(modMMRE, bmr = 0.1, backgType = "modelBased", def = "relative")
172182
#'
173183
#' @export
174184
bmd<-function(object, bmr, backgType = c("modelBased", "absolute", "hybridSD", "hybridPercentile"),

R/bmdBoot.R

Lines changed: 39 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,27 @@
55
#'
66
#' BMDL is defined as the 5th percentile in the bootstrap distribution.
77
#'
8-
#' Bootstrapping with the argument boot = "nonparametric" is done by sampling
8+
#' Bootstrapping with the argument bootType = "nonparametric" is done by sampling
99
#' with replacement from the original data set. Bootstrapping with the argument
10-
#' boot = "parametric" is done by sampling from norm(mean(Y_i),sd(Y_0)),
10+
#' bootType = "parametric" is done by sampling from norm(mean(Y_i),sd(Y_0)),
1111
#' assuming equal variance between groups, in case of continuous data. For
1212
#' binomial data, each bootstrap data set is sampled from binom(N_i,Y_i/N_i).
1313
#' In case of Y_i = 0 or Y_i = N_i shrinkage is used to avoid that the
1414
#' resampling always produces 0 or 1, respectively. In this case data is
1515
#' sampled from binom(N_i,(Y_i+1/4)/(N_i+1/2)). Bootstrapping with argument
16-
#' boot = "semiparametric" is done by sampling with replacement from the
17-
#' residuals.
16+
#' bootType = "semiparametric" is done by sampling with replacement from the
17+
#' residuals. Bootstrapping with argument bootType = "wild" is done by resampling
18+
#' with replacement from the residuals multiplied by a random sign (-1 or +1).
1819
#'
19-
#' All sampling is made within dose groups.
20+
#' All sampling is made within dose groups. When a meta-analytic random effects
21+
#' model is supplied, sampling is made within dose groups within each experiment.
2022
#'
21-
#' @param object object of class \code{drc}
23+
#' @param object object of class \code{drc} or \code{drcMMRE}
2224
#' @param bmr numeric value of benchmark response level for which to calculate
2325
#' the benchmark dose
2426
#' @param R number of bootstrap samples. Default is 1000
2527
#' @param bootType character string specifying type of bootstrap used.
26-
#' "nonparametric" (default), "semiparametric" or "parametric". "Semiparametric
28+
#' "nonparametric" (default), "semiparametric", "parametric" or "wild". "semiparametric" and "wild"
2729
#' is only available for continuous data and "nonparametric" is at present the
2830
#' only option for count data. See details below
2931
#' @param bmdType Type of estimate for BMD. Default is "orig" the bmd estimate
@@ -102,10 +104,10 @@
102104
#' and adjusted)
103105
#' @param display logical. If TRUE the results are displayed; otherwise they
104106
#' are not
105-
#' @param level numeric value specifying the levle of the confidence interval
107+
#' @param level numeric value specifying the level of the confidence interval
106108
#' underlying BMDL. Default is 0.95
107-
#' @return A list of three elements: Results contain the estimated BMD and
108-
#' BMDL, bootEst is a vector af all of the estimated BMD values from each
109+
#' @return A list of four elements: Results contain the estimated BMD and
110+
#' BMDL, Boot.samples.used is the number of samples used, bootEst is a vector af all of the estimated BMD values from each
109111
#' bootstrap sample, Interval gives BMDL and BMDU, which is identical to the
110112
#' confidence interval for the percentile interval approach.
111113
#' @author Signe M. Jensen
@@ -125,6 +127,17 @@
125127
#' ## BMD from the same definitions but using parametric bootstrap
126128
#' bmdBoot(ryegrass.m1, 0.05, backgType = "hybridSD", def = "hybridAdd", bootType="parametric",R = 50)
127129
#'
130+
#' ## BMD on meta-analytic random effects model
131+
#' set.seed(1)
132+
#' data0 <- data.frame(x = rep(drcData::ryegrass$conc, 2),
133+
#' y = rep(drcData::ryegrass$rootl, 2) +
134+
#' c(rnorm(n = nrow(drcData::ryegrass), mean = 2, sd = 0.5),
135+
#' rnorm(n = nrow(drcData::ryegrass), mean = 2.7, sd = 0.7)),
136+
#' EXP_ID = rep(as.character(1:2), each = nrow(drcData::ryegrass)))
137+
#'
138+
#' modMMRE <- drmMMRE(y~x, exp_id = EXP_ID, data = data0, fct = LL.4())
139+
#' bmdBoot(modMMRE, bmr = 0.1, backgType = "modelBased", def = "relative", R = 50, bootType = "wild")
140+
#'
128141
#' @export
129142
bmdBoot <- function(object, bmr, R=1000, bootType="nonparametric", bmdType = "orig",
130143
backgType = c("modelBased", "absolute", "hybridSD", "hybridPercentile"),
@@ -183,7 +196,16 @@ bmdBoot <- function(object, bmr, R=1000, bootType="nonparametric", bmdType = "or
183196
interval = "delta", display = FALSE)
184197

185198
get.drm.list <- function(tmp.data){
186-
if(ncol(object$parmMat) == 1){
199+
if(inherits(object, "drcMMRE")){
200+
drm.list.tmp <- lapply(tmp.data, function(x){
201+
try(eval(substitute(drmMMRE(formula0, exp_id = exp_id0, data = x, type = object$type, fct = object[["fct"]]),
202+
list(formula0 = object$call$formula,
203+
exp_id0 = object$call$exp_id)
204+
)), TRUE)
205+
}
206+
)
207+
}
208+
else if(ncol(object$parmMat) == 1){
187209
drm.list.tmp <- lapply(tmp.data, function(x){
188210
try(eval(substitute(drm(formula0, data = x, type = object$type, fct = object[["fct"]],
189211
control = drmc(noMessage = TRUE)),
@@ -193,9 +215,6 @@ bmdBoot <- function(object, bmr, R=1000, bootType="nonparametric", bmdType = "or
193215
)
194216
} else if(is.null(object$call$pmodels)){
195217
drm.list.tmp <- lapply(tmp.data, function(x){
196-
# if(object$type != "binomial"){
197-
# x[[as.character(object$call$curveid)]] <- x[[paste0("orig.", as.character(object$call$curveid))]]
198-
# }
199218
try(
200219
eval(substitute(drm(object$call$formula, weights = weights0, curveid = curveid0,
201220
data = x, type = object$type, fct = object$fct, control = drmc(noMessage = TRUE)),
@@ -206,9 +225,6 @@ bmdBoot <- function(object, bmr, R=1000, bootType="nonparametric", bmdType = "or
206225
})
207226
} else {
208227
drm.list.tmp <- lapply(tmp.data, function(x){
209-
# if(object$type != "binomial"){
210-
# x[[as.character(object$call$curveid)]] <- x[[paste0("orig.", as.character(object$call$curveid))]]
211-
# }
212228
try(
213229
eval(substitute(drm(formula0, weights = weights0, curveid = curveid0, pmodels = pmodels0,
214230
data = x, type = object$type, fct = object$fct, control = drmc(noMessage = TRUE)),
@@ -233,11 +249,14 @@ bmdBoot <- function(object, bmr, R=1000, bootType="nonparametric", bmdType = "or
233249
}
234250

235251
if (object$type %in% c("binomial","continuous")) {
236-
237-
tmp.data <- bootDataGen(object,R,bootType,aggregated=FALSE)
252+
if(inherits(object, "drcMMRE")){
253+
tmp.data <- bootDataGenMMRE(object,R,bootType)
254+
} else {
255+
tmp.data <- bootDataGen(object,R,bootType,aggregated=FALSE)
256+
}
238257

239258
drm.list.tmp <- get.drm.list(tmp.data)
240-
list.condition <- sapply(drm.list.tmp, function(x) class(x)=="drc")
259+
list.condition <- sapply(drm.list.tmp, inherits, "drc")
241260
drm.list <- drm.list.tmp[list.condition]
242261

243262
bmd.list.try <- lapply(drm.list,get.bmd)

R/bootDataGen.R

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,23 @@ bootDataGen <- function(object, R=1000, bootType="nonparametric",aggregated=TRUE
239239
}
240240
}
241241
}
242+
else if(bootType=="wild"){
243+
if(object$type=="binomial"){
244+
stop(paste("wild is not possible for binomial data", sep=""))
245+
}
246+
if(object$type=="continuous"){
247+
data.st<-object$data
248+
249+
tmp.data <- list()
250+
for(i in 1:R){
251+
random.sign <- sample(c(-1,1), size = length(resid(object)), replace = TRUE)
252+
sampled <- data.frame(y = fitted(object)+sample(resid(object),replace=TRUE)*random.sign,
253+
dose = object$data[,as.character(object$call$formula[[3]])])
254+
colnames(sampled) <- c(as.character(object$call$formula[[2]]), as.character(object$call$formula[[3]]))
255+
tmp.data[[i]] <- sampled
256+
}
257+
}
258+
}
242259
tmp.data
243260
}
244261

R/bootDataGenMMRE.R

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#' Generate Bootstrap Data for meta-analytic BMD Analysis
2+
#'
3+
#' @description
4+
#' Helper function for `bmdBoot` that generates bootstrap datasets from fitted
5+
#' dose-response models. Supports multiple bootstrap methods (nonparametric,
6+
#' parametric, semiparametric and wild) and handles different response types
7+
#' (binomial, continuous).
8+
#'
9+
#' @param object A fitted dose-response model object of class "drcMMRE" (typically from `drmMMRE()`)
10+
#' containing the original data and model specifications
11+
#' @param R integer; Number of bootstrap replicates to generate (default: 1000)
12+
#' @param bootType character; Type of bootstrap resampling method. Options are:
13+
#' \itemize{
14+
#' \item \code{"nonparametric"}: Resamples observations within dose groups
15+
#' \item \code{"parametric"}: Generates new data from fitted distributions
16+
#' \item \code{"semiparametric"}: Uses fitted values plus resampled residuals
17+
#' \item \code{"wild"}: Uses fitted values plus resampled residuals
18+
#' }
19+
#'
20+
#' @details
21+
#' The function implements different bootstrap strategies based on data type and method:
22+
#'
23+
#' **Nonparametric Bootstrap:**
24+
#' \itemize{
25+
#' \item \strong{Binomial}: Expands binomial data to individual observations,
26+
#' resamples within dose groups, then optionally re-aggregates
27+
#' \item \strong{Continuous/Count}: Resamples observations within dose groups
28+
#' from the original dataset
29+
#' }
30+
#'
31+
#' **Parametric Bootstrap:**
32+
#' \itemize{
33+
#' \item \strong{Binomial}: Generates new binomial observations using estimated
34+
#' success probabilities, with continuity correction (adds 0.25/0.5) for
35+
#' boundary cases
36+
#' \item \strong{Continuous}: Generates normal random variables using
37+
#' dose-specific means and standard deviations from original data
38+
#' }
39+
#'
40+
#' **Semiparametric Bootstrap:**
41+
#' \itemize{
42+
#' \item \strong{Continuous only}: Uses fitted values plus resampled residuals
43+
#' \item \strong{Binomial}: Not supported (throws error)
44+
#' }
45+
#'
46+
#' @return A list of length R containing bootstrap datasets. Each element is a
47+
#' data.frame with the same structure as the original data, containing:
48+
#' \itemize{
49+
#' \item Dose variable (same name as original)
50+
#' \item Response variable(s) (same name(s) as original)
51+
#' \item For binomial data: number of successes and total observations
52+
#' \item For multi-curve data: curve identifier (if present)
53+
#' }
54+
#'
55+
#' @section Data Type Handling:
56+
#'
57+
#' **Binomial Data:**
58+
#' \itemize{
59+
#' \item Handles both aggregated (n successes out of N trials) and expanded formats
60+
#' \item Preserves dose group structure during resampling
61+
#' \item Applies continuity correction in parametric bootstrap
62+
#' }
63+
#'
64+
#' **Continuous Data:**
65+
#' \itemize{
66+
#' \item Maintains dose group structure
67+
#' \item Preserves within-group variability patterns
68+
#' \item Uses original data (`origData`) when available
69+
#' }
70+
#'
71+
#'
72+
#' @note
73+
#' This is an internal helper function for `bmdBoot`. It assumes the input object
74+
#' has the standard structure from `drmMMRE()` fitting, including components like
75+
#' `data`, `origData`, `call`, `type`, etc.
76+
#'
77+
#' **Important considerations:**
78+
#' \itemize{
79+
#' \item Semiparametric bootstrap requires model residuals and fitted values
80+
#' \item Parametric bootstrap assumes distributional assumptions are met
81+
#' \item Large R values may require substantial memory for complex datasets
82+
#' }
83+
#'
84+
#' @seealso
85+
#' \code{\link{bmdBoot}} for the main bootstrap BMD function,
86+
#'
87+
#' @examples
88+
#' \dontrun{
89+
#' # Typically called internally by bmdBoot, but can be used directly:
90+
#'
91+
#' set.seed(1)
92+
#' data0 <- data.frame(x = rep(drcData::ryegrass$conc, 2),
93+
#' y = rep(drcData::ryegrass$rootl, 2) +
94+
#' c(rnorm(n = nrow(drcData::ryegrass), mean = 2, sd = 0.5),
95+
#' rnorm(n = nrow(drcData::ryegrass), mean = 2.7, sd = 0.7)),
96+
#' EXP_ID = rep(as.character(1:2), each = nrow(drcData::ryegrass)))
97+
#'
98+
#' modMMRE <- drmMMRE(y~x, exp_id = EXP_ID, data = data0, fct = LL.4())
99+
#' boot_data <- bootDataGenMMRE(modMMRE, R = 1000, bootType = "parametric")
100+
#'
101+
#' # Access first bootstrap sample
102+
#' first_sample <- boot_data[[1]]
103+
#' }
104+
bootDataGenMMRE <- function(object, R=1000, bootType=c("nonparametric", "semiparametric", "parametric", "wild")){
105+
if(!inherits(object, "drcMMRE")){
106+
stop("bootDataGenMMRE only works for object of type drcMMRE")
107+
}
108+
109+
R <- as.integer(R)
110+
if(is.na(R)){
111+
stop("R must be a positive integer")
112+
}
113+
if(R<=0){
114+
stop("R must be a positive integer")
115+
}
116+
117+
bootType <- match.arg(bootType)
118+
119+
# resample for each experiment independently, then collect individual resampled datasets
120+
boot.data.per.exp_id <- lapply(names(object$objList),
121+
function(x){
122+
object0 <- object$objList[[x]]
123+
object0$call$formula <- object$call$formula
124+
boot.data.per.exp_id0 <- bootDataGen(object0,R=R, bootType=bootType, aggregated = FALSE)
125+
# add exp_id to data
126+
boot.data.per.exp_id0 <- lapply(boot.data.per.exp_id0,
127+
function(z) {
128+
z[[as.character(object$call$exp_id)]] <- rep(x, nrow(z))
129+
z
130+
}
131+
)
132+
boot.data.per.exp_id0
133+
}
134+
)
135+
# collect individual resampled datasets
136+
tmp.data <- lapply(
137+
1:R,
138+
function(r) do.call(rbind, lapply(boot.data.per.exp_id, function(list) list[[r]]))
139+
)
140+
141+
tmp.data
142+
}
143+
144+

R/drmMMRE.R

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
#'
1818
#' @param formula a symbolic description of the model to be fit of the form
1919
#' 'response ~ dose'
20-
#' @param exp_id the name of the column in the data set that specifies the
20+
#' @param exp_id the column in the data set that specifies the
2121
#' hierarchical structure of the data
2222
#' @param data a data frame containing the variables in the model.
2323
#' @param fct a list with three or more elements specifying the non-linear
@@ -34,7 +34,7 @@
3434
#' structure of class \code{drcMMRE}.
3535
#'
3636
#' The primary objective is to use this model for benchmark dose estimation
37-
#' based on dose-response data with a heterogeneous variance structure.
37+
#' based on dose-response data with a hierarchical variance structure.
3838
#' @author Signe M. Jensen and Jens Riis Baalkilde
3939
#' @keywords models nonlinear
4040
#' @examples
@@ -90,11 +90,6 @@ drmMMRE <- function(formula, exp_id, data, fct, type = c("continuous", "binomial
9090
drc.objList <- lapply(exp_id_unique,
9191
function(exp_id){
9292
drm(formula, data = subset(data, data[[exp_id_char]] == exp_id), fct = fct, type = type)
93-
# eval(substitute(drm(formula = formula0, data = subset(data, data[[exp_id_char]] == x), fct = fct0, type = type0),
94-
# list(formula0 = formula,
95-
# data0 = subset(data, data[[exp_id_char]] == x),
96-
# fct0 = fct,
97-
# type0 = type)))
9893
})
9994
names(drc.objList) <- exp_id_unique
10095

@@ -108,7 +103,7 @@ drmMMRE <- function(formula, exp_id, data, fct, type = c("continuous", "binomial
108103
}
109104
))
110105

111-
# Fit MA_MA_model
106+
# Fit MV_MA_model
112107
MV_MA_model<-tryCatch( expr={metafor::rma.mv(Estimate
113108
,(block_diag_matrix+t(block_diag_matrix))/2
114109
, mods = ~0+Coef
@@ -144,13 +139,9 @@ drmMMRE <- function(formula, exp_id, data, fct, type = c("continuous", "binomial
144139
object$summary <- NULL
145140
object$start <- NULL
146141
for(exp_i in exp_id_unique){
147-
object$predres[data[[exp_id_char]] == exp_i,] <- drc.objList[[exp_i]]$predres
142+
object$predres[data[[exp_id_char]] == exp_i,] <- drc.objList[[as.character(exp_i)]]$predres
148143
}
149144
object$call <- call_expr
150-
#
151-
# object$df.residual <- NA
152-
# object$sumList$df.residual <- NA
153-
# object$deriv1 <- NULL
154145

155146
# add MV_MA_model and drc.objList
156147
object$objList <- drc.objList
@@ -165,7 +156,7 @@ drmMMRE <- function(formula, exp_id, data, fct, type = c("continuous", "binomial
165156
#'
166157
#' @description
167158
#' S3 method to extract the variance-covariance matrix from fitted drcMMRE objects
168-
#' by delegating to the underlying model-averaged model.
159+
#' by delegating to the underlying multivariate meta-analytic model.
169160
#'
170161
#' @param object An object of class "drcMMRE"
171162
#' @param ... Additional arguments (currently unused)

0 commit comments

Comments
 (0)