In the present vignette, we want to discuss how to specify phylogenetic multilevel models using **brms**. These models are relevant in evolutionary biology when data of many species are analyzed at the same time. The usual approach would be to model species as a grouping factor in a multilevel model and estimate varying intercepts (and possibly also varying slopes) over species. However, species are not independent as they come from the same phylogenetic tree and we thus have to adjust our model to incorporate this dependency. The examples discussed here are from the book *Modern Phylogenetic Comparative Methods and the application in Evolutionary Biology* (Garamszegi, 2014) – more specifically from the online practice material of Chapter 11. The necessary data can be downloaded from http://www.mpcm-evolution.org/practice/online-practical-material-chapter-11. Some of these models may take a few minutes to fit.

Assume we have measurements of a phenotype, `phen`

(say the body size), and a `cofactor`

variable (say the temperature of the environment). We prepare the data using the following code.

```
# uncomment and set your path to the folder where you stored the data
# my_path <- "insert your path here"
phylo <- ape::read.nexus(paste0(my_path, "phylo.nex"))
data_simple <- read.table(paste0(my_path, "data_simple.txt"), header = TRUE)
head(data_simple)
```

```
phen cofactor phylo
1 107.06595 10.309588 sp_1
2 79.61086 9.690507 sp_2
3 116.38186 15.007825 sp_3
4 143.28705 19.087673 sp_4
5 139.60993 15.658404 sp_5
6 68.50657 6.005236 sp_6
```

The `phylo`

object contains information on the relationship between species. Using this information, we can construct a covariance matrix of species.

```
inv.phylo <- MCMCglmm::inverseA(phylo, nodes = "TIPS", scale = TRUE)
A <- solve(inv.phylo$Ainv)
rownames(A) <- rownames(inv.phylo$Ainv)
```

In contrast to **MCMCglmm**, **brms** requires the covariance matrix and not its inverse. Now, we are ready to fit our first phylogenetic multilevel model:

```
library(brms)
model_simple <- brm(phen ~ cofactor + (1|phylo), data = data_simple,
family = gaussian(), cov_ranef = list(phylo = A),
prior = c(prior(normal(0, 10), "b"),
prior(normal(0, 50), "Intercept"),
prior(student_t(3, 0, 20), "sd"),
prior(student_t(3, 0, 20), "sigma")))
```

With the exception of `cov_ranef = list(phylo = A)`

this is a basic multilevel model with a varying intercept over species (`phylo`

is an indicator of species in this data set). However, by using the `cov_ranef`

argument, we make sure that species are correlated as specified by the covariance matrix `A`

. Setting priors is not required for achieving good convergence for this model, but it improves sampling speed a bit. After fitting, the results can be investigated in detail.

`summary(model_simple)`

```
Family: gaussian(identity)
Formula: phen ~ cofactor + (1 | phylo)
Data: data_simple (Number of observations: 200)
Samples: 4 chains, each with iter = 2000; warmup = 1000; thin = 1;
total post-warmup samples = 4000
ICs: LOO = Not computed; WAIC = Not computed
Group-Level Effects:
~phylo (Number of levels: 200)
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
sd(Intercept) 14.48 2.12 10.48 18.87 732 1.01
Population-Level Effects:
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
Intercept 38.01 6.98 23.76 51.86 1329 1
cofactor 5.18 0.14 4.90 5.46 4000 1
Family Specific Parameters:
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
sigma 9.23 0.72 7.89 10.7 1237 1.01
Samples were drawn using sampling(NUTS). For each parameter, Eff.Sample
is a crude measure of effective sample size, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).
```

`plot(model_simple, N = 2, ask = FALSE)`

`plot(marginal_effects(model_simple), points = TRUE) `

The so called phylogenetic signal (often symbolize by \(\lambda\)) can be computed with the `hypothesis`

method and is roughly \(\lambda = 0.7\) for this example.

```
hyp <- "sd_phylo__Intercept^2 / (sd_phylo__Intercept^2 + sigma^2) = 0"
(hyp <- hypothesis(model_simple, hyp, class = NULL))
```

```
Hypothesis Tests for class :
Estimate Est.Error l-95% CI u-95% CI Evid.Ratio Star
(sd_phylo__Interc... = 0 0.7 0.08 0.52 0.84 NA *
---
'*': The expected value under the hypothesis lies outside the 95% CI.
```

`plot(hyp)`

Note that the phylogenetic signal is just a synonym of the intra-class correlation (ICC) used in the context phylogenetic analysis.

Often, we have multiple observations per species and this allows to fit more complicated phylogenetic models.

