--- title: "When variance carries signal, Part 1: location-scale models" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{When variance carries signal, Part 1: location-scale models} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 4.1, dpi = 144 ) if (!"package:drmTMB" %in% search()) { library(drmTMB) } location_scale_theme <- function() { ggplot2::theme_minimal(base_size = 11) + ggplot2::theme( panel.grid.minor = ggplot2::element_blank(), panel.grid.major.x = ggplot2::element_line( colour = "grey90", linewidth = 0.3 ), panel.grid.major.y = ggplot2::element_line( colour = "grey87", linewidth = 0.35 ), axis.title = ggplot2::element_text(colour = "grey15"), axis.text = ggplot2::element_text(colour = "grey25"), plot.title = ggplot2::element_text( face = "bold", colour = "grey10", margin = ggplot2::margin(b = 4) ), plot.subtitle = ggplot2::element_text( colour = "grey30", margin = ggplot2::margin(b = 8) ), legend.position = "bottom", legend.title = ggplot2::element_text(colour = "grey20") ) } location_scale_habitat_scales <- function() { list( ggplot2::scale_colour_manual( values = c("forest" = "#0072B2", "grassland" = "#009E73") ), ggplot2::scale_fill_manual( values = c("forest" = "#0072B2", "grassland" = "#009E73") ) ) } ``` This tutorial starts with a biological question: do two habitats differ only in mean growth, or does one habitat also make growth less predictable? In a Gaussian location-scale model, the location part models the expected response `mu`, while the scale part models the residual standard deviation `sigma`. That residual scale is not just a nuisance parameter; it can be the scientific answer when the question is about individual variability, predictability, or remaining heterogeneity after the mean model is accounted for. This is Part 1 of a two-article sequence. It models the response mean `mu` and the residual standard deviation `sigma`. Continue to [Part 2: location-scale-scale models](location-scale-scale.html) when a predictor should model the standard deviation of a latent group or phylogenetic random effect through `sd()`. Read this after you have fit a first model in [Distributional regression with drmTMB](drmTMB.html) and confirmed Gaussian is the right family in [Choosing response families](distribution-families.html); this tutorial goes deeper into interpreting `mu` and `sigma` once that choice is made. If you are fitting your first model, start with the [worked growth example](#worked-example-growth-mean-and-predictability), then return to the syntax overview below when you want to adapt the formula. The core Gaussian location-scale route supports fixed effects, optional random effects in the location formula, and residual-scale random intercepts and independent random slopes in the `sigma` formula. The same pattern is used throughout the documentation: write the model symbolically first, show the matching R syntax, fit the model, and interpret the fitted output. ## Model equations and matching R syntax For Gaussian location-scale regression, each estimated parameter has its own linear predictor: \[ \begin{aligned} y_i \mid \mu_i, \sigma_i &\sim \operatorname{Normal}(\mu_i, \sigma_i^2),\\ \mu_i &= \mathbf{x}_i^{\mathsf T}\boldsymbol\beta,\\ \log(\sigma_i) &= \mathbf{z}_i^{\mathsf T}\boldsymbol\gamma. \end{aligned} \] The matching syntax is: ```r drmTMB( bf(y ~ x, sigma ~ z), family = gaussian(), data = dat ) ``` The location formula defines the design matrix for `beta`; the scale formula defines the design matrix for `gamma`. The log link keeps the fitted residual SD positive. | Quantity | R source | Interpretation | |---|---|---| | `mu_i` | `y ~ x` | expected response | | `beta` | `coef(fit, "mu")` | additive effects on the expected response | | `sigma_i` | `sigma ~ z` | residual SD around the fitted mean | | `gamma` | `coef(fit, "sigma")` | effects on log residual SD | ### Grouped effects do not change the meaning of sigma A random intercept in the location model adds among-group variation in expected responses: \[ \mu_{ij} = \mathbf{x}_{ij}^{\mathsf T}\boldsymbol\beta + b_j, \qquad b_j \sim \operatorname{Normal}(0,sd_{\mu,group}^2). \] ```r drmTMB( bf(y ~ x + (1 | group), sigma ~ z), family = gaussian(), data = dat ) ``` Here `sd_mu_group` is the SD of group-level mean deviations. It is not the residual SD `sigma`. A random effect inside the `sigma` formula asks a third question: do groups differ in their residual variability? ```r drmTMB( bf(y ~ x + (1 | group), sigma ~ z + (1 | group)), family = gaussian(), data = dat ) ``` More elaborate ordinary intercept-slope blocks and their covariance output are documented in [Which scale are you modelling?](which-scale.html) and [Structural dependence](structural-dependence.html). To model predictors of the group-level SD itself, continue to [Part 2: location-scale-scale models](location-scale-scale.html). ### Interpret sigma on the SD or variance scale Because `log(sigma_i) = z_i^T gamma`, exponentiating a scale coefficient gives an SD ratio. If `gamma_temperature = 0.3`, then a one-unit temperature increase multiplies residual SD by `exp(0.3) = 1.35`. Squaring the ratio gives the residual-variance ratio, `exp(2 * 0.3) = 1.82`. | Formula component | Report | Biological reading | |---|---|---| | `growth ~ temperature` | `beta_temperature` | additive change in expected growth | | `sigma ~ temperature` | `exp(gamma_temperature)` | residual-SD ratio per temperature unit | | `(0 + temperature | population)` in `mu` | random-slope SD | variation among population-specific mean slopes | | `sigma ~ (0 + temperature | population)` | residual-scale random-slope SD | variation among population-specific log-SD slopes | Name the measured response and model component before stating a biological conclusion. “Temperature-dependent variability” is ambiguous until the reader knows whether the model changed residual `sigma`, a mean random-slope SD, or a random effect inside `sigma`. ## Worked example: growth mean and predictability Suppose an ecologist measures juvenile growth in two habitats and records the temperature at each observation. The location question is whether mean growth differs between habitats and changes with temperature. The scale question is whether residual growth variability differs between habitats after accounting for those mean effects. For this example, the fitted model is: \[ \begin{aligned} \text{growth}_i &\sim \operatorname{Normal}(\mu_i, \sigma_i^2), \\ \mu_i &= \beta_0 + \beta_1 \mathbb{1}(\text{habitat}_i = \text{grassland}) + \beta_2 \text{temperature}_i, \\ \log(\sigma_i) &= \gamma_0 + \gamma_1 \mathbb{1}(\text{habitat}_i = \text{grassland}). \end{aligned} \] The matching `drmTMB` syntax is: ```r drmTMB( drm_formula(growth ~ habitat + temperature, sigma ~ habitat), family = gaussian(), data = dat ) ``` The code below simulates one dataset with the same structure. Simulated data keep the vignette small and reproducible; a real analysis would replace this block with field or laboratory measurements. ```{r location-scale-simulate} set.seed(42) n <- 240 dat <- data.frame( habitat = factor(rep(c("forest", "grassland"), each = n / 2)), temperature = rnorm(n) ) mu <- 8 + 1.2 * (dat$habitat == "grassland") + 0.6 * dat$temperature sigma_true <- exp(log(0.6) + 0.55 * (dat$habitat == "grassland")) dat$growth <- rnorm(n, mean = mu, sd = sigma_true) ``` Fit the location-scale model: ```{r location-scale-fit} fit_growth <- drmTMB( drm_formula(growth ~ habitat + temperature, sigma ~ habitat), family = gaussian(), data = dat ) ``` Before interpreting coefficients, check the fitted object: ```{r location-scale-check} check_drm(fit_growth) ``` The interval target inventory is part of the same interpretation gate. For this fixed-effect example, every coefficient is a direct profile target: ```{r location-scale-profile-targets} profile_targets(fit_growth)[ , c("parm", "estimate", "profile_ready", "profile_note") ] ``` Now print the coefficient table: ```{r location-scale-summary} summary(fit_growth) ``` How to read this output: 1. Rows beginning with `mu:` are mean-growth effects on the response scale. For example, `mu:temperature` is the expected change in mean growth for a one-unit increase in temperature. 2. Rows beginning with `sigma:` are log-residual-standard-deviation effects. For example, `sigma:habitatgrassland` is not an additive change in growth; it is a log-scale change in residual variability. 3. Exponentiating a `sigma` coefficient gives the multiplicative change in residual standard deviation. ```{r location-scale-interpret-sigma} sigma_habitat <- coef(fit_growth, "sigma")["habitatgrassland"] data.frame( coefficient = sigma_habitat, residual_sd_ratio = exp(sigma_habitat), residual_variance_ratio = exp(2 * sigma_habitat) ) ``` In this fitted example, `exp(sigma:habitatgrassland)` is the estimated ratio of grassland residual SD to forest residual SD. A value near 2 means that grassland has about twice the residual SD of forest after accounting for mean habitat and temperature effects. Because residual variance is SD squared, the same fitted coefficient implies about four times the residual variance. It is often clearer to report the fitted residual SDs and variances directly: ```{r location-scale-response-table} newdat <- data.frame( habitat = factor(c("forest", "grassland"), levels = levels(dat$habitat)), temperature = 0 ) growth_report <- data.frame( habitat = newdat$habitat, fitted_mean_growth = predict(fit_growth, newdata = newdat, dpar = "mu"), fitted_residual_sd = predict(fit_growth, newdata = newdat, dpar = "sigma") ) growth_report$fitted_residual_variance <- growth_report$fitted_residual_sd^2 growth_report ``` This table maps the model back to the scientific question. The mean column summarises the location model. The residual SD column summarises the scale model in the units of growth. The residual variance column is the variance-facing version of the same fitted Gaussian scale model. The fitted mean can be checked against the raw response pattern. Here the points are observed growth values; the fitted lines and ribbons come from `predict_parameters()` on an explicit temperature-by-habitat grid: ```{r location-scale-growth-figure, fig.cap = "Raw growth observations and fitted response-scale `mu` surfaces for the Gaussian location-scale example; ribbons are 95% Wald confidence bands from `predict_parameters()`.", fig.alt = "Scatterplot of simulated growth over temperature for forest and grassland habitats, with fitted mean growth lines and 95 percent Wald confidence bands for each habitat."} growth_mu_grid <- prediction_grid( fit_growth, focal = c("temperature", "habitat"), at = list( temperature = seq( min(dat$temperature), max(dat$temperature), length.out = 80 ) ) ) growth_mu_surface <- predict_parameters( fit_growth, newdata = growth_mu_grid, dpar = "mu", conf.int = TRUE ) unique(growth_mu_surface[, c( "dpar", "conf.status", "interval_source", "conf.level" )]) if (requireNamespace("ggplot2", quietly = TRUE)) { ggplot2::ggplot( dat, ggplot2::aes(x = temperature, y = growth, colour = habitat) ) + ggplot2::geom_point(alpha = 0.38, size = 1.25) + ggplot2::geom_ribbon( data = growth_mu_surface, ggplot2::aes( x = temperature, ymin = conf.low, ymax = conf.high, fill = habitat ), inherit.aes = FALSE, alpha = 0.18, colour = NA ) + ggplot2::geom_line( data = growth_mu_surface, ggplot2::aes(y = estimate), linewidth = 0.85 ) + location_scale_habitat_scales() + ggplot2::labs( title = "Mean growth and observed scatter", subtitle = "Points are raw growth; ribbons are 95% Wald bands for mu", x = "Temperature", y = "Growth", colour = "Habitat", fill = "Habitat" ) + location_scale_theme() + ggplot2::guides(fill = "none") } ``` The scale model is a different display. Do not put raw `growth` points on the `sigma` axis; plot the fitted residual SD and name the interval source: ```{r location-scale-sigma-contrast-figure, fig.width = 5.8, fig.height = 2.6, fig.cap = "Fitted residual standard deviations by habitat at average temperature; horizontal intervals are 95% Wald confidence intervals requested from `predict_parameters()`.", fig.alt = "Horizontal point interval plot comparing fitted residual standard deviation for forest and grassland habitats at temperature zero, with 95 percent Wald confidence intervals."} growth_sigma_grid <- prediction_grid( fit_growth, focal = "habitat", condition = list(temperature = 0) ) growth_sigma_surface <- predict_parameters( fit_growth, newdata = growth_sigma_grid, dpar = "sigma", conf.int = TRUE ) unique(growth_sigma_surface[, c( "dpar", "conf.status", "interval_source", "conf.level" )]) if (requireNamespace("ggplot2", quietly = TRUE)) { ggplot2::ggplot( growth_sigma_surface, ggplot2::aes(x = estimate, y = habitat, colour = habitat) ) + ggplot2::geom_segment( ggplot2::aes(x = conf.low, xend = conf.high, yend = habitat), linewidth = 0.8 ) + ggplot2::geom_point(size = 2.8) + location_scale_habitat_scales() + ggplot2::labs( title = "Residual SD differs by habitat", subtitle = "Points are fitted sigma values; bars are 95% Wald intervals", x = "Fitted residual SD (sigma)", y = NULL, colour = "Habitat" ) + location_scale_theme() + ggplot2::guides(colour = "none") } ``` The compact translation table below is the reporting layer Pat should be able to read without returning to the equations: ```{r location-scale-translation-table} growth_translation <- data.frame( model_piece = c( "fixed mean slope", "fixed residual-SD contrast", "fixed residual-variance contrast" ), fitted_term = c( "mu:temperature", "exp(sigma:habitatgrassland)", "exp(2 * sigma:habitatgrassland)" ), response_scale_value = c( unname(coef(fit_growth, "mu")["temperature"]), unname(exp(sigma_habitat)), unname(exp(2 * sigma_habitat)) ), interpretation = c( "mean growth change per one-unit temperature increase", "grassland residual SD divided by forest residual SD", "grassland residual variance divided by forest residual variance" ) ) growth_translation$response_scale_value <- round(growth_translation$response_scale_value, 3) growth_translation ``` A report can now say three concrete things. Mean growth increases by the fitted `mu:temperature` slope for each one-unit temperature increase. Grassland has the fitted residual-SD ratio in the second row after accounting for mean habitat and temperature effects. Because Gaussian variance is `sigma^2`, the residual variance contrast is the third row. If that third row is larger than one, individual growth is less predictable in grassland. The same reporting discipline carries over to hierarchical versions of the question: | If the biological question is... | Fit this kind of term | Report this quantity | Check before reporting | |---|---|---|---| | Do populations differ in thermal plasticity? | `(0 + temperature | population)` or `(1 + temperature | population)` in the `mu` formula | the random-slope SD, in growth-per-temperature units | `check_drm(fit)` and the matching `sd:mu:...temperature...` row in `profile_targets(fit)` | | Do high-baseline populations also have steeper thermal reaction norms? | `(1 + temperature | population)` in the `mu` formula | the random-slope SD, and when the correlated block is fitted, the group-level intercept-slope correlation from `corpairs(fit, class = "mean-slope")` | `check_drm(fit)`, the matching `sd:mu:...temperature...` row, and the fitted correlation row in `profile_targets(fit)` | | Does predictability change with temperature? | `sigma ~ temperature` | `exp(gamma_temperature)` as the residual-SD ratio, and `exp(2 * gamma_temperature)` when the paper talks about variance | `check_drm(fit)` and the `fixef:sigma:temperature` row in `profile_targets(fit)` | | Do habitats differ in among-population variation? | `(1 | population)` plus `sd(population) ~ habitat` | `exp(alpha_habitat)` as the ratio of among-population SDs | `check_drm(fit)` and the `fixef:sd(population):...` row in `profile_targets(fit)` | These three rows answer different biological questions. `sigma ~ temperature` models residual variation among observations. `(0 + temperature | population)` models population-to-population differences in the mean slope. `sd(population) ~ habitat` models the size of among-population mean differences. ## Curved responses and interactions Quadratic terms and interactions can appear in either formula: ```r fit_curve <- drmTMB( bf( growth ~ habitat * temperature + I(temperature^2), sigma ~ I(temperature^2) ), family = gaussian(), data = dat ) ``` A linear coefficient is only a local slope when its model also contains a quadratic term or interaction. Interpret the fitted curve at biologically meaningful predictor values rather than reading that coefficient alone: ```r curve_grid <- expand.grid( habitat = c("forest", "grassland"), temperature = c(-1.5, 0, 1.5) ) data.frame( curve_grid, mu = predict(fit_curve, newdata = curve_grid, dpar = "mu"), sigma = predict(fit_curve, newdata = curve_grid, dpar = "sigma") ) ``` If the `sigma` formula is omitted, `drmTMB` fits one intercept-only residual SD. Use that simpler model only when a common residual SD is scientifically and diagnostically adequate. ## Continue to Part 2 Part 1 has separated expected response `mu` from residual SD `sigma`. Continue to [Part 2: location-scale-scale models](location-scale-scale.html) when the scientific question also asks whether a predictor changes the SD of a grouped or phylogenetic location effect through `sd()`. - Use [Mean effects and residual heterogeneity](meta-analysis.html) for Gaussian meta-analysis with known sampling covariance through `meta_V(V = V)`. - Use [Changing residual coupling with rho12](bivariate-coscale.html) for two responses and predictor-dependent residual correlation. - Use [Structural dependence](structural-dependence.html) for `phylo()`, `animal()`, `spatial()`, and `relmat()`. ## Current implementation boundaries - This article documents the univariate Gaussian `mu` and `sigma` route. - Random intercepts, independent slopes, and ordinary correlated intercept-slope blocks are available in `mu`; `sigma` supports its documented random-intercept and random-slope slices. - Scale coefficients use a log-SD link. Exponentiate once for an SD ratio and twice in the exponent for a variance ratio. - Missing rows are removed when variables used by either formula are missing. - A fitted optimizer code is not enough: run `check_drm()`, inspect gradient and Hessian diagnostics, and use [Can I fit and report this model?](capability-and-limits.html) before widening the claim. - `sigma` is not a group random-effect SD. Use Part 2 for the separate `sd(group) ~ predictors` grammar.