Repository: scverse/scanpy

Issue:
enable highly_variable_genes_seurat_v3 to work with pseudocounts
Hi,

I'm working with pseudocounts data (kallisto/alevin/salomon output). I think they are called "pseudocount": if a read is assigned to two regions (genes) , a probability is assigned (e.g. gene1=0.2, gene2=0.8). Nevertheless, they can still considered counts and so it would be cool to use the `highly_variable_genes flavour=seuratv3` . 
I added an additional argument in case users would like to enforce this, as it was similarly done/discussed in  https://github.com/theislab/scvelo/issues/190

Would like to hear what you guys think, pinging @adamgayoso (thanks for the great overleaf doc! )




Discussion:

Resolving patch:
diff --git a/scanpy/preprocessing/_highly_variable_genes.py b/scanpy/preprocessing/_highly_variable_genes.py
--- a/scanpy/preprocessing/_highly_variable_genes.py
+++ b/scanpy/preprocessing/_highly_variable_genes.py
@@ -1,6 +1,5 @@
 import warnings
 from typing import Optional
-
 import numpy as np
 import pandas as pd
 import scipy.sparse as sp_sparse
@@ -21,7 +20,8 @@ def _highly_variable_genes_seurat_v3(
     layer: Optional[str] = None,
     n_top_genes: int = 2000,
     batch_key: Optional[str] = None,
-    span: Optional[float] = 0.3,
+    check_values: bool = True,
+    span: float = 0.3,
     subset: bool = False,
     inplace: bool = True,
 ) -> Optional[pd.DataFrame]:
@@ -57,10 +57,10 @@ def _highly_variable_genes_seurat_v3(
         )
 
     X = adata.layers[layer] if layer is not None else adata.X
-    if check_nonnegative_integers(X) is False:
-        raise ValueError(
-            "`pp.highly_variable_genes` with `flavor='seurat_v3'` expects "
-            "raw count data."
+    if check_values and (check_nonnegative_integers(X) == False):
+        warnings.warn(
+            "`flavor='seurat_v3'` expects raw count data, but non-integers were found.",
+            UserWarning,
         )
 
     if batch_key is None:
@@ -97,7 +97,9 @@ def _highly_variable_genes_seurat_v3(
         else:
             clip_val_broad = np.broadcast_to(clip_val, batch_counts.shape)
             np.putmask(
-                batch_counts, batch_counts > clip_val_broad, clip_val_broad,
+                batch_counts,
+                batch_counts > clip_val_broad,
+                clip_val_broad,
             )
 
         if sp_sparse.issparse(batch_counts):
@@ -300,6 +302,7 @@ def highly_variable_genes(
     subset: bool = False,
     inplace: bool = True,
     batch_key: Optional[str] = None,
+    check_values: bool = True,
 ) -> Optional[pd.DataFrame]:
     """\
     Annotate highly variable genes [Satija15]_ [Zheng17]_ [Stuart19]_.
@@ -366,6 +369,9 @@ def highly_variable_genes(
         by how many batches they are a HVG. For dispersion-based flavors ties are broken
         by normalized dispersion. If `flavor = 'seurat_v3'`, ties are broken by the median
         (across batches) rank based on within-batch normalized variance.
+    check_values
+        Check if counts in selected layer are integers. A Warning is returned if set to True.
+        Only used if `flavor='seurat_v3'`.
 
     Returns
     -------
@@ -417,6 +423,7 @@ def highly_variable_genes(
             layer=layer,
             n_top_genes=n_top_genes,
             batch_key=batch_key,
+            check_values=check_values,
             span=span,
             subset=subset,
             inplace=inplace,
@@ -461,7 +468,8 @@ def highly_variable_genes(
 
             # Add 0 values for genes that were filtered out
             missing_hvg = pd.DataFrame(
-                np.zeros((np.sum(~filt), len(hvg.columns))), columns=hvg.columns,
+                np.zeros((np.sum(~filt), len(hvg.columns))),
+                columns=hvg.columns,
             )
             missing_hvg['highly_variable'] = missing_hvg['highly_variable'].astype(bool)
             missing_hvg['gene'] = gene_list[~filt]


Test patch:
diff --git a/scanpy/tests/test_highly_variable_genes.py b/scanpy/tests/test_highly_variable_genes.py
--- a/scanpy/tests/test_highly_variable_genes.py
+++ b/scanpy/tests/test_highly_variable_genes.py
@@ -1,3 +1,4 @@
+import pytest
 import pandas as pd
 import numpy as np
 import scanpy as sc
@@ -73,10 +74,16 @@ def test_higly_variable_genes_compare_to_seurat():
     # (still) Not equal to tolerance rtol=2e-05, atol=2e-05
     # np.testing.assert_allclose(4, 3.9999, rtol=2e-05, atol=2e-05)
     np.testing.assert_allclose(
-        seurat_hvg_info['means'], pbmc.var['means'], rtol=2e-05, atol=2e-05,
+        seurat_hvg_info['means'],
+        pbmc.var['means'],
+        rtol=2e-05,
+        atol=2e-05,
     )
     np.testing.assert_allclose(
-        seurat_hvg_info['dispersions'], pbmc.var['dispersions'], rtol=2e-05, atol=2e-05,
+        seurat_hvg_info['dispersions'],
+        pbmc.var['dispersions'],
+        rtol=2e-05,
+        atol=2e-05,
     )
     np.testing.assert_allclose(
         seurat_hvg_info['dispersions_norm'],
@@ -104,7 +111,10 @@ def test_higly_variable_genes_compare_to_seurat_v3():
         seurat_hvg_info['highly_variable'], pbmc.var['highly_variable']
     )
     np.testing.assert_allclose(
-        seurat_hvg_info['variances'], pbmc.var['variances'], rtol=2e-05, atol=2e-05,
+        seurat_hvg_info['variances'],
+        pbmc.var['variances'],
+        rtol=2e-05,
+        atol=2e-05,
     )
     np.testing.assert_allclose(
         seurat_hvg_info['variances_norm'],
@@ -140,6 +150,13 @@ def test_higly_variable_genes_compare_to_seurat_v3():
     seu = pd.Index(seurat_hvg_info_batch['x'].values)
     assert len(seu.intersection(df.index)) / 4000 > 0.95
 
+    sc.pp.log1p(pbmc)
+    with pytest.warns(
+        UserWarning,
+        match="`flavor='seurat_v3'` expects raw count data, but non-integers were found.",
+    ):
+        sc.pp.highly_variable_genes(pbmc, n_top_genes=1000, flavor='seurat_v3')
+
 
 def test_filter_genes_dispersion_compare_to_seurat():
     seurat_hvg_info = pd.read_csv(FILE, sep=' ')
@@ -166,10 +183,16 @@ def test_filter_genes_dispersion_compare_to_seurat():
     # (still) Not equal to tolerance rtol=2e-05, atol=2e-05:
     # np.testing.assert_allclose(4, 3.9999, rtol=2e-05, atol=2e-05)
     np.testing.assert_allclose(
-        seurat_hvg_info['means'], pbmc.var['means'], rtol=2e-05, atol=2e-05,
+        seurat_hvg_info['means'],
+        pbmc.var['means'],
+        rtol=2e-05,
+        atol=2e-05,
     )
     np.testing.assert_allclose(
-        seurat_hvg_info['dispersions'], pbmc.var['dispersions'], rtol=2e-05, atol=2e-05,
+        seurat_hvg_info['dispersions'],
+        pbmc.var['dispersions'],
+        rtol=2e-05,
+        atol=2e-05,
     )
     np.testing.assert_allclose(
         seurat_hvg_info['dispersions_norm'],
@@ -188,7 +211,10 @@ def test_highly_variable_genes_batches():
     adata_2 = adata[adata.obs.batch.isin(['1']), :]
 
     sc.pp.highly_variable_genes(
-        adata, batch_key='batch', flavor='cell_ranger', n_top_genes=200,
+        adata,
+        batch_key='batch',
+        flavor='cell_ranger',
+        n_top_genes=200,
     )
 
     sc.pp.filter_genes(adata_1, min_cells=1)