Tuesday, December 5, 2017

Type I error rates for variable-rate/process Mk discrete character evolution model

Earlier today I described a new method for testing hypotheses about heterogeneity in the rate (or process) of discrete character evolution across a tree.

Although mathematically quite different, this methods owes its intellectual provenance to an article by Brian O'Meara & colleagues published nearly a dozen years ago in 2006. The tactic here is basically the same, but applied to discrete rather than continuous traits.

I just pushed another small update that modifies the logLik methods of the "fitMk" and "fitmultiMk" object classes to facilitate comparison of models in which the rate (or process) is either homogeneous ("fitMk") or non-homogeneous ("fitmultiMk") across the edges of the tree.

In ensuring too that full credit goes where it is due, I should note that for computing the likelihood I adapted Felsenstein's pruning algorithm as implemented in Emmanuel Paradis's ape package.

Below, I'll examine the type I error of the method when used in hypothesis testing against a homogeneous rate/process model.

First, we need to simulate our 'regimes' on the trees. These could be specified arbitrarily, but in this case I will just simulate a binary character on a set of pure-birth phylogenies using sim.history:

library(phytools)
packageVersion("phytools")
## [1] '0.6.51'
trees<-pbtree(n=100,scale=1,nsim=200)
Q<-matrix(c(-0.5,0.5,0.5,-0.5),2,2)
rownames(Q)<-colnames(Q)<-letters[1:2]
trees<-lapply(trees,sim.history,Q=Q,message=FALSE)
class(trees)<-c("multiSimmap","multiPhylo")

I generated 200 trees. Let's plot the first 100 of these:

par(mfrow=c(10,10))
nulo<-sapply(trees[1:100],plot,colors=setNames(c("blue","red"),
    letters[1:2]),ftype="off",lwd=1)

plot of chunk unnamed-chunk-2

Next, we can simulate a discrete character on each of these trees, but in which the transition process is unrelated to the mapped regimes - that is, in which the null hypothesis of no difference in rate or process exists:

Q<-matrix(c(-1,1,1,-1),2,2)
rownames(Q)<-colnames(Q)<-0:1
X<-lapply(trees,function(x,Q) getStates(sim.history(x,Q,
    message=FALSE),"tips"),Q=Q)

Now let's fit our two models to each data vector & tree:

fits.single<-mapply(fitMk,tree=trees,x=X,MoreArgs=list(model="ER"),
    SIMPLIFY=FALSE)
fits.multi<-mapply(fitmultiMk,tree=trees,x=X,MoreArgs=list(model="ER"),
    SIMPLIFY=FALSE)

We can look at a single fitted model of each type:

fits.single[[1]]
## Object of class "fitMk".
## 
## Fitted (or set) value of Q:
##           0         1
## 0 -0.912078  0.912078
## 1  0.912078 -0.912078
## 
## Fitted (or set) value of pi:
##   0   1 
## 0.5 0.5 
## 
## Log-likelihood: -41.127165 
## 
## Optimization method used was "nlminb"
fits.multi[[1]]
## Object of class "fitmultiMk".
## 
## Fitted value of Q[a]:
##           0         1
## 0 -0.888472  0.888472
## 1  0.888472 -0.888472
## 
## Fitted value of Q[b]:
##           0         1
## 0 -1.032995  1.032995
## 1  1.032995 -1.032995
## 
## Fitted (or set) value of pi:
##   0   1 
## 0.5 0.5 
## 
## Log-likelihood: -41.116092 
## 
## Optimization method used was "nlminb"

We can also conduct a likelihood-ratio test on each pair of fitted models. I will do that using lmtest::lrtest as follows:

library(lmtest)
suppressWarnings(LR.test<-mapply(lrtest,fits.single,fits.multi,
    SIMPLIFY=FALSE))
