Background

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.

The phylogeny

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.

Getting chronological data with nbaR

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

Exploring the data

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.

Getting absolute ages

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

Calibrating the phylogeny

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)

Genus level

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)

Family level

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)

References



naturalis/nbaR documentation built on Nov. 12, 2023, 4:47 p.m.