Skip to content

fix(tools): raise when filter_rank_genes_groups genes are missing from adata - #4497

Open
kroy3 wants to merge 2 commits into
scverse:mainfrom
kroy3:fix-1487-filter-rank-genes-missing
Open

kroy3 wants to merge 2 commits into
scverse:mainfrom
kroy3:fix-1487-filter-rank-genes-missing

Conversation

@kroy3

@kroy3 kroy3 commented Nov 8, 2026 •

Copy link
Copy Markdown

When filter_rank_genes_groups recomputes fold changes or fractions (for example with a different use_raw, layer or groupby than rank_genes_groups), gene positions come from var_names.get_indexer, which returns -1 for names that are not present. x[:, -1] then silently scores those genes with the values of the last gene in adata, so genes that were removed can still pass the filter.

The original KeyError from #1487 no longer occurs on main, but the same situation now gives wrong results without any error.

Reproduction on current main

import scanpy as sc

adata = sc.datasets.pbmc68k_reduced().raw.to_adata()
sc.tl.rank_genes_groups(adata, "bulk_labels", method="wilcoxon", n_genes=20)
removed = list(adata.uns["rank_genes_groups"]["names"]["CD14+ Monocyte"][:3])  # FTL, AIF1, PSAP
sub = adata[:, ~adata.var_names.isin(removed)].copy()
order = [g for g in sub.var_names if g != "FCGR3A"] + ["FCGR3A"]  # strong marker last
sub = sub[:, order].copy()
sc.tl.filter_rank_genes_groups(
    sub, min_in_group_fraction=0.9, min_fold_change=1, max_out_group_fraction=0.3
)
sub.uns["rank_genes_groups_filtered"]["names"]["CD14+ Monocyte"][:3]
# -> FTL, AIF1, PSAP are kept although they are not in `sub`

Change

  • Gene positions are looked up through a small helper, _get_var_idx, which raises a KeyError naming the missing genes, the uns key and the group, and suggests rerunning rank_genes_groups or passing the same use_raw / use.
  • NaN names are skipped, so an already filtered result (key="rank_genes_groups_filtered") can still be filtered again.
  • Only the recompute path is affected. When pts and logfoldchanges are reused from the stored results, behavior is unchanged.

I chose to raise rather than treat missing genes as filtered, since that would hide a likely mistake (for example ranking on .raw and filtering an HVG subset). Happy to switch to a warning plus NaN if you prefer.

Tests

  • test_filter_rank_genes_groups_missing_genes: fails on main, passes with this change.
  • test_filter_rank_genes_groups_refilter: filtering a filtered result still works.
  • tests/test_rank_genes_groups.py, tests/test_filter_rank_genes_groups.py, tests/test_queries.py and the filter_rank_genes_groups plotting test pass locally; ruff check and ruff format are clean.

LLM usage

Kushal Raj Roy added 2 commits November 7, 2026 21:24
…m adata

When fold changes or fractions are recomputed, gene positions come from
`var_names.get_indexer`, which returns -1 for names that are not present.
`x[:, -1]` then silently scores those genes with the values of the last
gene in `adata`, so removed genes can pass the filter.

Raise a KeyError naming the missing genes instead, and skip NaN names so
an already filtered result can still be filtered again.

Fixes scverse#1487
@read-the-docs-business

Copy link
Copy Markdown

Documentation build overview

📚 scanpy | 🛠️ Build #4692695 | 📁 Comparing a3d0edb against latest (117682e)

  🔍 Preview build  

3 files changed
± release-notes/index.html
± api/generated/scanpy.pp.calculate_qc_metrics.html
± api/generated/scanpy.settings.html

@codecov

codecov Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.14%. Comparing base (117682e) to head (a3d0edb).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4497   +/-   ##
=======================================
  Coverage   85.13%   85.14%           
=======================================
  Files         138      138           
  Lines       12247    12253    +6     
=======================================
+ Hits        10427    10433    +6     
  Misses       1820     1820           
Flag Coverage Δ
hatch-test.low-vers 78.67% <100.00%> (+0.01%) ⬆️
hatch-test.pre 84.93% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/scanpy/tools/_rank_genes_groups.py 96.12% <100.00%> (+0.07%) ⬆️

@kroy3 kroy3 changed the title fix(tools): raise when filter_rank_genes_groups genes are missing fro… fix(tools): raise when filter_rank_genes_groups genes are missing from adata Nov 8, 2026
def test_filter_rank_genes_groups_refilter() -> None:
"""Filtering an already filtered result (with NaN names) still works."""
adata = pbmc68k_reduced()
rank_genes_groups(adata, "bulk_labels", method="wilcoxon", n_genes=5)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Could this refilter test also cover pts=True? With cached pts/pts_rest, the first filter leaves NaNs in names, and a second filter_rank_genes_groups(key='rank_genes_groups_filtered') calls .loc[var_names] with those NaNs, raising KeyError. The new helper skips NaNs for matrix indexing, but the cached-fraction path still needs the same handling.

This branch has not been deployed

No deployments
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.

f"Values {list(not_found)}, from {list(indexer)}, " "are not valid obs/ var names or indices."

2 participants