```
data_repeat <- read.table(paste0(my_path, "data_repeat.txt"), header = TRUE)
data_repeat$spec_mean_cf <-
with(data_repeat, sapply(split(cofactor, phylo), mean)[phylo])
head(data_repeat)
```

```
phen cofactor species phylo spec_mean_cf
1 107.41919 11.223724 sp_1 sp_1 10.309588
2 109.16403 9.805934 sp_1 sp_1 10.309588
3 91.88672 10.308423 sp_1 sp_1 10.309588
4 121.54341 8.355349 sp_1 sp_1 10.309588
5 105.31638 11.854510 sp_1 sp_1 10.309588
6 64.99859 4.314015 sp_2 sp_2 3.673914
```

The variable `spec_mean_cf`

just contains the mean of the cofactor for each species. The code for the repeated measurement phylogenetic model looks as follows:

```
model_repeat1 <- brm(phen ~ spec_mean_cf + (1|phylo) + (1|species),
data = data_repeat, family = gaussian(),
cov_ranef = list(phylo = A),
prior = c(prior(normal(0,10), "b"),
prior(normal(0,50), "Intercept"),
prior(student_t(3,0,20), "sd"),
prior(student_t(3,0,20), "sigma")),
sample_prior = TRUE, chains = 2, cores = 2,
iter = 4000, warmup = 1000)
```

The variables `phylo`

and `species`

are identical as they are both identifiers of the species. However, we model the phylogenetic covariance only for `phylo`

and thus the `species`

variable accounts for any specific effect that would be independent of the phylogenetic relationship between species (e.g., environmental or niche effects). Again we can obtain model summaries as well as estimates of the phylogenetic signal.

`summary(model_repeat1)`

```
Family: gaussian(identity)
Formula: phen ~ spec_mean_cf + (1 | phylo) + (1 | species)
Data: data_repeat (Number of observations: 1000)
Samples: 2 chains, each with iter = 4000; warmup = 1000; thin = 1;
total post-warmup samples = 6000
ICs: LOO = Not computed; WAIC = Not computed
Group-Level Effects:
~phylo (Number of levels: 200)
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
sd(Intercept) 16.44 1.91 12.89 20.43 1308 1
~species (Number of levels: 200)
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
sd(Intercept) 4.99 0.89 3.22 6.63 558 1
Population-Level Effects:
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
Intercept 36.16 7.85 20.27 51.0 4077 1
spec_mean_cf 5.10 0.10 4.90 5.3 6000 1
Family Specific Parameters:
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
sigma 8.1 0.2 7.72 8.51 6000 1
Samples were drawn using sampling(NUTS). For each parameter, Eff.Sample
is a crude measure of effective sample size, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).
```

```
hyp <- paste(
"sd_phylo__Intercept^2 /",
"(sd_phylo__Intercept^2 + sd_species__Intercept^2 + sigma^2) = 0"
)
(hyp <- hypothesis(model_repeat1, hyp, class = NULL))
```

```
Hypothesis Tests for class :
Estimate Est.Error l-95% CI u-95% CI Evid.Ratio Star
(sd_phylo__Interc... = 0 0.74 0.06 0.62 0.84 0 *
---
'*': The expected value under the hypothesis lies outside the 95% CI.
```

`plot(hyp)`

So far, we have completely ignored the variability of the cofactor within species. To incorporate this into the model, we define

`data_repeat$within_spec_cf <- data_repeat$cofactor - data_repeat$spec_mean_cf`

and then fit it again using `within_spec_cf`

as an additional predictor.

```
model_repeat2 <- update(model_repeat1, formula = ~ . + within_spec_cf,
newdata = data_repeat, chains = 2, cores = 2,
iter = 4000, warmup = 1000)
```

The results are almost unchanged, with apparently no relationship between the phenotype and the within species variance of `cofactor`

.

`summary(model_repeat2)`

```
Family: gaussian(identity)
Formula: phen ~ spec_mean_cf + (1 | phylo) + (1 | species) + within_spec_cf
Data: data_repeat (Number of observations: 1000)
Samples: 2 chains, each with iter = 4000; warmup = 1000; thin = 1;
total post-warmup samples = 6000
ICs: LOO = Not computed; WAIC = Not computed
Group-Level Effects:
~phylo (Number of levels: 200)
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
sd(Intercept) 16.48 1.93 12.95 20.57 1300 1
~species (Number of levels: 200)
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
sd(Intercept) 4.94 0.86 3.18 6.55 987 1
Population-Level Effects:
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
Intercept 36.23 7.77 21.01 51.22 3178 1
spec_mean_cf 5.09 0.10 4.89 5.30 6000 1
within_spec_cf -0.06 0.18 -0.42 0.30 6000 1
Family Specific Parameters:
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
sigma 8.11 0.2 7.72 8.52 6000 1
Samples were drawn using sampling(NUTS). For each parameter, Eff.Sample
is a crude measure of effective sample size, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).
```

