phykit 2.1.44__tar.gz → 2.1.46__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {phykit-2.1.44 → phykit-2.1.46}/PKG-INFO +1 -1
- {phykit-2.1.44 → phykit-2.1.46}/phykit/cli_registry.py +4 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/phykit.py +157 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/service_factories.py +2 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/__init__.py +2 -0
- phykit-2.1.46/phykit/services/tree/independent_contrasts.py +210 -0
- phykit-2.1.46/phykit/services/tree/phylo_heatmap.py +315 -0
- phykit-2.1.46/phykit/version.py +1 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit.egg-info/PKG-INFO +1 -1
- {phykit-2.1.44 → phykit-2.1.46}/phykit.egg-info/SOURCES.txt +2 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit.egg-info/entry_points.txt +6 -0
- {phykit-2.1.44 → phykit-2.1.46}/setup.py +6 -0
- phykit-2.1.44/phykit/version.py +0 -1
- {phykit-2.1.44 → phykit-2.1.46}/LICENSE.md +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/README.md +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/__init__.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/__main__.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/errors.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/helpers/__init__.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/helpers/boolean_argument_parsing.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/helpers/caching.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/helpers/discrete_models.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/helpers/files.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/helpers/json_output.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/helpers/parallel.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/helpers/plot_config.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/helpers/quartet_utils.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/helpers/stats_summary.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/helpers/streaming.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/__init__.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/__init__.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/alignment_entropy.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/alignment_length.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/alignment_recoding.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/base.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/column_score.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/composition_per_taxon.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/dna_threader.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/faidx.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/gc_content.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/mask_alignment.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/pairwise_identity.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/plot_alignment_qc.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/rcv.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/rcvt.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/rename_fasta_entries.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/alignment/variable_sites.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/base.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/ancestral_reconstruction.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/base.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/bipartition_support_stats.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/branch_length_multiplier.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/collapse_branches.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/concordance_asr.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/consensus_network.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/consensus_tree.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/cont_map.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/cophylo.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/density_map.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/discordance_asymmetry.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/dvmc.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/evo_tempo_map.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/evolutionary_rate.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/fit_continuous.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/fit_discrete.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/hidden_paralogy_check.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/internal_branch_stats.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/internode_labeler.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/kf_distance.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/lb_score.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/ltt.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/monophyly_check.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/network_signal.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/ou_shift_detection.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/ouwie.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/patristic_distances.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/phenogram.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/phylogenetic_glm.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/phylogenetic_ordination.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/phylogenetic_regression.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/phylogenetic_signal.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/phylomorphospace.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/polytomy_test.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/print_tree.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/prune_tree.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/quartet_network.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/quartet_pie.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/rate_heterogeneity.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/relative_rate_test.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/rename_tree_tips.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/rf_distance.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/root_tree.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/saturation.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/spectral_discordance.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/spurious_sequence.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/stochastic_character_map.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/terminal_branch_stats.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/threshold_model.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/tip_labels.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/tip_to_tip_distance.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/total_tree_length.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/treeness.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/treeness_over_rcv.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit/services/tree/vcv_utils.py +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit.egg-info/dependency_links.txt +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit.egg-info/requires.txt +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/phykit.egg-info/top_level.txt +0 -0
- {phykit-2.1.44 → phykit-2.1.46}/setup.cfg +0 -0
|
@@ -80,12 +80,16 @@ ALIAS_TO_HANDLER: Dict[str, str] = {
|
|
|
80
80
|
"phylo_dimreduce": "phylogenetic_ordination",
|
|
81
81
|
"dimreduce": "phylogenetic_ordination",
|
|
82
82
|
"pdr": "phylogenetic_ordination",
|
|
83
|
+
"pheatmap": "phylo_heatmap",
|
|
84
|
+
"ph": "phylo_heatmap",
|
|
83
85
|
"phylomorpho": "phylomorphospace",
|
|
84
86
|
"phmo": "phylomorphospace",
|
|
85
87
|
"phylo_regression": "phylogenetic_regression",
|
|
86
88
|
"pgls": "phylogenetic_regression",
|
|
87
89
|
"phylo_glm": "phylogenetic_glm",
|
|
88
90
|
"pglm": "phylogenetic_glm",
|
|
91
|
+
"pic": "independent_contrasts",
|
|
92
|
+
"phylo_contrasts": "independent_contrasts",
|
|
89
93
|
"asr": "ancestral_state_reconstruction",
|
|
90
94
|
"anc_recon": "ancestral_state_reconstruction",
|
|
91
95
|
"conc_asr": "concordance_asr",
|
|
@@ -161,6 +161,8 @@ class Phykit:
|
|
|
161
161
|
|
|
162
162
|
Tree-based commands
|
|
163
163
|
===================
|
|
164
|
+
independent_contrasts (alias: pic; phylo_contrasts)
|
|
165
|
+
- Felsenstein's phylogenetically independent contrasts
|
|
164
166
|
ancestral_state_reconstruction (alias: asr; anc_recon)
|
|
165
167
|
- estimate ancestral states for continuous traits using
|
|
166
168
|
ML (fast or VCV-based) with optional contMap plot
|
|
@@ -207,6 +209,8 @@ class Phykit:
|
|
|
207
209
|
phylo_pca; phyl_pca; ppca; phylo_dimreduce; dimreduce; pdr)
|
|
208
210
|
- phylogenetic ordination (PCA, t-SNE, or UMAP) on
|
|
209
211
|
continuous multi-trait data
|
|
212
|
+
phylo_heatmap (alias: pheatmap; ph)
|
|
213
|
+
- phylogeny alongside a heatmap of numeric trait values
|
|
210
214
|
phylomorphospace (alias: phylomorpho; phmo)
|
|
211
215
|
- plot raw traits with phylogeny overlaid via ancestral
|
|
212
216
|
reconstruction
|
|
@@ -1675,6 +1679,57 @@ class Phykit:
|
|
|
1675
1679
|
_run_service(parser, argv, VariableSites)
|
|
1676
1680
|
|
|
1677
1681
|
## Tree functions
|
|
1682
|
+
@staticmethod
|
|
1683
|
+
def independent_contrasts(argv):
|
|
1684
|
+
parser = _new_parser(
|
|
1685
|
+
description=textwrap.dedent(
|
|
1686
|
+
f"""\
|
|
1687
|
+
{help_header}
|
|
1688
|
+
|
|
1689
|
+
Compute Felsenstein's (1985) phylogenetically independent
|
|
1690
|
+
contrasts (PIC) for a continuous trait on a phylogeny.
|
|
1691
|
+
|
|
1692
|
+
Each internal node yields one standardized contrast,
|
|
1693
|
+
producing n-1 contrasts for n tips. Contrasts are
|
|
1694
|
+
computed by postorder traversal, dividing the trait
|
|
1695
|
+
difference between sister clades by the square root of
|
|
1696
|
+
the sum of their branch lengths.
|
|
1697
|
+
|
|
1698
|
+
Multifurcations are automatically resolved by adding
|
|
1699
|
+
zero-length branches.
|
|
1700
|
+
|
|
1701
|
+
Cross-validated against R's ape::pic().
|
|
1702
|
+
|
|
1703
|
+
Aliases:
|
|
1704
|
+
independent_contrasts, pic, phylo_contrasts
|
|
1705
|
+
Command line interfaces:
|
|
1706
|
+
pk_independent_contrasts, pk_pic
|
|
1707
|
+
|
|
1708
|
+
Usage:
|
|
1709
|
+
phykit independent_contrasts -t <tree> -d <trait_data>
|
|
1710
|
+
[--json]
|
|
1711
|
+
|
|
1712
|
+
Options
|
|
1713
|
+
=====================================================
|
|
1714
|
+
-t/--tree tree file (required)
|
|
1715
|
+
|
|
1716
|
+
-d/--trait_data trait data file, two columns:
|
|
1717
|
+
taxon<tab>value (required)
|
|
1718
|
+
|
|
1719
|
+
--json optional argument to output
|
|
1720
|
+
results as JSON
|
|
1721
|
+
"""
|
|
1722
|
+
),
|
|
1723
|
+
)
|
|
1724
|
+
parser.add_argument(
|
|
1725
|
+
"-t", "--tree", type=str, required=True, help=SUPPRESS, metavar=""
|
|
1726
|
+
)
|
|
1727
|
+
parser.add_argument(
|
|
1728
|
+
"-d", "--trait_data", type=str, required=True, help=SUPPRESS, metavar=""
|
|
1729
|
+
)
|
|
1730
|
+
_add_json_argument(parser)
|
|
1731
|
+
_run_service(parser, argv, IndependentContrasts)
|
|
1732
|
+
|
|
1678
1733
|
@staticmethod
|
|
1679
1734
|
def ancestral_state_reconstruction(argv):
|
|
1680
1735
|
parser = _new_parser(
|
|
@@ -3013,6 +3068,100 @@ class Phykit:
|
|
|
3013
3068
|
def phylogenetic_dimreduce(argv):
|
|
3014
3069
|
Phykit.phylogenetic_ordination(argv)
|
|
3015
3070
|
|
|
3071
|
+
@staticmethod
|
|
3072
|
+
def phylo_heatmap(argv):
|
|
3073
|
+
parser = _new_parser(
|
|
3074
|
+
description=textwrap.dedent(
|
|
3075
|
+
f"""\
|
|
3076
|
+
{help_header}
|
|
3077
|
+
|
|
3078
|
+
Draw a phylogenetic heatmap: a phylogeny alongside a
|
|
3079
|
+
color-coded matrix of numeric trait values. Rows are
|
|
3080
|
+
aligned to tree tips.
|
|
3081
|
+
|
|
3082
|
+
Analogous to R's phytools::phylo.heatmap().
|
|
3083
|
+
|
|
3084
|
+
Aliases:
|
|
3085
|
+
phylo_heatmap, pheatmap, ph
|
|
3086
|
+
Command line interfaces:
|
|
3087
|
+
pk_phylo_heatmap, pk_pheatmap, pk_ph
|
|
3088
|
+
|
|
3089
|
+
Usage:
|
|
3090
|
+
phykit phylo_heatmap -t <tree> -d <data> -o <output>
|
|
3091
|
+
[--split 0.3] [--standardize] [--cmap viridis]
|
|
3092
|
+
[--json]
|
|
3093
|
+
[--fig-width <float>] [--fig-height <float>]
|
|
3094
|
+
[--dpi <int>] [--no-title] [--title <str>]
|
|
3095
|
+
[--ylabel-fontsize <float>] [--xlabel-fontsize <float>]
|
|
3096
|
+
|
|
3097
|
+
Options
|
|
3098
|
+
=====================================================
|
|
3099
|
+
-t/--tree tree file (required)
|
|
3100
|
+
|
|
3101
|
+
-d/--data numeric data matrix in TSV
|
|
3102
|
+
format with header row
|
|
3103
|
+
(required)
|
|
3104
|
+
|
|
3105
|
+
-o/--output output figure path (required;
|
|
3106
|
+
supports .png, .pdf, .svg)
|
|
3107
|
+
|
|
3108
|
+
--split fraction of figure width for
|
|
3109
|
+
the tree panel (default: 0.3)
|
|
3110
|
+
|
|
3111
|
+
--standardize z-score each column before
|
|
3112
|
+
coloring
|
|
3113
|
+
|
|
3114
|
+
--cmap matplotlib colormap name
|
|
3115
|
+
(default: viridis)
|
|
3116
|
+
|
|
3117
|
+
--fig-width figure width in inches
|
|
3118
|
+
(auto-scaled if omitted)
|
|
3119
|
+
|
|
3120
|
+
--fig-height figure height in inches
|
|
3121
|
+
(auto-scaled if omitted)
|
|
3122
|
+
|
|
3123
|
+
--dpi resolution in DPI
|
|
3124
|
+
(default: 300)
|
|
3125
|
+
|
|
3126
|
+
--no-title hide the plot title
|
|
3127
|
+
|
|
3128
|
+
--title custom title text
|
|
3129
|
+
|
|
3130
|
+
--ylabel-fontsize font size for taxon labels;
|
|
3131
|
+
0 to hide
|
|
3132
|
+
|
|
3133
|
+
--xlabel-fontsize font size for trait column
|
|
3134
|
+
labels; 0 to hide
|
|
3135
|
+
|
|
3136
|
+
--json optional argument to output
|
|
3137
|
+
metadata as JSON
|
|
3138
|
+
"""
|
|
3139
|
+
),
|
|
3140
|
+
)
|
|
3141
|
+
parser.add_argument(
|
|
3142
|
+
"-t", "--tree", type=str, required=True, help=SUPPRESS, metavar=""
|
|
3143
|
+
)
|
|
3144
|
+
parser.add_argument(
|
|
3145
|
+
"-d", "--data", type=str, required=True, help=SUPPRESS, metavar=""
|
|
3146
|
+
)
|
|
3147
|
+
parser.add_argument(
|
|
3148
|
+
"-o", "--output", type=str, required=True, help=SUPPRESS, metavar=""
|
|
3149
|
+
)
|
|
3150
|
+
parser.add_argument(
|
|
3151
|
+
"--split", type=float, required=False, default=0.3,
|
|
3152
|
+
help=SUPPRESS, metavar=""
|
|
3153
|
+
)
|
|
3154
|
+
parser.add_argument(
|
|
3155
|
+
"--standardize", action="store_true", required=False, help=SUPPRESS
|
|
3156
|
+
)
|
|
3157
|
+
parser.add_argument(
|
|
3158
|
+
"--cmap", type=str, required=False, default="viridis",
|
|
3159
|
+
help=SUPPRESS, metavar=""
|
|
3160
|
+
)
|
|
3161
|
+
add_plot_arguments(parser)
|
|
3162
|
+
_add_json_argument(parser)
|
|
3163
|
+
_run_service(parser, argv, PhyloHeatmap)
|
|
3164
|
+
|
|
3016
3165
|
@staticmethod
|
|
3017
3166
|
def phylomorphospace(argv):
|
|
3018
3167
|
parser = _new_parser(
|
|
@@ -6576,6 +6725,10 @@ def variable_sites(argv=None):
|
|
|
6576
6725
|
|
|
6577
6726
|
|
|
6578
6727
|
# Tree-based functions
|
|
6728
|
+
def independent_contrasts(argv=None):
|
|
6729
|
+
Phykit.independent_contrasts(sys.argv[1:])
|
|
6730
|
+
|
|
6731
|
+
|
|
6579
6732
|
def ancestral_state_reconstruction(argv=None):
|
|
6580
6733
|
Phykit.ancestral_state_reconstruction(sys.argv[1:])
|
|
6581
6734
|
|
|
@@ -6656,6 +6809,10 @@ def phylogenetic_dimreduce(argv=None):
|
|
|
6656
6809
|
Phykit.phylogenetic_ordination(sys.argv[1:])
|
|
6657
6810
|
|
|
6658
6811
|
|
|
6812
|
+
def phylo_heatmap(argv=None):
|
|
6813
|
+
Phykit.phylo_heatmap(sys.argv[1:])
|
|
6814
|
+
|
|
6815
|
+
|
|
6659
6816
|
def phylomorphospace(argv=None):
|
|
6660
6817
|
Phykit.phylomorphospace(sys.argv[1:])
|
|
6661
6818
|
|
|
@@ -83,10 +83,12 @@ RelativeRateTest = _LazyServiceFactory("phykit.services.tree.relative_rate_test"
|
|
|
83
83
|
ThresholdModel = _LazyServiceFactory("phykit.services.tree.threshold_model", "ThresholdModel")
|
|
84
84
|
PolytomyTest = _LazyServiceFactory("phykit.services.tree.polytomy_test", "PolytomyTest")
|
|
85
85
|
PrintTree = _LazyServiceFactory("phykit.services.tree.print_tree", "PrintTree")
|
|
86
|
+
PhyloHeatmap = _LazyServiceFactory("phykit.services.tree.phylo_heatmap", "PhyloHeatmap")
|
|
86
87
|
PruneTree = _LazyServiceFactory("phykit.services.tree.prune_tree", "PruneTree")
|
|
87
88
|
QuartetPie = _LazyServiceFactory("phykit.services.tree.quartet_pie", "QuartetPie")
|
|
88
89
|
RenameTreeTips = _LazyServiceFactory("phykit.services.tree.rename_tree_tips", "RenameTreeTips")
|
|
89
90
|
FitDiscrete = _LazyServiceFactory("phykit.services.tree.fit_discrete", "FitDiscrete")
|
|
91
|
+
IndependentContrasts = _LazyServiceFactory("phykit.services.tree.independent_contrasts", "IndependentContrasts")
|
|
90
92
|
KuhnerFelsensteinDistance = _LazyServiceFactory("phykit.services.tree.kf_distance", "KuhnerFelsensteinDistance")
|
|
91
93
|
RobinsonFouldsDistance = _LazyServiceFactory("phykit.services.tree.rf_distance", "RobinsonFouldsDistance")
|
|
92
94
|
RootTree = _LazyServiceFactory("phykit.services.tree.root_tree", "RootTree")
|
|
@@ -27,9 +27,11 @@ _EXPORTS = {
|
|
|
27
27
|
"RelativeRateTest": "relative_rate_test",
|
|
28
28
|
"PolytomyTest": "polytomy_test",
|
|
29
29
|
"PrintTree": "print_tree",
|
|
30
|
+
"PhyloHeatmap": "phylo_heatmap",
|
|
30
31
|
"PruneTree": "prune_tree",
|
|
31
32
|
"QuartetPie": "quartet_pie",
|
|
32
33
|
"RenameTreeTips": "rename_tree_tips",
|
|
34
|
+
"IndependentContrasts": "independent_contrasts",
|
|
33
35
|
"KuhnerFelsensteinDistance": "kf_distance",
|
|
34
36
|
"RobinsonFouldsDistance": "rf_distance",
|
|
35
37
|
"RootTree": "root_tree",
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Phylogenetically independent contrasts (PIC).
|
|
3
|
+
|
|
4
|
+
Computes Felsenstein's (1985) phylogenetically independent contrasts
|
|
5
|
+
for a continuous trait on a phylogeny. Each internal node yields one
|
|
6
|
+
contrast (standardized difference), producing n-1 contrasts for n tips.
|
|
7
|
+
|
|
8
|
+
Cross-validated against R's ape::pic().
|
|
9
|
+
"""
|
|
10
|
+
import copy
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Dict, List, Tuple
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
|
|
16
|
+
from .base import Tree
|
|
17
|
+
from ...helpers.json_output import print_json
|
|
18
|
+
from ...errors import PhykitUserError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class IndependentContrasts(Tree):
|
|
22
|
+
def __init__(self, args) -> None:
|
|
23
|
+
parsed = self.process_args(args)
|
|
24
|
+
super().__init__(tree_file_path=parsed["tree_file_path"])
|
|
25
|
+
self.trait_data_path = parsed["trait_data_path"]
|
|
26
|
+
self.json_output = parsed["json_output"]
|
|
27
|
+
|
|
28
|
+
def run(self) -> None:
|
|
29
|
+
tree = self.read_tree_file()
|
|
30
|
+
tree = copy.deepcopy(tree)
|
|
31
|
+
self._validate_tree(tree)
|
|
32
|
+
|
|
33
|
+
tree_tips = [t.name for t in tree.get_terminals()]
|
|
34
|
+
tip_traits = self._parse_trait_data(self.trait_data_path, tree_tips)
|
|
35
|
+
|
|
36
|
+
# Prune tree to shared taxa
|
|
37
|
+
shared = set(tip_traits.keys())
|
|
38
|
+
tips_to_prune = [t for t in tree_tips if t not in shared]
|
|
39
|
+
if tips_to_prune:
|
|
40
|
+
tree = self.prune_tree_using_taxa_list(tree, tips_to_prune)
|
|
41
|
+
|
|
42
|
+
# Resolve multifurcations (PIC requires fully dichotomous tree)
|
|
43
|
+
self._resolve_polytomies(tree)
|
|
44
|
+
|
|
45
|
+
contrasts, node_labels = self._compute_pic(tree, tip_traits)
|
|
46
|
+
|
|
47
|
+
if self.json_output:
|
|
48
|
+
self._print_json(contrasts, node_labels, tip_traits)
|
|
49
|
+
else:
|
|
50
|
+
self._print_text(contrasts, node_labels)
|
|
51
|
+
|
|
52
|
+
def process_args(self, args) -> Dict:
|
|
53
|
+
return dict(
|
|
54
|
+
tree_file_path=args.tree,
|
|
55
|
+
trait_data_path=args.trait_data,
|
|
56
|
+
json_output=getattr(args, "json", False),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def _validate_tree(self, tree) -> None:
|
|
60
|
+
tips = list(tree.get_terminals())
|
|
61
|
+
if len(tips) < 3:
|
|
62
|
+
raise PhykitUserError(
|
|
63
|
+
["Tree must have at least 3 tips."], code=2
|
|
64
|
+
)
|
|
65
|
+
for clade in tree.find_clades():
|
|
66
|
+
if clade.branch_length is None and clade != tree.root:
|
|
67
|
+
clade.branch_length = 1e-8
|
|
68
|
+
|
|
69
|
+
def _parse_trait_data(
|
|
70
|
+
self, path: str, tree_tips: List[str]
|
|
71
|
+
) -> Dict[str, float]:
|
|
72
|
+
"""Parse two-column trait file (taxon<tab>value)."""
|
|
73
|
+
try:
|
|
74
|
+
with open(path) as f:
|
|
75
|
+
lines = f.readlines()
|
|
76
|
+
except FileNotFoundError:
|
|
77
|
+
raise PhykitUserError(
|
|
78
|
+
[f"{path} not found. Check filename and path."], code=2
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
traits = {}
|
|
82
|
+
for line in lines:
|
|
83
|
+
stripped = line.strip()
|
|
84
|
+
if not stripped or stripped.startswith("#"):
|
|
85
|
+
continue
|
|
86
|
+
parts = stripped.split("\t")
|
|
87
|
+
if len(parts) != 2:
|
|
88
|
+
continue
|
|
89
|
+
try:
|
|
90
|
+
traits[parts[0]] = float(parts[1])
|
|
91
|
+
except ValueError:
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
shared = set(tree_tips) & set(traits.keys())
|
|
95
|
+
if len(shared) < 3:
|
|
96
|
+
raise PhykitUserError(
|
|
97
|
+
[
|
|
98
|
+
f"Only {len(shared)} shared taxa between tree and trait file.",
|
|
99
|
+
"At least 3 shared taxa are required.",
|
|
100
|
+
],
|
|
101
|
+
code=2,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return {t: traits[t] for t in shared}
|
|
105
|
+
|
|
106
|
+
def _resolve_polytomies(self, tree) -> None:
|
|
107
|
+
"""Resolve multifurcations by adding zero-length branches."""
|
|
108
|
+
for clade in tree.find_clades(order="postorder"):
|
|
109
|
+
while len(clade.clades) > 2:
|
|
110
|
+
# Take the last two children and merge them
|
|
111
|
+
from Bio.Phylo import Newick
|
|
112
|
+
child1 = clade.clades.pop()
|
|
113
|
+
child2 = clade.clades.pop()
|
|
114
|
+
new_internal = Newick.Clade(branch_length=0.0)
|
|
115
|
+
new_internal.clades = [child1, child2]
|
|
116
|
+
clade.clades.append(new_internal)
|
|
117
|
+
|
|
118
|
+
def _compute_pic(
|
|
119
|
+
self, tree, tip_traits: Dict[str, float]
|
|
120
|
+
) -> Tuple[List[float], List[List[str]]]:
|
|
121
|
+
"""Compute phylogenetically independent contrasts.
|
|
122
|
+
|
|
123
|
+
Felsenstein (1985) algorithm:
|
|
124
|
+
- Postorder traversal through tree
|
|
125
|
+
- At each internal node with children L, R:
|
|
126
|
+
contrast = (x_L - x_R) / sqrt(v_L + v_R)
|
|
127
|
+
x_node = (x_L/v_L + x_R/v_R) / (1/v_L + 1/v_R)
|
|
128
|
+
v_node += v_L * v_R / (v_L + v_R)
|
|
129
|
+
|
|
130
|
+
Returns (contrasts, node_tip_labels) where node_tip_labels[i]
|
|
131
|
+
is the list of tip names descending from the node that produced
|
|
132
|
+
contrast[i].
|
|
133
|
+
"""
|
|
134
|
+
# Store trait values and effective branch lengths per node
|
|
135
|
+
node_val = {}
|
|
136
|
+
node_bl = {} # effective branch length (adjusted during traversal)
|
|
137
|
+
|
|
138
|
+
# Initialize tips
|
|
139
|
+
for tip in tree.get_terminals():
|
|
140
|
+
if tip.name in tip_traits:
|
|
141
|
+
node_val[id(tip)] = tip_traits[tip.name]
|
|
142
|
+
node_bl[id(tip)] = tip.branch_length if tip.branch_length else 1e-8
|
|
143
|
+
|
|
144
|
+
contrasts = []
|
|
145
|
+
node_labels = []
|
|
146
|
+
|
|
147
|
+
for clade in tree.find_clades(order="postorder"):
|
|
148
|
+
if clade.is_terminal():
|
|
149
|
+
continue
|
|
150
|
+
if len(clade.clades) != 2:
|
|
151
|
+
continue
|
|
152
|
+
|
|
153
|
+
left, right = clade.clades
|
|
154
|
+
lid, rid = id(left), id(right)
|
|
155
|
+
|
|
156
|
+
if lid not in node_val or rid not in node_val:
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
x_l = node_val[lid]
|
|
160
|
+
x_r = node_val[rid]
|
|
161
|
+
v_l = node_bl[lid]
|
|
162
|
+
v_r = node_bl[rid]
|
|
163
|
+
|
|
164
|
+
# Standardized contrast
|
|
165
|
+
contrast = (x_l - x_r) / np.sqrt(v_l + v_r)
|
|
166
|
+
contrasts.append(float(contrast))
|
|
167
|
+
|
|
168
|
+
# Tip labels for this node
|
|
169
|
+
tips_here = sorted(t.name for t in clade.get_terminals())
|
|
170
|
+
node_labels.append(tips_here)
|
|
171
|
+
|
|
172
|
+
# Weighted average trait value for this node
|
|
173
|
+
node_val[id(clade)] = (x_l / v_l + x_r / v_r) / (1.0 / v_l + 1.0 / v_r)
|
|
174
|
+
|
|
175
|
+
# Adjusted branch length
|
|
176
|
+
parent_bl = clade.branch_length if clade.branch_length else 0.0
|
|
177
|
+
node_bl[id(clade)] = parent_bl + (v_l * v_r) / (v_l + v_r)
|
|
178
|
+
|
|
179
|
+
return contrasts, node_labels
|
|
180
|
+
|
|
181
|
+
def _print_text(self, contrasts, node_labels):
|
|
182
|
+
print(f"Number of contrasts: {len(contrasts)}")
|
|
183
|
+
print()
|
|
184
|
+
print(f"{'Node':<6}{'Contrast':>12} Tips")
|
|
185
|
+
print("-" * 60)
|
|
186
|
+
for i, (c, tips) in enumerate(zip(contrasts, node_labels), 1):
|
|
187
|
+
tips_str = ", ".join(tips[:3])
|
|
188
|
+
if len(tips) > 3:
|
|
189
|
+
tips_str += f", ... ({len(tips)} total)"
|
|
190
|
+
print(f"{i:<6}{c:>12.6f} {tips_str}")
|
|
191
|
+
print()
|
|
192
|
+
print(f"Mean absolute contrast: {np.mean(np.abs(contrasts)):.6f}")
|
|
193
|
+
print(f"Variance of contrasts: {np.var(contrasts, ddof=1):.6f}")
|
|
194
|
+
|
|
195
|
+
def _print_json(self, contrasts, node_labels, tip_traits):
|
|
196
|
+
nodes = []
|
|
197
|
+
for i, (c, tips) in enumerate(zip(contrasts, node_labels)):
|
|
198
|
+
nodes.append({
|
|
199
|
+
"node": i + 1,
|
|
200
|
+
"contrast": round(c, 6),
|
|
201
|
+
"tips": tips,
|
|
202
|
+
})
|
|
203
|
+
payload = {
|
|
204
|
+
"n_taxa": len(tip_traits),
|
|
205
|
+
"n_contrasts": len(contrasts),
|
|
206
|
+
"contrasts": nodes,
|
|
207
|
+
"mean_absolute_contrast": round(float(np.mean(np.abs(contrasts))), 6),
|
|
208
|
+
"variance_of_contrasts": round(float(np.var(contrasts, ddof=1)), 6),
|
|
209
|
+
}
|
|
210
|
+
print_json(payload)
|