Skip to content

Add parsimony and fitch_count; consolidate linkage tests (v0.3.0)#59

Merged
colganwi merged 7 commits into
mainfrom
parsimony
Jul 8, 2026
Merged

Add parsimony and fitch_count; consolidate linkage tests (v0.3.0)#59
colganwi merged 7 commits into
mainfrom
parsimony

Conversation

@colganwi

@colganwi colganwi commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Migrates small-parsimony scoring and the FitchCount algorithm from Cassiopeia into pycea, cleans up the test suite, and cuts the v0.3.0 release.

New functions (pycea.tl)

  • parsimony — small-parsimony score of a categorical character. Reconstructs ancestral states via ancestral_states (default fitch_hartigan) or uses states already on the tree (reconstruct=False). Returns an int for a single tree, a Series across trees; also stored in tdata.uns.
  • fitch_count — FitchCount transition-count algorithm across all equally-parsimonious solutions, returning an MxM DataFrame summed across trees.

Both reuse pycea's Fitch–Hartigan machinery: the down-pass was extracted into a shared _fitch_hartigan_downpass helper in ancestral_states.py (behavior-preserving for existing reconstruction). Wired into the API, docs, changelog, and the Quinn 2021 citation added.

Test suite cleanup

  • Suppressed all expected test warnings via module-level filterwarnings markers (small-category linkage, multi-threaded-fork, empty-legend). Suite-wide warning count is now 0.
  • Consolidated test_ancestral_linkage.py from 71 test functions to 22, grouping per-assertion tests into coherent per-feature tests and parametrizing the brute-force regressions over the ultrametric/non-ultrametric fixtures. Distinct behavior coverage preserved.

Other

  • Type-checking / robustness fixes across tl/pl (get_keyed_obs_data, ancestral_linkage, partition_test, pl.ancestral_linkage).
  • v0.3.0 release (CHANGELOG + pyproject.toml version bump).

Test plan

  • conda run -n pycea python -m pytest tests/ --ignore-glob='**/._*.py' → passing, 0 warnings.
  • New tests/test_parsimony.py covers both functions, including hand-computed FitchCount matrices and the multi-tree summation.

🤖 Generated with Claude Code

colganwi and others added 5 commits July 8, 2026 18:40
Migrate small-parsimony scoring and the FitchCount transition-count
algorithm from Cassiopeia into first-class pycea.tl functions operating on
TreeData. Reuse pycea's Fitch-Hartigan machinery by extracting a shared
_fitch_hartigan_downpass helper in ancestral_states.py, used by both the
existing reconstruction and fitch_count's state-set computation.

- parsimony: reconstructs states (via ancestral_states) or uses existing
  ones; returns an int for a single tree, a Series across trees.
- fitch_count: returns an MxM count matrix, summed across trees.

Wire both into the API, docs, changelog, and add the Quinn 2021 citation.
Includes tests in tests/test_parsimony.py.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add module-level filterwarnings markers for warnings the tests
intentionally trigger: the "no artists with labels" legend warning in the
hex-color tree plots, and the small-category / multi-threaded-fork warnings
in the ancestral_linkage plot tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reduce test_ancestral_linkage from 71 test functions to 22, grouping
redundant per-assertion tests into coherent per-feature tests (pairwise
values, output structure, min_size, symmetrize, single-target, by_tree,
permutation, alternative, permutation_mode, normalize) and parametrizing
the brute-force regression checks over the ultrametric and non-ultrametric
fixtures. Also add filterwarnings markers for the expected small-category
and multi-threaded-fork warnings. Coverage of distinct behaviors is
preserved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- get_keyed_obs_data: pass through already-categorical array data unchanged
  instead of recoercing.
- ancestral_linkage: accept Mapping trees, map obs via plain dicts, and
  annotate tree_distance/compute_scores calls for the type checker.
- partition_test: support callable metrics, use np.atleast_2d for stat
  reshaping, and quiet ttest_ind typing.
- pl.ancestral_linkage: clearer error when the stats key or figure is
  missing.
- docs: register tl.ancestral_linkage in the API reference.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.95376% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.83%. Comparing base (32044bd) to head (f6e90c0).

Files with missing lines Patch % Lines
src/pycea/tl/parsimony.py 97.63% 3 Missing ⚠️
src/pycea/pl/plot_ancestral_linkage.py 80.00% 1 Missing ⚠️
src/pycea/tl/_metrics.py 85.71% 1 Missing ⚠️
src/pycea/tl/ancestral_states.py 91.66% 1 Missing ⚠️
src/pycea/utils.py 88.88% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #59      +/-   ##
==========================================
+ Coverage   93.74%   93.83%   +0.09%     
==========================================
  Files          36       37       +1     
  Lines        3195     3344     +149     
==========================================
+ Hits         2995     3138     +143     
- Misses        200      206       +6     
Files with missing lines Coverage Δ
src/pycea/tl/__init__.py 100.00% <100.00%> (ø)
src/pycea/tl/ancestral_linkage.py 95.06% <100.00%> (ø)
src/pycea/tl/partition_test.py 92.17% <100.00%> (+0.13%) ⬆️
src/pycea/pl/plot_ancestral_linkage.py 97.50% <80.00%> (-0.78%) ⬇️
src/pycea/tl/_metrics.py 97.14% <85.71%> (-2.86%) ⬇️
src/pycea/tl/ancestral_states.py 97.51% <91.66%> (+0.04%) ⬆️
src/pycea/utils.py 90.68% <88.88%> (-0.26%) ⬇️
src/pycea/tl/parsimony.py 97.63% <97.63%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de6ef97837

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/pycea/tl/parsimony.py
# Set leaf states then compute Fitch-Hartigan state sets
leaf_states = {node: values[node] for node in g.nodes if g.out_degree(node) == 0 and node in values.index}
nx.set_node_attributes(g, leaf_states, key)
_fitch_hartigan_downpass(g, key, missing_state, set_key="_fitch_set")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Count parent-compatible states in FitchCount

When a child clade has an ambiguous downpass set, a globally equally parsimonious solution can assign that child the parent's state even if that state is not in the child's downpass set. For example, a root with one child containing leaves 0,0 and another containing 1,2 has an equally parsimonious assignment root=right_child=0, but _fitch_set for the right child is only {1,2}, so the DP never counts those solutions and undercounts transitions for such multi-state trees.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is expected behavior, not a regression. fitch_count intentionally counts transitions over the Fitch–Hartigan solution set (states enumerable from the per-node Fitch sets), which is the space defined by the original FitchCount algorithm (Quinn et al. 2021). The _N_fitch_count/_C_fitch_count DP is ported unchanged from Cassiopeia and produces identical counts. Assignments like root=right_child=0 in your example are maximally parsimonious but lie outside the Fitch enumeration, so neither this implementation nor the reference counts them. Broadening to the full MP space would be a different algorithm (e.g. a Sankoff-based count) rather than FitchCount, so I've left the semantics faithful to the reference.

Comment thread src/pycea/tl/parsimony.py Outdated
g = nx.DiGraph(g.subgraph(nx.descendants(g, root) | {root}).copy())
actual_root = root if root is not None else get_root(g)
# Set leaf states then compute Fitch-Hartigan state sets
leaf_states = {node: values[node] for node in g.nodes if g.out_degree(node) == 0 and node in values.index}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat pandas NaNs as missing before building Fitch sets

When key contains ordinary pandas missing values and missing_state is left as the default, this still writes NaN into leaf states; observed was computed with dropna(), so NaN is absent from label_to_j, and _N_fitch_count later raises KeyError: nan instead of treating the missing leaf as the documented wildcard. This affects any categorical column with standard missing observations unless users pre-encode a custom sentinel.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f6e90c0: fitch_count now coerces pandas NaN leaf values to the missing sentinel so they're treated as Fitch wildcards instead of raising KeyError. Added a regression test.

Comment thread src/pycea/tl/partition_test.py Outdated
Comment on lines +257 to +258
elif callable(metric):
metric_fn = metric

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Call custom metric functions instead of storing them raw

For a plain callable metric (which the overload accepts as _MetricFn), assigning the function directly means the later unconditional calls to metric_fn.pairwise(...) fail with AttributeError before a result is produced, even when test=None. Either wrap custom functions in a pairwise adapter or branch at the call sites so callable metrics are actually invoked.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f6e90c0: callable metrics are now wrapped in a _CallableMetric adapter that provides .pairwise, so a plain metric(a, b) callable works instead of raising AttributeError. Added a test asserting it matches the built-in mean_difference.

colganwi and others added 2 commits July 8, 2026 19:01
Set typehints_document_overloads = False so sphinx-autodoc-typehints no
longer emits every @typing.overload signature. This restores the short
description in the API summary table for overloaded functions (parsimony,
fitch_count, autocorr, clades, distance, ...) and shows a single
implementation signature on each function's page instead of the copy-only
overload stubs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…partition_test

- fitch_count: coerce pandas NaN leaf values to the missing sentinel so
  missing observations are treated as Fitch wildcards instead of raising
  KeyError when missing_state is left at the default.
- partition_test: wrap a user-supplied callable metric in a _CallableMetric
  adapter that provides the .pairwise API, so callable metrics no longer
  fail with AttributeError.
- Add tests covering both paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@colganwi colganwi merged commit b3b8abd into main Jul 8, 2026
5 of 8 checks passed
@colganwi colganwi deleted the parsimony branch July 8, 2026 23:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant