1. Preamble

First we load the required packages and custom functions.

set.seed(73)

# required packages
library(igraph)
library(ape)
library(phytools)
library(psych)
library(rstatix)
library(nlme)
library(geiger)
library(evomap)
library(ggplot2)
library(gridExtra)
library(nortest)
library(dplyr)

# custom functions
# Heterogeneity of connections (H)
heterogeneity<-function(graph){
  deg<-igraph::degree(graph)
  res<-sd(deg, na.rm=TRUE)/mean(deg, na.rm=FALSE)
  return(res)
}
# Parcellation index (P)
parcellation_index<-function(membership){
  Nm<-vector()
  for (i in 1:max(membership)){
    Nm[i]<-length(which(membership==i))
  }
  res<-1-sum((Nm/length(membership))^2)
  return(res)
}
# Color-blind palette
gg_color_blind<-c("#33a02c", "#1f78b4", "#b2df8a", "#a6cee3", "#ffb39c")
# Get outliers
is_outlier <- function(x) {
  # we cut at 1.7 which corresponds to 3*sigma in a gaussian distribution
  return(x < quantile(x, 0.25) - 1.5 * IQR(x) | x > quantile(x, 0.75) + 1.5 * IQR(x))
}
# Min-Max rescaling
rescaling<-function(values){
  r_values<-(values-min(values))/(max(values)-min(values))
  return(r_values)
}
# Plot phylomorphospace with ggplot
source("ggphylomorpho.R") # from https://github.com/wabarr/ggphylomorpho

# sesion info
pk<-c("ape", "evomap", "geiger", "ggplot2", "gridExtra", "igraph", "nlme", "nortest", "phytools", "psych", "rstatix")
knitr::kable(data.frame(packages=pk, version=unlist(lapply(lapply(pk, packageVersion), paste))), caption=R.version.string)
R version 4.0.3 (2020-10-10)
packages version
ape 5.4.1
evomap 0.0.0.9000
geiger 2.0.7
ggplot2 3.3.2
gridExtra 2.3
igraph 1.2.6
nlme 3.1.150
nortest 1.0.4
phytools 0.7.70
psych 2.0.9
rstatix 0.6.0

\(~\)

2. Data

Now we load the anatomical networks of the skull of 32 mammals (17 primates, 15 non-primates) and a set of 100 calibrated phylogenies to build a the consensus tree.

# network labels
networks<-c("Ornithorhynchus", "Phascolarctos", "Didelphis", "Tursiops","Equus", "Antilope", "Dasypus", "Sus", "Canis", "Felis", "Pteropus", "Oryctolagus", "Mus", "Tupaia", "Cynocephalus", "Loris", "Lemur", "Propithecus", "Tarsius", "Pithecia", "Aotus", "Callithrix", "Saimiri", "Colobus", "Cercopithecus", "Papio", "Macaca", "Hylobates", "Pongo", "Gorilla", "Pan", "Homo")

# group them
clade<-c(rep("Non-Primates", 15), rep("Primates", 17))

# load edge lists and create igraph objects 
graph.list<-list()
for (i in 1:length(networks)){
  net<-read.delim(paste("./xarxes/", networks[i], ".txt", sep=""), header=FALSE, sep=" ")
  graph.list[[i]]<-graph_from_data_frame(net, directed=FALSE)
}
graph.list<-lapply(graph.list, simplify) # fix potential loops and multi-links
names(graph.list)<-networks

# load phylogenetic tree
t<-read.nexus(file="output2.nex")
ct<-consensus.edges(t,method="mean.edge",if.absent="ignore")
tree<-force.ultrametric(ct, method="nnls")

# set names to match networks
binomial_name<-tree$tip.label
tree$tip.label<-gsub("\\_.*", "", tree$tip.label) # use genera as short names

The consensus calibrated phylogenetic tree for the species studied was built from 100 trees prunned from http://www.vertlife.org/.

plotTree(tree)
Figure S1. Calibrated phylogeny.

Figure S1. Calibrated phylogeny.

Network models of the type skull of young adults for each species were compiled from previous publications or built for this study. A skull network models has nodes (N) that represent individual bones and links (K) that represent physical joints (sutures and synchondroses) between pairs of bones.

df<-data.frame(N=mapply(vcount, graph.list), K=mapply(ecount, graph.list), row.names=networks)
sp<-matrix(as.numeric(gsub(".*:", "", summary(df))), ncol=2)
colnames(sp)<-colnames(df)
rownames(sp)<-names(summary(df[, 1]))

knitr::kable(sp, caption="Table S1. Range of values for nodes and links in skull networks.")
Table S1. Range of values for nodes and links in skull networks.
N K
Min. 21.00 47.00
1st Qu. 23.00 61.75
Median 23.50 64.50
Mean 24.91 68.09
3rd Qu. 26.25 70.25
Max. 32.00 99.00

In a graphical representation, network nodes and links are usually drawn as dots and lines. The layout is not dependent on the actual position of elements, because spatial position is not part of the network model directly (only presence/absence of connections).

par(mar=c(1,1,1,1))
plot(graph.list[[which(networks=="Homo")]], vertex.label=1:21)
Figure S2. Example of a skull network (human).

Figure S2. Example of a skull network (human).

\(~\)

3. Network Analysis

Following previous studies, we will measure six topological variables to characterize anatomical organization:

To calculate P first we need to delimit the community structure of the network. Here we will use an optimization function of the mpodularity Q value, implemented in cluster_optimal (see details with ?cluster_optimal).

A note on the morphological intepretation of topological variables

Topological variables are interpretable in the context of key concepts in morphology, such as complexity, integration, modularity, and disparity. Their interpretation derives from thinking of connections as co-dependences, as in Riedl’s concept of burden. A connection imposes a dependency (physical, developmental, variational) between two bones that affects the morphology of both bones and of the skull as a whole.

