phykit 2.1.69__tar.gz → 2.1.70__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.69 → phykit-2.1.70}/PKG-INFO +1 -1
- {phykit-2.1.69 → phykit-2.1.70}/phykit/phykit.py +8 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/phylogenetic_signal.py +243 -0
- phykit-2.1.70/phykit/version.py +1 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit.egg-info/PKG-INFO +1 -1
- phykit-2.1.69/phykit/version.py +0 -1
- {phykit-2.1.69 → phykit-2.1.70}/LICENSE.md +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/README.md +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/__init__.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/__main__.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/cli_registry.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/errors.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/__init__.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/boolean_argument_parsing.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/caching.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/circular_layout.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/color_annotations.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/discrete_models.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/files.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/json_output.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/parallel.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/parsimony_utils.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/plot_config.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/quartet_utils.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/stats_summary.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/streaming.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/service_factories.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/__init__.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/__init__.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/alignment_entropy.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/alignment_length.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/alignment_recoding.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/alignment_subsample.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/base.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/column_score.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/composition_per_taxon.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/dfoil.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/dna_threader.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/dstatistic.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/faidx.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/gc_content.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/identity_matrix.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/mask_alignment.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/pairwise_identity.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/plot_alignment_qc.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/rcv.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/rcvt.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/rename_fasta_entries.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/variable_sites.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/base.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/__init__.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/ancestral_reconstruction.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/base.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/bipartition_support_stats.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/branch_length_multiplier.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/character_map.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/collapse_branches.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/concordance_asr.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/consensus_network.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/consensus_tree.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/cont_map.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/cophylo.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/density_map.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/discordance_asymmetry.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/dvmc.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/evo_tempo_map.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/evolutionary_rate.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/fit_continuous.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/fit_discrete.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/hidden_paralogy_check.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/independent_contrasts.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/internal_branch_stats.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/internode_labeler.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/kf_distance.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/lb_score.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/ltt.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/monophyly_check.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/network_signal.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/ou_shift_detection.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/ouwie.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/parsimony_score.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/patristic_distances.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/phenogram.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/phylo_heatmap.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/phylo_logistic.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/phylogenetic_glm.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/phylogenetic_ordination.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/phylogenetic_regression.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/phylomorphospace.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/polytomy_test.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/print_tree.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/prune_tree.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/quartet_network.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/quartet_pie.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/rate_heterogeneity.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/relative_rate_test.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/rename_tree_tips.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/rf_distance.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/root_tree.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/saturation.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/spectral_discordance.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/spurious_sequence.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/stochastic_character_map.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/terminal_branch_stats.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/threshold_model.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/tip_labels.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/tip_to_tip_distance.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/total_tree_length.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/trait_correlation.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/trait_rate_map.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/tree_space.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/treeness.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/treeness_over_rcv.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/vcv_utils.py +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit.egg-info/SOURCES.txt +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit.egg-info/dependency_links.txt +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit.egg-info/entry_points.txt +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit.egg-info/requires.txt +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/phykit.egg-info/top_level.txt +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/setup.cfg +0 -0
- {phykit-2.1.69 → phykit-2.1.70}/setup.py +0 -0
|
@@ -3442,6 +3442,10 @@ class Phykit:
|
|
|
3442
3442
|
gene trees for discordance-aware
|
|
3443
3443
|
VCV computation
|
|
3444
3444
|
|
|
3445
|
+
--multivariate compute K_mult (Adams 2014)
|
|
3446
|
+
for multivariate traits using
|
|
3447
|
+
a multi-column TSV file
|
|
3448
|
+
|
|
3445
3449
|
--json optional argument to output
|
|
3446
3450
|
results as JSON
|
|
3447
3451
|
"""
|
|
@@ -3476,6 +3480,10 @@ class Phykit:
|
|
|
3476
3480
|
"-g", "--gene-trees", type=str, required=False, default=None,
|
|
3477
3481
|
help=SUPPRESS, metavar=""
|
|
3478
3482
|
)
|
|
3483
|
+
parser.add_argument(
|
|
3484
|
+
"--multivariate", action="store_true", default=False,
|
|
3485
|
+
help=SUPPRESS,
|
|
3486
|
+
)
|
|
3479
3487
|
_add_json_argument(parser)
|
|
3480
3488
|
_run_service(parser, argv, PhylogeneticSignal)
|
|
3481
3489
|
|
|
@@ -19,6 +19,7 @@ class PhylogeneticSignal(Tree):
|
|
|
19
19
|
self.permutations = parsed["permutations"]
|
|
20
20
|
self.json_output = parsed["json_output"]
|
|
21
21
|
self.gene_trees_path = parsed["gene_trees_path"]
|
|
22
|
+
self.multivariate = parsed["multivariate"]
|
|
22
23
|
|
|
23
24
|
def run(self) -> None:
|
|
24
25
|
from .vcv_utils import build_vcv_matrix, build_discordance_vcv, parse_gene_trees
|
|
@@ -27,6 +28,53 @@ class PhylogeneticSignal(Tree):
|
|
|
27
28
|
self._validate_tree(tree)
|
|
28
29
|
|
|
29
30
|
tree_tips = self.get_tip_names_from_tree(tree)
|
|
31
|
+
|
|
32
|
+
if self.multivariate:
|
|
33
|
+
if self.method == "lambda":
|
|
34
|
+
raise PhykitUserError(
|
|
35
|
+
[
|
|
36
|
+
"K_mult (--multivariate) only works with Blomberg's K framework.",
|
|
37
|
+
"Pagel's lambda is not supported for multivariate data.",
|
|
38
|
+
"Use --method blombergs_k (the default) instead.",
|
|
39
|
+
],
|
|
40
|
+
code=2,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
trait_names, traits_multi = self._parse_multi_trait_file(
|
|
44
|
+
self.trait_data_path, tree_tips
|
|
45
|
+
)
|
|
46
|
+
ordered_names = sorted(traits_multi.keys())
|
|
47
|
+
|
|
48
|
+
if self.gene_trees_path:
|
|
49
|
+
gene_trees = parse_gene_trees(self.gene_trees_path)
|
|
50
|
+
vcv, vcv_meta = build_discordance_vcv(tree, gene_trees, ordered_names)
|
|
51
|
+
shared = vcv_meta["shared_taxa"]
|
|
52
|
+
if set(shared) != set(ordered_names):
|
|
53
|
+
traits_multi = {k: traits_multi[k] for k in shared}
|
|
54
|
+
ordered_names = shared
|
|
55
|
+
else:
|
|
56
|
+
vcv = build_vcv_matrix(tree, ordered_names)
|
|
57
|
+
vcv_meta = None
|
|
58
|
+
|
|
59
|
+
p = len(trait_names)
|
|
60
|
+
Y = np.array(
|
|
61
|
+
[[traits_multi[name][j] for j in range(p)] for name in ordered_names]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
result = self._kmult(Y, vcv, self.permutations)
|
|
65
|
+
if vcv_meta is not None:
|
|
66
|
+
result["vcv_metadata"] = vcv_meta
|
|
67
|
+
if self.json_output:
|
|
68
|
+
print_json(result)
|
|
69
|
+
return
|
|
70
|
+
print(
|
|
71
|
+
f"{round(result['K_mult'], 4)}\t"
|
|
72
|
+
f"{round(result['p_value'], 4)}\t"
|
|
73
|
+
f"{result['n_traits']}\t"
|
|
74
|
+
f"{result['permutations']}"
|
|
75
|
+
)
|
|
76
|
+
return
|
|
77
|
+
|
|
30
78
|
traits = self._parse_trait_file(self.trait_data_path, tree_tips)
|
|
31
79
|
|
|
32
80
|
ordered_names = sorted(traits.keys())
|
|
@@ -84,6 +132,7 @@ class PhylogeneticSignal(Tree):
|
|
|
84
132
|
permutations=getattr(args, "permutations", 1000),
|
|
85
133
|
json_output=getattr(args, "json", False),
|
|
86
134
|
gene_trees_path=getattr(args, "gene_trees", None),
|
|
135
|
+
multivariate=getattr(args, "multivariate", False),
|
|
87
136
|
)
|
|
88
137
|
|
|
89
138
|
def _validate_tree(self, tree) -> None:
|
|
@@ -171,6 +220,200 @@ class PhylogeneticSignal(Tree):
|
|
|
171
220
|
|
|
172
221
|
return {taxon: traits[taxon] for taxon in shared}
|
|
173
222
|
|
|
223
|
+
def _parse_multi_trait_file(
|
|
224
|
+
self, path: str, tree_tips: List[str]
|
|
225
|
+
) -> Tuple[List[str], Dict[str, List[float]]]:
|
|
226
|
+
"""Parse a multi-column TSV trait file with header row.
|
|
227
|
+
|
|
228
|
+
Format:
|
|
229
|
+
taxon trait1 trait2 trait3
|
|
230
|
+
A 1.0 2.0 3.0
|
|
231
|
+
B 0.5 1.5 2.5
|
|
232
|
+
|
|
233
|
+
Returns (trait_names, {taxon: [val1, val2, ...]}).
|
|
234
|
+
"""
|
|
235
|
+
try:
|
|
236
|
+
with open(path) as f:
|
|
237
|
+
lines = f.readlines()
|
|
238
|
+
except FileNotFoundError:
|
|
239
|
+
raise PhykitUserError(
|
|
240
|
+
[
|
|
241
|
+
f"{path} corresponds to no such file or directory.",
|
|
242
|
+
"Please check filename and pathing",
|
|
243
|
+
],
|
|
244
|
+
code=2,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
data_lines = []
|
|
248
|
+
for line in lines:
|
|
249
|
+
stripped = line.strip()
|
|
250
|
+
if not stripped or stripped.startswith("#"):
|
|
251
|
+
continue
|
|
252
|
+
data_lines.append(stripped)
|
|
253
|
+
|
|
254
|
+
if len(data_lines) < 2:
|
|
255
|
+
raise PhykitUserError(
|
|
256
|
+
[
|
|
257
|
+
"Multi-trait file must have a header row and at least one data row.",
|
|
258
|
+
],
|
|
259
|
+
code=2,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
header_parts = data_lines[0].split("\t")
|
|
263
|
+
n_cols = len(header_parts)
|
|
264
|
+
if n_cols < 2:
|
|
265
|
+
raise PhykitUserError(
|
|
266
|
+
[
|
|
267
|
+
"Header must have at least 2 columns (taxon + at least 1 trait).",
|
|
268
|
+
],
|
|
269
|
+
code=2,
|
|
270
|
+
)
|
|
271
|
+
trait_names = header_parts[1:]
|
|
272
|
+
|
|
273
|
+
traits = {}
|
|
274
|
+
for line_idx, line in enumerate(data_lines[1:], 2):
|
|
275
|
+
parts = line.split("\t")
|
|
276
|
+
if len(parts) != n_cols:
|
|
277
|
+
raise PhykitUserError(
|
|
278
|
+
[
|
|
279
|
+
f"Line {line_idx} has {len(parts)} columns; expected {n_cols}.",
|
|
280
|
+
f"Each line should have: taxon_name<tab>{'<tab>'.join(['trait'] * len(trait_names))}",
|
|
281
|
+
],
|
|
282
|
+
code=2,
|
|
283
|
+
)
|
|
284
|
+
taxon = parts[0]
|
|
285
|
+
values = []
|
|
286
|
+
for i, val_str in enumerate(parts[1:]):
|
|
287
|
+
try:
|
|
288
|
+
values.append(float(val_str))
|
|
289
|
+
except ValueError:
|
|
290
|
+
raise PhykitUserError(
|
|
291
|
+
[
|
|
292
|
+
f"Non-numeric trait value '{val_str}' for taxon '{taxon}' "
|
|
293
|
+
f"(trait '{trait_names[i]}') on line {line_idx}.",
|
|
294
|
+
],
|
|
295
|
+
code=2,
|
|
296
|
+
)
|
|
297
|
+
traits[taxon] = values
|
|
298
|
+
|
|
299
|
+
tree_tip_set = set(tree_tips)
|
|
300
|
+
trait_taxa_set = set(traits.keys())
|
|
301
|
+
shared = tree_tip_set & trait_taxa_set
|
|
302
|
+
|
|
303
|
+
tree_only = tree_tip_set - trait_taxa_set
|
|
304
|
+
trait_only = trait_taxa_set - tree_tip_set
|
|
305
|
+
|
|
306
|
+
if tree_only:
|
|
307
|
+
print(
|
|
308
|
+
f"Warning: {len(tree_only)} taxa in tree but not in trait file: "
|
|
309
|
+
f"{', '.join(sorted(tree_only))}",
|
|
310
|
+
file=sys.stderr,
|
|
311
|
+
)
|
|
312
|
+
if trait_only:
|
|
313
|
+
print(
|
|
314
|
+
f"Warning: {len(trait_only)} taxa in trait file but not in tree: "
|
|
315
|
+
f"{', '.join(sorted(trait_only))}",
|
|
316
|
+
file=sys.stderr,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if len(shared) < 3:
|
|
320
|
+
raise PhykitUserError(
|
|
321
|
+
[
|
|
322
|
+
f"Only {len(shared)} shared taxa between tree and trait file.",
|
|
323
|
+
"At least 3 shared taxa are required.",
|
|
324
|
+
],
|
|
325
|
+
code=2,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
filtered = {taxon: traits[taxon] for taxon in shared}
|
|
329
|
+
return trait_names, filtered
|
|
330
|
+
|
|
331
|
+
def _kmult(
|
|
332
|
+
self, Y: np.ndarray, vcv: np.ndarray, n_perm: int
|
|
333
|
+
) -> Dict:
|
|
334
|
+
"""Multivariate K (Adams 2014).
|
|
335
|
+
|
|
336
|
+
Y: n x p matrix of trait values (n taxa, p traits)
|
|
337
|
+
vcv: n x n phylogenetic VCV matrix
|
|
338
|
+
|
|
339
|
+
K_mult = observed_ratio / expected_ratio
|
|
340
|
+
|
|
341
|
+
Where:
|
|
342
|
+
- observed_ratio = MSE_obs / MSE_phylo
|
|
343
|
+
- MSE_obs = sum of squared distances from each species to the
|
|
344
|
+
phylogenetic mean (using Euclidean distance in trait space)
|
|
345
|
+
- MSE_phylo = sum of squared phylogenetically weighted distances
|
|
346
|
+
(using C_inv weighting)
|
|
347
|
+
- expected_ratio = same formula as univariate K but uses trace
|
|
348
|
+
operations on the multivariate analogue
|
|
349
|
+
"""
|
|
350
|
+
n, p = Y.shape
|
|
351
|
+
ones = np.ones(n)
|
|
352
|
+
|
|
353
|
+
C = vcv.copy()
|
|
354
|
+
C_inv = np.linalg.inv(C)
|
|
355
|
+
sum_C_inv = float(ones @ C_inv @ ones)
|
|
356
|
+
|
|
357
|
+
# GLS estimate of phylogenetic mean (p-dimensional vector)
|
|
358
|
+
# a_hat = (1^T C^{-1} 1)^{-1} * (1^T C^{-1} Y)
|
|
359
|
+
a_hat = (ones @ C_inv @ Y) / sum_C_inv # shape: (p,)
|
|
360
|
+
|
|
361
|
+
# Residual matrix: each row is (Y_i - a_hat)
|
|
362
|
+
E = Y - a_hat # n x p
|
|
363
|
+
|
|
364
|
+
# MSE_obs: sum of squared Euclidean distances from each species to phylo mean
|
|
365
|
+
# = trace(E^T @ E)
|
|
366
|
+
MSE_obs = np.trace(E.T @ E)
|
|
367
|
+
|
|
368
|
+
# MSE_phylo: phylogenetically-weighted version
|
|
369
|
+
# = trace(E^T @ C_inv @ E)
|
|
370
|
+
MSE_phylo = np.trace(E.T @ C_inv @ E)
|
|
371
|
+
|
|
372
|
+
# Observed ratio
|
|
373
|
+
if MSE_phylo == 0:
|
|
374
|
+
observed_ratio = 0.0
|
|
375
|
+
else:
|
|
376
|
+
observed_ratio = MSE_obs / MSE_phylo
|
|
377
|
+
|
|
378
|
+
# Expected ratio under BM (same as univariate, from Blomberg et al. 2003)
|
|
379
|
+
expected_ratio = (np.trace(C) - n / sum_C_inv) / (n - 1)
|
|
380
|
+
|
|
381
|
+
# K_mult
|
|
382
|
+
if expected_ratio == 0:
|
|
383
|
+
K_mult = 0.0
|
|
384
|
+
else:
|
|
385
|
+
K_mult = observed_ratio / expected_ratio
|
|
386
|
+
|
|
387
|
+
# Permutation test: shuffle rows of Y, recompute K_mult
|
|
388
|
+
rng = np.random.default_rng(seed=42)
|
|
389
|
+
k_perm = np.empty(n_perm)
|
|
390
|
+
for perm_i in range(n_perm):
|
|
391
|
+
# Permute rows (species) of Y -- keeps trait correlations intact
|
|
392
|
+
perm_idx = rng.permutation(n)
|
|
393
|
+
Y_perm = Y[perm_idx]
|
|
394
|
+
|
|
395
|
+
a_p = (ones @ C_inv @ Y_perm) / sum_C_inv
|
|
396
|
+
E_p = Y_perm - a_p
|
|
397
|
+
|
|
398
|
+
MSE_obs_p = np.trace(E_p.T @ E_p)
|
|
399
|
+
MSE_phylo_p = np.trace(E_p.T @ C_inv @ E_p)
|
|
400
|
+
|
|
401
|
+
if MSE_phylo_p == 0:
|
|
402
|
+
obs_ratio_p = 0.0
|
|
403
|
+
else:
|
|
404
|
+
obs_ratio_p = MSE_obs_p / MSE_phylo_p
|
|
405
|
+
|
|
406
|
+
k_perm[perm_i] = obs_ratio_p / expected_ratio if expected_ratio != 0 else 0.0
|
|
407
|
+
|
|
408
|
+
p_value = float(np.mean(k_perm >= K_mult))
|
|
409
|
+
|
|
410
|
+
return dict(
|
|
411
|
+
K_mult=float(K_mult),
|
|
412
|
+
p_value=p_value,
|
|
413
|
+
permutations=n_perm,
|
|
414
|
+
n_traits=p,
|
|
415
|
+
)
|
|
416
|
+
|
|
174
417
|
def _build_vcv_matrix(
|
|
175
418
|
self, tree, ordered_names: List[str]
|
|
176
419
|
) -> np.ndarray:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.1.70"
|
phykit-2.1.69/phykit/version.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.1.69"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|