Friday, March 3, 2017

Statistical behavior of PGLS taking account sampling error of y

Earlier today I posted on taking sampling error in y into account during phylogenetic regression or ANOVA.

In the following, I'm going to do a small investigation into type I error & power of this method. I will compare type I error & power to phylogenetic regression ignoring error in y.

First, type I error using a simple bivariate regression.

library(phytools)
## Loading required package: ape
## Loading required package: maps
library(nlme)
packageVersion("phytools")
## [1] '0.5.80'
set.seed(1)
## simulate trees
trees<-pbtree(n=100,nsim=200)
## simulate true values of x & y
x<-lapply(trees,fastBM)
y<-lapply(trees,fastBM)
## simulate sampling variances for y
v<-lapply(trees, function(t) setNames(rexp(n=Ntip(t)),t$tip.label))
ye<-mapply(function(y,v) 
    setNames(sampleFrom(xbar=y,xvar=v,n=rep(1,length(y))),
    names(y)),y=y,v=v,SIMPLIFY=FALSE)
## fit PGLS ignoring sampling error in y
fitGLS<-function(t,x,y) gls(y~x,data.frame(x,y),
    correlation=corBrownian(1,t),method="ML")
fits.gls<-mapply(fitGLS,trees,x,ye,SIMPLIFY=FALSE)
## now taking it into account
fitSEy<-function(t,x,y,v) pgls.SEy(y~x,data.frame(x,y),
    tree=t,se=sqrt(v),method="ML")
fits.SEy<-mapply(fitSEy,trees,x,ye,v=v,SIMPLIFY=FALSE)
P.gls<-sapply(fits.gls,function(x) anova(x)$"p-value"[2])
mean(P.gls<=0.05) ## type I error method 1
## [1] 0.04
P.sey<-sapply(fits.SEy,function(x) anova(x)$"p-value"[2])
mean(P.sey<=0.05) ## type I error method 2
## [1] 0.05

So we should see straight away that there is no effect on type I error of ignoring sampling error in the estimation of species values for y. How about an effect on our estimated parameter, β1?

beta.gls<-sapply(fits.gls,function(x) coefficients(x)[2])
beta.sey<-sapply(fits.SEy,function(x) coefficients(x)[2])
d.gls<-density(beta.gls,bw=0.1)
d.sey<-density(beta.sey,bw=0.1)
plot(d.gls$x,d.gls$y,type="l",xlim=range(c(d.gls$x,d.sey$x)),
    ylim=range(c(d.gls$y,d.sey$y)),ylab="density",
    xlab=expression(beta[1]),lwd=2)
lines(d.sey$x,d.sey$y,lwd=2,lty="dotted")
text(d.gls$x[which(d.gls$y==max(d.gls$y))],
    max(d.gls$y),label="GLS (no error)",pos=4)
text(d.sey$x[which(d.sey$y==max(d.sey$y))],
    max(d.sey$y),label="GLS (with error)",pos=4)
abline(v=0,lty="dashed",col="grey")

plot of chunk unnamed-chunk-2

What's interesting here is that the type I error of both methods is basically identical; however the variance of the estimator that takes into count sampling error in y is much lower than the other.

Now, let's go a bit a farther & investigate the performance of each method for varying values of β1, our regression coefficient. We can do this as follows:

b1<-c(0,0.2,0.5,1,1.5,2)
P.gls<-P.sey<-beta.gls<-beta.sey<-matrix(NA,length(trees),length(b1),
    dimnames=list(NULL,b1))