With that in mind, let us now briefly revise the meaning of topological variables in morphological terms (see Box 1 in the main text for details). Variables D, C and L capture features related to the integration among bones, so more integrated skulls will have greater values of D and C and lower values L. Likewise, variable P captures the degree of modularity of the network, so more modular skulls will have greater values of P. Integration and modularity are two sides of the same coin, so in many ways they are inversely related. At the same time, variables H and A capture features related to the differentiation (anisomerism) or disparity of bones, so skulls more disparate bones will have greater values of H and A.

# set data frame
parameters<-data.frame(matrix(NA, nrow=length(networks), ncol=6))
rownames(parameters)<-networks
colnames(parameters)<-c("D", "C", "L", "P", "H", "A")

# measure variables
parameters[, 1]<-mapply(edge_density, graph.list)
parameters[, 2]<-mapply(transitivity, graph.list, type="average", isolates="zero")
parameters[, 3]<-mapply(mean_distance, graph.list)
  communities<-list() # get communities to calculate parcellation
  for(i in 1:length(networks)){communities[[i]]<-cluster_optimal(graph.list[[i]])}
for(i in 1:length(networks)){parameters[i, 4]<-parcellation_index(membership(communities[[i]]))}
parameters[, 5]<-mapply(heterogeneity, graph.list)
parameters[, 6]<-mapply(assortativity_degree, graph.list, directed=FALSE)

knitr::kable(parameters, digits=3, caption="Table S2. Topological variables for each taxa.")
Table S2. Topological variables for each taxa.
D C L P H A
Ornithorhynchus 0.200 0.547 2.218 0.666 0.445 -0.212
Phascolarctos 0.187 0.473 2.338 0.741 0.343 -0.130
Didelphis 0.203 0.433 2.231 0.654 0.390 -0.205
Tursiops 0.200 0.506 2.262 0.746 0.488 0.032
Equus 0.197 0.456 2.289 0.749 0.358 -0.073
Antilope 0.174 0.421 2.373 0.774 0.385 -0.176
Dasypus 0.271 0.458 1.990 0.662 0.406 -0.069
Sus 0.177 0.412 2.322 0.749 0.442 -0.117
Canis 0.222 0.597 2.123 0.661 0.505 -0.117
Felis 0.198 0.449 2.248 0.744 0.358 0.043
Pteropus 0.224 0.534 2.133 0.662 0.545 -0.179
Oryctolagus 0.193 0.431 2.274 0.744 0.405 -0.079
Mus 0.206 0.443 2.201 0.656 0.410 -0.092
Tupaia 0.268 0.553 1.957 0.625 0.463 -0.111
Cynocephalus 0.246 0.543 2.014 0.642 0.467 -0.143
Loris 0.246 0.523 2.007 0.743 0.493 -0.145
Lemur 0.225 0.359 2.051 0.663 0.463 -0.215
Propithecus 0.217 0.370 2.065 0.726 0.500 -0.234
Tarsius 0.257 0.603 1.933 0.628 0.548 -0.234
Pithecia 0.241 0.558 2.036 0.658 0.477 -0.170
Aotus 0.249 0.489 1.960 0.711 0.492 -0.208
Callithrix 0.249 0.484 1.957 0.658 0.513 -0.245
Saimiri 0.245 0.522 1.953 0.643 0.570 -0.269
Colobus 0.265 0.602 1.925 0.628 0.511 -0.205
Cercopithecus 0.245 0.490 1.929 0.665 0.570 -0.310
Papio 0.257 0.617 1.941 0.643 0.566 -0.253
Macaca 0.241 0.461 1.964 0.658 0.535 -0.284
Hylobates 0.273 0.576 1.917 0.665 0.485 -0.157
Pongo 0.237 0.450 2.024 0.726 0.526 -0.114
Gorilla 0.253 0.561 1.945 0.733 0.536 -0.159
Pan 0.237 0.614 1.960 0.688 0.669 -0.279
Homo 0.305 0.634 1.829 0.608 0.495 -0.247

For future analysies we will use rescaled topological parameters

parameters<-as.data.frame(apply(parameters, 2, rescaling))

\(~\)

Do raw topological variables discriminate between primates and non-primate mammals?

To answer this question, we performed a phylogenetic permutational multivariate analysis of variance (PPERMANOVA). A non-parametric multivariate statistical test of the null hypothesis that the centroids and dispersion of the two groups are equivalent. Rejecting the null hypothesis means that the groups are different.

# study groups
parameters$clade<-clade

# permanova
dat<-as.matrix(parameters[, 1:6])
grp<-as.factor(parameters$clade); names(grp)<-rownames(parameters)
manova.fit<-aov.phylo(dat~grp, tree, nsim=10000, test="Wilks")
knitr::kable(attributes(manova.fit)$summary, digits=4, caption="Table S3. Phylogenetic PERMANOVA of raw topological variables comparing primates and non-primates.")
Table S3. Phylogenetic PERMANOVA of raw topological variables comparing primates and non-primates.
Df Wilks approx-F num-Df den-Df Pr(>F) Pr(>F) given phy
group 1 0.1596 21.9322 6 25 0 0.0097
Residuals 30 NA NA NA NA NA NA

Primates and non-primate mammals show a significantly different distribution of topological variables (PPERMANOVA p-value<0.05).

CONCLUSION: Primates and non-primates have different skull network topologies, with different distribution of values raw topological variables.

\(~\)

4. Cranial Anatomical Organization

To explore in what way cranial anatomical organization differs between primates and non-primate mammals, we will perform a principal component analysis (PCA) to get the main axes of variation.

The objective here is two-fold: on the one hand, we want to explain most of the variance among skull networks with as fewer components as possible; on the other hand, we want (ideally) each variable to load onto only one component, to make interpretation easier. To this end, we first perform a PCA of the six variables (without rotation) to quantify the cumulative variance explained by each additional PC added. Then, we calculate the average load of variables to each PC depending on the number of PCs on which the PCA is ran, bearing in mind that we want to be as close to one as possible.

Let us examine this relationship visually.

