Skip to content

Commit cb88643

Browse files
Merge pull request #36 from DoseResponse/devel
Version 2.7.5 - Added bootstrap functionality for bmd estimation with drmMMRE models. - Updated documentation for bmdMA (details for count data were missing for def argument). - Added delta method confidence interval for bmdHetVar based on numerical differentiation. - Fixed issue with bmd estimation based on BC model, where maximum of dose-response curve was reached for very small dose values.
2 parents 5c19f49 + f3713bd commit cb88643

21 files changed

Lines changed: 624 additions & 104 deletions

DESCRIPTION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Package: bmd
22
Type: Package
33
Title: Benchmark dose estimation for dose-response data
4-
Version: 2.7.4
4+
Version: 2.7.5
55
Date: 2025-03-24
66
Author: Signe M.Jensen, Christian Ritz and Jens Riis Baalkilde
77
Maintainer: Signe M. Jensen <smj@plen.ku.dk>

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ S3method(print,bmdOrdinal)
1616
S3method(print,drcHetVar)
1717
S3method(print,drcMMRE)
1818
S3method(print,drcOrdinal)
19+
S3method(vcov,drcHetVar)
1920
S3method(vcov,drcMMRE)
2021
export(BCa)
2122
export(MACurve)

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/bmd.edfct.R

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,17 @@ bmd.edfct <- function(object){
213213
expVal <- exp(parmVec[1]*(log(dose)-log(parmVec[4])))
214214
parmVec[5]*(1+expVal*(1-parmVec[1]))-(parmVec[3]-parmVec[2])*expVal*parmVec[1]/dose
215215
}
216-
maxAt <- uniroot(helpEqn, interval)$root
216+
maxAt <- try(uniroot(helpEqn, interval)$root, TRUE)
217+
218+
# solution if maxAt fails
219+
if(inherits(maxAt, "try-error")){
220+
find_maxAt_tries <- 1
221+
while(inherits(maxAt, "try-error") & (find_maxAt_tries<10)){
222+
interval[1] <- interval[1]/1e2
223+
maxAt <- try(uniroot(helpEqn, interval)$root, TRUE)
224+
find_maxAt_tries <- find_maxAt_tries+1
225+
}
226+
}
217227

