Regression and causal inference

GLM / GLMM with biological interpretation

"How does Y change with X?", where Y is something you measured on each sampling unit:

Is this the right analysis?

Use it when

One response measured on sampling units (sites, plots, nests, animals, quadrats, surveys) and the question is how it changes with habitat, environmental or treatment predictors, possibly with repeated measures within sites/individuals/years (random intercepts). Counts, presence/absence (0/1), successes out of trials, proportions in (0,1), positive continuous measurements (mass, length, biomass) and normal measurements.

Not for

Imperfect detection that must be separated from occurrence or abundance (use occupancy / N-mixture modules); spatial or temporal autocorrelation that must be modelled explicitly; random slopes, nested (1|a/b) syntax, splines/GAMs, ordinal or multinomial responses; causal effect estimation (use the causal/SEM modules); proportions containing exact 0s or 1s (transform first or use binomial with trials).

The data you need

One CSV, one row per observation, with a response column, predictor columns, optional grouping column(s) (site, individual, year), optional effort column (offset) and, for binomial counts, a trials column.

What the example's main data file looks like:

First 5 of 180 rows of wood_thrush_counts.csv; 1 more column not shown.
stationvisitdateobserversurvey_minforest_typecanopy_cover_pctelevation_m
WT-0112025-05-20JL5mixed66725
WT-0122025-05-29JL10mixed66725
WT-0132025-06-09MR10mixed66725
WT-0142025-06-18JL10mixed66725
WT-0212025-05-21JL10deciduous66320

Not sure? Use the finder to compare modules.

Try it

The worked example

Wood Thrush point counts: negative-binomial GLMM with survey-effort offset and station random intercepts (simulated example)

Wood Thrush (Hylocichla mustelina) is a forest-interior songbird of eastern North America. It nests in the understorey of closed-canopy, mostly deciduous forest, and it has declined over much of its range. A field crew wants to know where the birds are most numerous, so that managers know which stands to protect.

The question: Does the number of Wood Thrushes counted change with canopy cover, elevation and forest type?

The data are simulated, so the true answer is known and the analysis can be checked against it.

What the researcher typed

I have Wood Thrush point counts in wood_thrush_counts.csv: 45 stations, each counted 4 times in May-June 2025. Most counts were 10 minutes, some were 5. I want to know whether the number of thrushes changes with canopy cover, elevation and forest type. Please analyse it and give me a report with figures I can use in a paper.

Run it yourself

  1. Download the example data below and unzip it.

  2. Open the unzipped folder in Claude Code (see Install).

  3. Paste the prompt above. The agent checks the files, writes a plan, runs planted truth, fits the model and writes the report. You can compare your results with the ones below.

Contains wood_thrush_counts.csv, the settings file (config.yaml), a description of the data and a step-by-step walkthrough.

Study design details
stations45
visits per station4
observations180
familynbinom2 (log link)
offsetsurvey_min (5 or 10 min); results per 10-min count
random effect(1|station)
predictorscanopy_cover_pct, elevation_m, forest_type (deciduous/mixed/conifer; all station-level)
forest type countsdeciduous: 15; mixed: 15; conifer: 15
What the report says

Results from the example data

Figure: Conditional effects of every predictor on the expected Wood Thrush count per 10 survey minutes (typical station).
Conditional effects of every predictor on the expected Wood Thrush count per 10 survey minutes (typical station). Panels: A, Canopy cover (%); B, Elevation (m); C, Forest type. Lines/points: estimates; bands/bars: 95% confidence intervals from the sampling distribution of the fixed effects; in each panel the other predictors are held at their mean (continuous) or reference level (factors) and random effects at zero. Rug: observed predictor values.

What this means

  • Per 10 percentage points of canopy cover (%) this is associated with a 39% higher expected Wood Thrush count (rate ratio 1.39, 95% CI 1.18 to 1.64).
  • Across the observed range of canopy cover (%) (42 to 98), with other continuous predictors at their mean; forest type = deciduous; typical station, the expected Wood Thrush count goes from 0.432 to 2.78.
  • Per 100 m of elevation (m) this is associated with a 5.1% lower expected Wood Thrush count (rate ratio 0.949, 95% CI 0.854 to 1.05). The 95% interval includes no effect: the data are compatible with effects from 31% lower to 13% higher, including none.
  • Compared with forest type = deciduous (reference), forest type = conifer is associated with a 52% lower expected Wood Thrush count (rate ratio 0.482, 95% CI 0.248 to 0.935): 0.528 vs 1.1.
  • Compared with forest type = deciduous (reference), forest type = mixed is associated with a 15% higher expected Wood Thrush count (rate ratio 1.15, 95% CI 0.651 to 2.01): 1.25 vs 1.1. The 95% interval includes no effect: the data are compatible with effects from 35% lower to 101% higher, including none.
  • Model checks: DHARMa simulated residuals showed no detectable problems with distribution, dispersion, outliers or zeros (all p >= 0.05).
  • These are effects on the expected count as recorded, which combines how many/whether animals are present with how likely they are to be detected or recorded.

More figures

Figure: Expected Wood Thrush count per 10 survey minutes (line) versus Canopy cover (%), with 95% confidence band from 2000 draws from the multivariate-normal sampling distribution of the fixed effects.
Expected Wood Thrush count per 10 survey minutes (line) versus Canopy cover (%), with 95% confidence band from 2000 draws from the multivariate-normal sampling distribution of the fixed effects. Other predictors held at: Elevation (m) at its mean; Forest type = deciduous; random effects at zero (a typical station). Rug: observed values of Canopy cover (%). The curve spans the observed range only.
Figure: Expected Wood Thrush count per 10 survey minutes (line) versus Elevation (m), with 95% confidence band from 2000 draws from the multivariate-normal sampling distribution of the fixed effects.
Expected Wood Thrush count per 10 survey minutes (line) versus Elevation (m), with 95% confidence band from 2000 draws from the multivariate-normal sampling distribution of the fixed effects. Other predictors held at: Canopy cover (%) at its mean; Forest type = deciduous; random effects at zero (a typical station). Rug: observed values of Elevation (m). The curve spans the observed range only.
Figure: Expected Wood Thrush count per 10 survey minutes for each level of Forest type (points) with 95% confidence intervals from 2000 draws from the sampling distribution of the fixed effects.
Expected Wood Thrush count per 10 survey minutes for each level of Forest type (points) with 95% confidence intervals from 2000 draws from the sampling distribution of the fixed effects. Other predictors held at: Canopy cover (%), Elevation (m) at their means; random effects at zero (a typical station).

