Skip to content

Tutorial: comparing two trees (tanglegrams)

A tanglegram faces two trees of the same taxa at each other and joins the shared tips, so disagreements show up as crossing links. It is the standard picture for questions like "does the transcriptome tree agree with the genome tree?", "do these two inference methods give the same answer?" or "do the parasites track their hosts?".

import phytreon as pt

genome = pt.build_tree("genome.fasta", method="nj")
transcriptome = pt.build_tree("transcriptome.fasta", method="nj")

fig = pt.TangleFigure(genome, transcriptome,
                      titles=("genome", "transcriptome"))
fig.untangle()                              # line the tips up first
fig.connect(highlight_discordant=True)      # red = this taxon disagrees
fig.save("tanglegram.pdf")

Untangle before you interpret

Rotating a node swaps the order of its children. That slides a clade up or down the page without changing the topology or any branch length — the tree says exactly the same thing, it just reads differently. So the raw number of crossings is partly an artefact of how each tree happened to be drawn.

untangle() hill-climbs over single rotations to remove the crossings that are mere drawing accidents; whatever still crosses afterwards is real disagreement.

fig = pt.TangleFigure(t1, t2)
print(fig.crossings())          # e.g. 111  -- mostly drawing artefact
fig.untangle()
print(fig.crossings())          # e.g. 17   -- real conflict
fix effect
"left" (default) keep the left tree's tip order, rotate only the right
"right" the reverse
None alternate between the two; usually untangles further, but neither tree keeps its original order

Zero crossings does not mean the trees are identical

Crossings measure tip order. Two trees can resolve splits differently and still admit one common tip order, giving zero crossings. Always read the Robinson-Foulds distance alongside it:

pt.robinson_foulds(t1, t2, normalized=True)   # rotation-independent
pt.crossing_number(t1, t2)                    # what the plot shows

In examples/tanglegram_demo.py, neighbour joining and parsimony untangle to 0 crossings while RF is still 0.067 — they agree on the order, not on every split.

Styling each side

fig.left and fig.right are ordinary TreeFigure objects, so every element works on either tree:

fig.left.tip_points(color="phylum", size=6)
fig.right.support_labels()
fig.left.highlight(node=mrca, fill="#eef3f8")

Pass ready-made figures when you want full control of both sides:

left = pt.TreeFigure(t1).tip_labels(italic=True).tip_points(color="host")
right = pt.TreeFigure(t2).tip_points(color="host")
fig = pt.TangleFigure(left, right)
fig.connect(color="#b0b0b0", width=0.7, dash="dot")   # plain styling
fig.connect(color="phylum")                           # colour by a data column
fig.connect(highlight_discordant=True)                # red = crossing link

color= accepts a column on the left tree's tips and emits a legend, which answers a different question from highlight_discordant: not which taxa move, but whether the movement is confined to one group or cuts across the tree.

Layout knobs

Tip labels and links share the middle band. Text is measured in points and the tree in data units, and the two are only related once the figure exists, so the band is sized automatically from the longest tip label — long species names widen it rather than overrunning the links. Three knobs override that:

  • tip_labels="both" (default), "left", "right" or False. Both sides are named by default so each tree can be read on its own.
  • gap= — set the band yourself, as a fraction of the two trees' combined depth, instead of the automatic estimate.
  • connect(inset=(left, right)) — fraction of the band to keep clear at each end. (0, 0) runs the links tip to tip.

The automatic estimate assumes the default figure width; if you pass a very different figsize to save(), set gap= too.

Trees with only partly overlapping taxa are fine: unmatched tips are still drawn, just left unlinked.

fig = pt.TangleFigure(t1, t2, tip_labels="left", gap=0.8)