for(i in 1:length(b1)){
    x<-lapply(trees,fastBM)
    y<-mapply(function(t,x,b) b*x+fastBM(t),t=trees,x=x,
        MoreArgs=list(b=b1[i]),SIMPLIFY=FALSE)
    v<-lapply(trees, function(t) setNames(rexp(n=Ntip(t)),t$tip.label))
    ye<-mapply(function(y,v) 
        setNames(sampleFrom(xbar=y,xvar=v,n=rep(1,length(y))),
        names(y)),y=y,v=v,SIMPLIFY=FALSE)
    fitGLS<-function(t,x,y) gls(y~x,data.frame(x,y),
        correlation=corBrownian(1,t),method="ML")
    fits.gls<-mapply(fitGLS,trees,x,ye,SIMPLIFY=FALSE)
    fitSEy<-function(t,x,y,v) pgls.SEy(y~x,data.frame(x,y),
        tree=t,se=sqrt(v),method="ML")
    fits.SEy<-mapply(fitSEy,trees,x,ye,v=v,SIMPLIFY=FALSE)
    P.gls[,i]<-sapply(fits.gls,function(x) anova(x)$"p-value"[2])
    P.sey[,i]<-sapply(fits.SEy,function(x) anova(x)$"p-value"[2])
    beta.gls[,i]<-sapply(fits.gls,function(x) coefficients(x)[2])
    beta.sey[,i]<-sapply(fits.SEy,function(x) coefficients(x)[2])
}

First, we can look the mean parameter estimates of each method:

par(mfrow=c(1,2))
boxplot(beta.gls,ylim=c(-0.5,3),main="GLS (no error)")
abline(h=b1,col="grey",lty="dashed")
boxplot(beta.sey,ylim=c(-0.5,3),main="GLS (with error)")
abline(h=b1,col="grey",lty="dashed")

plot of chunk unnamed-chunk-4

We can easily see that (although both methods are unbiased), variability among simulations is substantially higher when error in y is not taken into consideration.

We would expect this to affect our power to reject the null hypothesis of β1 = 1, and it does:

plot(b1,colMeans(P.gls<=0.05),type="b",lwd=2,xlim=c(0,2),
    ylim=c(0,1),xlab=expression(beta[1]),ylab="type I error / power")
lines(b1,colMeans(P.sey<=0.05),type="b",lwd=2,lty="dotted")
text(b1[3],colMeans(P.gls<=0.05)[3],label="GLS (no error)",
    pos=4)
text(b1[3],colMeans(P.sey<=0.05)[3],label="GLS (with error)",
    pos=2)
abline(h=0.05,lty="dashed",col="grey")
text(x=2,y=0.05,"0.05",pos=3)

plot of chunk unnamed-chunk-5

Neat.

More on PGLS with error in y

Nearly two years ago I posted about fitting a PGLS model taking into account sampling variance (uncertainty) in y.

Inspired by a student dataset & problem here at los Andes, the following is a slightly differently implemented solution that should lead to the same result:

pgls.SEy<-function(model,data,corClass=corBrownian,tree=tree,
    se=NULL,method=c("REML","ML"),interval=c(0,1000),...){
    corfunc<-corClass
    ## preliminaries
    data<-data[tree$tip.label,]
    if(is.null(se)) se<-setNames(rep(0,Ntip(tree),
        tree$tip.label))
    ## likelihood function
    lk<-function(sig2e,data,tree,model,ve){
        tree$edge.length<-tree$edge.length*sig2e
        ii<-sapply(1:Ntip(tree),function(x,e) which(e==x),
            e=tree$edge[,2])
        tree$edge.length[ii]<-tree$edge.length[ii]+
            ve[tree$tip.label]
        v<-diag(vcv(tree))
        vf<-varFixed(~v)
        COR<-corfunc(1,tree,...)
        fit<-gls(model,data=data,correlation=COR,weights=vf)
        -logLik(fit)
    }
    ## estimate sig2[e]
    fit<-optimize(lk,interval=interval,
        data=data,tree=tree,model=model,ve=se^2)
    tree$edge.length<-tree$edge.length*fit$minimum
    ii<-sapply(1:Ntip(tree),function(x,e) which(e==x),
        e=tree$edge[,2])
    tree$edge.length[ii]<-tree$edge.length[ii]+
        se[tree$tip.label]^2
    v<-diag(vcv(tree))
    vf<-varFixed(~v)
    ## fit & return model
    gls(model,data,correlation=corfunc(1,tree),weights=vf,
        method=method)
}

Now let's try to apply it:

