Skip to content

Fix Leiden multi-pair null-model normalisation to match Louvain (#111)#115

Open
arnaudon wants to merge 1 commit into
masterfrom
claude/issue-111-mHXID
Open

Fix Leiden multi-pair null-model normalisation to match Louvain (#111)#115
arnaudon wants to merge 1 commit into
masterfrom
claude/issue-111-mHXID

Conversation

@arnaudon

@arnaudon arnaudon commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Fixes #111.

Problem

For a null model with more than one pair (n_null = len(null_model)/2 >= 2), the Leiden and Louvain backends optimised different objectives:

  • Leiden: edges − (1/n_null)·Σₖ pairₖ
  • Louvain: edges − Σₖ pairₖ

These agree only for n_null = 1. For n_null >= 2 they differ by a partition-dependent term, so the two backends could in principle disagree on the argmax. Affected constructors: signed_modularity (always 2 pairs) and linearized_directed with α < 1 (after #110).

Fix (Solution 1 from the issue)

Scale node_sizes by √n_null before constructing CPMVertexPartition, in both _optimise and _evaluate_quality. The CPM null term is quadratic in node_sizes, so each layer then contributes n_null·pairₖ; after Leiden's 1/n_null averaging this yields Σₖ pairₖ, matching Louvain.

Prerequisite bug fix in constructor_signed_modularity

While verifying on signed_modularity (the issue's motivating reproducer) I found the Leiden path was completely broken there: adj_pos.sum(1).flatten() on the sparse matrix returns a (1, n) np.matrix row, producing a (4, 1, n) null model that crashed Leiden with "Node size vector not the same size as the number of nodes." Wrapping in np.asarray(...) — as the other constructors already do — yields the correct (4, n) shape.

The underlying float buffer is byte-identical and shape[0] is unchanged, so the Louvain path is unaffected. The signed_modularity fixtures are regenerated to drop the spurious extra dimension (values unchanged).

Tests

  • Added test__evaluate_quality_leiden_multi_pair_normalisation, which isolates the null-model contribution (Q(nm1) − Q(nm2) with two identical symmetric pairs) and verifies the Leiden and Louvain objectives agree for n_null = 2. Confirmed it fails without the fix.
  • Full suite passes (22 tests).

Out of scope

A separate, deeper limitation remains (acknowledged in the issue): Leiden's CPM uses a single node_sizes vector per layer (nᵢ·nⱼ), so it only exactly represents symmetric null pairs. signed_modularity's pairs are asymmetric, so absolute Leiden/Louvain values still differ there by a partition-dependent term. This PR fixes only the 1/n_null normalisation factor that #111 targets.

https://claude.ai/code/session_01JKbhASQT3f4MRAJbTfkCph


Generated by Claude Code

For a null model with more than one pair (n_null >= 2), the Leiden
backend optimised edges - (1/n_null) * sum_k pair_k while the Louvain
backend optimised edges - sum_k pair_k. These agree only for n_null = 1,
so the two backends could in principle disagree on the argmax for
multi-pair constructors (signed_modularity, and linearized_directed with
alpha < 1).

Scale node_sizes by sqrt(n_null) before constructing CPMVertexPartition
in both _optimise and _evaluate_quality. The CPM null term is quadratic
in node_sizes, so each layer then contributes n_null * pair_k; after
Leiden's 1/n_null averaging this yields sum_k pair_k, matching Louvain.

Also fix a shape bug in constructor_signed_modularity: the sparse
.sum(1).flatten() returned a (1, n) np.matrix row, producing a
(4, 1, n) null model that crashed the Leiden path ("Node size vector
not the same size as the number of nodes"). Wrapping in np.asarray
yields the expected (4, n) shape. The underlying float buffer is
byte-identical and shape[0] is unchanged, so the Louvain path is
unaffected; the signed_modularity fixtures are regenerated to drop the
spurious extra dimension (values unchanged).

Add a regression test isolating the null-model contribution that
verifies the Leiden and Louvain objectives now agree for n_null = 2.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Leiden vs Louvain disagree on multi-pair null model normalisation

2 participants