Parrot vocal repertoire evolution

Statistical analysis SEM on imputed data

Author
Published

June 9, 2026

Code
# print link to github repo if any
if (file.exists("./.git/config")){
  config <- readLines("./.git/config")
  url <- grep("url",  config, value = TRUE)
  url <- gsub("\\turl = |.git$", "", url)
  cat("\nSource code and data found at [", url, "](", url, ")", sep = "")
  }

Source code and data found at

Overview

This document fits a phylogenetic Structural Equation Model (SEM) to investigate the drivers of vocal repertoire size in parrots. The model accounts for shared evolutionary history via a phylogenetic covariance matrix and handles missing trait data through multiple imputation. Three sub-models are linked: body mass predicts longevity and brain size, which in turn — alongside gregariousness, flock size, and coloration — predict repertoire size.


Load Packages

Code
# knitr is require for creating html/pdf/word reports
# formatR is used for soft-wrapping code

# install knitr package if not installed
if (!requireNamespace("sketchy", quietly = TRUE)) {
  install.packages("sketchy")
}

packages <- c(
 "tidyverse",
 "reshape2",
 "ape",
 "phytools",
 "mice",
 "brms",
 "brmsish",
 "cmdstanr",
 "ggplot2",
 "patchwork",
 "dagitty",
 "ggdag",
 "posterior",
 "qs2",
 "pbapply",
 "scico",
 "ggridges"
 )

# install/ load packages
sketchy::load_packages(packages = packages)


options(knitr.kable.NA = '')

print <- function(x, row.names = FALSE) {
    kb <- kable(x, row.names = row.names, digits = 4, "html")
    kb <- kable_styling(kb,
                        bootstrap_options = c("striped", "hover", "condensed", "responsive"))
    scroll_box(kb, width = "100%")
}

0.1 Data Import and Visualisation

The main trait dataset contains species-level data on repertoire size, brain size, flock size, gregariousness, longevity, and body mass. A separate colour dataset (Carballo et al. 2020) provides chromatic and achromatic colour scores that are matched to the trait data by species name. Species with no repertoire data are excluded, as is one extreme outlier value for Melopsittacus undulatus that is almost an order of magnitude larger than the next largest value for that species.

Code
# main trait dataset
traits <- read.csv("./data/raw/06052026ALLTRAITS.csv")

# rename columns for clarity
traits <- traits |>
  rename(phylo = "gs", repertoire = "sp", flock = "log.maxflock") |>
  mutate(species = phylo)

traits <- traits |> select(phylo, species,
                           repertoire,
                           brain, flock, greg, longevity, mass)

# remove species with no repertoire data
traits <- traits |> filter(!is.na(repertoire))

# colour dataset
parrot_col <- read.csv("./data/raw/carballo_2020_colour_avg.csv")

# capitalise species names for matching
parrot_col$species <- stringr::str_to_title(parrot_col$scinam)

# match colour scores to trait data
traits$cds_avg <- parrot_col$cds_avg[match(traits$phylo, parrot_col$species)]
traits$ces_avg <- parrot_col$ces_avg[match(traits$phylo, parrot_col$species)]

# remove the one outlier M. undulatus repertoire value
traits <- traits |> filter(!(phylo == "Melopsittacus_undulatus" & repertoire == 1050))

A quick look at the raw distributions of all traits before any transformation:

Code
reshape2::melt(traits) |>
  ggplot(aes(x = value)) +
  facet_wrap(~variable, scales = "free") +
  geom_histogram() +
  theme_bw()

Raw distributions of all trait variables prior to transformation.

0.2 DAG

Code
library(dagitty)
library(ggdag)
library(ggplot2)
library(viridis)
library(scico)

# DAG corresponding to the SEM
dag_repertoire <- dagify(

  # longevity sub-model
  longevity ~ mass,

  # brain size sub-model
  brain ~ mass,

  # repertoire sub-model
  repertoire ~ longevity,
  repertoire ~ brain,
  repertoire ~ mass,
  repertoire ~ flock,
  repertoire ~ greg,
  repertoire ~ cds_avg,

  labels = c(
    mass = "Body\nmass",

    longevity = "Longevity",

    brain = "Relative\nbrain size",

    flock = "Flocking",

    greg = "Gregariousness",

    cds_avg = "Climate\nstability",

    repertoire = "Vocal\nrepertoire"
  )
)

# Create tidy DAG
set.seed(5)

tidy_dag <- tidy_dagitty(dag_repertoire)

# Assign node categories
tidy_dag$data$type <- "predictor"

tidy_dag$data$type[
  tidy_dag$data$name %in% c("mass")
] <- "background"

tidy_dag$data$type[
  tidy_dag$data$name %in% c(
    "flock",
    "greg",
    "cds_avg"
  )
] <- "ecology"

tidy_dag$data$type[
  tidy_dag$data$name %in% c(
    "brain",
    "longevity"
  )
] <- "mediator"

tidy_dag$data$type[
  tidy_dag$data$name %in% c("repertoire")
] <- "outcome"