# exploring PCA
load<-1
for(i in 2:6){
  PCA<-principal(parameters[, -7], i, rotate="none", cor="cor")
  load[i]<-(mean(PCA$complexity))
}

# make plot
# axes
PC<-1:6
expvar<-PCA$Vaccounted[5,]
# add extra space to right margin of plot within frame
par(mar=c(5, 4, 4, 6) + 0.1)
# plot first set of data and draw its axis
plot(PC, load, pch=16, axes=FALSE, ylim=c(1,3), xlab="", ylab="", 
     type="b",col="black", main="PCA")
axis(2, ylim=c(1,3),col="black",las=1)  ## las=1 makes horizontal labels
mtext("Mean variable loading",side=2,line=2.5)
abline(h=1.5,col="black",lty=2)
box()
# allow a second plot on the same graph
par(new=TRUE)
# plot the second plot and put axis scale on right
plot(PC, expvar, pch=15,  xlab="", ylab="", ylim=c(0,1), 
     axes=FALSE, type="b", col="red", abline=abline(h=.5,col="red",lty=3))
# a little farther out (line=4) to make room for labels
mtext("Cumulative variance",side=4,col="red",line=4) 
axis(4, ylim=c(0,1), col="red",col.axis="red",las=1)
# draw the PC axis
axis(1,pretty(range(PC),6))
mtext("Number of PCs",side=1,col="black",line=2.5)
Figure S3. Exploring loads and explained variance by the number of components.

Figure S3. Exploring loads and explained variance by the number of components.

Taking the first 2 PCs is the most sensible option. That way we capture almost 80% of the variance and, at the same time, we keep the load of variables mostly to one single PC.

Let us now compute the first 2 PCs, but considering that they are probably correlated, so we apply an oblique rotation. Note that rotated principal components (PC) are relabeled as rotated components (RC).

PCA<-principal(parameters[, -7], 2, rotate="promax", cor="cor")

# show loadings
loads<-matrix(PCA$loadings, ncol=PCA$factors)
colnames(loads)<-colnames(PCA$loadings)
rownames(loads)<-rownames(PCA$loadings)
knitr::kable(data.frame(PCA$Vaccounted[1:3, ]), digits=4, caption="Table S4. Importance of rotated components.")
Table S4. Importance of rotated components.
RC1 RC2
SS loadings 2.9977 1.7246
Proportion Var 0.4996 0.2874
Cumulative Var 0.4996 0.7871

RC1 alone captures 50% of the total variance, and together with RC2 they capture the 78.7% of the variance. Let us see the loadings of topological variables on each RC.

# change the sign of RC2 to ease interpretation
loads[,2]<-loads[,2]*-1
PCA$scores[,2]<-PCA$scores[,2]*-1

knitr::kable(data.frame(loads), digits=4, caption="Table S5. Load of topological variable on RC1 and RC2.")

biplot.psych(PCA, main=NULL)
Table S5. Load of topological variable on RC1 and RC2.
RC1 RC2
D 0.9404 -0.0101
C 0.9465 -0.2320
L -0.7296 -0.3036
P -0.7511 -0.0810
H 0.2397 0.6980
A 0.1438 -1.0207
Figure S4. Biplot of the PCA results.

Figure S4. Biplot of the PCA results.

Taking a consensus threshold of 0.4 for meaningful loadings, we can see that RC1 captures mostly variables D, C, L and P (variables related to anatomical integration). Greater values of RC1 align with greater values of D and C and smaller values of L and P, that is, RC1 ranges from less to more anatomical integration. Complementarily, RC2 captures mostly variables H and P (variables related to anatomical disparity). Greater values of RC2 align with greater values of H and smaller values of A, that is, RC2 ranges from less to more disparity among bones.

(NB: the sign of RC2 has been changed to make interpretation easier.)

We can now ask, are anatomical integration (RC1) and disparity (RC2) different between primates and non-primate mammals?

# set data frame
df<-cbind(data.frame(PCA$scores), clade=parameters$clade, species=rownames(df))
plot_limits<-max(abs(PCA$scores)) + 0.25*max(abs(PCA$scores))
  
# make plot
p<-ggphylomorpho(tree=tree, tipinfo=df,xvar=RC1, yvar=RC2, factorvar=clade, labelvar=species, title=NULL, xlab=paste("Anatomical integration (RC1; var exp. ", round(100*(PCA$Vaccounted[2,1]),2),  "%)", sep=""), ylab=paste("Anatomical disparity (RC2; var exp. ", round(100*(PCA$Vaccounted[2,2]),2),  "%)", sep="")) + 
  scale_color_manual(values=c("Non-Primates"=gg_color_blind[2], "Primates"=gg_color_blind[3])) + 
  xlim(-1*plot_limits, plot_limits) + ylim(-1*plot_limits, plot_limits)
p

# save plot
ggplot2::ggsave(filename="Fig 2. Phylomorphospace of RC1 and RC2.pdf",
                width=120, height=120, units="mm", dpi=600)
Fig 2. Phylomorphospace of RC1 and RC2.

Fig 2. Phylomorphospace of RC1 and RC2.

# permanova
dat<-PCA$scores
grp<-as.factor(parameters$clade); names(grp)<-rownames(parameters)
manova.fit<-aov.phylo(dat~grp, tree, nsim=10000, test="Wilks")
knitr::kable(attributes(manova.fit)$summary, digits=4, caption="Table S6. Phylogenetic PERMANOVA of RC1 and RC2 comparing primates and non-primates.")
Table S6. Phylogenetic PERMANOVA of RC1 and RC2 comparing primates and non-primates.
Df Wilks approx-F num-Df den-Df Pr(>F) Pr(>F) given phy
group 1 0.3865 23.012 2 29 0 0.0298
Residuals 30 NA NA NA NA NA NA

The test is significant; thus, we reject the null hypothesis that the centroids and/or dispersion of the two groups are equivalent.

