In this tutorial, you can reproduce the analyses performed in the paper ‘Bridging performance and adaptive landscapes to understand long-term functional evolution’. Our main aim was to address whether aquatic and terrestrial lifestyles have influenced the location of performance and adaptive peaks for turtle shells.
We re-analyzed turtle shell data collected by Stayton et al. (2018) for 237 species, the same data also used in Stayton (2019). We performed first the performance landscape approach (PLA, Part I), then the adaptive landscape approach (ALA, Part II), and finally the merged framework: comparing performance and adaptive peaks, then comparing species’ phenotypes with inferred primary adaptive optima (Part III).
R.Version()$version.string
## [1] "R version 4.2.0 (2022-04-22)"
R.Version()$platform
## [1] "x86_64-apple-darwin17.0"
library(metR)
packageVersion("metR")
## [1] '0.14.0'
library(geomorph)
packageVersion("geomorph")
## [1] '4.0.5'
library(phytools)
packageVersion("phytools")
## [1] '1.2.0'
library(ape)
packageVersion("ape")
## [1] '5.6.2'
library(tidyverse)
packageVersion("tidyverse")
## [1] '1.3.2'
library(ouch)
packageVersion("ouch")
## [1] '2.18'
library(ggplot2)
packageVersion("ggplot2")
## [1] '3.4.0'
library(viridis)
packageVersion("viridis")
## [1] '0.6.2'
library(phylolm)
packageVersion("phylolm")
## [1] '2.6.2'
We will first read data provided by Stayton (2019) to construct a theoretical morphospace using the principal component (PC) axes that explain the most variance in the shape of turtle shells. Then, we will plot the performance surface for each function known to be relevant in turtle shells: breaking stress, drag, and self-righting ability. Stayton (2019) determined the association between PC scores and function using modeling and experiments.
stayton_data <- read.csv("stayton_data.csv", header = TRUE)
colnames(stayton_data) <- c("PC1", "PC2", "stress", "drag", "self.righting")
head(stayton_data)
## PC1 PC2 stress drag self.righting
## 1 -0.15 -0.150 276.10 0.19815 3.6765
## 2 -0.15 -0.125 255.17 0.18320 3.6275
## 3 -0.15 -0.100 239.30 0.17985 3.5202
## 4 -0.15 -0.075 222.68 0.18298 3.4146
## 5 -0.15 -0.050 209.36 0.19181 3.2992
## 6 -0.15 -0.025 193.85 0.20483 3.1750
plot(stayton_data$PC1, stayton_data$PC2, xlab = "PC1", ylab = "PC2", main = "Theoretical morphospace")
To plot the performance surfaces, we will use the package metR that can create contour plots using the ggplot framework.
stress_contour <- ggplot(stayton_data, aes(PC1, PC2, z = stress)) + geom_contour_fill(breaks = MakeBreaks(binwidth = 15,
exclude = 0)) + scale_fill_divergent(breaks = MakeBreaks(binwidth = 15,
exclude = 0)) + coord_equal() + theme_bw() + scale_fill_viridis() + guides(fill = guide_legend(title = "Stress")) +
theme(text = element_text(size = 13, color = "black"), panel.grid.major = element_blank(),
panel.grid.minor = element_blank(), axis.text = element_text(size = 12,
color = "black"), axis.title = element_text(size = 16, color = "black"),
legend.text = element_text(size = 10), legend.title = element_text(size = 12))
stress_contour
drag_contour <- ggplot(stayton_data, aes(PC1, PC2, z = drag)) + geom_contour_fill() +
coord_equal() + theme_bw() + scale_fill_viridis() + guides(fill = guide_legend(title = "Drag")) +
theme(text = element_text(size = 13, color = "black"), panel.grid.major = element_blank(),
panel.grid.minor = element_blank(), axis.text = element_text(size = 12,
color = "black"), axis.title = element_text(size = 16, color = "black"),
legend.text = element_text(size = 10), legend.title = element_text(size = 12))
drag_contour
righting_contour <- ggplot(stayton_data, aes(PC1, PC2, z = self.righting)) +
geom_contour_fill() + coord_equal() + theme_bw() + scale_fill_viridis() +
guides(fill = guide_legend(title = "Self righting")) + theme(text = element_text(size = 13,
color = "black"), panel.grid.major = element_blank(), panel.grid.minor = element_blank(),
axis.text = element_text(size = 12, color = "black"), axis.title = element_text(size = 16,
color = "black"), legend.text = element_text(size = 10), legend.title = element_text(size = 12))
righting_contour
With these performance surfaces, we can see the mapping of the shell phenotypes (PC scores) to performance on each of the three functions. Note that hotter colors actually indicate lower performance because they reflect higher stress, higher drag, and longer self-righting time. We can also confirm that these surfaces are the same as in Stayton (2019).
We will now to estimate the combined performance landscape for terrestrial and aquatic species. To do this, we will first re-scale performance to range from 0 (minimum performance) to 1 (maximum performance), as described in Polly et al. (2016). We will also subtract from 1, such that greater values indicate better performance (hotter colors) in the combined landscapes (Stayton 2019). Then, we will use the average weights for stress, drag, and self-righting ability calculated for aquatic and terrestrial species, as reported in Stayton (2019).
str(stayton_data)
## 'data.frame': 255 obs. of 5 variables:
## $ PC1 : num -0.15 -0.15 -0.15 -0.15 -0.15 -0.15 -0.15 -0.15 -0.15 -0.15 ...
## $ PC2 : num -0.15 -0.125 -0.1 -0.075 -0.05 -0.025 0 0.025 0.05 0.075 ...
## $ stress : num 276 255 239 223 209 ...
## $ drag : num 0.198 0.183 0.18 0.183 0.192 ...
## $ self.righting: num 3.68 3.63 3.52 3.41 3.3 ...
perf_rescaled <- as.data.frame(apply(stayton_data[, 3:5], 2, function(x) 1 -
((x - min(x))/(max(x) - min(x)))))
head(perf_rescaled)
## stress drag self.righting
## 1 0.5580185 0.9822614 0.00000000
## 2 0.6252570 0.9967528 0.01971672
## 3 0.6762400 1.0000000 0.06289232
## 4 0.7296325 0.9969660 0.10538387
## 5 0.7724235 0.9884069 0.15181877
## 6 0.8222501 0.9757864 0.20179462
str(perf_rescaled)
## 'data.frame': 255 obs. of 3 variables:
## $ stress : num 0.558 0.625 0.676 0.73 0.772 ...
## $ drag : num 0.982 0.997 1 0.997 0.988 ...
## $ self.righting: num 0 0.0197 0.0629 0.1054 0.1518 ...
perf_aquatic <- (0.47 * perf_rescaled$stress) + (0.38 * perf_rescaled$drag) +
(0.15 * perf_rescaled$self.righting)
combined_aquatic <- cbind(stayton_data[, 1:2], perf_aquatic)
perf_terrestrial <- (0.67 * perf_rescaled$stress) + (0.18 * perf_rescaled$drag) +
(0.16 * perf_rescaled$self.righting)
# Combine datasets
all_perf <- cbind(stayton_data[, 1:2], perf_aquatic, perf_terrestrial) %>%
as_tibble() %>%
pivot_longer(cols = c(perf_aquatic, perf_terrestrial), names_to = "type",
values_to = "Performance") %>%
separate(col = "type", into = c(NA, "type"), sep = "_") %>%
mutate(type = factor(type), type = fct_recode(type, `A. Combined aquatic landscape` = "aquatic",
`B. Combined terrestrial landscape` = "terrestrial"))
all_perf
## # A tibble: 510 × 4
## PC1 PC2 type Performance
## <dbl> <dbl> <fct> <dbl>
## 1 -0.15 -0.15 A. Combined aquatic landscape 0.636
## 2 -0.15 -0.15 B. Combined terrestrial landscape 0.551
## 3 -0.15 -0.125 A. Combined aquatic landscape 0.676
## 4 -0.15 -0.125 B. Combined terrestrial landscape 0.601
## 5 -0.15 -0.1 A. Combined aquatic landscape 0.707
## 6 -0.15 -0.1 B. Combined terrestrial landscape 0.643
## 7 -0.15 -0.075 A. Combined aquatic landscape 0.738
## 8 -0.15 -0.075 B. Combined terrestrial landscape 0.685
## 9 -0.15 -0.05 A. Combined aquatic landscape 0.761
## 10 -0.15 -0.05 B. Combined terrestrial landscape 0.720
## # … with 500 more rows
# Color breaks
min_perf <- round(min(all_perf$Performance), 2)
max_perf <- 1
mybreaks <- seq.int(min_perf, max_perf, length.out = 12)
# Plot together with facetting to put on same color scale
all_perf %>%
ggplot(aes(PC1, PC2, z = Performance)) + geom_contour_filled(breaks = mybreaks) +
coord_equal() + theme_bw() + guides(fill = guide_legend(title = "Performance")) +
theme(panel.grid.major = element_blank(), panel.grid.minor = element_blank(),
axis.text = element_text(size = 14, color = "black"), axis.title = element_text(size = 16,
color = "black"), legend.text = element_text(size = 8), legend.title = element_text(size = 10),
strip.text = element_text(size = 12, hjust = 0), strip.background = element_blank()) +
facet_wrap(~type)
With the combined landscapes, we can locate the performance peaks, that is, the PC scores that correspond to the highest performance. We will also calculate a region of high performance that is up to 5% lower than peak performance, to later compare with the 95% confidence intervals of inferred primary adaptive peaks using OU modeling (Table A1, available online).
combined_aquatic <- cbind(stayton_data[, 1:2], perf_aquatic)
colnames(combined_aquatic) <- c("PC1", "PC2", "perf_aquatic")
max_comb_aquatic <- combined_aquatic[combined_aquatic$perf_aquatic == max(combined_aquatic$perf_aquatic),
]
max_comb_aquatic #aquatic performance peak
## PC1 PC2 perf_aquatic
## 82 -0.025 0 0.8619485
max_comb_aquatic$perf_aquatic * 0.95
## [1] 0.8188511
# region of 5% lower performance
summary(combined_aquatic[which(combined_aquatic$perf_aquatic > 0.818), ])
## PC1 PC2 perf_aquatic
## Min. :-0.12500 Min. :-0.02500 Min. :0.8181
## 1st Qu.:-0.07500 1st Qu.: 0.00000 1st Qu.:0.8294
## Median :-0.05000 Median : 0.02500 Median :0.8418
## Mean :-0.05403 Mean : 0.03468 Mean :0.8404
## 3rd Qu.:-0.02500 3rd Qu.: 0.06250 3rd Qu.:0.8513
## Max. : 0.02500 Max. : 0.12500 Max. :0.8619
combined_terrestrial <- cbind(stayton_data[, 1:2], perf_terrestrial)
colnames(combined_terrestrial) <- c("PC1", "PC2", "perf_terrestrial")
max_comb_terrestrial <- combined_terrestrial[combined_terrestrial$perf_terrestrial ==
max(combined_terrestrial$perf_terrestrial), ]
max_comb_terrestrial #terrestrial performance peak
## PC1 PC2 perf_terrestrial
## 173 0.125 0.025 0.9047935
max_comb_terrestrial$perf_terrestrial * 0.95
## [1] 0.8595538
# region of 5% lower performance
summary(combined_terrestrial[which(combined_terrestrial$perf_terrestrial > 0.859),
])
## PC1 PC2 perf_terrestrial
## Min. :-0.05000 Min. :-0.02500 Min. :0.8591
## 1st Qu.: 0.00000 1st Qu.: 0.00000 1st Qu.:0.8669
## Median : 0.07500 Median : 0.02500 Median :0.8739
## Mean : 0.06223 Mean : 0.03723 Mean :0.8754
## 3rd Qu.: 0.12500 3rd Qu.: 0.07500 3rd Qu.:0.8835
## Max. : 0.17500 Max. : 0.10000 Max. :0.9048
We can see that the regions of 5% lower performance from the peak are quite large. They range from -0.125 to 0.025 for PC1 and -0.025 to 0.125 for PC2 in the aquatic combined landscape, and from -0.05 to 0.175 for PC1 and -0.025 to 0.100 for PC2 in the terrestrial combined landscape.
We will begin with the 53 three-dimensional landmarks placed on the shells of 274 species of turtles, provided by Stayton (2019). We will then perform a General Procrustes Analysis to project each species onto the PCs that reflect variation in shell shape. These PC axes are the same as the ones used to estimate performance landscapes. Note that we downloaded the original data from Dryad but corrected some misspellings in the names of some species.
turtle_data <- read.csv("turtle_data.csv", row.names = 1)
length(levels(as.factor(turtle_data$Name)))
## [1] 274
# Changing the data frame to an array
turtle_array <- arrayspecs(turtle_data[, 8:166], 53, 3, sep = "")
length(turtle_data$Name)
## [1] 2722
dimnames(turtle_array)[[3]] <- turtle_data$Museum.Number
# had to put individuals names to get specimens' scores
head(turtle_array[, , 1])
## X Y Z
## 1 -72.66066 111.0733 -48.95750
## 2 -75.59137 133.1744 -53.16478
## 3 -86.27445 151.4542 -54.64143
## 4 -98.03491 167.3384 -61.81512
## 5 -109.82220 177.7144 -69.63888
## 6 -129.79511 185.4073 -72.50737
# 53 landmarks (x, y, z) for the first specimen
# Align all specimens with GPA
proc_turtle <- gpagen(turtle_array)
##
## Performing GPA
##
|
| | 0%
|
|================== | 25%
|
|=================================== | 50%
|
|==================================================== | 75%
|
|======================================================================| 100%
##
## Making projections... Finished!
# shape variables
# Perform a PCA analysis and get PCs scores of all specimens
dimnames(proc_turtle$coords)[[3]] <- turtle_data$Museum.Number
proc_turtle_pca <- gm.prcomp(proc_turtle$coords)
plot(proc_turtle_pca, main = "PC scores for all specimens of turtles")
From the plot above, we can confirm that the PCs we obtained are the same as the ones reported in Stayton (2019), because they explain the same amount of variance in shape.
We will now calculate the average scores for each species, then use the consensus phylogeny from McLaughlin and Stayton (2016) to keep only the 237 species that we analyzed because they are present in this phylogeny. To do this, we will have to match the shape data with the tip labels in the phylogeny.
# Getting average scores per species
proc_turtle_pca$x <- as.data.frame(proc_turtle_pca$x)
specimens <- turtle_data$Name
proc_turtle_pca$x$specimens <- specimens
length(proc_turtle_pca$x$specimens)
## [1] 2722
turtle_mean_pc <- aggregate(proc_turtle_pca$x[, 1:2], by = list(proc_turtle_pca$x$specimens),
mean)
head(turtle_mean_pc)
## Group.1 Comp1 Comp2
## 1 Acanthochelys_macrocephala 0.02881497 0.010182069
## 2 Acanthochelys_pallidipectoris 0.06379932 -0.030418939
## 3 Acanthochelys_radiata 0.03855873 0.001652305
## 4 Acanthochelys_spixii 0.03111754 -0.006863926
## 5 Aldabrachelys_gigantea -0.08764407 0.041687006
## 6 Astrochelys_radiata -0.14901220 0.059577794
turtle_tree <- read.tree("turtle_tree.tre")
turtle_tree #using consensus tree already pruned to have the 237 species
##
## Phylogenetic tree with 237 tips and 236 internal nodes.
##
## Tip labels:
## Caretta_caretta, Lepidochelys_kempii, Lepidochelys_olivacea, Eretmochelys_imbricata, Chelonia_mydas, Natator_depressus, ...
##
## Rooted; includes branch lengths.
## Need to match species data with species phylogeny
which(is.na(match(levels(as.factor(turtle_data$Name)), turtle_tree$tip.label)))
## [1] 15 17 18 19 20 21 23 24 25 26 27 28 29 36 37 65 66 67 86
## [20] 95 97 112 148 164 165 166 167 168 169 174 197 203 204 227 260 271 273
# species of turtle data that are not in the phylogeny (n=37)
# Now exclude the extra species to have n=237
species <- turtle_tree$tip.label
# Adding species names again now that some names in the data have been
# changed
specimens <- turtle_data$Name
proc_turtle_pca$x$specimens <- specimens
length(proc_turtle_pca$x$specimens)
## [1] 2722
turtle_mean_pc <- aggregate(proc_turtle_pca$x[, 1:2], by = list(proc_turtle_pca$x$specimens),
mean)
str(turtle_mean_pc) #274 species
## 'data.frame': 274 obs. of 3 variables:
## $ Group.1: chr "Acanthochelys_macrocephala" "Acanthochelys_pallidipectoris" "Acanthochelys_radiata" "Acanthochelys_spixii" ...
## $ Comp1 : num 0.0288 0.0638 0.0386 0.0311 -0.0876 ...
## $ Comp2 : num 0.01018 -0.03042 0.00165 -0.00686 0.04169 ...
# Removing extra specimens
proc_turtle_pca$x <- proc_turtle_pca$x[-c(which(is.na(match(proc_turtle_pca$x$specimens,
species)))), ]
# calculating means again using only specimens that correspond to the 237
# species we will analyze:
turtle_mean_pc.1 <- aggregate(proc_turtle_pca$x[, 1:2], by = list(proc_turtle_pca$x$specimens),
mean)
length(levels(as.factor(proc_turtle_pca$x$specimens))) #237
## [1] 237
## Multiplying all pc1 scores by -1.Signs flipped when comparing with
## Stayton (2019)
turtle_mean_pc.1$Comp1 = turtle_mean_pc.1$Comp1 * -1
# Include habitat info
turtle_data_clean <- turtle_data[-c(which(is.na(match(turtle_data$Name, species)))),
]
habitat <- (aggregate(turtle_data_clean$Habitat, by = list(turtle_data_clean$Name,
turtle_data_clean$Habitat), function(x) x))[, 1:2]
# it is alphabetic order per habitat: change to alphabetic order to match
# species
habitat.1 <- as.data.frame(habitat[order(habitat$Group.1), ])
# substitute 'A' and 'T' by colors blue and brown
habitat.1[c(which(habitat.1$Group.2 == "A")), 2] = "blue"
habitat.1[c(which(habitat.1$Group.2 == "T")), 2] = "brown"
plot(turtle_mean_pc.1$Comp1, turtle_mean_pc.1$Comp2, pch = 21, bg = habitat.1$Group.2,
xlab = "PC1 (21.9%)", ylab = "PC2 (10.6%)")
legend("topright", pch = 21, pt.bg = unique(habitat.1$Group.2), legend = c("aquatic",
"terrestrial"), box.lty = 0)
The plot above shows the average scores for the 237 species we will analyze, separated into aquatic and terrestrial habitats.
Now we will create a data frame with all the information we need to reconstruct ancestral states of habitat, to then use them in the OU modeling using the R package ouch. From now on, we will run analyses using several functions created in previous work by one of us. These functions are provided in the ‘R_functions_PBZ.R’ script as supplementary material. The phylogeny with the reconstructed ancestral states is presented in Fig. A1 of the online appendix.
# Joining habitat info with PC scores
habitat.1 <- as.data.frame(habitat[order(habitat$Group.1), ])
turtle_dat <- cbind(habitat.1, turtle_mean_pc.1[, 2:3])
# putting in the same order as the phylogeny
turtle_dat <- turtle_dat[match(turtle_tree$tip.label, turtle_dat$Group.1), ]
colnames(turtle_dat) <- c("Species", "Habitat", "PC1", "PC2")
# Reconstructing ancestral states with ace
head(turtle_dat)
## Species Habitat PC1 PC2
## 11 Caretta_caretta A -0.06998729 -0.0006996088
## 78 Lepidochelys_kempii A -0.07129763 -0.0191977281
## 79 Lepidochelys_olivacea A -0.03459191 0.0095317479
## 41 Eretmochelys_imbricata A -0.07389981 0.0102543461
## 13 Chelonia_mydas A -0.05316871 -0.0234931753
## 100 Natator_depressus A -0.07356504 -0.0633769270
states_ou <- setNames(turtle_dat$Habitat, turtle_dat$Species)
# Put the data in the same order as the phylogeny
states_ou <- states_ou[turtle_tree$tip.label]
aic_ER <- AIC(ace(states_ou, turtle_tree, type = "discrete", model = "ER"))
aic_ER
## [1] 120.9997
aic_ARD <- AIC(ace(states_ou, turtle_tree, type = "discrete", model = "ARD"))
aic_ARD
## [1] 122.9325
# Calculating AIC weights
source("R_functions_PBZ.R")
aic_list <- list(ER = aic_ER, ARD = aic_ARD)
aicc_wgt_calc(unlist(aic_list))
## ER ARD
## 0.7244039 0.2755961
# function to calculate AICc weights
anc_states <- ace(states_ou, turtle_tree, type = "discrete", model = "ER")
anc_states$rates
## [1] 0.005006582
head(anc_states$lik.anc)
## A T
## 238 0.7737137 0.2262863052
## 239 0.8900989 0.1099010564
## 240 0.8902528 0.1097471985
## 241 0.9942198 0.0057802000
## 242 0.9941277 0.0058723000
## 243 0.9997557 0.0002442976
# use this as internal states
# vector of ancestral states (get states with higher support for each
# node)
turtle_tree$Nnode
## [1] 236
x = anc_states$lik.anc[, 1] > 0.51 #higher support for A (TRUE)
length(anc_states$lik.anc[, 1][x]) #178 nodes aquatic
## [1] 178
y = anc_states$lik.anc[, 2] > 0.51 #higher support for T (TRUE)
length(anc_states$lik.anc[, 2][y]) #58 nodes terrestrial
## [1] 58
head(anc_states$lik.anc[, 1][x])
## 238 239 240 241 242 243
## 0.7737137 0.8900989 0.8902528 0.9942198 0.9941277 0.9997557
# names are nodes and numbers are support for A replace support for letter
# A
str(anc_states$lik.anc[, 1][x])
## Named num [1:178] 0.774 0.89 0.89 0.994 0.994 ...
## - attr(*, "names")= chr [1:178] "238" "239" "240" "241" ...
as.numeric(names(anc_states$lik.anc[, 1][x]))
## [1] 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
## [19] 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
## [37] 274 275 276 277 278 279 280 285 286 287 288 289 290 291 292 293 294 295
## [55] 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
## [73] 314 315 316 318 319 320 321 322 323 324 325 326 327 328 329 335 336 337
## [91] 381 382 383 384 385 386 388 393 394 395 396 397 398 399 400 401 402 403
## [109] 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421
## [127] 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
## [145] 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457
## [163] 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473
nodes.1 = c(rep("A", 178))
nodes.2 = c(rep("T", 58))
nodes_states <- c(nodes.1, nodes.2)
nodes_names <- c(as.numeric(names(anc_states$lik.anc[, 1][x])), as.numeric(names(anc_states$lik.anc[,
2][y])))
node_states <- nodes_states[order(nodes_names)]
## Internal states in the phylogeny
turtle_tree$node.label = node_states
turtle_tree
##
## Phylogenetic tree with 237 tips and 236 internal nodes.
##
## Tip labels:
## Caretta_caretta, Lepidochelys_kempii, Lepidochelys_olivacea, Eretmochelys_imbricata, Chelonia_mydas, Natator_depressus, ...
## Node labels:
## A, A, A, A, A, A, ...
##
## Rooted; includes branch lengths.
We can now see that the object turtle_tree has node
labels (A for aquatic, and T for terrestrial).
The next step is to set up the data frame to be used in ouch, so that we can compare different multivariate models: Brownian motion, single optimum Ornstein-Uhlenbeck (OU), and 2-peak OU (aquatic and terrestrial).
## Set up data for ouch
source("R_functions_PBZ.R")
turtle_ouch_OU2 <- data.frame(turtle_dat[, 2:4])
rownames(turtle_ouch_OU2) <- turtle_dat$Species
# Now we add the phylogeny and internal-state estimates
head(rownames(turtle_ouch_OU2) == turtle_tree$tip.label) #true
## [1] TRUE TRUE TRUE TRUE TRUE TRUE
turtle_ouch_OU2 <- divers_to_ouch(turtle_tree, turtle_tree$node.label, turtle_ouch_OU2) #function to construct the data frame to use in 'ouch' analyses
## Loading required package: geiger
max(node.depth.edgelength(turtle_tree)) #length of phylogeny = 227.77 mya
## [1] 227.7755
# Plotting the internal states
ouch_tree <- with(turtle_ouch_OU2, ouchtree(nodes, ancestors, times, labels))
plot(ouch_tree, regimes = turtle_ouch_OU2["model"], palette = c("blue", "brown"),
margin = 0.15, labels = TRUE)
# different colors represent different selective regimes
# Set ouchtree for all analyses
ouch_tree <- with(turtle_ouch_OU2, ouchtree(nodes, ancestors, times, labels))
## OUCH analysis: comparing multivariate models (a) Brownian motion for
## PC1 and PC2 scores at once
brown_ouch_turtle <- brown(turtle_ouch_OU2[c("PC1", "PC2")], ouch_tree)
# (b) Single-optimum OU
set.seed(10) #setting seed so that analysis can be reproduced
turtle_ouch_OU2$global <- as.factor("global") # Set single regime for OU1
OU1_ouch_turtle <- hansen(turtle_ouch_OU2[c("PC1", "PC2")], ouch_tree, regimes = turtle_ouch_OU2["global"],
sqrt.alpha = c(1, 0, 1), sigma = c(1, 0, 1), maxit = 1000)
# (c) Two optima OU2: changed sigma values to closer to 0 to converge
# better
OU2_ouch_turtle <- hansen(turtle_ouch_OU2[c("PC1", "PC2")], ouch_tree, regimes = turtle_ouch_OU2["model"],
sqrt.alpha = c(2, 0, 2), sigma = c(0.001, 0, 0.001), maxit = 1000)
# Put output objects into a list to more easily summarize results
all_out <- list(brown_ouch_turtle, OU1_ouch_turtle, OU2_ouch_turtle)
names(all_out) <- c("BM", "OU1", "OU2")
# Summarize model fits
ouch_sum <- map(all_out, summary)
turtle_fits <- tibble(model = names(all_out), k = map_dbl(ouch_sum, ~.$dof),
loglik = map_dbl(ouch_sum, ~.$loglik), AICc = map_dbl(ouch_sum, ~.$aic.c),
wgt = aicc_wgt_calc(AICc))
turtle_fits
## # A tibble: 3 × 5
## model k loglik AICc wgt
## <chr> <dbl> <dbl> <dbl> <dbl>
## 1 BM 5 1120. -2229. 3.54e-12
## 2 OU1 8 1139. -2262. 3.97e- 5
## 3 OU2 10 1151. -2282. 1.00e+ 0
From the table above, we can conclude that the most-supported model is the 2-peak OU model, describing two divergent optima for aquatic and terrestrial turtles. We can now move forward and apply the merged framework to the turtle shell phenotypes.
We will first calculate 95% confidence intervals for the OU model parameters we just inferred for aquatic and terrestrial species. We can do this by performing a parametric bootstrap analysis using the ouch package.
# adaptive peaks for aquatic and terrestrial species
ouch_sum$OU2$optima
## $PC1
## A T
## -0.04126470 0.07602188
##
## $PC2
## A T
## 0.008458624 -0.013919114
boot_OU2 <- bootstrap(OU2_ouch_turtle, nboot = 3)
# increase nboot=1000 to get similar results as the paper (Table A1),
# minus slight differences due to the random aspects of simulation
boot_CI <- t(sapply(boot_OU2, quantile, probs = c(0.025, 0.975)))
head(boot_CI)
## 2.5% 97.5%
## alpha1 3.656151957 4.915617509
## alpha2 -0.597278119 0.936948869
## alpha3 -0.597278119 0.936948869
## alpha4 4.573345791 5.618456248
## sigma.squared1 0.013013418 0.016545763
## sigma.squared2 0.006863161 0.007460083
Now we can compare the locations of performance peaks (point estimate and region of 5% lower highest performance) and the 95% CI of adaptive peaks. This comparison is shown in Table A1 when running 1,000 bootstrap iterations and in Figure 2 of our paper.
# 95% CI of aquatic adaptive peak
boot_CI[c(9, 11), ]
## 2.5% 97.5%
## optima.PC1.A -0.053574777 -0.050095505
## optima.PC2.A 0.003521865 0.005573743
# 95% CI of terrestrial adaptive peak
boot_CI[c(10, 12), ]
## 2.5% 97.5%
## optima.PC1.T 0.03276136 0.067258335
## optima.PC2.T -0.06689918 -0.007268892
# point estimates of performance peaks
max_comb_aquatic
## PC1 PC2 perf_aquatic
## 82 -0.025 0 0.8619485
max_comb_terrestrial
## PC1 PC2 perf_terrestrial
## 173 0.125 0.025 0.9047935
# min and max PC scores of the region with 5% lower performance
summary(combined_aquatic[which(combined_aquatic$perf_aquatic > 0.818), ])
## PC1 PC2 perf_aquatic
## Min. :-0.12500 Min. :-0.02500 Min. :0.8181
## 1st Qu.:-0.07500 1st Qu.: 0.00000 1st Qu.:0.8294
## Median :-0.05000 Median : 0.02500 Median :0.8418
## Mean :-0.05403 Mean : 0.03468 Mean :0.8404
## 3rd Qu.:-0.02500 3rd Qu.: 0.06250 3rd Qu.:0.8513
## Max. : 0.02500 Max. : 0.12500 Max. :0.8619
summary(combined_terrestrial[which(combined_terrestrial$perf_terrestrial > 0.859),
])
## PC1 PC2 perf_terrestrial
## Min. :-0.05000 Min. :-0.02500 Min. :0.8591
## 1st Qu.: 0.00000 1st Qu.: 0.00000 1st Qu.:0.8669
## Median : 0.07500 Median : 0.02500 Median :0.8739
## Mean : 0.06223 Mean : 0.03723 Mean :0.8754
## 3rd Qu.: 0.12500 3rd Qu.: 0.07500 3rd Qu.:0.8835
## Max. : 0.17500 Max. : 0.10000 Max. :0.9048
With the 1,000 iterations, we would see that the point estimate for the aquatic performance peak matches the 95% CI of the adaptive peak, but this is not the case for the terrestrial performance peak. However, when considering the region of 5% lower performance from the peak, the performance and adaptive peaks do overlap for terrestrial species.
The next step in the merged framework is to calculate the distances of species’ phenotypes to the inferred adaptive peaks. We will first reproduce Figure 3A here to plot all species’ phenotypes and the aquatic and terrestrial adaptive peaks.
Fig.3A <- ggplot(turtle_dat, aes(x = PC1, y = PC2, fill = Habitat, shape = Habitat)) +
geom_point(size = 2, color = "black") + scale_shape_manual(values = c(21,
24)) + scale_fill_manual(values = c("gray70", "black")) + scale_x_continuous(name = "PC1 (21.9%)",
breaks = seq(-0.1, 0.15, 0.05)) + scale_y_continuous(name = "PC2 (10.6%)",
breaks = seq(-0.1, 0.15, 0.05)) + coord_fixed() + theme_bw() + theme(text = element_text(size = 13,
color = "black"), panel.grid.major = element_blank(), panel.grid.minor = element_blank(),
axis.text = element_text(size = 12, color = "black"), axis.title = element_text(size = 16,
color = "black"), legend.position = c(0.9, 0.85), legend.text = element_text(size = 10),
legend.title = element_text(size = 12), legend.background = element_rect(fill = "white",
color = "black"))
# Adding the adaptive peaks
PC1 <- c(-0.041, 0.076)
PC2 <- c(0.008, -0.0139)
Habitat <- c("A", "T", "A", "T")
opt_df <- data.frame(PC1, PC2, Habitat)
Fig.3A_final <- Fig.3A + geom_point(data = opt_df, aes(x = PC1, y = PC2, fill = Habitat),
shape = 21, size = 6)
Fig.3A_final
From the plot above, we can see that several species’ phenotypes are far from the adaptive peak of their current environment. We can also see that some terrestrial species are closer to the aquatic adaptive peak than to their own peak. This suggests that some terrestrial lineages have only recently changed from an aquatic ancestral environment to the terrestrial environment (as can be seen in Fig. A2)
We can now test for the effects of historical factors versus species-specific factors on phenotypic variation. We expect a high proportion of variation explained by history for lineages that changed environments more recently (i.e., terrestrial lineages).
We will perform this test separately for aquatic and terrestrial species, first including the family Testudinidae and then excluding this family. Results of these tests are shown in Table 2 of our paper.
source("R_functions_PBZ.R")
turtle_optima <- t(as.data.frame(ouch_sum$OU2$optima))
turtle_optima
## A T
## PC1 -0.041264703 0.07602188
## PC2 0.008458624 -0.01391911
# Separating aquatic and terrestrial lineages aquatic:
aquatic_dat <- turtle_dat[turtle_dat$Habitat == "A", 2:4]
aquatic_opt <- as.data.frame(turtle_optima[, 1])
colnames(aquatic_opt) <- "A"
hist_test_A <- history.test(aquatic_dat, aquatic_opt, c("PC1", "PC2"))
# function to run the test of historical vs species-specific factors
hist_test_A
## hist.var specific.var total.var prop.hist prop.specific
## PC1 2.519244e-04 0.0009823335 0.001234258 0.20411003 0.7958900
## PC2 5.840659e-05 0.0019059855 0.001964392 0.02973265 0.9702673
## Total 3.103310e-04 0.0028883191 0.003198650 0.09701937 0.9029806
# terrestrial
terrestrial_dat <- turtle_dat[turtle_dat$Habitat == "T", 2:4]
terrestrial_opt <- as.data.frame(turtle_optima[, 2])
colnames(terrestrial_opt) <- "T"
hist_test_T <- history.test(terrestrial_dat, terrestrial_opt, c("PC1", "PC2"))
hist_test_T
## hist.var specific.var total.var prop.hist prop.specific
## PC1 -9.253154e-06 0.002798929 0.002789676 -0.003316928 1.0033169
## PC2 1.567038e-04 0.000878833 0.001035537 0.151326170 0.8486738
## Total 1.474507e-04 0.003677762 0.003825213 0.038547048 0.9614530
# terrestrial excluding Testudinidae
turtle_dat.1 <- turtle_dat[-c(115, 99, 100, 109, 105, 106, 110, 111, 104, 107,
108, 137:140, 102, 103, 114, 124, 129:131, 116:123, 136, 141, 142, 125:127,
112, 113, 101, 128, 132:135), ]
terrestrial_dat.1 <- turtle_dat.1[turtle_dat.1$Habitat == "T", 2:4] #n=24
hist_test_T.1 <- history.test(terrestrial_dat.1, terrestrial_opt, c("PC1", "PC2"))
hist_test_T.1
## hist.var specific.var total.var prop.hist prop.specific
## PC1 3.901390e-03 0.0007217576 0.0046231475 0.84388177 0.1561182
## PC2 -3.696362e-05 0.0009263081 0.0008893445 -0.04156277 1.0415628
## Total 3.864426e-03 0.0016480658 0.0055124920 0.70103072 0.2989693
From the results above, we can see that the proportion of phenotypic variation explained by history is only high for terrestrial species when removing Testudinidae. This is because Testudinidae lineages have spent more time in their current environment compared with other terrestrial lineages.
For aquatic lineages, much more phenotypic variation is explained by species-specific factors, such as other (untested) ecological factors and genetic constraints.
We can now specifically test if lineages that have spent less time in their current environment show a higher deviation from their adaptive peak. To do this, we will perform a phylogenetic generalized least-squares (PGLS) regression of distance from optimum on time spent in current environment.
## Make node list (i.e., nodes where changes in microhabitat occurred;
## determined in advance) Note that these are class 'phylo' node orders,
## NOT 'ouch' order Jump intermediary nodes and associate descendants
## arising from the node that changed Node 274 that originates
## Testudnidae: use Aldabrachelys, Astrochelys, Centrochelys, Chelonoidis,
## Chersina, Geochelone, Gopherus, Homopus, Indotestudo, Kinixys,
## Malachocersus, Manouria, Psammobates, Pyxis, Stigmochelys, Testudo
nodes <- c(286, 280, 298, 320, 329, 327, 315, 334, 275, 274, 388, 390, 384,
450) #13 changes of environment
name.list <- list("Cuora_mouhotii", c("Cuora_mccordi", "Cuora_picturata", "Cuora_bourreti",
"Cuora_galbinifrons", "Cuora_flavomarginata"), "Vijayachelys_silvatica",
"Leucocephalon_yuwonoi", "Heosemys_depressa", "Heosemys_spinosa", c("Geoemyda_japonica",
"Geoemyda_spengleri"), c("Rhinoclemmys_funerea", "Rhinoclemmys_melanosterna",
"Rhinoclemmys_punctularia", "Rhinoclemmys_diademata"), c("Rhinoclemmys_areolata",
"Rhinoclemmys_nasuta", "Rhinoclemmys_rubida", "Rhinoclemmys_annulata",
"Rhinoclemmys_pulcherrima"), c("Aldabrachelys", "Astrochelys", "Centrochelys",
"Chelonoidis", "Chersina", "Geochelone", "Gopherus", "Homopus", "Indotestudo",
"Kinixys", "Malacochersus", "Manouria", "Psammobates", "Pyxis", "Stigmochelys",
"Testudo"), c("Terrapene_mexicana", "Terrapene_carolina", "Terrapene_ornata",
"Terrapene_nelsoni"), "Terrapene_coahuila", c("Glyptemys_muhlenbergii",
"Glyptemys_insculpta"), "Platemys_platycephala")
# Get habitat states for all species
habitat <- turtle_dat$Habitat
names(habitat) <- turtle_dat$Species
age.mat <- as.data.frame(matrix(nrow = length(turtle_tree$tip.label), ncol = 2))
rownames(age.mat) <- turtle_dat$Species
age.mat[, 2] <- "A"
# just to start filling in the habitats
for (i in seq(nodes)) {
foo <- age.regime(x = nodes[i], tr = turtle_tree, name = name.list[[i]],
dat = habitat, check.regimes = FALSE)
age.mat[rownames(foo), ] <- foo
}
age.mat[, 2] <- as.factor(age.mat[, 2])
all(age.mat[, 2] == habitat) #TRUE
## [1] TRUE
levels(as.factor(age.mat$V1))
## [1] "6.970332804" "8.028824792" "8.178711937" "8.905407712" "9.3549541895"
## [6] "12.373138447" "14.088181072" "14.11416946" "14.52109394" "18.73936313"
## [11] "19.15355694" "22.164540147" "36.97324032" "59.902911403"
# 14 different time lengths for aquatic lineages that switched to
# terrestrial or secondary transitions from terrestrial back to aquatic
## Now calculating the multivariate distance from optima
turtle_optima <- as.data.frame(ouch_sum$OU2$optima)
turtle_optima
## PC1 PC2
## A -0.04126470 0.008458624
## T 0.07602188 -0.013919114
# use it like this for arg optima
turtle_data <- turtle_dat[, 2:4]
# Use it
distances <- dist.from.opt(dat = turtle_data, model = "Habitat", optima = turtle_optima,
cont.char = paste0("PC", 1:2))
# function to calculate species' phenotypes distances
head(distances)
## 11 78 79 41 13 100
## 0.030147312 0.040827083 0.006758528 0.032684473 0.034097257 0.078763303
# Append to previous matrix
age.mat$dist <- distances
age.mat <- age.mat[, c(1, 3, 2)]
colnames(age.mat) <- c("time", "dist", "habitat")
# Remove all aquatic taxa that have a history of 100% water(they have 'NA'
# for time values)
age.mat <- age.mat[complete.cases(age.mat), ]
red.tr <- drop.tip(turtle_tree, name.check(turtle_tree, age.mat)$tree_not_data)
age.mat <- age.mat[red.tr$tip.label, ]
# rownames(age.mat) == red.tr$tip.label #TRUE
## Using BM with phylolm package
head(age.mat)
## time dist habitat
## Cuora_mccordi 9.354954 0.04186560 T
## Cuora_picturata 9.354954 0.03213078 T
## Cuora_bourreti 9.354954 0.03249231 T
## Cuora_galbinifrons 9.354954 0.02695018 T
## Cuora_flavomarginata 9.354954 0.04786849 T
## Cuora_mouhotii 6.970333 0.04793729 T
log.dat <- cbind(time = log(age.mat[, 1]), age.mat[, 2:3])
# Need to use absolute distances
head(log.dat$dist)
## [1] 0.04186560 0.03213078 0.03249231 0.02695018 0.04786849 0.04793729
# data frame must have row names
phy.log.lm <- summary(phylolm(dist ~ time, log.dat, red.tr, model = "BM"))
phy.log.lm
##
## Call:
## phylolm(formula = dist ~ time, data = log.dat, phy = red.tr,
## model = "BM")
##
## AIC logLik
## -331.6 168.8
##
## Raw residuals:
## Min 1Q Median 3Q Max
## -0.08071 -0.06128 -0.04230 -0.01770 0.04783
##
## Mean tip height: 227.7755
## Parameter estimate(s) using ML:
## sigma2: 3.462345e-05
##
## Coefficients:
## Estimate StdErr t.value p.value
## (Intercept) 0.0997085 0.0686145 1.4532 0.1506
## time -0.0019733 0.0107082 -0.1843 0.8543
##
## R-squared: 0.0004781 Adjusted R-squared: -0.0136
# Same test excluding Testudinidae
# Excluding Testudinidae: only include rows that have time < 59
age.mat.1 <- age.mat[age.mat$time < 59, ] #n=29
log.dat.1 <- cbind(time = log(age.mat.1[, 1]), age.mat.1[, 2:3])
phy.log.lm.1 <- summary(phylolm(dist ~ time, log.dat.1, red.tr, model = "BM"))
## Warning in phylolm(dist ~ time, log.dat.1, red.tr, model = "BM"): will drop from
## the tree 44 taxa with missing data
phy.log.lm.1
##
## Call:
## phylolm(formula = dist ~ time, data = log.dat.1, phy = red.tr,
## model = "BM")
##
## AIC logLik
## -145.53 75.76
##
## Raw residuals:
## Min 1Q Median 3Q Max
## -0.07433 -0.05341 -0.03218 -0.01612 0.03370
##
## Mean tip height: 227.7755
## Parameter estimate(s) using ML:
## sigma2: 1.766727e-05
##
## Coefficients:
## Estimate StdErr t.value p.value
## (Intercept) 0.0965134 0.0501587 1.9242 0.06493 .
## time 0.0021319 0.0084593 0.2520 0.80293
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## R-squared: 0.002347 Adjusted R-squared: -0.0346
We can see from the results above that variation in distance from the optimum is not explained by time spent in current environment, even when excluding Testudinidae. These results are shown in Table A2 for multivariate distances.
We can plot these results as shown in Figure 3B of the paper.
# plot in log scale but use original time as labels in axis
colnames(age.mat)[3] <- "Habitat"
Fig.3B <- ggplot(data = age.mat, aes(x = time, y = dist, fill = Habitat, shape = Habitat,
size = Habitat)) + geom_point(color = "black") + scale_size_manual(values = c(4,
3)) + scale_shape_manual(values = c(21, 24)) + scale_x_continuous(trans = "log",
breaks = c(7, 14, 28, 56)) + scale_fill_manual(values = c("gray70", "black")) +
labs(x = "Time in current environment (mya)", y = "Multivariate distance to optimum") +
ylim(0, 0.15) + theme_bw() + theme(text = element_text(size = 15), panel.grid.major = element_blank(),
panel.grid.minor = element_blank(), axis.text = element_text(size = 12,
color = "black"), axis.title = element_text(size = 16, color = "black"),
legend.position = "none")
Fig.3B
From all the analyses performed, we can conclude that performance and adaptive peaks match for turtle shells, indicating that the three functions measured (stress, drag, and self-righting ability) are relevant for fitness. Moreover, stress has a higher weight for terrestrial species and the same is true for drag for aquatic species, as one might expect. We can also infer that most species’ phenotypes deviate from inferred adaptive peaks, but probably because of constraints that are not generally related to an influence of past adaptation to ancestral environments.
McLaughlin C.J. and C.T. Stayton. 2016. Convergent evolution provides evidence of similar radiations in shell shape in the turtle families Emydidae and Geoemydidae. Herpetologica 72:120–129.
Polly P.D., C.T. Stayton, E.R. Dumont, S.E. Pierce, E.J. Rayfield, and K.D. Angielczyk. 2016. Combining geometric morphometrics and finite element analysis with evolutionary modeling: towards a synthesis. J. Vertebr. Paleontol. 36:e1111225.
Stayton C.T. 2019. Performance in three shell functions predicts the phenotypic distribution of hard‐shelled turtles. Evolution 73:720–734.
Stayton C.T., L.F. O’Connor and N.M. Nisivoccia. 2018. The influence of multiple functional demands on morphological diversification: a test on turtle shells. Evolution 72:1933–1949.