Also, the phylogenetic signal remains more or less the same.

```
hyp <- paste(
"sd_phylo__Intercept^2 /",
"(sd_phylo__Intercept^2 + sd_species__Intercept^2 + sigma^2) = 0"
)
(hyp <- hypothesis(model_repeat2, hyp, class = NULL))
```

```
Hypothesis Tests for class :
Estimate Est.Error l-95% CI u-95% CI Evid.Ratio Star
(sd_phylo__Interc... = 0 0.74 0.06 0.62 0.84 0 *
---
'*': The expected value under the hypothesis lies outside the 95% CI.
```

Let’s say we have Fisher’s z-transformated correlation coefficients \(Zr\) per species along with corresponding sample sizes (e.g., correlations between male coloration and reproductive success):

```
data_fisher <- read.table(paste0(my_path, "data_effect.txt"), header = TRUE)
data_fisher$obs <- 1:nrow(data_fisher)
head(data_fisher)
```

```
Zr N phylo obs
1 0.28917549 13 sp_1 1
2 0.02415579 40 sp_2 2
3 0.19513651 39 sp_3 3
4 0.09831239 40 sp_4 4
5 0.13780152 66 sp_5 5
6 0.13710587 41 sp_6 6
```

We assume the sampling variance to be known and as \(V(Zr) = \frac{1}{N - 3}\) for Fisher’s values, where \(N\) is the sample size per species. Incorporating the known sampling variance into the model is straight forward. One has to keep in mind though, that **brms** requires the sampling standard deviation (square root of the variance) as input instead of the variance itself. The group-level effect of `obs`

represents the residual variance, which we have to model explicitly in a meta-analytic model.

```
model_fisher <- brm(Zr | se(sqrt(1 / (N - 3))) ~ 1 + (1|phylo) + (1|obs),
data = data_fisher, family = gaussian(),
cov_ranef = list(phylo = A),
prior = c(prior(normal(0, 10), "Intercept"),
prior(student_t(3, 0, 10), "sd")),
control = list(adapt_delta = 0.95),
chains = 2, cores = 2, iter = 4000, warmup = 1000)
```

A summary of the fitted model is obtained via

`summary(model_fisher)`

```
Family: gaussian(identity)
Formula: Zr | se(sqrt(1/(N - 3))) ~ 1 + (1 | phylo) + (1 | obs)
Data: data_fisher (Number of observations: 200)
Samples: 2 chains, each with iter = 4000; warmup = 1000; thin = 1;
total post-warmup samples = 6000
ICs: LOO = Not computed; WAIC = Not computed
Group-Level Effects:
~obs (Number of levels: 200)
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
sd(Intercept) 0.05 0.03 0 0.1 1041 1
~phylo (Number of levels: 200)
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
sd(Intercept) 0.06 0.04 0 0.15 982 1
Population-Level Effects:
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
Intercept 0.16 0.04 0.08 0.24 2751 1
Samples were drawn using sampling(NUTS). For each parameter, Eff.Sample
is a crude measure of effective sample size, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).
```

`plot(model_fisher)`

The meta-analytic mean (i.e., the model intercept) is \(0.16\) with a credible interval of \([0.08, 0.25]\). Thus the mean correlation across species is positive according to the model.