library(phytools)
library(nlme)
## here's our data
tree
## 
## Phylogenetic tree with 100 tips and 99 internal nodes.
## 
## Tip labels:
##  t65, t66, t29, t30, t13, t78, ...
## 
## Rooted; includes branch lengths.
X
##               ye           x1          x2
## t65  -0.77900192 -0.933177258 -0.50289849
## t66   0.84646891 -0.360779073 -0.67343194
## t29   1.95846040 -0.004324650 -0.86464732
## t30   0.24373967 -0.283674892 -0.05950573
## t13  -5.54444003 -0.260222231  1.69823520
## t78   1.88453970 -0.030577008 -0.12238217
## t79  -0.59317590 -0.242023433  0.32537283
## t5    1.17182511 -0.351473922 -0.86081240
## t26   3.36099814 -0.417929491 -0.87860040
## t67  -0.21357307 -0.494481915 -1.04212209
## t68   0.73039688 -0.588127345 -0.71373910
## t86   2.81142943 -0.045597241 -1.28701218
## t87   4.89158144 -0.284624727 -1.19177925
## t25   2.69692032 -0.099326903 -1.26492190
## t16   1.27290355 -0.329391279 -0.69189543
## t47  -0.57529243 -0.998675854 -0.60213264
## t48  -4.91449264 -1.135659972 -0.90384083
## t10   2.88459685 -0.170786099 -0.84404497
## t20   1.11432889 -0.116176393 -1.43439903
## t63   0.67056966 -0.271788410 -0.48803237
## t64   2.25646332 -0.122474607 -0.87832126
## t53   2.52828710 -0.188851979 -0.48504631
## t21   1.78891253 -0.666761834 -1.27216994
## t23   1.45124819 -0.263860837 -1.16042313
## t49   1.03438725 -0.143231501 -1.19002853
## t50   0.61071781  0.258295208 -0.60080140
## t80   1.00845570 -0.326915114  0.10754248
## t81  -1.01437643 -0.507894799  0.26522058
## t19  -1.71820534 -0.811681993 -0.26318414
## t84   0.71358338 -0.737986864 -1.02763382
## t85   1.56004661 -0.712068874 -0.77728970
## t40  -0.38524799 -0.933452671 -0.83696879
## t41   0.49296392 -0.229120475 -1.49848945
## t18   2.76362994 -0.204012732 -1.64088635
## t76  -0.38900308 -0.117917709 -0.77909630
## t77   0.16193894  0.287088622 -0.72342580
## t27   2.64224382 -0.358620911 -1.71171019
## t92  -0.08353593 -0.337589308 -1.45006236
## t93   0.31220124 -0.312388755 -1.60509872
## t8   -1.04234066 -0.676186538 -0.41457179
## t2   -0.24958117 -1.382336906 -0.60476399
## t95   0.30844124  0.071824226 -0.04136211
## t96   0.17344643 -0.000369423  0.04677993
## t6   -4.30672086 -1.228130839  1.24440860
## t44  -0.39925053 -0.778840190 -0.10432506
## t51  -0.06845018 -1.529691824 -0.63523463
## t52  -0.33920397 -0.671563524  0.25110920
## t72  -0.22233595 -0.208175485  0.21888791
## t73  -0.74246626 -0.280504466 -0.05425123
## t35   0.58660363 -0.432133697 -0.60337432
## t38  -0.02995503 -1.599070451 -1.12646600
## t39  -0.99818963 -1.019397388 -0.14879313
## t99   2.02180211  0.819938089  0.10457809
## t100  3.03396613  0.797765277  0.09518138
## t24  -2.28364797  0.132350015  1.25951182
## t11  -1.03843807 -0.338010923  0.63895729
## t36   3.08690868 -0.109222940 -0.84517257
## t37   1.66001718 -0.783798640  0.25056370
## t28  -0.42294326 -0.624356825 -0.18617947
## t7    1.63649995 -0.995914763 -0.82088749
## t12   0.30240267 -0.430696448 -0.37673980
## t33  -0.90310597 -0.459191936  0.14792529
## t34  -0.46084029 -1.369205857  0.15734648
## t15  -0.95017011 -0.668597155 -0.19397849
## t42  -0.40629518 -1.006046065 -0.59102696
## t43  -1.45997398 -0.657881494 -0.76035426
## t22  -0.06754584 -0.549687736 -0.49300322
## t1   -0.52189221 -0.735847227 -0.10188643
## t3    1.42350778 -1.255254006 -1.36243767
## t4   -1.66965072  0.036571658  0.12260330
## t74   3.03252085  1.178218512 -0.76133067
## t75   2.48876980  1.064090251 -0.95706527
## t31  -0.97972153 -0.435236571  0.54331428
## t59  -3.89650669 -0.567740108  0.91285849
## t60  -1.92222453 -0.516435916  0.64042685
## t9    3.53788280  1.375046920 -1.24425619
## t90   6.09565095  0.878034624 -2.01772447
## t91   6.88955565  0.935005458 -2.09172115
## t56  -0.35774725 -0.517446554  0.30596671
## t57   1.23478325 -0.116159889  0.05991413
## t14  -1.27430128 -0.906819465  0.46607917
## t61  -4.01746299 -1.312009985  1.32878564
## t62  -2.40971394 -1.612880369  1.08221968
## t69  -2.04466742 -0.242584724  0.76102366
## t70  -4.84522907 -0.396631201  1.07417172
## t88  -0.17336430  0.237257899  0.41246936
## t89  -2.05031514  0.434875538  0.30737619
## t54  -0.80195450 -0.569749931  1.07419352
## t55  -1.48926859 -0.152194258  1.08008287
## t32  -0.87454629  0.602680886  0.75156851
## t45  -3.25099532 -1.438964529  2.01395563
## t46  -2.70322247 -1.404376125  1.86329858
## t17   0.38924791 -0.217652443  0.32900077
## t58  -3.40005156 -1.498640558  1.17233451
## t82  -1.89445105 -1.348265012  1.08151593
## t83  -2.05781804 -1.381394718  1.22167672
## t71  -1.44719125 -1.170708866  1.02083131
## t94  -2.16041746 -1.200085947  1.03456095
## t97  -1.44175507 -1.213089703  1.18276260
## t98  -3.88229471 -1.402582088  1.24727944
SE
##       t65       t66       t29       t30       t13       t78       t79 
## 1.5378419 0.6315825 0.8879493 0.5928441 1.0271921 1.0799337 0.2015944 
##        t5       t26       t67       t68       t86       t87       t25 
## 1.4215427 1.6140998 1.1763704 0.3116548 0.3712812 2.1127712 1.7325671 
##       t16       t47       t48       t10       t20       t63       t64 
## 0.6036392 0.2305401 2.1632606 0.5891664 1.6319491 0.1739720 0.2086460 
##       t53       t21       t23       t49       t50       t80       t81 
## 1.4821123 0.4555341 0.3351531 1.2322628 0.3969949 0.8365659 0.2839710 
##       t19       t84       t85       t40       t41       t18       t76 
## 0.3487848 1.1665896 1.5049262 0.5291481 0.5503178 0.7437542 0.9244183 
##       t77       t27       t92       t93        t8        t2       t95 
## 0.4384969 0.6420107 0.6988273 0.7246152 0.8660270 1.5637886 1.8151181 
##       t96        t6       t44       t51       t52       t72       t73 
## 1.3655987 0.9416576 0.7521436 0.4052548 0.7403313 0.3570909 1.2056779 
##       t35       t38       t39       t99      t100       t24       t11 
## 0.8792995 0.8277867 1.3258279 0.1751977 0.8815140 0.4549706 0.1941065 
##       t36       t37       t28        t7       t12       t33       t34 
## 1.4607861 1.1021203 0.6038450 0.9676804 0.2473128 0.1840373 1.6307541 
##       t15       t42       t43       t22        t1        t3        t4 
## 0.7664964 1.1059112 1.1145843 0.9188596 1.5650796 0.4641816 0.6615129 
##       t74       t75       t31       t59       t60        t9       t90 
## 0.9735615 1.0394457 1.5125566 1.8766065 0.5621988 0.9335404 0.2087170 
##       t91       t56       t57       t14       t61       t62       t69 
## 0.7721703 1.8066518 0.4828995 0.8829816 0.7459172 2.1854232 0.7337223 
##       t70       t88       t89       t54       t55       t32       t45 
## 1.4301866 0.2787410 1.0083834 0.5330130 1.1466571 1.7584170 0.3040079 
##       t46       t17       t58       t82       t83       t71       t94 
## 0.8741375 0.8283463 0.5284757 1.1277393 0.3540923 0.3703903 1.0910862 
##       t97       t98 
## 0.9403271 1.3459290
fit<-pgls.SEy(ye~x1+x2,data=X,se=SE,tree=tree,method="ML")
fit
## Generalized least squares fit by maximum likelihood
##   Model: model 
##   Data: data 
##   Log-likelihood: -166.1253
## 
## Coefficients:
## (Intercept)          x1          x2 
##   0.6837662   1.5101552  -1.6872891 
## 
## Correlation Structure: corBrownian
##  Formula: ~1 
##  Parameter estimate(s):
## numeric(0)
## Variance function:
##  Structure: fixed weights
##  Formula: ~v 
## Degrees of freedom: 100 total; 97 residual
## Residual standard error: 1.774718
summary(fit)
## Generalized least squares fit by maximum likelihood
##   Model: model 
##   Data: data 
##        AIC      BIC    logLik
##   340.2505 350.6712 -166.1253
## 
## Correlation Structure: corBrownian
##  Formula: ~1 
##  Parameter estimate(s):
## numeric(0)
## Variance function:
##  Structure: fixed weights
##  Formula: ~v 
## 
## Coefficients:
##                  Value Std.Error    t-value p-value
## (Intercept)  0.6837662 0.1650265   4.143373   1e-04
## x1           1.5101552 0.1365145  11.062231   0e+00
## x2          -1.6872891 0.1153723 -14.624738   0e+00
## 
##  Correlation: 
##    (Intr) x1   
## x1 0.122       
## x2 0.078  0.376
## 
## Standardized residuals:
##        Min         Q1        Med         Q3        Max 
## -2.7961712 -0.8297315 -0.2312799  0.4027741  3.0330635 
## 
## Residual standard error: 1.774718 
## Degrees of freedom: 100 total; 97 residual