## for example
LR.test[[1]]
## Likelihood ratio test
## 
## Model 1: dots[[1L]][[1L]]
## Model 2: dots[[2L]][[1L]]
##   #Df  LogLik Df  Chisq Pr(>Chisq)
## 1   1 -41.127                     
## 2   2 -41.116  1 0.0221     0.8817

Let's pull out the P-values from these tests & plot them:

P<-sapply(LR.test,function(x) x[["Pr(>Chisq)"]][2])
obj<-hist(P,20,plot=FALSE)
plot(obj$mids,obj$counts/sum(obj$counts),type="h",
    lwd=18,col=make.transparent("blue",0.4),lend=1,
    xlab="P-value",ylab="relative frequency",ylim=c(0,0.2))
title(main="P-values for LR-test for data simulated under the null")
abline(h=0.05,col=make.transparent("red",0.5),lwd=1,
    lty="dotted")
text(0.96,0.05,"0.05",pos=3)

plot of chunk unnamed-chunk-7

Neat. This is close to what we'd hope for.

Fitting a variable-process model of discrete character evolution on the tree using phytools

Now for something a little different.

Today, I have built a new method that fits a model of discrete character evolution in which the transition matrix Q varies among different parts of the tree.

These might be edges of clades specified arbitrarily by the user (for instance, using paintBranches or paintSubTree in phytools), or they could be regimes mapped onto the tree using the procedure of stochastic character mapping.

The way I did this was pretty simple. I just took the function that implements Felsenstein's famous pruning algorithm to compute the likelihood, but then I modified so that it could use a different Q for different edges. The only complication was that we might like our regime to change along an edge rather than merely at a node. To solve that, I used the phytools function map.to.singleton to convert our "simmap" object with singleton nodes and only a single regime per edge. Problem solved.

Note that this function, like fitMk, uses code for the pruning algorithm adapted from Emmanuel Paradis' ape package.

Let's try it.

First, our data:

library(phytools)
packageVersion("phytools")
## [1] '0.6.50'
plot(tree,ftype="off",colors=setNames(c("blue","red"),
    mapped.states(tree)),xlim=c(0,1.05*max(nodeHeights(tree))))
tiplabels(pie=to.matrix(x,c(0,1)),piecol=c("black","white"),
    cex=0.4,offset=0.01)

plot of chunk unnamed-chunk-1

So the idea is simply that we will fit a model in which the rate of transition between 0 & 1 (show here as black & white) depends on the state (red or blue) mapped onto the edges of the tree. Right?

Let's fit our model:

fitERmulti<-fitmultiMk(tree,x,model="ER")
fitERmulti
## Object of class "fitmultiMk".
## 
## Fitted value of Q[a]:
##           0         1
## 0 -0.646581  0.646581
## 1  0.646581 -0.646581
## 
## Fitted value of Q[b]:
##           0         1
## 0 -10.14495  10.14495
## 1  10.14495 -10.14495
## 
## Fitted (or set) value of pi:
##   0   1 
## 0.5 0.5 
## 
## Log-likelihood: -35.883658 
## 
## Optimization method used was "nlminb"

Or an "ARD" model that differs between parts of the tree:

fitARDmulti<-fitmultiMk(tree,x,model="ARD")
fitARDmulti
## Object of class "fitmultiMk".
## 
## Fitted value of Q[a]:
##           0         1
## 0 -2.218063  2.218063
## 1  0.604146 -0.604146
## 
## Fitted value of Q[b]:
##           0         1
## 0 -25.36092  25.36092
## 1  12.73165 -12.73165
## 
## Fitted (or set) value of pi:
##   0   1 
## 0.5 0.5 
## 
## Log-likelihood: -34.515745 
## 
## Optimization method used was "nlminb"

Of course, we can compare this, if we'd like, to a model with but a single regime on the tree. For instance:

fitER<-fitMk(tree,x,model="ER")
fitER
## Object of class "fitMk".
## 
## Fitted (or set) value of Q:
##           0         1
## 0 -1.651121  1.651121
## 1  1.651121 -1.651121
## 
## Fitted (or set) value of pi:
##   0   1 
## 0.5 0.5 
## 
## Log-likelihood: -41.613061 
## 
## Optimization method used was "nlminb"

