This online material is a supplement to our paper “Quantifying macro-evolutionary patterns of trait mean and variance with phylogenetic location-scale models”. You will see how to fit phylogenetic location-scale models (PLSMs) in a Bayesian framework using brms package in R. All the code and related data are available in this repository here.
2 Contents
In this online material, we will first introduce how to fit phylogenetic location-only models (nomral phylgoenetic mixed-effects model) using brms.
Then, we will illustrate how to fit three models described in our paper, subtitled as:
Different Trait Variance in Two Groups (Model 3)
Co-evolution of Mean and Variance (Model 4)
Co-evolution of Two Traits (Model 5)
All results presented in this online material are for illustrative purposes only (more careful thoughts are required to draw biological conclusions).
Additionally, we will show how to obtain phylogenetic heritability and macro-evolvability from the fitted brms object.
Also, importantly, there is a paper, which focuses on location-scale meta-analytic and meta-regression. You can find the sister paper’s tutorial here; also, the preprint of this sister paper is available on DOI:
Nakagawa, S., Mizuno, A., Morrison, K., Ricolfi, L., Williams, C., Drobniak, S. M., … & Yang, Y. (2025). Location‐Scale Meta‐Analysis and Meta‐Regression as a Tool to Capture Large‐Scale Changes in Biological and Methodological Heterogeneity: A Spotlight on Heteroscedasticity. Global Change Biology, 31(5), e70204.
This tutorial has several tips to run location-scale models using brms.
Furthermore, there are two papers, which teaches Bayesian generalised linear modelling.
The first one’s tutorial here; also, the preprint is available on DOI:
Halliwell, B., Holland, B. R., & Yates, L. A. (2025). Multi‐response phylogenetic mixed models: concepts and application. Biological Reviews, 100(3), 1294-1316.
The second one’s tutorial here; also, the preprint is available on EcoEvoRxiv:
Mizuno, A., Drobniak, S.M., Williams, C., Lagisz, M. and Nakagawa, S., 2025. Promoting the use of phylogenetic multinomial generalised mixed-effects model to understand the evolution of discrete traits. EcoEvoRxiv. 2025.
Also, these tutorials teach you how to diagnose Bayesian models and how to interpret the results.
3 Prerequisites
3.1 Loading pacakges
Our tutorial uses R statistical software and existing R packages, which you will first need to download and install.
If the packages are archived in CRAN, use install.packages() to install them. For example, to install the brms , you can execute install.packages("brms") in the console (bottom left pane of R Studio).
Version information of each package is listed at the end of this tutorial.
We also provide some additional helper functions to help visualize the results (mainly extracted from the brms object). If you want to use these custom functions in your own data, you’ll need to change the variable names according to your own data (check out the R code and you’ll see what we mean).
Code
# Function to get variable names dynamicallyget_variables_dynamic <-function(model, pattern) { variables <-get_variables(model) variables[grep(pattern, variables)]}rename_vars <-function(variable) {# b variable <-gsub("b_Intercept", "l_int", variable) variable <-gsub("b_sigma_Intercept", "s_int", variable) variable <-gsub("b_cmass", "l_cmass", variable) variable <-gsub("b_sigma_cmass", "s_cmass", variable) variable <-gsub("b_forest", "l_contrast", variable) variable <-gsub("b_sigma_forest", "s_contrast", variable) # mod3 variable <-gsub("b_cbeakwidth_Intercept", "l_width_int", variable) variable <-gsub("b_sigma_cbeakwidth_Intercept", "s_width_int", variable) variable <-gsub("b_cbeakdepth_Intercept", "l_depth_int", variable) variable <-gsub("b_sigma_cbeakdepth_Intercept", "s_depth_int", variable) variable <-gsub("b_cbeakwidth_cmass", "l_width_cmass", variable) variable <-gsub("b_sigma_cbeakwidth_cmass", "s_width_cmass", variable) variable <-gsub("b_cbeakdepth_cmass", "l_depth_cmass", variable) variable <-gsub("b_sigma_cbeakdepth_cmass", "s_depth_cmass", variable) # sd variable <-gsub("sd_Phylo__Intercept", "l_sd", variable) variable <-gsub("sd_Phylo__sigma_Intercept", "s_sd", variable) # mod3 variable <-gsub("sd_Phylo__cbeakwidth_Intercept", "l_width_sd", variable) variable <-gsub("sd_Phylo__sigma_cbeakwidth_Intercept", "s_width_sd", variable) variable <-gsub("sd_Phylo__cbeakdepth_Intercept", "l_depth_sd", variable) variable <-gsub("sd_Phylo__sigma_cbeakdepth_Intercept", "s_depth_sd", variable) # corr variable <-gsub("cor_Phylo__Intercept__sigma_Intercept", "ls_cor", variable) # mod3 variable <-gsub("cor_Phylo__cbeakwidth_Intercept__sigma_cbeakwidth_Intercept", "ls_width-width_cor", variable) variable <-gsub("cor_Phylo__cbeakwidth_Intercept__cbeakdepth_Intercept", "ll_width-depth_cor", variable) variable <-gsub("cor_Phylo__sigma_cbeakwidth_Intercept__cbeakdepth_Intercept", "sl_width-depth_cor", variable) variable <-gsub("cor_Phylo__cbeakwidth_Intercept__sigma_cbeakdepth_Intercept", "ls_width-depth_cor", variable) variable <-gsub("cor_Phylo__sigma_cbeakwidth_Intercept__sigma_cbeakdepth_Intercept", "ss_width-depth_cor", variable) variable <-gsub("cor_Phylo__cbeakdepth_Intercept__sigma_cbeakdepth_Intercept", "ls_depth-depth_cor", variable) return(variable)}# Function to visualize fixed effectsvisualize_fixed_effects <-function(model) { fixed_effect_vars <-get_variables_dynamic(model, "^b_")if (length(fixed_effect_vars) ==0) {message("No fixed effects found")return(NULL) }tryCatch({ fixed_effects_samples <- model %>%spread_draws(!!!syms(fixed_effect_vars)) %>%pivot_longer(cols =all_of(fixed_effect_vars), names_to =".variable", values_to =".value") %>%mutate(.variable =rename_vars(.variable))ggplot(fixed_effects_samples, aes(x = .value, y = .variable)) +stat_halfeye(normalize ="xy", point_interval ="mean_qi", fill ="lightcyan3", color ="lightcyan4" ) +geom_vline(xintercept =0, linetype ="dashed", color ="#005") +labs(y ="Fixed effects", x ="Posterior values") +theme_classic() }, error =function(e) {message("Error in visualize_fixed_effects: ", e$message)return(NULL) })}# Function to visualize random effectsvisualize_random_effects <-function(model) { random_effect_vars <-get_variables_dynamic(model, "^sd_") random_effect_vars <- random_effect_vars[random_effect_vars !="sd_es_ID__Intercept"]if (length(random_effect_vars) ==0) {message("No random effects found")return(NULL) }tryCatch({ random_effects_samples <- model %>%spread_draws(!!!syms(random_effect_vars)) %>%pivot_longer(cols =all_of(random_effect_vars), names_to =".variable", values_to =".value") %>%mutate(.variable =rename_vars(.variable)) #%>%#mutate(.value = .value) # leave SD as it isggplot(random_effects_samples, aes(x = .value, y = .variable)) +stat_halfeye(normalize ="xy", point_interval ="mean_qi", fill ="olivedrab3", color ="olivedrab4" ) +geom_vline(xintercept =0, linetype ="dashed", color ="#005") +labs(y ="Random effects (SD)", x ="Posterior values") +theme_classic() }, error =function(e) {message("Error in visualize_random_effects: ", e$message)return(NULL) })}# Function to visualize correlationsvisualize_correlations <-function(model) { correlation_vars <-get_variables_dynamic(model, "^cor_")if (length(correlation_vars) ==0) {message("No correlations found")return(NULL) }tryCatch({ correlation_samples <- model %>%spread_draws(!!!syms(correlation_vars)) %>%pivot_longer(cols =all_of(correlation_vars), names_to =".variable", values_to =".value") %>%mutate(.variable =rename_vars(.variable))ggplot(correlation_samples, aes(x = .value, y = .variable)) +stat_halfeye(normalize ="xy", fill ="#FF6347", color ="#8B3626" ) +geom_vline(xintercept =0, linetype ="dashed", color ="#005") +labs(y ="Correlations", x ="Posterior values") +theme_classic() }, error =function(e) {message("Error in visualize_correlations: ", e$message)return(NULL) })}
4 Data loading and preparation
We use the data from Avonet:
J. A. Tobias, C. Sheard, A. L. Pigot, A. J. Devenish, J. Yang, F. Sayol, M. H. Neate-Clegg, N. Alioravainen, T. L. Weeks, R. A. Barber, et al. Avonet: morphological, ecological and geographical data for all birds. Ecology Letters, 25(3):581–597, 2022
As described in the paper, we only use parrot species (N = 354) and we use phylogenetic trees form Jetz et al. (2012):
W. Jetz, G. H. Thomas, J. B. Joy, K. Hartmann, and A. O. Mooers. The global diversity of birds in space and time. Nature, 491(7424):444–448, 2012
Code
# load datadat <-read.csv(here("data", "Psittaciformes", "Psittaciformes_354spp.csv"))#str(dat)# reading nexus file (use ape package)tree_all <-read.nexus(here("data", "Psittaciformes", "Psittaciformes_354spp_100.nex"))tree <- tree_all[[1]]# Turning phylogenetic tree into phylognetic correlation matrixA <-vcv.phylo(tree, corr =TRUE)# center data of interestdat$cbeak_length <-scale(log(dat$Beak.Length_Culmen), center =TRUE, scale =FALSE)dat$cbeak_width <-scale(log(dat$Beak.Width), center =TRUE, scale =FALSE)dat$cbeak_depth <-scale(log(dat$Beak.Depth), center =TRUE, scale =FALSE)dat$cmass <-scale(log(dat$Mass), center =TRUE, scale =FALSE)dat$crange_size <-scale(log(dat$Range.Size), center =TRUE, scale =FALSE)# phynotipic correlation among three beak traits# cor(dat$cbeak_length, dat$cbeak_width)# cor(dat$cbeak_length, dat$cbeak_depth)# cor(dat$cbeak_width, dat$cbeak_depth)# create a variable which says whether they live in Forest or notdat$forest <-ifelse(dat$Habitat =="Forest", 1, 0)
We use the following variables in our models:
cbeak_length: log-transformed and centred beak length (Beak.Length_Culmen)
cbeak_width: log-transformed and centred beak width (Beak.Width)
cbeak_depth: log-transformed and centred beak depth (Beak.Depth)
cmass: log-transformed and centred body mass (Mass)
crange_size: log-transformed and centred range size (Range.Size)
forest: a binary variable indicating whether the species lives in the forest or other habits (created based on Habitat)
5 A baseline: fitting a location-only phylogenetic model
Before adding a scale component it is helpful to see the “classic” phylogenetic generalised linear mixed model (PGLMM) that acts only on the mean.
Here we predict centred log-range size (crange_size) from centred body mass (cmass), while allowing a Brownian-motion random effect (Phylo) on the location part and letting the residual variance stay constant across species.
5.1 Fitting the model
Code
# creating the formulaform0 <-bf(cbeak_length ~1+ cmass + forest + (1|gr(Phylo, cov = A)) # phylogenetic random effect)# setting priorsprior0 <-default_prior(form0, data = dat, data2 =list(A = A),family =gaussian())# running the modelmod0 <-brm(form0, data = dat, data2 =list(A = A),chains =2, cores =2, iter =8000, warmup =5000,prior = prior0,control =list(adapt_delta =0.99, max_treedepth =15))# time taken to run the model# 179.714 seconds (Total)saveRDS(mod0, here("Rdata", "mod0.rds"))
Check the results:
Code
mod0 <-readRDS(here("Rdata", "mod0.rds"))summary(mod0)## Family: gaussian ## Links: mu = identity; sigma = identity ## Formula: cbeak_length ~ 1 + cmass + forest + (1 | gr(Phylo, cov = A)) ## Data: dat (Number of observations: 353) ## Draws: 2 chains, each with iter = 8000; warmup = 5000; thin = 1;## total post-warmup draws = 6000## ## Multilevel Hyperparameters:## ~Phylo (Number of levels: 353) ## Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS## sd(Intercept) 0.26 0.03 0.20 0.32 1.00 605 1151## ## Regression Coefficients:## Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS## Intercept 0.02 0.14 -0.25 0.30 1.00 2006 2917## cmass 0.34 0.02 0.31 0.37 1.00 1634 3672## forest 0.03 0.02 -0.01 0.06 1.00 4329 4685## ## Further Distributional Parameters:## Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS## sigma 0.09 0.01 0.08 0.11 1.00 633 1294## ## 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).
5.2 Visualizing results
Code
# Fixed and random effectsplots_mod0 <-list(visualize_fixed_effects(mod0),visualize_random_effects(mod0))plots_mod0[[1]] / plots_mod0[[2]] +plot_layout(heights =c(2, 1))
Posterior distributions of different types of parameters; the vertical dashed line indicates zero, aiding the interpretation of effect direction with thick lines showing 66% credible intervals and thin whiskers 95% credible intervals (not spanning zero being interpreted as ‘statistical significant’).
5.3 Result summary
Larger-bodied species consistently show higher average beak length than smaller-bodied species (as expected, we were controlling for body size).
Species living in the forest have larger beak length than those living in other habitats although non-significant.
6 Different trait variance in two groups (Model 3)
In this first phylogenetic location-scale model (PLSM), we will fit a location-scale model to investigate whether the mean and variance of beak length (cbeak_length) differs between species living in the forest and those living in other habitats.
6.1 Fitting the model
The brms syntax for Model 3 is as follows:
Code
# creating the formulaform1 <-bf(cbeak_length ~1+ cmass + forest + (1|gr(Phylo, cov = A)), # how to model phylogenetic correlation sigma ~1+ cmass + forest)# setting priorsprior1 <-default_prior(form1, data = dat, data2 =list(A = A),family =gaussian())# running the modelmod1 <-brm(form1, data = dat, data2 =list(A = A),chains =2, cores =2, iter =80000, warmup =20000,prior = prior1,control =list(adapt_delta =0.99, max_treedepth =15))# time taken to run the model# 8673 seconds (Total)# savingsaveRDS(mod1, here("Rdata", "mod1.rds"))
Check the results:
Code
# loading and checkingmod1 <-readRDS(here("Rdata", "mod1.rds"))summary(mod1)## Warning: There were 329 divergent transitions after warmup. Increasing## adapt_delta above 0.99 may help. See## http://mc-stan.org/misc/warnings.html#divergent-transitions-after-warmup## Family: gaussian ## Links: mu = identity; sigma = log ## Formula: cbeak_length ~ 1 + cmass + forest + (1 | gr(Phylo, cov = A)) ## sigma ~ 1 + cmass + forest## Data: dat (Number of observations: 353) ## Draws: 2 chains, each with iter = 80000; warmup = 20000; thin = 1;## total post-warmup draws = 120000## ## Multilevel Hyperparameters:## ~Phylo (Number of levels: 353) ## Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS## sd(Intercept) 0.27 0.03 0.22 0.33 1.00 3122 6870## ## Regression Coefficients:## Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS## Intercept 0.00 0.14 -0.27 0.28 1.00 24037 39809## sigma_Intercept -3.35 0.47 -4.65 -2.71 1.00 1852 1237## cmass 0.36 0.01 0.33 0.39 1.00 28036 51686## forest 0.02 0.01 -0.01 0.05 1.00 21346 54450## sigma_cmass -0.22 0.08 -0.39 -0.07 1.00 7401 15081## sigma_forest 1.00 0.44 0.39 2.23 1.00 2049 1263## ## 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).
6.2 Visualizing results
We can visualize the results (posterior distributions) of parameters (i.e., regression coefficients, variance and standard deviation components, and correlations) using the custom functions we defined earlier.
Posterior distributions of different types of parameters; the vertical dashed line indicates zero, aiding the interpretation of effect direction with thick lines showing 66% credible intervals and thin whiskers 95% credible intervals (not spanning zero being interpreted as ‘statistical significant’).
6.3 Result summary
Trait variability itself is structured: larger species are less variable beak length.
Forest dwellers are more variable, pointing to environment-specific heteroscedasticity athough the difference in the location model was limited.
7 Co-evolution of mean and variance (Model 4)
In this second model, we will fit a phylogenetic location-scale model to investigate whether the mean and variance of beak length (cbeak_length) are evolving together among parrots, after controlling body mass (cmass).
7.1 Fitting the model
The brms syntax for Model 4 is as follows:
Code
# creating the formula# |p| indicates we are going to estimate correlations between these two random effectsform2 <-bf(crange_size ~1+ cmass + (1|p|gr(Phylo, cov = A)), sigma ~1+ cmass + (1|p|gr(Phylo, cov = A)))# setting priorsprior2 <-default_prior(form2, data = dat, data2 =list(A = A),family =gaussian())# running the modelmod2 <-brm(form2, data = dat, data2 =list(A = A),chains =2, cores =2, iter =6000, warmup =3000,prior = prior2,control =list(adapt_delta =0.99, max_treedepth =15))# time taken to run the model# 14265.3 seconds (Total)# savingsaveRDS(mod2, here("Rdata", "mod2.rds"))
Check the results:
Code
# loading and checkingmod2 <-readRDS(here("Rdata", "mod2.rds"))summary(mod2)## Family: gaussian ## Links: mu = identity; sigma = log ## Formula: crange_size ~ 1 + cmass + (1 | p | gr(Phylo, cov = A)) ## sigma ~ 1 + cmass + (1 | p | gr(Phylo, cov = A))## Data: dat (Number of observations: 351) ## Draws: 2 chains, each with iter = 6000; warmup = 3000; thin = 1;## total post-warmup draws = 6000## ## Multilevel Hyperparameters:## ~Phylo (Number of levels: 351) ## Estimate Est.Error l-95% CI u-95% CI Rhat## sd(Intercept) 3.11 0.40 2.36 3.92 1.00## sd(sigma_Intercept) 0.78 0.15 0.51 1.10 1.00## cor(Intercept,sigma_Intercept) -0.94 0.05 -1.00 -0.81 1.00## Bulk_ESS Tail_ESS## sd(Intercept) 1417 2702## sd(sigma_Intercept) 1612 4149## cor(Intercept,sigma_Intercept) 2677 3439## ## Regression Coefficients:## Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS## Intercept 0.17 1.38 -2.59 2.86 1.00 3439 3368## sigma_Intercept 0.63 0.38 -0.12 1.36 1.00 3231 3588## cmass -0.25 0.23 -0.72 0.20 1.00 3378 3552## sigma_cmass 0.18 0.08 0.03 0.33 1.00 3569 4099## ## 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).
Posterior distributions of different types of parameters; the vertical dashed line indicates zero, aiding the interpretation of effect direction with thick lines showing 66% credible intervals and thin whiskers 95% credible intervals (not spanning zero being interpreted as ‘statistical significant’).
7.3 Result summary
The range size’s mean and variance are evolving together, as indicated by the strong negative correlation
That is, larger species have larger range size but smaller variance in range size.
8 Co-evolution of two traits (Model 5)
In this third model, we will fit a phylogenetic bivariate location-scale model to investigate whether the mean and variance of beak length (cbeak_length) and beak width (cbeak_width) are evolving together among parrots, after controlling body mass (cmass).
8.1 Fitting the model
The brms syntax for Model 5 is as follows:
Code
# creating the formula# |p| indicates we are going to estimate correlations between these 4 random effectsform3A <-bf(cbeak_width ~1+ cmass + (1|p|gr(Phylo, cov = A)), sigma ~1+ cmass + (1|p|gr(Phylo, cov = A)))form3B <-bf(cbeak_depth ~1+ cmass + (1|p|gr(Phylo, cov = A)), sigma ~1+ cmass + (1|p|gr(Phylo, cov = A)))form3 <- form3A + form3B +set_rescor(TRUE) # setting priorprior3 <-default_prior(form3, data = dat, data2 =list(A = A),family =gaussian())# fit modelmod3 <-brm(form3, data = dat, data2 =list(A = A),chains =2, cores =2, iter =15000, warmup =5000,prior = prior3,control =list(adapt_delta =0.99, max_treedepth =15))summary(mod3)# time taken to run the model# 439397 seconds (Total)# savingsaveRDS(mod3, here("Rdata", "mod3.rds"))
Posterior distributions of different types of parameters; the vertical dashed line indicates zero, aiding the interpretation of effect direction with thick lines showing 66% credible intervals and thin whiskers 95% credible intervals (not spanning zero being interpreted as ‘statistical significant’).
8.3 Result summary
The beak length and beak width are evolving together, as indicated by the strong positive correlation between the two traits.
Also, variances of the two traits are evolving together, as indicated by the strong positive correlation between the two scale components.
9 Bonus 1: implementing the Figure 2 cells in brms
This table links each conceptual cell in Figure 2 (the main text) to the corresponding distributional brms formulae and the parameters to report. Use the same group-level ID (e.g., a) in random effects you want correlated across parts/traits.
Implementing the Figure 2 cells in brms (pseudo-code)
Figure 2 cell / biological question
Minimal distributional formulae
Within-trait mean–variance (ρ[a(ls)])
Do lineages with larger means show greater or lower dispersion in the *same* trait?
bf(
y ~1+ X + (1| a |gr(species, cov = A)),
sigma ~1+ X + (1| a |gr(species, cov = A))
)
family =gaussian()
bf( y ~1+ X + (1| a |gr(species, cov = A)), sigma ~1+ X + (1| a |gr(species, cov = A)))family =gaussian()
Across-trait mean–mean (ρ[a(l1 l2)])
Do two traits coevolve in their means (integration vs. trade-off)?
Does a shift in the mean of trait 1 relax or constrain variability in trait 2 (and vice versa)?
# same multivariate distributional model; cross-part/trait correlations learned via shared ID `a`brm(bf1 + bf2, data2 =list(A = A), family =gaussian(), ...)
# same multivariate distributional model; cross-part/trait correlations learned via shared ID `a`brm(bf1 + bf2, data2 =list(A = A), family =gaussian(), ...)
10 Bonus 2: obtaining phylogenetic heritability and macro-evolvability (phylogenetic CV)
Below, we show how to calculate phylogenetic heritability and macro-evolvability from the fitted brms object with and without a fixed effect(s). As you will see, phylogenetic heritability is fairly straightforward to obtain but macro-evolvability is a bit more complex as it is not clear what scale to calculate it on (e.g., log-scale or original scale). In fact, the same issue should be relevant to phylogenetic heritability as well but we do not consider this here.
10.1 a model without fixed effects
We first create a phylogenetic location-scale (double-hierarchical) model for cmass without any fixed effects.
The brms syntax for this phylogenetic location-scale model is as follows:
Code
# creating the formula# |p| indicates we are going to estimate correlations between these 2 random effectsform4 <-bf(crange_size ~1+ (1| p |gr(Phylo, cov = A)), sigma ~1+ (1| p |gr(Phylo, cov = A)))# setting priorprior4 <-default_prior(form4, data = dat, data2 =list(A = A),family =gaussian())# fit modelmod4 <-brm(form4, data = dat, data2 =list(A = A),chains =2, cores =2, iter =8000, warmup =1000,prior = prior4,control =list(adapt_delta =0.99, max_treedepth =15))# time taken to run the model# 8523.23 seconds (Total)# savingsaveRDS(mod4, here("Rdata", "mod4.rds"))
Check the results:
Code
# loading and checkingmod4 <-readRDS(here("Rdata", "mod4.rds"))summary(mod4)## Family: gaussian ## Links: mu = identity; sigma = log ## Formula: crange_size ~ 1 + (1 | p | gr(Phylo, cov = A)) ## sigma ~ 1 + (1 | p | gr(Phylo, cov = A))## Data: dat (Number of observations: 351) ## Draws: 2 chains, each with iter = 8000; warmup = 1000; thin = 1;## total post-warmup draws = 14000## ## Multilevel Hyperparameters:## ~Phylo (Number of levels: 351) ## Estimate Est.Error l-95% CI u-95% CI Rhat## sd(Intercept) 3.14 0.43 2.34 4.02 1.00## sd(sigma_Intercept) 0.81 0.15 0.53 1.14 1.00## cor(Intercept,sigma_Intercept) -0.92 0.06 -0.99 -0.77 1.00## Bulk_ESS Tail_ESS## sd(Intercept) 2357 4994## sd(sigma_Intercept) 2820 4818## cor(Intercept,sigma_Intercept) 3935 4577## ## Regression Coefficients:## Estimate Est.Error l-95% CI u-95% CI Rhat Bulk_ESS Tail_ESS## Intercept -0.12 1.40 -2.94 2.62 1.00 3716 6128## sigma_Intercept 0.72 0.39 -0.07 1.50 1.00 3684 5326## ## 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).
10.1.1 Phylogenetic heritability
Using Equations 14, 15 & 17 in the paper, we can calculate the phylogenetic heritability of the trait crange_size (the location part):
We can see the median phylogenetic heritablity is around 39%.
Using Equations 18-21 in the paper, we can calculate the phylogenetic heritability of the trait crange_size (the scale part):
Code
# Equation 21 - sigma2_sigma2_e (variance of the residual variance)sigma2_sigma2_e <- (exp(4*post$sd_Phylo__sigma_Intercept^2) -1) *exp(4*(post$b_sigma_Intercept + post$sd_Phylo__sigma_Intercept^2))# Equation 19 - sigma2_a_star sigma2_as_star <- sigma2_sigma2_e # Equation 20 - sigma2_sigma2_p (variance of the total phenotypic variance)sigma2_sigma2_p = sigma2_p ^2+3* sigma2_sigma2_e# Equation 18H2s <- sigma2_as_star / sigma2_sigma2_p#summary(H2)quantile(H2s, probs =c(0.025, 0.5, 0.975)) %>%pander()
2.5%
50%
97.5%
0.1537
0.3081
0.3323
Here we found a median phylogenetic heritability of around 31%.
10.1.2 Macro-evolvability
Using Equations 6 in the paper, we can calculate the macro-evolvability (CV_Al) of the trait crange_size (log-transformed trait; the location part), but the difficulty is that we need to calculate the phylogenetic variance on the original scale. We can do this by using the delta method. The details of this calculation are provided in this earlier work (see the supplementary material):
O’Dea RE, Noble DW, Nakagawa S. Unifying individual differences in personality, predictability and plasticity: a practical guide. Methods in Ecology and Evolution. 2022 Feb;13(2):278-93.
Code
# exp(b0) is the median of the traitln_b0 <- post$b_Intercept +mean(log(dat$Range.Size), na.rm = T)sigma2_al <- post$sd_Phylo__Intercept^2mean_b0 <-exp(ln_b0 +0.5* (sigma2_al + sigma2_e_bar) )# conversion formula from log-scale to original scalesd_al <-sqrt( (exp(sigma2_al) -1) *exp(2*ln_b0 + sigma2_al) )CV_Al <- sd_al / mean_b0quantile(CV_Al, probs =c(0.025, 0.5, 0.975)) %>%pander()
2.5%
50%
97.5%
3.659e-24
0.06627
58.41
We can see the median macro-evolvability is around 0.07
Code
# alternatively, we can get CV on ln-scaleCV_Al2 <- sigma2_al / ln_b0quantile(CV_Al2, probs =c(0.025, 0.5, 0.975)) %>%pander()
2.5%
50%
97.5%
0.4634
0.8535
1.549
If we get the CV on the log-scale, we can see the median macro-evolvability is around 0.85 (nearly 10 times bigger); this CV for the location is probably more comparable to CV for the scale below (as it turns, our CV for the scale is not estimable on the original scale).
We now estimate the macro-evolvability of the trait crange_size (the scale part) using Equation 24 (we assume that this CV_As is the same as the value for the original scale):
We get the median macro-evolvability of around 3.4. This is macro-evolvability of variance but we can get macro-evolvability of the standard deviation using Equation 25.
Code
# Alternatively, we can get CV_As on the SD scaleCV_As2 <-sqrt(exp(post$sd_Phylo__sigma_Intercept^2) -1)quantile(CV_As2, probs =c(0.025, 0.5, 0.975)) %>%pander()
2.5%
50%
97.5%
0.5746
0.9425
1.627
We get the median macro-evolvability of around 0.94, which is not that different from the macro-evolvability of the mean estimated on the log scale.
This estimation, as expected, matches with that from Equation 24.
We also try to get the macro-evolvability for the scale part on the original scale
Code
# original scale conversion does not seem to make sense so commented outsigma2_as_star_orig <-sqrt( (exp(sigma2_as_star) -1) *exp(2*ln_b0 + sigma2_as_star) )sigma2_e_bar_orig <-sqrt( (exp(sigma2_e_bar) -1) *exp(2*ln_b0 + sigma2_e_bar) )CV_As2_orig <-sqrt(sigma2_as_star_orig) / sigma2_e_bar_origquantile(CV_As2_orig, probs =c(0.025, 0.5, 0.975), na.rm = T) %>%pander()
2.5%
50%
97.5%
11483244
Inf
Inf
We get the median macro-evolvability of a Inf value so this does not seem to work.
10.2 a model with fixed effects
Here we use mod2 as an example to see the phylogenetic heritability and macro-evolvability of the trait crange_size after controlling for body mass cmass.
10.2.1 Phylogenetic heritability
Using Equations 14-17 in the paper, we can calculate the phylogenetic heritability of the trait crange_size (the location part) after controlling for body mass cmass:
Here we found a median phylogenetic heritability of around 29%.
10.2.2 Macro-evolvability
Using Equations 6 in the paper, we can calculate the macro-evolvability (CV_Al) of the trait crange_size (log-transformed trait; the location part), but we first need to back-transform as we did above.
Code
# exp(b0) is the median of the traitln_b0 <- post$b_Intercept +mean(log(dat$Range.Size), na.rm = T)sigma2_al <- post$sd_Phylo__Intercept^2mean_b0 <-exp(ln_b0 +0.5* (sigma2_al + sigma2_e_bar) )# (exp(sigma_ln^2) - 1) * exp(2 * mu_ln + sigma_ln^2)sd_al <-sqrt( (exp(sigma2_al) -1) *exp(2*ln_b0 + sigma2_al) )CV_Al <- sd_al / mean_b0quantile(CV_Al, probs =c(0.025, 0.5, 0.975)) %>%pander()
2.5%
50%
97.5%
2.556e-16
0.3042
66.77
We can see the median macro-evolvability is around 0.30.
Code
# alternatively, we can get CV on ln-scaleCV_Al2 <- sigma2_al / ln_b0quantile(CV_Al2, probs =c(0.025, 0.5, 0.975)) %>%pander()
2.5%
50%
97.5%
0.4576
0.8214
1.449
If we get the CV on the log-scale, we can see the median macro-evolvability is around 0.82, which is higher than what we got on the original scale (like above).
We now estimate the macro-evolvability of the trait crange_size (the scale part) using Equation 24 (we assume that this CV_As is the same as the value for the original scale):
We get the median macro-evolvability of around 3.2. As above, this is macro-evolvability of variance but we can get macro-evolvability of the standard deviation using Equation 25.
Code
# Alternatively, we can get CV_As on the SD scaleCV_As2 <-sqrt(exp(post$sd_Phylo__sigma_Intercept^2) -1)quantile(CV_As2, probs =c(0.025, 0.5, 0.975)) %>%pander()
2.5%
50%
97.5%
0.5414
0.9083
1.539
We get the median macro-evolvability of around 0.91, which is not that different from the macro-evolvability of the mean estimated on the log scale.
This estimation matches the result from Equation 24, unlike the one above. This is because we have now a fixed effect which is taken into account for obtaining the phylogenetic variance on the scale part: sigma2_as_star (we will not try to get the macro-evolvability on the original scale as it does not seem to work).
11 Figure code
Code
# putting all plots together (plots_mod1[[1]] + plots_mod1[[2]]) +(plots_mod2[[1]] / plots_mod2[[2]] /plots_mod2[[3]] ) +(plots_mod3[[1]] / plots_mod3[[2]] /plots_mod3[[3]]) +plot_layout(heights =c(1, 3), widths =c(1, 1)) +# put A, B, Cplot_annotation(tag_levels ='A')
---title: "ONLINE SUPPLEMENT: Quantifying macro-evolutionary patterns of trait mean and variance with phylogenetic location-scale models"author: "**TBA**"date: "`r format(Sys.time(), '%d %B %Y')`"format: html: toc: true toc-location: left toc-depth: 3 toc-title: "**Table of Contents**" output-file: "index.html" theme: simplex embed-resources: true code-fold: show code-tools: true number-sections: true #bibliography: ./bib/ref.bib fontsize: "12" max-width: "10" code-overflow: wrapcrossref: fig-title: Figure # (default is "Figure") tbl-title: Table # (default is "Table") title-delim: — # (default is ":") fig-prefix: Fig. # (default is "Figure") tbl-prefix: Tab. # (default is "Table")editor_options: chunk_output_type: consoleeditor: markdown: wrap: sentence---```{r setup}#| include: falseknitr::opts_chunk$set( collapse = TRUE, message = FALSE, warnings = FALSE, echo = TRUE#, #comment = "#>")```# IntroductionThis online material is a supplement to our paper "Quantifying macro-evolutionary patterns of trait mean and variance with phylogenetic location-scale models".You will see how to fit phylogenetic location-scale models (PLSMs) in a Bayesian framework using `brms` package in `R`. All the code and related data are available in this repository [here](https://github.com/itchyshin/phylo_location_scale).# ContentsIn this online material, we will first introduce how to fit phylogenetic *location-only* models (nomral phylgoenetic mixed-effects model) using `brms`.Then, we will illustrate how to fit three models described in our paper, subtitled as:- Different Trait Variance in Two Groups (Model 3)- Co-evolution of Mean and Variance (Model 4)- Co-evolution of Two Traits (Model 5)All results presented in this online material are for illustrative purposes only (more careful thoughts are required to draw biological conclusions). Additionally, we will show how to obtain phylogenetic heritability and macro-evolvability from the fitted `brms` object.Also, importantly, there is a paper, which focuses on location-scale meta-analytic and meta-regression. You can find the sister paper's tutorial [here](https://itchyshin.github.io/location-scale_meta-analysis/#multilevel-meta-analysis-and-location-scale-meta-regression); also, the preprint of this sister paper is available on [DOI]( https://doi.org/10.1111/gcb.70204):> Nakagawa, S., Mizuno, A., Morrison, K., Ricolfi, L., Williams, C., Drobniak, S. M., ... & Yang, Y. (2025). Location‐Scale Meta‐Analysis and Meta‐Regression as a Tool to Capture Large‐Scale Changes in Biological and Methodological Heterogeneity: A Spotlight on Heteroscedasticity. Global Change Biology, 31(5), e70204. This tutorial has several tips to run location-scale models using `brms`. Furthermore, there are two papers, which teaches Bayesian generalised linear modelling. The first one's tutorial [here](https://github.com/Benjamin-Halliwell/MR-PMM); also, the preprint is available on [DOI]( https://doi.org/10.1111/brv.70001):> Halliwell, B., Holland, B. R., & Yates, L. A. (2025). Multi‐response phylogenetic mixed models: concepts and application. Biological Reviews, 100(3), 1294-1316.The second one's tutorial [here](https://ayumi-495.github.io/multinomial-GLMM-tutorial/); also, the preprint is available on [EcoEvoRxiv](https://ecoevorxiv.org/repository/view/8458/):> Mizuno, A., Drobniak, S.M., Williams, C., Lagisz, M. and Nakagawa, S., 2025. Promoting the use of phylogenetic multinomial generalised mixed-effects model to understand the evolution of discrete traits. EcoEvoRxiv. 2025. Also, these tutorials teach you how to diagnose Bayesian models and how to interpret the results.# Prerequisites## Loading pacakgesOur tutorial uses `R` statistical software and existing `R` packages, which you will first need to download and install.If the packages are archived in CRAN, use `install.packages()` to install them.For example, to install the `brms` , you can execute `install.packages("brms")` in the console (bottom left pane of `R Studio`).Version information of each package is listed at the end of this tutorial.```{r packages}# attempt to load or install necessary packagesif(!require(pacman)) install.packages("pacman")pacman::p_load(tidyverse, ggplot2, brms, bayesplot, tidybayes, here, ape, patchwork, pander, stringr, tibble, dplyr)html_out <- knitr::is_html_output() ```## Custom functionsWe also provide some additional helper functions to help visualize the results (mainly extracted from the `brms` object).If you want to use these custom functions in your own data, you'll need to change the variable names according to your own data (check out the `R` code and you'll see what we mean).```{r}#| code-fold: true# Function to get variable names dynamicallyget_variables_dynamic <-function(model, pattern) { variables <-get_variables(model) variables[grep(pattern, variables)]}rename_vars <-function(variable) {# b variable <-gsub("b_Intercept", "l_int", variable) variable <-gsub("b_sigma_Intercept", "s_int", variable) variable <-gsub("b_cmass", "l_cmass", variable) variable <-gsub("b_sigma_cmass", "s_cmass", variable) variable <-gsub("b_forest", "l_contrast", variable) variable <-gsub("b_sigma_forest", "s_contrast", variable) # mod3 variable <-gsub("b_cbeakwidth_Intercept", "l_width_int", variable) variable <-gsub("b_sigma_cbeakwidth_Intercept", "s_width_int", variable) variable <-gsub("b_cbeakdepth_Intercept", "l_depth_int", variable) variable <-gsub("b_sigma_cbeakdepth_Intercept", "s_depth_int", variable) variable <-gsub("b_cbeakwidth_cmass", "l_width_cmass", variable) variable <-gsub("b_sigma_cbeakwidth_cmass", "s_width_cmass", variable) variable <-gsub("b_cbeakdepth_cmass", "l_depth_cmass", variable) variable <-gsub("b_sigma_cbeakdepth_cmass", "s_depth_cmass", variable) # sd variable <-gsub("sd_Phylo__Intercept", "l_sd", variable) variable <-gsub("sd_Phylo__sigma_Intercept", "s_sd", variable) # mod3 variable <-gsub("sd_Phylo__cbeakwidth_Intercept", "l_width_sd", variable) variable <-gsub("sd_Phylo__sigma_cbeakwidth_Intercept", "s_width_sd", variable) variable <-gsub("sd_Phylo__cbeakdepth_Intercept", "l_depth_sd", variable) variable <-gsub("sd_Phylo__sigma_cbeakdepth_Intercept", "s_depth_sd", variable) # corr variable <-gsub("cor_Phylo__Intercept__sigma_Intercept", "ls_cor", variable) # mod3 variable <-gsub("cor_Phylo__cbeakwidth_Intercept__sigma_cbeakwidth_Intercept", "ls_width-width_cor", variable) variable <-gsub("cor_Phylo__cbeakwidth_Intercept__cbeakdepth_Intercept", "ll_width-depth_cor", variable) variable <-gsub("cor_Phylo__sigma_cbeakwidth_Intercept__cbeakdepth_Intercept", "sl_width-depth_cor", variable) variable <-gsub("cor_Phylo__cbeakwidth_Intercept__sigma_cbeakdepth_Intercept", "ls_width-depth_cor", variable) variable <-gsub("cor_Phylo__sigma_cbeakwidth_Intercept__sigma_cbeakdepth_Intercept", "ss_width-depth_cor", variable) variable <-gsub("cor_Phylo__cbeakdepth_Intercept__sigma_cbeakdepth_Intercept", "ls_depth-depth_cor", variable) return(variable)}# Function to visualize fixed effectsvisualize_fixed_effects <-function(model) { fixed_effect_vars <-get_variables_dynamic(model, "^b_")if (length(fixed_effect_vars) ==0) {message("No fixed effects found")return(NULL) }tryCatch({ fixed_effects_samples <- model %>%spread_draws(!!!syms(fixed_effect_vars)) %>%pivot_longer(cols =all_of(fixed_effect_vars), names_to =".variable", values_to =".value") %>%mutate(.variable =rename_vars(.variable))ggplot(fixed_effects_samples, aes(x = .value, y = .variable)) +stat_halfeye(normalize ="xy", point_interval ="mean_qi", fill ="lightcyan3", color ="lightcyan4" ) +geom_vline(xintercept =0, linetype ="dashed", color ="#005") +labs(y ="Fixed effects", x ="Posterior values") +theme_classic() }, error =function(e) {message("Error in visualize_fixed_effects: ", e$message)return(NULL) })}# Function to visualize random effectsvisualize_random_effects <-function(model) { random_effect_vars <-get_variables_dynamic(model, "^sd_") random_effect_vars <- random_effect_vars[random_effect_vars !="sd_es_ID__Intercept"]if (length(random_effect_vars) ==0) {message("No random effects found")return(NULL) }tryCatch({ random_effects_samples <- model %>%spread_draws(!!!syms(random_effect_vars)) %>%pivot_longer(cols =all_of(random_effect_vars), names_to =".variable", values_to =".value") %>%mutate(.variable =rename_vars(.variable)) #%>%#mutate(.value = .value) # leave SD as it isggplot(random_effects_samples, aes(x = .value, y = .variable)) +stat_halfeye(normalize ="xy", point_interval ="mean_qi", fill ="olivedrab3", color ="olivedrab4" ) +geom_vline(xintercept =0, linetype ="dashed", color ="#005") +labs(y ="Random effects (SD)", x ="Posterior values") +theme_classic() }, error =function(e) {message("Error in visualize_random_effects: ", e$message)return(NULL) })}# Function to visualize correlationsvisualize_correlations <-function(model) { correlation_vars <-get_variables_dynamic(model, "^cor_")if (length(correlation_vars) ==0) {message("No correlations found")return(NULL) }tryCatch({ correlation_samples <- model %>%spread_draws(!!!syms(correlation_vars)) %>%pivot_longer(cols =all_of(correlation_vars), names_to =".variable", values_to =".value") %>%mutate(.variable =rename_vars(.variable))ggplot(correlation_samples, aes(x = .value, y = .variable)) +stat_halfeye(normalize ="xy", fill ="#FF6347", color ="#8B3626" ) +geom_vline(xintercept =0, linetype ="dashed", color ="#005") +labs(y ="Correlations", x ="Posterior values") +theme_classic() }, error =function(e) {message("Error in visualize_correlations: ", e$message)return(NULL) })}```# Data loading and preparationWe use the data from **Avonet**:> J. A. Tobias, C. Sheard, A. L. Pigot, A. J. Devenish, J. Yang, F. Sayol, M. H. Neate-Clegg, N. Alioravainen, T. L. Weeks, R. A. Barber, et al.> Avonet: morphological, ecological and geographical data for all birds.> Ecology Letters, 25(3):581–597, 2022As described in the paper, we only use parrot species (*N* = 354) and we use phylogenetic trees form Jetz et al. (2012):> W. Jetz, G. H. Thomas, J. B. Joy, K. Hartmann, and A. O. Mooers. The global diversity of birds in space and time.Nature, 491(7424):444–448, 2012```{r}# load datadat <-read.csv(here("data", "Psittaciformes", "Psittaciformes_354spp.csv"))#str(dat)# reading nexus file (use ape package)tree_all <-read.nexus(here("data", "Psittaciformes", "Psittaciformes_354spp_100.nex"))tree <- tree_all[[1]]# Turning phylogenetic tree into phylognetic correlation matrixA <-vcv.phylo(tree, corr =TRUE)# center data of interestdat$cbeak_length <-scale(log(dat$Beak.Length_Culmen), center =TRUE, scale =FALSE)dat$cbeak_width <-scale(log(dat$Beak.Width), center =TRUE, scale =FALSE)dat$cbeak_depth <-scale(log(dat$Beak.Depth), center =TRUE, scale =FALSE)dat$cmass <-scale(log(dat$Mass), center =TRUE, scale =FALSE)dat$crange_size <-scale(log(dat$Range.Size), center =TRUE, scale =FALSE)# phynotipic correlation among three beak traits# cor(dat$cbeak_length, dat$cbeak_width)# cor(dat$cbeak_length, dat$cbeak_depth)# cor(dat$cbeak_width, dat$cbeak_depth)# create a variable which says whether they live in Forest or notdat$forest <-ifelse(dat$Habitat =="Forest", 1, 0)```We use the following variables in our models:- `cbeak_length`: log-transformed and centred beak length (`Beak.Length_Culmen`)- `cbeak_width`: log-transformed and centred beak width (`Beak.Width`)- `cbeak_depth`: log-transformed and centred beak depth (`Beak.Depth`)- `cmass`: log-transformed and centred body mass (`Mass`)- `crange_size`: log-transformed and centred range size (`Range.Size`)- `forest`: a binary variable indicating whether the species lives in the forest or other habits (created based on `Habitat`)# A baseline: fitting a *location-only* phylogenetic model Before adding a scale component it is helpful to see the “classic” phylogenetic generalised linear mixed model (PGLMM) that acts *only* on the mean. Here we predict centred log-range size (`crange_size`) from centred body mass (`cmass`), while allowing a Brownian-motion random effect (`Phylo`) on the location part and letting the residual variance stay constant across species.## Fitting the model```{r}#| eval: false# creating the formulaform0 <-bf(cbeak_length ~1+ cmass + forest + (1|gr(Phylo, cov = A)) # phylogenetic random effect)# setting priorsprior0 <-default_prior(form0, data = dat, data2 =list(A = A),family =gaussian())# running the modelmod0 <-brm(form0, data = dat, data2 =list(A = A),chains =2, cores =2, iter =8000, warmup =5000,prior = prior0,control =list(adapt_delta =0.99, max_treedepth =15))# time taken to run the model# 179.714 seconds (Total)saveRDS(mod0, here("Rdata", "mod0.rds"))```Check the results: ```{r}#| warning: falsemod0 <-readRDS(here("Rdata", "mod0.rds"))summary(mod0)```## Visualizing results```{r}#| warning: false# Fixed and random effectsplots_mod0 <-list(visualize_fixed_effects(mod0),visualize_random_effects(mod0))plots_mod0[[1]] / plots_mod0[[2]] +plot_layout(heights =c(2, 1))```**Posterior distributions of different types of parameters**; the vertical dashed line indicates zero, aiding the interpretation of effect direction with thick lines showing 66% credible intervals and thin whiskers 95% credible intervals (not spanning zero being interpreted as 'statistical significant'). ## Result summary- Larger-bodied species consistently show higher average beak length than smaller-bodied species (as expected, we were controlling for body size).- Species living in the forest have larger beak length than those living in other habitats although non-significant.# Different trait variance in two groups (Model 3)In this first phylogenetic location-scale model (PLSM), we will fit a location-scale model to investigate whether the mean and variance of beak length (`cbeak_length`) differs between species living in the forest and those living in other habitats.## Fitting the modelThe `brms` syntax for Model 3 is as follows:```{r}#| eval: false# creating the formulaform1 <-bf(cbeak_length ~1+ cmass + forest + (1|gr(Phylo, cov = A)), # how to model phylogenetic correlation sigma ~1+ cmass + forest)# setting priorsprior1 <-default_prior(form1, data = dat, data2 =list(A = A),family =gaussian())# running the modelmod1 <-brm(form1, data = dat, data2 =list(A = A),chains =2, cores =2, iter =80000, warmup =20000,prior = prior1,control =list(adapt_delta =0.99, max_treedepth =15))# time taken to run the model# 8673 seconds (Total)# savingsaveRDS(mod1, here("Rdata", "mod1.rds"))```Check the results: ```{r}# loading and checkingmod1 <-readRDS(here("Rdata", "mod1.rds"))summary(mod1)```## Visualizing resultsWe can visualize the results (posterior distributions) of parameters (i.e., regression coefficients, variance and standard deviation components, and correlations) using the custom functions we defined earlier.```{r}#| warning: false#| fig-height: 6# getting plotsplots_mod1 <-list(visualize_fixed_effects(mod1),visualize_random_effects(mod1))plots_mod1[[1]] / plots_mod1[[2]] +plot_layout(heights =c(2, 1))```**Posterior distributions of different types of parameters**; the vertical dashed line indicates zero, aiding the interpretation of effect direction with thick lines showing 66% credible intervals and thin whiskers 95% credible intervals (not spanning zero being interpreted as 'statistical significant'). ## Result summary- Trait variability itself is structured: larger species are less variable beak length.- Forest dwellers are more variable, pointing to environment-specific heteroscedasticity athough the difference in the location model was limited.# Co-evolution of mean and variance (Model 4)In this second model, we will fit a phylogenetic location-scale model to investigate whether the mean and variance of beak length (`cbeak_length`) are evolving together among parrots, after controlling body mass (`cmass`).## Fitting the modelThe `brms` syntax for Model 4 is as follows:```{r}#| eval: false# creating the formula# |p| indicates we are going to estimate correlations between these two random effectsform2 <-bf(crange_size ~1+ cmass + (1|p|gr(Phylo, cov = A)), sigma ~1+ cmass + (1|p|gr(Phylo, cov = A)))# setting priorsprior2 <-default_prior(form2, data = dat, data2 =list(A = A),family =gaussian())# running the modelmod2 <-brm(form2, data = dat, data2 =list(A = A),chains =2, cores =2, iter =6000, warmup =3000,prior = prior2,control =list(adapt_delta =0.99, max_treedepth =15))# time taken to run the model# 14265.3 seconds (Total)# savingsaveRDS(mod2, here("Rdata", "mod2.rds"))```Check the results: ```{r}# loading and checkingmod2 <-readRDS(here("Rdata", "mod2.rds"))summary(mod2)```## Visualizing resultsAs above, using the custom functions:```{r}#| warning: false#| fig-height: 8# getting plotsplots_mod2 <-list(visualize_fixed_effects(mod2),visualize_random_effects(mod2),visualize_correlations(mod2))plots_mod2[[1]] / plots_mod2[[2]] /plots_mod2[[3]] +plot_layout(heights =c(2, 1, 1))```**Posterior distributions of different types of parameters**; the vertical dashed line indicates zero, aiding the interpretation of effect direction with thick lines showing 66% credible intervals and thin whiskers 95% credible intervals (not spanning zero being interpreted as 'statistical significant'). ## Result summary- The range size's mean and variance are evolving together, as indicated by the strong negative correlation - That is, larger species have larger range size but smaller variance in range size.# Co-evolution of two traits (Model 5)In this third model, we will fit a phylogenetic bivariate location-scale model to investigate whether the mean and variance of beak length (`cbeak_length`) and beak width (`cbeak_width`) are evolving together among parrots, after controlling body mass (`cmass`).## Fitting the modelThe `brms` syntax for Model 5 is as follows:```{r}#| eval: false# creating the formula# |p| indicates we are going to estimate correlations between these 4 random effectsform3A <-bf(cbeak_width ~1+ cmass + (1|p|gr(Phylo, cov = A)), sigma ~1+ cmass + (1|p|gr(Phylo, cov = A)))form3B <-bf(cbeak_depth ~1+ cmass + (1|p|gr(Phylo, cov = A)), sigma ~1+ cmass + (1|p|gr(Phylo, cov = A)))form3 <- form3A + form3B +set_rescor(TRUE) # setting priorprior3 <-default_prior(form3, data = dat, data2 =list(A = A),family =gaussian())# fit modelmod3 <-brm(form3, data = dat, data2 =list(A = A),chains =2, cores =2, iter =15000, warmup =5000,prior = prior3,control =list(adapt_delta =0.99, max_treedepth =15))summary(mod3)# time taken to run the model# 439397 seconds (Total)# savingsaveRDS(mod3, here("Rdata", "mod3.rds"))```Check the results: ```{r}# loading and checkingmod3 <-readRDS(here("Rdata", "mod3.rds"))summary(mod3)```## Visualizing resultsAgain, using the custom functions:```{r}#| warning: false#| fig-height: 10# getting plotsplots_mod3 <-list(visualize_fixed_effects(mod3),visualize_random_effects(mod3),visualize_correlations(mod3))plots_mod3[[1]] / plots_mod3[[2]] /plots_mod3[[3]]```**Posterior distributions of different types of parameters**; the vertical dashed line indicates zero, aiding the interpretation of effect direction with thick lines showing 66% credible intervals and thin whiskers 95% credible intervals (not spanning zero being interpreted as 'statistical significant'). ## Result summary- The beak length and beak width are evolving together, as indicated by the strong positive correlation between the two traits.- Also, variances of the two traits are evolving together, as indicated by the strong positive correlation between the two scale components.# Bonus 1: implementing the Figure 2 cells in brms This table links each conceptual cell in Figure 2 (the main text) to the corresponding distributional brms formulae and the parameters to report.Use the same group-level ID (e.g., `a`) in random effects you want correlated across parts/traits.```{r}#| label: fig2map-data#| include: falsecode_chunk <-function(x) paste0("```r\n", x, "\n```")tbl_fig2 <-tribble(~cell, ~question, ~pseudo,"Within-trait mean–variance (ρ[a(ls)])","Do lineages with larger means show greater or lower dispersion in the *same* trait?",code_chunk("bf( y ~ 1 + X + (1 | a | gr(species, cov = A)), sigma ~ 1 + X + (1 | a | gr(species, cov = A)))family = gaussian()"),"Across-trait mean–mean (ρ[a(l1 l2)])","Do two traits coevolve in their means (integration vs. trade-off)?",code_chunk("bf1 <- bf( y1 ~ 1 + X1 + (1 | a | gr(species, cov = A)), sigma ~ 1 + X1 + (1 | a | gr(species, cov = A)))bf2 <- bf( y2 ~ 1 + X2 + (1 | a | gr(species, cov = A)), sigma ~ 1 + X2 + (1 | a | gr(species, cov = A)))brm(bf1 + bf2, data2 = list(A = A), family = gaussian(), ...)"),"Across-trait variance–variance (ρ[a(s1 s2)])","Do two traits co-diverge (or contra-diverge) in dispersion?",code_chunk("# same multivariate structure as above; ensure both traits have distributional partsbf1 <- bf( y1 ~ 1 + X1 + (1 | a | gr(species, cov = A)), sigma ~ 1 + X1 + (1 | a | gr(species, cov = A)))bf2 <- bf( y2 ~ 1 + X2 + (1 | a | gr(species, cov = A)), sigma ~ 1 + X2 + (1 | a | gr(species, cov = A)))"),"Across-trait mean–variance (ρ[a(l1 s2)], ρ[a(l2 s1)])","Does a shift in the mean of trait 1 relax or constrain variability in trait 2 (and vice versa)?",code_chunk("# same multivariate distributional model; cross-part/trait correlations learned via shared ID `a`brm(bf1 + bf2, data2 = list(A = A), family = gaussian(), ...)")) %>%mutate(cell = stringr::str_replace_all(cell, "\\[", "\\\\["),question = stringr::str_replace_all(question, "\\*", "\\\\*") )``````{r}#| label: fig2map-render#| echo: false#| message: false#| warning: falseif (html_out &&requireNamespace("gt", quietly =TRUE)) {library(gt)gt(tbl_fig2) |> gt::fmt_markdown(columns =c(cell, question, pseudo)) |> gt::tab_header(title =md("**Implementing the Figure 2 cells in `brms` (pseudo-code)**") ) |> gt::cols_label(cell ="Figure 2 cell / biological question",question ="",pseudo ="Minimal distributional formulae" ) |> gt::cols_width( cell ~ gt::px(300) # wider first column ) |> gt::opt_row_striping() |> gt::tab_options(table.font.size = gt::px(14))} else { knitr::kable( tbl_fig2,format =if (knitr::is_latex_output()) "latex"else"html",booktabs =TRUE,col.names =c("Figure 2 cell / question", "", "Minimal distributional formulae"),escape =FALSE,longtable =TRUE )}```# Bonus 2: obtaining phylogenetic heritability and macro-evolvability (phylogenetic CV)Below, we show how to calculate phylogenetic heritability and macro-evolvability from the fitted `brms` object with and without a fixed effect(s). As you will see, phylogenetic heritability is fairly straightforward to obtain but macro-evolvability is a bit more complex as it is not clear what scale to calculate it on (e.g., log-scale or original scale). In fact, the same issue should be relevant to phylogenetic heritability as well but we do not consider this here. ## a model without fixed effectsWe first create a phylogenetic location-scale (double-hierarchical) model for `cmass` without any fixed effects. The `brms` syntax for this phylogenetic location-scale model is as follows:```{r}#| eval: false# creating the formula# |p| indicates we are going to estimate correlations between these 2 random effectsform4 <-bf(crange_size ~1+ (1| p |gr(Phylo, cov = A)), sigma ~1+ (1| p |gr(Phylo, cov = A)))# setting priorprior4 <-default_prior(form4, data = dat, data2 =list(A = A),family =gaussian())# fit modelmod4 <-brm(form4, data = dat, data2 =list(A = A),chains =2, cores =2, iter =8000, warmup =1000,prior = prior4,control =list(adapt_delta =0.99, max_treedepth =15))# time taken to run the model# 8523.23 seconds (Total)# savingsaveRDS(mod4, here("Rdata", "mod4.rds"))```Check the results: ```{r}# loading and checkingmod4 <-readRDS(here("Rdata", "mod4.rds"))summary(mod4)```### Phylogenetic heritabilityUsing Equations 14, 15 & 17 in the paper, we can calculate the phylogenetic heritability of the trait `crange_size` (the location part):```{r}# sigma2_p (total phenotypic variance)# posterior drawspost <-as_draws_df(mod4)# Equation 17: first calculate sigma2_e_bar (average residual variance)sigma2_e_bar <-exp(2*post$b_sigma_Intercept +2*post$sd_Phylo__sigma_Intercept^2)# Equation 15 (no fixed effects)sigma2_p <- post$sd_Phylo__Intercept^2+ sigma2_e_bar# Equation 14H2l <- post$sd_Phylo__Intercept^2/ sigma2_p#summary(H2)quantile(H2l, probs =c(0.025, 0.5, 0.975)) %>%pander()```We can see the median phylogenetic heritablity is around 39%.Using Equations 18-21 in the paper, we can calculate the phylogenetic heritability of the trait `crange_size` (the scale part):```{r}# Equation 21 - sigma2_sigma2_e (variance of the residual variance)sigma2_sigma2_e <- (exp(4*post$sd_Phylo__sigma_Intercept^2) -1) *exp(4*(post$b_sigma_Intercept + post$sd_Phylo__sigma_Intercept^2))# Equation 19 - sigma2_a_star sigma2_as_star <- sigma2_sigma2_e # Equation 20 - sigma2_sigma2_p (variance of the total phenotypic variance)sigma2_sigma2_p = sigma2_p ^2+3* sigma2_sigma2_e# Equation 18H2s <- sigma2_as_star / sigma2_sigma2_p#summary(H2)quantile(H2s, probs =c(0.025, 0.5, 0.975)) %>%pander()```Here we found a median phylogenetic heritability of around 31%.### Macro-evolvabilityUsing Equations 6 in the paper, we can calculate the macro-evolvability (CV_Al) of the trait `crange_size` (log-transformed trait; the location part), but the difficulty is that we need to calculate the phylogenetic variance on the original scale. We can do this by using the delta method. The details of this calculation are provided in this earlier work (see the supplementary material):> O'Dea RE, Noble DW, Nakagawa S. Unifying individual differences in personality, predictability and plasticity: a practical guide. Methods in Ecology and Evolution. 2022 Feb;13(2):278-93.```{r}# exp(b0) is the median of the traitln_b0 <- post$b_Intercept +mean(log(dat$Range.Size), na.rm = T)sigma2_al <- post$sd_Phylo__Intercept^2mean_b0 <-exp(ln_b0 +0.5* (sigma2_al + sigma2_e_bar) )# conversion formula from log-scale to original scalesd_al <-sqrt( (exp(sigma2_al) -1) *exp(2*ln_b0 + sigma2_al) )CV_Al <- sd_al / mean_b0quantile(CV_Al, probs =c(0.025, 0.5, 0.975)) %>%pander()```We can see the median macro-evolvability is around 0.07```{r}# alternatively, we can get CV on ln-scaleCV_Al2 <- sigma2_al / ln_b0quantile(CV_Al2, probs =c(0.025, 0.5, 0.975)) %>%pander()```If we get the CV on the log-scale, we can see the median macro-evolvability is around 0.85 (nearly 10 times bigger); this CV for the location is probably more comparable to CV for the scale below (as it turns, our CV for the scale is not estimable on the original scale). We now estimate the macro-evolvability of the trait `crange_size` (the scale part) using Equation 24 (we assume that this CV_As is the same as the value for the original scale):```{r}CV_As <-sqrt(exp(4*post$sd_Phylo__sigma_Intercept^2) -1)quantile(CV_As, probs =c(0.025, 0.5, 0.975)) %>%pander()```We get the median macro-evolvability of around 3.4. This is macro-evolvability of variance but we can get macro-evolvability of the standard deviation using Equation 25. ```{r}# Alternatively, we can get CV_As on the SD scaleCV_As2 <-sqrt(exp(post$sd_Phylo__sigma_Intercept^2) -1)quantile(CV_As2, probs =c(0.025, 0.5, 0.975)) %>%pander()```We get the median macro-evolvability of around 0.94, which is not that different from the macro-evolvability of the mean estimated on the log scale. Also, we can use Equation 23:```{r}CV_As2 <-sqrt(sigma2_as_star) / sigma2_e_barquantile(CV_As2, probs =c(0.025, 0.5, 0.975), na.rm = T) %>%pander()```This estimation, as expected, matches with that from Equation 24. We also try to get the macro-evolvability for the scale part on the original scale```{r}# original scale conversion does not seem to make sense so commented outsigma2_as_star_orig <-sqrt( (exp(sigma2_as_star) -1) *exp(2*ln_b0 + sigma2_as_star) )sigma2_e_bar_orig <-sqrt( (exp(sigma2_e_bar) -1) *exp(2*ln_b0 + sigma2_e_bar) )CV_As2_orig <-sqrt(sigma2_as_star_orig) / sigma2_e_bar_origquantile(CV_As2_orig, probs =c(0.025, 0.5, 0.975), na.rm = T) %>%pander()```We get the median macro-evolvability of a `Inf` value so this does not seem to work. ## a model with fixed effectsHere we use `mod2` as an example to see the phylogenetic heritability and macro-evolvability of the trait `crange_size` after controlling for body mass `cmass`.### Phylogenetic heritabilityUsing Equations 14-17 in the paper, we can calculate the phylogenetic heritability of the trait `crange_size` (the location part) after controlling for body mass `cmass`:```{r}# sigma2_p (total phenotypic variance)# posterior drawspost <-as_draws_df(mod2)# Equation 17: first calculate sigma2_e_bar (average residual variance)sigma2_e_bar <-exp(2*post$b_sigma_Intercept +2*post$sd_Phylo__sigma_Intercept^2)# Equation 16 - sigma2_f (variance of the fixed effects)sigma2_fl <-sapply(post$b_cmass, function(b) var(b*dat$cmass))# Equation 15 (no fixed effects)sigma2_p <- post$sd_Phylo__Intercept^2+ sigma2_fl + sigma2_e_bar# Equation 14H2l <- post$sd_Phylo__Intercept^2/ sigma2_p#summary(H2)quantile(H2l, probs =c(0.025, 0.5, 0.975)) %>%pander()```We can see the median phylogenetic heritablity is around 44%.Using Equations 18-21 in the paper, we can calculate the phylogenetic heritability of the trait `crange_size` (the scale part):```{r}# Equation 22 - sigma2_fssigma2_fs <-sapply(post$b_sigma_cmass, function(b) var(b*dat$cmass))# Equation 21 - sigma2_sigma2_e (variance of the residual variance)sigma2_sigma2_e <- (exp(4*(post$sd_Phylo__sigma_Intercept^2+ sigma2_fs)) -1) *exp(4*(post$b_sigma_Intercept + post$sd_Phylo__sigma_Intercept^2+ sigma2_fs))# Equation 19 - sigma2_a_star sigma2_as_star <- sigma2_sigma2_e * (post$sd_Phylo__sigma_Intercept^2/(post$sd_Phylo__sigma_Intercept^2+ sigma2_fs))# Equation 20 - sigma2_sigma2_p (variance of the total phenotypic variance)sigma2_sigma2_p = sigma2_p ^2+3* sigma2_sigma2_e# Equation 18H2s <- sigma2_as_star / sigma2_sigma2_p#summary(H2)quantile(H2s, probs =c(0.025, 0.5, 0.975)) %>%pander()```Here we found a median phylogenetic heritability of around 29%.### Macro-evolvabilityUsing Equations 6 in the paper, we can calculate the macro-evolvability (CV_Al) of the trait `crange_size` (log-transformed trait; the location part), but we first need to back-transform as we did above. ```{r}# exp(b0) is the median of the traitln_b0 <- post$b_Intercept +mean(log(dat$Range.Size), na.rm = T)sigma2_al <- post$sd_Phylo__Intercept^2mean_b0 <-exp(ln_b0 +0.5* (sigma2_al + sigma2_e_bar) )# (exp(sigma_ln^2) - 1) * exp(2 * mu_ln + sigma_ln^2)sd_al <-sqrt( (exp(sigma2_al) -1) *exp(2*ln_b0 + sigma2_al) )CV_Al <- sd_al / mean_b0quantile(CV_Al, probs =c(0.025, 0.5, 0.975)) %>%pander()```We can see the median macro-evolvability is around 0.30.```{r}# alternatively, we can get CV on ln-scaleCV_Al2 <- sigma2_al / ln_b0quantile(CV_Al2, probs =c(0.025, 0.5, 0.975)) %>%pander()```If we get the CV on the log-scale, we can see the median macro-evolvability is around 0.82, which is higher than what we got on the original scale (like above).We now estimate the macro-evolvability of the trait `crange_size` (the scale part) using Equation 24 (we assume that this CV_As is the same as the value for the original scale):```{r}# Using Equation 24CV_As <-sqrt(exp(4*post$sd_Phylo__sigma_Intercept^2) -1)quantile(CV_As, probs =c(0.025, 0.5, 0.975)) %>%pander()```We get the median macro-evolvability of around 3.2. As above, this is macro-evolvability of variance but we can get macro-evolvability of the standard deviation using Equation 25.```{r}# Alternatively, we can get CV_As on the SD scaleCV_As2 <-sqrt(exp(post$sd_Phylo__sigma_Intercept^2) -1)quantile(CV_As2, probs =c(0.025, 0.5, 0.975)) %>%pander()```We get the median macro-evolvability of around 0.91, which is not that different from the macro-evolvability of the mean estimated on the log scale.Additionally, we can use Equation 23:```{r}CV_As <-sqrt(sigma2_as_star) / sigma2_e_barquantile(CV_As, probs =c(0.025, 0.5, 0.975), na.rm = T) %>%pander()```This estimation matches the result from Equation 24, unlike the one above. This is because we have now a fixed effect which is taken into account for obtaining the phylogenetic variance on the scale part: `sigma2_as_star` (we will not try to get the macro-evolvability on the original scale as it does not seem to work).# Figure code```{r}#| code-fold: true#| fig-height: 10#| # putting all plots together (plots_mod1[[1]] + plots_mod1[[2]]) +(plots_mod2[[1]] / plots_mod2[[2]] /plots_mod2[[3]] ) +(plots_mod3[[1]] / plots_mod3[[2]] /plots_mod3[[3]]) +plot_layout(heights =c(1, 3), widths =c(1, 1)) +# put A, B, Cplot_annotation(tag_levels ='A')```# Software and package versions```{r}#| code-fold: truesessionInfo() %>%pander()```