Hierarchical Poisson — R cross-check¶

lme4::glmer (Poisson GLMM) vs the from-scratch Gibbs sampler¶

Companion to hpois_python.ipynb (from-scratch Metropolis-within-Gibbs). We refit the same data with R's standard mixed-model tool, lme4::glmer(family = poisson) with a patient random intercept (1 | patient). Synthetic data is shared via hpois_synth.csv; the real data is epil.csv.

Frequentist vs Bayesian note. glmer uses Laplace-approximate maximum likelihood (point estimate of σ_b, asymptotic SEs); the from-scratch sampler is fully Bayesian (marginalises σ_b uncertainty). They agree closely, with a revealing difference at the significance boundary (see Section 2).

In [1]:
.libPaths(c("C:/Users/user/R/win-library/4.6", .libPaths()))
suppressMessages({library(lme4); library(MASS)})

# --- §1 synthetic: recover beta and sigma_b ---
d <- read.csv("hpois_synth.csv")
m <- glmer(y ~ x1 + x2 + (1 | group), data = d, family = poisson)
cat("glmer fixed effects (truth 0.5 / 0.8 / -0.4):\n"); print(round(fixef(m), 3))
cat(sprintf("sigma_b = %.3f   (truth 0.70;  from-scratch 0.68)\n", sqrt(VarCorr(m)$group[1])))
glmer fixed effects (truth 0.5 / 0.8 / -0.4):
(Intercept)          x1          x2 
      0.512       0.807      -0.415 
sigma_b = 0.675   (truth 0.70;  from-scratch 0.68)

1. synthetic recovery matches¶

glmer: β = 0.512 / 0.807 / −0.415 and σ_b = 0.675 — essentially identical to the from-scratch Gibbs (0.525 / 0.811 / −0.417, σ_b 0.679) and the truth. The two engines agree on the GLMM fit.

In [2]:
# --- §2 Epil: hierarchical Poisson, and the treatment effect across models ---
e <- read.csv("epil.csv"); e$lBase <- log(e$Base/4); e$lAge <- log(e$Age); e$patient <- factor(e$patient)
mp   <- glm(seizures ~ lBase + Trt + lAge + V4, family = poisson, data = e)            # single-level Poisson
mnb  <- glm.nb(seizures ~ lBase + Trt + lAge + V4, data = e)                            # single-level NegBin
mgl  <- glmer(seizures ~ lBase + Trt + lAge + V4 + (1 | patient), data = e, family = poisson)  # hierarchical

cat("glmer (hierarchical) fixed effects:\n"); print(round(summary(mgl)$coef[, 1:2], 3))
cat(sprintf("sigma_b = %.3f\n\n", sqrt(VarCorr(mgl)$patient[1])))
tab <- rbind(
  Poisson      = summary(mp)$coef['Trt', c(1,2,4)],
  NegBin       = summary(mnb)$coef['Trt', c(1,2,4)],
  glmer_hier   = summary(mgl)$coef['Trt', c(1,2,4)])
colnames(tab) <- c('Trt', 'SE', 'p'); tab <- cbind(tab, RR = exp(tab[,'Trt']))
cat("Treatment effect across models:\n"); print(round(tab, 3))
glmer (hierarchical) fixed effects:
            Estimate Std. Error
(Intercept)   -1.087      1.197
lBase          1.027      0.101
Trt           -0.315      0.150
lAge           0.332      0.343
V4            -0.160      0.054
sigma_b = 0.516

Treatment effect across models:
              Trt    SE     p    RR
Poisson    -0.017 0.048 0.727 0.983
NegBin     -0.233 0.100 0.019 0.792
glmer_hier -0.315 0.150 0.036 0.730
In [3]:
# treatment-effect forest: SE widens single-level -> hierarchical
est <- tab[, 'Trt']; se <- tab[, 'SE']; labs <- c('Poisson (1-level)','NegBin (1-level)','glmer (hierarchical)')
cols <- c('steelblue','steelblue','firebrick')
plot(est, 1:3, xlim=range(c(est-1.96*se, est+1.96*se, 0.1)), ylim=c(0.5,3.5), yaxt='n',
     pch=19, col=cols, xlab='Trt coefficient (95% CI)', ylab='', main='Epil: treatment effect by model')
arrows(est-1.96*se, 1:3, est+1.96*se, 1:3, angle=90, code=3, length=.06, col=cols)
abline(v=0, lty=3, col='gray'); axis(2, at=1:3, labels=labs, las=1, cex.axis=.8)
text(est, 1:3+0.18, sprintf('p=%.3f', tab[,'p']), cex=.8)
No description has been provided for this image

2. Epil: R confirms the result, and pins down the boundary¶

Hierarchical glmer fixed effects (matching the from-scratch Bayesian in parentheses):

coef glmer (SE) from-scratch Bayes (SD)
log(Base/4) 1.027 (0.101) 1.13 (0.10)
Trt −0.315 (0.150) −0.30 (0.18)
log(Age) 0.332 (0.343) 0.20 (0.45)
V4 −0.160 (0.054) −0.16 (0.06)
σ_b 0.516 0.576

Treatment effect across the three models:

model Trt SE RR p / 95% CI
Poisson (single-level) −0.017 0.048 0.98 p ≈ 0.7
NegBin (single-level) −0.233 0.100 0.79 p ≈ 0.02 (sig)
glmer (hierarchical) −0.315 0.150 0.73 p = 0.036 (borderline)

The story is the same as the from-scratch notebook: adding the patient random intercept widens the treatment SE (0.10 → 0.15) and pushes the effect from clearly significant toward the boundary.

The instructive nuance — frequentist vs Bayesian disagree at the margin: glmer's Laplace ML gives p = 0.036 (just significant), while the full-Bayes posterior gives RR 0.74 with 95% CrI [0.53, 1.07] (just not significant). The difference is exactly the extra uncertainty the Bayesian carries — it marginalises over σ_b (and small-cluster, 4-visits-per-patient effects that the Laplace approximation under-states). Robust conclusion: once within-patient correlation is modelled the progabide effect is fragile — sitting right on the 5% boundary, no longer the clear effect the single-level NegBin reported. Whether you call it "significant" depends on the inference framework, which is itself the cautionary lesson.

Conclusion¶

lme4::glmer and the from-scratch Gibbs agree on the GLMM fit (β, σ_b, the per-visit V4 effect). Both show the hierarchical model inflates the treatment SE ~50% vs single-level. The treatment effect lands on the significance boundary — frequentist marginally in, Bayesian marginally out — a textbook demonstration that modelling correlation changes conclusions, and that the verdict here is genuinely borderline.