CONCLUSION: Following the morphological interpretation of topological variables and the loads of variable RC1 and RC2, results indicate that skull are more integrated (hence, less modular) and more disparate in primates than in non-primate mammals, even when we account phylogenetic relatedness of primates compared with the diverse group of non-primate mammals.

\(~\)

5. Anatomical Organization and Brain Size

Here we explore whether differences in brain size can explain differences in anatomical integration (RC1) and disparity (RC2) between primates and non-primate mammals. To this end, we will fit a phylogenetic generalized least square (PGLS) using a lambda correlation structure.

As a proxy of brain size, we use the residuals of a PGLS of brain mass on body mass. Original data comes from a large sample of mammals (Burger et al. 2019; see main text).

# load data and prepare dataset for analysis
phenodata<-read.delim("Burgeretal2019brainallometry.tsv")
phenodata<-phenodata[is.element(phenodata$Binomial, binomial_name), c(1, 13)]
rownames(phenodata)<-gsub("\\_.*", "", phenodata$Binomial)
phenodata$Binomial<-NULL
# add C. volans from Lewitus et al 2014 (brain=6.2g; body=1300g),
# using Burger et al 2019 fit (Brain = -1.26 + 0.75*Body) with log10
phenodata<-rbind(phenodata, Cynocephalus=log10(6.2)-(-1.26+(0.75*log10(1300))))
phenodata<-phenodata[match(rownames(parameters), rownames(phenodata)), ]
names(phenodata)<-rownames(parameters)
dat<-data.frame(cbind(Brain.res=phenodata, RC=PCA$scores))

\(~\)

5.1. Anatomical Integration (RC1) and Brain Size

Let us first test if our data-model departs from the usual regression assumptions. Acording to Dr. Liam Revell1, “we do not expect our input data in phylogenetic ANOVA or regression to be normally distributed - just the residual error controlling both for the main effects in our model, and the tree.” We can test that using the Lilliefors (Kolmogorov-Smirnov) test. A significant result means the residuals are not normally distributed. the association between integration and brain size.

# prepare data
Y<-"RC1"; X<-"Brain.res"
data<-dat[, c(which(colnames(dat)==Y), which(colnames(dat)==X)), drop=F]
tree<-treedata(tree, data, sort=T, warnings=T)$phy
data<-as.data.frame(treedata(tree, data, sort=T, warnings=T)$data)
colnames(data)<-c("Dependent", "Independent") 

# preliminary model fit
model_pgls<-gls(Dependent~Independent, data, correlation=corPagel(1, tree))

# normality of residuals test
test<-lillie.test(chol(solve(vcv(tree)))%*%residuals(model_pgls))
dft<-data.frame(D=as.numeric(test$statistic), p.value=test$p.value)
colnames(dft)<-c("D", "p-value")

knitr::kable(dft, digits=4, caption="Table S7. Lilliefors test of normality for the PGLS residuals.")
Table S7. Lilliefors test of normality for the PGLS residuals.
D p-value
0.0813 0.8524

Lilliefors test is not significant. We can assume that the residual errors of the PGLS are normally (enough) distributed.

df1<-data.frame(residuals=chol(solve(vcv(tree))) %*% residuals(model_pgls))
df2 <- df1 %>% tibble::rownames_to_column(var="outlier") %>% mutate(is_outlier=ifelse(is_outlier(residuals), residuals, as.numeric(NA)))
df2$outlier[which(is.na(df2$is_outlier))] <- as.numeric(NA)
ggplot(df2, aes(x="Mammals", y=residuals)) +
  geom_boxplot() +
  geom_text(aes(label=outlier), na.rm=TRUE, hjust=1.25) +
  xlab(element_blank()) +
  theme_classic()
knitr::kable(data.frame(outliers=rownames(df1)[!is.na(df2$outlier)]))
outliers
Figure S5. IQR outlier test.

Figure S5. IQR outlier test.

No outliers seem to stand out neither (interquartile rule).

# pgls
model_pgls<-gls(Dependent~Independent, data, correlation=corPagel(1, tree))
Sigma<-vcv(corPagel(1, phy=tree))

# intervals
model_ci<-gls.ci(data$Dependent, data$Independent, Sigma) #ci
model_pi<-gls.pi(data$Dependent, data$Independent, Sigma, k=1) #ci
df<-data.frame(cbind(x=model_ci$CI$X, y=model_ci$CI$Yhat, 
                     ci.min=model_ci$CI$Lower2.5, 
                     ci.max=model_ci$CI$Upper2.5, 
                     pi.min=model_pi$PI$Lower2.5, 
                     pi.max=model_pi$PI$Upper2.5))

# make/show plot
ggplot(data=data, aes(x=Independent, y=Dependent)) +
  geom_point(col="black", size=3) +
  geom_line(data=df, aes(x=x, y=y), size=1, inherit.aes=FALSE) +
  geom_line(data=df, aes(x=x, y=ci.min), size=0.5, linetype="dotted", inherit.aes=FALSE) +
  geom_line(data=df, aes(x=x, y=ci.max), size=0.5, linetype="dotted", inherit.aes=FALSE) +
  geom_line(data=df, aes(x=x, y=pi.min), size=0.5, linetype="dashed", inherit.aes=FALSE) +
  geom_line(data=df, aes(x=x, y=pi.max), size=0.5, linetype="dashed", inherit.aes=FALSE) +
  xlab("Brain size (Brain~Body residuals)") + ylab("Anatomical integration (RC1)") +
  theme_classic()

# pgls table
knitr::kable(as.data.frame(summary(model_pgls)$tTable), digits=4, caption="Table S8. Summary statics of the PGLS fit of brain size residuals on RC1.")

# correlation between x & y
obj<-phyl.vcv(as.matrix(data), vcv(corPagel(1, tree)), 1)
r.xy<-cov2cor(obj$R)["Independent", "Dependent"]
t.xy<-r.xy*sqrt((Ntip(tree)-2)/(1-r.xy^2))
P.xy<-2*pt(abs(t.xy), df=Ntip(tree)-2, lower.tail=F)
dfPearson<-data.frame(r=r.xy, t=t.xy, p.value=P.xy)
colnames(dfPearson)<-c("r", "t-value", "p-value")
knitr::kable(dfPearson, digits=4, caption="Table S9. Phylogenetic Pearson's correlation between RC1 and brain size residuals.")
Table S8. Summary statics of the PGLS fit of brain size residuals on RC1.
Value Std.Error t-value p-value
(Intercept) -0.3324 0.6231 -0.5334 0.5977
Independent 1.3769 0.6327 2.1763 0.0375
Table S9. Phylogenetic Pearson’s correlation between RC1 and brain size residuals.
r t-value p-value
0.5467 3.5766 0.0012
Figure S6. PGLS fit of brain size residuals on RC1. Dotted lines 95% CI; dashed lines 95% PI.

Figure S6. PGLS fit of brain size residuals on RC1. Dotted lines 95% CI; dashed lines 95% PI.

The PGLS fit shows this positive relation between brain size and anatomical integration in mammals, with a moderate correlation between variables. In general, bigger brains predict greater integration of the skull.

\(~\)

We can now proceed with the main question, does the effect of brain size on anatomical integration is different in primates and non-primate mammals?

# assign groups
data$clade<-rep("Non-Primates", length(tree$tip.label))
data$clade[getTips(tree, findMRCA(tree, c("Loris", "Pan")))]<-"Primates"

fit_mammals<-gls(Dependent~Independent, data=subset(data, data$clade=="Non-Primates"), correlation=corPagel(1,phy=drop.tip(tree, which(data$clade!="Non-Primates"))))
fit_primates<-gls(Dependent~Independent, data=subset(data, data$clade=="Primates"), corPagel(1,phy=drop.tip(tree, which(data$clade!="Primates"))))

# get the lines
# primates
p_data<-subset(data, data$clade=="Primates")
p_tree<-drop.tip(tree, which(data$clade!="Primates"))
p_Sigma<-vcv(corPagel(1, phy=p_tree, fixed=FALSE))
p_model_ci<-gls.ci(p_data$Dependent, p_data$Independent, p_Sigma) #ci
p_df<-data.frame(cbind(x=p_model_ci$CI$X, 
                       y=p_model_ci$CI$Yhat, 
                       ci.min=p_model_ci$CI$Lower2.5, 
                       ci.max=p_model_ci$CI$Upper2.5))
# mammals
m_data<-subset(data, data$clade=="Non-Primates")
m_tree<-drop.tip(tree, which(data$clade!="Non-Primates"))
m_Sigma<-vcv(corPagel(1, phy=m_tree, fixed=FALSE))
m_model_ci<-gls.ci(m_data$Dependent, m_data$Independent, m_Sigma) #ci
m_df<-data.frame(cbind(x=m_model_ci$CI$X, 
                       y=m_model_ci$CI$Yhat, 
                       ci.min=m_model_ci$CI$Lower2.5, 
                       ci.max=m_model_ci$CI$Upper2.5))

# make/show plot
ggplot(data=data, aes(x=Independent, y=Dependent, col=clade)) +
  geom_point(size=3) +
  scale_color_manual(values=gg_color_blind[c(2, 3, 4)]) +
  geom_line(data=m_df, aes(x=x, y=y), size=1, col=gg_color_blind[2], inherit.aes=FALSE) +
  geom_line(data=p_df, aes(x=x, y=y), size=1, col=gg_color_blind[3], inherit.aes=FALSE) +
  geom_line(data=df, aes(x=x, y=y), size=1, linetype="dashed", inherit.aes=FALSE) +
  xlab("Brain size (Brain~Body residuals)") + ylab("Anatomical integration (RC1)") +
  theme_classic() + theme(legend.position="top", legend.title=element_blank())

# save plot
ggplot2::ggsave(filename="Fig 3. PGLS fit of brain size on anatomical integration in primates and non-primates.pdf", width=120, height=90, units="mm", dpi=600)
Fig 3. PGLS fit of brain size on anatomical integration in primates and non-primates.

Fig 3. PGLS fit of brain size on anatomical integration in primates and non-primates.

At first sight, it seems there may be some difference between groups, although data for non-primate mammals is very scattered. We will use a phylogenetic analysis of covariance (PANCOVA) to test whether a model with more parameters (different regressions for primates and non-primates) fits significantly better to the data than the baseline model (Figure S6).

The predictions are the following:

  • No support for any alternative model. This means that the baseline model (one slope and one intercept) fits the data better. Thus, all mammals share the same relation between brain size and integration.

  • Support for differences in intercepts. This means that there are differences in integration, but the relation to brain size is the same for all mammals.

  • Support for differences in slopes. This means that primates and non-primate mammals have a different relation between brain size and anatomy, within a similar range of integration.

  • Support for different slopes and intercepts. This means that primates and non-primate mammals have a completely different relation between brain size and anatomical integration.

# set groups to be compared
Primates<-getTips(tree, findMRCA(tree, c("Loris", "Homo")))
Mammals<-c(1:length(tree$tip.label))[-Primates]

# prepare group allocation variables
# for differences in slope:
grpS<-rep("A", length(rownames(data)))
grpS[Primates]<-"B"
grpS<-as.factor(grpS)
names(grpS)<-rownames(data)
# for differences in intercept:
grpI<-rep("A", length(rownames(data)))
grpI[Primates]<-"B"
grpI<-as.factor(grpI)
names(grpI)<-rownames(data)

# set the models to test with pANCOVA
# baseline model with one slope and one intercept:
Model<-model.matrix(as.formula(Dependent~Independent), data)
# model 1: differences in slopes, holding intercept constant:
Model_S<-model.matrix(as.formula(Dependent~grpS:Independent), data)
# model 2: differences in intercept, holding slopes constant:
Model_I<-model.matrix(as.formula(Dependent~grpI + Independent), data)
# model 3: differences in slopes and differences in intercept:
Model_SI<-model.matrix(as.formula(Dependent~grpI + grpS:Independent), data)

