A step-by-step tutorial

Published

September 27, 2026

This online tutorial accompanies the paper “Promoting the use of phylogenetic multinomial generalised mixed-effects model to understand the evolution of discrete traits.”

This is an updated version of the tutorial. The original version, published in 2025, is kept unchanged here.

When we reviewed the tutorial about a year after publication, we found several errors and inconsistencies, which we have corrected in this version:

  • Some of the output shown did not match the code or the saved model objects (e.g. output pasted from earlier model fits, and mismatched object names).
  • Convergence was assessed with a single chain for the MCMCglmm models and with two chains for many of the brms models. All models are now run with four independent chains, and we interpret only fits that pass the between-chain diagnostics.
  • For logit models, the phylogenetic variance proportion was calculated with a latent residual variance of 1 instead of \(\pi^2/3\) and was described as Pagel’s \(\lambda\). The MCMCglmm rescaling of nominal models and the reporting of the (fixed) residual correlation have also been corrected. All derived quantities are calculated for each posterior draw, and all intervals are 95% equal-tailed credible intervals.
  • In the second nominal example, the effect of the migration variable was interpreted in the wrong direction. The variable is now called IsSedentary (1 = sedentary).
  • The simulation code did not reproduce the data used for the saved simulation fits. The simulated datasets are now saved and used directly.
  • The second ordinal example is now presented as a cautionary example about identification and prior sensitivity, and an interpretation that the results did not support has been removed.

As a result, some numerical results differ from those in the original tutorial. Where they differ, the values in this version are the corrected ones.

Introduction

This tutorial provides a step-by-step guide to model different types of variables using MCMCglmm and brms. We show and explain how to implement Gaussian, binary, ordinal, and nominal models and how to interpret the results. We also provide an extension: models with multiple data points per species, fitted to simulated data. Simulation is a powerful tool for understanding the behaviour of a model and the effect of the prior distribution on the results.

Why use phylogenetic comparative methods?

Phylogenetic comparative methods are used to account for the non-independence among species due to shared evolutionary history. If we ignore the phylogenetic relationship among species, we may violate the assumption of independence and underestimate the standard errors of the estimated parameters. These methods are widely used in evolutionary biology and ecology to test hypotheses about the evolution of traits and behaviours, reconstruct common ancestral traits, and investigate how different traits have co-evolved across species.

Why use MCMCglmm and brms in this tutorial?

MCMCglmm and brms allow us to fit hierarchical and nonlinear models. We can create more appropriate models that accurately reflect the underlying relationships within the data. Additionally, both packages can handle various types of response variables, including continuous, Poisson, binary, ordinal, and nominal data. Using Bayesian methods for parameter estimation, MCMCglmm and brms can incorporate prior distributions, which is particularly beneficial when working with limited data.

Intended audience

Our tutorial is designed for students and researchers who have at least basic familiarity with the R language and have an interest in using phylogenetic comparative methods. Although the examples in our tutorial focus on questions and data from Behavioural Ecology and Evolutionary Biology, we provide detailed explanations. Therefore, we believe these techniques are versatile and can be applied in other scientific disciplines, such as environmental science, psychology, and beyond.

Set up R on your computer

Please install the following packages before running the code: MCMCglmm, brms, ape, phytools, here, coda, MASS, tidyverse, arm. brms fits models with Stan, so it needs a working Stan backend: either rstan (the default; see the rstan installation instructions) or cmdstanr (see the cmdstanr website). If this is your first time using brms, please install and test one of these backends first.

pacman::p_load("ape", "coda", "tidyverse", "here", 
              "MCMCglmm", "brms", "MASS", 
              "phytools",  "arm", "posterior")

Note 1: Some models (especially nominal models) take a long time to run. We therefore recommend saving each fitted model as an .rds file with saveRDS() and reloading it with readRDS() when you want to inspect or compare the results. This is also how this tutorial is rendered: the model-fitting chunks are shown but not run, and the precomputed model objects are loaded from .rds files in a (hidden) chunk before the results are shown.

Note 2: Because of the stochastic nature of MCMC, every time you (re)run a model you will get slightly different output, even when the model mixes well and has converged. Even if you run the same model on the same computer, your output will differ slightly from the output shown here (unless you fix the random seed).

Note 3: As a practical target, we aim for an effective sample size (ESS) above 400 for the parameters we report, following Vehtari et al. (2021), who suggest that this is typically enough for stable estimates of posterior means, quantiles and Monte Carlo standard errors when chains are sampling well. ESS is parameter-specific, and a large ESS alone does not establish that a model has converged. We therefore look at ESS together with other diagnostics: the rank-normalised R-hat (Rhat) computed from multiple chains, trace and autocorrelation plots, Monte Carlo error and, for Stan (brms), divergent transitions and other sampler warnings.

Note 4: For every MCMCglmm model we run four independent chains (the same model, data, priors and MCMC settings, each with its own random seed), so that convergence can be assessed by comparing chains. We check the rank-normalised Rhat and the bulk and tail effective sample sizes across chains, and we report posterior summaries (means and 95% equal-tailed credible intervals) from the pooled draws of all four chains. Derived quantities (e.g. variance proportions, correlations, probabilities and c2-corrected estimates) are calculated for every pooled draw before summarising. The helper functions below implement this; brms runs four chains by default (chains = 4).

# Run the same MCMCglmm model as independent chains, one seed per chain.
run_mcmcglmm_chains <- function(..., seeds) {
  lapply(seeds, function(seed) {
    set.seed(seed)
    MCMCglmm(..., verbose = FALSE)
  })
}

# Pool the draws of one component ("Sol", "VCV" or "CP") across chains.
pool_chains <- function(fits, what = "Sol") {
  coda::mcmc(do.call(rbind, lapply(fits, function(m) as.matrix(m[[what]]))))
}

# Keep the chains separate (for trace plots and autocorrelation plots).
chains_list <- function(fits, what = "Sol") {
  coda::mcmc.list(lapply(fits, function(m) m[[what]]))
}

# Posterior summaries from the pooled draws (mean, SD, 95% equal-tailed interval)
# and between-chain diagnostics (R-hat, bulk and tail ESS) for each parameter.
# Fixed (non-estimated) parameters have SD = 0 and no diagnostics.
summarise_chains <- function(fits, what = c("Sol", "VCV", "CP")) {
  what <- what[vapply(what, function(w) !is.null(fits[[1]][[w]]), logical(1))]
  out <- do.call(rbind, lapply(what, function(w) {
    draws <- posterior::as_draws_array(chains_list(fits, w))
    posterior::summarise_draws(draws, "mean", "sd", ~quantile(.x, c(0.025, 0.975)),
                               "rhat", "ess_bulk", "ess_tail")
  }))
  out <- as.data.frame(out)
  names(out) <- c("parameter", "mean", "sd", "Q2.5", "Q97.5", "rhat", "ess_bulk", "ess_tail")
  out[2:5] <- lapply(out[2:5], signif, digits = 4)
  out$rhat <- round(out$rhat, 3)
  out[7:8] <- lapply(out[7:8], round)
  out
}

1. Gaussian models

Gaussian models are used when the response variable is continuous (numeric) and its conditional distribution (i.e. the residual variation around the model’s predictions) can be modelled as normal; the raw response itself does not need to be exactly normally distributed. For example, age, body size (e.g. weight, length, or height of an organism), temperature, and distance.

Explanation of dataset

We use the rodent dataset and phylogenetic tree provided by Sheard et al. (2024) to explain how to model Gaussian models. The data is about the rodent suborder Sciuromorpha (223 species), which includes squirrels, chipmunks, dormice, and the mountain beaver. The dataset contains information including tail length, body mass, mean annual temperature, and the presence of a contrasting tail tip (a white or black tip at the end of the tail). In this section, we test the evolutionary relationships between tail length and body mass (response variables) and mean annual temperature and the presence of a contrasting tail tip (predictor variables) in rodents.

First of all, we modify the original dataset.

# 1. Read dataset
dt <- read.csv(here("data", "potential", "Rodent_tail", "RodentData.csv")) #  Replace with your own folder path to where the data is stored

str(dt) # check the structure of the dataset

dt <- dt %>% rename(Phylo = UphamTreeName.full) # Rename the 'UphamTreeName.full' column to 'Phylo'
dt <- dt %>% filter(Suborder == "Sciuromorpha") # Filter the rodent suborder
dt <- subset(dt, !is.na(Mass)) # Remove species that do not have body mass data

# Tail length and body mass: log-transform to reduce skewness, then standardise (centre and scale) to have a mean of 0 and a standard deviation of 1, making it easier to compare with other variables.
# Temperature, shade score and litter size: standardise
dt$zLength <- scale(log(dt$Tail_length)) 
dt$zMass <- scale(log(dt$Mass))
dt$zShade<-scale(dt$shade_score)
dt$zTemp <- scale(dt$Mean_Annual_Temp)
dt$zLitter_size <- scale(dt$Litter_size)

# Visualise zLength distribution
# hist(dt$zLength)

# Also, convert some columns to factors for analysis just in case
dt <- dt %>%
  mutate(across(c(White.tips, Black.tips, Tufts, Naked, Fluffy, Contrasting, Noc, Di, Crep, Autotomy), as.factor))

# Keep species with litter-size data (223 species). Note that the variables above were
# standardised using all 300 Sciuromorpha species with body mass data.
dt <- subset(dt, !is.na(Litter_size))

# 2. Read phylogenetic tree
trees <- read.tree(here("data", "potential", "Rodent_tail", "RodentTrees.tre")) # 100 trees
tree <- trees[[1]] # Here we use only one tree

# Trim out everything from the tree that's not in the modified dataset
trees <- lapply(trees, drop.tip,tip=setdiff(tree$tip.label, dt$Phylo))

# Select one tree for trimming purposes
tree <- trees[[1]]
tree <- force.ultrametric(tree, method = c("extend")) # Convert non-ultrametric tree to ultrametric tree

How to model? How to interpret the output?

Let’s start by running the model. The first model we will fit is a simple animal model with no explanatory variables (only an intercept as the fixed effect) and a phylogenetic random effect. From here on, we will always begin with this simplest model to assess the phylogenetic signal of the response variable before adding more complexity by including one continuous and one categorical explanatory variable.

Fitting a model basically involves three or four steps:

  1. Get the phylogenetic relatedness matrix: its inverse for MCMCglmm (inverseA()) and the phylogenetic correlation matrix for brms (vcv.phylo(..., corr = TRUE))

  2. Set priors for your model and run the model with the default number of iterations, thinning interval and burn-in (warm-up)

  3. Check mixing and convergence, and inspect the posterior distributions of the fixed and random effects

  4. If the chains do not mix well or show signs of non-convergence…

    • run longer chains (more iterations after burn-in) and check whether the burn-in (warm-up) is long enough
    • reconsider the prior settings and the model specification

Univariate model

Intercept-only model

MCMCglmm

To get the variance-covariance matrix, we use the inverseA() function in MCMCglmm:

inv_phylo <- inverseA(tree, nodes = "ALL", scale = TRUE) # Calculate the inverse of the phylogenetic relatedness matrix for all nodes, scaling the results

Then, we will define the priors for the phylogenetic effect and the residual variance using the following code:

prior1 <- list(R = list(V = 1, nu = 0.002),  # Prior for residuals: weak inverse-Wishart prior for residual variance
               G = list(G1 = list(V = 1, nu = 0.002)))  # Prior for random effect: weak inverse-Wishart for random effect

We set inverse-Wishart priors with V = 1 and nu = 0.002 for both the residual variance and the phylogenetic variance. For a single variance, this corresponds to an inverse-gamma prior with shape and scale of 0.001, which is commonly used as a weakly informative prior. It is not “uninformative” in every situation: when the data contain little information about a variance component, especially when the variance is close to zero, this prior can still influence the posterior. We therefore recommend checking how sensitive your results are to the prior (e.g. by refitting the model with a parameter-expanded prior, as we do for the discrete models below). If you have prior knowledge about your study system, you can also use a more informative prior.

In MCMCglmm, the prior list can contain four elements: R (R-structure, residual variances), G (G-structure, random-effect variances), B (fixed effects), and S (the theta_scale parameter). Here, we explain B, R and G.

Meaning of parameters in the MCMCglmm prior specification. Parameter expansion (alpha.mu, alpha.V) also tends to improve mixing of variance components close to zero. See the MCMCglmm course notes (Hadfield) for details.
Parameter Which elements Meaning Effect of increasing the value Effect of decreasing the value
V B Prior (co)variance matrix of the fixed effects (multivariate normal prior) Wider prior on the fixed effects; the data have more influence Narrower prior; estimates are shrunk more strongly towards mu
mu B Prior mean vector of the fixed effects Shifts the prior centre upwards Shifts the prior centre downwards
V R, G Scale matrix of the inverse-Wishart prior on a (co)variance matrix. Together with nu, it determines where the prior places its mass (for a single variance, the prior is inverse-gamma with shape nu/2 and scale nu*V/2) Places more prior mass on larger variances Places more prior mass on smaller variances
nu R, G Degree-of-belief parameter of the inverse-Wishart prior The prior becomes more concentrated (more informative) The prior becomes more diffuse (less informative); nu = 0.002 is often used for a single variance
fix R, G If fix = 1, the (co)variance matrix (from the fix-th dimension onwards) is fixed at V and not estimated (e.g. the residual variance of binary models) NA NA
alpha.mu G (and R) Prior mean of the redundant working parameters used for parameter expansion. With alpha.mu/alpha.V specified, the prior on the variance is a scaled non-central F-distribution (for a single variance with V = 1, nu = 1, alpha.mu = 0, the prior on the standard deviation is half-Cauchy with scale sqrt(alpha.V)) Changes the location of the implied prior on the variance Changes the location of the implied prior on the variance
alpha.V G (and R) Prior (co)variance of the working parameters; it acts as the scale of the implied prior on the variance Heavier upper tail; larger variances receive more prior support Implied prior concentrates on smaller variances

Note that the G element contains one prior for each random-effect term (G1, G2, G3…). For example, if you have three random-effect terms, you need to define three priors in G.

In MCMCglmm, the variance components are divided into two structures: G (here, the phylogenetic variance) and R (the residual, non-phylogenetic variance). From these, we can calculate the phylogenetic heritability (the proportion of the variance on the modelled scale that is attributed to the phylogenetic random effect) using the following equation:

\[ H^2 = \frac{\sigma_{a}^2}{\sigma_{a}^2 + \sigma_{e}^2} \]

Throughout this tutorial, we call this proportion the “phylogenetic signal” for brevity. It is a variance-partitioning measure (a phylogenetic intraclass correlation, or phylogenetic heritability) on the scale of the model, i.e. on the latent scale for discrete responses. It is not Pagel’s \(\lambda\), although the two are closely related for Gaussian data. We calculate it for every posterior draw and then summarise the resulting posterior distribution.

When we do not include any explanatory variables (intercept-only model), we use ~ 1. We use larger values of nitt, thin and burnin than the defaults (nitt = 13000, thin = 10, burnin = 3000). The longer chain after burn-in gives more posterior samples and therefore more precise Monte Carlo estimates, and the larger thinning interval reduces autocorrelation among the stored samples and the storage required; neither of these guarantees convergence, which still needs to be checked.

# You can check model running time by system.time() function. Knowing the running time is important when you trial and error to find the best model setting!

system.time(
  mcmcglmm_mg1 <- run_mcmcglmm_chains(zLength ~ 1,             # Response variable zLength with an intercept-only model
                           random = ~ Phylo,        # Random effect for Phylo
                           ginverse = list(Phylo = inv_phylo$Ainv),  # Specifies the inverse phylogenetic covariance matrix Ainv for Phylo, which accounts for phylogenetic relationships.
                           prior = prior1,          # Prior distributions for the model parameters
                           family = "gaussian",     # Family of the response variable (Gaussian for continuous data)
                           data = dt,               # Dataset used for fitting the model
                           seeds = c(20263026, 20263027, 20263028, 20263029), # one seed per chain (four chains)
                           nitt = 13000*20,       # Total number of iterations (20 times the default)
                           thin = 10*20,          # Thinning interval (20 times the default)
                           burnin = 3000*20       # Number of burn-in iterations (20 times the default)
                           ) 
            )
summarise_chains(mcmcglmm_mg1)

To begin with, we should check mixing and convergence. We can assess them using trace plots, autocorrelation plots and the effective sample size. A trace plot that looks like a “fuzzy caterpillar” (no trends and no long excursions) is evidence of good mixing and stationarity, but it is not proof of convergence; running several independent chains and comparing them (e.g. with R-hat) gives stronger evidence.

plot(chains_list(mcmcglmm_mg1, "VCV")) # Visualise variance component 

plot(chains_list(mcmcglmm_mg1, "Sol")) # Visualise location effects

autocorr.plot(chains_list(mcmcglmm_mg1, "VCV")) # Check chain mixing

autocorr.plot(chains_list(mcmcglmm_mg1, "Sol")) # Check chain mixing

summarise_chains(mcmcglmm_mg1) # pooled posterior summaries and between-chain diagnostics
#>     parameter     mean      sd     Q2.5   Q97.5 rhat ess_bulk ess_tail
#> 1 (Intercept) -0.18450 0.61460 -1.41800 1.01700    1     3984     4000
#> 2       Phylo  1.50000 0.22080  1.11100 1.98400    1     3833     3835
#> 3       units  0.02811 0.01025  0.01077 0.05096    1     3639     3892

The four chains agree with each other: all R-hat values are below 1.01 and the bulk and tail effective sample sizes are above 3,600. Throughout this tutorial, we report the posterior mean and the 95% equal-tailed credible interval (the Q2.5 and Q97.5 columns) calculated from the pooled draws of the four chains. The phylogenetic variance (Phylo) is 1.50 (95% CI 1.11, 1.98), which is much larger than the residual variance (units, 0.028 [0.011, 0.051]). The intercept is −0.18 (−1.42, 1.02).

brms

The formula syntax is similar to that of the lme4 package (lmer()). One of the advantages of brms is that it can fit complex models with multiple random effects and non-linear terms. The model fitting process is similar to MCMCglmm but with a different syntax. The output also differs between the two packages.

In brms, we can use the default_prior() function (formerly get_prior()) to see the priors that brms would use for the model. Note that the defaults are not all weakly informative: population-level effects (class = "b") get flat (improper) priors by default, whereas intercepts and standard deviations get weakly informative Student-t priors scaled to the data (see ?set_prior and the brms documentation). In this tutorial we pass these default priors explicitly via prior = ..., which is equivalent to not specifying them. You can also set your own priors if you know your study system well (see Section 4).

In brms, the phylogeny enters the model through gr(Phylo, cov = A), where A is a matrix proportional to the covariance matrix of the phylogenetic effects. The official brms phylogenetic vignette uses vcv.phylo(tree), i.e. the phylogenetic variance-covariance matrix, and that is valid. Here we use vcv.phylo(tree, corr = TRUE), which returns the phylogenetic correlation matrix. For an ultrametric tree, this simply rescales the tree to a root-to-tip distance of 1, so the estimated phylogenetic variance is expressed per unit tree height. We do this deliberately so that the scale matches the MCMCglmm models, where inverseA(tree, scale = TRUE) applies the same rescaling. With the unscaled covariance matrix, the phylogenetic standard deviation would be rescaled by the square root of the tree height, but the fitted model would otherwise be the same.

We use larger values of iter and warmup than the defaults (iter = 2000, warmup = 1000) to obtain more post-warm-up draws and hence larger effective sample sizes. We also set the control argument control = list(adapt_delta = 0.99, max_treedepth = 15) to reduce the risk of divergent transitions. Setting adapt_delta closer to 1 generally leads Stan to use a smaller step size, which can help reduce divergent transitions, and a larger max_treedepth allows longer trajectories per iteration.

By default, brms runs four independent Markov chains (chains = 4). Multiple chains are needed to compute between-chain diagnostics such as R-hat, and four chains give more reliable diagnostics than two. All brms models in this tutorial use four chains; cores = 4 runs them in parallel, and seed makes the fit reproducible. The MCMCglmm models also use four independent chains (see Note 4 above).

A <- ape::vcv.phylo(tree, corr = TRUE) # Phylogenetic correlation matrix (tree rescaled to unit height)

default_priors1 <- default_prior(
                         zLength ~ 1 + (1|gr(Phylo, cov = A)), 
                         data = dt, 
                         data2 = list(A = A),
                         family = gaussian()
                         )

system.time(
  brms_mg1 <- brm(zLength ~ 1 + (1|gr(Phylo, cov = A)),
                  data = dt, 
                  data2 = list(A = A),
                  family = gaussian(), 
                  prior = default_priors1,
                  iter = 10000, 
                  warmup = 5000, 
                  thin = 1,
                  chains = 4,
                  cores = 4,
                  seed = 20266826,
                  control = list(adapt_delta = 0.99, max_treedepth = 15)
                )
             )

In brms, we can also check the Rhat value, which compares the chains with each other. Values close to 1.00 (we use < 1.01 as a rule of thumb) indicate no evidence of non-convergence; it does not prove convergence.

plot(brms_mg1) # Visualise effects

summary(brms_mg1) #  See the result
#>  Family: gaussian 
#>   Links: mu = identity 
#> Formula: zLength ~ 1 + (1 | gr(Phylo, cov = A)) 
#>    Data: dt (Number of observations: 223) 
#>   Draws: 4 chains, each with iter = 10000; warmup = 5000; thin = 1;
#>          total post-warmup draws = 20000
#>
#> Multilevel Hyperparameters:
#> ~Phylo (Number of levels: 223) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     1.22      0.09     1.05     1.40 1.01     1215     2945
#>
#> Regression Coefficients:
#>           Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept    -0.13      0.60    -1.30     1.04 1.00     3141     5966
#>
#> Further Distributional Parameters:
#>       Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sigma     0.17      0.03     0.11     0.23 1.01      757     1143
#>
#> Draws were sampled using sampling(NUTS). 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).

To check the consistency between the results obtained from MCMCglmm and brms, we compare the posterior means of the fixed effects and the random-effect variances. Note that the random-effect and residual estimates are reported on different scales in the two packages. MCMCglmm reports variances (G-structure / R-structure), whereas brms reports standard deviations (Multilevel Hyperparameters / Further Distributional Parameters). To compare them, we square the brms standard deviations. We do this for each posterior draw and then summarise the draws, because the square of the posterior mean of a standard deviation is not the posterior mean of the variance. Derived quantities, such as the proportion of variance attributed to phylogeny, are likewise calculated for every posterior draw before summarising.

As you can see below, the MCMCglmm and brms estimates do not match exactly. However, the values are close and the overall pattern is consistent, so the two approaches lead to the same conclusions. The differences come from factors such as the sampling algorithms and the prior settings of each package.

Before comparing the estimates, we check the sampler diagnostics of the brms fit (for MCMCglmm, which uses a different sampler, these diagnostics do not exist). There are no divergent transitions, and the tree depth stayed below the maximum. However, the E-BFMI (energy Bayesian fraction of missing information) of each chain is low (about 0.12; values below 0.3 are usually flagged). A low E-BFMI means that the sampler explores some parts of the posterior, here mainly the residual standard deviation, inefficiently. Because the four chains agree (R-hat < 1.01) and the effective sample sizes are adequate (bulk ESS of sigma ≈ 760), we still use this fit. The low E-BFMI is a warning, though, that the posterior geometry of this model is difficult; we return to this below, where the same problem causes a sampling failure.

# sampler diagnostics for the Gaussian brms models (four chains each)
sampler_check <- function(fit) {
  np <- nuts_params(fit)
  c(divergent = sum(np$Value[np$Parameter == "divergent__"]),
    max_treedepth_used = max(np$Value[np$Parameter == "treedepth__"]),
    min_E_BFMI = round(min(rstan::get_bfmi(fit$fit)), 2))
}
sampler_check(brms_mg1)
#>          divergent max_treedepth_used         min_E_BFMI 
#>               0.00              10.00               0.12 
Intercept
posterior_summary(pool_chains(mcmcglmm_mg1, "Sol")) # MCMCglmm (pooled draws of four chains)
#>               Estimate Est.Error     Q2.5    Q97.5
#> (Intercept) -0.1845167 0.6146319 -1.41773 1.017192

draws_df <- as_draws_df(brms_mg1) # Convert brms object to data frame
posterior_summary(draws_df$b_Intercept) # brms
#>        Estimate Est.Error      Q2.5    Q97.5
#> [1,] -0.1281425 0.5960931 -1.295102 1.040134
Random effect and phylogenetic signals (and also residuals)
VCV <- pool_chains(mcmcglmm_mg1, "VCV")

# phylogenetic variance
posterior_summary(VCV[, "Phylo"])             # MCMCglmm (G-structure)
#>      Estimate Est.Error     Q2.5    Q97.5
#> var1 1.500394 0.2207623 1.110933 1.983682

posterior_summary(draws_df$sd_Phylo__Intercept^2) # brms (SD squared for each draw)
#>      Estimate Est.Error    Q2.5    Q97.5
#> [1,] 1.491179 0.2176515 1.09338 1.946597

# residual variance
posterior_summary(VCV[, "units"])             # MCMCglmm (R-structure)
#>        Estimate Est.Error       Q2.5      Q97.5
#> var1 0.02811027 0.0102538 0.01076925 0.05096317

posterior_summary(draws_df$sigma^2)           # brms
#>       Estimate  Est.Error       Q2.5      Q97.5
#> [1,] 0.0293677 0.01092228 0.01220014 0.05452798

# phylogenetic heritability (calculated for each draw)
h2_mcmcglmm <- VCV[, "Phylo"] / (VCV[, "Phylo"] + VCV[, "units"])
h2_brms <- with(draws_df, sd_Phylo__Intercept^2 / (sd_Phylo__Intercept^2 + sigma^2))
posterior_summary(h2_mcmcglmm)
#>      Estimate   Est.Error      Q2.5    Q97.5
#> var1 0.980738 0.008752216 0.9604654 0.994118

posterior_summary(h2_brms)
#>       Estimate   Est.Error      Q2.5     Q97.5
#> [1,] 0.9797531 0.009534434 0.9565043 0.9931725

The two packages give almost identical results. The phylogenetic variance is 1.50 (1.11, 1.98) in MCMCglmm and 1.49 (1.09, 1.95) in brms, and the residual variance is 0.028 (0.011, 0.051) and 0.029 (0.012, 0.055), respectively. The phylogenetic heritability is 0.98 (0.96, 0.99) in both packages: almost all of the variation in standardised tail length is attributed to phylogeny. Note that this intercept-only model does not account for body size.

One continuous explanatory variable model

Note

In a real analysis we should control for the relationship between body size and tail length. We ignore it here for simplicity, so that the example contains only one explanatory variable.

Then, we will add one continuous explanatory variable, zTemp, to the model. In this model, we test whether we can obtain results consistent with previous findings that species with longer tails tend to live in warmer environments.

MCMCglmm
system.time(
  mcmcglmm_mg2 <- run_mcmcglmm_chains(zLength ~ zTemp, 
                           random = ~ Phylo, 
                           ginverse = list(Phylo = inv_phylo$Ainv), 
                           prior = prior1, 
                           family = "gaussian", 
                           data = dt,
                           seeds = c(20263126, 20263127, 20263128, 20263129), # one seed per chain (four chains)
                           nitt = 13000*25,
                           thin = 10*25,
                           burnin = 3000*25
                           ) 
            )
summarise_chains(mcmcglmm_mg2)
#>     parameter     mean      sd     Q2.5   Q97.5  rhat ess_bulk ess_tail
#> 1 (Intercept) -0.18190 0.60860 -1.39700 1.00200 1.000     4313     4010
#> 2       zTemp  0.03621 0.04637 -0.05496 0.12720 1.000     4039     3826
#> 3       Phylo  1.46600 0.22910  1.06900 1.96300 1.002     3713     3925
#> 4       units  0.03080 0.01132  0.01217 0.05668 1.000     3831     3886

posterior_summary(pool_chains(mcmcglmm_mg2, "VCV"))
#>         Estimate  Est.Error      Q2.5      Q97.5
#> Phylo 1.46551508 0.22910115 1.0693894 1.96334803
#> units 0.03079797 0.01131706 0.0121687 0.05668468

posterior_summary(pool_chains(mcmcglmm_mg2, "Sol"))
#>                Estimate  Est.Error        Q2.5     Q97.5
#> (Intercept) -0.18190030 0.60860496 -1.39717815 1.0020822
#> zTemp        0.03621378 0.04637304 -0.05495517 0.1271544

The posterior mean of the slope of standardised temperature is 0.036, with a 95% credible interval of [−0.055, 0.127]. The interval includes zero, so there is little evidence for a relationship between temperature and tail length in this model. The intercept, the expected (standardised) tail length when standardised temperature is zero, is −0.18 (−1.40, 1.00).

The summary() of a single MCMCglmm fit reports 95% HPD (highest posterior density) intervals, whereas brms reports 95% equal-tailed credible intervals (CI). The two intervals serve a similar purpose, but they are constructed differently and can differ, especially for skewed posterior distributions such as those of variances. In this tutorial, we therefore report 95% equal-tailed intervals for both packages (the Q2.5 and Q97.5 columns of summarise_chains() and posterior_summary()), so that the packages are compared on the same footing.

Highest Posterior Density (HPD) Interval:

The HPD interval is the narrowest interval that contains a specified proportion (e.g., 95%) of the posterior distribution. It includes the most probable values of the parameter, ensuring that every point inside the interval has a higher posterior density than any point outside.

Equal-Tailed Credible Interval (CI):

An equal-tailed credible interval includes the central 95% of the posterior distribution, leaving equal probabilities (2.5%) in each tail. This means that there’s a 2.5% chance that the parameter is below the interval and a 2.5% chance that it’s above.

See, for example, the documentation of HDInterval::hdi() for more details.

brms
default_priors2 <- default_prior(
                         zLength ~ zTemp + (1|gr(Phylo, cov = A)), 
                         data = dt, 
                         data2 = list(A = A),
                         family = gaussian()
                         )

system.time(
  brms_mg2 <- brm(zLength ~ zTemp + (1|gr(Phylo, cov = A)),
                  data = dt, 
                  data2 = list(A = A),
                  family = gaussian(), 
                  prior = default_priors2,
                  iter = 10000, 
                  warmup = 5000, 
                  thin = 1,
                  chains = 4,
                  cores = 4,
                  seed = 20269226,
                  control = list(adapt_delta = 0.99, max_treedepth = 15)
                )
             )
summary(brms_mg2)
#>  Family: gaussian 
#>   Links: mu = identity 
#> Formula: zLength ~ zTemp + (1 | gr(Phylo, cov = A)) 
#>    Data: dt (Number of observations: 223) 
#>   Draws: 4 chains, each with iter = 10000; warmup = 5000; thin = 1;
#>          total post-warmup draws = 20000
#>
#> Multilevel Hyperparameters:
#> ~Phylo (Number of levels: 223) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     1.20      0.09     1.03     1.38 1.00     1360     2782
#>
#> Regression Coefficients:
#>           Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept    -0.16      0.59    -1.31     1.00 1.00     2935     5247
#> zTemp         0.04      0.05    -0.05     0.13 1.00     4481     8278
#>
#> Further Distributional Parameters:
#>       Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sigma     0.18      0.03     0.12     0.24 1.01      914     1289
#>
#> Draws were sampled using sampling(NUTS). 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).

sampler_check(brms_mg2)
#>          divergent max_treedepth_used         min_E_BFMI 
#>               0.00              11.00               0.11 

The posterior estimate for the effect of zTemp on zLength is 0.04, with a 95% credible interval of [−0.05, 0.13]. The interval includes zero, so there is little evidence for a relationship between standardised temperature and tail length. The intercept is −0.16 (−1.31, 1.00). As for the intercept-only model, there are no divergent transitions, but the E-BFMI is low; the chains agree and the effective sample sizes are adequate.

The results of MCMCglmm and brms are consistent: the slope of zTemp is 0.036 (−0.055, 0.127) and 0.04 (−0.05, 0.13), respectively, and the variance components are also very similar. The small remaining differences come from Monte Carlo error and the different priors, and they do not affect the conclusions.

One continuous and one categorical explanatory variable model

Finally, we add a categorical explanatory variable, Contrasting, to the above model. In this model, we test whether species with contrasting tail tips have longer tails than those without contrasting tail tips, after controlling for the effect of temperature.

MCMCglmm
system.time(
  mcmcglmm_mg3 <- run_mcmcglmm_chains(zLength ~ zTemp + Contrasting, 
                           random = ~ Phylo, 
                           ginverse = list(Phylo = inv_phylo$Ainv), 
                           prior = prior1, 
                           family = "gaussian", 
                           data = dt,
                           seeds = c(20263226, 20263227, 20263228, 20263229), # one seed per chain (four chains)
                           nitt=13000*30,
                           thin=10*30,
                           burnin=3000*30
                           ) 
            )
summarise_chains(mcmcglmm_mg3)
#>      parameter     mean      sd     Q2.5   Q97.5  rhat ess_bulk ess_tail
#> 1  (Intercept) -0.18680 0.62510 -1.44400 1.04900 1.000     3776     4015
#> 2        zTemp  0.03268 0.04540 -0.05417 0.12370 1.000     3968     3738
#> 3 Contrasting1  0.14530 0.06260  0.02089 0.26430 1.000     3866     3930
#> 4        Phylo  1.54200 0.23330  1.11900 2.04600 1.000     3919     3974
#> 5        units  0.02370 0.01059  0.00623 0.04724 1.001     3948     3774

posterior_summary(pool_chains(mcmcglmm_mg3, "VCV"))
#>        Estimate  Est.Error       Q2.5      Q97.5
#> Phylo 1.5422156 0.23329896 1.11915578 2.04637457
#> units 0.0237028 0.01058709 0.00623028 0.04723906

posterior_summary(pool_chains(mcmcglmm_mg3, "Sol"))
#>                 Estimate  Est.Error        Q2.5     Q97.5
#> (Intercept)  -0.18682738 0.62506063 -1.44395589 1.0493791
#> zTemp         0.03267584 0.04540308 -0.05417217 0.1236664
#> Contrasting1  0.14525559 0.06260417  0.02089266 0.2643381

After accounting for temperature, species with contrasting tail tips have longer tails than species without them (Contrasting1: 0.145, 95% CI [0.021, 0.264]; the interval excludes zero). The temperature effect remains small and uncertain (0.033 [−0.054, 0.124]).

brms
default_priors3 <- default_prior(
                         zLength ~ zTemp + Contrasting + (1|gr(Phylo, cov = A)), 
                         data = dt, 
                         data2 = list(A = A),
                         family = gaussian()
                         )

system.time(
  brms_mg3 <- brm(zLength ~ zTemp + Contrasting + (1|gr(Phylo, cov = A)),
                  data = dt, 
                  data2 = list(A = A),
                  family = gaussian(), 
                  prior = default_priors3,
                  iter = 10000, 
                  warmup = 5000, 
                  thin = 1,
                  chains = 4,
                  cores = 4,
                  seed = 20269326,
                  control = list(adapt_delta = 0.99, max_treedepth = 15)
                )
             )
# sampler diagnostics first: this fit did NOT sample successfully
sampler_check(brms_mg3)
#>          divergent max_treedepth_used         min_E_BFMI 
#>            4931.00              10.00               0.08 

# posterior summaries of the key parameters for each chain separately
draws_mg3 <- as_draws_df(brms_mg3)
aggregate(cbind(sigma, sd_Phylo__Intercept, b_Contrasting1) ~ .chain, data = draws_mg3, FUN = median)
#>   .chain      sigma sd_Phylo__Intercept b_Contrasting1
#> 1      1 0.15780387            1.228627      0.1465252
#> 2      2 0.02097356            1.380296      0.2211975
#> 3      3 0.15121142            1.241000      0.1427636
#> 4      4 0.15256776            1.235058      0.1453377

The brms fit of this model is a sampling failure. With four chains, adapt_delta = 0.99 and max_treedepth = 15, one chain became trapped in a region with a very small residual standard deviation (sigma close to 0.02, compared with about 0.15 in the other chains) and produced 4,931 divergent transitions. As a result, R-hat is far above 1.01 (up to 1.2) and the bulk ESS of several parameters is below 10. The other three chains agree with each other, but discarding the failed chain would not make the fit valid, so we do not interpret the brms estimates of this model.

The problem is the posterior geometry of these Gaussian models with one observation per species: the residual standard deviation and the 223 phylogenetic effects are strongly dependent (a “funnel”). This also causes the low E-BFMI values of the brms fits of the two simpler models (see above), although those fits have no divergent transitions and their chains agree. Possible remedies are a different (e.g. centred or marginalised) parameterisation of the phylogenetic effect, which is beyond the scope of this tutorial. For this model, we therefore rely on the MCMCglmm fit, whose four chains converged.

Important

This example shows why the sampler diagnostics must be checked for every fit, not only for the first model. The summary() of a failed fit still prints estimates that look plausible, and the warnings about divergent transitions are easy to overlook when many models are fitted.

Bivariate model

