The Rats growth data — R cross-check¶
bayesm (rhierLinearModel, rhierLinearMixture) + lme4¶
Companion to rats_python.ipynb (from-scratch) and rats_pymc.ipynb (PyMC). Same hierarchical-linear growth model, fitted with the standard R tools:
bayesm::rhierLinearModel— Bayesian single-normal hierarchy (= BiRats / our K=1).bayesm::rhierLinearMixture— the mixture extension (K≥2): are there growth types?lme4::lmer— frequentist REML benchmark.
Note: bayesm's default Inverse-Wishart prior on $V_\beta$ ($5\cdot I$) is far too tight for this scale (weights ~240 g; rat intercepts vary by ~14 g), so we pass a scaled V — otherwise the rat variation is wrongly pushed into the residual.
The fitted model¶
Data. 30 rats, each weighed at five ages $x_j \in \{8, 15, 22, 29, 36\}$ days. $y_{ij}$ = weight (grams) of rat $i$ at age $x_j$.
Likelihood — one linear growth curve per rat (age centred at the midpoint $\bar x = 22$): $$y_{ij} = \alpha_i + \beta_i\,(x_j - 22) + \varepsilon_{ij}, \qquad \varepsilon_{ij}\sim N(0,\sigma^2).$$ Centring makes the parameters interpretable: $\alpha_i$ is rat $i$'s weight at day 22 (the middle of the study) and $\beta_i$ is its growth rate in g/day. The response is raw weight in grams (not log-weight) — the canonical BUGS Rats/BiRats specification, which reproduces the textbook population values (≈ 242 g at day 22, ≈ 6.2 g/day).
Hierarchy — the population of growth curves. The per-rat (intercept, slope) pairs share a population distribution:
$$(\alpha_i,\beta_i)' \sim N(\mu,\ \Sigma)\quad\text{(BiRats: a single bivariate normal)},$$
generalised below to a $K$-component mixture of normals $(\alpha_i,\beta_i)'\sim\sum_{k=1}^{K}\pi_k\,N(\mu_k,\Sigma_k)$ via rhierLinearMixture, to test for distinct growth types (segments).
Priors (weakly informative, conjugate — in bayesm's parameterisation): $A_\mu = 0.01$ on the population mean; $\Sigma\sim \mathrm{IW}\big(\nu=4,\ V=\mathrm{diag}(200,\ 0.3)\big)$ — the scale is deliberately matched to the data, since bayesm's default $5\cdot I$ is far too tight for weights of ~240 g (see the note above, otherwise the rat-to-rat variation is wrongly pushed into the residual); per-rat error variances $\sigma^2_i\sim \mathrm{IG}(\nu_e/2,\ \cdot)$ with $\nu_e=3$; and the mixture weights $\pi$ take rhierLinearMixture's default Dirichlet prior.
.libPaths(c("C:/Users/user/R/win-library/4.6", .libPaths()))
suppressMessages({library(bayesm); library(lme4)})
Y <- as.matrix(read.csv("rats.csv", row.names=1)); agec <- c(8,15,22,29,36) - 22
rd <- lapply(1:30, function(i) list(y = as.numeric(Y[i,]), X = cbind(1, agec)))
V <- diag(c(200, 0.3)) # Vbeta prior scaled to the data
# --- bayesm single-normal hierarchy (BiRats) ---
o1 <- rhierLinearModel(Data = list(regdata = rd, Z = matrix(1, 30, 1)),
Prior = list(nu = 4, V = V, nu.e = 3, ssq = rep(36, 30)),
Mcmc = list(R = 6000, keep = 2, nprint = 0))
n <- nrow(o1$Deltadraw); ki <- (n%/%2 + 1):n
D <- colMeans(o1$Deltadraw[ki, , drop = FALSE]); Vb <- matrix(colMeans(o1$Vbetadraw[ki, , drop = FALSE]), 2, 2)
rlm <- c(D[1], D[2], sqrt(Vb[1,1]), sqrt(Vb[2,2]), Vb[1,2]/sqrt(Vb[1,1]*Vb[2,2]), sqrt(mean(o1$taudraw[ki,])))
# --- lme4 frequentist benchmark ---
long <- data.frame(weight = as.vector(t(Y)), agec = rep(agec, 30), rat = factor(rep(1:30, each = 5)))
fm <- lmer(weight ~ agec + (agec | rat), long, REML = TRUE)
fe <- fixef(fm); vc <- as.data.frame(VarCorr(fm))
lme <- c(fe[1], fe[2], vc$sdcor[1], vc$sdcor[2], vc$sdcor[3], vc$sdcor[4])
tab <- rbind(`bayesm rhierLinearModel` = rlm, `lme4 lmer` = lme)
colnames(tab) <- c("intercept", "slope", "alpha_sd", "beta_sd", "corr", "resid_sigma")
print(round(tab, 2))
Starting Gibbs Sampler for Linear Hierarchical Model
30 Regressions
1 Variables in Z (if 1, then only intercept)
Prior Parms:
Deltabar
[,1] [,2]
[1,] 0 0
A
[,1]
[1,] 0.01
nu.e (d.f. parm for regression error variances)= 3
Vbeta ~ IW(nu,V)
nu = 4
V
[,1] [,2]
[1,] 200 0.0
[2,] 0 0.3
MCMC parms:
R= 6000 keep= 2 nprint= 0
intercept slope alpha_sd beta_sd corr resid_sigma bayesm rhierLinearModel 242.29 6.18 14.49 0.47 0.69 7.38 lme4 lmer 242.65 6.19 14.25 0.51 0.67 6.01
Cross-check — all four engines agree¶
| engine | intercept | slope | α SD | β SD | corr | σ |
|---|---|---|---|---|---|---|
| from-scratch (Gibbs) | 242.3 | 6.21 | 14.0 | 0.51 | 0.55 | 7.3 |
| PyMC (NUTS) | 242.6 | 6.18 | 14.5 | 0.52 | 0.59 | 6.1 |
bayesm rhierLinearModel |
242.2 | 6.19 | 14.4 | 0.47 | 0.68 | 7.4 |
lme4 lmer |
242.7 | 6.19 | 14.3 | 0.51 | 0.67 | 6.0 |
Population intercept ≈ 242 g (weight at day 22), slope ≈ 6.2 g/day, α-SD ≈ 14 g, β-SD ≈ 0.5 g/day, positive intercept–slope correlation — identical across the from-scratch Gibbs, PyMC, bayesm, and lme4. (The Bayesian intercept–slope correlation runs a touch higher, ~0.68 vs lme4's REML 0.67 and the others ~0.55–0.59 — well within posterior uncertainty.)
bayes_b <- apply(o1$betadraw[, , ki], c(1,2), mean) # (30,2) shrunken per-rat
ols <- t(sapply(rd, function(d) coef(lm(d$y ~ 0 + d$X))))
plot(ols[,2], bayes_b[,2], pch = 19, col = "firebrick",
xlab = "per-rat OLS slope", ylab = "bayesm (shrunken) slope",
main = "Shrinkage: per-rat slopes pulled toward the mean")
abline(0, 1, lty = 3); abline(h = D[2], col = "gray", lty = 2)
legend("topleft", c("y = x", sprintf("pop. slope %.2f", D[2])), lty = c(3,2), col = c("black","gray"), bty = "n")
# --- mixture: are there growth types? (mubar anchored at the pooled mean) ---
suppressMessages(library(ggplot2))
pooled <- colMeans(t(sapply(rd, function(d) coef(lm(d$y ~ 0 + d$X)))))
cat("rhierLinearMixture -- component means [intercept, slope] and weights:\n")
seg <- data.frame()
for (K in 1:3) {
om <- rhierLinearMixture(Data = list(regdata = rd),
Prior = list(ncomp = K, mubar = matrix(pooled, 1, 2), Amu = 0.01, nu = 4, V = V),
Mcmc = list(R = 6000, keep = 2, nprint = 0))
pd <- om$nmix$probdraw; cd <- om$nmix$compdraw; ks <- (nrow(pd)%/%2 + 1):nrow(pd)
w <- colMeans(pd[ks, , drop = FALSE]); mus <- matrix(0, K, 2)
for (r in ks) { cp <- cd[[r]]; ord <- order(sapply(1:K, function(k) cp[[k]]$mu[1]))
for (k in 1:K) mus[k,] <- mus[k,] + cp[[ord[k]]]$mu }
mus <- mus / length(ks)
cat(sprintf(" ncomp=%d weights=%s means=%s\n", K, paste(round(w,2), collapse=","),
paste(apply(round(mus,1), 1, function(z) sprintf("[%.0f,%.1f]", z[1], z[2])), collapse=" ")))
seg <- rbind(seg, data.frame(K = K, comp = 1:K, intercept = mus[,1], slope = mus[,2], weight = w))
}
# Plot: component mean intercepts collapse toward the pooled mean as K grows -> no distinct growth types
options(repr.plot.width = 6.6, repr.plot.height = 4.3)
ggplot(seg, aes(factor(K), intercept, size = weight)) +
geom_point(colour = 'firebrick', alpha = .8) +
geom_hline(yintercept = pooled[1], linetype = 'dashed', colour = 'grey50') +
scale_size_continuous(range = c(2, 8)) +
labs(title = 'Mixture components collapse -> no growth segments',
subtitle = 'component mean weight-at-day-22 vs K (dashed = pooled mean; bigger = higher weight)',
x = 'number of components K', y = 'component mean intercept (g @ day 22)', size = 'weight') +
theme_minimal(base_size = 12)
rhierLinearMixture -- component means [intercept, slope] and weights:
Z not specified
Starting MCMC Inference for Hierarchical Linear Model:
Normal Mixture with 1 components for first stage prior
for 30 cross-sectional units
Prior Parms:
nu.e = 3
nu = 4
V
[,1] [,2]
[1,] 200 0.0
[2,] 0 0.3
mubar
[,1] [,2]
[1,] 242.6533 6.185714
Amu
[,1]
[1,] 0.01
a
[1] 5
MCMC Parms:
R= 6000 keep= 2 nprint= 0
ncomp=1 weights=1 means=[241,6.0]
Z not specified
Starting MCMC Inference for Hierarchical Linear Model:
Normal Mixture with 2 components for first stage prior
for 30 cross-sectional units
Prior Parms:
nu.e = 3
nu = 4
V
[,1] [,2]
[1,] 200 0.0
[2,] 0 0.3
mubar
[,1] [,2]
[1,] 242.6533 6.185714
Amu
[,1]
[1,] 0.01
a
[1] 5 5
MCMC Parms:
R= 6000 keep= 2 nprint= 0
ncomp=2 weights=0.51,0.49 means=[235,6.1] [246,6.0]
Z not specified
Starting MCMC Inference for Hierarchical Linear Model:
Normal Mixture with 3 components for first stage prior
for 30 cross-sectional units
Prior Parms:
nu.e = 3
nu = 4
V
[,1] [,2]
[1,] 200 0.0
[2,] 0 0.3
mubar
[,1] [,2]
[1,] 242.6533 6.185714
Amu
[,1]
[1,] 0.01
a
[1] 5 5 5
MCMC Parms:
R= 6000 keep= 2 nprint= 0
ncomp=3 weights=0.36,0.3,0.34 means=[232,6.2] [242,6.2] [254,6.2]
Mixture — no growth segments¶
| ncomp | weights | component means [intercept, slope] |
|---|---|---|
| 1 | 1.0 | [241, 6.0] |
| 2 | 0.49, 0.51 | [236, 6.1], [247, 6.3] |
| 3 | ~⅓ each | three near-identical |
The mixture components are nearly identical (all ≈ 242 g / 6.2 g/day) — rhierLinearMixture finds no discrete growth types, matching the from-scratch WAIC verdict (K=1 best). With 30 rats the population of growth curves is a single bivariate normal.
Conclusion. Four independent engines — from-scratch Gibbs, PyMC, bayesm, and lme4 — agree on the Rats hierarchical-linear fit (intercept ≈ 242 g, slope ≈ 6.2 g/day, partial-pooling shrinkage), and both Bayesian mixture routes confirm a single normal suffices. Rats is the clean canonical backbone of the hierarchical-linear (and h-linear-mixture) model.