Figures are the module's default publication figures, reduced for the web. The report also saves them as PDF and full-resolution PNG.

How we know it works

Planted truth at the example's design

Before trusting the example's results, the module was run on many simulated data sets with the same design and known true values. Each row is one quantity: did the method get it back? (What the thresholds are.)

Verdict for this example: ✓ DONE

All interpreted estimands (canopy, elevation, forest-type effects, mean count) pass planted truth at this design (|rel. bias| <= 7.2%, coverage 0.905-0.940); fit converged; DHARMa all p > 0.5; module cross-checked and negative controls in VALIDATION.md. Station SD (FAIL, -17.6% bias, 23% singular fits) and NB theta (unbounded in 7/199) are nuisance parameters and are not interpreted.

The example's design compared with the validated design: differs from validated
This example45 bird-count stations, 4 visits each (180 counts)
Design the module was validated at30 sites, 8 visits each (240 counts), negative binomial mixed model

The example has more stations (45 vs 30) but only half the visits per station, so fewer counts in total than the validated default. At 45 x 4 the effects that are interpreted passed (bias within 7.2%, coverage, meaning how often the 95% interval contained the truth, 0.905-0.940), but the between-station spread was biased -17.6% with 23% of simulated fits singular, so it is not interpreted. VALIDATION.md found the module breaks down at 10 sites x 4 visits (coverage 0.875-0.88 for site-level effects, station SD -42%).

≈ Borderline results

These calls sit close to the pass/fail line. With another 199 simulated data sets they could come out the other way, so read them as “about at the limit” rather than as a clear pass or fail.

  • beta_elevation_m (✓ PASS): coverage 0.905 against a minimum of 0.904.
  • beta_intercept (✓ PASS): coverage 0.915 against a minimum of 0.904.
  • mean_response_at_reference (✓ PASS): coverage 0.915 against a minimum of 0.904.
  • rate_ratio_elevation_m (✓ PASS): coverage 0.905 against a minimum of 0.904.

10 of 12 rows pass; 4 are borderline.

Bias: average estimate minus truth, as a percentage of the truth. Allowed bias: the largest bias that still passes, the bigger of 10% of the truth and 3 Monte Carlo standard errors († = set by that simulation-noise floor, so the allowance is wider than 10%). Coverage: share of 95% intervals that contained the true value, with the minimum needed in brackets. ≈ borderline: the bias used 80–125% of its allowance, or coverage was within 0.015 of the minimum; such a result could flip between PASS and FAIL with another set of simulations. Quantity names are as they appear in the report.
QuantityTrue valueAverage estimateBiasAllowed biasCoverage (needed)Result
beta_canopy_cover_pct0.390.387-0.9%±10.0%0.94 (0.904)✓ PASS
beta_elevation_m-0.28-0.275+1.8%±10.7%†0.91 (0.904)✓ PASS ≈ borderline
beta_forest_type_conifer-0.9-0.89+1.1%±10.0%0.94 (0.904)✓ PASS
beta_forest_type_mixed-0.25-0.245+2.0%±25.1%†0.93 (0.904)✓ PASS
beta_intercept-2.08-2.1-1.0%±10.0%0.92 (0.904)✓ PASS ≈ borderline
mean_response_at_reference0.1250.125+0.2%±10.0%0.92 (0.904)✓ PASS ≈ borderline
nb_size_theta3––±1916038694.7%†– (0.904)✗ FAIL
rate_ratio_canopy_cover_pct1.481.48+0.5%±10.0%0.94 (0.904)✓ PASS
rate_ratio_elevation_m0.7560.767+1.5%±10.0%0.91 (0.904)✓ PASS ≈ borderline
rate_ratio_forest_type_conifer0.4070.436+7.2%±10.0%0.94 (0.904)✓ PASS
rate_ratio_forest_type_mixed0.7790.817+4.9%±10.0%0.93 (0.904)✓ PASS
re_sd_station0.450.371-17.6%±11.1%†0.94 (0.904)✗ FAIL

Limits that matter

  • At 45 stations x 4 visits with ~1 bird per count, the station random-intercept SD is biased -18% (23% of simulated fits singular) and NB theta is sometimes unbounded: neither is interpreted (this run).
  • Rate ratios for imprecise contrasts are biased upward in the mean (exp of a noisy log ratio): +7.2% conifer, +4.9% mixed here; log-scale coefficients unbiased (Known limit 1).
  • Wald intervals only; recovery coverage 0.905-0.940 at this design (Known limit 4).
  • Expected counts are for a typical station (random effect 0), not the landscape mean (Known limit 5).
  • Counts confound abundance and detection; a canopy effect could partly be detectability (N-mixture module separates them).
  • Spatial autocorrelation among stations is not modelled (Known limit 7).
  • Associations, not causal effects.
All known limits from the module's validation record
  1. Odds/rate ratios for imprecise contrasts are biased upward in the mean. exp(beta-hat) is median-unbiased but not mean-unbiased: E[exp(b)] is about exp(beta + SE^2/2). When the SE of a log ratio exceeds about 0.35 (factor contrasts in Bernoulli GLMMs; small samples), the ratio estimand fails the mean-bias gate: +13% (binomial_glmm habitat), +15-90% in the small designs. The link-scale coefficient passes and the ratio's 95% CI keeps nominal coverage. Report ratios with their CIs; do not average point ratios across studies.
  2. Few groups. With 10 sites x 4 visits (NB GLMM) Wald intervals for site-level effects under-cover (0.875-0.88) and the site SD is biased -42%. With 15 Bernoulli sites x 4 the site SD interval under-covers (0.845). The module flags fewer than 5 levels; between 5 and ~15 groups, treat site-level effects' intervals as somewhat optimistic and run a planted-truth check at the researcher's design. Power below ~20 sites is low for site-level effects in any case.
  3. NB size theta needs data. With 100 observations and 30% structural zeros, theta is biased +29% (coverage 0.87). Overdispersion is then hard to separate from zero inflation; the coefficients and zero-inflation probability are still recovered.
  4. Wald intervals. All intervals are Wald (log scale for SDs and dispersion). Profile or bootstrap intervals are not implemented. For random-effect SDs near zero the interval is meaningless and the report says the fit is singular.
  5. mean_response_at_reference is for a typical group (random effect = 0), not the population average.
  6. Not validated: random slopes (rejected by the parser), non-default links (probit, cloglog, inverse; accepted but their interpretation sentences are generic), zero-inflation formulas other than ~1 (supported, zi_beta_* reported, not recovery-tested), polynomial terms (supported in fitting and plots, not recovery-tested), the stats::glm fallback engine (cross-checked to agree, not separately recovery-tested), and DHARMa test calibration (the tests come from DHARMa, unmodified).
  7. Spatial/temporal autocorrelation is not modelled or tested for.

