Back to Claude Scientific Skills

ETE 4 Workflows

skills/etetoolkit/references/workflows.md

2.55.014.9 KB
Original Source

ETE 4 Workflows

Complete patterns for common ETE 4.4.0 tasks. Adapt parsers and biological assumptions to the actual data source.

Guide Map

  • Validate names, branch lengths, and support before analysis
  • Preprocess and root an existing tree reproducibly
  • Compare rooted or unrooted topologies
  • Join metadata without silently dropping records
  • Detect evolutionary events or reconcile against a species tree
  • Split duplicated gene families
  • Query NCBI or GTDB taxonomy
  • Batch-process and cache large trees
  • Hand off clustering and tree inference to the right upstream tools
  • Match trusted topology patterns

The fenced Python snippets are examples within this Markdown guide, not a single executable program. Run only the section needed for the current task.

1. Validate Before Analysis

Name-based operations and tree comparisons become unreliable when labels are empty or duplicated.

python
from collections import Counter
from pathlib import Path

from ete4 import Tree


def load_and_validate(path: Path, parser=1) -> Tree:
    with path.open(encoding="utf-8") as handle:
        tree = Tree(handle, parser=parser)

    names = list(tree.leaf_names())
    empty = [leaf.id for leaf in tree.leaves() if not leaf.name]
    duplicate_counts = {
        name: count
        for name, count in Counter(names).items()
        if name and count > 1
    }

    if empty:
        raise ValueError(f"{path}: unnamed leaves at positions {empty[:10]}")
    if duplicate_counts:
        raise ValueError(f"{path}: duplicate leaf names {duplicate_counts}")
    if len(names) < 2:
        raise ValueError(f"{path}: expected at least two leaves")

    return tree


tree = load_and_validate(Path("tree.nw"), parser=1)

Also verify that the parsed support and branch-length ranges are plausible for the program that produced the tree:

python
supports = [
    node.support
    for node in tree.traverse()
    if not node.is_leaf and node.support is not None
]
distances = [
    node.dist
    for node in tree.traverse()
    if not node.is_root and node.dist is not None
]

if any(distance < 0 for distance in distances):
    raise ValueError("Negative branch length detected")

print("support range:", (min(supports), max(supports)) if supports else None)
print("distance range:", (min(distances), max(distances)) if distances else None)

Support may be represented as fractions or percentages. Do not apply a threshold before checking its scale.

2. Reproducible Tree Preprocessing

python
from pathlib import Path

from ete4 import Tree

input_path = Path("inferred_tree.nw")
output_path = Path("processed_tree.nw")

with input_path.open(encoding="utf-8") as handle:
    tree = Tree(handle, parser="support")

# Keep a known sample set and preserve retained pairwise distances.
keep = ["sample_A", "sample_B", "sample_C", "outgroup"]
missing = sorted(set(keep) - set(tree.leaf_names()))
if missing:
    raise ValueError(f"Requested leaves are absent: {missing}")

tree.prune(keep, preserve_branch_length=True)

# Prefer a biological outgroup when justified.
tree.set_outgroup(tree["outgroup"])

# Stable presentation order only; this does not alter clade membership.
tree.ladderize()

tree.write(
    outfile=str(output_path),
    parser="support",
    props=[],
)

Record the original input checksum, ETE version, parser, retained sample set, rooting rule, and output parser in an analysis manifest.

3. Midpoint Rooting

Midpoint rooting is useful when a defensible biological outgroup is not available, but it assumes the longest leaf-to-leaf path can approximate a clock-like split.

python
from ete4 import Tree

tree = Tree("((A:1,B:1):1,(C:2,D:2):1);")
tree.set_midpoint_outgroup()
print(tree.write(props=[]))

For auditability:

python
tree = Tree("((A:1,B:1):1,(C:2,D:2):1);")
candidate = tree.get_midpoint_outgroup()
candidate_id = candidate.id
tree.set_outgroup(candidate)
print("midpoint candidate:", candidate_id)

Do not describe midpoint rooting as evidence for the direction of evolution.

4. Compare Two Trees

python
from collections import Counter
from pathlib import Path

from ete4 import Tree


def load(path: Path, parser=1) -> Tree:
    with path.open(encoding="utf-8") as handle:
        return Tree(handle, parser=parser)


def assert_unique_leaf_names(tree: Tree, label: str) -> None:
    counts = Counter(tree.leaf_names())
    duplicates = sorted(name for name, count in counts.items() if count > 1)
    if duplicates:
        raise ValueError(f"{label} has duplicate leaves: {duplicates}")


tree_a = load(Path("method_a.nw"))
tree_b = load(Path("method_b.nw"))
assert_unique_leaf_names(tree_a, "method_a")
assert_unique_leaf_names(tree_b, "method_b")

(
    rf,
    max_rf,
    common,
    edges_a,
    edges_b,
    discarded_a,
    discarded_b,
) = tree_a.robinson_foulds(
    tree_b,
    unrooted_trees=True,
)

