File size: 12,238 Bytes
39dbaed | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 | """Functions for splitting a network into two communities (finding a bipartition)."""
import random
from copy import deepcopy
from itertools import count
import networkx as nx
__all__ = [
"kernighan_lin_bisection",
"spectral_modularity_bipartition",
"greedy_node_swap_bipartition",
]
def _kernighan_lin_sweep(edge_info, side):
"""
This is a modified form of Kernighan-Lin, which moves single nodes at a
time, alternating between sides to keep the bisection balanced. We keep
two min-heaps of swap costs to make optimal-next-move selection fast.
"""
heap0, heap1 = cost_heaps = nx.utils.BinaryHeap(), nx.utils.BinaryHeap()
# we use heap methods insert, pop, and get
for u, nbrs in edge_info.items():
cost_u = sum(wt if side[v] else -wt for v, wt in nbrs.items())
if side[u]:
heap1.insert(u, cost_u)
else:
heap0.insert(u, -cost_u)
def _update_heap_values(node):
side_node = side[node]
for nbr, wt in edge_info[node].items():
side_nbr = side[nbr]
if side_nbr == side_node:
wt = -wt
heap_nbr = cost_heaps[side_nbr]
if nbr in heap_nbr:
cost_nbr = heap_nbr.get(nbr) + 2 * wt
# allow_increase lets us update a value already on the heap
heap_nbr.insert(nbr, cost_nbr, allow_increase=True)
i = 0
totcost = 0
while heap0 and heap1:
u, cost_u = heap0.pop()
_update_heap_values(u)
v, cost_v = heap1.pop()
_update_heap_values(v)
totcost += cost_u + cost_v
i += 1
yield totcost, i, (u, v)
@nx.utils.not_implemented_for("directed")
@nx.utils.py_random_state(4)
@nx._dispatchable(edge_attrs="weight")
def kernighan_lin_bisection(G, partition=None, max_iter=10, weight="weight", seed=None):
"""Partition a graph into two blocks using the Kernighan–Lin algorithm.
This algorithm partitions a network into two sets by iteratively
swapping pairs of nodes to reduce the edge cut between the two sets. The
pairs are chosen according to a modified form of Kernighan-Lin [1]_, which
moves nodes individually, alternating between sides to keep the bisection
balanced.
Kernighan-Lin is an approximate algorithm for maximal modularity bisection.
In [2]_ they suggest that fine-tuned improvements can be made using
greedy node swapping, (see `greedy_node_swap_bipartition`).
The improvements are typically only a few percent of the modularity value.
But they claim that can make a difference between a good and excellent method.
This function does not perform any improvements. But you can do that yourself.
Parameters
----------
G : NetworkX graph
Graph must be undirected.
partition : tuple
Pair of iterables containing an initial partition. If not
specified, a random balanced partition is used.
max_iter : int
Maximum number of times to attempt swaps to find an
improvement before giving up.
weight : string or function (default: "weight")
If this is a string, then edge weights will be accessed via the
edge attribute with this key (that is, the weight of the edge
joining `u` to `v` will be ``G.edges[u, v][weight]``). If no
such edge attribute exists, the weight of the edge is assumed to
be one.
If this is a function, the weight of an edge is the value
returned by the function. The function must accept exactly three
positional arguments: the two endpoints of an edge and the
dictionary of edge attributes for that edge. The function must
return a number or None to indicate a hidden edge.
seed : integer, random_state, or None (default)
Indicator of random number generation state.
See :ref:`Randomness<randomness>`.
Only used if partition is None
Returns
-------
partition : tuple
A pair of sets of nodes representing the bipartition.
Raises
------
NetworkXError
If `partition` is not a valid partition of the nodes of the graph.
References
----------
.. [1] Kernighan, B. W.; Lin, Shen (1970).
"An efficient heuristic procedure for partitioning graphs."
*Bell Systems Technical Journal* 49: 291--307.
Oxford University Press 2011.
.. [2] M. E. J. Newman,
"Modularity and community structure in networks",
PNAS, 103 (23), p. 8577-8582,
https://doi.org/10.1073/pnas.0601602103
"""
nodes = list(G)
if partition is None:
seed.shuffle(nodes)
mid = len(nodes) // 2
A, B = nodes[:mid], nodes[mid:]
else:
try:
A, B = partition
except (TypeError, ValueError) as err:
raise nx.NetworkXError("partition must be two sets") from err
if not nx.community.is_partition(G, [A, B]):
raise nx.NetworkXError("partition invalid")
side = {node: (node in A) for node in nodes}
# ruff: noqa: E731 skips check for no lambda functions
# Using shortest_paths _weight_function with sum instead of min on multiedges
if callable(weight):
sum_weight = weight
elif G.is_multigraph():
sum_weight = lambda u, v, d: sum(dd.get(weight, 1) for dd in d.values())
else:
sum_weight = lambda u, v, d: d.get(weight, 1)
edge_info = {
u: {v: wt for v, d in nbrs.items() if (wt := sum_weight(u, v, d)) is not None}
for u, nbrs in G._adj.items()
}
for i in range(max_iter):
costs = list(_kernighan_lin_sweep(edge_info, side))
# find out how many edges to update: min_i
min_cost, min_i, _ = min(costs)
if min_cost >= 0:
break
for _, _, (u, v) in costs[:min_i]:
side[u] = 1
side[v] = 0
part1 = {u for u, s in side.items() if s == 0}
part2 = {u for u, s in side.items() if s == 1}
return part1, part2
@nx.utils.not_implemented_for("directed")
@nx.utils.not_implemented_for("multigraph")
def spectral_modularity_bipartition(G):
r"""Return a bipartition of the nodes based on the spectrum of the
modularity matrix of the graph.
This method calculates the eigenvector associated with the second
largest eigenvalue of the modularity matrix, where the modularity
matrix *B* is defined by
..math::
B_{i j} = A_{i j} - \frac{k_i k_j}{2 m},
where *A* is the adjacency matrix, `k_i` is the degree of node *i*,
and *m* is the number of edges in the graph. Nodes whose
corresponding values in the eigenvector are negative are placed in
one block, nodes whose values are nonnegative are placed in another
block.
Parameters
----------
G : NetworkX graph
Returns
-------
C : tuple
Pair of communities as two sets of nodes of ``G``, partitioned
according to second largest eigenvalue of the modularity matrix.
Examples
--------
>>> G = nx.karate_club_graph()
>>> MrHi, Officer = nx.community.spectral_modularity_bipartition(G)
>>> MrHi, Officer = sorted([sorted(MrHi), sorted(Officer)])
>>> MrHi
[0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 16, 17, 19, 21]
>>> Officer
[8, 9, 14, 15, 18, 20, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]
References
----------
.. [1] M. E. J. Newman *Networks: An Introduction*, pages 373--378
Oxford University Press 2011.
.. [2] M. E. J. Newman,
"Modularity and community structure in networks",
PNAS, 103 (23), p. 8577-8582,
https://doi.org/10.1073/pnas.0601602103
"""
import numpy as np
B = nx.linalg.modularity_matrix(G)
eigenvalues, eigenvectors = np.linalg.eig(B)
index = np.argsort(eigenvalues)[-1]
v2 = zip(np.real(eigenvectors[:, index]), G)
left, right = set(), set()
for u, n in v2:
if u < 0:
left.add(n)
else:
right.add(n)
return left, right
@nx.utils.not_implemented_for("multigraph")
def greedy_node_swap_bipartition(G, *, init_split=None, max_iter=10):
"""Split the nodes into two communities based on greedy
modularity maximization.
The algorithm works by selecting a node to change communities which
will maximize the modularity. The swap is made and the community
structure with the highest modularity is kept.
Parameters
----------
G : NetworkX graph
init_split : 2-tuple of sets of nodes
Pair of sets of nodes in ``G`` providing an initial bipartition
for the algorithm. If not specified, a random balanced partition
is used. If this pair of sets is not a partition of the nodes of `G`,
:exc:`NetworkXException` is raised.
max_iter : int
Maximum number of iterations of attempting swaps to find an improvement.
Returns
-------
max_split : 2-tuple of sets of nodes
Pair of sets of nodes of ``G``, partitioned according to a
node swap greedy modularity maximization algorithm.
Raises
------
NetworkXError
if init_split is not a valid partition of the
graph into two communities or if G is a MultiGraph
Examples
--------
>>> G = nx.barbell_graph(3, 0)
>>> left, right = nx.community.greedy_node_swap_bipartition(G)
>>> # Sort the communities so the nodes appear in increasing order.
>>> left, right = sorted([sorted(left), sorted(right)])
>>> sorted(left)
[0, 1, 2]
>>> sorted(right)
[3, 4, 5]
Notes
-----
This function is not implemented for multigraphs.
References
----------
.. [1] M. E. J. Newman "Networks: An Introduction", pages 373--375.
Oxford University Press 2011.
"""
if init_split is None:
m1 = len(G) // 2
m2 = len(G) - m1
some_nodes = set(random.sample(list(G), m1))
other_nodes = {n for n in G if n not in some_nodes}
best_split_so_far = (some_nodes, other_nodes)
else:
if not nx.community.is_partition(G, init_split):
raise nx.NetworkXError("init_split is not a partition of G")
if not len(init_split) == 2:
raise nx.NetworkXError("init_split must be a bipartition")
best_split_so_far = deepcopy(init_split)
best_mod = nx.community.modularity(G, best_split_so_far)
max_split, max_mod = best_split_so_far, best_mod
its = 0
m = G.number_of_edges()
G_degree = dict(G.degree)
while max_mod >= best_mod and its < max_iter:
best_split_so_far = max_split
best_mod = max_mod
next_split = deepcopy(max_split)
next_mod = max_mod
nodes = set(G)
while nodes:
max_swap = -1
max_node = None
max_node_comm = None
left, right = next_split
leftd = sum(G_degree[n] for n in left)
rightd = sum(G_degree[n] for n in right)
for n in nodes:
if n in left:
in_comm, out_comm = left, right
in_deg, out_deg = leftd, rightd
else:
in_comm, out_comm = right, left
in_deg, out_deg = rightd, leftd
d_eii = -len(G[n].keys() & in_comm) / m
d_ejj = len(G[n].keys() & out_comm) / m
deg = G_degree[n]
d_sum_ai = (deg / (2 * m**2)) * (in_deg - out_deg - deg)
swap_change = d_eii + d_ejj + d_sum_ai
if swap_change > max_swap:
max_swap = swap_change
max_node = n
max_node_comm = in_comm
non_max_node_comm = out_comm
# swap the node from one comm to the other
max_node_comm.remove(max_node)
non_max_node_comm.add(max_node)
next_mod += max_swap
# deepcopy next_split each time it reaches a high (might go lower later)
if next_mod > max_mod:
max_split, max_mod = deepcopy(next_split), next_mod
nodes.remove(max_node)
its += 1
return best_split_so_far
|