Files

15 KiB

title, task, lineage_type, upstream_source, upstream_sha, imported_at, prompt_class, upstream_changes, author, validated
title task lineage_type upstream_source upstream_sha imported_at prompt_class upstream_changes author validated
Tree Building and Analysis import https://github.com/mims-harvard/ToolUniverse/blob/e2520a96/skills/tooluniverse-phylogenetics/references/tree_building.md e2520a96 2026-06-26 prompt accepted upstream false

Tree Building and Analysis

Detailed guide for phylogenetic tree construction and analysis using Biopython, PhyKIT, and DendroPy.


Tree File Loading

Auto-detect Tree Format

from Bio import Phylo
import io

def load_tree(filepath):
    """Load tree with auto-format detection.

    Supports: Newick, Nexus
    """
    with open(filepath, 'r') as f:
        content = f.read().strip()

    # Try Newick first (most common)
    try:
        tree = Phylo.read(io.StringIO(content), 'newick')
        return tree, 'newick'
    except Exception:
        pass

    # Try Nexus
    try:
        tree = Phylo.read(io.StringIO(content), 'nexus')
        return tree, 'nexus'
    except Exception:
        pass

    raise ValueError(f"Cannot parse tree: {filepath}")

Usage

# Load any tree format
tree, format_detected = load_tree("gene1.nwk")
print(f"Format: {format_detected}")
print(f"Terminals: {tree.count_terminals()}")

PhyKIT Tree Metrics

Treeness

from phykit.services.tree.treeness import Treeness
from types import SimpleNamespace

def phykit_treeness(tree_file):
    """Calculate treeness (internal branch length / total branch length).

    Treeness measures the proportion of tree length on internal branches.
    Higher values (closer to 1) indicate stronger phylogenetic signal.

    Returns: float (0-1)
    """
    t = Treeness(SimpleNamespace(tree=tree_file))
    tree = t.read_tree_file()
    return t.calculate_treeness(tree)

Tree Length

from phykit.services.tree.total_tree_length import TotalTreeLength

def phykit_tree_length(tree_file):
    """Calculate total tree length (sum of all branch lengths).

    Returns: float
    """
    tl = TotalTreeLength(SimpleNamespace(tree=tree_file))
    tree = tl.read_tree_file()
    return tl.calculate_total_tree_length(tree)

Evolutionary Rate

from phykit.services.tree.evolutionary_rate import EvolutionaryRate

def phykit_evolutionary_rate(tree_file):
    """Calculate evolutionary rate (total branch length / number of terminals).

    Represents average substitution rate per lineage.

    Returns: float
    """
    er = EvolutionaryRate(SimpleNamespace(tree=tree_file))
    tree = er.read_tree_file()
    total_bl = tree.total_branch_length()
    num_terminals = tree.count_terminals()
    return total_bl / num_terminals

Degree of Violation of Molecular Clock (DVMC)

from phykit.services.tree.dvmc import DVMC

def phykit_dvmc(tree_file):
    """Calculate Degree of Violation of Molecular Clock.

    DVMC is the standard deviation of root-to-tip distances.
    Lower values indicate tree is more clock-like.

    Returns: float
    """
    d = DVMC(SimpleNamespace(tree=tree_file))
    tree = d.read_tree_file()
    return d.determine_dvmc(tree)

Treeness/RCV

def phykit_treeness_over_rcv(tree_file, aln_file):
    """Calculate treeness/RCV ratio.

    Combines phylogenetic signal (treeness) with compositional uniformity (RCV).
    Higher values indicate better phylogenetic quality.

    Returns: (treeness_over_rcv, treeness, rcv)
    """
    treeness = phykit_treeness(tree_file)
    rcv = phykit_rcv(aln_file)

    if rcv == 0:
        return float('inf'), treeness, rcv

    return treeness / rcv, treeness, rcv

Distance-Based Tree Construction

Neighbor-Joining (NJ)

from Bio.Phylo.TreeConstruction import DistanceCalculator, DistanceTreeConstructor

def build_nj_tree(aln_file, model='identity'):
    """Build Neighbor-Joining tree from alignment.

    NJ is fast and reasonably accurate for most cases.

    Args:
        aln_file: path to alignment file
        model: distance model ('identity' or 'blosum62')

    Returns: Biopython Phylo.BaseTree
    """
    alignment, _ = load_alignment(aln_file)
    calculator = DistanceCalculator(model)
    dm = calculator.get_distance(alignment)
    constructor = DistanceTreeConstructor()
    return constructor.nj(dm)

UPGMA

def build_upgma_tree(aln_file, model='identity'):
    """Build UPGMA tree from alignment.

    UPGMA assumes molecular clock. Only use for ultrametric data.

    Args:
        aln_file: path to alignment file
        model: distance model ('identity' or 'blosum62')

    Returns: Biopython Phylo.BaseTree
    """
    alignment, _ = load_alignment(aln_file)
    calculator = DistanceCalculator(model)
    dm = calculator.get_distance(alignment)
    constructor = DistanceTreeConstructor()
    return constructor.upgma(dm)