How this module was validated

  • 200 simulated data sets per design (500 for false-positive checks), fitted with glmmTMB using REML for mixed models; all 200 converged at the default design and every one of 12 quantities passed.
  • Nine model families (Gaussian, binomial, Poisson, beta, Gamma, zero-inflated and interaction models) each tested with 200 replicates; all passed except odds ratios for factor contrasts in the binary mixed model (+13% mean bias, a known back-transformation effect).
  • Harder small designs failed as expected: 10 sites x 4 visits (coverage 0.875-0.88, site SD -42%), 15 binary sites x 4 visits, and a small zero-inflated design (theta +29%).
  • Negative controls: with no true effect, intervals excluded zero 5.8% and 4.2% of the time (nominal 5%); fitting Poisson to overdispersed data or dropping the site random effect failed the gate, as it should.
  • Cross-checked on 20 data sets per case against lme4, stats::glm, MASS::glm.nb and hand-written likelihoods; coefficients agreed to within 0.14 standard errors at most.
  • Power: about 40 sites give 80% power for a site-level effect of 0.3 per SD (power 0.74 at 30 sites, 0.93 at 50).
  • A separate verifier agent reran the builder's runs exactly, passed three new designs with its own simulators, confirmed the controls fail, and fixed an offset-matching bug and several report-wording issues.

Every run uses the acceptance gate described on the validation page. The full record follows, section by section, exactly as kept in the module's VALIDATION.md.

Settings and pass/fail rule used for every run

Date: 2026-10-18. Code state: dev/glm/code_md5_final_suite.txt (all runs below). Later changes touch only plotting code in output.R and the engine description string in module.R (residual-panel subtitles, and a module-local grouped conditional-effects plot for interactions); recovery never calls it. Final hashes: dev/glm/code_md5_after_plot_tweak.txt. 2026-10-19: figures.R (publication figure suite, glm_figures) added and registered in module.R; it only reads the fit (no change to simulate/fit/estimates), so the results below still apply. Dev tests: dev/glm/figs/test_figs.R. Engine: glmmTMB 1.1.15.2, R 4.5.3. Gate: recovery_gate() with its fixed thresholds (|bias| <= max(10% of truth, 3 MC SE); coverage >= 0.95 - 3 MC SE, here 0.904 for 200 reps and 0.921 for 500; >= 90% converged). Recovery uses fast_settings (identical to the real-data settings except no DHARMa/R2), REML for mixed models.

Reproduce: bash dev/glm/run_validation.sh (configs in modules/glm/validation/; outputs in results/glm/validation/<run>/), bin/Rscript dev/glm/crosscheck.R, bin/Rscript dev/glm/nb_ll_check.R. Tables below come from dev/glm/validation_tables.R. Each fit takes 0.3-0.9 s, so all 3,700 recovery fits took about 25 min on 16 cores.

1. Default design (nsim = 200): PASS on every estimand

NB2 GLMM, 30 sites x 8 visits, site-level forest cover (normal 55 +/- 20) and habitat (3 levels), visit-level temperature, effort offset U(0.5, 2), theta = 2, site SD = 0.5. 200/200 converged.

estimandtruthmean estrel. bias %bias MCSEcoveragepowergate
beta_forest0.30.283-5.80.00840.9550.645PASS
beta_habitat_riparian0.40.4020.40.0210.9300.310PASS
beta_habitat_wetland-0.3-0.26710.90.0190.9450.170PASS
beta_intercept0.70.682-2.60.0140.930-PASS
beta_temp-0.2-0.206-3.20.00460.9650.885PASS
mean_response_at_reference2.012.020.10.0280.930-PASS
nb_size_theta22.052.50.0280.965-PASS
rate_ratio_forest1.351.34-1.00.0110.955-PASS
rate_ratio_habitat_riparian1.491.564.60.0340.930-PASS
rate_ratio_habitat_wetland0.7410.7957.30.0160.945-PASS
rate_ratio_temp0.8190.815-0.40.00380.965-PASS
re_sd_site0.50.499-0.30.00820.970-PASS

(beta_habitat_wetland: 10.9% relative bias but |bias| 0.033 < 3 MC SE = 0.057, so it is within Monte Carlo noise.)

2. Every family (nsim = 200 each)

Full tables are in the appendix. Designs: 30 groups x 8 obs unless noted.

rundesignconvergedgate
gaussian_lmmGaussian LMM, sigma 2, site SD 1200/200PASS all 8 estimands (re_sd -0.8%, residual_sd +0.4%)
binomial_glmmBernoulli GLMM, 40 sites x 8, site SD 0.7200/200PASS 9/11. FAIL (mean bias): odds_ratio_habitat_riparian +13.1%, odds_ratio_habitat_wetland +13.3%. The link-scale contrasts PASS (+8.1%, +10.7%, within 3 MC SE) and their coverage is 0.945/0.970; see Known limits 1
binomial_trials_glmsuccesses/trials (2-6), 120 units, no RE200/200PASS all 10
poisson_glmPoisson + offset, 150 units, no RE200/200PASS all 10
beta_glmmbeta, phi 15, site SD 0.3200/200PASS all 12
gamma_glmmGamma (log), CV 0.4, site SD 0.3200/200PASS all 12
zip_glmmZIP GLMM, 20% structural zeros200/200PASS all 12
zinb_glmmZINB GLMM, 20% zeros, theta 2200/200PASS all 13
interaction_glmmPoisson GLMM + forest:temp + temp:habitat (one interaction truly 0)200/200PASS all 17
3. Harder designs (nsim = 200): where the module stops working
rundesignresult
hard_nb_smallNB GLMM with only 10 sites x 4 visits (40 obs)FAIL. Coverage 0.875-0.88 for site-level effects (forest, wetland), intercept and mean response; re_sd_site biased -42%; rate_ratio_habitat_riparian +17% mean bias. Visit-level temp still PASSES (coverage 0.945).
hard_binomial_smallBernoulli GLMM, 15 sites x 4 visits (60 obs)Link-scale coefficients all PASS (coverage 0.965-0.98). FAIL: odds_ratio_forest mean bias +15.5% (retransformation); re_sd_site coverage 0.845. Power for any effect <= 0.17.
hard_zinb_smallZINB, 100 obs, no RE, 30% zeros, theta 1All coefficients and zero_inflation_prob PASS. FAIL: nb_size_theta +29% bias, coverage 0.87.
4. Negative controls: the gate can fail, and false positives are at the nominal rate

