Everything so far has been Gaussian. This chapter switches to counts,
which changes the sampler substantially even though the interface barely
moves: replace lmerb() with glmerb() and add
family = poisson().
The data is airbnb from bayesrules
(Johnson et al. 2022): listings in Chicago
neighborhoods, with the number of reviews as the count outcome. The fit
takes about half a minute because every Block 1 draw runs an envelope
accept-reject sampler, so it is precomputed by
data-raw/make_vignette_outputs.R.
| Gaussian | Poisson | |
|---|---|---|
| Block 1 draw | Conjugate normal, closed form | Envelope accept-reject |
| Joint posterior | Multivariate normal (when \(\tau^2\) fixed) | Never normal |
| Exact iid route | Available | Not available |
| Convergence rate | Certified by theorem | Local-Gaussian heuristic |
| Pilot stage | Only when \(\tau^2\) sampled | Always |
The last three rows matter. For a Gaussian target the rate \(\lambda^*\) of Chapter 06 is a theorem; for
a Poisson likelihood it is evaluated at IRLS-style curvature weights
taken at the posterior mode, which makes it a good practical guide
rather than a certificate. The engine compensates by always running a
pilot stage. The distinction is spelled out in
vignette("Chapter-B02", package = "lmebayesCore"), under
“Beyond the Gaussian case”.
Log-concavity is what makes the Block 1 draw possible at all. The Poisson log-likelihood with a log link is log-concave in the linear predictor, so the likelihood-subgradient envelope of Nygren and Nygren (2006) is a valid bound and accept-reject sampling is exact — not a Metropolis approximation.
data(airbnb, package = "bayesrules")
ab <- airbnb[, c("neighborhood", "reviews", "rating", "room_type")]
ab <- ab[complete.cases(ab), ]
ab$neighborhood <- droplevels(factor(ab$neighborhood))
ab$rating_c <- ab$rating - mean(ab$rating)
form <- reviews ~ rating_c + (1 + rating_c || neighborhood)
model_setup(form, data = ab, family = poisson())#> Call:
#> model_setup(formula = form, data = ab, family = poisson())
#> --- Level 1 (Within-Group) ---
#> reviews ~ (Intercept) + rating_c
#> Observations : 1561
#> Group-effect coefficients: 2
#> Group : neighborhood [43 levels]
#> Full-rank D_j: 35 of 43 groups
#> rank-deficient: Beverly, Brighton Park, Burnside, Edison Park, Englewood, O'Hare, West Englewood, West Lawn
#> Full-rank & estimable: 35 of 35 full-rank (35 of 43 total neighborhood)
#> --- Level 2 (Across-Group) ---
#> (Intercept) ~ 1
#> rating_c ~ 1
#> --- Level 2 Rank ---
#> (Restricted to 35 estimable neighborhood)
#> (Intercept) groups=35 predictors=1 full-rank
#> rating_c groups=35 predictors=1 full-rank
#> Rank-deficient flags:
#> (Intercept) FALSE
#> rating_c FALSE
#> popef.rank_ok: TRUE -- model rank looks OK
1561 listings across 43 neighborhoods. Each neighborhood gets its own baseline review rate and its own rating effect. Rating is centered so the intercept means “log expected reviews for an average-rated listing”.
Eight neighborhoods are rank-deficient. For a count model that usually means too few listings, or no variation in rating among them. They stay in the fit and still receive coefficients — with nothing to learn from in the deficient direction, they are shrunk to the population value — but they are set aside when the level-2 rank condition is checked and when priors are calibrated.
ps <- Prior_Setup_GLMM(form, data = ab, family = poisson(), pop.pwt = 0.01)
set.seed(2024)
fit <- glmerb(
form,
data = ab,
family = poisson(),
pfamily_list = pfamily_list(ps),
n = 200L,
progbar = FALSE
)
fit#> Call:
#> glmerb(formula = form, data = ab, family = poisson(), pfamily_list = pfamily_list(ps),
#> n = 200L, progbar = FALSE)
#> Bayesian generalized linear mixed model [poisson; 200 draws, two-block Gibbs]
#> Formula: reviews ~ rating_c + (1 + rating_c || neighborhood)
#> Random effects (variance components fixed at glmer estimates):
#> Groups Name Std.Dev.
#> neighborhood (Intercept) 0.5135
#> neighborhood.1 rating_c 1.2326
#> Number of obs: 1561, groups: neighborhood, 43
#> --- Block 2 hyperparameters (ICM posterior mode; MCMC means when simulated) ---
#> RE component parameter ICM mode popef.means draws SD
#> ------------ ----------- ------------ ------------ ----------
#> (Intercept) (Intercept) 3.1713 3.1595 0.0864
#> rating_c (Intercept) 0.2312 0.2597 0.1927
Two interface differences from lmerb(). There is no
dispersion_ranef argument — the Poisson family fixes the
dispersion at one, so there is no \(\sigma^2\) to specify. And the reference
fit is glmer() rather than lmer(), which is
what Prior_Setup_GLMM() calibrates against.
The column header reads ICM mode rather than
fixef.mode: for a non-Gaussian likelihood the pre-sampling
point estimate is the posterior mode, found by iterated
conditional modes, and the mode is not the mean. That is precisely why a
pilot stage is needed — a chain started at the mode begins at an unknown
positive distance from the target mean, so the mean-error term of the
convergence bound is no longer zero.
Coefficients are on the log scale, so exponentiate to get rate ratios.
pm <- unlist(ch09$popef_means)
round(rbind(log_scale = pm, rate_ratio = exp(pm)), 3)
#> (Intercept).(Intercept) rating_c.(Intercept)
#> log_scale 3.16 0.260
#> rate_ratio 23.56 1.297The intercept exponentiates to the expected number of reviews for an
average-rated listing in an average neighborhood — about 24. The
rating_c coefficient is a multiplicative effect per unit of
rating: each additional rating point multiplies expected reviews by
roughly 1.3.
But look at the posterior standard deviation on that slope — it is nearly as large as the coefficient. The population-average rating effect is not well determined, and the summary explains why.
#> Call:
#> glmerb(formula = form, data = ab, family = poisson(), pfamily_list = pfamily_list(ps),
#> n = 200L, progbar = FALSE)
#> Bayesian linear mixed model fit [200 draws, two-block Gibbs]
#> Formula: reviews ~ rating_c + (1 + rating_c || neighborhood)
#> Random effects (variance components fixed at glmer estimates):
#> Groups Name Std.Dev.
#> neighborhood (Intercept) 0.5135
#> neighborhood.1 rating_c 1.2326
#> Number of obs: 1561, groups: neighborhood, 43
#> === Block 2 dispersion (RE variance tau^2_k) ===
#> Prior and glmer reference:
#> Prior 1/E[1/tau2] E[tau2] sqrt(E[tau2]) disp_lower disp_upper
#> (Intercept) dNormal 0.2637 0.2637 0.5135 NA NA
#> rating_c dNormal 1.5194 1.5194 1.2326 NA NA
#> glmer glmer SD
#> (Intercept) 0.2637 0.5135
#> rating_c 1.5194 1.2326
#> Overview:
#> Post.Mode Post.Mean Post.Sd MC Error Mean SD Pr(Prior_tail)
#> (Intercept) 0.2637 0.2637 0.0000 0.0000 0.5135 NA
#> rating_c 1.5194 1.5194 0.0000 0.0000 1.2326 NA
#> Cand/draw
#> (Intercept) 1
#> rating_c 1
#> Distribution percentiles (tau^2):
#> 1.0% 2.5% 5.0% Median 95.0% 97.5% 99.0%
#> (Intercept) 0.2637 0.2637 0.2637 0.2637 0.2637 0.2637 0.264
#> rating_c 1.5194 1.5194 1.5194 1.5194 1.5194 1.5194 1.519
#> SD credible interval (sqrt(tau^2) draws; glmer SD for reference):
#> 2.5% Median 97.5% glmer SD
#> (Intercept) 0.5135 0.5135 0.5135 0.513
#> rating_c 1.2326 1.2326 1.2326 1.233
#> === Block 2: Level-2 fixed effects (hyperparameters) ===
#> Prior and glmer reference:
#> Prior Mean Prior.sd glmer glmer.se
#> (Intercept)::(Intercept) 3.1946 0.8256 3.1645 0.083
#> rating_c::(Intercept) 0.0000 2.0697 0.2394 0.208
#> Overview:
#> fixef.mode fixef.means Post.Sd MC.Error Pr(Prior_tail)
#> (Intercept)::(Intercept) 3.17126 3.15954 0.08644 0.006 0.320
#> rating_c::(Intercept) 0.23124 0.25969 0.19266 0.014 0.085
#>
#> (Intercept)::(Intercept)
#> rating_c::(Intercept) .
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#> Distribution percentiles:
#> 1.0% 2.5% 5.0% Median 95.0% 97.5%
#> (Intercept)::(Intercept) 2.99686 3.00845 3.02642 3.15813 3.30024 3.33337
#> rating_c::(Intercept) -0.15794 -0.09201 -0.03112 0.26369 0.58579 0.68493
#> 99.0%
#> (Intercept)::(Intercept) 3.379
#> rating_c::(Intercept) 0.736
#> === Block 1: Random effects (group-level) ===
#> Summary of posterior mode (ranef.mode) across groups:
#> Mean SD Min Q1 Median Q3 Max MCMC.mean
#> (Intercept) 3.1711 0.4874 2.1303 2.8718 3.2571 3.3901 4.2406 3.164
#> rating_c 0.2331 1.1223 -1.8120 -0.6153 0.2560 0.9860 2.2906 0.243
#> Per-group random effects: inspect fit$groupef.mode or fit$groupef,
#> or call summary(fit, groups = <level ids>) for selected groups.
The random-effects standard deviation for rating_c is
larger than the population mean effect itself. Neighborhoods genuinely
differ in how rating relates to review volume — in some, better-rated
listings get many more reviews; in others the relationship is flat or
reversed. Reporting only the population coefficient would describe none
of them well. This is the case for a random slope rather than a fixed
one, and it is visible directly in the variance components.
There is no Residual row in the dispersion table.
Poisson has no free dispersion parameter; the variance is determined by
the mean. If your counts are overdispersed relative to that, the random
intercept absorbs some of it, but a genuine overdispersion model is
outside this package’s model class.
3 sweeps per stored draw, and a pilot stage ran before the main
chains. Because the rate is heuristic here rather than certified, the
sensible check is empirical: the sweep history should show the
cross-chain mean flat and the cross-chain variance settled by the last
sweep, exactly as in Chapter 06. fit$sweep_history$main
carries it.
The Gaussian fits in earlier chapters took a second or two. This one
takes about thirty. The difference is entirely Block 1: every group, on
every sweep, on every chain, runs an envelope construction and an
accept-reject loop. fit$groupef.iters reports how many
candidates were needed per accepted draw — around eight here, which is
normal for a Poisson model and means the envelope is doing real
work.
Practical consequences: develop with small n and raise
it for the final run; prefer tv_tol at its default rather
than tightening it (each extra sweep is a full pass over all groups and
chains); and remember that glmbayes
Chapter-A10 documents optional OpenCL offload for envelope
construction, which this package inherits.
simulate = FALSE, when
you only want the mode.vignette("Chapter-A05", package = "glmbayes") —
likelihood subgradient densities, the basis of the Block 1 draw.?glmerb