Code
import numpy as np
x = np.array([-0.86, -0.30, -0.05, 0.73])
n = np.array([5, 5, 5, 5])
y = np.array([0, 1, 3, 5])Source notebooks: demo3_6.ipynb, demo10_3.ipynb
The bioassay example is the book’s first compact example of posterior geometry in more than one dimension. Four log-dose values are observed, five animals at each dose, and the response is the number of deaths:
import numpy as np
x = np.array([-0.86, -0.30, -0.05, 0.73])
n = np.array([5, 5, 5, 5])
y = np.array([0, 1, 3, 5])The model is
\[ y_i \sim \operatorname{Binomial}(n_i, p_i), \qquad \operatorname{logit}(p_i)=\alpha + \beta x_i. \]
In the demo, the default prior is flat over the plotted grid. That makes the posterior proportional to the likelihood:
\[ p(\alpha,\beta\mid y) \propto \prod_i \operatorname{logit}^{-1}(\alpha+\beta x_i)^{y_i} \left[1-\operatorname{logit}^{-1}(\alpha+\beta x_i)\right]^{n_i-y_i}. \]
The notebook computes the posterior density over a grid of \(\alpha\) and \(\beta\). For a durable Python translation, compute the same object on the log scale:
from scipy.special import expit, xlogy
A = np.linspace(-4, 8, 100)
B = np.linspace(-10, 40, 100)
eta = A[None, :, None] + B[:, None, None] * x[None, None, :]
p = expit(eta)
log_post = (xlogy(y, p) + xlogy(n - y, 1 - p)).sum(axis=2)
post = np.exp(log_post - log_post.max())
post = post / post.sum()The grid orientation is easy to get wrong: in the source notebook, rows correspond to B and columns to A. Making the singleton dimensions explicit prevents silent transposes.
Grid sampling is a useful bridge between exact calculation and MCMC. Draw cells with probabilities proportional to their posterior mass, then jitter inside the cell so the sample is not artificially stuck on lattice points:
rng = np.random.default_rng(123)
flat = rng.choice(post.size, size=1000, p=post.ravel())
ib, ia = np.unravel_index(flat, post.shape)
alpha = A[ia] + rng.uniform(-0.5, 0.5, size=ia.size) * (A[1] - A[0])
beta = B[ib] + rng.uniform(-0.5, 0.5, size=ib.size) * (B[1] - B[0])
ld50 = -alpha / betaThe derived quantity ld50 is the dose where death probability is 0.5. It is often more scientifically interpretable than either coefficient separately, and it exposes tail behavior: ratios of uncertain coefficients can have long, asymmetric tails.
The Stan version is almost trivial once the log-density is clear:
data {
int<lower=1> N;
array[N] int<lower=0> y;
array[N] int<lower=0> n;
vector[N] x;
}
parameters {
real alpha;
real beta;
}
model {
y ~ binomial_logit(n, alpha + beta * x);
}
generated quantities {
real ld50 = -alpha / beta;
}A real analysis should put weakly informative priors on alpha and beta. The flat-grid version is pedagogically clean, but it also lets extreme slopes leak into the posterior when data are nearly separated.