Chapter 10: Binomial GLMMs

library(lmebayes)
ch10 <- readRDS(system.file(
  "extdata", "Chapter10_book_banning_binom.rds", package = "lmebayes"
))

Chapter 10: Binomial GLMMs

Binary outcomes with group structure. The data is book_banning from bayesrules (Johnson et al. 2022): challenges to books in US libraries, with an indicator for whether the book was removed, grouped by state.

The mechanics are the same as Chapter 09 — glmerb() with family = binomial() — so this chapter concentrates on what is different about binary data: separation, estimability, and why the sweep count is larger.

Setting up

data(book_banning, package = "bayesrules")
bb <- book_banning[, c("state", "removed", "violent")]
bb <- bb[complete.cases(bb), ]
bb$removed_i <- as.integer(bb$removed == 1L | bb$removed == "1")
bb$violent_i <- as.integer(
  bb$violent == TRUE | bb$violent == 1L | bb$violent == "TRUE"
)
bb$state <- factor(bb$state)

## Twelve largest states, to keep the stored fit small.
keep <- names(sort(table(bb$state), decreasing = TRUE))[seq_len(12L)]
bb <- droplevels(subset(bb, state %in% keep))

form <- removed_i ~ violent_i + (1 + violent_i || state)
model_setup(form, data = bb, family = binomial())
#> Call:
#>   model_setup(formula = form, data = bb, family = binomial())
#> --- Level 1 (Within-Group) ---
#>   removed_i ~ (Intercept) + violent_i
#>   Observations             : 629
#>   Group-effect coefficients: 2
#>   Group                    : state  [12 levels]
#>   Full-rank D_j: 12 of 12 groups
#>   Full-rank & estimable: 11 of 12 full-rank (11 of 12 total state)
#>     full-rank but not estimable: MI
#> --- Level 2 (Across-Group) ---
#>   (Intercept) ~ 1
#>   violent_i   ~ 1
#> --- Level 2 Rank ---
#>   (Restricted to 11 estimable state)
#>   (Intercept)  groups=11   predictors=1   full-rank
#>   violent_i    groups=11   predictors=1   full-rank
#>   Rank-deficient flags:
#>     (Intercept)  FALSE
#>     violent_i    FALSE
#>   popef.rank_ok: TRUE  -- model rank looks OK

Note the explicit conversion to integers. Logical and factor predictors get expanded by model.matrix() in ways that can produce correlated random-effect terms, which this package rejects; converting to 0/1 integers keeps the || structure intact. If you hit Correlated random effects are not supported, check for logical or factor variables inside the random-effects term first.

Full rank is not the same as estimable

The rank report shows something that did not appear in earlier chapters:

Full-rank D_j: 12 of 12 groups
Full-rank & estimable: 11 of 12 full-rank (11 of 12 total state)
  full-rank but not estimable: MI

Michigan’s design matrix has full column rank — it contains both violent and non-violent challenges — but the model is still not estimable there. That is separation: within Michigan, the outcome is perfectly predicted by violent_i, so the maximum-likelihood coefficient runs off to infinity. The likelihood is log-concave but has no interior maximum in that direction.

Separation is a binary-data problem specifically; it cannot happen with Gaussian or (in this form) Poisson responses. model_setup() runs a separate estimability check for exactly this reason, and it is the reason the check is reported on its own line rather than folded into the rank test.

Michigan is not thrown away. It keeps its coefficients, drawn from the prior in the direction the likelihood cannot pin down — which is exactly what a proper prior is for. What it loses is standing in the level-2 checks: an infinite-in-the-limit estimate carries no usable information about the population parameters, so model_setup() restricts the across-group rank condition to the 11 estimable states, and Prior_Setup_GLMM() calibrates from those. Details in vignette("Chapter-B08", package = "lmebayesCore").

Fitting

ps <- Prior_Setup_GLMM(form, data = bb, family = binomial(), pop.pwt = 0.01)

