Skip to content

Commit 4ba8a8a

Browse files
committed
Initial commit
0 parents  commit 4ba8a8a

8 files changed

+304
-0
lines changed

.Rbuildignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
^drob\.Rproj$
2+
^\.Rproj\.user$
3+
^.vscode$

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.Rproj.user

DESCRIPTION

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
Package: drob
2+
Title: Compute robust estimates of dose-response model parameters
3+
Description:
4+
Compute robust estimates of potentially nonlinear and heteroscedastic
5+
dose-response model parameters using a 3-step algorithm in the spirit of
6+
MM-estimation.
7+
Version: 0.0.1
8+
Authors@R:
9+
person(
10+
"Carlos", "Pita",
11+
email = "[email protected]",
12+
role = c("cre", "aut")
13+
)
14+
URL: hhttps://github.com/memeplex/drob
15+
BugReports: https://github.com/memeplex/drob/issues
16+
License: `use_mit_license()`
17+
Encoding: UTF-8
18+
Roxygen: list(markdown = TRUE)
19+
RoxygenNote: 7.3.1
20+
Imports:
21+
drc (>= 3.0.1),
22+
DEoptimR (>= 1.1.3)

NAMESPACE

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Generated by roxygen2: do not edit by hand
2+
3+
export(bisquare)
4+
export(drob)
5+
export(fpl)
6+
export(m_scale)

R/drob.R