# test models agaings baseline model
lambda<-model_pgls$modelStruct$corStruct[1]
treevcv<-vcv(rescale(tree,"lambda",lambda))
# (1) Differences in slopes, holding intercept constant:
knitr::kable(gls.ancova(Dependent~Independent, treevcv, Model, Model_S), caption="Table S10. Differences in slopes, holding intercept constant (RC1).")
#(2) Differences in intercept, holding slopes constant:
knitr::kable(gls.ancova(Dependent~Independent, treevcv, Model, Model_I), caption="Table S11. Differences in intercept, holding slopes constant (RC1).")
#(3) Differences in slopes and differences in intercept:
knitr::kable(gls.ancova(Dependent~Independent, treevcv, Model, Model_SI), caption="Table S12. Differences in slopes and differences in intercept (RC1).")
Table S10. Differences in slopes, holding intercept constant (RC1).
df Sum Sq Mean Sum Sq F value Pr(>F)
FullModel 3 40.6783 1.4027 1.6802 0.2051
ReducedModel 2 43.0351 1.4345
Table S11. Differences in intercept, holding slopes constant (RC1).
df Sum Sq Mean Sum Sq F value Pr(>F)
FullModel 3 42.9379 1.4806 0.0656 0.7996
ReducedModel 2 43.0351 1.4345
Table S12. Differences in slopes and differences in intercept (RC1).
df Sum Sq Mean Sum Sq F value Pr(>F)
FullModel 4 40.658 1.4521 0.8185 0.4514
ReducedModel 2 43.0351 1.4345

PANCOVA is not significant for any alternative model. Thus, we can assume that the baseline model fits better to the data than alternative models with more parameters.

The dispersion of the data may have prevented us to detect a significant difference between primates and non-primates with PANCOVA. Let us nonetheless check the relation between brain size and integration in primates and non-primate mammals, separately.

# primates
obj<-phyl.vcv(as.matrix(p_data[,1:2]), vcv(corPagel(1, p_tree)), 1)
r.xy<-cov2cor(obj$R)["Independent", "Dependent"]
t.xy<-r.xy*sqrt((Ntip(p_tree)-2)/(1-r.xy^2))
P.xy<-2*pt(abs(t.xy), df=Ntip(p_tree)-2, lower.tail=F)

knitr::kable(data.frame(r=r.xy, t=t.xy, p.value=P.xy), digits=4, caption="Table S13. Primates phylogenetic Pearson's correlation between RC1 and brain size residuals.")
knitr::kable(as.data.frame(summary(fit_primates)$tTable), digits=4, caption="Table S14. Primates PGLS fit of brain size residuals on RC1.")

# non-primates
obj<-phyl.vcv(as.matrix(m_data[,1:2]), vcv(corPagel(1, m_tree)), 1)
r.xy<-cov2cor(obj$R)["Independent", "Dependent"]
t.xy<-r.xy*sqrt((Ntip(m_tree)-2)/(1-r.xy^2))
P.xy<-2*pt(abs(t.xy), df=Ntip(m_tree)-2, lower.tail=F)

knitr::kable(data.frame(r=r.xy, t=t.xy, p.value=P.xy), digits=4, caption="Table S15. Non-primates phylogenetic Pearson's correlation between RC1 and brain size residuals.")
knitr::kable(as.data.frame(summary(fit_mammals)$tTable), digits=4, caption="Table S16. Non-primates PGLS fit of brain size residuals on RC1.")
Table S13. Primates phylogenetic Pearson’s correlation between RC1 and brain size residuals.
r t p.value
0.6356 3.1888 0.0061
Table S14. Primates PGLS fit of brain size residuals on RC1.
Value Std.Error t-value p-value
(Intercept) -0.1851 0.3322 -0.5571 0.5857
Independent 2.2078 0.8786 2.5129 0.0239
Table S15. Non-primates phylogenetic Pearson’s correlation between RC1 and brain size residuals.
r t p.value
0.1258 0.4571 0.6552
Table S16. Non-primates PGLS fit of brain size residuals on RC1.
Value Std.Error t-value p-value
(Intercept) -0.5726 0.0579 -9.8829 0.0000
Independent 0.2334 1.1389 0.2049 0.8408

CONCLUSION: Bigger brains predict greater anatomical integration of the skulls of the mammals in the sample. But this trend is mostly driven by Primates, while non-primates show no relationship between brain size and integration. Because of the lack of relation in non-primates the PANCOVA fails to confirm such difference between primates and non-primates.

\(~\)

5.2. Anatomical Disparity (RC2) and Brain Size

As before, let us begin testing PGLS normality of residuals and outliers.

# prepare data
Y<-"RC2"; X<-"Brain.res"
data<-dat[, c(which(colnames(dat)==Y), which(colnames(dat)==X)), drop=F]
tree<-treedata(tree, data, sort=T, warnings=T)$phy
data<-as.data.frame(treedata(tree, data, sort=T, warnings=T)$data)
colnames(data)<-c("Dependent", "Independent") 

# preliminary model fit
model_pgls<-gls(Dependent~Independent, data, correlation=corPagel(1, tree))

# normality of residuals test
test<-lillie.test(chol(solve(vcv(tree)))%*%residuals(model_pgls))
dft<-data.frame(D=as.numeric(test$statistic), p.value=test$p.value)
colnames(dft)<-c("D", "p-value")

knitr::kable(dft, digits=4, caption="Table S17. Lilliefors test of normality for the PGLS residuals.")
Table S17. Lilliefors test of normality for the PGLS residuals.
D p-value
0.1788 0.0107

Lilliefors test is significant. We reject the null hypothesis of residuals following a normal distribution.