Saving Trees

# Build tree
tree = build_nj_tree("alignment.fa")

# Save as Newick
Phylo.write(tree, "tree.nwk", "newick")

# Save as Nexus
Phylo.write(tree, "tree.nex", "nexus")

Maximum Parsimony Tree Construction

from Bio.Phylo.TreeConstruction import ParsimonyScorer, NNITreeSearcher, ParsimonyTreeConstructor

def build_parsimony_tree(aln_file):
    """Build Maximum Parsimony tree from alignment.

    Uses Nearest Neighbor Interchange (NNI) search.
    Best for small alignments (<50 sequences).

    Returns: Biopython Phylo.BaseTree
    """
    alignment, _ = load_alignment(aln_file)

    # Start with NJ tree
    calculator = DistanceCalculator('identity')
    dm = calculator.get_distance(alignment)
    constructor = DistanceTreeConstructor()
    starting_tree = constructor.nj(dm)

    # Parsimony search
    scorer = ParsimonyScorer()
    searcher = NNITreeSearcher(scorer)
    pars_constructor = ParsimonyTreeConstructor(searcher, starting_tree)

    return pars_constructor.build_tree(alignment)

Getting Parsimony Score

from Bio.Phylo.TreeConstruction import ParsimonyScorer

def get_parsimony_score(tree, alignment):
    """Calculate parsimony score for a tree.

    Lower scores indicate fewer required substitutions.

    Returns: int (total substitutions)
    """
    scorer = ParsimonyScorer()
    return scorer.get_score(tree, alignment)

Bootstrap Analysis

Bootstrap Support

from Bio.Phylo.Consensus import bootstrap_consensus, bootstrap_trees, majority_consensus

def bootstrap_analysis(aln_file, n_replicates=100, model='identity'):
    """Perform bootstrap analysis on alignment.

    Generates bootstrap replicates and builds consensus tree.

    Args:
        aln_file: path to alignment
        n_replicates: number of bootstrap replicates (default 100)
        model: distance model

    Returns: consensus tree with support values
    """
    alignment, _ = load_alignment(aln_file)
    calculator = DistanceCalculator(model)
    constructor = DistanceTreeConstructor(calculator, 'nj')

    # Generate bootstrap trees
    trees = list(bootstrap_trees(alignment, n_replicates, constructor))

    # Build majority consensus
    consensus = majority_consensus(trees, cutoff=0.5)

    return consensus

Extracting Bootstrap Support Values

def extract_bootstrap_support(tree_file):
    """Extract bootstrap support values from internal nodes.

    Returns: dict with support statistics
    """
    tree, _ = load_tree(tree_file)

    supports = []
    for clade in tree.get_nonterminals():
        if clade.confidence is not None:
            supports.append(clade.confidence)

    if not supports:
        return {'supports': [], 'mean': None, 'median': None}

    return {
        'supports': supports,
        'mean': float(np.mean(supports)),
        'median': float(np.median(supports)),
        'min': float(np.min(supports)),
        'max': float(np.max(supports)),
        'n_nodes': len(supports),
        'above_70': sum(1 for s in supports if s >= 70),
        'above_90': sum(1 for s in supports if s >= 90),
    }

Branch Length Analysis

Branch Statistics

def tree_branch_stats(tree_file):
    """Compute branch length statistics from a tree.

    Returns: dict with branch statistics
    """
    tree, _ = load_tree(tree_file)

    internal_lengths = []
    terminal_lengths = []

    for clade in tree.find_clades():
        if clade.branch_length is not None:
            if clade.is_terminal():
                terminal_lengths.append(clade.branch_length)
            else:
                internal_lengths.append(clade.branch_length)

    all_lengths = internal_lengths + terminal_lengths

    return {
        'total_length': sum(all_lengths),
        'n_internal': len(internal_lengths),
        'n_terminal': len(terminal_lengths),
        'internal_sum': sum(internal_lengths),
        'terminal_sum': sum(terminal_lengths),
        'mean_branch': np.mean(all_lengths) if all_lengths else 0,
        'max_branch': max(all_lengths) if all_lengths else 0,
        'min_branch': min(all_lengths) if all_lengths else 0,
    }

Example

stats = tree_branch_stats("tree.nwk")

print(f"Total length: {stats['total_length']:.4f}")
print(f"Internal branches: {stats['n_internal']}")
print(f"Terminal branches: {stats['n_terminal']}")
print(f"Mean branch length: {stats['mean_branch']:.4f}")

Tree Comparison

Robinson-Foulds Distance

import dendropy
from dendropy.calculate import treecompare

def robinson_foulds_distance(tree_file1, tree_file2):
    """Calculate Robinson-Foulds distance between two trees.

    RF distance measures topological difference between trees.
    0 = identical topology, higher = more different.

    Returns: int (number of differing splits)
    """
    tree1 = dendropy.Tree.get(path=tree_file1, schema="newick")
    tree2 = dendropy.Tree.get(
        path=tree_file2,
        schema="newick",
        taxon_namespace=tree1.taxon_namespace  # Must share namespace
    )

    return treecompare.symmetric_difference(tree1, tree2)

