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:

In total, there are eight extant shark orders.

We can then formulate a query condition for specimens that are identified in one of these orders using the operator IN:

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:

Now we can do the query:

## [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:

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

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.

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:

## [1] 3

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

## [1] "Neogene"
##           Neogene
## early_age   23.03
## late_age     2.59

We can now make a table with all interesting data, below this is done for the first 50 specimen objects:

## 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
## "Unspecified 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
## 1     Ginglymostoma             sp.            <NA>         Neogene
## 2  Megascyliorhinus      sp. indet.            <NA>  Middle Miocene
## 3      Heterodontus            <NA>            <NA>            <NA>
## 4      Heterodontus            <NA>            <NA>            <NA>
## 5      Eostegostoma         angusta            <NA>   Middle Eocene
## 6   Palaeorhincodon           wardi            <NA>   Middle Eocene
## 7   Palaeorhincodon           wardi            <NA>   Middle Eocene
## 8     Pristiophorus             sp.            <NA>         Mioceen
## 9     Pristiophorus     rupeliensis            <NA>       Oligocene
## 10    Pristiophorus     rupeliensis            <NA>       Oligocene
## 11    Pristiophorus      schroederi         Miocene         Miocene
## 12    Chiloscyllium         minutum            <NA>            <NA>
## 13        Notidanus     primigenius            <NA>   Lower Miocene
## 14        Hexanchus     primigenius         Miocene         Miocene
## 15     Notorhynchus     primigenius            <NA>         Mioceen
## 16    Pristiophorus      schroederi        Pliocene         Miocene
## 17     Heterodontus            <NA>            <NA>            <NA>
## 18     Heterodontus        vincenti            <NA>   Middle Eocene
## 19     Heterodontus        vincenti            <NA>   Middle Eocene
## 20     Heterodontus            <NA>            <NA>            <NA>
## 21     Heterodontus            <NA>            <NA>            <NA>
## 22    Pristiophorus      schroederi        Pliocene            <NA>
## 23    Pristiophorus      schroederi        Pliocene         Miocene
## 24    Pristiophorus             sp.            <NA>         Mioceen
## 25   Pristiophorus?            <NA>         Miocene         Miocene
## 26    Pristiophorus     rupeliensis            <NA>       Oligocene
## 27   Pristiophorus?      schroederi        Pliocene         Miocene
## 28    Pristiophorus      schroederi        Pliocene         Miocene
## 29    Pristiophorus      schroederi        Pliocene         Miocene
## 30    Pristiophorus     rupeliensis            <NA>       Oligocene
## 31    Pristiophorus     rupeliensis            <NA>        Rupelian
## 32    Pristiophorus     rupeliensis            <NA>       Oligocene
## 33    Pristiophorus     rupeliensis            <NA>       Oligocene
## 34    Pristiophorus     rupeliensis            <NA>       Oligocene
## 35    Pristiophorus             sp.            <NA>         Mioceen
## 36    Pristiophorus      schroederi        Pliocene         Miocene
## 37     Heterodontus            <NA>            <NA>            <NA>
## 38     Heterodontus            <NA>            <NA>            <NA>
## 39     Heterodontus            <NA>            <NA>            <NA>
## 40    Pristiophorus             sp.            <NA>         Miocene
## 41    Pristiophorus     rupeliensis            <NA>       Oligocene
## 42    Pristiophorus             sp.            <NA>         Mioceen
## 43     Heterodontus            <NA>            <NA>            <NA>
## 44     Heterodontus            <NA>            <NA>            <NA>
## 45     Heterodontus            <NA>            <NA>            <NA>
## 46         Squatina           prima            <NA> Unspecified age
## 47         Squatina         biforis            <NA>  Middle Miocene
## 48         Squatina             sp.        Pliocene            <NA>
## 49         Squatina         biforis            <NA>  Middle Miocene
## 50         Squatina         biforis            <NA>  Middle Miocene
##    young_age old_age
## 1         NA   23.03
## 2         NA   15.97
## 3         NA      NA
## 4         NA      NA
## 5         NA   48.60
## 6         NA   48.60
## 7         NA   48.60
## 8         NA      NA
## 9         NA   33.90
## 10        NA   33.90
## 11      5.33   23.03
## 12        NA      NA
## 13        NA   23.03
## 14      5.33   23.03
## 15        NA      NA
## 16      2.59   23.03
## 17        NA      NA
## 18        NA   48.60
## 19        NA   48.60
## 20        NA      NA
## 21        NA      NA
## 22      2.59      NA
## 23      2.59   23.03
## 24        NA      NA
## 25      5.33   23.03
## 26        NA   33.90
## 27      2.59   23.03
## 28      2.59   23.03
## 29      2.59   23.03
## 30        NA   33.90
## 31        NA   33.90
## 32        NA   33.90
## 33        NA   33.90
## 34        NA   33.90
## 35        NA      NA
## 36      2.59   23.03
## 37        NA      NA
## 38        NA      NA
## 39        NA      NA
## 40        NA   23.03
## 41        NA   33.90
## 42        NA      NA
## 43        NA      NA
## 44        NA      NA
## 45        NA      NA
## 46        NA      NA
## 47        NA   15.97
## 48      2.59      NA
## 49        NA   15.97
## 50        NA   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.

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:

##    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:

## 
## Setting initial dates...
## Fitting in progress... get a first set of estimates
##          Penalised log-lik = -57.8126 
## Optimising rates... dates... -57.8126 
## 
## Done.
## 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.