This suggests that our data justifies the greater model complexity of multiple regimes on the tree. That's good, because we simulated them that way!

tree<-pbtree(n=100,tip.label=LETTERS,scale=0.5)
Q<-matrix(c(-1,1,1,-1),2,2)
rownames(Q)<-colnames(Q)<-letters[1:2]
tree<-sim.history(tree,Q,anc="a")
sim.tree<-as.phylo(tree)
q<-setNames(c(1,10),letters[1:2])
sim.tree$edge.length<-colSums(t(tree$mapped.edge[,letters[1:2]])*q)
rownames(Q)<-colnames(Q)<-0:1
x<-as.factor(sim.history(sim.tree,Q)$states)

(As of yet there is not function to simulate multiple Mk models in different parts of the tree - so what I did above is to stretch the edge lengths of the tree by regime, and simulate under a constant regime on the stretched tree.)

Monday, December 4, 2017

More on visualizing the rate of discrete evolution through time

Last week I posted on visualizing the mean number of changes (or changes per unit of edge length) per interval of time from the root of the tree to the present day. This can create a nice plot; however it is more or less meaningless unless we can compare it to what we might expect under some kind of neutral process.

Consequently, today I have added some new function & methods that:

1) Automate calculation of this CTT or 'changes-through-time' plot based on stochastic mapped trees.

2) Simulate CTT plots for a given transition matrix, Q and (optionally) ancestral state, by first simulating a discrete character & then sampling stochastic character maps consistent with this character. This can be done a large number of times to generate a null distribution of the accumulation of changes through time under the model.

3) Visualize our empirical CTT plot and a 100×(1-α)% distribution of CTTs under the a null model.

Here's what that looks like:

library(phytools)
packageVersion("phytools")
## [1] '0.6.49'
tree
## 
## Phylogenetic tree with 200 tips and 199 internal nodes.
## 
## Tip labels:
##  t5, t29, t68, t199, t200, t11, ...
## 
## Rooted; includes branch lengths.
y
##   t5  t29  t68 t199 t200  t11  t18 t107 t108  t72   t3  t24  t85  t86  t44 
##    b    c    c    c    c    c    c    c    c    c    c    c    c    c    b 
##  t48  t51  t52 t186 t195 t196 t115 t116  t91 t134 t135 t113  t42  t90  t97 
##    b    b    b    b    b    b    c    c    c    c    c    c    c    a    a 
##  t98 t158 t159  t50 t144 t145  t32  t43 t151 t152 t112  t30  t53  t54  t47 
##    b    a    a    a    a    a    a    a    a    a    a    a    a    a    a 
##  t82  t83  t57  t58 t157 t176 t177  t76  t75  t92  t93  t95 t139 t140 t153 
##    a    a    a    a    a    a    a    a    a    a    a    a    a    a    a 
## t154  t36  t37  t80 t101 t102  t73  t74  t31 t124 t125  t33 t162 t163  t96 
##    a    a    a    a    a    a    a    a    a    a    a    a    a    a    a 
## t114 t122 t123 t132 t133  t78  t79  t71  t17  t21  t22  t14  t25  t26  t89 
##    a    a    a    a    a    b    a    a    a    a    a    a    a    a    a 
## t187 t188 t170 t171  t23 t172 t173  t84 t148 t149 t156 t182 t183  t81 t197 
##    a    a    a    a    a    c    c    a    a    a    c    c    c    c    c 
## t198 t141  t49 t189 t190  t19  t20 t184 t185 t155 t110 t111 t106 t117 t118 
##    c    c    c    c    c    c    c    c    c    c    c    c    c    c    c 
##  t13   t4  t38  t39  t12 t150 t180 t181  t45  t46   t8 t142 t143  t40 t164 
##    b    c    c    c    c    c    c    c    c    c    c    c    c    c    c 
## t165 t178 t179  t55  t56  t41  t59  t87  t88  t35 t193 t194 t138 t174 t175 
##    c    c    c    c    c    c    a    a    a    a    a    a    c    c    c 
##  t15   t9 t128 t129 t160 t161 t119  t62  t63  t10   t2 t191 t192  t69  t70 
##    c    a    a    a    a    a    a    a    a    a    a    b    b    b    b 
## t146 t147 t103  t77 t109 t168 t169 t136 t137 t130 t131 t120 t121 t126 t127 
##    b    b    b    b    b    b    b    b    b    b    b    b    b    b    b 
## t166 t167   t7  t64  t65  t34  t16  t27  t66  t67  t99 t100  t60  t61 t104 
##    b    b    b    a    a    a    b    b    b    b    b    b    b    b    b 
## t105  t94  t28   t6   t1 
##    b    b    b    b    b 
## Levels: a b c
trees<-make.simmap(tree,y,nsim=100,model="ER")
## make.simmap is sampling character histories conditioned on the transition matrix
## 
## Q =
##              a            b            c
## a -0.003042964  0.001521482  0.001521482
## b  0.001521482 -0.003042964  0.001521482
## c  0.001521482  0.001521482 -0.003042964
## (estimated using likelihood);
## and (mean) root node prior probabilities
## pi =
##         a         b         c 
## 0.3333333 0.3333333 0.3333333
## Done.
object<-ctt(trees)
object
## Object of class "ctt" consisting of:
##    (1) a matrix (segments) with the beginning & ending time of each segment.
##    (2) a vector (nchanges) with the mean number of changes in each segment.
##    (3) a vector (edge.length) containing the total edge length of each segement.
##    (4) an object of class "phylo".
plot(object,type="number")

