Roteiro prático — A árvore e os dados

Aula 1 · Métodos Filogenéticos Comparativos

Este roteiro roda de verdade: os resultados abaixo saíram do código mostrado. Copie os blocos para um script, na pasta com os dados da aula, e rode em ordem numa sessão limpa do R. No fim, o desafio pede o mesmo raciocínio com outro conjunto de dados.

0. Pacotes e o básico do R

library(ape)
library(phytools)
library(geiger)

Os objetos que mais vamos usar: vetores com nomes (um valor por espécie), tabelas (data.frame) e matrizes.

x <- c(sp1 = 2.51, sp2 = 2.80, sp3 = 2.23)
x["sp2"]
sp2 
2.8 
names(x)
[1] "sp1" "sp2" "sp3"
d <- data.frame(sp = names(x), A = x)
d[d$A > 2.3, ]
     sp    A
sp1 sp1 2.51
sp2 sp2 2.80

1. O objeto phylo

Uma árvore no R é uma lista da classe phylo. Vamos ler a árvore-guia do curso a partir do texto Newick.

guia <- read.tree(text = "(((sp1:0.18,sp2:0.18):1.06,(sp3:0.02,sp4:0.02):1.22):0.20,(sp5:1.37,(sp6:0.23,sp7:0.23):1.14):0.07);")
guia

Phylogenetic tree with 7 tips and 6 internal nodes.

Tip labels:
  sp1, sp2, sp3, sp4, sp5, sp6, ...

Rooted; includes branch length(s).
str(guia)
List of 4
 $ edge       : int [1:12, 1:2] 8 9 10 10 9 11 11 8 12 12 ...
 $ edge.length: num [1:12] 0.2 1.06 0.18 0.18 1.22 0.02 0.02 0.07 1.37 1.14 ...
 $ Nnode      : int 6
 $ tip.label  : chr [1:7] "sp1" "sp2" "sp3" "sp4" ...
 - attr(*, "class")= chr "phylo"
 - attr(*, "order")= chr "cladewise"

As peças: edge (cada linha é um ramo, do nó pai ao filho), tip.label (as pontas são os números 1 a n), Nnode e edge.length, na mesma ordem de edge. Os nós internos são numerados de n + 1 em diante, e a raiz é n + 1.

cbind(guia$edge, comprimento = guia$edge.length)
            comprimento
 [1,]  8  9        0.20
 [2,]  9 10        1.06
 [3,] 10  1        0.18
 [4,] 10  2        0.18
 [5,]  9 11        1.22
 [6,] 11  3        0.02
 [7,] 11  4        0.02
 [8,]  8 12        0.07
 [9,] 12  5        1.37
[10,] 12 13        1.14
[11,] 13  6        0.23
[12,] 13  7        0.23
plot(guia, label.offset = .03)
nodelabels(frame = "circle", bg = "white", cex = .8)
edgelabels(round(guia$edge.length, 2), frame = "none", adj = c(.5, -.4), cex = .75)

2. Escrevendo uma árvore: Newick passo a passo

Os mesmos cinco passos dos slides. Rode um por vez e compare o texto com o desenho.

passos <- c("(A,B);", "(A:2.75,B:3.76);", "((A:2.75,B:3.76):2.0,C:2.6);",
            "((A:2.75,B:3.76)N1:2.0,C:2.6);", "((A:2.75,B:3.76)N1:2.0,C:2.6)N2;")
t5 <- read.tree(text = passos[5])
t5$node.label
[1] "N2" "N1"
plot(t5, show.node.label = TRUE, cex = 1.3, x.lim = c(0, 7))

E o caminho inverso, da árvore para o texto:

write.tree(t5)
[1] "((A:2.75,B:3.76)N1:2,C:2.6)N2;"

3. Lendo arquivos: Nexus e Newick

barb <- read.nexus("BarbetTree.nex")   # Nexus (salvo pelo Mesquite)
cent <- read.tree("Centrarchidae.tre") # Newick
barb

Phylogenetic tree with 42 tips and 41 internal nodes.

Tip labels:
  M_asiatica_asiatica, M_lineata_hodgsoni, M_australis_australis, M_haemacephala_intermedia, M_eximia, M_viridis, ...

Rooted; includes branch length(s).
cent

Phylogenetic tree with 28 tips and 27 internal nodes.

Tip labels:
  Acantharchus_pomotis, Lepomis_gibbosus, Lepomis_microlophus, Lepomis_punctatus, Lepomis_miniatus, Lepomis_auritus, ...

Rooted; includes branch length(s).

Uma árvore sem comprimentos de ramo (só a topologia) e a mesma árvore datada, dos anuros de Gomes:

gomes     <- read.tree("Gomes_phylo.txt")
gomes_dat <- read.tree("Gomes_tree_datada.txt")
is.null(gomes$edge.length)      # TRUE: só a topologia
[1] TRUE
is.ultrametric(gomes_dat)
[1] FALSE

O is.ultrametric() usa uma tolerância numérica: arredondamentos no arquivo podem fazer uma árvore datada “falhar”. Compare a maior e a menor distância raiz–ponta:

alturas <- node.depth.edgelength(gomes_dat)[1:Ntip(gomes_dat)]
range(alturas)
[1] 154.1 154.1
diff(range(alturas))   # diferença de arredondamento, não de biologia
[1] 5e-06
is.ultrametric(gomes_dat, tol = 1e-4)
[1] TRUE
plot(gomes_dat, cex = .7); axisPhylo()

4. Enraizamento, politomias e comprimentos

is.rooted(barb); is.binary(barb); is.ultrametric(barb)
[1] TRUE
[1] TRUE
[1] TRUE

Desenraizar e enraizar de novo muda a leitura da árvore (slides, bloco 1):

u  <- unroot(read.tree(text = "((A:1,B:1):1,(C:1,D:1):1);"))
r1 <- root(u, "A", resolve.root = TRUE)
par(mfrow = c(1, 3))
plot(u, type = "unrooted", main = "não enraizada", cex = 1.6, lab4ut = "axial")
plot(r1, main = "raiz no ramo de A", cex = 1.6)
plot(read.tree(text = "((A,B),(C,D));"), main = "raiz no ramo central", cex = 1.6)

Uma politomia e como resolvê-la (aleatoriamente, com ramos de comprimento zero) para os métodos que exigem árvore binária:

p <- read.tree(text = "(A,(B,C,D));")
is.binary(p)
[1] FALSE
set.seed(1)
is.binary(multi2di(p))
[1] TRUE

Quando só há topologia, compute.brlen() atribui comprimentos (por exemplo, pelo método de Grafen). Os comprimentos são inventados: servem para desenhar ou explorar, não como tempo.

g1 <- compute.brlen(gomes, method = "Grafen")
is.ultrametric(g1)
[1] TRUE
plot(g1, cex = .7); axisPhylo()

5. A árvore e a tabela de atributos

Os atributos ficam numa planilha; para os métodos comparativos, quase sempre precisamos de um vetor com os nomes das espécies.

obj <- read.csv("Centrarchidae.csv", row.names = 1)
head(obj)
                     feeding.mode gape.width buccal.length
Acantharchus_pomotis         pisc      0.114        -0.009
Lepomis_gibbosus              non     -0.133        -0.009
Lepomis_microlophus           non     -0.151         0.012
Lepomis_punctatus             non     -0.103        -0.019
Lepomis_miniatus              non     -0.134         0.001
Lepomis_auritus               non     -0.222        -0.039
gape <- setNames(obj$gape.width, rownames(obj))
head(gape)
Acantharchus_pomotis     Lepomis_gibbosus  Lepomis_microlophus 
               0.114               -0.133               -0.151 
   Lepomis_punctatus     Lepomis_miniatus      Lepomis_auritus 
              -0.103               -0.134               -0.222 

Agora o passo que mais dá problema: os nomes da árvore e da tabela batem?

dados_barb <- read.csv("Barbetdata.csv", row.names = 1)
nc <- name.check(barb, dados_barb)
nc
$tree_not_data
[1] "M_asiatica_chersonesus"    "M_australis_australis"    
[3] "M_haemacephala_celestinoi" "M_haemacephala_delica"    
[5] "M_haemacephala_intermedia" "M_haemacephala_rosea"     
[7] "M_lineata_lineata"         "M_oorti_faber"            
[9] "M_oorti_sini"             

$data_not_tree
character(0)

Nove subespécies da árvore não têm dados. Duas saídas: podar a árvore (drop.tip()) ou deixar o treedata() casar as duas coisas de uma vez.

casado <- treedata(barb, dados_barb, warnings = FALSE)
Ntip(casado$phy); nrow(casado$data)
[1] 33
[1] 33
all(casado$phy$tip.label == rownames(casado$data))
[1] FALSE

Atenção à última linha: por padrão, o treedata() não põe as linhas da tabela na ordem das pontas. Funções que usam vetores com nomes (como phylosig() e fitContinuous()) casam pelos nomes e não se importam; as que usam a ordem das linhas (como gls() sem form = ~sp) dão resultados errados em silêncio. Peça a ordenação, ou ordene você mesmo:

casado <- treedata(barb, dados_barb, sort = TRUE, warnings = FALSE)
all(casado$phy$tip.label == rownames(casado$data))
[1] TRUE
# ou: dados_barb[casado$phy$tip.label, ]

