Skip to content
7 changes: 6 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,13 @@ jobs:

- name: Install dependencies
run: |
uv tool install --with='click!=8.3.0' hatch
echo "::group::Install hatch"
uv tool install hatch
echo "::endgroup::"
echo "::group::Create environment"
hatch -v env create ${{ matrix.env.name }}
echo "::endgroup::"
hatch run ${{ matrix.env.name }}:session-info scanpy anndata

- name: Run tests
if: matrix.env.test-type == null
Expand Down
1 change: 1 addition & 0 deletions docs/release-notes/3929.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix compatibility with pandas 3.0 {smaller}`P Angerer`
1 change: 1 addition & 0 deletions hatch.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ overrides.matrix.deps.python = [
]
overrides.matrix.deps.extra-dependencies = [
{ if = [ "pre" ], value = "anndata @ git+https://github.com/scverse/anndata.git" },
{ if = [ "pre" ], value = "pandas>=3rc0" },
Copy link
Contributor

Choose a reason for hiding this comment

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

why do we need this?

Copy link
Member Author

Choose a reason for hiding this comment

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

🤷 uv chose to resolve things without pandas 3rc0 without it

]
overrides.matrix.deps.dependency-groups = [
{ if = [ "stable", "pre", "low-vers" ], value = "test" },
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ dependencies = [
"numpy>=2",
"fast-array-utils[accel,sparse]>=1.2.1",
"matplotlib>=3.9",
"pandas >=2.2.2, <3.0.0rc0",
"pandas >=2.2.2",
"scipy>=1.13",
"seaborn>=0.13.2",
"h5py>=3.11",
Expand Down
22 changes: 21 additions & 1 deletion src/scanpy/_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

import h5py
import numpy as np
import pandas as pd
from anndata._core.sparse_dataset import BaseCompressedSparseDataset
from packaging.version import Version

Expand All @@ -44,6 +45,7 @@
from anndata import AnnData
from igraph import Graph
from numpy.typing import ArrayLike, NDArray
from pandas._typing import Dtype as PdDtype

from .._compat import CSRBase
from ..neighbors import NeighborsParams, RPForestDict
Expand Down Expand Up @@ -79,6 +81,7 @@
"sanitize_anndata",
"select_groups",
"update_params",
"with_cat_dtype",
]


Expand Down Expand Up @@ -287,7 +290,7 @@ def get_igraph_from_adjacency(adjacency: CSBase, *, directed: bool = False) -> G
import igraph as ig

sources, targets = adjacency.nonzero()
weights = dematrix(adjacency[sources, targets]).ravel()
weights = dematrix(adjacency[sources, targets]).ravel() if len(sources) else []
g = ig.Graph(directed=directed)
g.add_vertices(adjacency.shape[0]) # this adds adjacency.shape[0] vertices
g.add_edges(list(zip(sources, targets, strict=True)))
Expand Down Expand Up @@ -494,6 +497,23 @@ def moving_average(a: np.ndarray, n: int):
return ret[n - 1 :] / n


@singledispatch
def with_cat_dtype[X: pd.Series | pd.CategoricalIndex | pd.Categorical](
x: X, dtype: PdDtype
) -> X:
raise NotImplementedError


@with_cat_dtype.register(pd.Series)
def _(x: pd.Series, dtype: PdDtype) -> pd.Series:
return x.cat.set_categories(x.cat.categories.astype(dtype))


@with_cat_dtype.register(pd.Categorical | pd.CategoricalIndex)
def _[X: pd.Categorical | pd.CategoricalIndex](x: X, dtype: PdDtype) -> X:
return x.set_categories(x.categories.astype(dtype))


# --------------------------------------------------------------------------------
# Deal with tool parameters
# --------------------------------------------------------------------------------
Expand Down
6 changes: 3 additions & 3 deletions src/scanpy/external/exporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ def spring_project( # noqa: PLR0912, PLR0915
np.save(subplot_dir / "cell_filter.npy", np.arange(x.shape[0]))

# Write 2-D coordinates, after adjusting to roughly match SPRING's default d3js force layout parameters
coords = coords - coords.min(0)[None, :]
coords = coords - coords.min(axis=0)[None, :]
coords = (
coords * (np.array([1000, 1000]) / coords.ptp(0))[None, :]
+ np.array([200, -200])[None, :]
Expand Down Expand Up @@ -342,8 +342,8 @@ def _get_color_stats_genes(color_stats, x, gene_list):
means, variances = mean_var(x, axis=0, correction=1)
stdevs = np.zeros(variances.shape, dtype=float)
stdevs[variances > 0] = np.sqrt(variances[variances > 0])
mins = x.min(0).todense().A1
maxes = x.max(0).todense().A1
mins = x.min(axis=0).todense().A1
maxes = x.max(axis=0).todense().A1

pctl = 99.6
pctl_n = (100 - pctl) / 100.0 * x.shape[0]
Expand Down
4 changes: 2 additions & 2 deletions src/scanpy/get/get.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,8 @@ def obs_df(
>>> plotdf = sc.get.obs_df(
... pbmc, keys=["CD8B", "n_genes"], obsm_keys=[("X_umap", 0), ("X_umap", 1)]
... )
>>> plotdf.columns
Index(['CD8B', 'n_genes', 'X_umap-0', 'X_umap-1'], dtype='object')
>>> plotdf.columns.astype("string")
Index(['CD8B', 'n_genes', 'X_umap-0', 'X_umap-1'], dtype='string')
>>> plotdf.plot.scatter("X_umap-0", "X_umap-1", c="CD8B") # doctest: +SKIP
<Axes: xlabel='X_umap-0', ylabel='X_umap-1'>

Expand Down
10 changes: 5 additions & 5 deletions src/scanpy/plotting/_anndata.py
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,7 @@ def violin( # noqa: PLR0912, PLR0913, PLR0915
layer: str | None = None,
density_norm: DensityNorm = "width",
order: Sequence[str] | None = None,
multi_panel: bool | None = None,
multi_panel: bool = False,
xlabel: str = "",
ylabel: str | Sequence[str] | None = None,
rotation: float | None = None,
Expand Down Expand Up @@ -1202,11 +1202,11 @@ def heatmap( # noqa: PLR0912, PLR0913, PLR0915
).issubset(categories)

if standard_scale == "obs":
obs_tidy = obs_tidy.sub(obs_tidy.min(1), axis=0)
obs_tidy = obs_tidy.div(obs_tidy.max(1), axis=0).fillna(0)
obs_tidy = obs_tidy.sub(obs_tidy.min(axis=1), axis=0)
obs_tidy = obs_tidy.div(obs_tidy.max(axis=1), axis=0).fillna(0)
elif standard_scale == "var":
obs_tidy -= obs_tidy.min(0)
obs_tidy = (obs_tidy / obs_tidy.max(0)).fillna(0)
obs_tidy -= obs_tidy.min(axis=0)
obs_tidy = (obs_tidy / obs_tidy.max(axis=0)).fillna(0)
elif standard_scale is None:
pass
else:
Expand Down
14 changes: 8 additions & 6 deletions src/scanpy/plotting/_dotplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,11 +213,13 @@ def __init__( # noqa: PLR0913
dot_color_df = self.obs_tidy.groupby(level=0, observed=True).mean()

if standard_scale == "group":
dot_color_df = dot_color_df.sub(dot_color_df.min(1), axis=0)
dot_color_df = dot_color_df.div(dot_color_df.max(1), axis=0).fillna(0)
dot_color_df = dot_color_df.sub(dot_color_df.min(axis=1), axis=0)
dot_color_df = dot_color_df.div(
dot_color_df.max(axis=1), axis=0
).fillna(0)
elif standard_scale == "var":
dot_color_df -= dot_color_df.min(0)
dot_color_df = (dot_color_df / dot_color_df.max(0)).fillna(0)
dot_color_df -= dot_color_df.min(axis=0)
dot_color_df = (dot_color_df / dot_color_df.max(axis=0)).fillna(0)
elif standard_scale is None:
pass
else:
Expand Down Expand Up @@ -696,10 +698,10 @@ def _dotplot( # noqa: PLR0912, PLR0913, PLR0915
group_axis = 1
if standard_scale is not None:
dot_color = dot_color.sub(
dot_color.min((group_axis + 1) % 2), axis=group_axis
dot_color.min(axis=1 - group_axis), axis=group_axis
)
dot_color = dot_color.div(
dot_color.max((group_axis + 1) % 2), axis=group_axis
dot_color.max(axis=1 - group_axis), axis=group_axis
).fillna(0)
# make scatter plot in which
# x = var_names
Expand Down
8 changes: 4 additions & 4 deletions src/scanpy/plotting/_matrixplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,11 @@ def __init__( # noqa: PLR0913
)

if standard_scale == "group":
values_df = values_df.sub(values_df.min(1), axis=0)
values_df = values_df.div(values_df.max(1), axis=0).fillna(0)
values_df = values_df.sub(values_df.min(axis=1), axis=0)
values_df = values_df.div(values_df.max(axis=1), axis=0).fillna(0)
elif standard_scale == "var":
values_df -= values_df.min(0)
values_df = (values_df / values_df.max(0)).fillna(0)
values_df -= values_df.min(axis=0)
values_df = (values_df / values_df.max(axis=0)).fillna(0)
elif standard_scale is None:
pass
else:
Expand Down
2 changes: 1 addition & 1 deletion src/scanpy/plotting/_scrublet.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ def scrublet_score_distribution(

if "batched_by" in adata.uns["scrublet"]:
batched_by = adata.uns["scrublet"]["batched_by"]
batches = adata.obs[batched_by].astype("category", copy=False)
batches = adata.obs[batched_by].astype("category")
n_batches = len(batches.cat.categories)
figsize = (figsize[0], figsize[1] * n_batches)
else:
Expand Down
12 changes: 7 additions & 5 deletions src/scanpy/plotting/_stacked_violin.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,11 +229,13 @@ def __init__( # noqa: PLR0913
msg = "`standard_scale='obs'` is deprecated, use `standard_scale='group'` instead"
warn(msg, FutureWarning)
if standard_scale == "group":
self.obs_tidy = self.obs_tidy.sub(self.obs_tidy.min(1), axis=0)
self.obs_tidy = self.obs_tidy.div(self.obs_tidy.max(1), axis=0).fillna(0)
self.obs_tidy = self.obs_tidy.sub(self.obs_tidy.min(axis=1), axis=0)
self.obs_tidy = self.obs_tidy.div(self.obs_tidy.max(axis=1), axis=0).fillna(
0
)
elif standard_scale == "var":
self.obs_tidy -= self.obs_tidy.min(0)
self.obs_tidy = (self.obs_tidy / self.obs_tidy.max(0)).fillna(0)
self.obs_tidy -= self.obs_tidy.min(axis=0)
self.obs_tidy = (self.obs_tidy / self.obs_tidy.max(axis=0)).fillna(0)
elif standard_scale is None:
pass
else:
Expand Down Expand Up @@ -556,7 +558,7 @@ def _make_rows_of_violinplots(
x=x,
y="values",
data=_df,
orient="vertical",
orient="v",
ax=row_ax,
# use a single `color`` if row_colors[idx] is defined
# else use the palette
Expand Down
9 changes: 5 additions & 4 deletions src/scanpy/plotting/_tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from ... import logging as logg
from ..._compat import old_positionals
from ..._settings import settings
from ..._utils import _doc_params, _empty, sanitize_anndata
from ..._utils import _doc_params, _empty, sanitize_anndata, with_cat_dtype
from ...get import rank_genes_groups_df
from .._anndata import ranking
from .._docs import (
Expand Down Expand Up @@ -1295,12 +1295,13 @@ def rank_genes_groups_violin( # noqa: PLR0913
_gene_names = _gene_names.tolist()
df = obs_df(adata, _gene_names, use_raw=use_raw, gene_symbols=gene_symbols)
new_gene_names = df.columns
df["hue"] = adata.obs[groups_key].astype(str).values
df["hue"] = adata.obs[groups_key].astype(str).array
if reference == "rest":
df.loc[df["hue"] != group_name, "hue"] = "rest"
else:
df.loc[~df["hue"].isin([group_name, reference]), "hue"] = np.nan
df["hue"] = df["hue"].astype("category")
# Convert categories to object because of https://github.com/mwaskom/seaborn/issues/3893
df["hue"] = with_cat_dtype(df["hue"].astype("category"), object)
df_tidy = pd.melt(df, id_vars="hue", value_vars=new_gene_names)
x = "variable"
y = "value"
Expand All @@ -1316,7 +1317,7 @@ def rank_genes_groups_violin( # noqa: PLR0913
hue="hue",
split=split,
density_norm=density_norm,
orient="vertical",
orient="v",
ax=ax,
)
if strip:
Expand Down
4 changes: 1 addition & 3 deletions src/scanpy/preprocessing/_highly_variable_genes.py
Original file line number Diff line number Diff line change
Expand Up @@ -814,9 +814,7 @@ def highly_variable_genes( # noqa: PLR0913
adata.var["highly_variable"] = df["highly_variable"]
adata.var["means"] = df["means"]
adata.var["dispersions"] = df["dispersions"]
adata.var["dispersions_norm"] = df["dispersions_norm"].astype(
np.float32, copy=False
)
adata.var["dispersions_norm"] = df["dispersions_norm"].astype(np.float32)

if batch_key is not None:
adata.var["highly_variable_nbatches"] = df["highly_variable_nbatches"]
Expand Down
7 changes: 7 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import shutil
import sys
from contextlib import ExitStack
from pathlib import Path
from textwrap import dedent
from typing import TYPE_CHECKING, TypedDict, cast
Expand Down Expand Up @@ -148,3 +149,9 @@ def plt():
from matplotlib import pyplot as plt

return plt


@pytest.fixture
def exit_stack() -> Generator[ExitStack]:
with ExitStack() as stack:
yield stack
3 changes: 2 additions & 1 deletion tests/external/test_hashsolo.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def test_cell_demultiplexing():
expected = pd.array(doublets + classes + negatives, dtype="string")
classification = test_data.obs["Classification"].array.astype("string")
# This is a bit flaky, so allow some mismatches:
if (expected != classification).sum() > 3:
# (Series() because of https://github.com/pandas-dev/pandas/issues/63458)
if pd.Series(expected != classification).sum() > 3:
# Compare lists for better error message
assert classification.tolist() == expected.tolist()
Loading
Loading