TL;DR @kjetilbhalvorsen explains how to compute confidence intervals based on the profile likelihood in Confidence interval for difference between two predicted probabilities in R. The entire thread is worth reading as it describes several methods to compute confidence intervals. I summarize the profile likelihood method briefly and then apply it to calculate a CI for the median lethal dose, LD50, on the tobacco budworms dataset. See Section 7.2 of Modern Applied Statistics with S by Venables and Ripley.
Let $\theta(\boldsymbol{\beta})$ be a scalar function of the model parameters $\boldsymbol{\beta}$. Then the profile likelihood of $\theta$ is:
$$
\begin{aligned}
L_P(\theta) = \max_{\theta:\theta(\boldsymbol{\beta})=\theta}L(\boldsymbol{\beta})
\end{aligned}
$$
where $L(\boldsymbol{\beta})$ is the likelihood. For the dose-response example, $\boldsymbol{\beta} = (\beta_0,\beta_1)$ but the theory — and the R code — are straightforward to extend to more parameters. The LD50, the dose expected to kill 50% of individuals, is the point where the log-odds of survival are equal to zero: $\theta = -\beta_0/\beta_1$.
x <- budworm$x # dose
y <- budworm$y # survival (0 or 1)
# The LD50 (dose that is expected to kill 50% of individuals) is defined as
# the point where the log-odds of survival are equal to zero.
theta <- function(beta0, beta1) {
-beta0 / beta1
}
# The negative log-likelihood of the logistic regression model
# with an intercept and one predictor x.
negloglik_lrm <- function(beta0, beta1) {
prob <- plogis(beta0 + beta1 * x)
-sum(dbinom(y, 1, prob, log = TRUE))
}
# Fit a simple linear regression to find reasonable starting values.
initialize_betas <- function(x, y) {
betas <- coef(lm(y ~ x))
list(beta0 = betas[1], beta1 = betas[2])
}
The crux of the method is to profile the likelihood, ie, to maximize the likelihood over a grid of $\theta$ values. For a fixed $\theta$ we find $\max L(\boldsymbol{\beta})$ over the set $\left\{\boldsymbol{\beta}:\theta(\boldsymbol{\beta})=\theta\right\}$.
# Minimize the negative log-likelihood instead of maximizing the likelihood
model <- bbmle::mle2(negloglik_lrm, start = initialize_betas(x, y))
# nlp = negative profile log-likelihood
grid <- profile_negloglik(model)
grid
#> # A tibble: 201 × 2
#> theta npl
#> <dbl> <dbl>
#> 1 1.65 141.
#> 2 1.67 140.
#> 3 1.68 139.
#> 4 1.70 138.
#> 5 1.71 137.
#> 6 1.73 136.
#> 7 1.74 135.
#> 8 1.75 134.
#> 9 1.77 133.
#> 10 1.78 132.
#> # … with 191 more rows
Now that we have calculated the profile likelihood for $\theta$ at a grid of values, it remains to find a subset of those to represent a confidence interval.
Under regularity conditions (which mean the log likelihood is well approximated by a quadratic function), the ratio likelihood test states that under the null hypothesis $H_0:\theta=\theta_0$:
$$
\begin{aligned}
2\log\frac{L(\widehat{\theta})}{L(\theta_0)} \sim \chi^2_p
\end{aligned}
$$
where $p$ is the number of parameters $\theta$, $\widehat{\theta}$ is the maximum likelihood estimate (MLE) and $\chi^2_p$ is the Chi-squared distribution with $p$ degrees of freedom. This is the value at which the (profile) likelihood is minimized.
By inverting the hypothesis test we get a 100(1-$\theta$)% confidence interval (or region) for $\theta$.
$$
\begin{aligned}
\left\{\theta:2\log\frac{L(\widehat{\theta})}{L(\theta)}<\chi^2_{p,(1-\alpha)}\right\}
\end{aligned}
$$
For the budworms example, the LD50 $\theta$ is a scalar, so $p=1$ and we can find the profile likelihood 95% and 99% confidence intervals numerically:
#> conf.level conf.low conf.high nll
#> 1 0.95 2.415057 3.084742 113.6548
#> 2 0.99 2.312052 3.195448 115.0515
I also use the MASS:dose.p
function to compute the Wald 95% and 99% confidence intervals. (See R code attached.)
data.frame(
conf.level,
conf.low = dose.hat + dose.se * qnorm((1 - conf.level) / 2),
conf.high = dose.hat - dose.se * qnorm((1 - conf.level) / 2)
)
#> conf.level conf.low conf.high
#> 1 0.95 2.422786 3.071986
#> 2 0.99 2.320789 3.173983
In this example, the profile and the Wald confidence intervals for $\theta$ agree well.
References
In addition to @kjetilbhalvorsen's posts about profile likelihood, I'm also reading "In All Likelihood: Statistical Modelling And Inference Using Likelihood" by Yudi Pawitan. This book explains the theory in great detail and with many examples. (Not about budworms though.)
R code to compute profile likelihood confidence intervals:
library("MASS")
library("tidyverse")
# Compute negative log-likelihood-based intervals for a scalar parameter theta
# at the specified alpha levels.
# This implementation is based on the program `li.r` for computing likelihood
# intervals which accompanies the book "In All Likelihood" by Yudi Pawitan.
# https://www.meb.ki.se/sites/yudpaw/book/
confint_negloglik <- function(theta, nll, conf.level = 0.95) {
nll_min <- min(nll)
# 2*{ log L(theta_mle) - log L(theta) } < qchisq(df=1,p=1-alpha)
nll_cut <- nll_min + qchisq(conf.level, 1) / 2
theta_mle <- mean(theta[nll == nll_min])
theta_below <- theta[theta < theta_mle]
if (length(theta_below) < 2) {
conf.low <- min(theta)
} else {
nll.below <- nll[theta < theta_mle]
conf.low <- approx(nll.below, theta_below, xout = nll_cut)$y
}
theta_above <- theta[theta > theta_mle]
if (length(theta_above) < 2) {
conf.high <- max(theta)
} else {
nll.above <- nll[theta > theta_mle]
conf.high <- approx(nll.above, theta_above, xout = nll_cut)$y
}
data.frame(conf.level, conf.low, conf.high, nll = nll_cut)
}
grid_params <- function(model, pts = 100) {
seq_range <- function(x, n) {
seq(min(x), max(x), len = n)
}
# Use the confidence intervals to find reasonable ranges for the betas.
range_betas <- bbmle::confint(bbmle::profile(model))
expand_grid(
# Create a grid (= an outer product) for beta0 and beta1
beta0 = seq_range(range_betas["beta0", ], pts),
beta1 = seq_range(range_betas["beta1", ], pts)
) %>%
# For each point in the grid, compute the parameter of interest theta
# and the negative log-likelihood of the model. Both are functions of
# the regression coefficients beta0 and beta1.
mutate(
theta = theta(beta0, beta1),
nll = map2_dbl(beta0, beta1, negloglik_lrm)
)
}
profile_negloglik <- function(profile, bins = 201) {
profile %>%
grid_params() %>%
group_by(
# Subdivide the observed range of theta into many small intervals.
cut_interval(theta, bins),
.drop = TRUE
) %>%
summarise(
# For each interval, find its midpoint as well as the minimum of
# the negative log-likelihood. This is the negative profile
# log-likelihood (npl).
theta = (min(theta) + max(theta)) / 2,
npl = min(nll)
) %>%
select(
theta, npl
)
}
plot_profile <- function(grid, confint) {
grid %>%
ggplot(
aes(theta, npl)
) +
geom_line(
linewidth = 1
) +
geom_segment(
aes(
x = conf.low, xend = conf.high,
y = nll, yend = nll,
color = factor(conf.level)
),
inherit.aes = FALSE,
data = confint,
linewidth = 1
) +
guides(
color = guide_legend(title = "confidence level")
)
}
# Tobacco budworms survival data. See `MASS::dose.p`.
ldose <- rep(0:5, 2)
numdead <- c(1, 4, 9, 13, 18, 20, 0, 2, 6, 10, 12, 16)
numalive <- 20 - numdead
budworm <- list(
x = c(rep(ldose, numdead), rep(ldose, numalive)),
y = c(rep(0, sum(numdead)), rep(1, sum(numalive)))
)
conf.level <- c(0.95, 0.99)
x <- budworm$x
y <- budworm$y
# The negative log-likelihood of the logistic regression model
# with an intercept and one predictor x.
negloglik_lrm <- function(beta0, beta1) {
prob <- plogis(beta0 + beta1 * x)
-sum(dbinom(y, 1, prob, log = TRUE))
}
# The LD50 (dose that is expected to kill 50% of individuals) is defined as
# the point where the log-odds of survival are equal to zero.
theta <- function(beta0, beta1) {
-beta0 / beta1
}
# Fit a simple linear regression to find reasonable starting values.
initialize_betas <- function(x, y) {
betas <- coef(lm(y ~ x))
list(beta0 = betas[1], beta1 = betas[2])
}
# Minimize the negative log-likelihood instead of maximizing the likelihood
model <- bbmle::mle2(negloglik_lrm, start = initialize_betas(x, y))
profile <- bbmle::profile(model)
grid <- profile_negloglik(model)
grid
# Profile confidence intervals for the betas parameters and for theta
ci.betas <- bbmle::confint(profile)
ci.betas
ci.theta <- confint_negloglik(grid$theta, grid$npl, conf.level)
ci.theta
plt <- plot_profile(grid, ci.theta)
plt +
labs(
x = expression(paste(θ, "=", -beta[0] / beta[1])),
y = "negative profile log-likelihood",
title = "Negative profile log-likelihood for LD50"
)
model <- glm(
y ~ x,
family = binomial
)
dose.p(model)
dose.hat <- 2.747386
dose.se <- 0.1656153
# Wald confidence interval
data.frame(
conf.level,
conf.low = dose.hat + dose.se * qnorm((1 - conf.level) / 2),
conf.high = dose.hat - dose.se * qnorm((1 - conf.level) / 2)
)
MASS
package is installed then callingconfint()
on aglm
ornls
object will end up calling the correspondingMASS
functions, which do use profile likelihoods. Otherwise, the Wald CI based on the covariance matrix of the coefficient estimates will be used. $\endgroup$predict
are profile likelihood intervals? I've never heard that term in that context. Thank you. $\endgroup$dose.p
is based on the delta method. The resulting confidence interval could also be described as a Wald-based interval, but the standard error is estimated differently. $\endgroup$