--- title: "Summarizing tree samples with ConsTree" output: rmarkdown::html_vignette bibliography: ../inst/REFERENCES.bib csl: ../inst/apa.csl vignette: > %\VignetteIndexEntry{Summarizing tree samples with ConsTree} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 2.4, fig.align = "center") ``` 'ConsTree' condenses a collection of phylogenetic trees -- a bootstrap or Bayesian posterior sample, perhaps -- into a single summary tree. It implements two categories of method: (i) those that retain groupings based on a voting rule; and (ii) summary trees selected based on their distance from other trees in the sample. ```{r load, message = FALSE} # Load packages with: library("ConsTree") library("TreeTools", quietly = TRUE) ``` ## Split-selection methods The split-selection methods differ only in *which* groupings they keep, so they form a nested sequence of increasing resolution. To see this, take seven trees that share a backbone but disagree on the placement of a few leaves: ```{r exemplar} trees <- ape::read.tree(text = c( "((((((t1,t3),t2),t4),(t5,t6)),t7),(t8,t9));", "((((((t1,t3),(t5,t6)),t4),t2),t7),(t8,t9));", "((((((t1,t2),t3),t4),(t5,t6)),t7),(t8,t9));", "(((((t1,(t2,t3)),(t5,t6)),t4),t7),(t8,t9));", "((((((t1,t2),t3),t4),(t7,(t5,t6))),t9),t8);", "((((((t1,t3),t2),(t5,t6)),t4),t7),(t8,t9));", "((((t1,((t2,t3),(t5,t6))),t4),(t8,t9)),t7);")) ``` ```{r palette} # Colour leaf labels consistently nTip <- 9 leafCol <- setNames(hcl.colors(nTip + 1), TipLabels(nTip + 1)) plotCons <- function(tree, main = "") { plot(tree, tip.color = leafCol[tree$tip.label], main = main, font = 2, cex = 1, edge.width = 1.5) } ``` ```{r plot-inputs, fig.height = 4.6} oldPar <- par(mfrow = c(2, 4), mar = c(0.5, 0.5, 1.5, 0.5)) for (i in seq_along(trees)) plotCons(trees[[i]], main = paste("Tree", i)) par(oldPar) ``` Each method retains a superset of the groupings kept by the one before it: ```{r plot-gradient, fig.height = 5} oldPar <- par(mfrow = c(2, 2), mar = c(0.5, 0.5, 1.5, 0.5)) plotCons(Strict(trees), "Strict") plotCons(Majority(trees), "Majority-rule") plotCons(Frequency(trees), "Frequency difference") plotCons(Greedy(trees), "Greedy (extended majority)") par(oldPar) ``` ```{r resolution} data.frame( method = c("Strict", "Majority", "Frequency", "Greedy"), splits = c(NSplits(Strict(trees)), NSplits(Majority(trees)), NSplits(Frequency(trees)), NSplits(Greedy(trees))) ) ``` `Strict()` keeps only the two groupings that occur in every tree; `Majority()` adds those that occur in more than half; `Frequency()` keeps any grouping that beats every grouping it conflicts with; and `Greedy()` adds compatible groupings, most frequent first, giving the most resolved summary. Simply counting splits overlooks the possibility that some splits are more heavily contradicted by other trees. `Loose()` (the semi-strict, or combinable-component, consensus) keeps every grouping that no tree contradicts, whereas `MajorityPlus()` keeps any grouping that is displayed by more trees than contradict it. ## Rooted methods `Adams()`, `RStar()` and `Local()` treat the input as **rooted** and reason about clusters and rooted triplets rather than unrooted splits. They may therefore recover structure that an unrooted strict consensus would collapse: ```{r rooted} rTrees <- ape::read.tree(text = c( "((((t1,t2),t3),(((t5,t6),t4),t7)),t8);", "(((((t1,t2),t3),(t5,(t6,t4))),t7),t8);", "(((((t1,t2),t3),((t5,t4),t6)),t7),t8);", "((((((t1,t2),t4),(t5,t6)),t3),t7),t8);", "((((((t1,t2),t3),t4),(t5,t6)),t7),t8);", "(((((t1,t2),t4),(t3,(t5,t6))),t7),t8);")) ``` ```{r plot-rooted-in, fig.height = 4.6} oldPar <- par(mfrow = c(2, 3), mar = c(0.5, 0.5, 1.5, 0.5)) for (i in seq_along(rTrees)) plotCons(rTrees[[i]], main = paste("Tree", i)) par(oldPar) ``` The unrooted strict consensus keeps a single grouping, but the rooted methods recover more: ```{r plot-rooted-out, fig.height = 5} oldPar <- par(mfrow = c(2, 2), mar = c(0.5, 0.5, 1.5, 0.5)) plotCons(Strict(rTrees), "Strict") plotCons(Adams(rTrees), "Adams") plotCons(RStar(rTrees), "RStar") plotCons(Local(rTrees), "Local") par(oldPar) ``` ```{r rooted-splits} data.frame( method = c("Strict", "Adams", "RStar", "Local"), splits = c(NSplits(Strict(rTrees)), NSplits(Adams(rTrees)), NSplits(RStar(rTrees)), NSplits(Local(rTrees))) ) ``` An `Adams()` consensus may display groupings that no input tree contains. `RStar()` keeps each rooted triplet that wins a plurality over both alternatives; `Local()` returns the minimum local consensus of the shared triplets (limited to 20 leaves, and best suited to congruent samples). ## Distance and branch-length summaries Maximizing the counts of individual groupings is equivalent to finding the tree with the minimum total Robinson--Foulds distance to all input trees. As the Robinson--Foulds distance exhibits shortcomings [@Smith2020], other distance measures potentially provide more instructive summary trees. `Quartet()` is the quartet-distance analogue, seeking (an approximation to) the tree that minimizes the total quartet distance to the inputs instead [@Takazawa2026]. Because the quartet distance gives extra weight to deep branches, the result is often more resolved than the majority-rule tree: ```{r quartet} c(majority = NSplits(Majority(trees)), quartet = NSplits(Quartet(trees))) ``` `median.multiPhylo()` in the ['TreeDist'](https://ms609.github.io/TreeDist/) package allows such a median to be selected from within a sample of trees under any metric; unlike the previous approaches, this is restricted to tree topologies within the sample. When the trees carry edge lengths, two further summaries become available. `Average()` returns the tree whose path-length (patristic) distances best match the average of the input distance matrices, while `BHVMean()` computes the Fréchet mean in Billera–Holmes–Vogtmann treespace, with branch lengths; `BHVDistance()`, `BHVPairwiseDistances()` and `BHVVariance()` provide the underlying geodesic distances and dispersion. ```{r branch-length, fig.height = 4.6} blTrees <- ape::read.tree(text = c( "(((t1:0.64,t2:0.84):0.52,(t3:0.84,t4:0.87):0.42):0.46,(t5:0.68,t6:0.34):0.70,t7:0.42);", "(((t1:0.64,t2:0.40):0.72,(t3:0.87,t4:0.38):0.87):0.41,(t5:0.58,t6:0.63):0.80,t7:0.63);", "(((t1:0.53,t2:0.50):0.78,(t3:0.66,t4:0.37):0.66):0.40,(t5:0.65,t6:0.68):0.48,t7:0.61);", "(((t1:0.48,t2:0.47):0.31,(t3:0.46,t4:0.73):0.79):0.65,(t5:0.87,t6:0.34):0.84,t7:0.75);", "(((t1:0.85,t2:0.47):0.71,(t4:0.72,(t5:0.78,t6:0.87):0.62):0.36):0.42,t3:0.37,t7:0.46);")) meanTree <- BHVMean(blTrees) oldPar <- par(mfrow = c(2, 3), mar = c(0.5, 0.5, 1.5, 0.5)) for (i in seq_along(blTrees)) plotCons(blTrees[[i]], main = paste("Tree", i)) plotCons(meanTree, main = "BHV mean") par(oldPar) ``` ```{r bhv-variance} BHVVariance(blTrees, mean = meanTree) ``` The mean recovers the shared topology. The two groupings that every tree displays keep their full length; the two that the fifth tree contradicts exhibit shorter branches. # Method selection The most appropriate method in a particular circumstance depends on the value of precision versus accuracy; trees that resolve more groupings are progressively less likely to be accurate [@Smith2019]. By omitting unstable leaves from the input trees, perhaps via the ['Rogue'](https://ms609.github.io/Rogue/) package, it is possible to improve the resolution of consensus trees by reconciling groupings that only differ in the position of rogue taxa [@Smith2022r]. # References