mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 23:49:14 +00:00
With exclude_hubs_percentile set, excluded hubs are removed from the
graph that gets partitioned. A node whose only neighbours are hubs (e.g.
a local symbol of a hub file, or a dependency entry of a manifest) is
then isolated in that subgraph and came back as a singleton community,
while the hub itself was reattached by majority vote. Every such node
ended up cut off from the only node it connects to, and the community
count inflated with singletons.
Such nodes are now held out of the partition and placed after the hubs
are reattached, with the same majority vote the hubs use; since all of
their neighbours are hubs and every hub is placed by then, the vote
always has a winner. A self-loop does not count as a neighbour. True
isolates, and nodes whose only edge is a self-loop, are unchanged.
Follow-up to #919, where --exclude-hubs was introduced. The cluster()
docstring now says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 39a24a0626)
76 lines
2.9 KiB
Python
76 lines
2.9 KiB
Python
"""cluster() under exclude_hubs_percentile keeps hub-only neighbours with their hub.
|
|
|
|
Excluded hubs are removed from the subgraph that gets partitioned, so a node
|
|
whose only neighbours are hubs is isolated there. It used to come back as a
|
|
singleton community while the hub itself was reattached by majority vote.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import networkx as nx
|
|
|
|
from graphify.cluster import cluster
|
|
|
|
|
|
def _hub_with_private_leaves():
|
|
"""Two 4-cliques, one hub H joined to all of A and to b1, and three
|
|
leaves that are connected to H only. Degrees: H=8, A=4, b1=4, B=3,
|
|
leaves=1, so exclude_hubs_percentile=90 excludes exactly H."""
|
|
G = nx.Graph()
|
|
A = ["a1", "a2", "a3", "a4"]
|
|
B = ["b1", "b2", "b3", "b4"]
|
|
for group in (A, B):
|
|
for i, u in enumerate(group):
|
|
for v in group[i + 1:]:
|
|
G.add_edge(u, v)
|
|
for a in A:
|
|
G.add_edge("H", a)
|
|
G.add_edge("H", "b1")
|
|
for leaf in ("l1", "l2", "l3"):
|
|
G.add_edge("H", leaf)
|
|
return G
|
|
|
|
|
|
def test_exclude_hubs_keeps_hub_only_neighbours_with_their_hub():
|
|
"""A node whose only neighbours are excluded hubs has degree 0 in the
|
|
partitioned subgraph. It used to come back as a singleton community while
|
|
the hub itself was reattached elsewhere, so every such node was cut off
|
|
from the only node it connects to."""
|
|
G = _hub_with_private_leaves()
|
|
communities = cluster(G, exclude_hubs_percentile=90)
|
|
node_to_cid = {n: cid for cid, nodes in communities.items() for n in nodes}
|
|
assert set(node_to_cid) == set(G.nodes())
|
|
for leaf in ("l1", "l2", "l3"):
|
|
assert node_to_cid[leaf] == node_to_cid["H"]
|
|
assert not [nodes for nodes in communities.values() if len(nodes) == 1]
|
|
|
|
|
|
def test_exclude_hubs_leaves_true_isolates_alone():
|
|
G = _hub_with_private_leaves()
|
|
G.add_node("lonely")
|
|
communities = cluster(G, exclude_hubs_percentile=90)
|
|
assert ["lonely"] in communities.values()
|
|
|
|
|
|
def test_exclude_hubs_without_hubs_is_unchanged():
|
|
G = _hub_with_private_leaves()
|
|
assert cluster(G, exclude_hubs_percentile=100) == cluster(G)
|
|
|
|
|
|
def test_exclude_hubs_hub_only_neighbour_with_self_loop_joins_its_hub():
|
|
"""A self-loop is not a real neighbour: a node whose other neighbours are
|
|
all hubs is stranded just the same."""
|
|
G = _hub_with_private_leaves()
|
|
G.add_edge("l1", "l1")
|
|
communities = cluster(G, exclude_hubs_percentile=90)
|
|
node_to_cid = {n: cid for cid, nodes in communities.items() for n in nodes}
|
|
assert node_to_cid["l1"] == node_to_cid["H"]
|
|
|
|
|
|
def test_exclude_hubs_self_loop_only_node_is_left_alone():
|
|
"""A node whose only edge is a self-loop has no hub to join; it keeps
|
|
the behaviour it has without the exclusion (its own community)."""
|
|
G = _hub_with_private_leaves()
|
|
G.add_edge("solo", "solo")
|
|
communities = cluster(G, exclude_hubs_percentile=90)
|
|
assert ["solo"] in communities.values()
|