diff --git a/docs/api/deprecated.md b/docs/api/deprecated.md index d09c1af405..78a5b12b60 100644 --- a/docs/api/deprecated.md +++ b/docs/api/deprecated.md @@ -12,4 +12,5 @@ pp.filter_genes_dispersion pp.normalize_per_cell pp.subsample + tl.louvain ``` diff --git a/docs/api/plotting.md b/docs/api/plotting.md index 09811df2d3..502b132b98 100644 --- a/docs/api/plotting.md +++ b/docs/api/plotting.md @@ -118,7 +118,7 @@ Compute densities on embeddings. #### Branching trajectories and pseudotime, clustering -Visualize clusters using one of the embedding methods passing `color='louvain'`. +Visualize clusters using one of the embedding methods passing e.g. `color='leiden'`. ```{eval-rst} .. autosummary:: diff --git a/docs/api/tools.md b/docs/api/tools.md index 13d82b46c7..27cd324ab7 100644 --- a/docs/api/tools.md +++ b/docs/api/tools.md @@ -42,7 +42,6 @@ Compute densities on embeddings. :toctree: ../generated/ tl.leiden - tl.louvain tl.dendrogram tl.dpt tl.paga diff --git a/docs/dev/documentation.md b/docs/dev/documentation.md index dcad9533ed..3ee2f09747 100644 --- a/docs/dev/documentation.md +++ b/docs/dev/documentation.md @@ -45,10 +45,11 @@ Some key points: - When docs exist in the same file as code, line length restrictions still apply. In files which are just docs, go with a sentence per line (for easier `git diff`s). - Check that the docs look like what you expect them too! It's easy to forget to add a reference to function, be sure it got added and looks right. -Look at [sc.tl.louvain](https://github.com/scverse/scanpy/blob/a811fee0ef44fcaecbde0cad6336336bce649484/scanpy/tools/_louvain.py#L22-L90) as an example for everything mentioned here. +Look at [`sc.tl.leiden`’s docstring][] as an example for everything mentioned here. [napolean guide to numpy style docstrings]: https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_numpy.html#example-numpy [sphinx rst primer]: https://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html +[`sc.tl.leiden`’s docstring]: https://github.com/scverse/scanpy/blob/350c3424d2f96c4a3a7bb3b7d0428d38d842ebe8/src/scanpy/tools/_leiden.py#L49-L120 ### Plots in docstrings diff --git a/docs/release-notes/3658.misc.md b/docs/release-notes/3658.misc.md new file mode 100644 index 0000000000..af345761e2 --- /dev/null +++ b/docs/release-notes/3658.misc.md @@ -0,0 +1 @@ +Deprecate {func}`scanpy.tl.louvain`. {smaller}`P Angerer` diff --git a/pyproject.toml b/pyproject.toml index 2558eabdf0..ff3af91e64 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,7 +101,6 @@ test = [ "zarr<3", # additional tested algorithms "scanpy[scrublet]", - "scanpy[louvain]", "scanpy[leiden]", "scanpy[skmisc]", "scanpy[dask-ml]", @@ -134,14 +133,14 @@ dev = [ ] # Algorithms paga = [ "igraph" ] -louvain = [ "igraph", "louvain>=0.8.2" ] # Louvain community detection -leiden = [ "igraph>=0.10.8", "leidenalg>=0.9.0" ] # Leiden community detection -bbknn = [ "bbknn" ] # Batch balanced KNN (batch correction) -magic = [ "magic-impute>=2.0.4" ] # MAGIC imputation method -skmisc = [ "scikit-misc>=0.5.1" ] # highly_variable_genes method 'seurat_v3' -harmony = [ "harmonypy" ] # Harmony dataset integration -scanorama = [ "scanorama" ] # Scanorama dataset integration -scrublet = [ "scikit-image>=0.20.0" ] # Doublet detection with automatic thresholds +louvain = [ "igraph", "louvain>=0.8.2", "setuptools" ] # Louvain community detection +leiden = [ "igraph>=0.10.8", "leidenalg>=0.9.0" ] # Leiden community detection +bbknn = [ "bbknn" ] # Batch balanced KNN (batch correction) +magic = [ "magic-impute>=2.0.4" ] # MAGIC imputation method +skmisc = [ "scikit-misc>=0.5.1" ] # highly_variable_genes method 'seurat_v3' +harmony = [ "harmonypy" ] # Harmony dataset integration +scanorama = [ "scanorama" ] # Scanorama dataset integration +scrublet = [ "scikit-image>=0.20.0" ] # Doublet detection with automatic thresholds # Acceleration rapids = [ "cudf>=0.9", "cuml>=0.9", "cugraph>=0.9" ] # GPU accelerated calculation of neighbors dask = [ "dask[array]>=2023.5.1" ] # Use the Dask parallelization engine @@ -175,9 +174,6 @@ filterwarnings = [ "error:The specified parameters:FutureWarning", # When calling `.show()` in tests, this is raised "ignore:FigureCanvasAgg is non-interactive:UserWarning", - # Deprecated tools we still test - "ignore:pkg_resources:UserWarning:louvain", - "ignore:.*superseded by.*leidenalg:DeprecationWarning", # We explicitly handle the below errors in tests "error:`anndata.read` is deprecated:FutureWarning", @@ -212,6 +208,7 @@ exclude_also = [ "if TYPE_CHECKING:", # https://github.com/numba/numba/issues/4268 '@(numba\.|nb\.)?njit.*', + "@deprecated.*", ] [tool.ruff] diff --git a/src/scanpy/tools/_leiden.py b/src/scanpy/tools/_leiden.py index fa06d8699d..5658721609 100644 --- a/src/scanpy/tools/_leiden.py +++ b/src/scanpy/tools/_leiden.py @@ -20,12 +20,12 @@ from .._compat import CSBase from .._utils.random import _LegacyRandom -try: # separate block for fallible import - from leidenalg.VertexPartition import MutableVertexPartition -except ImportError: - if not TYPE_CHECKING: - MutableVertexPartition = type("MutableVertexPartition", (), {}) - MutableVertexPartition.__module__ = "leidenalg.VertexPartition" + try: # sphinx-autodoc-typehints + optional dependency + from leidenalg.VertexPartition import MutableVertexPartition + except ImportError: + if not TYPE_CHECKING: + MutableVertexPartition = type("MutableVertexPartition", (), {}) + MutableVertexPartition.__module__ = "leidenalg.VertexPartition" def leiden( # noqa: PLR0912, PLR0913, PLR0915 diff --git a/src/scanpy/tools/_louvain.py b/src/scanpy/tools/_louvain.py index 2d1253bc4d..16f3e33563 100644 --- a/src/scanpy/tools/_louvain.py +++ b/src/scanpy/tools/_louvain.py @@ -11,7 +11,7 @@ from .. import _utils from .. import logging as logg -from .._compat import old_positionals +from .._compat import deprecated, old_positionals from .._utils import _choose_graph, dematrix from ._utils_clustering import rename_groups, restrict_adjacency @@ -24,14 +24,12 @@ from .._compat import CSBase from .._utils.random import _LegacyRandom -try: - from louvain.VertexPartition import MutableVertexPartition -except ImportError: - - class MutableVertexPartition: - pass - - MutableVertexPartition.__module__ = "louvain.VertexPartition" + try: # sphinx-autodoc-typehints + optional dependency + from louvain.VertexPartition import MutableVertexPartition + except ImportError: + if not TYPE_CHECKING: + MutableVertexPartition = type("MutableVertexPartition", (), {}) + MutableVertexPartition.__module__ = "louvain.VertexPartition" @old_positionals( @@ -48,6 +46,7 @@ class MutableVertexPartition: "obsp", "copy", ) +@deprecated("Use `scanpy.tl.leiden` instead") def louvain( # noqa: PLR0912, PLR0913, PLR0915 adata: AnnData, resolution: float | None = None, @@ -67,6 +66,9 @@ def louvain( # noqa: PLR0912, PLR0913, PLR0915 ) -> AnnData | None: """Cluster cells into subgroups :cite:p:`Blondel2008,Levine2015,Traag2017`. + .. deprecated:: 1.12.0 + Use :func:`scanpy.tl.leiden` instead. + Cluster cells using the Louvain algorithm :cite:p:`Blondel2008` in the implementation of :cite:t:`Traag2017`. The Louvain algorithm was proposed for single-cell analysis by :cite:t:`Levine2015`. diff --git a/tests/notebooks/test_paga_paul15_subsampled.py b/tests/notebooks/test_paga_paul15_subsampled.py deleted file mode 100644 index 5d8c17d336..0000000000 --- a/tests/notebooks/test_paga_paul15_subsampled.py +++ /dev/null @@ -1,143 +0,0 @@ -# PAGA for hematopoiesis in mouse [(Paul *et al.*, 2015)](https://doi.org/10.1016/j.cell.2015.11.013) -# Hematopoiesis: trace myeloid and erythroid differentiation for data of [Paul *et al.* (2015)](https://doi.org/10.1016/j.cell.2015.11.013). -# -# This is the subsampled notebook for testing. -from __future__ import annotations - -from functools import partial -from pathlib import Path - -import numpy as np -import pytest -from matplotlib.testing import setup - -import scanpy as sc -from testing.scanpy._helpers.data import paul15 -from testing.scanpy._pytest.marks import needs - -HERE: Path = Path(__file__).parent -ROOT = HERE / "_images_paga_paul15_subsampled" - - -@pytest.mark.skip(reason="Broken, needs fixing") -@needs.igraph -@needs.louvain -def test_paga_paul15_subsampled(image_comparer, plt): - setup() - save_and_compare_images = partial(image_comparer, ROOT, tol=25) - - adata = paul15() - sc.pp.subsample(adata, n_obs=200) - del adata.uns["iroot"] - adata.X = adata.X.astype("float64") - - # Preprocessing and Visualization - sc.pp.recipe_zheng17(adata) - sc.pp.pca(adata, svd_solver="arpack") - sc.pp.neighbors(adata, n_neighbors=4, n_pcs=20) - sc.tl.draw_graph(adata) - sc.pl.draw_graph(adata, color="paul15_clusters", legend_loc="on data") - - sc.tl.diffmap(adata) - sc.tl.diffmap(adata) # See #1262 - sc.pp.neighbors(adata, n_neighbors=10, use_rep="X_diffmap") - sc.tl.draw_graph(adata) - - sc.pl.draw_graph(adata, color="paul15_clusters", legend_loc="on data") - - # TODO: currently needs skip if louvain isn't installed, do major rework - - # Clustering and PAGA - sc.tl.louvain(adata, resolution=1.0) - sc.tl.paga(adata, groups="louvain") - # sc.pl.paga(adata, color=['louvain', 'Hba-a2', 'Elane', 'Irf8']) - # sc.pl.paga(adata, color=['louvain', 'Itga2b', 'Prss34']) - - adata.obs["louvain_anno"] = adata.obs["louvain"] - sc.tl.paga(adata, groups="louvain_anno") - - PAGA_CONNECTIVITIES = np.array( - [ - [0.0, 0.128553, 0.0, 0.07825, 0.0, 0.0, 0.238741, 0.0, 0.0, 0.657049], - [ - *[0.128553, 0.0, 0.480676, 0.257505, 0.533036], - *[0.043871, 0.0, 0.032903, 0.0, 0.087743], - ], - ] - ) - - assert np.allclose( - adata.uns["paga"]["connectivities"].toarray()[:2], - PAGA_CONNECTIVITIES, - atol=1e-4, - ) - - sc.pl.paga(adata, threshold=0.03) - - # !!!! no clue why it doesn't produce images with the same shape - # save_and_compare_images('paga') - - sc.tl.draw_graph(adata, init_pos="paga") - sc.pl.paga_compare( - adata, - threshold=0.03, - title="", - right_margin=0.2, - size=10, - edge_width_scale=0.5, - legend_fontsize=12, - fontsize=12, - frameon=False, - edges=True, - ) - - # slight deviations because of graph drawing - # save_and_compare_images('paga_compare') - - adata.uns["iroot"] = np.flatnonzero(adata.obs["louvain_anno"] == "3")[0] - sc.tl.dpt(adata) - gene_names = [ - "Gata2", - "Gata1", - "Klf1", - "Hba-a2", # erythroid - "Elane", - "Cebpe", # neutrophil - "Irf8", - ] # monocyte - - paths = [ - ("erythrocytes", [3, 9, 0, 6]), - ("neutrophils", [3, 1, 2]), - ("monocytes", [3, 1, 4, 5]), - ] - - adata.obs["distance"] = adata.obs["dpt_pseudotime"] - - _, axs = plt.subplots( - ncols=3, figsize=(6, 2.5), gridspec_kw={"wspace": 0.05, "left": 0.12} - ) - plt.subplots_adjust(left=0.05, right=0.98, top=0.82, bottom=0.2) - for ipath, (descr, path) in enumerate(paths): - _, data = sc.pl.paga_path( - adata, - path, - gene_names, - show_node_names=False, - ax=axs[ipath], - ytick_fontsize=12, - left_margin=0.15, - n_avg=50, - annotations=["distance"], - show_yticks=ipath == 0, - show_colorbar=False, - color_map="Greys", - color_maps_annotations={"distance": "viridis"}, - title=f"{descr} path", - return_data=True, - show=False, - ) - # add a test for this at some point - # data.to_csv(f"./write/paga_path_{descr}.csv") - - save_and_compare_images("paga_path") diff --git a/tests/test_clustering.py b/tests/test_clustering.py index 5028ab5e4d..9cb9e2f32d 100644 --- a/tests/test_clustering.py +++ b/tests/test_clustering.py @@ -149,7 +149,6 @@ def test_leiden_objective_function(adata_neighbors): @pytest.mark.parametrize( ("clustering", "key"), [ - pytest.param(sc.tl.louvain, "louvain", marks=needs.louvain), pytest.param(sc.tl.leiden, "leiden", marks=needs.leidenalg), ], ) @@ -180,45 +179,10 @@ def test_clustering_subset(adata_neighbors, clustering, key): assert len(common_cat) == 0 -@needs.louvain -@needs.igraph -def test_louvain_basic(adata_neighbors): - sc.tl.louvain(adata_neighbors) - sc.tl.louvain(adata_neighbors, use_weights=True) - sc.tl.louvain(adata_neighbors, use_weights=True, flavor="igraph") - sc.tl.louvain(adata_neighbors, flavor="igraph") - - -@needs.louvain -@pytest.mark.parametrize("random_state", [10, 999]) -@pytest.mark.parametrize("resolution", [0.9, 1.1]) -def test_louvain_custom_key(adata_neighbors, resolution, random_state): - sc.tl.louvain( - adata_neighbors, - key_added="louvain_custom", - random_state=random_state, - resolution=resolution, - ) - assert ( - adata_neighbors.uns["louvain_custom"]["params"]["random_state"] == random_state - ) - assert adata_neighbors.uns["louvain_custom"]["params"]["resolution"] == resolution - - -@needs.louvain -@needs.igraph -def test_partition_type(adata_neighbors): - import louvain - - sc.tl.louvain(adata_neighbors, partition_type=louvain.RBERVertexPartition) - sc.tl.louvain(adata_neighbors, partition_type=louvain.SurpriseVertexPartition) - - @pytest.mark.parametrize( ("clustering", "default_key", "default_res", "custom_resolutions"), [ pytest.param(sc.tl.leiden, "leiden", 0.8, [0.9, 1.1], marks=needs.leidenalg), - pytest.param(sc.tl.louvain, "louvain", 0.8, [0.9, 1.1], marks=needs.louvain), ], ) def test_clustering_custom_key( diff --git a/tests/test_neighbors_key_added.py b/tests/test_neighbors_key_added.py index 1da87f8ba9..a3bbb4464b 100644 --- a/tests/test_neighbors_key_added.py +++ b/tests/test_neighbors_key_added.py @@ -89,23 +89,3 @@ def test_neighbors_key_obsp(adata, field): adata.uns["paga"]["connectivities_tree"].toarray(), adata1.uns["paga"]["connectivities_tree"].toarray(), ) - - -@needs.louvain -@pytest.mark.parametrize("field", ["neighbors_key", "obsp"]) -def test_neighbors_key_obsp_louvain(adata, field): - adata1 = adata.copy() - - sc.pp.neighbors(adata, n_neighbors=n_neighbors, random_state=0) - sc.pp.neighbors(adata1, n_neighbors=n_neighbors, random_state=0, key_added=key) - - if field == "neighbors_key": - arg = {field: key} - else: - arg = {field: adata1.uns[key]["connectivities_key"]} - - sc.tl.louvain(adata, random_state=0) - sc.tl.louvain(adata1, random_state=0, **arg) - - assert adata.uns["louvain"]["params"] == adata1.uns["louvain"]["params"] - assert np.all(adata.obs["louvain"] == adata1.obs["louvain"])