Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/check-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ jobs:
id: changes
with:
filters: | # this is intentionally a string
relnotes: 'docs/release-notes/${{ github.event.pull_request.number }}.*.md'
relnotes: 'docs/release-notes/${{ github.event.pull_request.number }}.${{ (contains(github.event.pull_request.title, '!') && 'breaking') || needs.check-milestone.outputs.type }}.md'
- name: Check if a relevant release fragment is added
uses: flying-sheep/check@v1
with:
Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions docs/release-notes/4045.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Make {func}`scanpy.metrics.modularity` actually use edge weights {smaller}`P Angerer`
6 changes: 3 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,17 @@ dependencies = [
"numba>=0.60",
"numpy>=2",
"packaging>=25",
"pandas>=2.2.2",
"pandas>=2.3",
"patsy",
"pynndescent>=0.5.13",
"scikit-learn>=1.4.2",
"scikit-learn>=1.6",
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

required by umap 0.5.12

"scipy>=1.13",
"seaborn>=0.13.2",
"session-info2",
"statsmodels>=0.14.5",
"tqdm",
"typing-extensions; python_version<'3.13'",
"umap-learn>=0.5.7",
"umap-learn>=0.5.12",
]
urls.Bluesky = "https://bsky.app/profile/scverse.bsky.social"
urls.Discourse = "https://discourse.scverse.org/c/help/scanpy/37"
Expand Down
2 changes: 1 addition & 1 deletion src/scanpy/metrics/_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ def modularity_array(
raise
igraph_mode: str = ig.ADJ_DIRECTED if is_directed else ig.ADJ_UNDIRECTED
graph: ig.Graph = ig.Graph.Weighted_Adjacency(connectivities, mode=igraph_mode)
return graph.modularity(_codes(labels))
return graph.modularity(_codes(labels), "weight")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can you explain this a bit? Why was this broken before?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

the Graph.Weighted_Adjacency() class method creates a graph with a "weights" edge attribute (parameter default attr='weight'), using the values of the passed sparse matrix as edge weights.

the Graph.modularity() instance method has a weights: str | array | None = None parameter that specify which edge weights to take into account when calculating the modularity.

In #3613 we forgot to specify that.



def _codes(labels: AnyArrayLike) -> AnyArrayLike:
Expand Down
8 changes: 0 additions & 8 deletions src/testing/scanpy/_pytest/marks.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
from __future__ import annotations

from enum import Enum, auto
from importlib.metadata import version
from importlib.util import find_spec

import pytest
from packaging.version import Version


class QuietMarkDecorator(pytest.MarkDecorator):
Expand Down Expand Up @@ -71,9 +69,3 @@ def skip_reason(self) -> str | None:
if self._name_.casefold() != self.mod.casefold().replace("-", "_"):
reason = f"{reason} (`pip install {self.mod}`)"
return reason


# TODO: remove once https://github.com/numba/numba/issues/10319 is fixed
skip_numba_0_63 = pytest.mark.skipif(
Version(version=version("numba")) >= Version("0.63b0"), reason="numba 0.63 bug"
)
5 changes: 1 addition & 4 deletions tests/test_highly_variable_genes.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
from scanpy._compat import CSRBase
from testing.scanpy._helpers import _check_check_values_warnings
from testing.scanpy._helpers.data import pbmc3k, pbmc68k_reduced
from testing.scanpy._pytest.marks import needs, skip_numba_0_63
from testing.scanpy._pytest.marks import needs
from testing.scanpy._pytest.params import ARRAY_TYPES

if TYPE_CHECKING:
Expand Down Expand Up @@ -165,7 +165,6 @@ def _check_pearson_hvg_columns(output_df: pd.DataFrame, n_top_genes: int):
assert np.nanmax(output_df["highly_variable_rank"].to_numpy()) <= n_top_genes - 1


@skip_numba_0_63
def test_pearson_residuals_inputchecks(
pbmc3k_parametrized_small: Callable[[], AnnData],
) -> None:
Expand Down Expand Up @@ -202,7 +201,6 @@ def test_pearson_residuals_inputchecks(
)


@skip_numba_0_63
@pytest.mark.parametrize("subset", [True, False], ids=["subset", "full"])
@pytest.mark.parametrize(
"clip", [None, np.inf, 30], ids=["noclip", "infclip", "30clip"]
Expand Down Expand Up @@ -296,7 +294,6 @@ def test_pearson_residuals_general(
_check_pearson_hvg_columns(output_df, n_top_genes)


@skip_numba_0_63
@pytest.mark.parametrize("subset", [True, False], ids=["subset", "full"])
@pytest.mark.parametrize("n_top_genes", [100, 200], ids=["100n", "200n"])
def test_pearson_residuals_batch(
Expand Down
14 changes: 2 additions & 12 deletions tests/test_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,11 @@
import pytest
import threadpoolctl
from anndata import AnnData
from packaging.version import Version
from scipy import sparse

import scanpy as sc
from scanpy._compat import pkg_version
from scanpy.metrics import modularity
from testing.scanpy._helpers.data import pbmc3k, pbmc68k_reduced
from testing.scanpy._pytest.context import xfail
from testing.scanpy._pytest.marks import needs
from testing.scanpy._pytest.params import ARRAY_TYPES

Expand Down Expand Up @@ -336,15 +333,8 @@ def test_modularity_adata(
with subtests.test("bounds", score=name):
assert 0 <= s <= 1
for (n0, s0), (n1, s1) in combinations(scores.items(), 2):
approx = {n0, n1} != {"update", "calculate"}
with (
subtests.test("equality", l=n0, r=n1),
xfail(
approx and pkg_version("igraph") < Version("1"),
reason="igraph 0.x has different modularity behavior",
),
):
assert pytest.approx(s0, rel=1e-3 if approx else 1e-6) == s1
with subtests.test("equality", l=n0, r=n1):
assert pytest.approx(s0, rel=1e-6) == s1
with subtests.test("update"):
assert adata.uns["leiden"]["modularity"] is scores["update"]

Expand Down
11 changes: 1 addition & 10 deletions tests/test_neighbors.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,19 @@
import numpy as np
import pytest
from anndata import AnnData
from packaging.version import Version
from scipy import sparse
from sklearn.neighbors import KNeighborsTransformer

import scanpy as sc
from scanpy import Neighbors
from scanpy._compat import CSBase, pkg_version
from scanpy._compat import CSBase
from testing.scanpy._helpers.data import pbmc68k_reduced

if TYPE_CHECKING:
from typing import Literal

from pytest_mock import MockerFixture

# https://github.com/lmcinnes/umap/issues/1216
XFAIL_IF_UMAP_BROKEN = pytest.mark.xfail(
pkg_version("umap-learn") < Version("0.6a0.dev0")
and pkg_version("numba") >= Version("0.62.0rc1"),
reason="umap<0.6 is broken with numba≥0.62.0rc1",
)

# the input data
X = [[1, 0], [3, 0], [5, 6], [0, 4]]
n_neighbors = 3 # includes data points themselves
Expand Down Expand Up @@ -207,7 +199,6 @@ def test_distances_all(neigh: Neighbors, transformer, knn):
connectivities_umap,
transitions_umap,
transitions_sym_umap,
marks=XFAIL_IF_UMAP_BROKEN,
id="umap",
),
pytest.param(
Expand Down
3 changes: 0 additions & 3 deletions tests/test_normalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
check_rep_mutation,
check_rep_results,
)
from testing.scanpy._pytest.marks import skip_numba_0_63

# TODO: Add support for sparse-in-dask
from testing.scanpy._pytest.params import ARRAY_TYPES, ARRAY_TYPES_DENSE
Expand Down Expand Up @@ -210,7 +209,6 @@ def _check_pearson_pca_fields(ad, n_cells, n_comps):
), "Wrong shape of PCA output in `X_pca`"


@skip_numba_0_63
@pytest.mark.parametrize("n_hvgs", [100, 200])
@pytest.mark.parametrize("n_comps", [30, 50])
@pytest.mark.parametrize(
Expand Down Expand Up @@ -283,7 +281,6 @@ def test_normalize_pearson_residuals_pca(
np.testing.assert_array_equal(adata.obsm["X_pca"], adata_pca.obsm["X_pca"])


@skip_numba_0_63
@pytest.mark.parametrize("n_hvgs", [100, 200])
@pytest.mark.parametrize("n_comps", [30, 50])
def test_normalize_pearson_residuals_recipe(
Expand Down
Loading