False-positive rate (effect = 0), nsim = 500 (null_effects: forest and temp effects 0, default design otherwise):

estimandrejection rate (95% CI excludes 0)MC SEcoverage
beta_forest (site-level)0.0580.0100.942
beta_temp (visit-level)0.0420.0090.958

Both are consistent with 0.05. For comparison, with ML instead of REML the site-level rate was 0.084 (coverage 0.916; results/glm/validation_ml/null_effects), which is why REML is the default for mixed models.

Mis-specified fits must fail the gate:

runmis-specificationoutcome
misspec_poissonNB data (theta 2) fitted as Poisson GLMMFAIL as expected: beta_temp coverage 0.680 (bound 0.904), re_sd_site +14% bias / coverage 0.845
misspec_norandomsite random effect dropped (pseudoreplication of site-level covariates)FAIL as expected: coverage beta_forest 0.715, riparian 0.685, wetland 0.760, intercept 0.695; nb_size_theta -29% bias, coverage 0.39
5. Cross-check against independent implementations

dev/glm/crosscheck.R: 20 simulated datasets per case. The module's estimates are compared with lme4 (lmer, glmer, glmer.nb), stats::glm, MASS::glm.nb, and, where no package was available, hand-written likelihoods maximised with optim (beta, ZIP and ZINB GLMs; betareg/pscl are not installed). GLMM comparisons use the module with REML: false because glmer is ML only. Output: results/glm/validation/crosscheck_summary.csv.

caseindependent routemax \coef diff\/ SEmax SE rel. diffmax dispersion/SD rel. diff
gaussian LMM, REMLlmer(REML = TRUE)1.0e-60.9%3.3e-6
gaussian LMM, MLlmer(REML = FALSE)5.2e-51.1%6.6e-6
Bernoulli GLMMglmer (Laplace)0.00331.1%8.6e-4
Poisson GLMMglmer0.000980.7%1.2e-4
NB2 GLMMglmer.nb0.1436.7%1.2%
NB2 GLMMASS::glm.nb0.000176.5%1.9e-5
binomial trials GLMglm6.4e-52.4e-6-
Gamma GLMglm (coefs) + MASS::gamma.shape (ML shape)0.000146.1%8.1e-6
beta GLMhand-written likelihood9.6e-52.4e-64.6e-6
ZIP GLMhand-written likelihood1.2e-45.0e-61.3e-5 (zero-inflation prob.)
ZINB GLMhand-written likelihood1.3e-41.3e-52.5e-5

Explained differences:

  • SE differences of about 6% for glm.nb and glm(Gamma) arise because glm uses expected information and, for Gamma, the Pearson dispersion. glmmTMB uses the observed Hessian at the ML shape.
  • NB2 GLMM vs glmer.nb: coefficients differ by up to 0.14 SE (median 0.11 SE). In dev/glm/nb_ll_check.R the glmmTMB objective at the glmmTMB MLE is higher than at glmer.nb's estimates in all 20 datasets, by 0.006 to 0.024 log-likelihood units (results/glm/validation/nb_glmm_loglik_check.csv). The module therefore reaches the better optimum. glmer.nb's own reported log-likelihood differs from glmmTMB's by -0.06 to +0.12, so the two packages evaluate this likelihood differently; glmer.nb fits theta in an outer loop. The difference is small next to the SEs.
6. Power over a design knob (nsim = 100 per point)

core/run.R power glm --vary n_groups=10,20,30,50,80 --nsim 100 (default design, 8 visits per site; results/glm/validation/power_ngroups/).

sitespower: forest (site-level, 0.3/SD)power: temp (visit-level, -0.2/SD)power: riparian vs upland (0.4)coverage forestre_sd rel. bias
100.340.320.190.88-16%
200.530.690.290.94-2%
300.740.860.450.95-6%
500.930.970.530.96-2%
801.001.000.680.95-1%

About 40 sites give 80% power for a site-level effect of 0.3 per SD (rate ratio 1.35); a 3-level habitat contrast of 0.4 needs more than 80 sites. At 10 sites coverage is below nominal (0.88), consistent with hard_nb_small.

7. Example data (real-data format) and reports

bin/Rscript core/run.R fit glm --config modules/glm/example/config.yaml --out results/glm/example (plus config_binomial.yaml, config_beta.yaml). The examples were simulated with known values (dev/glm/make_examples.R, dev/glm/example_truth.txt). All three converged, DHARMa tests all p > 0.4, and every generating value is inside its 95% CI:

  • Wood Thrush NB2 GLMM: canopy rate ratio per 10 points 1.38 (1.17-1.62), truth exp(0.25) = 1.28; conifer vs deciduous 0.50 (0.26-0.96), truth 0.41; elevation per 100 m 0.95 (0.86-1.06), truth 0.89; station SD 0.56 (0.34-0.93), truth 0.45.
  • Hatching binomial GLMM: OR per 1 m nest height 1.28 (1.09-1.50), truth 1.28; per 50 m from edge 1.65 (1.30-2.09), truth 1.49; predator guard 1.87 (1.17-3.0), truth 2.01.
  • Seagrass beta GLMM: OR per 1 m depth 0.645 (0.576-0.721), truth 0.638; per 5 NTU 0.727 (0.60-0.88), truth 0.741; phi 12.1 (8.8-16.6), truth 12.

I read the HTML reports. The interpretation sentences match the estimates table: rate ratios, percentage changes, response-scale predictions across observed ranges, the random-effect spread and the dispersion statements. Conditional-effect plots render on the response scale with 95% bands and rugs. Factors are drawn as points with intervals, and interactions as several coloured curves with bands (dev/glm/inter_png.R; the core cond_effect_plot() loses its grouping under ggplot2 4.0, so the module draws grouped curves itself). The forest plot and DHARMa panels render as well.

ML vs REML (why REML is the default for mixed models)

An earlier full suite used ML (results/glm/validation_ml/, same configs). Under ML, re_sd_site was biased low in every mixed design: -11.9% default, -11.6% Gaussian, -17.9% Bernoulli, -9.6% beta, -8.3% Gamma, -10.1% ZIP, -17.3% ZINB. Group-level contrasts were under-covered in the ZIP/ZINB GLMMs (0.880-0.890 < 0.904), and the null false-positive rate for the site-level covariate was 0.084. Under REML (started from the ML solution) all of these pass: re_sd within +/-3%, ZI coverage 0.915-0.95, false-positive rate 0.058. With glmmTMB's default start, REML stopped at a spurious zero-SD optimum in 7/200 beta GLMM datasets. Restarting from the ML estimates reached a better REML optimum (lower objective, positive-definite Hessian) in all 7, so the module always starts REML from ML. If REML still fails to converge, the ML fit is reported with a note.

