Chapter 08: Observation dispersion

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

Chapter 08: Observation dispersion

Chapter 07 relaxed the assumption that the between-group variances \(\tau^2_k\) are known. This chapter does the same for the within-group variance \(\sigma^2\) — the residual scatter of individual observations around their group’s fitted line.

Three levels of ambition:

Specification \(\sigma^2\) Argument
Fixed scalar One number, held at the lmer estimate dispersion_ranef = ps$group.dispersion
Sampled scalar One number with a posterior dispersion_ranef = dGamma(...)
Per-group One \(\sigma^2_j\) per group, each sampled dispformula = ~group, dGamma_list()

The per-group case is the interesting one, and it is what this chapter demonstrates. Because it runs an accept-reject dispersion draw for every group on every sweep, the fit takes about a minute, so it is precomputed by data-raw/make_vignette_outputs.R and loaded here.

The default: one fixed variance

data(sleepstudy, package = "lme4")
dat <- sleepstudy
dat$Days_c <- dat$Days - mean(dat$Days)
form <- Reaction ~ Days_c + (1 + Days_c || Subject)

ps <- Prior_Setup_GLMM(form, data = dat, pop.pwt = 0.01)
ps$group.dispersion
#> [1] 709.826
#> attr(,"source")
#> [1] "pooled S_marg (A12 3.3.4)"
#> attr(,"classical")
#> [1] 654.941

One number, 709.8, applied to all 180 observations. It assumes every subject’s reaction times scatter by the same amount around their own line — which, for a sleep-deprivation study, is implausible on its face. Some people get erratic when tired and some do not, and that heterogeneity is a finding, not a nuisance.

Per-group dispersion

Ask Prior_Setup_GLMM() for a per-group calibration with dispformula:

ps_group <- Prior_Setup_GLMM(form, data = dat, pop.pwt = 0.01,
                             dispformula = ~Subject)
dl <- dGamma_list(ps_group)
length(dl)
dl[[1]]
#> 18
#> Call:  glmbayesCore::dGamma(shape = g$shape_ING, rate = g$rate, beta = matrix(0, 
#>     1, 1, dimnames = list("(Intercept)", NULL)), Inv_Dispersion = TRUE, 
#>     max_disp_perc = mdp_lev, disp_lower = disp_lower, disp_upper = disp_upper)
#> Prior Family: dGamma 
#> Prior List:
#> $shape
#> [1] 1.550505
#> $rate
#> [1] 1501.98
#> $beta
#>             [,1]
#> (Intercept)    0
#> $Inv_Dispersion
#> [1] TRUE
#> $max_disp_perc
#> [1] 0.99
#> $disp_lower
#> [1] 1088.082
#> $disp_upper
#> [1] 7277.047
#> attr(,"Prior Type")
#> [1] "dGamma"

dGamma_list() returns one dGamma prior per group. Each carries a shape and rate for a Gamma prior on the precision \(1/\sigma^2_j\) (hence Inv_Dispersion = TRUE), plus a truncation window disp_lower / disp_upper calibrated from that group’s own data.

The windows differ across groups by more than an order of magnitude, because they are quantiles of each group’s limiting posterior and the subjects genuinely differ. Calling dGamma_list() on a dispformula = ~1 setup is an error rather than a silent fallback:

dGamma_list(ps)
#> Error:
#> ! object's group.ing_prior is not per-group; call Prior_Setup_GLMM(..., dispformula = ~Subject) on a Gaussian model to calibrate per-group measurement-dispersion priors (dispformula = ~1, the default, skips this calibration).

Fitting

set.seed(2024)
fit <- lmerb(
  form, data = dat,
  pfamily_list     = pfamily_list(ps_group),
  dispersion_ranef = dl,
  dispformula      = ~Subject,
  n                = 200L,
  progbar          = FALSE
)
fit
#> Call:
#>   lmerb(formula = form, data = dat, pfamily_list = pfamily_list(ps_group), 
#>     dispersion_ranef = dl, dispformula = ~Subject, n = 200L, 
#>     progbar = FALSE)
#> Bayesian linear mixed model  [200 draws, two-block Gibbs]
#> Formula: Reaction ~ Days_c + (1 + Days_c || Subject) 
#> Random effects (variance components fixed at lmer estimates):
#> Conditional model:
#>  Groups   Name        Std.Dev.
#>  Subject  (Intercept) 38.265  
#>           Days_c       6.059  
#>  Residual                 NA  
#> Number of obs: 180,  groups: Subject, 18
#> --- Posterior means (ICM exact, under fixed variance components) ---
#>   RE component  parameter      fixef.mode   fixef.means    draws SD
#>   ------------  -----------  ------------  ------------  ----------
#>   (Intercept)   (Intercept)      298.4862      297.4782      9.0110
#>   Days_c        (Intercept)       10.3898       10.0397      1.4591

The engine is two-block Gibbs with m_convergence = 4. Sampling \(\sigma^2\) breaks the joint normality that made the earlier fits exact, so sweeps are required — the same mechanism as Chapter 07, applied at the observation level instead of the group level.