Here, we provide a more advanced extension: a bivariate model. A bivariate model has a structure similar to the nominal model (several linear predictors with correlated random effects), and it allows us to examine the relationship between two response variables while accounting for explanatory variables. In this model, we will include two response variables, zLength and zMass, and test the relationship between these two variables and the explanatory variables, zTemp and Contrasting. This model can examine the evolutionary correlation between tail length and body mass in rodents, considering the phylogenetic relationships.

In MCMCglmm, we use cbind(traitA, traitB) to set the response variables (this allows us to estimate the covariance between the two response variables). In brms, we use mvbind(traitA, traitB) to set the response variables. The family argument specifies the distribution (and link function) of the response variables; if a single family is given, it is used for both responses. It does not specify the distribution of the random effects, which are always assumed to be (multivariate) normal.

An important point is that MCMCglmm estimates the covariances between the two response variables (with us(trait)), not their correlations, whereas brms reports correlations directly. Two different correlations are involved:

  • Phylogenetic (group-level) correlation: in brms, we write (1|a|gr(Phylo, cov = A)) instead of (1|gr(Phylo, cov = A)). The shared ID a (any label can be used) tells brms that the phylogenetic effects of the two responses are correlated; without it, they are modelled as independent.
  • Residual correlation: set_rescor(TRUE) estimates the correlation between the residuals of the two responses. This is a separate quantity and is available only for some families (e.g. Gaussian); it is unrelated to the phylogenetic correlation.

In MCMCglmm, we calculate each correlation from the (co)variance estimates of each posterior draw, by dividing the covariance by the product of the standard deviations of the two traits.

inv_phylo <- inverseA(tree, nodes = "ALL", scale = TRUE)
prior2 <- list(G = list(G1 = list(V = diag(2), 
                                  nu = 2, alpha.mu = rep(0, 2), 
                                  alpha.V = diag(2) * 1000)),
               R = list(V = diag(2), nu = 0.002)
              )

system.time(
        mcmcglmm_mg4 <- run_mcmcglmm_chains(cbind(zLength, zMass) ~ trait - 1,
                        random = ~ us(trait):Phylo,
                       rcov = ~ us(trait):units,
                       family = c("gaussian", "gaussian"),
                       ginv = list(Phylo = inv_phylo$Ainv),
                       data = dt, 
                       prior = prior2,
                       seeds = c(20263326, 20263327, 20263328, 20263329), # one seed per chain (four chains)
                       nitt = 13000*55,
                       thin = 10*55,
                      burnin = 3000*55
                                )
        )


system.time(
        mcmcglmm_mg5 <- run_mcmcglmm_chains(cbind(zLength, zMass) ~ zTemp:trait + trait - 1,
                        random = ~ us(trait):Phylo,
                        rcov = ~ us(trait):units,
                        family = c("gaussian", "gaussian"),
                        ginv = list(Phylo = inv_phylo$Ainv),
                        data = dt, 
                        prior = prior2,
                        seeds = 1:4, # extension: results are not shown in this tutorial
                        nitt = 13000*55,
                        thin = 10*55,
                        burnin = 3000*55
                        )
        )

system.time(
        mcmcglmm_mg6 <- run_mcmcglmm_chains(cbind(zLength, zMass) ~ zTemp:trait + Contrasting:trait + trait -1,
                        random = ~ us(trait):Phylo,
                        rcov = ~ us(trait):units,
                        family = c("gaussian", "gaussian"),
                        ginv = list(Phylo = inv_phylo$Ainv),
                        data = dt, 
                        prior = prior2,
                        seeds = 1:4, # extension: results are not shown in this tutorial
                        nitt = 13000*55,
                        thin = 10*55,
                        burnin = 3000*55
                        )
        )


## brms
A <- ape::vcv.phylo(tree, corr = TRUE)

formula <- bf(mvbind(zLength, zMass) ~ 1 + 
               (1|a|gr(Phylo, cov = A)), 
              set_rescor(rescor = TRUE)
              )

default_prior1 <- default_prior(formula, 
                               data = dt, 
                               data2 = list(A = A),
                               family = gaussian()
                               )

system.time(
  brms_mg4 <- brm(formula = formula, 
                data = dt, 
                data2 = list(A = A),
                family = gaussian(), 
                prior = default_prior1,
                iter = 35000, 
                warmup = 25000, 
                thin = 1,
                chains = 4,
                cores = 4,
                seed = 20272226,
                control = list(adapt_delta = 0.99)
                )
             )

formula2 <- bf(mvbind(zLength, zMass) ~ zTemp + (1|a|gr(Phylo, cov = A)), set_rescor(TRUE))

default_prior2 <- default_prior(formula2, 
                               data = dt, 
                               data2 = list(A = A),
                               family = gaussian()
                               )

system.time(
  brms_mg5 <- brm(formula = formula2, 
                data = dt, 
                data2 = list(A = A),
                family = gaussian(), 
                prior = default_prior2,
                iter = 55000, 
                warmup = 45000, 
                thin = 1,
                chains = 4,
                cores = 4, # extension: results are not shown in this tutorial
                control = list(adapt_delta = 0.99)
                )
             )

formula3 <- bf(mvbind(zLength, zMass) ~ zTemp + Contrasting + (1|a|gr(Phylo, cov = A)), set_rescor(TRUE))

default_prior3 <- default_prior(formula3, 
                               data = dt, 
                               data2 = list(A = A),
                               family = gaussian()
                               )

system.time(
   brms_mg6 <- brm(formula = formula3,  
                data = dt, 
                data2 = list(A = A),
                family = gaussian(), 
                prior = default_prior3,
                iter = 35000, 
                warmup = 10000, 
                thin = 1,
                chains = 4,
                cores = 4, # extension: results are not shown in this tutorial
                control = list(adapt_delta = 0.99)
                )
              )
summarise_chains(mcmcglmm_mg4)
#>                          parameter      mean       sd      Q2.5   Q97.5  rhat ess_bulk ess_tail
#> 1                     traitzLength -0.169000 0.627700 -1.371000 1.08800 1.000     3478     3800
#> 2                       traitzMass -0.441100 0.671500 -1.762000 0.89030 1.001     3961     3935
#> 3  traitzLength:traitzLength.Phylo  1.523000 0.227300  1.135000 2.01200 1.000     3731     4075
#> 4    traitzMass:traitzLength.Phylo -0.098280 0.154900 -0.408500 0.20720 1.000     4062     3702
#> 5    traitzLength:traitzMass.Phylo -0.098280 0.154900 -0.408500 0.20720 1.000     4062     3702
#> 6      traitzMass:traitzMass.Phylo  1.690000 0.241400  1.272000 2.20400 1.000     3918     3971
#> 7  traitzLength:traitzLength.units  0.028000 0.010500  0.010290 0.05145 1.001     4245     4030
#> 8    traitzMass:traitzLength.units  0.006463 0.007942 -0.008468 0.02276 1.000     4189     3691
#> 9    traitzLength:traitzMass.units  0.006463 0.007942 -0.008468 0.02276 1.000     4189     3691
#> 10     traitzMass:traitzMass.units  0.062630 0.014050  0.039370 0.09475 1.000     4037     3999

posterior_summary(pool_chains(mcmcglmm_mg4, "VCV"))
#>                                     Estimate   Est.Error         Q2.5      Q97.5
#> traitzLength:traitzLength.Phylo  1.522956574 0.227298775  1.135194847 2.01208051
#> traitzMass:traitzLength.Phylo   -0.098275892 0.154877997 -0.408486575 0.20723699
#> traitzLength:traitzMass.Phylo   -0.098275892 0.154877997 -0.408486575 0.20723699
#> traitzMass:traitzMass.Phylo      1.690069097 0.241438904  1.271843051 2.20377962
#> traitzLength:traitzLength.units  0.028002415 0.010501743  0.010288271 0.05144526
#> traitzMass:traitzLength.units    0.006462884 0.007941619 -0.008467541 0.02275695
#> traitzLength:traitzMass.units    0.006462884 0.007941619 -0.008467541 0.02275695
#> traitzMass:traitzMass.units      0.062626562 0.014051796  0.039370515 0.09475061

posterior_summary(pool_chains(mcmcglmm_mg4, "Sol"))
#>                Estimate Est.Error      Q2.5     Q97.5
#> traitzLength -0.1689755 0.6276594 -1.371252 1.0877905
#> traitzMass   -0.4411276 0.6714691 -1.762460 0.8903338

# Phylogenetic and residual (non-phylogenetic) correlations between zLength and zMass,
# calculated for each posterior draw: covariance / (SD1 * SD2)
VCV <- pool_chains(mcmcglmm_mg4, "VCV")
corr_p <- VCV[, "traitzMass:traitzLength.Phylo"] /
  sqrt(VCV[, "traitzLength:traitzLength.Phylo"] * VCV[, "traitzMass:traitzMass.Phylo"])
corr_nonp <- VCV[, "traitzMass:traitzLength.units"] /
  sqrt(VCV[, "traitzLength:traitzLength.units"] * VCV[, "traitzMass:traitzMass.units"])
posterior_summary(corr_p)
#>         Estimate Est.Error       Q2.5     Q97.5
#> var1 -0.06237168 0.0963816 -0.2500739 0.1273954

posterior_summary(corr_nonp)
#>       Estimate Est.Error       Q2.5     Q97.5
#> var1 0.1554446  0.185267 -0.2161848 0.4952001

summary(brms_mg4)
#>  Family: MV(gaussian, gaussian) 
#>   Links: mu = identity
#>          mu = identity 
#> Formula: zLength ~ 1 + (1 | a | gr(Phylo, cov = A)) 
#>          zMass ~ 1 + (1 | a | gr(Phylo, cov = A)) 
#>    Data: dt (Number of observations: 223) 
#>   Draws: 4 chains, each with iter = 35000; warmup = 25000; thin = 1;
#>          total post-warmup draws = 40000
#>
#> Multilevel Hyperparameters:
#> ~Phylo (Number of levels: 223) 
#>                                        Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(zLength_Intercept)                      1.21      0.09     1.04     1.39 1.00     2813     7426
#> sd(zMass_Intercept)                        1.29      0.09     1.12     1.48 1.00     7806    16716
#> cor(zLength_Intercept,zMass_Intercept)    -0.06      0.10    -0.26     0.13 1.00     5211    10724
#>
#> Regression Coefficients:
#>                   Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> zLength_Intercept    -0.14      0.60    -1.31     1.04 1.00     8144    14872
#> zMass_Intercept      -0.42      0.63    -1.66     0.81 1.00    10395    16326
#>
#> Further Distributional Parameters:
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sigma_zLength     0.17      0.03     0.11     0.24 1.00     1851     3159
#> sigma_zMass       0.25      0.03     0.20     0.31 1.00     5465    12265
#>
#> Residual Correlations: 
#>                       Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> rescor(zLength,zMass)     0.15      0.18    -0.21     0.48 1.00     4216     8216
#>
#> Draws were sampled using sampling(NUTS). 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).

The two packages give consistent results. The phylogenetic correlation between tail length and body mass is close to zero, with 95% credible intervals that include zero: −0.06 (95% CI −0.25, 0.13) in MCMCglmm and −0.06 (−0.26, 0.13) in brms. The residual (non-phylogenetic) correlation is also uncertain: 0.16 (−0.22, 0.50) and 0.15 (−0.21, 0.48), respectively. As in the univariate models, the brms fit has no divergent transitions but a low E-BFMI (0.17), and its four chains agree (R-hat < 1.01).

Below, we compare the variance components and the phylogenetic heritabilities of the two traits. Each quantity is calculated for every posterior draw (for brms, by squaring the standard deviations draw by draw) before summarising.

draws_df <- as_draws_df(brms_mg4) # Convert brms object to data frame
VCV <- pool_chains(mcmcglmm_mg4, "VCV")

# phylogenetic variance - length
posterior_summary(VCV[, "traitzLength:traitzLength.Phylo"]) # MCMCglmm (G-structure)
#>      Estimate Est.Error     Q2.5    Q97.5
#> var1 1.522957 0.2272988 1.135195 2.012081

posterior_summary(draws_df$sd_Phylo__zLength_Intercept^2)    # brms (SD squared for each draw)
#>      Estimate Est.Error     Q2.5   Q97.5
#> [1,] 1.478843 0.2174682 1.089848 1.94505

# phylogenetic variance - mass
posterior_summary(VCV[, "traitzMass:traitzMass.Phylo"])
#>      Estimate Est.Error     Q2.5   Q97.5
#> var1 1.690069 0.2414389 1.271843 2.20378

posterior_summary(draws_df$sd_Phylo__zMass_Intercept^2)
#>      Estimate Est.Error     Q2.5    Q97.5
#> [1,] 1.673684 0.2353912 1.258035 2.179566

# residual variance - length
posterior_summary(VCV[, "traitzLength:traitzLength.units"]) # MCMCglmm (R-structure)
#>        Estimate  Est.Error       Q2.5      Q97.5
#> var1 0.02800242 0.01050174 0.01028827 0.05144526

posterior_summary(draws_df$sigma_zLength^2)                  # brms
#>        Estimate  Est.Error       Q2.5      Q97.5
#> [1,] 0.03088028 0.01090177 0.01288607 0.05529436

# residual variance - mass
posterior_summary(VCV[, "traitzMass:traitzMass.units"])
#>        Estimate Est.Error       Q2.5      Q97.5
#> var1 0.06262656 0.0140518 0.03937052 0.09475061

posterior_summary(draws_df$sigma_zMass^2)
#>        Estimate  Est.Error       Q2.5      Q97.5
#> [1,] 0.06410401 0.01452617 0.03986941 0.09650143

# phylogenetic heritability - length (calculated for each draw)
h2_length_mcmcglmm <- VCV[, "traitzLength:traitzLength.Phylo"] /
  (VCV[, "traitzLength:traitzLength.Phylo"] + VCV[, "traitzLength:traitzLength.units"])
h2_length_brms <- with(draws_df, sd_Phylo__zLength_Intercept^2 /
                         (sd_Phylo__zLength_Intercept^2 + sigma_zLength^2))
posterior_summary(h2_length_mcmcglmm)
#>       Estimate Est.Error      Q2.5     Q97.5
#> var1 0.9810507 0.0088351 0.9608274 0.9945062

posterior_summary(h2_length_brms)
#>       Estimate   Est.Error      Q2.5     Q97.5
#> [1,] 0.9786051 0.009547454 0.9559388 0.9927668

# phylogenetic heritability - mass
h2_mass_mcmcglmm <- VCV[, "traitzMass:traitzMass.Phylo"] /
  (VCV[, "traitzMass:traitzMass.Phylo"] + VCV[, "traitzMass:traitzMass.units"])
h2_mass_brms <- with(draws_df, sd_Phylo__zMass_Intercept^2 /
                       (sd_Phylo__zMass_Intercept^2 + sigma_zMass^2))
posterior_summary(h2_mass_mcmcglmm)
#>       Estimate  Est.Error      Q2.5     Q97.5
#> var1 0.9632801 0.01096532 0.9385308 0.9803033

posterior_summary(h2_mass_brms)
#>      Estimate  Est.Error      Q2.5     Q97.5
#> [1,] 0.962113 0.01134253 0.9356861 0.9797784

The phylogenetic variances are 1.52 (95% CI 1.14, 2.01) for tail length and 1.69 (1.27, 2.20) for body mass in MCMCglmm, and 1.48 (1.09, 1.95) and 1.67 (1.26, 2.18) in brms. Both traits have very high phylogenetic heritabilities: 0.98 (0.96, 0.99) and 0.96 (0.94, 0.98) for tail length and body mass in both packages. On the modelled scale, almost all of the variation in both traits (after standardisation) is attributed to the phylogenetic random effect, i.e. closely related species have similar tail lengths and body masses.

2. Binary models

Binary models are used when the response variable is binary (0 or 1), such as presence (1) vs. absence (0), survival (1) vs. death (0), success (1) vs. failure (0), or female vs. male. The model describes the probability of the event coded as 1 (the “success”) given the predictor variables. Which category is treated as the event depends on how the response is coded: for a factor, the first level is the reference (failure) and the second level is the event, and by default factor levels are sorted alphabetically. It is therefore good practice to code the response explicitly (e.g. 0 = absent, 1 = present) or set the factor levels yourself.

Explanation of dataset

We tested the relationships between the presence of red–orange pelage on limbs (response variable) and average social group size and activity cycle in primates (265 species). We used the dataset and tree from Macdonald et al. (2024). The dataset contains information on skin and pelage coloration, average social group size, the presence or absence of multilevel hierarchical societies, the colour visual system, and the activity cycle. We aim to test whether the presence of red–orange pelage on limbs is associated with social group size and activity cycle in primates.

Note

Presence of red–orange pelage on limbs:

0 = Absent. Species does not have red–orange pelage on limbs. 1 = Present. Species has red–orange pelage on limbs.

Average social group size: Continuous variable.

Activity cycle: Each species was categorised as nocturnal (noct), cathemeral (cath) or diurnal (di).

To run the model, we need to prepare the data in a suitable format.

p_trees <- read.nexus(here("data", "potential", "primate", "trees100m.nex"))
primate_data <- read.csv(here("data",  "potential", "primate", "Primate_data_male.csv"))
p_dat <- primate_data

p_dat <- subset(p_dat, !is.na(vs_male))     # omit NAs by VS predictor
p_dat <- subset(p_dat, !is.na(vs_female))
p_dat <- subset(p_dat, !is.na(activity_cycle)) # omit NAs by activity cycle
p_dat <- subset(p_dat, !is.na(redpeachpink_facial_skin))  # omit NAs in facial skin colour
p_dat <- subset(p_dat, !is.na(social_group_size))  # omit NAs in social group size
p_dat <- subset(p_dat, !is.na(multilevel))  # omit NAs in multilevel society

# Rename the column 'social_group_size' to 'cSocial_group_size' - all continuous variables were centred and standardized by authors
p_dat <- p_dat %>% rename(cSocial_group_size = social_group_size)

p_dat <- p_dat %>%
  mutate(across(c(vs_female, vs_male, redpeachpink_facial_skin,red_genitals,red_pelage_head, red_pelage_body_limbs, red_pelage_tail), as.factor))

p_tree <- p_trees[[1]]
p_trees <- lapply(p_trees, drop.tip,tip = setdiff(p_tree$tip.label, p_dat$PhyloName)) #trim out everything from the tree that's not in the dataset
p_tree <- p_trees[[1]] # select one tree for trimming purposes
p_tree <- force.ultrametric(p_tree) # force tree to be ultrametric - all tips equidistant from root

How to implement models and interpret the outputs?

In MCMCglmm, the binary model can take two kinds of link functions: logit and probit link functions. The logit link function (family = "categorical") is the default in MCMCglmm for the binary model, while the probit link function can be defined using family = "threshold" or family = "ordinal". We recommend using family = "threshold", because it corresponds directly to the standard probit (threshold) model with a residual variance of 1, which has a natural interpretation as an underlying continuous liability, and it often mixes better. Here, we show both models.

Univariate model

Intercept-only model

Probit model
MCMCglmm

We need to set a different prior for binary models from the Gaussian model. Binary models still have unexplained variation, but the observation-level (residual) variance on the latent scale cannot be estimated from 0/1 data: it is not identifiable separately from the scale of the other parameters. It is therefore fixed by convention, and we only set priors for the random effects. In MCMCglmm, we fix the residual (units) variance at 1 with R = list(V = 1, fix = 1), for both the probit (threshold) and the logit (categorical) model. On the latent scale, the distribution-specific variance is 1 for the probit link and \(\pi^2/3\) for the logit link; we use these values when calculating variance proportions below.

inv.phylo <- inverseA(p_tree, nodes = "ALL", scale = TRUE)

prior1 <- list(R = list(V = 1, fix = 1), # fix residual variance = 1
               G = list(G1 = list(V = 1, nu = 1, alpha.mu = 0, alpha.V = 10)
                       )
               )
system.time(
  mcmcglmm_BP1 <- run_mcmcglmm_chains(red_pelage_body_limbs ~ 1, 
                            random = ~ PhyloName, 
                            family = "threshold", 
                            data = p_dat, 
                            prior = prior1, 
                            ginverse = list(PhyloName = inv.phylo$Ainv),
                            seeds = c(20263426, 20263427, 20263428, 20263429), # one seed per chain (four chains)
                            nitt = 13000*20, 
                            thin = 10*20, 
                            burnin = 3000*20)
                 )
summarise_chains(mcmcglmm_BP1) # pooled draws of four chains; 95% equal-tailed intervals
#>     parameter    mean     sd    Q2.5  Q97.5  rhat ess_bulk ess_tail
#> 1 (Intercept) -0.8754 0.9221 -2.9640 0.8567 1.001     3782     3772
#> 2   PhyloName  3.1730 2.0310  0.6637 8.1880 1.001     3733     4101
#> 3       units  1.0000 0.0000  1.0000 1.0000    NA       NA       NA

posterior_summary(pool_chains(mcmcglmm_BP1, "VCV")) # 95% CI
#>           Estimate Est.Error     Q2.5    Q97.5
#> PhyloName 3.173375  2.031145 0.663664 8.188148
#> units     1.000000  0.000000 1.000000 1.000000

posterior_summary(pool_chains(mcmcglmm_BP1, "Sol"))
#>             Estimate Est.Error      Q2.5     Q97.5
#> (Intercept) -0.87537 0.9221209 -2.963645 0.8567179

As you can see here, the residual variance is fixed at 1: the model did not estimate the residual variance (R-structure). The four chains mix well and agree with each other (R-hat < 1.01). The posterior mean of the phylogenetic variance is 3.17 (95% CI 0.66, 8.19) on the latent (probit) scale, which suggests that the presence of red pelage on the limbs is similar among closely related species. Note that a variance cannot be negative, so its credible interval can never include zero exactly; that the lower limit is away from zero is therefore weaker evidence than for a fixed effect. We assess how much of the variation is attributed to phylogeny with the variance proportion below.

brms

The family argument is set to bernoulli() (not binomial()). The bernoulli() family is used for binary (0/1) data, while the binomial() family is used for the number of successes out of a known number of trials (e.g. N_success | trials(N_trials)).

A <- ape::vcv.phylo(p_tree, corr = TRUE)

priors_brms <- default_prior(red_pelage_body_limbs ~ 1 + (1 | gr(PhyloName, cov = A)), 
                             data = p_dat, 
                             data2 = list(A = A),
                             family = bernoulli(link = "probit"))

system.time(
  brms_BP1 <- brm(red_pelage_body_limbs ~ 1 + (1 | gr(PhyloName, cov = A)), 
                 data = p_dat, 
                 data2 = list(A = A),
                 family = bernoulli(link = "probit"),
                 prior = priors_brms,
                 iter = 6000, 
                 warmup = 5000, 
                 thin = 1,
                 chains = 4,
                 cores = 4,
                 seed = 20271026,
                 control = list(adapt_delta = 0.95)
  )
)
summary(brms_BP1)
#>  Family: bernoulli 
#>   Links: mu = probit 
#> Formula: red_pelage_body_limbs ~ 1 + (1 | gr(PhyloName, cov = A)) 
#>    Data: p_dat (Number of observations: 265) 
#>   Draws: 4 chains, each with iter = 6000; warmup = 5000; thin = 1;
#>          total post-warmup draws = 4000
#>
#> Multilevel Hyperparameters:
#> ~PhyloName (Number of levels: 265) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     1.68      0.52     0.80     2.88 1.01      992     1772
#>
#> Regression Coefficients:
#>           Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept    -0.77      0.82    -2.53     0.85 1.00     2458     2581
#>
#> Draws were sampled using sampling(NUTS). 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).

brms gave similar results. Note that the phylogenetic random effect in brms is reported as a standard deviation (sd), so to compare it with MCMCglmm we need to square it (draw by draw). The intercepts were similar in brms and MCMCglmm. brms summaries report 95% equal-tailed credible intervals.

Phylogenetic signals (probit model)
# MCMCglmm: the residual (units) variance is fixed at 1,
# which is the latent-scale residual variance of the probit (threshold) model
h2_mcmcglmm_BP <- pool_chains(mcmcglmm_BP1, "VCV")[, "PhyloName"] / (pool_chains(mcmcglmm_BP1, "VCV")[, "PhyloName"] + 1)
posterior_summary(h2_mcmcglmm_BP)
#>       Estimate Est.Error     Q2.5     Q97.5
#> var1 0.7084735 0.1290554 0.398917 0.8911641

# brms: probit latent-scale residual variance = 1 (SD squared for each draw)
h2_brms_BP <- as_draws_df(brms_BP1) %>%
  mutate(h2 = sd_PhyloName__Intercept^2 / (sd_PhyloName__Intercept^2 + 1)) %>%
  pull(h2)
posterior_summary(h2_brms_BP)
#>      Estimate Est.Error      Q2.5     Q97.5
#> [1,] 0.704689 0.1284413 0.3900843 0.8926042

The proportion of the latent-scale variance attributed to the phylogenetic random effect is 0.71 (95% CI 0.40, 0.89) in MCMCglmm and 0.70 (0.39, 0.89) in brms. The difference from the Gaussian model is that the residual variance is not estimated: for the probit link, the latent-scale residual variance is 1. The same applies to the other probit (threshold) models in this tutorial.

Logit model
MCMCglmm

c2 correction

In the logit model, MCMCglmm always includes an additive overdispersion (residual, units) term on the latent scale, here fixed at 1. The estimates are therefore on a different scale from a standard logistic regression without this term, such as the one fitted by brms: the location effects and variance components are inflated (see also Nakagawa & Schielzeth 2010). To make them comparable, we rescale the location effects and the variance components with the constant \(c^2 = (16\sqrt{3}/(15\pi))^2\), which approximately integrates out the residual term (see the MCMCglmm course notes by Jarrod Hadfield). We use the code below:

c2 <- (16 * sqrt(3) / (15 * pi))^2
res_1 <- model$Sol / sqrt(1+c2) # for fixed effects
res_2 <- model$VCV / (1+c2) # for variance components

In MCMCglmm, the logit model usually mixes more slowly than the probit model, so we use larger values of nitt, thin and burnin than for the probit model to obtain enough effective samples.

inv.phylo <- inverseA(p_tree, nodes = "ALL", scale = TRUE)
prior1 <- list(R = list(V = 1, fix = 1),
               G = list(G1 = list(V = 1, nu = 1, alpha.mu = 0, alpha.V = 10)
                       )
               )

system.time(
mcmcglmm_BL1 <- run_mcmcglmm_chains(red_pelage_body_limbs ~ 1, 
                          random = ~ PhyloName, 
                          family = "categorical", 
                          data = p_dat, 
                          prior = prior1, 
                          ginverse = list(PhyloName = inv.phylo$Ainv),
                          seeds = c(20263526, 20263527, 20263528, 20263529), # one seed per chain (four chains)
                          nitt = 13000*60, 
                          thin = 10*60, 
                          burnin = 3000*60)
)
brms

In brms, we do not apply the c2 correction. The brms Bernoulli-logit model has no additive overdispersion term, so its estimates are already on the scale of a standard logistic regression; the MCMCglmm-specific c2 transformation does not apply to it.

A <- ape::vcv.phylo(p_tree, corr = TRUE)

priors_brms2 <- default_prior(red_pelage_body_limbs ~ 1 + (1 | gr(PhyloName, cov = A)), 
                             data = p_dat, 
                             data2 = list(A = A),
                             family = bernoulli(link = "logit"))

system.time(
  brms_BL1 <- brm(red_pelage_body_limbs ~ 1 + (1 | gr(PhyloName, cov = A)), 
                 data = p_dat, 
                 data2 = list(A = A),
                 family = bernoulli(link = "logit"),
                 prior = priors_brms2,
                 iter = 7500, 
                 warmup = 6500, 
                 thin = 1,
                 chains = 4,
                 cores = 4,
                 seed = 20271126,
                 control = list(adapt_delta = 0.95),
  )
)
# MCMCglmm
summarise_chains(mcmcglmm_BL1)
#>     parameter   mean    sd   Q2.5  Q97.5  rhat ess_bulk ess_tail
#> 1 (Intercept) -1.671 1.802 -5.640  1.718 1.001     3936     3778
#> 2   PhyloName 11.320 7.403  2.033 30.350 1.000     3755     3728
#> 3       units  1.000 0.000  1.000  1.000    NA       NA       NA

c2 <- (16 * sqrt(3) / (15 * pi))^2
res_1 <- pool_chains(mcmcglmm_BL1, "Sol") / sqrt(1 + c2) # c2-corrected fixed effects
res_2 <- pool_chains(mcmcglmm_BL1, "VCV") / (1 + c2)     # c2-corrected variance components
posterior_summary(res_1)
#>              Estimate Est.Error      Q2.5    Q97.5
#> (Intercept) -1.440472  1.553058 -4.861265 1.480952

posterior_summary(res_2)
#>            Estimate Est.Error      Q2.5      Q97.5
#> PhyloName 8.4083590   5.50044 1.5102077 22.5484656
#> units     0.7430287   0.00000 0.7430287  0.7430287

# brms
summary(brms_BL1)
#>  Family: bernoulli 
#>   Links: mu = logit 
#> Formula: red_pelage_body_limbs ~ 1 + (1 | gr(PhyloName, cov = A)) 
#>    Data: p_dat (Number of observations: 265) 
#>   Draws: 4 chains, each with iter = 7500; warmup = 6500; thin = 1;
#>          total post-warmup draws = 4000
#>
#> Multilevel Hyperparameters:
#> ~PhyloName (Number of levels: 265) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     2.56      0.82     1.13     4.34 1.01      976     1645
#>
#> Regression Coefficients:
#>           Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept    -1.00      1.16    -3.47     1.20 1.00     2546     2282
#>
#> Draws were sampled using sampling(NUTS). 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).

After the c2 correction, the point estimates and 95% CIs of the fixed and random effects are similar between the two packages.

Phylogenetic signals (logit model)

Then, we can obtain the proportion of the latent-scale variance attributed to phylogeny using the conventional distribution-specific variance of the logit link, \(\pi^2/3\):

\[ H^2 = \frac{\sigma_{a}^2}{\sigma_{a}^2 + \pi^2/3} \]

For MCMCglmm, we first apply the c2 correction to the phylogenetic variance, which puts it on the scale of a standard logistic model without the additive overdispersion (units) term, i.e. the same scale as brms. We then use the same definition.

# MCMCglmm: c2-corrected phylogenetic variance
var_phylo_BL <- pool_chains(mcmcglmm_BL1, "VCV")[, "PhyloName"] / (1 + c2)
h2_mcmcglmm_BL <- var_phylo_BL / (var_phylo_BL + pi^2 / 3)
posterior_summary(h2_mcmcglmm_BL)
#>      Estimate Est.Error      Q2.5     Q97.5
#> var1 0.661523 0.1441775 0.3146215 0.8726749

# brms
h2_brms_BL <- as_draws_df(brms_BL1) %>%
  mutate(h2 = sd_PhyloName__Intercept^2 / (sd_PhyloName__Intercept^2 + pi^2 / 3)) %>%
  pull(h2)
posterior_summary(h2_brms_BL)
#>       Estimate Est.Error     Q2.5     Q97.5
#> [1,] 0.6306616 0.1484916 0.279862 0.8515329

The two estimates are similar: 0.66 (95% CI 0.31, 0.87) in MCMCglmm and 0.63 (0.28, 0.85) in brms. They are also close to the estimates from the probit model (0.71 and 0.70), as expected, because the variance proportion is defined on the latent scale of each link function.

One continuous explanatory variable model

For the next step, we examine whether group size affects the presence or absence of red pelage on the body/limbs in primates to test the possibility that red pelage is related to social roles and behaviours within the group.

Probit model

The model using MCMCglmm is

inv.phylo <- inverseA(p_tree, nodes = "ALL", scale = TRUE)
prior1 <- list(R = list(V = 1, fix = 1),
               G = list(G1 = list(V = 1, nu = 1, alpha.mu = 0, alpha.V = 10)
                       )
               )

system.time(
  mcmcglmm_BP2 <- run_mcmcglmm_chains(red_pelage_body_limbs ~ cSocial_group_size, 
                            random = ~ PhyloName, 
                            family = "threshold", 
                            data = p_dat, 
                            prior = prior1, 
                            ginverse = list(PhyloName = inv.phylo$Ainv),
                            seeds = c(20263626, 20263627, 20263628, 20263629), # one seed per chain (four chains)
                            nitt = 13000*30, 
                            thin = 10*30, 
                            burnin = 3000*30)
            )

For brms…

A <- ape::vcv.phylo(p_tree, corr = TRUE)

priors_brms3 <- default_prior(red_pelage_body_limbs ~ cSocial_group_size + (1 | gr(PhyloName, cov = A)), 
                             data = p_dat, 
                             data2 = list(A = A),
                             family = bernoulli(link = "probit"))

system.time(
  brms_BP2 <- brm(red_pelage_body_limbs ~ cSocial_group_size + (1 | gr(PhyloName, cov = A)), 
                 data = p_dat, 
                 data2 = list(A = A),
                 family = bernoulli(link = "probit"),
                 prior = priors_brms3,
                 iter = 10000, 
                 warmup = 8500, 
                 thin = 1,
                 chains = 4,
                 cores = 4,
                 seed = 20271226,
                 control = list(adapt_delta = 0.95)
         )
      )
# MCMCglmm
summarise_chains(mcmcglmm_BP2)
#>            parameter    mean     sd    Q2.5  Q97.5 rhat ess_bulk ess_tail
#> 1        (Intercept) -0.9072 0.9514 -2.9660 0.7824    1     3987     3773
#> 2 cSocial_group_size -0.1087 0.1342 -0.4051 0.1209    1     3704     3686
#> 3          PhyloName  3.2910 2.2100  0.6581 8.9400    1     3790     3500
#> 4              units  1.0000 0.0000  1.0000 1.0000   NA       NA       NA

posterior_summary(pool_chains(mcmcglmm_BP2, "Sol"))
#>                      Estimate Est.Error      Q2.5     Q97.5
#> (Intercept)        -0.9072480 0.9514194 -2.966058 0.7824094
#> cSocial_group_size -0.1086576 0.1342213 -0.405128 0.1208716

                     # Estimate Est.Error       Q2.5     Q97.5
posterior_summary(pool_chains(mcmcglmm_BP2, "VCV"))
#>           Estimate Est.Error      Q2.5    Q97.5
#> PhyloName 3.291433  2.210177 0.6580979 8.940137
#> units     1.000000  0.000000 1.0000000 1.000000

#brms
summary(brms_BP2)
#>  Family: bernoulli 
#>   Links: mu = probit 
#> Formula: red_pelage_body_limbs ~ cSocial_group_size + (1 | gr(PhyloName, cov = A)) 
#>    Data: p_dat (Number of observations: 265) 
#>   Draws: 4 chains, each with iter = 10000; warmup = 8500; thin = 1;
#>          total post-warmup draws = 6000
#>
#> Multilevel Hyperparameters:
#> ~PhyloName (Number of levels: 265) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     1.69      0.54     0.79     2.92 1.00     1350     2296
#>
#> Regression Coefficients:
#>                    Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept             -0.81      0.84    -2.66     0.76 1.00     2410     2342
#> cSocial_group_size    -0.11      0.13    -0.39     0.12 1.00     6308     4171
#>
#> Draws were sampled using sampling(NUTS). 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).

Neither model provides evidence that social group size is associated with the presence of red pelage on the body/limbs in primates: in both packages, the 95% credible intervals of the slope include zero (MCMCglmm: −0.11, 95% CI −0.41, 0.12; brms: −0.11, −0.39, 0.12).

Logit model

The model for MCMCglmm…

inv.phylo <- inverseA(p_tree, nodes = "ALL", scale = TRUE)
prior1 <- list(R = list(V = 1, fix = 1),
               G = list(G1 = list(V = 1, nu = 1, alpha.mu = 0, alpha.V = 10)
                       )
               )

system.time(
  mcmcglmm_BL2 <- run_mcmcglmm_chains(red_pelage_body_limbs ~ cSocial_group_size, 
                            random = ~ PhyloName, 
                            family = "categorical", 
                            data = p_dat, 
                            prior = prior1, 
                            ginverse = list(PhyloName = inv.phylo$Ainv),
                            seeds = c(20263726, 20263727, 20263728, 20263729), # one seed per chain (four chains)
                            nitt = 13000*60, 
                            thin = 10*60, 
                            burnin = 3000*60)
            )

For brms…

A <- ape::vcv.phylo(p_tree, corr = TRUE)

priors_brms4 <- default_prior(red_pelage_body_limbs ~ cSocial_group_size + (1 | gr(PhyloName, cov = A)), 
                              data = p_dat, 
                              data2 = list(A = A),
                              family = bernoulli(link = "logit"))

system.time(
  brms_BL2 <- brm(red_pelage_body_limbs ~ cSocial_group_size + (1 | gr(PhyloName, cov = A)), 
                   data = p_dat, 
                   data2 = list(A = A),
                   family = bernoulli(link = "logit"),
                   prior = priors_brms4,
                   iter = 18000, 
                   warmup = 8000, 
                   thin = 1,
                   chains = 4,
                   cores = 4,
                   seed = 20271326,
                   control = list(adapt_delta = 0.95)
                  )
            )