# Plot DAG
ggplot(
  tidy_dag,
  aes(
    x = x,
    y = y,
    xend = xend,
    yend = yend
  )
) +

  scale_color_viridis_d(
    option = "G",
    begin = 0.2,
    end = 0.8,
    alpha = 0.8
  ) +

  geom_dag_edges_fan(
    edge_color = mako(10, alpha = 0.5)[2],
    arrow = grid::arrow(
      length = grid::unit(10, "pt"),
      type = "closed"
    )
  ) +

  geom_dag_point(
    aes(color = type),
    size = 24
  ) +

  geom_dag_text(
    aes(label = label),
    color = "black",
    size = 4
  ) +

  theme_dag() +

  ggtitle("DAG: Drivers of Vocal Repertoire Size") +

  theme(
    legend.position = "bottom"
  )

0.3 Data Transformation

Continuous predictors (brain size, longevity, body mass, flock size, and colour scores) are log-transformed where appropriate and then standardised to mean = 0, SD = 1. Repertoire size is left on its raw count scale, as transforming count data prior to modelling is generally discouraged; instead, it will be modelled with a negative binomial distribution.

Code
traits <- traits |>
  mutate_at(c("brain", "longevity", "mass"), ~(log(.))) |>
  mutate_at(c("brain", "longevity", "mass", "flock", "cds_avg", "ces_avg"),
            ~(scale(.) |> as.vector()))

0.4 Phylogeny: Import and Matching

The phylogenetic tree is pruned to retain only species present in the trait dataset. A variance-covariance matrix is then computed from the pruned tree; this matrix is passed to brms to model the expected covariance among species due to shared evolutionary history (a Brownian motion model of trait evolution).

Code
tree <- read.tree("./data/raw/08112025_BrianSmith.treefile")

# prune to species in trait data
matched_tips <- intersect(tree$tip.label, traits$phylo)
tree <- drop.tip(tree, setdiff(tree$tip.label, matched_tips))

# generate phylogenetic variance-covariance matrix
A <- ape::vcv.phylo(tree)

# trim trait data to match pruned tree
traits_filtered <- traits[traits$phylo %in% matched_tips, ]

Distribution check after filtering to matched species:

Code
reshape2::melt(traits_filtered) |>
  ggplot(aes(x = value)) +
  facet_wrap(~variable, scales = "free") +
  geom_histogram() +
  theme_bw()

Trait distributions after filtering to species present in the phylogeny.

0.5 Multiple Imputation

Missing trait values are handled using Multiple Imputation by Chained Equations (MICE), implemented via the mice package. We generate 100 imputed datasets, each with a different plausible set of values for the missing observations. The model is later run on all 100 datasets and results are combined, propagating imputation uncertainty into the final inference.

Code
md.pattern(traits_filtered)

Missing data pattern across traits.
    phylo species repertoire mass greg cds_avg ces_avg flock longevity brain
903     1       1          1    1    1       1       1     1         1     1
138     1       1          1    1    1       1       1     1         1     0
30      1       1          1    1    1       1       1     1         0     1
30      1       1          1    1    1       1       1     1         0     0
24      1       1          1    1    1       1       1     0         1     1
3       1       1          1    1    1       1       1     0         1     0
11      1       1          1    1    0       0       0     1         1     1
1       1       1          1    1    0       0       0     1         0     0
1       1       1          1    0    1       1       1     0         0     0
3       1       1          1    0    0       0       0     0         0     1
        0       0          0    4   15      15      15    31        65   173
       
903   0
138   1
30    1
30    2
24    1
3     2
11    3
1     5
1     4
3     6
    318
Code
# 100 imputations, default mice algorithm (non-phylogenetic)
imp <- mice(traits_filtered, m = 100, seed = 1859)

# store as long-format data frame with .imp variable (1:100)
imp_long <- complete(imp, action = "long")
qs_save(imp_long, "./data/raw/imputed_data.qs")
rm(imp)

0.6 Model Specification

The SEM consists of three linked sub-models:

  • Longevity: predicted by body mass and phylogeny
  • Brain size: predicted by body mass and phylogeny
  • Repertoire size: predicted by longevity, brain size, body mass, flock size, gregariousness, and chromatic colour score — plus phylogeny

All sub-models include a phylogenetic random effect via gr(phylo, cov = A) and a species-level random intercept to capture residual non-phylogenetic species variation. The set_rescor(FALSE) call means residual correlations among the three responses are not estimated (i.e., any correlation is assumed to be fully mediated by the explicit paths in the model).

Code
# longevity sub-model
long_mod <- bf(
  longevity ~ mass +
    (1 | gr(phylo, cov = A)) + (1 | species)
)

# brain size sub-model
br_mod <- bf(
  brain ~ mass +
    (1 | gr(phylo, cov = A)) + (1 | species)
)

# repertoire sub-model (negative binomial for count data)
rep_mod <- bf(
  repertoire ~ longevity + brain + mass + flock + greg + cds_avg +
    (1 | gr(phylo, cov = A)) + (1 | species),
  family = negbinomial()
)

0.6.1 Priors

Weakly informative priors are used throughout. Priors on the standardised predictors in the brain and longevity sub-models are more regularising (SD = 0.5) than those on repertoire predictors (SD = 1), reflecting the fact that repertoire is on its original count scale.

Code
priors <- c(
  prior(normal(0, 1),   class = "b", resp = "repertoire"),
  prior(normal(0, 0.5), class = "b", resp = "brain"),
  prior(normal(0, 0.5), class = "b", resp = "longevity")
)

0.7 Initial Model Fit (Single Imputation)

Before running the full 100-imputation pipeline, the model is first fit on a single imputed dataset to check for convergence, inspect posterior predictive fit, and confirm the model structure is sensible.

Code
mod_SEM <- brm(
  rep_mod + br_mod + long_mod + set_rescor(FALSE),
  data     = imp_long |> filter(.imp == 1),
  data2    = list(A = A),
  prior    = priors,
  backend  = "cmdstanr",
  chains   = 4,
  cores    = 4,
  iter     = 10000,
  seed     = 123,
  file = "./data/processed/base_SEM_model_fit",
  control  = list(adapt_delta = 0.95),
  file_refit = "on_change"
)

0.7.1 Model Summary

Rhat values close to 1.0 and large effective sample sizes (ESS) indicate good convergence.

Code
summary(mod_SEM)
 Family: MV(negbinomial, gaussian, gaussian) 
  Links: mu = log
         mu = identity
         mu = identity 
Formula: repertoire ~ longevity + brain + mass + flock + greg + cds_avg + (1 | gr(phylo, cov = A)) + (1 | species) 
         brain ~ mass + (1 | gr(phylo, cov = A)) + (1 | species) 
         longevity ~ mass + (1 | gr(phylo, cov = A)) + (1 | species) 
   Data: filter(imp_long, .imp == 1) (Number of observations: 1144) 
  Draws: 4 chains, each with iter = 10000; warmup = 5000; thin = 1;
         total post-warmup draws = 20000

Multilevel Hyperparameters:
~phylo (Number of levels: 75) 
                         Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS
sd(repertoire_Intercept)     0.07      0.03     0.02     0.13 1.00     2639
sd(brain_Intercept)          0.05      0.01     0.03     0.07 1.00     1496
sd(longevity_Intercept)      0.13      0.03     0.08     0.19 1.00     1848
                         Tail_ESS
sd(repertoire_Intercept)     4090
sd(brain_Intercept)          2898
sd(longevity_Intercept)      4197

~species (Number of levels: 75) 
                         Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS
sd(repertoire_Intercept)     0.22      0.11     0.02     0.43 1.00     2310
sd(brain_Intercept)          0.06      0.04     0.00     0.13 1.01      993
sd(longevity_Intercept)      0.29      0.11     0.05     0.48 1.00     1470
                         Tail_ESS
sd(repertoire_Intercept)     6041
sd(brain_Intercept)          2758
sd(longevity_Intercept)      1852

Regression Coefficients:
                     Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS
repertoire_Intercept     2.59      0.21     2.18     3.03 1.00    11523
brain_Intercept         -0.23      0.12    -0.49     0.01 1.00     4342
longevity_Intercept      0.08      0.31    -0.53     0.70 1.00     7224
repertoire_longevity     0.17      0.10    -0.04     0.37 1.00    19474
repertoire_brain         0.53      0.31    -0.09     1.15 1.00    13785
repertoire_mass         -0.31      0.30    -0.92     0.28 1.00    13204
repertoire_flock         0.04      0.06    -0.09     0.16 1.00    22620
repertoire_greg          0.20      0.19    -0.19     0.56 1.00    10177
repertoire_cds_avg      -0.05      0.07    -0.19     0.09 1.00    16771
brain_mass               0.79      0.04     0.71     0.87 1.00     2626
longevity_mass           0.57      0.09     0.39     0.75 1.00    10228
                     Tail_ESS
repertoire_Intercept    11350
brain_Intercept          7313
longevity_Intercept     10470
repertoire_longevity    14963
repertoire_brain        14214
repertoire_mass         14346
repertoire_flock        16122
repertoire_greg         13859
repertoire_cds_avg      15796
brain_mass               5531
longevity_mass          12430

Further Distributional Parameters:
                 Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
shape_repertoire     1.49      0.06     1.36     1.61 1.00    38319    14587
sigma_brain          0.03      0.00     0.03     0.03 1.00    33954    13820
sigma_longevity      0.10      0.00     0.10     0.11 1.00    39407    14579

Draws were sampled using sample(hmc). For each parameter, Bulk_ESS
and Tail_ESS are effective sample size measures, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).

0.7.2 Posterior Predictive Checks

Visual inspection of the posterior predictive distribution against the observed data. The model-predicted distributions (blue lines) should closely follow the observed data distribution (black line).

Code
p1 <- pp_check(mod_SEM, ndraws = 100, resp = "repertoire") +
  ggtitle("Response: repertoire")
p2 <- pp_check(mod_SEM, ndraws = 100, resp = "brain") +
  ggtitle("Response: brain")
p3 <- pp_check(mod_SEM, ndraws = 100, resp = "longevity") +
  ggtitle("Response: longevity")

p1 + p2 + p3 +
  plot_annotation("Posterior predictive checks (blue: model, black: data)")

Posterior predictive checks for the initial single-imputation model fit.

0.8 Full Pipeline: All 100 Imputed Datasets

The model is first compiled with zero chains (no sampling), producing a compiled Stan program. This compiled object is then updated via update() for each of the 100 imputed datasets in parallel, avoiding recompilation on every iteration.

0.8.1 Compile (No Sampling)

Code
mod_SEM_empty <- brm(
  rep_mod + br_mod + long_mod + set_rescor(FALSE),
  data    = imp_long |> filter(.imp == 1),
  data2   = list(A = A),
  prior   = priors,
  backend = "cmdstanr",
  chains  = 0,
  cores   = 0,
  iter    = 1000,
  seed    = 123,
  file    = "./data/processed/SEM_model_fit_empty.rds",
  control = list(adapt_delta = 0.95)
)

0.8.2 Run Across All Imputations

Each imputed dataset gets its own set of 2 chains × 5000 iterations (1000 warmup). Running in parallel across 20 cores substantially reduces wall-clock time.

Code
models <- pblapply(1:100, cl = 20, function(i) {
  update(mod_SEM_empty,
         newdata  = imp_long |> filter(.imp == i),
         data2    = list(A = A),
         prior    = priors,
         backend  = "cmdstanr",
         chains   = 2,
         cores    = 2,
         iter     = 10000,
         seed     = 123,
         control  = list(adapt_delta = 0.95))
})

saveRDS(models, "./data/processed/SEM_models_all_imputations.rds")

0.9 Convergence Diagnostics

After fitting all 100 models, Rhat values are checked for each. Rhat > 1.05 is considered a convergence warning. The table below summarises how many parameters with Rhat > 1.05 each model has.

Code
models <- readRDS("./data/processed/SEM_models_all_imputations.rds")

n_bad_rhat <- sapply(models, function(mod) {
  rhats <- summarise_draws(as_draws_array(mod))$rhat
  sum(rhats > 1.05, na.rm = TRUE)
})

max_rhat <- sapply(models, function(mod) {

  rhats <- summarise_draws(
    as_draws_array(mod)
  )$rhat

  max(rhats, na.rm = TRUE)
})

df_rhats <- data.frame(
  model = 1:length(models),
  n_bad_rhat = n_bad_rhat,
  max_rhat = max_rhat
)

saveRDS(df_rhats, "./data/processed/n_bad_rhat.rds")

Check R hats across all models. The majority of models should have zero parameters with Rhat > 1.05, and the maximum Rhat values should be close to 1.0.

Code
df_rhats <- readRDS("./data/processed/n_bad_rhat.rds")

df_rhats |>  subset(max_rhat > 1.05)
model n_bad_rhat max_rhat
Code
# proportion of models with any bad Rhats
table(df_rhats$n_bad_rhat > 0)

FALSE 
  100 
Code
# distribution of bad Rhat counts
summary(df_rhats$n_bad_rhat)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
      0       0       0       0       0       0 

0.10 Combine Models and Final Inference

The 100 individual model fits are combined into a single brmsfit object using combine_models(). This pools the posterior draws across all imputed datasets, so that subsequent inference integrates over both sampling uncertainty and imputation uncertainty.

Code
comb_mod <- combine_models(mlist = models, check_data = FALSE)
saveRDS(comb_mod, "./data/processed/SEM_combined_model.rds")

0.10.1 Results Summary

Fixed effect estimates with 95% credible intervals. Predictors whose credible intervals exclude zero are interpreted as having a meaningful effect on the response.

Code
extended_summary(
  comb_mod,
  highlight = TRUE,
  trace.palette = viridis::mako,
  remove.intercepts = TRUE,
  trace = FALSE, 
  print.name = FALSE
)
formula family priors iterations chains thinning warmup diverg_transitions rhats > 1.05 min_bulk_ESS min_tail_ESS seed
1 list(repertoire = list(formula = repertoire ~ longevity + brain + mass + flock + greg + cds_avg + (1 | gr(phylo, cov = A)) + (1 | species), pforms = list(), pfix = list(), family = list(family = “negbinomial”, link = “log”, linkfun = function (mu) link(mu, link = slink), linkinv = function (eta) inv_link(eta, link = slink), dpars = c(“mu”, “shape”), type = “int”, ybounds = c(0, Inf), closed = c(TRUE, NA), ad = c(“weights”, “subset”, “cens”, “trunc”, “rate”, “index”), specials = “sbi_log”, prior = function (dpar, link = “identity”, …) { if (dpar == “shape” && link == “identity”) { return(“inv_gamma(0.4, 0.3)”) } NULL }, link_shape = “log”), resp = “repertoire”, mecor = TRUE), brain = list(formula = brain ~ mass + (1 | gr(phylo, cov = A)) + (1 | species), pforms = list(), pfix = list(), resp = “brain”, family = list(family = “gaussian”, link = “identity”, linkfun = function (mu) link(mu, link = slink), linkinv = function (eta) inv_link(eta, link = slink), dpars = c(“mu”, “sigma”), type = “real”, ybounds = c(-Inf, Inf), closed = c(NA, NA), ad = c(“weights”, “subset”, “se”, “cens”, “trunc”, “mi”, “index”), normalized = c(“_time_hom”, “_time_het”, “_lagsar”, “_errorsar”, “_fcor”), specials = c(“residuals”, “rescor”)), mecor = TRUE), longevity = list(formula = longevity ~ mass + (1 | gr(phylo, cov = A)) + (1 | species), pforms = list(), pfix = list(), resp = “longevity”, family = list(family = “gaussian”, link = “identity”, linkfun = function (mu) link(mu, link = slink), linkinv = function (eta) inv_link(eta, link = slink), dpars = c(“mu”, “sigma”), type = “real”, ybounds = c(-Inf, Inf), closed = c(NA, NA), ad = c(“weights”, “subset”, “se”, “cens”, “trunc”, “mi”, “index”), normalized = c(“_time_hom”, “_time_het”, “_lagsar”, “_errorsar”, “_fcor”), specials = c(“residuals”, “rescor”)), mecor = TRUE)) () b-normal(0, 0.5) b-normal(0, 0.5) b-normal(0, 1) Intercept-student_t(3, -0.1, 2.5) Intercept-student_t(3, -0.1, 2.5) Intercept-student_t(3, 2.4, 2.5) sd-student_t(3, 0, 2.5) sd-student_t(3, 0, 2.5) sd-student_t(3, 0, 2.5) shape-inv_gamma(0.4, 0.3) sigma-student_t(3, 0, 2.5) sigma-student_t(3, 0, 2.5) 5000 200 1 2500 3657 (0.0073%) 9 212.327 307.283 123
Estimate l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
b_repertoire_Intercept 2.388 1.957 2.769 1.037 3013.692 9532.431
b_brain_Intercept -0.609 -1.608 0.219 1.104 1138.671 3603.219
b_longevity_Intercept -0.350 -0.800 0.129 1.078 1478.698 4918.833
b_repertoire_longevity 0.044 -0.092 0.161 1.543 346.405 515.745
b_repertoire_brain 0.017 -0.084 0.109 1.656 316.205 550.669
b_repertoire_mass 0.003 -0.111 0.124 1.541 347.166 614.765
b_repertoire_flock -0.033 -0.161 0.100 1.049 2272.522 8035.299
b_repertoire_greg 0.565 0.157 0.949 1.17 751.071 1222.692
b_repertoire_cds_avg -0.088 -0.230 0.065 1.135 905.268 2312.961
b_brain_mass 0.448 0.125 0.769 4.27 212.327 307.283
b_longevity_mass 0.171 0.026 0.348 3.119 223.702 338.484