O treedata() devolve uma matriz de texto quando a tabela tem colunas de tipos diferentes; converta os atributos numéricos antes de usar.

asa <- setNames(as.numeric(casado$data[, "wing"]), rownames(casado$data))
plotTree.barplot(casado$phy, asa, args.plotTree = list(fsize = .5),
                 args.barplot = list(xlab = "comprimento da asa (log)"))

6. Por que a árvore importa: dois atributos independentes

A simulação dos slides (bloco 0), passo a passo. Simulamos uma árvore e dois atributos que evoluem por movimento browniano sem nenhuma relação entre si.

set.seed(21)
arv <- rcoal(n = 100)
x <- fastBM(arv); y <- fastBM(arv)
ajuste_ols <- lm(y ~ x)
anova(ajuste_ols)
Analysis of Variance Table

Response: y
          Df Sum Sq Mean Sq F value   Pr(>F)   
x          1  3.275  3.2745  8.7871 0.003809 **
Residuals 98 36.520  0.3726                    
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
phylomorphospace(arv, cbind(x, y), label = "off", node.size = c(.4, .6))
abline(ajuste_ols, lwd = 2, lty = "dashed", col = "firebrick")

O filomorfoespaço mostra o motivo: espécies próximas estão próximas no gráfico porque herdaram valores parecidos. Com os contrastes independentes (Aula 2), a “relação” desaparece:

ix <- pic(x, arv); iy <- pic(y, arv)
anova(lm(iy ~ ix - 1))
Analysis of Variance Table

Response: iy
          Df Sum Sq Mean Sq F value Pr(>F)
ix         1  0.182 0.18238  0.1988 0.6567
Residuals 98 89.917 0.91752               

Rode de novo com outras sementes (set.seed(2), set.seed(3)…): quantas vezes o OLS dá p < 0,05? E os contrastes?

7. Suporte dos nós: um bootstrap de verdade

A árvore com bootstrap dos slides (bloco 1), com as 15 sequências de camundongos-do-mato do ape.

data(woodmouse)
f  <- function(s) root(nj(dist.dna(s, model = "K80")), 1, resolve.root = TRUE)
nj_wm <- f(woodmouse)
set.seed(103)
bs <- boot.phylo(nj_wm, woodmouse, f, B = 100, quiet = TRUE, rooted = TRUE)
bs
 [1] 100  31  31  55  48  44  33  12  43  70  84  83  46   9
plot(nj_wm, cex = .8)
nodelabels(bs, frame = "none", adj = c(1.2, -.4), cex = .75)

8. A forma da árvore

O índice de Colless soma, em cada nó, a diferença entre o número de pontas dos dois lados. Escrever a função é um bom exercício de navegação no objeto phylo:

colless <- function(tr) {
  n <- Ntip(tr)
  dif <- sapply((n + 1):(n + tr$Nnode), \(no) {
    filhos <- tr$edge[tr$edge[, 1] == no, 2]
    pontas <- sapply(filhos, \(f) if (f <= n) 1 else sum(getDescendants(tr, f) <= n))
    abs(diff(pontas))
  })
  sum(dif)
}
c(balanceada = colless(stree(8, "balanced")), pente = colless(stree(8, "left")))
balanceada      pente 
         0         21 

O γ de Pybus compara a posição dos nós no tempo com a esperada sob um processo de nascimento puro: γ < 0, nós concentrados perto da raiz.

gammaStat(cent)
[1] -0.2837864

9. Para fora do R: obter árvores

Estes pacotes não estão instalados nos computadores do curso (precisam de internet); o código fica de referência, sem rodar aqui.

# Open Tree of Life: árvore sintética para uma lista de nomes
library(rotl)
nomes <- tnrs_match_names(c("Rhinella icterica", "Physalaemus cuvieri", "Scinax hayii"))
arv_otl <- tol_induced_subtree(ott_ids = ott_id(nomes))

# phyndr: trocar espécies da árvore sem dados por equivalentes com dados
# (ver o roteiro de 2020 e Pennell et al. 2016)

Resumo

  • Uma árvore é um objeto phylo: edge, tip.label, Nnode, edge.length.
  • Newick e Nexus leem com read.tree() e read.nexus(); confira sempre se a árvore é enraizada, binária e ultramétrica.
  • Antes de qualquer análise: name.check() e treedata(), e confira se a ordem das linhas é a das pontas (sort = TRUE).
  • Dados de espécies aparentadas não são independentes: o OLS pode achar relação onde não há.

O roteiro da edição de 2020 traz mais exemplos de manipulação de dados e o phyndr.