The time-calibration of molecular phylogenies is essential to many phylogenetic comparative analyses. Time-calibration can be accomplished with e.g. data from fossil specimen that can be assigned to a certain taxonomic group. Specimen ages can be determined by e.g. carbon dating or stratigraphic methods. Node ages are usually assigned using Bayesian or maximum-likelihood methods.
Some of the specimen records at Naturalis hold stratigraphic information.
Here we will demonstrate how to extract this data using nbaR
and
how to create input for the popular tree-calibration function chronos
from the phylogenetic analysis
package ape
.
As input, we will use a species-level molecular shark phylogeny published by [@VELEZZUAZO2011207].
The phylogeny is a majority-rule consensus tree inferred from molecular markers using Bayesian inference and comprises 229 species in all eight orders of the sharks (superorder Selachimorpha):
The non-ultrametric tree in the above figure is not time-calibrated, the branch lengths thus represent molecular distances.
Chronological association is associated with Specimen
data, we thus instantiate
a SpecimenClient
:
library(nbaR) sc <- SpecimenClient$new()
In total, there are eight extant shark orders.
shark_orders <- c( "Carcharhiniformes", "Heterodontiformes", "Hexanchiformes", "Lamniformes", "Orectolobiformes", "Pristiophoriformes", "Squaliformes", "Squatiniformes" )
We can then formulate a query condition for specimens that are identified in one of these orders
using the operator IN
:
qc <- QueryCondition$new(field = "identifications.defaultClassification.order", operator = "IN", value = shark_orders)
For many specimens, chronological information is available in the fields gatheringEvent.chronoStratigraphy.youngChronoName
and gatheringEvent.chronoStratigraphy.oldChronoName
which represent upper- and lower time bounds, respectively.
Below, we will formulate a QueryCondition
that requires either one of these fields to be non-empty:
## formulate query conditions for fields to be non-empty qc2 <- QueryCondition$new(field = "gatheringEvent.chronoStratigraphy.youngChronoName", operator = "NOT_EQUALS", value = "") qc3 <- QueryCondition$new(field = "gatheringEvent.chronoStratigraphy.oldChronoName", operator = "NOT_EQUALS", value = "") ## join qc2 and qc3 with operator OR qc2$or <- list(qc3)
Now we can do the query:
## instantiate QuerySpec, give size qs <- QuerySpec$new(conditions = list(qc, qc3), size = 5000) res <- sc$query(querySpec = qs) ## how many hits? res$content$totalSize
Note: By default, a query returns only data for the first 10 hits.
Above, we set the size
parameter to the QuerySpec constructor to allow the download of
up to 5000 values This number was based on the prior knowloedge that there are less
than 5000 records for our query; We advise to always check the length of the resultSet
against the totalSize
of a QueryResult
to make sure that all records are downloaded,
e.g.
length(res$content$resultSet) == res$content$totalSize
Now we can explore the data:
## load all specimens specimens <- lapply(res$content$resultSet, function(x) x$item) ## check the fields youngCronoName and oldChronoName unique(unlist( lapply(specimens, function(x) x$gatheringEvent$chronoStratigraphy[[1]]$youngChronoName) )) unique(unlist( lapply(specimens, function(x) x$gatheringEvent$chronoStratigraphy[[1]]$oldChronoName) ))
Caution: As you see above, there can be more than one chronoStratigraphy assigned with a specimen. Check how many we have per specimen:
unique(sapply(res$content$resultSet, function(x) length(x$item$gatheringEvent$chronoStratigraphy)))
Each gatheringEvent
in each Specimen
has only one chronoStratigraphy
. We thus do not miss data by taking
the first one (chronoStratigraphy[[1]]
).
Do we have hits for all orders? Below, we make an overview of the field identifications.defaultClassification.order
for all specimen.
## make table with counts for each order tab <- table(unlist( lapply(specimens, function(x) x$identifications[[1]]$defaultClassification$order) )) par(mar = c(5.1, 8, 4.1, 2.1)) barplot(sort(tab), horiz = TRUE, las = 2, xlab = "Number of specimens")
Caution: There can be multiple identifications for one specimen. Some specimens
in our data are for instance assigned to different genera. However,
the field preferred
in an Identification
object states if this is the most accurate and
recent identification. The preferred identification is usually the first in the list of a specimen's identifications.
It can, however, occur that there are multiple identifications of which none is preferred; it is best to filter them out:
## get the specimens which have a preferred identification ident <- sapply(specimens, function(s) any(sapply(s$identifications, function(i) i$preferred))) ## how many do not have a preferred identification? sum(!ident) ## filter specimens specimens <- specimens[ident]
Let's further explore the data. To see what part of the animals were preserved, we can look into
the field kindOfUnit
:
table(sapply(specimens, `[[`, "kindOfUnit"))
Almost all of them are teeth.
The type of chronostratigraphic data (as shown above) is given in divisions on the geological time scale, e.g. Miocene.
These strings can refer to eons, eras, periods, epochs and ages. In order to
translate this into absolute ages, we will use the API of the Earth Life Consortium.
This is possible using the convenience function geo_age
. For example
## get the upper chrono name for the first specimen name <- specimens[[1]]$gatheringEvent$chronoStratigraphy[[1]]$oldChronoName name ## get the lower and upper bounds for this division geo_age(name)
We can now make a table with all interesting data, below this is done for the first 50 specimen objects:
## Get genus, species and chrono information from specimen records data <- as.data.frame(do.call(rbind, lapply(specimens[1:50], function(x) { genus <- x$identifications[[1]]$defaultClassification$genus if (is.null(genus)) genus <- NA specificEpithet <- x$identifications[[1]]$defaultClassification$specificEpithet if (is.null(specificEpithet)) specificEpithet <- NA youngChronoName <- x$gatheringEvent$chronoStratigraphy[[1]]$youngChronoName if (is.null(youngChronoName)) youngChronoName <- NA oldChronoName <- x$gatheringEvent$chronoStratigraphy[[1]]$oldChronoName if (is.null(oldChronoName)) oldChronoName <- NA c( genus = genus, specificEpithet = specificEpithet, youngChronoName = youngChronoName, oldChronoName = oldChronoName ) }))) ## Get absolute ages from earth life consortium times <- geo_age(unique(c( as.character(data$youngChronoName), as.character(data$oldChronoName) ))) ## add upper and lower bounds to ages data$young_age <- sapply(data$youngChronoName, function(x) ifelse(is.na(x), NA, unlist(times["late_age", as.character(x)]))) data$old_age <- sapply(data$oldChronoName, function(x) ifelse(is.na(x), NA, unlist(times["early_age", as.character(x)]))) data
Depending on the data, calibration points could be chosen on different taxonomic levels.
If there are sufficient specimens determined at species level, one could use the upper- and
lower bounds above. It is also possible to average upper- and lower values for a higher taxonomic
group, such as genus or family. Since this requires some data cleaning, such as dealing with duplicates,
missing data, etc,
nbaR
offers the function chronos_calib
which takes a set of specimen object, and a tree and
averages the data for a user-defined taxonomic
group and returns a calibration table that can be directly used as input for the function chronos
from the package ape
. The function also determines the node which will be calibrated in the phylogenetic tree.
The shark phylogeny comes with the package and can be
parsed using the package ape
:
library(ape) ## read data data(shark_tree) ## plot the tree plot(shark_tree, cex = 0.3)
Now we use chronos_calib
to get the table at the genus level. chronos_calib
also selects the nodes in the tree that will be calibrated. This is done by
selecting the most recent common ancestor (mrca) of the species for which the data are averaged.
## make calibration table on genus level calibration_table <- chronos_calib(specimens, shark_tree, "genus")
Note: This function can produce many warnings, a warning is emitted whenever, for some geological division, no data can be obtained from the earth life consortium. This can be due to misspellings or words in foreign languages.
The calibration table looks as follows:
calibration_table
Note: It is essential to thoroughly evaluate the calibration table! In this example, for example, the genus Squatina is non-monophyletic and one species is placed somewhere else in the tree. Calibrating the most recent common ancestor node for this genus can therefore result in errors using the calibration routine. We will therefore remove the calibration points for genus Squatina before calibration:
## clean up: one rogue taxon in genus "Squatina"! Skip this genus calibration_table <- calibration_table[calibration_table$taxon != "Squatina",] ## run ape's chronos chronogram <- chronos(shark_tree, calibration = calibration_table) ## plot tree with time axis plot(chronogram, cex = 0.3) axisPhylo() ## plot calibrated genera nodelabels(calibration_table$taxon, calibration_table$node)
We can also calibrate the tree on the family level:
## make calibration table on family level calibration_table <- chronos_calib(specimens, shark_tree, "family") ## run ape's chronos chronogram <- chronos(shark_tree, calibration = calibration_table) ## plot tree with time axis plot(chronogram, cex = 0.3) axisPhylo() ## plot calibrated families nodelabels(calibration_table$taxon, calibration_table$node)
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.