Skip to main content

Phylogenetic trees and publication figures in Python

Project description

phytreon logo

Phylogenetic trees and publication-quality figures in Python.

phytreon combines tree inference, metadata-aware visualization, and static/interactive figure export in a fluent, pure-Python workflow.

MIT license Python 3.9+ CI status PyPI version Documentation Pure Python matplotlib + plotly backends Bioinformatics

Why phytreon?

🌿 Fluent TreeFigure builder
Compose a figure by chaining visual layers onto a tree.
🎨 Static + interactive backends
matplotlib for PDF/SVG/PNG, plotly for interactive HTML.
🧬 Sequence-to-tree pipeline
One call: align → trim → infer → bootstrap.
🧫 Metadata rings / heatmaps / tracks
Annotate tips with rings, heatmaps, bars, alignments.
🌳 ML / parsimony / NJ inference
Pure-Python likelihood, parsimony, and distance trees.
📄 Publication-ready exports
Vector PDF/SVG, raster PNG, or interactive HTML.

Circular microbial tree with domain, phylum, and length metadata rings

A circular 16S rRNA tree of common microbes (real NCBI data bundled in examples/), with domain / phylum / length rings — built and drawn entirely in phytreon.


Quickstart

pip install phytreon                  # from PyPI
pip install phytreon[interactive]     # + plotly (interactive HTML backend)

Developing locally (from a clone of this repo):

pip install -e .                 # core (numpy, scipy, pandas, matplotlib, biopython)
pip install -e .[interactive]    # + plotly (interactive HTML backend)
pip install -e .[dev]            # + pytest, plotly
import phytreon as pt

tr = pt.datasets.primates()                      # a small illustrative toy tree
meta = pt.datasets.primates_metadata().reset_index()
tr.join_data(meta, on="name")                    # attach metadata to tips

(pt.TreeFigure(tr)                               # skeleton drawn for you
    .tip_points(color="habitat", size=9)         # color mapped from metadata
    .tip_labels()
    .support_labels()                            # node support values
).save("tree.pdf")                               # PDF/SVG/PNG -> matplotlib
 # .save("tree.svg")                             # SVG: editable text -> drop into PowerPoint
 # .save("tree.html")                            # HTML        -> plotly (zoom/hover)

The output backend is chosen from the file extension: .pdf / .svg / .png render through matplotlib, .html renders an interactive plotly figure.


Gallery

Every figure below is produced by a script in examples/ (regenerate with python examples/<name>.py).

Rectangular phylogram with domain-colored tips and bootstrap support
Rectangular
16S tree of life, tips colored by domain, bootstrap support (200 reps) — tree_of_life_demo.py
Annotated circular tree with straight diagonal lineage-colored branches, shaped tips, and rings
Annotated circular
Slanted (starburst) branches coloured by lineage, shaped tips, tile + bar rings, three legends — showcase_circular.py
Tree with stacked categorical tile tracks and a numeric bar track
Aligned tracks
Stacked categorical tile tracks plus a numeric bar track — tracks_demo.py
Tree beside a residue-colored multiple sequence alignment raster
Alignment track
The multiple-sequence alignment as a residue raster — tracks_demo.py
Tanglegram of 106 taxa: two facing 16S trees with discordant links highlighted
Tanglegram
106 taxa, 25 phyla: neighbour joining vs UPGMA, untangled, with the links that still cross highlighted — tanglegram_demo.py

From sequences to a tree

phytreon pipeline from FASTA sequences through align, trim, infer, bootstrap, into a TreeFigure, then exported as pdf, svg, png, or html

One configurable call runs align → trim → infer → bootstrap, each stage opt-in and fully parameterized:

tree = pt.build_tree(
    "seqs.fasta",                       # path, list of (name, seq), or Alignment
    aligner="builtin",                  # pure-Python MSA  (or "mafft"/"muscle"/"none")
    align_kw=dict(match=2, gap=-3),
    trim_kw=dict(max_gap=0.4, min_occupancy=0.5, min_conservation=0.3),
    method="nj",                        # "nj" | "upgma" | "ml" | "parsimony"
    root="midpoint",
    bootstrap=200,                      # bipartition support
)
# distances are JC69-corrected by default (dist_model="jc69"|"k2p"|"poisson"|"raw");
# negative NJ branch lengths are clamped to 0.

# maximum likelihood (pure Python), HKY85 + Γ4 rate variation + NNI:
ml = pt.build_tree("seqs.fasta", method="ml", ml_model="HKY85", ml_gamma=4,
                   bootstrap=100)               # bootstrap works for nj/ml/parsimony
print(ml.data["logL"], ml.data["AIC"], ml.data["gamma_shape"])
pt.model_finder("seqs.fasta")                   # rank JC/K80/HKY/GTR (or JTT/WAG/LG) ±G by AIC
# large datasets -> external engines: build_tree(..., method="ml", ml_engine="iqtree")

# protein sequences work the same way -- pass amino acid sequences and an
# explicit protein model (ml_model's default stays "HKY85"; there is no
# "auto" alphabet detection, so a nucleotide/protein mismatch raises
# instead of silently miscoding the data):
prot = pt.build_tree("proteins.fasta", method="ml", ml_model="LG", bootstrap=100)
# dist_model's default stays "jc69", which falls back to raw p-distance on
# protein data unless you opt in to the protein-specific correction:
prot_nj = pt.build_tree("proteins.fasta", method="nj", dist_model="poisson")

Each step is also usable on its own: pt.align, pt.trim, pt.neighbor_joining, pt.bootstrap_support, pt.infer_ml, pt.parsimony_tree.

From a distance or character matrix

A precomputed distance matrix (samples × samples) skips alignment entirely:

import pandas as pd

df = pd.read_csv("distances.csv", index_col=0)     # square matrix, taxa on both axes
tree = pt.neighbor_joining(list(df.index), df.values.tolist())   # or pt.upgma(...)

A discrete character/trait matrix (samples × characters -- e.g. a 0/1 gene presence/absence table) goes through read_character_matrix and straight into parsimony:

aln = pt.read_character_matrix("genes.csv", taxa_col="name")   # or a DataFrame
tree = pt.parsimony_tree(aln, search=True)
(pt.TreeFigure(tree.ladderize()).tip_labels().support_labels()).save("tree.pdf")

Any small set of hashable states per column works (numbers, strings, booleans); missing values (NaN, or an explicit missing= sentinel) are encoded as ambiguous so they never force a false character change.

Single-cell CRISPR lineage-tracing data (a Cassiopeia-style allele table) -- or any somatic-mutation genotype matrix, via read_mutation_matrix -- works the same way, but with an irreversible ("Camin-Sokal") parsimony model appropriate for scars/mutations that can never revert:

aln = pt.read_allele_table("alleletable.txt")      # cellBC/intBC/r1/r2/r3
tree = pt.lineage_tree(aln, search=True)            # or build_tree(..., parsimony_model="camin_sokal")
print(tree.data["camin_sokal_score"], tree.data["excess_origins"])

Gene expression data is a different question -- similarity reflects cell state, not ancestry, so it gets a distinctly-named, non-phylogenetic dendrogram instead of a tree:

import pandas as pd

expr = pd.read_csv("expression.csv", index_col=0)   # cells x genes
tree = pt.expression_dendrogram(expr, genes=["CD3D"])   # NOT a phylogeny

What phytreon includes