df1<-data.frame(residuals=chol(solve(vcv(tree))) %*% residuals(model_pgls))
df2 <- df1 %>% tibble::rownames_to_column(var="outlier") %>% mutate(is_outlier=ifelse(is_outlier(residuals), residuals, as.numeric(NA)))
df2$outlier[which(is.na(df2$is_outlier))] <- as.numeric(NA)
ggplot(df2, aes(x="Mammals", y=residuals)) +
  geom_boxplot() +
  geom_text(aes(label=outlier), na.rm=TRUE, hjust=1.25) +
  xlab(element_blank()) +
  theme_classic()
knitr::kable(data.frame(outliers=rownames(df1)[!is.na(df2$outlier)]))
outliers
Pan
Cercopithecus
Figure S7. IQR outlier test.

Figure S7. IQR outlier test.

In addition, it seems that we have two potential outliers (interquartile rule). Let us have a closer look at them.

raw_out<-read.delim("Burgeretal2019brainallometry.tsv")
raw_out<-raw_out[is.element(raw_out$Binomial, c("Cercopithecus_mitis", "Pan_paniscus")), c(9, 10, 13)]
raw_out<-data.frame(cbind(raw_out,data$Dependent[c(which(rownames(data)=="Cercopithecus"),which(rownames(data)=="Pan"))]))
rownames(raw_out)<-c("Pan", "Cercopithecus")
colnames(raw_out)<-c("Brain mass (g)", "Body mass (g)", "Brain residual / Independent", "RC2 / Dependent")
knitr::kable(raw_out, digits=3)
Brain mass (g) Body mass (g) Brain residual / Independent RC2 / Dependent
Pan 329.7 39700 0.329 1.726
Cercopithecus 75.0 6300 0.286 1.763

Nothing looks wrong in the data except that these species are at the upper end of the RC2 distribution. We can use PANCOVA to test if they are trully outliers of the PGLS by comparing intercepts (as explained before).

# set groups to be compared
outlier<-c(which(tree$tip.label=="Pan"), which(tree$tip.label=="Cercopithecus"))

# set group allocation for the intercepts
grpI<-rep("A",length(rownames(data)))
grpI[outlier]<-"B" 
grpI<-as.factor(grpI)
names(grpI)<-rownames(data)

# test against the baseline one-slope one-intercept model
Model<-model.matrix(as.formula(Dependent~Independent),data)
Model_I<-model.matrix(as.formula(Dependent~grpI + Independent),data)
knitr::kable(gls.ancova(Dependent~Independent,vcv(tree),Model,Model_I),caption="Table S18. PANCOVA test for outliers in PGLS of brain size residuals on RC2.")
Table S18. PANCOVA test for outliers in PGLS of brain size residuals on RC2.
df Sum Sq Mean Sum Sq F value Pr(>F)
FullModel 3 57.6918 1.9894 29.9505 0
ReducedModel 2 117.2744 3.9091

The PANCOVA test is significant; thus, we reject the model with fewer parameters (one intercept). Pan and Cercopithecus are outliers.

Let us now remove the outliers and test if the PGLS residuals have a normal distribution.

# prepare data without outliers
data<-data[-outlier,-3]
tree<-drop.tip(tree,outlier)

# preliminary model fit
model_pgls<-gls(Dependent~Independent, data, correlation=corPagel(1, tree))

# normality of residuals test
test<-lillie.test(chol(solve(vcv(tree)))%*%residuals(model_pgls))
dft<-data.frame(D=as.numeric(test$statistic), p.value=test$p.value)
colnames(dft)<-c("D", "p-value")

knitr::kable(dft, digits=4, caption="Table S19. Lilliefors test of normality for the PGLS residuals without outliers.")
Table S19. Lilliefors test of normality for the PGLS residuals without outliers.
D p-value
0.1143 0.406

Without the outliers, the Lilliefors test is not significant. Thus, we can assume that the residuals of the PGLS are normally (enough) distributed.

We will proceed the analysis without the two outliers.

# pgls
model_pgls<-gls(Dependent~Independent, data, correlation=corPagel(1, tree))
Sigma<-vcv(corPagel(1, phy=tree))

# intervals
model_ci<-gls.ci(data$Dependent, data$Independent, Sigma) #ci
model_pi<-gls.pi(data$Dependent, data$Independent, Sigma, k=1) #ci
df<-data.frame(cbind(x=model_ci$CI$X, y=model_ci$CI$Yhat, 
                     ci.min=model_ci$CI$Lower2.5, 
                     ci.max=model_ci$CI$Upper2.5, 
                     pi.min=model_pi$PI$Lower2.5, 
                     pi.max=model_pi$PI$Upper2.5))

# make/show plot
ggplot(data=data, aes(x=Independent, y=Dependent)) +
  geom_point(col="black", size=3) +
  geom_line(data=df, aes(x=x, y=y), size=1, inherit.aes=FALSE) +
  geom_line(data=df, aes(x=x, y=ci.min), size=0.5, linetype="dotted", inherit.aes=FALSE) +
  geom_line(data=df, aes(x=x, y=ci.max), size=0.5, linetype="dotted", inherit.aes=FALSE) +
  geom_line(data=df, aes(x=x, y=pi.min), size=0.5, linetype="dashed", inherit.aes=FALSE) +
  geom_line(data=df, aes(x=x, y=pi.max), size=0.5, linetype="dashed", inherit.aes=FALSE) +
  xlab("Brain size (Brain~Body residuals)") + ylab("Anatomical disparity (RC2)") +
  theme_classic()

# pgls table
knitr::kable(as.data.frame(summary(model_pgls)$tTable), digits=4, caption="Table S20. Summary statics of the PGLS fit of brain size residuals on RC2.")

