--- title: "Visualising model estimates" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Visualising model estimates} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} bibliography: ../inst/REFERENCES.bib csl: apa.csl link-citations: true --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = FALSE, comment = "", fig.width = 7, fig.height = 4.5, dpi = 96, dev.args = list(bg = "transparent")) # Console colour carries no meaning on a rendered page. pkgdown turns it on for # its own build, and the escape sequences then reach the reader as literal text, # so colour is switched off here for a plain vignette render and a site build # alike. The fixed width keeps printed output inside the documentation column. options(cli.num_colors = 1, cli.hyperlink = FALSE, crayon.enabled = FALSE, width = 80) # Figures on the package website sit on a warm off-white page in light mode and # are inverted by pkgdown in dark mode, so an opaque background would read as a # pale slab one way and a black plate the other. Two things paint one. The # device # canvas is made transparent by `dev.args` above, and theme_depictr() then # inherits theme_minimal()'s white plot.background, which is drawn over that # canvas, so it is cleared as each figure is printed. This is deliberately a # vignette-level choice: theme_depictr() keeps its opaque background, which is # what a figure saved for a paper wants. transparent_bg <- ggplot2::theme( plot.background = ggplot2::element_rect(fill = NA, colour = NA), panel.background = ggplot2::element_rect(fill = NA, colour = NA) ) knit_print.ggplot <- function(x, ...) knitr::normal_print(x + transparent_bg) knit_print.patchwork <- function(x, ...) knitr::normal_print(x & transparent_bg) library(depictr) # Every chunk that touches the fitted model is guarded on lmerTest, which imports # lme4, so one condition covers both packages. has_lmer <- requireNamespace("lmerTest", quietly = TRUE) has_ggdist <- requireNamespace("ggdist", quietly = TRUE) ``` This is the flagship article. It works through depictr's model-result plots (coefficients, model comparison, predicted values, interactions, random effects and goodness-of-fit) and then reaches the capability that `frequentist_bayesian_plot()` is named for: showing a frequentist estimate and a full Bayesian posterior for the same model on one figure. The running example is `lexical_decision`, a counterbalanced, crossed lexical-decision experiment (24 participants, 40 items, 960 trials). We model reaction time on correct trials as a function of priming `condition` (related/unrelated), presentation `modality` (visual/auditory) and item `word_frequency`, with crossed random intercepts for participant and item. The design is counterbalanced, so neither fixed factor is collinear with the item random effect, a clean fit for a mixed model. ```{r fit, eval = has_lmer} correct <- subset(lexical_decision, accuracy == 1) fit <- lmerTest::lmer( RT ~ condition + modality + word_frequency + (1 | participant) + (1 | item), data = correct ) ``` ## Coefficient (forest) plots `coefficient_plot()` reads the fitted mixed model directly (through `tidy_estimates()`) and draws a horizontal point-and-interval ('forest') plot. Unrelated primes and auditory presentation both slow responses, while more frequent words speed them up. ```{r coef, eval = has_lmer} coefficient_plot( fit, order = "ascending", labels = c(conditionunrelated = "Unrelated priming", modalityauditory = "Auditory modality", word_frequency = "Word frequency (Zipf)"), title = "Predictors of lexical-decision RT (ms)" ) ``` Coefficient names are tidied to the effect (variable) name automatically (`conditionunrelated` becomes `condition`, `word_frequency` becomes `word frequency`). Pass `labels` to override any of them. The slopes also sit on quite different scales, so `standardise = TRUE` rescales each coefficient by its predictor's standard deviation, putting them on one comparable axis: ```{r coef-std, eval = has_lmer} coefficient_plot(fit, standardise = TRUE, order = "ascending", title = "Standardised predictors of RT") ``` To keep the raw units instead, including a large intercept that would otherwise squash the slopes, `facet = TRUE` gives each term its own free-scaled panel (the layout the frequentist-vs-Bayesian comparison below uses by default). ## Comparing models `compare_models()` overlays the estimates from several models so you can see how a coefficient moves as the specification changes. Here a reduced model (dropping `modality`) is compared with the full model, and `model_fit_table()` summarises their fit. ```{r compare, eval = has_lmer} reduced <- lmerTest::lmer( RT ~ condition + word_frequency + (1 | participant) + (1 | item), data = correct ) compare_models(Reduced = reduced, Full = fit, order = "descending") ``` ```{r fit-table, eval = has_lmer} knitr::kable(model_fit_table(Reduced = reduced, Full = fit)) ``` For `glm` models the `R2` column reports McFadden's pseudo-R-squared [@mcfadden1974] in place of the ordinary coefficient of determination. ## Predicted values and interactions `effects_plot()` shows what the model predicts as one predictor varies, holding the others at typical values. It also works on the mixed model: predictions use the fixed effects only (`re.form = NA`) and the band is built from the fixed-effect design matrix and `vcov()`. ```{r effects, eval = has_lmer} effects_plot(fit, "word_frequency", title = "Predicted RT across word frequency") ``` `interaction_plot()` shows how a relationship changes across a second predictor. The lexical-decision model is additive, so for a *genuine* interaction we switch to `crop_yield`, whose data-generating process contains a real fertiliser-by-treatment effect: fertiliser raises yield far more under the `enhanced` treatment than under `standard`, so the slopes diverge. ```{r interaction} crop_fit <- lm(yield ~ fertiliser * treatment + rainfall, data = crop_yield) interaction_plot(crop_fit, "fertiliser", "treatment", title = "Fertiliser x treatment interaction") ``` ## Frequentist and Bayesian estimates together This is the capability `frequentist_bayesian_plot()` is named for. Given the frequentist fit and a set of Bayesian posterior draws, the function draws the *full posterior distribution* for each term (a 'ggdist' half-eye) and overlays the frequentist point and confidence interval at the same position. The entire shape of the posterior appears next to the frequentist estimate, rather than a point and two limits alone, with the two sources in the two leading colourblind-safe palette colours. The draws here are the real fixed-effect posterior from a `brms` fit of the same model (1000 draws x 4 parameters), shipped with the package so the slow MCMC need not be re-run. Terms are matched by canonical label, so the `brms` parameter names line up with the frequentist ones automatically. ```{r fbp, eval = has_lmer && has_ggdist, fig.height = 4} draws <- readRDS( system.file("extdata", "lexdec_draws.rds", package = "depictr") ) frequentist_bayesian_plot( fit, draws, intercept = FALSE, note_frequentist_no_prior = TRUE, title = "Frequentist estimate over the full Bayesian posterior" ) ``` The frequentist confidence interval and the bulk of the Bayesian posterior land in the same place (reassuring agreement between the two paradigms), but only the posterior shows the density, the skew and the mass on either side of zero. If `ggdist` is unavailable the function falls back to a point-and-interval forest plot of the two sources, and when the Bayesian side is supplied as a *summary* table instead of draws (for instance the `Estimate`/`Q2.5`/`Q97.5` of `brms::fixef()`) it draws the familiar two-source forest plot. ## Posterior distributions on their own `posterior_plot()` summarises any draws (posterior, bootstrap or simulation) as a distribution per parameter. With `style = "halfeye"` it shows the density slab and a point-and-interval. A region of practical equivalence (ROPE) can be shaded and each parameter annotated with its probability of direction (the posterior mass on its majority side of the reference line). ```{r posterior, eval = has_ggdist, fig.height = 4} draws <- readRDS( system.file("extdata", "lexdec_draws.rds", package = "depictr") ) slopes <- draws[c("conditionunrelated", "modalityauditory", "word_frequency")] posterior_plot( slopes, style = "halfeye", rope = c(-5, 5), pd = TRUE, labels = c(conditionunrelated = "condition", modalityauditory = "modality", word_frequency = "word frequency"), title = "Fixed-effect posteriors (ms), with ROPE and pd" ) ``` The probability of direction for `word_frequency` and `conditionunrelated` is effectively 100%: the posterior sits almost entirely on one side of zero. ## Random effects `random_effects_plot()` draws a caterpillar plot of the conditional modes ('BLUPs'). Reading the fitted model directly, it shows the by-item and by-participant departures from the average, sorted, with their uncertainty, the usual way to spot unusual groups. ```{r ranef, eval = has_lmer, fig.height = 6} random_effects_plot(fit, title = "By-group departures (random intercepts)") ``` ## Optimiser checks A mixed-model fit should be stable across optimisers. `lme4::allFit()` refits the model with every available optimiser, and `optimizer_fixef_plot()` then shows the fixed effects side by side, one panel per term. Tight clusters mean the fit has settled, while scatter would signal a fragile solution. The package ships the `allFit()` summary for this model, so we can plot it without re-running the (slow) refits. The plot accepts a tidy data frame of optimiser-by-term values, which we read straight off the stored summary. ```{r optim, fig.height = 4.5} af <- readRDS(system.file("extdata", "allfit_lexdec.rds", package = "depictr")) fx <- af$fixef # optimisers x fixed effects opt_long <- data.frame( optimizer = rep(rownames(fx), times = ncol(fx)), term = rep(colnames(fx), each = nrow(fx)), value = as.vector(fx) ) optimizer_fixef_plot( opt_long, title = "Fixed effects across optimisers", labels = c(conditionunrelated = "condition", modalityauditory = "modality", word_frequency = "word frequency") ) ``` Every optimiser lands on the same estimate for each term (the points coincide within each panel), so this fit is stable. ## A one-figure model report `model_report()` composes several views (coefficients, the effect of a focal predictor, residuals against fitted values and a Q-Q plot, with a fit-statistics subtitle) into a single figure for a rapid review or a report appendix. It works on `lm`/`glm` models, and here we use the crop-yield model. ```{r report, fig.width = 9, fig.height = 7} full <- lm(yield ~ rainfall + fertiliser + soil_ph + treatment, data = crop_yield) model_report(full, title = "Crop-yield model") ``` ## References