Note the Residual line now reads NA: there is no single residual standard deviation to report, because there are 18 of them.

Reading the dispersion

summary_sigma2() exists for exactly this case. On a scalar-dispersion fit it stops with a message; here it produces a full report.

summary_sigma2(fit)
#> Measurement dispersion (sigma^2, per group)
#> Formula: Reaction ~ Days_c + (1 + Days_c || Subject) 
#> Groups: Subject (18 level(s))  [200 draws]
#> Prior and lmer reference:
#>      Prior n_prior 1/E[1/sigma2] E[sigma2] sqrt(E[sigma2]) disp_lower
#> 308 dGamma   0.101      968.7038 2728.3675         52.2338  1088.0817
#> 309 dGamma   0.101       28.0287   78.9431          8.8850    31.4828
#> 310 dGamma   0.101       54.8003  154.3459         12.4236    61.5536
#> 330 dGamma   0.101      177.2113  499.1180         22.3409   199.0498
#> 331 dGamma   0.101      197.2977  555.6917         23.5731   221.6116
#> 332 dGamma   0.101     1363.6127 3840.6339         61.9728  1531.6571
#> 333 dGamma   0.101       57.1848  161.0618         12.6910    64.2320
#> 334 dGamma   0.101      160.0552  450.7975         21.2320   179.7795
#> 335 dGamma   0.101       46.2221  130.1852         11.4099    51.9183
#> 337 dGamma   0.101      109.9134  309.5725         17.5947   123.4585
#> 349 dGamma   0.101       75.4226  212.4288         14.5749    84.7173
#> 350 dGamma   0.101      246.1052  693.1587         26.3279   276.4339
#> 351 dGamma   0.101      187.2562  527.4097         22.9654   210.3327
#> 352 dGamma   0.101      249.9618  704.0209         26.5334   280.7658
#> 369 dGamma   0.101       94.1328  265.1264         16.2827   105.7333
#> 370 dGamma   0.101      236.2855  665.5014         25.7973   265.4041
#> 371 dGamma   0.101      231.5002  652.0234         25.5347   260.0290
#> 372 dGamma   0.101       47.8088  134.6543         11.6041    53.7006
#>     disp_upper n_data     lmer lmer SD
#> 308  7277.0467     10 654.9399 25.5918
#> 309   210.5554     10 654.9399 25.5918
#> 310   411.6681     10 654.9399 25.5918
#> 330  1331.2373     10 654.9399 25.5918
#> 331  1482.1296     10 654.9399 25.5918
#> 332 10243.6613     10 654.9399 25.5918
#> 333   429.5809     10 654.9399 25.5918
#> 334  1202.3581     10 654.9399 25.5918
#> 335   347.2273     10 654.9399 25.5918
#> 337   825.6855     10 654.9399 25.5918
#> 349   566.5858     10 654.9399 25.5918
#> 350  1848.7789     10 654.9399 25.5918
#> 351  1406.6965     10 654.9399 25.5918
#> 352  1877.7504     10 654.9399 25.5918
#> 369   707.1399     10 654.9399 25.5918
#> 370  1775.0120     10 654.9399 25.5918
#> 371  1739.0636     10 654.9399 25.5918
#> 372   359.1472     10 654.9399 25.5918
#> Overview:
#>      n_total Post.Mode Post.Mean  Post.Sd MC Error  Mean SD Pr(Prior_tail)
#> 308   10.101        NA  2416.164 1136.283   80.347   48.040          0.300
#> 309   10.101        NA    76.385   33.520    2.370    8.560          0.350
#> 310   10.101        NA   150.516   67.874    4.799   11.994          0.360
#> 330   10.101        NA   509.060  225.143   15.920   22.070          0.410
#> 331   10.101        NA   551.793  253.699   17.939   22.953          0.360
#> 332   10.101        NA  3484.084 1389.546   98.256   58.000          0.305
#> 333   10.101        NA   162.006   61.802    4.370   12.513          0.420
#> 334   10.101        NA   425.047  197.092   13.937   20.154          0.310
#> 335   10.101        NA   119.281   46.612    3.296   10.739          0.345
#> 337   10.101        NA   266.730  108.863    7.698   16.040          0.250
#> 349   10.101        NA   206.168   91.679    6.483   14.049          0.370
#> 350   10.101        NA   595.623  253.575   17.930   23.936          0.250
#> 351   10.101        NA   481.447  192.553   13.616   21.543          0.300
#> 352   10.101        NA   620.597  251.072   17.753   24.458          0.315
#> 369   10.101        NA   252.266  112.848    7.980   15.537          0.345
#> 370   10.101        NA   600.942  265.415   18.768   24.016          0.295
#> 371   10.101        NA   606.422  291.508   20.613   24.042          0.305
#> 372   10.101        NA   126.839   58.315    4.123   11.009          0.325
#>     Cand/draw
#> 308   454.199
#> 309   142.227
#> 310   102.754
#> 330   127.725
#> 331    68.914
#> 332     8.523
#> 333    55.625
#> 334    49.414
#> 335   364.715
#> 337  1443.852
#> 349    79.311
#> 350   626.420
#> 351    63.456
#> 352   102.732
#> 369    51.851
#> 370   326.788
#> 371    32.358
#> 372    67.639
#> Distribution percentiles (sigma^2):
#>          1.0%      2.5%      5.0%    Median     95.0%     97.5%     99.0%
#> 308 1132.2951 1163.9960 1252.8164 2043.3335 4611.9647 5586.9382 6360.7105
#> 309   35.3387   39.1729   41.2409   65.6035  146.5303  170.0677  179.0980
#> 310   62.9709   65.7958   68.6617  134.9033  302.8436  344.7183  359.4439
#> 330  224.6992  237.3274  260.3205  455.2978  985.9787 1053.1138 1194.4626
#> 331  231.7889  257.8157  278.4663  472.8253 1118.5123 1246.0137 1331.9463
#> 332 1583.8944 1793.5572 1843.9301 3109.2937 5989.4070 6787.0218 7938.0145
#> 333   70.0109   78.6750   87.0476  148.4158  272.9803  307.1649  338.9115
#> 334  193.7870  199.2195  212.9288  360.8734  828.4528  990.4675 1090.7642
#> 335   60.1902   63.9069   66.4300  103.7073  214.8687  246.7130  264.6717
#> 337  127.2926  133.8304  138.0755  240.6378  448.2460  587.8152  624.8474
#> 349   91.7745   97.5812  101.8211  185.3812  381.0241  419.3905  516.8079
#> 350  296.6683  304.3242  316.1304  529.5270 1184.2007 1294.6091 1398.3420
#> 351  225.1037  237.8322  252.2384  445.7604  872.0049  947.5692 1083.2297
#> 352  292.7784  312.4403  336.9877  579.1063 1107.0397 1176.3035 1363.3322
#> 369  109.2150  110.4281  122.5052  222.1470  477.6992  548.7330  621.7914
#> 370  284.6313  293.9897  303.5037  528.8531 1047.1712 1370.6783 1590.2976
#> 371  275.7320  288.7767  313.9880  529.1661 1252.6591 1497.1955 1572.1433
#> 372   57.2172   59.8910   61.4589  113.9158  246.4217  292.9594  321.2456