Area Capabilities
I/O & data model Newick / Nexus / PhyloXML read-write; annotated NEXUS from BEAST / MrBayes (fmt="beast") keeping node ages, HPD intervals and posteriors; metadata joins (Tree.join_data)
Layouts rectangular, slanted, dendrogram, circular, fan, radial, circular-slanted (straight diagonal edges), inward-circular, unrooted (equal-angle / equal-daylight)
Inference NJ, UPGMA (model-corrected distances or a precomputed distance matrix), ML for nucleotide (JC69/K80/HKY85/GTR) and protein (JTT/WAG/LG) data, +Γ, NNI, AIC/BIC, model_finder, parsimony (from sequences, a discrete character/trait matrix via read_character_matrix, or single-cell lineage-tracing data via read_allele_table/read_mutation_matrix + irreversible Camin-Sokal parsimony), expression-similarity dendrograms (expression_dendrogram -- explicitly not phylogenetic), bootstrap, built-in MSA, trimming
Comparative ancestral states (parsimony / Mk-ML ER·SYM·ARD / Brownian), stochastic mapping, painted branches, node pies
Figure tracks tip / node / support labels, tip points, metadata rings, heatmaps, bar tracks, alignment rasters
Tree comparison tanglegrams (TangleFigure) with rotation-based untangle, crossing counts, Robinson-Foulds; DensiTree clouds (DensiTreeFigure) for a whole tree set
Tree operations rotate, flip, ladderize, collapse, scale clade, midpoint root, cut tree, Robinson-Foulds

The TreeFigure builder

TreeFigure starts from a tree skeleton and lets you compose visual layers fluently — every method returns the figure, so calls chain.

Method Draws
.branches(color=, size=) the tree skeleton (size= sets line width globally; e.g. color by lineage)
.tip_labels() / .node_labels() / .support_labels() text labels
.tip_points() / .node_points() / .points() markers (color / size / shape mapping)
.highlight(node=) / .clade_label(...) shade / bracket a clade
.heatmap(df) a matrix of cells aligned to the tips (rectangular)
.ring(df, columns=…) concentric metadata rings (circular), tile or bar
.bar_track(df, col) a horizontal bar track
.alignment(aln) a residue-colored MSA raster
.painted_branches() branches painted by stochastic-map state
.node_pies() ancestral-state pies at internal nodes
.time_axis(geo=True) / .scale_bar() a time / geological-period axis, or a compact branch-length scale
.collapsed_clades() collapsed clades as triangles (with pt.collapse_clade)
.node_bars() node age intervals, e.g. 95% HPD on a dated tree
.connections(pairs) curved links between tips (HGT, co-occurrence)

Continuous columns get a colorbar, categorical ones a legend; tracks, labels, and legends are placed so nothing overlaps. Layouts: rectangular, slanted, dendrogram, circular, fan, radial, circular_slanted, inward_circular, unrooted / daylight (equal-daylight), equal_angle.


Comparing two trees

TangleFigure faces two trees at each other and links their shared tips, so disagreements read as crossing links — the picture for "does the transcriptome tree agree with the genome tree?".

fig = pt.TangleFigure(genome_tree, transcriptome_tree,
                      titles=("genome", "transcriptome"))
fig.untangle()                            # rotate clades to line the tips up
fig.connect(highlight_discordant=True)    # red = this taxon disagrees
fig.left.tip_points(color="phylum")       # each side is a full TreeFigure
fig.save("tanglegram.pdf")

Rotating a node reorders its children, so untangling changes only how the trees read, never what they say. Whatever still crosses afterwards is real conflict. Note that zero crossings does not mean the trees are identical — they may merely admit a common tip order — so read pt.robinson_foulds() alongside pt.crossing_number(). See the tutorial and examples/tanglegram_demo.py.


Architecture

The single design decision that makes the dual backend work: layout and rendering are completely decoupled. A layout computes final cartesian coordinates and emits backend-agnostic primitives; matplotlib and plotly are "dumb" renderers that only translate those primitives. Adding a backend means writing one translator — nothing in the phylogenetic logic changes.

Module Responsibility
core Tree / Node data model + I/O
layout topology → display coordinates
scene Path / Marker / Label / Polygon primitives
plot TreeFigure builder + matplotlib / plotly backends
infer alignment / trimming / NJ / ML / parsimony / bootstrap
comparative ancestral states + stochastic mapping

Comparison to other Python tools