summarise_chains(mcmcglmm_BL2)
#>            parameter    mean     sd   Q2.5   Q97.5  rhat ess_bulk ess_tail
#> 1        (Intercept) -1.7570 1.8290 -5.746  1.5180 1.002     3496     3596
#> 2 cSocial_group_size -0.2368 0.2793 -0.867  0.2286 1.000     3850     3894
#> 3          PhyloName 11.8200 8.0720  2.191 32.8700 1.000     3722     3816
#> 4              units  1.0000 0.0000  1.000  1.0000    NA       NA       NA

c2 <- (16 * sqrt(3) / (15 * pi))^2
res_1 <- pool_chains(mcmcglmm_BL2, "Sol") / sqrt(1 + c2)
res_2 <- pool_chains(mcmcglmm_BL2, "VCV") / (1 + c2)
posterior_summary(res_1)
#>                      Estimate Est.Error       Q2.5     Q97.5
#> (Intercept)        -1.5148667 1.5765837 -4.9529623 1.3084470
#> cSocial_group_size -0.2041211 0.2407207 -0.7473157 0.1970581

posterior_summary(res_2)
#>            Estimate Est.Error      Q2.5      Q97.5
#> PhyloName 8.7802554  5.997474 1.6279013 24.4250920
#> units     0.7430287  0.000000 0.7430287  0.7430287

# brms
summary(brms_BL2)
#>  Family: bernoulli 
#>   Links: mu = logit 
#> Formula: red_pelage_body_limbs ~ cSocial_group_size + (1 | gr(PhyloName, cov = A)) 
#>    Data: p_dat (Number of observations: 265) 
#>   Draws: 4 chains, each with iter = 18000; warmup = 8000; thin = 1;
#>          total post-warmup draws = 40000
#>
#> Multilevel Hyperparameters:
#> ~PhyloName (Number of levels: 265) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     2.66      0.86     1.18     4.59 1.00     7816    12510
#>
#> Regression Coefficients:
#>                    Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept             -1.09      1.24    -3.67     1.27 1.00    23855    24601
#> cSocial_group_size    -0.22      0.25    -0.78     0.19 1.00    52898    28030
#>
#> Draws were sampled using sampling(NUTS). 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).

After the c2 correction, the estimates from the two packages were similar (slope of social group size: −0.20, 95% CI −0.75, 0.20 in MCMCglmm and −0.22, −0.78, 0.19 in brms), and the logit model led to the same conclusion as the probit model: the 95% credible intervals of the slope include zero, so there is no evidence that social group size is associated with red pelage on the body/limbs.

One continuous and one categorical explanatory variable model

Next, we hypothesised that the presence of red pelage might be related to the activity cycle of a species, and we added the activity cycle as a categorical predictor with three levels: cathemeral (cath, the reference level), diurnal (di) and nocturnal (noct). Red colouration may not be as visible in the dark, making it potentially less advantageous for nocturnal species. On the other hand, diurnal species may benefit more from the visibility of red pelage, which could play a role in social/sexual signalling or other ecological functions during the day. The coefficients activity_cycledi and activity_cyclenoct compare diurnal and nocturnal species, respectively, with cathemeral species.

Probit model

The MCMCglmm model is as follows:

inv.phylo <- inverseA(p_tree, nodes = "ALL", scale = TRUE)
prior1 <- list(R = list(V = 1, fix = 1),
               G = list(G1 = list(V = 1, nu = 1, alpha.mu = 0, alpha.V = 10)
                       )
               )

system.time(
    mcmcglmm_BP3 <- run_mcmcglmm_chains(red_pelage_body_limbs ~ cSocial_group_size + activity_cycle, 
                              random = ~ PhyloName, 
                              family = "threshold", 
                              data = p_dat, 
                              prior = prior1, 
                              ginverse = list(PhyloName = inv.phylo$Ainv),
                              seeds = c(20263826, 20263827, 20263828, 20263829), # one seed per chain (four chains)
                              nitt = 13000*50, 
                              thin = 10*50, 
                              burnin = 3000*50)
            )

For brms,

A <- ape::vcv.phylo(p_tree, corr = TRUE)

priors_brms5 <- default_prior(red_pelage_body_limbs ~ cSocial_group_size + activity_cycle + (1 | gr(PhyloName, cov = A)), 
                             data = p_dat, 
                             data2 = list(A = A),
                             family = bernoulli(link = "probit"))

system.time(
    brms_BP3 <- brm(red_pelage_body_limbs ~ cSocial_group_size + activity_cycle + (1 | gr(PhyloName, cov = A)), 
                     data = p_dat, 
                     data2 = list(A = A),
                     family = bernoulli(link = "probit"),
                     prior = priors_brms5,
                     iter = 18000, 
                     warmup = 8000, 
                     thin = 1,
                     chains = 4,
                     cores = 4,
                     seed = 20271426,
                     control = list(adapt_delta = 0.95)
             )
      )
# MCMCglmm
summarise_chains(mcmcglmm_BP3)
#>            parameter     mean     sd    Q2.5   Q97.5  rhat ess_bulk ess_tail
#> 1        (Intercept) -0.75240 1.3720 -3.5530  1.9790 1.000     4012     4000
#> 2 cSocial_group_size -0.11040 0.1401 -0.4077  0.1315 1.000     3906     3753
#> 3   activity_cycledi -0.56440 0.9665 -2.6270  1.2360 1.001     4153     4026
#> 4 activity_cyclenoct -0.01321 0.9999 -2.0650  1.8640 1.000     3932     3801
#> 5          PhyloName  4.27100 3.2390  0.8638 11.5500 1.000     3654     3763
#> 6              units  1.00000 0.0000  1.0000  1.0000    NA       NA       NA

posterior_summary(pool_chains(mcmcglmm_BP3, "VCV"))
#>           Estimate Est.Error     Q2.5  Q97.5
#> PhyloName 4.270996  3.238801 0.863752 11.551
#> units     1.000000  0.000000 1.000000  1.000

posterior_summary(pool_chains(mcmcglmm_BP3, "Sol"))
#>                       Estimate Est.Error      Q2.5     Q97.5
#> (Intercept)        -0.75241514 1.3719111 -3.552856 1.9786102
#> cSocial_group_size -0.11043587 0.1401454 -0.407711 0.1315496
#> activity_cycledi   -0.56442502 0.9664803 -2.626842 1.2360335
#> activity_cyclenoct -0.01321463 0.9999195 -2.065144 1.8640638

# brms
summary(brms_BP3)
#>  Family: bernoulli 
#>   Links: mu = probit 
#> Formula: red_pelage_body_limbs ~ cSocial_group_size + activity_cycle + (1 | gr(PhyloName, cov = A)) 
#>    Data: p_dat (Number of observations: 265) 
#>   Draws: 4 chains, each with iter = 18000; warmup = 8000; thin = 1;
#>          total post-warmup draws = 40000
#>
#> Multilevel Hyperparameters:
#> ~PhyloName (Number of levels: 265) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     1.86      0.59     0.89     3.20 1.00     7521    13607
#>
#> Regression Coefficients:
#>                    Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept             -0.54      1.23    -3.00     1.91 1.00    15134    20513
#> cSocial_group_size    -0.11      0.14    -0.40     0.13 1.00    36416    24459
#> activity_cycledi      -0.51      0.92    -2.45     1.22 1.00    17688    19607
#> activity_cyclenoct    -0.02      0.95    -1.97     1.79 1.00    18086    19939
#>
#> Draws were sampled using sampling(NUTS). 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).

# phylogenetic variance on the same scale (brms SD squared for each draw)
posterior_summary(pool_chains(mcmcglmm_BP3, "VCV")[, "PhyloName"])
#>      Estimate Est.Error     Q2.5  Q97.5
#> var1 4.270996  3.238801 0.863752 11.551

posterior_summary(as_draws_df(brms_BP3)$sd_PhyloName__Intercept^2)
#>      Estimate Est.Error     Q2.5    Q97.5
#> [1,]  3.82254   2.54472 0.784099 10.24598

According to the diagnostics we examined, there are no obvious problems with either model: the four MCMCglmm chains agree (R-hat < 1.01, bulk and tail ESS > 3,600 for 4,000 pooled draws), and the brms Rhat values are 1.00 with large bulk and tail ESS.

The phylogenetic variance is similar in the two packages (posterior mean 4.27, 95% CI 0.86, 11.55 in MCMCglmm; 3.82, 0.78, 10.25 in brms, where the SD was squared for each draw), so there is still substantial phylogenetic variation in the presence of red pelage, although with considerable uncertainty.

For the fixed effects, neither social group size (MCMCglmm: −0.11, brms: −0.11) nor activity cycle (diurnal vs cathemeral: −0.56 and −0.51; nocturnal vs cathemeral: −0.01 and −0.02) showed clear effects: all 95% CIs include zero. There is therefore no evidence that group size or activity cycle is associated with the presence of red pelage on the body/limbs.

Logit model

Finally, the logit model. For MCMCglmm:

inv.phylo <- inverseA(p_tree, nodes = "ALL", scale = TRUE)
prior1 <- list(R = list(V = 1, fix = 1),
               G = list(G1 = list(V = 1, nu = 1, alpha.mu = 0, alpha.V = 10)
                       )
               )

system.time(
    mcmcglmm_BL3 <- run_mcmcglmm_chains(red_pelage_body_limbs ~ cSocial_group_size + activity_cycle, 
                              random = ~ PhyloName, 
                              family = "categorical", 
                              data = p_dat, 
                              prior = prior1, 
                              ginverse = list(PhyloName = inv.phylo$Ainv),
                              seeds = c(20263926, 20263927, 20263928, 20263929), # one seed per chain (four chains)
                              nitt = 13000*80, 
                              thin = 10*80, 
                              burnin = 3000*80)
           )

And brms,

A <- ape::vcv.phylo(p_tree, corr = TRUE)
priors_brms6 <- default_prior(red_pelage_body_limbs ~ cSocial_group_size + activity_cycle + (1 | gr(PhyloName, cov = A)), 
                              data = p_dat, 
                              data2 = list(A = A),
                              family = bernoulli(link = "logit"))

system.time(
  brms_BL3 <- brm(red_pelage_body_limbs ~ cSocial_group_size + activity_cycle + (1 | gr(PhyloName, cov = A)), 
                   data = p_dat, 
                   data2 = list(A = A),
                   family = bernoulli(link = "logit"),
                   prior = priors_brms6,
                   iter = 18000, 
                   warmup = 8000, 
                   thin = 1,
                   chains = 4,
                   cores = 4,
                   seed = 20271526,
                   control = list(adapt_delta = 0.95)
                   )
             )
summarise_chains(mcmcglmm_BL3)
#>            parameter     mean      sd    Q2.5   Q97.5  rhat ess_bulk ess_tail
#> 1        (Intercept) -1.36400  2.5590 -6.6460  3.3520 1.000     3742     3700
#> 2 cSocial_group_size -0.24360  0.2927 -0.8906  0.2399 1.000     3936     3689
#> 3   activity_cycledi -1.07600  1.8910 -5.0010  2.4090 1.001     3804     3988
#> 4 activity_cyclenoct -0.02888  1.9110 -3.9850  3.7470 1.003     3786     3759
#> 5          PhyloName 15.09000 11.0400  2.8460 42.5300 1.001     3786     3285
#> 6              units  1.00000  0.0000  1.0000  1.0000    NA       NA       NA

c2 <- (16 * sqrt(3) / (15 * pi))^2
res_1 <- pool_chains(mcmcglmm_BL3, "Sol") / sqrt(1 + c2)
res_2 <- pool_chains(mcmcglmm_BL3, "VCV") / (1 + c2)
posterior_summary(res_1)
#>                       Estimate Est.Error       Q2.5     Q97.5
#> (Intercept)        -1.17583413 2.2062217 -5.7288183 2.8895852
#> cSocial_group_size -0.21000988 0.2523115 -0.7676931 0.2067998
#> activity_cycledi   -0.92784238 1.6304116 -4.3105068 2.0763444
#> activity_cyclenoct -0.02489107 1.6469996 -3.4349573 3.2303073

posterior_summary(res_2)
#>             Estimate Est.Error      Q2.5      Q97.5
#> PhyloName 11.2148132  8.199333 2.1144001 31.6044294
#> units      0.7430287  0.000000 0.7430287  0.7430287

# brms
summary(brms_BL3)
#>  Family: bernoulli 
#>   Links: mu = logit 
#> Formula: red_pelage_body_limbs ~ cSocial_group_size + activity_cycle + (1 | gr(PhyloName, cov = A)) 
#>    Data: p_dat (Number of observations: 265) 
#>   Draws: 4 chains, each with iter = 18000; warmup = 8000; thin = 1;
#>          total post-warmup draws = 40000
#>
#> Multilevel Hyperparameters:
#> ~PhyloName (Number of levels: 265) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     2.95      0.95     1.38     5.08 1.00     8401    13548
#>
#> Regression Coefficients:
#>                    Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept             -0.66      1.88    -4.39     3.15 1.00    19670    22040
#> cSocial_group_size    -0.21      0.25    -0.77     0.20 1.00    40230    24813
#> activity_cycledi      -0.78      1.50    -3.97     2.04 1.00    21366    21364
#> activity_cyclenoct    -0.06      1.53    -3.23     2.86 1.00    20679    21244
#>
#> Draws were sampled using sampling(NUTS). 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).

The logit model gave the same qualitative results as the probit model. After the c2 correction, the MCMCglmm and brms estimates were similar, and all 95% credible intervals of the slopes included zero. In conclusion, the models are consistent and provide no evidence that social group size or activity cycle is associated with the presence of red pelage on the body/limbs.

Bivariate model

We jointly analysed two binary traits, the presence of red pelage on the body/limbs and on the head, using bivariate probit (threshold) and logit models with a phylogenetic random effect. Residual variances were fixed at 1 for identification, and the residual cross-trait covariance was fixed at 0. We fitted three specifications: intercept-only, adding centred social group size, and additionally adding activity cycle.

3. Ordinal (threshold) models (ordered multinomial model)

For the ordinal (threshold) and nominal models, we show two realistic examples each. As explained in Box 2 of the main text, MCMCglmm and brms use different parametrisations of the threshold model. At first sight, the outputs about the thresholds (cutpoints) may look difficult, but we will guide you step by step through understanding and interpreting them. By the end of this section, you will be able to compare the threshold estimates from both models and understand their implications.

Explanation of dataset

We used the dataset from AVONET (Tobias et al. (2022)) and the phylogenetic tree from BirdTree.

For the first example, we tested the relationships between migration level (response variable) and body mass and habitat density in hawks and eagles (Accipitridae; 136 species).

For the second example, we investigated whether habitat density within the family Phasianidae (179 species) is associated with tail length and diet (herbivory).

NoteExplanation of variables

Migration level: 1 = Sedentary. 2 = Partially migratory, i.e. a minority of the population migrates long distances, or most of the population undergoes short-distance migration, nomadic movements, distinct altitudinal migration, etc. 3 = Migratory, i.e. the majority of the population undertakes long-distance migration.

Habitat density: 1 = Dense habitats. The species primarily lives in the lower or middle storey of forest, or in dense thickets, dense shrubland, etc. 2 = Semi-open habitats. The species primarily lives in open shrubland, scattered bushes, parkland, low dry or deciduous forest, or thorn forest. 3 = Open habitats. The species primarily lives in desert, grassland, open water, low shrubs, rocky habitats, seashores or cities; this also applies to species living mainly on top of the forest canopy (i.e. mostly in the open).

Body mass: Average body mass of the species (adult males and females).

Tail length: The distance from the tip of the longest tail feather to the point where the two central tail feathers emerge from the body. This is usually measured by inserting a ruler between the two central tail feathers.

Herbivore: A species obtaining at least 70% of its food resources from plant material.

Dataset for example 1

dat <- read.csv(here("data", "bird body mass", "accipitridae_sampled.csv"))
dat <- dat %>% 
 mutate(across(c(Trophic.Level, Trophic.Niche, Primary.Lifestyle, Migration, Habitat, Species.Status), as.factor))

# rename: numbers to descriptive name
dat$Migration_ordered <- factor(dat$Migration,  levels = c("sedentary", "partially_migratory", "migratory"), ordered = TRUE)
dat <- dat %>%
 mutate(Habitat.Density = case_when(
    Habitat.Density == 1 ~ "dense",
    Habitat.Density == 2 ~ "semi-open",
    Habitat.Density == 3 ~ "open",
    TRUE ~ as.character(Habitat.Density)
  ))

table(dat$Habitat.Density)
dat$logMass <- log(dat$Mass)
hist(dat$logMass)
boxplot(logMass ~ Habitat.Density, data = dat)
summary(dat)
trees <- read.nexus(here("data", "bird body mass", "accipitridae_sampled.nex"))
tree <- trees[[1]]

summary(dat)

Example 1

Intercept-only model

MCMCglmm

When using MCMCglmm for ordinal data, you can choose between two options for the family parameter: "threshold" and "ordinal". These options differ in how they handle residual variances and error distributions.

We generally recommend using family = "threshold" for the following reasons: it corresponds directly to the standard probit (threshold) model with a residual variance of 1, so no correction of the outputs is necessary, and it usually mixes better.

We show both models throughout this section.

inv_phylo <- inverseA(tree, nodes = "ALL", scale = TRUE)
prior1 <- list(R = list(V = 1, fix = 1), 
              G = list(G1 = list(V = 1, nu = 1, alpha.mu = 0, alpha.V = 10)))

system.time(
  mcmcglmm_T1_1 <- run_mcmcglmm_chains(Migration_ordered ~ 1,
                        random = ~ Phylo,
                        ginverse = list(Phylo = inv_phylo$Ainv),
                        family = "threshold",
                        data = dat,
                        prior = prior1,
                        seeds = c(20264526, 20264527, 20264528, 20264529), # one seed per chain (four chains)
                        nitt = 13000*40, 
                        thin = 10*40, 
                        burnin = 3000*40 
                        )
            )

system.time(
  mcmcglmm_O1_1 <- run_mcmcglmm_chains(Migration_ordered ~ 1,
                        random = ~ Phylo,
                        ginverse = list(Phylo = inv_phylo$Ainv),
                        family = "ordinal",
                        data = dat,
                        prior = prior1,
                        seeds = c(20264626, 20264627, 20264628, 20264629), # one seed per chain (four chains)
                        nitt = 13000*40, 
                        thin = 10*40, 
                        burnin = 3000*40 
                        )
            )

For the ordinal family, the latent variable has two error terms, the units residual (fixed at 1) and the standard normal error of the probit link (variance 1). To express the estimates on the standard probit scale (total residual variance 1), we apply the following correction:

c2 <- 1
res_1 <- model$Sol / sqrt(1+c2) # for fixed effects
res_2 <- model$VCV / (1+c2) # for variance components
summarise_chains(mcmcglmm_T1_1) # pooled draws of four chains; 95% equal-tailed intervals
#>                           parameter   mean     sd      Q2.5 Q97.5 rhat ess_bulk ess_tail
#> 1                       (Intercept) 0.3108 0.5248 -0.760900 1.434    1     4144     3972
#> 2                             Phylo 1.1720 1.6200  0.001486 5.738    1     4092     4057
#> 3                             units 1.0000 0.0000  1.000000 1.000   NA       NA       NA
#> 4 cutpoint.traitMigration_ordered.1 0.9913 0.1755  0.707600 1.404    1     4028     3819

posterior_summary(pool_chains(mcmcglmm_T1_1, "VCV")) # 95% CI
#>       Estimate Est.Error        Q2.5   Q97.5
#> Phylo 1.171787  1.620105 0.001485821 5.73793
#> units 1.000000  0.000000 1.000000000 1.00000

posterior_summary(pool_chains(mcmcglmm_T1_1, "Sol"))
#>              Estimate Est.Error       Q2.5    Q97.5
#> (Intercept) 0.3108338 0.5247659 -0.7609432 1.433756

posterior_summary(pool_chains(mcmcglmm_T1_1, "CP"))
#>                                    Estimate Est.Error      Q2.5    Q97.5
#> cutpoint.traitMigration_ordered.1 0.9912625 0.1755448 0.7075925 1.404084

summarise_chains(mcmcglmm_O1_1) # pooled draws of four chains; 95% equal-tailed intervals
#>                           parameter   mean     sd      Q2.5  Q97.5  rhat ess_bulk ess_tail
#> 1                       (Intercept) 0.4637 0.7321 -0.958200  2.110 1.001     3745     3971
#> 2                             Phylo 2.1220 3.1020  0.003452 10.210 1.000     3640     3835
#> 3                             units 1.0000 0.0000  1.000000  1.000    NA       NA       NA
#> 4 cutpoint.traitMigration_ordered.1 1.3990 0.2409  1.014000  1.936 1.000     3850     3634

posterior_summary(pool_chains(mcmcglmm_O1_1, "VCV")) # 95% CI
#>       Estimate Est.Error        Q2.5    Q97.5
#> Phylo 2.121944  3.101576 0.003451651 10.20916
#> units 1.000000  0.000000 1.000000000  1.00000

posterior_summary(pool_chains(mcmcglmm_O1_1, "Sol"))
#>              Estimate Est.Error      Q2.5   Q97.5
#> (Intercept) 0.4636583 0.7320707 -0.958243 2.11015

c2 <- 1
res_1 <- pool_chains(mcmcglmm_O1_1, "Sol") / sqrt(1+c2) # for fixed effects
res_2 <- pool_chains(mcmcglmm_O1_1, "VCV") / (1+c2) # for variance components
res_3 <- pool_chains(mcmcglmm_O1_1, "CP") / sqrt(1+c2) # for cutpoint
posterior_summary(res_1)
#>              Estimate Est.Error       Q2.5    Q97.5
#> (Intercept) 0.3278559 0.5176521 -0.6775802 1.492102

posterior_summary(res_2)
#>       Estimate Est.Error        Q2.5    Q97.5
#> Phylo 1.060972  1.550788 0.001725826 5.104579
#> units 0.500000  0.000000 0.500000000 0.500000

posterior_summary(res_3)
#>                                    Estimate Est.Error      Q2.5    Q97.5
#> cutpoint.traitMigration_ordered.1 0.9892338 0.1703678 0.7169798 1.368876

The raw estimates of the ordinal model are larger than those of the threshold model because they are on a scale with a total residual variance of 2; after the correction, the two models give similar estimates. The four chains of both models agree (R-hat < 1.01, bulk and tail ESS > 3,600), although ordinal models in MCMCglmm generally mix more slowly than threshold models and needed longer chains. As in the binary models, the residual variance is fixed at 1: the model did not estimate the residual variance (R-structure).

Look at the output more closely: only one cutpoint is reported, although our response variable has three ordered categories. As explained in the main text, MCMCglmm fixes the first cutpoint at 0, so the intercept is estimated relative to this first threshold (and the reported cutpoint is the second threshold).

brms

The family argument is set to cumulative() with link = "probit", which uses the standard probit parametrisation where the residual variance is fixed at 1. Since this is the standard parametrisation for ordinal probit regression, no correcting of coefficients is necessary.

A <- ape::vcv.phylo(tree, corr = TRUE)

default_priors <- default_prior(
           Migration_ordered ~ 1 + (1 | gr(Phylo, cov = A)),
                    data = dat,
                    family = cumulative(link = "probit"),
                    data2 = list(A = A)
                    )

# Fit the model
system.time(
 brms_OT1_1 <- brm(
               formula = Migration_ordered ~ 1 + (1 | gr(Phylo, cov = A)),
               data = dat,
               family = cumulative(link = "probit"),
               data2 = list(A = A),
               prior = default_priors,
               iter = 20000,
               warmup = 10000,
               thin = 1,
               chains = 4,
               cores = 4,
               seed = 20272526,
               control = list(adapt_delta = 0.999)
                )
            )
summary(brms_OT1_1)
#>  Family: cumulative 
#>   Links: mu = probit 
#> Formula: Migration_ordered ~ 1 + (1 | gr(Phylo, cov = A)) 
#>    Data: dat (Number of observations: 136) 
#>   Draws: 4 chains, each with iter = 20000; warmup = 10000; thin = 1;
#>          total post-warmup draws = 40000
#>
#> Multilevel Hyperparameters:
#> ~Phylo (Number of levels: 136) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     0.86      0.61     0.04     2.29 1.00     2933     6424
#>
#> Regression Coefficients:
#>              Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept[1]    -0.33      0.48    -1.38     0.64 1.00    20259    13831
#> Intercept[2]     0.66      0.48    -0.27     1.75 1.00    20358    15823
#>
#> Further Distributional Parameters:
#>      Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> disc     1.00      0.00     1.00     1.00   NA       NA       NA
#>
#> Draws were sampled using sampling(NUTS). 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).

In brms, you can see two intercepts and no cutpoint in the output. These intercepts are the cutpoints (thresholds) themselves: brms fixes the linear predictor’s intercept at 0 and estimates both thresholds. The two parametrisations are equivalent and imply very similar predicted proportions of species in each category, as shown below.

Probabilities

We can obtain the probabilities of each category (migration level: sedentary, partially migratory, migratory) using pnorm(). With the probit link, the probability of the lowest category is \(\Phi(\tau_1 - \eta)\), that of the middle category is \(\Phi(\tau_2 - \eta) - \Phi(\tau_1 - \eta)\), and that of the highest category is \(1 - \Phi(\tau_2 - \eta)\). Here \(\tau_1 < \tau_2\) are the thresholds and \(\eta\) is the linear predictor. In MCMCglmm, \(\tau_1 = 0\), \(\tau_2\) is the reported cutpoint, and \(\eta\) includes the intercept. In brms, \(\tau_1\) and \(\tau_2\) are the two intercepts and \(\eta\) has no intercept.

We made a simple function for this and use it for both packages. We calculate the probabilities for every posterior draw and then summarise them. The probabilities refer to a species whose phylogenetic random effect is zero (i.e. at the phylogenetic mean); they are not averaged over species.

# Category probabilities of a three-category probit threshold model.
# cutpoint0, cutpoint1: lower and upper thresholds; l: linear predictor.
# The arguments can be vectors of posterior draws (one value per draw).
calculate_probabilities <- function(cutpoint0, cutpoint1, l,
                                    labels = c("sedentary", "partially_migratory", "migratory")) {
  probs <- cbind(pnorm(cutpoint0 - l),
                 pnorm(cutpoint1 - l) - pnorm(cutpoint0 - l),
                 1 - pnorm(cutpoint1 - l))
  colnames(probs) <- labels
  probs
}
# MCMCglmm: first cutpoint fixed at 0, second cutpoint estimated, intercept in the linear predictor
probabilities_mcmcglmm <- calculate_probabilities(0, pool_chains(mcmcglmm_T1_1, "CP")[, 1], pool_chains(mcmcglmm_T1_1, "Sol")[, "(Intercept)"])
posterior_summary(probabilities_mcmcglmm)
#>                      Estimate  Est.Error       Q2.5     Q97.5
#> sedentary           0.3906299 0.16573527 0.07582100 0.7766545
#> partially_migratory 0.3369165 0.07216428 0.16558423 0.4722490
#> migratory           0.2724537 0.14589284 0.03023044 0.6313140

# brms: both cutpoints estimated (Intercept[1], Intercept[2]), no intercept in the linear predictor
draws <- as_draws_df(brms_OT1_1)
probabilities_brms <- calculate_probabilities(draws$`b_Intercept[1]`, draws$`b_Intercept[2]`, 0)
posterior_summary(probabilities_brms)
#>                      Estimate  Est.Error       Q2.5     Q97.5
#> sedentary           0.3838747 0.15607591 0.08370244 0.7385190
#> partially_migratory 0.3410405 0.06677686 0.19473741 0.4726157
#> migratory           0.2750848 0.13777236 0.03986000 0.6075200

The posterior mean probabilities are 0.39 (sedentary), 0.34 (partially migratory) and 0.27 (migratory) in MCMCglmm, and 0.38, 0.34 and 0.28 in brms. Although MCMCglmm and brms use the true-intercept and the zero-intercept parametrisations, respectively, the implied probabilities are very similar (see Box 2 in the main text). The wide 95% CIs (e.g. 0.08 to 0.78 for P(sedentary) in MCMCglmm) reflect the uncertainty in the intercept and in the phylogenetic variance.

Phylogenetic signals

# MCMCglmm, threshold family: latent-scale residual variance = 1
h2_mcmcglmm_T <- pool_chains(mcmcglmm_T1_1, "VCV")[, "Phylo"] / (pool_chains(mcmcglmm_T1_1, "VCV")[, "Phylo"] + 1)
posterior_summary(h2_mcmcglmm_T)
#>       Estimate Est.Error        Q2.5     Q97.5
#> var1 0.3825315 0.2643585 0.001483617 0.8515861

# MCMCglmm, ordinal family: correct the variance first (c2 = 1), then use the same definition
c2 <- 1
var_phylo_O <- pool_chains(mcmcglmm_O1_1, "VCV")[, "Phylo"] / (1 + c2)
h2_mcmcglmm_O <- var_phylo_O / (var_phylo_O + 1)
posterior_summary(h2_mcmcglmm_O)
#>       Estimate Est.Error        Q2.5     Q97.5
#> var1 0.3683238  0.257143 0.001722852 0.8361885

# brms: probit latent-scale residual variance = 1
h2_brms_OT <- as_draws_df(brms_OT1_1) %>%
  mutate(h2 = sd_Phylo__Intercept^2 / (sd_Phylo__Intercept^2 + 1)) %>%
  pull(h2)
posterior_summary(h2_brms_OT)
#>       Estimate Est.Error        Q2.5     Q97.5
#> [1,] 0.3766213 0.2602529 0.001679635 0.8399828

The proportion of the latent-scale variance attributed to phylogeny is similar in the three models: 0.38 (95% CI 0.00, 0.85) for the MCMCglmm threshold model, 0.37 (0.00, 0.84) for the MCMCglmm ordinal model after the correction, and 0.38 (0.00, 0.84) for brms. The posterior means suggest a moderate phylogenetic signal in migration level, but the estimates are very uncertain: the intervals range from almost zero to high values. The variation that is not attributed to phylogeny is residual variation on the latent scale; its causes are not identified by this model.

One continuous explanatory variable model

The model using MCMCglmm is

inv_phylo <- inverseA(tree, nodes = "ALL", scale = TRUE)
prior1 <- list(R = list(V = 1, fix = 1), 
              G = list(G1 = list(V = 1, nu = 1, alpha.mu = 0, alpha.V = 10)))
 
system.time(
   mcmcglmm_T1_2 <- run_mcmcglmm_chains(Migration_ordered ~ logMass,
                    random = ~ Phylo,
                    ginverse = list(Phylo = inv_phylo$Ainv),
                    family = "threshold",
                    data = dat,
                    prior = prior1,
                    seeds = c(20264726, 20264727, 20264728, 20264729), # one seed per chain (four chains)
                    nitt = 13000*45, 
                    thin = 10*45, 
                    burnin = 3000*45 
                    )
            )

system.time(
   mcmcglmm_O1_2 <- run_mcmcglmm_chains(Migration_ordered ~ logMass,
                        random = ~ Phylo,
                        ginverse = list(Phylo = inv_phylo$Ainv),
                        family = "ordinal",
                        data = dat,
                        prior = prior1,
                        seeds = c(20264826, 20264827, 20264828, 20264829), # one seed per chain (four chains)
                        nitt = 13000*60, 
                        thin = 10*60, 
                        burnin = 3000*60 
                        )
            )

For brms…

A <- ape::vcv.phylo(tree, corr = TRUE)


default_priors2 <- default_prior(
           Migration_ordered ~ logMass + (1 | gr(Phylo, cov = A)),
                    data = dat,
                    family = cumulative(link = "probit"),
                    data2 = list(A = A)
                    )

# Fit the model
system.time(
  brms_OT1_2 <- brm(
                    formula = Migration_ordered ~ logMass + (1 | gr(Phylo, cov = A)),
                    data = dat,
                    family = cumulative(link = "probit"),
                    data2 = list(A = A),
                    prior = default_priors2,
                    iter = 20000,
                    warmup = 10000,
                    thin = 1,
                    chains = 4,
                    cores = 4,
                    seed = 20272626,
                    control = list(adapt_delta = 0.999)
                    )
            )
# MCMCglmm
summarise_chains(mcmcglmm_T1_2)
#>                           parameter     mean     sd      Q2.5  Q97.5 rhat ess_bulk ess_tail
#> 1                       (Intercept)  0.84720 1.1390 -1.489000 3.2410    1     3778     3677
#> 2                           logMass -0.07311 0.1336 -0.341700 0.1911    1     3744     3671
#> 3                             Phylo  1.52200 2.2030  0.002314 7.3210    1     3828     3877
#> 4                             units  1.00000 0.0000  1.000000 1.0000   NA       NA       NA
#> 5 cutpoint.traitMigration_ordered.1  1.02300 0.1943  0.720000 1.4760    1     3818     3713

posterior_summary(pool_chains(mcmcglmm_T1_2, "Sol"))
#>                Estimate Est.Error       Q2.5     Q97.5
#> (Intercept)  0.84715067 1.1389852 -1.4888332 3.2412696
#> logMass     -0.07310501 0.1336424 -0.3417473 0.1911071

posterior_summary(pool_chains(mcmcglmm_T1_2, "VCV"))
#>       Estimate Est.Error        Q2.5    Q97.5
#> Phylo 1.521726  2.202714 0.002314321 7.321273
#> units 1.000000  0.000000 1.000000000 1.000000

posterior_summary(pool_chains(mcmcglmm_T1_2, "CP"))
#>                                   Estimate Est.Error      Q2.5    Q97.5
#> cutpoint.traitMigration_ordered.1 1.022506 0.1943492 0.7200179 1.475685

summarise_chains(mcmcglmm_O1_2) # pooled draws of four chains; 95% equal-tailed intervals
#>                           parameter    mean     sd      Q2.5   Q97.5  rhat ess_bulk ess_tail
#> 1                       (Intercept)  1.2300 1.5980 -1.800000  4.6620 1.002     3874     3835
#> 2                           logMass -0.1052 0.1876 -0.477100  0.2527 1.000     3908     3972
#> 3                             Phylo  2.7160 3.8890  0.003291 12.8300 1.001     3877     3931
#> 4                             units  1.0000 0.0000  1.000000  1.0000    NA       NA       NA
#> 5 cutpoint.traitMigration_ordered.1  1.4250 0.2649  1.010000  2.0340 1.000     3861     3737

posterior_summary(pool_chains(mcmcglmm_O1_2, "VCV")) # 95% CI
#>       Estimate Est.Error        Q2.5    Q97.5
#> Phylo 2.716335    3.8887 0.003290997 12.83115
#> units 1.000000    0.0000 1.000000000  1.00000

posterior_summary(pool_chains(mcmcglmm_O1_2, "Sol"))
#>               Estimate Est.Error       Q2.5     Q97.5
#> (Intercept)  1.2299533 1.5975621 -1.8002910 4.6622096
#> logMass     -0.1052461 0.1875512 -0.4771215 0.2527177

c2 <- 1
res_1 <- pool_chains(mcmcglmm_O1_2, "Sol") / sqrt(1+c2) # for fixed effects
res_2 <- pool_chains(mcmcglmm_O1_2, "VCV") / (1+c2) # for variance components
res_3 <- pool_chains(mcmcglmm_O1_2, "CP") / sqrt(1+c2)
posterior_summary(res_1)
#>                Estimate Est.Error       Q2.5     Q97.5
#> (Intercept)  0.86970830 1.1296470 -1.2729979 3.2966800
#> logMass     -0.07442024 0.1326187 -0.3373759 0.1786984

posterior_summary(res_2)
#>       Estimate Est.Error        Q2.5    Q97.5
#> Phylo 1.358167   1.94435 0.001645499 6.415576
#> units 0.500000   0.00000 0.500000000 0.500000

posterior_summary(res_3)
#>                                   Estimate Est.Error      Q2.5    Q97.5
#> cutpoint.traitMigration_ordered.1 1.007521 0.1873045 0.7145004 1.437937

#brms
summary(brms_OT1_2)
#>  Family: cumulative 
#>   Links: mu = probit 
#> Formula: Migration_ordered ~ logMass + (1 | gr(Phylo, cov = A)) 
#>    Data: dat (Number of observations: 136) 
#>   Draws: 4 chains, each with iter = 20000; warmup = 10000; thin = 1;
#>          total post-warmup draws = 40000
#>
#> Multilevel Hyperparameters:
#> ~Phylo (Number of levels: 136) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     0.93      0.64     0.04     2.41 1.00     3062     6369
#>
#> Regression Coefficients:
#>              Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept[1]    -0.85      1.08    -3.11     1.28 1.00    26327    16066
#> Intercept[2]     0.16      1.07    -2.01     2.37 1.00    27603    17560
#> logMass         -0.07      0.13    -0.35     0.19 1.00    34306    20111
#>
#> Further Distributional Parameters:
#>      Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> disc     1.00      0.00     1.00     1.00   NA       NA       NA
#>
#> Draws were sampled using sampling(NUTS). 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).

In both packages, the 95% credible interval of the slope of logMass includes zero (MCMCglmm threshold model: −0.07, 95% CI −0.34, 0.19; brms: −0.07, −0.35, 0.19), so there is no clear evidence that body mass is associated with migration level.

To calculate the category probabilities from this model, we need to choose a value of the predictor. We use the mean of logMass; at logMass = 0 (a body mass of 1 g), the intercept would be an extrapolation far outside the data.

mean_logMass <- mean(dat$logMass)

# MCMCglmm
l_mcmcglmm <- pool_chains(mcmcglmm_T1_2, "Sol")[, "(Intercept)"] + pool_chains(mcmcglmm_T1_2, "Sol")[, "logMass"] * mean_logMass
probabilities_mcmcglmm <- calculate_probabilities(0, pool_chains(mcmcglmm_T1_2, "CP")[, 1], l_mcmcglmm)
posterior_summary(probabilities_mcmcglmm)
#>                      Estimate  Est.Error       Q2.5     Q97.5
#> sedentary           0.3758873 0.18063839 0.04594959 0.7957818
#> partially_migratory 0.3397463 0.07994925 0.14404028 0.4874953
#> migratory           0.2843664 0.16348271 0.02219209 0.7091940

# brms
draws <- as_draws_df(brms_OT1_2)
probabilities_brms <- calculate_probabilities(draws$`b_Intercept[1]`, draws$`b_Intercept[2]`,
                                              draws$b_logMass * mean_logMass)
posterior_summary(probabilities_brms)
#>                      Estimate  Est.Error       Q2.5     Q97.5
#> sedentary           0.3711340 0.16375446 0.06317060 0.7472438
#> partially_migratory 0.3433715 0.07069778 0.18474606 0.4796120
#> migratory           0.2854944 0.14929631 0.03698734 0.6550769

For a species of average body mass, the probabilities (MCMCglmm: 0.38, 0.34 and 0.28; brms: 0.37, 0.34 and 0.29 for sedentary, partially migratory and migratory) are very similar to those from the intercept-only model, which is consistent with a slope close to zero. Note that the probabilities have to be calculated from the thresholds and the linear predictor evaluated at a chosen predictor value: plugging in the intercept alone would correspond to logMass = 0, far outside the range of the data.

One continuous and one categorical explanatory variable model

The MCMCglmm model is as follows. Here, we use gelman.prior() to specify the prior covariance matrix of the fixed effects. It constructs the weakly informative prior recommended by Gelman et al. (2008), scaled so that it is approximately flat on the probability scale given the latent residual variance (set through scale; see Section 4). Such a prior regularises the fixed effects when the data contain little information about them (e.g. under near-separation) and can make sampling more stable, but it does not guarantee convergence.

inv_phylo <- inverseA(tree, nodes = "ALL", scale = TRUE)
prior1 <- list(B = list(mu = rep(0,4),
                        V = gelman.prior(~logMass + Habitat.Density, data = dat, scale = sqrt(1+1))), 
               R = list(V = 1, fix = 1), 
               G = list(G1 = list(V = 1, nu = 1, alpha.mu = 0, alpha.V = 10)))

system.time(
  mcmcglmm_T1_3 <- run_mcmcglmm_chains(Migration_ordered ~ logMass + Habitat.Density,
               random = ~ Phylo,
               ginverse = list(Phylo = inv_phylo$Ainv),
               family = "threshold",
               data = dat,
               prior = prior1,
               seeds = c(20264926, 20264927, 20264928, 20264929), # one seed per chain (four chains)
               nitt = 13000*60, 
               thin = 10*60, 
               burnin = 3000*60 
                  )
            )


system.time(
  mcmcglmm_O1_3 <- run_mcmcglmm_chains(Migration_ordered ~ logMass + Habitat.Density,
                        random = ~ Phylo,
                        ginverse = list(Phylo = inv_phylo$Ainv),
                        family = "ordinal",
                        data = dat,
                        prior = prior1,
                        seeds = c(20265026, 20265027, 20265028, 20265029), # one seed per chain (four chains)
                        nitt = 13000*150, 
                        thin = 10*150, 
                        burnin = 3000*150 
                        )
            )

For brms,

A <- ape::vcv.phylo(tree, corr = TRUE)

default_priors3 <- default_prior(
           Migration_ordered ~ logMass + Habitat.Density + (1 | gr(Phylo, cov = A)),
                    data = dat,
                    family = cumulative(link = "probit"),
                    data2 = list(A = A)
                    )

system.time(
  brms_OT1_3 <- brm(
                formula = Migration_ordered ~ logMass + Habitat.Density + (1 | gr(Phylo, cov = A)),
                data = dat,
                family = cumulative(link = "probit"),
                data2 = list(A = A),
                prior = default_priors3,
                iter = 20000,
                warmup = 10000,
                thin = 1,
                chains = 4,
                cores = 4,
                seed = 20272726,
                control = list(adapt_delta = 0.999)
                )  
            )
# MCMCglmm
summarise_chains(mcmcglmm_T1_3)
#>                           parameter    mean     sd      Q2.5  Q97.5  rhat ess_bulk ess_tail
#> 1                       (Intercept)  1.1610 1.0140 -0.870300 3.1800 1.001     3675     3887
#> 2                           logMass -0.2324 0.1345 -0.503200 0.0240 1.001     3558     3932
#> 3               Habitat.Densityopen  1.1560 0.3447  0.534400 1.8830 1.000     3928     3877
#> 4          Habitat.Densitysemi-open  0.3179 0.2898 -0.227600 0.9148 1.000     4093     3976
#> 5                             Phylo  1.1160 1.7900  0.004338 4.6970 1.000     4037     4100
#> 6                             units  1.0000 0.0000  1.000000 1.0000    NA       NA       NA
#> 7 cutpoint.traitMigration_ordered.1  1.0640 0.1803  0.771000 1.4610 1.001     4174     3827

posterior_summary(pool_chains(mcmcglmm_T1_3, "VCV"))
#>       Estimate Est.Error        Q2.5    Q97.5
#> Phylo 1.115626  1.790052 0.004337597 4.697408
#> units 1.000000  0.000000 1.000000000 1.000000

posterior_summary(pool_chains(mcmcglmm_T1_3, "Sol"))
#>                            Estimate Est.Error       Q2.5      Q97.5
#> (Intercept)               1.1608052 1.0144979 -0.8703382 3.17981821
#> logMass                  -0.2324211 0.1345127 -0.5032255 0.02399722
#> Habitat.Densityopen       1.1557006 0.3447066  0.5343777 1.88302297
#> Habitat.Densitysemi-open  0.3179227 0.2897567 -0.2275658 0.91480935

posterior_summary(pool_chains(mcmcglmm_T1_3, "CP"))
#>                                   Estimate Est.Error      Q2.5    Q97.5
#> cutpoint.traitMigration_ordered.1 1.063591 0.1802779 0.7710347 1.461318

summarise_chains(mcmcglmm_O1_3) # pooled draws of four chains; 95% equal-tailed intervals
#>                           parameter    mean     sd      Q2.5   Q97.5  rhat ess_bulk ess_tail
#> 1                       (Intercept)  1.5590 1.3130 -1.050000 4.20300 1.000     4103     3904
#> 2                           logMass -0.3012 0.1739 -0.655400 0.03802 1.000     4194     3822
#> 3               Habitat.Densityopen  1.5100 0.4501  0.671700 2.44500 1.000     3868     3849
#> 4          Habitat.Densitysemi-open  0.3873 0.3961 -0.395700 1.17600 1.001     4210     3852
#> 5                             Phylo  1.6430 2.1780  0.006435 7.34100 1.001     3961     3914
#> 6                             units  1.0000 0.0000  1.000000 1.00000    NA       NA       NA
#> 7 cutpoint.traitMigration_ordered.1  1.4650 0.2317  1.071000 2.00700 1.001     4052     3973

posterior_summary(pool_chains(mcmcglmm_O1_3, "VCV")) # 95% CI
#>       Estimate Est.Error        Q2.5    Q97.5
#> Phylo 1.643271  2.177759 0.006434844 7.340953
#> units 1.000000  0.000000 1.000000000 1.000000

posterior_summary(pool_chains(mcmcglmm_O1_3, "Sol"))
#>                            Estimate Est.Error       Q2.5     Q97.5
#> (Intercept)               1.5592094 1.3126355 -1.0495310 4.2034257
#> logMass                  -0.3012380 0.1738655 -0.6554180 0.0380167
#> Habitat.Densityopen       1.5099793 0.4501488  0.6717383 2.4445734
#> Habitat.Densitysemi-open  0.3873436 0.3961165 -0.3956748 1.1762203

c2 <- 1
res_1 <- pool_chains(mcmcglmm_O1_3, "Sol") / sqrt(1+c2) # for fixed effects
res_2 <- pool_chains(mcmcglmm_O1_3, "VCV") / (1+c2) # for variance components
res_3 <- pool_chains(mcmcglmm_O1_3, "CP") / sqrt(1+c2)
posterior_summary(res_1)
#>                            Estimate Est.Error       Q2.5      Q97.5
#> (Intercept)               1.1025275 0.9281735 -0.7421305 2.97227079
#> logMass                  -0.2130074 0.1229415 -0.4634505 0.02688186
#> Habitat.Densityopen       1.0677166 0.3183032  0.4749907 1.72857442
#> Habitat.Densitysemi-open  0.2738933 0.2800966 -0.2797843 0.83171336

posterior_summary(res_2)
#>        Estimate Est.Error        Q2.5    Q97.5
#> Phylo 0.8216356  1.088879 0.003217422 3.670476
#> units 0.5000000  0.000000 0.500000000 0.500000

posterior_summary(res_3)
#>                                   Estimate Est.Error      Q2.5   Q97.5
#> cutpoint.traitMigration_ordered.1  1.03606 0.1638556 0.7573277 1.41949

# brms
summary(brms_OT1_3)
#>  Family: cumulative 
#>   Links: mu = probit 
#> Formula: Migration_ordered ~ logMass + Habitat.Density + (1 | gr(Phylo, cov = A)) 
#>    Data: dat (Number of observations: 136) 
#>   Draws: 4 chains, each with iter = 20000; warmup = 10000; thin = 1;
#>          total post-warmup draws = 40000
#>
#> Multilevel Hyperparameters:
#> ~Phylo (Number of levels: 136) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     0.92      0.56     0.08     2.24 1.00     3404     6772
#>
#> Regression Coefficients:
#>                          Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept[1]                -1.34      1.08    -3.60     0.74 1.00    19581    15738
#> Intercept[2]                -0.27      1.06    -2.41     1.84 1.00    21093    18645
#> logMass                     -0.26      0.14    -0.56     0.01 1.00    19501    16938
#> Habitat.Densityopen          1.24      0.37     0.57     2.03 1.00    10758    13795
#> Habitat.DensitysemiMopen     0.36      0.30    -0.21     0.98 1.00    23357    21646
#>
#> Further Distributional Parameters:
#>      Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> disc     1.00      0.00     1.00     1.00   NA       NA       NA
#>
#> Draws were sampled using sampling(NUTS). 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).

Species living in open habitats have higher migration levels than those living in dense habitats: Habitat.Densityopen is 1.16 (95% CI 0.53, 1.88) in the MCMCglmm threshold model and 1.24 (0.57, 2.03) in brms, and both intervals exclude zero. The difference between semi-open and dense habitats is smaller, and its intervals include zero (0.32 [−0.23, 0.91] and 0.36 [−0.21, 0.98]). The slope of logMass is negative (−0.23 [−0.50, 0.02] and −0.26 [−0.56, 0.01]), but its intervals narrowly include zero, so there is at most weak evidence that heavier species are less migratory after accounting for habitat density. The MCMCglmm ordinal model gives very similar estimates after the correction.

Below, we calculate the probabilities of each migration category for species of average body mass (mean logMass) living in each habitat type.

mean_logMass <- mean(dat$logMass)

# MCMCglmm: linear predictor for each habitat (dense is the reference level)
Sol <- pool_chains(mcmcglmm_T1_3, "Sol")
l_dense <- Sol[, "(Intercept)"] + Sol[, "logMass"] * mean_logMass
l_mcmcglmm <- list(dense = l_dense,
                   `semi-open` = l_dense + Sol[, "Habitat.Densitysemi-open"],
                   open = l_dense + Sol[, "Habitat.Densityopen"])

# posterior mean probability of each migration category (rows) in each habitat (columns)
sapply(l_mcmcglmm, function(l) colMeans(calculate_probabilities(0, pool_chains(mcmcglmm_T1_3, "CP")[, 1], l)))
#>                         dense semi-open      open
#> sedentary           0.6263582 0.5164413 0.2381862
#> partially_migratory 0.2690477 0.3199206 0.3577025
#> migratory           0.1045942 0.1636382 0.4041112

# brms
draws <- as_draws_df(brms_OT1_3)
l_dense <- draws$b_logMass * mean_logMass
l_brms <- list(dense = l_dense,
               `semi-open` = l_dense + draws$b_Habitat.DensitysemiMopen,
               open = l_dense + draws$b_Habitat.Densityopen)
sapply(l_brms, function(l) colMeans(calculate_probabilities(draws$`b_Intercept[1]`, draws$`b_Intercept[2]`, l)))
#>                         dense semi-open      open
#> sedentary           0.6188009 0.4936859 0.2144837
#> partially_migratory 0.2734490 0.3287402 0.3496559
#> migratory           0.1077501 0.1775739 0.4358604

For a species of average body mass, the probability of being sedentary decreases and the probability of being migratory increases from dense to open habitats. In MCMCglmm, P(sedentary) is 0.63 in dense, 0.52 in semi-open and 0.24 in open habitats, and P(migratory) is 0.10, 0.16 and 0.40. brms gives similar values (0.62, 0.49 and 0.21; 0.11, 0.18 and 0.44).

Example 2

The second example is a cautionary example about identification and prior sensitivity. The response variable is Habitat.Density of the Phasianidae (179 species), with the ordered categories dense < semi-open < open, and the explanatory variables are the centred log tail length (cTail_length) and herbivory (IsHerbivore, 1 = at least 70% of food from plants). Almost all of the variation in habitat density in this family is phylogenetically structured, and we will see that this makes the latent scale of the threshold model hard to identify.

Dataset for example 2

dat <- read.csv(here("data", "bird body mass", "9993spp_clearned.csv"))

phasianidae_dat <- dat %>%
  filter(Family == "Phasianidae") %>%
  mutate(
    # set the order of the categories explicitly (the default would be alphabetical)
    Habitat.Density = factor(case_when(Habitat.Density == 1 ~ "dense",
                                       Habitat.Density == 2 ~ "semi-open",
                                       Habitat.Density == 3 ~ "open"),
                             levels = c("dense", "semi-open", "open"), ordered = TRUE),
    # one species (Lophura hatinhensis) has no trophic level and is coded as non-herbivore
    IsHerbivore = ifelse(Trophic.Level %in% "Herbivore", 1, 0)
  )
phasianidae_dat$cTail_length <- as.numeric(scale(log(phasianidae_dat$Tail.Length), center = TRUE, scale = FALSE))

phasianidae_tree <- read.nexus(here("data", "bird body mass", "Phasianidae.nex"))[[1]]

Models and prior specifications

We fit the intercept-only model, the model with tail length, and the model with tail length and herbivory, first with the same prior as in Example 1: a parameter-expanded prior that corresponds to a half-Cauchy prior with scale \(\sqrt{10}\) on the phylogenetic standard deviation. To check the sensitivity to this choice, we vary the scale within the same prior family (half-Cauchy scales 1 and 5). As an additional check of the prior family, we also use a scaled \(\chi^2_1\) prior on the phylogenetic variance, which is much more regularising.

inv_phylo2 <- inverseA(phasianidae_tree, nodes = "ALL", scale = TRUE)

# half-Cauchy(0, s) prior on the phylogenetic SD (parameter expansion with alpha.V = s^2)
prior_hc <- function(s) list(R = list(V = 1, fix = 1),
                             G = list(G1 = list(V = 1, nu = 1, alpha.mu = 0, alpha.V = s^2)))
# scaled chi-square (1 df) prior on the phylogenetic variance: a more regularising prior family
prior_chisq <- list(R = list(V = 1, fix = 1),
                    G = list(G1 = list(V = 1, nu = 1000, alpha.mu = 0, alpha.V = 1)))

formulas2 <- list(Habitat.Density ~ 1,
                  Habitat.Density ~ cTail_length,
                  Habitat.Density ~ cTail_length + IsHerbivore)

# four chains of one model; k multiplies the default nitt, thin and burnin
fit_ex2 <- function(formula, prior, seeds, k, family = "threshold") {
  run_mcmcglmm_chains(formula, random = ~ Phylo, ginverse = list(Phylo = inv_phylo2$Ainv),
                      family = family, data = phasianidae_dat, prior = prior, seeds = seeds,
                      nitt = 13000*k, thin = 10*k, burnin = 3000*k)
}

# main specification: half-Cauchy(0, sqrt(10)), threshold and ordinal families
mcmcglmm_T2_1 <- fit_ex2(formulas2[[1]], prior_hc(sqrt(10)), c(20261026, 20261027, 20261028, 20261029), k = 65)
mcmcglmm_T2_2 <- fit_ex2(formulas2[[2]], prior_hc(sqrt(10)), c(20261126, 20261127, 20261128, 20261129), k = 600)
mcmcglmm_T2_3 <- fit_ex2(formulas2[[3]], prior_hc(sqrt(10)), c(20261226, 20261227, 20261228, 20261229), k = 600)
mcmcglmm_O2_1 <- fit_ex2(formulas2[[1]], prior_hc(sqrt(10)), c(20261326, 20261327, 20261328, 20261329), k = 500, family = "ordinal")
mcmcglmm_O2_2 <- fit_ex2(formulas2[[2]], prior_hc(sqrt(10)), c(20261426, 20261427, 20261428, 20261429), k = 600, family = "ordinal")
mcmcglmm_O2_3 <- fit_ex2(formulas2[[3]], prior_hc(sqrt(10)), c(20261526, 20261527, 20261528, 20261529), k = 600, family = "ordinal")

# prior sensitivity (threshold family): half-Cauchy scales 1 and 5, and the chi-square prior
mcmcglmm_T2_1_hc1 <- fit_ex2(formulas2[[1]], prior_hc(1), c(20265626, 20265627, 20265628, 20265629), k = 65)
mcmcglmm_T2_2_hc1 <- fit_ex2(formulas2[[2]], prior_hc(1), c(20265726, 20265727, 20265728, 20265729), k = 600)
mcmcglmm_T2_3_hc1 <- fit_ex2(formulas2[[3]], prior_hc(1), c(20265826, 20265827, 20265828, 20265829), k = 600)
mcmcglmm_T2_1_hc5 <- fit_ex2(formulas2[[1]], prior_hc(5), c(20265926, 20265927, 20265928, 20265929), k = 65)
mcmcglmm_T2_2_hc5 <- fit_ex2(formulas2[[2]], prior_hc(5), c(20266026, 20266027, 20266028, 20266029), k = 600)
mcmcglmm_T2_3_hc5 <- fit_ex2(formulas2[[3]], prior_hc(5), c(20266126, 20266127, 20266128, 20266129), k = 600)
mcmcglmm_T2_1_chisq <- fit_ex2(formulas2[[1]], prior_chisq, c(20268926, 20268927, 20268928, 20268929), k = 65)
mcmcglmm_T2_2_chisq <- fit_ex2(formulas2[[2]], prior_chisq, c(20269026, 20269027, 20269028, 20269029), k = 600)
mcmcglmm_T2_3_chisq <- fit_ex2(formulas2[[3]], prior_chisq, c(20269126, 20269127, 20269128, 20269129), k = 600)

In brms, the default prior on the phylogenetic SD is a Student-t(3, 0, 2.5) distribution. We use it as the main specification and vary its scale (1 and 5) for the sensitivity analysis.

A <- ape::vcv.phylo(phasianidae_tree, corr = TRUE)

brms_formulas2 <- list(Habitat.Density ~ 1 + (1 | gr(Phylo, cov = A)),
                       Habitat.Density ~ cTail_length + (1 | gr(Phylo, cov = A)),
                       Habitat.Density ~ cTail_length + IsHerbivore + (1 | gr(Phylo, cov = A)))
iter2 <- c(6000, 10000, 8000); warmup2 <- c(4000, 8000, 6000); adapt2 <- c(0.95, 0.99, 0.999) # model 3: 0.999 (at 0.95, a few divergent transitions)

# model i with a Student-t(3, 0, s) prior on the phylogenetic SD (s = 2.5 is the default)
fit_ex2_brms <- function(i, s, seed) {
  pri <- default_prior(brms_formulas2[[i]], data = phasianidae_dat, data2 = list(A = A),
                       family = cumulative(link = "probit"))
  pri$prior[pri$class == "sd" & pri$coef == "" & pri$group == ""] <- sprintf("student_t(3, 0, %s)", s)
  brm(brms_formulas2[[i]], data = phasianidae_dat, data2 = list(A = A),
      family = cumulative(link = "probit"), prior = pri,
      iter = iter2[i], warmup = warmup2[i], thin = 1, chains = 4, cores = 4, seed = seed,
      control = list(adapt_delta = adapt2[i]))
}

brms_OT2_1 <- fit_ex2_brms(1, 2.5, seed = 20261626)
brms_OT2_2 <- fit_ex2_brms(2, 2.5, seed = 20261726)
brms_OT2_3 <- fit_ex2_brms(3, 2.5, seed = 20273026)
brms_OT2_1_sd1 <- fit_ex2_brms(1, 1, seed = 20266226)
brms_OT2_2_sd1 <- fit_ex2_brms(2, 1, seed = 20266326)
brms_OT2_3_sd1 <- fit_ex2_brms(3, 1, seed = 20273126)
brms_OT2_1_sd5 <- fit_ex2_brms(1, 5, seed = 20266526)
brms_OT2_2_sd5 <- fit_ex2_brms(2, 5, seed = 20266626)
brms_OT2_3_sd5 <- fit_ex2_brms(3, 5, seed = 20273226)

The half-Cauchy specifications do not identify the latent scale

First, we check the between-chain diagnostics of the MCMCglmm fits. For each prior and model, we show the largest Rhat, the smallest bulk ESS, and the median of the phylogenetic variance in each of the four chains.

diag_ex2 <- function(fits) {
  s <- summarise_chains(fits); s <- s[s$sd > 0, ]
  data.frame(max_rhat = round(max(s$rhat), 2), min_ess_bulk = round(min(s$ess_bulk)),
             phylo_var_median_per_chain = paste(signif(sapply(fits, function(m) median(m$VCV[, "Phylo"])), 2),
                                                collapse = " / "))
}
ex2_fits <- list(
  "half-Cauchy(0, 1)"       = list(mcmcglmm_T2_1_hc1, mcmcglmm_T2_2_hc1, mcmcglmm_T2_3_hc1),
  "half-Cauchy(0, sqrt(10))" = list(mcmcglmm_T2_1, mcmcglmm_T2_2, mcmcglmm_T2_3),
  "half-Cauchy(0, 5)"       = list(mcmcglmm_T2_1_hc5, mcmcglmm_T2_2_hc5, mcmcglmm_T2_3_hc5),
  "ordinal family, half-Cauchy(0, sqrt(10))" = list(mcmcglmm_O2_1, mcmcglmm_O2_2, mcmcglmm_O2_3),
  "chi-square(1)"           = list(mcmcglmm_T2_1_chisq, mcmcglmm_T2_2_chisq, mcmcglmm_T2_3_chisq))
do.call(rbind, lapply(names(ex2_fits), function(p)
  cbind(prior = p, model = 1:3, do.call(rbind, lapply(ex2_fits[[p]], diag_ex2)))))
#>                                       prior model max_rhat min_ess_bulk      phylo_var_median_per_chain
#> 1                         half-Cauchy(0, 1)     1     1.57            7             29 / 46 / 3700 / 25
#> 2                         half-Cauchy(0, 1)     2     1.71            6 150000 / 98000 / 38000 / 130000
#> 3                         half-Cauchy(0, 1)     3     2.40            5 82000 / 460000 / 19000 / 130000
#> 4                  half-Cauchy(0, sqrt(10))     1     1.04          199               48 / 29 / 32 / 35
#> 5                  half-Cauchy(0, sqrt(10))     2     1.72            6   71000 / 20000 / 86000 / 62000
#> 6                  half-Cauchy(0, sqrt(10))     3     2.58            5 72000 / 290000 / 350000 / 14000
#> 7                         half-Cauchy(0, 5)     1     2.32            5        19000 / 6800 / 810 / 740
#> 8                         half-Cauchy(0, 5)     2     1.63            7   47000 / 40000 / 32000 / 15000
#> 9                         half-Cauchy(0, 5)     3     1.73            6 62000 / 180000 / 68000 / 210000
#> 10 ordinal family, half-Cauchy(0, sqrt(10))     1     1.10           33              90 / 210 / 67 / 56
#> 11 ordinal family, half-Cauchy(0, sqrt(10))     2     1.69            6    250000 / 7900 / 2500 / 10000
#> 12 ordinal family, half-Cauchy(0, sqrt(10))     3     1.44            8  87000 / 61000 / 150000 / 53000
#> 13                            chi-square(1)     1     1.00         3980             6.7 / 7 / 6.6 / 6.6
#> 14                            chi-square(1)     2     1.00         3932           6.6 / 6.7 / 6.5 / 6.7
#> 15                            chi-square(1)     3     1.00         3774               7 / 7.1 / 7.1 / 7

The trace plots of the phylogenetic variance make the problem visible: with the half-Cauchy prior, the four chains drift to different, extremely large values.

plot(chains_list(mcmcglmm_T2_3, "VCV")[, "Phylo"], main = "Phylogenetic variance, half-Cauchy(0, sqrt(10))")

plot(chains_list(mcmcglmm_T2_3_chisq, "VCV")[, "Phylo"], main = "Phylogenetic variance, chi-square(1)")

With all three half-Cauchy scales, the MCMCglmm fits do not converge. For eight of the nine fits, R-hat is far above 1.01 (1.6–2.6) and the bulk ESS is below 10; the ordinal family behaves in the same way. The per-chain medians of the phylogenetic variance show why: the four chains drift to widely different values, most of them in the thousands or more on a latent scale whose residual variance is 1. Only the intercept-only model with scale \(\sqrt{10}\) comes close to convergence (R-hat 1.04), and even there the phylogenetic variance is about 30–50.

The reason is that almost all of the variation in habitat density is shared among closely related species. When the phylogenetic variance is very large relative to the residual variance (fixed at 1), the likelihood is almost flat along the overall scale of the latent variable: multiplying the phylogenetic effects, the fixed effects and the cutpoint by a common factor hardly changes the predicted categories. The heavy-tailed half-Cauchy prior does not prevent the chains from drifting along this direction, so the raw latent-scale quantities (the phylogenetic variance, the fixed effects and the cutpoint) are not identified by these fits, and longer chains would not solve the problem. We therefore do not interpret the half-Cauchy fits.

With the scaled \(\chi^2_1\) prior, which puts much less mass on very large variances, the chains converge (R-hat 1.00, bulk ESS > 3,700) and the phylogenetic variance is about 7. This is a check of the prior family, not a “fix”: the \(\chi^2_1\) prior makes the latent scale identifiable by regularising it, and the magnitudes of the estimates depend on this choice.

The identified fits: MCMCglmm with the chi-square prior and brms

brms_diag <- function(fit) {
  s <- posterior::summarise_draws(fit, "rhat", "ess_bulk")
  s <- s[grepl("^(b_|sd_)", s$variable), ]
  np <- nuts_params(fit)
  data.frame(max_rhat = round(max(s$rhat), 3), min_ess_bulk = round(min(s$ess_bulk)),
             divergent = sum(np$Value[np$Parameter == "divergent__"]))
}
ex2_brms <- list("Student-t(3, 0, 1)"   = list(brms_OT2_1_sd1, brms_OT2_2_sd1, brms_OT2_3_sd1),
                 "Student-t(3, 0, 2.5)" = list(brms_OT2_1, brms_OT2_2, brms_OT2_3),
                 "Student-t(3, 0, 5)"   = list(brms_OT2_1_sd5, brms_OT2_2_sd5, brms_OT2_3_sd5))
do.call(rbind, lapply(names(ex2_brms), function(p)
  cbind(sd_prior = p, model = 1:3, do.call(rbind, lapply(ex2_brms[[p]], brms_diag)))))
#>               sd_prior model max_rhat min_ess_bulk divergent
#> 1   Student-t(3, 0, 1)     1    1.002         1093         0
#> 2   Student-t(3, 0, 1)     2    1.002         1053         0
#> 3   Student-t(3, 0, 1)     3    1.006          673         0
#> 4 Student-t(3, 0, 2.5)     1    1.001          975         0
#> 5 Student-t(3, 0, 2.5)     2    1.002          785         0
#> 6 Student-t(3, 0, 2.5)     3    1.009          626         0
#> 7   Student-t(3, 0, 5)     1    1.005          967         0
#> 8   Student-t(3, 0, 5)     2    1.003          882         0
#> 9   Student-t(3, 0, 5)     3    1.003          724         0

Across all identified fits, we compare the proportion of the latent-scale variance attributed to phylogeny (intercept-only model; residual variance 1 for the probit link) and the effects of tail length and herbivory (full model). All quantities are calculated for each posterior draw.

q <- function(x) sprintf("%.2f (%.2f, %.2f)", mean(x), quantile(x, 0.025), quantile(x, 0.975))
ex2_mcmc_row <- function(f1, f3) {
  v <- pool_chains(f1, "VCV")[, "Phylo"]; S <- pool_chains(f3, "Sol")
  c(H2 = q(v / (v + 1)), tail_length = q(S[, "cTail_length"]), herbivore = q(S[, "IsHerbivore"]),
    P_herbivore_positive = sprintf("%.2f", mean(S[, "IsHerbivore"] > 0)))
}
ex2_brms_row <- function(f1, f3) {
  d1 <- as_draws_df(f1); d3 <- as_draws_df(f3); v <- d1$sd_Phylo__Intercept^2
  c(H2 = q(v / (v + 1)), tail_length = q(d3$b_cTail_length), herbivore = q(d3$b_IsHerbivore),
    P_herbivore_positive = sprintf("%.2f", mean(d3$b_IsHerbivore > 0)))
}
rbind("MCMCglmm, chi-square(1)"    = ex2_mcmc_row(mcmcglmm_T2_1_chisq, mcmcglmm_T2_3_chisq),
      "brms, Student-t(3, 0, 1)"   = ex2_brms_row(brms_OT2_1_sd1, brms_OT2_3_sd1),
      "brms, Student-t(3, 0, 2.5)" = ex2_brms_row(brms_OT2_1, brms_OT2_3),
      "brms, Student-t(3, 0, 5)"   = ex2_brms_row(brms_OT2_1_sd5, brms_OT2_3_sd5))
#>                            H2                  tail_length           herbivore             P_herbivore_positive
#> MCMCglmm, chi-square(1)    "0.86 (0.76, 0.93)" "-0.39 (-1.22, 0.44)" "-0.10 (-0.99, 0.73)" "0.42"              
#> brms, Student-t(3, 0, 1)   "0.91 (0.80, 0.98)" "-0.32 (-1.75, 1.28)" "-0.57 (-2.74, 0.68)" "0.24"              
#> brms, Student-t(3, 0, 2.5) "0.93 (0.84, 0.99)" "-0.35 (-2.01, 1.42)" "-0.74 (-3.16, 0.72)" "0.20"              
#> brms, Student-t(3, 0, 5)   "0.95 (0.86, 0.99)" "-0.27 (-2.53, 2.29)" "-1.19 (-4.61, 0.61)" "0.14"              

All brms fits are identified. With each of the three prior scales, R-hat is at most 1.01, the bulk ESS is above 600, and there are no divergent transitions (for the full model we used adapt_delta = 0.999, because a few divergent transitions occurred at 0.95). The Student-t prior used by brms has lighter tails than the half-Cauchy prior, which is enough to keep the latent scale in a plausible region.

Across the identified fits, the qualitative conclusion is similar. Almost all of the latent-scale variation in habitat density is attributed to phylogeny (0.86 with the \(\chi^2_1\) prior in MCMCglmm and 0.91–0.95 in brms), and neither tail length nor herbivory has a clear effect: all 95% CIs include zero. The magnitudes, however, depend on the prior. The larger the prior scale of the phylogenetic SD, the larger the estimated phylogenetic variance and the larger (in absolute value) and more uncertain the fixed effects. For example, the herbivory effect ranges from −0.10 (95% CI −0.99, 0.73) with the \(\chi^2_1\) prior to −1.19 (−4.61, 0.61) with the Student-t(3, 0, 5) prior. This is expected, because the fixed effects are expressed in units of the latent residual SD, and the data determine the overall scale of the latent variable only weakly.

Important

Main message of Example 2. When almost all of the variation in an ordinal trait is phylogenetically structured, the latent scale of the threshold model can be weakly identified or not identified. Heavy-tailed priors on the phylogenetic variance (here, the half-Cauchy priors in MCMCglmm) may then fail to identify it, and the chains drift without converging. Priors that regularise the variance more (the \(\chi^2_1\) prior in MCMCglmm, the Student-t priors in brms) give identified fits with similar qualitative conclusions, but the magnitudes of the phylogenetic variance and of the fixed effects are prior-sensitive. In such cases, we recommend: (i) running several chains and checking R-hat and the effective sample sizes; (ii) checking the sensitivity of the results to the prior family and scale; (iii) focusing on quantities that are less sensitive to the latent scale, such as variance proportions and predicted category probabilities; and (iv) reporting the prior sensitivity together with the results. The data do not support any conclusion about the habitat preferences of herbivorous Phasianidae.

4. Nominal models

When the response variable has more than two unordered categories, it is treated as a nominal variable.

Prior setting

Obtaining well-mixing chains can be challenging, particularly for discrete (and especially nominal) models. In such cases, replacing the default prior with a carefully chosen custom prior (i.e. an appropriately regularising prior, not an overly informative one) can stabilise the estimation. Whether this helps, and how much it changes the results, depends on the scale of the parameters, how much information the data contain about them, (quasi-)separation and the model structure, so its effect should always be checked.

Fixed Effects

When the data contain plenty of information about the fixed effects, the prior on them has little influence on the results. This is often, but not always, the case in discrete models: with few observations per category or (quasi-)complete separation, flat or very wide priors on the fixed effects can lead to extreme estimates and poor mixing, and weakly informative (regularising) priors are then appropriate. Priors on fixed effects are also useful when you know biological limits (e.g. some values of bird morphology are not realistic). In MCMCglmm, the gelman.prior() function is particularly useful for logit and probit models. It constructs the prior covariance matrix for the fixed effects recommended by Gelman et al. (2008). Specifically…

  • For logit regression, where the variance of the latent error distribution is \(\pi^2/3\) (plus the units variance in MCMCglmm), the scale argument is set so that the prior is approximately flat on the probability scale.
  • For probit regression, where the variance of the latent error distribution is 1 (plus the units variance for family = "ordinal"), the scale is adjusted accordingly.

gelman.prior() thus provides weakly informative priors on the fixed effects that regularise extreme values without dominating the information in the data, as recommended by Gelman et al. (2008).

Random effects

Mixing problems often arise with random effects, because variance components are harder to estimate than fixed effects: they describe the variability around the mean, which requires more data to estimate precisely, and their posterior distributions are often skewed and bounded at zero. In contrast, fixed effects describe means (e.g. the overall mean in an intercept-only model) and usually stabilise with less data. More informative priors on the variance components can help in such cases, but they change what the posterior represents, so the benefit for sampling has to be weighed against their influence on the estimates that you interpret.

Key points

Check autocorrelation: An autocorrelation plot helps assess the mixing of the chain. It shows the relationship between current and past samples in the MCMC chain. High autocorrelation (i.e. slow decay) indicates poor mixing because the chain is highly correlated across iterations, meaning that it is not exploring the parameter space efficiently. Ideally, the autocorrelation should drop to near zero quickly, indicating that the chain is moving smoothly and independently across the parameter space. - Lag (X-axis): The interval between the current and past samples. - Autocorrelation coefficient (Y-axis): Indicates the strength of the correlation, ranging from -1 to 1. A value close to 0 (excluding lag 0) is ideal.

In the context of MCMC, it would be important to differentiate between convergence and mixing, as these two terms are often conflated, but they refer to distinct aspects of the MCMC process. Convergence refers to whether the Markov chains have reached the target posterior distribution. For MCMC to be valid, the chains should eventually explore the entire parameter space of the target distribution. In many cases, this is evaluated by running multiple chains from dispersed starting points and checking whether they all sample the same distribution (e.g. Rhat in brms). An Rhat value close to 1.0 indicates no evidence of non-convergence; it cannot prove convergence.

Mixing refers to how well the Markov chain explores the parameter space during the sampling process. Good mixing means that the chain moves smoothly across the parameter space, avoiding long periods of stagnation in any particular region. Poor mixing can result in the chain getting stuck in one area of the parameter space and not exploring others effectively.

In MCMCglmm, one call to MCMCglmm() runs a single chain. To check convergence with multiple chains, run the same model several times with different random seeds and compare the chains with coda::gelman.diag() or posterior::rhat(). We do this for every MCMCglmm model in this tutorial with run_mcmcglmm_chains() (see Note 4). Autocorrelation plots and effective sample sizes of a single chain give additional insight into the quality of the sampling.

Practical benefits of informative priors for random effects

Informative priors for random effects can regularise weakly identified variance components and sometimes improve mixing and shorten the required run length, because they restrict the sampler to a smaller region of the parameter space. This is not guaranteed, and poorly chosen priors can distort inference. Such priors change the point estimates (often) and the uncertainty (always) of the random effects (i.e. variances and covariances). Their impact on the fixed-effect estimates is often smaller, but it should be checked, because the random-effect variances determine the scale of the latent variable in discrete models. If your primary interest lies in the fixed effects, you can use this approach, but report the prior and check the sensitivity of your conclusions to it.

Potential problems with informative priors

  1. Over-constrained parameters: Excessively narrow priors can provide us with incorrect parameter estimates by limiting them to a small range.

  2. Small sample size issues: When sample sizes are small, informative priors may dominate the posterior distribution, overshadowing the data. This can lead to the model failing to capture the variability in the data and to an oversimplified representation of the data-generating process.

There is no general rule that “uninformative” priors are needed to estimate random effects properly, and truly uninformative priors for variance components do not exist: every prior has some influence when the data are weakly informative. A practical approach is to use weakly informative priors (e.g. parameter-expanded priors in MCMCglmm), check the sensitivity of the results to the prior, and follow a diagnostic workflow when the chains mix slowly:

  • run longer chains (more iterations after burn-in), which increases the number of effective posterior samples if the chains are sampling correctly
  • check that the burn-in (MCMCglmm) / warm-up (brms) is long enough to remove the initial transient; a longer burn-in alone does not guarantee convergence
  • run several independent chains and compare them (R-hat), together with trace plots, autocorrelation and effective sample sizes
  • increase thinning in MCMCglmm if needed to reduce storage requirements or autocorrelation among retained samples, although thinning itself does not improve convergence

Examples Here is an example of a weakly informative vs an informative prior…

inv_phylo  <- inverseA(tree, nodes = "ALL", scale = TRUE)

# weakly informative (parameter-expanded) prior
prior1 <- list(
  R = list(V = (matrix(1, 2, 2) + diag(2)) / 3, fix = 1), 
  G = list(G1 = list(V = diag(2), nu = 2, 
                     alpha.mu = rep(0, 2), alpha.V =  diag(2)
  )
  )
)

system.time(
    mcmcglmm_m1_100 <- MCMCglmm(Primary.Lifestyle ~ trait -1, 
                        random = ~us(trait):Phylo,
                        rcov = ~us(trait):units,
                        ginverse = list(Phylo = inv_phylo$Ainv),
                        family = "categorical",
                        data = dat,
                        prior = prior1,
                        nitt = 13000*100, 
                        thin = 10*100, 
                        burnin = 3000*100 
                        )
            )


system.time(
    mcmcglmm_m1_1000 <- MCMCglmm(Primary.Lifestyle ~ trait -1, 
                        random = ~us(trait):Phylo,
                        rcov = ~us(trait):units,
                        ginverse = list(Phylo = inv_phylo$Ainv),
                        family = "categorical",
                        data = dat,
                        prior = prior1,
                        nitt = 13000*1000,
                        thin = 10*1000,
                        burnin = 3000*1000
                        )
            )

# informative prior 
prior2 <- list(
  R = list(V = (matrix(1, 2, 2) + diag(2)) / 3, fix = 1), 
  G = list(G1 = list(V = diag(2), nu = 200, 
                     alpha.mu = rep(0, 2), alpha.V =  diag(2)
  )
  )
)

system.time(
    mcmcglmm_m2 <- MCMCglmm(Primary.Lifestyle ~ trait -1, 
                        random = ~us(trait):Phylo,
                        rcov = ~us(trait):units,
                        ginverse = list(Phylo = inv_phylo$Ainv),
                        family = "categorical",
                        data = dat,
                        prior = prior2,
                        nitt = 13000*75, 
                        thin = 10*75, 
                        burnin = 3000*75 
                        )
            )

These three fits are single-chain illustrations of the effect of chain length and prior; they are not part of the analyses that we interpret, so we show their summary() output directly. With the weakly informative prior, the short run (mcmcglmm_m1_100) has very small effective sample sizes for the variance components (about 30–40 out of 1,000 stored samples). Running the chain 10 times longer (mcmcglmm_m1_1000) increases the effective sample sizes (about 90–270), although the phylogenetic variances still mix slowly. With the informative prior (mcmcglmm_m2, nu = 200), the chain mixes well, but the variance estimates are pulled strongly towards the prior (compare the posterior means). Better mixing obtained through the prior is therefore not free: it changes the estimates.

summary(mcmcglmm_m1_100)
#>
#>  Iterations = 300001:1299001
#>  Thinning interval  = 1000
#>  Sample size  = 1000 
#>
#>  DIC: 259.4007 
#>
#>  G-structure:  ~us(trait):Phylo
#>
#>                                                                             post.mean l-95% CI u-95% CI eff.samp
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.Phylo     31.18    1.252    83.25    37.85
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.Phylo    -12.08  -70.340    59.37    41.96
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Terrestrial.Phylo    -12.08  -70.340    59.37    41.96
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.Phylo    104.23    1.118   408.78    28.75
#>
#>  R-structure:  ~us(trait):units
#>
#>                                                                             post.mean l-95% CI u-95% CI eff.samp
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.units    0.6667   0.6667   0.6667        0
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.units    0.3333   0.3333   0.3333        0
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Terrestrial.units    0.3333   0.3333   0.3333        0
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.units    0.6667   0.6667   0.6667        0
#>
#>  Location effects: Primary.Lifestyle ~ trait - 1 
#>
#>                                    post.mean l-95% CI u-95% CI eff.samp pMCMC
#> traitPrimary.Lifestyle.Insessorial    -1.628  -10.272    7.019    148.9 0.662
#> traitPrimary.Lifestyle.Terrestrial     5.279   -7.006   19.131    125.6 0.314

summary(mcmcglmm_m1_1000)
#>
#>  Iterations = 3000001:12990001
#>  Thinning interval  = 10000
#>  Sample size  = 1000 
#>
#>  DIC: 273.7709 
#>
#>  G-structure:  ~us(trait):Phylo
#>
#>                                                                             post.mean l-95% CI u-95% CI eff.samp
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.Phylo     30.88    1.314    99.34   270.66
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.Phylo    -25.45  -94.475    29.87   218.26
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Terrestrial.Phylo    -25.45  -94.475    29.87   218.26
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.Phylo     81.67    1.144   369.51    89.38
#>
#>  R-structure:  ~us(trait):units
#>
#>                                                                             post.mean l-95% CI u-95% CI eff.samp
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.units    0.6667   0.6667   0.6667        0
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.units    0.3333   0.3333   0.3333        0
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Terrestrial.units    0.3333   0.3333   0.3333        0
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.units    0.6667   0.6667   0.6667        0
#>
#>  Location effects: Primary.Lifestyle ~ trait - 1 
#>
#>                                    post.mean l-95% CI u-95% CI eff.samp pMCMC
#> traitPrimary.Lifestyle.Insessorial   -1.3033  -5.3581   2.3037     1000 0.478
#> traitPrimary.Lifestyle.Terrestrial    0.9187  -3.7341   5.2263     1000 0.676

summary(mcmcglmm_m2)
#>
#>  Iterations = 225001:974251
#>  Thinning interval  = 750
#>  Sample size  = 1000 
#>
#>  DIC: 298.0892 
#>
#>  G-structure:  ~us(trait):Phylo
#>
#>                                                                             post.mean l-95% CI u-95% CI eff.samp
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.Phylo    6.0759   0.2940  12.1120    731.3
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.Phylo   -0.0885  -0.9723   0.9838   1000.0
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Terrestrial.Phylo   -0.0885  -0.9723   0.9838   1000.0
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.Phylo    6.8348   1.5880  13.6006    820.7
#>
#>  R-structure:  ~us(trait):units
#>
#>                                                                             post.mean l-95% CI u-95% CI eff.samp
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.units    0.6667   0.6667   0.6667        0
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.units    0.3333   0.3333   0.3333        0
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Terrestrial.units    0.3333   0.3333   0.3333        0
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.units    0.6667   0.6667   0.6667        0
#>
#>  Location effects: Primary.Lifestyle ~ trait - 1 
#>
#>                                    post.mean l-95% CI u-95% CI eff.samp pMCMC
#> traitPrimary.Lifestyle.Insessorial   -0.3218  -3.7935   3.0988   1000.0 0.908
#> traitPrimary.Lifestyle.Terrestrial    2.2780  -1.3172   5.8322    888.4 0.180
autocorr.plot(mcmcglmm_m1_100$VCV)

autocorr.plot(mcmcglmm_m1_1000$VCV)

autocorr.plot(mcmcglmm_m2$VCV)

NoteNOTE

Comparing brms and MCMCglmm is a useful sensitivity and reproducibility check, even if the convergence diagnostics (e.g. R-hat, effective sample size) appear acceptable in the package you selected.

Why? brms allows more flexibility with setting priors, including specifying distributions for hyperparameters, while MCMCglmm has less flexibility, which can sometimes lead to over-constraining the posterior.

If the results are consistent in both packages, the findings are not overly sensitive to the software-specific prior settings and sampling algorithms. This agreement is reassuring, but it cannot establish convergence or correctness by itself: both fits can share the same problem (e.g. the same misspecified model, or weakly identified parameters dominated by similar priors). Use it together with, not instead of, the convergence diagnostics.

Explanation of dataset

Dataset for example 1

This dataset focuses on thrushes (Turdidae; 173 species). The response variable is the primary lifestyle (Generalist, Insessorial or Terrestrial; Generalist is the reference category), and we create a binary variable IsOmnivore based on the trophic level.

trees  <- read.nexus(here("data", "potential", "avonet", "trees.nex"))
tree <- trees[[1]]
dat <- read.csv(here("data", "potential", "avonet", "turdidae.csv"))

# Check tree$tip.label matches dat$Scientific_name
match_result <- setequal(tree$tip.label, dat$Phylo)

# Centering for continuous variables
dat <- dat %>%
  mutate(
    log_Mass_centered = scale(log(Mass), center = TRUE, scale = FALSE),
    log_Tail_Length_centered = scale(log(Tail.Length), center = TRUE, scale = FALSE)
  )

dat <- dat %>% 
 mutate(across(c(Trophic.Level, Trophic.Niche, Primary.Lifestyle, Migration, Habitat, Species.Status), as.factor))

dat$IsOmnivore <- ifelse(dat$Trophic.Level == "Omnivore", 1, 0)

Example 1

Intercept-only model

MCMCglmm

inv_phylo  <- inverseA(tree, nodes = "ALL", scale = TRUE)

prior5 <- list(
  R = list(V = (matrix(1, 2, 2) + diag(2)) / 3, fix = 1), 
  G = list(G1 = list(V = diag(2), nu = 20, 
                     alpha.mu = rep(0, 2), alpha.V = diag(2)*(pi^2/3))
           )
  )

 system.time(
   mcmcglmm_mn1_1 <- run_mcmcglmm_chains(Primary.Lifestyle ~ trait -1, 
                      random = ~us(trait):Phylo,
                      rcov = ~us(trait):units,
                      ginverse = list(Phylo = inv_phylo$Ainv),
                      family = "categorical",
                      data = dat,
                      prior = prior5,
                      seeds = c(20265126, 20265127, 20265128, 20265129), # one seed per chain (four chains)
                      nitt = 13000*100, 
                      thin = 10*100, 
                      burnin = 3000*100
                     )
            )

brms

A <- ape::vcv.phylo(tree, corr = TRUE)

# Weakly informative priors used for all three nominal brms models:
# half-normal(0, 2) on the phylogenetic SDs, Student-t(3, 0, 1) on the intercepts,
# LKJ(2) on the phylogenetic correlation, and Normal(0, 10) on the slopes (models 2 and 3).
# (With brms' default priors, the sampler produced divergent transitions for these models.)
nominal_priors <- function(slopes = character(0)) {
  p <- c(prior(student_t(3, 0, 1), class = "Intercept", dpar = "muInsessorial"),
         prior(student_t(3, 0, 1), class = "Intercept", dpar = "muTerrestrial"),
         prior(normal(0, 2), class = "sd", group = "Phylo", dpar = "muInsessorial"),
         prior(normal(0, 2), class = "sd", group = "Phylo", dpar = "muTerrestrial"),
         prior(lkj(2), class = "cor", group = "Phylo"))
  for (sl in slopes) for (dp in c("muInsessorial", "muTerrestrial"))
    p <- c(p, set_prior("normal(0, 10)", class = "b", coef = sl, dpar = dp))
  p
}
priors_brms1 <- nominal_priors()

system.time(
  brms_mn1_1 <- brm(Primary.Lifestyle ~ 1 + (1 |a| gr(Phylo, cov = A)),
                data = dat,
                data2 = list(A = A),
                family = categorical(link = "logit"),
                prior = priors_brms1,
                iter = 15000, 
                warmup = 10000,
                chains = 4,
                cores = 4,
                seed = 20270526,
                thin = 1
                )
            )

As with the binary logit model in MCMCglmm, we rescale the estimates of the nominal model to remove the effect of the additive overdispersion (units) term, using the code below. For a nominal response with \(K\) categories and the residual covariance matrix fixed at \((\mathbf{I} + \mathbf{J})/K\), the residual variance of each of the \(K-1\) latent variables is \((K-1)/K\) (here \(2/3\)), so the constant becomes \(c^2 (K-1)/K\) (Hadfield, MCMCglmm course notes). Integrating over the residuals, the category probabilities are then approximately those of a standard multinomial logit (as fitted by brms) with location effects \(\beta/\sqrt{1 + c^2 (K-1)/K}\) and (co)variances \(\Sigma/(1 + c^2 (K-1)/K)\).

The residual covariance matrix is fixed by this parameterisation. The correlation between the residuals of the two contrasts (0.5, from the fixed matrix) is therefore not an estimated model result, and we do not report it.

c2 <- (16 * sqrt(3)/(15 * pi))^2
c2a <- (16 * sqrt(3)/(15 * pi))^2*(2/3)

The results are…

summarise_chains(mcmcglmm_mn1_1)
#>                                                                      parameter    mean     sd    Q2.5   Q97.5  rhat ess_bulk ess_tail
#> 1                                           traitPrimary.Lifestyle.Insessorial -0.9318  2.865 -7.0620  4.2450 1.000     3536     3512
#> 2                                           traitPrimary.Lifestyle.Terrestrial  3.0440  2.808 -1.9320  9.4450 1.001     3333     3206
#> 3  traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.Phylo 15.9600 10.650  3.3580 43.3600 1.001     1904     2097
#> 4  traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.Phylo -1.2750  4.072 -9.7880  6.8650 1.000     3031     3159
#> 5  traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Terrestrial.Phylo -1.2750  4.072 -9.7880  6.8650 1.000     3031     3159
#> 6  traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.Phylo 17.4900 11.930  3.9910 48.2000 1.001     1777     1879
#> 7  traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.units  0.6667  0.000  0.6667  0.6667    NA       NA       NA
#> 8  traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.units  0.3333  0.000  0.3333  0.3333    NA       NA       NA
#> 9  traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Terrestrial.units  0.3333  0.000  0.3333  0.3333    NA       NA       NA
#> 10 traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.units  0.6667  0.000  0.6667  0.6667    NA       NA       NA

# c2-corrected estimates (K = 3 categories, so c2a = c2 * 2/3)
res_1 <- pool_chains(mcmcglmm_mn1_1, "Sol") / sqrt(1 + c2a) # fixed effects
res_2 <- pool_chains(mcmcglmm_mn1_1, "VCV") / (1 + c2a)     # (co)variances; the units (residual) rows are fixed, not estimated
posterior_summary(res_1)
#>                                      Estimate Est.Error      Q2.5    Q97.5
#> traitPrimary.Lifestyle.Insessorial -0.8399931  2.582816 -6.365688 3.826457
#> traitPrimary.Lifestyle.Terrestrial  2.7440926  2.531627 -1.741326 8.513926

posterior_summary(res_2)
#>                                                                               Estimate Est.Error       Q2.5      Q97.5
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.Phylo 12.9712860  8.652459  2.7288766 35.2387543
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.Phylo -1.0362239  3.309313 -7.9543471  5.5788505
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Terrestrial.Phylo -1.0362239  3.309313 -7.9543471  5.5788505
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.Phylo 14.2120253  9.690745  3.2436397 39.1677224
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.units  0.5417579  0.000000  0.5417579  0.5417579
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.units  0.2708789  0.000000  0.2708789  0.2708789
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Terrestrial.units  0.2708789  0.000000  0.2708789  0.2708789
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.units  0.5417579  0.000000  0.5417579  0.5417579

# phylogenetic correlation between the two contrasts, calculated for each posterior draw
# (a correlation is not changed by the c2 correction, so the raw (co)variances can be used)
VCV <- pool_chains(mcmcglmm_mn1_1, "VCV")
corr_phylo <- VCV[, "traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.Phylo"] /
  sqrt(VCV[, "traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.Phylo"] * VCV[, "traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.Phylo"])
posterior_summary(corr_phylo)
#>         Estimate Est.Error       Q2.5     Q97.5
#> var1 -0.09169124 0.2220161 -0.5066453 0.3467325

summary(brms_mn1_1)
#>  Family: categorical 
#>   Links: muInsessorial = logit; muTerrestrial = logit 
#> Formula: Primary.Lifestyle ~ 1 + (1 | a | gr(Phylo, cov = A)) 
#>    Data: dat (Number of observations: 173) 
#>   Draws: 4 chains, each with iter = 15000; warmup = 10000; thin = 1;
#>          total post-warmup draws = 20000
#>
#> Multilevel Hyperparameters:
#> ~Phylo (Number of levels: 173) 
#>                                                      Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(muInsessorial_Intercept)                              3.27      1.02     1.57     5.53 1.00     8105    11946
#> sd(muTerrestrial_Intercept)                              3.57      1.08     1.77     5.96 1.00     7498    12068
#> cor(muInsessorial_Intercept,muTerrestrial_Intercept)    -0.25      0.32    -0.80     0.40 1.00     3566     8096
#>
#> Regression Coefficients:
#>                         Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> muInsessorial_Intercept    -0.20      1.07    -2.50     1.86 1.00    21827    10668
#> muTerrestrial_Intercept     0.56      1.17    -1.45     3.15 1.00    22141    11093
#>
#> Draws were sampled using sampling(NUTS). 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).

After the c2 correction, the intercepts are very uncertain in both packages (Insessorial vs Generalist: −0.84, 95% CI −6.37, 3.83 in MCMCglmm and −0.20, −2.50, 1.86 in brms; Terrestrial vs Generalist: 2.74, −1.74, 8.51 and 0.56, −1.45, 3.15). The brms intervals are narrower, mainly because of its Student-t(3, 0, 1) prior on the intercepts. The phylogenetic variances are similar: the c2-corrected MCMCglmm variances are 13.0 and 14.2 (i.e. SDs of about 3.6 and 3.8), and the brms SDs are 3.27 and 3.57. The phylogenetic correlation between the two contrasts is weak and uncertain in both packages (−0.09, 95% CI −0.51, 0.35 in MCMCglmm; −0.25, −0.80, 0.40 in brms).

Phylogenetic variance proportions

For a nominal response, a single “phylogenetic signal” is not defined. Instead, we calculate a category-specific phylogenetic variance proportion on the latent scale of the reference-category multinomial logit. Each of the \(K-1\) linear predictors describes the log-odds of one category relative to the reference category (here, Generalist). In the random-utility representation of the multinomial logit, the latent residual of each such contrast is the difference between two standard Gumbel variables. This is a logistic variable with variance \(\pi^2/3\), and the residuals of two contrasts that share the reference category are correlated at 0.5. The proportion for contrast \(j\) is therefore

\[ H^2_j = \frac{\sigma^2_{\text{phylo},j}}{\sigma^2_{\text{phylo},j} + \pi^2/3}, \]

with the species-level variance added to the denominator when the model includes it. For MCMCglmm, we use the c2-corrected variances, which are on the scale of a standard multinomial logit (the same scale as brms). This quantity refers to a particular contrast and depends on the choice of reference category. It is neither an overall phylogenetic signal of the nominal trait nor Pagel’s \(\lambda\).

# Category-specific phylogenetic variance proportion on the latent scale of the
# reference-category multinomial logit (reference: Generalist):
#   sigma2_phylo / (sigma2_phylo + pi^2/3), calculated for each posterior draw

## MCMCglmm: c2-corrected phylogenetic variances
var_Insessorial <- pool_chains(mcmcglmm_mn1_1, "VCV")[, "traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.Phylo"] / (1 + c2a)
var_Terrestrial <- pool_chains(mcmcglmm_mn1_1, "VCV")[, "traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.Phylo"] / (1 + c2a)
posterior_summary(var_Insessorial / (var_Insessorial + pi^2 / 3)) # Insessorial vs Generalist
#>       Estimate Est.Error      Q2.5     Q97.5
#> var1 0.7463375 0.1204887 0.4533963 0.9146124

posterior_summary(var_Terrestrial / (var_Terrestrial + pi^2 / 3)) # Terrestrial vs Generalist
#>       Estimate Est.Error      Q2.5    Q97.5
#> var1 0.7620238 0.1122212 0.4964622 0.922514

## brms
draws <- as_draws_df(brms_mn1_1)
posterior_summary(with(draws, sd_Phylo__muInsessorial_Intercept^2 / (sd_Phylo__muInsessorial_Intercept^2 + pi^2 / 3)))
#>       Estimate Est.Error      Q2.5     Q97.5
#> [1,] 0.7303241 0.1238207 0.4273746 0.9028302

posterior_summary(with(draws, sd_Phylo__muTerrestrial_Intercept^2 / (sd_Phylo__muTerrestrial_Intercept^2 + pi^2 / 3)))
#>      Estimate Est.Error      Q2.5     Q97.5
#> [1,] 0.762588 0.1114635 0.4891413 0.9153566

For the Insessorial vs Generalist contrast, the phylogenetic variance proportion is 0.75 (95% CI 0.45, 0.91) in MCMCglmm and 0.73 (0.43, 0.90) in brms. For Terrestrial vs Generalist, it is 0.76 (0.50, 0.92) in both packages (brms: 0.49, 0.92). Both packages indicate that a large part of the latent-scale variation in these contrasts is attributed to phylogeny, i.e. closely related species tend to have similar lifestyles.

One continuous explanatory variable model

MCMCglmm

inv_phylo  <- inverseA(tree, nodes = "ALL", scale = TRUE)

prior5 <- list(
  R = list(V = (matrix(1, 2, 2) + diag(2)) / 3, fix = 1), 
  G = list(G1 = list(V = diag(2), nu = 20, 
                     alpha.mu = rep(0, 2), alpha.V = diag(2)*(pi^2/3))
           )
  )

system.time(
    mcmcglmm_mn1_2 <- run_mcmcglmm_chains(Primary.Lifestyle ~ log_Tail_Length_centered:trait + trait -1, 
                        random = ~us(trait):Phylo,
                        rcov = ~us(trait):units,
                        ginverse = list(Phylo = inv_phylo$Ainv),
                        family = "categorical",
                        data = dat,
                        prior = prior5,
                        seeds = c(20265226, 20265227, 20265228, 20265229), # one seed per chain (four chains)
                        nitt = 13000*200, 
                        thin = 10*200, 
                        burnin = 3000*200 
                        )
            )

brms

priors_brms2 <- nominal_priors("log_Tail_Length_centered")

system.time(
  brms_mn1_2 <- brm(Primary.Lifestyle ~ log_Tail_Length_centered + (1 |a| gr(Phylo, cov = A)),
                 data = dat,
                 data2 = list(A = A),
                 family = categorical(link = "logit"),
                 prior = priors_brms2,
                 iter = 15000,
                 warmup = 10000,
                 chains = 4,
                 cores = 4,
                 seed = 20270626,
                 thin = 1
  )
)

Output correction and summary

summarise_chains(mcmcglmm_mn1_2)
#>                                                                      parameter    mean     sd     Q2.5   Q97.5  rhat ess_bulk ess_tail
#> 1                                           traitPrimary.Lifestyle.Insessorial -0.9554  3.110  -7.7960  4.8520 1.001     3656     3226
#> 2                                           traitPrimary.Lifestyle.Terrestrial  3.1520  2.925  -2.1450  9.6970 1.001     3725     3812
#> 3                  log_Tail_Length_centered:traitPrimary.Lifestyle.Insessorial -1.2340  2.159  -5.8620  2.6810 1.000     3689     3812
#> 4                  log_Tail_Length_centered:traitPrimary.Lifestyle.Terrestrial -2.6770  2.148  -7.0490  1.4810 1.000     3906     3633
#> 5  traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.Phylo 18.4000 12.550   3.8860 51.1600 1.001     2663     2855
#> 6  traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.Phylo -0.8501  4.683 -10.3000  8.7690 1.001     3798     3526
#> 7  traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Terrestrial.Phylo -0.8501  4.683 -10.3000  8.7690 1.001     3798     3526
#> 8  traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.Phylo 18.8500 12.920   4.2340 52.7200 1.000     2455     2858
#> 9  traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.units  0.6667  0.000   0.6667  0.6667    NA       NA       NA
#> 10 traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.units  0.3333  0.000   0.3333  0.3333    NA       NA       NA
#> 11 traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Terrestrial.units  0.3333  0.000   0.3333  0.3333    NA       NA       NA
#> 12 traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.units  0.6667  0.000   0.6667  0.6667    NA       NA       NA

# c2-corrected estimates (K = 3 categories, so c2a = c2 * 2/3)
res_1 <- pool_chains(mcmcglmm_mn1_2, "Sol") / sqrt(1 + c2a) # fixed effects
res_2 <- pool_chains(mcmcglmm_mn1_2, "VCV") / (1 + c2a)     # (co)variances; the units (residual) rows are fixed, not estimated
posterior_summary(res_1)
#>                                                               Estimate Est.Error      Q2.5    Q97.5
#> traitPrimary.Lifestyle.Insessorial                          -0.8612723  2.803701 -7.027441 4.373773
#> traitPrimary.Lifestyle.Terrestrial                           2.8409701  2.637037 -1.933388 8.741751
#> log_Tail_Length_centered:traitPrimary.Lifestyle.Insessorial -1.1128506  1.945898 -5.283948 2.417050
#> log_Tail_Length_centered:traitPrimary.Lifestyle.Terrestrial -2.4134294  1.936139 -6.354559 1.335403

posterior_summary(res_2)
#>                                                                               Estimate Est.Error       Q2.5      Q97.5
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.Phylo 14.9506323 10.196735  3.1576515 41.5741178
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.Phylo -0.6908167  3.805556 -8.3737632  7.1258724
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Terrestrial.Phylo -0.6908167  3.805556 -8.3737632  7.1258724
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.Phylo 15.3168361 10.495985  3.4403951 42.8459507
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.units  0.5417579  0.000000  0.5417579  0.5417579
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.units  0.2708789  0.000000  0.2708789  0.2708789
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Terrestrial.units  0.2708789  0.000000  0.2708789  0.2708789
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.units  0.5417579  0.000000  0.5417579  0.5417579

# phylogenetic correlation between the two contrasts, calculated for each posterior draw
# (a correlation is not changed by the c2 correction, so the raw (co)variances can be used)
VCV <- pool_chains(mcmcglmm_mn1_2, "VCV")
corr_phylo <- VCV[, "traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.Phylo"] /
  sqrt(VCV[, "traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.Phylo"] * VCV[, "traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.Phylo"])
posterior_summary(corr_phylo)
#>         Estimate Est.Error       Q2.5     Q97.5
#> var1 -0.05767058 0.2283348 -0.4889063 0.4003758

summary(brms_mn1_2)
#>  Family: categorical 
#>   Links: muInsessorial = logit; muTerrestrial = logit 
#> Formula: Primary.Lifestyle ~ log_Tail_Length_centered + (1 | a | gr(Phylo, cov = A)) 
#>    Data: dat (Number of observations: 173) 
#>   Draws: 4 chains, each with iter = 15000; warmup = 10000; thin = 1;
#>          total post-warmup draws = 20000
#>
#> Multilevel Hyperparameters:
#> ~Phylo (Number of levels: 173) 
#>                                                      Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(muInsessorial_Intercept)                              3.45      1.06     1.66     5.77 1.00     7494    11487
#> sd(muTerrestrial_Intercept)                              3.62      1.12     1.75     6.09 1.00     6387    10639
#> cor(muInsessorial_Intercept,muTerrestrial_Intercept)    -0.17      0.33    -0.76     0.48 1.00     3875     7814
#>
#> Regression Coefficients:
#>                                        Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> muInsessorial_Intercept                   -0.19      1.09    -2.50     1.87 1.00    26606    11739
#> muTerrestrial_Intercept                    0.58      1.17    -1.45     3.21 1.00    23137     9866
#> muInsessorial_log_Tail_Length_centered    -0.88      1.86    -4.73     2.60 1.00    18463    14741
#> muTerrestrial_log_Tail_Length_centered    -2.24      1.82    -5.92     1.24 1.00    18016    15183
#>
#> Draws were sampled using sampling(NUTS). 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).

The slopes of tail length are negative for both contrasts in both packages (c2-corrected MCMCglmm: −1.11 [−5.28, 2.42] for Insessorial and −2.41 [−6.35, 1.34] for Terrestrial; brms: −0.88 [−4.73, 2.60] and −2.24 [−5.92, 1.24]), but all 95% CIs include zero. The phylogenetic variances are large (c2-corrected MCMCglmm variances 15.0 and 15.3; brms SDs 3.45 and 3.62), and the phylogenetic correlation between the two contrasts is weak and uncertain (−0.06 [−0.49, 0.40] and −0.17 [−0.76, 0.48]). The two packages give similar estimates.

Model with one continuous and one binary explanatory variable

MCMCglmm

prior5 <- list(
  R = list(V = (matrix(1, 2, 2) + diag(2)) / 3, fix = 1), 
  G = list(G1 = list(V = diag(2), nu = 20, 
                     alpha.mu = rep(0, 2), alpha.V = diag(2)*(pi^2/3))
           )
  )

system.time(
    mcmcglmm_mn1_3 <- run_mcmcglmm_chains(Primary.Lifestyle ~ log_Tail_Length_centered:trait + IsOmnivore:trait + trait -1, 
                        random = ~us(trait):Phylo,
                        rcov = ~us(trait):units,
                        ginverse = list(Phylo = inv_phylo$Ainv),
                        family = "categorical",
                        data = dat,
                        prior = prior5,
                        seeds = c(20265326, 20265327, 20265328, 20265329), # one seed per chain (four chains)
                        nitt = 13000*250, 
                        thin = 10*250, 
                        burnin = 3000*250 
                        )
            )

brms

# the same weakly informative priors as for models 1 and 2 (see nominal_priors() above)
custom_priors <- nominal_priors(c("log_Tail_Length_centered", "IsOmnivore"))

system.time(
brms_mn1_3 <- brm(
      Primary.Lifestyle ~ log_Tail_Length_centered + IsOmnivore + (1 |a| gr(Phylo, cov = A)),
      data = dat,
      data2 = list(A = A),
      family = categorical(link = "logit"),
      prior = custom_priors,
      iter = 15000, 
      warmup = 10000,
      chains = 4,
      cores = 4,
      seed = 20270326,
      thin = 1
    )
)

Outputs are…

summarise_chains(mcmcglmm_mn1_3)
#>                                                                      parameter    mean     sd     Q2.5   Q97.5  rhat ess_bulk ess_tail
#> 1                                           traitPrimary.Lifestyle.Insessorial -1.3670  3.510  -9.1020  4.9220 1.000     3528     3540
#> 2                                           traitPrimary.Lifestyle.Terrestrial  4.0320  3.017  -0.7998 10.9400 1.000     3591     3682
#> 3                  log_Tail_Length_centered:traitPrimary.Lifestyle.Insessorial -1.2630  2.378  -6.1590  3.1260 1.001     3705     3786
#> 4                  log_Tail_Length_centered:traitPrimary.Lifestyle.Terrestrial -1.9340  2.282  -6.5810  2.2900 1.001     3463     3528
#> 5                                traitPrimary.Lifestyle.Insessorial:IsOmnivore  0.1230  1.065  -1.8400  2.4450 1.000     3016     2888
#> 6                                traitPrimary.Lifestyle.Terrestrial:IsOmnivore -5.1330  1.400  -8.5070 -3.0240 1.000     2203     2692
#> 7  traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.Phylo 23.4400 15.860   5.0000 62.9100 1.000     2570     2883
#> 8  traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.Phylo -1.7940  5.434 -14.6500  8.2370 1.000     2771     2430
#> 9  traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Terrestrial.Phylo -1.7940  5.434 -14.6500  8.2370 1.000     2771     2430
#> 10 traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.Phylo 16.7100 15.520   0.4001 55.2900 1.000     1813     2163
#> 11 traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.units  0.6667  0.000   0.6667  0.6667    NA       NA       NA
#> 12 traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.units  0.3333  0.000   0.3333  0.3333    NA       NA       NA
#> 13 traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Terrestrial.units  0.3333  0.000   0.3333  0.3333    NA       NA       NA
#> 14 traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.units  0.6667  0.000   0.6667  0.6667    NA       NA       NA

# c2-corrected estimates (K = 3 categories, so c2a = c2 * 2/3)
res_1 <- pool_chains(mcmcglmm_mn1_3, "Sol") / sqrt(1 + c2a) # fixed effects
res_2 <- pool_chains(mcmcglmm_mn1_3, "VCV") / (1 + c2a)     # (co)variances; the units (residual) rows are fixed, not estimated
posterior_summary(res_1)
#>                                                               Estimate Est.Error       Q2.5     Q97.5
#> traitPrimary.Lifestyle.Insessorial                          -1.2323992 3.1638373 -8.2055382  4.437070
#> traitPrimary.Lifestyle.Terrestrial                           3.6344251 2.7201356 -0.7209607  9.862223
#> log_Tail_Length_centered:traitPrimary.Lifestyle.Insessorial -1.1382685 2.1435010 -5.5519990  2.818025
#> log_Tail_Length_centered:traitPrimary.Lifestyle.Terrestrial -1.7434276 2.0571589 -5.9326730  2.064426
#> traitPrimary.Lifestyle.Insessorial:IsOmnivore                0.1108556 0.9600501 -1.6584532  2.204396
#> traitPrimary.Lifestyle.Terrestrial:IsOmnivore               -4.6270949 1.2624973 -7.6686042 -2.726334

posterior_summary(res_2)
#>                                                                               Estimate Est.Error        Q2.5      Q97.5
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.Phylo 19.0450408 12.888904   4.0631255 51.1231196
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.Phylo -1.4580410  4.415706 -11.9056762  6.6936544
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Terrestrial.Phylo -1.4580410  4.415706 -11.9056762  6.6936544
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.Phylo 13.5753700 12.616010   0.3251009 44.9286093
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.units  0.5417579  0.000000   0.5417579  0.5417579
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.units  0.2708789  0.000000   0.2708789  0.2708789
#> traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Terrestrial.units  0.2708789  0.000000   0.2708789  0.2708789
#> traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.units  0.5417579  0.000000   0.5417579  0.5417579

# phylogenetic correlation between the two contrasts, calculated for each posterior draw
# (a correlation is not changed by the c2 correction, so the raw (co)variances can be used)
VCV <- pool_chains(mcmcglmm_mn1_3, "VCV")
corr_phylo <- VCV[, "traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Insessorial.Phylo"] /
  sqrt(VCV[, "traitPrimary.Lifestyle.Insessorial:traitPrimary.Lifestyle.Insessorial.Phylo"] * VCV[, "traitPrimary.Lifestyle.Terrestrial:traitPrimary.Lifestyle.Terrestrial.Phylo"])
posterior_summary(corr_phylo)
#>         Estimate Est.Error       Q2.5     Q97.5
#> var1 -0.09887199 0.2389172 -0.5391202 0.3738546