set.seed(2024)
fit <- glmerb(
  form,
  data         = bb,
  family       = binomial(),
  pfamily_list = pfamily_list(ps),
  n            = 200L,
  progbar      = FALSE
)
fit
#> Call:
#>   glmerb(formula = form, data = bb, family = binomial(), pfamily_list = pfamily_list(ps), 
#>     n = 200L, progbar = FALSE)
#> Bayesian generalized linear mixed model  [binomial; 200 draws, two-block Gibbs]
#> Formula: removed_i ~ violent_i + (1 + violent_i || state) 
#> Random effects (variance components fixed at glmer estimates):
#>  Groups  Name        Std.Dev.
#>  state   (Intercept) 0.8630  
#>  state.1 violent_i   0.8198  
#> Number of obs: 629,  groups: state, 12
#> --- Block 2 hyperparameters (ICM posterior mode; MCMC means when simulated) ---
#>   RE component  parameter        ICM mode   popef.means    draws SD
#>   ------------  -----------  ------------  ------------  ----------
#>   (Intercept)   (Intercept)       -1.1770       -1.2250      0.3586
#>   violent_i     (Intercept)        0.3342        0.2685      0.3985

Reading it on the probability scale

Coefficients are log-odds. Two useful transformations:

pm <- unlist(ch10$popef_means)
round(rbind(log_odds = pm, odds_ratio = exp(pm)), 3)
#>            (Intercept).(Intercept) violent_i.(Intercept)
#> log_odds                    -1.225                 0.268
#> odds_ratio                   0.294                 1.308

plogis(pm[[1]])            # P(removed), non-violent
#> [1] 0.2270659
plogis(pm[[1]] + pm[[2]])  # P(removed), violent
#> [1] 0.2775859

In an average state, a challenged non-violent book has roughly a 23 percent chance of removal, rising modestly for books challenged as violent. The posterior SD on the violent_i effect is larger than the effect itself, so this is weak evidence of any population-level difference.

Note that plogis() of the mean log-odds is not the mean probability — the transformation is nonlinear. For a proper posterior on the probability scale, transform the draws and then summarize:

p_draws <- plogis(fit$popef[["(Intercept)"]][, 1])
quantile(p_draws, c(0.025, 0.5, 0.975))

Per-state effects

#>    grpvar        term grp    condval condsd
#> 1   state (Intercept)  CA -0.8438486     NA
#> 2   state (Intercept)  CO -1.9240385     NA
#> 3   state (Intercept)  FL -0.9772028     NA
#> 4   state (Intercept)  IL -0.9855758     NA
#> 5   state (Intercept)  IN -1.0040763     NA
#> 6   state (Intercept)  MI -1.3526249     NA
#> 7   state (Intercept)  NY -0.4239438     NA
#> 8   state (Intercept)  OH -0.8093770     NA
#> 9   state (Intercept)  OK -0.7953365     NA
#> 10  state (Intercept)  OR -2.8705434     NA
#> 11  state (Intercept)  PA -2.5431215     NA
#> 12  state (Intercept)  VA -0.0915660     NA

These are log-odds intercepts per state, and they are full coefficients rather than deviations (Chapter 02). Oregon at about -2.87 and Virginia at about -0.09 correspond to removal probabilities of roughly 5 percent and 48 percent — a large practical difference that a state-level fixed effect would have estimated far more noisily, and that complete pooling would have erased entirely.

Why the sweep count is higher

c(m_convergence = ch10$m_convergence)
#> m_convergence 
#>             8

8 sweeps, against 3 for the Poisson model of Chapter 09 and 1 for the Gaussian fits.

The reason is information per observation. A binary response carries at most about 0.25 units of Fisher information per observation, and much less when the probability is near 0 or 1 — which it is here, since most challenges do not result in removal. With only 12 states and weak per-observation information, the group coefficients and the population coefficients are tightly coupled, the rate \(\lambda^*\) is close to one, and more sweeps are needed.

This is the same relationship as in Chapter 02: models that shrink hard mix slowly. Binary outcomes with few groups and rare events sit at the difficult end of that spectrum, and the sweep count reports it honestly rather than hiding it.

The full summary

