---
title: "Five-parameters logistic regression"
author: "Stéphane Laurent"
date: '2019-11-20'
tags: maths, statistics, R
rbloggers: yes
output:
md_document:
variant: markdown
preserve_yaml: true
html_document:
highlight: kate
keep_md: no
highlighter: pandoc-solarized
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE, fig.path = "./figures/5pl-",
fig.width = 3.5, fig.align = "center",
attr.source = ".numberLines")
```
The five-parameters logistic curve is commonly defined by
$$
f(x) = A + \frac{D-A}{\Bigl(1+\exp\bigl(B(C-x)\bigr)\Bigr)^S}.
$$
Assuming $B>0$ and $S>0$,
- $A$ is the value of the horizontal asymptote when $x \to -\infty$;
- $D$ is the value of the horizontal asymptote when $x \to +\infty$;
- $B$ describes how rapidly the curve makes its transition between the two
asymptotes;
- $C$ is a location parameter, which does not have a nice interpretation
(except if $S=1$);
- $S$ describes the asymmetry of the curve (the curve is symmetric when $S=1$).
In the case when $S=1$, the parameter $C$ is the value of $x$ for which the
corresponding value $f(x)$ is the midpoint between the two asymptotes;
moreover, the curve has an inflection point at $x = C$.
In the general case, the value of $x$ for which the
corresponding value $f(x)$ is the midpoint between the two asymptotes is
$$
x_{\text{mid}} = C - \frac{\log\Bigl(2^{\frac{1}{S}}-1\Bigr)}{B}.
$$
It is obtained by solving
$\Bigl(1+\exp\bigl(B(C-x)\bigr)\Bigr)^S = 2$.
```{r plot_5pl, collapse=TRUE, fig.height=2.5}
n <- 100
x <- seq(49, 60, length.out = n)
A <- 30; D <- 100; B <- 1; C <- 50; S <- 10
f <- function(x) A + (D-A) / (1 + exp(B*(C-x)))^S
y0 <- f(x)
par(mar = c(4, 4, 0.5, 1))
plot(x, y0, type = "l", cex.axis = 0.5, ylab = "f(x)")
abline(v = C, col = "green", lty = "dashed")
( xmid <- C - log(2^(1/S) - 1)/B )
abline(v = xmid, col = "red", lwd = 2)
abline(h = (A+D)/2, col = "red", lwd = 2)
```
Note that the inflection point of the curve is *not* the point correspoding
to $x_{\text{mid}}$:
```{r inflection, fig.height=2.5}
library(numDeriv)
df <- grad(f, x)
par(mar = c(4, 4, 0.5, 1))
plot(x, df, type = "l", cex.axis = 0.5, ylab="f'(x)")
abline(v = xmid, col = "red", lwd = 2)
```
In practice, we are often interested in estimating $x_{\text{mid}}$.
So it is better to use this other parameterization of the five-parameters
logistic curve:
$$
g(x) =
A + \frac{D-A}{{\biggl(1+\exp\Bigl(\log\bigl(2^{\frac{1}{S}}-1\bigr) + B(x_{\text{mid}}-x)\Bigr)\biggr)}^S}
$$
because fitting this curve will directly give the estimate of $x_{\text{mid}}$
and its standard error.
Another advantage of this parameterization is that there is a way to get
a good starting value of $x_{\text{mid}}$ when one wants to fit the
five-parameters logistic regression model:
```{r getInitialSSfpl}
getInitial1 <- function(x, y){
s <- getInitial(y ~ SSfpl(x, A, D, xmid, inverseB),
data = data.frame(x = x, y = y))
c(A = s[["A"]],
B = 1/s[["inverseB"]],
xmid = s[["xmid"]],
D = s[["D"]],
S = 1)
}
```
I don't know how to get a good starting value for $S$, so I always take $1$.
Sometimes, `SSfpl` can fail. Here is another function which returns some
starting values:
```{r getInitial2}
getInitial2 <- function(x, y){
NAs <- union(which(is.na(x)), which(is.na(y)))
if(length(NAs)){
x <- x[-NAs]
y <- y[-NAs]
}
low_init <- min(y)
high_init <- max(y)
minmax <- c(which(y == low_init), which(y == high_init))
X <- cbind(1, x[-minmax])
Y <- log((high_init-y[-minmax])/(y[-minmax]-low_init))
fit <- lm.fit(x = X, y = Y)
b_init <- fit$coefficients[[2]]
xmid_init <- -fit$coefficients[[1]] / b_init
if(b_init < 0){
b_init <- -b_init
A <- low_init
D <- high_init
}else{
A <- high_init
D <- low_init
}
c(A = A, B = b_init, xmid = xmid_init, D = D, S = 1)
}
```
Now we wrap these two functions into a single one:
```{r getInitial5pl}
getInitial5PL <- function(x, y){
tryCatch({
getInitial1(x, y)
}, error = function(e){
getInitial2(x, y)
})
}
```
And finally we can write a function for the fitting:
```{r fit5pl, message=FALSE}
library(minpack.lm)
fit5pl <- function(x, y){
startingValues <- getInitial5PL(x, y)
fit <- tryCatch({
nlsLM(
y ~ A + (D-A)/(1 + exp(log(2^(1/S)-1) + B*(xmid-x)))^S,
data = data.frame(x = x, y = y),
start = startingValues,
lower = c(-Inf, 0, -Inf, -Inf, 0),
control = nls.lm.control(maxiter = 1024, maxfev=10000))
}, error = function(e){
paste0("Failure of model fitting: ", e$message)
})
if(class(fit) == "nls" && fit[["convInfo"]][["isConv"]]){
fit
}else if(class(fit) == "nls" && !fit[["convInfo"]][["isConv"]]){
"Convergence not achieved"
}else{ # in this case, 'fit' is the error message
fit
}
}
```
Let's try it on a couple of simulated samples:
```{r simulations}
set.seed(666)
nsims <- 25
epsilon <- matrix(rnorm(nsims*n, 0, 5), nrow = nsims, ncol = n)
estimates <- matrix(NA_real_, nrow = nsims, ncol = 5)
colnames(estimates) <- c("A", "B", "xmid", "D", "S")
for(i in 1:nsims){
fit <- fit5pl(x, y0 + epsilon[i,])
if(class(fit) == "nls"){
estimates[i, ] <- coef(fit)
}else{
estimates[i, ] <- c(NaN, NaN, NaN, NaN, NaN)
}
}
summary(estimates)
```
The estimate of $x_{\text{mid}}$ is excellent.
As you can see, the estimate of $S$ is sometimes much larger than the true
value. Let's have a look at the worst case:
```{r worstcase, collapse=TRUE, fig.height=3}
i0 <- match(max(estimates[, "S"]), estimates[, "S"])
estimates[i0, ]
# sample
par(mar = c(4, 4, 0.5, 1))
plot(x, y0 + epsilon[i0, ], col = "yellow", cex.axis = 0.6)
# true curve
curve(A + (D-A)/(1 + exp(log(2^(1/S)-1) + B*(xmid-x)))^S,
add = TRUE, col = "red", lwd = 2)
# fitted curve
with(as.list(estimates[i0, ]),
curve(A + (D-A)/(1 + exp(log(2^(1/S)-1) + B*(xmid-x)))^S,
add = TRUE, col = "blue", lwd = 2, lty = "dashed")
)
```
Thus, while the estimate of $S$ is very far from the true value of $S$,
the fitted curve correctly estimates the true curve.
And in such cases, the standard error of the estimate of $S$ is big:
```{r fitworstcase}
fit <- fit5pl(x, y0 + epsilon[i0,])
summary(fit)
```
Note that `nlsLM` provides a test of the nullity of $S$.
This is not interesting, whereas the equality $S = 1$ is of interest. So it is
better to parametrize the logistic function with $L = \log(S)$ instead of $S$:
$$
h(x) =
A + \frac{D-A}{{\biggl(1+\exp\Bigl(\log\bigl(2^{\exp(-L)}-1\bigr) + B(x_{\text{mid}}-x)\Bigr)\biggr)}^{\exp(L)}}.
$$
In this way we can get a test of $L = 0$, that is $S = 1$.