Skip to content

Use Pearson residuals for glmmTMB/MixMod homogeneity panel - #927

Open
jbogomolovas2 wants to merge 3 commits into
easystats:mainfrom
jbogomolovas2:fix-glmmtmb-homogeneity
Open

Use Pearson residuals for glmmTMB/MixMod homogeneity panel#927
jbogomolovas2 wants to merge 3 commits into
easystats:mainfrom
jbogomolovas2:fix-glmmtmb-homogeneity

Conversation

@jbogomolovas2

Copy link
Copy Markdown

Closes issue #926.

The problem

.model_diagnostic_homogeneity() branches by class. The glm branch uses rstandard(type = "pearson"), which divides each residual by sqrt(V(mu_i) * phi * (1 - h_ii)). The glmmTMB/MixMod branch instead divides by a single scalar:

stats::residuals(model) / residual_sigma

residuals.glmmTMB() defaults to type = "response", and residual_sigma is one number for the whole model. That is only correct when V() does not depend on mu. For non-mixed fits .sigma_glmmTMB_nonmixed() returns 1 outright for binomial, poisson and truncated_poisson, so no standardization happens at all and the panel plots sqrt(|raw response residuals|).

Since binomial spread is proportional to sqrt(p(1-p)) and the panel plots sqrt(|r_i|), the smooth follows V(mu_i)^(1/4) and must bow near p = 0.5 however well specified the model is.

This also differs from what ?check_model documents under Residuals for (Generalized) Linear Models, which says these plots use standardized Pearson's residuals for generalized linear models.

Scope: not just binomial

Implied per-observation SD (residuals(type="response") / residuals(type="pearson")) across families, glmmTMB 1.1.15:

family implied SD affected
gaussian constant [1, 1] no
t_family constant no
binomial varies yes
betabinomial varies yes
poisson varies yes
nbinom1 [0.99, 2.00] yes
nbinom2 varies yes
Gamma [1.02, 2.19] yes
beta [0.119, 0.154] yes
ordbeta [0.388, 0.500] yes
tweedie [0.766, 1.60] yes
genpois [0.991, 2.01] yes
compois [0.969, 2.02] yes
truncated_poisson [1.05, 1.86] yes
gaussian + dispformula [0.966, 1.07] yes
poisson/nbinom2 + ZI varies yes

No family errored, so the fallback added here is protection for future or exotic families rather than a workaround for current failures.

The change

Try residuals(type = "pearson") first; keep the existing expression as a fallback when glmmTMB cannot compute them. .safe() catches errors, and the all(is.na()) guard avoids a silently blank panel.

Delegating rather than computing V(mu) here keeps per-family variance knowledge in one place. performance already maintains .expected_variance() for check_overdispersion(), and see carries a duplicate of this same homogeneity branch, so a per-family table here would be the third copy.

Effect

Ratio of max to min of the panel's loess smooth (1 = flat), n = 600, size = 20, fitted probabilities spanning about 0.03 to 0.95:

                        before   after
glm binomial              1.04    1.04
glmmTMB binomial          1.57    1.04
glmmTMB combinomial       1.57    1.04

The glm column is the control and does not move. Both glmmTMB columns flatten to the same value, and the flat level sits near 0.85, close to E sqrt|Z| = 0.822 for a standard normal, i.e. the residuals are now scaled to unit variance rather than merely looking flat.

The third column is a Conway-Maxwell-Binomial family from a pending glmmTMB PR. It is included because it makes the delegation argument concretely: the panel comes out correct for a family performance has never heard of, with no changes here.

homogeneity_before_after

Two things to flag

gaussian. This is the one family the current code gets right: betadisp is sigma and gaussian variance does not depend on mu, so a constant divisor is correct. glmmTMB deliberately returns Pearson residuals unscaled by sigma for fixed-dispersion and constant-dispformula cases, for base R compatibility. So after this change gaussian glmmTMB panels lose the sigma division. Panel shape is unaffected, but the y-scale no longer matches the glm branch. Restoring parity would mean multiplying by sigma(model) in exactly the case glmmTMB omits it, which reintroduces family-specific logic. Happy to add it if you would rather have the parity.