+199
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
library(drc)
2+
library(DEoptimR)
3+
4+
#' The 4-parameter logistic function (4PL)
5+
#'
6+
#' `fpl` is a list containing three elements related to the FPL model, as
7+
#' required by the `model` parameter of the `drob` function:
8+
#' - `fun`: the FPL function itself. `fpl$fun` takes as arguments a vector
9+
#' of values `x` and a vector of 4 parameters `t` .
10+
#' - `grad`: the gradient of the 4PL function, also expressed as a function
11+
#' of `x` and `t`.
12+
#' - `init`: a function that computes initial parameters and search bounds.
13+
#'
14+
#' @export
15+
fpl <- list(
16+
fun = function(x, t) t[4] + (t[1] - t[4]) / (1 + (x / t[3])^t[2]),
17+
grad = function(x, t) {
18+
a <- (x / t[3])^t[2]
19+
b <- ((t[1] - t[4]) / (1 + a)^2) * a
20+
c(
21+
1 / (1 + a),
22+
-b * max(log(x / t[3]), -100),
23+
b * t[2] / t[3],
24+
a / (1 + a)
25+
)
26+
},
27+
init = function(x, y, scale = 3) {
28+
coef <- summary(drm(y ~ x, fct = LL.4()))$coefficients
29+
idx <- if (coef[1] >= 0) c(3, 1, 4, 2) else c(2, 1, 4, 3)
30+
t <- unname(coef[idx, 1])
31+
se <- unname(coef[idx, 2])
32+
t[2] <- abs(t[2])
33+
extend <- function(x, dir) x + dir * scale * abs(x)
34+
init <- list(t = t, se = se, lower = extend(t, -1), upper = extend(t, +1))
35+
init$lower[c(2, 3)] <- 1e-100
36+
init
37+
}
38+
)
39+
40+
#' Return bisquare and its derivatives
41+
#'
42+
#' `bisquare` computes bisquare (aka Tukey's biweight) function for a given
43+
#' cutoff point. It also computes its first two derivatives. All three functions
44+
#' are returned as elements of a list, as required by the `step_1` parameter of
45+
#' the `drob` function.
46+
#'
47+
#' @param k The cutoff point below and above which rho evaluates to 1.
48+
#'
49+
#' @return A list with three elements:
50+
#' - `rho`: The bisquare function
51+
#' - `psi`: The first derivative of rho
52+
#' - `dpsi`: The second derivative of rho
53+
#'
54+
#' @export
55+
bisquare <- function(k) {
56+
f <- function(r, a, b = 0) ifelse(abs(r) <= k, a, b)
57+
list(
58+
rho = function(r) f(r, 1 - (1 - (r / k)^2)^3, 1),
59+
psi = function(r) f(r, 6 * r * (1 - (r / k)^2)^2 / k^2),
60+
dpsi = function(r) f(r, 6 * (1 - (r / k)^2) * (1 - 5 * (r / k)^2) / k^2)
61+
)
62+
}
63+
64+
#' Compute M-estimate of scale
65+
#'
66+
#' `m_scale` computes an M-estimate of scale for a given rho function using
67+
#' one-dimensional root finding.
68+
#'
69+
#' @param r A vector of values (typically residuals) which scale is to be
70+
#' computed.
71+
#' @param rho The rho function that defines the M-estimate of scale.
72+
#' @param extend The interval for root finding will extend from 0 to `extend`
73+
#' times the median absolute value of `r`. Defaults to 5.
74+
#'
75+
#' @return The `rho`-based M-estimate of scale for `r`.
76+
#'
77+
#' @export
78+
m_scale <- function(r, rho, extend = 5) {
79+
f <- function(s) mean(rho(r / s)) - 0.5
80+
m <- median(abs(r))
81+
uniroot(f, lower = 0, upper = extend * m, check.conv = TRUE)$root
82+
}
83+
84+
#' @export
85+
drob <- function( # nolint
86+
x, y,
87+
model = "fpl",
88+
step_1 = "sbi",
89+
step_2 = "sbi",
90+
step_3 = "mbi",
91+
lts_q = 0.7,
92+
mbi_k = 3.44,
93+
sbi_k = 1.55,
94+
sli_k = 1.48,
95+
de_args = identity,
96+
qn_args = identity,
97+
qn_gr = FALSE,
98+
ms_extend = 5,
99+
init_extend = 5
100+
) {
101+
select <- function(arg, ...) if (is.character(arg)) switch(arg, ...) else arg
102+
mbi <- bisquare(mbi_k)
103+
sbi <- bisquare(sbi_k)
104+
model <- select(
105+
model,
106+
"fpl" = fpl,
107+
stop("Invalid model '", model, "'")
108+
)
109+
init <- model$init(x, y, init_extend)
110+
lower <- init$lower
111+
upper <- init$upper
112+
113+
# Step 1 (t0)
114+
115+
loss <- select(
116+
step_1,
117+
"lts" = function(r) mean((r^2)[r <= quantile(r, lts_q)]),
118+
"lms" = function(r) median(r^2),
119+
"ml1" = function(r) mean(abs(r)),
120+
"sl1" = function(r) median(abs(r)),
121+
"mbi" = { s <- mad(y); function(r) mean(mbi$rho(r / s)) }, # nolint
122+
"sbi" = function(r) m_scale(r, sbi$rho, ms_extend),
123+
stop("Step 1: invalid value '", step_1, "'")
124+
)
125+
grid <- matrix(runif(1000, lower, upper), nrow = length(lower))
126+
mloss <- median(abs(apply(grid, 2, loss)))
127+
args <- de_args(list(
128+
fn = function(t) loss(y - model$fun(x, t)),
129+
lower = lower, upper = upper, fnscale = mloss, tol = 1e-6
130+
))
131+
res <- do.call(JDEoptim, args)
132+
if (res$convergence == 1) stop("Step 1: optimizer failed")
133+
t0 <- res$par
134+
135+
# Step 2 (s, sx)
136+
137+
scale <- select(
138+
step_2,
139+
"sl1" = function(r) median(abs(r)) * sli_k,
140+
"sbi" = function(r) m_scale(r, sbi$rho, ms_extend),
141+
stop("Step 2: invalid value '", step_2, "'")
142+
)
143+
idx <- as.factor(x)
144+
s <- tapply(y - model$fun(x, t0), idx, scale)
145+
sx <- s[idx]
146+
147+
# Step 3 (t)
148+
149+
loss <- select(
150+
step_3,
151+
"mbi" = mbi,
152+
stop("Step 3: invalid value '", step_3, "'")
153+
)
154+
args <- list(
155+
fn = function(t) mean(loss$rho((y - model$fun(x, t)) / sx)),
156+
gr = if (qn_gr && !is.null(model$grad) && !is.null(loss$psi)) function(t) {
157+
f <- function(r, x) r * model$grad(x, t)
158+
rowMeans(mapply(f, -loss$psi((y - model$fun(x, t)) / sx) / sx, x))
159+
},
160+
par = t0,
161+
control = list(parscale = t0, trace = 0)
162+
)
163+
optimize <- function(...) {
164+
res <- do.call(optim, qn_args(append(args, list(...))))
165+
if (res$convergence %in% c(51, 52)) {
166+
stop("Step 3: optimizer failed with '", res$message, "'")
167+
}
168+
res
169+
}
170+
res <- try(optimize(method = "L-BFGS-B", lower = lower, upper = upper))
171+
if (inherits(res, "try-error")) res <- try(optimize(method = "BFGS"))
172+
if (inherits(res, "try-error")) res <- optimize(method = "CG")
173+
t <- res$par
174+
175+
# Standard errors (se)
176+
177+
se <- if (!is.null(model$grad) && !is.null(loss$psi) && !is.null(loss$dpsi)) {
178+
r <- (y - model$fun(x, t)) / sx
179+
mp <- mean(loss$psi(r)^2)
180+
md <- mean(loss$dpsi(r))^2
181+
mg <- Reduce("+", mapply(function(x, s) {
182+
g <- model$grad(x, t)
183+
outer(g, g) / s^2
184+
}, x, sx, SIMPLIFY = FALSE)) / length(x)
185+
sqrt(((mp / md) * diag(ginv(mg))) / length(x))
186+
}
187+
188+
list(t = t, t0 = t0, s = s, se = se, init = init, loss = res$value)
189+
}
190+
191+
192+
# https://cran.r-project.org/web/packages/robustbase/vignettes/psi_functions.pdf
193+
# The constant k for 95% efficiency of the regression estimator is 4.685 and
194+
# the constant for a breakdown point of 0.5 of the S-estimator is 1.548
195+
196+
# p.35, rho(r) = I(|r|>1), d = 0.5 => median(abs(r))
197+
# p.130
198+
# When computing an initial estimate β0 for the MM-estimate,
199+
# the bisquare ρ works quite well and we recommend its use for this purpose.

man/bisquare.Rd

+25
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/fpl.Rd

+24
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/m_scale.Rd

+24
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)