Good morning blog readers.
Recently I’ve posted about a new discrete character dependent multi- \(\theta\) (i.e., multi-optimum) OU model in phytools (e.g., 1, 2, 3).
Since this is very new it should only be used with the utmost caution. Nonetheless, I thought I’d post up a quick demo of how it works, and how to do a model comparison to a simpler model of joint discrete & continuous trait evolution but without dependence. (We also might consider an alternative null model with hidden characters, but this will have to be covered in a future post!)
This will only work on recent (at the time of writing) versions of phytools, so we can start by loading the package & checking which version we have.
library(phytools)
## Loading required package: ape
## Loading required package: maps
packageVersion("phytools")
## [1] '2.6.3'
To fit this model I’m going to need some data. With none readily at hand, I’m going to use phytools to simulate some.
We can start with a tree:
N<-200 ## number of taxa
phy<-pbtree(n=N,scale=10)
phy
##
## Phylogenetic tree with 200 tips and 199 internal nodes.
##
## Tip labels:
## t6, t7, t44, t57, t136, t177, ...
##
## Rooted; includes branch length(s).
Next, we want a generating discrete character history for our multi-regime OU process. Note that though we use this regime history for simulation, in an empirical case it would’ve been unknown, so shall naturally be set aside when we move forward to estimation.
## set the transition matrix of our
## discrete trait
q<-0.2
Q<-matrix(c(
-2*q,q,q,
q,-2*q,q,
q,q,-2*q),3,3,
dimnames=list(letters[1:3],letters[1:3]))
Q
## a b c
## a -0.4 0.2 0.2
## b 0.2 -0.4 0.2
## c 0.2 0.2 -0.4
k<-nrow(Q) ## trait levels
k
## [1] 3
sim_tree<-sim.history(phy,Q,anc="a")
## Done simulation(s).
sim_tree
##
## Phylogenetic tree with 200 tips and 199 internal nodes.
##
## Tip labels:
## t6, t7, t44, t57, t136, t177, ...
##
## The tree includes a mapped, 3-state discrete character
## with states:
## a, b, c
##
## Rooted; includes branch lengths.
Let’s plot our generating tree as follows.
cols<-setNames(hcl.colors(n=3),letters[1:k])
plot(sim_tree,cols,ftype="off",lwd=1,
direction="upwards")
par(lend=1)
legend("bottomleft",letters[1:3],lwd=3,
col=hcl.colors(n=k),cex=0.8,bty="n")
Next, we can set the generating conditions for our continous trait simulation. Our model allows for multiple \(\theta\) by discrete character state, but assumes constant \(\alpha\) and \(\sigma^2\) across the \(k\) levels of our discrete trait, so let’s simulate that.
alpha<-setNames(rep(0.3,k),letters[1:k])
alpha
## a b c
## 0.3 0.3 0.3
sig2<-setNames(rep(0.1,k),letters[1:k])
sig2
## a b c
## 0.1 0.1 0.1
theta<-setNames(c(-0.5,1,2),letters[1:k])
theta
## a b c
## -0.5 1.0 2.0
Now we’re nearly ready to simulate our continuous trait. To do that, I’ll use phytools::multiOU as I have in prior posts.
x<-multiOU(sim_tree,alpha,sig2,theta,a0=0)
head(x)
## t6 t7 t44 t57 t136 t177
## 1.9418267 0.5788125 1.2039455 0.7955580 0.6064443 0.9103976
Though we’ve simulated our discrete character history already, for our analysis we’ll use just the tip states, so let’s pull those into a factor vector using phytools::getStates.
y<-as.factor(getStates(sim_tree,"tips"))
head(y)
## t6 t7 t44 t57 t136 t177
## a b c a c c
## Levels: a b c
Awesome. Now let’s first fit our null model using fitmultiOU.
fit_null<-fitmultiOU(phy,x,y,model="ER",levs=100,
parallel=TRUE,ncores=10,root="mle",trace=1,
null_model=TRUE)
## iter theta alpha sigsq q[1] log(L)
## 0 1.0601 0.2025 0.2682 0.0177 -409.2704
## 100 0.4702 0.0182 0.0846 0.2622 -287.2005
## 200 0.5735 0.0150 0.0829 0.2569 -287.0941
## 279 0.5743 0.0141 0.0829 0.2555 -287.0493
## Done optimizing.
fit_null
## Object of class "fitmultiOU" based on
## a discretization with k = 100 levels.
##
## Fitted multi-theta OU model parameters:
## levels: [ a, b, c ]
## theta: [ 0.5743 ]
## alpha: 0.0141
## sigsq: 0.0829
##
## Estimated Q matrix:
## a b c
## a -0.5109335 0.2554668 0.2554668
## b 0.2554668 -0.5109335 0.2554668
## c 0.2554668 0.2554668 -0.5109335
##
## Log-likelihood: -287.0493
##
## R thinks it has found the ML solution.
Let’s confirm that our parameter estimates and log-likelihood match what we would’ve obtained using a geiger::fitContinuous and phytools::fitMk. This isn’t hard.
ou_fit<-geiger::fitContinuous(phy,x,model="OU")
ou_fit
## GEIGER-fitted comparative model of continuous data
## fitted 'OU' model parameters:
## alpha = 0.012919
## sigsq = 0.081833
## z0 = 0.655514
##
## model summary:
## log-likelihood = -89.743851
## AIC = 185.487702
## AICc = 185.610151
## free parameters = 3
##
## Convergence diagnostics:
## optimization iterations = 100
## failed iterations = 0
## number of iterations with same best fit = 50
## frequency of best fit = 0.500
##
## object summary:
## 'lik' -- likelihood function
## 'bnd' -- bounds for likelihood search
## 'res' -- optimization iteration summary
## 'opt' -- maximum likelihood parameter estimates
mk_fit<-fitMk(phy,y,model="ER",pi="equal")
mk_fit
## Object of class "fitMk".
##
## Fitted (or set) value of Q:
## a b c
## a -0.511178 0.255589 0.255589
## b 0.255589 -0.511178 0.255589
## c 0.255589 0.255589 -0.511178
##
## Fitted (or set) value of pi:
## a b c
## 0.333333 0.333333 0.333333
## due to treating the root prior as (a) flat.
##
## Log-likelihood: -195.760975
##
## Optimization method used was "nlminb"
##
## R thinks it has found the ML solution.
null_logL<-logLik(ou_fit)+logLik(mk_fit)
null_logL
## [1] -285.5048
## attr(,"df")
## [1] 3
This should be very close to the values we obtained in fit_null. In fact, the two values are a bit farther apart than I'm comfortable with, but would undoubtedly converge if we were to increase `levs`. We should do this with some caution, though, because it very substantially is going to increase our run time.
Finally, we can fit our discrete character dependent multi- \(\theta\) OU model!
fit_mou<-fitmultiOU(phy,x,y,model="ER",levs=20,
parallel=TRUE,ncores=10,root="mle",trace=1,
maxit=2000)
## iter the[a] the[b] the[c] alpha sigsq q[1] log(L)
## 0 -0.3575 0.3680 1.8215 0.2403 0.0115 0.2066 -309.6850
## 100 -0.3023 1.0148 1.6594 0.2779 0.0615 0.1947 -284.8126
## 200 -0.2191 1.1288 1.9057 0.2837 0.0611 0.1861 -284.1910
## 300 -0.2140 1.1372 1.9328 0.2794 0.0554 0.1914 -284.0423
## 400 -0.2355 1.1808 1.9946 0.2713 0.0499 0.1976 -283.9893
## 500 -0.2229 1.1901 2.0192 0.2716 0.0514 0.2043 -283.9538
## 600 -0.2202 1.2210 1.9799 0.2748 0.0519 0.2054 -283.9183
## 700 -0.2172 1.2083 1.9723 0.2747 0.0528 0.2056 -283.9137
## 800 -0.2164 1.2088 1.9738 0.2751 0.0528 0.2053 -283.9135
## 900 -0.2156 1.2094 1.9724 0.2753 0.0527 0.2052 -283.9134
## 913 -0.2157 1.2097 1.9725 0.2752 0.0527 0.2053 -283.9134
## Done optimizing.
Just because I know that optimization of this model is difficult, I'm a bit suspicious we may not have converged on the true MLE. Let’s try again, but with “sensible” starting values for all our different model parameters.
init<-setNames(
c(
mean(x[y==levels(y)[1]]),
mean(x[y==levels(y)[2]]),
mean(x[y==levels(y)[3]]),
log(2)/max(nodeHeights(phy)),
var(x)/max(nodeHeights(phy)),
fitMk(phy,y,model="ER")$rates),
c("theta[a]","theta[b]","theta[c]",
"alpha","sigsq","q[1]"))
init
## theta[a] theta[b] theta[c] alpha sigsq q[1]
## 0.23239962 0.64856597 0.93721136 0.06931472 0.04357107 0.25558924
fit_mou<-fitmultiOU(phy,x,y,model="ER",levs=100,
parallel=TRUE,ncores=10,root="mle",trace=1,
maxit=2000,init=init)
## iter the[a] the[b] the[c] alpha sigsq q[1] log(L)
## 0 0.2324 0.6486 0.9372 0.0693 0.0436 0.2556 -315.4681
## 100 -0.5480 0.6211 2.3112 0.1711 0.0816 0.1765 -270.5138
## 200 -0.9475 0.9025 2.0608 0.1907 0.0725 0.2221 -268.0952
## 300 -0.8971 0.7990 2.0222 0.1960 0.0707 0.2176 -267.9648
## 400 -0.8941 0.8285 2.0684 0.1935 0.0701 0.2202 -267.9573
## 500 -0.8831 0.8318 2.0694 0.1936 0.0707 0.2166 -267.9496
## 600 -0.8828 0.8724 2.0711 0.1903 0.0708 0.2171 -267.9394
## 700 -0.8728 0.8737 2.0625 0.1894 0.0710 0.2152 -267.9342
## 800 -0.8513 0.8355 1.9719 0.2003 0.0724 0.2116 -267.9037
## 900 -0.8589 0.8295 1.9641 0.2014 0.0720 0.2139 -267.9002
## 1000 -0.8613 0.8330 1.9623 0.2014 0.0719 0.2149 -267.8995
## 1100 -0.8608 0.8220 1.9628 0.2017 0.0713 0.2154 -267.8972
## 1200 -0.8600 0.8276 1.9672 0.2008 0.0712 0.2156 -267.8957
## 1300 -0.8269 1.0429 2.0894 0.1860 0.0700 0.2134 -267.8096
## 1400 -0.8193 1.0484 2.1319 0.1836 0.0691 0.2161 -267.7998
## 1500 -0.8143 1.0503 2.1171 0.1844 0.0696 0.2132 -267.7953
## 1600 -0.7951 1.0509 2.1326 0.1842 0.0690 0.2107 -267.7829
## 1700 -0.7939 1.0513 2.1340 0.1851 0.0689 0.2095 -267.7812
## 1723 -0.7939 1.0513 2.1335 0.1852 0.0689 0.2094 -267.7812
## Done optimizing.
fit_mou
## Object of class "fitmultiOU" based on
## a discretization with k = 100 levels.
##
## Fitted multi-theta OU model parameters:
## levels: [ a, b, c ]
## theta: [ -0.7939, 1.0513, 2.1335 ]
## alpha: 0.1852
## sigsq: 0.0689
##
## Estimated Q matrix:
## a b c
## a -0.4188483 0.2094242 0.2094242
## b 0.2094242 -0.4188483 0.2094242
## c 0.2094242 0.2094242 -0.4188483
##
## Log-likelihood: -267.7812
##
## R thinks it has found the ML solution.
Here it seems that we've definitely done much better.
Remember, the generating values of \(\theta\) were as follows:
theta
## a b c
## -0.5 1.0 2.0
Our estimates are remarkably close to the generating conditions of our simulation!
Naturally, we might be interested to know whether our discrete character dependent model better explains our continuous trait data than the independent null model. This comparison is very easy.
anova(fit_null,fit_mou)
## log(L) d.f. AIC weight
## fit_null -287.0493 4 582.0985 3.166496e-08
## fit_mou -267.7812 6 547.5624 1.000000e+00
This tells us that basically all of the weight of evidence falls on our discrete character dependent multi-regime OU model compared to the null model.
That’s all there is to it!
Preliminary, but very cool.