Repository: scverse/scanpy

Issue:
rank_genes_groups layer option
Can a layer option be added to`sc.tl.rank_genes_groups`? 


Discussion:

Resolving patch:
diff --git a/scanpy/tools/_rank_genes_groups.py b/scanpy/tools/_rank_genes_groups.py
--- a/scanpy/tools/_rank_genes_groups.py
+++ b/scanpy/tools/_rank_genes_groups.py
@@ -13,6 +13,7 @@
 from ..preprocessing._simple import _get_mean_var
 
 
+# TODO: Make arguments after groupby keyword only
 def rank_genes_groups(
     adata: AnnData,
     groupby: str,
@@ -25,6 +26,7 @@ def rank_genes_groups(
     copy: bool = False,
     method: str = 't-test_overestim_var',
     corr_method: str = 'benjamini-hochberg',
+    layer: Optional[str] = None,
     **kwds
 ):
     """Rank genes for characterizing groups.
@@ -37,6 +39,8 @@ def rank_genes_groups(
         The key of the observations grouping to consider.
     use_raw
         Use `raw` attribute of `adata` if present.
+    layer
+        Key from `adata.layers` whose value will be used to perform tests on.
     groups
         Subset of groups, e.g. [`'g1'`, `'g2'`, `'g3'`], to which comparison
         shall be restricted, or `'all'` (default), for all groups.
@@ -145,15 +149,21 @@ def rank_genes_groups(
         'reference': reference,
         'method': method,
         'use_raw': use_raw,
+        'layer': layer,
         'corr_method': corr_method,
     }
 
     # adata_comp mocks an AnnData object if use_raw is True
     # otherwise it's just the AnnData object
     adata_comp = adata
-    if adata.raw is not None and use_raw:
-        adata_comp = adata.raw
-    X = adata_comp.X
+    if layer is not None:
+        if use_raw:
+            raise ValueError("Cannot specify `layer` and have `use_raw=True`.")
+        X = adata_comp.layers[layer]
+    else:
+        if use_raw and adata.raw is not None:
+            adata_comp = adata.raw
+        X = adata_comp.X
 
     # for clarity, rename variable
     n_genes_user = n_genes
@@ -242,14 +252,13 @@ def rank_genes_groups(
         reference = groups_order[0]
         if len(groups) == 1:
             raise Exception('Cannot perform logistic regression on a single cluster.')
-        adata_copy = adata[adata.obs[groupby].isin(groups_order)]
-        adata_comp = adata_copy
-        if adata.raw is not None and use_raw:
-            adata_comp = adata_copy.raw
-        X = adata_comp.X
+
+        grouping_mask = adata.obs[groupby].isin(groups_order)
+        grouping = adata.obs.loc[grouping_mask, groupby]
+        X = X[grouping_mask.values, :]  # Indexing with a series causes issues, possibly segfault
 
         clf = LogisticRegression(**kwds)
-        clf.fit(X, adata_copy.obs[groupby].cat.codes)
+        clf.fit(X, grouping.cat.codes)
         scores_all = clf.coef_
         for igroup, group in enumerate(groups_order):
             if len(groups_order) <= 2:  # binary logistic regression


Test patch:
diff --git a/scanpy/tests/test_rank_genes_groups.py b/scanpy/tests/test_rank_genes_groups.py
--- a/scanpy/tests/test_rank_genes_groups.py
+++ b/scanpy/tests/test_rank_genes_groups.py
@@ -86,3 +86,38 @@ def test_results_sparse():
     for name in true_scores_t_test.dtype.names:
         assert np.allclose(true_scores_wilcoxon[name][:7], adata.uns['rank_genes_groups']['scores'][name][:7])
     assert np.array_equal(true_names_wilcoxon[:7], adata.uns['rank_genes_groups']['names'][:7])
+
+
+def test_results_layers():
+    seed(1234)
+
+    adata = get_example_data(sparse=False)
+    adata.layers["to_test"] = adata.X.copy()
+    adata.X = adata.X * np.random.randint(0, 2, adata.shape, dtype=bool)
+
+    true_names_t_test, true_names_wilcoxon,\
+    true_scores_t_test, true_scores_wilcoxon = get_true_scores()
+
+    # Wilcoxon
+    rank_genes_groups(
+        adata, 'true_groups', method='wilcoxon',
+        layer="to_test", use_raw=False, n_genes=20,
+    )
+    for name in true_scores_t_test.dtype.names:
+        assert np.allclose(true_scores_wilcoxon[name][:7], adata.uns['rank_genes_groups']['scores'][name][:7])
+
+    rank_genes_groups(adata, 'true_groups', method='wilcoxon', n_genes=20)
+    for name in true_scores_t_test.dtype.names:
+        assert not np.allclose(true_scores_wilcoxon[name][:7], adata.uns['rank_genes_groups']['scores'][name][:7])
+
+    # t-test
+    rank_genes_groups(
+        adata, 'true_groups', method='t-test',
+        layer="to_test", use_raw=False, n_genes=20,
+    )
+    for name in true_scores_t_test.dtype.names:
+        assert np.allclose(true_scores_t_test[name][:7], adata.uns['rank_genes_groups']['scores'][name][:7])
+
+    rank_genes_groups(adata, 'true_groups', method='t-test', n_genes=20)
+    for name in true_scores_t_test.dtype.names:
+        assert not np.allclose(true_scores_t_test[name][:7], adata.uns['rank_genes_groups']['scores'][name][:7])
diff --git a/scanpy/tests/test_rank_genes_groups_logreg.py b/scanpy/tests/test_rank_genes_groups_logreg.py
--- a/scanpy/tests/test_rank_genes_groups_logreg.py
+++ b/scanpy/tests/test_rank_genes_groups_logreg.py
@@ -5,7 +5,7 @@
 def test_rank_genes_groups_with_renamed_categories():
     adata = sc.datasets.blobs(n_variables=4, n_centers=3, n_observations=200)
     assert np.allclose(adata.X[1], [9.214668, -2.6487126, 4.2020774, 0.51076424])
-    
+
     sc.tl.pca(adata)
     sc.pp.neighbors(adata)
 
@@ -21,3 +21,23 @@ def test_rank_genes_groups_with_renamed_categories():
 
         sc.tl.rank_genes_groups(adata, 'louvain', method=method)
         assert adata.uns['rank_genes_groups']['names'][0].tolist() == ('3', '1', '0')
+
+def test_rank_genes_groups_with_renamed_categories_use_rep():
+    adata = sc.datasets.blobs(n_variables=4, n_centers=3, n_observations=200)
+    assert np.allclose(adata.X[1], [9.214668, -2.6487126, 4.2020774, 0.51076424])
+
+    sc.tl.pca(adata)
+    sc.pp.neighbors(adata)
+
+    adata.layers["to_test"] = adata.X.copy()
+    adata.X = adata.X[::-1, :]
+
+    sc.tl.louvain(adata)
+    sc.tl.rank_genes_groups(
+        adata, 'louvain', method='logreg', layer="to_test", use_raw=False
+    )
+    assert adata.uns['rank_genes_groups']['names'].dtype.names == ('0', '1', '2')
+    assert adata.uns['rank_genes_groups']['names'][0].tolist() == ('3', '1', '0')
+
+    sc.tl.rank_genes_groups(adata, 'louvain', method="logreg")
+    assert not adata.uns['rank_genes_groups']['names'][0].tolist() == ('3', '1', '0')
\ No newline at end of file