Independent verification (2026-10-18, separate verifier agent)

Scripts: dev/glm/verify/ (verify_recovery.R = own simulators + driver, run_verify.sh, rerun_after_fix.sh, usability.sh, messy/). Outputs: results/glm/verify/. Log: logs/glm_verify.log. Final code hashes: dev/glm/verify/code_md5_verified.txt.

Literature / parameterisation check. Engine assumptions were checked against glmmTMB 1.1.15.2 ?sigma.glmmTMB and Brooks et al. (2017): NB2 Var = mu(1 + mu/theta) with betadisp = log(theta); beta Var = mu(1-mu)/(1+phi) (Ferrari & Cribari-Neto 2004); Gamma sigma = 1/sqrt(shape) = CV, so the module's log CV = -0.5 x betadisp is right; zero inflation is a logit-link mixture with E[y] = (1-pi) mu. The module's simulator shares canon_family, link_inv and coef_label with the fitter and standardises with the sample SD exactly as the fitter does, so I did not reuse it.

Independent planted truth. My simulators (no module code) set truth on the RAW covariate scale and derive the per-SD estimands analytically (beta_SD = beta_raw x sample SD; intercept at the sample mean; for x:factor interactions, factor contrast = b2 + b3 x mean(x)). The per-unit estimands (*_per_<u>) are therefore checked against raw-scale truth that does not depend on the scaling code. Fitted with module$fit() + fast_settings; summarised with core summarise_recovery() / recovery_gate() (thresholds unchanged). All replicates converged.

design (seed 777000+r)nsimresult
Gamma GLMM, 40 ponds x 6, depth U(0.5,4) obs-level (-0.35/m), 2-level pond-level treatment (+0.25), CV 0.6, pond SD 0.4200PASS 9/9. Max \rel. bias\2.4% (mean response); coverage 0.93-0.96; ratio_of_means_depth_per_1 +0.5%, gamma_cv -0.7%, re_sd_pond -2.2%
Binomial successes/trials (1-10) GLMM, 25 colonies x 6, shrub (normal 30 +/- 12) x grazed interaction, colony SD 0.6200PASS 10/10. Interaction coefficient +3.2%, odds_ratio_shrub_per_10 +0.2%, re_sd -0.2%; coverage 0.94-0.98
Zero-inflated Poisson GLM, n = 300, flow U(5,60), 3-level substrate, area offset reported per 10 quadrats (offset_per: 10), pi = 0.3200PASS 10/10. Max \rel. bias\0.7%; coverage 0.92-0.965; mean_response_at_reference (incl. zero inflation, per 10 units) -0.4%

Controls (my designs).

  • False-positive rate, Gamma design with both effects 0, nsim 500: obs-level depth 0.048, pond-level treatment 0.066 (MC SE 0.010). All 9 estimands PASS.
  • ZIP data fitted as plain Poisson: FAIL as required (beta_intercept -43%, coverage 0.000; slopes coverage 0.745-0.815).
  • Gamma GLMM fitted without the pond random effect: FAIL as required (pond-level treatment coverage 0.740, intercept 0.680, gamma_cv +15.5% / coverage 0.115). The obs-level depth effect still passes.

Builder's numbers reproduced. Rerunning recover for default (200), binomial_glmm (200) and null_effects (500) with the same seeds gave the tables in sections 1, 2 and 4 exactly (default PASS 12/12; binomial_glmm odds ratio FAILs +13.1% / +13.3%; FPR 0.058 / 0.042). After my engine fix (below), default, poisson_glm and gamma_glmm summaries are byte-identical to the builder's (results/glm/verify/after_fix/).

Problems found and fixed (module files only; core untouched).

  1. Offset partial matching (engine.R, module.R). sp$offset used R's partial $ matching: if the spec had no offset element but had offset_per, the value of offset_per (e.g. 1 or 10) was taken as the offset column, so log(df[[1]]) (often the response) entered as an offset. In my first smoke test this made every Gamma GLMM fit collapse (slopes 0, CV 1e-7) and every binomial-trials fit fail. Through read_data() the same happened when a config had offset_per: without offset: (stopped with a confusing "column(s) 10 not found"). Now exact [["offset"]] lookups, plus a clear check_data message for offset_per without offset. The builder's suite was not affected (its simulator always sets offset).
  2. Misspelled group ids (module.R). WT03 vs WT-03 was silently fitted as a 46th station. check_data now stops with "grouping column station has labels that look like the same station spelled differently: 'WT-03' (3 rows) vs 'WT03' (1 rows) ..." (case, punctuation and leading zeros ignored). fit notes also flag single-observation groups when the typical group has >= 3.
  3. Figure export (output.R). The figure named "Effect of canopy cover (%)" failed to save in core export_figures() (% in the file name; ggsave treats it as a format string). The figure list names now spell out % and /. The underlying core issue (file stem = figure name, unsanitised) remains for other modules.
  4. Interaction sentences (output.R). They read "Each 1-SD increase (or, for a factor, the named level instead of the reference) in forest_type_conifer multiplies the rate ratio for canopy_cover_pct by 0.922", with raw column names. Now: "when forest type = conifer rather than deciduous, the rate ratio per 1 SD of canopy cover (%) (15.6) is multiplied by 0.922 (95% CI 0.49 to 1.74)". Identity-link and continuous x continuous versions were checked too.
  5. Detection caveat (output.R). For count and 0/1 families the interpretation now says that effects on recorded counts/detections combine abundance/occurrence with detectability, and points to occupancy / N-mixture modules. Not added for successes/trials, beta, Gamma or gaussian.
  6. Missing-value message now says "data rows N, not counting the header line".

Usability. On the Wood Thrush example every sentence was checked by hand against the estimates table (for example RR per 10 points = exp(0.500 x 10/15.6) = 1.378; station spread exp(+/-1.96 x 0.56) = 0.334-3.0; NB variance/mean at the mean count 1 + 1.13/5.89 = 1.19; conifer 1.156 x 0.498 = 0.576). The canopy curve (0.47 to 2.84) and forest-type points (1.16 / 0.576 / 1.32) match. Wording is associational and states that the response is a count per 10 minutes. The NA, wrong-column-name (lists the columns present, explains canopy cover pct became canopy.cover.pct), typo and offset_per messages are actionable. Binomial and beta example sentences were checked the same way.