Weighted Robinson-Foulds

def weighted_robinson_foulds(tree_file1, tree_file2):
    """Calculate weighted RF distance (includes branch lengths)."""
    tree1 = dendropy.Tree.get(path=tree_file1, schema="newick")
    tree2 = dendropy.Tree.get(
        path=tree_file2,
        schema="newick",
        taxon_namespace=tree1.taxon_namespace
    )

    return treecompare.weighted_robinson_foulds_distance(tree1, tree2)

Batch Processing

Tree Metric Batching

def batch_treeness(gene_files):
    """Compute treeness for all genes."""
    return batch_compute_metric(gene_files, phykit_treeness, requires='tree')

def batch_tree_length(gene_files):
    """Compute tree length for all genes."""
    return batch_compute_metric(gene_files, phykit_tree_length, requires='tree')

def batch_evolutionary_rate(gene_files):
    """Compute evolutionary rate for all genes."""
    return batch_compute_metric(gene_files, phykit_evolutionary_rate, requires='tree')

def batch_dvmc(gene_files):
    """Compute DVMC for all genes."""
    return batch_compute_metric(gene_files, phykit_dvmc, requires='tree')

def batch_treeness_over_rcv(gene_files):
    """Compute treeness/RCV for all genes (requires both tree and alignment)."""
    results = {}
    for entry in gene_files:
        if 'tree_file' in entry and 'aln_file' in entry:
            try:
                results[entry['gene_id']] = phykit_treeness_over_rcv(
                    entry['tree_file'], entry['aln_file']
                )
            except Exception:
                pass
    return results

Decision Guide: Which Tree Method?

For Exploratory Analysis

Use Neighbor-Joining (NJ):

  • Fast and reasonably accurate
  • No assumptions about clock-like evolution
  • Good for initial tree estimation
tree = build_nj_tree("alignment.fa")

For Clock-Like Data

Use UPGMA:

  • Assumes molecular clock (equal evolutionary rates)
  • Faster than NJ
  • Only if data truly clock-like
tree = build_upgma_tree("alignment.fa")

For Small Datasets

Use Maximum Parsimony:

  • Good for discrete characters
  • Interpretable (minimum substitutions)
  • Slow for large datasets
tree = build_parsimony_tree("alignment.fa")

For Production Analysis

Use External Tools:

  • IQ-TREE: Fast Maximum Likelihood, automatic model selection
  • RAxML: Maximum Likelihood, bootstrap support
  • PhyML: Fast ML for moderate datasets
  • MrBayes: Bayesian inference

Then analyze resulting trees with this skill:

# After running IQ-TREE externally
treeness = phykit_treeness("iqtree_output.treefile")
dvmc = phykit_dvmc("iqtree_output.treefile")

Common Patterns

Pattern: Median Treeness

# Question: "What is the median treeness for fungi?"

# 1. Discover and compute
fungi_genes = discover_gene_files("data/fungi")
fungi_treeness = batch_treeness(fungi_genes)

# 2. Extract values
treeness_values = list(fungi_treeness.values())

# 3. Compute median
median_treeness = np.median(treeness_values)

print(f"Median treeness: {median_treeness:.4f}")

Pattern: Percentage Above Threshold

# Question: "What percentage of trees have treeness > 0.5?"

treeness_values = list(batch_treeness(gene_files).values())
above_threshold = sum(1 for v in treeness_values if v > 0.5)
percentage = (above_threshold / len(treeness_values)) * 100

print(f"Percentage > 0.5: {percentage:.2f}%")

Pattern: Fold Change

# Question: "What is the fold change in median tree length (fungi / animals)?"

fungi_lengths = list(batch_tree_length(fungi_genes).values())
animal_lengths = list(batch_tree_length(animal_genes).values())

fold_change = np.median(fungi_lengths) / np.median(animal_lengths)

print(f"Fold change: {fold_change:.4f}")

Troubleshooting

"Cannot parse tree" Error

Cause: Format detection failed or malformed Newick.

Solution: Check Newick syntax:

# Valid Newick must end with semicolon
# (A:0.1,B:0.2,(C:0.3,D:0.4):0.5);

Missing Branch Lengths

Issue: Tree has no branch lengths for PhyKIT metrics.

Solution: PhyKIT requires branch lengths. Rebuild tree with distances:

tree = build_nj_tree("alignment.fa")  # NJ always has branch lengths

Bootstrap Values Not Found

Issue: extract_bootstrap_support() returns empty list.

Solution: Check if tree has confidence values:

tree, _ = load_tree("tree.nwk")
for clade in tree.get_nonterminals():
    print(f"Confidence: {clade.confidence}")

If None, tree doesn't have bootstrap support. Run bootstrap analysis:

consensus = bootstrap_analysis("alignment.fa", n_replicates=100)

See Also

  • sequence_alignment.md - Alignment analysis
  • parsimony_analysis.md - Statistical workflows
  • scripts/tree_statistics.py - Implementation code