phykit 2.1.67__tar.gz → 2.1.69__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.67 → phykit-2.1.69}/PKG-INFO +1 -1
  2. {phykit-2.1.67 → phykit-2.1.69}/phykit/cli_registry.py +3 -0
  3. {phykit-2.1.67 → phykit-2.1.69}/phykit/phykit.py +102 -0
  4. {phykit-2.1.67 → phykit-2.1.69}/phykit/service_factories.py +1 -0
  5. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/__init__.py +1 -0
  6. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/ancestral_reconstruction.py +63 -2
  7. phykit-2.1.69/phykit/services/tree/phylo_logistic.py +607 -0
  8. phykit-2.1.69/phykit/version.py +1 -0
  9. {phykit-2.1.67 → phykit-2.1.69}/phykit.egg-info/PKG-INFO +1 -1
  10. {phykit-2.1.67 → phykit-2.1.69}/phykit.egg-info/SOURCES.txt +1 -0
  11. {phykit-2.1.67 → phykit-2.1.69}/phykit.egg-info/entry_points.txt +3 -0
  12. {phykit-2.1.67 → phykit-2.1.69}/setup.py +3 -0
  13. phykit-2.1.67/phykit/version.py +0 -1
  14. {phykit-2.1.67 → phykit-2.1.69}/LICENSE.md +0 -0
  15. {phykit-2.1.67 → phykit-2.1.69}/README.md +0 -0
  16. {phykit-2.1.67 → phykit-2.1.69}/phykit/__init__.py +0 -0
  17. {phykit-2.1.67 → phykit-2.1.69}/phykit/__main__.py +0 -0
  18. {phykit-2.1.67 → phykit-2.1.69}/phykit/errors.py +0 -0
  19. {phykit-2.1.67 → phykit-2.1.69}/phykit/helpers/__init__.py +0 -0
  20. {phykit-2.1.67 → phykit-2.1.69}/phykit/helpers/boolean_argument_parsing.py +0 -0
  21. {phykit-2.1.67 → phykit-2.1.69}/phykit/helpers/caching.py +0 -0
  22. {phykit-2.1.67 → phykit-2.1.69}/phykit/helpers/circular_layout.py +0 -0
  23. {phykit-2.1.67 → phykit-2.1.69}/phykit/helpers/color_annotations.py +0 -0
  24. {phykit-2.1.67 → phykit-2.1.69}/phykit/helpers/discrete_models.py +0 -0
  25. {phykit-2.1.67 → phykit-2.1.69}/phykit/helpers/files.py +0 -0
  26. {phykit-2.1.67 → phykit-2.1.69}/phykit/helpers/json_output.py +0 -0
  27. {phykit-2.1.67 → phykit-2.1.69}/phykit/helpers/parallel.py +0 -0
  28. {phykit-2.1.67 → phykit-2.1.69}/phykit/helpers/parsimony_utils.py +0 -0
  29. {phykit-2.1.67 → phykit-2.1.69}/phykit/helpers/plot_config.py +0 -0
  30. {phykit-2.1.67 → phykit-2.1.69}/phykit/helpers/quartet_utils.py +0 -0
  31. {phykit-2.1.67 → phykit-2.1.69}/phykit/helpers/stats_summary.py +0 -0
  32. {phykit-2.1.67 → phykit-2.1.69}/phykit/helpers/streaming.py +0 -0
  33. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/__init__.py +0 -0
  34. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/__init__.py +0 -0
  35. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/alignment_entropy.py +0 -0
  36. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/alignment_length.py +0 -0
  37. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/alignment_length_no_gaps.py +0 -0
  38. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/alignment_outlier_taxa.py +0 -0
  39. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/alignment_recoding.py +0 -0
  40. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/alignment_subsample.py +0 -0
  41. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/base.py +0 -0
  42. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/column_score.py +0 -0
  43. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/composition_per_taxon.py +0 -0
  44. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/compositional_bias_per_site.py +0 -0
  45. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/create_concatenation_matrix.py +0 -0
  46. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/dfoil.py +0 -0
  47. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/dna_threader.py +0 -0
  48. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/dstatistic.py +0 -0
  49. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/evolutionary_rate_per_site.py +0 -0
  50. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/faidx.py +0 -0
  51. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/gc_content.py +0 -0
  52. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/identity_matrix.py +0 -0
  53. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/mask_alignment.py +0 -0
  54. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/occupancy_per_taxon.py +0 -0
  55. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/pairwise_identity.py +0 -0
  56. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/parsimony_informative_sites.py +0 -0
  57. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/plot_alignment_qc.py +0 -0
  58. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/rcv.py +0 -0
  59. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/rcvt.py +0 -0
  60. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/rename_fasta_entries.py +0 -0
  61. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/sum_of_pairs_score.py +0 -0
  62. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/alignment/variable_sites.py +0 -0
  63. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/base.py +0 -0
  64. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/base.py +0 -0
  65. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/bipartition_support_stats.py +0 -0
  66. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/branch_length_multiplier.py +0 -0
  67. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/character_map.py +0 -0
  68. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/collapse_branches.py +0 -0
  69. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/concordance_asr.py +0 -0
  70. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/consensus_network.py +0 -0
  71. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/consensus_tree.py +0 -0
  72. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/cont_map.py +0 -0
  73. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/cophylo.py +0 -0
  74. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/covarying_evolutionary_rates.py +0 -0
  75. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/density_map.py +0 -0
  76. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/discordance_asymmetry.py +0 -0
  77. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/dvmc.py +0 -0
  78. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/evo_tempo_map.py +0 -0
  79. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/evolutionary_rate.py +0 -0
  80. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/fit_continuous.py +0 -0
  81. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/fit_discrete.py +0 -0
  82. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/hidden_paralogy_check.py +0 -0
  83. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/independent_contrasts.py +0 -0
  84. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/internal_branch_stats.py +0 -0
  85. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/internode_labeler.py +0 -0
  86. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/kf_distance.py +0 -0
  87. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/last_common_ancestor_subtree.py +0 -0
  88. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/lb_score.py +0 -0
  89. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/ltt.py +0 -0
  90. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/monophyly_check.py +0 -0
  91. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/nearest_neighbor_interchange.py +0 -0
  92. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/network_signal.py +0 -0
  93. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/ou_shift_detection.py +0 -0
  94. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/ouwie.py +0 -0
  95. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/parsimony_score.py +0 -0
  96. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/patristic_distances.py +0 -0
  97. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/phenogram.py +0 -0
  98. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/phylo_heatmap.py +0 -0
  99. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/phylogenetic_glm.py +0 -0
  100. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/phylogenetic_ordination.py +0 -0
  101. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/phylogenetic_regression.py +0 -0
  102. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/phylogenetic_signal.py +0 -0
  103. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/phylomorphospace.py +0 -0
  104. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/polytomy_test.py +0 -0
  105. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/print_tree.py +0 -0
  106. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/prune_tree.py +0 -0
  107. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/quartet_network.py +0 -0
  108. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/quartet_pie.py +0 -0
  109. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/rate_heterogeneity.py +0 -0
  110. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/relative_rate_test.py +0 -0
  111. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/rename_tree_tips.py +0 -0
  112. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/rf_distance.py +0 -0
  113. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/root_tree.py +0 -0
  114. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/saturation.py +0 -0
  115. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/spectral_discordance.py +0 -0
  116. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/spurious_sequence.py +0 -0
  117. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/stochastic_character_map.py +0 -0
  118. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/terminal_branch_stats.py +0 -0
  119. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/threshold_model.py +0 -0
  120. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/tip_labels.py +0 -0
  121. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/tip_to_tip_distance.py +0 -0
  122. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/tip_to_tip_node_distance.py +0 -0
  123. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/total_tree_length.py +0 -0
  124. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/trait_correlation.py +0 -0
  125. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/trait_rate_map.py +0 -0
  126. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/tree_space.py +0 -0
  127. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/treeness.py +0 -0
  128. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/treeness_over_rcv.py +0 -0
  129. {phykit-2.1.67 → phykit-2.1.69}/phykit/services/tree/vcv_utils.py +0 -0
  130. {phykit-2.1.67 → phykit-2.1.69}/phykit.egg-info/dependency_links.txt +0 -0
  131. {phykit-2.1.67 → phykit-2.1.69}/phykit.egg-info/requires.txt +0 -0
  132. {phykit-2.1.67 → phykit-2.1.69}/phykit.egg-info/top_level.txt +0 -0
  133. {phykit-2.1.67 → phykit-2.1.69}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phykit
3
- Version: 2.1.67
3
+ Version: 2.1.69
4
4
  Home-page: https://github.com/jlsteenwyk/phykit
5
5
  Author: Jacob L. Steenwyk
6
6
  Author-email: jlsteenwyk@gmail.com
@@ -97,6 +97,9 @@ ALIAS_TO_HANDLER: Dict[str, str] = {
97
97
  "pgls": "phylogenetic_regression",
98
98
  "phylo_glm": "phylogenetic_glm",
99
99
  "pglm": "phylogenetic_glm",
100
+ "phylo_logistic": "phylo_logistic",
101
+ "phylo_logreg": "phylo_logistic",
102
+ "plogreg": "phylo_logistic",
100
103
  "parsimony": "parsimony_score",
101
104
  "pars": "parsimony_score",
102
105
  "charmap": "character_map",
@@ -237,6 +237,8 @@ class Phykit:
237
237
  - fit phylogenetic generalized least squares (PGLS) regression
238
238
  phylogenetic_glm (alias: phylo_glm; pglm)
239
239
  - fit phylogenetic GLM for binary (logistic) or count (Poisson) data
240
+ phylo_logistic (alias: phylo_logreg; plogreg)
241
+ - fit phylogenetic logistic regression (Ives & Garland 2010)
240
242
  stochastic_character_map (alias: simmap; scm)
241
243
  - stochastic character mapping (SIMMAP) of discrete traits
242
244
  cont_map (alias: contmap; cmap)
@@ -2431,6 +2433,16 @@ class Phykit:
2431
2433
  and branch colors (iTOL-
2432
2434
  inspired TSV format)
2433
2435
 
2436
+ --plot-ci draw confidence interval bars
2437
+ at internal nodes on the
2438
+ contMap plot (requires --ci
2439
+ and --plot)
2440
+
2441
+ --ci-size scale factor for CI bar
2442
+ size (default: 1.0; use
2443
+ 2.0 for larger, 0.5 for
2444
+ smaller)
2445
+
2434
2446
  --json output results as JSON
2435
2447
  """
2436
2448
  ),
@@ -2464,6 +2476,14 @@ class Phykit:
2464
2476
  "--plot", type=str, required=False, default=None,
2465
2477
  help=SUPPRESS, metavar=""
2466
2478
  )
2479
+ parser.add_argument(
2480
+ "--plot-ci", action="store_true", required=False, default=False,
2481
+ help=SUPPRESS,
2482
+ )
2483
+ parser.add_argument(
2484
+ "--ci-size", type=float, required=False, default=1.0,
2485
+ help=SUPPRESS, metavar=""
2486
+ )
2467
2487
  add_plot_arguments(parser)
2468
2488
  _add_json_argument(parser)
2469
2489
  _run_service(parser, argv, AncestralReconstruction)
@@ -4269,6 +4289,84 @@ class Phykit:
4269
4289
  _add_json_argument(parser)
4270
4290
  _run_service(parser, argv, PhylogeneticGLM)
4271
4291
 
4292
+ @staticmethod
4293
+ def phylo_logistic(argv):
4294
+ parser = _new_parser(
4295
+ description=textwrap.dedent(
4296
+ f"""\
4297
+ {help_header}
4298
+
4299
+ Fit a Phylogenetic Logistic Regression for binary (0/1)
4300
+ response data while accounting for phylogenetic
4301
+ non-independence among species (Ives & Garland 2010).
4302
+
4303
+ Uses Maximum Penalized Likelihood Estimation (logistic_MPLE)
4304
+ with Firth's bias-correction penalty and jointly estimates
4305
+ the phylogenetic signal parameter alpha via the
4306
+ OU-transformed variance-covariance matrix.
4307
+
4308
+ Input is a phylogenetic tree and a tab-delimited multi-trait
4309
+ file with a header row:
4310
+ taxon<tab>trait1<tab>trait2<tab>...
4311
+
4312
+ Output includes coefficient estimates, standard errors,
4313
+ z-values, p-values, alpha, log-likelihood, penalized
4314
+ log-likelihood, and AIC.
4315
+
4316
+ Aliases:
4317
+ phylo_logistic, phylo_logreg, plogreg
4318
+ Command line interfaces:
4319
+ pk_phylo_logistic, pk_phylo_logreg, pk_plogreg
4320
+
4321
+ Usage:
4322
+ phykit phylo_logistic -t <tree> -d <trait_data> --response <column> --predictor <column> [--method logistic_MPLE|logistic_IG10] [--json]
4323
+
4324
+ Options
4325
+ =====================================================
4326
+ -t/--tree a tree file
4327
+
4328
+ -d/--trait-data tab-delimited multi-trait file
4329
+ with header row
4330
+
4331
+ --response binary response column name
4332
+ (must contain only 0 and 1)
4333
+
4334
+ --predictor predictor column name(s),
4335
+ comma-separated for multiple
4336
+
4337
+ --method estimation method: logistic_MPLE
4338
+ or logistic_IG10
4339
+ (default: logistic_MPLE)
4340
+
4341
+ --json optional argument to output
4342
+ results as JSON
4343
+ """
4344
+ ),
4345
+ )
4346
+ parser.add_argument(
4347
+ "-t", "--tree", type=str, required=True, help=SUPPRESS, metavar=""
4348
+ )
4349
+ parser.add_argument(
4350
+ "-d", "--trait-data", type=str, required=True, help=SUPPRESS, metavar=""
4351
+ )
4352
+ parser.add_argument(
4353
+ "--response", type=str, required=True, help=SUPPRESS, metavar=""
4354
+ )
4355
+ parser.add_argument(
4356
+ "--predictor", type=str, required=True, help=SUPPRESS, metavar=""
4357
+ )
4358
+ parser.add_argument(
4359
+ "--method",
4360
+ type=str,
4361
+ required=False,
4362
+ default="logistic_MPLE",
4363
+ choices=["logistic_MPLE", "logistic_IG10"],
4364
+ help=SUPPRESS,
4365
+ metavar="",
4366
+ )
4367
+ _add_json_argument(parser)
4368
+ _run_service(parser, argv, PhyloLogistic)
4369
+
4272
4370
  @staticmethod
4273
4371
  def stochastic_character_map(argv):
4274
4372
  parser = _new_parser(
@@ -8174,6 +8272,10 @@ def phylogenetic_glm(argv=None):
8174
8272
  Phykit.phylogenetic_glm(sys.argv[1:])
8175
8273
 
8176
8274
 
8275
+ def phylo_logistic(argv=None):
8276
+ Phykit.phylo_logistic(sys.argv[1:])
8277
+
8278
+
8177
8279
  def stochastic_character_map(argv=None):
8178
8280
  Phykit.stochastic_character_map(sys.argv[1:])
8179
8281
 
@@ -71,6 +71,7 @@ PhylogeneticOrdination = _LazyServiceFactory("phykit.services.tree.phylogenetic_
71
71
  Phylomorphospace = _LazyServiceFactory("phykit.services.tree.phylomorphospace", "Phylomorphospace")
72
72
  PhylogeneticRegression = _LazyServiceFactory("phykit.services.tree.phylogenetic_regression", "PhylogeneticRegression")
73
73
  PhylogeneticGLM = _LazyServiceFactory("phykit.services.tree.phylogenetic_glm", "PhylogeneticGLM")
74
+ PhyloLogistic = _LazyServiceFactory("phykit.services.tree.phylo_logistic", "PhyloLogistic")
74
75
  StochasticCharacterMap = _LazyServiceFactory("phykit.services.tree.stochastic_character_map", "StochasticCharacterMap")
75
76
  ContMap = _LazyServiceFactory("phykit.services.tree.cont_map", "ContMap")
76
77
  DensityMap = _LazyServiceFactory("phykit.services.tree.density_map", "DensityMap")
@@ -48,6 +48,7 @@ _EXPORTS = {
48
48
  "ThresholdModel": "threshold_model",
49
49
  "TreenessOverRCV": "treeness_over_rcv",
50
50
  "ConcordanceAsr": "concordance_asr",
51
+ "PhyloLogistic": "phylo_logistic",
51
52
  "TraitCorrelation": "trait_correlation",
52
53
  "TraitRateMap": "trait_rate_map",
53
54
  "TreeSpace": "tree_space",
@@ -48,6 +48,8 @@ class AncestralReconstruction(Tree):
48
48
  self.json_output = parsed["json_output"]
49
49
  self.trait_type = parsed["trait_type"]
50
50
  self.model = parsed["model"]
51
+ self.plot_ci = parsed["plot_ci"]
52
+ self.ci_size = parsed["ci_size"]
51
53
  self.plot_config = parsed["plot_config"]
52
54
 
53
55
  def run(self) -> None:
@@ -112,9 +114,11 @@ class AncestralReconstruction(Tree):
112
114
  )
113
115
 
114
116
  if self.plot_output:
117
+ ci_data = node_cis if (self.plot_ci and self.ci and node_cis) else None
115
118
  self._plot_contmap(
116
119
  tree_copy, node_estimates, node_labels,
117
- trait_values, trait_name, self.plot_output
120
+ trait_values, trait_name, self.plot_output,
121
+ node_cis=ci_data,
118
122
  )
119
123
  result["plot_output"] = self.plot_output
120
124
 
@@ -144,6 +148,8 @@ class AncestralReconstruction(Tree):
144
148
  json_output=getattr(args, "json", False),
145
149
  trait_type=getattr(args, "type", "continuous"),
146
150
  model=getattr(args, "model", "ER"),
151
+ plot_ci=getattr(args, "plot_ci", False),
152
+ ci_size=getattr(args, "ci_size", 1.0),
147
153
  plot_config=PlotConfig.from_args(args),
148
154
  )
149
155
 
@@ -809,7 +815,7 @@ class AncestralReconstruction(Tree):
809
815
 
810
816
  def _plot_contmap(
811
817
  self, tree, node_estimates, node_labels, trait_values,
812
- trait_name, output_path,
818
+ trait_name, output_path, node_cis=None,
813
819
  ) -> None:
814
820
  try:
815
821
  import matplotlib
@@ -827,6 +833,7 @@ class AncestralReconstruction(Tree):
827
833
 
828
834
  # Build estimates dict keyed by id(clade) for all nodes
829
835
  all_estimates = {}
836
+ node_labels_map = {} # id(clade) → label string (for CI lookup)
830
837
  for clade in tree.find_clades(order="preorder"):
831
838
  if clade.is_terminal():
832
839
  if clade.name in trait_values:
@@ -834,6 +841,7 @@ class AncestralReconstruction(Tree):
834
841
  else:
835
842
  if id(clade) in node_labels:
836
843
  label = node_labels[id(clade)]
844
+ node_labels_map[id(clade)] = label
837
845
  if label in node_estimates:
838
846
  all_estimates[id(clade)] = node_estimates[label]
839
847
 
@@ -1043,6 +1051,59 @@ class AncestralReconstruction(Tree):
1043
1051
  fontsize=config.title_fontsize,
1044
1052
  )
1045
1053
 
1054
+ # Draw CI bars at internal nodes if requested
1055
+ if node_cis:
1056
+ ci_scale = self.ci_size
1057
+ # Build label→id mapping for looking up CI values
1058
+ label_to_id = {}
1059
+ for clade in tree.find_clades(order="preorder"):
1060
+ if not clade.is_terminal() and id(clade) in node_labels_map:
1061
+ label_to_id[node_labels_map[id(clade)]] = id(clade)
1062
+
1063
+ if config.circular:
1064
+ # Circular: use data coordinates from coords dict
1065
+ for label, (ci_lo, ci_hi) in node_cis.items():
1066
+ cid = label_to_id.get(label)
1067
+ if cid is None or cid not in coords:
1068
+ continue
1069
+ cx = coords[cid]["x"]
1070
+ cy = coords[cid]["y"]
1071
+ angle = coords[cid]["angle"]
1072
+ # CI bar perpendicular to the radius (tangential)
1073
+ bar_len = (ci_hi - ci_lo) * ci_scale * 0.3
1074
+ dx = -np.sin(angle) * bar_len / 2
1075
+ dy = np.cos(angle) * bar_len / 2
1076
+ # Main bar
1077
+ ax.plot([cx - dx, cx + dx], [cy - dy, cy + dy],
1078
+ color="black", lw=1.5 * ci_scale, zorder=8)
1079
+ # Point estimate dot
1080
+ ax.scatter(cx, cy, s=15 * ci_scale, c="black", zorder=9)
1081
+ else:
1082
+ # Rectangular: vertical bars at node positions
1083
+ max_x_val = max(node_x.values()) if node_x else 1.0
1084
+ for label, (ci_lo, ci_hi) in node_cis.items():
1085
+ cid = label_to_id.get(label)
1086
+ if cid is None or cid not in node_x or cid not in node_y:
1087
+ continue
1088
+ cx = node_x[cid]
1089
+ cy = node_y[cid]
1090
+ est = all_estimates.get(cid, (ci_lo + ci_hi) / 2)
1091
+ # Scale CI width relative to y-axis range
1092
+ bar_half = (ci_hi - ci_lo) / (vmax - vmin) * 0.4 * ci_scale if vmax != vmin else 0.2 * ci_scale
1093
+ cap_width = max_x_val * 0.008 * ci_scale
1094
+ # Vertical bar
1095
+ ax.plot([cx, cx], [cy - bar_half, cy + bar_half],
1096
+ color="black", lw=1.2 * ci_scale, zorder=8)
1097
+ # Caps
1098
+ ax.plot([cx - cap_width, cx + cap_width],
1099
+ [cy - bar_half, cy - bar_half],
1100
+ color="black", lw=1.0 * ci_scale, zorder=8)
1101
+ ax.plot([cx - cap_width, cx + cap_width],
1102
+ [cy + bar_half, cy + bar_half],
1103
+ color="black", lw=1.0 * ci_scale, zorder=8)
1104
+ # Point estimate dot
1105
+ ax.scatter(cx, cy, s=12 * ci_scale, c="black", zorder=9)
1106
+
1046
1107
  fig.tight_layout()
1047
1108
  fig.savefig(output_path, dpi=config.dpi, bbox_inches="tight")
1048
1109
  plt.close(fig)