plot of chunk unnamed-chunk-1

More often it seems more likely that we'd be interested in tracking the number of changes per unit of edge length rather than the total number of changes - because in all reconstructed phylogeny of extant taxa there is more edge length towards the tips of the tree than towards the root.

plot(object,type="rate")

plot of chunk unnamed-chunk-2

Of course, as I mentioned in the preamble, it probably doesn't make much sense to generate this type of visualization unless we have a sense of what might be expected under some kind of reasonable null hypothesis - such as a constant rate of character evolution through time & among lineages. Luckily, we can simulate this too. The function sim.ctt will simulate a CTT plot for a given transition matrix, Q, by first simulating a discrete character and then by sampling stochastic character maps for that character. This is still somewhat slow, so it may take a few minutes to run.

I'll use the function sim.multiCtt to simulate various rather than a single CTT:

Q<-trees[[1]]$Q
Q
##              a            b            c
## a -0.003042964  0.001521482  0.001521482
## b  0.001521482 -0.003042964  0.001521482
## c  0.001521482  0.001521482 -0.003042964
nulo<-sim.multiCtt(tree,Q,nsim=100)
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
## Starting stochastic mapping with simulated data vector.... Done.
nulo
## 100 objects of class "ctt" in a list.
plot(nulo,type="number")
plot(object,add=TRUE,type="number")

plot of chunk unnamed-chunk-3

That's kind of neat. Now, for the rate. Here, I will change the α level for our confidence interval.

plot(nulo,alpha=0.2,ylim=c(0,0.025))
plot(object,add=TRUE)

plot of chunk unnamed-chunk-4

In this case, the data were simulated under a process of declining rate towards the present. Perhaps that was captured by our plots?

tree<-pbtree(n=200,scale=100)
Q<-matrix(c(-0.004,0.002,0.002,
    0.002,-0.004,0.002,
    0.002,0.002,-0.004),3,3)
rownames(Q)<-colnames(Q)<-letters[1:3]
EB<-phytools:::ebTree(tree,-0.06)
EB$edge.length<-EB$edge.length/max(nodeHeights(EB))*100
y<-as.factor(sim.history(EB,10*Q)$states)