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 (Vélez-Zuazo and Agnarsson 2011).

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
## [1] 4009

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
## [1] TRUE

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)
))
##  [1] "Lower Miocene"  "Pleistocene"    "Pliocene"       "Upper Miocene" 
##  [5] "Miocene"        "Oligocene"      "Pliocene?"      "Cretaceous"    
##  [9] "Chattian"       "Paleocene"      "Maastrichtian"  "Middle Miocene"
unique(unlist(
  lapply(specimens, function(x)
    x$gatheringEvent$chronoStratigraphy[[1]]$oldChronoName)
))
##  [1] "Lower Miocene"                                    
##  [2] "Mioceen"                                          
##  [3] "Middle Miocene"                                   
##  [4] "M.mioceen"                                        
##  [5] "Upper Miocene"                                    
##  [6] "Miocene"                                          
##  [7] "Recent"                                           
##  [8] "Unindentified Age"                                
##  [9] "Pliocene"                                         
## [10] "Oligocene"                                        
## [11] "Unspecified age"                                  
## [12] "Rupelian"                                         
## [13] "Middle Eocene"                                    
## [14] "Ypresian"                                         
## [15] "Oligoceen, Rupelien"                              
## [16] "Kwartaire afzetting op zanden van Grimmertingen"  
## [17] "Lower Rupelian"                                   
## [18] "Pleistocene"                                      
## [19] "Eocene"                                           
## [20] "Cretaceous"                                       
## [21] "Neogene"                                          
## [22] "Priabonian"                                       
## [23] "A. karsteni - Niso sp. Ass. Zone"                 
## [24] "Middle Pliocene"                                  
## [25] "Middle Miocen"                                    
## [26] "? Kwartaire afzetting op zanden van Grimmertingen"
## [27] "Paleocene"                                        
## [28] "Cenomanian"                                       
## [29] "Maastrichtian"                                    
## [30] "Mioceen?"                                         
## [31] "Early Miocene"                                    
## [32] "Unspecified Age"                                  
## [33] "Recent?"                                          
## [34] "Plio-Pleistocene?"                                
## [35] "Plio-Pleistoceen"                                 
## [36] "Unindentified A"                                  
## [37] "B.Mioceen"                                        
## [38] "Onder-Plioceen"                                   
## [39] "Lower Miocene?"                                   
## [40] "Pliocene?"                                        
## [41] "Onderste Rupelien"                                
## [42] "Oligoceen"                                        
## [43] "M.Miocen"                                         
## [44] "Tertiair"                                         
## [45] "Upper Eocene"

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)))
## [1] 1

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)
## [1] 3
## 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"))
## 
## AnimalPart      tooth   vertebra 
##          2       4003          1

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
## NULL
## get the lower and upper bounds for this division
geo_age(name)
## data frame with 0 columns and 2 rows

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)
  )))
