4 Estimate calibration curve
4.1 The four-parameter logistic curve
The relationship between concentration and absorbance can be described by the so called four-parameter logistic equation (4-PL). We will use the following equation:
\[A = f(c) = \mathrm{A_{min}} + \frac{\mathrm{A_{max}} - \mathrm{A_{min}}}{1+\exp(\mathrm{n} \cdot (\log_2(c)-\log_2(\mathrm{EC}_{50})))}\],
where \(\mathrm{A}\) stands for absorbance and \(c\) for concentration. The parameters that we are going to estimate are \(\mathrm{A_{max}}\), which is the maximal absorbance that can be obtained (for infinite concentration \(c \rightarrow \infty\)), \(\mathrm{A_{min}}\), which is the minimum absorbance that can be obtained (for \(c \rightarrow 0\)), and parameters \(\mathrm{EC}_{50}\) and \(n\). The value of \(\log_2 \mathrm{EC}_{50}\) is the inflection point of the curve. This means, \(\log_2 \mathrm{EC}_{50}\) is the concentration for which we obtain 50% of \(\mathrm{A_{max}}\). Parameter \(n\) is related to the steepness of the curve at the inflection point.
For convenience, we use \(\log_2\) in the equation (and not \(log_{10}\) as often used in literature), because then the estimated \(\log_2 EC_{50}\) is easier to interpret, since we performed a 1:2 dilution series to generate the concentrations of our standard. Note that \(\log_2(x) = \frac{log_{10}(x)}{log_2(10)} = 0.30103 \cdot \log_{10}(x)\). Changing the base simply scales all concentration by a constant value (linear transformation).
4.2 Regression of the standard
We fit the data obtained for the standard to the four-parameter logistic curve. This is a non-linear least squares problem and we use the Levenberg-Marquadt algorithm to solve it.
The Levenberg-Marquardt algorithm is more robust than the Gauss–Newton algorithm (used by default by the nls()
function of the nls
package), which means it converges to a minimum even if the initial starting values are far from the final minimum.
# Fit 4-parameter logistic dose-response curve
# Approach 1): Use nlsLM pacakge for Levenberg-Marquardt optimization
<- standard %>% dplyr::mutate(log2concentration = log2(concentration))
standard <- nlsLM(
levm.fit ~
absorbance + ((Amax - Amin) / (1 + exp(n * (log2concentration - log2EC50)))),
Amin data = standard,
start = list(Amin = 0, Amax = 2, log2EC50 = 0.5, n = -1)
)# Show summary of the fit
summary(levm.fit)
##
## Formula: absorbance ~ Amin + ((Amax - Amin)/(1 + exp(n * (log2concentration -
## log2EC50))))
##
## Parameters:
## Estimate Std. Error t value Pr(>|t|)
## Amin -0.05700 0.05978 -0.954 0.359117
## Amax 2.44950 0.39531 6.196 4.61e-05 ***
## log2EC50 -1.29803 0.49578 -2.618 0.022462 *
## n -0.64181 0.11414 -5.623 0.000112 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.05644 on 12 degrees of freedom
##
## Number of iterations to convergence: 10
## Achieved convergence tolerance: 1.49e-08
# View(standard)
Apparently, we can not estimate \(\mathrm{A_{min}}\) from the data. But we can fix it to a certain value: we know that \(\mathrm{A_{min}}\) can not be negative and we can use the values obtained from the blank wells as an estimate for \(\mathrm{A_{min}}\).
# Set Amin to blank values
<- median(blanks$blank)
Amin # Re-fit with only three free parameters
<- nlsLM(
levm.fit ~
absorbance + ((Amax - Amin) / (1 + exp(n * (log2concentration - log2EC50)))),
Amin data = standard,
start = list(Amax = 2, log2EC50 = 0.5, n = -1)
)# Check new fit results
summary(levm.fit)
##
## Formula: absorbance ~ Amin + ((Amax - Amin)/(1 + exp(n * (log2concentration -
## log2EC50))))
##
## Parameters:
## Estimate Std. Error t value Pr(>|t|)
## Amax 2.21560 0.20145 10.998 5.90e-08 ***
## log2EC50 -1.53719 0.29608 -5.192 0.000174 ***
## n -0.74553 0.06922 -10.771 7.55e-08 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.05767 on 13 degrees of freedom
##
## Number of iterations to convergence: 9
## Achieved convergence tolerance: 1.49e-08
# Get 95% confidence interval
confint(levm.fit)
## 2.5% 97.5%
## Amax 1.8848732 2.8761358
## log2EC50 -2.0560171 -0.6645695
## n -0.9010853 -0.6099279
## Add calibration curve to plot
# Save estimated parameters
<- coef(levm.fit)[[1]]
Amax.est <- coef(levm.fit)[[2]]
log2EC50.est <- coef(levm.fit)[[3]]
n.est # Full range of considered concentrations
<- log2(seq(0.008, 2, by = 0.001))
log2concentration.new # Predict new absorbance values from calibration curve
<- tibble(
calibration.curve concentration.new = 2^log2concentration.new,
log2concentration.new = log2concentration.new,
absorbance.est = predict(levm.fit, tibble(log2concentration = log2concentration.new))
)
## Compute prediction and confidence intervals
# TODO: experimental -- to double-check
<- deriv(
fgh ~
absorbance + ((Amax - Amin) / (1 + exp(n * (log2concentration - log2EC50)))),
Amin c("Amax", "log2EC50", "n"), function(Amax, log2EC50, n, log2concentration) {}
)<- fgh(Amax.est, log2EC50.est, n.est, log2concentration.new)
f.new <- attr(f.new, "gradient")
g.new <- vcov(levm.fit)
cov.fit <- rowSums((g.new %*% cov.fit) * g.new)
gs <- 0.05
alpha <- sqrt(gs) * qt(1 - alpha/2, 15)
delta.f <- summary(levm.fit)$sigma
sigma.est <- sqrt(gs + sigma.est^2) * qt(1 - alpha / 2, 15)
delta.y <- calibration.curve %>%
calibration.curve ::mutate(pred.lwr = absorbance.est - delta.y) %>%
dplyr::mutate(pred.upr = absorbance.est + delta.y) %>%
dplyr::mutate(conf.lwr = absorbance.est - delta.f) %>%
dplyr::mutate(conf.upr = absorbance.est + delta.f)
dplyr
## Plot calibration curve
<- plot.std(semilog = TRUE) +
plot.ccurve geom_line(
data = calibration.curve,
aes(concentration.new, absorbance.est), alpha = 0.8
+
) geom_ribbon(
data = calibration.curve,
aes(x = concentration.new, ymin = pred.lwr, ymax = pred.upr),
alpha = 0.2, fill = "grey"
+
) geom_ribbon(
data = calibration.curve,
aes(x = concentration.new,
ymin = conf.lwr,
ymax = conf.upr),
alpha = 0.1, fill = NA, color = "grey70", lty = 3
+
) scale_y_continuous(limits = c(-0.3, 3), breaks = seq(-0.3, 3, 0.5))
plot.ccurve
The grey band shows the prediction interval and the dashed line the confidence interval (both for 95%).
Alternatively, one can use the drc
package to fit a 4-PL curve using. However, the drm
function performs a \(\ln\) transformation of the independent variable by itself. So in this case, the results are for \(\ln\)-transformed concentrations:
# Fit 4-parameter logistic dose-response curve
# Approach 2): Use drc package
# NOTE: the drm function performs a ln(x) transformation by itself.
# See also ?LL2.4 for more information
<- drm(absorbance ~ concentration,
drc.fit data = standard,
fct = LL2.4(names = c("n", "Amin", "Amax", "EC50"))
)summary(drc.fit)
##
## Model fitted: Log-logistic (log(ED50) as parameter) (4 parms)
##
## Parameter estimates:
##
## Estimate Std. Error t-value p-value
## n:(Intercept) -0.926589 0.154888 -5.9823 6.388e-05 ***
## Amin:(Intercept) -0.056483 0.055791 -1.0124 0.33132
## Amax:(Intercept) 2.449055 0.383663 6.3834 3.487e-05 ***
## EC50:(Intercept) -0.899679 0.337454 -2.6661 0.02056 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error:
##
## 0.05644039 (12 degrees of freedom)
plot(drc.fit)
ggplot(as.tibble(drc.fit$predres)) +
geom_point(aes(`Predicted values`, Residuals),
pch = 1, size = 3, color = "cornflowerblue") +
theme_plot() +
panel_border() +
geom_hline(yintercept = 0, lwd = 0.2)
Using the estimated parameters and the 4-PL equation, we can estimate unknown concentrations based on measured absorbance.