Not fixed / still open. The module has no figures(fit, data, opts) per the newer MODULE_SPEC / FIGURES.md; core falls back to plots(). Core's journal styling moves the interaction legend to the right, which squeezes the panel. Core export_figures() does not sanitise file names. The known limits above (ratio retransformation bias, few groups, theta with 100 obs) are confirmed, not changed.

Appendix: full recovery tables (REML suite)

default: 200 replicates, 200 converged (100.0%), 0 errors, coverage bound 0.904; mean fit time 0.87 s

estimandtruthmean estrel. bias %bias MCSEcoveragepowergate
beta_forest0.30.283-5.80.00840.9550.645PASS
beta_habitat_riparian0.40.4020.40.0210.9300.310PASS
beta_habitat_wetland-0.3-0.26710.90.0190.9450.170PASS
beta_intercept0.70.682-2.60.0140.930-PASS
beta_temp-0.2-0.206-3.20.00460.9650.885PASS
mean_response_at_reference2.012.020.10.0280.930-PASS
nb_size_theta22.052.50.0280.965-PASS
rate_ratio_forest1.351.34-1.00.0110.955-PASS
rate_ratio_habitat_riparian1.491.564.60.0340.930-PASS
rate_ratio_habitat_wetland0.7410.7957.30.0160.945-PASS
rate_ratio_temp0.8190.815-0.40.00380.965-PASS
re_sd_site0.50.499-0.30.00820.970-PASS

gaussian_lmm: 200 replicates, 200 converged (100.0%), 0 errors, coverage bound 0.904; mean fit time 0.53 s

estimandtruthmean estrel. bias %bias MCSEcoveragepowergate
beta_forest10.967-3.30.0150.9500.990PASS
beta_habitat_riparian0.80.8324.00.040.9350.300PASS
beta_habitat_wetland-0.6-0.52811.90.0380.9500.170PASS
beta_intercept109.96-0.40.0270.950-PASS
beta_temp-0.5-0.4941.20.00970.9250.955PASS
mean_response_at_reference109.96-0.40.0270.950-PASS
re_sd_site10.992-0.80.0140.975-PASS
residual_sd22.010.40.00720.930-PASS

binomial_glmm: 200 replicates, 200 converged (100.0%), 0 errors, coverage bound 0.904; mean fit time 0.57 s

estimandtruthmean estrel. bias %bias MCSEcoveragepowergate
beta_forest0.40.397-0.80.0110.9700.585PASS
beta_habitat_riparian0.50.5418.10.0290.9450.240PASS
beta_habitat_wetland-0.4-0.35710.70.0280.9700.135PASS
beta_intercept-0.5-0.4941.30.0190.970-PASS
beta_temp-0.3-0.30.10.00830.9800.650PASS
mean_response_at_reference0.3780.3811.00.00440.970-PASS
odds_ratio_forest1.491.50.80.0170.970-PASS
odds_ratio_habitat_riparian1.651.8713.10.0580.945-FAIL (bias)
odds_ratio_habitat_wetland0.670.75913.30.0230.970-FAIL (bias)
odds_ratio_temp0.7410.7460.70.0060.980-PASS
re_sd_site0.70.692-1.20.0180.945-PASS

binomial_trials_glm: 200 replicates, 200 converged (100.0%), 0 errors, coverage bound 0.904; mean fit time 0.32 s

estimandtruthmean estrel. bias %bias MCSEcoveragepowergate
beta_forest0.40.397-0.70.00540.9601.000PASS
beta_habitat_riparian0.50.471-5.90.0140.9400.690PASS
beta_habitat_wetland-0.4-0.427-6.70.0120.9700.660PASS
beta_intercept0.30.3258.20.00910.945-PASS
beta_temp-0.3-0.301-0.30.0050.9600.985PASS
mean_response_at_reference0.5740.581.00.00220.945-PASS
odds_ratio_forest1.491.490.00.00820.960-PASS
odds_ratio_habitat_riparian1.651.63-1.10.0220.940-PASS
odds_ratio_habitat_wetland0.670.662-1.30.00780.970-PASS
odds_ratio_temp0.7410.7420.20.00370.960-PASS

poisson_glm: 200 replicates, 200 converged (100.0%), 0 errors, coverage bound 0.904; mean fit time 0.34 s

estimandtruthmean estrel. bias %bias MCSEcoveragepowergate
beta_forest0.30.293-2.30.0030.9701.000PASS
beta_habitat_riparian0.40.387-3.20.00690.9750.980PASS
beta_habitat_wetland-0.3-0.307-2.40.00790.9500.800PASS
beta_intercept0.70.7071.00.00530.960-PASS
beta_temp-0.2-0.20.10.00310.9500.985PASS
mean_response_at_reference2.012.031.00.0110.960-PASS
rate_ratio_forest1.351.34-0.60.0040.970-PASS
rate_ratio_habitat_riparian1.491.48-0.80.010.975-PASS
rate_ratio_habitat_wetland0.7410.74-0.10.00590.950-PASS
rate_ratio_temp0.8190.820.10.00260.950-PASS

beta_glmm: 200 replicates, 200 converged (100.0%), 0 errors, coverage bound 0.904; mean fit time 0.91 s

estimandtruthmean estrel. bias %bias MCSEcoveragepowergate
beta_forest0.30.291-3.10.00460.9450.995PASS
beta_habitat_riparian0.30.313.40.0110.9450.465PASS
beta_habitat_wetland-0.3-0.293.40.0110.9750.420PASS
beta_intercept-0.5-0.502-0.40.0080.945-PASS
beta_precision_phi1515.10.40.0990.955-PASS
beta_temp-0.2-0.1990.60.00240.9701.000PASS
mean_response_at_reference0.3780.377-0.00.00190.945-PASS
odds_ratio_forest1.351.34-0.70.00620.945-PASS
odds_ratio_habitat_riparian1.351.382.20.0150.945-PASS
odds_ratio_habitat_wetland0.7410.7572.20.00790.975-PASS
odds_ratio_temp0.8190.820.20.0020.970-PASS
re_sd_site0.30.3020.50.00420.955-PASS

gamma_glmm: 200 replicates, 200 converged (100.0%), 0 errors, coverage bound 0.904; mean fit time 0.65 s