Three blocks, one per question.

Prior and reference lists, per subject, the Gamma prior in use, the prior mean, the truncation window, the number of observations, and the pooled lmer residual variance for comparison. n_data = 10 everywhere — ten observations is very little information about a variance, which is why the prior weight matters here more than anywhere else in the package.

Overview gives the posterior mean and standard deviation of each \(\sigma^2_j\), Monte Carlo error, and Cand/draw. That last column is accept-reject efficiency: values in the tens or hundreds mean the envelope is loose for that group, usually because its posterior sits near a truncation boundary.

Percentiles shows the spread directly, and the spread is the finding. The posterior means run from roughly 80 to 3600 — subject 308’s reaction times scatter more than an order of magnitude more than subject 309’s. A single pooled \(\sigma^2\) of 710 splits the difference and describes neither.

That heterogeneity also propagates: groups with large \(\sigma^2_j\) carry less weight in the population estimate and get shrunk harder toward it, which is the statistically correct behaviour and is not something a pooled model can express.

Sampling a single scalar

Between the two extremes there is a middle option: one \(\sigma^2\) shared by all groups, but sampled rather than fixed. Pass a single dGamma() object instead of a list:

fit_scalar_sampled <- lmerb(
  form, data = dat,
  pfamily_list     = pfamily_list(ps),
  dispersion_ranef = dGamma(shape = 2, rate = 2 * ps$group.dispersion,
                            Inv_Dispersion = TRUE),
  n = 200L, progbar = FALSE
)

This is the usual Bayesian treatment of a residual variance, and it is what you want when you believe the groups share a scatter but do not want to pretend you know its value. It costs sweeps, like any non-Gaussian target, but far less than the per-group version.

Choosing

Situation Use
Many observations per group, homogeneous scatter Fixed scalar
Few groups, homogeneous scatter, honest intervals wanted Sampled scalar
Groups visibly differ in residual spread dispformula = ~group with dGamma_list()

Diagnosing which you are in is easy before fitting: compute the residual variance per group from an lmer fit and look at the ratio of largest to smallest. Anything beyond a factor of three or four is worth modeling.

m <- lme4::lmer(form, data = dat, REML = TRUE)
v <- tapply(residuals(m), dat$Subject, var)
round(c(min = min(v), median = median(v), max = max(v),
        ratio = max(v) / min(v)), 1)
#>    min median    max  ratio 
#>   91.0  417.9 3296.6   36.2

A ratio of that size is not something a single pooled variance can absorb.

See also

  • Chapter 07 — the same relaxation applied to \(\tau^2_k\).
  • vignette("Chapter-B07", package = "lmebayesCore") — the per-group dispersion algebra and the marginal precision blocks.
  • ?dGamma_list, ?summary_sigma2