0.10.1.1 Total effects

Code
draws <- as_draws_df(comb_mod)

# Total effects
draws$total_mass <-
  draws$b_repertoire_mass +
  draws$b_brain_mass * draws$b_repertoire_brain +
  draws$b_longevity_mass * draws$b_repertoire_longevity

draws$total_brain <- draws$b_repertoire_brain
draws$total_longevity <- draws$b_repertoire_longevity
draws$total_flock <- draws$b_repertoire_flock
draws$total_greg <- draws$b_repertoire_greg
draws$total_cds_avg <- draws$b_repertoire_cds_avg

total_effects_tbl <-
  draws |>
  select(starts_with("total_")) |>
  pivot_longer(
    everything(),
    names_to = "parameter",
    values_to = "estimate"
  ) |>
  group_by(parameter) |>
  summarise(
    Estimate = median(estimate),
    Lower95 = quantile(estimate, 0.025),
    Upper95 = quantile(estimate, 0.975),
    .groups = "drop"
  ) |>
  mutate(
    parameter = gsub("^total_", "", parameter)
  )

as.data.frame(total_effects_tbl)
parameter Estimate Lower95 Upper95
brain 0.0187756 -0.0840195 0.1088290
cds_avg -0.0901534 -0.2302631 0.0646350
flock -0.0336944 -0.1606140 0.1002841
greg 0.5691015 0.1568058 0.9489450
longevity 0.0478347 -0.0924442 0.1613741
mass 0.0161071 -0.0802855 0.1226039
Code
# Direct effects
draws$direct_mass <- draws$b_repertoire_mass
draws$direct_brain <- draws$b_repertoire_brain
draws$direct_longevity <- draws$b_repertoire_longevity
draws$direct_flock <- draws$b_repertoire_flock
draws$direct_greg <- draws$b_repertoire_greg
draws$direct_cds_avg <- draws$b_repertoire_cds_avg

plot_dat <- bind_rows(

  draws |>
    select(starts_with("direct_")) |>
    pivot_longer(
      everything(),
      names_to = "parameter",
      values_to = "estimate"
    ) |>
    mutate(effect_type = "Direct"),

  draws |>
    select(starts_with("total_")) |>
    pivot_longer(
      everything(),
      names_to = "parameter",
      values_to = "estimate"
    ) |>
    mutate(effect_type = "Total")

) |>
  mutate(
    parameter = gsub("^direct_|^total_", "", parameter),
    parameter = factor(
      parameter,
      levels = c(
        "mass",
        "brain",
        "longevity",
        "flock",
        "greg",
        "cds_avg"
      )
    )
  )

ggplot(
  plot_dat,
  aes(
    x = estimate,
    fill = effect_type,
    colour = effect_type
  )
) +
  geom_density(
    alpha = 0.25,
    linewidth = 0.8
  ) +
  geom_vline(
    xintercept = 0,
    linetype = 2,
    colour = "grey70"
  ) +
  facet_wrap(
    ~ parameter,
    scales = "free"
  ) +
  theme_classic() +
    scale_fill_viridis_d(option = "G", begin = 0.2, end = 0.8) +
    scale_color_viridis_d(option = "G", begin = 0.2, end = 0.8) +
  labs(
    x = "Effect size",
    y = "Density",
    fill = NULL,
    colour = NULL
  )

Takeaways

  • Repertoire size is positively associated with gregariousness
  • Body mass total effect is positively associated with brain size and longevity

 


─ Session info ───────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.5.2 (2025-10-31)
 os       Ubuntu 22.04.5 LTS
 system   x86_64, linux-gnu
 ui       X11
 language (EN)
 collate  en_US.UTF-8
 ctype    en_US.UTF-8
 tz       America/Costa_Rica
 date     2026-06-09
 pandoc   3.2 @ /usr/lib/rstudio/resources/app/bin/quarto/bin/tools/x86_64/ (via rmarkdown)
 quarto   1.7.31 @ /usr/local/bin/quarto