estimandtruthmean estrel. bias %bias MCSEcoveragepowergate
beta_forest0.20.194-2.90.00440.9700.865PASS
beta_habitat_riparian0.250.2625.00.0110.9250.385PASS
beta_habitat_wetland-0.2-0.1847.90.0110.9350.235PASS
beta_intercept32.98-0.50.00750.935-PASS
beta_temp-0.15-0.1481.10.0020.9651.000PASS
gamma_cv0.40.398-0.50.00130.965-PASS
mean_response_at_reference20.119.9-1.00.150.935-PASS
ratio_of_means_forest1.221.22-0.40.00540.970-PASS
ratio_of_means_habitat_riparian1.281.312.40.0140.925-PASS
ratio_of_means_habitat_wetland0.8190.8412.70.0090.935-PASS
ratio_of_means_temp0.8610.8630.20.00170.965-PASS
re_sd_site0.30.3010.40.00370.965-PASS

zip_glmm: 200 replicates, 200 converged (100.0%), 0 errors, coverage bound 0.904; mean fit time 0.61 s

estimandtruthmean estrel. bias %bias MCSEcoveragepowergate
beta_forest0.30.293-2.40.00770.9550.740PASS
beta_habitat_riparian0.40.4041.10.020.9150.360PASS
beta_habitat_wetland-0.3-0.2787.30.0180.9500.165PASS
beta_intercept0.70.7020.30.0130.965-PASS
beta_temp-0.2-0.201-0.60.00310.9550.985PASS
mean_response_at_reference1.611.641.80.0220.975-PASS
rate_ratio_forest1.351.35-0.10.0110.955-PASS
rate_ratio_habitat_riparian1.491.564.40.0310.915-PASS
rate_ratio_habitat_wetland0.7410.7835.70.0140.950-PASS
rate_ratio_temp0.8190.819-0.00.00260.955-PASS
re_sd_site0.50.5020.40.00630.980-PASS
zero_inflation_prob0.20.20.10.00260.930-PASS

zinb_glmm: 200 replicates, 200 converged (100.0%), 0 errors, coverage bound 0.904; mean fit time 0.88 s

estimandtruthmean estrel. bias %bias MCSEcoveragepowergate
beta_forest0.30.282-5.90.00930.9350.595PASS
beta_habitat_riparian0.40.4133.20.0230.9150.315PASS
beta_habitat_wetland-0.3-0.25813.90.0220.9300.145PASS
beta_intercept0.70.653-6.70.0160.915-PASS
beta_temp-0.2-0.208-4.00.00580.9450.740PASS
mean_response_at_reference1.611.59-1.20.0250.925-PASS
nb_size_theta22.136.70.0620.955-PASS
rate_ratio_forest1.351.34-0.90.0130.935-PASS
rate_ratio_habitat_riparian1.491.596.90.0390.915-PASS
rate_ratio_habitat_wetland0.7410.819.40.0180.930-PASS
rate_ratio_temp0.8190.815-0.50.00480.945-PASS
re_sd_site0.50.485-2.90.00980.980-PASS
zero_inflation_prob0.20.189-5.40.00410.955-PASS

interaction_glmm: 200 replicates, 200 converged (100.0%), 0 errors, coverage bound 0.904; mean fit time 0.62 s

estimandtruthmean estrel. bias %bias MCSEcoveragepowergate
beta_forest0.30.289-3.70.00730.9300.780PASS
beta_forest_x_temp0.20.2052.40.00310.965-PASS
beta_habitat_riparian0.40.4143.40.0180.9350.360PASS
beta_habitat_wetland-0.3-0.2738.90.0170.9550.155PASS
beta_intercept0.70.697-0.50.0130.945-PASS
beta_temp-0.2-0.206-2.90.0050.9400.850PASS
beta_temp_x_habitat_riparian0.30.3082.70.00690.970-PASS
beta_temp_x_habitat_wetland0-0.000992-0.0080.955-PASS
mean_response_at_reference2.012.041.20.0250.945-PASS
rate_ratio_forest1.351.34-0.60.010.930-PASS
rate_ratio_forest_x_temp1.221.230.60.00380.965-PASS
rate_ratio_habitat_riparian1.491.564.80.0290.935-PASS
rate_ratio_habitat_wetland0.7410.7845.90.0140.955-PASS
rate_ratio_temp0.8190.816-0.30.00410.940-PASS
rate_ratio_temp_x_habitat_riparian1.351.371.30.00940.970-PASS
rate_ratio_temp_x_habitat_wetland11.010.50.00810.955-PASS
re_sd_site0.50.5051.10.0060.960-PASS

hard_nb_small: 200 replicates, 200 converged (100.0%), 0 errors, coverage bound 0.904; mean fit time 0.56 s

estimandtruthmean estrel. bias %bias MCSEcoveragepowergate
beta_forest0.30.3020.60.020.8800.315FAIL (coverage)
beta_habitat_riparian0.40.394-1.40.040.9050.155PASS
beta_habitat_wetland-0.3-0.2845.30.0460.8750.140FAIL (coverage)
beta_intercept0.70.658-6.00.0280.880-FAIL (coverage)
beta_temp-0.2-0.227-13.30.0130.9250.225PASS
mean_response_at_reference2.012.093.80.060.880-FAIL (coverage)
nb_size_theta228100014034737.92800000.965-PASS
rate_ratio_forest1.351.414.50.0310.880-FAIL (coverage)
rate_ratio_habitat_riparian1.491.7517.00.0770.905-FAIL (bias)
rate_ratio_habitat_wetland0.7410.96730.60.080.875-FAIL (coverage)
rate_ratio_temp0.8190.811-1.00.010.925-PASS
re_sd_site0.50.288-42.40.0270.980-FAIL (bias)

hard_binomial_small: 200 replicates, 200 converged (100.0%), 0 errors, coverage bound 0.904; mean fit time 0.51 s

estimandtruthmean estrel. bias %bias MCSEcoveragepowergate
beta_forest0.40.45914.70.0290.9700.180PASS
beta_habitat_riparian0.50.5295.70.0670.9600.075PASS
beta_habitat_wetland-0.4-0.423-5.80.0690.9650.060PASS
beta_intercept-0.5-0.516-3.10.0510.950-PASS
beta_temp-0.3-0.351-16.90.0250.9600.140PASS
mean_response_at_reference0.3780.3872.50.010.950-PASS
odds_ratio_forest1.491.7215.50.0530.970-FAIL (bias)
odds_ratio_habitat_riparian1.653.2295.20.720.960-PASS
odds_ratio_habitat_wetland0.671.2281.90.270.965-PASS
odds_ratio_temp0.7410.7491.10.0180.960-PASS
re_sd_site0.70.7598.40.0660.845-FAIL (coverage)

hard_zinb_small: 200 replicates, 200 converged (100.0%), 0 errors, coverage bound 0.904; mean fit time 0.36 s