In a separate post, I'm going to look at power & type I error when SEs are ignored vs. taken into account.

Note that also taking uncertainty in the xs is account is considerably more complicated. For that we would need to employ an approach similar to that of Ives et al. (2007).

The data & tree for this exercise were simulated as follows:

## simulate tree & data
set.seed(999)
tree<-pbtree(n=100,scale=1)
X<-fastBM(tree,nsim=2)
colnames(X)<-c("x1","x2")
beta<-c(1,1,-2)
y<-cbind(rep(1,Ntip(tree)),X)%*%beta+fastBM(tree)
v<-setNames(rexp(n=Ntip(tree)),tree$tip.label)
ye<-setNames(sampleFrom(xbar=y,xvar=v,n=rep(1,length(y))),
    rownames(y))
X<-as.data.frame(cbind(ye,X))
SE<-sqrt(v)

Thursday, March 2, 2017

Function to plot a tree with error bars on divergence dates in R

Yesterday I posted about plotting error bars around divergence times onto internal nodes of the tree.

The following (plotTree.errorbars) is a function to automate this procedure, to be shortly added to phytools.

## plot tree with error bars around divergence times at nodes
## written by Liam J. Revell 2017
plotTree.errorbars<-function(tree,CI,...){
    args<-list(...)
    if(!is.null(args$gridlines)){ 
        gridlines<-args$gridlines
        args$gridlines<-NULL
    } else gridlines<-TRUE
    if(is.null(args$mar)) args$mar<-c(4.1,1.1,1.1,1.1)
    if(is.null(args$ftype)) args$ftype<-"i"
    fsize<-if(!is.null(args$fsize)) args$fsize else 1
    if(is.null(args$direction)) args$direction<-"leftwards"
    if(!is.null(args$bar.width)){
        bar.width<-args$bar.width
        args$bar.width<-NULL
    } else bar.width<-11
    if(!is.null(args$cex)){
        cex<-args$cex
        args$cex<-NULL
    } else cex<-1.2
    if(!is.null(args$bar.col)){
        bar.col<-args$bar.col
        args$bar.col<-NULL
    } else bar.col<-"blue"
    par(mar=args$mar)
    plot.new()      
    th<-max(nodeHeights(tree))
    h<-max(th,max(CI))
    if(is.null(args$xlim)){
        m<-min(min(nodeHeights(tree)),min(CI))
        d<-diff(c(m,h))
        pp<-par("pin")[1]
        sw<-fsize*(max(strwidth(tree$tip.label,units="inches")))+
            1.37*fsize*strwidth("W",units="inches")
        alp<-optimize(function(a,d,sw,pp) (a*1.04*d+sw-pp)^2,
            d=d,sw=sw,pp=pp,
            interval=c(0,1e6))$minimum
        args$xlim<-if(args$direction=="leftwards") c(h,m-sw/alp) else 
            c(m,h+sw/alp)
    }
    if(is.null(args$at)) at<-seq(0,h,by=h/5)
    else {
        at<-args$at
        args$at<-NULL
    }
    args$tree<-tree
    args$add<-TRUE
    do.call(plotTree,args=args)
    if(gridlines) abline(v=at,lty="dashed",
        col=make.transparent("grey",0.5))
    axis(1,at=at,labels=signif(at,3))
    obj<-get("last_plot.phylo",envir=.PlotPhyloEnv)
    for(i in 1:tree$Nnode+Ntip(tree))
        lines(x=c(CI[i-Ntip(tree),1],CI[i-Ntip(tree),2]),
            y=rep(obj$yy[i],2),lwd=bar.width,lend=0,
            col=make.transparent(bar.col,0.4))
    points(obj$xx[1:tree$Nnode+Ntip(tree)],
        obj$yy[1:tree$Nnode+Ntip(tree)],pch=19,col=bar.col,
        cex=cex)
}

