From 85a2baa86e5f62cf3b2f9bf1cb6f102dd1b6f6e7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:44:49 +0000 Subject: [PATCH 01/15] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/mirrors-mypy: v1.6.1 → v1.7.1](https://github.com/pre-commit/mirrors-mypy/compare/v1.6.1...v1.7.1) - [github.com/psf/black: 23.10.1 → 23.11.0](https://github.com/psf/black/compare/23.10.1...23.11.0) - [github.com/pre-commit/mirrors-prettier: v3.0.3 → v4.0.0-alpha.3](https://github.com/pre-commit/mirrors-prettier/compare/v3.0.3...v4.0.0-alpha.3) - [github.com/astral-sh/ruff-pre-commit: v0.1.4 → v0.1.6](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.4...v0.1.6) --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4ee1e4ec..5cdd65c1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,18 +7,18 @@ default_stages: minimum_pre_commit_version: 2.9.3 repos: - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.6.1 + rev: v1.7.1 hooks: - id: mypy additional_dependencies: [numpy, pandas, types-requests] exclude: .scripts/ci/download_data.py|squidpy/datasets/_(dataset|image).py # See https://github.com/pre-commit/mirrors-mypy/issues/33 - repo: https://github.com/psf/black - rev: 23.10.1 + rev: 23.11.0 hooks: - id: black additional_dependencies: [toml] - repo: https://github.com/pre-commit/mirrors-prettier - rev: v3.0.3 + rev: v4.0.0-alpha.3 hooks: - id: prettier - repo: https://github.com/pre-commit/pre-commit-hooks @@ -56,7 +56,7 @@ repos: - id: blacken-docs - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.1.4 + rev: v0.1.6 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From f58dc2f48cdb5c6d4f1c37a3a61ffefe0bb67af6 Mon Sep 17 00:00:00 2001 From: giovp Date: Fri, 8 Dec 2023 10:48:59 +0100 Subject: [PATCH 02/15] update --- .pre-commit-config.yaml | 4 ++-- src/squidpy/datasets/_utils.py | 2 +- src/squidpy/gr/_build.py | 2 +- src/squidpy/tl/_var_by_distance.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5cdd65c1..fbf28e0b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - id: black additional_dependencies: [toml] - repo: https://github.com/pre-commit/mirrors-prettier - rev: v4.0.0-alpha.3 + rev: v4.0.0-alpha.3-1 hooks: - id: prettier - repo: https://github.com/pre-commit/pre-commit-hooks @@ -56,7 +56,7 @@ repos: - id: blacken-docs - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.1.6 + rev: v0.1.7 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/src/squidpy/datasets/_utils.py b/src/squidpy/datasets/_utils.py index d2e11103..ed3cf6bd 100644 --- a/src/squidpy/datasets/_utils.py +++ b/src/squidpy/datasets/_utils.py @@ -11,7 +11,7 @@ from scanpy import read from scanpy._utils import check_presence_download -PathLike = Union[os.PathLike, str] +PathLike = Union[os.PathLike[str], str] Function_t = Callable[..., Union[AnnData, Any]] diff --git a/src/squidpy/gr/_build.py b/src/squidpy/gr/_build.py index a1e93839..104ed9c6 100644 --- a/src/squidpy/gr/_build.py +++ b/src/squidpy/gr/_build.py @@ -202,7 +202,7 @@ def _spatial_neighbor( raise NotImplementedError(f"Coordinate type `{coord_type}` is not yet implemented.") if coord_type == CoordType.GENERIC and isinstance(radius, Iterable): - minn, maxx = sorted(radius)[:2] # type: ignore[var-annotated] + minn, maxx = sorted(radius)[:2] mask = (Dst.data < minn) | (Dst.data > maxx) a_diag = Adj.diagonal() diff --git a/src/squidpy/tl/_var_by_distance.py b/src/squidpy/tl/_var_by_distance.py index c48d11f8..86a73309 100644 --- a/src/squidpy/tl/_var_by_distance.py +++ b/src/squidpy/tl/_var_by_distance.py @@ -244,10 +244,10 @@ def _get_coordinates(adata: AnnData, anchor: str, annotation: str, spatial_key: def _normalize_distances( - mapping_design_matrix: dict[tuple[str | None, str], pd.DataFrame], + mapping_design_matrix: dict[tuple[Any | None, Any], pd.DataFrame], anchor: str | list[str], slides: list[str] | list[None], - mapping_max_distances: dict[tuple[str | None, str], float], + mapping_max_distances: dict[tuple[Any | None, Any], float], ) -> pd.DataFrame: """Normalize distances to anchor.""" if not isinstance(anchor, list): From 2b2ef21e1c063d10cf20d6a00a37ea9687b4f028 Mon Sep 17 00:00:00 2001 From: giovp Date: Fri, 8 Dec 2023 11:25:21 +0100 Subject: [PATCH 03/15] update --- src/squidpy/gr/_nhood.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/squidpy/gr/_nhood.py b/src/squidpy/gr/_nhood.py index 593c8309..47775a0d 100644 --- a/src/squidpy/gr/_nhood.py +++ b/src/squidpy/gr/_nhood.py @@ -124,6 +124,7 @@ def _create_function(n_cls: int, parallel: bool = False) -> Callable[[NDArrayA, def nhood_enrichment( adata: AnnData | SpatialData, cluster_key: str, + library_key: str | None = None, connectivity_key: str | None = None, n_perms: int = 1000, numba_parallel: bool = False, @@ -140,6 +141,7 @@ def nhood_enrichment( ---------- %(adata)s %(cluster_key)s + %(library_key)s %(conn_key)s %(n_perms)s %(numba_parallel)s @@ -168,6 +170,13 @@ def nhood_enrichment( clust_map = {v: i for i, v in enumerate(original_clust.cat.categories.values)} # map categories int_clust = np.array([clust_map[c] for c in original_clust], dtype=ndt) + if library_key is not None: + _assert_categorical_obs(adata, key=library_key) + libs: list[Any] | None = adata.obs[library_key].cat.categories + else: + libs = None + print(libs) + indices, indptr = (adj.indices.astype(ndt), adj.indptr.astype(ndt)) n_cls = len(clust_map) From 77cda86bf2509952d50e46c6e80689c35faffa56 Mon Sep 17 00:00:00 2001 From: giovp Date: Fri, 8 Dec 2023 11:50:18 +0100 Subject: [PATCH 04/15] add shuffle group per categories --- src/squidpy/gr/_nhood.py | 27 ++++++++++++++++++---- src/squidpy/gr/_utils.py | 3 ++- src/squidpy/pl/_interactive/_controller.py | 3 ++- src/squidpy/pl/_interactive/_utils.py | 5 ++-- src/squidpy/pl/_spatial_utils.py | 16 ++++++------- src/squidpy/pl/_utils.py | 3 ++- 6 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/squidpy/gr/_nhood.py b/src/squidpy/gr/_nhood.py index 47775a0d..cdd6a10a 100644 --- a/src/squidpy/gr/_nhood.py +++ b/src/squidpy/gr/_nhood.py @@ -172,10 +172,9 @@ def nhood_enrichment( if library_key is not None: _assert_categorical_obs(adata, key=library_key) - libs: list[Any] | None = adata.obs[library_key].cat.categories + libraries: pd.Series | None = adata.obs[library_key] else: - libs = None - print(libs) + libraries = None indices, indptr = (adj.indices.astype(ndt), adj.indptr.astype(ndt)) n_cls = len(clust_map) @@ -193,7 +192,7 @@ def nhood_enrichment( n_jobs=n_jobs, backend=backend, show_progress_bar=show_progress_bar, - )(callback=_test, indices=indices, indptr=indptr, int_clust=int_clust, n_cls=n_cls, seed=seed) + )(callback=_test, indices=indices, indptr=indptr, int_clust=int_clust, libraries=libraries, n_cls=n_cls, seed=seed) zscore = (count - perms.mean(axis=0)) / perms.std(axis=0) if copy: @@ -405,6 +404,7 @@ def _nhood_enrichment_helper( indices: NDArrayA, indptr: NDArrayA, int_clust: NDArrayA, + libraries: pd.Series | None, n_cls: int, seed: int | None = None, queue: SigQueue | None = None, @@ -414,7 +414,10 @@ def _nhood_enrichment_helper( rs = np.random.RandomState(seed=None if seed is None else seed + ixs[0]) for i in range(len(ixs)): - rs.shuffle(int_clust) + if libraries is not None: + int_clust = _shuffle_group(int_clust, libraries, rs) + else: + rs.shuffle(int_clust) perms[i, ...] = callback(indices, indptr, int_clust) if queue is not None: @@ -424,3 +427,17 @@ def _nhood_enrichment_helper( queue.put(Signal.FINISH) return perms + + +def _shuffle_group( + arr: NDArrayA, + categories: pd.Series, + rs: np.random.RandomState, +) -> NDArrayA: + categories_output = np.empty(categories.shape) + for c in categories.cat.categories: + idx = np.where(categories == c)[0] + arr_group = arr[idx].copy() + rs.shuffle(arr_group) + categories_output[idx] = arr_group + return categories_output diff --git a/src/squidpy/gr/_utils.py b/src/squidpy/gr/_utils.py index 53a9a5d1..edcd7f0a 100644 --- a/src/squidpy/gr/_utils.py +++ b/src/squidpy/gr/_utils.py @@ -16,6 +16,7 @@ from anndata import AnnData from anndata._core.views import ArrayView, SparseCSCView, SparseCSRView from anndata.utils import make_index_unique +from pandas import CategoricalDtype from pandas.api.types import infer_dtype, is_categorical_dtype from scanpy import logging as logg from scipy.sparse import csc_matrix, csr_matrix, issparse, spmatrix @@ -138,7 +139,7 @@ def _assert_categorical_obs(adata: AnnData, key: str) -> None: if key not in adata.obs: raise KeyError(f"Cluster key `{key}` not found in `adata.obs`.") - if not is_categorical_dtype(adata.obs[key]): + if not isinstance(adata.obs[key], CategoricalDtype): raise TypeError(f"Expected `adata.obs[{key!r}]` to be `categorical`, found `{infer_dtype(adata.obs[key])}`.") diff --git a/src/squidpy/pl/_interactive/_controller.py b/src/squidpy/pl/_interactive/_controller.py index 14620ad7..c256989a 100644 --- a/src/squidpy/pl/_interactive/_controller.py +++ b/src/squidpy/pl/_interactive/_controller.py @@ -9,6 +9,7 @@ from anndata import AnnData from napari import Viewer from napari.layers import Points, Shapes +from pandas import CategoricalDtype from pandas.core.dtypes.common import is_categorical_dtype from PyQt5.QtWidgets import QGridLayout, QLabel, QWidget from scanpy import logging as logg @@ -181,7 +182,7 @@ def add_points(self, vec: NDArrayA | pd.Series, layer_name: str, key: str | None **properties, ) # TODO(michalk8): add contrasting fg/bg color once https://github.com/napari/napari/issues/2019 is done - self._hide_points_controls(layer, is_categorical=is_categorical_dtype(vec)) + self._hide_points_controls(layer, is_categorical=isinstance(vec, CategoricalDtype)) layer.editable = False return True diff --git a/src/squidpy/pl/_interactive/_utils.py b/src/squidpy/pl/_interactive/_utils.py index 190408e2..5d7f78fe 100644 --- a/src/squidpy/pl/_interactive/_utils.py +++ b/src/squidpy/pl/_interactive/_utils.py @@ -6,6 +6,7 @@ from anndata import AnnData from matplotlib.colors import to_hex, to_rgb from numba import njit +from pandas import CategoricalDtype from pandas._libs.lib import infer_dtype from pandas.core.dtypes.common import is_categorical_dtype from scanpy import logging as logg @@ -23,7 +24,7 @@ def _get_categorical( vec: pd.Series | None = None, ) -> NDArrayA: if vec is not None: - if not is_categorical_dtype(vec): + if not isinstance(vec, CategoricalDtype): raise TypeError(f"Expected a `categorical` type, found `{infer_dtype(vec)}`.") if key in adata.obs: logg.debug(f"Overwriting `adata.obs[{key!r}]`") @@ -39,7 +40,7 @@ def _get_categorical( def _position_cluster_labels(coords: NDArrayA, clusters: pd.Series, colors: NDArrayA) -> dict[str, NDArrayA]: - if not is_categorical_dtype(clusters): + if not isinstance(clusters, CategoricalDtype): raise TypeError(f"Expected `clusters` to be `categorical`, found `{infer_dtype(clusters)}`.") coords = coords[:, 1:] # TODO(michalk8): account for current Z-dim? diff --git a/src/squidpy/pl/_spatial_utils.py b/src/squidpy/pl/_spatial_utils.py index ad3ad561..9c1e1f8b 100644 --- a/src/squidpy/pl/_spatial_utils.py +++ b/src/squidpy/pl/_spatial_utils.py @@ -39,7 +39,7 @@ from matplotlib.gridspec import GridSpec from matplotlib.patches import Circle, Polygon, Rectangle from matplotlib_scalebar.scalebar import ScaleBar -from pandas.api.types import CategoricalDtype +from pandas import CategoricalDtype from pandas.core.dtypes.common import is_categorical_dtype from scanpy import logging as logg from scanpy._settings import settings as sc_settings @@ -385,7 +385,7 @@ def subset_by_key( ) -> tuple[AnnData, NDArrayA]: if key is None or values is None: return adata, coords - if key not in adata.obs or not is_categorical_dtype(adata.obs[key]): + if key not in adata.obs or not isinstance(adata.obs[key], CategoricalDtype): return adata, coords try: mask = adata.obs[key].isin(values).values @@ -468,7 +468,7 @@ def _set_color_source_vec( else: color_source_vector = adata.obs_vector(value_to_plot, layer=layer) - if not is_categorical_dtype(color_source_vector): + if not isinstance(color_source_vector, CategoricalDtype): return None, color_source_vector, False color_source_vector = pd.Categorical(color_source_vector) # convert, e.g., `pd.Series` @@ -646,7 +646,7 @@ def _decorate_axs( path_effect = [] # Adding legends - if is_categorical_dtype(color_source_vector): + if isinstance(color_source_vector, CategoricalDtype): clusters = color_source_vector.categories palette = _get_palette(adata, cluster_key=value_to_plot, categories=clusters, palette=palette, alpha=alpha) _add_categorical_legend( @@ -691,11 +691,11 @@ def _map_color_seg( ) -> NDArrayA: cell_id = np.array(cell_id) - if is_categorical_dtype(color_vector): + if isinstance(color_vector, CategoricalDtype): if isinstance(na_color, tuple) and len(na_color) == 4 and np.any(color_source_vector.isna()): cell_id[color_source_vector.isna()] = 0 - val_im: NDArrayA = map_array(seg, cell_id, color_vector.codes + 1) # type: ignore - cols = colors.to_rgba_array(color_vector.categories) # type: ignore + val_im: NDArrayA = map_array(seg, cell_id, color_vector.codes + 1) + cols = colors.to_rgba_array(color_vector.categories) else: val_im = map_array(seg, cell_id, cell_id) # replace with same seg id to remove missing segs try: @@ -744,7 +744,7 @@ def _prepare_args_plot( # set palette if missing for c in color: - if c is not None and c in adata.obs and is_categorical_dtype(adata.obs[c]): + if c is not None and c in adata.obs and isinstance(adata.obs[c], CategoricalDtype): _maybe_set_colors(source=adata, target=adata, key=c, palette=palette) # check raw diff --git a/src/squidpy/pl/_utils.py b/src/squidpy/pl/_utils.py index 861c68e8..8c155a59 100644 --- a/src/squidpy/pl/_utils.py +++ b/src/squidpy/pl/_utils.py @@ -30,6 +30,7 @@ from matplotlib.figure import Figure from mpl_toolkits.axes_grid1 import make_axes_locatable from numba import njit, prange +from pandas import CategoricalDtype from pandas._libs.lib import infer_dtype from pandas.core.dtypes.common import ( is_bool_dtype, @@ -232,7 +233,7 @@ def decorator(self: ALayer, *args: Any, **kwargs: Any) -> Vector_name_t: return None, None if isinstance(res, pd.Series): - if is_categorical_dtype(res): + if isinstance(res, CategoricalDtype): return res, fmt if is_string_dtype(res) or is_object_dtype(res) or is_bool_dtype(res): return res.astype("category"), fmt From daf70069586a73de4b332e39c2f5716f836945db Mon Sep 17 00:00:00 2001 From: giovp Date: Fri, 8 Dec 2023 11:54:24 +0100 Subject: [PATCH 05/15] move it around --- src/squidpy/gr/_nhood.py | 15 +-------------- src/squidpy/gr/_utils.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/squidpy/gr/_nhood.py b/src/squidpy/gr/_nhood.py index cdd6a10a..d9db2fa8 100644 --- a/src/squidpy/gr/_nhood.py +++ b/src/squidpy/gr/_nhood.py @@ -28,6 +28,7 @@ _assert_connectivity_key, _assert_positive, _save_data, + _shuffle_group, ) __all__ = ["nhood_enrichment", "centrality_scores", "interaction_matrix"] @@ -427,17 +428,3 @@ def _nhood_enrichment_helper( queue.put(Signal.FINISH) return perms - - -def _shuffle_group( - arr: NDArrayA, - categories: pd.Series, - rs: np.random.RandomState, -) -> NDArrayA: - categories_output = np.empty(categories.shape) - for c in categories.cat.categories: - idx = np.where(categories == c)[0] - arr_group = arr[idx].copy() - rs.shuffle(arr_group) - categories_output[idx] = arr_group - return categories_output diff --git a/src/squidpy/gr/_utils.py b/src/squidpy/gr/_utils.py index edcd7f0a..a1db797a 100644 --- a/src/squidpy/gr/_utils.py +++ b/src/squidpy/gr/_utils.py @@ -300,3 +300,33 @@ def key_present() -> bool: # in principle we assume the callee doesn't change the index # otherwise, would need to check whether it has been changed and add an option to determine what to do adata.var.index = var_names + + +def _shuffle_group( + cluster_annotation: NDArrayA, + libraries: pd.Series, + rs: np.random.RandomState, +) -> NDArrayA: + """ + Shuffle values in ``arr`` for each category in ``categories``. + + Useful when the shuffling of categories is used in permutation tests where the order of values in ``arr`` matters (e.g. you only want to shuffle cluster annotations for the same slide/library_key, and not across slides + + Parameters + ---------- + cluster_annotation + Array to shuffle. + libraries + Categories (e.g. libraries) to subset for shuffling. + + Returns + ------- + Shuffled annotations. + """ + cluster_annotation_output = np.empty(libraries.shape) + for c in libraries.cat.categories: + idx = np.where(libraries == c)[0] + arr_group = cluster_annotation[idx].copy() + rs.shuffle(arr_group) # it's done in place hence copy before + cluster_annotation_output[idx] = arr_group + return cluster_annotation_output From 0a1763770c275043398d9430de0798d56b315d36 Mon Sep 17 00:00:00 2001 From: giovp Date: Fri, 8 Dec 2023 12:38:01 +0100 Subject: [PATCH 06/15] add test --- src/squidpy/gr/_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/squidpy/gr/_utils.py b/src/squidpy/gr/_utils.py index a1db797a..a62f2582 100644 --- a/src/squidpy/gr/_utils.py +++ b/src/squidpy/gr/_utils.py @@ -323,7 +323,7 @@ def _shuffle_group( ------- Shuffled annotations. """ - cluster_annotation_output = np.empty(libraries.shape) + cluster_annotation_output = np.empty(libraries.shape, dtype=cluster_annotation.dtype) for c in libraries.cat.categories: idx = np.where(libraries == c)[0] arr_group = cluster_annotation[idx].copy() From f5b23c419c984ea26d648db25e2f605f73d8f37d Mon Sep 17 00:00:00 2001 From: giovp Date: Fri, 8 Dec 2023 14:48:01 +0100 Subject: [PATCH 07/15] fix tests --- src/squidpy/gr/_nhood.py | 3 ++- src/squidpy/gr/_utils.py | 4 ++-- src/squidpy/pl/_interactive/_controller.py | 2 +- src/squidpy/pl/_interactive/_utils.py | 4 ++-- src/squidpy/pl/_spatial_utils.py | 14 +++++++------- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/squidpy/gr/_nhood.py b/src/squidpy/gr/_nhood.py index d9db2fa8..720ac502 100644 --- a/src/squidpy/gr/_nhood.py +++ b/src/squidpy/gr/_nhood.py @@ -16,6 +16,7 @@ import pandas as pd from anndata import AnnData from numba import njit, prange # noqa: F401 +from pandas import CategoricalDtype from scanpy import logging as logg from spatialdata import SpatialData @@ -405,7 +406,7 @@ def _nhood_enrichment_helper( indices: NDArrayA, indptr: NDArrayA, int_clust: NDArrayA, - libraries: pd.Series | None, + libraries: pd.Series[CategoricalDtype] | None, n_cls: int, seed: int | None = None, queue: SigQueue | None = None, diff --git a/src/squidpy/gr/_utils.py b/src/squidpy/gr/_utils.py index a62f2582..9c4bfafc 100644 --- a/src/squidpy/gr/_utils.py +++ b/src/squidpy/gr/_utils.py @@ -139,7 +139,7 @@ def _assert_categorical_obs(adata: AnnData, key: str) -> None: if key not in adata.obs: raise KeyError(f"Cluster key `{key}` not found in `adata.obs`.") - if not isinstance(adata.obs[key], CategoricalDtype): + if not isinstance(adata.obs[key].dtype, CategoricalDtype): raise TypeError(f"Expected `adata.obs[{key!r}]` to be `categorical`, found `{infer_dtype(adata.obs[key])}`.") @@ -304,7 +304,7 @@ def key_present() -> bool: def _shuffle_group( cluster_annotation: NDArrayA, - libraries: pd.Series, + libraries: pd.Series[CategoricalDtype], rs: np.random.RandomState, ) -> NDArrayA: """ diff --git a/src/squidpy/pl/_interactive/_controller.py b/src/squidpy/pl/_interactive/_controller.py index c256989a..abe21dff 100644 --- a/src/squidpy/pl/_interactive/_controller.py +++ b/src/squidpy/pl/_interactive/_controller.py @@ -182,7 +182,7 @@ def add_points(self, vec: NDArrayA | pd.Series, layer_name: str, key: str | None **properties, ) # TODO(michalk8): add contrasting fg/bg color once https://github.com/napari/napari/issues/2019 is done - self._hide_points_controls(layer, is_categorical=isinstance(vec, CategoricalDtype)) + self._hide_points_controls(layer, is_categorical=isinstance(vec.dtype, CategoricalDtype)) layer.editable = False return True diff --git a/src/squidpy/pl/_interactive/_utils.py b/src/squidpy/pl/_interactive/_utils.py index 5d7f78fe..2eb002ed 100644 --- a/src/squidpy/pl/_interactive/_utils.py +++ b/src/squidpy/pl/_interactive/_utils.py @@ -24,7 +24,7 @@ def _get_categorical( vec: pd.Series | None = None, ) -> NDArrayA: if vec is not None: - if not isinstance(vec, CategoricalDtype): + if not isinstance(vec.dtype, CategoricalDtype): raise TypeError(f"Expected a `categorical` type, found `{infer_dtype(vec)}`.") if key in adata.obs: logg.debug(f"Overwriting `adata.obs[{key!r}]`") @@ -40,7 +40,7 @@ def _get_categorical( def _position_cluster_labels(coords: NDArrayA, clusters: pd.Series, colors: NDArrayA) -> dict[str, NDArrayA]: - if not isinstance(clusters, CategoricalDtype): + if not isinstance(clusters.dtype, CategoricalDtype): raise TypeError(f"Expected `clusters` to be `categorical`, found `{infer_dtype(clusters)}`.") coords = coords[:, 1:] # TODO(michalk8): account for current Z-dim? diff --git a/src/squidpy/pl/_spatial_utils.py b/src/squidpy/pl/_spatial_utils.py index 9c1e1f8b..3304fae5 100644 --- a/src/squidpy/pl/_spatial_utils.py +++ b/src/squidpy/pl/_spatial_utils.py @@ -385,7 +385,7 @@ def subset_by_key( ) -> tuple[AnnData, NDArrayA]: if key is None or values is None: return adata, coords - if key not in adata.obs or not isinstance(adata.obs[key], CategoricalDtype): + if key not in adata.obs or not isinstance(adata.obs[key].dtype, CategoricalDtype): return adata, coords try: mask = adata.obs[key].isin(values).values @@ -468,7 +468,7 @@ def _set_color_source_vec( else: color_source_vector = adata.obs_vector(value_to_plot, layer=layer) - if not isinstance(color_source_vector, CategoricalDtype): + if not isinstance(color_source_vector.dtype, CategoricalDtype): return None, color_source_vector, False color_source_vector = pd.Categorical(color_source_vector) # convert, e.g., `pd.Series` @@ -646,7 +646,7 @@ def _decorate_axs( path_effect = [] # Adding legends - if isinstance(color_source_vector, CategoricalDtype): + if isinstance(color_source_vector.dtype, CategoricalDtype): clusters = color_source_vector.categories palette = _get_palette(adata, cluster_key=value_to_plot, categories=clusters, palette=palette, alpha=alpha) _add_categorical_legend( @@ -691,11 +691,11 @@ def _map_color_seg( ) -> NDArrayA: cell_id = np.array(cell_id) - if isinstance(color_vector, CategoricalDtype): + if isinstance(color_vector.dtype, CategoricalDtype): if isinstance(na_color, tuple) and len(na_color) == 4 and np.any(color_source_vector.isna()): cell_id[color_source_vector.isna()] = 0 - val_im: NDArrayA = map_array(seg, cell_id, color_vector.codes + 1) - cols = colors.to_rgba_array(color_vector.categories) + val_im: NDArrayA = map_array(seg, cell_id, color_vector.codes + 1) # type: ignore[union-attr] + cols = colors.to_rgba_array(color_vector.categories) # type: ignore[union-attr] else: val_im = map_array(seg, cell_id, cell_id) # replace with same seg id to remove missing segs try: @@ -744,7 +744,7 @@ def _prepare_args_plot( # set palette if missing for c in color: - if c is not None and c in adata.obs and isinstance(adata.obs[c], CategoricalDtype): + if c is not None and c in adata.obs and isinstance(adata.obs[c].dtype, CategoricalDtype): _maybe_set_colors(source=adata, target=adata, key=c, palette=palette) # check raw From 04d1b90f45d48a9ec63284fc34a8f1e731db9826 Mon Sep 17 00:00:00 2001 From: giovp Date: Fri, 8 Dec 2023 14:48:18 +0100 Subject: [PATCH 08/15] add test --- tests/graph/test_utils.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/graph/test_utils.py diff --git a/tests/graph/test_utils.py b/tests/graph/test_utils.py new file mode 100644 index 00000000..86b52535 --- /dev/null +++ b/tests/graph/test_utils.py @@ -0,0 +1,28 @@ +import numpy as np +import pandas as pd +import pytest +from anndata import AnnData +from squidpy._constants._pkg_constants import Key +from squidpy.gr._utils import _shuffle_group + + +class TestUtils: + @pytest.mark.parametrize("cluster_annotations_type", [int, str]) + @pytest.mark.parametrize("library_annotations_type", [int, str]) + @pytest.mark.parametrize("seed", [422, 422222]) + def test_shuffle_group(self, cluster_annotations_type: type, library_annotations_type: type, seed: int): + size = 6 + rng = np.random.default_rng(seed) + if cluster_annotations_type == int: + libraries = pd.Series(rng.choice([1, 2, 3, 4], size=(size,)), dtype="category") + else: + libraries = pd.Series(rng.choice(["a", "b", "c"], size=(size,)), dtype="category") + + if library_annotations_type == int: + cluster_annotations = rng.choice([1, 2, 3, 4], size=(size,)) + else: + cluster_annotations = rng.choice(["X", "Y", "Z"], size=(size,)) + rs = np.random.RandomState(seed) + out = _shuffle_group(cluster_annotations, libraries, rs) + for c in libraries.cat.categories: + assert set(out[libraries == c]) == set(cluster_annotations[libraries == c]) From 02cdfa896c03f84031ca4cf002ef2c0afc2b6cf3 Mon Sep 17 00:00:00 2001 From: giovp Date: Fri, 8 Dec 2023 14:57:31 +0100 Subject: [PATCH 09/15] fix tests --- src/squidpy/pl/_spatial_utils.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/squidpy/pl/_spatial_utils.py b/src/squidpy/pl/_spatial_utils.py index 3304fae5..a3b2d4c0 100644 --- a/src/squidpy/pl/_spatial_utils.py +++ b/src/squidpy/pl/_spatial_utils.py @@ -385,7 +385,7 @@ def subset_by_key( ) -> tuple[AnnData, NDArrayA]: if key is None or values is None: return adata, coords - if key not in adata.obs or not isinstance(adata.obs[key].dtype, CategoricalDtype): + if key not in adata.obs or not is_categorical_dtype(adata.obs[key]): return adata, coords try: mask = adata.obs[key].isin(values).values @@ -468,7 +468,7 @@ def _set_color_source_vec( else: color_source_vector = adata.obs_vector(value_to_plot, layer=layer) - if not isinstance(color_source_vector.dtype, CategoricalDtype): + if not is_categorical_dtype(color_source_vector): return None, color_source_vector, False color_source_vector = pd.Categorical(color_source_vector) # convert, e.g., `pd.Series` @@ -646,7 +646,7 @@ def _decorate_axs( path_effect = [] # Adding legends - if isinstance(color_source_vector.dtype, CategoricalDtype): + if is_categorical_dtype(color_source_vector): clusters = color_source_vector.categories palette = _get_palette(adata, cluster_key=value_to_plot, categories=clusters, palette=palette, alpha=alpha) _add_categorical_legend( @@ -691,7 +691,7 @@ def _map_color_seg( ) -> NDArrayA: cell_id = np.array(cell_id) - if isinstance(color_vector.dtype, CategoricalDtype): + if is_categorical_dtype(color_vector): if isinstance(na_color, tuple) and len(na_color) == 4 and np.any(color_source_vector.isna()): cell_id[color_source_vector.isna()] = 0 val_im: NDArrayA = map_array(seg, cell_id, color_vector.codes + 1) # type: ignore[union-attr] From f908ab1e9a7df6fc5ba30f4ec1b6574dd9898e42 Mon Sep 17 00:00:00 2001 From: giovp Date: Mon, 5 Feb 2024 17:50:37 +0100 Subject: [PATCH 10/15] update --- src/squidpy/gr/_utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/squidpy/gr/_utils.py b/src/squidpy/gr/_utils.py index a1fdedf3..3b4483b6 100644 --- a/src/squidpy/gr/_utils.py +++ b/src/squidpy/gr/_utils.py @@ -311,7 +311,8 @@ def _shuffle_group( """ Shuffle values in ``arr`` for each category in ``categories``. - Useful when the shuffling of categories is used in permutation tests where the order of values in ``arr`` matters (e.g. you only want to shuffle cluster annotations for the same slide/library_key, and not across slides + Useful when the shuffling of categories is used in permutation tests where the order of values in ``arr`` matters + (e.g. you only want to shuffle cluster annotations for the same slide/library_key, and not across slides) Parameters ---------- From 961a879c4be3ea00aa3c05f0c454f4be0687e83a Mon Sep 17 00:00:00 2001 From: giovp Date: Mon, 5 Feb 2024 17:54:42 +0100 Subject: [PATCH 11/15] fix test --- tests/graph/test_utils.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/graph/test_utils.py b/tests/graph/test_utils.py index 86b52535..9b8915c0 100644 --- a/tests/graph/test_utils.py +++ b/tests/graph/test_utils.py @@ -22,7 +22,6 @@ def test_shuffle_group(self, cluster_annotations_type: type, library_annotations cluster_annotations = rng.choice([1, 2, 3, 4], size=(size,)) else: cluster_annotations = rng.choice(["X", "Y", "Z"], size=(size,)) - rs = np.random.RandomState(seed) - out = _shuffle_group(cluster_annotations, libraries, rs) + out = _shuffle_group(cluster_annotations, libraries, rng) for c in libraries.cat.categories: assert set(out[libraries == c]) == set(cluster_annotations[libraries == c]) From fefef28277ae0afefed71968e6e814357ffed207 Mon Sep 17 00:00:00 2001 From: giovp Date: Mon, 5 Feb 2024 18:03:10 +0100 Subject: [PATCH 12/15] update --- .pre-commit-config.yaml | 2 +- docs/release/notes-1.4.0.rst | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 docs/release/notes-1.4.0.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 413b1064..1678813a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -56,7 +56,7 @@ repos: - id: blacken-docs - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.1.14 + rev: v0.2.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] diff --git a/docs/release/notes-1.4.0.rst b/docs/release/notes-1.4.0.rst new file mode 100644 index 00000000..b6f5b95d --- /dev/null +++ b/docs/release/notes-1.4.0.rst @@ -0,0 +1,25 @@ +Squidpy 1.4.0 (2024-02-05) +========================== + +Bugfixes +-------- + +- Fix building graph in ``knn`` and ``delaunay`` mode. + `@giovp `__ + `#792 `__ + + +Miscellaneous +------------- + +- Fix napari installation. + `@giovp `__ + `#767 `__ + +- Made nanostring reader more flexible by adjusting loading of images. + `@FrancescaDr `__ + `#766 `__ + +- Fix ``sq.tl.var_by_distance`` method to support ``pandas 2.2.0``. + `@LLehner `__ + `#794 `__ From 8d078ac049082553ed04d4ec62f2cb911d514e6d Mon Sep 17 00:00:00 2001 From: giovp Date: Mon, 5 Feb 2024 18:04:30 +0100 Subject: [PATCH 13/15] enable M1 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 96639c97..ed82a016 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,13 +20,13 @@ jobs: strategy: fail-fast: false matrix: - python: [3.9, "3.10"] + python: [3.9, "3.10", "3.11"] os: [ubuntu-latest] include: - python: 3.9 os: macos-latest - python: "3.10" - os: macos-latest + os: macos-14 env: OS: ${{ matrix.os }} PYTHON: ${{ matrix.python }} From 80c26f7edfbef80b1fded073318adbfe2777dfd8 Mon Sep 17 00:00:00 2001 From: giovp Date: Mon, 5 Feb 2024 18:05:59 +0100 Subject: [PATCH 14/15] add final release note --- docs/release/notes-1.4.0.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/release/notes-1.4.0.rst b/docs/release/notes-1.4.0.rst index b6f5b95d..ddd89035 100644 --- a/docs/release/notes-1.4.0.rst +++ b/docs/release/notes-1.4.0.rst @@ -8,6 +8,10 @@ Bugfixes `@giovp `__ `#792 `__ +- Correct shuffling of annotations in ``sq.gr.nhood_enrichment``. + `@giovp `__ + `#775 `__ + Miscellaneous ------------- From c132877a718efd0b176fbed09541e66b0fcfb878 Mon Sep 17 00:00:00 2001 From: giovp Date: Mon, 5 Feb 2024 18:08:32 +0100 Subject: [PATCH 15/15] fix release note --- docs/release/notes-1.4.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/release/notes-1.4.0.rst b/docs/release/notes-1.4.0.rst index ddd89035..b030de90 100644 --- a/docs/release/notes-1.4.0.rst +++ b/docs/release/notes-1.4.0.rst @@ -5,7 +5,7 @@ Bugfixes -------- - Fix building graph in ``knn`` and ``delaunay`` mode. - `@giovp `__ + `@michalk8 `__ `#792 `__ - Correct shuffling of annotations in ``sq.gr.nhood_enrichment``.