estimandtruthmean estrel. bias %bias MCSEcoveragepowergate
beta_forest0.30.29-3.30.00790.9250.795PASS
beta_habitat_riparian0.40.392-2.00.0180.9400.330PASS
beta_habitat_wetland-0.3-0.2980.50.0190.9500.205PASS
beta_intercept0.70.679-3.10.0170.935-PASS
beta_temp-0.2-0.1980.90.00860.9200.475PASS
mean_response_at_reference1.411.4-0.90.020.935-PASS
nb_size_theta11.2929.10.0390.870-FAIL (bias, coverage)
rate_ratio_forest1.351.34-0.40.0110.925-PASS
rate_ratio_habitat_riparian1.491.532.60.0290.940-PASS
rate_ratio_habitat_wetland0.7410.774.00.0160.950-PASS
rate_ratio_temp0.8190.8260.90.00720.920-PASS
zero_inflation_prob0.30.3-0.10.00680.915-PASS

null_effects: 500 replicates, 500 converged (100.0%), 0 errors, coverage bound 0.921; mean fit time 0.67 s

estimandtruthmean estrel. bias %bias MCSEcoveragepowergate
beta_forest07.81e-05-0.00530.9420.058PASS
beta_habitat_riparian0.40.39-2.60.0120.9500.284PASS
beta_habitat_wetland-0.3-0.2826.10.0120.9520.164PASS
beta_intercept0.70.685-2.20.00870.954-PASS
beta_temp00.000329-0.00280.9580.042PASS
mean_response_at_reference2.012.020.40.0180.954-PASS
nb_size_theta22.042.00.0180.938-PASS
rate_ratio_forest11.010.70.00540.942-PASS
rate_ratio_habitat_riparian1.491.532.80.0190.950-PASS
rate_ratio_habitat_wetland0.7410.7835.70.00980.952-PASS
rate_ratio_temp110.20.00280.958-PASS
re_sd_site0.50.49-1.90.00510.976-PASS

misspec_poisson: 200 replicates, 200 converged (100.0%), 0 errors, coverage bound 0.904; mean fit time 0.55 s

estimandtruthmean estrel. bias %bias MCSEcoveragepowergate
beta_forest0.30.281-6.30.00840.9500.645PASS
beta_habitat_riparian0.40.4030.70.0210.9450.310PASS
beta_habitat_wetland-0.3-0.26411.80.020.9400.150PASS
beta_intercept0.70.67-4.30.0140.925-PASS
beta_temp-0.2-0.206-3.20.00530.6800.960FAIL (coverage)
mean_response_at_reference2.011.99-1.00.0280.925-PASS
rate_ratio_forest1.351.33-1.20.0110.950-PASS
rate_ratio_habitat_riparian1.491.564.70.0340.945-PASS
rate_ratio_habitat_wetland0.7410.7987.70.0160.940-PASS
rate_ratio_temp0.8190.816-0.40.00440.680-FAIL (coverage)
re_sd_site0.50.57214.30.0070.845-FAIL (bias, coverage)

misspec_norandom: 200 replicates, 200 converged (100.0%), 0 errors, coverage bound 0.904; mean fit time 0.35 s

estimandtruthmean estrel. bias %bias MCSEcoveragepowergate
beta_forest0.30.283-5.80.00880.7150.880FAIL (coverage)
beta_habitat_riparian0.40.4071.80.0220.6850.590FAIL (coverage)
beta_habitat_wetland-0.3-0.2738.90.020.7600.410FAIL (coverage)
beta_intercept0.70.79313.30.0150.695-FAIL (bias, coverage)
beta_temp-0.2-0.21-4.80.0050.9450.850PASS
mean_response_at_reference2.012.2612.20.0340.695-FAIL (bias, coverage)
nb_size_theta21.43-28.70.0190.390-FAIL (bias, coverage)
rate_ratio_forest1.351.34-1.00.0120.715-FAIL (coverage)
rate_ratio_habitat_riparian1.491.585.90.0360.685-FAIL (coverage)
rate_ratio_habitat_wetland0.7410.7947.10.0170.760-FAIL (coverage)
rate_ratio_temp0.8190.813-0.70.00410.945-PASS

Engine, references and full details

Engine: glmmTMB (REML for models with random intercepts, ML otherwise; Laplace approximation); stats::glm / MASS::glm.nb fallback for fixed-effects-only models.

References

  • Brooks ME, Kristensen K, van Benthem KJ, Magnusson A, Berg CW, Nielsen A, Skaug HJ, Maechler M, Bolker BM (2017). glmmTMB balances speed and flexibility among packages for zero-inflated generalized linear mixed modeling. The R Journal 9(2):378-400. doi:10.32614/RJ-2017-066
  • Bolker BM, Brooks ME, Clark CJ, Geange SW, Poulsen JR, Stevens MHH, White J-SS (2009). Generalized linear mixed models: a practical guide for ecology and evolution. Trends in Ecology and Evolution 24(3):127-135.
  • McCullagh P, Nelder JA (1989). Generalized Linear Models, 2nd ed. Chapman & Hall.
  • Hardin JW, Hilbe JM (2007). Generalized Linear Models and Extensions, 2nd ed. Stata Press.
  • Ferrari SLP, Cribari-Neto F (2004). Beta regression for modelling rates and proportions. Journal of Applied Statistics 31(7):799-815.
  • Smithson M, Verkuilen J (2006). A better lemon squeezer? Maximum-likelihood regression with beta-distributed dependent variables. Psychological Methods 11(1):54-71.
  • Dunn PK, Smyth GK (1996). Randomized quantile residuals. Journal of Computational and Graphical Statistics 5(3):236-244.
  • Hartig F (2026). DHARMa: Residual Diagnostics for Hierarchical (Multi-Level / Mixed) Regression Models. R package 0.5.0. doi:10.32614/CRAN.package.DHARMa
  • Nakagawa S, Johnson PCD, Schielzeth H (2017). The coefficient of determination R2 and intra-class correlation coefficient from generalized linear mixed-effects models revisited and expanded. Journal of the Royal Society Interface 14:20170213.
  • Luedecke D, Ben-Shachar MS, Patil I, Waggoner P, Makowski D (2021). performance: an R package for assessment, comparison and testing of statistical models. Journal of Open Source Software 6(60):3139.
  • Schielzeth H (2010). Simple means to improve the interpretability of regression coefficients. Methods in Ecology and Evolution 1(2):103-113.

CascadiaTools is made by Taal Levi, Cascadia Wildlife Lab, Oregon State University. Source code on GitHub · How to cite

Based at
Oregon State University