Suppose that we analyze a phenotype that consists of counts instead of being a continuous variable. In such a case, the normality assumption will likely not be justified and it is recommended to use a distribution explicitely suited for count data, for instance the Poisson distribution. The following data set (again retrieved from http://www.mpcm-evolution.org/practice/online-practical-material-chapter-11) provides an example.

```
data_pois <- read.table(paste0(my_path, "data_pois.txt"), header = TRUE)
data_pois$obs <- 1:nrow(data_pois)
head(data_pois)
```

```
phen_pois cofactor phylo obs
1 1 7.8702830 sp_1 1
2 0 3.4690529 sp_2 2
3 1 2.5478774 sp_3 3
4 14 18.2286628 sp_4 4
5 1 2.5302806 sp_5 5
6 1 0.5145559 sp_6 6
```

As the poisson distribution does not have a natural overdispersion parameter, we model the residual variance via the group-level effects of `obs`

(e.g., see Lawless, 1987).

```
model_pois <- brm(phen_pois ~ cofactor + (1|phylo) + (1|obs),
data = data_pois, family = poisson("log"),
cov_ranef = list(phylo = A),
chains = 2, cores = 2, iter = 4000,
control = list(adapt_delta = 0.95))
```

Again, we obtain a summary of the fitted model via

`summary(model_pois)`

```
Family: poisson(log)
Formula: phen_pois ~ cofactor + (1 | phylo) + (1 | obs)
Data: data_pois (Number of observations: 200)
Samples: 2 chains, each with iter = 4000; warmup = 2000; thin = 1;
total post-warmup samples = 4000
ICs: LOO = Not computed; WAIC = Not computed
Group-Level Effects:
~obs (Number of levels: 200)
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
sd(Intercept) 0.19 0.08 0.02 0.34 715 1
~phylo (Number of levels: 200)
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
sd(Intercept) 0.19 0.1 0.02 0.41 958 1
Population-Level Effects:
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
Intercept -2.08 0.21 -2.50 -1.67 3050 1
cofactor 0.25 0.01 0.23 0.27 4000 1
Samples were drawn using sampling(NUTS). For each parameter, Eff.Sample
is a crude measure of effective sample size, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).
```

`plot(marginal_effects(model_pois), points = TRUE) `

Now, assume we ignore the fact that the phenotype is count data and fit a linear normal model instead.

```
model_normal <- brm(phen_pois ~ cofactor + (1|phylo),
data = data_pois, family = gaussian(),
cov_ranef = list(phylo = A),
chains = 2, cores = 2, iter = 4000,
control = list(adapt_delta = 0.95))
```

`summary(model_normal)`

```
Family: gaussian(identity)
Formula: phen_pois ~ cofactor + (1 | phylo)
Data: data_pois (Number of observations: 200)
Samples: 2 chains, each with iter = 4000; warmup = 2000; thin = 1;
total post-warmup samples = 4000
ICs: LOO = Not computed; WAIC = Not computed
Group-Level Effects:
~phylo (Number of levels: 200)
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
sd(Intercept) 0.77 0.58 0.04 2.16 251 1.01
Population-Level Effects:
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
Intercept -2.89 0.87 -4.35 -0.08 83 1.02
cofactor 0.68 0.04 0.60 0.76 4000 1.00
Family Specific Parameters:
Estimate Est.Error l-95% CI u-95% CI Eff.Sample Rhat
sigma 3.44 0.18 3.12 3.82 4000 1
Samples were drawn using sampling(NUTS). For each parameter, Eff.Sample
is a crude measure of effective sample size, and Rhat is the potential
scale reduction factor on split chains (at convergence, Rhat = 1).
```

We see that `cofactor`

has a positive relationship with the phenotype in both models. One should keep in mind, though, that the estimates of the Poisson model are on the log-scale, as we applied the canonical log-link function in this example. Therefore, estimates are not comparable to a linear normal model even if applied to the same data. What we can compare, however, is the model fit, for instance graphically via posterior predictive checks.

`pp_check(model_pois)`

`pp_check(model_normal)`

Apparently, the distribution of the phenotype predicted by the Poisson model resembles the original distribution of the phenotype pretty closely, while the normal models fails to do so. We can also apply leave-one-out cross-validation for direct numerical comparison of model fit.

`LOO(model_pois, model_normal)`

```
LOOIC SE
model_pois 696.93 33.99
model_normal 1071.81 31.24
model_pois - model_normal -374.88 35.71
```

Since smaller values of LOO indicate better fit, it is again evident that the Poisson model fits the data better than the normal model. Of course, the Poisson model is not the only reasonable option here. For instance, you could use a negative binomial model (via family `negative_binomial`

), which already contains an overdispersion parameter so that modeling a varying intercept of `obs`

becomes obsolete.

In the above examples, we have only used a single group-level effect (i.e., a varying intercept) for the phylogenetic grouping factors. In **brms**, it is also possible to estimate multiple group-level effects (e.g., a varying intercept and a varying slope) for these grouping factors. However, it requires repeatedly computing Kronecker products and Cholesky factors of covariance matrices while fitting the model. This will be very slow especially when the grouping factors have many levels and matrices are thus large. I hope to increase efficiency for such models in the future.

Garamszegi, L. Z. (2014). *Modern Phylogenetic Comparative Methods and the application in Evolutionary Biology*. London, UK: Springer.

Lawless, J. F. (1987). Negative binomial and mixed Poisson regression. *Canadian Journal of Statistics*, 15(3), 209-225.