summary(fit)
#> Call:
#>   glmerb(formula = form, data = bb, family = binomial(), pfamily_list = pfamily_list(ps), 
#>     n = 200L, progbar = FALSE)
#> Bayesian linear mixed model fit  [200 draws, two-block Gibbs]
#> Formula: removed_i ~ violent_i + (1 + violent_i || state) 
#> Random effects (variance components fixed at glmer estimates):
#>  Groups  Name        Std.Dev.
#>  state   (Intercept) 0.8630  
#>  state.1 violent_i   0.8198  
#> Number of obs: 629,  groups: state, 12
#> === 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.7448  0.7448        0.8630         NA         NA
#> violent_i   dNormal      0.6721  0.6721        0.8198         NA         NA
#>              glmer glmer SD
#> (Intercept) 0.7448   0.8630
#> violent_i   0.6721   0.8198
#> Overview:
#>             Post.Mode Post.Mean Post.Sd MC Error Mean SD Pr(Prior_tail)
#> (Intercept)    0.7448    0.7448  0.0000   0.0000  0.8630             NA
#> violent_i      0.6721    0.6721  0.0000   0.0000  0.8198             NA
#>             Cand/draw
#> (Intercept)         1
#> violent_i           1
#> Distribution percentiles (tau^2):
#>               1.0%   2.5%   5.0% Median  95.0%  97.5% 99.0%
#> (Intercept) 0.7448 0.7448 0.7448 0.7448 0.7448 0.7448 0.745
#> violent_i   0.6721 0.6721 0.6721 0.6721 0.6721 0.6721 0.672
#> SD credible interval (sqrt(tau^2) draws; glmer SD for reference):
#>               2.5% Median  97.5% glmer SD
#> (Intercept) 0.8630 0.8630 0.8630    0.863
#> violent_i   0.8198 0.8198 0.8198    0.820
#> === Block 2: Level-2 fixed effects (hyperparameters) ===
#> Prior and glmer reference:
#>                          Prior Mean Prior.sd   glmer glmer.se
#> (Intercept)::(Intercept)    -1.1702   2.7541 -1.2075    0.277
#> violent_i::(Intercept)       0.0000   4.1137  0.2859    0.413
#> Overview:
#>                          fixef.mode fixef.means Post.Sd MC.Error Pr(Prior_tail)
#> (Intercept)::(Intercept)    -1.1770     -1.2250  0.3586    0.025           0.43
#> violent_i::(Intercept)       0.3342      0.2685  0.3985    0.028           0.22
#> Distribution percentiles:
#>                             1.0%    2.5%    5.0%  Median   95.0%   97.5% 99.0%
#> (Intercept)::(Intercept) -1.9542 -1.8585 -1.7845 -1.2609 -0.6676 -0.5536 -0.44
#> violent_i::(Intercept)   -0.6092 -0.5794 -0.4291  0.2895  0.8636  0.9277  1.28
#> === Block 1: Random effects (group-level) ===
#> Summary of posterior mode (ranef.mode) across groups:
#>                 Mean       SD      Min       Q1   Median       Q3      Max
#> (Intercept) -1.17703  0.80772 -2.76751 -1.47585 -0.92871 -0.77131 -0.07223
#> violent_i    0.33533  0.50096 -0.23663  0.11177  0.20765  0.44171  1.73505
#>             MCMC.mean
#> (Intercept)    -1.218
#> violent_i       0.288
#> Per-group random effects: inspect fit$groupef.mode or fit$groupef,
#>   or call summary(fit, groups = <level ids>) for selected groups.

Both random-effect standard deviations are near 0.85 on the log-odds scale, which is substantial: a one-standard-deviation state differs from average by a factor of about 2.4 in odds. As in Chapter 09, the between-group variation exceeds the population-level effect, which is the signature of a model that needed random slopes.

Practical notes for binary data

  • Convert to integers before fitting; logical and factor variables in the random-effects term commonly trigger the correlated-effects error.
  • Check the estimability line, not just the rank line. Separation is silent otherwise.
  • Expect more sweeps. If m_convergence climbs into the tens, the cause is usually too few groups or too rare an outcome, not a sampler problem.
  • Watch for a lambda* > 0.95 warning. The chain is still valid, but the model is close to the boundary where the group and population levels cannot be separated.

See also

  • Chapter 09 — the Poisson case and the shared GLMM machinery.
  • Chapter 11 — mode-only fits, which avoid the sampling cost entirely.
  • vignette("Chapter-B08", package = "lmebayesCore") — the rank and estimability conditions.
  • ?glmerb, ?model_setup

References

Johnson, Alicia A., Miles Q. Ott, and Mine Dogucu. 2022. Bayes Rules! An Introduction to Applied Bayesian Modeling. CRC Press. https://www.bayesrulesbook.com.