3

BACKGROUND

This question is a bit complex so I will first introduce the background. To generate an example of species birth-death table (the L table) I would suggest to use dd_sim() function from DDD package.

library(DDD)
library(tidyverse)
library(picante)

result <- dd_sim(c(0.2, 0.1, 20), 10) 
# with birth rate 0.2, death rate 0.1, carrying capacity 20 and overall 10 million years.
L <- result$L

L

            [,1] [,2] [,3]       [,4]
 [1,] 10.0000000    0   -1 -1.0000000
 [2,] 10.0000000   -1    2  2.7058965
 [3,]  8.5908774    2    3  6.6301616
 [4,]  8.4786474    3    4  3.3866813
 [5,]  8.4455262   -1   -5 -1.0000000
 [6,]  8.3431071    4    6  3.5624756
 [7,]  5.3784683    2    7  0.6975934
 [8,]  3.8950593    6    8 -1.0000000
 [9,]  1.5032100   -5   -9 -1.0000000
[10,]  0.8393589    7   10 -1.0000000
[11,]  0.6118985   -5  -11 -1.0000000

The L table has 4 columns:

  1. the first column is the time at which a species is born in Mya
  2. the second column is the label of the parent of the species; positive and negative values indicate whether the species belongs to the left or right crown lineage
  3. the third column is the label of the daughter species itself; positive and negative values indicate whether the species belongs to the left or right crown lineage
  4. the fourth column is the time of extinction of the species; if the fourth element equals -1, then the species is still extant.

WHAT I DID