phytreon ete3 toytree Bio.Phylo dendropy
Fluent figure builder partial
Static and interactive backend ✅ mpl + plotly own GUI / SVG toyplot basic mpl
Annotation tracks (heatmap / rings / MSA / bars) partial partial
Built-in ML (+Γ) / parsimony ✅ pure-Python
Comparative (ancestral states / stochastic map) partial
Pure Python, pip-installable ✅ (Qt for GUI)

phytreon's niche is a fluent figure builder plus a self-contained phylogenetics stack. The built-in aligner and native ML engine are designed for small to medium examples, teaching, prototyping, and reproducible pure-Python workflows — not as a replacement for MAFFT/MUSCLE or IQ-TREE/RAxML/FastTree on large production alignments. Plug those in when you need them (aligner="mafft", ml_engine="iqtree"), then use phytreon for tree manipulation, metadata integration, and visualization.


Validation

validation/validate.py checks the core algorithms in pure Python (no external tools):

  • the likelihood engine (pattern-compressed, rescaled) matches an independent naive re-implementation to machine precision (|Δ| ≈ 1e-13);
  • neighbor-joining recovers a tree exactly from its own additive (patristic) distances (Robinson-Foulds = 0, via pt.robinson_foulds) — the defining guarantee of NJ;
  • ML recovers a known clade and reports a finite logL / AIC.

More

Reshaping trees (move branches freely)
pt.ladderize(tree)                      # tidy ordering
pt.rotate(tree, node)                   # flip a clade's vertical order
pt.flip(tree, node_a, node_b)           # swap two clades' positions
pt.collapse_low_support(tree, 70)       # weak edges -> polytomies
pt.scale_clade(tree, node, 0.5)         # de-emphasize a clade's branch lengths
pt.midpoint_root(tree)                  # root an unrooted (NJ) tree
clusters = pt.cut_tree(tree, k=4)       # {tip_name: cluster_id}

Because layout derives tip rows from child order, rotate / flip are exactly how you nudge branches up and down on the plot.

Comparative & time-scaled trees
pt.stochastic_map(tree, trait, n=200)        # stochastic character mapping
(pt.TreeFigure(tree).painted_branches()      # branches painted by inferred state
    .tip_labels())

(pt.TreeFigure(dated_tree)                    # branch lengths = time
    .time_axis(geo=True, gridlines=True, unit="Mya")  # geological bands
    .tip_labels())
Circular tree with metadata rings
(pt.TreeFigure(tree, layout="circular", extent=320)
    .ring(meta_df,                             # DataFrame indexed by tip name
          columns=["habitat", "diet", "body_mass_kg"],
          width=0.13, gap=0.03)                # each column -> one ring
    .tip_labels()
).save("rings.png")

Rings stack outward (categorical → palette, numeric → gradient, each with its own legend); tip labels are pushed outside all rings automatically, so the tree, rings, labels, and legends never overlap.

Colors