print(
    {
        "rf": rf,
        "max_rf": max_rf,
        "normalized_rf": rf / max_rf if max_rf else 0.0,
        "common_leaf_count": len(common),
        "discarded_edges_a": len(discarded_a),
        "discarded_edges_b": len(discarded_b),
    }
)

Checklist:

  • Rooted and unrooted RF answer different questions.
  • ETE compares the intersection when leaf sets differ; report its size.
  • Duplicate labels violate the usual tip-identity assumption.
  • Support filtering and polytomy expansion materially change results.
  • A high RF distance does not explain which biological split is preferable.

For a higher-level result:

python
summary = tree_a.compare(tree_b, unrooted=True)
print(summary["rf"], summary["max_rf"], summary["norm_rf"])

Keep labels unique. Do not use Tree.compare(has_duplications=True) in ETE 4.4.0; upstream marks that TreeKO branch as likely broken. The packaged ete4 compare CLI also fails because it still passes the removed format= keyword, so use the methods above or scripts/tree_operations.py compare.

5. Annotate a Tree from a Metadata Table

Keep parsing and schema validation explicit.

python
import csv
from pathlib import Path

from ete4 import Tree

tree = Tree("((sample_1,sample_2),sample_3);")

metadata: dict[str, dict[str, str]] = {}
with Path("metadata.tsv").open(encoding="utf-8", newline="") as handle:
    reader = csv.DictReader(handle, delimiter="\t")
    required = {"sample", "host", "location"}
    if not reader.fieldnames or not required.issubset(reader.fieldnames):
        raise ValueError(f"metadata.tsv must contain columns {sorted(required)}")
    for row in reader:
        sample = row["sample"].strip()
        if not sample or sample in metadata:
            raise ValueError(f"Empty or duplicate metadata key: {sample!r}")
        metadata[sample] = {
            "host": row["host"].strip(),
            "location": row["location"].strip(),
        }

unmatched_tree = []
for leaf in tree.leaves():
    values = metadata.get(leaf.name)
    if values is None:
        unmatched_tree.append(leaf.name)
        continue
    leaf.add_props(**values)

unmatched_metadata = sorted(set(metadata) - set(tree.leaf_names()))
if unmatched_tree or unmatched_metadata:
    raise ValueError(
        f"Unmatched tree leaves={unmatched_tree}; "
        f"unmatched metadata rows={unmatched_metadata}"
    )

tree.write(
    outfile="annotated.nhx",
    props=["host", "location"],
)

Use an explicit props list when exporting. props=None writes every available property and may disclose internal annotations unintentionally.

6. Species-Overlap Event Detection

python
from ete4 import PhyloTree

gene_tree = PhyloTree(
    "((Hsa|gene1,Ptr|gene1),(Hsa|gene2,Mmu|gene1));",
    sp_naming_function=lambda name: name.split("|", 1)[0],
)

events = gene_tree.get_descendant_evol_events(sos_thr=0.0)
for event in events:
    relationship = {
        "S": "orthology",
        "D": "paralogy",
    }[event.etype]
    print(
        relationship,
        sorted(event.in_seqs),
        sorted(event.out_seqs),
    )

for node in gene_tree.traverse():
    event_type = node.props.get("evoltype")
    if event_type:
        print(node.id, event_type)

Interpretation limits:

  • The gene tree must be rooted and fully bifurcating.
  • Pass the species naming function explicitly; ETE 4.4.0 defaults it to None.
  • Results depend on the topology and the species naming function.
  • A single shared species triggers duplication at sos_thr=0.0.
  • Gene loss, incomplete lineage sorting, horizontal transfer, and tree error can violate a simple species-overlap interpretation.
  • Keep inferred events separate from curated orthology evidence.

7. Gene-Tree/Species-Tree Reconciliation

python
from ete4 import PhyloTree

gene_newick = (
    "((Dme_001,Dme_002),"
    "(((Cfa_001,Mms_001),((Hsa_001,Ptr_001),Mmu_001)),"
    "(Ptr_002,(Hsa_002,Mmu_002))));"
)
species_newick = "((((Hsa,Ptr),Mmu),(Mms,Cfa)),Dme);"

species_from_gene = lambda name: name.split("_", 1)[0]

gene_tree = PhyloTree(
    gene_newick,
    sp_naming_function=species_from_gene,
)
species_tree = PhyloTree(
    species_newick,
    sp_naming_function=lambda name: name,
)

gene_species = {leaf.species for leaf in gene_tree.leaves()}
species_tree_tips = set(species_tree.leaf_names())
missing = sorted(gene_species - species_tree_tips)
if missing:
    raise ValueError(f"Species absent from species tree: {missing}")

reconciled_tree, events = gene_tree.reconcile(species_tree)

for event in events:
    if event.etype == "S":
        print("orthology", event.inparalogs, event.orthologs)
    elif event.etype == "D":
        print("paralogy", event.inparalogs, event.outparalogs)

reconciled_tree.write(
    outfile="reconciled.nhx",
    props=["evoltype"],
)