## Warning in FUN(X[[i]], ...): Could not retrieve time values for geo unit
## "Mioceen" from earthlifeconsortium.org, returning NA
## Warning in FUN(X[[i]], ...): Could not retrieve time values for geo unit
## "M.mioceen" from earthlifeconsortium.org, returning NA
## Warning in FUN(X[[i]], ...): Could not retrieve time values for geo unit
## "Recent" from earthlifeconsortium.org, returning NA
## Warning in FUN(X[[i]], ...): Could not retrieve time values for geo unit
## "Unindentified Age" from earthlifeconsortium.org, returning NA
## 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
##             genus specificEpithet youngChronoName     oldChronoName young_age
## 1          Isurus        benedeni            <NA>              <NA>        NA
## 2          Isurus        benedeni   Lower Miocene     Lower Miocene     15.97
## 3     Carcharodon       megalodon            <NA>           Mioceen        NA
## 4       Notidanus     primigenius            <NA>    Middle Miocene        NA
## 5      Galeocerdo         aduncus            <NA>    Middle Miocene        NA
## 6      Galeocerdo         aduncus            <NA>    Middle Miocene        NA
## 7       Notidanus     primigenius            <NA>    Middle Miocene        NA
## 8       Notidanus             sp.            <NA>           Mioceen        NA
## 9       Hypoprion      acanthodon            <NA>    Middle Miocene        NA
## 10       Squatina      sp. indet.            <NA>    Middle Miocene        NA
## 11     Odontaspis      acutissima            <NA>         M.mioceen        NA
## 12         Isurus        hastalis            <NA>     Upper Miocene        NA
## 13 Cosmopolitodus         escheri            <NA>           Miocene        NA
## 14     Cetorhinus         maximus            <NA>           Miocene        NA
## 15     Carcharias             sp.            <NA>           Miocene        NA
## 16       Squatina        squatina            <NA>            Recent        NA
## 17   Scyliorhinus       stellaris            <NA>            Recent        NA
## 18  Centroscymnus      coelolepis            <NA>            Recent        NA
## 19   Carcharhinus       elongatus     Pleistocene           Miocene      0.01
## 20    Galeorhinus      recticonus            <NA>           Mioceen        NA
## 21     Odontaspis      acutissima            <NA>     Upper Miocene        NA
## 22         Isurus        hastalis            <NA> Unindentified Age        NA
## 23      Notidanus      sp. indet.            <NA> Unindentified Age        NA
## 24      Notidanus      sp. indet.            <NA>          Pliocene        NA
## 25 Cosmopolitodus        hastalis        Pliocene              <NA>      2.59
## 26        Squalus       alsaticus            <NA>         Oligocene        NA
## 27     Odontaspis      acutissima            <NA>    Middle Miocene        NA
## 28     Odontaspis      acutissima            <NA>    Middle Miocene        NA
## 29     Odontaspis      acutissima            <NA>    Middle Miocene        NA
## 30     Odontaspis      acutissima            <NA>    Middle Miocene        NA
## 31     Odontaspis      acutissima            <NA>    Middle Miocene        NA
## 32     Odontaspis      acutissima            <NA>           Miocene        NA
## 33     Odontaspis      acutissima            <NA>    Middle Miocene        NA
## 34     Odontaspis      acutissima            <NA>    Middle Miocene        NA
## 35     Odontaspis      acutissima            <NA>    Middle Miocene        NA
## 36     Odontaspis           vorax            <NA>           Miocene        NA
## 37     Odontaspis           vorax            <NA>    Middle Miocene        NA
## 38     Odontaspis           vorax            <NA>    Middle Miocene        NA
## 39     Odontaspis      acutissima            <NA>    Middle Miocene        NA
## 40     Odontaspis      acutissima            <NA>    Middle Miocene        NA
## 41     Odontaspis      sp. indet.            <NA>    Middle Miocene        NA
## 42          Lamna         cattica            <NA>    Middle Miocene        NA
## 43          Lamna         cattica            <NA>    Middle Miocene        NA
## 44      Alopecias          exigua            <NA>    Middle Miocene        NA
## 45         Isurus      sp. indet.            <NA>    Middle Miocene        NA
## 46     Cetorhinus          parvus            <NA>    Middle Miocene        NA
## 47     Cetorhinus          parvus            <NA>    Middle Miocene        NA
## 48     Cetorhinus          parvus            <NA>    Middle Miocene        NA
## 49     Cetorhinus         maximus            <NA>    Middle Miocene        NA
## 50   Carcharhinus      acanthodon            <NA>    Middle Miocene        NA
##    old_age
## 1       NA
## 2    23.03
## 3       NA
## 4    15.97
## 5    15.97
## 6    15.97
## 7    15.97
## 8       NA
## 9    15.97
## 10   15.97
## 11      NA
## 12   11.61
## 13   23.03
## 14   23.03
## 15   23.03
## 16      NA
## 17      NA
## 18      NA
## 19   23.03
## 20      NA
## 21   11.61
## 22      NA
## 23      NA
## 24    5.33
## 25      NA
## 26   33.90
## 27   15.97
## 28   15.97
## 29   15.97
## 30   15.97
## 31   15.97
## 32   23.03
## 33   15.97
## 34   15.97
## 35   15.97
## 36   23.03
## 37   15.97
## 38   15.97
## 39   15.97
## 40   15.97
## 41   15.97
## 42   15.97
## 43   15.97
## 44   15.97
## 45   15.97
## 46   15.97
## 47   15.97
## 48   15.97
## 49   15.97
## 50   15.97

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
##    node   age.min  age.max soft.bounds         taxon
## 1   432  2.635714 21.35846       FALSE  Carcharhinus
## 2   364  0.870000 25.12714       FALSE        Galeus
## 3   311 66.000000 83.20000       FALSE  Heterodontus
## 4   243  5.444545 21.84214       FALSE     Hexanchus
## 5   320  4.745833 16.46489       FALSE        Isurus
## 6   318 28.984615 49.32840       FALSE         Lamna
## 7   245  3.503333 26.13571       FALSE Pristiophorus
## 8   346  3.960000 30.67263       FALSE  Scyliorhinus
## 9   428  0.010000 20.20600       FALSE       Sphyrna
## 10  297  5.708182 30.13364       FALSE       Squalus
## 11  237 16.384000 30.11038       FALSE      Squatina

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)
## 
## Setting initial dates...
## Fitting in progress... get a first set of estimates
##          (Penalised) log-lik = -57.81392 
## Optimising rates... dates... -57.81392 
## Optimising rates... dates... -57.81392 
## Optimising rates... dates... -57.81391 
## Optimising rates... dates... -57.8139 
## Optimising rates... dates... -57.81389 
## Optimising rates... dates... -57.81389 
## Optimising rates... dates... -57.81388 
## Optimising rates... dates... -57.81387 
## Optimising rates... dates... -57.81387 
## Optimising rates... dates... -57.81386 
## Optimising rates... dates... -57.81386 
## Optimising rates... dates... -57.81385 
## Optimising rates... dates... -57.81385 
## Optimising rates... dates... -57.81384 
## Optimising rates... dates... -57.81384 
## Optimising rates... dates... -57.81383 
## Optimising rates... dates... -57.81383 
## Optimising rates... dates... -57.81382 
## Optimising rates... dates... -57.81382 
## Optimising rates... dates... -57.81381
## Warning: Maximum number of dual iterations reached.
## 
## log-Lik = -57.81105 
## PHIIC = 1503.62
## 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

Vélez-Zuazo, Ximena, and Ingi Agnarsson. 2011. “Shark Tales: A Molecular Species-Level Phylogeny of Sharks (Selachimorpha, Chondrichthyes).” Molecular Phylogenetics and Evolution 58 (2): 207–17. https://doi.org/https://doi.org/10.1016/j.ympev.2010.11.018.