Categorical aesthetics use a curated, colourblind-safe eight-hue palette by default (for 3 levels: a restrained blue / teal / amber #2a78d6 #1baf7a #eda100), verified against the Machado-2009 CVD model; counts above eight extend with a muted hue wheel. Continuous aesthetics use a single-hue blue ramp, light (low) → deep (high). Override per element:

fig.tip_points(color="habitat", palette="dark2")   # curated | hue | set2 | dark2 | tab10
fig.heatmap(mat, cmap="viridis")                    # name, or ("#fff","#c00") gradient

See phytreon/plot/palettes.py (hue_palette, lerp_color).

Environment notes
  • Static figures use matplotlib (.save("x.pdf"/".png"/".svg")) and are always reliable. Interactive output uses plotly (.save("x.html")).
  • plotly's static PNG export (kaleido) is flaky on some Windows setups; pin kaleido==0.2.1 for plotly 5, or just use matplotlib for static figures and HTML for interactivity.
  • The plotly backend's legends are click-to-toggle traces and track placement is heuristic (it cannot measure text); the matplotlib backend is the reference for exact, publication-quality layout.
Caveats
  • The built-in aligner is a single-pass progressive aligner (linear gaps); fine for small/medium inputs, but use MAFFT (aligner="mafft") for publication alignments.
  • Native ML / parsimony assume binary trees; NNI search skips polytomies (resolve them first if needed). Pure-Python ML / MSA target tens of taxa — see benchmark/.
Extending
  • New layout: subclass phytreon.layout.base.Layout, implement compute() (write node.x / node.y), branch_path(), child_connector(), and register it in phytreon/layout/__init__.py::LAYOUTS. The builder and both backends pick it up automatically.
  • New element: subclass phytreon.plot.figure._Element, implement apply(ctx) to read coordinates and append scene primitives (use ctx.resolve_color(...) for metadata-driven aesthetics + legends), then add it with TreeFigure.add(...).
Example data (real, from NCBI)

examples/data/ ships a small microbial "tree of life" 16S rRNA set downloaded from NCBI — common model organisms and type strains across the major bacterial phyla plus four archaea (a natural outgroup) — so the whole pipeline runs on real, public sequences:

examples/data/tol_16S.fasta          18 unaligned 16S sequences (mostly RefSeq NR_*)
examples/data/tol_16S_aligned.fasta  built-in MSA of the above (cached)
examples/data/tol_metadata.csv       domain / phylum / organism / accession / length
examples/data/fetch_example_data.py  re-download script (Entrez)

An NJ/ML tree on this set recovers the four archaea as a monophyletic clade (deep bacterial splits from a single 16S gene are, as expected, only weakly supported). Accession versions may advance over time, so re-downloading need not byte-match the shipped snapshot; the cached alignment keeps the examples reproducible offline. Full accession list and license: examples/data/SOURCES.md.


Examples & tests

python examples/demo.py               # rect / circular / heatmap / nj / ancestral
python examples/pipeline_demo.py      # raw sequences -> align -> trim -> NJ -> bootstrap
python examples/tree_of_life_demo.py  # real 16S -> tree + circular metadata rings
python examples/showcase_circular.py  # lineage colors + tile + bar rings + shapes
python examples/tracks_demo.py        # rectangular tile / bar tracks + alignment track
python examples/ml_demo.py            # native pure-Python ML tree (HKY85)
python validation/validate.py         # pure-Python correctness checks
python benchmark/benchmark.py         # timings + validated-core guidance
pytest -q                             # 37 tests

# docs: pip install mkdocs-material mkdocstrings[python]; mkdocs serve

MIT licensed · Built for reproducible phylogenetic visualization in Python.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

phytreon-0.3.1.tar.gz (137.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

phytreon-0.3.1-py3-none-any.whl (122.0 kB view details)

Uploaded Python 3

File details

Details for the file phytreon-0.3.1.tar.gz.

File metadata

  • Download URL: phytreon-0.3.1.tar.gz
  • Upload date:
  • Size: 137.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for phytreon-0.3.1.tar.gz
Algorithm Hash digest
SHA256 bb9657a20587dee36d721cbe698d33b98e3ca2ab3d6243875039675e6ed62995
MD5 864269249725900597b0017f94875a63
BLAKE2b-256 eeba6272038e6b704217ba22a36fcd8f511194be4554a29a9eb0138e72da0116

See more details on using hashes here.

Provenance

The following attestation bundles were made for phytreon-0.3.1.tar.gz:

Publisher: publish.yml on DeweyYihengDu/phytreon

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file phytreon-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: phytreon-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 122.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for phytreon-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 cc63ad6e9b2c3dc27f29832c92d99cd0316d26663bd8a44f6f2f1a1fdaf772a0
MD5 ebf85b9a4075f3d31d2a23cb8d2e9a53
BLAKE2b-256 d1d352ac44c8b4ef0b98be2cef5b0bc3e4a1c779e6dc2e5beaa907853a994f5e

See more details on using hashes here.

Provenance

The following attestation bundles were made for phytreon-0.3.1-py3-none-any.whl:

Publisher: publish.yml on DeweyYihengDu/phytreon

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page