summary(brms_mn1_3)
#>  Family: categorical 
#>   Links: muInsessorial = logit; muTerrestrial = logit 
#> Formula: Primary.Lifestyle ~ log_Tail_Length_centered + IsOmnivore + (1 | a | gr(Phylo, cov = A)) 
#>    Data: dat (Number of observations: 173) 
#>   Draws: 4 chains, each with iter = 15000; warmup = 10000; thin = 1;
#>          total post-warmup draws = 20000
#>
#> Multilevel Hyperparameters:
#> ~Phylo (Number of levels: 173) 
#>                                                      Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(muInsessorial_Intercept)                              3.86      1.16     1.87     6.39 1.00     7032    10377
#> sd(muTerrestrial_Intercept)                              2.96      1.42     0.36     5.93 1.00     3675     4309
#> cor(muInsessorial_Intercept,muTerrestrial_Intercept)    -0.22      0.35    -0.82     0.51 1.00     7400    10609
#>
#> Regression Coefficients:
#>                                        Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> muInsessorial_Intercept                   -0.29      1.23    -2.90     2.01 1.00    20980    10780
#> muTerrestrial_Intercept                    2.55      1.15     0.62     5.09 1.00    13453    12633
#> muInsessorial_log_Tail_Length_centered    -0.92      2.00    -5.15     2.79 1.00    20438    15111
#> muInsessorial_IsOmnivore                   0.12      0.90    -1.59     1.97 1.00    17370    15540
#> muTerrestrial_log_Tail_Length_centered    -1.53      1.92    -5.38     2.23 1.00    20721    13386
#> muTerrestrial_IsOmnivore                  -4.46      1.09    -6.96    -2.75 1.00     8126    12176
#>
#> Draws were sampled using sampling(NUTS). 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).
  • The slopes of tail length are negative for both contrasts in both packages, but their 95% CIs include zero, so there is no clear evidence for an effect.
  • Omnivorous thrushes are much less likely than non-omnivorous thrushes to be terrestrial rather than generalist: IsOmnivore for the Terrestrial contrast is −4.63 (95% CI −7.67, −2.73; c2-corrected) in MCMCglmm and −4.46 (−6.96, −2.75) in brms. There is no clear difference for the Insessorial contrast (0.11 and 0.12, with intervals including zero).
  • Results from MCMCglmm and brms are consistent in the direction and magnitude of the effects.
  • The phylogenetic correlation between the two contrasts is weak and uncertain (−0.10 and −0.22).

Example 2

We use habitat-category data for the Sylviidae (Old World warblers; 294 species) as the response variable. Body mass and sedentary status (IsSedentary: 1 = sedentary, 0 = partially migratory or migratory) are used as explanatory variables.

Dataset for example 2

For Example 2, data for the Sylviidae (Old World warblers) were extracted from the AVONET dataset. The habitat categories were reclassified into three categories, migration status was converted into a binary variable (IsSedentary, 1 = sedentary), and body mass was log-transformed and centred.

dat <- read.csv(here("data", "bird body mass", "9993spp_clearned.csv"))
dat <- dat %>% 
  mutate(across(c(Trophic.Level, Trophic.Niche,
                  Primary.Lifestyle, Migration, Habitat, Habitat.Density, Species.Status), as.factor),
         Habitat.Density = factor(Habitat.Density, ordered = TRUE))

dat <- dat %>%
  mutate(
    Habitat_Category = case_when(
      Habitat %in% c("Desert", "Rock", "Grassland", "Shrubland") ~ "Arid_Open",
      Habitat %in% c("Woodland", "Forest") ~ "Forested_Vegetated",
      Habitat == "Human modified" ~ "Human-Modified",
      Habitat %in% c("Riverine", "Coastal", "Marine", "Wetland") ~ "Aquatic_Coastal",
      TRUE ~ "Other"  
    )
  )

Family_habitat <- dat %>% 
  group_by(Family, Habitat_Category) %>%
  tally() %>%
  group_by(Family) %>% 
  ungroup()

Sylviidae_dat <- dat %>% 
  filter(Family == "Sylviidae") %>% 
  mutate(Habitat_Category = factor(Habitat_Category, ordered = FALSE))

Sylviidae_dat <- Sylviidae_dat %>%
  mutate(IsSedentary = ifelse(Migration == "1", 1, 0) # AVONET: Migration 1 = sedentary
         )
table(Sylviidae_dat$Habitat_Category)
Sylviidae_dat <- Sylviidae_dat %>%
  mutate(Habitat_Category = recode(Habitat_Category, 
                                   "Aquatic_Coastal" = "Others", 
                                   "Other" = "Others"))

Sylviidae_dat$cMass <- scale(log(Sylviidae_dat$Mass), center = TRUE, scale = FALSE)

Sylviidae_tree <- read.nexus(here("data", "bird body mass", "Sylviidae.nex"))
tree <- Sylviidae_tree[[1]]

Intercept-only model

MCMCglmm

inv_phylo  <- inverseA(tree, nodes = "ALL", scale = TRUE)

prior <- list(
  R = list(V = (matrix(1, 2, 2) + diag(2)) / 3, fix = 1), 
  G = list(G1 = list(V = diag(2), nu = 20, 
                     alpha.mu = rep(0, 2), alpha.V = diag(2)*(pi^2/3))
  )
)

mcmcglmm_mn2_1 <- run_mcmcglmm_chains(Habitat_Category ~ trait -1, 
                  random = ~us(trait):Phylo,
                  rcov = ~us(trait):units,
                  ginverse = list(Phylo = inv_phylo$Ainv),
                  family = "categorical",
                  data = Sylviidae_dat,
                  prior = prior,
                  seeds = c(20265426, 20265427, 20265428, 20265429), # one seed per chain (four chains)
                  nitt = 13000*500, 
                  thin = 10*500, 
                  burnin = 3000*500 
)

brms

A <- ape::vcv.phylo(tree, corr = TRUE)
priors_brms1 <- default_prior(Habitat_Category ~ 1 + (1 |a| gr(Phylo, cov = A)),
                        data = Sylviidae_dat,
                        data2 = list(A = A),
                        family = categorical(link = "logit")
                        )

system.time(
  brms_mn2_1 <- brm(Habitat_Category ~ 1 + (1 |a| gr(Phylo, cov = A)),
                data = Sylviidae_dat,
                data2 = list(A = A),
                family = categorical(link = "logit"),
                prior = priors_brms1,
                iter = 18000, 
                warmup = 8000,
                chains = 4,
                cores = 4,
                seed = 20272326,
                thin = 1
                )
            )

Before checking the output, we need to correct the results from MCMCglmm using the following equation:

c2 <- (16 * sqrt(3) / (15 * pi))^2
c2a <- c2*(2/3)
summarise_chains(mcmcglmm_mn2_1)
#>                                                                                  parameter    mean    sd     Q2.5   Q97.5  rhat ess_bulk
#> 1                                                          traitHabitat_Category.Arid_Open  2.3960 1.247  0.08665  4.9900 0.999     3010
#> 2                                                 traitHabitat_Category.Forested_Vegetated  4.5260 1.575  1.85600  8.0370 1.000     3599
#> 3                    traitHabitat_Category.Arid_Open:traitHabitat_Category.Arid_Open.Phylo  7.1010 4.635  1.56500 19.7000 1.001     3627
#> 4           traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Arid_Open.Phylo  1.9300 2.490 -2.09400  7.8600 1.000     3593
#> 5           traitHabitat_Category.Arid_Open:traitHabitat_Category.Forested_Vegetated.Phylo  1.9300 2.490 -2.09400  7.8600 1.000     3593
#> 6  traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Forested_Vegetated.Phylo 14.5300 7.932  4.60000 33.8700 1.000     3351
#> 7                    traitHabitat_Category.Arid_Open:traitHabitat_Category.Arid_Open.units  0.6667 0.000  0.66670  0.6667    NA       NA
#> 8           traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Arid_Open.units  0.3333 0.000  0.33330  0.3333    NA       NA
#> 9           traitHabitat_Category.Arid_Open:traitHabitat_Category.Forested_Vegetated.units  0.3333 0.000  0.33330  0.3333    NA       NA
#> 10 traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Forested_Vegetated.units  0.6667 0.000  0.66670  0.6667    NA       NA
#>    ess_tail
#> 1      3708
#> 2      3765
#> 3      3935
#> 4      3598
#> 5      3598
#> 6      3373
#> 7        NA
#> 8        NA
#> 9        NA
#> 10       NA

# c2-corrected estimates (K = 3 categories, so c2a = c2 * 2/3)
res_1 <- pool_chains(mcmcglmm_mn2_1, "Sol") / sqrt(1 + c2a) # fixed effects
res_2 <- pool_chains(mcmcglmm_mn2_1, "VCV") / (1 + c2a)     # (co)variances; the units (residual) rows are fixed, not estimated
posterior_summary(res_1)
#>                                          Estimate Est.Error       Q2.5    Q97.5
#> traitHabitat_Category.Arid_Open          2.160304  1.124218 0.07810752 4.498600
#> traitHabitat_Category.Forested_Vegetated 4.079834  1.420158 1.67266839 7.245016

posterior_summary(res_2)
#>                                                                                           Estimate Est.Error       Q2.5      Q97.5
#> traitHabitat_Category.Arid_Open:traitHabitat_Category.Arid_Open.Phylo                    5.7702266  3.766750  1.2715460 16.0126913
#> traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Arid_Open.Phylo           1.5681213  2.023516 -1.7013510  6.3871177
#> traitHabitat_Category.Arid_Open:traitHabitat_Category.Forested_Vegetated.Phylo           1.5681213  2.023516 -1.7013510  6.3871177
#> traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Forested_Vegetated.Phylo 11.8072430  6.446057  3.7381973 27.5219928
#> traitHabitat_Category.Arid_Open:traitHabitat_Category.Arid_Open.units                    0.5417579  0.000000  0.5417579  0.5417579
#> traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Arid_Open.units           0.2708789  0.000000  0.2708789  0.2708789
#> traitHabitat_Category.Arid_Open:traitHabitat_Category.Forested_Vegetated.units           0.2708789  0.000000  0.2708789  0.2708789
#> traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Forested_Vegetated.units  0.5417579  0.000000  0.5417579  0.5417579

# phylogenetic correlation between the two contrasts, calculated for each posterior draw
# (a correlation is not changed by the c2 correction, so the raw (co)variances can be used)
VCV <- pool_chains(mcmcglmm_mn2_1, "VCV")
corr_phylo <- VCV[, "traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Arid_Open.Phylo"] /
  sqrt(VCV[, "traitHabitat_Category.Arid_Open:traitHabitat_Category.Arid_Open.Phylo"] * VCV[, "traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Forested_Vegetated.Phylo"])
posterior_summary(corr_phylo)
#>       Estimate Est.Error     Q2.5     Q97.5
#> var1 0.1891776 0.2055073 -0.22399 0.5703709

summary(brms_mn2_1)
#>  Family: categorical 
#>   Links: muAridOpen = logit; muForestedVegetated = logit 
#> Formula: Habitat_Category ~ 1 + (1 | a | gr(Phylo, cov = A)) 
#>    Data: Sylviidae_dat (Number of observations: 294) 
#>   Draws: 4 chains, each with iter = 18000; warmup = 8000; thin = 1;
#>          total post-warmup draws = 40000
#>
#> Multilevel Hyperparameters:
#> ~Phylo (Number of levels: 294) 
#>                                                         Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(muAridOpen_Intercept)                                    2.56      0.89     1.22     4.64 1.00     7469    12859
#> sd(muForestedVegetated_Intercept)                           4.16      1.31     2.22     7.20 1.00     4175    10490
#> cor(muAridOpen_Intercept,muForestedVegetated_Intercept)     0.44      0.30    -0.25     0.90 1.00     2042     5685
#>
#> Regression Coefficients:
#>                               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> muAridOpen_Intercept              2.23      1.15    -0.00     4.62 1.00     6913    13544
#> muForestedVegetated_Intercept     3.62      1.49     0.90     6.83 1.00    24620    22982
#>
#> Draws were sampled using sampling(NUTS). 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).

The category-specific phylogenetic variance proportions (reference category: Others) are…

# Category-specific phylogenetic variance proportion on the latent scale of the
# reference-category multinomial logit (reference: Others):
#   sigma2_phylo / (sigma2_phylo + pi^2/3), calculated for each posterior draw

## MCMCglmm: c2-corrected phylogenetic variances
var_Arid_Open <- pool_chains(mcmcglmm_mn2_1, "VCV")[, "traitHabitat_Category.Arid_Open:traitHabitat_Category.Arid_Open.Phylo"] / (1 + c2a)
var_Forested_Vegetated <- pool_chains(mcmcglmm_mn2_1, "VCV")[, "traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Forested_Vegetated.Phylo"] / (1 + c2a)
posterior_summary(var_Arid_Open / (var_Arid_Open + pi^2 / 3)) # Arid_Open vs Others
#>       Estimate Est.Error      Q2.5     Q97.5
#> var1 0.5847748  0.142353 0.2787614 0.8295631

posterior_summary(var_Forested_Vegetated / (var_Forested_Vegetated + pi^2 / 3)) # Forested_Vegetated vs Others
#>       Estimate  Est.Error      Q2.5     Q97.5
#> var1 0.7473551 0.09442562 0.5318956 0.8932272

## brms
draws <- as_draws_df(brms_mn2_1)
posterior_summary(with(draws, sd_Phylo__muAridOpen_Intercept^2 / (sd_Phylo__muAridOpen_Intercept^2 + pi^2 / 3)))
#>       Estimate Est.Error      Q2.5     Q97.5
#> [1,] 0.6276732 0.1459667 0.3124393 0.8676483

posterior_summary(with(draws, sd_Phylo__muForestedVegetated_Intercept^2 / (sd_Phylo__muForestedVegetated_Intercept^2 + pi^2 / 3)))
#>       Estimate  Est.Error      Q2.5     Q97.5
#> [1,] 0.8118798 0.08821898 0.5995163 0.9402853

The reference category is Others, which contains the 34 wetland species plus one species associated with human-modified habitats and one species without habitat information. The intercepts are the log-odds of each habitat category relative to Others for a species at the phylogenetic mean. They are positive (c2-corrected MCMCglmm: 2.16 [0.08, 4.50] for Arid_Open and 4.08 [1.67, 7.25] for Forested_Vegetated; brms: 2.23 [−0.00, 4.62] and 3.62 [0.90, 6.83]), reflecting that most warblers are in the two larger categories.

The phylogenetic variance proportion is higher for the Forested_Vegetated vs Others contrast (0.75, 95% CI 0.53, 0.89 in MCMCglmm; 0.81, 0.60, 0.94 in brms) than for Arid_Open vs Others (0.58, 0.28, 0.83; 0.63, 0.31, 0.87). The phylogenetic correlation between the two contrasts tends to be positive (0.19 [−0.22, 0.57] and 0.44 [−0.25, 0.90]), but its 95% credible intervals include zero in both packages.

One continuous explanatory variable model

MCMCglmm

mcmcglmm_mn2_2 <- run_mcmcglmm_chains(Habitat_Category ~ cMass:trait + trait -1, 
                  random = ~us(trait):Phylo,
                  rcov = ~us(trait):units,
                  ginverse = list(Phylo = inv_phylo$Ainv),
                  family = "categorical",
                  data = Sylviidae_dat,
                  prior = prior,
                  seeds = c(20265526, 20265527, 20265528, 20265529), # one seed per chain (four chains)
                  nitt = 13000*500, 
                  thin = 10*500, 
                  burnin = 3000*500 
)

brms

A <- ape::vcv.phylo(tree, corr = TRUE)
priors_brms2 <- default_prior(Habitat_Category ~ cMass + (1 |a| gr(Phylo, cov = A)),
                        data = Sylviidae_dat,
                        data2 = list(A = A),
                        family = categorical(link = "logit")
                        )

system.time(
  brms_mn2_2 <- brm(Habitat_Category ~ cMass + (1 |a| gr(Phylo, cov = A)),
                data = Sylviidae_dat,
                data2 = list(A = A),
                family = categorical(link = "logit"),
                prior = priors_brms2,
                iter = 18000, 
                warmup = 8000,
                chains = 4,
                cores = 4,
                seed = 20272926,
                control = list(adapt_delta = 0.999),
                thin = 1
                )
            )
summarise_chains(mcmcglmm_mn2_2)
#>                                                                                  parameter    mean      sd    Q2.5   Q97.5  rhat ess_bulk
#> 1                                                          traitHabitat_Category.Arid_Open  2.6340  1.3220  0.2029  5.4390 1.001     3283
#> 2                                                 traitHabitat_Category.Forested_Vegetated  4.7240  1.7630  1.6470  8.6880 1.000     3461
#> 3                                                    cMass:traitHabitat_Category.Arid_Open -0.7872  0.7814 -2.3650  0.7039 1.000     3642
#> 4                                           cMass:traitHabitat_Category.Forested_Vegetated  1.0230  0.9563 -0.8467  2.9920 1.000     4112
#> 5                    traitHabitat_Category.Arid_Open:traitHabitat_Category.Arid_Open.Phylo  7.6270  5.3110  1.5310 20.8300 1.001     3280
#> 6           traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Arid_Open.Phylo  2.2770  3.1490 -2.6480 10.0800 1.001     3451
#> 7           traitHabitat_Category.Arid_Open:traitHabitat_Category.Forested_Vegetated.Phylo  2.2770  3.1490 -2.6480 10.0800 1.001     3451
#> 8  traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Forested_Vegetated.Phylo 18.5300 10.2100  5.8350 43.7200 1.001     3130
#> 9                    traitHabitat_Category.Arid_Open:traitHabitat_Category.Arid_Open.units  0.6667  0.0000  0.6667  0.6667    NA       NA
#> 10          traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Arid_Open.units  0.3333  0.0000  0.3333  0.3333    NA       NA
#> 11          traitHabitat_Category.Arid_Open:traitHabitat_Category.Forested_Vegetated.units  0.3333  0.0000  0.3333  0.3333    NA       NA
#> 12 traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Forested_Vegetated.units  0.6667  0.0000  0.6667  0.6667    NA       NA
#>    ess_tail
#> 1      3509
#> 2      3419
#> 3      3738
#> 4      4055
#> 5      3380
#> 6      3233
#> 7      3233
#> 8      3314
#> 9        NA
#> 10       NA
#> 11       NA
#> 12       NA

# c2-corrected estimates (K = 3 categories, so c2a = c2 * 2/3)
res_1 <- pool_chains(mcmcglmm_mn2_2, "Sol") / sqrt(1 + c2a) # fixed effects
res_2 <- pool_chains(mcmcglmm_mn2_2, "VCV") / (1 + c2a)     # (co)variances; the units (residual) rows are fixed, not estimated
posterior_summary(res_1)
#>                                                  Estimate Est.Error       Q2.5    Q97.5
#> traitHabitat_Category.Arid_Open                 2.3748428 1.1914160  0.1829041 4.903220
#> traitHabitat_Category.Forested_Vegetated        4.2581539 1.5891416  1.4844561 7.832036
#> cMass:traitHabitat_Category.Arid_Open          -0.7096197 0.7044276 -2.1317032 0.634580
#> cMass:traitHabitat_Category.Forested_Vegetated  0.9218267 0.8620547 -0.7632911 2.696953

posterior_summary(res_2)
#>                                                                                           Estimate Est.Error       Q2.5      Q97.5
#> traitHabitat_Category.Arid_Open:traitHabitat_Category.Arid_Open.Phylo                    6.1981391  4.316021  1.2439553 16.9307269
#> traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Arid_Open.Phylo           1.8500984  2.559050 -2.1519801  8.1937077
#> traitHabitat_Category.Arid_Open:traitHabitat_Category.Forested_Vegetated.Phylo           1.8500984  2.559050 -2.1519801  8.1937077
#> traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Forested_Vegetated.Phylo 15.0599978  8.298526  4.7420004 35.5286096
#> traitHabitat_Category.Arid_Open:traitHabitat_Category.Arid_Open.units                    0.5417579  0.000000  0.5417579  0.5417579
#> traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Arid_Open.units           0.2708789  0.000000  0.2708789  0.2708789
#> traitHabitat_Category.Arid_Open:traitHabitat_Category.Forested_Vegetated.units           0.2708789  0.000000  0.2708789  0.2708789
#> traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Forested_Vegetated.units  0.5417579  0.000000  0.5417579  0.5417579

# phylogenetic correlation between the two contrasts, calculated for each posterior draw
# (a correlation is not changed by the c2 correction, so the raw (co)variances can be used)
VCV <- pool_chains(mcmcglmm_mn2_2, "VCV")
corr_phylo <- VCV[, "traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Arid_Open.Phylo"] /
  sqrt(VCV[, "traitHabitat_Category.Arid_Open:traitHabitat_Category.Arid_Open.Phylo"] * VCV[, "traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Forested_Vegetated.Phylo"])
posterior_summary(corr_phylo)
#>       Estimate Est.Error       Q2.5    Q97.5
#> var1 0.1888491 0.2145376 -0.2500307 0.578823

summary(brms_mn2_2)
#>  Family: categorical 
#>   Links: muAridOpen = logit; muForestedVegetated = logit 
#> Formula: Habitat_Category ~ cMass + (1 | a | gr(Phylo, cov = A)) 
#>    Data: Sylviidae_dat (Number of observations: 294) 
#>   Draws: 4 chains, each with iter = 18000; warmup = 8000; thin = 1;
#>          total post-warmup draws = 40000
#>
#> Multilevel Hyperparameters:
#> ~Phylo (Number of levels: 294) 
#>                                                         Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(muAridOpen_Intercept)                                    2.65      0.98     1.23     5.00 1.00     4779     8265
#> sd(muForestedVegetated_Intercept)                           4.98      1.67     2.57     8.96 1.00     2954     7819
#> cor(muAridOpen_Intercept,muForestedVegetated_Intercept)     0.46      0.32    -0.26     0.92 1.00     1283     3844
#>
#> Regression Coefficients:
#>                               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> muAridOpen_Intercept              2.42      1.20     0.07     4.94 1.00     3950     8590
#> muForestedVegetated_Intercept     3.58      1.70     0.45     7.21 1.00    19446    18306
#> muAridOpen_cMass                 -0.57      0.77    -2.13     0.91 1.00    11786    15863
#> muForestedVegetated_cMass         1.28      1.06    -0.64     3.56 1.00    12291    16100
#>
#> Draws were sampled using sampling(NUTS). 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).

Summary

  • Intercepts: The intercepts are the log-odds of belonging to a habitat category rather than the reference category (Others) for a species of average body mass (cMass is centred, so cMass = 0 is the mean log body mass) at the phylogenetic mean. They are positive for Arid_Open and Forested_Vegetated (c2-corrected MCMCglmm: 2.37 [0.18, 4.90] and 4.26 [1.48, 7.83]; brms: 2.43 [0.08, 4.94] and 3.58 [0.45, 7.21]). Such species are therefore more likely to occur in these two habitat categories than in Others.

  • Mass effects: The slope of centred log body mass (cMass) is negative for Arid_Open (−0.71 and −0.57) and positive for Forested_Vegetated (0.92 and 1.28), but in both packages the 95% credible intervals include zero, so there is no clear evidence for an effect.

  • Phylogenetic variances: Substantial phylogenetic variance remains for both contrasts (c2-corrected MCMCglmm variances 6.20 and 15.06; brms SDs 2.65 and 4.98).

  • Phylogenetic correlation: The correlation between the two contrasts is positive but highly uncertain (0.19 [−0.25, 0.58] and 0.46 [−0.26, 0.92]), with 95% credible intervals including zero.

Model with one continuous and one binary explanatory variable

MCMCglmm

mcmcglmm_mn2_3 <- run_mcmcglmm_chains(Habitat_Category ~ cMass:trait + IsSedentary:trait + trait -1, 
                  random = ~us(trait):Phylo,
                  rcov = ~us(trait):units,
                  ginverse = list(Phylo = inv_phylo$Ainv),
                  family = "categorical",
                  data = Sylviidae_dat,
                  prior = prior,
                  seeds = c(20262126, 20262127, 20262128, 20262129), # one seed per chain (four chains)
                  nitt = 13000*750, 
                  thin = 10*750, 
                  burnin = 3000*750 
)

brms

A <- ape::vcv.phylo(tree, corr = TRUE)
priors_brms3 <- default_prior(Habitat_Category ~ cMass + IsSedentary + (1 |a| gr(Phylo, cov = A)),
                        data = Sylviidae_dat,
                        data2 = list(A = A),
                        family = categorical(link = "logit")
                        )

system.time(
  brms_mn2_3 <- brm(Habitat_Category ~ cMass + IsSedentary + (1 |a| gr(Phylo, cov = A)),
                data = Sylviidae_dat,
                data2 = list(A = A),
                family = categorical(link = "logit"),
                prior = priors_brms3,
                iter = 18000, 
                warmup = 8000,
                chains = 4,
                cores = 4,
                seed = 20272826,
                control = list(adapt_delta = 0.999),
                thin = 1
                )
            )
summarise_chains(mcmcglmm_mn2_3)
#>                                                                                  parameter    mean      sd    Q2.5   Q97.5  rhat ess_bulk
#> 1                                                          traitHabitat_Category.Arid_Open  2.4200  1.4200 -0.1270  5.4920 1.000     3726
#> 2                                                 traitHabitat_Category.Forested_Vegetated  2.5190  1.8150 -0.9269  6.3210 1.001     3770
#> 3                                                    cMass:traitHabitat_Category.Arid_Open -0.9057  0.8288 -2.5890  0.7164 1.000     4002
#> 4                                           cMass:traitHabitat_Category.Forested_Vegetated  0.8202  1.0510 -1.1400  3.0170 1.000     3482
#> 5                                              traitHabitat_Category.Arid_Open:IsSedentary  0.4466  0.7255 -0.9812  1.8400 1.000     4126
#> 6                                     traitHabitat_Category.Forested_Vegetated:IsSedentary  3.0130  0.9842  1.2350  5.1370 1.000     3691
#> 7                    traitHabitat_Category.Arid_Open:traitHabitat_Category.Arid_Open.Phylo  9.2020  6.8050  1.7900 26.9800 1.001     3158
#> 8           traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Arid_Open.Phylo  1.8500  3.5310 -4.0690 10.0900 1.001     3633
#> 9           traitHabitat_Category.Arid_Open:traitHabitat_Category.Forested_Vegetated.Phylo  1.8500  3.5310 -4.0690 10.0900 1.001     3633
#> 10 traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Forested_Vegetated.Phylo 20.2300 12.3800  5.6640 50.6900 1.000     3169
#> 11                   traitHabitat_Category.Arid_Open:traitHabitat_Category.Arid_Open.units  0.6667  0.0000  0.6667  0.6667    NA       NA
#> 12          traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Arid_Open.units  0.3333  0.0000  0.3333  0.3333    NA       NA
#> 13          traitHabitat_Category.Arid_Open:traitHabitat_Category.Forested_Vegetated.units  0.3333  0.0000  0.3333  0.3333    NA       NA
#> 14 traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Forested_Vegetated.units  0.6667  0.0000  0.6667  0.6667    NA       NA
#>    ess_tail
#> 1      3759
#> 2      3520
#> 3      3763
#> 4      3891
#> 5      3836
#> 6      3559
#> 7      3358
#> 8      3541
#> 9      3541
#> 10     3408
#> 11       NA
#> 12       NA
#> 13       NA
#> 14       NA

# c2-corrected estimates (K = 3 categories, so c2a = c2 * 2/3)
res_1 <- pool_chains(mcmcglmm_mn2_3, "Sol") / sqrt(1 + c2a) # fixed effects
res_2 <- pool_chains(mcmcglmm_mn2_3, "VCV") / (1 + c2a)     # (co)variances; the units rows are fixed
posterior_summary(res_1)
#>                                                        Estimate Est.Error       Q2.5     Q97.5
#> traitHabitat_Category.Arid_Open                       2.1813459 1.2797391 -0.1145080 4.9509953
#> traitHabitat_Category.Forested_Vegetated              2.2709316 1.6358620 -0.8355574 5.6985839
#> cMass:traitHabitat_Category.Arid_Open                -0.8164503 0.7471639 -2.3336545 0.6458467
#> cMass:traitHabitat_Category.Forested_Vegetated        0.7393625 0.9476727 -1.0278661 2.7197079
#> traitHabitat_Category.Arid_Open:IsSedentary           0.4026041 0.6540102 -0.8844728 1.6585819
#> traitHabitat_Category.Forested_Vegetated:IsSedentary  2.7157353 0.8872243  1.1128631 4.6308503

posterior_summary(res_2)
#>                                                                                           Estimate Est.Error       Q2.5      Q97.5
#> traitHabitat_Category.Arid_Open:traitHabitat_Category.Arid_Open.Phylo                    7.4781171  5.530261  1.4546449 21.9218257
#> traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Arid_Open.Phylo           1.5035466  2.869816 -3.3064294  8.1966051
#> traitHabitat_Category.Arid_Open:traitHabitat_Category.Forested_Vegetated.Phylo           1.5035466  2.869816 -3.3064294  8.1966051
#> traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Forested_Vegetated.Phylo 16.4417601 10.061428  4.6023748 41.1917348
#> traitHabitat_Category.Arid_Open:traitHabitat_Category.Arid_Open.units                    0.5417579  0.000000  0.5417579  0.5417579
#> traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Arid_Open.units           0.2708789  0.000000  0.2708789  0.2708789
#> traitHabitat_Category.Arid_Open:traitHabitat_Category.Forested_Vegetated.units           0.2708789  0.000000  0.2708789  0.2708789
#> traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Forested_Vegetated.units  0.5417579  0.000000  0.5417579  0.5417579

# phylogenetic correlation between the two contrasts, calculated for each pooled draw
VCV <- pool_chains(mcmcglmm_mn2_3, "VCV")
corr_phylo <- VCV[, "traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Arid_Open.Phylo"] /
  sqrt(VCV[, "traitHabitat_Category.Arid_Open:traitHabitat_Category.Arid_Open.Phylo"] *
         VCV[, "traitHabitat_Category.Forested_Vegetated:traitHabitat_Category.Forested_Vegetated.Phylo"])
posterior_summary(corr_phylo)
#>       Estimate Est.Error       Q2.5     Q97.5
#> var1 0.1346218 0.2216828 -0.3129609 0.5467625

summary(brms_mn2_3)
#>  Family: categorical 
#>   Links: muAridOpen = logit; muForestedVegetated = logit 
#> Formula: Habitat_Category ~ cMass + IsSedentary + (1 | a | gr(Phylo, cov = A)) 
#>    Data: Sylviidae_dat (Number of observations: 294) 
#>   Draws: 4 chains, each with iter = 18000; warmup = 8000; thin = 1;
#>          total post-warmup draws = 40000
#>
#> Multilevel Hyperparameters:
#> ~Phylo (Number of levels: 294) 
#>                                                         Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(muAridOpen_Intercept)                                    2.95      1.26     1.27     6.04 1.00     5011     8361
#> sd(muForestedVegetated_Intercept)                           6.49      3.47     2.66    15.31 1.00     2716     6752
#> cor(muAridOpen_Intercept,muForestedVegetated_Intercept)     0.35      0.36    -0.44     0.91 1.00     1289     2677
#>
#> Regression Coefficients:
#>                                 Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> muAridOpen_Intercept                2.06      1.30    -0.48     4.72 1.00     8225    13625
#> muForestedVegetated_Intercept       0.77      2.35    -4.34     5.03 1.00    12228    10688
#> muAridOpen_cMass                   -0.63      0.83    -2.33     0.94 1.00    12284    16991
#> muAridOpen_IsSedentary              0.52      0.73    -0.92     1.97 1.00    13563    18266
#> muForestedVegetated_cMass           1.47      1.43    -0.84     4.72 1.00     8076    11250
#> muForestedVegetated_IsSedentary     3.66      1.75     1.36     8.03 1.00     7658     8239
#>
#> Draws were sampled using sampling(NUTS). 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).

The first four-chain brms fit of this model (with the default adapt_delta) had 441 divergent transitions. With adapt_delta = 0.999, there are none, and the four chains agree (R-hat < 1.01).

IsSedentary is 1 for sedentary species and 0 for partially migratory or migratory species, so its coefficient compares sedentary with (partially) migratory species. For the Forested_Vegetated vs Others contrast, the coefficient is positive and its 95% CI excludes zero (c2-corrected MCMCglmm: 2.72, 95% CI 1.11, 4.63; brms: 3.66, 1.36, 8.03). For Arid_Open vs Others, it is small and uncertain (0.40, −0.88, 1.66 and 0.52, −0.93, 1.97). The slopes of body mass are similar to those of the previous model, and their 95% CIs include zero.

For the Forested_Vegetated contrast, the brms estimates are larger in magnitude and much more uncertain than those of MCMCglmm: the phylogenetic SD is 6.49 (2.66, 15.31) in brms, compared with a c2-corrected MCMCglmm variance of 16.44 (i.e. an SD of about 4), and the intercept is 0.77 (−4.34, 5.03) in brms and 2.27 (−0.84, 5.70) in MCMCglmm. The reference category contains only 36 species, so the data contain limited information about this contrast once sedentary status is included, and the estimates depend on the priors of each package. The direction of the effect of sedentary status is the same in both packages.

Summary

  • Body mass: There is no clear evidence that heavier or lighter species are more likely to be in Arid_Open or Forested_Vegetated habitats rather than in Others.

  • Sedentary status: Sedentary species are more likely than partially migratory or migratory species to be in Forested_Vegetated habitats rather than in Others, after accounting for body mass and phylogeny; the magnitude of this effect is uncertain and differs between the packages. There is no clear difference for Arid_Open habitats.

  • Phylogenetic patterns: The phylogenetic correlation between the two contrasts is positive but uncertain (0.13, 95% CI −0.31, 0.55 in MCMCglmm; 0.35, −0.44, 0.91 in brms), and both intervals include zero.

Extension: models with multiple data points per species

Tips for simulating datasets

Unfortunately, there are few published datasets with multiple data points per species. We therefore simulate Gaussian, binary, ordinal and nominal data for this section and fit MCMCglmm and brms models to them.

Before moving on to each section, we share some tips to keep in mind when simulating data:

  1. Make the dataset as realistic as possible
    Simulated data should reflect the real-world context. If you are simulating biological data, consider the typical distributions, relationships, and range of values that could appear in your real dataset. You may need to check published studies to assess whether your assumptions are suitable.

  2. Carefully define random effect(s) and fixed effect(s) parameters
    For generating a response variable, ensure that both fixed and random effects are specified clearly. The fixed effects could represent known, deterministic influences (like treatment groups), while random effects account for variability due to unmeasured factors (like species or individuals). Choose the variance components (including the residual variance) so that they represent the data-generating scenario you want to study; there is no general rule about whether the residual variance should be smaller or larger than the random-effect variances. Keep track of whether each parameter is a variance or a standard deviation.

  3. Check true values (we set) and model estimates
    After generating the data and fitting your model, compare the “true” parameter values you set for the simulation with the estimates your model provides. This will help you assess the accuracy of your model and how well it can recover the true underlying parameters. It is important to check whether the model overestimates or underestimates certain effects.

  4. Ensure all created variables are properly aligned
    When simulating data, it is essential to check that all the variables are correctly aligned and correspond to each other as intended. Specifically, pay attention to the following points:

    • Matching species names with those in the phylogenetic tree
      Ensure that the species names used in your data set are consistent with those in the phylogenetic tree (if applicable). If these names don’t match, the integrity of the data could be compromised, leading to inaccurate results (or the model cannot run). For instance, if a species is named “SpeciesA” in the data but referred to as “SpeciesB” in the phylogeny, the data won’t be interpreted correctly.

    • Correspondence between variables
      Also check that other variables, such as individual IDs, environmental factors, gender, or group differences, are correctly matched. For example, verify that data points for each individual are correctly associated with the intended ID, and that variables like age and gender follow expected distributions. How to check: After creating your data frame, use functions, such as head(), summary(), or View() in R (you can find other functions as well) to check for any discrepancies, such as duplicated species names or missing values. This helps ensure that your dataset is internally consistent.

  5. Other small things…

  • Read the documentation
    It may not be worth mentioning again, but always read the documentation for the functions you are using. Understand how they work, what parameters they take, and what outputs they produce.
  • Test with small examples
    If you are using a new function, start by testing it on a small sample of data to see how it behaves before using it in your actual analysis. This helps you avoid larger mistakes when you apply it to the full dataset.
  • Stick to familiar methods
    However, stick to methods and functions you are familiar with, especially when you are starting. As you encounter issues or need more advanced features, gradually learn new functions and techniques. It is a safer approach and will help you build confidence.

Simulate datasets and run models

The simulated datasets are saved in data/simulated/ together with their trees and true parameter values. This function reads one of them:

# Read a canonical simulated dataset and its tree (see generate_simulated_data.R).
read_simulated_data <- function(name, dir = here("data", "simulated")) {
  d <- read.csv(file.path(dir, paste0("sim_", name, ".csv")))
  d$sex <- factor(d$sex, levels = c("F", "M"))
  if (name == "ordinal") d$aggressive_level <- factor(d$aggressive_level, levels = c("Low", "Medium", "High"), ordered = TRUE)
  if (name == "nominal") d$lateralisation <- factor(d$lateralisation, levels = c("Both", "Left", "Right"))
  list(data = d, tree = ape::read.tree(file.path(dir, paste0("sim_", name, "_tree.nwk"))),
       effects = read.csv(file.path(dir, paste0("sim_", name, "_effects.csv"))),
       params = jsonlite::read_json(file.path(dir, paste0("sim_", name, "_params.json")), simplifyVector = TRUE))
}

In this extension, we create datasets for models with different types of response variables: Gaussian, binary, ordinal, and nominal traits. The explanatory variables are the same in all datasets (body mass and sex), except for the nominal example, which is intercept-only. All datasets consist of 100 bird species, each observed five times.

Gaussian

