Please make sure these conditions are met
What happened?
The igraph modularity recorded by sc.tl.leiden and the one computed by sc.metrics.modularity differ, when the reasonable expectation is for them to be equal.
This is because the graphs over which they are computed are not the same. When the graph is constructed during sc.tl.leiden, connectivities are passed to sc._utils.get_igraph_from_adjacency, resulting in a graph in which each pair of connected vertices is connected by two edges (possibly directed when flavor is not igraph). In sc.metrics.modularity, on the other hand, connectivities are passed to ig.Graph.Weighted_Adjacency, and the in resulting graph, each pair of connected vertices is connected by a single edge.
Minimal code sample
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "scanpy@git+https://github.com/scverse/scanpy.git@main",
# "igraph",
# ]
# ///
#
# This script automatically imports the development branch of scanpy to check for issues
import igraph as ig
import scanpy as sc
adata = sc.datasets.pbmc3k_processed()
sc.tl.leiden(adata, flavor="igraph", n_iterations=10, directed=False, random_state=1234)
modularity_recorded = adata.uns["leiden"]["modularity"]
modularity_metrics = sc.metrics.modularity(adata, labels="leiden", mode="calculate")
# graph creation equivalent to sc.metrics.modularity
graph_1 = ig.Graph.Weighted_Adjacency(adata.obsp["connectivities"], mode=ig.ADJ_UNDIRECTED)
modularity_1 = graph_1.modularity(adata.obs["leiden"].array.codes, "weight")
# graph creation equivalent to sc.tl.leiden
graph_2 = sc._utils.get_igraph_from_adjacency(adata.obsp["connectivities"], directed=False)
modularity_2 = graph_2.modularity(adata.obs["leiden"].array.codes, "weight")
# creation methods agree for directed graphs
graph_3 = ig.Graph.Weighted_Adjacency(adata.obsp["connectivities"], mode=ig.ADJ_DIRECTED)
graph_4 = sc._utils.get_igraph_from_adjacency(adata.obsp["connectivities"], directed=True)
# tests
assert graph_3.isomorphic(graph_4)
assert not graph_2.isomorphic(graph_1) # passes, should fail
assert graph_2.subisomorphic_vf2(graph_1)
assert graph_2.ecount() == 2 * graph_1.ecount() # passes, should fail
assert modularity_1 == modularity_metrics
assert modularity_2 == modularity_recorded
assert modularity_metrics == modularity_recorded, "modularities unequal" # fails
Error output
AssertionError: modularities unequal
Versions
Details
| Package | Version |
| ------- | ------- |
| igraph | 1.0.0 |
| scanpy | 1.12.1 |
| anndata | 0.12.11 |
| Dependency | Version |
| ----------------- | ------------ |
| parso | 0.8.7 |
| numpy | 2.4.3 |
| asttokens | 3.0.1 |
| jedi | 0.19.2 |
| cycler | 0.12.1 |
| google-crc32c | 1.8.0 |
| threadpoolctl | 3.6.0 |
| traitlets | 5.14.3 |
| fast-array-utils | 1.4.1 |
| prompt_toolkit | 3.0.52 |
| jupyter_client | 8.8.0 |
| python-dateutil | 2.9.0.post0 |
| Pygments | 2.20.0 |
| kiwisolver | 1.5.0 |
| packaging | 26.2 |
| executing | 2.2.1 |
| ipykernel | 7.2.0 |
| donfig | 0.8.1.post1 |
| debugpy | 1.8.20 |
| platformdirs | 4.9.6 |
| h5py | 3.16.0 |
| six | 1.17.0 |
| pure_eval | 0.2.3 |
| PyYAML | 6.0.3 |
| ipython | 9.13.0 |
| pyparsing | 3.3.2 |
| decorator | 5.2.1 |
| scipy | 1.17.1 |
| scikit-learn | 1.8.0 |
| tornado | 6.5.5 |
| psutil | 7.2.2 |
| zarr | 3.2.0 |
| comm | 0.2.3 |
| numcodecs | 0.16.5 |
| llvmlite | 0.47.0 |
| typing_extensions | 4.15.0 |
| pillow | 12.2.0 |
| natsort | 8.4.0 |
| stack_data | 0.6.3 |
| pyzmq | 27.1.0 |
| setuptools | 82.0.1 |
| session-info2 | 0.4.1 |
| wcwidth | 0.7.0 |
| pytz | 2026.1.post1 |
| msgpack | 1.1.2 |
| legacy-api-wrap | 1.5 |
| numba | 0.65.1 |
| matplotlib | 3.10.9 |
| scverse-misc | 0.0.5 |
| appnope | 0.1.4 |
| joblib | 1.5.3 |
| pandas | 2.3.3 |
| texttable | 1.7.0 |
| jupyter_core | 5.9.1 |
| Component | Info |
| --------- | --------------------------------------------------------------------------------- |
| Python | 3.13.13 | packaged by conda-forge | (main, Apr 8 2026, 02:29:07) [Clang 19.1.7 ] |
| OS | macOS-26.4.1-arm64-arm-64bit-Mach-O |
| CPU | 10/10 logical CPU cores, arm |
| GPU | No GPU found |
| Updated | 2026-05-03 15:31 |
Please make sure these conditions are met
What happened?
The
igraphmodularity recorded bysc.tl.leidenand the one computed bysc.metrics.modularitydiffer, when the reasonable expectation is for them to be equal.This is because the graphs over which they are computed are not the same. When the graph is constructed during
sc.tl.leiden, connectivities are passed tosc._utils.get_igraph_from_adjacency, resulting in a graph in which each pair of connected vertices is connected by two edges (possibly directed whenflavoris notigraph). Insc.metrics.modularity, on the other hand, connectivities are passed toig.Graph.Weighted_Adjacency, and the in resulting graph, each pair of connected vertices is connected by a single edge.Minimal code sample
Error output
Versions
Details