218228
eqn <- function(dose) {tempVal*(1+exp(parmVec[1]*(log(dose)-log(parmVec[4]))))-(1+parmVec[5]*dose/(parmVec[3]-parmVec[2]))}
219229
EDp <- uniroot(eqn, lower = maxAt, upper = upper)$root

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/bmdHetVar.R

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,12 @@
4949
#' is the normal distribution function and sigma(BMD) is the SD at the
5050
#' benchmark dose.
5151
#' @param interval character string specifying the type of confidence interval
52-
#' to use: "boot" (default) or "none"
52+
#' to use: "boot" (default), "delta" or "none"
5353
#'
5454
#' "boot" - BMDL is based on percentile bootstrapping.
5555
#'
56+
#' "delta" - BMDL is based on the delta method.
57+
#'
5658
#' "none" - no confidence interval is computed.
5759
#' @param R number of bootstrap samples. Ignored if \code{interval = "none"}
5860
#' @param level numeric value specifying the levle of the confidence interval
@@ -113,7 +115,7 @@
113115
#' def = "hybridExc", R = 50, level = 0.95, progressInfo = TRUE, display = TRUE)
114116
#'
115117
#' @export
116-
bmdHetVar <- function(object, bmr, backgType = c("absolute", "hybridSD", "hybridPercentile"), backg = NA, def = c("hybridExc", "hybridAdd"), interval = c("boot", "none"), R = 1000, level = 0.95, bootType = "nonparametric", progressInfo = TRUE, display = TRUE){
118+
bmdHetVar <- function(object, bmr, backgType = c("absolute", "hybridSD", "hybridPercentile"), backg = NA, def = c("hybridExc", "hybridAdd"), interval = c("boot", "delta", "none"), R = 1000, level = 0.95, bootType = "nonparametric", progressInfo = TRUE, display = TRUE){
117119
### Assertions ###
118120
# object
119121
if(!inherits(object,"drcHetVar")){
@@ -158,14 +160,19 @@ bmdHetVar <- function(object, bmr, backgType = c("absolute", "hybridSD", "hybrid
158160

159161
level <- 1-2*(1-level)
160162

163+
# Model parameters
164+
curveParInd <- 1:length(object$curvePar)
165+
sigmaParInd <- (length(object$curvePar)+1):(length(object$curvePar)+length(object$sigmaPar))
166+
161167
# SLOPE
162168
slope <- drop(ifelse(object$curve(0)-object$curve(Inf)>0,"decreasing","increasing"))
163169
if(is.na(object$curve(0)-object$curve(Inf))){
164170
slope <- drop(ifelse(object$curve(0.00000001)-object$curve(100000000)>0,"decreasing","increasing"))
165171
}
166172

167-
# sigmaFun
168-
sigmaFun0 <- object$sigmaFun # sigmaFun(object, var.formula)
173+
# functions
174+
curveFun0 <- object$funList$curveFun
175+
sigmaFun0 <- object$funList$sigmaFun # sigmaFun(object, var.formula)
169176

170177
# bmrScaled
171178
if(slope == "increasing"){
@@ -174,50 +181,51 @@ bmdHetVar <- function(object, bmr, backgType = c("absolute", "hybridSD", "hybrid
174181
if(is.na(backg)){
175182
stop('backgType = absolute, but backg not supplied')
176183
}
177-
p0 <- 1 - pnorm((backg - object$curve(0)) / sigmaFun0(0))
184+
p0 <- function(par) 1 - pnorm((backg - curveFun0(0, par[curveParInd])) / sigmaFun0(0, sigmaPar = par[sigmaParInd], curvePar = par[curveParInd]))
178185
}
179186
if(identical(backgType, "hybridPercentile")) {
180-
p0 <- ifelse(is.na(backg),1-0.9,1-backg)
187+
p0 <- function(par) ifelse(is.na(backg),1-0.9,1-backg)
181188
}
182189
if (identical(backgType,"hybridSD")) {
183-
p0 <- ifelse(is.na(backg), 1-pnorm(2), 1-pnorm(backg))
190+
p0 <- function(par) ifelse(is.na(backg), 1-pnorm(2), 1-pnorm(backg))
184191
}
185192

186193
# BMRSCALED
187194
bmrScaled <- switch(
188195
def,
189-
hybridExc = function(x){ sigmaFun0(x) *
190-
(qnorm(1 - p0) - qnorm(1 - p0 - (1 - p0)*bmr)) + object$curve(0)},
191-
hybridAdd = function(x){ sigmaFun0(x) *
192-
(qnorm(1 - p0) - qnorm(1 - (p0 + bmr))) + object$curve(0)}
196+
hybridExc = function(x, par){ sigmaFun0(x) *
197+
(qnorm(1 - p0(par)) - qnorm(1 - p0(par) - (1 - p0)*bmr)) + curveFun0(0, par[curveParInd])},
198+
hybridAdd = function(x, par){ sigmaFun0(x) *
199+
(qnorm(1 - p0(par)) - qnorm(1 - (p0(par) + bmr))) + curveFun0(0, par[curveParInd])}
193200
)
194201
} else {
195202
# BACKGROUND
196203
if (identical(backgType,"absolute")) {
197204
if(is.na(backg)){
198205
stop('backgType = absolute, but backg not supplied')
199206
}
200-
p0 <- pnorm((backg - object$curve(0)) / sigmaFun0(0))
207+
p0 <- function(par) pnorm((backg - curveFun0(0, par[curveParInd])) / sigmaFun0(0, sigmaPar = par[sigmaParInd], curvePar = par[curveParInd]))
201208
}
202209
if(identical(backgType, "hybridPercentile")) {
203-
p0 <- ifelse(is.na(backg),0.1,backg)
210+
p0 <- function(par) ifelse(is.na(backg),0.1,backg)
204211
}
205212
if (identical(backgType,"hybridSD")) {
206-
p0 <- ifelse(is.na(backg), pnorm(-2), pnorm(-backg))
213+
p0 <- function(par) ifelse(is.na(backg), pnorm(-2), pnorm(-backg))
207214
}
208215

209216
# BMRSCALED
210217
bmrScaled <- switch(
211218
def,
212-
hybridExc = function(x){ sigmaFun0(x) *
213-
(qnorm(p0) - qnorm(bmr + (1-bmr) * p0)) + object$curve(0)},
214-
hybridAdd = function(x){ sigmaFun0(x) *
215-
(qnorm(p0) - qnorm(bmr + p0)) + object$curve(0)}
219+
hybridExc = function(x, par){ sigmaFun0(x, sigmaPar = par[sigmaParInd], curvePar = par[curveParInd]) *
220+
(qnorm(p0(par)) - qnorm(bmr + (1-bmr) * p0(par))) + curveFun0(0, par[curveParInd])},
221+
hybridAdd = function(x, par){ sigmaFun0(x, sigmaPar = par[sigmaParInd], curvePar = par[curveParInd]) *
222+
(qnorm(p0(par)) - qnorm(bmr + p0(par))) + curveFun0(0, par[curveParInd])}
216223
)
217224
}
218225

219226
# BMD ESTIMATION
220-
f0 <- function(x) object$curve(x) - bmrScaled(x)
227+
par0 <- c(object$curvePar, object$sigmaPar)
228+
f0 <- function(x) curveFun0(x, par0[curveParInd]) - bmrScaled(x, par0)
221229
interval0 <- range(object$dataList$dose, na.rm = TRUE)
222230
uniroot0 <- try(uniroot(f = f0, interval = interval0), silent = TRUE)
223231

@@ -228,17 +236,34 @@ bmdHetVar <- function(object, bmr, backgType = c("absolute", "hybridSD", "hybrid
228236
bmdEst <- uniroot0$root
229237
}
230238

239+
231240
# INTERVAL
232241
interval <- match.arg(interval)
233242
if(identical(interval, "none")){
234243
BMDL <- NA
235244
BMDU <- NA
236-
} else {
237-
# drc_obj <- eval(substitute(drm(formula0, data = object$data, fct = fct0, type = "continuous", control = drmc(maxIt = 1, noMessage = TRUE)),
238-
# list(formula0 = object$formula,
239-
# fct0 = object$fct
240-
# )))
241-
# bootDataList <- bootDataGen(drc_obj, R=R, bootType="nonparametric",aggregated=FALSE)
245+
SDbmd <- NA
246+
} else if(identical(interval, "delta")){
247+
if(!requireNamespace("numDeriv")){
248+
stop('package "numDeriv" must be installed to compute delta confidence intervals with bmdHetVar')
249+
}
250+
251+
getBmdEst <- function(par){
252+
f0 <- function(x) curveFun0(x, par[curveParInd]) - bmrScaled(x, par)
253+
interval0 <- range(object$dataList$dose, na.rm = TRUE)
254+
uniroot0 <- try(uniroot(f = f0, interval = interval0), silent = TRUE)
255+
bmdEst <- as.numeric(uniroot0$root)
256+
}
257+
258+
dBmd <- try(numDeriv::grad(getBmdEst, par0))
259+
260+
Vbmd <- dBmd %*% vcov(object) %*% dBmd
261+
SDbmd <- sqrt(Vbmd)
262+
263+
BMDL <- qnorm(c(1-level)/2, mean = bmdEst, sd = SDbmd)
264+
BMDU <- qnorm(1-(1-level)/2, mean = bmdEst, sd = SDbmd)
265+
266+
} else if(identical(interval, "boot")) {
242267
bootDataList <- bootDataGenHetVar(object, R = R, bootType = bootType)
243268

244269
bmdHetVarBoot <- function(bootData){
@@ -265,9 +290,11 @@ bmdHetVar <- function(object, bmr, backgType = c("absolute", "hybridSD", "hybrid
265290
if(length(boot0) == 0){
266291
BMDL <- NA
267292
BMDU <- NA
293+
SDbmd <- NA
268294
} else {
269295
BMDL <- quantile(boot0,p=c(1-level), na.rm = TRUE) # ABC percentile lims.
270296
BMDU <- quantile(boot0,p=c(level), na.rm = TRUE)
297+
SDbmd <- sd(boot0)
271298
}
272299
}
273300

@@ -282,6 +309,7 @@ bmdHetVar <- function(object, bmr, backgType = c("absolute", "hybridSD", "hybrid
282309

283310
resBMD<-list(Results = resMat,
284311
bmrScaled = bmrScaled,
312+
SE = SDbmd,
285313
interval = bmdInterval,
286314
model = object)
287315
class(resBMD) <- c("bmdHetVar", "bmd")

R/bmdMA.R

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@
5757
#' dose-response models), 2 SD for "hybridSD" background and 0.9 for
5858
#' "hybridpercentile"
5959
#' @param def character string specifying the definition of the benchmark dose
60-
#' to use in the calculations. "excess" , "additional" and "point" are for
61-
#' binomial response whereas "relative", "extra", "added", "hybridExc" (excess
60+
#' to use in the calculations. "excess", "additional" and "point" are for
61+
#' binomial response. "relative", "extra", "added", "hybridExc" (excess
6262
#' hybrid), "hybridAdd" (additional hybrid), and "point" are for continuous
63-
#' response
63+
#' response. "relative", "extra", and "point" are for count response data.
6464
#'
6565
#' "excess" - BMR is defined as: BMR = (f(BMD) - p0)/(1 - p0). Works for
6666
#' binomial response. BMR should be between 0 and 1.

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

0 commit comments

Comments
 (0)