As I have mentioned in an earlier post - one of the most difficult components of plotting a tree in R is leaving enough space in the plotting window for the tip labels. This is because we normally don't know how much space a particular string will occupy (in user units) until we have already opened our plotting device & set our xlim and ylim values. Consequently, the example will use a tree with tip labels B through Z, plus one really long tip label, to make sure that enough space has been allocated. BTW, my solution to this problem (above) is adapted from plot.phylo in the ape package.

OK, now let's test it out:

library(phytools)
tree
## 
## Phylogenetic tree with 26 tips and 25 internal nodes.
## 
## Tip labels:
##  Z, Y, X, W, V, U, ...
## 
## Rooted; includes branch lengths.
CI
##        lower     upper
## 27 2.8414221 2.2855101
## 28 2.4025799 1.7851208
## 29 1.1518398 0.6932472
## 30 1.1016629 0.5915718
## 31 1.5786906 0.9097206
## 32 1.2866289 0.6838407
## 33 0.4025250 0.0000000
## 34 0.7735031 0.2052436
## 35 0.6981268 0.2212514
## 36 2.7675974 2.1440829
## 37 2.3925982 1.8320681
## 38 1.5706797 0.9821414
## 39 1.2980457 0.9041184
## 40 1.1321683 0.5452341
## 41 0.4097613 0.0000000
## 42 0.4676039 0.0000000
## 43 0.7283202 0.2276139
## 44 0.4388847 0.0000000
## 45 1.2948954 0.7909557
## 46 0.6314838 0.1167126
## 47 1.2049871 0.6604358
## 48 1.0396180 0.5733247
## 49 0.5778952 0.1890986
## 50 0.3843571 0.0000000
## 51 1.0729443 0.5512091
plotTree.errorbars(tree,CI)