Reconciliation assumes the species tree and species mapping are correct. State the species-tree source and treatment of uncertain branches.

8. Split Duplicated Gene Families

python
from pathlib import Path

from ete4 import PhyloTree

tree = PhyloTree(
    "((Human_1,Chimp_1),(Human_2,(Chimp_2,Mouse_1)));",
    sp_naming_function=lambda name: name.split("_", 1)[0],
)

output_dir = Path("subfamilies")
output_dir.mkdir(parents=True, exist_ok=True)

for index, subtree in enumerate(tree.split_by_dups(), start=1):
    subtree.write(
        outfile=str(output_dir / f"subfamily_{index:03d}.nw"),
        props=[],
    )

For TreeKO-style enumeration, get_speciation_trees() can generate very many topologies. Consider newick_only=True, monitor output size, and define a scientifically justified limit before materializing results.

python
tree_count, duplication_count, newicks = tree.get_speciation_trees(
    newick_only=True,
)
for newick in newicks:
    process(newick)

9. NCBI and GTDB Taxonomy

NCBI topology:

python
from ete4 import NCBITaxa

ncbi = NCBITaxa()
taxids = [9606, 9598, 10090]
tree = ncbi.get_topology(taxids, intermediate_nodes=False, annotate=True)
print(tree.to_str(props=["sci_name", "rank", "taxid"]))

GTDB topology:

python
from ete4 import GTDBTaxa

gtdb = GTDBTaxa()
taxa = ["p__Huberarchaeota", "o__Peptococcales", "f__Korarchaeaceae"]
tree = gtdb.get_topology(taxa, intermediate_nodes=True, annotate=True)
print(tree.to_str(props=["sci_name", "rank"]))

See taxonomy.md before downloading, updating, or annotating production data.

10. Batch Processing

python
from pathlib import Path

from ete4 import Tree

input_dir = Path("trees")
output_dir = Path("processed")
output_dir.mkdir(parents=True, exist_ok=True)

for input_path in sorted(input_dir.glob("*.nw")):
    with input_path.open(encoding="utf-8") as handle:
        tree = Tree(handle, parser="support")

    names = list(tree.leaf_names())
    if len(names) != len(set(names)):
        raise ValueError(f"{input_path}: duplicate leaf names")

    tree.set_midpoint_outgroup()
    tree.ladderize()

    output_path = output_dir / input_path.name
    tree.write(
        outfile=str(output_path),
        parser="support",
        props=[],
    )

Do not catch and discard parse exceptions in a batch. Fail with the source path, or record failures in a structured report and return a nonzero status.

11. Large Trees

Prefer generators:

python
for leaf in tree.leaves():
    process(leaf)

Materialize only when indexing, sorting, or repeated traversal requires it:

python
leaves = list(tree.leaves())

Cache descendant content for repeated clade calculations:

python
leaf_cache = tree.get_cached_content(prop="name")
for node in tree.traverse("postorder"):
    descendant_names = leaf_cache[node]
    summarize(node, descendant_names)

Use SmartView for adaptive exploration. Static rendering of every label on a very large tree is usually unreadable and expensive; use collapsed-node layouts or render selected subtrees.

12. Clustering Dendrograms

ETE 4.4.0 does not export ETE 3's ClusterTree. For a clustering workflow:

  1. Compute linkage and validation metrics with SciPy or another maintained clustering package.
  2. Convert the resulting hierarchy to Newick or construct an ETE Tree.
  3. Store cluster labels or statistics as node properties.
  4. Use SmartView layouts to display those properties.

Do not claim that ETE 4 calculated silhouette, Dunn, or matrix-linked ClusterTree metrics unless another library actually performed those steps.

13. End-to-End Phylogenomic Handoff

A defensible division of labor is:

  1. Perform sequence quality control outside ETE.
  2. Align sequences with MAFFT or a domain-appropriate aligner.
  3. Infer topology/support with IQ-TREE 2, FastTree, or another explicit method.
  4. Load the inferred Newick into ETE with the correct parser.
  5. Validate tip identity and support scale.
  6. Root, prune, annotate, compare, or reconcile in ETE.
  7. Render and export with explicit properties.
  8. Report both inference-tool settings and ETE transformation settings.

ETE manipulates and interprets the supplied topology; it does not make upstream model choice, alignment quality, or sampling bias disappear.

14. Match Repeated Topologies

Use a topology-only TreePattern when the structural pattern is easier to state as a small tree:

python
from ete4 import Tree
from ete4.treematcher import TreePattern

tree = Tree("((K,((A,B),C),D),(E,F));")
pattern = TreePattern("(,,)", safer=True)

for match in pattern.search(tree):
    print("three-child node:", match.id, match.name)

The matcher tries child permutations, so sibling order does not prevent a topological match. Expression-bearing patterns are executable conditions: never construct them from user input, imported data, model output, or other untrusted text. Prefer topology-only patterns or explicit traversal predicates for dynamic criteria.

Upstream References