Phylogenetic trees and publication figures in Python
Project description
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.
Why phytreon?
🌿 Fluent TreeFigure builderCompose 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. |
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 16S tree of life, tips colored by domain, bootstrap support (200 reps) — tree_of_life_demo.py
|
Annotated circular Slanted (starburst) branches coloured by lineage, shaped tips, tile + bar rings, three legends — showcase_circular.py
|
|
Aligned tracks Stacked categorical tile tracks plus a numeric bar track — tracks_demo.py
|
Alignment track The multiple-sequence alignment as a residue raster — tracks_demo.py
|
From sequences to a tree
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; 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 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) |
a time / geological-period axis |
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.
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.1for 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, implementcompute()(writenode.x/node.y),branch_path(),child_connector(), and register it inphytreon/layout/__init__.py::LAYOUTS. The builder and both backends pick it up automatically. - New element: subclass
phytreon.plot.figure._Element, implementapply(ctx)to read coordinates and appendsceneprimitives (usectx.resolve_color(...)for metadata-driven aesthetics + legends), then add it withTreeFigure.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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file phytreon-0.2.2.tar.gz.
File metadata
- Download URL: phytreon-0.2.2.tar.gz
- Upload date:
- Size: 107.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
852afe1c45e7a069db2a6e1e051ddf119be93d379e9a474a686610853d4729ee
|
|
| MD5 |
13954d4d0997a257a9863cbfcc83e53f
|
|
| BLAKE2b-256 |
242018b4182e9820be591a1f165916af7f08b90d81ad06022565ac41bd6f930a
|
Provenance
The following attestation bundles were made for phytreon-0.2.2.tar.gz:
Publisher:
publish.yml on DeweyYihengDu/phytreon
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
phytreon-0.2.2.tar.gz -
Subject digest:
852afe1c45e7a069db2a6e1e051ddf119be93d379e9a474a686610853d4729ee - Sigstore transparency entry: 2191074872
- Sigstore integration time:
-
Permalink:
DeweyYihengDu/phytreon@1cf968388a02cf8e3e74b7f69d400b7e2be415dc -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/DeweyYihengDu
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1cf968388a02cf8e3e74b7f69d400b7e2be415dc -
Trigger Event:
push
-
Statement type:
File details
Details for the file phytreon-0.2.2-py3-none-any.whl.
File metadata
- Download URL: phytreon-0.2.2-py3-none-any.whl
- Upload date:
- Size: 99.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
284f52d80c0d0669ea5576adf113ddf4d267c40db4f5c1f02d9f279a32e5d81b
|
|
| MD5 |
d5c0141ed6e932f93a17b805dbeaa78e
|
|
| BLAKE2b-256 |
1e89fb2a20ffe0cc9e3187ffa5b0bbf245dafdd48e3edcf84c4d9f1583ab5fda
|
Provenance
The following attestation bundles were made for phytreon-0.2.2-py3-none-any.whl:
Publisher:
publish.yml on DeweyYihengDu/phytreon
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
phytreon-0.2.2-py3-none-any.whl -
Subject digest:
284f52d80c0d0669ea5576adf113ddf4d267c40db4f5c1f02d9f279a32e5d81b - Sigstore transparency entry: 2191075678
- Sigstore integration time:
-
Permalink:
DeweyYihengDu/phytreon@1cf968388a02cf8e3e74b7f69d400b7e2be415dc -
Branch / Tag:
refs/tags/v0.2.2 - Owner: https://github.com/DeweyYihengDu
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1cf968388a02cf8e3e74b7f69d400b7e2be415dc -
Trigger Event:
push
-
Statement type: