|
| 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. |
0 commit comments