This gallery shows the kinds of figures drmTMB users can
build for model interpretation, uncertainty display, fitted
interactions, fitted correlation summaries, and later simulation
diagnostics. It is different from the internal count-pilot diagnostics
used while developing the first Poisson/NB2 simulation plumbing.
The examples here use small simulated data so the page can build quickly. They are meant to show figure patterns, not biological conclusions. The interaction figures deliberately keep three pieces visible: the observed data, the fitted surface or cell means, and the conditioning values used to make the display.
library(drmTMB)
#>
#> Attaching package: 'drmTMB'
#> The following object is masked from 'package:base':
#>
#> beta
library(ggplot2)
theme_drmtmb_gallery <- function() {
theme_minimal(base_size = 11) +
theme(
panel.grid.minor = element_blank(),
panel.grid.major.x = element_blank(),
axis.title = element_text(colour = "grey15"),
plot.title.position = "plot",
plot.title = element_text(face = "bold"),
plot.subtitle = element_text(colour = "grey30"),
strip.text = element_text(face = "bold"),
legend.position = "bottom",
legend.title = element_text(colour = "grey20"),
legend.key.width = grid::unit(1.1, "lines")
)
}
gallery_colours <- c(
blue = "#0072B2",
green = "#009E73",
orange = "#D55E00",
yellow = "#E69F00",
pink = "#CC79A7",
sky = "#56B4E9",
charcoal = "#4D4D4D"
)
gallery_colour <- function(name) unname(gallery_colours[name])
habitat_palette <- c(
"reef" = gallery_colour("blue"),
"kelp" = gallery_colour("orange")
)
season_palette <- c(
"dry" = gallery_colour("blue"),
"wet" = gallery_colour("yellow")
)
surface_palette <- c(
"Gaussian location-scale" = gallery_colour("blue"),
"Proportion" = gallery_colour("green"),
"Count" = gallery_colour("pink")
)
make_confidence_distribution <- function(
parameter,
estimate,
std_error,
transform = c("identity", "exp"),
null_value = 0,
n = 500
) {
transform <- match.arg(transform)
theta <- seq(
estimate - 4 * std_error,
estimate + 4 * std_error,
length.out = n
)
value <- switch(
transform,
identity = theta,
exp = exp(theta)
)
density <- switch(
transform,
identity = dnorm(theta, mean = estimate, sd = std_error),
exp = dnorm(theta, mean = estimate, sd = std_error) / value
)
data.frame(
parameter = parameter,
theta = theta,
value = value,
cdf = pnorm(theta, mean = estimate, sd = std_error),
density = density,
null_value = switch(transform, identity = null_value, exp = exp(null_value)),
estimate_value = switch(transform, identity = estimate, exp = exp(estimate))
)
}
make_confidence_intervals <- function(
parameter,
estimate,
std_error,
transform = c("identity", "exp"),
levels = c(0.66, 0.95)
) {
transform <- match.arg(transform)
z_low <- qnorm((1 - levels) / 2)
z_high <- qnorm(1 - (1 - levels) / 2)
low <- estimate + z_low * std_error
high <- estimate + z_high * std_error
data.frame(
parameter = parameter,
level = sprintf("%d%%", round(100 * levels)),
y = seq(0.10, 0.22, length.out = length(levels)),
xmin = switch(transform, identity = low, exp = exp(low)),
xmax = switch(transform, identity = high, exp = exp(high))
)
}
scale_drmtmb_colour <- scale_colour_manual(
values = habitat_palette,
limits = names(habitat_palette)
)
scale_drmtmb_fill <- scale_fill_manual(
values = habitat_palette,
limits = names(habitat_palette)
)Fitted mean with a confidence band
Start with the figure most readers expect: raw observations, fitted mean, and a 95% confidence band on the response scale. This example is also a categorical-by-continuous interaction: the temperature slope differs by habitat.
set.seed(2591)
n <- 160
habitat_levels <- c("reef", "kelp")
fish <- data.frame(
temperature = runif(n, -1.6, 1.6),
habitat = factor(
sample(habitat_levels, n, replace = TRUE),
levels = habitat_levels
)
)
eta_mu <- 0.4 +
0.7 * fish$temperature +
0.35 * (fish$habitat == "kelp") +
0.18 * fish$temperature * (fish$habitat == "kelp")
eta_sigma <- -0.55 + 0.35 * fish$temperature
fish$growth <- rnorm(n, mean = eta_mu, sd = exp(eta_sigma))
fit_growth <- drmTMB(
bf(growth ~ temperature * habitat, sigma ~ temperature),
family = gaussian(),
data = fish
)
grid_growth <- expand.grid(
temperature = seq(-1.6, 1.6, length.out = 80),
habitat = levels(fish$habitat)
)
pred_growth <- predict_parameters(
fit_growth,
newdata = grid_growth,
dpar = c("mu", "sigma"),
conf.int = TRUE
)
pred_growth$panel <- factor(
ifelse(
pred_growth$dpar == "mu",
"Expected growth (mu)",
"Residual SD (sigma)"
),
levels = c("Expected growth (mu)", "Residual SD (sigma)")
)
pred_mu <- pred_growth[pred_growth$dpar == "mu", , drop = FALSE]
ggplot(fish, aes(temperature, growth, colour = habitat)) +
geom_point(alpha = 0.35, size = 1.4) +
geom_ribbon(
data = pred_mu,
aes(
x = temperature,
y = estimate,
ymin = conf.low,
ymax = conf.high,
fill = habitat
),
inherit.aes = FALSE,
alpha = 0.16,
colour = NA
) +
geom_line(data = pred_mu, aes(y = estimate), linewidth = 0.8) +
scale_drmtmb_colour +
scale_drmtmb_fill +
labs(
title = "Expected growth along a temperature gradient",
subtitle = "Raw observations, fitted mean, and 95% confidence band",
x = "Temperature anomaly",
y = "Growth",
colour = "Habitat",
fill = "Habitat"
) +
theme_drmtmb_gallery()
Distributional-parameter surfaces
The same fit can show more than the mean.
predict_parameters() returns a long table with one row per
distributional parameter, and plot_parameter_surface()
draws estimate lines plus finite interval bands when they are
present.
plot_parameter_surface(
pred_growth,
x = "temperature",
colour = "habitat",
dpar = c("mu", "sigma"),
facet = "panel"
) +
scale_drmtmb_colour +
scale_drmtmb_fill +
labs(
title = "Location and residual-scale surfaces",
subtitle = "Both panels use the same prediction grid",
x = "Temperature anomaly",
y = "Estimate on response scale",
colour = "Habitat"
) +
theme_drmtmb_gallery()
The mu panel is the expected response. The
sigma panel is the fitted residual standard deviation, so
an upward line means lower predictability or larger residual
heterogeneity at warmer values.
For scale models, the observed-data analogue is not the raw response
itself. Raw growth lives on the location scale. A useful
scale check is the residual magnitude after subtracting the fitted
mu: under a Gaussian model,
E(|residual|) = sigma * sqrt(2 / pi). This keeps the
observed scatter visible without putting raw response points on a
sigma axis.
pred_row_growth <- predict_parameters(
fit_growth,
newdata = fish,
dpar = c("mu", "sigma")
)
row_mu <- pred_row_growth[pred_row_growth$dpar == "mu", c("row", "estimate")]
row_sigma <- pred_row_growth[
pred_row_growth$dpar == "sigma",
c("row", "estimate")
]
names(row_mu)[2] <- "fitted_mu"
names(row_sigma)[2] <- "fitted_sigma"
scale_check <- merge(row_mu, row_sigma, by = "row")
scale_check <- cbind(fish[scale_check$row, ], scale_check)
scale_check$abs_residual <- abs(scale_check$growth - scale_check$fitted_mu)
pred_sigma <- pred_growth[pred_growth$dpar == "sigma", , drop = FALSE]
pred_sigma$expected_abs_residual <- pred_sigma$estimate * sqrt(2 / pi)
pred_sigma$quantity <- "Expected absolute residual"
ggplot(scale_check, aes(temperature, abs_residual, colour = habitat)) +
geom_point(alpha = 0.22, size = 1.2) +
geom_line(
data = pred_sigma,
aes(
y = expected_abs_residual,
colour = quantity,
group = quantity
),
linewidth = 0.9
) +
scale_colour_manual(values = c(
habitat_palette,
"Expected absolute residual" = gallery_colour("green")
)) +
labs(
title = "Residual magnitudes carry the scale information",
subtitle = "The green curve is sigma converted to expected absolute residual size",
x = "Temperature anomaly",
y = "|Observed growth - fitted mu|",
colour = NULL
) +
theme_drmtmb_gallery()
Confidence intervals are only one slice through the uncertainty surface. A confidence distribution is a frequentist, posterior-like curve over parameter values whose quantiles are confidence limits. The code below keeps that estimate-plus-standard-error contract dependency-free, then draws a compact cloud so the no-effect line, estimate, and interval width are visible together. The scale coefficient is shown as a residual-SD ratio: values above one mean that one unit of temperature increases residual scale.
coef_growth <- summary(fit_growth)$coefficients
confidence_curves <- rbind(
make_confidence_distribution(
parameter = "mu temperature slope",
estimate = coef_growth["mu:temperature", "estimate"],
std_error = coef_growth["mu:temperature", "std_error"],
transform = "identity",
null_value = 0
),
make_confidence_distribution(
parameter = "sigma temperature SD ratio",
estimate = coef_growth["sigma:temperature", "estimate"],
std_error = coef_growth["sigma:temperature", "std_error"],
transform = "exp",
null_value = 0
)
)
confidence_curves$parameter <- factor(
confidence_curves$parameter,
levels = c("mu temperature slope", "sigma temperature SD ratio")
)
confidence_curves$density_scaled <- ave(
confidence_curves$density,
confidence_curves$parameter,
FUN = function(x) x / max(x, na.rm = TRUE)
)
confidence_intervals <- rbind(
make_confidence_intervals(
parameter = "mu temperature slope",
estimate = coef_growth["mu:temperature", "estimate"],
std_error = coef_growth["mu:temperature", "std_error"],
transform = "identity"
),
make_confidence_intervals(
parameter = "sigma temperature SD ratio",
estimate = coef_growth["sigma:temperature", "estimate"],
std_error = coef_growth["sigma:temperature", "std_error"],
transform = "exp"
)
)
confidence_intervals$parameter <- factor(
confidence_intervals$parameter,
levels = levels(confidence_curves$parameter)
)
confidence_intervals$level <- factor(
confidence_intervals$level,
levels = c("95%", "66%")
)
confidence_intervals <- confidence_intervals[
order(confidence_intervals$parameter, confidence_intervals$level),
]
confidence_reference <- unique(
confidence_curves[c("parameter", "null_value", "estimate_value")]
)
confidence_reference$null_label <- c("no slope", "no scale change")
confidence_reference$label_x <- confidence_reference$null_value + c(0.03, 0.02)
confidence_palette <- c(
"mu temperature slope" = gallery_colour("blue"),
"sigma temperature SD ratio" = gallery_colour("green")
)
ggplot(confidence_curves, aes(value, density_scaled, fill = parameter)) +
geom_area(alpha = 0.24, colour = NA) +
geom_line(aes(colour = parameter), linewidth = 0.65, alpha = 0.85) +
geom_vline(
data = confidence_reference,
aes(xintercept = null_value),
inherit.aes = FALSE,
linetype = "dashed",
colour = "grey45",
linewidth = 0.45
) +
geom_text(
data = confidence_reference,
aes(x = label_x, y = 0.98, label = null_label),
inherit.aes = FALSE,
colour = "grey35",
size = 3,
hjust = 0,
vjust = 0
) +
geom_segment(
data = confidence_intervals,
aes(
x = xmin,
xend = xmax,
y = 0,
yend = 0,
colour = parameter,
linewidth = level,
alpha = level
),
inherit.aes = FALSE,
lineend = "round"
) +
geom_point(
data = confidence_reference,
aes(x = estimate_value, y = 0, colour = parameter),
inherit.aes = FALSE,
size = 2.4
) +
facet_wrap(~parameter, scales = "free_x") +
scale_colour_manual(values = confidence_palette) +
scale_fill_manual(values = confidence_palette) +
scale_linewidth_manual(values = c("95%" = 1.2, "66%" = 4.8)) +
scale_alpha_manual(values = c("95%" = 0.50, "66%" = 0.85)) +
scale_y_continuous(
breaks = NULL,
limits = c(-0.03, 1.10),
expand = expansion(mult = c(0, 0.02))
) +
labs(
title = "Confidence clouds show the effect away from no change",
subtitle = "Dense colour marks more compatible values; bars show central 66% and 95% confidence intervals",
x = "Effect value",
y = NULL,
colour = "Parameter"
) +
theme_drmtmb_gallery() +
theme(
legend.position = "none",
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank()
)
Distributional parameters are not all residual standard deviations. Shape, inflation, hurdle, and residual-correlation parameters answer different questions and often live on bounded response scales. The next panel set uses three fitted models and the same plotting helper, but each panel names the estimand explicitly.
set.seed(2594)
n_tail <- 2500
tail_dat <- data.frame(tail_pressure = runif(n_tail, -1.5, 1.5))
tail_nu <- 2 + exp(log(4) - tail_dat$tail_pressure)
tail_dat$robust_trait <- 0.35 +
0.45 * tail_dat$tail_pressure +
0.55 * stats::rt(n_tail, df = tail_nu)
fit_tail <- drmTMB(
bf(robust_trait ~ tail_pressure, sigma ~ 1, nu ~ tail_pressure),
family = student(),
data = tail_dat
)
tail_grid <- data.frame(tail_pressure = seq(-1.5, 1.5, length.out = 80))
pred_tail <- predict_parameters(
fit_tail,
newdata = tail_grid,
dpar = "nu",
conf.int = TRUE
)
pred_tail$gradient <- pred_tail$tail_pressure
pred_tail$panel <- "Tail weight (nu)"
pred_tail$model <- "Tail weight"
set.seed(2595)
n_count <- 180
count_dat <- data.frame(detection_difficulty = runif(n_count, -1.2, 1.2))
mu_count <- exp(0.9 + 0.35 * count_dat$detection_difficulty)
zi_count <- plogis(-1.1 + 1.25 * count_dat$detection_difficulty)
count_dat$count <- ifelse(
runif(n_count) < zi_count,
0,
stats::rpois(n_count, mu_count)
)
fit_zi <- drmTMB(
bf(count ~ detection_difficulty, zi ~ detection_difficulty),
family = poisson(link = "log"),
data = count_dat
)
zi_grid <- data.frame(detection_difficulty = seq(-1.2, 1.2, length.out = 80))
pred_zi <- predict_parameters(
fit_zi,
newdata = zi_grid,
dpar = "zi",
conf.int = TRUE
)
pred_zi$gradient <- pred_zi$detection_difficulty
pred_zi$panel <- "Structural zero (zi)"
pred_zi$model <- "Structural-zero probability"
set.seed(2596)
n_pair <- 160
pair_dat <- data.frame(disturbance = runif(n_pair, -1.4, 1.4))
rho_true <- tanh(-0.15 + 0.75 * pair_dat$disturbance)
z1 <- stats::rnorm(n_pair)
z2 <- stats::rnorm(n_pair)
pair_dat$activity <- 0.25 + 0.35 * pair_dat$disturbance + z1
pair_dat$boldness <- -0.1 +
0.2 * pair_dat$disturbance +
rho_true * z1 +
sqrt(1 - rho_true^2) * z2
fit_pair <- drmTMB(
bf(
mu1 = activity ~ disturbance,
mu2 = boldness ~ disturbance,
sigma1 ~ 1,
sigma2 ~ 1,
rho12 ~ disturbance
),
family = c(gaussian(), gaussian()),
data = pair_dat
)
rho_grid <- data.frame(disturbance = seq(-1.4, 1.4, length.out = 80))
pred_rho <- predict_parameters(
fit_pair,
newdata = rho_grid,
dpar = "rho12",
conf.int = TRUE
)
pred_rho$gradient <- pred_rho$disturbance
pred_rho$panel <- "Residual rho12"
pred_rho$model <- "Residual correlation"
pred_extra <- rbind(
pred_tail[intersect(names(pred_tail), names(pred_zi))],
pred_zi[intersect(names(pred_tail), names(pred_zi))],
pred_rho[intersect(names(pred_tail), names(pred_zi))]
)
pred_extra$panel <- factor(
pred_extra$panel,
levels = c(
"Tail weight (nu)",
"Structural zero (zi)",
"Residual rho12"
)
)
plot_parameter_surface(
pred_extra,
x = "gradient",
colour = "model",
facet = "panel",
dpar = c("nu", "zi", "rho12"),
point = FALSE
) +
geom_hline(
data = data.frame(
.drmTMB_plot_facet = factor(
"Residual rho12",
levels = levels(pred_extra$panel)
)
),
aes(yintercept = 0),
inherit.aes = FALSE,
linetype = "dashed",
colour = "grey55"
) +
scale_colour_manual(values = c(
"Tail weight" = "#0072B2",
"Structural-zero probability" = "#009E73",
"Residual correlation" = "#D55E00"
)) +
scale_fill_manual(values = c(
"Tail weight" = "#0072B2",
"Structural-zero probability" = "#009E73",
"Residual correlation" = "#D55E00"
)) +
labs(
title = "Shape, inflation, and residual-correlation panels",
subtitle = "Each panel is a different distributional parameter on its response scale",
x = "Predictor gradient",
y = "Estimate on response scale",
colour = "Parameter",
fill = "Parameter"
) +
theme_drmtmb_gallery() +
theme(legend.position = "none")
The nu panel is a Student-t degrees-of-freedom
parameter: lower values mean heavier tails, and here the fitted model
estimates a changing tail shape through nu ~ tail_pressure.
The zi panel is a structural-zero probability, not a mean
count. The rho12 panel is residual coupling between two
responses after their own means and scales have been modelled.
Random-effect and variance-component figures
Random-effect figures need a different visual grammar from
residual-scale figures. Residual sigma is the
observation-level standard deviation. A group-level SD, such as the SD
of site intercepts or temperature slopes, is among-group variation in a
fitted random effect.
set.seed(2621)
n_site <- 24
n_site_obs <- 6
site_levels <- seq_len(n_site)
site_random <- data.frame(
site = factor(site_levels),
intercept = rnorm(n_site, mean = 0, sd = 0.42),
slope = rnorm(n_site, mean = 0, sd = 0.24)
)
reef_re <- data.frame(
site = factor(rep(site_levels, each = n_site_obs), levels = site_levels)
)
reef_re$temperature <- rep(seq(-1, 1, length.out = n_site_obs), n_site)
site_lookup <- as.integer(reef_re$site)
reef_re$growth <- 0.25 +
0.62 * reef_re$temperature +
site_random$intercept[site_lookup] +
site_random$slope[site_lookup] * reef_re$temperature +
rnorm(nrow(reef_re), mean = 0, sd = 0.34)
fit_re_slopes <- drmTMB(
bf(growth ~ temperature + (1 + temperature | site), sigma ~ 1),
family = gaussian(),
data = reef_re,
control = drm_control(optimizer = list(eval.max = 160L, iter.max = 160L))
)
component_rows <- summary(fit_re_slopes)$parameters
variance_components <- data.frame(
component = c(
"Residual sigma",
"Site intercept SD",
"Site temperature-slope SD"
),
component_type = c(
"Residual scale",
"Group intercept SD",
"Group slope SD"
),
estimate = c(
component_rows["sigma", "estimate"],
component_rows[
"sd:mu:(1 + temperature | site):(Intercept)",
"estimate"
],
component_rows[
"sd:mu:(1 + temperature | site):temperature",
"estimate"
]
)
)
variance_components$component <- factor(
variance_components$component,
levels = rev(variance_components$component)
)
variance_components$component_type <- factor(
variance_components$component_type,
levels = c("Residual scale", "Group intercept SD", "Group slope SD")
)
component_palette <- c(
"Residual scale" = gallery_colour("blue"),
"Group intercept SD" = gallery_colour("green"),
"Group slope SD" = gallery_colour("orange")
)
ggplot(variance_components, aes(estimate, component, colour = component_type)) +
geom_segment(
aes(x = 0, xend = estimate, yend = component),
linewidth = 0.7,
alpha = 0.55
) +
geom_point(size = 2.8) +
scale_colour_manual(values = component_palette) +
labs(
title = "Residual scale and group-level SDs are different quantities",
subtitle = "The slope SD measures among-site variation in temperature responses",
x = "Fitted standard deviation",
y = NULL,
colour = "Component"
) +
theme_drmtmb_gallery() +
guides(colour = "none")
The dot plot compares three standard deviations, but only one is
residual sigma. The other two are random-effect SDs: how
much sites differ in baseline growth, and how much sites differ in the
temperature slope. For intervals on these SDs, use
profile_targets(fit_re_slopes) to choose direct
profile-ready targets rather than treating the quick point plot as an
uncertainty analysis.
Readers used to Bayesian model summaries often expect one display that keeps fixed effects, standard deviations, and correlations together. For this frequentist fit, a direct interval plot is clearer than a cloud: the point is the estimate, the thick bar is the central 66% Wald interval, and the thin bar is the 95% Wald interval.
fixed_rows <- summary(fit_re_slopes)$coefficients
parameter_rows <- summary(fit_re_slopes)$parameters
coefficient_specs <- data.frame(
group = c(
"Fixed effects",
"Fixed effects",
"SD parameters",
"SD parameters",
"SD parameters",
"Correlation"
),
parameter = c(
"mu intercept",
"mu temperature slope",
"residual sigma",
"site intercept SD",
"site temperature-slope SD",
"intercept-slope correlation"
),
estimate = c(
fixed_rows["mu:(Intercept)", "estimate"],
fixed_rows["mu:temperature", "estimate"],
parameter_rows["sigma", "estimate"],
parameter_rows[
"sd:mu:(1 + temperature | site):(Intercept)",
"estimate"
],
parameter_rows[
"sd:mu:(1 + temperature | site):temperature",
"estimate"
],
parameter_rows[
"cor:mu:cor((Intercept),temperature | site)",
"estimate"
]
),
std_error = c(
fixed_rows["mu:(Intercept)", "std_error"],
fixed_rows["mu:temperature", "std_error"],
parameter_rows["sigma", "std_error"],
parameter_rows[
"sd:mu:(1 + temperature | site):(Intercept)",
"std_error"
],
parameter_rows[
"sd:mu:(1 + temperature | site):temperature",
"std_error"
],
parameter_rows[
"cor:mu:cor((Intercept),temperature | site)",
"std_error"
]
),
null_value = c(0, 0, 0, 0, 0, 0)
)
coefficient_levels <- rev(coefficient_specs$parameter)
coefficient_specs$parameter <- factor(
coefficient_specs$parameter,
levels = coefficient_levels
)
coefficient_intervals <- do.call(
rbind,
lapply(seq_len(nrow(coefficient_specs)), function(i) {
row <- coefficient_specs[i, ]
make_confidence_intervals(
parameter = row$parameter,
estimate = row$estimate,
std_error = row$std_error,
transform = "identity"
)
})
)
coefficient_intervals <- merge(
coefficient_intervals,
coefficient_specs[c("parameter", "group")],
by = "parameter"
)
coefficient_intervals$parameter <- factor(
coefficient_intervals$parameter,
levels = coefficient_levels
)
coefficient_intervals$level <- factor(
coefficient_intervals$level,
levels = c("95%", "66%")
)
coefficient_intervals <- coefficient_intervals[
order(coefficient_intervals$parameter, coefficient_intervals$level),
]
interval_group_palette <- c(
"Fixed effects" = gallery_colour("blue"),
"SD parameters" = gallery_colour("green"),
"Correlation" = gallery_colour("orange")
)
ggplot() +
geom_vline(xintercept = 0, linetype = "dashed", colour = "grey55") +
geom_segment(
data = coefficient_intervals[coefficient_intervals$level == "95%", ],
aes(
x = xmin,
xend = xmax,
y = parameter,
yend = parameter,
colour = group
),
linewidth = 1.0,
alpha = 0.45,
lineend = "round"
) +
geom_segment(
data = coefficient_intervals[coefficient_intervals$level == "66%", ],
aes(
x = xmin,
xend = xmax,
y = parameter,
yend = parameter,
colour = group
),
linewidth = 3.4,
alpha = 0.72,
lineend = "round"
) +
geom_point(
data = coefficient_specs,
aes(x = estimate, y = parameter, colour = group),
size = 2.3
) +
scale_colour_manual(values = interval_group_palette) +
scale_y_discrete(drop = FALSE) +
labs(
title = "Coefficient intervals separate fixed effects, SDs, and correlations",
subtitle = "Points are estimates; thick and thin bars show 66% and 95% Wald intervals",
x = "Estimate on displayed scale",
y = NULL,
colour = "Parameter class"
) +
theme_drmtmb_gallery() +
theme(
legend.position = "bottom",
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank()
)
A random-slope figure should show repeated site behaviour. The translucent lines below are conditional site-level trajectories: each site borrows strength from the population fit, but sites can still differ in both intercept and temperature slope.
site_intercept <- ranef(fit_re_slopes, "mu")$terms[[
"(1 + temperature | site):(Intercept)"
]]
site_slope_deviation <- ranef(fit_re_slopes, "mu")$terms[[
"(1 + temperature | site):temperature"
]]
site_effects <- data.frame(
site = names(site_slope_deviation),
intercept_deviation = unname(site_intercept),
slope_deviation = unname(site_slope_deviation)
)
site_effects$slope_direction <- factor(
ifelse(
site_effects$slope_deviation < 0,
"Lower than average",
"Higher than average"
),
levels = c("Lower than average", "Higher than average")
)
fixed_re <- summary(fit_re_slopes)$coefficients
site_lines <- expand.grid(
site = names(site_slope_deviation),
temperature = seq(-1, 1, length.out = 50),
KEEP.OUT.ATTRS = FALSE
)
site_lines <- merge(site_lines, site_effects, by = "site")
site_lines$fitted_growth <- (
fixed_re["mu:(Intercept)", "estimate"] +
site_lines$intercept_deviation
) + (
fixed_re["mu:temperature", "estimate"] +
site_lines$slope_deviation
) * site_lines$temperature
population_line <- data.frame(
temperature = seq(-1, 1, length.out = 80)
)
population_line$fitted_growth <- fixed_re["mu:(Intercept)", "estimate"] +
fixed_re["mu:temperature", "estimate"] * population_line$temperature
ggplot() +
geom_point(
data = reef_re,
aes(temperature, growth),
colour = "grey55",
alpha = 0.38,
size = 1.15
) +
geom_line(
data = site_lines,
aes(
temperature,
fitted_growth,
group = site,
colour = slope_direction
),
alpha = 0.48,
linewidth = 0.55
) +
geom_line(
data = population_line,
aes(temperature, fitted_growth),
colour = gallery_colour("charcoal"),
linewidth = 1.05
) +
scale_colour_manual(values = c(
"Lower than average" = gallery_colour("blue"),
"Higher than average" = gallery_colour("orange")
)) +
labs(
title = "Translucent site trajectories show random slopes",
subtitle = "Grey points are repeated site observations; the dark line is the population slope",
x = "Temperature anomaly",
y = "Growth",
colour = "Site slope"
) +
theme_drmtmb_gallery()
A direct sd(site) ~ reef_cover formula asks a third
question: does a site-level predictor change the among-site SD in
expected growth? The prediction table keeps this target as
sd(site), not residual sigma.
set.seed(2622)
n_sd_site <- 28
n_sd_each <- 5
site_sd_data <- data.frame(
site = factor(seq_len(n_sd_site)),
reef_cover = runif(n_sd_site, -1.2, 1.2)
)
site_sd <- exp(-0.6 + 0.45 * site_sd_data$reef_cover)
site_deviation <- rnorm(n_sd_site, mean = 0, sd = site_sd)
reef_sd <- site_sd_data[rep(seq_len(n_sd_site), each = n_sd_each), ]
reef_sd$temperature <- rep(seq(-1.2, 1.2, length.out = n_sd_each), n_sd_site)
reef_sd$growth <- 0.4 +
0.5 * reef_sd$temperature +
site_deviation[as.integer(reef_sd$site)] +
rnorm(nrow(reef_sd), mean = 0, sd = 0.35)
fit_site_sd <- drmTMB(
bf(growth ~ temperature + (1 | site), sigma ~ 1, sd(site) ~ reef_cover),
family = gaussian(),
data = reef_sd,
control = drm_control(optimizer = list(eval.max = 180L, iter.max = 180L))
)
grid_site_sd <- prediction_grid(
fit_site_sd,
focal = "reef_cover",
at = list(reef_cover = seq(-1.2, 1.2, length.out = 80))
)
pred_site_sd <- predict_parameters(
fit_site_sd,
newdata = grid_site_sd,
dpar = "sd(site)",
conf.int = TRUE
)
pred_site_sd$quantity <- "Among-site SD"
plot_parameter_surface(
pred_site_sd,
x = "reef_cover",
colour = "quantity",
facet = NULL,
point = FALSE
) +
scale_colour_manual(values = c(
"Among-site SD" = gallery_colour("green")
)) +
labs(
title = "Among-site SD surface",
subtitle = "Requested Wald intervals remain unavailable for direct random-effect SD surfaces",
x = "Reef cover",
y = "Among-site SD in expected growth",
colour = NULL
) +
theme_drmtmb_gallery() +
guides(colour = "none")
This line is a fitted random-effect SD surface. It should not be read
as a raw growth curve, and it should not be overlaid with raw
growth points. The table behind the plot reports
conf.status = "wald_unavailable", which is the honest
interval boundary until a profile or bootstrap route is attached to
these derived surfaces.
Discrete comparisons
For a factor predictor, the same plotting helper uses interval bars rather than ribbons. This is useful for compact tutorial figures and estimated marginal comparisons.
habitat_grid <- data.frame(
temperature = 0,
habitat = factor(c("reef", "kelp"), levels = levels(fish$habitat))
)
pred_habitat <- predict_parameters(
fit_growth,
newdata = habitat_grid,
dpar = "mu",
conf.int = TRUE
)
plot_parameter_surface(
pred_habitat,
x = "habitat",
colour = "habitat",
line = FALSE,
point = TRUE
) +
scale_drmtmb_colour +
labs(
title = "Estimated habitat contrast at average temperature",
subtitle = "Intervals are Wald intervals from predict_parameters()",
x = NULL,
y = "Expected growth",
colour = "Habitat"
) +
theme_drmtmb_gallery()
Categorical by categorical interaction
For two categorical predictors, show every fitted cell and its interval. Use position dodging so the uncertainty intervals do not sit on top of each other. Raw observations can sit behind the intervals when they help readers see the data support for each cell.
set.seed(2592)
n_cell <- 30
reef <- expand.grid(
habitat = factor(habitat_levels, levels = habitat_levels),
season = factor(c("dry", "wet"), levels = c("dry", "wet")),
replicate = seq_len(n_cell)
)
reef$mu <- with(
reef,
0.6 +
0.35 * (habitat == "kelp") -
0.15 * (season == "wet") +
0.45 * (habitat == "kelp") * (season == "wet")
)
reef$growth <- rnorm(nrow(reef), mean = reef$mu, sd = 0.45)
fit_cat_cat <- drmTMB(
bf(growth ~ habitat * season, sigma ~ 1),
family = gaussian(),
data = reef
)
grid_cat_cat <- expand.grid(
habitat = levels(reef$habitat),
season = levels(reef$season)
)
pred_cat_cat <- predict_parameters(
fit_cat_cat,
newdata = grid_cat_cat,
dpar = "mu",
conf.int = TRUE
)
raw_cat_cat <- reef
ggplot(pred_cat_cat, aes(habitat, estimate, colour = season)) +
geom_point(
data = raw_cat_cat,
aes(y = growth, colour = season),
position = position_jitterdodge(
jitter.width = 0.08,
jitter.height = 0,
dodge.width = 0.45
),
alpha = 0.22,
size = 1.1,
show.legend = FALSE
) +
geom_pointrange(
aes(ymin = conf.low, ymax = conf.high),
position = position_dodge(width = 0.45),
linewidth = 0.65,
fatten = 2.3
) +
scale_colour_manual(values = season_palette) +
labs(
title = "Categorical by categorical interaction",
subtitle = "Small points are observations; intervals are fitted cell means",
x = "Habitat",
y = "Growth",
colour = "Season"
) +
theme_drmtmb_gallery()
#> Warning: The `fatten` argument of `geom_pointrange()` is deprecated as of ggplot2 4.0.0.
#> ℹ Please use the `size` aesthetic instead.
#> This warning is displayed once per session.
#> Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
#> generated.
Estimated marginal means and marginal summaries
For the first supported emmeans path, a fixed-effect
univariate mu model can produce estimated marginal means
that are easy to turn into publication-style figures. The same rule
applies: keep the target and conditioning values visible. These displays
are mu summaries, not sigma, fitted-response,
random-effect, or bivariate summaries.
emm_habitat <- suppressMessages(
as.data.frame(
emmeans::emmeans(
fit_growth,
~ habitat,
at = list(temperature = 0),
type = "response"
)
)
)
ggplot(emm_habitat, aes(habitat, emmean, colour = habitat)) +
geom_pointrange(
aes(ymin = asymp.LCL, ymax = asymp.UCL),
linewidth = 0.7,
fatten = 2.4
) +
scale_drmtmb_colour +
labs(
title = "Estimated marginal mean growth",
subtitle = "Supported fixed-effect mu target, conditioned at temperature = 0",
x = NULL,
y = "Expected growth",
colour = "Habitat"
) +
theme_drmtmb_gallery()
emmeans reference grids can also condition on a factor.
In the next display, the mu cell means are grouped by
season, so the reader sees the fitted habitat contrast inside each
season rather than a single averaged contrast.
emm_season <- suppressMessages(
as.data.frame(
emmeans::emmeans(
fit_cat_cat,
~ habitat | season,
type = "response"
)
)
)
ggplot(emm_season, aes(habitat, emmean, colour = season)) +
geom_pointrange(
aes(ymin = asymp.LCL, ymax = asymp.UCL),
position = position_dodge(width = 0.35),
linewidth = 0.65,
fatten = 2.3
) +
scale_colour_manual(values = season_palette) +
labs(
title = "Factor-conditioned estimated marginal means",
subtitle = "Habitat mu summaries are shown separately for each season",
x = "Habitat",
y = "Expected growth",
colour = "Season"
) +
theme_drmtmb_gallery()
For a numeric interaction, use an explicit reference grid. Here
temperature is held at three named values so the habitat
contrast does not hide the conditioning rule.
emm_temperature <- suppressMessages(
as.data.frame(
emmeans::emmeans(
fit_growth,
~ habitat | temperature,
at = list(temperature = c(-1, 0, 1)),
type = "response"
)
)
)
emm_temperature$temperature_slice <- factor(
emm_temperature$temperature,
levels = c(-1, 0, 1),
labels = c("cooler", "average", "warmer")
)
ggplot(emm_temperature, aes(temperature_slice, emmean, colour = habitat)) +
geom_pointrange(
aes(ymin = asymp.LCL, ymax = asymp.UCL),
position = position_dodge(width = 0.42),
linewidth = 0.65,
fatten = 2.3
) +
scale_drmtmb_colour +
labs(
title = "Estimated marginal means across an interaction grid",
subtitle = "The same fixed-effect mu bridge, evaluated at named temperature slices",
x = "Temperature slice",
y = "Expected growth",
colour = "Habitat"
) +
theme_drmtmb_gallery()
Empirical marginal summaries use a different rule.
prediction_grid(..., margin = "empirical") crosses a focal
value with fitted rows, and marginal_parameters() averages
the resulting parameter predictions. The helper itself is still a
plug-in point summary, so the interval bars below are a display
approximation: they average row-wise Wald prediction limits from
predict_parameters(conf.int = TRUE) within each
habitat.
empirical_habitat_grid <- prediction_grid(
fit_growth,
focal = "habitat",
at = list(habitat = levels(fish$habitat)),
margin = "empirical"
)
empirical_habitat <- marginal_parameters(
fit_growth,
newdata = empirical_habitat_grid,
dpar = "mu",
by = "habitat"
)
empirical_habitat_rowwise <- predict_parameters(
fit_growth,
newdata = empirical_habitat_grid,
dpar = "mu",
conf.int = TRUE
)
empirical_habitat_wald <- aggregate(
cbind(conf.low, conf.high) ~ habitat,
data = empirical_habitat_rowwise,
FUN = mean
)
empirical_habitat_plot <- empirical_habitat
empirical_habitat_plot$conf.low <- empirical_habitat_wald$conf.low[
match(empirical_habitat_plot$habitat, empirical_habitat_wald$habitat)
]
empirical_habitat_plot$conf.high <- empirical_habitat_wald$conf.high[
match(empirical_habitat_plot$habitat, empirical_habitat_wald$habitat)
]
empirical_habitat_plot$conf.status <- "wald"
empirical_habitat_plot$interval_source <- "averaged_rowwise_wald"
plot_parameter_surface(
empirical_habitat_plot,
x = "habitat",
colour = "habitat",
facet = NULL,
line = FALSE,
point = TRUE
) +
scale_drmtmb_colour +
labs(
title = "Empirical marginal mu summary",
subtitle = "Bars average row-wise Wald prediction limits over fitted rows",
x = NULL,
y = "Average expected growth",
colour = "Habitat",
caption = "marginal_parameters() returns plug-in point averages; bars are a display approximation."
) +
theme_drmtmb_gallery()
Unsupported targets should be visible before a reader tries to plot
them. The current emmeans bridge is for fixed-effect
univariate mu; other targets should use parameter
prediction, corpairs(), profile intervals, or a future
feature-specific design.
emmeans_boundary <- data.frame(
target = c(
"Fixed-effect univariate mu",
"sigma",
"Bivariate responses",
"Zero-inflated or hurdle means",
"Ordinal expected scores",
"Random-effect targets"
),
status = c(
"Supported emmeans route",
"Unsupported boundary",
"Unsupported boundary",
"Unsupported boundary",
"Unsupported boundary",
"Unsupported boundary"
)
)
emmeans_boundary$target <- factor(
emmeans_boundary$target,
levels = rev(emmeans_boundary$target)
)
emmeans_boundary$status <- factor(
emmeans_boundary$status,
levels = c("Supported emmeans route", "Unsupported boundary")
)
emmeans_boundary$status_label <- ifelse(
emmeans_boundary$status == "Supported emmeans route",
"supported\nmu EMM",
"unsupported\nboundary"
)
emmeans_boundary$status_text_colour <- ifelse(
emmeans_boundary$status == "Supported emmeans route",
"white",
"grey10"
)
ggplot(emmeans_boundary, aes(status, target, fill = status)) +
geom_tile(width = 0.84, height = 0.72, colour = "white", linewidth = 0.6) +
geom_text(
aes(label = status_label, colour = status_text_colour),
size = 3.1,
lineheight = 0.9
) +
scale_fill_manual(values = c(
"Supported emmeans route" = "#0072B2",
"Unsupported boundary" = "#E69F00"
)) +
scale_colour_identity() +
labs(
title = "Current emmeans support boundary",
subtitle = "Only fixed-effect univariate mu is advertised as an emmeans target",
x = "Current status",
y = NULL,
fill = "Status"
) +
theme_drmtmb_gallery() +
theme(
panel.grid = element_blank(),
legend.position = "none"
)
Continuous by continuous interaction
For two continuous predictors, choose scientifically meaningful slices of one predictor and show fitted slopes across the other. The values should be named in the legend so the reader can interpret the conditioning.
set.seed(2593)
n_cc <- 180
soil <- data.frame(
temperature = runif(n_cc, -1.5, 1.5),
moisture = runif(n_cc, -1.4, 1.4)
)
soil$growth <- rnorm(
n_cc,
mean = 0.3 +
0.55 * soil$temperature +
0.45 * soil$moisture -
0.35 * soil$temperature * soil$moisture,
sd = 0.45
)
fit_cont_cont <- drmTMB(
bf(growth ~ temperature * moisture, sigma ~ 1),
family = gaussian(),
data = soil
)
grid_cont_cont <- expand.grid(
temperature = seq(-1.5, 1.5, length.out = 80),
moisture = c(-1, 0, 1)
)
grid_cont_cont$moisture_level <- factor(
grid_cont_cont$moisture,
levels = c(-1, 0, 1),
labels = c("low moisture", "average moisture", "high moisture")
)
pred_cont_cont <- predict_parameters(
fit_cont_cont,
newdata = grid_cont_cont,
dpar = "mu",
conf.int = TRUE
)
ggplot(
pred_cont_cont,
aes(temperature, estimate, colour = moisture_level, fill = moisture_level)
) +
geom_point(
data = soil,
aes(temperature, growth),
inherit.aes = FALSE,
alpha = 0.18,
size = 1,
colour = "grey35"
) +
geom_ribbon(aes(ymin = conf.low, ymax = conf.high), alpha = 0.14, colour = NA) +
geom_line(linewidth = 0.8) +
scale_colour_manual(values = c(
"low moisture" = gallery_colour("blue"),
"average moisture" = gallery_colour("green"),
"high moisture" = gallery_colour("orange")
)) +
scale_fill_manual(values = c(
"low moisture" = gallery_colour("blue"),
"average moisture" = gallery_colour("green"),
"high moisture" = gallery_colour("orange")
)) +
labs(
title = "Continuous by continuous interaction",
subtitle = "Moisture is held at three named slices",
x = "Temperature anomaly",
y = "Growth",
colour = "Moisture slice",
fill = "Moisture slice"
) +
theme_drmtmb_gallery()
The raw point cloud is not coloured by the sliced predictor because the fitted lines are conditional displays, not clusters in the data. This keeps the visual message honest: the model is being read at three moisture values, while the observations span the whole moisture gradient.
Fitted correlation summaries
Correlation displays should keep layers separate. Residual
rho12, ordinary-group correlations, phylogenetic
correlations, spatial correlations, and future animal or
relmat() layers are not the same parameter even when all
are correlations. A fitted workflow should start from
corpairs(fit); this gallery uses a compact compatible table
so the page builds quickly while preserving the same required
columns.
pair_table <- data.frame(
level = c("residual", "group", "phylogenetic"),
class = c("residual", "mean-slope", "mean-mean"),
parameter = c(
"rho12",
"cor((Intercept),temperature | site)",
"cor(mu1,mu2 | phylo | species)"
),
estimate = c(-0.32, 0.44, 0.61),
modelled = c(TRUE, FALSE, FALSE),
conf.low = c(-0.54, 0.12, 0.29),
conf.high = c(-0.05, 0.68, 0.79),
conf.status = c("wald", "profile", "profile"),
layer = c("Residual coscale", "Ordinary group", "Phylogenetic")
)
pair_table$layer <- factor(
pair_table$layer,
levels = c("Residual coscale", "Ordinary group", "Phylogenetic")
)
pair_table$display <- factor(
c(
"Residual\nrho12",
"Group\nintercept-slope",
"Phylogenetic\nlocation-location"
),
levels = c(
"Residual\nrho12",
"Group\nintercept-slope",
"Phylogenetic\nlocation-location"
)
)
correlation_palette <- c(
"Residual coscale" = "#0072B2",
"Ordinary group" = "#009E73",
"Phylogenetic" = "#D55E00"
)
plot_corpairs(pair_table, colour = "layer", facet = "layer", label = "display") +
scale_colour_manual(values = correlation_palette) +
labs(
title = "Implemented correlation rows stay in their own layers",
subtitle = "Intervals are drawn only when the pair table has finite bounds",
colour = "Layer"
) +
theme_drmtmb_gallery() +
theme(
axis.text.y = element_text(size = 10, lineheight = 0.95),
legend.position = "none"
)
The residual row is the fitted within-observation coscale
rho12. The group row is a latent random-effect correlation,
here an intercept-slope row from an ordinary grouped model. The
phylogenetic row is a species-level structured location-location
correlation. These rows can live in the same corpairs()
table, but the figure keeps them apart because they answer different
biological questions.
layer_boundary <- data.frame(
layer = c(
"Residual rho12",
"Ordinary group",
"Phylogenetic",
"Spatial",
"Animal",
"relmat"
),
status = c(
"Fitted corpairs row",
"Fitted corpairs row",
"Fitted corpairs row",
"Planned boundary",
"Planned boundary",
"Planned boundary"
)
)
layer_boundary$layer <- factor(
layer_boundary$layer,
levels = rev(layer_boundary$layer)
)
layer_boundary$status <- factor(
layer_boundary$status,
levels = c("Fitted corpairs row", "Planned boundary")
)
layer_boundary$status_label <- ifelse(
layer_boundary$status == "Fitted corpairs row",
"fitted\nrow",
"planned\nboundary"
)
layer_boundary$status_text_colour <- ifelse(
layer_boundary$status == "Fitted corpairs row",
"white",
"grey10"
)
ggplot(layer_boundary, aes(status, layer, fill = status)) +
geom_tile(width = 0.82, height = 0.72, colour = "white", linewidth = 0.6) +
geom_text(
aes(label = status_label, colour = status_text_colour),
size = 3.1,
lineheight = 0.9
) +
scale_fill_manual(values = c(
"Fitted corpairs row" = "#0072B2",
"Planned boundary" = "#E69F00"
)) +
scale_colour_identity() +
labs(
title = "Correlation-layer support boundary",
subtitle = "Spatial, animal, and relmat rows are reserved until fitted correlation-pair evidence exists",
x = "Current status",
y = NULL,
fill = "Status"
) +
theme_drmtmb_gallery() +
theme(
panel.grid = element_blank(),
legend.position = "none"
)
The status strip is not an estimate plot. Coordinate-spatial Gaussian
mu slopes are fitted elsewhere, but spatial
corpairs() rows remain planned. animal() and
relmat() are also reserved structured-effect markers until
the likelihood, diagnostics, profile-target, recovery-test, and example
evidence exist.
Simulation operating characteristics
Simulation figures answer different questions from model-interpretation figures. They should show bias, error, coverage, runtime, and failures beside the model surface being tested.
sim_summary <- data.frame(
surface = rep(
c("Gaussian location-scale", "Proportion", "Count"),
each = 4
),
estimand = rep(c("beta_x", "sigma", "sd_intercept", "rho12"), 3),
bias = c(
0.01, -0.02, 0.04, 0.00,
-0.01, 0.03, NA, NA,
0.02, -0.01, -0.04, NA
),
rmse = c(
0.07, 0.09, 0.13, 0.11,
0.08, 0.10, NA, NA,
0.10, 0.09, 0.16, NA
),
coverage = c(
0.95, 0.94, 0.91, 0.93,
0.94, 0.92, NA, NA,
0.93, 0.95, 0.89, NA
)
)
sim_summary$n_rep <- 500L
sim_summary$bias_mcse <- sim_summary$rmse / sqrt(sim_summary$n_rep)
sim_summary$bias_low <- sim_summary$bias - 1.96 * sim_summary$bias_mcse
sim_summary$bias_high <- sim_summary$bias + 1.96 * sim_summary$bias_mcse
sim_summary$coverage_mcse <- sqrt(
sim_summary$coverage * (1 - sim_summary$coverage) / sim_summary$n_rep
)
sim_summary$coverage_low <- pmax(
0,
sim_summary$coverage - 1.96 * sim_summary$coverage_mcse
)
sim_summary$coverage_high <- pmin(
1,
sim_summary$coverage + 1.96 * sim_summary$coverage_mcse
)
sim_summary$surface <- factor(
sim_summary$surface,
levels = names(surface_palette)
)
estimand_labels <- c(
"beta_x" = "beta_x fixed slope",
"sigma" = "sigma residual SD",
"sd_intercept" = "sd_intercept group SD",
"rho12" = "rho12 residual correlation"
)
sim_summary$estimand_label <- factor(
estimand_labels[sim_summary$estimand],
levels = rev(unname(estimand_labels))
)
display_replicates <- 45
sim_rain <- do.call(
rbind,
lapply(seq_len(nrow(sim_summary)), function(i) {
row <- sim_summary[i, ]
if (is.na(row$bias)) {
return(NULL)
}
data.frame(
surface = row$surface,
estimand_label = row$estimand_label,
bias_replicate = row$bias +
qnorm(ppoints(display_replicates)) * row$rmse
)
})
)
sim_rain$surface <- factor(sim_rain$surface, levels = levels(sim_summary$surface))
sim_rain$estimand_label <- factor(
sim_rain$estimand_label,
levels = levels(sim_summary$estimand_label)
)
bias_position <- position_dodge(width = 0.65)
ggplot() +
geom_hline(yintercept = 0, linetype = "dashed", colour = "grey60") +
geom_violin(
data = sim_rain,
aes(
estimand_label,
bias_replicate,
group = interaction(estimand_label, surface),
fill = surface
),
position = bias_position,
width = 0.62,
alpha = 0.16,
colour = NA,
scale = "width",
trim = FALSE
) +
geom_point(
data = sim_rain,
aes(estimand_label, bias_replicate, colour = surface),
position = position_jitterdodge(jitter.width = 0.08, dodge.width = 0.65),
alpha = 0.22,
size = 0.75,
show.legend = FALSE
) +
geom_linerange(
data = sim_summary,
aes(
estimand_label,
ymin = bias_low,
ymax = bias_high,
colour = surface
),
position = bias_position,
linewidth = 0.95,
na.rm = TRUE
) +
geom_point(
data = sim_summary,
aes(estimand_label, bias, colour = surface),
position = bias_position,
size = 2.6,
na.rm = TRUE
) +
coord_flip() +
scale_colour_manual(values = surface_palette, drop = FALSE) +
scale_fill_manual(values = surface_palette, drop = FALSE) +
labs(
title = "Bias by estimand",
subtitle = "Rainclouds show replicate errors; large points are mean bias with 95% MCSE intervals",
x = NULL,
y = "Estimate minus truth",
colour = "Surface"
) +
guides(fill = "none") +
theme_drmtmb_gallery()
coverage_position <- position_dodge2(width = 0.78, preserve = "single")
ggplot(sim_summary, aes(estimand_label, coverage, fill = surface)) +
geom_hline(yintercept = 0.95, linetype = "dashed", colour = "grey60") +
geom_col(
position = coverage_position,
width = 0.65,
colour = "white",
linewidth = 0.25,
na.rm = TRUE
) +
geom_errorbar(
aes(ymin = coverage_low, ymax = coverage_high, colour = surface),
position = coverage_position,
width = 0.22,
linewidth = 0.45,
na.rm = TRUE
) +
coord_flip(ylim = c(0.75, 1)) +
scale_fill_manual(values = surface_palette, drop = FALSE) +
scale_colour_manual(values = surface_palette, drop = FALSE, guide = "none") +
labs(
title = "Interval coverage",
subtitle = "Bars are means; whiskers are approximate 95% binomial MCSE intervals",
x = NULL,
y = "Empirical coverage",
fill = "Surface"
) +
theme_drmtmb_gallery()
These figures use a small illustrative table with the same columns
that simulation summaries should carry. The bias panel shows the
intended raincloud-style grammar: real reports should draw the
replicate-level estimates, then overplot the mean and MCSE interval for
each defined surface-estimand cell. The panel still leaves estimands
disconnected because beta_x, sigma,
sd_intercept, and rho12 are categorical
targets, not steps along one trajectory. The comprehensive simulation
programme should not be count-only. It should add continuous,
meta-analysis, proportion, count, ordinal, bivariate, random-slope, and
structured-dependence surfaces as each surface becomes ready. Real
simulation reports should also show the run manifest and warning/error
ledger so missing or failed fits are not hidden behind clean-looking
panels.
Gallery source map
The gallery is easier to audit when every figure names its source object, extractor, interval source, and support boundary. Use this table as a quick check before adapting a plot to a manuscript, tutorial, or simulation report.
gallery_source_map <- data.frame(
figure = c(
"Mean with confidence band",
"mu and sigma surfaces",
"nu, zi, and rho12 panels",
"Variance-component dot plot",
"Conditional random-slope deviations",
"sd(site) surface",
"Discrete habitat comparison",
"Categorical interaction",
"Simple emmeans display",
"Factor-conditioned emmeans grid",
"Interaction emmeans grid",
"Empirical marginal mu summary",
"emmeans support boundary",
"Continuous interaction",
"Correlation estimate rows",
"Correlation support boundary",
"Simulation bias and coverage"
),
source = c(
"fit_growth",
"fit_growth",
"fit_tail, fit_zi, fit_pair",
"fit_re_slopes",
"fit_re_slopes",
"fit_site_sd",
"fit_growth",
"fit_cat_cat",
"fit_growth",
"fit_cat_cat",
"fit_growth",
"fit_growth",
"emmeans_boundary",
"fit_cont_cont",
"pair_table",
"layer_boundary",
"sim_summary"
),
extractor_or_plotter = c(
"predict_parameters(dpar = \"mu\") + ggplot2",
"predict_parameters() + plot_parameter_surface()",
"predict_parameters() + plot_parameter_surface()",
"summary(fit)$parameters + ggplot2",
"ranef() + ggplot2",
"prediction_grid() + predict_parameters() + plot_parameter_surface()",
"predict_parameters(dpar = \"mu\") + plot_parameter_surface()",
"predict_parameters(dpar = \"mu\") + ggplot2",
"emmeans::emmeans(type = \"response\") + ggplot2",
"emmeans::emmeans(~ habitat | season) + ggplot2",
"emmeans::emmeans(~ habitat | temperature) + ggplot2",
"prediction_grid(margin = \"empirical\") + marginal_parameters() + predict_parameters()",
"ggplot2 status strip",
"predict_parameters(dpar = \"mu\") + ggplot2",
"plot_corpairs()",
"ggplot2 status strip",
"ggplot2 fixture display"
),
interval_source = c(
"Wald intervals from predict_parameters()",
"Wald intervals when finite",
"Wald intervals from each fitted parameter table",
"Point estimates only",
"Conditional modes only",
"conf.status = \"wald_unavailable\"",
"Wald intervals from predict_parameters()",
"Wald intervals from predict_parameters()",
"asymptotic emmeans intervals",
"asymptotic emmeans intervals",
"asymptotic emmeans intervals",
"Display approximation from averaged row-wise Wald limits",
"No intervals; status only",
"Wald intervals from predict_parameters()",
"Finite bounds supplied in compatible pair table",
"No intervals; status only",
"Illustrative summaries, not final evidence"
),
support_boundary = c(
"fixed-effect univariate mu",
"fixed-effect mu and sigma",
"fixed-effect shape, inflation, and residual rho12",
"ordinary Gaussian random-slope fit",
"shrunken deviations, not variance intervals",
"direct random-effect SD surface without Wald bands",
"fixed-effect univariate mu",
"fixed-effect univariate mu",
"fixed-effect univariate mu only",
"fixed-effect univariate mu only",
"fixed-effect univariate mu only",
"empirical marginalisation, not emmeans",
"unsupported targets remain unsupported",
"fixed-effect univariate mu",
"implemented residual, group, and phylogenetic rows",
"spatial, animal, and relmat rows remain planned",
"display grammar for later result articles"
),
stringsAsFactors = FALSE
)
knitr::kable(gallery_source_map)| figure | source | extractor_or_plotter | interval_source | support_boundary |
|---|---|---|---|---|
| Mean with confidence band | fit_growth | predict_parameters(dpar = “mu”) + ggplot2 | Wald intervals from predict_parameters() | fixed-effect univariate mu |
| mu and sigma surfaces | fit_growth | predict_parameters() + plot_parameter_surface() | Wald intervals when finite | fixed-effect mu and sigma |
| nu, zi, and rho12 panels | fit_tail, fit_zi, fit_pair | predict_parameters() + plot_parameter_surface() | Wald intervals from each fitted parameter table | fixed-effect shape, inflation, and residual rho12 |
| Variance-component dot plot | fit_re_slopes | summary(fit)$parameters + ggplot2 | Point estimates only | ordinary Gaussian random-slope fit |
| Conditional random-slope deviations | fit_re_slopes | ranef() + ggplot2 | Conditional modes only | shrunken deviations, not variance intervals |
| sd(site) surface | fit_site_sd | prediction_grid() + predict_parameters() + plot_parameter_surface() | conf.status = “wald_unavailable” | direct random-effect SD surface without Wald bands |
| Discrete habitat comparison | fit_growth | predict_parameters(dpar = “mu”) + plot_parameter_surface() | Wald intervals from predict_parameters() | fixed-effect univariate mu |
| Categorical interaction | fit_cat_cat | predict_parameters(dpar = “mu”) + ggplot2 | Wald intervals from predict_parameters() | fixed-effect univariate mu |
| Simple emmeans display | fit_growth | emmeans::emmeans(type = “response”) + ggplot2 | asymptotic emmeans intervals | fixed-effect univariate mu only |
| Factor-conditioned emmeans grid | fit_cat_cat | emmeans::emmeans(~ habitat | season) + ggplot2 | asymptotic emmeans intervals | fixed-effect univariate mu only |
| Interaction emmeans grid | fit_growth | emmeans::emmeans(~ habitat | temperature) + ggplot2 | asymptotic emmeans intervals | fixed-effect univariate mu only |
| Empirical marginal mu summary | fit_growth | prediction_grid(margin = “empirical”) + marginal_parameters() + predict_parameters() | Display approximation from averaged row-wise Wald limits | empirical marginalisation, not emmeans |
| emmeans support boundary | emmeans_boundary | ggplot2 status strip | No intervals; status only | unsupported targets remain unsupported |
| Continuous interaction | fit_cont_cont | predict_parameters(dpar = “mu”) + ggplot2 | Wald intervals from predict_parameters() | fixed-effect univariate mu |
| Correlation estimate rows | pair_table | plot_corpairs() | Finite bounds supplied in compatible pair table | implemented residual, group, and phylogenetic rows |
| Correlation support boundary | layer_boundary | ggplot2 status strip | No intervals; status only | spatial, animal, and relmat rows remain planned |
| Simulation bias and coverage | sim_summary | ggplot2 fixture display | Illustrative summaries, not final evidence | display grammar for later result articles |
Future simulation result articles
Specialised simulation result articles should come later, after the
comprehensive simulation surfaces are ready. Those articles can be
organised by scientific question: power, bias, coverage, runtime,
convergence, and failure patterns. They should also be organised across
data types, not only counts: continuous location-scale models,
meta-analysis with known V, proportions, counts, ordinal
responses, bivariate rho12, random-slope surfaces, and
structured dependence.
This figure gallery is broader and lighter. It shows the visual language users should see across tutorials, examples, model interpretation, and later simulation reports.