─ Packages ───────────────────────────────────────────────────────────────────
 package           * version    date (UTC) lib source
 abind               1.4-8      2024-09-12 [1] CRAN (R 4.5.0)
 ape               * 5.8-1      2024-12-16 [1] CRAN (R 4.5.0)
 arrayhelpers        1.1-0      2020-02-04 [1] CRAN (R 4.5.0)
 backports           1.5.1      2026-04-03 [1] CRAN (R 4.5.2)
 bayesplot           1.15.0     2025-12-12 [1] CRAN (R 4.5.2)
 boot                1.3-32     2025-08-29 [4] CRAN (R 4.5.1)
 bridgesampling      1.2-1      2025-11-19 [1] CRAN (R 4.5.0)
 brms              * 2.23.0     2025-09-09 [1] CRAN (R 4.5.0)
 brmsish           * 1.0.0      2026-01-06 [1] CRANs (R 4.5.2)
 Brobdingnag         1.2-9      2022-10-19 [1] CRAN (R 4.5.0)
 broom               1.0.8      2025-03-28 [3] CRAN (R 4.5.0)
 cachem              1.1.0      2024-05-16 [1] CRAN (R 4.5.0)
 checkmate           2.3.4      2026-02-03 [1] CRAN (R 4.5.2)
 cli                 3.6.6      2026-04-09 [1] CRAN (R 4.5.2)
 clusterGeneration   1.3.8      2023-08-16 [1] CRAN (R 4.5.0)
 cmdstanr          * 0.9.0      2025-08-21 [1] https://stan-dev.r-universe.dev (R 4.5.0)
 coda                0.19-4.1   2024-01-31 [1] CRAN (R 4.5.0)
 codetools           0.2-20     2024-03-31 [4] CRAN (R 4.5.0)
 combinat            0.0-8      2012-10-29 [1] CRAN (R 4.5.0)
 cowplot             1.2.0      2025-07-07 [1] CRAN (R 4.5.0)
 crayon              1.5.3      2024-06-20 [1] CRAN (R 4.5.0)
 curl                7.1.0      2026-04-22 [1] CRAN (R 4.5.2)
 dagitty           * 0.3-4      2023-12-07 [1] CRAN (R 4.5.0)
 DEoptim             2.2-8      2022-11-11 [1] CRAN (R 4.5.0)
 devtools            2.4.5      2022-10-11 [1] CRAN (R 4.5.0)
 digest              0.6.39     2025-11-19 [1] CRAN (R 4.5.0)
 distributional      0.5.0      2024-09-17 [1] CRAN (R 4.5.0)
 doParallel          1.0.17     2022-02-07 [1] CRAN (R 4.5.0)
 dplyr             * 1.1.4      2023-11-17 [1] CRAN (R 4.5.0)
 ellipsis            0.3.2      2021-04-29 [3] CRAN (R 4.1.1)
 emmeans             1.11.1     2025-05-04 [3] CRAN (R 4.5.0)
 estimability        1.5.1      2024-05-12 [3] CRAN (R 4.5.0)
 evaluate            1.0.5      2025-08-27 [1] CRAN (R 4.5.0)
 expm                1.0-0      2024-08-19 [3] CRAN (R 4.5.0)
 farver              2.1.2      2024-05-13 [1] CRAN (R 4.5.0)
 fastmap             1.2.0      2024-05-15 [1] CRAN (R 4.5.0)
 fastmatch           1.1-6      2024-12-23 [3] CRAN (R 4.5.0)
 forcats           * 1.0.0      2023-01-29 [1] CRAN (R 4.5.0)
 foreach             1.5.2      2022-02-02 [3] CRAN (R 4.1.2)
 fs                  2.1.0      2026-04-18 [1] CRAN (R 4.5.2)
 generics            0.1.4      2025-05-09 [1] CRAN (R 4.5.0)
 ggdag             * 0.2.13     2024-07-22 [1] CRAN (R 4.5.0)
 ggdist              3.3.3      2025-04-23 [1] CRAN (R 4.5.0)
 ggforce             0.3.3      2021-03-05 [3] CRAN (R 4.1.1)
 ggplot2           * 4.0.3      2026-04-22 [1] CRAN (R 4.5.2)
 ggraph              2.0.5      2021-02-23 [3] CRAN (R 4.1.1)
 ggrepel             0.9.6      2024-09-07 [1] CRAN (R 4.5.0)
 ggridges          * 0.5.7      2025-08-27 [1] CRAN (R 4.5.0)
 glmnet              4.1-8      2023-08-22 [1] CRAN (R 4.5.0)
 glue                1.8.1      2026-04-17 [1] CRAN (R 4.5.2)
 graphlayouts        0.8.0      2022-01-03 [3] CRAN (R 4.1.2)
 gridExtra           2.3        2017-09-09 [1] CRAN (R 4.5.0)
 gtable              0.3.6      2024-10-25 [1] CRAN (R 4.5.0)
 hms                 1.1.3      2023-03-21 [3] CRAN (R 4.5.0)
 htmltools           0.5.9      2025-12-04 [1] CRAN (R 4.5.0)
 htmlwidgets         1.6.4      2023-12-06 [1] RSPM (R 4.5.0)
 httpuv              1.6.16     2025-04-16 [1] RSPM (R 4.5.0)
 igraph              2.2.1      2025-10-27 [1] CRAN (R 4.5.0)
 inline              0.3.21     2025-01-09 [1] CRAN (R 4.5.0)
 iterators           1.0.14     2022-02-05 [3] CRAN (R 4.1.2)
 jomo                2.7-6      2023-04-15 [1] CRAN (R 4.5.2)
 jsonlite            2.0.0      2025-03-27 [1] CRAN (R 4.5.0)
 kableExtra          1.4.0      2024-01-24 [1] CRAN (R 4.5.0)
 knitr               1.51       2025-12-20 [1] CRAN (R 4.5.2)
 labeling            0.4.3      2023-08-29 [1] CRAN (R 4.5.0)
 later               1.4.2      2025-04-08 [1] RSPM (R 4.5.0)
 lattice             0.22-9     2026-02-09 [4] CRAN (R 4.5.2)
 lifecycle           1.0.5      2026-01-08 [1] CRAN (R 4.5.2)
 lme4                1.1-37     2025-03-26 [1] CRAN (R 4.5.0)
 loo                 2.9.0      2025-12-23 [1] CRAN (R 4.5.2)
 lubridate         * 1.9.5      2026-02-04 [1] CRAN (R 4.5.2)
 magrittr            2.0.5      2026-04-04 [1] CRAN (R 4.5.2)
 maps              * 3.4.2.1    2024-11-10 [3] CRAN (R 4.5.0)
 MASS                7.3-65     2025-02-28 [4] CRAN (R 4.4.3)
 Matrix              1.7-4      2025-08-28 [4] CRAN (R 4.5.1)
 matrixStats         1.5.0      2025-01-07 [1] CRAN (R 4.5.0)
 memoise             2.0.1      2021-11-26 [1] CRAN (R 4.5.0)
 mice              * 3.19.0     2025-12-10 [1] CRAN (R 4.5.2)
 mime                0.13       2025-03-17 [1] CRAN (R 4.5.0)
 miniUI              0.1.2      2025-04-17 [3] CRAN (R 4.5.0)
 minqa               1.2.4      2014-10-09 [3] CRAN (R 4.0.1)
 mitml               0.4-5      2023-03-08 [1] CRAN (R 4.5.2)
 mnormt              2.1.1      2022-09-26 [3] CRAN (R 4.5.0)
 multcomp            1.4-28     2025-01-29 [3] CRAN (R 4.5.0)
 mvtnorm             1.3-3      2025-01-10 [1] CRAN (R 4.5.0)
 nlme                3.1-168    2025-03-31 [4] CRAN (R 4.4.3)
 nloptr              2.2.1      2025-03-17 [3] CRAN (R 4.5.0)
 nnet                7.3-20     2025-01-01 [4] CRAN (R 4.4.2)
 numDeriv            2016.8-1.1 2019-06-06 [1] CRAN (R 4.5.0)
 optimParallel       1.0-2      2021-02-11 [1] CRAN (R 4.5.0)
 otel                0.2.0      2025-08-29 [1] CRAN (R 4.5.2)
 packrat             0.9.3      2025-06-16 [1] CRAN (R 4.5.2)
 pan                 1.9        2023-12-07 [1] CRAN (R 4.5.2)
 patchwork         * 1.3.0      2024-09-16 [1] CRAN (R 4.5.0)
 pbapply           * 1.7-4      2025-07-20 [1] CRAN (R 4.5.0)
 phangorn            2.12.1     2024-09-17 [1] CRAN (R 4.5.0)
 phytools          * 2.4-4      2025-01-08 [1] CRAN (R 4.5.0)
 pillar              1.11.1     2025-09-17 [1] CRAN (R 4.5.0)
 pkgbuild            1.4.8      2025-05-26 [1] CRAN (R 4.5.0)
 pkgconfig           2.0.3      2019-09-22 [1] CRAN (R 4.5.0)
 pkgload             1.4.1      2025-09-23 [1] CRAN (R 4.5.0)
 plyr                1.8.9      2023-10-02 [1] CRAN (R 4.5.0)
 polyclip            1.10-7     2024-07-23 [1] CRAN (R 4.5.0)
 posterior         * 1.6.1      2025-02-27 [1] CRAN (R 4.5.0)
 processx            3.8.6      2025-02-21 [1] CRAN (R 4.5.0)
 profvis             0.4.0      2024-09-20 [1] CRAN (R 4.5.0)
 promises            1.3.3      2025-05-29 [1] RSPM (R 4.5.0)
 ps                  1.9.1      2025-04-12 [1] CRAN (R 4.5.0)
 purrr             * 1.2.0      2025-11-04 [1] CRAN (R 4.5.0)
 qs2               * 0.2.1      2026-05-04 [1] CRAN (R 4.5.2)
 quadprog            1.5-8      2019-11-20 [3] CRAN (R 4.0.1)
 QuickJSR            1.8.1      2025-09-20 [1] CRAN (R 4.5.0)
 R6                  2.6.1      2025-02-15 [1] CRAN (R 4.5.0)
 rbibutils           2.3        2024-10-04 [1] CRAN (R 4.5.0)
 RColorBrewer        1.1-3      2022-04-03 [1] CRAN (R 4.5.0)
 Rcpp              * 1.1.1-1.1  2026-04-24 [1] CRAN (R 4.5.2)
 RcppParallel        5.1.11-1   2025-08-27 [1] CRAN (R 4.5.0)
 Rdpack              2.6.4      2025-04-09 [1] CRAN (R 4.5.0)
 readr             * 2.1.5      2024-01-10 [1] CRAN (R 4.5.0)
 reformulas          0.4.1      2025-04-30 [1] CRAN (R 4.5.0)
 remotes             2.5.0      2024-03-17 [1] CRAN (R 4.5.0)
 reshape2          * 1.4.5      2025-11-12 [1] CRAN (R 4.5.0)
 rlang               1.2.0      2026-04-06 [1] CRAN (R 4.5.2)
 rmarkdown           2.31       2026-03-26 [1] CRAN (R 4.5.2)
 rpart               4.1.24     2025-01-07 [4] CRAN (R 4.4.2)
 rstan               2.32.7     2025-03-10 [1] CRAN (R 4.5.0)
 rstantools          2.5.0      2025-09-01 [1] CRAN (R 4.5.0)
 rstudioapi          0.17.1     2024-10-22 [1] CRAN (R 4.5.0)
 S7                  0.2.2      2026-04-22 [1] CRAN (R 4.5.2)
 sandwich            3.1-1      2024-09-15 [3] CRAN (R 4.5.0)
 scales              1.4.0      2025-04-24 [1] CRAN (R 4.5.0)
 scatterplot3d       0.3-44     2023-05-05 [1] CRAN (R 4.5.0)
 scico             * 1.5.0      2023-08-14 [1] RSPM (R 4.5.0)
 sessioninfo         1.2.3      2025-02-05 [3] CRAN (R 4.5.0)
 shape               1.4.6.1    2024-02-23 [3] CRAN (R 4.5.0)
 shiny               1.10.0     2024-12-14 [1] CRAN (R 4.5.0)
 sketchy             1.0.7      2026-01-28 [1] CRANs (R 4.5.2)
 StanHeaders         2.32.10    2024-07-15 [1] CRAN (R 4.5.0)
 stringfish          0.19.0     2026-04-21 [1] CRAN (R 4.5.2)
 stringi             1.8.7      2025-03-27 [1] CRAN (R 4.5.0)
 stringr           * 1.6.0      2025-11-04 [1] CRAN (R 4.5.0)
 survival            3.8-6      2026-01-16 [4] CRAN (R 4.5.2)
 svglite             2.2.2      2025-10-21 [1] CRAN (R 4.5.0)
 svUnit              1.0.8      2025-08-26 [1] CRAN (R 4.5.0)
 systemfonts         1.3.1      2025-10-01 [1] CRAN (R 4.5.0)
 tensorA             0.36.2.1   2023-12-13 [1] CRAN (R 4.5.0)
 textshaping         1.0.4      2025-10-10 [1] CRAN (R 4.5.0)
 TH.data             1.1-3      2025-01-17 [3] CRAN (R 4.5.0)
 tibble            * 3.3.0      2025-06-08 [1] RSPM (R 4.5.0)
 tidybayes           3.0.7      2024-09-15 [1] CRAN (R 4.5.0)
 tidygraph           1.2.0      2020-05-12 [3] CRAN (R 4.0.1)
 tidyr             * 1.3.2      2025-12-19 [1] CRAN (R 4.5.2)
 tidyselect          1.2.1      2024-03-11 [1] CRAN (R 4.5.0)
 tidyverse         * 2.0.0      2023-02-22 [1] RSPM (R 4.5.0)
 timechange          0.4.0      2026-01-29 [1] CRAN (R 4.5.2)
 tweenr              2.0.3      2024-02-26 [3] CRAN (R 4.5.0)
 tzdb                0.5.0      2025-03-15 [1] CRAN (R 4.5.0)
 urlchecker          1.0.1      2021-11-30 [1] CRAN (R 4.5.0)
 usethis             3.1.0      2024-11-26 [3] CRAN (R 4.5.0)
 V8                  6.0.4      2025-06-04 [1] RSPM (R 4.5.0)
 vctrs               0.7.3      2026-04-11 [1] CRAN (R 4.5.2)
 viridis           * 0.6.5      2024-01-29 [1] CRAN (R 4.5.0)
 viridisLite       * 0.4.3      2026-02-04 [1] CRAN (R 4.5.2)
 withr               3.0.2      2024-10-28 [1] CRAN (R 4.5.0)
 xaringanExtra       0.8.0      2024-05-19 [1] CRAN (R 4.5.0)
 xfun                0.57       2026-03-20 [1] CRAN (R 4.5.2)
 xml2                1.5.2      2026-01-17 [1] CRAN (R 4.5.2)
 xtable              1.8-8      2026-02-22 [1] CRAN (R 4.5.2)
 yaml                2.3.12     2025-12-10 [1] CRAN (R 4.5.2)
 zoo                 1.8-14     2025-04-10 [3] CRAN (R 4.5.0)

 [1] /home/m/R/x86_64-pc-linux-gnu-library/4.5
 [2] /usr/local/lib/R/site-library
 [3] /usr/lib/R/site-library
 [4] /usr/lib/R/library
 * ── Packages attached to the search path.

──────────────────────────────────────────────────────────────────────────────