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.
Files changed (133) hide show
  1. {phykit-2.1.69 → phykit-2.1.70}/PKG-INFO +1 -1
  2. {phykit-2.1.69 → phykit-2.1.70}/phykit/phykit.py +8 -0
  3. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/phylogenetic_signal.py +243 -0
  4. phykit-2.1.70/phykit/version.py +1 -0
  5. {phykit-2.1.69 → phykit-2.1.70}/phykit.egg-info/PKG-INFO +1 -1
  6. phykit-2.1.69/phykit/version.py +0 -1
  7. {phykit-2.1.69 → phykit-2.1.70}/LICENSE.md +0 -0
  8. {phykit-2.1.69 → phykit-2.1.70}/README.md +0 -0
  9. {phykit-2.1.69 → phykit-2.1.70}/phykit/__init__.py +0 -0
  10. {phykit-2.1.69 → phykit-2.1.70}/phykit/__main__.py +0 -0
  11. {phykit-2.1.69 → phykit-2.1.70}/phykit/cli_registry.py +0 -0
  12. {phykit-2.1.69 → phykit-2.1.70}/phykit/errors.py +0 -0
  13. {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/__init__.py +0 -0
  14. {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/boolean_argument_parsing.py +0 -0
  15. {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/caching.py +0 -0
  16. {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/circular_layout.py +0 -0
  17. {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/color_annotations.py +0 -0
  18. {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/discrete_models.py +0 -0
  19. {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/files.py +0 -0
  20. {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/json_output.py +0 -0
  21. {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/parallel.py +0 -0
  22. {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/parsimony_utils.py +0 -0
  23. {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/plot_config.py +0 -0
  24. {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/quartet_utils.py +0 -0
  25. {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/stats_summary.py +0 -0
  26. {phykit-2.1.69 → phykit-2.1.70}/phykit/helpers/streaming.py +0 -0
  27. {phykit-2.1.69 → phykit-2.1.70}/phykit/service_factories.py +0 -0
  28. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/__init__.py +0 -0
  29. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/__init__.py +0 -0
  30. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/alignment_entropy.py +0 -0
  31. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/alignment_length.py +0 -0
  32. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
  33. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
  34. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/alignment_recoding.py +0 -0
  35. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/alignment_subsample.py +0 -0
  36. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/base.py +0 -0
  37. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/column_score.py +0 -0
  38. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/composition_per_taxon.py +0 -0
  39. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
  40. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
  41. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/dfoil.py +0 -0
  42. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/dna_threader.py +0 -0
  43. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/dstatistic.py +0 -0
  44. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
  45. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/faidx.py +0 -0
  46. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/gc_content.py +0 -0
  47. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/identity_matrix.py +0 -0
  48. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/mask_alignment.py +0 -0
  49. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
  50. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/pairwise_identity.py +0 -0
  51. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
  52. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/plot_alignment_qc.py +0 -0
  53. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/rcv.py +0 -0
  54. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/rcvt.py +0 -0
  55. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/rename_fasta_entries.py +0 -0
  56. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
  57. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/alignment/variable_sites.py +0 -0
  58. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/base.py +0 -0
  59. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/__init__.py +0 -0
  60. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/ancestral_reconstruction.py +0 -0
  61. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/base.py +0 -0
  62. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/bipartition_support_stats.py +0 -0
  63. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/branch_length_multiplier.py +0 -0
  64. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/character_map.py +0 -0
  65. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/collapse_branches.py +0 -0
  66. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/concordance_asr.py +0 -0
  67. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/consensus_network.py +0 -0
  68. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/consensus_tree.py +0 -0
  69. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/cont_map.py +0 -0
  70. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/cophylo.py +0 -0
  71. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
  72. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/density_map.py +0 -0
  73. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/discordance_asymmetry.py +0 -0
  74. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/dvmc.py +0 -0
  75. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/evo_tempo_map.py +0 -0
  76. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/evolutionary_rate.py +0 -0
  77. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/fit_continuous.py +0 -0
  78. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/fit_discrete.py +0 -0
  79. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/hidden_paralogy_check.py +0 -0
  80. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/independent_contrasts.py +0 -0
  81. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/internal_branch_stats.py +0 -0
  82. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/internode_labeler.py +0 -0
  83. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/kf_distance.py +0 -0
  84. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
  85. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/lb_score.py +0 -0
  86. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/ltt.py +0 -0
  87. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/monophyly_check.py +0 -0
  88. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
  89. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/network_signal.py +0 -0
  90. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/ou_shift_detection.py +0 -0
  91. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/ouwie.py +0 -0
  92. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/parsimony_score.py +0 -0
  93. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/patristic_distances.py +0 -0
  94. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/phenogram.py +0 -0
  95. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/phylo_heatmap.py +0 -0
  96. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/phylo_logistic.py +0 -0
  97. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/phylogenetic_glm.py +0 -0
  98. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/phylogenetic_ordination.py +0 -0
  99. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/phylogenetic_regression.py +0 -0
  100. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/phylomorphospace.py +0 -0
  101. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/polytomy_test.py +0 -0
  102. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/print_tree.py +0 -0
  103. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/prune_tree.py +0 -0
  104. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/quartet_network.py +0 -0
  105. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/quartet_pie.py +0 -0
  106. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/rate_heterogeneity.py +0 -0
  107. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/relative_rate_test.py +0 -0
  108. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/rename_tree_tips.py +0 -0
  109. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/rf_distance.py +0 -0
  110. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/root_tree.py +0 -0
  111. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/saturation.py +0 -0
  112. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/spectral_discordance.py +0 -0
  113. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/spurious_sequence.py +0 -0
  114. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/stochastic_character_map.py +0 -0
  115. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/terminal_branch_stats.py +0 -0
  116. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/threshold_model.py +0 -0
  117. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/tip_labels.py +0 -0
  118. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/tip_to_tip_distance.py +0 -0
  119. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
  120. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/total_tree_length.py +0 -0
  121. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/trait_correlation.py +0 -0
  122. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/trait_rate_map.py +0 -0
  123. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/tree_space.py +0 -0
  124. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/treeness.py +0 -0
  125. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/treeness_over_rcv.py +0 -0
  126. {phykit-2.1.69 → phykit-2.1.70}/phykit/services/tree/vcv_utils.py +0 -0
  127. {phykit-2.1.69 → phykit-2.1.70}/phykit.egg-info/SOURCES.txt +0 -0
  128. {phykit-2.1.69 → phykit-2.1.70}/phykit.egg-info/dependency_links.txt +0 -0
  129. {phykit-2.1.69 → phykit-2.1.70}/phykit.egg-info/entry_points.txt +0 -0
  130. {phykit-2.1.69 → phykit-2.1.70}/phykit.egg-info/requires.txt +0 -0
  131. {phykit-2.1.69 → phykit-2.1.70}/phykit.egg-info/top_level.txt +0 -0
  132. {phykit-2.1.69 → phykit-2.1.70}/setup.cfg +0 -0
  133. {phykit-2.1.69 → phykit-2.1.70}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phykit
3
- Version: 2.1.69
3
+ Version: 2.1.70
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
@@ -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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phykit
3
- Version: 2.1.69
3
+ Version: 2.1.70
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
@@ -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