plot of chunk unnamed-chunk-2

Note that there is a lot we can do to modify this plot. Firstly, any argument of plotTree can be passed internally to that function. For instance:

plotTree.errorbars(tree,CI,lwd=5,color="navy",lend=0)

plot of chunk unnamed-chunk-3

However, it is also possible to modify the specify attributes of the plot. For instance:

plotTree.errorbars(tree,CI,lwd=1,bar.col="red",bar.width=7,cex=1,
    at=seq(0,3,by=0.5))

plot of chunk unnamed-chunk-4

Pretty neat.

The tree & CIs for this example were simulated as follows:

tree<-pbtree(n=26,tip.label=LETTERS[26:1])
tree$tip.label[26]<-"Some really long tip label."
h<-sapply(1:tree$Nnode+Ntip(tree),nodeheight,tree=tree)
d<-max(nodeHeights(tree))
CI<-cbind(h-runif(n=length(h),min=.10*d,max=.20*d),h+runif(n=length(h),
    min=.05*d,max=.10*d))
CI[CI>max(nodeHeights(tree))]<-max(nodeHeights(tree))
CI<-max(nodeHeights(tree))-CI
rownames(CI)<-1:tree$Nnode+Ntip(tree)
colnames(CI)<-c("lower","upper")

Wednesday, March 1, 2017

Error bars on divergence times on a phylogeny plotted in R

Today a colleague at Universidad de los Andes (where I'm currently on sabbatical, BTW) asked me how to add error bars for divergence times to the nodes of a plotted phylogeny in R.

Well, as is the case with many things, I bet that there is a ton of different ways to do this; however, the following is one very simple technique.

In the following, I have a "phylo" object tree; and I also have a matrix (CI) containing the times before the present for the lower & upper confidence limits on each estimated divergence time corresponding to the nodes of my tree. Note that these CIs have been simulated to be intentionally assymetric - as such confidence intervals often are:

library(phytools)
## here are the data:
tree
## 
## Phylogenetic tree with 26 tips and 25 internal nodes.
## 
## Tip labels:
##  Z, Y, X, W, V, U, ...
## 
## Rooted; includes branch lengths.
CI
##    lower (MYBP) upper (MYBP)
## 27    111.80101    92.599420
## 28     90.13474    64.370663
## 29     48.45811    28.394226
## 30     42.77937    22.359999
## 31     83.01736    61.962733
## 32     41.48123    18.614973
## 33     26.10956     0.000000
## 34     95.60433    75.950823
## 35     88.41807    68.551308
## 36     93.32274    69.118385
## 37     84.96369    66.249093
## 38     29.45826     6.303606
## 39     72.61971    54.372320
## 40     73.15644    45.563312
## 41     65.70458    37.262456
## 42     35.86271    18.184414
## 43     34.17755    12.586219
## 44     47.11305    26.530968
## 45     50.74606    24.070106
## 46     41.64973    23.878326
## 47     58.47533    35.912961
## 48     13.19029     0.000000
## 49     35.42824    11.007959
## 50     27.73721     6.913533
## 51     15.31440     0.000000

Now let's plot it:

plotTree(tree,xlim=c(110,-5),direction="leftwards",
    mar=c(4.1,1.1,1.1,1.1),ftype="i")
abline(v=seq(0,120,by=10),lty="dashed",
    col=make.transparent("grey",0.5))
axis(1,at=seq(0,120,by=20))
obj<-get("last_plot.phylo",envir=.PlotPhyloEnv)
for(i in 1:tree$Nnode+Ntip(tree))
    lines(x=c(CI[i-Ntip(tree),1],CI[i-Ntip(tree),2]),
        y=rep(obj$yy[i],2),lwd=11,lend=0,
        col=make.transparent("blue",0.4))
points(obj$xx[1:tree$Nnode+Ntip(tree)],
    obj$yy[1:tree$Nnode+Ntip(tree)],pch=19,col="blue",
    cex=1.8)

plot of chunk unnamed-chunk-2

(I added a few embellishments, but it should be straightforward to pick these out.)

I like it.

As with most such plots, it will tend to look better if expored as a PDF.

Note that because my CIs are in time for the present, I decide to plot my tree "leftwards", but then flip my x axis to run from higher to lesser values using the argument value xlim=c(110,-5).

Note also that in the example above I have assumed that the order of the rows of CI matches the order of the node indices in the tree.

These were simulated data, obviously. The code used to simulate these values was as follows:

tree<-pbtree(n=26,tip.label=LETTERS[26:1],scale=100)
h<-sapply(1:tree$Nnode+Ntip(tree),nodeheight,tree=tree)
CI<-cbind(h-runif(n=length(h),min=10,max=20),h+runif(n=length(h),
    min=5,max=10))
CI[CI>max(nodeHeights(tree))]<-max(nodeHeights(tree))
CI<-max(nodeHeights(tree))-CI
rownames(CI)<-1:tree$Nnode+Ntip(tree)
colnames(CI)<-paste(c("lower","upper"),"(MYBP)")