Skip to Content
Course content

164: Phylogenetic Tree Analysis in R

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen to almost everyone starting with phylogenetics in R. You've spent hours running a Maximum Likelihood tree in another program, you've imported your Newick file perfectly, and you've got a beautiful metadata CSV. But the moment you try to map that data onto the tree, everything breaks or, worse, the plot looks correct but the colors are all wrong because the data didn't actually attach.

library(ape)
library(ggtree)
library(ggplot2)

# Load a simple tree
tree <- read.tree(text = "((Species_A:0.1, Species_B:0.2):0.5, Species_C:0.7);")

# Load metadata
metadata <- data.frame(
  label = c("Species A", "Species B", "Species_C"),
  trait = c("Red", "Blue", "Green")
)

# Try to plot and color by trait
ggtree(tree) %<+% metadata + 
  aes(color = trait) + 
  geom_tippoint()

The Ghost of Missing Metadata

If you run the code above, you'll notice something frustrating: Species_C might get colored, but Species_A and Species_B will be missing or default to a NA color. There isn't a crash, just a silent failure. The problem is that ggtree (and the %<+% operator) performs a join based on the label column. In the tree file, the tips are named Species_A (with an underscore), but in the CSV, they are Species A (with a space).

In the world of bioinformatics, different software packages have different rules for "legal" characters. Some tools force underscores; others preserve spaces. When you're bridging the gap between a tree file and a data frame, these tiny string mismatches are the primary source of bugs.

Sanitizing Labels for a Perfect Join

The fix isn't to manually edit your CSV—that's a recipe for human error. Instead, we handle it in R by ensuring both the tree labels and the metadata labels follow the same naming convention. I usually prefer to force everything to a "clean" version using gsub().

# Fix the metadata labels to match the tree's underscore convention
metadata$label <- gsub(" ", "_", metadata$label)

# Now the join will actually find the matches
ggtree(tree) %<+% metadata + 
  aes(color = trait) + 
  geom_tippoint() +
  theme_tree2()

By normalizing the strings, we ensure the join key is identical. If you're dealing with a massive dataset, I'd recommend checking for mismatches explicitly using setdiff(tree$tip.label, metadata$label). If that returns anything, you know you've got a naming problem before you even try to plot.

Calculating Branch Lengths and Divergence

Once the visualization is sorted, you usually need to actually analyze the tree. A common task is determining the evolutionary distance between specific taxa. While you can eyeball the plot, you want the actual numbers for a paper or a report.

The ape package provides dist.phylo, which calculates the distance between all pairs of tips. I find it's often more useful to extract the path length between two specific nodes of interest.

# Get the distance matrix for all tips
dist_matrix <- dist.phylo(tree)
print(dist_matrix["Species_A", "Species_B"])

# To find the distance from the root to a specific tip:
# Sum the branch lengths from root to tip
node_path <- plot.phylo(tree)$node.depth # This is a shortcut via plotting
# Or more robustly using the tree structure:
sum(tree$edge.length[tree$edge[,2] == which(tree$tip.label == "Species_C")])

Refining the Tree Topology Visualization

Standard trees are often hard to read. I usually suggest using geom_tiplab() to ensure names are visible and geom_treescale() so the reader knows what the branch lengths actually represent. Without a scale bar, a phylogenetic tree is just a pretty drawing, not scientific data.

ggtree(tree) %<+% metadata + 
  aes(color = trait) + 
  geom_tippoint(size = 3) + 
  geom_tiplab(offset = 0.01) + 
  geom_treescale() + 
  labs(title = "Trait Distribution Across Taxa")

Notice how theme_tree2() adds the x-axis for branch length. When you're presenting this, always include that axis; it transforms the plot from a cladogram (which only shows relationship) into a phylogram (which shows evolutionary change).




📋 Practical Task

Exercise: Mapping Geographic Origins onto a Viral Phylogenetic Tree

You have been given a Newick string representing a small cluster of viral samples and a data frame containing the geographic origin of each sample. However, the sample IDs in the tree use dots (e.g., Sample.01), while the metadata uses dashes (e.g., Sample-01).

Your Task:

  1. Load the ape and ggtree libraries.
  2. Create the tree object from the following Newick string: "((Sample.01:0.12, Sample.02:0.15):0.2, (Sample.03:0.1, Sample.04:0.11):0.2);"
  3. Create a data frame named geo_data with two columns: label (containing "Sample-01", "Sample-02", "Sample-03", "Sample-04") and region (containing "North", "North", "South", "South").
  4. Clean the label column in geo_data so that it matches the tree's naming convention.
  5. Plot the tree and color the tips based on the region using the %<+% operator.
  6. Add a scale bar to the plot using geom_treescale().
Rating
0 0

There are no comments for now.

to be the first to leave a comment.