beta-binomial with non-constant dispformula. glmmTMB documents that beta-binomial Pearson residuals are not scaled by the dispersion factor, because that factor needs the number of trials. With constant dispersion this is a constant multiplier and panel shape is fine; with dispformula = ~x the factor varies and some curvature would remain. Upstream limitation, not introduced here.

Related

see::plot.see_check_heteroscedasticity() contains a copy of this branch, so plot(check_heteroscedasticity(m)) is affected the same way. Happy to open the matching PR there if useful.

Separately, that copy reads model$fit$par["betad"] while glmmTMB renamed the parameter to betadisp (glmmTMB commit 472ccbe0). [ does not partial-match, so the divisor is NA and the panel comes out all-NA with no error for the tryCatch to catch. That affects gaussian and Gamma glmmTMB fits. Filing separately.

Notes

  • No test added yet, tell me where you would want one and I will add it.
  • Reproducible example and the family table script available if useful.

The glmmTMB/MixMod branch of .model_diagnostic_homogeneity() divided
residuals by a single scalar, which is only correct when V(mu) does not
depend on mu. For binomial and poisson that scalar is 1, so no
standardization occurred and the panel bowed for correctly specified
models. Delegate to residuals(type = "pearson"), falling back to the
previous behaviour where glmmTMB cannot compute them.
@strengejacke

Copy link
Copy Markdown
Member

Thanks for this PR! Will review manually later, and also start a co-pilot AI review (so don't worry that review is only done by AI, there'a always a human review of PR's as well).

Happy to open the matching PR there if useful.

That would be great!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses issue #926 by changing how the homogeneity-of-variance diagnostic computes residuals for glmmTMB/MixMod models so the panel uses per-observation variance scaling (Pearson residuals) rather than a single global scale factor.

Changes:

  • Prefer stats::residuals(model, type = "pearson") for glmmTMB/MixMod homogeneity diagnostics.
  • Retain the existing scalar-standardization approach as a fallback when Pearson residuals fail or are entirely NA.
  • Add inline rationale explaining why scalar division is insufficient for many non-Gaussian families.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread R/check_model_diagnostics.R Outdated
Comment on lines +267 to +271
## Pearson residuals are scaled by the family's variance function
## V(mu_i), which varies across observations. The fallback below
## divides by a single scalar, which is only correct when V() does
## not depend on mu (e.g. gaussian); for binomial and poisson that
## scalar is 1, i.e. no standardization at all.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jbogomolovas2 This is "nitpicking", but you can apply this suggestion, if you like.

Comment thread R/check_model_diagnostics.R Outdated
} else {
.sigma_glmmTMB_nonmixed(model, faminfo)
}
stats::residuals(model) / residual_sigma

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jbogomolovas2 Same here - I don't think defaults will change, but you may decide to be definitely safe and apply this suggestion.

Comment on lines +272 to 282
r_pearson <- .safe(stats::residuals(model, type = "pearson"))
if (is.null(r_pearson) || all(is.na(r_pearson))) {
residual_sigma <- if (faminfo$is_mixed) {
sqrt(insight::get_variance_residual(model))
} else {
.sigma_glmmTMB_nonmixed(model, faminfo)
}
stats::residuals(model) / residual_sigma
} else {
.sigma_glmmTMB_nonmixed(model, faminfo)
r_pearson
}
@strengejacke

Copy link
Copy Markdown
Member

Thanks, looks good to me! Can you also please increase the version number? (4th digit increase by 1) and add a news item?

After merging this PR, I can add some graphical snapshot tests.

@jbogomolovas2

Copy link
Copy Markdown
Author

Thanks, looks good to me! Can you also please increase the version number? (4th digit increase by 1) and add a news item?

After merging this PR, I can add some graphical snapshot tests.

Done: Applied both suggestions, bumped to 0.17.1.3, NEWS item added."

@strengejacke

Copy link
Copy Markdown
Member

Great! One last thing, you may add yourself as contributor to the DESCRIPTION file, if you like (unless you want to stay "incognito" 😉)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants