Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/4329.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{func}`~scanpy.io.read_10x_mtx` no longer treats the gene symbol `nan` as missing.
31 changes: 31 additions & 0 deletions src/scanpy/io/_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,35 @@ def _read_mtx(
return AnnData(x)


# Copy of pandas._libs.parsers.STR_NA_VALUES (pandas 2.3 / 3.0). Kept here so
# production does not import pandas._libs. If
# test_pandas_str_na_values_unchanged fails, pandas changed this set: update it,
# then _10X_FEATURE_NA_VALUES follows automatically.
_PANDAS_STR_NA_VALUES = frozenset({
"",
"#N/A",
"#N/A N/A",
"#NA",
"-1.#IND",
"-1.#QNAN",
"-NaN",
"-nan",
"1.#IND",
"1.#QNAN",
"<NA>",
"N/A",
"NA",
"NULL",
"NaN",
"None",
"n/a",
"nan",
"null",
})
# FlyBase gene symbol `nan` is a real identifier; treat other pandas NA tokens as missing.
_10X_FEATURE_NA_VALUES = _PANDAS_STR_NA_VALUES - {"nan"}


def _read_10x_mtx(
path: Path,
*,
Expand All @@ -634,6 +663,8 @@ def _read_10x_mtx(
path / f"{prefix}{'genes' if is_legacy else 'features'}.tsv{suffix}",
header=None,
sep="\t",
keep_default_na=False,
na_values=_10X_FEATURE_NA_VALUES,
)
if var_names == "gene_symbols":
var_names_idx = pd.Index(genes[1].array)
Expand Down
55 changes: 55 additions & 0 deletions tests/test_read_10x.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import scanpy as sc
from scanpy._compat import CSRBase
from scanpy.io._read import _PANDAS_STR_NA_VALUES

if TYPE_CHECKING:
from pathlib import Path
Expand Down Expand Up @@ -111,6 +112,60 @@ def test_read_10x_mtx_int(
assert dict(adata.var.dtypes) == dict(feature_types=str_dt, **col_dtypes)


def _mtx_dir_with_symbol(tmp_path: Path, data_10x: Path, symbol: str) -> Path:
dest = tmp_path / "mtx"
shutil.copytree(data_10x / "int-ids", dest)
lines = (dest / "features.tsv").read_text().splitlines()
cols = lines[0].split("\t")
cols[0] = "FBgn0036414"
cols[1] = symbol
lines[0] = "\t".join(cols)
(dest / "features.tsv").write_text("\n".join(lines) + "\n")
return dest


@pytest.mark.parametrize("var_names", ["gene_symbols", "gene_ids"])
def test_read_10x_mtx_gene_symbol_nan(
tmp_path: Path, data_10x: Path, var_names: Literal["gene_symbols", "gene_ids"]
) -> None:
mtx_path = _mtx_dir_with_symbol(tmp_path, data_10x, "nan")
adata = sc.io.read_10x_mtx(mtx_path, var_names=var_names, compressed=False)

if var_names == "gene_symbols":
assert adata.var_names[0] == "nan"
assert not pd.isna(adata.var_names[0])
adata.var["mt"] = adata.var_names.str.startswith("mt:")
assert not adata.var["mt"].isna().any()
sc.pp.calculate_qc_metrics(
adata, qc_vars=["mt"], percent_top=None, log1p=False, inplace=True
)
else:
assert adata.var["gene_symbols"].iloc[0] == "nan"
assert not adata.var["gene_symbols"].isna().iloc[0]


@pytest.mark.parametrize("symbol", ["NaN", "NA"])
def test_read_10x_mtx_other_na_gene_symbols(
tmp_path: Path, data_10x: Path, symbol: str
) -> None:
mtx_path = _mtx_dir_with_symbol(tmp_path, data_10x, symbol)
adata = sc.io.read_10x_mtx(mtx_path, var_names="gene_ids", compressed=False)
assert adata.var["gene_symbols"].isna().iloc[0]


def test_pandas_str_na_values_unchanged() -> None:
"""Fail if pandas changes its default NA tokens.

Production copies ``STR_NA_VALUES`` as ``_PANDAS_STR_NA_VALUES`` and derives
``_10X_FEATURE_NA_VALUES`` by dropping ``nan``. Importing pandas._libs only
here avoids a private import in ``read_10x_mtx``. On failure, update
``_PANDAS_STR_NA_VALUES`` in ``scanpy.io._read``.
"""
from pandas._libs.parsers import STR_NA_VALUES

assert frozenset(STR_NA_VALUES) == _PANDAS_STR_NA_VALUES


def test_read_10x_h5_v1(data_10x: Path) -> None:
spec_genome_v1 = sc.io.read_10x_h5(
data_10x / "1.2.0" / "filtered_gene_bc_matrices_h5.h5",
Expand Down
Loading