We simulated the habitat range (log-transformed) of each species as the continuous response variable. We assumed habitat range is more strongly influenced by body mass than by sex, and phylogenetic factors are expected to have a greater impact than non-phylogenetic factors.

# Code used to generate the canonical dataset (see data/simulated/generate_simulated_data.R).
# Phylogenetically structured values are drawn with u = L z, where L = t(chol(Sigma)).
# helper functions used by all simulations
suppressMessages({library(ape); library(phytools); library(dplyr); library(jsonlite)})
out_dir <- file.path("data", "simulated")

# one draw from N(mu, Sigma) using the lower-triangular Cholesky factor
rmvnorm_chol <- function(mu, Sigma) {
  L <- t(chol(Sigma))
  stopifnot(max(abs(L %*% t(L) - Sigma)) < 1e-10)  # L L' reproduces Sigma
  as.vector(mu + L %*% rnorm(nrow(Sigma)))
}

make_tree <- function(seed, name) {
  set.seed(seed)
  tree <- pbtree(n = 100, scale = 1)
  f <- file.path(out_dir, paste0("sim_", name, "_tree.nwk"))
  write.tree(tree, f, digits = 17)
  read.tree(f)
}

save_sim <- function(name, data, effects, params) {
  write.csv(data, file.path(out_dir, paste0("sim_", name, ".csv")), row.names = FALSE)
  write.csv(effects, file.path(out_dir, paste0("sim_", name, "_effects.csv")), row.names = FALSE)
  write_json(params, file.path(out_dir, paste0("sim_", name, "_params.json")), auto_unbox = TRUE, digits = NA, pretty = TRUE)
}

n_species <- 100; obs_per_species <- 5; n <- n_species * obs_per_species
mu_mass <- (log(4) + log(2500)) / 2   # mean log body mass

## 1. Gaussian: habitat range --------------------------------------------------------
seed <- 1234
tree <- make_tree(seed, "gaussian")     # set.seed() is called inside, before pbtree()
A <- vcv(tree, corr = TRUE)
mu_species_mass <- rmvnorm_chol(rep(mu_mass, n_species), A)
obs_mass <- as.vector(sapply(mu_species_mass, function(x) rnorm(obs_per_species, mean = x, sd = sqrt(0.1))))
sex <- rbinom(n, 1, 0.5)
var_phylo <- 2; var_non <- 1; var_residual <- 0.2
beta <- c(intercept = 0, mass = 1.2, sexM = 0.1)
u_phylo <- rmvnorm_chol(rep(0, n_species), var_phylo * A)
u_non <- rnorm(n_species, sd = sqrt(var_non))
residual <- rnorm(n, sd = sqrt(var_residual))
species <- rep(tree$tip.label, each = obs_per_species)
y <- beta[["intercept"]] + beta[["mass"]] * obs_mass + beta[["sexM"]] * sex +
  rep(u_phylo, each = obs_per_species) + rep(u_non, each = obs_per_species) + residual
save_sim("gaussian",
         data.frame(individual_id = 1:n, species = species, phylo = species, habitat_range = y,
                    mass = obs_mass, sex = factor(sex, levels = 0:1, labels = c("F", "M"))),
         data.frame(species = tree$tip.label, phylo_effect = u_phylo, non_phylo_effect = u_non),
         list(seed = seed, n_species = n_species, obs_per_species = obs_per_species,
              beta = as.list(beta), var_phylo = var_phylo, var_species = var_non, var_residual = var_residual,
              mass = list(mean = mu_mass, var_among_species = 1, var_within_species = 0.1)))

The simulated dataset, the tree, the simulated species-level effects and the true parameter values are saved in data/simulated/. We use these saved files (rather than re-running the simulation) for fitting and for this tutorial:

sim_gaussian <- read_simulated_data("gaussian")
sim_data1 <- sim_gaussian$data
tree <- sim_gaussian$tree
head(sim_data1)
  individual_id species phylo habitat_range     mass sex
1             1     t84   t84      6.528482 4.552272   M
2             2     t84   t84      6.996712 4.964915   F
3             3     t84   t84      4.915026 4.513204   F
4             4     t84   t84      6.303287 4.400882   F
5             5     t84   t84      4.653596 3.993849   F
6             6     t85   t85      4.351218 5.143677   M

Run models

Ainv <- inverseA(tree, nodes = "ALL", scale = TRUE)$Ainv
prior1_mcmcglmm <- list(R = list(V = 1, nu = 0.002), 
                        G = list(G1 = list(V = 1, nu = 1, alpha.mu = 0, alpha.V = 100),
                                 G2 = list(V = 1, nu = 1, alpha.mu = 0, alpha.V = 100)
                                 )
                        )

system.time(
  mod1_mcmcglmm <- run_mcmcglmm_chains(
    habitat_range ~ 1,
    random = ~ phylo + species,        
    ginverse = list(phylo = Ainv), 
    prior = prior1_mcmcglmm,          
    family = "gaussian",    
    data = sim_data1,              
    seeds = c(20266926, 20266927, 20266928, 20266929), # one seed per chain (four chains)
    nitt = 13000*60,       
    thin = 10*60,         
    burnin = 3000*60      
  ) 
)

system.time(
  mod1_mcmcglmm2 <- run_mcmcglmm_chains(
    habitat_range ~ mass,
    random = ~ phylo + species,        
    ginverse = list(phylo = Ainv), 
    prior = prior1_mcmcglmm,          
    family = "gaussian",    
    data = sim_data1,              
    seeds = c(20267026, 20267027, 20267028, 20267029), # one seed per chain (four chains)
    nitt = 13000*100,       
    thin = 10*100,         
    burnin = 3000*100      
  ) 
)

system.time(
  mod1_mcmcglmm3 <- run_mcmcglmm_chains(
    habitat_range ~ mass + sex,
    random = ~ phylo + species,        
    ginverse = list(phylo = Ainv), 
    prior = prior1_mcmcglmm,          
    family = "gaussian",    
    data = sim_data1,              
    seeds = c(20267126, 20267127, 20267128, 20267129), # one seed per chain (four chains)
    nitt = 13000*250,       
    thin = 10*250,         
    burnin = 3000*250      
  ) 
)

# brms

A <- ape::vcv.phylo(tree, corr = TRUE)

priors1_brms <- default_prior(
  habitat_range ~ 1 + (1 | gr(phylo, cov = A)) + (1 | species), 
  data = sim_data1, 
  data2 = list(A = A),
  family = gaussian()
)

system.time(
  mod1_brms <- brm(habitat_range ~ 1 + (1 | gr(phylo, cov = A)) + (1 | species), 
                   data = sim_data1, 
                   data2 = list(A = A),
                   family = gaussian(),
                   prior = priors1_brms,
                   iter = 8000, 
                   warmup = 6000, 
                   thin = 1,
                   chains = 4,
                   cores = 4,
                   seed = 20267226,
                   control = list(adapt_delta = 0.95)
  )
)

priors1_brms2 <- default_prior(
  habitat_range ~ mass + (1 | gr(phylo, cov = A)) + (1 | species), 
  data = sim_data1, 
  data2 = list(A = A),
  family = gaussian()
)

system.time(
  mod1_brms2 <- brm(habitat_range ~ mass + (1 | gr(phylo, cov = A)) + (1 | species), 
                   data = sim_data1, 
                   data2 = list(A = A),
                   family = gaussian(),
                   prior = priors1_brms2,
                   iter = 8000, 
                   warmup = 6000, 
                   thin = 1,
                   chains = 4,
                   cores = 4,
                   seed = 20267326,
                   control = list(adapt_delta = 0.95)
  )
)

priors1_brms3 <- default_prior(
  habitat_range ~ mass + sex + (1 | gr(phylo, cov = A)) + (1 | species), 
  data = sim_data1, 
  data2 = list(A = A),
  family = gaussian()
)

system.time(
  mod1_brms3 <- brm(habitat_range ~ mass + sex + (1 | gr(phylo, cov = A)) + (1 | species), 
                   data = sim_data1, 
                   data2 = list(A = A),
                   family = gaussian(),
                   prior = priors1_brms3,
                   iter = 8000, 
                   warmup = 6000, 
                   thin = 1,
                   chains = 4,
                   cores = 4,
                   seed = 20267426,
                   control = list(adapt_delta = 0.95)
  )
)

Results

The true parameter values (and, for reference only, the realised variances of the simulated species-level effects) are:

sim_gaussian$params  # seed and true parameter values used to generate the data
$seed
[1] 1234

$n_species
[1] 100

$obs_per_species
[1] 5

$beta
$beta$intercept
[1] 0

$beta$mass
[1] 1.2

$beta$sexM
[1] 0.1


$var_phylo
[1] 2

$var_species
[1] 1

$var_residual
[1] 0.2

$mass
$mass$mean
[1] 4.60517

$mass$var_among_species
[1] 1

$mass$var_within_species
[1] 0.1
# realised variances of the simulated species-level effects (for reference only: these are
# sample variances across phylogenetically correlated species, not the model's estimands)
sapply(sim_gaussian$effects[-1], var)
    phylo_effect non_phylo_effect 
       1.0396430        0.8428301 
summarise_chains(mod1_mcmcglmm)
#>     parameter   mean     sd   Q2.5  Q97.5 rhat ess_bulk ess_tail
#> 1 (Intercept) 5.1280 0.8622 3.4110 6.8280    1     4043     3598
#> 2       phylo 2.8210 1.3160 0.8930 5.8350    1     3207     3571
#> 3     species 0.8734 0.2737 0.4180 1.4750    1     3449     3891
#> 4       units 0.3571 0.0252 0.3124 0.4097    1     3750     3837

summary(mod1_brms)
#>  Family: gaussian 
#>   Links: mu = identity 
#> Formula: habitat_range ~ 1 + (1 | gr(phylo, cov = A)) + (1 | species) 
#>    Data: sim_data1 (Number of observations: 500) 
#>   Draws: 4 chains, each with iter = 8000; warmup = 6000; thin = 1;
#>          total post-warmup draws = 8000
#>
#> Multilevel Hyperparameters:
#> ~phylo (Number of levels: 100) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     1.59      0.37     0.91     2.35 1.00     1192     2437
#>
#> ~species (Number of levels: 100) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     0.93      0.14     0.66     1.22 1.00     1678     3018
#>
#> Regression Coefficients:
#>           Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept     5.13      0.76     3.64     6.64 1.00     6755     5368
#>
#> Further Distributional Parameters:
#>       Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sigma     0.60      0.02     0.56     0.64 1.00     9478     5975
#>
#> Draws were sampled using sampling(NUTS). 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).
autocorr.plot(chains_list(mod1_mcmcglmm, "Sol"))

autocorr.plot(chains_list(mod1_mcmcglmm, "VCV"))

posterior_summary(pool_chains(mod1_mcmcglmm, "Sol"))
            Estimate Est.Error     Q2.5    Q97.5
(Intercept) 5.127816 0.8621979 3.411233 6.828309
posterior_summary(pool_chains(mod1_mcmcglmm, "VCV"))
         Estimate  Est.Error      Q2.5     Q97.5
phylo   2.8209368 1.31573741 0.8930311 5.8351749
species 0.8734431 0.27374844 0.4179891 1.4751615
units   0.3571458 0.02519675 0.3123906 0.4097058

We will now verify whether the model could show estimates close to the true values from the simulated data - there are several points to check during this process. First, check mixing and convergence using the effective sample size, autocorrelation and (for multiple chains) R-hat. Then, compare the true values with the model estimates and check whether the 95% credible intervals contain the true values. The intercept-only model does not include any explanatory variables. The four chains of both packages converged (R-hat < 1.01). Because body mass affects habitat range in the simulation but is not in this model, its effect is absorbed by the other terms. The species’ mean log body masses are phylogenetically structured (variance 1), so the among-species part of the mass effect (\(1.2^2 \times 1 = 1.44\)) adds to the phylogenetic variance, and the within-species part (\(1.2^2 \times 0.1 = 0.144\)) adds to the residual variance. The values expected for this model are therefore a phylogenetic variance of about 3.44, a species variance of 1 and a residual variance of about 0.34, and the intercept is simply the overall mean of habitat range (it is not comparable with the true intercept of 0, which refers to a log body mass of 0). The 95% CIs contain these values: in MCMCglmm, the phylogenetic variance is 2.82 (95% CI 0.89, 5.84), the species variance 0.87 (0.42, 1.48) and the residual variance 0.357 (0.312, 0.410). brms gives similar estimates (SDs 1.59, 0.93 and 0.60, i.e. variances of about 2.7, 0.89 and 0.36 when squared draw by draw).

# MCMCglmm
phylo_signal_mod1_mcmcglmm <- ((pool_chains(mod1_mcmcglmm, "VCV")[, "phylo"]) / (pool_chains(mod1_mcmcglmm, "VCV")[, "phylo"] + pool_chains(mod1_mcmcglmm, "VCV")[, "species"] + pool_chains(mod1_mcmcglmm, "VCV")[, "units"]))

phylo_signal_mod1_mcmcglmm %>% mean()
#> [1] 0.6636644

phylo_signal_mod1_mcmcglmm %>% quantile(probs = c(0.025,0.5,0.975))
#>      2.5%       50%     97.5% 
#> 0.3480827 0.6839835 0.8749849 

# brms
phylo_signal_mod1_brms <- mod1_brms %>% as_tibble() %>% 
        dplyr::select(Sigma_phy = sd_phylo__Intercept, Sigma_non_phy = sd_species__Intercept, Res = sigma) %>% 
        mutate(h2_gaussian = Sigma_phy^2 / (Sigma_phy^2 + Sigma_non_phy^2 + Res^2)) %>% 
        pull(h2_gaussian)

phylo_signal_mod1_brms %>% mean()
#> [1] 0.6488532

phylo_signal_mod1_brms %>% quantile(probs = c(0.025,0.5,0.975))
#>      2.5%       50%     97.5% 
#> 0.3361122 0.6664383 0.8665302 

The proportion of the total variance attributed to phylogeny was estimated as 0.66 (95% CI 0.35, 0.87) using MCMCglmm and 0.65 (0.34, 0.87) using brms, compared with about 0.72 implied by the simulation settings for this model (3.44 / (3.44 + 1 + 0.34)). With repeated observations per species, the variance can be decomposed further: the species random effect (species) represents among-species variation that is not explained by phylogeny, while the observation-level residual (units / sigma) represents within-species (observation-level) variation.

summarise_chains(mod1_mcmcglmm2)
#>     parameter   mean      sd    Q2.5  Q97.5  rhat ess_bulk ess_tail
#> 1 (Intercept) 0.6492 0.87530 -1.1360 2.3410 1.002     3891     3612
#> 2        mass 1.1870 0.06803  1.0530 1.3210 1.001     4279     4065
#> 3       phylo 2.9090 1.22700  0.9974 5.7630 1.001     3124     3754
#> 4     species 0.8229 0.24990  0.4077 1.3900 1.001     3395     3682
#> 5       units 0.2060 0.01492  0.1792 0.2374 1.000     3796     3917

posterior_summary(pool_chains(mod1_mcmcglmm2, "Sol"))
#>              Estimate  Est.Error      Q2.5    Q97.5
#> (Intercept) 0.6491579 0.87533007 -1.136301 2.341200
#> mass        1.1870123 0.06803484  1.052997 1.320591

posterior_summary(pool_chains(mod1_mcmcglmm2, "VCV"))
#>          Estimate  Est.Error      Q2.5     Q97.5
#> phylo   2.9092444 1.22698737 0.9974151 5.7628715
#> species 0.8228845 0.24986257 0.4077050 1.3898469
#> units   0.2060390 0.01491852 0.1792353 0.2373614

summary(mod1_brms2)
#>  Family: gaussian 
#>   Links: mu = identity 
#> Formula: habitat_range ~ mass + (1 | gr(phylo, cov = A)) + (1 | species) 
#>    Data: sim_data1 (Number of observations: 500) 
#>   Draws: 4 chains, each with iter = 8000; warmup = 6000; thin = 1;
#>          total post-warmup draws = 8000
#>
#> Multilevel Hyperparameters:
#> ~phylo (Number of levels: 100) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     1.64      0.35     0.97     2.35 1.00     1549     3168
#>
#> ~species (Number of levels: 100) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     0.90      0.14     0.64     1.18 1.00     1828     3329
#>
#> Regression Coefficients:
#>           Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept     0.69      0.82    -0.94     2.28 1.00     8845     5484
#> mass          1.19      0.07     1.05     1.32 1.00    13300     5353
#>
#> Further Distributional Parameters:
#>       Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sigma     0.45      0.02     0.42     0.49 1.00     7899     5795
#>
#> Draws were sampled using sampling(NUTS). 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).

Next, in the model that includes log body mass, the slope of mass is 1.19 (95% CI 1.05, 1.32) in both packages, close to the true value of 1.2. Now that mass is in the model, the variance components are close to the values used in the simulation: in MCMCglmm, the phylogenetic variance is 2.91 (1.00, 5.76; true value 2), the species variance 0.82 (0.41, 1.39; true value 1) and the residual variance 0.206 (0.179, 0.237; true value 0.2).

summarise_chains(mod1_mcmcglmm3)
#>     parameter   mean      sd     Q2.5  Q97.5  rhat ess_bulk ess_tail
#> 1 (Intercept) 0.5555 0.90330 -1.18800 2.3330 1.000     4023     3972
#> 2        mass 1.1900 0.06854  1.05600 1.3220 1.000     3663     3723
#> 3        sexM 0.1059 0.04512  0.01748 0.1940 1.001     3893     3940
#> 4       phylo 2.9400 1.21400  1.03700 5.6860 1.001     3764     3776
#> 5     species 0.8145 0.24500  0.41730 1.3780 1.000     3887     3847
#> 6       units 0.2034 0.01475  0.17720 0.2355 0.999     4000     4119

posterior_summary(pool_chains(mod1_mcmcglmm3, "Sol"))
#>              Estimate  Est.Error        Q2.5     Q97.5
#> (Intercept) 0.5554969 0.90325136 -1.18753492 2.3334630
#> mass        1.1899918 0.06853612  1.05574099 1.3223772
#> sexM        0.1058576 0.04511897  0.01747735 0.1939708

posterior_summary(pool_chains(mod1_mcmcglmm3, "VCV"))
#>          Estimate  Est.Error      Q2.5     Q97.5
#> phylo   2.9402860 1.21364010 1.0372368 5.6861959
#> species 0.8145237 0.24498792 0.4172766 1.3780247
#> units   0.2033919 0.01475445 0.1771749 0.2355433

summary(mod1_brms3)
#>  Family: gaussian 
#>   Links: mu = identity 
#> Formula: habitat_range ~ mass + sex + (1 | gr(phylo, cov = A)) + (1 | species) 
#>    Data: sim_data1 (Number of observations: 500) 
#>   Draws: 4 chains, each with iter = 8000; warmup = 6000; thin = 1;
#>          total post-warmup draws = 8000
#>
#> Multilevel Hyperparameters:
#> ~phylo (Number of levels: 100) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     1.62      0.36     0.96     2.36 1.00     1513     2701
#>
#> ~species (Number of levels: 100) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     0.91      0.13     0.65     1.18 1.00     2057     3765
#>
#> Regression Coefficients:
#>           Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept     0.63      0.83    -0.98     2.26 1.00     8128     5668
#> mass          1.19      0.07     1.05     1.33 1.00    13092     5394
#> sexM          0.10      0.04     0.02     0.19 1.00    13299     5669
#>
#> Further Distributional Parameters:
#>       Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sigma     0.45      0.02     0.42     0.48 1.00     7062     5793
#>
#> Draws were sampled using sampling(NUTS). 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).

Finally, let’s take a look at the results when both mass and sex are included in the model. We assumed that the expected habitat range of males is 0.1 units larger than that of females, conditional on body mass. The estimates (0.106, 95% CI 0.017, 0.194 in MCMCglmm; 0.10, 0.02, 0.19 in brms) are consistent with this value, and the other estimates hardly change.

Binary

The response variable in this analysis is breeding success, defined as whether a species reproduced or not. The assumptions are as follows: phylogeny has a weak relationship with breeding success, while some non-phylogenetic factors, such as environmental conditions, are likely to play a role. Additionally, heavier species are assumed to have a higher likelihood of breeding success, and females (coded as 0) are expected to succeed in breeding more often than males.

# Code used to generate the canonical dataset (see data/simulated/generate_simulated_data.R).
# Phylogenetically structured values are drawn with u = L z, where L = t(chol(Sigma)).
## 2. Binary: breeding success (logit) ----------------------------------------------
seed <- 456
tree <- make_tree(seed, "binary")
A <- vcv(tree, corr = TRUE)
mu_species_mass <- rmvnorm_chol(rep(mu_mass, n_species), A)
obs_mass <- as.vector(sapply(mu_species_mass, function(x) rnorm(obs_per_species, mean = x, sd = sqrt(0.2))))
sex <- rbinom(n, 1, 0.5)
var_phylo <- 0.4; var_non <- 0.3
beta <- c(intercept = -0.5, mass = 0.4, sexM = -1.5)
u_phylo <- rmvnorm_chol(rep(0, n_species), var_phylo * A)
u_non <- rnorm(n_species, sd = sqrt(var_non))   # one value per species
species <- rep(tree$tip.label, each = obs_per_species)
eta <- beta[["intercept"]] + beta[["mass"]] * obs_mass + beta[["sexM"]] * sex +
  rep(u_phylo, each = obs_per_species) + rep(u_non, each = obs_per_species)
y <- rbinom(n, 1, plogis(eta))
save_sim("binary",
         data.frame(individual_id = 1:n, species = species, phylo = species, breeding = y,
                    mass = obs_mass, sex = factor(sex, levels = 0:1, labels = c("F", "M"))),
         data.frame(species = tree$tip.label, phylo_effect = u_phylo, non_phylo_effect = u_non),
         list(seed = seed, n_species = n_species, obs_per_species = obs_per_species, link = "logit",
              beta = as.list(beta), var_phylo = var_phylo, var_species = var_non,
              mass = list(mean = mu_mass, var_among_species = 1, var_within_species = 0.2)))

The simulated dataset, the tree, the simulated species-level effects and the true parameter values are saved in data/simulated/. We use these saved files (rather than re-running the simulation) for fitting and for this tutorial:

sim_binary <- read_simulated_data("binary")
sim_data2 <- sim_binary$data
tree2 <- sim_binary$tree
head(sim_data2)
  individual_id species phylo breeding     mass sex
1             1     t97   t97        0 4.147570   M
2             2     t97   t97        0 5.194576   F
3             3     t97   t97        1 4.300113   F
4             4     t97   t97        0 3.941728   F
5             5     t97   t97        0 3.933581   M
6             6     t98   t98        1 4.240386   F

Run models

# MCMCglmm

inv_phylo <- inverseA(tree2, nodes = "ALL", scale = TRUE)
prior2_mcmcglmm <- list(R = list(V = 1, fix = 1),
                        G = list(G1 = list(V = 1, nu = 1, alpha.mu = 0, alpha.V = 10),
                                 G2 = list(V = 1, nu = 1, alpha.mu = 0, alpha.V = 10)
                        )
)

system.time(
  mod2_mcmcglmm_logit <- run_mcmcglmm_chains(breeding ~ 1, 
                                  random = ~ phylo + species, 
                                  family = "categorical", 
                                  data = sim_data2, 
                                  prior = prior2_mcmcglmm, 
                                  ginverse = list(phylo = inv_phylo$Ainv),
                                  seeds = c(20267526, 20267527, 20267528, 20267529), # one seed per chain (four chains)
                                  nitt = 13000*10, 
                                  thin = 10*10, 
                                  burnin = 3000*10)                  
)

system.time(
  mod2_mcmcglmm_logit2 <- run_mcmcglmm_chains(breeding ~ mass, 
                                  random = ~ phylo + species, 
                                  family = "categorical", 
                                  data = sim_data2, 
                                  prior = prior2_mcmcglmm, 
                                  ginverse = list(phylo = inv_phylo$Ainv),
                                  seeds = c(20267626, 20267627, 20267628, 20267629), # one seed per chain (four chains)
                                  nitt = 13000*20, 
                                  thin = 10*20, 
                                  burnin = 3000*20)                  
)


system.time(
  mod2_mcmcglmm_logit3 <- run_mcmcglmm_chains(breeding ~ mass + sex, 
                                  random = ~ phylo + species, 
                                  family = "categorical", 
                                  data = sim_data2, 
                                  prior = prior2_mcmcglmm, 
                                  ginverse = list(phylo = inv_phylo$Ainv),
                                  seeds = c(20267726, 20267727, 20267728, 20267729), # one seed per chain (four chains)
                                  nitt = 13000*20, 
                                  thin = 10*20, 
                                  burnin = 3000*20)                  
)

# brms

A <- ape::vcv.phylo(tree2, corr = TRUE)

priors2_brms <- default_prior(
  breeding ~ 1 + (1 | gr(phylo, cov = A)) + (1 | species), 
  data = sim_data2, 
  data2 = list(A = A),
  family = bernoulli(link = "logit")
)

system.time(
  mod2_brms_logit <- brm(breeding ~ 1 + (1 | gr(phylo, cov = A)) + (1 | species), 
                         data = sim_data2, 
                         data2 = list(A = A),
                         family = bernoulli(link = "logit"),
                         prior = priors2_brms,
                         iter = 8000, 
                         warmup = 6000, 
                         thin = 1,
                         chains = 4,
                         cores = 4,
                         seed = 20270426,
                         control = list(adapt_delta = 0.999)
  )
)

priors2_brms2 <- default_prior(
  breeding ~ mass + (1 | gr(phylo, cov = A)) + (1 | species), 
  data = sim_data2, 
  data2 = list(A = A),
  family = bernoulli(link = "logit")
)

system.time(
  mod2_brms_logit2 <- brm(breeding ~ mass + (1 | gr(phylo, cov = A)) + (1 | species), 
                         data = sim_data2, 
                         data2 = list(A = A),
                         family = bernoulli(link = "logit"),
                         prior = priors2_brms2,
                         iter = 8000, 
                         warmup = 6000, 
                         thin = 1,
                         chains = 4,
                         cores = 4,
                         seed = 20269526,
                         control = list(adapt_delta = 0.99)
  )
)

priors2_brms3 <- default_prior(
  breeding ~ mass + sex + (1 | gr(phylo, cov = A)) + (1 | species), 
  data = sim_data2, 
  data2 = list(A = A),
  family = bernoulli(link = "logit")
)

system.time(
  mod2_brms_logit3 <- brm(breeding ~ mass + sex + (1 | gr(phylo, cov = A)) + (1 | species), 
                         data = sim_data2, 
                         data2 = list(A = A),
                         family = bernoulli(link = "logit"),
                         prior = priors2_brms3,
                         iter = 8000, 
                         warmup = 6000, 
                         thin = 1,
                         chains = 4,
                         cores = 4,
                         seed = 20269626,
                         control = list(adapt_delta = 0.99)
  )
)

Results

True values are here:

sim_binary$params  # seed and true parameter values used to generate the data
$seed
[1] 456

$n_species
[1] 100

$obs_per_species
[1] 5

$link
[1] "logit"

$beta
$beta$intercept
[1] -0.5

$beta$mass
[1] 0.4

$beta$sexM
[1] -1.5


$var_phylo
[1] 0.4

$var_species
[1] 0.3

$mass
$mass$mean
[1] 4.60517

$mass$var_among_species
[1] 1

$mass$var_within_species
[1] 0.2
# realised variances of the simulated species-level effects (for reference only: these are
# sample variances across phylogenetically correlated species, not the model's estimands)
sapply(sim_binary$effects[-1], var)
    phylo_effect non_phylo_effect 
       0.2462704        0.2817466 

Before checking the output, we need to correct the results from MCMCglmm using the following equation:

c2 <- (16 * sqrt(3) / (15 * pi))^2
summarise_chains(mod2_mcmcglmm_logit)
#>     parameter   mean     sd       Q2.5 Q97.5  rhat ess_bulk ess_tail
#> 1 (Intercept) 0.1714 0.2883 -0.3895000 0.787 1.000     4079     3637
#> 2       phylo 0.2487 0.3403  0.0003167 1.176 1.000     3398     3558
#> 3     species 0.6603 0.3737  0.0508000 1.525 1.002     2786     3451
#> 4       units 1.0000 0.0000  1.0000000 1.000    NA       NA       NA

# c2-corrected estimates (logit model with the residual variance fixed at 1)
posterior_summary(pool_chains(mod2_mcmcglmm_logit, "Sol") / sqrt(1 + c2))
#>              Estimate Est.Error       Q2.5     Q97.5
#> (Intercept) 0.1477881 0.2485321 -0.3357053 0.6783537

posterior_summary(pool_chains(mod2_mcmcglmm_logit, "VCV") / (1 + c2))
#>          Estimate Est.Error         Q2.5     Q97.5
#> phylo   0.1847717 0.2528659 0.0002353433 0.8736034
#> species 0.4905952 0.2776548 0.0377472310 1.1330256
#> units   0.7430287 0.0000000 0.7430287334 0.7430287

summary(mod2_brms_logit)
#>  Family: bernoulli 
#>   Links: mu = logit 
#> Formula: breeding ~ 1 + (1 | gr(phylo, cov = A)) + (1 | species) 
#>    Data: sim_data2 (Number of observations: 500) 
#>   Draws: 4 chains, each with iter = 8000; warmup = 6000; thin = 1;
#>          total post-warmup draws = 8000
#>
#> Multilevel Hyperparameters:
#> ~phylo (Number of levels: 100) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     0.34      0.24     0.02     0.91 1.00     1402     2764
#>
#> ~species (Number of levels: 100) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     0.66      0.20     0.23     1.04 1.00     1808     1641
#>
#> Regression Coefficients:
#>           Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept     0.15      0.24    -0.30     0.67 1.00     4540     3778
#>
#> Draws were sampled using sampling(NUTS). 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).

This intercept-only model omits mass and sex, so its intercept (c2-corrected MCMCglmm: 0.15, 95% CI −0.34, 0.68; brms: 0.15, −0.30, 0.67) is not comparable with the true intercept (−0.5, for females with a log body mass of 0). The phylogenetic variance is estimated to be small and very uncertain (c2-corrected MCMCglmm: 0.18, 95% CI 0.00, 0.87; true value 0.4), and the species variance is 0.49 (0.04, 1.13; true value 0.3). With 100 species and binary observations, the data contain little information to separate the phylogenetic and the non-phylogenetic species variance.

And phylogenetic signal is…

# phylogenetic variance proportion on the latent logit scale:
# sigma2_phylo / (sigma2_phylo + sigma2_species + pi^2/3), calculated for each draw
## MCMCglmm: c2-corrected variances
VCV <- pool_chains(mod2_mcmcglmm_logit, "VCV") / (1 + c2)
posterior_summary(VCV[, "phylo"] / (VCV[, "phylo"] + VCV[, "species"] + pi^2 / 3))
#>        Estimate  Est.Error         Q2.5     Q97.5
#> var1 0.04400099 0.05408224 6.007242e-05 0.1935107

## brms
draws <- as_draws_df(mod2_brms_logit)
posterior_summary(with(draws, sd_phylo__Intercept^2 / (sd_phylo__Intercept^2 + sd_species__Intercept^2 + pi^2 / 3)))
#>        Estimate  Est.Error         Q2.5     Q97.5
#> [1,] 0.04213701 0.05083829 8.298749e-05 0.1810616

# value implied by the simulation settings (variances 0.4 and 0.3)
0.4 / (0.4 + 0.3 + pi^2 / 3)
#> [1] 0.1002539

The phylogenetic variance proportion is 0.04 (95% CI 0.00, 0.19) in MCMCglmm and 0.04 (0.00, 0.18) in brms. It is lower than the value of 0.10 computed from the simulation parameters, but its 95% CI includes that value; the estimate is very imprecise.

summarise_chains(mod2_mcmcglmm_logit2)
#>     parameter    mean     sd       Q2.5   Q97.5 rhat ess_bulk ess_tail
#> 1 (Intercept) -3.0660 0.8710 -4.8060000 -1.4040    1     4060     3955
#> 2        mass  0.7476 0.1935  0.3741000  1.1430    1     4064     3661
#> 3       phylo  0.1790 0.2686  0.0001957  0.9325    1     3954     3718
#> 4     species  0.6185 0.3575  0.0453200  1.4270    1     3698     3720
#> 5       units  1.0000 0.0000  1.0000000  1.0000   NA       NA       NA

# c2-corrected estimates (logit model with the residual variance fixed at 1)
posterior_summary(pool_chains(mod2_mcmcglmm_logit2, "Sol") / sqrt(1 + c2))
#>               Estimate Est.Error       Q2.5      Q97.5
#> (Intercept) -2.6424724 0.7507854 -4.1428362 -1.2103611
#> mass         0.6444592 0.1668177  0.3224617  0.9856797

posterior_summary(pool_chains(mod2_mcmcglmm_logit2, "VCV") / (1 + c2))
#>          Estimate Est.Error         Q2.5     Q97.5
#> phylo   0.1329656 0.1995897 0.0001453904 0.6928691
#> species 0.4595888 0.2656326 0.0336717109 1.0603366
#> units   0.7430287 0.0000000 0.7430287334 0.7430287

summary(mod2_brms_logit2)
#>  Family: bernoulli 
#>   Links: mu = logit 
#> Formula: breeding ~ mass + (1 | gr(phylo, cov = A)) + (1 | species) 
#>    Data: sim_data2 (Number of observations: 500) 
#>   Draws: 4 chains, each with iter = 8000; warmup = 6000; thin = 1;
#>          total post-warmup draws = 8000
#>
#> Multilevel Hyperparameters:
#> ~phylo (Number of levels: 100) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     0.28      0.21     0.01     0.80 1.00     2098     3751
#>
#> ~species (Number of levels: 100) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     0.62      0.22     0.13     1.01 1.00     1461     1087
#>
#> Regression Coefficients:
#>           Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept    -2.58      0.75    -4.09    -1.18 1.00     8511     6001
#> mass          0.63      0.17     0.32     0.97 1.00     9123     5808
#>
#> Draws were sampled using sampling(NUTS). 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).
summarise_chains(mod2_mcmcglmm_logit3)
#>     parameter    mean     sd       Q2.5   Q97.5  rhat ess_bulk ess_tail
#> 1 (Intercept) -2.2560 0.9425 -4.1550000 -0.4386 1.000     3978     4015
#> 2        mass  0.7611 0.2085  0.3563000  1.1880 0.999     4025     4058
#> 3        sexM -1.6290 0.2615 -2.1590000 -1.1290 1.000     3682     3894
#> 4       phylo  0.1877 0.2938  0.0002213  1.0630 1.000     3780     3745
#> 5     species  0.8405 0.4270  0.1369000  1.8160 1.000     3502     3770
#> 6       units  1.0000 0.0000  1.0000000  1.0000    NA       NA       NA

# c2-corrected estimates (logit model with the residual variance fixed at 1)
posterior_summary(pool_chains(mod2_mcmcglmm_logit3, "Sol") / sqrt(1 + c2))
#>               Estimate Est.Error       Q2.5      Q97.5
#> (Intercept) -1.9448453 0.8123957 -3.5818590 -0.3780742
#> mass         0.6560736 0.1797595  0.3071429  1.0242838
#> sexM        -1.4039239 0.2254451 -1.8612162 -0.9727763

posterior_summary(pool_chains(mod2_mcmcglmm_logit3, "VCV") / (1 + c2))
#>          Estimate Est.Error         Q2.5     Q97.5
#> phylo   0.1394553 0.2183029 0.0001644348 0.7896023
#> species 0.6245436 0.3172362 0.1017075569 1.3495600
#> units   0.7430287 0.0000000 0.7430287334 0.7430287

summary(mod2_brms_logit3)
#>  Family: bernoulli 
#>   Links: mu = logit 
#> Formula: breeding ~ mass + sex + (1 | gr(phylo, cov = A)) + (1 | species) 
#>    Data: sim_data2 (Number of observations: 500) 
#>   Draws: 4 chains, each with iter = 8000; warmup = 6000; thin = 1;
#>          total post-warmup draws = 8000
#>
#> Multilevel Hyperparameters:
#> ~phylo (Number of levels: 100) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     0.29      0.23     0.01     0.85 1.00     1813     3221
#>
#> ~species (Number of levels: 100) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     0.75      0.20     0.34     1.15 1.00     1930     1746
#>
#> Regression Coefficients:
#>           Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept    -1.92      0.78    -3.50    -0.40 1.00     8929     5985
#> mass          0.65      0.17     0.32     1.00 1.00     9010     6230
#> sexM         -1.38      0.22    -1.81    -0.96 1.00    10647     5823
#>
#> Draws were sampled using sampling(NUTS). 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).

In the full model, the sex effect is recovered well (c2-corrected MCMCglmm: −1.40, 95% CI −1.86, −0.97; brms: −1.38, −1.81, −0.96; true value −1.5). The slope of mass is estimated to be steeper than the true value of 0.4 (0.66, 95% CI 0.31, 1.02 and 0.65, 0.32, 1.00), although the 95% CIs include it. Because the intercept refers to a log body mass of 0, far outside the range of the data (mean log mass ≈ 4.3), it compensates for the steeper slope, and its 95% CI (−3.58, −0.38 in MCMCglmm) does not include the true value of −0.5. A single simulated dataset shows how the estimates behave for one realisation only; bias can be assessed only with many simulated datasets.