# correlation between x & y
obj<-phyl.vcv(as.matrix(data), vcv(corPagel(1, tree)), 1)
r.xy<-cov2cor(obj$R)["Independent", "Dependent"]
t.xy<-r.xy*sqrt((Ntip(tree)-2)/(1-r.xy^2))
P.xy<-2*pt(abs(t.xy), df=Ntip(tree)-2, lower.tail=F)
dfPearson<-data.frame(r=r.xy, t=t.xy, p.value=P.xy)
colnames(dfPearson)<-c("r", "t-value", "p-value")
knitr::kable(dfPearson, digits=4, caption="Table S21. Phylogenetic Pearson's correlation between RC2 and brain size residuals.")
Table S20. Summary statics of the PGLS fit of brain size residuals on RC2.
Value Std.Error t-value p-value
(Intercept) -0.3888 0.7098 -0.5478 0.5882
Independent 0.6045 0.4895 1.2349 0.2271
Table S21. Phylogenetic Pearson’s correlation between RC2 and brain size residuals.
r t-value p-value
0.2878 1.5903 0.123
Figure S8. PGLS fit of brain size residuals on RC2. Dotted lines 95% CI; dashed lines 95% PI.

Figure S8. PGLS fit of brain size residuals on RC2. Dotted lines 95% CI; dashed lines 95% PI.

The PGLS fit shows no relationship between brain size and anatomical disparity for the species in the sample.

Do at least primates or non-primates, separately, show some relationship between the two variables?

# assign groups
data$clade<-rep("Non-Primates", length(tree$tip.label))
data$clade[getTips(tree, findMRCA(tree, c("Loris", "Homo")))]<-"Primates"

fit_mammals<-gls(Dependent~Independent, data=subset(data, data$clade=="Non-Primates"), correlation=corPagel(1,phy=drop.tip(tree, which(data$clade!="Non-Primates"))))
fit_primates<-gls(Dependent~Independent, data=subset(data, data$clade=="Primates"), corPagel(1,phy=drop.tip(tree, which(data$clade!="Primates"))))

# primates
p_data<-subset(data, data$clade=="Primates")
p_tree<-drop.tip(tree, which(data$clade!="Primates"))
p_Sigma<-vcv(corPagel(1, phy=p_tree, fixed=FALSE))
p_model_ci<-gls.ci(p_data$Dependent, p_data$Independent, p_Sigma) #ci
p_df<-data.frame(cbind(x=p_model_ci$CI$X, 
                       y=p_model_ci$CI$Yhat, 
                       ci.min=p_model_ci$CI$Lower2.5, 
                       ci.max=p_model_ci$CI$Upper2.5))

obj<-phyl.vcv(as.matrix(p_data[,1:2]), vcv(corPagel(1, p_tree)), 1)
r.xy<-cov2cor(obj$R)["Independent", "Dependent"]
t.xy<-r.xy*sqrt((Ntip(p_tree)-2)/(1-r.xy^2))
P.xy<-2*pt(abs(t.xy), df=Ntip(p_tree)-2, lower.tail=F)
obj<-phyl.vcv(as.matrix(p_data[,1:2]), vcv(corPagel(1, p_tree)), 1)
r.xy<-cov2cor(obj$R)["Independent", "Dependent"]
t.xy<-r.xy*sqrt((Ntip(p_tree)-2)/(1-r.xy^2))
P.xy<-2*pt(abs(t.xy), df=Ntip(p_tree)-2, lower.tail=F)

knitr::kable(data.frame(r=r.xy, t=t.xy, p.value=P.xy), digits=4, caption="S22 Table. Primates phylogenetic Pearson's correlation between RC2 and brain size residuals.")
knitr::kable(as.data.frame(summary(fit_primates)$tTable), digits=4, caption="S23 Table. Primates PGLS fit of brain size residuals on RC2.")


# non-primates
m_data<-subset(data, data$clade=="Non-Primates")
m_tree<-drop.tip(tree, which(data$clade!="Non-Primates"))
m_Sigma<-vcv(corPagel(1, phy=m_tree, fixed=FALSE))
m_model_ci<-gls.ci(m_data$Dependent, m_data$Independent, m_Sigma) #ci
m_df<-data.frame(cbind(x=m_model_ci$CI$X, 
                       y=m_model_ci$CI$Yhat, 
                       ci.min=m_model_ci$CI$Lower2.5, 
                       ci.max=m_model_ci$CI$Upper2.5))

obj<-phyl.vcv(as.matrix(m_data[,1:2]), vcv(corPagel(1, m_tree)), 1)
r.xy<-cov2cor(obj$R)["Independent", "Dependent"]
t.xy<-r.xy*sqrt((Ntip(m_tree)-2)/(1-r.xy^2))
P.xy<-2*pt(abs(t.xy), df=Ntip(m_tree)-2, lower.tail=F)

knitr::kable(data.frame(r=r.xy, t=t.xy, p.value=P.xy), digits=4, caption="S24 Table. Non-primates phylogenetic Pearson's correlation between RC2 and brain size residuals.")
knitr::kable(as.data.frame(summary(fit_mammals)$tTable), digits=4, caption="S25 Table. Non-primates PGLS fit of brain size residuals on RC2.")
S22 Table. Primates phylogenetic Pearson’s correlation between RC2 and brain size residuals.
r t p.value
0.452 1.8269 0.0908
S23 Table. Primates PGLS fit of brain size residuals on RC2.
Value Std.Error t-value p-value
(Intercept) 0.4409 0.2310 1.9087 0.0786
Independent 0.4197 0.6702 0.6262 0.5420
S24 Table. Non-primates phylogenetic Pearson’s correlation between RC2 and brain size residuals.
r t p.value
-0.3767 -1.4664 0.1663
S25 Table. Non-primates PGLS fit of brain size residuals on RC2.
Value Std.Error t-value p-value
(Intercept) -0.9322 0.0367 -25.4156 0.0000
Independent -1.0761 0.7673 -1.4024 0.1842

Neither primates nor non-primates show a significant relationship between anatomical disparity and brain size.

CONCLUSION: Bigger brains do not predict changes in the anatomical disparity of skull bones in the species studied, and neither they do in primates or in non-primates, separately.


  1. http://blog.phytools.org/2013/02/a-comment-on-distribution-of-residuals.html↩︎