With this L table, I now have a extant community. I want to calculate its phylogenetic diversity (also called Faith's index)

Using DDD and picante functions I can do it:

# convert L table into community data matrix
comm = as.data.frame(L) %>% dplyr::select(V3:V4) %>%
dplyr::rename(id = V3, pa = V4) %>%
dplyr::mutate(id = paste0("t",abs(id))) %>%
dplyr::mutate(pa = dplyr::if_else(pa == -1, 1, 0)) %>%
dplyr::mutate(plot = 0) %>%
dplyr::select(plot, pa, id) %>%
picante::sample2matrix()

# convert L table into phylogeny
phy = DDD::L2phylo(L, dropextinct = T)

# calculate Faith's index using pd() function
Faith = picante::pd(comm,phy)

PROBLEM

Although I achieved my goal, the procedure seems to be redundant and time-consuming. I have to convert my original L table back and forth because I have to use existing functions.

By definition Faith's index is basically the sum of the total phylogenetic branch length of the community, so my question is:

Is it possible to calculate Faith's index directly from the L table?

Thank you advance!

Tianjian Qin
  • 525
  • 4
  • 14
  • Do you mean `Faith <- sum(L[,1]-pmax(0,L[,4]))`? Or perhaps `Faith <- sum(L[L[,4]<0,1])`? – Andrew Gustar Nov 14 '19 at 10:01
  • @AndrewGustar Hi Andrew, I tried both but none of them equals Faith in my code, what could be the problem? – Tianjian Qin Nov 14 '19 at 10:05
  • I've just had a closer look and I think the answer should be `sum(L[L[, 4] < 0, 1]) + sum(pmax(0, L[, 4]))` – Andrew Gustar Nov 14 '19 at 10:46
  • Actually that doesn't always work either - different random `L`s work for different versions of the formula - not sure why. Maybe best to stick with the long way! – Andrew Gustar Nov 14 '19 at 10:49
  • @AndrewGustar I thought for the whole afternoon but my code became more and more complicated, looking for an efficient way is not that easy – Tianjian Qin Nov 14 '19 at 16:23

2 Answers2

3

You can simply use the phy$edge.length component of the phylo object generated by DDD::L2phylo:

## Measuring the sum of the branch lengths from `phy`
sum_br_length <- sum(phy$edge.length)
sum_br_length == Faith$PD
# [1] TRUE

## Measuring the sum of the branch length from `L`
sum_br_length <- sum(DDD::L2phylo(L, dropextinct = TRUE)$edge.length)
sum_br_length == Faith$PD
# [1] TRUE

And some micro-benchmarking for fun:

library(microbenchmark)
## Function 1
fun1 <- function(L) {
    comm = as.data.frame(L) %>% dplyr::select(V3:V4) %>%
    dplyr::rename(id = V3, pa = V4) %>%
    dplyr::mutate(id = paste0("t",abs(id))) %>%
    dplyr::mutate(pa = dplyr::if_else(pa == -1, 1, 0)) %>%
    dplyr::mutate(plot = 0) %>%
    dplyr::select(plot, pa, id) %>%
    picante::sample2matrix()

    # convert L table into phylogeny
    phy = DDD::L2phylo(L, dropextinct = T)

    # calculate Faith's index using pd() function
    Faith = picante::pd(comm,phy)
    return(Faith$PD)
}
## Function 2
fun2 <- function(L) {
    phy <- DDD::L2phylo(L, dropextinct = T)
    return(sum(phy$edge.length))
}
## Function 3
fun3 <- function(L) {
    return(sum(DDD::L2phylo(L, dropextinct = TRUE)$edge.length))
}

## Do all of them give the same results
fun1(L) == Faith$PD
# [1] TRUE
fun2(L) == Faith$PD
# [1] TRUE
fun3(L) == Faith$PD
# [1] TRUE

## Which function fastest?
microbenchmark(fun1(L), fun2(L), fun3(L))
# Unit: milliseconds
#     expr      min       lq     mean   median       uq       max neval
#  fun1(L) 6.486462 6.900641 8.273386 7.445334 8.667535 16.888429   100
#  fun2(L) 1.627854 1.683204 2.215531 1.771219 2.229408  9.522366   100
#  fun3(L) 1.630635 1.663181 2.229206 1.859733 2.448196  7.573001   100
Thomas Guillerme
  • 1,747
  • 4
  • 16
  • 23
1

I examined pd::sample2matrix to see what it does internally. The tapply call and the following line look to be the only necessary pieces.

library(DDD)
library(tidyverse)
library(picante)
#> Loading required package: ape
#> Loading required package: vegan
#> Loading required package: permute
#> Loading required package: lattice
#> This is vegan 2.5-6
#> Loading required package: nlme
#> 
#> Attaching package: 'nlme'
#> The following object is masked from 'package:dplyr':
#> 
#>     collapse
set.seed(100)
result <- dd_sim(c(0.2, 0.1, 20), 10) 
# with birth rate 0.2, death rate 0.1, carrying capacity 20 and overall 10 million years.
L <- result$L

# convert L table into community data matrix
comm_original = as.data.frame(L) %>% dplyr::select(V3:V4) %>%
  dplyr::rename(id = V3, pa = V4) %>%
  dplyr::mutate(id = paste0("t",abs(id))) %>%
  dplyr::mutate(pa = dplyr::if_else(pa == -1, 1, 0)) %>%
  dplyr::mutate(plot = 0) %>%
  dplyr::select(plot, pa, id) %>%
  picante::sample2matrix()


# Instead of using dplyr, we'll do some base R operations
# on L. The code doesn't look as nice, but it should be
# significantly faster.
pa <- ifelse(L[, 4] == -1, 1, 0)
plot <- rep(0, length(pa))
id <- paste0("t", abs(L[,3]))
comm_new <- tapply(pa, list(plot, id), sum)
comm_new[is.na(comm_new)] <- 0
# convert L table into phylogeny
phy = DDD::L2phylo(L, dropextinct = T)

# calculate Faith's index using pd() function
picante::pd(comm_original,phy)
#>         PD SR
#> 0 29.82483  6

picante::pd(comm_new, phy)
#>         PD SR
#> 0 29.82483  6
Created on 2019-11-17 by the reprex package (v0.3.0)

Edit: original() is the way you originally constructed the comm, new() is the way given above. It looks like you can expect a 2x speedup if you swap this in. I know that's not a huge gain depending on the size of the workload, but better than nothing.

expression      min   median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time result               memory          time    gc            
  <bch:expr> <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm> <list>               <list>          <list>  <list>        
1 original()   9.76ms  10.24ms      96.1     552KB     2.04    47     1      489ms <df[,1125] [1 x 1,1~ <df[,3] [107 x~ <bch:t~ <tibble [48 x~
2 new()        4.57ms   4.84ms     201.      464KB     2.07    97     1      483ms <dbl[,1125] [1 x 1,~ <df[,3] [63 x ~ <bch:t~ <tibble [98 x~
smingerson
  • 1,368
  • 9
  • 12
  • Thank you for the answer, your work-around is a nice idea. But what I am really interested in is that how to directly derive PD from the L table, do we really need to reconstruct the phylogeny before calculating the branch lengths? I tried and discussed with others but have no answer till now. – Tianjian Qin Nov 17 '19 at 20:19
  • Based on the code of `picante::pd()` and `DDD::L2phylo`, I would say this is as good as it gets without some major tinkering on your part. I've edited my answer to demonstrate the amount of speed-up you can expect to see over original solution. – smingerson Nov 17 '19 at 20:41