Ordinal

The response variable is the aggressiveness level, which has three categories: Low, Medium, and High. It is assumed that phylogeny plays a substantial role in aggressiveness. However, non-phylogenetic factors are expected to have only a weak relationship with aggressiveness. The relationship between body mass and aggressiveness is unclear, and it may vary across species. Additionally, males are more likely to exhibit higher levels of aggressiveness compared to females.

# Code used to generate the canonical dataset (see data/simulated/generate_simulated_data.R).
# Phylogenetically structured values are drawn with u = L z, where L = t(chol(Sigma)).
## 3. Ordinal: aggressiveness (probit) ----------------------------------------------
seed <- 789
tree <- make_tree(seed, "ordinal")
A <- vcv(tree, corr = TRUE)
mu_species_mass <- rmvnorm_chol(rep(mu_mass, n_species), A)
obs_mass <- as.vector(sapply(mu_species_mass, function(x) rnorm(obs_per_species, mean = x, sd = sqrt(0.2))))
sex <- rbinom(n, 1, 0.5)
var_phylo <- 1; var_non <- 0.4
beta <- c(intercept = -0.5, mass = 0, sexM = 1)
cutpoints <- c(-0.5, 0.5)
u_phylo <- rmvnorm_chol(rep(0, n_species), var_phylo * A)
u_non <- rnorm(n_species, sd = sqrt(var_non))
species <- rep(tree$tip.label, each = obs_per_species)
eta <- beta[["intercept"]] + beta[["mass"]] * obs_mass + beta[["sexM"]] * sex +
  rep(u_phylo, each = obs_per_species) + rep(u_non, each = obs_per_species)
probs <- cbind(pnorm(cutpoints[1] - eta), pnorm(cutpoints[2] - eta) - pnorm(cutpoints[1] - eta), 1 - pnorm(cutpoints[2] - eta))
y <- apply(probs, 1, function(p) sample(1:3, 1, prob = p))
save_sim("ordinal",
         data.frame(individual_id = 1:n, species = species, phylo = species,
                    aggressive_level = factor(y, levels = 1:3, labels = c("Low", "Medium", "High")),
                    mass = obs_mass, sex = factor(sex, levels = 0:1, labels = c("F", "M"))),
         data.frame(species = tree$tip.label, phylo_effect = u_phylo, non_phylo_effect = u_non),
         list(seed = seed, n_species = n_species, obs_per_species = obs_per_species, link = "probit",
              beta = as.list(beta), cutpoints = cutpoints, var_phylo = var_phylo, var_species = var_non,
              mass = list(mean = mu_mass, var_among_species = 1, var_within_species = 0.2)))

The simulated dataset, the tree, the simulated species-level effects and the true parameter values are saved in data/simulated/. We use these saved files (rather than re-running the simulation) for fitting and for this tutorial:

sim_ordinal <- read_simulated_data("ordinal")
sim_data3 <- sim_ordinal$data
tree3 <- sim_ordinal$tree
head(sim_data3)
  individual_id species phylo aggressive_level     mass sex
1             1     t39   t39             High 5.413890   M
2             2     t39   t39             High 6.717631   M
3             3     t39   t39           Medium 6.222828   F
4             4     t39   t39             High 6.029930   M
5             5     t39   t39           Medium 6.332717   F
6             6     t40   t40           Medium 6.464850   F

Run models

# mcmcglmm
inv_phylo <- inverseA(tree3, nodes = "ALL", scale = TRUE)

prior1 <- list(R = list(V = 1, fix = 1), 
               G = list(G1 = list(V = 1, nu = 1, alpha.mu = 0, alpha.V = 10),
                        G2 = list(V = 1, nu = 1, alpha.mu = 0, alpha.V = 10)
               )
)

system.time(
  mod3_mcmcglmm <- run_mcmcglmm_chains(aggressive_level ~ 1,
                            random = ~ phylo + species,
                            ginverse = list(phylo = inv_phylo$Ainv),
                            family = "threshold",
                            data = sim_data3,
                            prior = prior1,
                            seeds = c(20268126, 20268127, 20268128, 20268129), # one seed per chain (four chains)
                            nitt = 13000*20, 
                            thin = 10*20, 
                            burnin = 3000*20 
  )
)

system.time(
  mod3_mcmcglmm2 <- run_mcmcglmm_chains(aggressive_level ~ mass,
                            random = ~ phylo + species,
                            ginverse = list(phylo = inv_phylo$Ainv),
                            family = "threshold",
                            data = sim_data3,
                            prior = prior1,
                            seeds = c(20268226, 20268227, 20268228, 20268229), # one seed per chain (four chains)
                            nitt = 13000*20, 
                            thin = 10*20, 
                            burnin = 3000*20 
  )
)

system.time(
  mod3_mcmcglmm3 <- run_mcmcglmm_chains(aggressive_level ~ mass + sex,
                            random = ~ phylo + species,
                            ginverse = list(phylo = inv_phylo$Ainv),
                            family = "threshold",
                            data = sim_data3,
                            prior = prior1,
                            seeds = c(20268326, 20268327, 20268328, 20268329), # one seed per chain (four chains)
                            nitt = 13000*20, 
                            thin = 10*20, 
                            burnin = 3000*20 
  )
)

# brms
A <- ape::vcv.phylo(tree3, corr = TRUE)

default_priors <- default_prior(
                    aggressive_level ~ 1 + (1 | gr(phylo, cov = A)) + (1 | species),
                    data = sim_data3,
                    family = cumulative(link = "probit"),
                    data2 = list(A = A)
                    )

system.time(
 mod3_brms <- brm(
            formula = aggressive_level ~ 1 + (1 | gr(phylo, cov = A)) + (1 | species),
            data = sim_data3,
            family = cumulative(link = "probit"),
            data2 = list(A = A),
            prior = default_priors,
            iter = 9000,
            warmup = 7000,
            thin = 1,
            chains = 4,
            cores = 4,
            seed = 20268426,
            control = list(adapt_delta = 0.95)
                )
            )

default_priors2 <- default_prior(
                    aggressive_level ~ mass + (1 | gr(phylo, cov = A)) + (1 | species),
                    data = sim_data3,
                    family = cumulative(link = "probit"),
                    data2 = list(A = A)
                    )

system.time(
 mod3_brms2 <- brm(
            formula = aggressive_level ~ mass + (1 | gr(phylo, cov = A)) + (1 | species),
            data = sim_data3,
            family = cumulative(link = "probit"),
            data2 = list(A = A),
            prior = default_priors2,
            iter = 9000,
            warmup = 7000,
            thin = 1,
            chains = 4,
            cores = 4,
            seed = 20268526,
            control = list(adapt_delta = 0.95)
                )
            )

default_priors3 <- default_prior(
                    aggressive_level ~ mass + sex + (1 | gr(phylo, cov = A)) + (1 | species),
                    data = sim_data3,
                    family = cumulative(link = "probit"),
                    data2 = list(A = A)
                    )

system.time(
 mod3_brms3 <- brm(
            formula = aggressive_level ~ mass + sex + (1 | gr(phylo, cov = A)) + (1 | species),
            data = sim_data3, 
            family = cumulative(link = "probit"),
            data2 = list(A = A),
            prior = default_priors3,
            iter = 9000,
            warmup = 7000,
            thin = 1,
            chains = 4,
            cores = 4,
            seed = 20268626,
            control = list(adapt_delta = 0.95)
                )
            )

Results

The estimates should align with the following values…

sim_ordinal$params  # seed and true parameter values used to generate the data
$seed
[1] 789

$n_species
[1] 100

$obs_per_species
[1] 5

$link
[1] "probit"

$beta
$beta$intercept
[1] -0.5

$beta$mass
[1] 0

$beta$sexM
[1] 1


$cutpoints
[1] -0.5  0.5

$var_phylo
[1] 1

$var_species
[1] 0.4

$mass
$mass$mean
[1] 4.60517

$mass$var_among_species
[1] 1

$mass$var_within_species
[1] 0.2
# realised variances of the simulated species-level effects (for reference only: these are
# sample variances across phylogenetically correlated species, not the model's estimands)
sapply(sim_ordinal$effects[-1], var)
    phylo_effect non_phylo_effect 
       0.6146812        0.3352239 
summarise_chains(mod3_mcmcglmm)
#>                          parameter   mean      sd    Q2.5  Q97.5  rhat ess_bulk ess_tail
#> 1                      (Intercept) 1.1380 0.32390 0.54820 1.8590 1.002     3872     3886
#> 2                            phylo 0.5243 0.39380 0.04160 1.5170 1.001     3783     3676
#> 3                          species 0.3898 0.18390 0.06292 0.7950 1.001     4175     3814
#> 4                            units 1.0000 0.00000 1.00000 1.0000    NA       NA       NA
#> 5 cutpoint.traitaggressive_level.1 0.8021 0.06999 0.67100 0.9435 1.000     3967     4098

posterior_summary(pool_chains(mod3_mcmcglmm, "Sol"))
#>             Estimate Est.Error      Q2.5    Q97.5
#> (Intercept) 1.137618 0.3239239 0.5481601 1.859035

posterior_summary(pool_chains(mod3_mcmcglmm, "VCV"))
#>          Estimate Est.Error       Q2.5     Q97.5
#> phylo   0.5242751 0.3938244 0.04160262 1.5169436
#> species 0.3897617 0.1839424 0.06291609 0.7949855
#> units   1.0000000 0.0000000 1.00000000 1.0000000

posterior_summary(pool_chains(mod3_mcmcglmm, "CP"))
#>                                   Estimate  Est.Error      Q2.5     Q97.5
#> cutpoint.traitaggressive_level.1 0.8020519 0.06998891 0.6709984 0.9435266

summary(mod3_brms)
#>  Family: cumulative 
#>   Links: mu = probit 
#> Formula: aggressive_level ~ 1 + (1 | gr(phylo, cov = A)) + (1 | species) 
#>    Data: sim_data3 (Number of observations: 500) 
#>   Draws: 4 chains, each with iter = 9000; warmup = 7000; thin = 1;
#>          total post-warmup draws = 8000
#>
#> Multilevel Hyperparameters:
#> ~phylo (Number of levels: 100) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     0.66      0.25     0.21     1.20 1.01      728     1714
#>
#> ~species (Number of levels: 100) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     0.61      0.14     0.31     0.88 1.00     1040     1765
#>
#> Regression Coefficients:
#>              Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept[1]    -1.10      0.30    -1.74    -0.54 1.00     4233     4091
#> Intercept[2]    -0.30      0.30    -0.93     0.26 1.00     4327     4208
#>
#> Further Distributional Parameters:
#>      Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> disc     1.00      0.00     1.00     1.00   NA       NA       NA
#>
#> Draws were sampled using sampling(NUTS). 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).

The two packages agree, and the four chains of each model converged. This intercept-only model omits the sex effect, so its intercept and cutpoint are not directly comparable with the true values. The phylogenetic variance (MCMCglmm: 0.52, 95% CI 0.04, 1.52) and the species variance (0.39, 0.06, 0.79) are consistent with the true values of 1 and 0.4. The proportion of the latent-scale variance attributed to phylogeny is…

# MCMCglmm
phylo_signal_mod3_mcmcglmm <- ((pool_chains(mod3_mcmcglmm, "VCV")[, "phylo"]) / (pool_chains(mod3_mcmcglmm, "VCV")[, "phylo"] + pool_chains(mod3_mcmcglmm, "VCV")[, "species"] + 1))

phylo_signal_mod3_mcmcglmm %>% mean()
#> [1] 0.2540334

phylo_signal_mod3_mcmcglmm %>% quantile(probs = c(0.025,0.5,0.975))
#>       2.5%        50%      97.5% 
#> 0.02521306 0.23818158 0.56177752 

# brms
phylo_signal_mod3_brms <- mod3_brms %>% as_tibble() %>% 
        dplyr::select(Sigma_phy = sd_phylo__Intercept, Sigma_non_phy = sd_species__Intercept) %>% 
        mutate(h2_ordinal = Sigma_phy^2 / (Sigma_phy^2 + Sigma_non_phy^2 +1)) %>% 
        pull(h2_ordinal)

phylo_signal_mod3_brms %>% mean()
#> [1] 0.2452954

phylo_signal_mod3_brms %>% quantile(probs = c(0.025,0.5,0.975))
#>       2.5%        50%      97.5% 
#> 0.02596721 0.22827043 0.54072770 

The two packages yielded nearly identical phylogenetic variance proportions (0.25, 95% CI 0.03, 0.56 in MCMCglmm; 0.25, 0.03, 0.54 in brms), compared with 1 / (1 + 0.4 + 1) ≈ 0.42 implied by the simulation settings. The estimates are imprecise, and the 95% CIs include this value.

summarise_chains(mod3_mcmcglmm2)
#>                          parameter    mean      sd     Q2.5  Q97.5 rhat ess_bulk ess_tail
#> 1                      (Intercept) 0.92620 0.58620 -0.20040 2.1270    1     4097     4098
#> 2                             mass 0.04098 0.10000 -0.15650 0.2367    1     4092     4101
#> 3                            phylo 0.55100 0.39430  0.04274 1.5900    1     3883     3847
#> 4                          species 0.39240 0.18290  0.07784 0.7994    1     3823     3766
#> 5                            units 1.00000 0.00000  1.00000 1.0000   NA       NA       NA
#> 6 cutpoint.traitaggressive_level.1 0.80210 0.07054  0.66940 0.9455    1     3769     3772

posterior_summary(pool_chains(mod3_mcmcglmm2, "Sol"))
#>               Estimate  Est.Error       Q2.5     Q97.5
#> (Intercept) 0.92617683 0.58624724 -0.2003995 2.1273916
#> mass        0.04098364 0.09999687 -0.1564994 0.2367123

posterior_summary(pool_chains(mod3_mcmcglmm2, "VCV"))
#>          Estimate Est.Error       Q2.5     Q97.5
#> phylo   0.5510121 0.3942541 0.04273544 1.5903447
#> species 0.3924480 0.1828755 0.07784035 0.7993512
#> units   1.0000000 0.0000000 1.00000000 1.0000000

posterior_summary(pool_chains(mod3_mcmcglmm2, "CP"))
#>                                   Estimate  Est.Error      Q2.5     Q97.5
#> cutpoint.traitaggressive_level.1 0.8021423 0.07054358 0.6694244 0.9455287

summary(mod3_brms2)
#>  Family: cumulative 
#>   Links: mu = probit 
#> Formula: aggressive_level ~ mass + (1 | gr(phylo, cov = A)) + (1 | species) 
#>    Data: sim_data3 (Number of observations: 500) 
#>   Draws: 4 chains, each with iter = 9000; warmup = 7000; thin = 1;
#>          total post-warmup draws = 8000
#>
#> Multilevel Hyperparameters:
#> ~phylo (Number of levels: 100) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     0.72      0.27     0.25     1.29 1.00      775     1608
#>
#> ~species (Number of levels: 100) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     0.60      0.16     0.25     0.89 1.00      894      902
#>
#> Regression Coefficients:
#>              Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept[1]    -0.90      0.58    -2.05     0.22 1.00     6184     4905
#> Intercept[2]    -0.10      0.58    -1.24     1.04 1.00     6136     4796
#> mass             0.04      0.10    -0.15     0.24 1.00     8678     6072
#>
#> Further Distributional Parameters:
#>      Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> disc     1.00      0.00     1.00     1.00   NA       NA       NA
#>
#> Draws were sampled using sampling(NUTS). 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).

summarise_chains(mod3_mcmcglmm3)
#>                          parameter   mean      sd     Q2.5 Q97.5  rhat ess_bulk ess_tail
#> 1                      (Intercept) 0.2333 0.64920 -1.03800 1.513 1.000     3992     3933
#> 2                             mass 0.1091 0.10960 -0.10730 0.322 1.000     3944     3914
#> 3                             sexM 1.1830 0.13620  0.92470 1.458 1.000     3869     3656
#> 4                            phylo 0.7907 0.55550  0.08275 2.148 1.000     3796     3766
#> 5                          species 0.5987 0.25520  0.16760 1.161 1.000     3989     3739
#> 6                            units 1.0000 0.00000  1.00000 1.000    NA       NA       NA
#> 7 cutpoint.traitaggressive_level.1 0.9494 0.08197  0.79510 1.119 1.002     3730     3374

posterior_summary(pool_chains(mod3_mcmcglmm3, "Sol"))
#>              Estimate Est.Error       Q2.5    Q97.5
#> (Intercept) 0.2333177 0.6491678 -1.0381977 1.513157
#> mass        0.1090975 0.1095780 -0.1073054 0.322027
#> sexM        1.1834417 0.1362275  0.9247171 1.458295

posterior_summary(pool_chains(mod3_mcmcglmm3, "VCV"))
#>          Estimate Est.Error       Q2.5    Q97.5
#> phylo   0.7906601 0.5555310 0.08275195 2.147999
#> species 0.5986651 0.2552422 0.16759208 1.161434
#> units   1.0000000 0.0000000 1.00000000 1.000000

posterior_summary(pool_chains(mod3_mcmcglmm3, "CP"))
#>                                   Estimate  Est.Error      Q2.5   Q97.5
#> cutpoint.traitaggressive_level.1 0.9494176 0.08197301 0.7950599 1.11937

summary(mod3_brms3)
#>  Family: cumulative 
#>   Links: mu = probit 
#> Formula: aggressive_level ~ mass + sex + (1 | gr(phylo, cov = A)) + (1 | species) 
#>    Data: sim_data3 (Number of observations: 500) 
#>   Draws: 4 chains, each with iter = 9000; warmup = 7000; thin = 1;
#>          total post-warmup draws = 8000
#>
#> Multilevel Hyperparameters:
#> ~phylo (Number of levels: 100) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     0.83      0.30     0.28     1.47 1.00      935     1689
#>
#> ~species (Number of levels: 100) 
#>               Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(Intercept)     0.76      0.17     0.41     1.08 1.00     1266     2155
#>
#> Regression Coefficients:
#>              Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> Intercept[1]    -0.19      0.64    -1.45     1.04 1.00     8766     6370
#> Intercept[2]     0.75      0.65    -0.51     2.00 1.00     8632     6266
#> mass             0.11      0.11    -0.10     0.33 1.00    10302     6625
#> sexM             1.18      0.13     0.92     1.45 1.00    11734     6150
#>
#> Further Distributional Parameters:
#>      Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> disc     1.00      0.00     1.00     1.00   NA       NA       NA
#>
#> Draws were sampled using sampling(NUTS). 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).

Both MCMCglmm and brms estimated very similar values. In the full model, the sex effect (1.18, 95% CI 0.92, 1.46 in MCMCglmm; 1.18, 0.92, 1.45 in brms) and the mass effect (0.11, −0.11, 0.32; true value 0) are consistent with the values set in the simulation. The same holds for the second cutpoint of MCMCglmm (0.95, 0.80, 1.12; true value 1, the distance between the two thresholds), the phylogenetic variance (0.79, 0.08, 2.15; true value 1) and the species variance (0.60, 0.17, 1.16; true value 0.4).

Nominal

The response variable is lateralisation (handedness/footedness), categorised as Left, Right, or Both, with “Both” serving as the reference category, representing no preference. The majority of individuals show a preference for either the left or the right side, with only a few exhibiting no clear preference.

Here, we create a simple dataset and fit only the intercept-only model. The nominal model is more complex, and with the sample size used above (100 species × 5 observations), variances and correlations were not recovered well when we included more effects. We therefore do not include effects of mass or sex in the linear predictor (the simulated data contain mass and sex columns only for consistency with the other datasets). Both the phylogenetic and the non-phylogenetic correlations between Left and Right are set to zero.

# Code used to generate the canonical dataset (see data/simulated/generate_simulated_data.R).
# Phylogenetically structured values are drawn with u = L z, where L = t(chol(Sigma)).
## 4. Nominal: lateralisation (multinomial logit, reference = Both) -------------------
seed <- 789
tree <- make_tree(seed, "nominal")
A <- vcv(tree, corr = TRUE)
sd_phylo <- c(Left = 1, Right = 1.5); cor_phylo <- 0
sd_species <- c(Left = 1.1, Right = 1); cor_species <- 0
cov_phylo <- diag(sd_phylo^2); cov_phylo[1, 2] <- cov_phylo[2, 1] <- cor_phylo * prod(sd_phylo)
cov_species <- diag(sd_species^2); cov_species[1, 2] <- cov_species[2, 1] <- cor_species * prod(sd_species)
beta0 <- c(Left = 0.4, Right = 0.8)   # intercepts relative to Both; no mass or sex effects
u_phylo <- matrix(rmvnorm_chol(rep(0, 2 * n_species), kronecker(cov_phylo, A)), ncol = 2)
L_species <- t(chol(cov_species))
u_species <- t(L_species %*% matrix(rnorm(2 * n_species), nrow = 2))
eta <- cbind(beta0[["Left"]] + rep(u_phylo[, 1], each = obs_per_species) + rep(u_species[, 1], each = obs_per_species),
             beta0[["Right"]] + rep(u_phylo[, 2], each = obs_per_species) + rep(u_species[, 2], each = obs_per_species))
probs <- exp(cbind(eta, 0)); probs <- probs / rowSums(probs)
y <- apply(probs, 1, function(p) sample(1:3, 1, prob = p))
# mass and sex are generated after the response and do not affect it
mu_species_mass <- rmvnorm_chol(rep(mu_mass, n_species), A)
obs_mass <- as.vector(sapply(mu_species_mass, function(x) rnorm(obs_per_species, mean = x, sd = sqrt(0.2))))
sex <- rbinom(n, 1, 0.5)
species <- rep(tree$tip.label, each = obs_per_species)
lat <- factor(c("Left", "Right", "Both")[y], levels = c("Both", "Left", "Right"))
save_sim("nominal",
         data.frame(individual_id = 1:n, species = species, phylo = species, lateralisation = lat,
                    mass = obs_mass, sex = factor(sex, levels = 0:1, labels = c("F", "M"))),
         data.frame(species = tree$tip.label, phylo_effect_left = u_phylo[, 1], phylo_effect_right = u_phylo[, 2],
                    species_effect_left = u_species[, 1], species_effect_right = u_species[, 2]),
         list(seed = seed, n_species = n_species, obs_per_species = obs_per_species, link = "multinomial logit",
              reference = "Both", intercepts = as.list(beta0), sd_phylo = as.list(sd_phylo), cor_phylo = cor_phylo,
              sd_species = as.list(sd_species), cor_species = cor_species))

The simulated dataset, the tree, the simulated species-level effects and the true parameter values are saved in data/simulated/. We use these saved files (rather than re-running the simulation) for fitting and for this tutorial:

sim_nominal <- read_simulated_data("nominal")
sim_data4 <- sim_nominal$data
tree4 <- sim_nominal$tree
head(sim_data4)
  individual_id species phylo lateralisation     mass sex
1             1     t39   t39          Right 4.835986   M
2             2     t39   t39           Left 4.885954   F
3             3     t39   t39           Left 5.590923   F
4             4     t39   t39           Left 6.680875   F
5             5     t39   t39           Left 4.927750   F
6             6     t40   t40           Left 5.328664   F

Run models

inv_phylo <- inverseA(tree4, nodes = "ALL", scale = TRUE)

prior1 <- list(
  R = list(V = (matrix(1, 2, 2) + diag(2)) / 3, fix = 1), 
  G = list(G1 = list(V = diag(2), nu = 2, 
                     alpha.mu = rep(0, 2), alpha.V =  diag(2)
  ),
  G2 = list(V = diag(2), nu = 2, 
            alpha.mu = rep(0, 2), alpha.V =  diag(2)
  )
  )
)

system.time(
  mod4_mcmcglmm <- run_mcmcglmm_chains(lateralisation ~ trait - 1, 
                            random = ~us(trait):phylo + us(trait):species,
                            rcov = ~us(trait):units,
                            ginverse = list(phylo = inv_phylo$Ainv),
                            family = "categorical",
                            data = sim_data4,
                            prior = prior1,
                            seeds = c(20268726, 20268727, 20268728, 20268729), # one seed per chain (four chains)
                            nitt = 13000*150, 
                            thin = 10*150, 
                            burnin = 3000*150
                            )
)

# brms
A <- ape::vcv.phylo(tree4, corr = TRUE)
priors_brms1 <- default_prior(lateralisation ~ 1 + (1 |a| gr(phylo, cov = A)) + (1 |b| species), 
                            data = sim_data4,
                            data2 = list(A = A),
                            family = categorical(link = "logit")
                            )

system.time(
  mod4_brms <- brm(lateralisation ~ 1 + (1 |a| gr(phylo, cov = A)) + (1 |b| species),
                data = sim_data4,
                data2 = list(A = A),
                family = categorical(link = "logit"),
                prior = priors_brms1,
                iter = 12000,  
                warmup = 8000,
                chains = 4,
                cores = 4,
                seed = 20268826,
                thin = 1,
                control = list(adapt_delta = 0.99)
                )
            )

Results

True values are…

sim_nominal$params  # seed and true parameter values used to generate the data
$seed
[1] 789

$n_species
[1] 100

$obs_per_species
[1] 5

$link
[1] "multinomial logit"

$reference
[1] "Both"

$intercepts
$intercepts$Left
[1] 0.4

$intercepts$Right
[1] 0.8


$sd_phylo
$sd_phylo$Left
[1] 1

$sd_phylo$Right
[1] 1.5


$cor_phylo
[1] 0

$sd_species
$sd_species$Left
[1] 1.1

$sd_species$Right
[1] 1


$cor_species
[1] 0
# realised variances of the simulated species-level effects (for reference only: these are
# sample variances across phylogenetically correlated species, not the model's estimands)
sapply(sim_nominal$effects[-1], var)
   phylo_effect_left   phylo_effect_right  species_effect_left 
            1.018585             1.681170             1.357229 
species_effect_right 
            1.010172 

Before checking the output, we need to correct the results from MCMCglmm using the following equation:

c2 <- (16 * sqrt(3) / (15 * pi))^2
c2a <- c2*(2/3)
summarise_chains(mod4_mcmcglmm)
#>                                                      parameter    mean     sd    Q2.5  Q97.5  rhat ess_bulk ess_tail
#> 1                                     traitlateralisation.Left  0.4472 0.8167 -1.1290 2.1510 1.000     3777     4016
#> 2                                    traitlateralisation.Right  0.5272 0.7786 -1.0960 1.9860 1.000     3313     3626
#> 3      traitlateralisation.Left:traitlateralisation.Left.phylo  3.6760 2.2710  0.8002 9.3580 1.000     3457     3300
#> 4     traitlateralisation.Right:traitlateralisation.Left.phylo -0.8510 1.4450 -3.7150 2.1540 1.001     3099     3787
#> 5     traitlateralisation.Left:traitlateralisation.Right.phylo -0.8510 1.4450 -3.7150 2.1540 1.001     3099     3787
#> 6    traitlateralisation.Right:traitlateralisation.Right.phylo  3.1880 2.3190  0.3155 9.1600 1.000     3394     3871
#> 7    traitlateralisation.Left:traitlateralisation.Left.species  1.8780 1.2620  0.1173 5.0080 1.000     2904     3682
#> 8   traitlateralisation.Right:traitlateralisation.Left.species -0.1705 0.7759 -1.5710 1.6440 1.000     3604     3363
#> 9   traitlateralisation.Left:traitlateralisation.Right.species -0.1705 0.7759 -1.5710 1.6440 1.000     3604     3363
#> 10 traitlateralisation.Right:traitlateralisation.Right.species  2.1910 1.2550  0.2598 5.1280 1.000     3429     3664
#> 11     traitlateralisation.Left:traitlateralisation.Left.units  0.6667 0.0000  0.6667 0.6667    NA       NA       NA
#> 12    traitlateralisation.Right:traitlateralisation.Left.units  0.3333 0.0000  0.3333 0.3333    NA       NA       NA
#> 13    traitlateralisation.Left:traitlateralisation.Right.units  0.3333 0.0000  0.3333 0.3333    NA       NA       NA
#> 14   traitlateralisation.Right:traitlateralisation.Right.units  0.6667 0.0000  0.6667 0.6667    NA       NA       NA

# c2-corrected estimates (K = 3 categories, so c2a = c2 * 2/3)
res_1 <- pool_chains(mod4_mcmcglmm, "Sol") / sqrt(1 + c2a) # fixed effects
res_2 <- pool_chains(mod4_mcmcglmm, "VCV") / (1 + c2a)     # (co)variances
posterior_summary(res_1)
#>                            Estimate Est.Error       Q2.5    Q97.5
#> traitlateralisation.Left  0.4031275 0.7362030 -1.0175797 1.938696
#> traitlateralisation.Right 0.4752124 0.7018534 -0.9880464 1.790223

posterior_summary(res_2)
#>                                                               Estimate Est.Error        Q2.5     Q97.5
#> traitlateralisation.Left:traitlateralisation.Left.phylo      2.9873267 1.8458033  0.65028683 7.6043507
#> traitlateralisation.Right:traitlateralisation.Left.phylo    -0.6915935 1.1746599 -3.01870132 1.7507496
#> traitlateralisation.Left:traitlateralisation.Right.phylo    -0.6915935 1.1746599 -3.01870132 1.7507496
#> traitlateralisation.Right:traitlateralisation.Right.phylo    2.5903444 1.8844413  0.25642202 7.4439733
#> traitlateralisation.Left:traitlateralisation.Left.species    1.5265349 1.0259124  0.09534705 4.0700028
#> traitlateralisation.Right:traitlateralisation.Left.species  -0.1385879 0.6305619 -1.27693543 1.3357064
#> traitlateralisation.Left:traitlateralisation.Right.species  -0.1385879 0.6305619 -1.27693543 1.3357064
#> traitlateralisation.Right:traitlateralisation.Right.species  1.7807773 1.0198215  0.21111802 4.1675122
#> traitlateralisation.Left:traitlateralisation.Left.units      0.5417579 0.0000000  0.54175789 0.5417579
#> traitlateralisation.Right:traitlateralisation.Left.units     0.2708789 0.0000000  0.27087895 0.2708789
#> traitlateralisation.Left:traitlateralisation.Right.units     0.2708789 0.0000000  0.27087895 0.2708789
#> traitlateralisation.Right:traitlateralisation.Right.units    0.5417579 0.0000000  0.54175789 0.5417579

# phylogenetic and species-level (non-phylogenetic) correlations between the two contrasts,
# calculated for each posterior draw (not changed by the c2 correction)
VCV <- pool_chains(mod4_mcmcglmm, "VCV")
corr_phylo <- VCV[, "traitlateralisation.Right:traitlateralisation.Left.phylo"] /
  sqrt(VCV[, "traitlateralisation.Left:traitlateralisation.Left.phylo"] *
         VCV[, "traitlateralisation.Right:traitlateralisation.Right.phylo"])
corr_species <- VCV[, "traitlateralisation.Right:traitlateralisation.Left.species"] /
  sqrt(VCV[, "traitlateralisation.Left:traitlateralisation.Left.species"] *
         VCV[, "traitlateralisation.Right:traitlateralisation.Right.species"])
posterior_summary(corr_phylo)
#>        Estimate Est.Error       Q2.5     Q97.5
#> var1 -0.3255912 0.4080378 -0.9464509 0.5212514

posterior_summary(corr_species)
#>        Estimate Est.Error       Q2.5     Q97.5
#> var1 -0.1703249 0.4116015 -0.9033031 0.5982851

summary(mod4_brms)
#>  Family: categorical 
#>   Links: muLeft = logit; muRight = logit 
#> Formula: lateralisation ~ 1 + (1 | a | gr(phylo, cov = A)) + (1 | b | species) 
#>    Data: sim_data4 (Number of observations: 500) 
#>   Draws: 4 chains, each with iter = 12000; warmup = 8000; thin = 1;
#>          total post-warmup draws = 16000
#>
#> Multilevel Hyperparameters:
#> ~phylo (Number of levels: 100) 
#>                                         Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(muLeft_Intercept)                        1.83      0.54     0.91     3.05 1.00     5668     8744
#> sd(muRight_Intercept)                       1.68      0.59     0.66     2.93 1.00     2778     5490
#> cor(muLeft_Intercept,muRight_Intercept)    -0.22      0.39    -0.91     0.53 1.00     2895     5511
#>
#> ~species (Number of levels: 100) 
#>                                         Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> sd(muLeft_Intercept)                        1.25      0.44     0.32     2.12 1.00     3071     2739
#> sd(muRight_Intercept)                       1.31      0.41     0.44     2.11 1.00     2261     2846
#> cor(muLeft_Intercept,muRight_Intercept)    -0.17      0.39    -0.91     0.55 1.00     1824     3271
#>
#> Regression Coefficients:
#>                   Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS
#> muLeft_Intercept      0.37      0.75    -1.13     1.85 1.00    13887    12464
#> muRight_Intercept     0.43      0.73    -1.08     1.82 1.00    14790    11493
#>
#> Draws were sampled using sampling(NUTS). 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).

The c2-corrected MCMCglmm intercepts (Left 0.40, 95% CI −1.02, 1.94; Right 0.48, −0.99, 1.79) and the brms intercepts (0.37 and 0.43) are consistent with the true values (0.4 and 0.8). The variances are estimated with much less precision: the phylogenetic variance is 2.99 (0.65, 7.60) for Left and 2.59 (0.26, 7.44) for Right (true values 1 and 2.25), and the species variance is 1.53 (0.10, 4.07) and 1.78 (0.21, 4.17) (true values 1.21 and 1). The 95% CIs contain the true values, but the point estimates can be far from them.

Please look at the correlations. In this simulation, the random-effect correlations were estimated much less precisely than the variances, and the variances less precisely than the fixed effects. The 95% credible intervals of the correlations are very wide (phylogenetic: −0.33, 95% CI −0.95, 0.52; species: −0.17, −0.90, 0.60 in MCMCglmm), so close recovery of the true correlation (0) is not expected. This ordering is typical for this kind of data, but it is a property of this particular simulation (sample size and parameter values), not a general rule.

The category-specific phylogenetic variance proportions (on the latent scale of the reference-category multinomial logit, reference: Both) are…

# sigma2_phylo / (sigma2_phylo + sigma2_species + pi^2/3), calculated for each posterior draw
## MCMCglmm: c2-corrected variances
VCV <- pool_chains(mod4_mcmcglmm, "VCV") / (1 + c2a)
h2_left_mcmcglmm <- VCV[, "traitlateralisation.Left:traitlateralisation.Left.phylo"] /
  (VCV[, "traitlateralisation.Left:traitlateralisation.Left.phylo"] +
     VCV[, "traitlateralisation.Left:traitlateralisation.Left.species"] + pi^2 / 3)
h2_right_mcmcglmm <- VCV[, "traitlateralisation.Right:traitlateralisation.Right.phylo"] /
  (VCV[, "traitlateralisation.Right:traitlateralisation.Right.phylo"] +
     VCV[, "traitlateralisation.Right:traitlateralisation.Right.species"] + pi^2 / 3)
posterior_summary(h2_left_mcmcglmm)  # Left vs Both
#>       Estimate Est.Error      Q2.5     Q97.5
#> var1 0.3606652 0.1427104 0.1063523 0.6513249

posterior_summary(h2_right_mcmcglmm) # Right vs Both
#>       Estimate Est.Error       Q2.5     Q97.5
#> var1 0.3137536 0.1584276 0.04067859 0.6472536

## brms
draws <- as_draws_df(mod4_brms)
h2_left_brms <- with(draws, sd_phylo__muLeft_Intercept^2 /
                       (sd_phylo__muLeft_Intercept^2 + sd_species__muLeft_Intercept^2 + pi^2 / 3))
h2_right_brms <- with(draws, sd_phylo__muRight_Intercept^2 /
                        (sd_phylo__muRight_Intercept^2 + sd_species__muRight_Intercept^2 + pi^2 / 3))
posterior_summary(h2_left_brms)
#>       Estimate Est.Error      Q2.5     Q97.5
#> [1,] 0.3945657 0.1442269 0.1265631 0.6819877

posterior_summary(h2_right_brms)
#>       Estimate Est.Error       Q2.5     Q97.5
#> [1,] 0.3515401 0.1619901 0.06580121 0.6717658

# values implied by the simulation settings (variances 1 and 1.5^2 for phylogeny, 1.1^2 and 1 for species)
c(left = 1 / (1 + 1.1^2 + pi^2 / 3), right = 1.5^2 / (1.5^2 + 1 + pi^2 / 3))
#>      left     right 
#> 0.1818225 0.3440436 

The estimates are 0.36 (95% CI 0.11, 0.65) for Left vs Both and 0.31 (0.04, 0.65) for Right vs Both in MCMCglmm, and 0.39 (0.13, 0.68) and 0.35 (0.07, 0.67) in brms. The values implied by the simulation settings are 0.18 (Left vs Both) and 0.34 (Right vs Both). The Right vs Both proportion is recovered well. The Left vs Both proportion is overestimated, although its 95% CIs include the true value; with 100 species, such differences between the estimates and the true values are expected for variance components of nominal models.