Restore 0.1.5 version from stash
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
from .betweenness import *
|
||||
from .betweenness_subset import *
|
||||
from .closeness import *
|
||||
from .current_flow_betweenness import *
|
||||
from .current_flow_betweenness_subset import *
|
||||
from .current_flow_closeness import *
|
||||
from .degree_alg import *
|
||||
from .dispersion import *
|
||||
from .eigenvector import *
|
||||
from .group import *
|
||||
from .harmonic import *
|
||||
from .katz import *
|
||||
from .load import *
|
||||
from .percolation import *
|
||||
from .reaching import *
|
||||
from .second_order import *
|
||||
from .subgraph_alg import *
|
||||
from .trophic import *
|
||||
from .voterank_alg import *
|
||||
from .laplacian import *
|
||||
+591
@@ -0,0 +1,591 @@
|
||||
"""Betweenness centrality measures."""
|
||||
|
||||
import math
|
||||
from collections import deque
|
||||
from heapq import heappop, heappush
|
||||
from itertools import count
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.shortest_paths.weighted import _weight_function
|
||||
from networkx.utils import py_random_state
|
||||
from networkx.utils.decorators import not_implemented_for
|
||||
|
||||
__all__ = ["betweenness_centrality", "edge_betweenness_centrality"]
|
||||
|
||||
|
||||
@py_random_state("seed")
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def betweenness_centrality(
|
||||
G, k=None, normalized=True, weight=None, endpoints=False, seed=None
|
||||
):
|
||||
r"""Compute the shortest-path betweenness centrality for nodes.
|
||||
|
||||
Betweenness centrality of a node $v$ is the sum of the
|
||||
fraction of all-pairs shortest paths that pass through $v$.
|
||||
|
||||
.. math::
|
||||
|
||||
c_B(v) = \sum_{s, t \in V} \frac{\sigma(s, t | v)}{\sigma(s, t)}
|
||||
|
||||
where $V$ is the set of nodes, $\sigma(s, t)$ is the number of
|
||||
shortest $(s, t)$-paths, and $\sigma(s, t | v)$ is the number of
|
||||
those paths passing through some node $v$ other than $s$ and $t$.
|
||||
If $s = t$, $\sigma(s, t) = 1$, and if $v \in \{s, t\}$,
|
||||
$\sigma(s, t | v) = 0$ [2]_.
|
||||
The denominator $\sigma(s, t)$ is a normalization factor that can be
|
||||
turned off to get the raw path counts.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph.
|
||||
|
||||
k : int, optional (default=None)
|
||||
If `k` is not `None`, use `k` sampled nodes as sources for the considered paths.
|
||||
The resulting sampled counts are then inflated to approximate betweenness.
|
||||
Higher values of `k` give better approximation. Must have ``k <= len(G)``.
|
||||
|
||||
normalized : bool, optional (default=True)
|
||||
If `True`, the betweenness values are rescaled by dividing by the number of
|
||||
possible $(s, t)$-pairs in the graph.
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
If `None`, all edge weights are 1.
|
||||
Otherwise holds the name of the edge attribute used as weight.
|
||||
Weights are used to calculate weighted shortest paths, so they are
|
||||
interpreted as distances.
|
||||
|
||||
endpoints : bool, optional (default=False)
|
||||
If `True`, include the endpoints $s$ and $t$ in the shortest path counts.
|
||||
This is taken into account when rescaling the values.
|
||||
|
||||
seed : integer, random_state, or None (default)
|
||||
Indicator of random number generation state.
|
||||
See :ref:`Randomness<randomness>`.
|
||||
Note that this is only used if ``k is not None``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dict
|
||||
Dictionary of nodes with betweenness centrality as the value.
|
||||
|
||||
See Also
|
||||
--------
|
||||
betweenness_centrality_subset
|
||||
edge_betweenness_centrality
|
||||
load_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
The algorithm is from Ulrik Brandes [1]_.
|
||||
See [4]_ for the original first published version and [2]_ for details on
|
||||
algorithms for variations and related metrics.
|
||||
|
||||
For approximate betweenness calculations, set `k` to the number of sampled
|
||||
nodes ("pivots") used as sources to estimate the betweenness values.
|
||||
The formula then sums over $s$ is in these pivots, instead of over all nodes.
|
||||
The resulting sum is then inflated to approximate the full sum.
|
||||
For a discussion of how to choose `k` for efficiency, see [3]_.
|
||||
|
||||
For weighted graphs the edge weights must be greater than zero.
|
||||
Zero edge weights can produce an infinite number of equal length
|
||||
paths between pairs of nodes.
|
||||
|
||||
Directed graphs and undirected graphs count paths differently.
|
||||
In directed graphs, each pair of source-target nodes is considered separately
|
||||
in each direction, as the shortest paths can differ by direction.
|
||||
However, in undirected graphs, each pair of nodes is considered only once,
|
||||
as the shortest paths are symmetric.
|
||||
This means the normalization factor to divide by is $N(N-1)$ for directed graphs
|
||||
and $N(N-1)/2$ for undirected graphs, where $N = n$ (the number of nodes)
|
||||
if endpoints are included and $N = n-1$ otherwise.
|
||||
|
||||
This algorithm is not guaranteed to be correct if edge weights
|
||||
are floating point numbers. As a workaround you can use integer
|
||||
numbers by multiplying the relevant edge attributes by a convenient
|
||||
constant factor (e.g. 100) and converting to integers.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Ulrik Brandes:
|
||||
A Faster Algorithm for Betweenness Centrality.
|
||||
Journal of Mathematical Sociology 25(2):163--177, 2001.
|
||||
https://doi.org/10.1080/0022250X.2001.9990249
|
||||
.. [2] Ulrik Brandes:
|
||||
On Variants of Shortest-Path Betweenness
|
||||
Centrality and their Generic Computation.
|
||||
Social Networks 30(2):136--145, 2008.
|
||||
https://doi.org/10.1016/j.socnet.2007.11.001
|
||||
.. [3] Ulrik Brandes and Christian Pich:
|
||||
Centrality Estimation in Large Networks.
|
||||
International Journal of Bifurcation and Chaos 17(7):2303--2318, 2007.
|
||||
https://dx.doi.org/10.1142/S0218127407018403
|
||||
.. [4] Linton C. Freeman:
|
||||
A set of measures of centrality based on betweenness.
|
||||
Sociometry 40: 35--41, 1977
|
||||
https://doi.org/10.2307/3033543
|
||||
|
||||
Examples
|
||||
--------
|
||||
Consider an undirected 3-path. Each pair of nodes has exactly one shortest
|
||||
path between them. Since the graph is undirected, only ordered pairs are counted.
|
||||
Of these (and when `endpoints` is `False`), none of the shortest paths pass
|
||||
through 0 and 2, and only the shortest path between 0 and 2 passes through 1.
|
||||
As such, the counts should be ``{0: 0, 1: 1, 2: 0}``.
|
||||
|
||||
>>> G = nx.path_graph(3)
|
||||
>>> nx.betweenness_centrality(G, normalized=False, endpoints=False)
|
||||
{0: 0.0, 1: 1.0, 2: 0.0}
|
||||
|
||||
If `endpoints` is `True`, we also need to count endpoints as being on the path:
|
||||
$\sigma(s, t | s) = \sigma(s, t | t) = \sigma(s, t)$.
|
||||
In our example, 0 is then part of two shortest paths (0 to 1 and 0 to 2);
|
||||
similarly, 2 is part of two shortest paths (0 to 2 and 1 to 2).
|
||||
1 is part of all three shortest paths. This makes the new raw
|
||||
counts ``{0: 2, 1: 3, 2: 2}``.
|
||||
|
||||
>>> nx.betweenness_centrality(G, normalized=False, endpoints=True)
|
||||
{0: 2.0, 1: 3.0, 2: 2.0}
|
||||
|
||||
With normalization, the values are divided by the number of ordered $(s, t)$-pairs.
|
||||
If we are not counting endpoints, there are $n - 1$ possible choices for $s$
|
||||
(all except the node we are computing betweenness centrality for), which in turn
|
||||
leaves $n - 2$ possible choices for $t$ as $s \ne t$.
|
||||
The total number of ordered pairs when `endpoints` is `False` is $(n - 1)(n - 2)/2 = 1$.
|
||||
If `endpoints` is `True`, there are $n(n - 1)/2 = 3$ ordered $(s, t)$-pairs to divide by.
|
||||
|
||||
>>> nx.betweenness_centrality(G, normalized=True, endpoints=False)
|
||||
{0: 0.0, 1: 1.0, 2: 0.0}
|
||||
>>> nx.betweenness_centrality(G, normalized=True, endpoints=True)
|
||||
{0: 0.6666666666666666, 1: 1.0, 2: 0.6666666666666666}
|
||||
|
||||
If the graph is directed instead, we now need to consider $(s, t)$-pairs
|
||||
in both directions. Our example becomes a directed 3-path.
|
||||
Without counting endpoints, we only have one path through 1 (0 to 2).
|
||||
This means the raw counts are ``{0: 0, 1: 1, 2: 0}``.
|
||||
|
||||
>>> DG = nx.path_graph(3, create_using=nx.DiGraph)
|
||||
>>> nx.betweenness_centrality(DG, normalized=False, endpoints=False)
|
||||
{0: 0.0, 1: 1.0, 2: 0.0}
|
||||
|
||||
If we do include endpoints, the raw counts are ``{0: 2, 1: 3, 2: 2}``.
|
||||
|
||||
>>> nx.betweenness_centrality(DG, normalized=False, endpoints=True)
|
||||
{0: 2.0, 1: 3.0, 2: 2.0}
|
||||
|
||||
If we want to normalize directed betweenness centrality, the raw counts
|
||||
are normalized by the number of $(s, t)$-pairs. There are $n(n - 1)$
|
||||
possible paths with endpoints and $(n - 1)(n - 2)$ without endpoints.
|
||||
In our example, that's 6 with endpoints and 2 without endpoints.
|
||||
|
||||
>>> nx.betweenness_centrality(DG, normalized=True, endpoints=True)
|
||||
{0: 0.3333333333333333, 1: 0.5, 2: 0.3333333333333333}
|
||||
>>> nx.betweenness_centrality(DG, normalized=True, endpoints=False)
|
||||
{0: 0.0, 1: 0.5, 2: 0.0}
|
||||
|
||||
Computing the full betweenness centrality can be costly.
|
||||
This function can also be used to compute approximate betweenness centrality
|
||||
by setting `k`. This only determines the number of source nodes to sample;
|
||||
all nodes are targets.
|
||||
|
||||
For simplicity, we only consider the case where endpoints are included in the counts.
|
||||
Since the partial sums only include `k` terms, instead of ``n``,
|
||||
we multiply them by ``n / k``, to approximate the full sum.
|
||||
As the sets of sources and targets are not the same anymore,
|
||||
paths have to be counted in a directed way. We thus count each as half a path.
|
||||
This ensures that the results approximate the standard betweenness for ``k == n``.
|
||||
|
||||
For instance, in the undirected 3-path graph case, setting ``k = 2`` (with ``seed=42``)
|
||||
selects nodes 0 and 2 as sources.
|
||||
This means only shortest paths starting at these nodes are considered.
|
||||
The raw counts with endpoints are ``{0: 3, 1: 4, 2: 3}``. Accounting for the partial sum
|
||||
and applying the undirectedness half-path correction, we get
|
||||
|
||||
>>> nx.betweenness_centrality(G, k=2, normalized=False, endpoints=True, seed=42)
|
||||
{0: 2.25, 1: 3.0, 2: 2.25}
|
||||
|
||||
When normalizing, we instead want to divide by the total number of $(s, t)$-pairs.
|
||||
This is $k(n - 1)$ with endpoints.
|
||||
|
||||
>>> nx.betweenness_centrality(G, k=2, normalized=True, endpoints=True, seed=42)
|
||||
{0: 0.75, 1: 1.0, 2: 0.75}
|
||||
"""
|
||||
betweenness = dict.fromkeys(G, 0.0) # b[v]=0 for v in G
|
||||
if k == len(G):
|
||||
# This is done for performance; the result is the same regardless.
|
||||
k = None
|
||||
if k is None:
|
||||
nodes = G
|
||||
else:
|
||||
nodes = seed.sample(list(G.nodes()), k)
|
||||
for s in nodes:
|
||||
# single source shortest paths
|
||||
if weight is None: # use BFS
|
||||
S, P, sigma, _ = _single_source_shortest_path_basic(G, s)
|
||||
else: # use Dijkstra's algorithm
|
||||
S, P, sigma, _ = _single_source_dijkstra_path_basic(G, s, weight)
|
||||
# accumulation
|
||||
if endpoints:
|
||||
betweenness, _ = _accumulate_endpoints(betweenness, S, P, sigma, s)
|
||||
else:
|
||||
betweenness, _ = _accumulate_basic(betweenness, S, P, sigma, s)
|
||||
# rescaling
|
||||
betweenness = _rescale(
|
||||
betweenness,
|
||||
len(G),
|
||||
normalized=normalized,
|
||||
directed=G.is_directed(),
|
||||
endpoints=endpoints,
|
||||
sampled_nodes=None if k is None else nodes,
|
||||
)
|
||||
return betweenness
|
||||
|
||||
|
||||
@py_random_state("seed")
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def edge_betweenness_centrality(G, k=None, normalized=True, weight=None, seed=None):
|
||||
r"""Compute betweenness centrality for edges.
|
||||
|
||||
Betweenness centrality of an edge $e$ is the sum of the
|
||||
fraction of all-pairs shortest paths that pass through $e$.
|
||||
|
||||
.. math::
|
||||
|
||||
c_B(e) = \sum_{s, t \in V} \frac{\sigma(s, t | e)}{\sigma(s, t)}
|
||||
|
||||
where $V$ is the set of nodes, $\sigma(s, t)$ is the number of
|
||||
shortest $(s, t)$-paths, and $\sigma(s, t | e)$ is the number of
|
||||
those paths passing through edge $e$ [1]_.
|
||||
The denominator $\sigma(s, t)$ is a normalization factor that can be
|
||||
turned off to get the raw path counts.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph.
|
||||
|
||||
k : int, optional (default=None)
|
||||
If `k` is not `None`, use `k` sampled nodes as sources for the considered paths.
|
||||
The resulting sampled counts are then inflated to approximate betweenness.
|
||||
Higher values of `k` give better approximation. Must have ``k <= len(G)``.
|
||||
|
||||
normalized : bool, optional (default=True)
|
||||
If `True`, the betweenness values are rescaled by dividing by the number of
|
||||
possible $(s, t)$-pairs in the graph.
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
If `None`, all edge weights are 1.
|
||||
Otherwise holds the name of the edge attribute used as weight.
|
||||
Weights are used to calculate weighted shortest paths, so they are
|
||||
interpreted as distances.
|
||||
|
||||
seed : integer, random_state, or None (default)
|
||||
Indicator of random number generation state.
|
||||
See :ref:`Randomness<randomness>`.
|
||||
Note that this is only used if ``k is not None``.
|
||||
|
||||
Returns
|
||||
-------
|
||||
edges : dict
|
||||
Dictionary of edges with betweenness centrality as the value.
|
||||
|
||||
See Also
|
||||
--------
|
||||
betweenness_centrality
|
||||
edge_betweenness_centrality_subset
|
||||
edge_load
|
||||
|
||||
Notes
|
||||
-----
|
||||
The algorithm is from Ulrik Brandes [1]_.
|
||||
|
||||
For weighted graphs the edge weights must be greater than zero.
|
||||
Zero edge weights can produce an infinite number of equal length
|
||||
paths between pairs of nodes.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Ulrik Brandes: On Variants of Shortest-Path Betweenness
|
||||
Centrality and their Generic Computation.
|
||||
Social Networks 30(2):136--145, 2008.
|
||||
https://doi.org/10.1016/j.socnet.2007.11.001
|
||||
|
||||
Examples
|
||||
--------
|
||||
Consider an undirected 3-path. Each pair of nodes has exactly one shortest
|
||||
path between them. Since the graph is undirected, only ordered pairs are counted.
|
||||
Each edge has two shortest paths passing through it.
|
||||
As such, the raw counts should be ``{(0, 1): 2, (1, 2): 2}``.
|
||||
|
||||
>>> G = nx.path_graph(3)
|
||||
>>> nx.edge_betweenness_centrality(G, normalized=False)
|
||||
{(0, 1): 2.0, (1, 2): 2.0}
|
||||
|
||||
With normalization, the values are divided by the number of ordered $(s, t)$-pairs,
|
||||
which is $n(n-1)/2$. For the 3-path, this is $3(3-1)/2 = 3$.
|
||||
|
||||
>>> nx.edge_betweenness_centrality(G, normalized=True)
|
||||
{(0, 1): 0.6666666666666666, (1, 2): 0.6666666666666666}
|
||||
|
||||
For a directed graph, all $(s, t)$-pairs are considered. The normalization factor
|
||||
is $n(n-1)$ to reflect this.
|
||||
|
||||
>>> DG = nx.path_graph(3, create_using=nx.DiGraph)
|
||||
>>> nx.edge_betweenness_centrality(DG, normalized=False)
|
||||
{(0, 1): 2.0, (1, 2): 2.0}
|
||||
>>> nx.edge_betweenness_centrality(DG, normalized=True)
|
||||
{(0, 1): 0.3333333333333333, (1, 2): 0.3333333333333333}
|
||||
|
||||
Computing the full edge betweenness centrality can be costly.
|
||||
This function can also be used to compute approximate edge betweenness centrality
|
||||
by setting `k`. This determines the number of source nodes to sample.
|
||||
|
||||
Since the partial sums only include `k` terms, instead of ``n``,
|
||||
we multiply them by ``n / k``, to approximate the full sum.
|
||||
As the sets of sources and targets are not the same anymore,
|
||||
paths have to be counted in a directed way. We thus count each as half a path.
|
||||
This ensures that the results approximate the standard betweenness for ``k == n``.
|
||||
|
||||
For instance, in the undirected 3-path graph case, setting ``k = 2`` (with ``seed=42``)
|
||||
selects nodes 0 and 2 as sources.
|
||||
This means only shortest paths starting at these nodes are considered.
|
||||
The raw counts are ``{(0, 1): 3, (1, 2): 3}``. Accounting for the partial sum
|
||||
and applying the undirectedness half-path correction, we get
|
||||
|
||||
>>> nx.edge_betweenness_centrality(G, k=2, normalized=False, seed=42)
|
||||
{(0, 1): 2.25, (1, 2): 2.25}
|
||||
|
||||
When normalizing, we instead want to divide by the total number of $(s, t)$-pairs.
|
||||
This is $k(n-1)$, which is $4$ in our case.
|
||||
|
||||
>>> nx.edge_betweenness_centrality(G, k=2, normalized=True, seed=42)
|
||||
{(0, 1): 0.75, (1, 2): 0.75}
|
||||
"""
|
||||
betweenness = dict.fromkeys(G, 0.0) # b[v]=0 for v in G
|
||||
# b[e]=0 for e in G.edges()
|
||||
betweenness.update(dict.fromkeys(G.edges(), 0.0))
|
||||
if k is None:
|
||||
nodes = G
|
||||
else:
|
||||
nodes = seed.sample(list(G.nodes()), k)
|
||||
for s in nodes:
|
||||
# single source shortest paths
|
||||
if weight is None: # use BFS
|
||||
S, P, sigma, _ = _single_source_shortest_path_basic(G, s)
|
||||
else: # use Dijkstra's algorithm
|
||||
S, P, sigma, _ = _single_source_dijkstra_path_basic(G, s, weight)
|
||||
# accumulation
|
||||
betweenness = _accumulate_edges(betweenness, S, P, sigma, s)
|
||||
# rescaling
|
||||
for n in G: # remove nodes to only return edges
|
||||
del betweenness[n]
|
||||
betweenness = _rescale(
|
||||
betweenness,
|
||||
len(G),
|
||||
normalized=normalized,
|
||||
directed=G.is_directed(),
|
||||
sampled_nodes=None if k is None else nodes,
|
||||
)
|
||||
if G.is_multigraph():
|
||||
betweenness = _add_edge_keys(G, betweenness, weight=weight)
|
||||
return betweenness
|
||||
|
||||
|
||||
# helpers for betweenness centrality
|
||||
|
||||
|
||||
def _single_source_shortest_path_basic(G, s):
|
||||
S = []
|
||||
P = {}
|
||||
for v in G:
|
||||
P[v] = []
|
||||
sigma = dict.fromkeys(G, 0.0) # sigma[v]=0 for v in G
|
||||
D = {}
|
||||
sigma[s] = 1.0
|
||||
D[s] = 0
|
||||
Q = deque([s])
|
||||
while Q: # use BFS to find shortest paths
|
||||
v = Q.popleft()
|
||||
S.append(v)
|
||||
Dv = D[v]
|
||||
sigmav = sigma[v]
|
||||
for w in G[v]:
|
||||
if w not in D:
|
||||
Q.append(w)
|
||||
D[w] = Dv + 1
|
||||
if D[w] == Dv + 1: # this is a shortest path, count paths
|
||||
sigma[w] += sigmav
|
||||
P[w].append(v) # predecessors
|
||||
return S, P, sigma, D
|
||||
|
||||
|
||||
def _single_source_dijkstra_path_basic(G, s, weight):
|
||||
weight = _weight_function(G, weight)
|
||||
# modified from Eppstein
|
||||
S = []
|
||||
P = {}
|
||||
for v in G:
|
||||
P[v] = []
|
||||
sigma = dict.fromkeys(G, 0.0) # sigma[v]=0 for v in G
|
||||
D = {}
|
||||
sigma[s] = 1.0
|
||||
seen = {s: 0}
|
||||
c = count()
|
||||
Q = [] # use Q as heap with (distance,node id) tuples
|
||||
heappush(Q, (0, next(c), s, s))
|
||||
while Q:
|
||||
(dist, _, pred, v) = heappop(Q)
|
||||
if v in D:
|
||||
continue # already searched this node.
|
||||
sigma[v] += sigma[pred] # count paths
|
||||
S.append(v)
|
||||
D[v] = dist
|
||||
for w, edgedata in G[v].items():
|
||||
vw_dist = dist + weight(v, w, edgedata)
|
||||
if w not in D and (w not in seen or vw_dist < seen[w]):
|
||||
seen[w] = vw_dist
|
||||
heappush(Q, (vw_dist, next(c), v, w))
|
||||
sigma[w] = 0.0
|
||||
P[w] = [v]
|
||||
elif vw_dist == seen[w]: # handle equal paths
|
||||
sigma[w] += sigma[v]
|
||||
P[w].append(v)
|
||||
return S, P, sigma, D
|
||||
|
||||
|
||||
def _accumulate_basic(betweenness, S, P, sigma, s):
|
||||
delta = dict.fromkeys(S, 0)
|
||||
while S:
|
||||
w = S.pop()
|
||||
coeff = (1 + delta[w]) / sigma[w]
|
||||
for v in P[w]:
|
||||
delta[v] += sigma[v] * coeff
|
||||
if w != s:
|
||||
betweenness[w] += delta[w]
|
||||
return betweenness, delta
|
||||
|
||||
|
||||
def _accumulate_endpoints(betweenness, S, P, sigma, s):
|
||||
betweenness[s] += len(S) - 1
|
||||
delta = dict.fromkeys(S, 0)
|
||||
while S:
|
||||
w = S.pop()
|
||||
coeff = (1 + delta[w]) / sigma[w]
|
||||
for v in P[w]:
|
||||
delta[v] += sigma[v] * coeff
|
||||
if w != s:
|
||||
betweenness[w] += delta[w] + 1
|
||||
return betweenness, delta
|
||||
|
||||
|
||||
def _accumulate_edges(betweenness, S, P, sigma, s):
|
||||
delta = dict.fromkeys(S, 0)
|
||||
while S:
|
||||
w = S.pop()
|
||||
coeff = (1 + delta[w]) / sigma[w]
|
||||
for v in P[w]:
|
||||
c = sigma[v] * coeff
|
||||
if (v, w) not in betweenness:
|
||||
betweenness[(w, v)] += c
|
||||
else:
|
||||
betweenness[(v, w)] += c
|
||||
delta[v] += c
|
||||
if w != s:
|
||||
betweenness[w] += delta[w]
|
||||
return betweenness
|
||||
|
||||
|
||||
def _rescale(
|
||||
betweenness, n, *, normalized, directed, endpoints=True, sampled_nodes=None
|
||||
):
|
||||
# For edge betweenness, `endpoints` is always `True`.
|
||||
|
||||
k = None if sampled_nodes is None else len(sampled_nodes)
|
||||
# N is used to count the number of valid (s, t) pairs where s != t that
|
||||
# could have a path pass through v. If endpoints is False, then v must
|
||||
# not be the target t, hence why we subtract by 1.
|
||||
N = n if endpoints else n - 1
|
||||
if N < 2:
|
||||
# No rescaling necessary: b=0 for all nodes
|
||||
return betweenness
|
||||
|
||||
K_source = N if k is None else k
|
||||
|
||||
if k is None or endpoints:
|
||||
# No sampling adjustment needed
|
||||
if normalized:
|
||||
# Divide by the number of valid (s, t) node pairs that could have
|
||||
# a path through v where s != t.
|
||||
scale = 1 / (K_source * (N - 1))
|
||||
else:
|
||||
# Scale to the full BC
|
||||
if not directed:
|
||||
# The non-normalized BC values are computed the same way for
|
||||
# directed and undirected graphs: shortest paths are computed and
|
||||
# counted for each *ordered* (s, t) pair. Undirected graphs should
|
||||
# only count valid *unordered* node pairs {s, t}; that is, (s, t)
|
||||
# and (t, s) should be counted only once. We correct for this here.
|
||||
correction = 2
|
||||
else:
|
||||
correction = 1
|
||||
scale = N / (K_source * correction)
|
||||
|
||||
if scale != 1:
|
||||
for v in betweenness:
|
||||
betweenness[v] *= scale
|
||||
return betweenness
|
||||
|
||||
# Sampling adjustment needed when excluding endpoints when using k. In this
|
||||
# case, we need to handle source nodes differently from non-source nodes,
|
||||
# because source nodes can't include themselves since endpoints are excluded.
|
||||
# Without this, k == n would be a special case that would violate the
|
||||
# assumption that node `v` is not one of the (s, t) node pairs.
|
||||
if normalized:
|
||||
# NaN for undefined 0/0; there is no data for source node when k=1
|
||||
scale_source = 1 / ((K_source - 1) * (N - 1)) if K_source > 1 else math.nan
|
||||
scale_nonsource = 1 / (K_source * (N - 1))
|
||||
else:
|
||||
correction = 1 if directed else 2
|
||||
scale_source = N / ((K_source - 1) * correction) if K_source > 1 else math.nan
|
||||
scale_nonsource = N / (K_source * correction)
|
||||
|
||||
sampled_nodes = set(sampled_nodes)
|
||||
for v in betweenness:
|
||||
betweenness[v] *= scale_source if v in sampled_nodes else scale_nonsource
|
||||
return betweenness
|
||||
|
||||
|
||||
@not_implemented_for("graph")
|
||||
def _add_edge_keys(G, betweenness, weight=None):
|
||||
r"""Adds the corrected betweenness centrality (BC) values for multigraphs.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph.
|
||||
|
||||
betweenness : dictionary
|
||||
Dictionary mapping adjacent node tuples to betweenness centrality values.
|
||||
|
||||
weight : string or function
|
||||
See `_weight_function` for details. Defaults to `None`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
edges : dictionary
|
||||
The parameter `betweenness` including edges with keys and their
|
||||
betweenness centrality values.
|
||||
|
||||
The BC value is divided among edges of equal weight.
|
||||
"""
|
||||
_weight = _weight_function(G, weight)
|
||||
|
||||
edge_bc = dict.fromkeys(G.edges, 0.0)
|
||||
for u, v in betweenness:
|
||||
d = G[u][v]
|
||||
wt = _weight(u, v, d)
|
||||
keys = [k for k in d if _weight(u, v, {k: d[k]}) == wt]
|
||||
bc = betweenness[(u, v)] / len(keys)
|
||||
for k in keys:
|
||||
edge_bc[(u, v, k)] = bc
|
||||
|
||||
return edge_bc
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
"""Betweenness centrality measures for subsets of nodes."""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.centrality.betweenness import (
|
||||
_add_edge_keys,
|
||||
_rescale,
|
||||
)
|
||||
from networkx.algorithms.centrality.betweenness import (
|
||||
_single_source_dijkstra_path_basic as dijkstra,
|
||||
)
|
||||
from networkx.algorithms.centrality.betweenness import (
|
||||
_single_source_shortest_path_basic as shortest_path,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"betweenness_centrality_subset",
|
||||
"edge_betweenness_centrality_subset",
|
||||
]
|
||||
|
||||
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def betweenness_centrality_subset(G, sources, targets, normalized=False, weight=None):
|
||||
r"""Compute betweenness centrality for a subset of nodes.
|
||||
|
||||
.. math::
|
||||
|
||||
c_B(v) = \sum_{s \in S, t \in T} \frac{\sigma(s, t | v)}{\sigma(s, t)}
|
||||
|
||||
where $S$ is the set of sources, $T$ is the set of targets,
|
||||
$\sigma(s, t)$ is the number of shortest $(s, t)$-paths,
|
||||
and $\sigma(s, t | v)$ is the number of those paths
|
||||
passing through some node $v$ other than $s$ and $t$.
|
||||
If $s = t$, $\sigma(s, t) = 1$,
|
||||
and if $v \in \{s, t\}$, $\sigma(s, t | v) = 0$ [2]_.
|
||||
The denominator $\sigma(s, t)$ is a normalization factor that can be
|
||||
turned off to get the raw path counts.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph.
|
||||
|
||||
sources: list of nodes
|
||||
Nodes to use as sources for shortest paths in betweenness.
|
||||
|
||||
targets: list of nodes
|
||||
Nodes to use as targets for shortest paths in betweenness.
|
||||
|
||||
normalized : bool, optional (default=False)
|
||||
If `True`, the betweenness values are rescaled by dividing by the number of
|
||||
possible $(s, t)$-pairs in the graph.
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
If `None`, all edge weights are 1.
|
||||
Otherwise holds the name of the edge attribute used as weight.
|
||||
Weights are used to calculate weighted shortest paths, so they are
|
||||
interpreted as distances.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dict
|
||||
Dictionary of nodes with betweenness centrality as the value.
|
||||
|
||||
See Also
|
||||
--------
|
||||
betweenness_centrality
|
||||
edge_betweenness_centrality
|
||||
edge_betweenness_centrality_subset
|
||||
load_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
The basic algorithm is from [1]_.
|
||||
|
||||
For weighted graphs the edge weights must be greater than zero.
|
||||
Zero edge weights can produce an infinite number of equal length
|
||||
paths between pairs of nodes.
|
||||
|
||||
The normalization might seem a little strange but it is
|
||||
designed to make betweenness_centrality(G) be the same as
|
||||
betweenness_centrality_subset(G,sources=G.nodes(),targets=G.nodes()).
|
||||
|
||||
The total number of paths between source and target is counted
|
||||
differently for directed and undirected graphs. Directed paths
|
||||
are easy to count. Undirected paths are tricky: should a path
|
||||
from ``u`` to ``v`` count as 1 undirected path or as 2 directed paths?
|
||||
We are only counting the paths in one direction. They are
|
||||
undirected paths but we are counting them in a directed way.
|
||||
To count them as undirected paths, each should count as half a path.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Ulrik Brandes, A Faster Algorithm for Betweenness Centrality.
|
||||
Journal of Mathematical Sociology 25(2):163-177, 2001.
|
||||
https://doi.org/10.1080/0022250X.2001.9990249
|
||||
.. [2] Ulrik Brandes: On Variants of Shortest-Path Betweenness
|
||||
Centrality and their Generic Computation.
|
||||
Social Networks 30(2):136-145, 2008.
|
||||
https://doi.org/10.1016/j.socnet.2007.11.001
|
||||
"""
|
||||
b = dict.fromkeys(G, 0.0) # b[v]=0 for v in G
|
||||
for s in sources:
|
||||
# single source shortest paths
|
||||
if weight is None: # use BFS
|
||||
S, P, sigma, _ = shortest_path(G, s)
|
||||
else: # use Dijkstra's algorithm
|
||||
S, P, sigma, _ = dijkstra(G, s, weight)
|
||||
b = _accumulate_subset(b, S, P, sigma, s, targets)
|
||||
b = _rescale(
|
||||
b, len(G), normalized=normalized, directed=G.is_directed(), endpoints=False
|
||||
)
|
||||
return b
|
||||
|
||||
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def edge_betweenness_centrality_subset(
|
||||
G, sources, targets, normalized=False, weight=None
|
||||
):
|
||||
r"""Compute betweenness centrality for edges for a subset of nodes.
|
||||
|
||||
.. math::
|
||||
|
||||
c_B(e) = \sum_{s \in S, t \in T} \frac{\sigma(s, t | e)}{\sigma(s, t)}
|
||||
|
||||
where $S$ is the set of sources, $T$ is the set of targets,
|
||||
$\sigma(s, t)$ is the number of shortest $(s, t)$-paths,
|
||||
and $\sigma(s, t | e)$ is the number of those paths
|
||||
passing through edge $e$ [1]_.
|
||||
The denominator $\sigma(s, t)$ is a normalization factor that can be
|
||||
turned off to get the raw path counts.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A networkx graph.
|
||||
|
||||
sources: list of nodes
|
||||
Nodes to use as sources for shortest paths in betweenness.
|
||||
|
||||
targets: list of nodes
|
||||
Nodes to use as targets for shortest paths in betweenness.
|
||||
|
||||
normalized : bool, optional (default=False)
|
||||
If `True`, the betweenness values are rescaled by dividing by the number of
|
||||
possible $(s, t)$-pairs in the graph.
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
If `None`, all edge weights are 1.
|
||||
Otherwise holds the name of the edge attribute used as weight.
|
||||
Weights are used to calculate weighted shortest paths, so they are
|
||||
interpreted as distances.
|
||||
|
||||
Returns
|
||||
-------
|
||||
edges : dict
|
||||
Dictionary of edges with betweenness centrality as the value.
|
||||
|
||||
See Also
|
||||
--------
|
||||
betweenness_centrality
|
||||
betweenness_centrality_subset
|
||||
edge_betweenness_centrality
|
||||
edge_load
|
||||
|
||||
Notes
|
||||
-----
|
||||
The basic algorithm is from [1]_.
|
||||
|
||||
For weighted graphs the edge weights must be greater than zero.
|
||||
Zero edge weights can produce an infinite number of equal length
|
||||
paths between pairs of nodes.
|
||||
|
||||
The normalization might seem a little strange but it is the same
|
||||
as in edge_betweenness_centrality() and is designed to make
|
||||
edge_betweenness_centrality(G) be the same as
|
||||
edge_betweenness_centrality_subset(G,sources=G.nodes(),targets=G.nodes()).
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Ulrik Brandes: On Variants of Shortest-Path Betweenness
|
||||
Centrality and their Generic Computation.
|
||||
Social Networks 30(2):136-145, 2008.
|
||||
https://doi.org/10.1016/j.socnet.2007.11.001
|
||||
"""
|
||||
b = dict.fromkeys(G, 0.0) # b[v]=0 for v in G
|
||||
b.update(dict.fromkeys(G.edges(), 0.0)) # b[e] for e in G.edges()
|
||||
for s in sources:
|
||||
# single source shortest paths
|
||||
if weight is None: # use BFS
|
||||
S, P, sigma, _ = shortest_path(G, s)
|
||||
else: # use Dijkstra's algorithm
|
||||
S, P, sigma, _ = dijkstra(G, s, weight)
|
||||
b = _accumulate_edges_subset(b, S, P, sigma, s, targets)
|
||||
for n in G: # remove nodes to only return edges
|
||||
del b[n]
|
||||
b = _rescale(b, len(G), normalized=normalized, directed=G.is_directed())
|
||||
if G.is_multigraph():
|
||||
b = _add_edge_keys(G, b, weight=weight)
|
||||
return b
|
||||
|
||||
|
||||
def _accumulate_subset(betweenness, S, P, sigma, s, targets):
|
||||
delta = dict.fromkeys(S, 0.0)
|
||||
target_set = set(targets) - {s}
|
||||
while S:
|
||||
w = S.pop()
|
||||
if w in target_set:
|
||||
coeff = (delta[w] + 1.0) / sigma[w]
|
||||
else:
|
||||
coeff = delta[w] / sigma[w]
|
||||
for v in P[w]:
|
||||
delta[v] += sigma[v] * coeff
|
||||
if w != s:
|
||||
betweenness[w] += delta[w]
|
||||
return betweenness
|
||||
|
||||
|
||||
def _accumulate_edges_subset(betweenness, S, P, sigma, s, targets):
|
||||
"""edge_betweenness_centrality_subset helper."""
|
||||
delta = dict.fromkeys(S, 0)
|
||||
target_set = set(targets)
|
||||
while S:
|
||||
w = S.pop()
|
||||
for v in P[w]:
|
||||
if w in target_set:
|
||||
c = (sigma[v] / sigma[w]) * (1.0 + delta[w])
|
||||
else:
|
||||
c = delta[w] / len(P[w])
|
||||
if (v, w) not in betweenness:
|
||||
betweenness[(w, v)] += c
|
||||
else:
|
||||
betweenness[(v, w)] += c
|
||||
delta[v] += c
|
||||
if w != s:
|
||||
betweenness[w] += delta[w]
|
||||
return betweenness
|
||||
@@ -0,0 +1,282 @@
|
||||
"""
|
||||
Closeness centrality measures.
|
||||
"""
|
||||
|
||||
import functools
|
||||
|
||||
import networkx as nx
|
||||
from networkx.exception import NetworkXError
|
||||
from networkx.utils.decorators import not_implemented_for
|
||||
|
||||
__all__ = ["closeness_centrality", "incremental_closeness_centrality"]
|
||||
|
||||
|
||||
@nx._dispatchable(edge_attrs="distance")
|
||||
def closeness_centrality(G, u=None, distance=None, wf_improved=True):
|
||||
r"""Compute closeness centrality for nodes.
|
||||
|
||||
Closeness centrality [1]_ of a node `u` is the reciprocal of the
|
||||
average shortest path distance to `u` over all `n-1` reachable nodes.
|
||||
|
||||
.. math::
|
||||
|
||||
C(u) = \frac{n - 1}{\sum_{v=1}^{n-1} d(v, u)},
|
||||
|
||||
where `d(v, u)` is the shortest-path distance between `v` and `u`,
|
||||
and `n-1` is the number of nodes reachable from `u`. Notice that the
|
||||
closeness distance function computes the incoming distance to `u`
|
||||
for directed graphs. To use outward distance, act on `G.reverse()`.
|
||||
|
||||
Notice that higher values of closeness indicate higher centrality.
|
||||
|
||||
Wasserman and Faust propose an improved formula for graphs with
|
||||
more than one connected component. The result is "a ratio of the
|
||||
fraction of actors in the group who are reachable, to the average
|
||||
distance" from the reachable actors [2]_. You might think this
|
||||
scale factor is inverted but it is not. As is, nodes from small
|
||||
components receive a smaller closeness value. Letting `N` denote
|
||||
the number of nodes in the graph,
|
||||
|
||||
.. math::
|
||||
|
||||
C_{WF}(u) = \frac{n-1}{N-1} \frac{n - 1}{\sum_{v=1}^{n-1} d(v, u)},
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph
|
||||
|
||||
u : node, optional
|
||||
Return only the value for node u
|
||||
|
||||
distance : edge attribute key, optional (default=None)
|
||||
Use the specified edge attribute as the edge distance in shortest
|
||||
path calculations. If `None` (the default) all edges have a distance of 1.
|
||||
Absent edge attributes are assigned a distance of 1. Note that no check
|
||||
is performed to ensure that edges have the provided attribute.
|
||||
|
||||
wf_improved : bool, optional (default=True)
|
||||
If True, scale by the fraction of nodes reachable. This gives the
|
||||
Wasserman and Faust improved formula. For single component graphs
|
||||
it is the same as the original formula.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with closeness centrality as the value.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)])
|
||||
>>> nx.closeness_centrality(G)
|
||||
{0: 1.0, 1: 1.0, 2: 0.75, 3: 0.75}
|
||||
|
||||
See Also
|
||||
--------
|
||||
betweenness_centrality, load_centrality, eigenvector_centrality,
|
||||
degree_centrality, incremental_closeness_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
The closeness centrality is normalized to `(n-1)/(|G|-1)` where
|
||||
`n` is the number of nodes in the connected part of graph
|
||||
containing the node. If the graph is not completely connected,
|
||||
this algorithm computes the closeness centrality for each
|
||||
connected part separately scaled by that parts size.
|
||||
|
||||
If the 'distance' keyword is set to an edge attribute key then the
|
||||
shortest-path length will be computed using Dijkstra's algorithm with
|
||||
that edge attribute as the edge weight.
|
||||
|
||||
The closeness centrality uses *inward* distance to a node, not outward.
|
||||
If you want to use outword distances apply the function to `G.reverse()`
|
||||
|
||||
In NetworkX 2.2 and earlier a bug caused Dijkstra's algorithm to use the
|
||||
outward distance rather than the inward distance. If you use a 'distance'
|
||||
keyword and a DiGraph, your results will change between v2.2 and v2.3.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Linton C. Freeman: Centrality in networks: I.
|
||||
Conceptual clarification. Social Networks 1:215-239, 1979.
|
||||
https://doi.org/10.1016/0378-8733(78)90021-7
|
||||
.. [2] pg. 201 of Wasserman, S. and Faust, K.,
|
||||
Social Network Analysis: Methods and Applications, 1994,
|
||||
Cambridge University Press.
|
||||
"""
|
||||
if G.is_directed():
|
||||
G = G.reverse() # create a reversed graph view
|
||||
|
||||
if distance is not None:
|
||||
# use Dijkstra's algorithm with specified attribute as edge weight
|
||||
path_length = functools.partial(
|
||||
nx.single_source_dijkstra_path_length, weight=distance
|
||||
)
|
||||
else:
|
||||
path_length = nx.single_source_shortest_path_length
|
||||
|
||||
if u is None:
|
||||
nodes = G.nodes
|
||||
else:
|
||||
nodes = [u]
|
||||
closeness_dict = {}
|
||||
for n in nodes:
|
||||
sp = path_length(G, n)
|
||||
totsp = sum(sp.values())
|
||||
len_G = len(G)
|
||||
_closeness_centrality = 0.0
|
||||
if totsp > 0.0 and len_G > 1:
|
||||
_closeness_centrality = (len(sp) - 1.0) / totsp
|
||||
# normalize to number of nodes-1 in connected part
|
||||
if wf_improved:
|
||||
s = (len(sp) - 1.0) / (len_G - 1)
|
||||
_closeness_centrality *= s
|
||||
closeness_dict[n] = _closeness_centrality
|
||||
if u is not None:
|
||||
return closeness_dict[u]
|
||||
return closeness_dict
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@nx._dispatchable(mutates_input=True)
|
||||
def incremental_closeness_centrality(
|
||||
G, edge, prev_cc=None, insertion=True, wf_improved=True
|
||||
):
|
||||
r"""Incremental closeness centrality for nodes.
|
||||
|
||||
Compute closeness centrality for nodes using level-based work filtering
|
||||
as described in Incremental Algorithms for Closeness Centrality by Sariyuce et al.
|
||||
|
||||
Level-based work filtering detects unnecessary updates to the closeness
|
||||
centrality and filters them out.
|
||||
|
||||
---
|
||||
From "Incremental Algorithms for Closeness Centrality":
|
||||
|
||||
Theorem 1: Let :math:`G = (V, E)` be a graph and u and v be two vertices in V
|
||||
such that there is no edge (u, v) in E. Let :math:`G' = (V, E \cup uv)`
|
||||
Then :math:`cc[s] = cc'[s]` if and only if :math:`\left|dG(s, u) - dG(s, v)\right| \leq 1`.
|
||||
|
||||
Where :math:`dG(u, v)` denotes the length of the shortest path between
|
||||
two vertices u, v in a graph G, cc[s] is the closeness centrality for a
|
||||
vertex s in V, and cc'[s] is the closeness centrality for a
|
||||
vertex s in V, with the (u, v) edge added.
|
||||
---
|
||||
|
||||
We use Theorem 1 to filter out updates when adding or removing an edge.
|
||||
When adding an edge (u, v), we compute the shortest path lengths from all
|
||||
other nodes to u and to v before the node is added. When removing an edge,
|
||||
we compute the shortest path lengths after the edge is removed. Then we
|
||||
apply Theorem 1 to use previously computed closeness centrality for nodes
|
||||
where :math:`\left|dG(s, u) - dG(s, v)\right| \leq 1`. This works only for
|
||||
undirected, unweighted graphs; the distance argument is not supported.
|
||||
|
||||
Closeness centrality [1]_ of a node `u` is the reciprocal of the
|
||||
sum of the shortest path distances from `u` to all `n-1` other nodes.
|
||||
Since the sum of distances depends on the number of nodes in the
|
||||
graph, closeness is normalized by the sum of minimum possible
|
||||
distances `n-1`.
|
||||
|
||||
.. math::
|
||||
|
||||
C(u) = \frac{n - 1}{\sum_{v=1}^{n-1} d(v, u)},
|
||||
|
||||
where `d(v, u)` is the shortest-path distance between `v` and `u`,
|
||||
and `n` is the number of nodes in the graph.
|
||||
|
||||
Notice that higher values of closeness indicate higher centrality.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph
|
||||
|
||||
edge : tuple
|
||||
The modified edge (u, v) in the graph.
|
||||
|
||||
prev_cc : dictionary
|
||||
The previous closeness centrality for all nodes in the graph.
|
||||
|
||||
insertion : bool, optional
|
||||
If True (default) the edge was inserted, otherwise it was deleted from the graph.
|
||||
|
||||
wf_improved : bool, optional (default=True)
|
||||
If True, scale by the fraction of nodes reachable. This gives the
|
||||
Wasserman and Faust improved formula. For single component graphs
|
||||
it is the same as the original formula.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with closeness centrality as the value.
|
||||
|
||||
See Also
|
||||
--------
|
||||
betweenness_centrality, load_centrality, eigenvector_centrality,
|
||||
degree_centrality, closeness_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
The closeness centrality is normalized to `(n-1)/(|G|-1)` where
|
||||
`n` is the number of nodes in the connected part of graph
|
||||
containing the node. If the graph is not completely connected,
|
||||
this algorithm computes the closeness centrality for each
|
||||
connected part separately.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Freeman, L.C., 1979. Centrality in networks: I.
|
||||
Conceptual clarification. Social Networks 1, 215--239.
|
||||
https://doi.org/10.1016/0378-8733(78)90021-7
|
||||
.. [2] Sariyuce, A.E. ; Kaya, K. ; Saule, E. ; Catalyiirek, U.V. Incremental
|
||||
Algorithms for Closeness Centrality. 2013 IEEE International Conference on Big Data
|
||||
http://sariyuce.com/papers/bigdata13.pdf
|
||||
"""
|
||||
if prev_cc is not None and set(prev_cc.keys()) != set(G.nodes()):
|
||||
raise NetworkXError("prev_cc and G do not have the same nodes")
|
||||
|
||||
# Unpack edge
|
||||
(u, v) = edge
|
||||
path_length = nx.single_source_shortest_path_length
|
||||
|
||||
if insertion:
|
||||
# For edge insertion, we want shortest paths before the edge is inserted
|
||||
du = path_length(G, u)
|
||||
dv = path_length(G, v)
|
||||
|
||||
G.add_edge(u, v)
|
||||
else:
|
||||
G.remove_edge(u, v)
|
||||
|
||||
# For edge removal, we want shortest paths after the edge is removed
|
||||
du = path_length(G, u)
|
||||
dv = path_length(G, v)
|
||||
|
||||
if prev_cc is None:
|
||||
return nx.closeness_centrality(G)
|
||||
|
||||
nodes = G.nodes()
|
||||
closeness_dict = {}
|
||||
for n in nodes:
|
||||
if n in du and n in dv and abs(du[n] - dv[n]) <= 1:
|
||||
closeness_dict[n] = prev_cc[n]
|
||||
else:
|
||||
sp = path_length(G, n)
|
||||
totsp = sum(sp.values())
|
||||
len_G = len(G)
|
||||
_closeness_centrality = 0.0
|
||||
if totsp > 0.0 and len_G > 1:
|
||||
_closeness_centrality = (len(sp) - 1.0) / totsp
|
||||
# normalize to number of nodes-1 in connected part
|
||||
if wf_improved:
|
||||
s = (len(sp) - 1.0) / (len_G - 1)
|
||||
_closeness_centrality *= s
|
||||
closeness_dict[n] = _closeness_centrality
|
||||
|
||||
# Leave the graph as we found it
|
||||
if insertion:
|
||||
G.remove_edge(u, v)
|
||||
else:
|
||||
G.add_edge(u, v)
|
||||
|
||||
return closeness_dict
|
||||
+364
@@ -0,0 +1,364 @@
|
||||
"""Current-flow betweenness centrality measures."""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.centrality.flow_matrix import (
|
||||
CGInverseLaplacian,
|
||||
FullInverseLaplacian,
|
||||
SuperLUInverseLaplacian,
|
||||
flow_matrix_row,
|
||||
)
|
||||
from networkx.utils import (
|
||||
not_implemented_for,
|
||||
py_random_state,
|
||||
reverse_cuthill_mckee_ordering,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"current_flow_betweenness_centrality",
|
||||
"approximate_current_flow_betweenness_centrality",
|
||||
"edge_current_flow_betweenness_centrality",
|
||||
]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@py_random_state("seed")
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def approximate_current_flow_betweenness_centrality(
|
||||
G,
|
||||
normalized=True,
|
||||
weight=None,
|
||||
dtype=float,
|
||||
solver="full",
|
||||
epsilon=0.5,
|
||||
kmax=10000,
|
||||
seed=None,
|
||||
*,
|
||||
sample_weight=1,
|
||||
):
|
||||
r"""Compute the approximate current-flow betweenness centrality for nodes.
|
||||
|
||||
Approximates the current-flow betweenness centrality within absolute
|
||||
error of epsilon with high probability [1]_.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph
|
||||
|
||||
normalized : bool, optional (default=True)
|
||||
If True the betweenness values are normalized by 2/[(n-1)(n-2)] where
|
||||
n is the number of nodes in G.
|
||||
|
||||
weight : string or None, optional (default=None)
|
||||
Key for edge data used as the edge weight.
|
||||
If None, then use 1 as each edge weight.
|
||||
The weight reflects the capacity or the strength of the
|
||||
edge.
|
||||
|
||||
dtype : data type (float)
|
||||
Default data type for internal matrices.
|
||||
Set to np.float32 for lower memory consumption.
|
||||
|
||||
solver : string (default='full')
|
||||
Type of linear solver to use for computing the flow matrix.
|
||||
Options are "full" (uses most memory), "lu" (recommended), and
|
||||
"cg" (uses least memory).
|
||||
|
||||
epsilon: float
|
||||
Absolute error tolerance. Note that smaller values of `epsilon` lead to
|
||||
higher numbers of sample pairs (``k``) and thus more computation time. The number
|
||||
of sample pairs is approximately ``(c/epsilon)^2 * log(n)`` where ``n`` is the
|
||||
number of nodes.
|
||||
|
||||
kmax: int
|
||||
Maximum number of sample node pairs to use for approximation.
|
||||
|
||||
sample_weight : float (default=1)
|
||||
Multiplicative factor for the number of sample node pairs used in approximation.
|
||||
Higher values may improve accuracy at the expense of increased computation time.
|
||||
|
||||
seed : integer, random_state, or None (default)
|
||||
Indicator of random number generation state.
|
||||
See :ref:`Randomness<randomness>`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with betweenness centrality as the value.
|
||||
|
||||
See Also
|
||||
--------
|
||||
current_flow_betweenness_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
The running time is $O((1/\epsilon^2)m{\sqrt k} \log n)$
|
||||
and the space required is $O(m)$ for $n$ nodes and $m$ edges.
|
||||
|
||||
If the edges have a 'weight' attribute they will be used as
|
||||
weights in this algorithm. Unspecified weights are set to 1.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Ulrik Brandes and Daniel Fleischer:
|
||||
Centrality Measures Based on Current Flow.
|
||||
Proc. 22nd Symp. Theoretical Aspects of Computer Science (STACS '05).
|
||||
LNCS 3404, pp. 533-544. Springer-Verlag, 2005.
|
||||
https://doi.org/10.1007/978-3-540-31856-9_44
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
if not nx.is_connected(G):
|
||||
raise nx.NetworkXError("Graph not connected.")
|
||||
|
||||
n = G.number_of_nodes()
|
||||
|
||||
# For small graphs (n < 3), betweenness centrality is always 0 for all nodes
|
||||
# since no node can be "between" any pair of other nodes
|
||||
if n < 3:
|
||||
return dict.fromkeys(G, 0.0)
|
||||
|
||||
if epsilon <= 0:
|
||||
raise nx.NetworkXError(f"Epsilon must be positive. Got {epsilon=}.")
|
||||
|
||||
if sample_weight <= 0:
|
||||
raise nx.NetworkXError(f"Sample weight must be positive. Got {sample_weight=}.")
|
||||
|
||||
nb = (n - 1.0) * (n - 2.0) # normalization factor
|
||||
cstar = n * (n - 1) / nb
|
||||
k = int(sample_weight * np.ceil((cstar / epsilon) ** 2 * np.log(n)))
|
||||
if k > kmax:
|
||||
msg = f"Number random pairs k>kmax ({k}>{kmax}) "
|
||||
raise nx.NetworkXError(msg, "Increase kmax or epsilon")
|
||||
|
||||
solvername = {
|
||||
"full": FullInverseLaplacian,
|
||||
"lu": SuperLUInverseLaplacian,
|
||||
"cg": CGInverseLaplacian,
|
||||
}
|
||||
ordering = list(reverse_cuthill_mckee_ordering(G))
|
||||
# make a copy with integer labels according to rcm ordering
|
||||
# this could be done without a copy if we really wanted to
|
||||
H = nx.relabel_nodes(G, dict(zip(ordering, range(n))))
|
||||
L = nx.laplacian_matrix(H, nodelist=range(n), weight=weight).asformat("csc")
|
||||
L = L.astype(dtype)
|
||||
C = solvername[solver](L, dtype=dtype) # initialize solver
|
||||
betweenness = dict.fromkeys(H, 0.0)
|
||||
cstar2k = cstar / (2 * k)
|
||||
for _ in range(k):
|
||||
s, t = pair = seed.sample(range(n), 2)
|
||||
b = np.zeros(n, dtype=dtype)
|
||||
b[s] = 1
|
||||
b[t] = -1
|
||||
p = C.solve(b)
|
||||
for v in H:
|
||||
if v in pair:
|
||||
continue
|
||||
for nbr in H[v]:
|
||||
w = H[v][nbr].get(weight, 1.0)
|
||||
betweenness[v] += float(w * np.abs(p[v] - p[nbr]) * cstar2k)
|
||||
if normalized:
|
||||
factor = 1.0
|
||||
else:
|
||||
factor = nb / 2.0
|
||||
# remap to original node names and "unnormalize" if required
|
||||
return {ordering[k]: v * factor for k, v in betweenness.items()}
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def current_flow_betweenness_centrality(
|
||||
G, normalized=True, weight=None, dtype=float, solver="full"
|
||||
):
|
||||
r"""Compute current-flow betweenness centrality for nodes.
|
||||
|
||||
Current-flow betweenness centrality uses an electrical current
|
||||
model for information spreading in contrast to betweenness
|
||||
centrality which uses shortest paths.
|
||||
|
||||
Current-flow betweenness centrality is also known as
|
||||
random-walk betweenness centrality [2]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph
|
||||
|
||||
normalized : bool, optional (default=True)
|
||||
If True the betweenness values are normalized by 2/[(n-1)(n-2)] where
|
||||
n is the number of nodes in G.
|
||||
|
||||
weight : string or None, optional (default=None)
|
||||
Key for edge data used as the edge weight.
|
||||
If None, then use 1 as each edge weight.
|
||||
The weight reflects the capacity or the strength of the
|
||||
edge.
|
||||
|
||||
dtype : data type (float)
|
||||
Default data type for internal matrices.
|
||||
Set to np.float32 for lower memory consumption.
|
||||
|
||||
solver : string (default='full')
|
||||
Type of linear solver to use for computing the flow matrix.
|
||||
Options are "full" (uses most memory), "lu" (recommended), and
|
||||
"cg" (uses least memory).
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with betweenness centrality as the value.
|
||||
|
||||
See Also
|
||||
--------
|
||||
approximate_current_flow_betweenness_centrality
|
||||
betweenness_centrality
|
||||
edge_betweenness_centrality
|
||||
edge_current_flow_betweenness_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
Current-flow betweenness can be computed in $O(I(n-1)+mn \log n)$
|
||||
time [1]_, where $I(n-1)$ is the time needed to compute the
|
||||
inverse Laplacian. For a full matrix this is $O(n^3)$ but using
|
||||
sparse methods you can achieve $O(nm{\sqrt k})$ where $k$ is the
|
||||
Laplacian matrix condition number.
|
||||
|
||||
The space required is $O(nw)$ where $w$ is the width of the sparse
|
||||
Laplacian matrix. Worse case is $w=n$ for $O(n^2)$.
|
||||
|
||||
If the edges have a 'weight' attribute they will be used as
|
||||
weights in this algorithm. Unspecified weights are set to 1.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Centrality Measures Based on Current Flow.
|
||||
Ulrik Brandes and Daniel Fleischer,
|
||||
Proc. 22nd Symp. Theoretical Aspects of Computer Science (STACS '05).
|
||||
LNCS 3404, pp. 533-544. Springer-Verlag, 2005.
|
||||
https://doi.org/10.1007/978-3-540-31856-9_44
|
||||
|
||||
.. [2] A measure of betweenness centrality based on random walks,
|
||||
M. E. J. Newman, Social Networks 27, 39-54 (2005).
|
||||
"""
|
||||
if not nx.is_connected(G):
|
||||
raise nx.NetworkXError("Graph not connected.")
|
||||
N = G.number_of_nodes()
|
||||
ordering = list(reverse_cuthill_mckee_ordering(G))
|
||||
# make a copy with integer labels according to rcm ordering
|
||||
# this could be done without a copy if we really wanted to
|
||||
H = nx.relabel_nodes(G, dict(zip(ordering, range(N))))
|
||||
betweenness = dict.fromkeys(H, 0.0) # b[n]=0 for n in H
|
||||
for row, (s, t) in flow_matrix_row(H, weight=weight, dtype=dtype, solver=solver):
|
||||
pos = dict(zip(row.argsort()[::-1], range(N)))
|
||||
for i in range(N):
|
||||
betweenness[s] += (i - pos[i]) * row.item(i)
|
||||
betweenness[t] += (N - i - 1 - pos[i]) * row.item(i)
|
||||
if normalized:
|
||||
nb = (N - 1.0) * (N - 2.0) # normalization factor
|
||||
else:
|
||||
nb = 2.0
|
||||
return {ordering[n]: (b - n) * 2.0 / nb for n, b in betweenness.items()}
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def edge_current_flow_betweenness_centrality(
|
||||
G, normalized=True, weight=None, dtype=float, solver="full"
|
||||
):
|
||||
r"""Compute current-flow betweenness centrality for edges.
|
||||
|
||||
Current-flow betweenness centrality uses an electrical current
|
||||
model for information spreading in contrast to betweenness
|
||||
centrality which uses shortest paths.
|
||||
|
||||
Current-flow betweenness centrality is also known as
|
||||
random-walk betweenness centrality [2]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph
|
||||
|
||||
normalized : bool, optional (default=True)
|
||||
If True the betweenness values are normalized by 2/[(n-1)(n-2)] where
|
||||
n is the number of nodes in G.
|
||||
|
||||
weight : string or None, optional (default=None)
|
||||
Key for edge data used as the edge weight.
|
||||
If None, then use 1 as each edge weight.
|
||||
The weight reflects the capacity or the strength of the
|
||||
edge.
|
||||
|
||||
dtype : data type (default=float)
|
||||
Default data type for internal matrices.
|
||||
Set to np.float32 for lower memory consumption.
|
||||
|
||||
solver : string (default='full')
|
||||
Type of linear solver to use for computing the flow matrix.
|
||||
Options are "full" (uses most memory), "lu" (recommended), and
|
||||
"cg" (uses least memory).
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of edge tuples with betweenness centrality as the value.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXError
|
||||
The algorithm does not support DiGraphs.
|
||||
If the input graph is an instance of DiGraph class, NetworkXError
|
||||
is raised.
|
||||
|
||||
See Also
|
||||
--------
|
||||
betweenness_centrality
|
||||
edge_betweenness_centrality
|
||||
current_flow_betweenness_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
Current-flow betweenness can be computed in $O(I(n-1)+mn \log n)$
|
||||
time [1]_, where $I(n-1)$ is the time needed to compute the
|
||||
inverse Laplacian. For a full matrix this is $O(n^3)$ but using
|
||||
sparse methods you can achieve $O(nm{\sqrt k})$ where $k$ is the
|
||||
Laplacian matrix condition number.
|
||||
|
||||
The space required is $O(nw)$ where $w$ is the width of the sparse
|
||||
Laplacian matrix. Worse case is $w=n$ for $O(n^2)$.
|
||||
|
||||
If the edges have a 'weight' attribute they will be used as
|
||||
weights in this algorithm. Unspecified weights are set to 1.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Centrality Measures Based on Current Flow.
|
||||
Ulrik Brandes and Daniel Fleischer,
|
||||
Proc. 22nd Symp. Theoretical Aspects of Computer Science (STACS '05).
|
||||
LNCS 3404, pp. 533-544. Springer-Verlag, 2005.
|
||||
https://doi.org/10.1007/978-3-540-31856-9_44
|
||||
|
||||
.. [2] A measure of betweenness centrality based on random walks,
|
||||
M. E. J. Newman, Social Networks 27, 39-54 (2005).
|
||||
"""
|
||||
if not nx.is_connected(G):
|
||||
raise nx.NetworkXError("Graph not connected.")
|
||||
N = G.number_of_nodes()
|
||||
ordering = list(reverse_cuthill_mckee_ordering(G))
|
||||
# make a copy with integer labels according to rcm ordering
|
||||
# this could be done without a copy if we really wanted to
|
||||
H = nx.relabel_nodes(G, dict(zip(ordering, range(N))))
|
||||
edges = (tuple(sorted((u, v))) for u, v in H.edges())
|
||||
betweenness = dict.fromkeys(edges, 0.0)
|
||||
if normalized:
|
||||
nb = (N - 1.0) * (N - 2.0) # normalization factor
|
||||
else:
|
||||
nb = 2.0
|
||||
for row, (e) in flow_matrix_row(H, weight=weight, dtype=dtype, solver=solver):
|
||||
pos = dict(zip(row.argsort()[::-1], range(1, N + 1)))
|
||||
for i in range(N):
|
||||
betweenness[e] += (i + 1 - pos[i]) * row.item(i)
|
||||
betweenness[e] += (N - i - pos[i]) * row.item(i)
|
||||
betweenness[e] /= nb
|
||||
return {(ordering[s], ordering[t]): b for (s, t), b in betweenness.items()}
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
"""Current-flow betweenness centrality measures for subsets of nodes."""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.centrality.flow_matrix import flow_matrix_row
|
||||
from networkx.utils import not_implemented_for, reverse_cuthill_mckee_ordering
|
||||
|
||||
__all__ = [
|
||||
"current_flow_betweenness_centrality_subset",
|
||||
"edge_current_flow_betweenness_centrality_subset",
|
||||
]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def current_flow_betweenness_centrality_subset(
|
||||
G, sources, targets, normalized=True, weight=None, dtype=float, solver="lu"
|
||||
):
|
||||
r"""Compute current-flow betweenness centrality for subsets of nodes.
|
||||
|
||||
Current-flow betweenness centrality uses an electrical current
|
||||
model for information spreading in contrast to betweenness
|
||||
centrality which uses shortest paths.
|
||||
|
||||
Current-flow betweenness centrality is also known as
|
||||
random-walk betweenness centrality [2]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph
|
||||
|
||||
sources: list of nodes
|
||||
Nodes to use as sources for current
|
||||
|
||||
targets: list of nodes
|
||||
Nodes to use as sinks for current
|
||||
|
||||
normalized : bool, optional (default=True)
|
||||
If True the betweenness values are normalized by b=b/(n-1)(n-2) where
|
||||
n is the number of nodes in G.
|
||||
|
||||
weight : string or None, optional (default=None)
|
||||
Key for edge data used as the edge weight.
|
||||
If None, then use 1 as each edge weight.
|
||||
The weight reflects the capacity or the strength of the
|
||||
edge.
|
||||
|
||||
dtype: data type (float)
|
||||
Default data type for internal matrices.
|
||||
Set to np.float32 for lower memory consumption.
|
||||
|
||||
solver: string (default='lu')
|
||||
Type of linear solver to use for computing the flow matrix.
|
||||
Options are "full" (uses most memory), "lu" (recommended), and
|
||||
"cg" (uses least memory).
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with betweenness centrality as the value.
|
||||
|
||||
See Also
|
||||
--------
|
||||
approximate_current_flow_betweenness_centrality
|
||||
betweenness_centrality
|
||||
edge_betweenness_centrality
|
||||
edge_current_flow_betweenness_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
Current-flow betweenness can be computed in $O(I(n-1)+mn \log n)$
|
||||
time [1]_, where $I(n-1)$ is the time needed to compute the
|
||||
inverse Laplacian. For a full matrix this is $O(n^3)$ but using
|
||||
sparse methods you can achieve $O(nm{\sqrt k})$ where $k$ is the
|
||||
Laplacian matrix condition number.
|
||||
|
||||
The space required is $O(nw)$ where $w$ is the width of the sparse
|
||||
Laplacian matrix. Worse case is $w=n$ for $O(n^2)$.
|
||||
|
||||
If the edges have a 'weight' attribute they will be used as
|
||||
weights in this algorithm. Unspecified weights are set to 1.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Centrality Measures Based on Current Flow.
|
||||
Ulrik Brandes and Daniel Fleischer,
|
||||
Proc. 22nd Symp. Theoretical Aspects of Computer Science (STACS '05).
|
||||
LNCS 3404, pp. 533-544. Springer-Verlag, 2005.
|
||||
https://doi.org/10.1007/978-3-540-31856-9_44
|
||||
|
||||
.. [2] A measure of betweenness centrality based on random walks,
|
||||
M. E. J. Newman, Social Networks 27, 39-54 (2005).
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
from networkx.utils import reverse_cuthill_mckee_ordering
|
||||
|
||||
if not nx.is_connected(G):
|
||||
raise nx.NetworkXError("Graph not connected.")
|
||||
N = G.number_of_nodes()
|
||||
ordering = list(reverse_cuthill_mckee_ordering(G))
|
||||
# make a copy with integer labels according to rcm ordering
|
||||
# this could be done without a copy if we really wanted to
|
||||
mapping = dict(zip(ordering, range(N)))
|
||||
H = nx.relabel_nodes(G, mapping)
|
||||
betweenness = dict.fromkeys(H, 0.0) # b[n]=0 for n in H
|
||||
for row, (s, t) in flow_matrix_row(H, weight=weight, dtype=dtype, solver=solver):
|
||||
for ss in sources:
|
||||
i = mapping[ss]
|
||||
for tt in targets:
|
||||
j = mapping[tt]
|
||||
betweenness[s] += 0.5 * abs(row.item(i) - row.item(j))
|
||||
betweenness[t] += 0.5 * abs(row.item(i) - row.item(j))
|
||||
if normalized:
|
||||
nb = (N - 1.0) * (N - 2.0) # normalization factor
|
||||
else:
|
||||
nb = 2.0
|
||||
for node in H:
|
||||
betweenness[node] = betweenness[node] / nb + 1.0 / (2 - N)
|
||||
return {ordering[node]: value for node, value in betweenness.items()}
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def edge_current_flow_betweenness_centrality_subset(
|
||||
G, sources, targets, normalized=True, weight=None, dtype=float, solver="lu"
|
||||
):
|
||||
r"""Compute current-flow betweenness centrality for edges using subsets
|
||||
of nodes.
|
||||
|
||||
Current-flow betweenness centrality uses an electrical current
|
||||
model for information spreading in contrast to betweenness
|
||||
centrality which uses shortest paths.
|
||||
|
||||
Current-flow betweenness centrality is also known as
|
||||
random-walk betweenness centrality [2]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph
|
||||
|
||||
sources: list of nodes
|
||||
Nodes to use as sources for current
|
||||
|
||||
targets: list of nodes
|
||||
Nodes to use as sinks for current
|
||||
|
||||
normalized : bool, optional (default=True)
|
||||
If True the betweenness values are normalized by b=b/(n-1)(n-2) where
|
||||
n is the number of nodes in G.
|
||||
|
||||
weight : string or None, optional (default=None)
|
||||
Key for edge data used as the edge weight.
|
||||
If None, then use 1 as each edge weight.
|
||||
The weight reflects the capacity or the strength of the
|
||||
edge.
|
||||
|
||||
dtype: data type (float)
|
||||
Default data type for internal matrices.
|
||||
Set to np.float32 for lower memory consumption.
|
||||
|
||||
solver: string (default='lu')
|
||||
Type of linear solver to use for computing the flow matrix.
|
||||
Options are "full" (uses most memory), "lu" (recommended), and
|
||||
"cg" (uses least memory).
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dict
|
||||
Dictionary of edge tuples with betweenness centrality as the value.
|
||||
|
||||
See Also
|
||||
--------
|
||||
betweenness_centrality
|
||||
edge_betweenness_centrality
|
||||
current_flow_betweenness_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
Current-flow betweenness can be computed in $O(I(n-1)+mn \log n)$
|
||||
time [1]_, where $I(n-1)$ is the time needed to compute the
|
||||
inverse Laplacian. For a full matrix this is $O(n^3)$ but using
|
||||
sparse methods you can achieve $O(nm{\sqrt k})$ where $k$ is the
|
||||
Laplacian matrix condition number.
|
||||
|
||||
The space required is $O(nw)$ where $w$ is the width of the sparse
|
||||
Laplacian matrix. Worse case is $w=n$ for $O(n^2)$.
|
||||
|
||||
If the edges have a 'weight' attribute they will be used as
|
||||
weights in this algorithm. Unspecified weights are set to 1.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Centrality Measures Based on Current Flow.
|
||||
Ulrik Brandes and Daniel Fleischer,
|
||||
Proc. 22nd Symp. Theoretical Aspects of Computer Science (STACS '05).
|
||||
LNCS 3404, pp. 533-544. Springer-Verlag, 2005.
|
||||
https://doi.org/10.1007/978-3-540-31856-9_44
|
||||
|
||||
.. [2] A measure of betweenness centrality based on random walks,
|
||||
M. E. J. Newman, Social Networks 27, 39-54 (2005).
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
if not nx.is_connected(G):
|
||||
raise nx.NetworkXError("Graph not connected.")
|
||||
N = G.number_of_nodes()
|
||||
ordering = list(reverse_cuthill_mckee_ordering(G))
|
||||
# make a copy with integer labels according to rcm ordering
|
||||
# this could be done without a copy if we really wanted to
|
||||
mapping = dict(zip(ordering, range(N)))
|
||||
H = nx.relabel_nodes(G, mapping)
|
||||
edges = (tuple(sorted((u, v))) for u, v in H.edges())
|
||||
betweenness = dict.fromkeys(edges, 0.0)
|
||||
if normalized:
|
||||
nb = (N - 1.0) * (N - 2.0) # normalization factor
|
||||
else:
|
||||
nb = 2.0
|
||||
for row, (e) in flow_matrix_row(H, weight=weight, dtype=dtype, solver=solver):
|
||||
for ss in sources:
|
||||
i = mapping[ss]
|
||||
for tt in targets:
|
||||
j = mapping[tt]
|
||||
betweenness[e] += 0.5 * abs(row.item(i) - row.item(j))
|
||||
betweenness[e] /= nb
|
||||
return {(ordering[s], ordering[t]): value for (s, t), value in betweenness.items()}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
"""Current-flow closeness centrality measures."""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.centrality.flow_matrix import (
|
||||
CGInverseLaplacian,
|
||||
FullInverseLaplacian,
|
||||
SuperLUInverseLaplacian,
|
||||
)
|
||||
from networkx.utils import not_implemented_for, reverse_cuthill_mckee_ordering
|
||||
|
||||
__all__ = ["current_flow_closeness_centrality", "information_centrality"]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def current_flow_closeness_centrality(G, weight=None, dtype=float, solver="lu"):
|
||||
"""Compute current-flow closeness centrality for nodes.
|
||||
|
||||
Current-flow closeness centrality is variant of closeness
|
||||
centrality based on effective resistance between nodes in
|
||||
a network. This metric is also known as information centrality.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph.
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
If None, all edge weights are considered equal.
|
||||
Otherwise holds the name of the edge attribute used as weight.
|
||||
The weight reflects the capacity or the strength of the
|
||||
edge.
|
||||
|
||||
dtype: data type (default=float)
|
||||
Default data type for internal matrices.
|
||||
Set to np.float32 for lower memory consumption.
|
||||
|
||||
solver: string (default='lu')
|
||||
Type of linear solver to use for computing the flow matrix.
|
||||
Options are "full" (uses most memory), "lu" (recommended), and
|
||||
"cg" (uses least memory).
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with current flow closeness centrality as the value.
|
||||
|
||||
See Also
|
||||
--------
|
||||
closeness_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
The algorithm is from Brandes [1]_.
|
||||
|
||||
See also [2]_ for the original definition of information centrality.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Ulrik Brandes and Daniel Fleischer,
|
||||
Centrality Measures Based on Current Flow.
|
||||
Proc. 22nd Symp. Theoretical Aspects of Computer Science (STACS '05).
|
||||
LNCS 3404, pp. 533-544. Springer-Verlag, 2005.
|
||||
https://doi.org/10.1007/978-3-540-31856-9_44
|
||||
|
||||
.. [2] Karen Stephenson and Marvin Zelen:
|
||||
Rethinking centrality: Methods and examples.
|
||||
Social Networks 11(1):1-37, 1989.
|
||||
https://doi.org/10.1016/0378-8733(89)90016-6
|
||||
"""
|
||||
if not nx.is_connected(G):
|
||||
raise nx.NetworkXError("Graph not connected.")
|
||||
solvername = {
|
||||
"full": FullInverseLaplacian,
|
||||
"lu": SuperLUInverseLaplacian,
|
||||
"cg": CGInverseLaplacian,
|
||||
}
|
||||
N = G.number_of_nodes()
|
||||
ordering = list(reverse_cuthill_mckee_ordering(G))
|
||||
# make a copy with integer labels according to rcm ordering
|
||||
# this could be done without a copy if we really wanted to
|
||||
H = nx.relabel_nodes(G, dict(zip(ordering, range(N))))
|
||||
betweenness = dict.fromkeys(H, 0.0) # b[n]=0 for n in H
|
||||
N = H.number_of_nodes()
|
||||
L = nx.laplacian_matrix(H, nodelist=range(N), weight=weight).asformat("csc")
|
||||
L = L.astype(dtype)
|
||||
C2 = solvername[solver](L, width=1, dtype=dtype) # initialize solver
|
||||
for v in H:
|
||||
col = C2.get_row(v)
|
||||
for w in H:
|
||||
betweenness[v] += col.item(v) - 2 * col.item(w)
|
||||
betweenness[w] += col.item(v)
|
||||
return {ordering[node]: 1 / value for node, value in betweenness.items()}
|
||||
|
||||
|
||||
information_centrality = current_flow_closeness_centrality
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
"""Degree centrality measures."""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils.decorators import not_implemented_for
|
||||
|
||||
__all__ = ["degree_centrality", "in_degree_centrality", "out_degree_centrality"]
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def degree_centrality(G):
|
||||
"""Compute the degree centrality for nodes.
|
||||
|
||||
The degree centrality for a node v is the fraction of nodes it
|
||||
is connected to.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A networkx graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with degree centrality as the value.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)])
|
||||
>>> nx.degree_centrality(G)
|
||||
{0: 1.0, 1: 1.0, 2: 0.6666666666666666, 3: 0.6666666666666666}
|
||||
|
||||
See Also
|
||||
--------
|
||||
betweenness_centrality, load_centrality, eigenvector_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
The degree centrality values are normalized by dividing by the maximum
|
||||
possible degree in a simple graph n-1 where n is the number of nodes in G.
|
||||
|
||||
For multigraphs or graphs with self loops the maximum degree might
|
||||
be higher than n-1 and values of degree centrality greater than 1
|
||||
are possible.
|
||||
"""
|
||||
if len(G) <= 1:
|
||||
return {n: 1 for n in G}
|
||||
|
||||
s = 1.0 / (len(G) - 1.0)
|
||||
centrality = {n: d * s for n, d in G.degree()}
|
||||
return centrality
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
@nx._dispatchable
|
||||
def in_degree_centrality(G):
|
||||
"""Compute the in-degree centrality for nodes.
|
||||
|
||||
The in-degree centrality for a node v is the fraction of nodes its
|
||||
incoming edges are connected to.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with in-degree centrality as values.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If G is undirected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.DiGraph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)])
|
||||
>>> nx.in_degree_centrality(G)
|
||||
{0: 0.0, 1: 0.3333333333333333, 2: 0.6666666666666666, 3: 0.6666666666666666}
|
||||
|
||||
See Also
|
||||
--------
|
||||
degree_centrality, out_degree_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
The degree centrality values are normalized by dividing by the maximum
|
||||
possible degree in a simple graph n-1 where n is the number of nodes in G.
|
||||
|
||||
For multigraphs or graphs with self loops the maximum degree might
|
||||
be higher than n-1 and values of degree centrality greater than 1
|
||||
are possible.
|
||||
"""
|
||||
if len(G) <= 1:
|
||||
return {n: 1 for n in G}
|
||||
|
||||
s = 1.0 / (len(G) - 1.0)
|
||||
centrality = {n: d * s for n, d in G.in_degree()}
|
||||
return centrality
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
@nx._dispatchable
|
||||
def out_degree_centrality(G):
|
||||
"""Compute the out-degree centrality for nodes.
|
||||
|
||||
The out-degree centrality for a node v is the fraction of nodes its
|
||||
outgoing edges are connected to.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with out-degree centrality as values.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If G is undirected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.DiGraph([(0, 1), (0, 2), (0, 3), (1, 2), (1, 3)])
|
||||
>>> nx.out_degree_centrality(G)
|
||||
{0: 1.0, 1: 0.6666666666666666, 2: 0.0, 3: 0.0}
|
||||
|
||||
See Also
|
||||
--------
|
||||
degree_centrality, in_degree_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
The degree centrality values are normalized by dividing by the maximum
|
||||
possible degree in a simple graph n-1 where n is the number of nodes in G.
|
||||
|
||||
For multigraphs or graphs with self loops the maximum degree might
|
||||
be higher than n-1 and values of degree centrality greater than 1
|
||||
are possible.
|
||||
"""
|
||||
if len(G) <= 1:
|
||||
return {n: 1 for n in G}
|
||||
|
||||
s = 1.0 / (len(G) - 1.0)
|
||||
centrality = {n: d * s for n, d in G.out_degree()}
|
||||
return centrality
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
from itertools import combinations
|
||||
|
||||
import networkx as nx
|
||||
|
||||
__all__ = ["dispersion"]
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def dispersion(G, u=None, v=None, normalized=True, alpha=1.0, b=0.0, c=0.0):
|
||||
r"""Calculate dispersion between `u` and `v` in `G`.
|
||||
|
||||
A link between two actors (`u` and `v`) has a high dispersion when their
|
||||
mutual ties (`s` and `t`) are not well connected with each other.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph.
|
||||
u : node, optional
|
||||
The source for the dispersion score (e.g. ego node of the network).
|
||||
v : node, optional
|
||||
The target of the dispersion score if specified.
|
||||
normalized : bool
|
||||
If True (default) normalize by the embeddedness of the nodes (u and v).
|
||||
alpha, b, c : float
|
||||
Parameters for the normalization procedure. When `normalized` is True,
|
||||
the dispersion value is normalized by::
|
||||
|
||||
result = ((dispersion + b) ** alpha) / (embeddedness + c)
|
||||
|
||||
as long as the denominator is nonzero.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
If u (v) is specified, returns a dictionary of nodes with dispersion
|
||||
score for all "target" ("source") nodes. If neither u nor v is
|
||||
specified, returns a dictionary of dictionaries for all nodes 'u' in the
|
||||
graph with a dispersion score for each node 'v'.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This implementation follows Lars Backstrom and Jon Kleinberg [1]_. Typical
|
||||
usage would be to run dispersion on the ego network $G_u$ if $u$ were
|
||||
specified. Running :func:`dispersion` with neither $u$ nor $v$ specified
|
||||
can take some time to complete.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Romantic Partnerships and the Dispersion of Social Ties:
|
||||
A Network Analysis of Relationship Status on Facebook.
|
||||
Lars Backstrom, Jon Kleinberg.
|
||||
https://arxiv.org/pdf/1310.6753v1.pdf
|
||||
|
||||
"""
|
||||
|
||||
def _dispersion(G_u, u, v):
|
||||
"""dispersion for all nodes 'v' in a ego network G_u of node 'u'"""
|
||||
u_nbrs = set(G_u[u])
|
||||
ST = {n for n in G_u[v] if n in u_nbrs}
|
||||
set_uv = {u, v}
|
||||
# all possible ties of connections that u and b share
|
||||
possib = combinations(ST, 2)
|
||||
total = 0
|
||||
for s, t in possib:
|
||||
# neighbors of s that are in G_u, not including u and v
|
||||
nbrs_s = u_nbrs.intersection(G_u[s]) - set_uv
|
||||
# s and t are not directly connected
|
||||
if t not in nbrs_s:
|
||||
# s and t do not share a connection
|
||||
if nbrs_s.isdisjoint(G_u[t]):
|
||||
# tick for disp(u, v)
|
||||
total += 1
|
||||
# neighbors that u and v share
|
||||
embeddedness = len(ST)
|
||||
|
||||
dispersion_val = total
|
||||
if normalized:
|
||||
dispersion_val = (total + b) ** alpha
|
||||
if embeddedness + c != 0:
|
||||
dispersion_val /= embeddedness + c
|
||||
|
||||
return dispersion_val
|
||||
|
||||
if u is None:
|
||||
# v and u are not specified
|
||||
if v is None:
|
||||
results = {n: {} for n in G}
|
||||
for u in G:
|
||||
for v in G[u]:
|
||||
results[u][v] = _dispersion(G, u, v)
|
||||
# u is not specified, but v is
|
||||
else:
|
||||
results = dict.fromkeys(G[v], {})
|
||||
for u in G[v]:
|
||||
results[u] = _dispersion(G, v, u)
|
||||
else:
|
||||
# u is specified with no target v
|
||||
if v is None:
|
||||
results = dict.fromkeys(G[u], {})
|
||||
for v in G[u]:
|
||||
results[v] = _dispersion(G, u, v)
|
||||
# both u and v are specified
|
||||
else:
|
||||
results = _dispersion(G, u, v)
|
||||
|
||||
return results
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
"""Functions for computing eigenvector centrality."""
|
||||
|
||||
import math
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import not_implemented_for
|
||||
|
||||
__all__ = ["eigenvector_centrality", "eigenvector_centrality_numpy"]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def eigenvector_centrality(G, max_iter=100, tol=1.0e-6, nstart=None, weight=None):
|
||||
r"""Compute the eigenvector centrality for the graph G.
|
||||
|
||||
Eigenvector centrality computes the centrality for a node by adding
|
||||
the centrality of its predecessors. The centrality for node $i$ is the
|
||||
$i$-th element of a left eigenvector associated with the eigenvalue $\lambda$
|
||||
of maximum modulus that is positive. Such an eigenvector $x$ is
|
||||
defined up to a multiplicative constant by the equation
|
||||
|
||||
.. math::
|
||||
|
||||
\lambda x^T = x^T A,
|
||||
|
||||
where $A$ is the adjacency matrix of the graph G. By definition of
|
||||
row-column product, the equation above is equivalent to
|
||||
|
||||
.. math::
|
||||
|
||||
\lambda x_i = \sum_{j\to i}x_j.
|
||||
|
||||
That is, adding the eigenvector centralities of the predecessors of
|
||||
$i$ one obtains the eigenvector centrality of $i$ multiplied by
|
||||
$\lambda$. In the case of undirected graphs, $x$ also solves the familiar
|
||||
right-eigenvector equation $Ax = \lambda x$.
|
||||
|
||||
By virtue of the Perron–Frobenius theorem [1]_, if G is strongly
|
||||
connected there is a unique eigenvector $x$, and all its entries
|
||||
are strictly positive.
|
||||
|
||||
If G is not strongly connected there might be several left
|
||||
eigenvectors associated with $\lambda$, and some of their elements
|
||||
might be zero.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A networkx graph.
|
||||
|
||||
max_iter : integer, optional (default=100)
|
||||
Maximum number of power iterations.
|
||||
|
||||
tol : float, optional (default=1.0e-6)
|
||||
Error tolerance (in Euclidean norm) used to check convergence in
|
||||
power iteration.
|
||||
|
||||
nstart : dictionary, optional (default=None)
|
||||
Starting value of power iteration for each node. Must have a nonzero
|
||||
projection on the desired eigenvector for the power method to converge.
|
||||
If None, this implementation uses an all-ones vector, which is a safe
|
||||
choice.
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
If None, all edge weights are considered equal. Otherwise holds the
|
||||
name of the edge attribute used as weight. In this measure the
|
||||
weight is interpreted as the connection strength.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with eigenvector centrality as the value. The
|
||||
associated vector has unit Euclidean norm and the values are
|
||||
nonegative.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.path_graph(4)
|
||||
>>> centrality = nx.eigenvector_centrality(G)
|
||||
>>> sorted((v, f"{c:0.2f}") for v, c in centrality.items())
|
||||
[(0, '0.37'), (1, '0.60'), (2, '0.60'), (3, '0.37')]
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXPointlessConcept
|
||||
If the graph G is the null graph.
|
||||
|
||||
NetworkXError
|
||||
If each value in `nstart` is zero.
|
||||
|
||||
PowerIterationFailedConvergence
|
||||
If the algorithm fails to converge to the specified tolerance
|
||||
within the specified number of iterations of the power iteration
|
||||
method.
|
||||
|
||||
See Also
|
||||
--------
|
||||
eigenvector_centrality_numpy
|
||||
:func:`~networkx.algorithms.link_analysis.pagerank_alg.pagerank`
|
||||
:func:`~networkx.algorithms.link_analysis.hits_alg.hits`
|
||||
|
||||
Notes
|
||||
-----
|
||||
Eigenvector centrality was introduced by Landau [2]_ for chess
|
||||
tournaments. It was later rediscovered by Wei [3]_ and then
|
||||
popularized by Kendall [4]_ in the context of sport ranking. Berge
|
||||
introduced a general definition for graphs based on social connections
|
||||
[5]_. Bonacich [6]_ reintroduced again eigenvector centrality and made
|
||||
it popular in link analysis.
|
||||
|
||||
This function computes the left dominant eigenvector, which corresponds
|
||||
to adding the centrality of predecessors: this is the usual approach.
|
||||
To add the centrality of successors first reverse the graph with
|
||||
``G.reverse()``.
|
||||
|
||||
The implementation uses power iteration [7]_ to compute a dominant
|
||||
eigenvector starting from the provided vector `nstart`. Convergence is
|
||||
guaranteed as long as `nstart` has a nonzero projection on a dominant
|
||||
eigenvector, which certainly happens using the default value.
|
||||
|
||||
The method stops when the change in the computed vector between two
|
||||
iterations is smaller than an error tolerance of ``G.number_of_nodes()
|
||||
* tol`` or after ``max_iter`` iterations, but in the second case it
|
||||
raises an exception.
|
||||
|
||||
This implementation uses $(A + I)$ rather than the adjacency matrix
|
||||
$A$ because the change preserves eigenvectors, but it shifts the
|
||||
spectrum, thus guaranteeing convergence even for networks with
|
||||
negative eigenvalues of maximum modulus.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Abraham Berman and Robert J. Plemmons.
|
||||
"Nonnegative Matrices in the Mathematical Sciences."
|
||||
Classics in Applied Mathematics. SIAM, 1994.
|
||||
|
||||
.. [2] Edmund Landau.
|
||||
"Zur relativen Wertbemessung der Turnierresultate."
|
||||
Deutsches Wochenschach, 11:366–369, 1895.
|
||||
|
||||
.. [3] Teh-Hsing Wei.
|
||||
"The Algebraic Foundations of Ranking Theory."
|
||||
PhD thesis, University of Cambridge, 1952.
|
||||
|
||||
.. [4] Maurice G. Kendall.
|
||||
"Further contributions to the theory of paired comparisons."
|
||||
Biometrics, 11(1):43–62, 1955.
|
||||
https://www.jstor.org/stable/3001479
|
||||
|
||||
.. [5] Claude Berge
|
||||
"Théorie des graphes et ses applications."
|
||||
Dunod, Paris, France, 1958.
|
||||
|
||||
.. [6] Phillip Bonacich.
|
||||
"Technique for analyzing overlapping memberships."
|
||||
Sociological Methodology, 4:176–185, 1972.
|
||||
https://www.jstor.org/stable/270732
|
||||
|
||||
.. [7] Power iteration:: https://en.wikipedia.org/wiki/Power_iteration
|
||||
|
||||
"""
|
||||
if len(G) == 0:
|
||||
raise nx.NetworkXPointlessConcept(
|
||||
"cannot compute centrality for the null graph"
|
||||
)
|
||||
# If no initial vector is provided, start with the all-ones vector.
|
||||
if nstart is None:
|
||||
nstart = {v: 1 for v in G}
|
||||
if all(v == 0 for v in nstart.values()):
|
||||
raise nx.NetworkXError("initial vector cannot have all zero values")
|
||||
# Normalize the initial vector so that each entry is in [0, 1]. This is
|
||||
# guaranteed to never have a divide-by-zero error by the previous line.
|
||||
nstart_sum = sum(nstart.values())
|
||||
x = {k: v / nstart_sum for k, v in nstart.items()}
|
||||
nnodes = G.number_of_nodes()
|
||||
# make up to max_iter iterations
|
||||
for _ in range(max_iter):
|
||||
xlast = x
|
||||
x = xlast.copy() # Start with xlast times I to iterate with (A+I)
|
||||
# do the multiplication y^T = x^T A (left eigenvector)
|
||||
for n in x:
|
||||
for nbr in G[n]:
|
||||
w = G[n][nbr].get(weight, 1) if weight else 1
|
||||
x[nbr] += xlast[n] * w
|
||||
# Normalize the vector. The normalization denominator `norm`
|
||||
# should never be zero by the Perron--Frobenius
|
||||
# theorem. However, in case it is due to numerical error, we
|
||||
# assume the norm to be one instead.
|
||||
norm = math.hypot(*x.values()) or 1
|
||||
x = {k: v / norm for k, v in x.items()}
|
||||
# Check for convergence (in the L_1 norm).
|
||||
if sum(abs(x[n] - xlast[n]) for n in x) < nnodes * tol:
|
||||
return x
|
||||
raise nx.PowerIterationFailedConvergence(max_iter)
|
||||
|
||||
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def eigenvector_centrality_numpy(G, weight=None, max_iter=50, tol=0):
|
||||
r"""Compute the eigenvector centrality for the graph `G`.
|
||||
|
||||
Eigenvector centrality computes the centrality for a node by adding
|
||||
the centrality of its predecessors. The centrality for node $i$ is the
|
||||
$i$-th element of a left eigenvector associated with the eigenvalue $\lambda$
|
||||
of maximum modulus that is positive. Such an eigenvector $x$ is
|
||||
defined up to a multiplicative constant by the equation
|
||||
|
||||
.. math::
|
||||
|
||||
\lambda x^T = x^T A,
|
||||
|
||||
where $A$ is the adjacency matrix of the graph `G`. By definition of
|
||||
row-column product, the equation above is equivalent to
|
||||
|
||||
.. math::
|
||||
|
||||
\lambda x_i = \sum_{j\to i}x_j.
|
||||
|
||||
That is, adding the eigenvector centralities of the predecessors of
|
||||
$i$ one obtains the eigenvector centrality of $i$ multiplied by
|
||||
$\lambda$. In the case of undirected graphs, $x$ also solves the familiar
|
||||
right-eigenvector equation $Ax = \lambda x$.
|
||||
|
||||
By virtue of the Perron--Frobenius theorem [1]_, if `G` is (strongly)
|
||||
connected, there is a unique eigenvector $x$, and all its entries
|
||||
are strictly positive.
|
||||
|
||||
However, if `G` is not (strongly) connected, there might be several left
|
||||
eigenvectors associated with $\lambda$, and some of their elements
|
||||
might be zero.
|
||||
Depending on the method used to choose eigenvectors, round-off error can affect
|
||||
which of the infinitely many eigenvectors is reported.
|
||||
This can lead to inconsistent results for the same graph,
|
||||
which the underlying implementation is not robust to.
|
||||
For this reason, only (strongly) connected graphs are accepted.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A connected NetworkX graph.
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
If ``None``, all edge weights are considered equal. Otherwise holds the
|
||||
name of the edge attribute used as weight. In this measure the
|
||||
weight is interpreted as the connection strength.
|
||||
|
||||
max_iter : integer, optional (default=50)
|
||||
Maximum number of Arnoldi update iterations allowed.
|
||||
|
||||
tol : float, optional (default=0)
|
||||
Relative accuracy for eigenvalues (stopping criterion).
|
||||
The default value of 0 implies machine precision.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dict of nodes
|
||||
Dictionary of nodes with eigenvector centrality as the value. The
|
||||
associated vector has unit Euclidean norm and the values are
|
||||
nonnegative.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.path_graph(4)
|
||||
>>> centrality = nx.eigenvector_centrality_numpy(G)
|
||||
>>> print([f"{node} {centrality[node]:0.2f}" for node in centrality])
|
||||
['0 0.37', '1 0.60', '2 0.60', '3 0.37']
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXPointlessConcept
|
||||
If the graph `G` is the null graph.
|
||||
|
||||
ArpackNoConvergence
|
||||
When the requested convergence is not obtained. The currently
|
||||
converged eigenvalues and eigenvectors can be found as
|
||||
eigenvalues and eigenvectors attributes of the exception object.
|
||||
|
||||
AmbiguousSolution
|
||||
If `G` is not connected.
|
||||
|
||||
See Also
|
||||
--------
|
||||
:func:`scipy.sparse.linalg.eigs`
|
||||
eigenvector_centrality
|
||||
:func:`~networkx.algorithms.link_analysis.pagerank_alg.pagerank`
|
||||
:func:`~networkx.algorithms.link_analysis.hits_alg.hits`
|
||||
|
||||
Notes
|
||||
-----
|
||||
Eigenvector centrality was introduced by Landau [2]_ for chess
|
||||
tournaments. It was later rediscovered by Wei [3]_ and then
|
||||
popularized by Kendall [4]_ in the context of sport ranking. Berge
|
||||
introduced a general definition for graphs based on social connections
|
||||
[5]_. Bonacich [6]_ reintroduced again eigenvector centrality and made
|
||||
it popular in link analysis.
|
||||
|
||||
This function computes the left dominant eigenvector, which corresponds
|
||||
to adding the centrality of predecessors: this is the usual approach.
|
||||
To add the centrality of successors first reverse the graph with
|
||||
``G.reverse()``.
|
||||
|
||||
This implementation uses the
|
||||
:func:`SciPy sparse eigenvalue solver<scipy.sparse.linalg.eigs>` (ARPACK)
|
||||
to find the largest eigenvalue/eigenvector pair using Arnoldi iterations
|
||||
[7]_.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Abraham Berman and Robert J. Plemmons.
|
||||
"Nonnegative Matrices in the Mathematical Sciences".
|
||||
Classics in Applied Mathematics. SIAM, 1994.
|
||||
|
||||
.. [2] Edmund Landau.
|
||||
"Zur relativen Wertbemessung der Turnierresultate".
|
||||
Deutsches Wochenschach, 11:366--369, 1895.
|
||||
|
||||
.. [3] Teh-Hsing Wei.
|
||||
"The Algebraic Foundations of Ranking Theory".
|
||||
PhD thesis, University of Cambridge, 1952.
|
||||
|
||||
.. [4] Maurice G. Kendall.
|
||||
"Further contributions to the theory of paired comparisons".
|
||||
Biometrics, 11(1):43--62, 1955.
|
||||
https://www.jstor.org/stable/3001479
|
||||
|
||||
.. [5] Claude Berge.
|
||||
"Théorie des graphes et ses applications".
|
||||
Dunod, Paris, France, 1958.
|
||||
|
||||
.. [6] Phillip Bonacich.
|
||||
"Technique for analyzing overlapping memberships".
|
||||
Sociological Methodology, 4:176--185, 1972.
|
||||
https://www.jstor.org/stable/270732
|
||||
|
||||
.. [7] Arnoldi, W. E. (1951).
|
||||
"The principle of minimized iterations in the solution of the matrix eigenvalue problem".
|
||||
Quarterly of Applied Mathematics. 9 (1): 17--29.
|
||||
https://doi.org/10.1090/qam/42792
|
||||
"""
|
||||
import numpy as np
|
||||
import scipy as sp
|
||||
|
||||
if len(G) == 0:
|
||||
raise nx.NetworkXPointlessConcept(
|
||||
"cannot compute centrality for the null graph"
|
||||
)
|
||||
connected = nx.is_strongly_connected(G) if G.is_directed() else nx.is_connected(G)
|
||||
if not connected: # See gh-6888.
|
||||
raise nx.AmbiguousSolution(
|
||||
"`eigenvector_centrality_numpy` does not give consistent results for disconnected graphs"
|
||||
)
|
||||
M = nx.to_scipy_sparse_array(G, nodelist=list(G), weight=weight, dtype=float)
|
||||
_, eigenvector = sp.sparse.linalg.eigs(
|
||||
M.T, k=1, which="LR", maxiter=max_iter, tol=tol
|
||||
)
|
||||
largest = eigenvector.flatten().real
|
||||
norm = np.sign(largest.sum()) * sp.linalg.norm(largest)
|
||||
return dict(zip(G, (largest / norm).tolist()))
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
# Helpers for current-flow betweenness and current-flow closeness
|
||||
# Lazy computations for inverse Laplacian and flow-matrix rows.
|
||||
import networkx as nx
|
||||
|
||||
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def flow_matrix_row(G, weight=None, dtype=float, solver="lu"):
|
||||
# Generate a row of the current-flow matrix
|
||||
import numpy as np
|
||||
|
||||
solvername = {
|
||||
"full": FullInverseLaplacian,
|
||||
"lu": SuperLUInverseLaplacian,
|
||||
"cg": CGInverseLaplacian,
|
||||
}
|
||||
n = G.number_of_nodes()
|
||||
L = nx.laplacian_matrix(G, nodelist=range(n), weight=weight).asformat("csc")
|
||||
L = L.astype(dtype)
|
||||
C = solvername[solver](L, dtype=dtype) # initialize solver
|
||||
w = C.w # w is the Laplacian matrix width
|
||||
# row-by-row flow matrix
|
||||
for u, v in sorted(sorted((u, v)) for u, v in G.edges()):
|
||||
B = np.zeros(w, dtype=dtype)
|
||||
c = G[u][v].get(weight, 1.0)
|
||||
B[u % w] = c
|
||||
B[v % w] = -c
|
||||
# get only the rows needed in the inverse laplacian
|
||||
# and multiply to get the flow matrix row
|
||||
row = B @ C.get_rows(u, v)
|
||||
yield row, (u, v)
|
||||
|
||||
|
||||
# Class to compute the inverse laplacian only for specified rows
|
||||
# Allows computation of the current-flow matrix without storing entire
|
||||
# inverse laplacian matrix
|
||||
class InverseLaplacian:
|
||||
def __init__(self, L, width=None, dtype=None):
|
||||
global np
|
||||
import numpy as np
|
||||
|
||||
(n, n) = L.shape
|
||||
self.dtype = dtype
|
||||
self.n = n
|
||||
if width is None:
|
||||
self.w = self.width(L)
|
||||
else:
|
||||
self.w = width
|
||||
self.C = np.zeros((self.w, n), dtype=dtype)
|
||||
self.L1 = L[1:, 1:]
|
||||
self.init_solver(L)
|
||||
|
||||
def init_solver(self, L):
|
||||
pass
|
||||
|
||||
def solve(self, r):
|
||||
raise nx.NetworkXError("Implement solver")
|
||||
|
||||
def solve_inverse(self, r):
|
||||
raise nx.NetworkXError("Implement solver")
|
||||
|
||||
def get_rows(self, r1, r2):
|
||||
for r in range(r1, r2 + 1):
|
||||
self.C[r % self.w, 1:] = self.solve_inverse(r)
|
||||
return self.C
|
||||
|
||||
def get_row(self, r):
|
||||
self.C[r % self.w, 1:] = self.solve_inverse(r)
|
||||
return self.C[r % self.w]
|
||||
|
||||
def width(self, L):
|
||||
m = 0
|
||||
for i, row in enumerate(L):
|
||||
w = 0
|
||||
y = np.nonzero(row)[-1]
|
||||
if len(y) > 0:
|
||||
v = y - i
|
||||
w = v.max() - v.min() + 1
|
||||
m = max(w, m)
|
||||
return m
|
||||
|
||||
|
||||
class FullInverseLaplacian(InverseLaplacian):
|
||||
def init_solver(self, L):
|
||||
self.IL = np.zeros(L.shape, dtype=self.dtype)
|
||||
self.IL[1:, 1:] = np.linalg.inv(self.L1.todense())
|
||||
|
||||
def solve(self, rhs):
|
||||
s = np.zeros(rhs.shape, dtype=self.dtype)
|
||||
s = self.IL @ rhs
|
||||
return s
|
||||
|
||||
def solve_inverse(self, r):
|
||||
return self.IL[r, 1:]
|
||||
|
||||
|
||||
class SuperLUInverseLaplacian(InverseLaplacian):
|
||||
def init_solver(self, L):
|
||||
import scipy as sp
|
||||
|
||||
self.lusolve = sp.sparse.linalg.factorized(self.L1.tocsc())
|
||||
|
||||
def solve_inverse(self, r):
|
||||
rhs = np.zeros(self.n, dtype=self.dtype)
|
||||
rhs[r] = 1
|
||||
return self.lusolve(rhs[1:])
|
||||
|
||||
def solve(self, rhs):
|
||||
s = np.zeros(rhs.shape, dtype=self.dtype)
|
||||
s[1:] = self.lusolve(rhs[1:])
|
||||
return s
|
||||
|
||||
|
||||
class CGInverseLaplacian(InverseLaplacian):
|
||||
def init_solver(self, L):
|
||||
global sp
|
||||
import scipy as sp
|
||||
|
||||
ilu = sp.sparse.linalg.spilu(self.L1.tocsc())
|
||||
n = self.n - 1
|
||||
self.M = sp.sparse.linalg.LinearOperator(shape=(n, n), matvec=ilu.solve)
|
||||
|
||||
def solve(self, rhs):
|
||||
s = np.zeros(rhs.shape, dtype=self.dtype)
|
||||
s[1:] = sp.sparse.linalg.cg(self.L1, rhs[1:], M=self.M, atol=0)[0]
|
||||
return s
|
||||
|
||||
def solve_inverse(self, r):
|
||||
rhs = np.zeros(self.n, self.dtype)
|
||||
rhs[r] = 1
|
||||
return sp.sparse.linalg.cg(self.L1, rhs[1:], M=self.M, atol=0)[0]
|
||||
@@ -0,0 +1,787 @@
|
||||
"""Group centrality measures."""
|
||||
|
||||
from copy import deepcopy
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.centrality.betweenness import (
|
||||
_accumulate_endpoints,
|
||||
_single_source_dijkstra_path_basic,
|
||||
_single_source_shortest_path_basic,
|
||||
)
|
||||
from networkx.utils.decorators import not_implemented_for
|
||||
|
||||
__all__ = [
|
||||
"group_betweenness_centrality",
|
||||
"group_closeness_centrality",
|
||||
"group_degree_centrality",
|
||||
"group_in_degree_centrality",
|
||||
"group_out_degree_centrality",
|
||||
"prominent_group",
|
||||
]
|
||||
|
||||
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def group_betweenness_centrality(G, C, normalized=True, weight=None, endpoints=False):
|
||||
r"""Compute the group betweenness centrality for a group of nodes.
|
||||
|
||||
Group betweenness centrality of a group of nodes $C$ is the sum of the
|
||||
fraction of all-pairs shortest paths that pass through any vertex in $C$
|
||||
|
||||
.. math::
|
||||
|
||||
c_B(v) =\sum_{s,t \in V} \frac{\sigma(s, t|v)}{\sigma(s, t)}
|
||||
|
||||
where $V$ is the set of nodes, $\sigma(s, t)$ is the number of
|
||||
shortest $(s, t)$-paths, and $\sigma(s, t|C)$ is the number of
|
||||
those paths passing through some node in group $C$. Note that
|
||||
$(s, t)$ are not members of the group ($V-C$ is the set of nodes
|
||||
in $V$ that are not in $C$).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph.
|
||||
|
||||
C : list or set or list of lists or list of sets
|
||||
A group or a list of groups containing nodes which belong to G, for which group betweenness
|
||||
centrality is to be calculated.
|
||||
|
||||
normalized : bool, optional (default=True)
|
||||
If True, group betweenness is normalized by `1/((|V|-|C|)(|V|-|C|-1))`
|
||||
where `|V|` is the number of nodes in G and `|C|` is the number of nodes in C.
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
If None, all edge weights are considered equal.
|
||||
Otherwise holds the name of the edge attribute used as weight.
|
||||
The weight of an edge is treated as the length or distance between the two sides.
|
||||
|
||||
endpoints : bool, optional (default=False)
|
||||
If True include the endpoints in the shortest path counts.
|
||||
|
||||
Raises
|
||||
------
|
||||
NodeNotFound
|
||||
If node(s) in C are not present in G.
|
||||
|
||||
Returns
|
||||
-------
|
||||
betweenness : list of floats or float
|
||||
If C is a single group then return a float. If C is a list with
|
||||
several groups then return a list of group betweenness centralities.
|
||||
|
||||
See Also
|
||||
--------
|
||||
betweenness_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
Group betweenness centrality is described in [1]_ and its importance discussed in [3]_.
|
||||
The initial implementation of the algorithm is mentioned in [2]_. This function uses
|
||||
an improved algorithm presented in [4]_.
|
||||
|
||||
The number of nodes in the group must be a maximum of n - 2 where `n`
|
||||
is the total number of nodes in the graph.
|
||||
|
||||
For weighted graphs the edge weights must be greater than zero.
|
||||
Zero edge weights can produce an infinite number of equal length
|
||||
paths between pairs of nodes.
|
||||
|
||||
The total number of paths between source and target is counted
|
||||
differently for directed and undirected graphs. Directed paths
|
||||
between "u" and "v" are counted as two possible paths (one each
|
||||
direction) while undirected paths between "u" and "v" are counted
|
||||
as one path. Said another way, the sum in the expression above is
|
||||
over all ``s != t`` for directed graphs and for ``s < t`` for undirected graphs.
|
||||
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] M G Everett and S P Borgatti:
|
||||
The Centrality of Groups and Classes.
|
||||
Journal of Mathematical Sociology. 23(3): 181-201. 1999.
|
||||
http://www.analytictech.com/borgatti/group_centrality.htm
|
||||
.. [2] Ulrik Brandes:
|
||||
On Variants of Shortest-Path Betweenness
|
||||
Centrality and their Generic Computation.
|
||||
Social Networks 30(2):136-145, 2008.
|
||||
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.72.9610&rep=rep1&type=pdf
|
||||
.. [3] Sourav Medya et. al.:
|
||||
Group Centrality Maximization via Network Design.
|
||||
SIAM International Conference on Data Mining, SDM 2018, 126–134.
|
||||
https://sites.cs.ucsb.edu/~arlei/pubs/sdm18.pdf
|
||||
.. [4] Rami Puzis, Yuval Elovici, and Shlomi Dolev.
|
||||
"Fast algorithm for successive computation of group betweenness centrality."
|
||||
https://journals.aps.org/pre/pdf/10.1103/PhysRevE.76.056709
|
||||
|
||||
"""
|
||||
GBC = [] # initialize betweenness
|
||||
list_of_groups = True
|
||||
# check weather C contains one or many groups
|
||||
if any(el in G for el in C):
|
||||
C = [C]
|
||||
list_of_groups = False
|
||||
set_v = {node for group in C for node in group}
|
||||
if set_v - G.nodes: # element(s) of C not in G
|
||||
raise nx.NodeNotFound(f"The node(s) {set_v - G.nodes} are in C but not in G.")
|
||||
|
||||
# pre-processing
|
||||
PB, sigma, D = _group_preprocessing(G, set_v, weight)
|
||||
|
||||
# the algorithm for each group
|
||||
for group in C:
|
||||
group = set(group) # set of nodes in group
|
||||
# initialize the matrices of the sigma and the PB
|
||||
GBC_group = 0
|
||||
sigma_m = deepcopy(sigma)
|
||||
PB_m = deepcopy(PB)
|
||||
sigma_m_v = deepcopy(sigma_m)
|
||||
PB_m_v = deepcopy(PB_m)
|
||||
for v in group:
|
||||
GBC_group += PB_m[v][v]
|
||||
for x in group:
|
||||
for y in group:
|
||||
dxvy = 0
|
||||
dxyv = 0
|
||||
dvxy = 0
|
||||
if not (
|
||||
sigma_m[x][y] == 0 or sigma_m[x][v] == 0 or sigma_m[v][y] == 0
|
||||
):
|
||||
if D[x][v] == D[x][y] + D[y][v]:
|
||||
dxyv = sigma_m[x][y] * sigma_m[y][v] / sigma_m[x][v]
|
||||
if D[x][y] == D[x][v] + D[v][y]:
|
||||
dxvy = sigma_m[x][v] * sigma_m[v][y] / sigma_m[x][y]
|
||||
if D[v][y] == D[v][x] + D[x][y]:
|
||||
dvxy = sigma_m[v][x] * sigma[x][y] / sigma[v][y]
|
||||
sigma_m_v[x][y] = sigma_m[x][y] * (1 - dxvy)
|
||||
PB_m_v[x][y] = PB_m[x][y] - PB_m[x][y] * dxvy
|
||||
if y != v:
|
||||
PB_m_v[x][y] -= PB_m[x][v] * dxyv
|
||||
if x != v:
|
||||
PB_m_v[x][y] -= PB_m[v][y] * dvxy
|
||||
sigma_m, sigma_m_v = sigma_m_v, sigma_m
|
||||
PB_m, PB_m_v = PB_m_v, PB_m
|
||||
|
||||
# endpoints
|
||||
v, c = len(G), len(group)
|
||||
if not endpoints:
|
||||
scale = 0
|
||||
# if the graph is connected then subtract the endpoints from
|
||||
# the count for all the nodes in the graph. else count how many
|
||||
# nodes are connected to the group's nodes and subtract that.
|
||||
if nx.is_directed(G):
|
||||
if nx.is_strongly_connected(G):
|
||||
scale = c * (2 * v - c - 1)
|
||||
elif nx.is_connected(G):
|
||||
scale = c * (2 * v - c - 1)
|
||||
if scale == 0:
|
||||
for group_node1 in group:
|
||||
for node in D[group_node1]:
|
||||
if node != group_node1:
|
||||
if node in group:
|
||||
scale += 1
|
||||
else:
|
||||
scale += 2
|
||||
GBC_group -= scale
|
||||
|
||||
# normalized
|
||||
if normalized:
|
||||
scale = 1 / ((v - c) * (v - c - 1))
|
||||
GBC_group *= scale
|
||||
|
||||
# If undirected than count only the undirected edges
|
||||
elif not G.is_directed():
|
||||
GBC_group /= 2
|
||||
|
||||
GBC.append(GBC_group)
|
||||
if list_of_groups:
|
||||
return GBC
|
||||
return GBC[0]
|
||||
|
||||
|
||||
def _group_preprocessing(G, set_v, weight):
|
||||
sigma = {}
|
||||
delta = {}
|
||||
D = {}
|
||||
betweenness = dict.fromkeys(G, 0)
|
||||
for s in G:
|
||||
if weight is None: # use BFS
|
||||
S, P, sigma[s], D[s] = _single_source_shortest_path_basic(G, s)
|
||||
else: # use Dijkstra's algorithm
|
||||
S, P, sigma[s], D[s] = _single_source_dijkstra_path_basic(G, s, weight)
|
||||
betweenness, delta[s] = _accumulate_endpoints(betweenness, S, P, sigma[s], s)
|
||||
for i in delta[s]: # add the paths from s to i and rescale sigma
|
||||
if s != i:
|
||||
delta[s][i] += 1
|
||||
if weight is not None:
|
||||
sigma[s][i] = sigma[s][i] / 2
|
||||
# building the path betweenness matrix only for nodes that appear in the group
|
||||
PB = dict.fromkeys(G)
|
||||
for group_node1 in set_v:
|
||||
PB[group_node1] = dict.fromkeys(G, 0.0)
|
||||
for group_node2 in set_v:
|
||||
if group_node2 not in D[group_node1]:
|
||||
continue
|
||||
for node in G:
|
||||
# if node is connected to the two group nodes than continue
|
||||
if group_node2 in D[node] and group_node1 in D[node]:
|
||||
if (
|
||||
D[node][group_node2]
|
||||
== D[node][group_node1] + D[group_node1][group_node2]
|
||||
):
|
||||
PB[group_node1][group_node2] += (
|
||||
delta[node][group_node2]
|
||||
* sigma[node][group_node1]
|
||||
* sigma[group_node1][group_node2]
|
||||
/ sigma[node][group_node2]
|
||||
)
|
||||
return PB, sigma, D
|
||||
|
||||
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def prominent_group(
|
||||
G, k, weight=None, C=None, endpoints=False, normalized=True, greedy=False
|
||||
):
|
||||
r"""Find the prominent group of size $k$ in graph $G$. The prominence of the
|
||||
group is evaluated by the group betweenness centrality.
|
||||
|
||||
Group betweenness centrality of a group of nodes $C$ is the sum of the
|
||||
fraction of all-pairs shortest paths that pass through any vertex in $C$
|
||||
|
||||
.. math::
|
||||
|
||||
c_B(v) =\sum_{s,t \in V} \frac{\sigma(s, t|v)}{\sigma(s, t)}
|
||||
|
||||
where $V$ is the set of nodes, $\sigma(s, t)$ is the number of
|
||||
shortest $(s, t)$-paths, and $\sigma(s, t|C)$ is the number of
|
||||
those paths passing through some node in group $C$. Note that
|
||||
$(s, t)$ are not members of the group ($V-C$ is the set of nodes
|
||||
in $V$ that are not in $C$).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph.
|
||||
|
||||
k : int
|
||||
The number of nodes in the group.
|
||||
|
||||
normalized : bool, optional (default=True)
|
||||
If True, group betweenness is normalized by ``1/((|V|-|C|)(|V|-|C|-1))``
|
||||
where ``|V|`` is the number of nodes in G and ``|C|`` is the number of
|
||||
nodes in C.
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
If None, all edge weights are considered equal.
|
||||
Otherwise holds the name of the edge attribute used as weight.
|
||||
The weight of an edge is treated as the length or distance between the two sides.
|
||||
|
||||
endpoints : bool, optional (default=False)
|
||||
If True include the endpoints in the shortest path counts.
|
||||
|
||||
C : list or set, optional (default=None)
|
||||
list of nodes which won't be candidates of the prominent group.
|
||||
|
||||
greedy : bool, optional (default=False)
|
||||
Using a naive greedy algorithm in order to find non-optimal prominent
|
||||
group. For scale free networks the results are negligibly below the optimal
|
||||
results.
|
||||
|
||||
Raises
|
||||
------
|
||||
NodeNotFound
|
||||
If node(s) in C are not present in G.
|
||||
|
||||
Returns
|
||||
-------
|
||||
max_GBC : float
|
||||
The group betweenness centrality of the prominent group.
|
||||
|
||||
max_group : list
|
||||
The list of nodes in the prominent group.
|
||||
|
||||
See Also
|
||||
--------
|
||||
betweenness_centrality, group_betweenness_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
Group betweenness centrality is described in [1]_ and its importance discussed in [3]_.
|
||||
The algorithm is described in [2]_ and is based on techniques mentioned in [4]_.
|
||||
|
||||
The number of nodes in the group must be a maximum of ``n - 2`` where ``n``
|
||||
is the total number of nodes in the graph.
|
||||
|
||||
For weighted graphs the edge weights must be greater than zero.
|
||||
Zero edge weights can produce an infinite number of equal length
|
||||
paths between pairs of nodes.
|
||||
|
||||
The total number of paths between source and target is counted
|
||||
differently for directed and undirected graphs. Directed paths
|
||||
between "u" and "v" are counted as two possible paths (one each
|
||||
direction) while undirected paths between "u" and "v" are counted
|
||||
as one path. Said another way, the sum in the expression above is
|
||||
over all ``s != t`` for directed graphs and for ``s < t`` for undirected graphs.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] M G Everett and S P Borgatti:
|
||||
The Centrality of Groups and Classes.
|
||||
Journal of Mathematical Sociology. 23(3): 181-201. 1999.
|
||||
http://www.analytictech.com/borgatti/group_centrality.htm
|
||||
.. [2] Rami Puzis, Yuval Elovici, and Shlomi Dolev:
|
||||
"Finding the Most Prominent Group in Complex Networks"
|
||||
AI communications 20(4): 287-296, 2007.
|
||||
https://www.researchgate.net/profile/Rami_Puzis2/publication/220308855
|
||||
.. [3] Sourav Medya et. al.:
|
||||
Group Centrality Maximization via Network Design.
|
||||
SIAM International Conference on Data Mining, SDM 2018, 126–134.
|
||||
https://sites.cs.ucsb.edu/~arlei/pubs/sdm18.pdf
|
||||
.. [4] Rami Puzis, Yuval Elovici, and Shlomi Dolev.
|
||||
"Fast algorithm for successive computation of group betweenness centrality."
|
||||
https://journals.aps.org/pre/pdf/10.1103/PhysRevE.76.056709
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
if C is not None:
|
||||
C = set(C)
|
||||
if C - G.nodes: # element(s) of C not in G
|
||||
raise nx.NodeNotFound(f"The node(s) {C - G.nodes} are in C but not in G.")
|
||||
nodes = list(G.nodes - C)
|
||||
else:
|
||||
nodes = list(G.nodes)
|
||||
DF_tree = nx.Graph()
|
||||
DF_tree.__networkx_cache__ = None # Disable caching
|
||||
PB, sigma, D = _group_preprocessing(G, nodes, weight)
|
||||
betweenness = pd.DataFrame.from_dict(PB)
|
||||
if C is not None:
|
||||
for node in C:
|
||||
# remove from the betweenness all the nodes not part of the group
|
||||
betweenness = betweenness.drop(index=node)
|
||||
betweenness = betweenness.drop(columns=node)
|
||||
CL = [node for _, node in sorted(zip(np.diag(betweenness), nodes), reverse=True)]
|
||||
max_GBC = 0
|
||||
max_group = []
|
||||
DF_tree.add_node(
|
||||
1,
|
||||
CL=CL,
|
||||
betweenness=betweenness,
|
||||
GBC=0,
|
||||
GM=[],
|
||||
sigma=sigma,
|
||||
cont=dict(zip(nodes, np.diag(betweenness))),
|
||||
)
|
||||
|
||||
# the algorithm
|
||||
DF_tree.nodes[1]["heu"] = 0
|
||||
for i in range(k):
|
||||
DF_tree.nodes[1]["heu"] += DF_tree.nodes[1]["cont"][DF_tree.nodes[1]["CL"][i]]
|
||||
max_GBC, DF_tree, max_group = _dfbnb(
|
||||
G, k, DF_tree, max_GBC, 1, D, max_group, nodes, greedy
|
||||
)
|
||||
|
||||
v = len(G)
|
||||
if not endpoints:
|
||||
scale = 0
|
||||
# if the graph is connected then subtract the endpoints from
|
||||
# the count for all the nodes in the graph. else count how many
|
||||
# nodes are connected to the group's nodes and subtract that.
|
||||
if nx.is_directed(G):
|
||||
if nx.is_strongly_connected(G):
|
||||
scale = k * (2 * v - k - 1)
|
||||
elif nx.is_connected(G):
|
||||
scale = k * (2 * v - k - 1)
|
||||
if scale == 0:
|
||||
for group_node1 in max_group:
|
||||
for node in D[group_node1]:
|
||||
if node != group_node1:
|
||||
if node in max_group:
|
||||
scale += 1
|
||||
else:
|
||||
scale += 2
|
||||
max_GBC -= scale
|
||||
|
||||
# normalized
|
||||
if normalized:
|
||||
scale = 1 / ((v - k) * (v - k - 1))
|
||||
max_GBC *= scale
|
||||
|
||||
# If undirected then count only the undirected edges
|
||||
elif not G.is_directed():
|
||||
max_GBC /= 2
|
||||
max_GBC = float(f"{max_GBC:.2f}")
|
||||
return max_GBC, max_group
|
||||
|
||||
|
||||
def _dfbnb(G, k, DF_tree, max_GBC, root, D, max_group, nodes, greedy):
|
||||
# stopping condition - if we found a group of size k and with higher GBC then prune
|
||||
if len(DF_tree.nodes[root]["GM"]) == k and DF_tree.nodes[root]["GBC"] > max_GBC:
|
||||
return DF_tree.nodes[root]["GBC"], DF_tree, DF_tree.nodes[root]["GM"]
|
||||
# stopping condition - if the size of group members equal to k or there are less than
|
||||
# k - |GM| in the candidate list or the heuristic function plus the GBC is below the
|
||||
# maximal GBC found then prune
|
||||
if (
|
||||
len(DF_tree.nodes[root]["GM"]) == k
|
||||
or len(DF_tree.nodes[root]["CL"]) <= k - len(DF_tree.nodes[root]["GM"])
|
||||
or DF_tree.nodes[root]["GBC"] + DF_tree.nodes[root]["heu"] <= max_GBC
|
||||
):
|
||||
return max_GBC, DF_tree, max_group
|
||||
|
||||
# finding the heuristic of both children
|
||||
node_p, node_m, DF_tree = _heuristic(k, root, DF_tree, D, nodes, greedy)
|
||||
|
||||
# finding the child with the bigger heuristic + GBC and expand
|
||||
# that node first if greedy then only expand the plus node
|
||||
if greedy:
|
||||
max_GBC, DF_tree, max_group = _dfbnb(
|
||||
G, k, DF_tree, max_GBC, node_p, D, max_group, nodes, greedy
|
||||
)
|
||||
|
||||
elif (
|
||||
DF_tree.nodes[node_p]["GBC"] + DF_tree.nodes[node_p]["heu"]
|
||||
> DF_tree.nodes[node_m]["GBC"] + DF_tree.nodes[node_m]["heu"]
|
||||
):
|
||||
max_GBC, DF_tree, max_group = _dfbnb(
|
||||
G, k, DF_tree, max_GBC, node_p, D, max_group, nodes, greedy
|
||||
)
|
||||
max_GBC, DF_tree, max_group = _dfbnb(
|
||||
G, k, DF_tree, max_GBC, node_m, D, max_group, nodes, greedy
|
||||
)
|
||||
else:
|
||||
max_GBC, DF_tree, max_group = _dfbnb(
|
||||
G, k, DF_tree, max_GBC, node_m, D, max_group, nodes, greedy
|
||||
)
|
||||
max_GBC, DF_tree, max_group = _dfbnb(
|
||||
G, k, DF_tree, max_GBC, node_p, D, max_group, nodes, greedy
|
||||
)
|
||||
return max_GBC, DF_tree, max_group
|
||||
|
||||
|
||||
def _heuristic(k, root, DF_tree, D, nodes, greedy):
|
||||
import numpy as np
|
||||
|
||||
# This helper function add two nodes to DF_tree - one left son and the
|
||||
# other right son, finds their heuristic, CL, GBC, and GM
|
||||
node_p = DF_tree.number_of_nodes() + 1
|
||||
node_m = DF_tree.number_of_nodes() + 2
|
||||
added_node = DF_tree.nodes[root]["CL"][0]
|
||||
|
||||
# adding the plus node
|
||||
DF_tree.add_nodes_from([(node_p, deepcopy(DF_tree.nodes[root]))])
|
||||
DF_tree.nodes[node_p]["GM"].append(added_node)
|
||||
DF_tree.nodes[node_p]["GBC"] += DF_tree.nodes[node_p]["cont"][added_node]
|
||||
root_node = DF_tree.nodes[root]
|
||||
for x in nodes:
|
||||
for y in nodes:
|
||||
dxvy = 0
|
||||
dxyv = 0
|
||||
dvxy = 0
|
||||
if not (
|
||||
root_node["sigma"][x][y] == 0
|
||||
or root_node["sigma"][x][added_node] == 0
|
||||
or root_node["sigma"][added_node][y] == 0
|
||||
):
|
||||
if D[x][added_node] == D[x][y] + D[y][added_node]:
|
||||
dxyv = (
|
||||
root_node["sigma"][x][y]
|
||||
* root_node["sigma"][y][added_node]
|
||||
/ root_node["sigma"][x][added_node]
|
||||
)
|
||||
if D[x][y] == D[x][added_node] + D[added_node][y]:
|
||||
dxvy = (
|
||||
root_node["sigma"][x][added_node]
|
||||
* root_node["sigma"][added_node][y]
|
||||
/ root_node["sigma"][x][y]
|
||||
)
|
||||
if D[added_node][y] == D[added_node][x] + D[x][y]:
|
||||
dvxy = (
|
||||
root_node["sigma"][added_node][x]
|
||||
* root_node["sigma"][x][y]
|
||||
/ root_node["sigma"][added_node][y]
|
||||
)
|
||||
DF_tree.nodes[node_p]["sigma"][x][y] = root_node["sigma"][x][y] * (1 - dxvy)
|
||||
DF_tree.nodes[node_p]["betweenness"].loc[y, x] = (
|
||||
root_node["betweenness"][x][y] - root_node["betweenness"][x][y] * dxvy
|
||||
)
|
||||
if y != added_node:
|
||||
DF_tree.nodes[node_p]["betweenness"].loc[y, x] -= (
|
||||
root_node["betweenness"][x][added_node] * dxyv
|
||||
)
|
||||
if x != added_node:
|
||||
DF_tree.nodes[node_p]["betweenness"].loc[y, x] -= (
|
||||
root_node["betweenness"][added_node][y] * dvxy
|
||||
)
|
||||
|
||||
DF_tree.nodes[node_p]["CL"] = [
|
||||
node
|
||||
for _, node in sorted(
|
||||
zip(np.diag(DF_tree.nodes[node_p]["betweenness"]), nodes), reverse=True
|
||||
)
|
||||
if node not in DF_tree.nodes[node_p]["GM"]
|
||||
]
|
||||
DF_tree.nodes[node_p]["cont"] = dict(
|
||||
zip(nodes, np.diag(DF_tree.nodes[node_p]["betweenness"]))
|
||||
)
|
||||
DF_tree.nodes[node_p]["heu"] = 0
|
||||
for i in range(k - len(DF_tree.nodes[node_p]["GM"])):
|
||||
DF_tree.nodes[node_p]["heu"] += DF_tree.nodes[node_p]["cont"][
|
||||
DF_tree.nodes[node_p]["CL"][i]
|
||||
]
|
||||
|
||||
# adding the minus node - don't insert the first node in the CL to GM
|
||||
# Insert minus node only if isn't greedy type algorithm
|
||||
if not greedy:
|
||||
DF_tree.add_nodes_from([(node_m, deepcopy(DF_tree.nodes[root]))])
|
||||
DF_tree.nodes[node_m]["CL"].pop(0)
|
||||
DF_tree.nodes[node_m]["cont"].pop(added_node)
|
||||
DF_tree.nodes[node_m]["heu"] = 0
|
||||
for i in range(k - len(DF_tree.nodes[node_m]["GM"])):
|
||||
DF_tree.nodes[node_m]["heu"] += DF_tree.nodes[node_m]["cont"][
|
||||
DF_tree.nodes[node_m]["CL"][i]
|
||||
]
|
||||
else:
|
||||
node_m = None
|
||||
|
||||
return node_p, node_m, DF_tree
|
||||
|
||||
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def group_closeness_centrality(G, S, weight=None):
|
||||
r"""Compute the group closeness centrality for a group of nodes.
|
||||
|
||||
Group closeness centrality of a group of nodes $S$ is a measure
|
||||
of how close the group is to the other nodes in the graph.
|
||||
|
||||
.. math::
|
||||
|
||||
c_{close}(S) = \frac{|V-S|}{\sum_{v \in V-S} d_{S, v}}
|
||||
|
||||
d_{S, v} = min_{u \in S} (d_{u, v})
|
||||
|
||||
where $V$ is the set of nodes, $d_{S, v}$ is the distance of
|
||||
the group $S$ from $v$ defined as above. ($V-S$ is the set of nodes
|
||||
in $V$ that are not in $S$).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph.
|
||||
|
||||
S : list or set
|
||||
S is a group of nodes which belong to G, for which group closeness
|
||||
centrality is to be calculated.
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
If None, all edge weights are considered equal.
|
||||
Otherwise holds the name of the edge attribute used as weight.
|
||||
The weight of an edge is treated as the length or distance between the two sides.
|
||||
|
||||
Raises
|
||||
------
|
||||
NodeNotFound
|
||||
If node(s) in S are not present in G.
|
||||
|
||||
Returns
|
||||
-------
|
||||
closeness : float
|
||||
Group closeness centrality of the group S.
|
||||
|
||||
See Also
|
||||
--------
|
||||
closeness_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
The measure was introduced in [1]_.
|
||||
The formula implemented here is described in [2]_.
|
||||
|
||||
Higher values of closeness indicate greater centrality.
|
||||
|
||||
It is assumed that 1 / 0 is 0 (required in the case of directed graphs,
|
||||
or when a shortest path length is 0).
|
||||
|
||||
The number of nodes in the group must be a maximum of n - 1 where `n`
|
||||
is the total number of nodes in the graph.
|
||||
|
||||
For directed graphs, the incoming distance is utilized here. To use the
|
||||
outward distance, act on `G.reverse()`.
|
||||
|
||||
For weighted graphs the edge weights must be greater than zero.
|
||||
Zero edge weights can produce an infinite number of equal length
|
||||
paths between pairs of nodes.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] M G Everett and S P Borgatti:
|
||||
The Centrality of Groups and Classes.
|
||||
Journal of Mathematical Sociology. 23(3): 181-201. 1999.
|
||||
http://www.analytictech.com/borgatti/group_centrality.htm
|
||||
.. [2] J. Zhao et. al.:
|
||||
Measuring and Maximizing Group Closeness Centrality over
|
||||
Disk Resident Graphs.
|
||||
WWWConference Proceedings, 2014. 689-694.
|
||||
https://doi.org/10.1145/2567948.2579356
|
||||
"""
|
||||
if G.is_directed():
|
||||
G = G.reverse() # reverse view
|
||||
closeness = 0 # initialize to 0
|
||||
V = set(G) # set of nodes in G
|
||||
S = set(S) # set of nodes in group S
|
||||
V_S = V - S # set of nodes in V but not S
|
||||
shortest_path_lengths = nx.multi_source_dijkstra_path_length(G, S, weight=weight)
|
||||
# accumulation
|
||||
for v in V_S:
|
||||
try:
|
||||
closeness += shortest_path_lengths[v]
|
||||
except KeyError: # no path exists
|
||||
closeness += 0
|
||||
try:
|
||||
closeness = len(V_S) / closeness
|
||||
except ZeroDivisionError: # 1 / 0 assumed as 0
|
||||
closeness = 0
|
||||
return closeness
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def group_degree_centrality(G, S):
|
||||
"""Compute the group degree centrality for a group of nodes.
|
||||
|
||||
Group degree centrality of a group of nodes $S$ is the fraction
|
||||
of non-group members connected to group members.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph.
|
||||
|
||||
S : list or set
|
||||
S is a group of nodes which belong to G, for which group degree
|
||||
centrality is to be calculated.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXError
|
||||
If node(s) in S are not in G.
|
||||
|
||||
Returns
|
||||
-------
|
||||
centrality : float
|
||||
Group degree centrality of the group S.
|
||||
|
||||
See Also
|
||||
--------
|
||||
degree_centrality
|
||||
group_in_degree_centrality
|
||||
group_out_degree_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
The measure was introduced in [1]_.
|
||||
|
||||
The number of nodes in the group must be a maximum of n - 1 where `n`
|
||||
is the total number of nodes in the graph.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] M G Everett and S P Borgatti:
|
||||
The Centrality of Groups and Classes.
|
||||
Journal of Mathematical Sociology. 23(3): 181-201. 1999.
|
||||
http://www.analytictech.com/borgatti/group_centrality.htm
|
||||
"""
|
||||
centrality = len(set().union(*[set(G.neighbors(i)) for i in S]) - set(S))
|
||||
centrality /= len(G.nodes()) - len(S)
|
||||
return centrality
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
@nx._dispatchable
|
||||
def group_in_degree_centrality(G, S):
|
||||
"""Compute the group in-degree centrality for a group of nodes.
|
||||
|
||||
Group in-degree centrality of a group of nodes $S$ is the fraction
|
||||
of non-group members connected to group members by incoming edges.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph.
|
||||
|
||||
S : list or set
|
||||
S is a group of nodes which belong to G, for which group in-degree
|
||||
centrality is to be calculated.
|
||||
|
||||
Returns
|
||||
-------
|
||||
centrality : float
|
||||
Group in-degree centrality of the group S.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If G is undirected.
|
||||
|
||||
NodeNotFound
|
||||
If node(s) in S are not in G.
|
||||
|
||||
See Also
|
||||
--------
|
||||
degree_centrality
|
||||
group_degree_centrality
|
||||
group_out_degree_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
The number of nodes in the group must be a maximum of n - 1 where `n`
|
||||
is the total number of nodes in the graph.
|
||||
|
||||
`G.neighbors(i)` gives nodes with an outward edge from i, in a DiGraph,
|
||||
so for group in-degree centrality, the reverse graph is used.
|
||||
"""
|
||||
return group_degree_centrality(G.reverse(), S)
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
@nx._dispatchable
|
||||
def group_out_degree_centrality(G, S):
|
||||
"""Compute the group out-degree centrality for a group of nodes.
|
||||
|
||||
Group out-degree centrality of a group of nodes $S$ is the fraction
|
||||
of non-group members connected to group members by outgoing edges.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph.
|
||||
|
||||
S : list or set
|
||||
S is a group of nodes which belong to G, for which group in-degree
|
||||
centrality is to be calculated.
|
||||
|
||||
Returns
|
||||
-------
|
||||
centrality : float
|
||||
Group out-degree centrality of the group S.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXNotImplemented
|
||||
If G is undirected.
|
||||
|
||||
NodeNotFound
|
||||
If node(s) in S are not in G.
|
||||
|
||||
See Also
|
||||
--------
|
||||
degree_centrality
|
||||
group_degree_centrality
|
||||
group_in_degree_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
The number of nodes in the group must be a maximum of n - 1 where `n`
|
||||
is the total number of nodes in the graph.
|
||||
|
||||
`G.neighbors(i)` gives nodes with an outward edge from i, in a DiGraph,
|
||||
so for group out-degree centrality, the graph itself is used.
|
||||
"""
|
||||
return group_degree_centrality(G, S)
|
||||
@@ -0,0 +1,88 @@
|
||||
"""Functions for computing the harmonic centrality of a graph."""
|
||||
|
||||
from functools import partial
|
||||
|
||||
import networkx as nx
|
||||
|
||||
__all__ = ["harmonic_centrality"]
|
||||
|
||||
|
||||
@nx._dispatchable(edge_attrs="distance")
|
||||
def harmonic_centrality(G, nbunch=None, distance=None, sources=None):
|
||||
r"""Compute harmonic centrality for nodes.
|
||||
|
||||
Harmonic centrality [1]_ of a node `u` is the sum of the reciprocal
|
||||
of the shortest path distances from all other nodes to `u`
|
||||
|
||||
.. math::
|
||||
|
||||
C(u) = \sum_{v \neq u} \frac{1}{d(v, u)}
|
||||
|
||||
where `d(v, u)` is the shortest-path distance between `v` and `u`.
|
||||
|
||||
If `sources` is given as an argument, the returned harmonic centrality
|
||||
values are calculated as the sum of the reciprocals of the shortest
|
||||
path distances from the nodes specified in `sources` to `u` instead
|
||||
of from all nodes to `u`.
|
||||
|
||||
Notice that higher values indicate higher centrality.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph
|
||||
|
||||
nbunch : container (default: all nodes in G)
|
||||
Container of nodes for which harmonic centrality values are calculated.
|
||||
|
||||
sources : container (default: all nodes in G)
|
||||
Container of nodes `v` over which reciprocal distances are computed.
|
||||
Nodes not in `G` are silently ignored.
|
||||
|
||||
distance : edge attribute key, optional (default=None)
|
||||
Use the specified edge attribute as the edge distance in shortest
|
||||
path calculations. If `None`, then each edge will have distance equal to 1.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with harmonic centrality as the value.
|
||||
|
||||
See Also
|
||||
--------
|
||||
betweenness_centrality, load_centrality, eigenvector_centrality,
|
||||
degree_centrality, closeness_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
If the 'distance' keyword is set to an edge attribute key then the
|
||||
shortest-path length will be computed using Dijkstra's algorithm with
|
||||
that edge attribute as the edge weight.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Boldi, Paolo, and Sebastiano Vigna. "Axioms for centrality."
|
||||
Internet Mathematics 10.3-4 (2014): 222-262.
|
||||
"""
|
||||
|
||||
nbunch = set(G.nbunch_iter(nbunch) if nbunch is not None else G.nodes)
|
||||
sources = set(G.nbunch_iter(sources) if sources is not None else G.nodes)
|
||||
|
||||
centrality = {u: 0 for u in nbunch}
|
||||
|
||||
transposed = False
|
||||
if len(nbunch) < len(sources):
|
||||
transposed = True
|
||||
nbunch, sources = sources, nbunch
|
||||
if nx.is_directed(G):
|
||||
G = nx.reverse(G, copy=False)
|
||||
|
||||
spl = partial(nx.shortest_path_length, G, weight=distance)
|
||||
for v in sources:
|
||||
dist = spl(v)
|
||||
for u, d_uv in dist.items():
|
||||
# Ignore self-loops and edges with 0 weight
|
||||
if d_uv != 0 and u in nbunch:
|
||||
centrality[v if transposed else u] += 1 / d_uv
|
||||
|
||||
return centrality
|
||||
@@ -0,0 +1,331 @@
|
||||
"""Katz centrality."""
|
||||
|
||||
import math
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import not_implemented_for
|
||||
|
||||
__all__ = ["katz_centrality", "katz_centrality_numpy"]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def katz_centrality(
|
||||
G,
|
||||
alpha=0.1,
|
||||
beta=1.0,
|
||||
max_iter=1000,
|
||||
tol=1.0e-6,
|
||||
nstart=None,
|
||||
normalized=True,
|
||||
weight=None,
|
||||
):
|
||||
r"""Compute the Katz centrality for the nodes of the graph G.
|
||||
|
||||
Katz centrality computes the centrality for a node based on the centrality
|
||||
of its neighbors. It is a generalization of the eigenvector centrality. The
|
||||
Katz centrality for node $i$ is
|
||||
|
||||
.. math::
|
||||
|
||||
x_i = \alpha \sum_{j} A_{ij} x_j + \beta,
|
||||
|
||||
where $A$ is the adjacency matrix of graph G with eigenvalues $\lambda$.
|
||||
|
||||
The parameter $\beta$ controls the initial centrality and
|
||||
|
||||
.. math::
|
||||
|
||||
\alpha < \frac{1}{\lambda_{\max}}.
|
||||
|
||||
Katz centrality computes the relative influence of a node within a
|
||||
network by measuring the number of the immediate neighbors (first
|
||||
degree nodes) and also all other nodes in the network that connect
|
||||
to the node under consideration through these immediate neighbors.
|
||||
|
||||
Extra weight can be provided to immediate neighbors through the
|
||||
parameter $\beta$. Connections made with distant neighbors
|
||||
are, however, penalized by an attenuation factor $\alpha$ which
|
||||
should be strictly less than the inverse largest eigenvalue of the
|
||||
adjacency matrix in order for the Katz centrality to be computed
|
||||
correctly. More information is provided in [1]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph.
|
||||
|
||||
alpha : float, optional (default=0.1)
|
||||
Attenuation factor
|
||||
|
||||
beta : scalar or dictionary, optional (default=1.0)
|
||||
Weight attributed to the immediate neighborhood. If not a scalar, the
|
||||
dictionary must have a value for every node.
|
||||
|
||||
max_iter : integer, optional (default=1000)
|
||||
Maximum number of iterations in power method.
|
||||
|
||||
tol : float, optional (default=1.0e-6)
|
||||
Error tolerance used to check convergence in power method iteration.
|
||||
|
||||
nstart : dictionary, optional
|
||||
Starting value of Katz iteration for each node.
|
||||
|
||||
normalized : bool, optional (default=True)
|
||||
If True normalize the resulting values.
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
If None, all edge weights are considered equal.
|
||||
Otherwise holds the name of the edge attribute used as weight.
|
||||
In this measure the weight is interpreted as the connection strength.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with Katz centrality as the value.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXError
|
||||
If the parameter `beta` is not a scalar but lacks a value for at least
|
||||
one node
|
||||
|
||||
PowerIterationFailedConvergence
|
||||
If the algorithm fails to converge to the specified tolerance
|
||||
within the specified number of iterations of the power iteration
|
||||
method.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import math
|
||||
>>> G = nx.path_graph(4)
|
||||
>>> phi = (1 + math.sqrt(5)) / 2.0 # largest eigenvalue of adj matrix
|
||||
>>> centrality = nx.katz_centrality(G, 1 / phi - 0.01)
|
||||
>>> for n, c in sorted(centrality.items()):
|
||||
... print(f"{n} {c:.2f}")
|
||||
0 0.37
|
||||
1 0.60
|
||||
2 0.60
|
||||
3 0.37
|
||||
|
||||
See Also
|
||||
--------
|
||||
katz_centrality_numpy
|
||||
eigenvector_centrality
|
||||
eigenvector_centrality_numpy
|
||||
:func:`~networkx.algorithms.link_analysis.pagerank_alg.pagerank`
|
||||
:func:`~networkx.algorithms.link_analysis.hits_alg.hits`
|
||||
|
||||
Notes
|
||||
-----
|
||||
Katz centrality was introduced by [2]_.
|
||||
|
||||
This algorithm it uses the power method to find the eigenvector
|
||||
corresponding to the largest eigenvalue of the adjacency matrix of ``G``.
|
||||
The parameter ``alpha`` should be strictly less than the inverse of largest
|
||||
eigenvalue of the adjacency matrix for the algorithm to converge.
|
||||
You can use ``max(nx.adjacency_spectrum(G))`` to get $\lambda_{\max}$ the largest
|
||||
eigenvalue of the adjacency matrix.
|
||||
The iteration will stop after ``max_iter`` iterations or an error tolerance of
|
||||
``number_of_nodes(G) * tol`` has been reached.
|
||||
|
||||
For strongly connected graphs, as $\alpha \to 1/\lambda_{\max}$, and $\beta > 0$,
|
||||
Katz centrality approaches the results for eigenvector centrality.
|
||||
|
||||
For directed graphs this finds "left" eigenvectors which corresponds
|
||||
to the in-edges in the graph. For out-edges Katz centrality,
|
||||
first reverse the graph with ``G.reverse()``.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Mark E. J. Newman:
|
||||
Networks: An Introduction.
|
||||
Oxford University Press, USA, 2010, p. 720.
|
||||
.. [2] Leo Katz:
|
||||
A New Status Index Derived from Sociometric Index.
|
||||
Psychometrika 18(1):39–43, 1953
|
||||
https://link.springer.com/content/pdf/10.1007/BF02289026.pdf
|
||||
"""
|
||||
if len(G) == 0:
|
||||
return {}
|
||||
|
||||
nnodes = G.number_of_nodes()
|
||||
|
||||
if nstart is None:
|
||||
# choose starting vector with entries of 0
|
||||
x = {n: 0 for n in G}
|
||||
else:
|
||||
x = nstart
|
||||
|
||||
try:
|
||||
b = dict.fromkeys(G, float(beta))
|
||||
except (TypeError, ValueError, AttributeError) as err:
|
||||
b = beta
|
||||
if set(beta) != set(G):
|
||||
raise nx.NetworkXError(
|
||||
"beta dictionary must have a value for every node"
|
||||
) from err
|
||||
|
||||
# make up to max_iter iterations
|
||||
for _ in range(max_iter):
|
||||
xlast = x
|
||||
x = dict.fromkeys(xlast, 0)
|
||||
# do the multiplication y^T = Alpha * x^T A + Beta
|
||||
for n in x:
|
||||
for nbr in G[n]:
|
||||
x[nbr] += xlast[n] * G[n][nbr].get(weight, 1)
|
||||
for n in x:
|
||||
x[n] = alpha * x[n] + b[n]
|
||||
|
||||
# check convergence
|
||||
error = sum(abs(x[n] - xlast[n]) for n in x)
|
||||
if error < nnodes * tol:
|
||||
if normalized:
|
||||
# normalize vector
|
||||
try:
|
||||
s = 1.0 / math.hypot(*x.values())
|
||||
except ZeroDivisionError:
|
||||
s = 1.0
|
||||
else:
|
||||
s = 1
|
||||
for n in x:
|
||||
x[n] *= s
|
||||
return x
|
||||
raise nx.PowerIterationFailedConvergence(max_iter)
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def katz_centrality_numpy(G, alpha=0.1, beta=1.0, normalized=True, weight=None):
|
||||
r"""Compute the Katz centrality for the graph G.
|
||||
|
||||
Katz centrality computes the centrality for a node based on the centrality
|
||||
of its neighbors. It is a generalization of the eigenvector centrality. The
|
||||
Katz centrality for node $i$ is
|
||||
|
||||
.. math::
|
||||
|
||||
x_i = \alpha \sum_{j} A_{ij} x_j + \beta,
|
||||
|
||||
where $A$ is the adjacency matrix of graph G with eigenvalues $\lambda$.
|
||||
|
||||
The parameter $\beta$ controls the initial centrality and
|
||||
|
||||
.. math::
|
||||
|
||||
\alpha < \frac{1}{\lambda_{\max}}.
|
||||
|
||||
Katz centrality computes the relative influence of a node within a
|
||||
network by measuring the number of the immediate neighbors (first
|
||||
degree nodes) and also all other nodes in the network that connect
|
||||
to the node under consideration through these immediate neighbors.
|
||||
|
||||
Extra weight can be provided to immediate neighbors through the
|
||||
parameter $\beta$. Connections made with distant neighbors
|
||||
are, however, penalized by an attenuation factor $\alpha$ which
|
||||
should be strictly less than the inverse largest eigenvalue of the
|
||||
adjacency matrix in order for the Katz centrality to be computed
|
||||
correctly. More information is provided in [1]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph
|
||||
|
||||
alpha : float
|
||||
Attenuation factor
|
||||
|
||||
beta : scalar or dictionary, optional (default=1.0)
|
||||
Weight attributed to the immediate neighborhood. If not a scalar the
|
||||
dictionary must have an value for every node.
|
||||
|
||||
normalized : bool
|
||||
If True normalize the resulting values.
|
||||
|
||||
weight : None or string, optional
|
||||
If None, all edge weights are considered equal.
|
||||
Otherwise holds the name of the edge attribute used as weight.
|
||||
In this measure the weight is interpreted as the connection strength.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with Katz centrality as the value.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXError
|
||||
If the parameter `beta` is not a scalar but lacks a value for at least
|
||||
one node
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import math
|
||||
>>> G = nx.path_graph(4)
|
||||
>>> phi = (1 + math.sqrt(5)) / 2.0 # largest eigenvalue of adj matrix
|
||||
>>> centrality = nx.katz_centrality_numpy(G, 1 / phi)
|
||||
>>> for n, c in sorted(centrality.items()):
|
||||
... print(f"{n} {c:.2f}")
|
||||
0 0.37
|
||||
1 0.60
|
||||
2 0.60
|
||||
3 0.37
|
||||
|
||||
See Also
|
||||
--------
|
||||
katz_centrality
|
||||
eigenvector_centrality_numpy
|
||||
eigenvector_centrality
|
||||
:func:`~networkx.algorithms.link_analysis.pagerank_alg.pagerank`
|
||||
:func:`~networkx.algorithms.link_analysis.hits_alg.hits`
|
||||
|
||||
Notes
|
||||
-----
|
||||
Katz centrality was introduced by [2]_.
|
||||
|
||||
This algorithm uses a direct linear solver to solve the above equation.
|
||||
The parameter ``alpha`` should be strictly less than the inverse of largest
|
||||
eigenvalue of the adjacency matrix for there to be a solution.
|
||||
You can use ``max(nx.adjacency_spectrum(G))`` to get $\lambda_{\max}$ the largest
|
||||
eigenvalue of the adjacency matrix.
|
||||
|
||||
For strongly connected graphs, as $\alpha \to 1/\lambda_{\max}$, and $\beta > 0$,
|
||||
Katz centrality approaches the results for eigenvector centrality.
|
||||
|
||||
For directed graphs this finds "left" eigenvectors which corresponds
|
||||
to the in-edges in the graph. For out-edges Katz centrality,
|
||||
first reverse the graph with ``G.reverse()``.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Mark E. J. Newman:
|
||||
Networks: An Introduction.
|
||||
Oxford University Press, USA, 2010, p. 173.
|
||||
.. [2] Leo Katz:
|
||||
A New Status Index Derived from Sociometric Index.
|
||||
Psychometrika 18(1):39–43, 1953
|
||||
https://link.springer.com/content/pdf/10.1007/BF02289026.pdf
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
if len(G) == 0:
|
||||
return {}
|
||||
try:
|
||||
nodelist = beta.keys()
|
||||
if set(nodelist) != set(G):
|
||||
raise nx.NetworkXError("beta dictionary must have a value for every node")
|
||||
b = np.array(list(beta.values()), dtype=float)
|
||||
except AttributeError:
|
||||
nodelist = list(G)
|
||||
try:
|
||||
b = np.ones((len(nodelist), 1)) * beta
|
||||
except (TypeError, ValueError, AttributeError) as err:
|
||||
raise nx.NetworkXError("beta must be a number") from err
|
||||
|
||||
A = nx.adjacency_matrix(G, nodelist=nodelist, weight=weight).todense().T
|
||||
n = A.shape[0]
|
||||
centrality = np.linalg.solve(np.eye(n, n) - (alpha * A), b).squeeze()
|
||||
|
||||
# Normalize: rely on truediv to cast to float, then tolist to make Python numbers
|
||||
norm = np.sign(sum(centrality)) * np.linalg.norm(centrality) if normalized else 1
|
||||
return dict(zip(nodelist, (centrality / norm).tolist()))
|
||||
@@ -0,0 +1,150 @@
|
||||
"""
|
||||
Laplacian centrality measures.
|
||||
"""
|
||||
|
||||
import networkx as nx
|
||||
|
||||
__all__ = ["laplacian_centrality"]
|
||||
|
||||
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def laplacian_centrality(
|
||||
G, normalized=True, nodelist=None, weight="weight", walk_type=None, alpha=0.95
|
||||
):
|
||||
r"""Compute the Laplacian centrality for nodes in the graph `G`.
|
||||
|
||||
The Laplacian Centrality of a node ``i`` is measured by the drop in the
|
||||
Laplacian Energy after deleting node ``i`` from the graph. The Laplacian Energy
|
||||
is the sum of the squared eigenvalues of a graph's Laplacian matrix.
|
||||
|
||||
.. math::
|
||||
|
||||
C_L(u_i,G) = \frac{(\Delta E)_i}{E_L (G)} = \frac{E_L (G)-E_L (G_i)}{E_L (G)}
|
||||
|
||||
E_L (G) = \sum_{i=0}^n \lambda_i^2
|
||||
|
||||
Where $E_L (G)$ is the Laplacian energy of graph `G`,
|
||||
E_L (G_i) is the Laplacian energy of graph `G` after deleting node ``i``
|
||||
and $\lambda_i$ are the eigenvalues of `G`'s Laplacian matrix.
|
||||
This formula shows the normalized value. Without normalization,
|
||||
the numerator on the right side is returned.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A networkx graph
|
||||
|
||||
normalized : bool (default = True)
|
||||
If True the centrality score is scaled so the sum over all nodes is 1.
|
||||
If False the centrality score for each node is the drop in Laplacian
|
||||
energy when that node is removed.
|
||||
|
||||
nodelist : list, optional (default = None)
|
||||
The rows and columns are ordered according to the nodes in nodelist.
|
||||
If nodelist is None, then the ordering is produced by G.nodes().
|
||||
|
||||
weight: string or None, optional (default=`weight`)
|
||||
Optional parameter `weight` to compute the Laplacian matrix.
|
||||
The edge data key used to compute each value in the matrix.
|
||||
If None, then each edge has weight 1.
|
||||
|
||||
walk_type : string or None, optional (default=None)
|
||||
Optional parameter `walk_type` used when calling
|
||||
:func:`directed_laplacian_matrix <networkx.directed_laplacian_matrix>`.
|
||||
One of ``"random"``, ``"lazy"``, or ``"pagerank"``. If ``walk_type=None``
|
||||
(the default), then a value is selected according to the properties of `G`:
|
||||
- ``walk_type="random"`` if `G` is strongly connected and aperiodic
|
||||
- ``walk_type="lazy"`` if `G` is strongly connected but not aperiodic
|
||||
- ``walk_type="pagerank"`` for all other cases.
|
||||
|
||||
alpha : real (default = 0.95)
|
||||
Optional parameter `alpha` used when calling
|
||||
:func:`directed_laplacian_matrix <networkx.directed_laplacian_matrix>`.
|
||||
(1 - alpha) is the teleportation probability used with pagerank.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with Laplacian centrality as the value.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.Graph()
|
||||
>>> edges = [(0, 1, 4), (0, 2, 2), (2, 1, 1), (1, 3, 2), (1, 4, 2), (4, 5, 1)]
|
||||
>>> G.add_weighted_edges_from(edges)
|
||||
>>> sorted((v, f"{c:0.2f}") for v, c in laplacian_centrality(G).items())
|
||||
[(0, '0.70'), (1, '0.90'), (2, '0.28'), (3, '0.22'), (4, '0.26'), (5, '0.04')]
|
||||
|
||||
Notes
|
||||
-----
|
||||
The algorithm is implemented based on [1]_ with an extension to directed graphs
|
||||
using the ``directed_laplacian_matrix`` function.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXPointlessConcept
|
||||
If the graph `G` is the null graph.
|
||||
ZeroDivisionError
|
||||
If the graph `G` has no edges (is empty) and normalization is requested.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Qi, X., Fuller, E., Wu, Q., Wu, Y., and Zhang, C.-Q. (2012).
|
||||
Laplacian centrality: A new centrality measure for weighted networks.
|
||||
Information Sciences, 194:240-253.
|
||||
https://math.wvu.edu/~cqzhang/Publication-files/my-paper/INS-2012-Laplacian-W.pdf
|
||||
|
||||
See Also
|
||||
--------
|
||||
:func:`~networkx.linalg.laplacianmatrix.directed_laplacian_matrix`
|
||||
:func:`~networkx.linalg.laplacianmatrix.laplacian_matrix`
|
||||
"""
|
||||
import numpy as np
|
||||
import scipy as sp
|
||||
|
||||
if len(G) == 0:
|
||||
raise nx.NetworkXPointlessConcept("null graph has no centrality defined")
|
||||
if G.size(weight=weight) == 0:
|
||||
if normalized:
|
||||
raise ZeroDivisionError("graph with no edges has zero full energy")
|
||||
return {n: 0 for n in G}
|
||||
|
||||
if nodelist is not None:
|
||||
nodeset = set(G.nbunch_iter(nodelist))
|
||||
if len(nodeset) != len(nodelist):
|
||||
raise nx.NetworkXError("nodelist has duplicate nodes or nodes not in G")
|
||||
nodes = nodelist + [n for n in G if n not in nodeset]
|
||||
else:
|
||||
nodelist = nodes = list(G)
|
||||
|
||||
if G.is_directed():
|
||||
lap_matrix = nx.directed_laplacian_matrix(G, nodes, weight, walk_type, alpha)
|
||||
else:
|
||||
lap_matrix = nx.laplacian_matrix(G, nodes, weight).toarray()
|
||||
|
||||
full_energy = np.sum(lap_matrix**2)
|
||||
|
||||
# calculate laplacian centrality
|
||||
laplace_centralities_dict = {}
|
||||
for i, node in enumerate(nodelist):
|
||||
# remove row and col i from lap_matrix
|
||||
all_but_i = list(np.arange(lap_matrix.shape[0]))
|
||||
all_but_i.remove(i)
|
||||
A_2 = lap_matrix[all_but_i, :][:, all_but_i]
|
||||
|
||||
# Adjust diagonal for removed row
|
||||
new_diag = lap_matrix.diagonal() - abs(lap_matrix[:, i])
|
||||
np.fill_diagonal(A_2, new_diag[all_but_i])
|
||||
|
||||
if len(all_but_i) > 0: # catches degenerate case of single node
|
||||
new_energy = np.sum(A_2**2)
|
||||
else:
|
||||
new_energy = 0.0
|
||||
|
||||
lapl_cent = full_energy - new_energy
|
||||
if normalized:
|
||||
lapl_cent = lapl_cent / full_energy
|
||||
|
||||
laplace_centralities_dict[node] = float(lapl_cent)
|
||||
|
||||
return laplace_centralities_dict
|
||||
@@ -0,0 +1,200 @@
|
||||
"""Load centrality."""
|
||||
|
||||
from operator import itemgetter
|
||||
|
||||
import networkx as nx
|
||||
|
||||
__all__ = ["load_centrality", "edge_load_centrality"]
|
||||
|
||||
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def newman_betweenness_centrality(G, v=None, cutoff=None, normalized=True, weight=None):
|
||||
"""Compute load centrality for nodes.
|
||||
|
||||
The load centrality of a node is the fraction of all shortest
|
||||
paths that pass through that node.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A networkx graph.
|
||||
|
||||
normalized : bool, optional (default=True)
|
||||
If True the betweenness values are normalized by b=b/(n-1)(n-2) where
|
||||
n is the number of nodes in G.
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
If None, edge weights are ignored.
|
||||
Otherwise holds the name of the edge attribute used as weight.
|
||||
The weight of an edge is treated as the length or distance between the two sides.
|
||||
|
||||
cutoff : bool, optional (default=None)
|
||||
If specified, only consider paths of length <= cutoff.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with centrality as the value.
|
||||
|
||||
See Also
|
||||
--------
|
||||
betweenness_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
Load centrality is slightly different than betweenness. It was originally
|
||||
introduced by [2]_. For this load algorithm see [1]_.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Mark E. J. Newman:
|
||||
Scientific collaboration networks. II.
|
||||
Shortest paths, weighted networks, and centrality.
|
||||
Physical Review E 64, 016132, 2001.
|
||||
http://journals.aps.org/pre/abstract/10.1103/PhysRevE.64.016132
|
||||
.. [2] Kwang-Il Goh, Byungnam Kahng and Doochul Kim
|
||||
Universal behavior of Load Distribution in Scale-Free Networks.
|
||||
Physical Review Letters 87(27):1–4, 2001.
|
||||
https://doi.org/10.1103/PhysRevLett.87.278701
|
||||
"""
|
||||
if v is not None: # only one node
|
||||
betweenness = 0.0
|
||||
for source in G:
|
||||
ubetween = _node_betweenness(G, source, cutoff, False, weight)
|
||||
betweenness += ubetween[v] if v in ubetween else 0
|
||||
if normalized:
|
||||
order = G.order()
|
||||
if order <= 2:
|
||||
return betweenness # no normalization b=0 for all nodes
|
||||
betweenness *= 1.0 / ((order - 1) * (order - 2))
|
||||
else:
|
||||
betweenness = {}.fromkeys(G, 0.0)
|
||||
for source in betweenness:
|
||||
ubetween = _node_betweenness(G, source, cutoff, False, weight)
|
||||
for vk in ubetween:
|
||||
betweenness[vk] += ubetween[vk]
|
||||
if normalized:
|
||||
order = G.order()
|
||||
if order <= 2:
|
||||
return betweenness # no normalization b=0 for all nodes
|
||||
scale = 1.0 / ((order - 1) * (order - 2))
|
||||
for v in betweenness:
|
||||
betweenness[v] *= scale
|
||||
return betweenness # all nodes
|
||||
|
||||
|
||||
def _node_betweenness(G, source, cutoff=False, normalized=True, weight=None):
|
||||
"""Node betweenness_centrality helper:
|
||||
|
||||
See betweenness_centrality for what you probably want.
|
||||
This actually computes "load" and not betweenness.
|
||||
See https://networkx.lanl.gov/ticket/103
|
||||
|
||||
This calculates the load of each node for paths from a single source.
|
||||
(The fraction of number of shortests paths from source that go
|
||||
through each node.)
|
||||
|
||||
To get the load for a node you need to do all-pairs shortest paths.
|
||||
|
||||
If weight is not None then use Dijkstra for finding shortest paths.
|
||||
"""
|
||||
# get the predecessor and path length data
|
||||
if weight is None:
|
||||
(pred, length) = nx.predecessor(G, source, cutoff=cutoff, return_seen=True)
|
||||
else:
|
||||
(pred, length) = nx.dijkstra_predecessor_and_distance(G, source, cutoff, weight)
|
||||
|
||||
# order the nodes by path length
|
||||
onodes = [(l, vert) for (vert, l) in length.items()]
|
||||
onodes.sort()
|
||||
onodes[:] = [vert for (l, vert) in onodes if l > 0]
|
||||
|
||||
# initialize betweenness
|
||||
between = {}.fromkeys(length, 1.0)
|
||||
|
||||
while onodes:
|
||||
v = onodes.pop()
|
||||
if v in pred:
|
||||
num_paths = len(pred[v]) # Discount betweenness if more than
|
||||
for x in pred[v]: # one shortest path.
|
||||
if x == source: # stop if hit source because all remaining v
|
||||
break # also have pred[v]==[source]
|
||||
between[x] += between[v] / num_paths
|
||||
# remove source
|
||||
for v in between:
|
||||
between[v] -= 1
|
||||
# rescale to be between 0 and 1
|
||||
if normalized:
|
||||
l = len(between)
|
||||
if l > 2:
|
||||
# scale by 1/the number of possible paths
|
||||
scale = 1 / ((l - 1) * (l - 2))
|
||||
for v in between:
|
||||
between[v] *= scale
|
||||
return between
|
||||
|
||||
|
||||
load_centrality = newman_betweenness_centrality
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def edge_load_centrality(G, cutoff=False):
|
||||
"""Compute edge load.
|
||||
|
||||
WARNING: This concept of edge load has not been analysed
|
||||
or discussed outside of NetworkX that we know of.
|
||||
It is based loosely on load_centrality in the sense that
|
||||
it counts the number of shortest paths which cross each edge.
|
||||
This function is for demonstration and testing purposes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A networkx graph
|
||||
|
||||
cutoff : bool, optional (default=False)
|
||||
If specified, only consider paths of length <= cutoff.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A dict keyed by edge 2-tuple to the number of shortest paths
|
||||
which use that edge. Where more than one path is shortest
|
||||
the count is divided equally among paths.
|
||||
"""
|
||||
betweenness = {}
|
||||
for u, v in G.edges():
|
||||
betweenness[(u, v)] = 0.0
|
||||
betweenness[(v, u)] = 0.0
|
||||
|
||||
for source in G:
|
||||
ubetween = _edge_betweenness(G, source, cutoff=cutoff)
|
||||
for e, ubetweenv in ubetween.items():
|
||||
betweenness[e] += ubetweenv # cumulative total
|
||||
return betweenness
|
||||
|
||||
|
||||
def _edge_betweenness(G, source, nodes=None, cutoff=False):
|
||||
"""Edge betweenness helper."""
|
||||
# get the predecessor data
|
||||
(pred, length) = nx.predecessor(G, source, cutoff=cutoff, return_seen=True)
|
||||
# order the nodes by path length
|
||||
onodes = [n for n, d in sorted(length.items(), key=itemgetter(1))]
|
||||
# initialize betweenness, doesn't account for any edge weights
|
||||
between = {}
|
||||
for u, v in G.edges(nodes):
|
||||
between[(u, v)] = 1.0
|
||||
between[(v, u)] = 1.0
|
||||
|
||||
while onodes: # work through all paths
|
||||
v = onodes.pop()
|
||||
if v in pred:
|
||||
# Discount betweenness if more than one shortest path.
|
||||
num_paths = len(pred[v])
|
||||
for w in pred[v]:
|
||||
if w in pred:
|
||||
# Discount betweenness, mult path
|
||||
num_paths = len(pred[w])
|
||||
for x in pred[w]:
|
||||
between[(w, x)] += between[(v, w)] / num_paths
|
||||
between[(x, w)] += between[(w, v)] / num_paths
|
||||
return between
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
"""Percolation centrality measures."""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.centrality.betweenness import (
|
||||
_single_source_dijkstra_path_basic as dijkstra,
|
||||
)
|
||||
from networkx.algorithms.centrality.betweenness import (
|
||||
_single_source_shortest_path_basic as shortest_path,
|
||||
)
|
||||
|
||||
__all__ = ["percolation_centrality"]
|
||||
|
||||
|
||||
@nx._dispatchable(node_attrs="attribute", edge_attrs="weight")
|
||||
def percolation_centrality(G, attribute="percolation", states=None, weight=None):
|
||||
r"""Compute the percolation centrality for nodes.
|
||||
|
||||
Percolation centrality of a node $v$, at a given time, is defined
|
||||
as the proportion of ‘percolated paths’ that go through that node.
|
||||
|
||||
This measure quantifies relative impact of nodes based on their
|
||||
topological connectivity, as well as their percolation states.
|
||||
|
||||
Percolation states of nodes are used to depict network percolation
|
||||
scenarios (such as during infection transmission in a social network
|
||||
of individuals, spreading of computer viruses on computer networks, or
|
||||
transmission of disease over a network of towns) over time. In this
|
||||
measure usually the percolation state is expressed as a decimal
|
||||
between 0.0 and 1.0.
|
||||
|
||||
When all nodes are in the same percolated state this measure is
|
||||
equivalent to betweenness centrality.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph.
|
||||
|
||||
attribute : None or string, optional (default='percolation')
|
||||
Name of the node attribute to use for percolation state, used
|
||||
if `states` is None. If a node does not set the attribute the
|
||||
state of that node will be set to the default value of 1.
|
||||
If all nodes do not have the attribute all nodes will be set to
|
||||
1 and the centrality measure will be equivalent to betweenness centrality.
|
||||
|
||||
states : None or dict, optional (default=None)
|
||||
Specify percolation states for the nodes, nodes as keys states
|
||||
as values.
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
If None, all edge weights are considered equal.
|
||||
Otherwise holds the name of the edge attribute used as weight.
|
||||
The weight of an edge is treated as the length or distance between the two sides.
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with percolation centrality as the value.
|
||||
|
||||
See Also
|
||||
--------
|
||||
betweenness_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
The algorithm is from Mahendra Piraveenan, Mikhail Prokopenko, and
|
||||
Liaquat Hossain [1]_
|
||||
Pair dependencies are calculated and accumulated using [2]_
|
||||
|
||||
For weighted graphs the edge weights must be greater than zero.
|
||||
Zero edge weights can produce an infinite number of equal length
|
||||
paths between pairs of nodes.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Mahendra Piraveenan, Mikhail Prokopenko, Liaquat Hossain
|
||||
Percolation Centrality: Quantifying Graph-Theoretic Impact of Nodes
|
||||
during Percolation in Networks
|
||||
http://journals.plos.org/plosone/article?id=10.1371/journal.pone.0053095
|
||||
.. [2] Ulrik Brandes:
|
||||
A Faster Algorithm for Betweenness Centrality.
|
||||
Journal of Mathematical Sociology 25(2):163-177, 2001.
|
||||
https://doi.org/10.1080/0022250X.2001.9990249
|
||||
"""
|
||||
percolation = dict.fromkeys(G, 0.0) # b[v]=0 for v in G
|
||||
|
||||
nodes = G
|
||||
|
||||
if states is None:
|
||||
states = nx.get_node_attributes(nodes, attribute, default=1)
|
||||
|
||||
# sum of all percolation states
|
||||
p_sigma_x_t = 0.0
|
||||
for v in states.values():
|
||||
p_sigma_x_t += v
|
||||
|
||||
for s in nodes:
|
||||
# single source shortest paths
|
||||
if weight is None: # use BFS
|
||||
S, P, sigma, _ = shortest_path(G, s)
|
||||
else: # use Dijkstra's algorithm
|
||||
S, P, sigma, _ = dijkstra(G, s, weight)
|
||||
# accumulation
|
||||
percolation = _accumulate_percolation(
|
||||
percolation, S, P, sigma, s, states, p_sigma_x_t
|
||||
)
|
||||
|
||||
n = len(G)
|
||||
|
||||
for v in percolation:
|
||||
percolation[v] *= 1 / (n - 2)
|
||||
|
||||
return percolation
|
||||
|
||||
|
||||
def _accumulate_percolation(percolation, S, P, sigma, s, states, p_sigma_x_t):
|
||||
delta = dict.fromkeys(S, 0)
|
||||
while S:
|
||||
w = S.pop()
|
||||
coeff = (1 + delta[w]) / sigma[w]
|
||||
for v in P[w]:
|
||||
delta[v] += sigma[v] * coeff
|
||||
if w != s:
|
||||
# percolation weight
|
||||
pw_s_w = states[s] / (p_sigma_x_t - states[w])
|
||||
percolation[w] += delta[w] * pw_s_w
|
||||
return percolation
|
||||
@@ -0,0 +1,209 @@
|
||||
"""Functions for computing reaching centrality of a node or a graph."""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import pairwise
|
||||
|
||||
__all__ = ["global_reaching_centrality", "local_reaching_centrality"]
|
||||
|
||||
|
||||
def _average_weight(G, path, weight=None):
|
||||
"""Returns the average weight of an edge in a weighted path.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A networkx graph.
|
||||
|
||||
path: list
|
||||
A list of vertices that define the path.
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
If None, edge weights are ignored. Then the average weight of an edge
|
||||
is assumed to be the multiplicative inverse of the length of the path.
|
||||
Otherwise holds the name of the edge attribute used as weight.
|
||||
"""
|
||||
path_length = len(path) - 1
|
||||
if path_length <= 0:
|
||||
return 0
|
||||
if weight is None:
|
||||
return 1 / path_length
|
||||
total_weight = sum(G.edges[i, j][weight] for i, j in pairwise(path))
|
||||
return total_weight / path_length
|
||||
|
||||
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def global_reaching_centrality(G, weight=None, normalized=True):
|
||||
"""Returns the global reaching centrality of a directed graph.
|
||||
|
||||
The *global reaching centrality* of a weighted directed graph is the
|
||||
average over all nodes of the difference between the local reaching
|
||||
centrality of the node and the greatest local reaching centrality of
|
||||
any node in the graph [1]_. For more information on the local
|
||||
reaching centrality, see :func:`local_reaching_centrality`.
|
||||
Informally, the local reaching centrality is the proportion of the
|
||||
graph that is reachable from the neighbors of the node.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : DiGraph
|
||||
A networkx DiGraph.
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
Attribute to use for edge weights. If ``None``, each edge weight
|
||||
is assumed to be one. A higher weight implies a stronger
|
||||
connection between nodes and a *shorter* path length.
|
||||
|
||||
normalized : bool, optional (default=True)
|
||||
Whether to normalize the edge weights by the total sum of edge
|
||||
weights.
|
||||
|
||||
Returns
|
||||
-------
|
||||
h : float
|
||||
The global reaching centrality of the graph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.DiGraph()
|
||||
>>> G.add_edge(1, 2)
|
||||
>>> G.add_edge(1, 3)
|
||||
>>> nx.global_reaching_centrality(G)
|
||||
1.0
|
||||
>>> G.add_edge(3, 2)
|
||||
>>> nx.global_reaching_centrality(G)
|
||||
0.75
|
||||
|
||||
See also
|
||||
--------
|
||||
local_reaching_centrality
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Mones, Enys, Lilla Vicsek, and Tamás Vicsek.
|
||||
"Hierarchy Measure for Complex Networks."
|
||||
*PLoS ONE* 7.3 (2012): e33799.
|
||||
https://doi.org/10.1371/journal.pone.0033799
|
||||
"""
|
||||
if nx.is_negatively_weighted(G, weight=weight):
|
||||
raise nx.NetworkXError("edge weights must be positive")
|
||||
total_weight = G.size(weight=weight)
|
||||
if total_weight <= 0:
|
||||
raise nx.NetworkXError("Size of G must be positive")
|
||||
# If provided, weights must be interpreted as connection strength
|
||||
# (so higher weights are more likely to be chosen). However, the
|
||||
# shortest path algorithms in NetworkX assume the provided "weight"
|
||||
# is actually a distance (so edges with higher weight are less
|
||||
# likely to be chosen). Therefore we need to invert the weights when
|
||||
# computing shortest paths.
|
||||
#
|
||||
# If weight is None, we leave it as-is so that the shortest path
|
||||
# algorithm can use a faster, unweighted algorithm.
|
||||
if weight is not None:
|
||||
|
||||
def as_distance(u, v, d):
|
||||
return total_weight / d.get(weight, 1)
|
||||
|
||||
shortest_paths = dict(nx.shortest_path(G, weight=as_distance))
|
||||
else:
|
||||
shortest_paths = dict(nx.shortest_path(G))
|
||||
|
||||
centrality = local_reaching_centrality
|
||||
# TODO This can be trivially parallelized.
|
||||
lrc = [
|
||||
centrality(G, node, paths=paths, weight=weight, normalized=normalized)
|
||||
for node, paths in shortest_paths.items()
|
||||
]
|
||||
|
||||
max_lrc = max(lrc)
|
||||
return sum(max_lrc - c for c in lrc) / (len(G) - 1)
|
||||
|
||||
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def local_reaching_centrality(G, v, paths=None, weight=None, normalized=True):
|
||||
"""Returns the local reaching centrality of a node in a directed
|
||||
graph.
|
||||
|
||||
The *local reaching centrality* of a node in a directed graph is the
|
||||
proportion of other nodes reachable from that node [1]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : DiGraph
|
||||
A NetworkX DiGraph.
|
||||
|
||||
v : node
|
||||
A node in the directed graph `G`.
|
||||
|
||||
paths : dictionary (default=None)
|
||||
If this is not `None` it must be a dictionary representation
|
||||
of single-source shortest paths, as computed by, for example,
|
||||
:func:`networkx.shortest_path` with source node `v`. Use this
|
||||
keyword argument if you intend to invoke this function many
|
||||
times but don't want the paths to be recomputed each time.
|
||||
|
||||
weight : None or string, optional (default=None)
|
||||
Attribute to use for edge weights. If `None`, each edge weight
|
||||
is assumed to be one. A higher weight implies a stronger
|
||||
connection between nodes and a *shorter* path length.
|
||||
|
||||
normalized : bool, optional (default=True)
|
||||
Whether to normalize the edge weights by the total sum of edge
|
||||
weights.
|
||||
|
||||
Returns
|
||||
-------
|
||||
h : float
|
||||
The local reaching centrality of the node ``v`` in the graph
|
||||
``G``.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.DiGraph()
|
||||
>>> G.add_edges_from([(1, 2), (1, 3)])
|
||||
>>> nx.local_reaching_centrality(G, 3)
|
||||
0.0
|
||||
>>> G.add_edge(3, 2)
|
||||
>>> nx.local_reaching_centrality(G, 3)
|
||||
0.5
|
||||
|
||||
See also
|
||||
--------
|
||||
global_reaching_centrality
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Mones, Enys, Lilla Vicsek, and Tamás Vicsek.
|
||||
"Hierarchy Measure for Complex Networks."
|
||||
*PLoS ONE* 7.3 (2012): e33799.
|
||||
https://doi.org/10.1371/journal.pone.0033799
|
||||
"""
|
||||
# Corner case: graph with single node containing a self-loop
|
||||
if (total_weight := G.size(weight=weight)) > 0 and len(G) == 1:
|
||||
raise nx.NetworkXError(
|
||||
"local_reaching_centrality of a single node with self-loop not well-defined"
|
||||
)
|
||||
if paths is None:
|
||||
if nx.is_negatively_weighted(G, weight=weight):
|
||||
raise nx.NetworkXError("edge weights must be positive")
|
||||
if total_weight <= 0:
|
||||
raise nx.NetworkXError("Size of G must be positive")
|
||||
if weight is not None:
|
||||
# Interpret weights as lengths.
|
||||
def as_distance(u, v, d):
|
||||
return total_weight / d.get(weight, 1)
|
||||
|
||||
paths = nx.shortest_path(G, source=v, weight=as_distance)
|
||||
else:
|
||||
paths = nx.shortest_path(G, source=v)
|
||||
# If the graph is unweighted, simply return the proportion of nodes
|
||||
# reachable from the source node ``v``.
|
||||
if weight is None and G.is_directed():
|
||||
return (len(paths) - 1) / (len(G) - 1)
|
||||
if normalized and weight is not None:
|
||||
norm = G.size(weight=weight) / G.size()
|
||||
else:
|
||||
norm = 1
|
||||
# TODO This can be trivially parallelized.
|
||||
avgw = (_average_weight(G, path, weight=weight) for path in paths.values())
|
||||
sum_avg_weight = sum(avgw) / norm
|
||||
return sum_avg_weight / (len(G) - 1)
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
"""Copyright (c) 2015 – Thomson Licensing, SAS
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted (subject to the limitations in the
|
||||
disclaimer below) provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of Thomson Licensing, or Technicolor, nor the names
|
||||
of its contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE
|
||||
GRANTED BY THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT
|
||||
HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
|
||||
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
||||
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
|
||||
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import not_implemented_for
|
||||
|
||||
# Authors: Erwan Le Merrer (erwan.lemerrer@technicolor.com)
|
||||
|
||||
__all__ = ["second_order_centrality"]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def second_order_centrality(G, weight="weight"):
|
||||
"""Compute the second order centrality for nodes of G.
|
||||
|
||||
The second order centrality of a given node is the standard deviation of
|
||||
the return times to that node of a perpetual random walk on G:
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX connected and undirected graph.
|
||||
|
||||
weight : string or None, optional (default="weight")
|
||||
The name of an edge attribute that holds the numerical value
|
||||
used as a weight. If None then each edge has weight 1.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary keyed by node with second order centrality as the value.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.star_graph(10)
|
||||
>>> soc = nx.second_order_centrality(G)
|
||||
>>> print(sorted(soc.items(), key=lambda x: x[1])[0][0]) # pick first id
|
||||
0
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXException
|
||||
If the graph G is empty, non connected or has negative weights.
|
||||
|
||||
See Also
|
||||
--------
|
||||
betweenness_centrality
|
||||
|
||||
Notes
|
||||
-----
|
||||
Lower values of second order centrality indicate higher centrality.
|
||||
|
||||
The algorithm is from Kermarrec, Le Merrer, Sericola and Trédan [1]_.
|
||||
|
||||
This code implements the analytical version of the algorithm, i.e.,
|
||||
there is no simulation of a random walk process involved. The random walk
|
||||
is here unbiased (corresponding to eq 6 of the paper [1]_), thus the
|
||||
centrality values are the standard deviations for random walk return times
|
||||
on the transformed input graph G (equal in-degree at each nodes by adding
|
||||
self-loops).
|
||||
|
||||
Complexity of this implementation, made to run locally on a single machine,
|
||||
is O(n^3), with n the size of G, which makes it viable only for small
|
||||
graphs.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Anne-Marie Kermarrec, Erwan Le Merrer, Bruno Sericola, Gilles Trédan
|
||||
"Second order centrality: Distributed assessment of nodes criticity in
|
||||
complex networks", Elsevier Computer Communications 34(5):619-628, 2011.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
n = len(G)
|
||||
|
||||
if n == 0:
|
||||
raise nx.NetworkXException("Empty graph.")
|
||||
if not nx.is_connected(G):
|
||||
raise nx.NetworkXException("Non connected graph.")
|
||||
if any(d.get(weight, 0) < 0 for u, v, d in G.edges(data=True)):
|
||||
raise nx.NetworkXException("Graph has negative edge weights.")
|
||||
|
||||
# balancing G for Metropolis-Hastings random walks
|
||||
G = nx.DiGraph(G)
|
||||
in_deg = dict(G.in_degree(weight=weight))
|
||||
d_max = max(in_deg.values())
|
||||
for i, deg in in_deg.items():
|
||||
if deg < d_max:
|
||||
G.add_edge(i, i, weight=d_max - deg)
|
||||
|
||||
P = nx.to_numpy_array(G)
|
||||
P /= P.sum(axis=1)[:, np.newaxis] # to transition probability matrix
|
||||
|
||||
def _Qj(P, j):
|
||||
P = P.copy()
|
||||
P[:, j] = 0
|
||||
return P
|
||||
|
||||
M = np.empty([n, n])
|
||||
|
||||
for i in range(n):
|
||||
M[:, i] = np.linalg.solve(
|
||||
np.identity(n) - _Qj(P, i), np.ones([n, 1])[:, 0]
|
||||
) # eq 3
|
||||
|
||||
return dict(
|
||||
zip(
|
||||
G.nodes,
|
||||
(float(np.sqrt(2 * np.sum(M[:, i]) - n * (n + 1))) for i in range(n)),
|
||||
)
|
||||
) # eq 6
|
||||
+361
@@ -0,0 +1,361 @@
|
||||
"""
|
||||
Subraph centrality and communicability betweenness.
|
||||
"""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import not_implemented_for
|
||||
|
||||
__all__ = [
|
||||
"subgraph_centrality_exp",
|
||||
"subgraph_centrality",
|
||||
"communicability_betweenness_centrality",
|
||||
"estrada_index",
|
||||
]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable
|
||||
def subgraph_centrality_exp(G, *, normalized=False):
|
||||
r"""Returns the subgraph centrality for each node of G.
|
||||
|
||||
Subgraph centrality of a node `n` is the sum of weighted closed
|
||||
walks of all lengths starting and ending at node `n`. The weights
|
||||
decrease with path length. Each closed walk is associated with a
|
||||
connected subgraph ([1]_).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G: graph
|
||||
normalized : bool
|
||||
If True, normalize the centrality values using the largest eigenvalue of the
|
||||
adjacency matrix so that the centrality values are generally between 0 and 1.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes:dictionary
|
||||
Dictionary of nodes with subgraph centrality as the value.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXError
|
||||
If the graph is not undirected and simple.
|
||||
|
||||
See Also
|
||||
--------
|
||||
subgraph_centrality:
|
||||
Alternative algorithm of the subgraph centrality for each node of G.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This version of the algorithm exponentiates the adjacency matrix.
|
||||
|
||||
The subgraph centrality of a node `u` in G can be found using
|
||||
the matrix exponential of the adjacency matrix of G [1]_,
|
||||
|
||||
.. math::
|
||||
|
||||
SC(u)=(e^A)_{uu} .
|
||||
|
||||
Examples
|
||||
--------
|
||||
(Example from [1]_)
|
||||
|
||||
>>> G = nx.Graph(
|
||||
... [
|
||||
... (1, 2),
|
||||
... (1, 5),
|
||||
... (1, 8),
|
||||
... (2, 3),
|
||||
... (2, 8),
|
||||
... (3, 4),
|
||||
... (3, 6),
|
||||
... (4, 5),
|
||||
... (4, 7),
|
||||
... (5, 6),
|
||||
... (6, 7),
|
||||
... (7, 8),
|
||||
... ]
|
||||
... )
|
||||
>>> sc = nx.subgraph_centrality_exp(G)
|
||||
>>> print([f"{node} {sc[node]:0.2f}" for node in sorted(sc)])
|
||||
['1 3.90', '2 3.90', '3 3.64', '4 3.71', '5 3.64', '6 3.71', '7 3.64', '8 3.90']
|
||||
>>> sc = nx.subgraph_centrality(G, normalized=True)
|
||||
>>> print([f"{node} {sc[node]:0.3f}" for node in sorted(sc)])
|
||||
['1 0.194', '2 0.194', '3 0.181', '4 0.184', '5 0.181', '6 0.184', '7 0.181', '8 0.194']
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Ernesto Estrada, Juan A. Rodriguez-Velazquez,
|
||||
"Subgraph centrality in complex networks",
|
||||
Physical Review E 71, 056103 (2005).
|
||||
https://arxiv.org/abs/cond-mat/0504730
|
||||
|
||||
"""
|
||||
# alternative implementation that calculates the matrix exponential
|
||||
import scipy as sp
|
||||
|
||||
nodelist = list(G) # ordering of nodes in matrix
|
||||
A = nx.to_numpy_array(G, nodelist)
|
||||
# convert to 0-1 matrix
|
||||
A[A != 0.0] = 1
|
||||
expA = sp.linalg.expm(A)
|
||||
values = map(float, expA.diagonal())
|
||||
if normalized:
|
||||
values = values / values.max()
|
||||
# convert diagonal to dictionary keyed by node
|
||||
sc = dict(zip(nodelist, values))
|
||||
return sc
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable
|
||||
def subgraph_centrality(G, *, normalized=False):
|
||||
r"""Returns subgraph centrality for each node in G.
|
||||
|
||||
Subgraph centrality of a node `n` is the sum of weighted closed
|
||||
walks of all lengths starting and ending at node `n`. The weights
|
||||
decrease with path length. Each closed walk is associated with a
|
||||
connected subgraph ([1]_).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G: Graph
|
||||
normalized : bool
|
||||
If True, normalize the centrality values using the largest eigenvalue of the
|
||||
adjacency matrix so that the centrality values are generally between 0 and 1.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with subgraph centrality as the value.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXError
|
||||
If the graph is not undirected and simple.
|
||||
|
||||
See Also
|
||||
--------
|
||||
subgraph_centrality_exp:
|
||||
Alternative algorithm of the subgraph centrality for each node of G.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This version of the algorithm computes eigenvalues and eigenvectors
|
||||
of the adjacency matrix.
|
||||
|
||||
Subgraph centrality of a node `u` in G can be found using
|
||||
a spectral decomposition of the adjacency matrix [1]_,
|
||||
|
||||
.. math::
|
||||
|
||||
SC(u)=\sum_{j=1}^{N}(v_{j}^{u})^2 e^{\lambda_{j}},
|
||||
|
||||
where `v_j` is an eigenvector of the adjacency matrix `A` of G
|
||||
corresponding to the eigenvalue `\lambda_j`.
|
||||
|
||||
Examples
|
||||
--------
|
||||
(Example from [1]_)
|
||||
|
||||
>>> G = nx.Graph(
|
||||
... [
|
||||
... (1, 2),
|
||||
... (1, 5),
|
||||
... (1, 8),
|
||||
... (2, 3),
|
||||
... (2, 8),
|
||||
... (3, 4),
|
||||
... (3, 6),
|
||||
... (4, 5),
|
||||
... (4, 7),
|
||||
... (5, 6),
|
||||
... (6, 7),
|
||||
... (7, 8),
|
||||
... ]
|
||||
... )
|
||||
>>> sc = nx.subgraph_centrality(G)
|
||||
>>> print([f"{node} {sc[node]:0.2f}" for node in sorted(sc)])
|
||||
['1 3.90', '2 3.90', '3 3.64', '4 3.71', '5 3.64', '6 3.71', '7 3.64', '8 3.90']
|
||||
>>> sc = nx.subgraph_centrality(G, normalized=True)
|
||||
>>> print([f"{node} {sc[node]:0.3f}" for node in sorted(sc)])
|
||||
['1 0.194', '2 0.194', '3 0.181', '4 0.184', '5 0.181', '6 0.184', '7 0.181', '8 0.194']
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Ernesto Estrada, Juan A. Rodriguez-Velazquez,
|
||||
"Subgraph centrality in complex networks",
|
||||
Physical Review E 71, 056103 (2005).
|
||||
https://arxiv.org/abs/cond-mat/0504730
|
||||
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
nodelist = list(G) # ordering of nodes in matrix
|
||||
A = nx.to_numpy_array(G, nodelist)
|
||||
# convert to 0-1 matrix
|
||||
A[np.nonzero(A)] = 1
|
||||
w, v = np.linalg.eigh(A)
|
||||
vsquare = np.array(v) ** 2
|
||||
if normalized:
|
||||
expw = np.exp(w - w.max())
|
||||
else:
|
||||
expw = np.exp(w)
|
||||
xg = vsquare @ expw
|
||||
# convert vector dictionary keyed by node
|
||||
sc = dict(zip(nodelist, map(float, xg)))
|
||||
return sc
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable
|
||||
def communicability_betweenness_centrality(G):
|
||||
r"""Returns subgraph communicability for all pairs of nodes in G.
|
||||
|
||||
Communicability betweenness measure makes use of the number of walks
|
||||
connecting every pair of nodes as the basis of a betweenness centrality
|
||||
measure.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G: graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with communicability betweenness as the value.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXError
|
||||
If the graph is not undirected and simple.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Let `G=(V,E)` be a simple undirected graph with `n` nodes and `m` edges,
|
||||
and `A` denote the adjacency matrix of `G`.
|
||||
|
||||
Let `G(r)=(V,E(r))` be the graph resulting from
|
||||
removing all edges connected to node `r` but not the node itself.
|
||||
|
||||
The adjacency matrix for `G(r)` is `A+E(r)`, where `E(r)` has nonzeros
|
||||
only in row and column `r`.
|
||||
|
||||
The subraph betweenness of a node `r` is [1]_
|
||||
|
||||
.. math::
|
||||
|
||||
\omega_{r} = \frac{1}{C}\sum_{p}\sum_{q}\frac{G_{prq}}{G_{pq}},
|
||||
p\neq q, q\neq r,
|
||||
|
||||
where
|
||||
`G_{prq}=(e^{A}_{pq} - (e^{A+E(r)})_{pq}` is the number of walks
|
||||
involving node r,
|
||||
`G_{pq}=(e^{A})_{pq}` is the number of closed walks starting
|
||||
at node `p` and ending at node `q`,
|
||||
and `C=(n-1)^{2}-(n-1)` is a normalization factor equal to the
|
||||
number of terms in the sum.
|
||||
|
||||
The resulting `\omega_{r}` takes values between zero and one.
|
||||
The lower bound cannot be attained for a connected
|
||||
graph, and the upper bound is attained in the star graph.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Ernesto Estrada, Desmond J. Higham, Naomichi Hatano,
|
||||
"Communicability Betweenness in Complex Networks"
|
||||
Physica A 388 (2009) 764-774.
|
||||
https://arxiv.org/abs/0905.4102
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.Graph([(0, 1), (1, 2), (1, 5), (5, 4), (2, 4), (2, 3), (4, 3), (3, 6)])
|
||||
>>> cbc = nx.communicability_betweenness_centrality(G)
|
||||
>>> print([f"{node} {cbc[node]:0.2f}" for node in sorted(cbc)])
|
||||
['0 0.03', '1 0.45', '2 0.51', '3 0.45', '4 0.40', '5 0.19', '6 0.03']
|
||||
"""
|
||||
import numpy as np
|
||||
import scipy as sp
|
||||
|
||||
nodelist = list(G) # ordering of nodes in matrix
|
||||
n = len(nodelist)
|
||||
A = nx.to_numpy_array(G, nodelist)
|
||||
# convert to 0-1 matrix
|
||||
A[np.nonzero(A)] = 1
|
||||
expA = sp.linalg.expm(A)
|
||||
mapping = dict(zip(nodelist, range(n)))
|
||||
cbc = {}
|
||||
for v in G:
|
||||
# remove row and col of node v
|
||||
i = mapping[v]
|
||||
row = A[i, :].copy()
|
||||
col = A[:, i].copy()
|
||||
A[i, :] = 0
|
||||
A[:, i] = 0
|
||||
B = (expA - sp.linalg.expm(A)) / expA
|
||||
# sum with row/col of node v and diag set to zero
|
||||
B[i, :] = 0
|
||||
B[:, i] = 0
|
||||
B -= np.diag(np.diag(B))
|
||||
cbc[v] = float(B.sum())
|
||||
# put row and col back
|
||||
A[i, :] = row
|
||||
A[:, i] = col
|
||||
# rescale when more than two nodes
|
||||
order = len(cbc)
|
||||
if order > 2:
|
||||
scale = 1.0 / ((order - 1.0) ** 2 - (order - 1.0))
|
||||
cbc = {node: value * scale for node, value in cbc.items()}
|
||||
return cbc
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def estrada_index(G):
|
||||
r"""Returns the Estrada index of a the graph G.
|
||||
|
||||
The Estrada Index is a topological index of folding or 3D "compactness" ([1]_).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G: graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
estrada index: float
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXError
|
||||
If the graph is not undirected and simple.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Let `G=(V,E)` be a simple undirected graph with `n` nodes and let
|
||||
`\lambda_{1}\leq\lambda_{2}\leq\cdots\lambda_{n}`
|
||||
be a non-increasing ordering of the eigenvalues of its adjacency
|
||||
matrix `A`. The Estrada index is ([1]_, [2]_)
|
||||
|
||||
.. math::
|
||||
EE(G)=\sum_{j=1}^n e^{\lambda _j}.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] E. Estrada, "Characterization of 3D molecular structure",
|
||||
Chem. Phys. Lett. 319, 713 (2000).
|
||||
https://doi.org/10.1016/S0009-2614(00)00158-5
|
||||
.. [2] José Antonio de la Peñaa, Ivan Gutman, Juan Rada,
|
||||
"Estimating the Estrada index",
|
||||
Linear Algebra and its Applications. 427, 1 (2007).
|
||||
https://doi.org/10.1016/j.laa.2007.06.020
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.Graph([(0, 1), (1, 2), (1, 5), (5, 4), (2, 4), (2, 3), (4, 3), (3, 6)])
|
||||
>>> ei = nx.estrada_index(G)
|
||||
>>> print(f"{ei:0.5}")
|
||||
20.55
|
||||
"""
|
||||
return sum(subgraph_centrality(G).values())
|
||||
+923
@@ -0,0 +1,923 @@
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
|
||||
|
||||
def weighted_G():
|
||||
G = nx.Graph()
|
||||
G.add_edge(0, 1, weight=3)
|
||||
G.add_edge(0, 2, weight=2)
|
||||
G.add_edge(0, 3, weight=6)
|
||||
G.add_edge(0, 4, weight=4)
|
||||
G.add_edge(1, 3, weight=5)
|
||||
G.add_edge(1, 5, weight=5)
|
||||
G.add_edge(2, 4, weight=1)
|
||||
G.add_edge(3, 4, weight=2)
|
||||
G.add_edge(3, 5, weight=1)
|
||||
G.add_edge(4, 5, weight=4)
|
||||
return G
|
||||
|
||||
|
||||
class TestBetweennessCentrality:
|
||||
def test_K5(self):
|
||||
"""Betweenness centrality: K5"""
|
||||
G = nx.complete_graph(5)
|
||||
b = nx.betweenness_centrality(G, weight=None, normalized=False)
|
||||
b_answer = {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0}
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_K5_endpoints(self):
|
||||
"""Betweenness centrality: K5 endpoints"""
|
||||
G = nx.complete_graph(5)
|
||||
b = nx.betweenness_centrality(G, weight=None, normalized=False, endpoints=True)
|
||||
b_answer = {0: 4.0, 1: 4.0, 2: 4.0, 3: 4.0, 4: 4.0}
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
# normalized = True case
|
||||
b = nx.betweenness_centrality(G, weight=None, normalized=True, endpoints=True)
|
||||
b_answer = {0: 0.4, 1: 0.4, 2: 0.4, 3: 0.4, 4: 0.4}
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_P3_normalized(self):
|
||||
"""Betweenness centrality: P3 normalized"""
|
||||
G = nx.path_graph(3)
|
||||
b = nx.betweenness_centrality(G, weight=None, normalized=True)
|
||||
b_answer = {0: 0.0, 1: 1.0, 2: 0.0}
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_P3(self):
|
||||
"""Betweenness centrality: P3"""
|
||||
G = nx.path_graph(3)
|
||||
b_answer = {0: 0.0, 1: 1.0, 2: 0.0}
|
||||
b = nx.betweenness_centrality(G, weight=None, normalized=False)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_sample_from_P3(self):
|
||||
"""Betweenness centrality: P3 sample"""
|
||||
G = nx.path_graph(3)
|
||||
b_answer = {0: 0.0, 1: 1.0, 2: 0.0}
|
||||
b = nx.betweenness_centrality(G, k=3, weight=None, normalized=False, seed=1)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
b = nx.betweenness_centrality(G, k=2, weight=None, normalized=False, seed=1)
|
||||
# python versions give different results with same seed
|
||||
b_approx1 = {0: 0.0, 1: 1.0, 2: 0.0}
|
||||
b_approx2 = {0: 0.0, 1: 0.5, 2: 0.0}
|
||||
for n in sorted(G):
|
||||
assert b[n] in (b_approx1[n], b_approx2[n])
|
||||
|
||||
def test_P3_endpoints(self):
|
||||
"""Betweenness centrality: P3 endpoints"""
|
||||
G = nx.path_graph(3)
|
||||
b_answer = {0: 2.0, 1: 3.0, 2: 2.0}
|
||||
b = nx.betweenness_centrality(G, weight=None, normalized=False, endpoints=True)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
# normalized = True case
|
||||
b_answer = {0: 2 / 3, 1: 1.0, 2: 2 / 3}
|
||||
b = nx.betweenness_centrality(G, weight=None, normalized=True, endpoints=True)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_krackhardt_kite_graph(self):
|
||||
"""Betweenness centrality: Krackhardt kite graph"""
|
||||
G = nx.krackhardt_kite_graph()
|
||||
b_answer = {
|
||||
0: 1.667,
|
||||
1: 1.667,
|
||||
2: 0.000,
|
||||
3: 7.333,
|
||||
4: 0.000,
|
||||
5: 16.667,
|
||||
6: 16.667,
|
||||
7: 28.000,
|
||||
8: 16.000,
|
||||
9: 0.000,
|
||||
}
|
||||
for b in b_answer:
|
||||
b_answer[b] /= 2
|
||||
b = nx.betweenness_centrality(G, weight=None, normalized=False)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-3)
|
||||
|
||||
def test_krackhardt_kite_graph_normalized(self):
|
||||
"""Betweenness centrality: Krackhardt kite graph normalized"""
|
||||
G = nx.krackhardt_kite_graph()
|
||||
b_answer = {
|
||||
0: 0.023,
|
||||
1: 0.023,
|
||||
2: 0.000,
|
||||
3: 0.102,
|
||||
4: 0.000,
|
||||
5: 0.231,
|
||||
6: 0.231,
|
||||
7: 0.389,
|
||||
8: 0.222,
|
||||
9: 0.000,
|
||||
}
|
||||
b = nx.betweenness_centrality(G, weight=None, normalized=True)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-3)
|
||||
|
||||
def test_florentine_families_graph(self):
|
||||
"""Betweenness centrality: Florentine families graph"""
|
||||
G = nx.florentine_families_graph()
|
||||
b_answer = {
|
||||
"Acciaiuoli": 0.000,
|
||||
"Albizzi": 0.212,
|
||||
"Barbadori": 0.093,
|
||||
"Bischeri": 0.104,
|
||||
"Castellani": 0.055,
|
||||
"Ginori": 0.000,
|
||||
"Guadagni": 0.255,
|
||||
"Lamberteschi": 0.000,
|
||||
"Medici": 0.522,
|
||||
"Pazzi": 0.000,
|
||||
"Peruzzi": 0.022,
|
||||
"Ridolfi": 0.114,
|
||||
"Salviati": 0.143,
|
||||
"Strozzi": 0.103,
|
||||
"Tornabuoni": 0.092,
|
||||
}
|
||||
|
||||
b = nx.betweenness_centrality(G, weight=None, normalized=True)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-3)
|
||||
|
||||
def test_les_miserables_graph(self):
|
||||
"""Betweenness centrality: Les Miserables graph"""
|
||||
G = nx.les_miserables_graph()
|
||||
b_answer = {
|
||||
"Napoleon": 0.000,
|
||||
"Myriel": 0.177,
|
||||
"MlleBaptistine": 0.000,
|
||||
"MmeMagloire": 0.000,
|
||||
"CountessDeLo": 0.000,
|
||||
"Geborand": 0.000,
|
||||
"Champtercier": 0.000,
|
||||
"Cravatte": 0.000,
|
||||
"Count": 0.000,
|
||||
"OldMan": 0.000,
|
||||
"Valjean": 0.570,
|
||||
"Labarre": 0.000,
|
||||
"Marguerite": 0.000,
|
||||
"MmeDeR": 0.000,
|
||||
"Isabeau": 0.000,
|
||||
"Gervais": 0.000,
|
||||
"Listolier": 0.000,
|
||||
"Tholomyes": 0.041,
|
||||
"Fameuil": 0.000,
|
||||
"Blacheville": 0.000,
|
||||
"Favourite": 0.000,
|
||||
"Dahlia": 0.000,
|
||||
"Zephine": 0.000,
|
||||
"Fantine": 0.130,
|
||||
"MmeThenardier": 0.029,
|
||||
"Thenardier": 0.075,
|
||||
"Cosette": 0.024,
|
||||
"Javert": 0.054,
|
||||
"Fauchelevent": 0.026,
|
||||
"Bamatabois": 0.008,
|
||||
"Perpetue": 0.000,
|
||||
"Simplice": 0.009,
|
||||
"Scaufflaire": 0.000,
|
||||
"Woman1": 0.000,
|
||||
"Judge": 0.000,
|
||||
"Champmathieu": 0.000,
|
||||
"Brevet": 0.000,
|
||||
"Chenildieu": 0.000,
|
||||
"Cochepaille": 0.000,
|
||||
"Pontmercy": 0.007,
|
||||
"Boulatruelle": 0.000,
|
||||
"Eponine": 0.011,
|
||||
"Anzelma": 0.000,
|
||||
"Woman2": 0.000,
|
||||
"MotherInnocent": 0.000,
|
||||
"Gribier": 0.000,
|
||||
"MmeBurgon": 0.026,
|
||||
"Jondrette": 0.000,
|
||||
"Gavroche": 0.165,
|
||||
"Gillenormand": 0.020,
|
||||
"Magnon": 0.000,
|
||||
"MlleGillenormand": 0.048,
|
||||
"MmePontmercy": 0.000,
|
||||
"MlleVaubois": 0.000,
|
||||
"LtGillenormand": 0.000,
|
||||
"Marius": 0.132,
|
||||
"BaronessT": 0.000,
|
||||
"Mabeuf": 0.028,
|
||||
"Enjolras": 0.043,
|
||||
"Combeferre": 0.001,
|
||||
"Prouvaire": 0.000,
|
||||
"Feuilly": 0.001,
|
||||
"Courfeyrac": 0.005,
|
||||
"Bahorel": 0.002,
|
||||
"Bossuet": 0.031,
|
||||
"Joly": 0.002,
|
||||
"Grantaire": 0.000,
|
||||
"MotherPlutarch": 0.000,
|
||||
"Gueulemer": 0.005,
|
||||
"Babet": 0.005,
|
||||
"Claquesous": 0.005,
|
||||
"Montparnasse": 0.004,
|
||||
"Toussaint": 0.000,
|
||||
"Child1": 0.000,
|
||||
"Child2": 0.000,
|
||||
"Brujon": 0.000,
|
||||
"MmeHucheloup": 0.000,
|
||||
}
|
||||
|
||||
b = nx.betweenness_centrality(G, weight=None, normalized=True)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-3)
|
||||
|
||||
def test_ladder_graph(self):
|
||||
"""Betweenness centrality: Ladder graph"""
|
||||
G = nx.Graph() # ladder_graph(3)
|
||||
G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (4, 5), (3, 5)])
|
||||
b_answer = {0: 1.667, 1: 1.667, 2: 6.667, 3: 6.667, 4: 1.667, 5: 1.667}
|
||||
for b in b_answer:
|
||||
b_answer[b] /= 2
|
||||
b = nx.betweenness_centrality(G, weight=None, normalized=False)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-3)
|
||||
|
||||
def test_disconnected_path(self):
|
||||
"""Betweenness centrality: disconnected path"""
|
||||
G = nx.Graph()
|
||||
nx.add_path(G, [0, 1, 2])
|
||||
nx.add_path(G, [3, 4, 5, 6])
|
||||
b_answer = {0: 0, 1: 1, 2: 0, 3: 0, 4: 2, 5: 2, 6: 0}
|
||||
b = nx.betweenness_centrality(G, weight=None, normalized=False)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_disconnected_path_endpoints(self):
|
||||
"""Betweenness centrality: disconnected path endpoints"""
|
||||
G = nx.Graph()
|
||||
nx.add_path(G, [0, 1, 2])
|
||||
nx.add_path(G, [3, 4, 5, 6])
|
||||
b_answer = {0: 2, 1: 3, 2: 2, 3: 3, 4: 5, 5: 5, 6: 3}
|
||||
b = nx.betweenness_centrality(G, weight=None, normalized=False, endpoints=True)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
# normalized = True case
|
||||
b = nx.betweenness_centrality(G, weight=None, normalized=True, endpoints=True)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n] / 21, abs=1e-7)
|
||||
|
||||
def test_directed_path(self):
|
||||
"""Betweenness centrality: directed path"""
|
||||
G = nx.DiGraph()
|
||||
nx.add_path(G, [0, 1, 2])
|
||||
b = nx.betweenness_centrality(G, weight=None, normalized=False)
|
||||
b_answer = {0: 0.0, 1: 1.0, 2: 0.0}
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_directed_path_normalized(self):
|
||||
"""Betweenness centrality: directed path normalized"""
|
||||
G = nx.DiGraph()
|
||||
nx.add_path(G, [0, 1, 2])
|
||||
b = nx.betweenness_centrality(G, weight=None, normalized=True)
|
||||
b_answer = {0: 0.0, 1: 0.5, 2: 0.0}
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("normalized", "endpoints", "is_directed", "k", "expected"),
|
||||
[
|
||||
(True, True, True, None, {0: 1.0, 1: 0.4, 2: 0.4, 3: 0.4, 4: 0.4}),
|
||||
(True, True, True, 1, {0: 1.0, 1: 1.0, 2: 0.25, 3: 0.25, 4: 0.25}),
|
||||
(True, True, False, None, {0: 1.0, 1: 0.4, 2: 0.4, 3: 0.4, 4: 0.4}),
|
||||
(True, True, False, 1, {0: 1.0, 1: 1.0, 2: 0.25, 3: 0.25, 4: 0.25}),
|
||||
(True, False, True, None, {0: 1.0, 1: 0, 2: 0.0, 3: 0.0, 4: 0.0}),
|
||||
(True, False, True, 1, {0: 1.0, 1: math.nan, 2: 0.0, 3: 0.0, 4: 0.0}),
|
||||
(True, False, False, None, {0: 1.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0}),
|
||||
(True, False, False, 1, {0: 1.0, 1: math.nan, 2: 0.0, 3: 0.0, 4: 0.0}),
|
||||
(False, True, True, None, {0: 20.0, 1: 8.0, 2: 8.0, 3: 8.0, 4: 8.0}),
|
||||
(False, True, True, 1, {0: 20.0, 1: 20.0, 2: 5.0, 3: 5.0, 4: 5.0}),
|
||||
(False, True, False, None, {0: 10.0, 1: 4.0, 2: 4.0, 3: 4.0, 4: 4.0}),
|
||||
(False, True, False, 1, {0: 10.0, 1: 10.0, 2: 2.5, 3: 2.5, 4: 2.5}),
|
||||
(False, False, True, None, {0: 12.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0}),
|
||||
(False, False, True, 1, {0: 12.0, 1: math.nan, 2: 0.0, 3: 0.0, 4: 0.0}),
|
||||
(False, False, False, None, {0: 6.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0}),
|
||||
(False, False, False, 1, {0: 6.0, 1: math.nan, 2: 0.0, 3: 0.0, 4: 0.0}),
|
||||
],
|
||||
)
|
||||
def test_scale_with_k_on_star_graph(
|
||||
self, normalized, endpoints, is_directed, k, expected
|
||||
):
|
||||
# seed=1 selects node 1 as the initial node when using k=1.
|
||||
# Recall node 0 is the center of the star graph.
|
||||
G = nx.star_graph(4)
|
||||
if is_directed:
|
||||
G = G.to_directed()
|
||||
b = nx.betweenness_centrality(
|
||||
G, k=k, seed=1, endpoints=endpoints, normalized=normalized
|
||||
)
|
||||
assert b == pytest.approx(expected, nan_ok=True)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("normalized", "endpoints", "is_directed", "k", "expected"),
|
||||
[
|
||||
(
|
||||
*(True, True, True, None), # Use *() splatting for better autoformat
|
||||
{0: 14 / 20, 1: 14 / 20, 2: 14 / 20, 3: 14 / 20, 4: 14 / 20},
|
||||
),
|
||||
(
|
||||
*(True, True, True, 3),
|
||||
{0: 9 / 12, 1: 11 / 12, 2: 9 / 12, 3: 6 / 12, 4: 7 / 12},
|
||||
),
|
||||
(
|
||||
*(True, True, False, None),
|
||||
{0: 10 / 20, 1: 10 / 20, 2: 10 / 20, 3: 10 / 20, 4: 10 / 20},
|
||||
),
|
||||
(
|
||||
*(True, True, False, 3),
|
||||
{0: 8 / 12, 1: 7 / 12, 2: 4 / 12, 3: 4 / 12, 4: 7 / 12},
|
||||
),
|
||||
(
|
||||
*(True, False, True, None),
|
||||
{0: 6 / 12, 1: 6 / 12, 2: 6 / 12, 3: 6 / 12, 4: 6 / 12},
|
||||
),
|
||||
(
|
||||
*(True, False, True, 3),
|
||||
# Use 6 instead of 9 for denominator for source nodes 0, 1, and 4
|
||||
{0: 3 / 6, 1: 5 / 6, 2: 6 / 9, 3: 3 / 9, 4: 1 / 6},
|
||||
),
|
||||
(
|
||||
*(True, False, False, None),
|
||||
{0: 2 / 12, 1: 2 / 12, 2: 2 / 12, 3: 2 / 12, 4: 2 / 12},
|
||||
),
|
||||
(
|
||||
*(True, False, False, 3),
|
||||
# Use 6 instead of 9 for denominator for source nodes 0, 1, and 4
|
||||
{0: 2 / 6, 1: 1 / 6, 2: 1 / 9, 3: 1 / 9, 4: 1 / 6},
|
||||
),
|
||||
(False, True, True, None, {0: 14, 1: 14, 2: 14, 3: 14, 4: 14}),
|
||||
(
|
||||
*(False, True, True, 3),
|
||||
{0: 9 * 5 / 3, 1: 11 * 5 / 3, 2: 9 * 5 / 3, 3: 6 * 5 / 3, 4: 7 * 5 / 3},
|
||||
),
|
||||
(False, True, False, None, {0: 5, 1: 5, 2: 5, 3: 5, 4: 5}),
|
||||
(
|
||||
*(False, True, False, 3),
|
||||
{0: 8 * 5 / 6, 1: 7 * 5 / 6, 2: 4 * 5 / 6, 3: 4 * 5 / 6, 4: 7 * 5 / 6},
|
||||
),
|
||||
(False, False, True, None, {0: 6, 1: 6, 2: 6, 3: 6, 4: 6}),
|
||||
(
|
||||
*(False, False, True, 3),
|
||||
# Use 2 instead of 3 for denominator for source nodes 0, 1, and 4
|
||||
{0: 3 * 4 / 2, 1: 5 * 4 / 2, 2: 6 * 4 / 3, 3: 3 * 4 / 3, 4: 1 * 4 / 2},
|
||||
),
|
||||
(False, False, False, None, {0: 1, 1: 1, 2: 1, 3: 1, 4: 1}),
|
||||
(
|
||||
*(False, False, False, 3),
|
||||
# Use 4 instead of 6 for denominator for source nodes 0, 1, and 4
|
||||
{0: 2 * 4 / 4, 1: 1 * 4 / 4, 2: 1 * 4 / 6, 3: 1 * 4 / 6, 4: 1 * 4 / 4},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_scale_with_k_on_cycle_graph(
|
||||
self, normalized, endpoints, is_directed, k, expected
|
||||
):
|
||||
# seed=1 selects nodes 0, 1, and 4 as the initial nodes when using k=3.
|
||||
G = nx.cycle_graph(5, create_using=nx.DiGraph if is_directed else nx.Graph)
|
||||
b = nx.betweenness_centrality(
|
||||
G, k=k, seed=1, endpoints=endpoints, normalized=normalized
|
||||
)
|
||||
assert b == pytest.approx(expected)
|
||||
|
||||
def test_k_out_of_bounds_raises(self):
|
||||
G = nx.cycle_graph(4)
|
||||
with pytest.raises(ValueError, match="larger"):
|
||||
nx.betweenness_centrality(G, k=5)
|
||||
with pytest.raises(ValueError, match="negative"):
|
||||
nx.betweenness_centrality(G, k=-1)
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
nx.betweenness_centrality(G, k=0)
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
nx.betweenness_centrality(G, k=0, normalized=False)
|
||||
# Test edge case: use full population when k == len(G)
|
||||
# Should we warn or raise instead?
|
||||
b1 = nx.betweenness_centrality(G, k=4, endpoints=False)
|
||||
b2 = nx.betweenness_centrality(G, endpoints=False)
|
||||
assert b1 == b2
|
||||
|
||||
|
||||
class TestWeightedBetweennessCentrality:
|
||||
def test_K5(self):
|
||||
"""Weighted betweenness centrality: K5"""
|
||||
G = nx.complete_graph(5)
|
||||
b = nx.betweenness_centrality(G, weight="weight", normalized=False)
|
||||
b_answer = {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0}
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_P3_normalized(self):
|
||||
"""Weighted betweenness centrality: P3 normalized"""
|
||||
G = nx.path_graph(3)
|
||||
b = nx.betweenness_centrality(G, weight="weight", normalized=True)
|
||||
b_answer = {0: 0.0, 1: 1.0, 2: 0.0}
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_P3(self):
|
||||
"""Weighted betweenness centrality: P3"""
|
||||
G = nx.path_graph(3)
|
||||
b_answer = {0: 0.0, 1: 1.0, 2: 0.0}
|
||||
b = nx.betweenness_centrality(G, weight="weight", normalized=False)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_krackhardt_kite_graph(self):
|
||||
"""Weighted betweenness centrality: Krackhardt kite graph"""
|
||||
G = nx.krackhardt_kite_graph()
|
||||
b_answer = {
|
||||
0: 1.667,
|
||||
1: 1.667,
|
||||
2: 0.000,
|
||||
3: 7.333,
|
||||
4: 0.000,
|
||||
5: 16.667,
|
||||
6: 16.667,
|
||||
7: 28.000,
|
||||
8: 16.000,
|
||||
9: 0.000,
|
||||
}
|
||||
for b in b_answer:
|
||||
b_answer[b] /= 2
|
||||
|
||||
b = nx.betweenness_centrality(G, weight="weight", normalized=False)
|
||||
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-3)
|
||||
|
||||
def test_krackhardt_kite_graph_normalized(self):
|
||||
"""Weighted betweenness centrality:
|
||||
Krackhardt kite graph normalized
|
||||
"""
|
||||
G = nx.krackhardt_kite_graph()
|
||||
b_answer = {
|
||||
0: 0.023,
|
||||
1: 0.023,
|
||||
2: 0.000,
|
||||
3: 0.102,
|
||||
4: 0.000,
|
||||
5: 0.231,
|
||||
6: 0.231,
|
||||
7: 0.389,
|
||||
8: 0.222,
|
||||
9: 0.000,
|
||||
}
|
||||
b = nx.betweenness_centrality(G, weight="weight", normalized=True)
|
||||
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-3)
|
||||
|
||||
def test_florentine_families_graph(self):
|
||||
"""Weighted betweenness centrality:
|
||||
Florentine families graph"""
|
||||
G = nx.florentine_families_graph()
|
||||
b_answer = {
|
||||
"Acciaiuoli": 0.000,
|
||||
"Albizzi": 0.212,
|
||||
"Barbadori": 0.093,
|
||||
"Bischeri": 0.104,
|
||||
"Castellani": 0.055,
|
||||
"Ginori": 0.000,
|
||||
"Guadagni": 0.255,
|
||||
"Lamberteschi": 0.000,
|
||||
"Medici": 0.522,
|
||||
"Pazzi": 0.000,
|
||||
"Peruzzi": 0.022,
|
||||
"Ridolfi": 0.114,
|
||||
"Salviati": 0.143,
|
||||
"Strozzi": 0.103,
|
||||
"Tornabuoni": 0.092,
|
||||
}
|
||||
|
||||
b = nx.betweenness_centrality(G, weight="weight", normalized=True)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-3)
|
||||
|
||||
def test_les_miserables_graph(self):
|
||||
"""Weighted betweenness centrality: Les Miserables graph"""
|
||||
G = nx.les_miserables_graph()
|
||||
b_answer = {
|
||||
"Napoleon": 0.000,
|
||||
"Myriel": 0.177,
|
||||
"MlleBaptistine": 0.000,
|
||||
"MmeMagloire": 0.000,
|
||||
"CountessDeLo": 0.000,
|
||||
"Geborand": 0.000,
|
||||
"Champtercier": 0.000,
|
||||
"Cravatte": 0.000,
|
||||
"Count": 0.000,
|
||||
"OldMan": 0.000,
|
||||
"Valjean": 0.454,
|
||||
"Labarre": 0.000,
|
||||
"Marguerite": 0.009,
|
||||
"MmeDeR": 0.000,
|
||||
"Isabeau": 0.000,
|
||||
"Gervais": 0.000,
|
||||
"Listolier": 0.000,
|
||||
"Tholomyes": 0.066,
|
||||
"Fameuil": 0.000,
|
||||
"Blacheville": 0.000,
|
||||
"Favourite": 0.000,
|
||||
"Dahlia": 0.000,
|
||||
"Zephine": 0.000,
|
||||
"Fantine": 0.114,
|
||||
"MmeThenardier": 0.046,
|
||||
"Thenardier": 0.129,
|
||||
"Cosette": 0.075,
|
||||
"Javert": 0.193,
|
||||
"Fauchelevent": 0.026,
|
||||
"Bamatabois": 0.080,
|
||||
"Perpetue": 0.000,
|
||||
"Simplice": 0.001,
|
||||
"Scaufflaire": 0.000,
|
||||
"Woman1": 0.000,
|
||||
"Judge": 0.000,
|
||||
"Champmathieu": 0.000,
|
||||
"Brevet": 0.000,
|
||||
"Chenildieu": 0.000,
|
||||
"Cochepaille": 0.000,
|
||||
"Pontmercy": 0.023,
|
||||
"Boulatruelle": 0.000,
|
||||
"Eponine": 0.023,
|
||||
"Anzelma": 0.000,
|
||||
"Woman2": 0.000,
|
||||
"MotherInnocent": 0.000,
|
||||
"Gribier": 0.000,
|
||||
"MmeBurgon": 0.026,
|
||||
"Jondrette": 0.000,
|
||||
"Gavroche": 0.285,
|
||||
"Gillenormand": 0.024,
|
||||
"Magnon": 0.005,
|
||||
"MlleGillenormand": 0.036,
|
||||
"MmePontmercy": 0.005,
|
||||
"MlleVaubois": 0.000,
|
||||
"LtGillenormand": 0.015,
|
||||
"Marius": 0.072,
|
||||
"BaronessT": 0.004,
|
||||
"Mabeuf": 0.089,
|
||||
"Enjolras": 0.003,
|
||||
"Combeferre": 0.000,
|
||||
"Prouvaire": 0.000,
|
||||
"Feuilly": 0.004,
|
||||
"Courfeyrac": 0.001,
|
||||
"Bahorel": 0.007,
|
||||
"Bossuet": 0.028,
|
||||
"Joly": 0.000,
|
||||
"Grantaire": 0.036,
|
||||
"MotherPlutarch": 0.000,
|
||||
"Gueulemer": 0.025,
|
||||
"Babet": 0.015,
|
||||
"Claquesous": 0.042,
|
||||
"Montparnasse": 0.050,
|
||||
"Toussaint": 0.011,
|
||||
"Child1": 0.000,
|
||||
"Child2": 0.000,
|
||||
"Brujon": 0.002,
|
||||
"MmeHucheloup": 0.034,
|
||||
}
|
||||
|
||||
b = nx.betweenness_centrality(G, weight="weight", normalized=True)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-3)
|
||||
|
||||
def test_ladder_graph(self):
|
||||
"""Weighted betweenness centrality: Ladder graph"""
|
||||
G = nx.Graph() # ladder_graph(3)
|
||||
G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (4, 5), (3, 5)])
|
||||
b_answer = {0: 1.667, 1: 1.667, 2: 6.667, 3: 6.667, 4: 1.667, 5: 1.667}
|
||||
for b in b_answer:
|
||||
b_answer[b] /= 2
|
||||
b = nx.betweenness_centrality(G, weight="weight", normalized=False)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-3)
|
||||
|
||||
def test_G(self):
|
||||
"""Weighted betweenness centrality: G"""
|
||||
G = weighted_G()
|
||||
b_answer = {0: 2.0, 1: 0.0, 2: 4.0, 3: 3.0, 4: 4.0, 5: 0.0}
|
||||
b = nx.betweenness_centrality(G, weight="weight", normalized=False)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_G2(self):
|
||||
"""Weighted betweenness centrality: G2"""
|
||||
G = nx.DiGraph()
|
||||
G.add_weighted_edges_from(
|
||||
[
|
||||
("s", "u", 10),
|
||||
("s", "x", 5),
|
||||
("u", "v", 1),
|
||||
("u", "x", 2),
|
||||
("v", "y", 1),
|
||||
("x", "u", 3),
|
||||
("x", "v", 5),
|
||||
("x", "y", 2),
|
||||
("y", "s", 7),
|
||||
("y", "v", 6),
|
||||
]
|
||||
)
|
||||
|
||||
b_answer = {"y": 5.0, "x": 5.0, "s": 4.0, "u": 2.0, "v": 2.0}
|
||||
|
||||
b = nx.betweenness_centrality(G, weight="weight", normalized=False)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_G3(self):
|
||||
"""Weighted betweenness centrality: G3"""
|
||||
G = nx.MultiGraph(weighted_G())
|
||||
es = list(G.edges(data=True))[::2] # duplicate every other edge
|
||||
G.add_edges_from(es)
|
||||
b_answer = {0: 2.0, 1: 0.0, 2: 4.0, 3: 3.0, 4: 4.0, 5: 0.0}
|
||||
b = nx.betweenness_centrality(G, weight="weight", normalized=False)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_G4(self):
|
||||
"""Weighted betweenness centrality: G4"""
|
||||
G = nx.MultiDiGraph()
|
||||
G.add_weighted_edges_from(
|
||||
[
|
||||
("s", "u", 10),
|
||||
("s", "x", 5),
|
||||
("s", "x", 6),
|
||||
("u", "v", 1),
|
||||
("u", "x", 2),
|
||||
("v", "y", 1),
|
||||
("v", "y", 1),
|
||||
("x", "u", 3),
|
||||
("x", "v", 5),
|
||||
("x", "y", 2),
|
||||
("x", "y", 3),
|
||||
("y", "s", 7),
|
||||
("y", "v", 6),
|
||||
("y", "v", 6),
|
||||
]
|
||||
)
|
||||
|
||||
b_answer = {"y": 5.0, "x": 5.0, "s": 4.0, "u": 2.0, "v": 2.0}
|
||||
|
||||
b = nx.betweenness_centrality(G, weight="weight", normalized=False)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
|
||||
class TestEdgeBetweennessCentrality:
|
||||
def test_K5(self):
|
||||
"""Edge betweenness centrality: K5"""
|
||||
G = nx.complete_graph(5)
|
||||
b = nx.edge_betweenness_centrality(G, weight=None, normalized=False)
|
||||
b_answer = dict.fromkeys(G.edges(), 1)
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_normalized_K5(self):
|
||||
"""Edge betweenness centrality: K5"""
|
||||
G = nx.complete_graph(5)
|
||||
b = nx.edge_betweenness_centrality(G, weight=None, normalized=True)
|
||||
b_answer = dict.fromkeys(G.edges(), 1 / 10)
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_C4(self):
|
||||
"""Edge betweenness centrality: C4"""
|
||||
G = nx.cycle_graph(4)
|
||||
b = nx.edge_betweenness_centrality(G, weight=None, normalized=True)
|
||||
b_answer = {(0, 1): 2, (0, 3): 2, (1, 2): 2, (2, 3): 2}
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n] / 6, abs=1e-7)
|
||||
|
||||
def test_P4(self):
|
||||
"""Edge betweenness centrality: P4"""
|
||||
G = nx.path_graph(4)
|
||||
b = nx.edge_betweenness_centrality(G, weight=None, normalized=False)
|
||||
b_answer = {(0, 1): 3, (1, 2): 4, (2, 3): 3}
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_normalized_P4(self):
|
||||
"""Edge betweenness centrality: P4"""
|
||||
G = nx.path_graph(4)
|
||||
b = nx.edge_betweenness_centrality(G, weight=None, normalized=True)
|
||||
b_answer = {(0, 1): 3, (1, 2): 4, (2, 3): 3}
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n] / 6, abs=1e-7)
|
||||
|
||||
def test_balanced_tree(self):
|
||||
"""Edge betweenness centrality: balanced tree"""
|
||||
G = nx.balanced_tree(r=2, h=2)
|
||||
b = nx.edge_betweenness_centrality(G, weight=None, normalized=False)
|
||||
b_answer = {(0, 1): 12, (0, 2): 12, (1, 3): 6, (1, 4): 6, (2, 5): 6, (2, 6): 6}
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_edge_betweenness_k(self):
|
||||
"""Ensure setting `k` properly limits the number of source nodes."""
|
||||
G = nx.path_graph(3)
|
||||
# This choice of `k` and `seed` selects nodes 0 and 2.
|
||||
# There is only one shortest path between any two pairs of nodes.
|
||||
# With source nodes 0 and 2, this means that both edges are part of
|
||||
# three shortest paths:
|
||||
# For (0, 1): sp(0, 1), sp(0, 2), sp(2, 0).
|
||||
# For (1, 2): sp(0, 2), sp(2, 0), sp(2, 1).
|
||||
# We normalize by 2 because the graph is undirected, and by
|
||||
# `k / n = 2 / 3` because we are only considering a subset of source
|
||||
# nodes.
|
||||
# This means the final eb centralities should be 3 / 2 / (2 / 3) = 9 / 4.
|
||||
eb = nx.edge_betweenness_centrality(G, k=2, seed=42, normalized=False)
|
||||
assert eb == {(0, 1): 9 / 4, (1, 2): 9 / 4}
|
||||
# When normalization is `True`, we instead divide by the number of total
|
||||
# `(s, t)` pairs, i.e. `k * (n - 1) = 4`, meaning we get an eb of `3 / 4`.
|
||||
eb = nx.edge_betweenness_centrality(G, k=2, seed=42, normalized=True)
|
||||
assert eb == {(0, 1): 3 / 4, (1, 2): 3 / 4}
|
||||
|
||||
|
||||
class TestWeightedEdgeBetweennessCentrality:
|
||||
def test_K5(self):
|
||||
"""Edge betweenness centrality: K5"""
|
||||
G = nx.complete_graph(5)
|
||||
b = nx.edge_betweenness_centrality(G, weight="weight", normalized=False)
|
||||
b_answer = dict.fromkeys(G.edges(), 1)
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_C4(self):
|
||||
"""Edge betweenness centrality: C4"""
|
||||
G = nx.cycle_graph(4)
|
||||
b = nx.edge_betweenness_centrality(G, weight="weight", normalized=False)
|
||||
b_answer = {(0, 1): 2, (0, 3): 2, (1, 2): 2, (2, 3): 2}
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_P4(self):
|
||||
"""Edge betweenness centrality: P4"""
|
||||
G = nx.path_graph(4)
|
||||
b = nx.edge_betweenness_centrality(G, weight="weight", normalized=False)
|
||||
b_answer = {(0, 1): 3, (1, 2): 4, (2, 3): 3}
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_balanced_tree(self):
|
||||
"""Edge betweenness centrality: balanced tree"""
|
||||
G = nx.balanced_tree(r=2, h=2)
|
||||
b = nx.edge_betweenness_centrality(G, weight="weight", normalized=False)
|
||||
b_answer = {(0, 1): 12, (0, 2): 12, (1, 3): 6, (1, 4): 6, (2, 5): 6, (2, 6): 6}
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_weighted_graph(self):
|
||||
"""Edge betweenness centrality: weighted"""
|
||||
eList = [
|
||||
(0, 1, 5),
|
||||
(0, 2, 4),
|
||||
(0, 3, 3),
|
||||
(0, 4, 2),
|
||||
(1, 2, 4),
|
||||
(1, 3, 1),
|
||||
(1, 4, 3),
|
||||
(2, 4, 5),
|
||||
(3, 4, 4),
|
||||
]
|
||||
G = nx.Graph()
|
||||
G.add_weighted_edges_from(eList)
|
||||
b = nx.edge_betweenness_centrality(G, weight="weight", normalized=False)
|
||||
b_answer = {
|
||||
(0, 1): 0.0,
|
||||
(0, 2): 1.0,
|
||||
(0, 3): 2.0,
|
||||
(0, 4): 1.0,
|
||||
(1, 2): 2.0,
|
||||
(1, 3): 3.5,
|
||||
(1, 4): 1.5,
|
||||
(2, 4): 1.0,
|
||||
(3, 4): 0.5,
|
||||
}
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_normalized_weighted_graph(self):
|
||||
"""Edge betweenness centrality: normalized weighted"""
|
||||
eList = [
|
||||
(0, 1, 5),
|
||||
(0, 2, 4),
|
||||
(0, 3, 3),
|
||||
(0, 4, 2),
|
||||
(1, 2, 4),
|
||||
(1, 3, 1),
|
||||
(1, 4, 3),
|
||||
(2, 4, 5),
|
||||
(3, 4, 4),
|
||||
]
|
||||
G = nx.Graph()
|
||||
G.add_weighted_edges_from(eList)
|
||||
b = nx.edge_betweenness_centrality(G, weight="weight", normalized=True)
|
||||
b_answer = {
|
||||
(0, 1): 0.0,
|
||||
(0, 2): 1.0,
|
||||
(0, 3): 2.0,
|
||||
(0, 4): 1.0,
|
||||
(1, 2): 2.0,
|
||||
(1, 3): 3.5,
|
||||
(1, 4): 1.5,
|
||||
(2, 4): 1.0,
|
||||
(3, 4): 0.5,
|
||||
}
|
||||
norm = len(G) * (len(G) - 1) / 2
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n] / norm, abs=1e-7)
|
||||
|
||||
def test_weighted_multigraph(self):
|
||||
"""Edge betweenness centrality: weighted multigraph"""
|
||||
eList = [
|
||||
(0, 1, 5),
|
||||
(0, 1, 4),
|
||||
(0, 2, 4),
|
||||
(0, 3, 3),
|
||||
(0, 3, 3),
|
||||
(0, 4, 2),
|
||||
(1, 2, 4),
|
||||
(1, 3, 1),
|
||||
(1, 3, 2),
|
||||
(1, 4, 3),
|
||||
(1, 4, 4),
|
||||
(2, 4, 5),
|
||||
(3, 4, 4),
|
||||
(3, 4, 4),
|
||||
]
|
||||
G = nx.MultiGraph()
|
||||
G.add_weighted_edges_from(eList)
|
||||
b = nx.edge_betweenness_centrality(G, weight="weight", normalized=False)
|
||||
b_answer = {
|
||||
(0, 1, 0): 0.0,
|
||||
(0, 1, 1): 0.5,
|
||||
(0, 2, 0): 1.0,
|
||||
(0, 3, 0): 0.75,
|
||||
(0, 3, 1): 0.75,
|
||||
(0, 4, 0): 1.0,
|
||||
(1, 2, 0): 2.0,
|
||||
(1, 3, 0): 3.0,
|
||||
(1, 3, 1): 0.0,
|
||||
(1, 4, 0): 1.5,
|
||||
(1, 4, 1): 0.0,
|
||||
(2, 4, 0): 1.0,
|
||||
(3, 4, 0): 0.25,
|
||||
(3, 4, 1): 0.25,
|
||||
}
|
||||
for n in sorted(G.edges(keys=True)):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_normalized_weighted_multigraph(self):
|
||||
"""Edge betweenness centrality: normalized weighted multigraph"""
|
||||
eList = [
|
||||
(0, 1, 5),
|
||||
(0, 1, 4),
|
||||
(0, 2, 4),
|
||||
(0, 3, 3),
|
||||
(0, 3, 3),
|
||||
(0, 4, 2),
|
||||
(1, 2, 4),
|
||||
(1, 3, 1),
|
||||
(1, 3, 2),
|
||||
(1, 4, 3),
|
||||
(1, 4, 4),
|
||||
(2, 4, 5),
|
||||
(3, 4, 4),
|
||||
(3, 4, 4),
|
||||
]
|
||||
G = nx.MultiGraph()
|
||||
G.add_weighted_edges_from(eList)
|
||||
b = nx.edge_betweenness_centrality(G, weight="weight", normalized=True)
|
||||
b_answer = {
|
||||
(0, 1, 0): 0.0,
|
||||
(0, 1, 1): 0.5,
|
||||
(0, 2, 0): 1.0,
|
||||
(0, 3, 0): 0.75,
|
||||
(0, 3, 1): 0.75,
|
||||
(0, 4, 0): 1.0,
|
||||
(1, 2, 0): 2.0,
|
||||
(1, 3, 0): 3.0,
|
||||
(1, 3, 1): 0.0,
|
||||
(1, 4, 0): 1.5,
|
||||
(1, 4, 1): 0.0,
|
||||
(2, 4, 0): 1.0,
|
||||
(3, 4, 0): 0.25,
|
||||
(3, 4, 1): 0.25,
|
||||
}
|
||||
norm = len(G) * (len(G) - 1) / 2
|
||||
for n in sorted(G.edges(keys=True)):
|
||||
assert b[n] == pytest.approx(b_answer[n] / norm, abs=1e-7)
|
||||
+354
@@ -0,0 +1,354 @@
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
|
||||
|
||||
class TestSubsetBetweennessCentrality:
|
||||
def test_K5(self):
|
||||
"""Betweenness Centrality Subset: K5"""
|
||||
G = nx.complete_graph(5)
|
||||
b = nx.betweenness_centrality_subset(
|
||||
G, sources=[0], targets=[1, 3], weight=None
|
||||
)
|
||||
b_answer = {0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0}
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_P5_directed(self):
|
||||
"""Betweenness Centrality Subset: P5 directed"""
|
||||
G = nx.DiGraph()
|
||||
nx.add_path(G, range(5))
|
||||
b_answer = {0: 0, 1: 1, 2: 1, 3: 0, 4: 0, 5: 0}
|
||||
b = nx.betweenness_centrality_subset(G, sources=[0], targets=[3], weight=None)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_P5(self):
|
||||
"""Betweenness Centrality Subset: P5"""
|
||||
G = nx.Graph()
|
||||
nx.add_path(G, range(5))
|
||||
b_answer = {0: 0, 1: 0.5, 2: 0.5, 3: 0, 4: 0, 5: 0}
|
||||
b = nx.betweenness_centrality_subset(G, sources=[0], targets=[3], weight=None)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_P5_multiple_target(self):
|
||||
"""Betweenness Centrality Subset: P5 multiple target"""
|
||||
G = nx.Graph()
|
||||
nx.add_path(G, range(5))
|
||||
b_answer = {0: 0, 1: 1, 2: 1, 3: 0.5, 4: 0, 5: 0}
|
||||
b = nx.betweenness_centrality_subset(
|
||||
G, sources=[0], targets=[3, 4], weight=None
|
||||
)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_box(self):
|
||||
"""Betweenness Centrality Subset: box"""
|
||||
G = nx.Graph()
|
||||
G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
|
||||
b_answer = {0: 0, 1: 0.25, 2: 0.25, 3: 0}
|
||||
b = nx.betweenness_centrality_subset(G, sources=[0], targets=[3], weight=None)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_box_and_path(self):
|
||||
"""Betweenness Centrality Subset: box and path"""
|
||||
G = nx.Graph()
|
||||
G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (4, 5)])
|
||||
b_answer = {0: 0, 1: 0.5, 2: 0.5, 3: 0.5, 4: 0, 5: 0}
|
||||
b = nx.betweenness_centrality_subset(
|
||||
G, sources=[0], targets=[3, 4], weight=None
|
||||
)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_box_and_path2(self):
|
||||
"""Betweenness Centrality Subset: box and path multiple target"""
|
||||
G = nx.Graph()
|
||||
G.add_edges_from([(0, 1), (1, 2), (2, 3), (1, 20), (20, 3), (3, 4)])
|
||||
b_answer = {0: 0, 1: 1.0, 2: 0.5, 20: 0.5, 3: 0.5, 4: 0}
|
||||
b = nx.betweenness_centrality_subset(
|
||||
G, sources=[0], targets=[3, 4], weight=None
|
||||
)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_diamond_multi_path(self):
|
||||
"""Betweenness Centrality Subset: Diamond Multi Path"""
|
||||
G = nx.Graph()
|
||||
G.add_edges_from(
|
||||
[
|
||||
(1, 2),
|
||||
(1, 3),
|
||||
(1, 4),
|
||||
(1, 5),
|
||||
(1, 10),
|
||||
(10, 11),
|
||||
(11, 12),
|
||||
(12, 9),
|
||||
(2, 6),
|
||||
(3, 6),
|
||||
(4, 6),
|
||||
(5, 7),
|
||||
(7, 8),
|
||||
(6, 8),
|
||||
(8, 9),
|
||||
]
|
||||
)
|
||||
b = nx.betweenness_centrality_subset(G, sources=[1], targets=[9], weight=None)
|
||||
|
||||
expected_b = {
|
||||
1: 0,
|
||||
2: 1.0 / 10,
|
||||
3: 1.0 / 10,
|
||||
4: 1.0 / 10,
|
||||
5: 1.0 / 10,
|
||||
6: 3.0 / 10,
|
||||
7: 1.0 / 10,
|
||||
8: 4.0 / 10,
|
||||
9: 0,
|
||||
10: 1.0 / 10,
|
||||
11: 1.0 / 10,
|
||||
12: 1.0 / 10,
|
||||
}
|
||||
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(expected_b[n], abs=1e-7)
|
||||
|
||||
def test_normalized_p2(self):
|
||||
"""
|
||||
Betweenness Centrality Subset: Normalized P2
|
||||
if n <= 2: no normalization, betweenness centrality should be 0 for all nodes.
|
||||
"""
|
||||
G = nx.Graph()
|
||||
nx.add_path(G, range(2))
|
||||
b_answer = {0: 0, 1: 0.0}
|
||||
b = nx.betweenness_centrality_subset(
|
||||
G, sources=[0], targets=[1], normalized=True, weight=None
|
||||
)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_normalized_P5_directed(self):
|
||||
"""Betweenness Centrality Subset: Normalized Directed P5"""
|
||||
G = nx.DiGraph()
|
||||
nx.add_path(G, range(5))
|
||||
b_answer = {0: 0, 1: 1.0 / 12.0, 2: 1.0 / 12.0, 3: 0, 4: 0, 5: 0}
|
||||
b = nx.betweenness_centrality_subset(
|
||||
G, sources=[0], targets=[3], normalized=True, weight=None
|
||||
)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_weighted_graph(self):
|
||||
"""Betweenness Centrality Subset: Weighted Graph"""
|
||||
G = nx.DiGraph()
|
||||
G.add_edge(0, 1, weight=3)
|
||||
G.add_edge(0, 2, weight=2)
|
||||
G.add_edge(0, 3, weight=6)
|
||||
G.add_edge(0, 4, weight=4)
|
||||
G.add_edge(1, 3, weight=5)
|
||||
G.add_edge(1, 5, weight=5)
|
||||
G.add_edge(2, 4, weight=1)
|
||||
G.add_edge(3, 4, weight=2)
|
||||
G.add_edge(3, 5, weight=1)
|
||||
G.add_edge(4, 5, weight=4)
|
||||
b_answer = {0: 0.0, 1: 0.0, 2: 0.5, 3: 0.5, 4: 0.5, 5: 0.0}
|
||||
b = nx.betweenness_centrality_subset(
|
||||
G, sources=[0], targets=[5], normalized=False, weight="weight"
|
||||
)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
|
||||
class TestEdgeSubsetBetweennessCentrality:
|
||||
def test_K5(self):
|
||||
"""Edge betweenness subset centrality: K5"""
|
||||
G = nx.complete_graph(5)
|
||||
b = nx.edge_betweenness_centrality_subset(
|
||||
G, sources=[0], targets=[1, 3], weight=None
|
||||
)
|
||||
b_answer = dict.fromkeys(G.edges(), 0)
|
||||
b_answer[(0, 3)] = b_answer[(0, 1)] = 0.5
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_P5_directed(self):
|
||||
"""Edge betweenness subset centrality: P5 directed"""
|
||||
G = nx.DiGraph()
|
||||
nx.add_path(G, range(5))
|
||||
b_answer = dict.fromkeys(G.edges(), 0)
|
||||
b_answer[(0, 1)] = b_answer[(1, 2)] = b_answer[(2, 3)] = 1
|
||||
b = nx.edge_betweenness_centrality_subset(
|
||||
G, sources=[0], targets=[3], weight=None
|
||||
)
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_P5(self):
|
||||
"""Edge betweenness subset centrality: P5"""
|
||||
G = nx.Graph()
|
||||
nx.add_path(G, range(5))
|
||||
b_answer = dict.fromkeys(G.edges(), 0)
|
||||
b_answer[(0, 1)] = b_answer[(1, 2)] = b_answer[(2, 3)] = 0.5
|
||||
b = nx.edge_betweenness_centrality_subset(
|
||||
G, sources=[0], targets=[3], weight=None
|
||||
)
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_P5_multiple_target(self):
|
||||
"""Edge betweenness subset centrality: P5 multiple target"""
|
||||
G = nx.Graph()
|
||||
nx.add_path(G, range(5))
|
||||
b_answer = dict.fromkeys(G.edges(), 0)
|
||||
b_answer[(0, 1)] = b_answer[(1, 2)] = b_answer[(2, 3)] = 1
|
||||
b_answer[(3, 4)] = 0.5
|
||||
b = nx.edge_betweenness_centrality_subset(
|
||||
G, sources=[0], targets=[3, 4], weight=None
|
||||
)
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_box(self):
|
||||
"""Edge betweenness subset centrality: box"""
|
||||
G = nx.Graph()
|
||||
G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3)])
|
||||
b_answer = dict.fromkeys(G.edges(), 0)
|
||||
b_answer[(0, 1)] = b_answer[(0, 2)] = 0.25
|
||||
b_answer[(1, 3)] = b_answer[(2, 3)] = 0.25
|
||||
b = nx.edge_betweenness_centrality_subset(
|
||||
G, sources=[0], targets=[3], weight=None
|
||||
)
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_box_and_path(self):
|
||||
"""Edge betweenness subset centrality: box and path"""
|
||||
G = nx.Graph()
|
||||
G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (4, 5)])
|
||||
b_answer = dict.fromkeys(G.edges(), 0)
|
||||
b_answer[(0, 1)] = b_answer[(0, 2)] = 0.5
|
||||
b_answer[(1, 3)] = b_answer[(2, 3)] = 0.5
|
||||
b_answer[(3, 4)] = 0.5
|
||||
b = nx.edge_betweenness_centrality_subset(
|
||||
G, sources=[0], targets=[3, 4], weight=None
|
||||
)
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_box_and_path2(self):
|
||||
"""Edge betweenness subset centrality: box and path multiple target"""
|
||||
G = nx.Graph()
|
||||
G.add_edges_from([(0, 1), (1, 2), (2, 3), (1, 20), (20, 3), (3, 4)])
|
||||
b_answer = dict.fromkeys(G.edges(), 0)
|
||||
b_answer[(0, 1)] = 1.0
|
||||
b_answer[(1, 20)] = b_answer[(3, 20)] = 0.5
|
||||
b_answer[(1, 2)] = b_answer[(2, 3)] = 0.5
|
||||
b_answer[(3, 4)] = 0.5
|
||||
b = nx.edge_betweenness_centrality_subset(
|
||||
G, sources=[0], targets=[3, 4], weight=None
|
||||
)
|
||||
for n in sorted(G.edges()):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_diamond_multi_path(self):
|
||||
"""Edge betweenness subset centrality: Diamond Multi Path"""
|
||||
G = nx.Graph()
|
||||
G.add_edges_from(
|
||||
[
|
||||
(1, 2),
|
||||
(1, 3),
|
||||
(1, 4),
|
||||
(1, 5),
|
||||
(1, 10),
|
||||
(10, 11),
|
||||
(11, 12),
|
||||
(12, 9),
|
||||
(2, 6),
|
||||
(3, 6),
|
||||
(4, 6),
|
||||
(5, 7),
|
||||
(7, 8),
|
||||
(6, 8),
|
||||
(8, 9),
|
||||
]
|
||||
)
|
||||
b_answer = dict.fromkeys(G.edges(), 0)
|
||||
b_answer[(8, 9)] = 0.4
|
||||
b_answer[(6, 8)] = b_answer[(7, 8)] = 0.2
|
||||
b_answer[(2, 6)] = b_answer[(3, 6)] = b_answer[(4, 6)] = 0.2 / 3.0
|
||||
b_answer[(1, 2)] = b_answer[(1, 3)] = b_answer[(1, 4)] = 0.2 / 3.0
|
||||
b_answer[(5, 7)] = 0.2
|
||||
b_answer[(1, 5)] = 0.2
|
||||
b_answer[(9, 12)] = 0.1
|
||||
b_answer[(11, 12)] = b_answer[(10, 11)] = b_answer[(1, 10)] = 0.1
|
||||
b = nx.edge_betweenness_centrality_subset(
|
||||
G, sources=[1], targets=[9], weight=None
|
||||
)
|
||||
for n in G.edges():
|
||||
sort_n = tuple(sorted(n))
|
||||
assert b[n] == pytest.approx(b_answer[sort_n], abs=1e-7)
|
||||
|
||||
def test_normalized_p1(self):
|
||||
"""
|
||||
Edge betweenness subset centrality: P1
|
||||
if n <= 1: no normalization b=0 for all nodes
|
||||
"""
|
||||
G = nx.Graph()
|
||||
nx.add_path(G, range(1))
|
||||
b_answer = dict.fromkeys(G.edges(), 0)
|
||||
b = nx.edge_betweenness_centrality_subset(
|
||||
G, sources=[0], targets=[0], normalized=True, weight=None
|
||||
)
|
||||
for n in G.edges():
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_normalized_P5_directed(self):
|
||||
"""Edge betweenness subset centrality: Normalized Directed P5"""
|
||||
G = nx.DiGraph()
|
||||
nx.add_path(G, range(5))
|
||||
b_answer = dict.fromkeys(G.edges(), 0)
|
||||
b_answer[(0, 1)] = b_answer[(1, 2)] = b_answer[(2, 3)] = 0.05
|
||||
b = nx.edge_betweenness_centrality_subset(
|
||||
G, sources=[0], targets=[3], normalized=True, weight=None
|
||||
)
|
||||
for n in G.edges():
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_weighted_graph(self):
|
||||
"""Edge betweenness subset centrality: Weighted Graph"""
|
||||
G = nx.DiGraph()
|
||||
G.add_edge(0, 1, weight=3)
|
||||
G.add_edge(0, 2, weight=2)
|
||||
G.add_edge(0, 3, weight=6)
|
||||
G.add_edge(0, 4, weight=4)
|
||||
G.add_edge(1, 3, weight=5)
|
||||
G.add_edge(1, 5, weight=5)
|
||||
G.add_edge(2, 4, weight=1)
|
||||
G.add_edge(3, 4, weight=2)
|
||||
G.add_edge(3, 5, weight=1)
|
||||
G.add_edge(4, 5, weight=4)
|
||||
b_answer = dict.fromkeys(G.edges(), 0)
|
||||
b_answer[(0, 2)] = b_answer[(2, 4)] = b_answer[(4, 5)] = 0.5
|
||||
b_answer[(0, 3)] = b_answer[(3, 5)] = 0.5
|
||||
b = nx.edge_betweenness_centrality_subset(
|
||||
G, sources=[0], targets=[5], normalized=False, weight="weight"
|
||||
)
|
||||
for n in G.edges():
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
|
||||
def test_equivalence_non_subset():
|
||||
"""Assert that subset betweenness centrality with all nodes in the
|
||||
subset is equivalent to full betweenness centrality.
|
||||
"""
|
||||
G = nx.path_graph(10, create_using=nx.DiGraph)
|
||||
|
||||
assert nx.betweenness_centrality(G) == nx.betweenness_centrality_subset(
|
||||
G, sources=G.nodes(), targets=G.nodes(), normalized=True
|
||||
)
|
||||
assert nx.edge_betweenness_centrality(G) == nx.edge_betweenness_centrality_subset(
|
||||
G, sources=G.nodes(), targets=G.nodes(), normalized=True
|
||||
)
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
"""
|
||||
Tests for closeness centrality.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def undirected_G():
|
||||
G = nx.fast_gnp_random_graph(n=100, p=0.6, seed=123)
|
||||
cc = nx.closeness_centrality(G)
|
||||
return G, cc
|
||||
|
||||
|
||||
class TestClosenessCentrality:
|
||||
def test_wf_improved(self):
|
||||
G = nx.union(nx.path_graph(4), nx.path_graph([4, 5, 6]))
|
||||
c = nx.closeness_centrality(G)
|
||||
cwf = nx.closeness_centrality(G, wf_improved=False)
|
||||
res = {0: 0.25, 1: 0.375, 2: 0.375, 3: 0.25, 4: 0.222, 5: 0.333, 6: 0.222}
|
||||
wf_res = {0: 0.5, 1: 0.75, 2: 0.75, 3: 0.5, 4: 0.667, 5: 1.0, 6: 0.667}
|
||||
for n in G:
|
||||
assert c[n] == pytest.approx(res[n], abs=1e-3)
|
||||
assert cwf[n] == pytest.approx(wf_res[n], abs=1e-3)
|
||||
|
||||
def test_digraph(self):
|
||||
G = nx.path_graph(3, create_using=nx.DiGraph)
|
||||
c = nx.closeness_centrality(G)
|
||||
cr = nx.closeness_centrality(G.reverse())
|
||||
d = {0: 0.0, 1: 0.500, 2: 0.667}
|
||||
dr = {0: 0.667, 1: 0.500, 2: 0.0}
|
||||
for n in G:
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
assert cr[n] == pytest.approx(dr[n], abs=1e-3)
|
||||
|
||||
def test_k5_closeness(self):
|
||||
G = nx.complete_graph(5)
|
||||
c = nx.closeness_centrality(G)
|
||||
d = {0: 1.000, 1: 1.000, 2: 1.000, 3: 1.000, 4: 1.000}
|
||||
for n in G:
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_p3_closeness(self):
|
||||
G = nx.path_graph(3)
|
||||
c = nx.closeness_centrality(G)
|
||||
d = {0: 0.667, 1: 1.000, 2: 0.667}
|
||||
for n in G:
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_krackhardt_closeness(self):
|
||||
G = nx.krackhardt_kite_graph()
|
||||
c = nx.closeness_centrality(G)
|
||||
d = {
|
||||
0: 0.529,
|
||||
1: 0.529,
|
||||
2: 0.500,
|
||||
3: 0.600,
|
||||
4: 0.500,
|
||||
5: 0.643,
|
||||
6: 0.643,
|
||||
7: 0.600,
|
||||
8: 0.429,
|
||||
9: 0.310,
|
||||
}
|
||||
for n in G:
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_florentine_families_closeness(self):
|
||||
G = nx.florentine_families_graph()
|
||||
c = nx.closeness_centrality(G)
|
||||
d = {
|
||||
"Acciaiuoli": 0.368,
|
||||
"Albizzi": 0.483,
|
||||
"Barbadori": 0.4375,
|
||||
"Bischeri": 0.400,
|
||||
"Castellani": 0.389,
|
||||
"Ginori": 0.333,
|
||||
"Guadagni": 0.467,
|
||||
"Lamberteschi": 0.326,
|
||||
"Medici": 0.560,
|
||||
"Pazzi": 0.286,
|
||||
"Peruzzi": 0.368,
|
||||
"Ridolfi": 0.500,
|
||||
"Salviati": 0.389,
|
||||
"Strozzi": 0.4375,
|
||||
"Tornabuoni": 0.483,
|
||||
}
|
||||
for n in G:
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_les_miserables_closeness(self):
|
||||
G = nx.les_miserables_graph()
|
||||
c = nx.closeness_centrality(G)
|
||||
d = {
|
||||
"Napoleon": 0.302,
|
||||
"Myriel": 0.429,
|
||||
"MlleBaptistine": 0.413,
|
||||
"MmeMagloire": 0.413,
|
||||
"CountessDeLo": 0.302,
|
||||
"Geborand": 0.302,
|
||||
"Champtercier": 0.302,
|
||||
"Cravatte": 0.302,
|
||||
"Count": 0.302,
|
||||
"OldMan": 0.302,
|
||||
"Valjean": 0.644,
|
||||
"Labarre": 0.394,
|
||||
"Marguerite": 0.413,
|
||||
"MmeDeR": 0.394,
|
||||
"Isabeau": 0.394,
|
||||
"Gervais": 0.394,
|
||||
"Listolier": 0.341,
|
||||
"Tholomyes": 0.392,
|
||||
"Fameuil": 0.341,
|
||||
"Blacheville": 0.341,
|
||||
"Favourite": 0.341,
|
||||
"Dahlia": 0.341,
|
||||
"Zephine": 0.341,
|
||||
"Fantine": 0.461,
|
||||
"MmeThenardier": 0.461,
|
||||
"Thenardier": 0.517,
|
||||
"Cosette": 0.478,
|
||||
"Javert": 0.517,
|
||||
"Fauchelevent": 0.402,
|
||||
"Bamatabois": 0.427,
|
||||
"Perpetue": 0.318,
|
||||
"Simplice": 0.418,
|
||||
"Scaufflaire": 0.394,
|
||||
"Woman1": 0.396,
|
||||
"Judge": 0.404,
|
||||
"Champmathieu": 0.404,
|
||||
"Brevet": 0.404,
|
||||
"Chenildieu": 0.404,
|
||||
"Cochepaille": 0.404,
|
||||
"Pontmercy": 0.373,
|
||||
"Boulatruelle": 0.342,
|
||||
"Eponine": 0.396,
|
||||
"Anzelma": 0.352,
|
||||
"Woman2": 0.402,
|
||||
"MotherInnocent": 0.398,
|
||||
"Gribier": 0.288,
|
||||
"MmeBurgon": 0.344,
|
||||
"Jondrette": 0.257,
|
||||
"Gavroche": 0.514,
|
||||
"Gillenormand": 0.442,
|
||||
"Magnon": 0.335,
|
||||
"MlleGillenormand": 0.442,
|
||||
"MmePontmercy": 0.315,
|
||||
"MlleVaubois": 0.308,
|
||||
"LtGillenormand": 0.365,
|
||||
"Marius": 0.531,
|
||||
"BaronessT": 0.352,
|
||||
"Mabeuf": 0.396,
|
||||
"Enjolras": 0.481,
|
||||
"Combeferre": 0.392,
|
||||
"Prouvaire": 0.357,
|
||||
"Feuilly": 0.392,
|
||||
"Courfeyrac": 0.400,
|
||||
"Bahorel": 0.394,
|
||||
"Bossuet": 0.475,
|
||||
"Joly": 0.394,
|
||||
"Grantaire": 0.358,
|
||||
"MotherPlutarch": 0.285,
|
||||
"Gueulemer": 0.463,
|
||||
"Babet": 0.463,
|
||||
"Claquesous": 0.452,
|
||||
"Montparnasse": 0.458,
|
||||
"Toussaint": 0.402,
|
||||
"Child1": 0.342,
|
||||
"Child2": 0.342,
|
||||
"Brujon": 0.380,
|
||||
"MmeHucheloup": 0.353,
|
||||
}
|
||||
for n in G:
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_weighted_closeness(self):
|
||||
edges = [
|
||||
("s", "u", 10),
|
||||
("s", "x", 5),
|
||||
("u", "v", 1),
|
||||
("u", "x", 2),
|
||||
("v", "y", 1),
|
||||
("x", "u", 3),
|
||||
("x", "v", 5),
|
||||
("x", "y", 2),
|
||||
("y", "s", 7),
|
||||
("y", "v", 6),
|
||||
]
|
||||
XG = nx.Graph()
|
||||
XG.add_weighted_edges_from(edges)
|
||||
c = nx.closeness_centrality(XG, distance="weight")
|
||||
d = {"y": 0.200, "x": 0.286, "s": 0.138, "u": 0.235, "v": 0.200}
|
||||
for n in sorted(XG):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
|
||||
class TestIncrementalClosenessCentrality:
|
||||
@staticmethod
|
||||
def pick_add_edge(G):
|
||||
u = nx.utils.arbitrary_element(G)
|
||||
possible_nodes = set(G) - (set(G.neighbors(u)) | {u})
|
||||
v = nx.utils.arbitrary_element(possible_nodes)
|
||||
return (u, v)
|
||||
|
||||
@staticmethod
|
||||
def pick_remove_edge(G):
|
||||
u = nx.utils.arbitrary_element(G)
|
||||
possible_nodes = list(G.neighbors(u))
|
||||
v = nx.utils.arbitrary_element(possible_nodes)
|
||||
return (u, v)
|
||||
|
||||
def test_directed_raises(self):
|
||||
dir_G = nx.gn_graph(n=5)
|
||||
prev_cc = None
|
||||
edge = self.pick_add_edge(dir_G)
|
||||
with pytest.raises(nx.NetworkXNotImplemented):
|
||||
nx.incremental_closeness_centrality(dir_G, edge, prev_cc, insertion=True)
|
||||
|
||||
def test_wrong_size_prev_cc_raises(self, undirected_G):
|
||||
G, prev_cc = undirected_G
|
||||
edge = self.pick_add_edge(G)
|
||||
prev_cc.pop(0)
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
nx.incremental_closeness_centrality(G, edge, prev_cc, insertion=True)
|
||||
|
||||
def test_wrong_nodes_prev_cc_raises(self, undirected_G):
|
||||
G, prev_cc = undirected_G
|
||||
|
||||
edge = self.pick_add_edge(G)
|
||||
num_nodes = len(prev_cc)
|
||||
prev_cc.pop(0)
|
||||
prev_cc[num_nodes] = 0.5
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
nx.incremental_closeness_centrality(G, edge, prev_cc, insertion=True)
|
||||
|
||||
def test_zero_centrality(self):
|
||||
G = nx.path_graph(3)
|
||||
prev_cc = nx.closeness_centrality(G)
|
||||
edge = self.pick_remove_edge(G)
|
||||
test_cc = nx.incremental_closeness_centrality(G, edge, prev_cc, insertion=False)
|
||||
G.remove_edges_from([edge])
|
||||
real_cc = nx.closeness_centrality(G)
|
||||
shared_items = set(test_cc.items()) & set(real_cc.items())
|
||||
assert len(shared_items) == len(real_cc)
|
||||
assert 0 in test_cc.values()
|
||||
|
||||
def test_incremental(self, undirected_G):
|
||||
# Check that incremental and regular give same output
|
||||
G, _ = undirected_G
|
||||
prev_cc = None
|
||||
for i in range(5):
|
||||
if i % 2 == 0:
|
||||
# Remove an edge
|
||||
insert = False
|
||||
edge = self.pick_remove_edge(G)
|
||||
else:
|
||||
# Add an edge
|
||||
insert = True
|
||||
edge = self.pick_add_edge(G)
|
||||
|
||||
test_cc = nx.incremental_closeness_centrality(G, edge, prev_cc, insert)
|
||||
|
||||
if insert:
|
||||
G.add_edges_from([edge])
|
||||
else:
|
||||
G.remove_edges_from([edge])
|
||||
|
||||
real_cc = nx.closeness_centrality(G)
|
||||
|
||||
assert set(test_cc.items()) == set(real_cc.items())
|
||||
|
||||
prev_cc = test_cc
|
||||
+259
@@ -0,0 +1,259 @@
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx import approximate_current_flow_betweenness_centrality as approximate_cfbc
|
||||
from networkx import edge_current_flow_betweenness_centrality as edge_current_flow
|
||||
|
||||
np = pytest.importorskip("numpy")
|
||||
pytest.importorskip("scipy")
|
||||
|
||||
|
||||
class TestFlowBetweennessCentrality:
|
||||
def test_K4_normalized(self):
|
||||
"""Betweenness centrality: K4"""
|
||||
G = nx.complete_graph(4)
|
||||
b = nx.current_flow_betweenness_centrality(G, normalized=True)
|
||||
b_answer = {0: 0.25, 1: 0.25, 2: 0.25, 3: 0.25}
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
G.add_edge(0, 1, weight=0.5, other=0.3)
|
||||
b = nx.current_flow_betweenness_centrality(G, normalized=True, weight=None)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
wb_answer = {0: 0.2222222, 1: 0.2222222, 2: 0.30555555, 3: 0.30555555}
|
||||
b = nx.current_flow_betweenness_centrality(G, normalized=True, weight="weight")
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(wb_answer[n], abs=1e-7)
|
||||
wb_answer = {0: 0.2051282, 1: 0.2051282, 2: 0.33974358, 3: 0.33974358}
|
||||
b = nx.current_flow_betweenness_centrality(G, normalized=True, weight="other")
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(wb_answer[n], abs=1e-7)
|
||||
|
||||
def test_K4(self):
|
||||
"""Betweenness centrality: K4"""
|
||||
G = nx.complete_graph(4)
|
||||
for solver in ["full", "lu", "cg"]:
|
||||
b = nx.current_flow_betweenness_centrality(
|
||||
G, normalized=False, solver=solver
|
||||
)
|
||||
b_answer = {0: 0.75, 1: 0.75, 2: 0.75, 3: 0.75}
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_P4_normalized(self):
|
||||
"""Betweenness centrality: P4 normalized"""
|
||||
G = nx.path_graph(4)
|
||||
b = nx.current_flow_betweenness_centrality(G, normalized=True)
|
||||
b_answer = {0: 0, 1: 2.0 / 3, 2: 2.0 / 3, 3: 0}
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_P4(self):
|
||||
"""Betweenness centrality: P4"""
|
||||
G = nx.path_graph(4)
|
||||
b = nx.current_flow_betweenness_centrality(G, normalized=False)
|
||||
b_answer = {0: 0, 1: 2, 2: 2, 3: 0}
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_star(self):
|
||||
"""Betweenness centrality: star"""
|
||||
G = nx.Graph()
|
||||
nx.add_star(G, ["a", "b", "c", "d"])
|
||||
b = nx.current_flow_betweenness_centrality(G, normalized=True)
|
||||
b_answer = {"a": 1.0, "b": 0.0, "c": 0.0, "d": 0.0}
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_solvers2(self):
|
||||
"""Betweenness centrality: alternate solvers"""
|
||||
G = nx.complete_graph(4)
|
||||
for solver in ["full", "lu", "cg"]:
|
||||
b = nx.current_flow_betweenness_centrality(
|
||||
G, normalized=False, solver=solver
|
||||
)
|
||||
b_answer = {0: 0.75, 1: 0.75, 2: 0.75, 3: 0.75}
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
|
||||
class TestApproximateFlowBetweennessCentrality:
|
||||
def test_K4_normalized(self):
|
||||
"Approximate current-flow betweenness centrality: K4 normalized"
|
||||
G = nx.complete_graph(4)
|
||||
b = nx.current_flow_betweenness_centrality(G, normalized=True)
|
||||
epsilon = 0.1
|
||||
ba = approximate_cfbc(G, normalized=True, epsilon=0.5 * epsilon)
|
||||
for n in sorted(G):
|
||||
np.testing.assert_allclose(b[n], ba[n], atol=epsilon)
|
||||
|
||||
def test_K4(self):
|
||||
"Approximate current-flow betweenness centrality: K4"
|
||||
G = nx.complete_graph(4)
|
||||
b = nx.current_flow_betweenness_centrality(G, normalized=False)
|
||||
epsilon = 0.1
|
||||
ba = approximate_cfbc(G, normalized=False, epsilon=0.5 * epsilon)
|
||||
for n in sorted(G):
|
||||
np.testing.assert_allclose(b[n], ba[n], atol=epsilon * len(G) ** 2)
|
||||
|
||||
def test_star(self):
|
||||
"Approximate current-flow betweenness centrality: star"
|
||||
G = nx.Graph()
|
||||
nx.add_star(G, ["a", "b", "c", "d"])
|
||||
b = nx.current_flow_betweenness_centrality(G, normalized=True)
|
||||
epsilon = 0.1
|
||||
ba = approximate_cfbc(G, normalized=True, epsilon=0.5 * epsilon)
|
||||
for n in sorted(G):
|
||||
np.testing.assert_allclose(b[n], ba[n], atol=epsilon)
|
||||
|
||||
def test_grid(self):
|
||||
"Approximate current-flow betweenness centrality: 2d grid"
|
||||
G = nx.grid_2d_graph(4, 4)
|
||||
b = nx.current_flow_betweenness_centrality(G, normalized=True)
|
||||
epsilon = 0.1
|
||||
ba = approximate_cfbc(G, normalized=True, epsilon=0.5 * epsilon)
|
||||
for n in sorted(G):
|
||||
np.testing.assert_allclose(b[n], ba[n], atol=epsilon)
|
||||
|
||||
def test_seed(self):
|
||||
G = nx.complete_graph(4)
|
||||
b = approximate_cfbc(G, normalized=False, epsilon=0.05, seed=1)
|
||||
b_answer = {0: 0.75, 1: 0.75, 2: 0.75, 3: 0.75}
|
||||
for n in sorted(G):
|
||||
np.testing.assert_allclose(b[n], b_answer[n], atol=0.1)
|
||||
|
||||
def test_solvers(self):
|
||||
"Approximate current-flow betweenness centrality: solvers"
|
||||
G = nx.complete_graph(4)
|
||||
epsilon = 0.1
|
||||
for solver in ["full", "lu", "cg"]:
|
||||
b = approximate_cfbc(
|
||||
G, normalized=False, solver=solver, epsilon=0.5 * epsilon
|
||||
)
|
||||
b_answer = {0: 0.75, 1: 0.75, 2: 0.75, 3: 0.75}
|
||||
for n in sorted(G):
|
||||
np.testing.assert_allclose(b[n], b_answer[n], atol=epsilon)
|
||||
|
||||
def test_lower_kmax(self):
|
||||
G = nx.complete_graph(4)
|
||||
with pytest.raises(nx.NetworkXError, match="Increase kmax or epsilon"):
|
||||
nx.approximate_current_flow_betweenness_centrality(G, kmax=4)
|
||||
|
||||
def test_sample_weight_positive_effect(self):
|
||||
G = nx.complete_graph(4)
|
||||
b1 = approximate_cfbc(G, epsilon=0.1, seed=42)
|
||||
b2 = approximate_cfbc(G, epsilon=0.1, sample_weight=2.0, seed=42)
|
||||
assert len(b1) == len(b2) == 4
|
||||
for node in G.nodes():
|
||||
assert node in b1 and node in b2
|
||||
assert isinstance(b1[node], float) and isinstance(b2[node], float)
|
||||
|
||||
def test_sample_weight_validation(self):
|
||||
G = nx.complete_graph(4)
|
||||
|
||||
with pytest.raises(
|
||||
nx.NetworkXError,
|
||||
match="Sample weight must be positive. Got sample_weight=-1.0",
|
||||
):
|
||||
approximate_cfbc(G, sample_weight=-1.0)
|
||||
|
||||
with pytest.raises(
|
||||
nx.NetworkXError,
|
||||
match="Sample weight must be positive. Got sample_weight=0.0",
|
||||
):
|
||||
approximate_cfbc(G, sample_weight=0.0)
|
||||
|
||||
result = approximate_cfbc(G, sample_weight=0.1, seed=42)
|
||||
assert len(result) == 4
|
||||
|
||||
def test_epsilon_validation(self):
|
||||
G = nx.complete_graph(4)
|
||||
|
||||
with pytest.raises(
|
||||
nx.NetworkXError, match="Epsilon must be positive. Got epsilon=-0.1"
|
||||
):
|
||||
approximate_cfbc(G, epsilon=-0.1)
|
||||
|
||||
with pytest.raises(
|
||||
nx.NetworkXError, match="Epsilon must be positive. Got epsilon=0.0"
|
||||
):
|
||||
approximate_cfbc(G, epsilon=0.0)
|
||||
|
||||
def test_normalization_edge_case_small_graph(self):
|
||||
G = nx.path_graph(2)
|
||||
|
||||
result_norm = approximate_cfbc(G, normalized=True, seed=42)
|
||||
result_unnorm = approximate_cfbc(G, normalized=False, seed=42)
|
||||
|
||||
assert len(result_norm) == 2
|
||||
assert len(result_unnorm) == 2
|
||||
assert all(v == 0.0 for v in result_norm.values())
|
||||
assert all(v == 0.0 for v in result_unnorm.values())
|
||||
|
||||
G1 = nx.Graph()
|
||||
G1.add_node(0)
|
||||
result1 = approximate_cfbc(G1, normalized=True, seed=42)
|
||||
assert result1 == {0: 0.0}
|
||||
|
||||
def test_sample_weight_interaction_with_kmax(self):
|
||||
G = nx.complete_graph(4)
|
||||
|
||||
with pytest.raises(nx.NetworkXError, match="Number random pairs k>kmax"):
|
||||
approximate_cfbc(G, sample_weight=10.0, epsilon=0.01, kmax=10)
|
||||
|
||||
|
||||
class TestWeightedFlowBetweennessCentrality:
|
||||
pass
|
||||
|
||||
|
||||
class TestEdgeFlowBetweennessCentrality:
|
||||
def test_K4(self):
|
||||
"""Edge flow betweenness centrality: K4"""
|
||||
G = nx.complete_graph(4)
|
||||
b = edge_current_flow(G, normalized=True)
|
||||
b_answer = dict.fromkeys(G.edges(), 0.25)
|
||||
for (s, t), v1 in b_answer.items():
|
||||
v2 = b.get((s, t), b.get((t, s)))
|
||||
assert v1 == pytest.approx(v2, abs=1e-7)
|
||||
|
||||
def test_K4_normalized(self):
|
||||
"""Edge flow betweenness centrality: K4"""
|
||||
G = nx.complete_graph(4)
|
||||
b = edge_current_flow(G, normalized=False)
|
||||
b_answer = dict.fromkeys(G.edges(), 0.75)
|
||||
for (s, t), v1 in b_answer.items():
|
||||
v2 = b.get((s, t), b.get((t, s)))
|
||||
assert v1 == pytest.approx(v2, abs=1e-7)
|
||||
|
||||
def test_C4(self):
|
||||
"""Edge flow betweenness centrality: C4"""
|
||||
G = nx.cycle_graph(4)
|
||||
b = edge_current_flow(G, normalized=False)
|
||||
b_answer = {(0, 1): 1.25, (0, 3): 1.25, (1, 2): 1.25, (2, 3): 1.25}
|
||||
for (s, t), v1 in b_answer.items():
|
||||
v2 = b.get((s, t), b.get((t, s)))
|
||||
assert v1 == pytest.approx(v2, abs=1e-7)
|
||||
|
||||
def test_P4(self):
|
||||
"""Edge betweenness centrality: P4"""
|
||||
G = nx.path_graph(4)
|
||||
b = edge_current_flow(G, normalized=False)
|
||||
b_answer = {(0, 1): 1.5, (1, 2): 2.0, (2, 3): 1.5}
|
||||
for (s, t), v1 in b_answer.items():
|
||||
v2 = b.get((s, t), b.get((t, s)))
|
||||
assert v1 == pytest.approx(v2, abs=1e-7)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"centrality_func",
|
||||
(
|
||||
nx.current_flow_betweenness_centrality,
|
||||
nx.edge_current_flow_betweenness_centrality,
|
||||
nx.approximate_current_flow_betweenness_centrality,
|
||||
),
|
||||
)
|
||||
def test_unconnected_graphs_betweenness_centrality(centrality_func):
|
||||
G = nx.Graph([(1, 2), (3, 4)])
|
||||
G.add_node(5)
|
||||
with pytest.raises(nx.NetworkXError, match="Graph not connected"):
|
||||
centrality_func(G)
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("numpy")
|
||||
pytest.importorskip("scipy")
|
||||
|
||||
import networkx as nx
|
||||
from networkx import edge_current_flow_betweenness_centrality as edge_current_flow
|
||||
from networkx import (
|
||||
edge_current_flow_betweenness_centrality_subset as edge_current_flow_subset,
|
||||
)
|
||||
|
||||
|
||||
class TestFlowBetweennessCentrality:
|
||||
def test_K4_normalized(self):
|
||||
"""Betweenness centrality: K4"""
|
||||
G = nx.complete_graph(4)
|
||||
b = nx.current_flow_betweenness_centrality_subset(
|
||||
G, list(G), list(G), normalized=True
|
||||
)
|
||||
b_answer = nx.current_flow_betweenness_centrality(G, normalized=True)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_K4(self):
|
||||
"""Betweenness centrality: K4"""
|
||||
G = nx.complete_graph(4)
|
||||
b = nx.current_flow_betweenness_centrality_subset(
|
||||
G, list(G), list(G), normalized=True
|
||||
)
|
||||
b_answer = nx.current_flow_betweenness_centrality(G, normalized=True)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
# test weighted network
|
||||
G.add_edge(0, 1, weight=0.5, other=0.3)
|
||||
b = nx.current_flow_betweenness_centrality_subset(
|
||||
G, list(G), list(G), normalized=True, weight=None
|
||||
)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
b = nx.current_flow_betweenness_centrality_subset(
|
||||
G, list(G), list(G), normalized=True
|
||||
)
|
||||
b_answer = nx.current_flow_betweenness_centrality(G, normalized=True)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
b = nx.current_flow_betweenness_centrality_subset(
|
||||
G, list(G), list(G), normalized=True, weight="other"
|
||||
)
|
||||
b_answer = nx.current_flow_betweenness_centrality(
|
||||
G, normalized=True, weight="other"
|
||||
)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_P4_normalized(self):
|
||||
"""Betweenness centrality: P4 normalized"""
|
||||
G = nx.path_graph(4)
|
||||
b = nx.current_flow_betweenness_centrality_subset(
|
||||
G, list(G), list(G), normalized=True
|
||||
)
|
||||
b_answer = nx.current_flow_betweenness_centrality(G, normalized=True)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_P4(self):
|
||||
"""Betweenness centrality: P4"""
|
||||
G = nx.path_graph(4)
|
||||
b = nx.current_flow_betweenness_centrality_subset(
|
||||
G, list(G), list(G), normalized=True
|
||||
)
|
||||
b_answer = nx.current_flow_betweenness_centrality(G, normalized=True)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_star(self):
|
||||
"""Betweenness centrality: star"""
|
||||
G = nx.Graph()
|
||||
nx.add_star(G, ["a", "b", "c", "d"])
|
||||
b = nx.current_flow_betweenness_centrality_subset(
|
||||
G, list(G), list(G), normalized=True
|
||||
)
|
||||
b_answer = nx.current_flow_betweenness_centrality(G, normalized=True)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
|
||||
# class TestWeightedFlowBetweennessCentrality():
|
||||
# pass
|
||||
|
||||
|
||||
class TestEdgeFlowBetweennessCentrality:
|
||||
def test_K4_normalized(self):
|
||||
"""Betweenness centrality: K4"""
|
||||
G = nx.complete_graph(4)
|
||||
b = edge_current_flow_subset(G, list(G), list(G), normalized=True)
|
||||
b_answer = edge_current_flow(G, normalized=True)
|
||||
for (s, t), v1 in b_answer.items():
|
||||
v2 = b.get((s, t), b.get((t, s)))
|
||||
assert v1 == pytest.approx(v2, abs=1e-7)
|
||||
|
||||
def test_K4(self):
|
||||
"""Betweenness centrality: K4"""
|
||||
G = nx.complete_graph(4)
|
||||
b = edge_current_flow_subset(G, list(G), list(G), normalized=False)
|
||||
b_answer = edge_current_flow(G, normalized=False)
|
||||
for (s, t), v1 in b_answer.items():
|
||||
v2 = b.get((s, t), b.get((t, s)))
|
||||
assert v1 == pytest.approx(v2, abs=1e-7)
|
||||
# test weighted network
|
||||
G.add_edge(0, 1, weight=0.5, other=0.3)
|
||||
b = edge_current_flow_subset(G, list(G), list(G), normalized=False, weight=None)
|
||||
# weight is None => same as unweighted network
|
||||
for (s, t), v1 in b_answer.items():
|
||||
v2 = b.get((s, t), b.get((t, s)))
|
||||
assert v1 == pytest.approx(v2, abs=1e-7)
|
||||
|
||||
b = edge_current_flow_subset(G, list(G), list(G), normalized=False)
|
||||
b_answer = edge_current_flow(G, normalized=False)
|
||||
for (s, t), v1 in b_answer.items():
|
||||
v2 = b.get((s, t), b.get((t, s)))
|
||||
assert v1 == pytest.approx(v2, abs=1e-7)
|
||||
|
||||
b = edge_current_flow_subset(
|
||||
G, list(G), list(G), normalized=False, weight="other"
|
||||
)
|
||||
b_answer = edge_current_flow(G, normalized=False, weight="other")
|
||||
for (s, t), v1 in b_answer.items():
|
||||
v2 = b.get((s, t), b.get((t, s)))
|
||||
assert v1 == pytest.approx(v2, abs=1e-7)
|
||||
|
||||
def test_C4(self):
|
||||
"""Edge betweenness centrality: C4"""
|
||||
G = nx.cycle_graph(4)
|
||||
b = edge_current_flow_subset(G, list(G), list(G), normalized=True)
|
||||
b_answer = edge_current_flow(G, normalized=True)
|
||||
for (s, t), v1 in b_answer.items():
|
||||
v2 = b.get((s, t), b.get((t, s)))
|
||||
assert v1 == pytest.approx(v2, abs=1e-7)
|
||||
|
||||
def test_P4(self):
|
||||
"""Edge betweenness centrality: P4"""
|
||||
G = nx.path_graph(4)
|
||||
b = edge_current_flow_subset(G, list(G), list(G), normalized=True)
|
||||
b_answer = edge_current_flow(G, normalized=True)
|
||||
for (s, t), v1 in b_answer.items():
|
||||
v2 = b.get((s, t), b.get((t, s)))
|
||||
assert v1 == pytest.approx(v2, abs=1e-7)
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("numpy")
|
||||
pytest.importorskip("scipy")
|
||||
|
||||
import networkx as nx
|
||||
|
||||
|
||||
class TestFlowClosenessCentrality:
|
||||
def test_K4(self):
|
||||
"""Closeness centrality: K4"""
|
||||
G = nx.complete_graph(4)
|
||||
b = nx.current_flow_closeness_centrality(G)
|
||||
b_answer = {0: 2.0 / 3, 1: 2.0 / 3, 2: 2.0 / 3, 3: 2.0 / 3}
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_P4(self):
|
||||
"""Closeness centrality: P4"""
|
||||
G = nx.path_graph(4)
|
||||
b = nx.current_flow_closeness_centrality(G)
|
||||
b_answer = {0: 1.0 / 6, 1: 1.0 / 4, 2: 1.0 / 4, 3: 1.0 / 6}
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_star(self):
|
||||
"""Closeness centrality: star"""
|
||||
G = nx.Graph()
|
||||
nx.add_star(G, ["a", "b", "c", "d"])
|
||||
b = nx.current_flow_closeness_centrality(G)
|
||||
b_answer = {"a": 1.0 / 3, "b": 0.6 / 3, "c": 0.6 / 3, "d": 0.6 / 3}
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_current_flow_closeness_centrality_not_connected(self):
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from([1, 2, 3])
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
nx.current_flow_closeness_centrality(G)
|
||||
|
||||
|
||||
class TestWeightedFlowClosenessCentrality:
|
||||
pass
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
Unit tests for degree centrality.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
|
||||
|
||||
class TestDegreeCentrality:
|
||||
def setup_method(self):
|
||||
self.K = nx.krackhardt_kite_graph()
|
||||
self.P3 = nx.path_graph(3)
|
||||
self.K5 = nx.complete_graph(5)
|
||||
|
||||
F = nx.Graph() # Florentine families
|
||||
F.add_edge("Acciaiuoli", "Medici")
|
||||
F.add_edge("Castellani", "Peruzzi")
|
||||
F.add_edge("Castellani", "Strozzi")
|
||||
F.add_edge("Castellani", "Barbadori")
|
||||
F.add_edge("Medici", "Barbadori")
|
||||
F.add_edge("Medici", "Ridolfi")
|
||||
F.add_edge("Medici", "Tornabuoni")
|
||||
F.add_edge("Medici", "Albizzi")
|
||||
F.add_edge("Medici", "Salviati")
|
||||
F.add_edge("Salviati", "Pazzi")
|
||||
F.add_edge("Peruzzi", "Strozzi")
|
||||
F.add_edge("Peruzzi", "Bischeri")
|
||||
F.add_edge("Strozzi", "Ridolfi")
|
||||
F.add_edge("Strozzi", "Bischeri")
|
||||
F.add_edge("Ridolfi", "Tornabuoni")
|
||||
F.add_edge("Tornabuoni", "Guadagni")
|
||||
F.add_edge("Albizzi", "Ginori")
|
||||
F.add_edge("Albizzi", "Guadagni")
|
||||
F.add_edge("Bischeri", "Guadagni")
|
||||
F.add_edge("Guadagni", "Lamberteschi")
|
||||
self.F = F
|
||||
|
||||
G = nx.DiGraph()
|
||||
G.add_edge(0, 5)
|
||||
G.add_edge(1, 5)
|
||||
G.add_edge(2, 5)
|
||||
G.add_edge(3, 5)
|
||||
G.add_edge(4, 5)
|
||||
G.add_edge(5, 6)
|
||||
G.add_edge(5, 7)
|
||||
G.add_edge(5, 8)
|
||||
self.G = G
|
||||
|
||||
def test_degree_centrality_1(self):
|
||||
d = nx.degree_centrality(self.K5)
|
||||
exact = dict(zip(range(5), [1] * 5))
|
||||
for n, dc in d.items():
|
||||
assert exact[n] == pytest.approx(dc, abs=1e-7)
|
||||
|
||||
def test_degree_centrality_2(self):
|
||||
d = nx.degree_centrality(self.P3)
|
||||
exact = {0: 0.5, 1: 1, 2: 0.5}
|
||||
for n, dc in d.items():
|
||||
assert exact[n] == pytest.approx(dc, abs=1e-7)
|
||||
|
||||
def test_degree_centrality_3(self):
|
||||
d = nx.degree_centrality(self.K)
|
||||
exact = {
|
||||
0: 0.444,
|
||||
1: 0.444,
|
||||
2: 0.333,
|
||||
3: 0.667,
|
||||
4: 0.333,
|
||||
5: 0.556,
|
||||
6: 0.556,
|
||||
7: 0.333,
|
||||
8: 0.222,
|
||||
9: 0.111,
|
||||
}
|
||||
for n, dc in d.items():
|
||||
assert exact[n] == pytest.approx(float(f"{dc:.3f}"), abs=1e-7)
|
||||
|
||||
def test_degree_centrality_4(self):
|
||||
d = nx.degree_centrality(self.F)
|
||||
names = sorted(self.F.nodes())
|
||||
dcs = [
|
||||
0.071,
|
||||
0.214,
|
||||
0.143,
|
||||
0.214,
|
||||
0.214,
|
||||
0.071,
|
||||
0.286,
|
||||
0.071,
|
||||
0.429,
|
||||
0.071,
|
||||
0.214,
|
||||
0.214,
|
||||
0.143,
|
||||
0.286,
|
||||
0.214,
|
||||
]
|
||||
exact = dict(zip(names, dcs))
|
||||
for n, dc in d.items():
|
||||
assert exact[n] == pytest.approx(float(f"{dc:.3f}"), abs=1e-7)
|
||||
|
||||
def test_indegree_centrality(self):
|
||||
d = nx.in_degree_centrality(self.G)
|
||||
exact = {
|
||||
0: 0.0,
|
||||
1: 0.0,
|
||||
2: 0.0,
|
||||
3: 0.0,
|
||||
4: 0.0,
|
||||
5: 0.625,
|
||||
6: 0.125,
|
||||
7: 0.125,
|
||||
8: 0.125,
|
||||
}
|
||||
for n, dc in d.items():
|
||||
assert exact[n] == pytest.approx(dc, abs=1e-7)
|
||||
|
||||
def test_outdegree_centrality(self):
|
||||
d = nx.out_degree_centrality(self.G)
|
||||
exact = {
|
||||
0: 0.125,
|
||||
1: 0.125,
|
||||
2: 0.125,
|
||||
3: 0.125,
|
||||
4: 0.125,
|
||||
5: 0.375,
|
||||
6: 0.0,
|
||||
7: 0.0,
|
||||
8: 0.0,
|
||||
}
|
||||
for n, dc in d.items():
|
||||
assert exact[n] == pytest.approx(dc, abs=1e-7)
|
||||
|
||||
def test_small_graph_centrality(self):
|
||||
G = nx.empty_graph(create_using=nx.DiGraph)
|
||||
assert {} == nx.degree_centrality(G)
|
||||
assert {} == nx.out_degree_centrality(G)
|
||||
assert {} == nx.in_degree_centrality(G)
|
||||
|
||||
G = nx.empty_graph(1, create_using=nx.DiGraph)
|
||||
assert {0: 1} == nx.degree_centrality(G)
|
||||
assert {0: 1} == nx.out_degree_centrality(G)
|
||||
assert {0: 1} == nx.in_degree_centrality(G)
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import networkx as nx
|
||||
|
||||
|
||||
def small_ego_G():
|
||||
"""The sample network from https://arxiv.org/pdf/1310.6753v1.pdf"""
|
||||
edges = [
|
||||
("a", "b"),
|
||||
("a", "c"),
|
||||
("b", "c"),
|
||||
("b", "d"),
|
||||
("b", "e"),
|
||||
("b", "f"),
|
||||
("c", "d"),
|
||||
("c", "f"),
|
||||
("c", "h"),
|
||||
("d", "f"),
|
||||
("e", "f"),
|
||||
("f", "h"),
|
||||
("h", "j"),
|
||||
("h", "k"),
|
||||
("i", "j"),
|
||||
("i", "k"),
|
||||
("j", "k"),
|
||||
("u", "a"),
|
||||
("u", "b"),
|
||||
("u", "c"),
|
||||
("u", "d"),
|
||||
("u", "e"),
|
||||
("u", "f"),
|
||||
("u", "g"),
|
||||
("u", "h"),
|
||||
("u", "i"),
|
||||
("u", "j"),
|
||||
("u", "k"),
|
||||
]
|
||||
G = nx.Graph()
|
||||
G.add_edges_from(edges)
|
||||
|
||||
return G
|
||||
|
||||
|
||||
class TestDispersion:
|
||||
def test_article(self):
|
||||
"""our algorithm matches article's"""
|
||||
G = small_ego_G()
|
||||
disp_uh = nx.dispersion(G, "u", "h", normalized=False)
|
||||
disp_ub = nx.dispersion(G, "u", "b", normalized=False)
|
||||
assert disp_uh == 4
|
||||
assert disp_ub == 1
|
||||
|
||||
def test_results_length(self):
|
||||
"""there is a result for every node"""
|
||||
G = small_ego_G()
|
||||
disp = nx.dispersion(G)
|
||||
disp_Gu = nx.dispersion(G, "u")
|
||||
disp_uv = nx.dispersion(G, "u", "h")
|
||||
assert len(disp) == len(G)
|
||||
assert len(disp_Gu) == len(G) - 1
|
||||
assert isinstance(disp_uv, float)
|
||||
|
||||
def test_dispersion_v_only(self):
|
||||
G = small_ego_G()
|
||||
disp_G_h = nx.dispersion(G, v="h", normalized=False)
|
||||
disp_G_h_normalized = nx.dispersion(G, v="h", normalized=True)
|
||||
assert disp_G_h == {"c": 0, "f": 0, "j": 0, "k": 0, "u": 4}
|
||||
assert disp_G_h_normalized == {"c": 0.0, "f": 0.0, "j": 0.0, "k": 0.0, "u": 1.0}
|
||||
|
||||
def test_impossible_things(self):
|
||||
G = nx.karate_club_graph()
|
||||
disp = nx.dispersion(G)
|
||||
for u in disp:
|
||||
for v in disp[u]:
|
||||
assert disp[u][v] >= 0
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
|
||||
np = pytest.importorskip("numpy")
|
||||
pytest.importorskip("scipy")
|
||||
|
||||
|
||||
class TestEigenvectorCentrality:
|
||||
def test_K5(self):
|
||||
"""Eigenvector centrality: K5"""
|
||||
G = nx.complete_graph(5)
|
||||
b = nx.eigenvector_centrality(G)
|
||||
v = math.sqrt(1 / 5.0)
|
||||
b_answer = dict.fromkeys(G, v)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
nstart = {n: 1 for n in G}
|
||||
b = nx.eigenvector_centrality(G, nstart=nstart)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
b = nx.eigenvector_centrality_numpy(G)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-3)
|
||||
|
||||
def test_P3(self):
|
||||
"""Eigenvector centrality: P3"""
|
||||
G = nx.path_graph(3)
|
||||
b_answer = {0: 0.5, 1: 0.7071, 2: 0.5}
|
||||
b = nx.eigenvector_centrality_numpy(G)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-4)
|
||||
b = nx.eigenvector_centrality(G)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-4)
|
||||
|
||||
def test_P3_unweighted(self):
|
||||
"""Eigenvector centrality: P3"""
|
||||
G = nx.path_graph(3)
|
||||
b_answer = {0: 0.5, 1: 0.7071, 2: 0.5}
|
||||
b = nx.eigenvector_centrality_numpy(G, weight=None)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-4)
|
||||
|
||||
def test_maxiter(self):
|
||||
with pytest.raises(nx.PowerIterationFailedConvergence):
|
||||
G = nx.path_graph(3)
|
||||
nx.eigenvector_centrality(G, max_iter=0)
|
||||
|
||||
|
||||
class TestEigenvectorCentralityDirected:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
G = nx.DiGraph()
|
||||
|
||||
edges = [
|
||||
(1, 2),
|
||||
(1, 3),
|
||||
(2, 4),
|
||||
(3, 2),
|
||||
(3, 5),
|
||||
(4, 2),
|
||||
(4, 5),
|
||||
(4, 6),
|
||||
(5, 6),
|
||||
(5, 7),
|
||||
(5, 8),
|
||||
(6, 8),
|
||||
(7, 1),
|
||||
(7, 5),
|
||||
(7, 8),
|
||||
(8, 6),
|
||||
(8, 7),
|
||||
]
|
||||
|
||||
G.add_edges_from(edges, weight=2.0)
|
||||
cls.G = G.reverse()
|
||||
cls.G.evc = [
|
||||
0.25368793,
|
||||
0.19576478,
|
||||
0.32817092,
|
||||
0.40430835,
|
||||
0.48199885,
|
||||
0.15724483,
|
||||
0.51346196,
|
||||
0.32475403,
|
||||
]
|
||||
|
||||
H = nx.DiGraph()
|
||||
|
||||
edges = [
|
||||
(1, 2),
|
||||
(1, 3),
|
||||
(2, 4),
|
||||
(3, 2),
|
||||
(3, 5),
|
||||
(4, 2),
|
||||
(4, 5),
|
||||
(4, 6),
|
||||
(5, 6),
|
||||
(5, 7),
|
||||
(5, 8),
|
||||
(6, 8),
|
||||
(7, 1),
|
||||
(7, 5),
|
||||
(7, 8),
|
||||
(8, 6),
|
||||
(8, 7),
|
||||
]
|
||||
|
||||
G.add_edges_from(edges)
|
||||
cls.H = G.reverse()
|
||||
cls.H.evc = [
|
||||
0.25368793,
|
||||
0.19576478,
|
||||
0.32817092,
|
||||
0.40430835,
|
||||
0.48199885,
|
||||
0.15724483,
|
||||
0.51346196,
|
||||
0.32475403,
|
||||
]
|
||||
|
||||
def test_eigenvector_centrality_weighted(self):
|
||||
G = self.G
|
||||
p = nx.eigenvector_centrality(G)
|
||||
for a, b in zip(list(p.values()), self.G.evc):
|
||||
assert a == pytest.approx(b, abs=1e-4)
|
||||
|
||||
def test_eigenvector_centrality_weighted_numpy(self):
|
||||
G = self.G
|
||||
p = nx.eigenvector_centrality_numpy(G)
|
||||
for a, b in zip(list(p.values()), self.G.evc):
|
||||
assert a == pytest.approx(b, abs=1e-7)
|
||||
|
||||
def test_eigenvector_centrality_unweighted(self):
|
||||
G = self.H
|
||||
p = nx.eigenvector_centrality(G)
|
||||
for a, b in zip(list(p.values()), self.G.evc):
|
||||
assert a == pytest.approx(b, abs=1e-4)
|
||||
|
||||
def test_eigenvector_centrality_unweighted_numpy(self):
|
||||
G = self.H
|
||||
p = nx.eigenvector_centrality_numpy(G)
|
||||
for a, b in zip(list(p.values()), self.G.evc):
|
||||
assert a == pytest.approx(b, abs=1e-7)
|
||||
|
||||
|
||||
class TestEigenvectorCentralityExceptions:
|
||||
def test_multigraph(self):
|
||||
with pytest.raises(nx.NetworkXException):
|
||||
nx.eigenvector_centrality(nx.MultiGraph())
|
||||
|
||||
def test_multigraph_numpy(self):
|
||||
with pytest.raises(nx.NetworkXException):
|
||||
nx.eigenvector_centrality_numpy(nx.MultiGraph())
|
||||
|
||||
def test_null(self):
|
||||
with pytest.raises(nx.NetworkXException):
|
||||
nx.eigenvector_centrality(nx.Graph())
|
||||
|
||||
def test_null_numpy(self):
|
||||
with pytest.raises(nx.NetworkXException):
|
||||
nx.eigenvector_centrality_numpy(nx.Graph())
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"G",
|
||||
[
|
||||
nx.empty_graph(3),
|
||||
nx.DiGraph([(0, 1), (1, 2)]),
|
||||
],
|
||||
)
|
||||
def test_disconnected_numpy(self, G):
|
||||
msg = "does not give consistent results for disconnected"
|
||||
with pytest.raises(nx.AmbiguousSolution, match=msg):
|
||||
nx.eigenvector_centrality_numpy(G)
|
||||
|
||||
def test_zero_nstart(self):
|
||||
G = nx.Graph([(1, 2), (1, 3), (2, 3)])
|
||||
with pytest.raises(
|
||||
nx.NetworkXException, match="initial vector cannot have all zero values"
|
||||
):
|
||||
nx.eigenvector_centrality(G, nstart={v: 0 for v in G})
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
"""
|
||||
Tests for Group Centrality Measures
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
|
||||
|
||||
class TestGroupBetweennessCentrality:
|
||||
def test_group_betweenness_single_node(self):
|
||||
"""
|
||||
Group betweenness centrality for single node group
|
||||
"""
|
||||
G = nx.path_graph(5)
|
||||
C = [1]
|
||||
b = nx.group_betweenness_centrality(
|
||||
G, C, weight=None, normalized=False, endpoints=False
|
||||
)
|
||||
b_answer = 3.0
|
||||
assert b == b_answer
|
||||
|
||||
def test_group_betweenness_with_endpoints(self):
|
||||
"""
|
||||
Group betweenness centrality for single node group
|
||||
"""
|
||||
G = nx.path_graph(5)
|
||||
C = [1]
|
||||
b = nx.group_betweenness_centrality(
|
||||
G, C, weight=None, normalized=False, endpoints=True
|
||||
)
|
||||
b_answer = 7.0
|
||||
assert b == b_answer
|
||||
|
||||
def test_group_betweenness_normalized(self):
|
||||
"""
|
||||
Group betweenness centrality for group with more than
|
||||
1 node and normalized
|
||||
"""
|
||||
G = nx.path_graph(5)
|
||||
C = [1, 3]
|
||||
b = nx.group_betweenness_centrality(
|
||||
G, C, weight=None, normalized=True, endpoints=False
|
||||
)
|
||||
b_answer = 1.0
|
||||
assert b == b_answer
|
||||
|
||||
def test_two_group_betweenness_value_zero(self):
|
||||
"""
|
||||
Group betweenness centrality value of 0
|
||||
"""
|
||||
G = nx.cycle_graph(7)
|
||||
C = [[0, 1, 6], [0, 1, 5]]
|
||||
b = nx.group_betweenness_centrality(G, C, weight=None, normalized=False)
|
||||
b_answer = [0.0, 3.0]
|
||||
assert b == b_answer
|
||||
|
||||
def test_group_betweenness_value_zero(self):
|
||||
"""
|
||||
Group betweenness centrality value of 0
|
||||
"""
|
||||
G = nx.cycle_graph(6)
|
||||
C = [0, 1, 5]
|
||||
b = nx.group_betweenness_centrality(G, C, weight=None, normalized=False)
|
||||
b_answer = 0.0
|
||||
assert b == b_answer
|
||||
|
||||
def test_group_betweenness_disconnected_graph(self):
|
||||
"""
|
||||
Group betweenness centrality in a disconnected graph
|
||||
"""
|
||||
G = nx.path_graph(5)
|
||||
G.remove_edge(0, 1)
|
||||
C = [1]
|
||||
b = nx.group_betweenness_centrality(G, C, weight=None, normalized=False)
|
||||
b_answer = 0.0
|
||||
assert b == b_answer
|
||||
|
||||
def test_group_betweenness_node_not_in_graph(self):
|
||||
"""
|
||||
Node(s) in C not in graph, raises NodeNotFound exception
|
||||
"""
|
||||
with pytest.raises(nx.NodeNotFound):
|
||||
nx.group_betweenness_centrality(nx.path_graph(5), [4, 7, 8])
|
||||
|
||||
def test_group_betweenness_directed_weighted(self):
|
||||
"""
|
||||
Group betweenness centrality in a directed and weighted graph
|
||||
"""
|
||||
G = nx.DiGraph()
|
||||
G.add_edge(1, 0, weight=1)
|
||||
G.add_edge(0, 2, weight=2)
|
||||
G.add_edge(1, 2, weight=3)
|
||||
G.add_edge(3, 1, weight=4)
|
||||
G.add_edge(2, 3, weight=1)
|
||||
G.add_edge(4, 3, weight=6)
|
||||
G.add_edge(2, 4, weight=7)
|
||||
C = [1, 2]
|
||||
b = nx.group_betweenness_centrality(G, C, weight="weight", normalized=False)
|
||||
b_answer = 5.0
|
||||
assert b == b_answer
|
||||
|
||||
|
||||
class TestProminentGroup:
|
||||
np = pytest.importorskip("numpy")
|
||||
pd = pytest.importorskip("pandas")
|
||||
|
||||
def test_prominent_group_single_node(self):
|
||||
"""
|
||||
Prominent group for single node
|
||||
"""
|
||||
G = nx.path_graph(5)
|
||||
k = 1
|
||||
b, g = nx.prominent_group(G, k, normalized=False, endpoints=False)
|
||||
b_answer, g_answer = 4.0, [2]
|
||||
assert b == b_answer and g == g_answer
|
||||
|
||||
def test_prominent_group_with_c(self):
|
||||
"""
|
||||
Prominent group without some nodes
|
||||
"""
|
||||
G = nx.path_graph(5)
|
||||
k = 1
|
||||
b, g = nx.prominent_group(G, k, normalized=False, C=[2])
|
||||
b_answer, g_answer = 3.0, [1]
|
||||
assert b == b_answer and g == g_answer
|
||||
|
||||
def test_prominent_group_normalized_endpoints(self):
|
||||
"""
|
||||
Prominent group with normalized result, with endpoints
|
||||
"""
|
||||
G = nx.cycle_graph(7)
|
||||
k = 2
|
||||
b, g = nx.prominent_group(G, k, normalized=True, endpoints=True)
|
||||
b_answer, g_answer = 1.7, [2, 5]
|
||||
assert b == b_answer and g == g_answer
|
||||
|
||||
def test_prominent_group_disconnected_graph(self):
|
||||
"""
|
||||
Prominent group of disconnected graph
|
||||
"""
|
||||
G = nx.path_graph(6)
|
||||
G.remove_edge(0, 1)
|
||||
k = 1
|
||||
b, g = nx.prominent_group(G, k, weight=None, normalized=False)
|
||||
b_answer, g_answer = 4.0, [3]
|
||||
assert b == b_answer and g == g_answer
|
||||
|
||||
def test_prominent_group_node_not_in_graph(self):
|
||||
"""
|
||||
Node(s) in C not in graph, raises NodeNotFound exception
|
||||
"""
|
||||
with pytest.raises(nx.NodeNotFound):
|
||||
nx.prominent_group(nx.path_graph(5), 1, C=[10])
|
||||
|
||||
def test_group_betweenness_directed_weighted(self):
|
||||
"""
|
||||
Group betweenness centrality in a directed and weighted graph
|
||||
"""
|
||||
G = nx.DiGraph()
|
||||
G.add_edge(1, 0, weight=1)
|
||||
G.add_edge(0, 2, weight=2)
|
||||
G.add_edge(1, 2, weight=3)
|
||||
G.add_edge(3, 1, weight=4)
|
||||
G.add_edge(2, 3, weight=1)
|
||||
G.add_edge(4, 3, weight=6)
|
||||
G.add_edge(2, 4, weight=7)
|
||||
k = 2
|
||||
b, g = nx.prominent_group(G, k, weight="weight", normalized=False)
|
||||
b_answer, g_answer = 5.0, [1, 2]
|
||||
assert b == b_answer and g == g_answer
|
||||
|
||||
def test_prominent_group_greedy_algorithm(self):
|
||||
"""
|
||||
Group betweenness centrality in a greedy algorithm
|
||||
"""
|
||||
G = nx.cycle_graph(7)
|
||||
k = 2
|
||||
b, g = nx.prominent_group(G, k, normalized=True, endpoints=True, greedy=True)
|
||||
b_answer, g_answer = 1.7, [6, 3]
|
||||
assert b == b_answer and g == g_answer
|
||||
|
||||
|
||||
class TestGroupClosenessCentrality:
|
||||
def test_group_closeness_single_node(self):
|
||||
"""
|
||||
Group closeness centrality for a single node group
|
||||
"""
|
||||
G = nx.path_graph(5)
|
||||
c = nx.group_closeness_centrality(G, [1])
|
||||
c_answer = nx.closeness_centrality(G, 1)
|
||||
assert c == c_answer
|
||||
|
||||
def test_group_closeness_disconnected(self):
|
||||
"""
|
||||
Group closeness centrality for a disconnected graph
|
||||
"""
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from([1, 2, 3, 4])
|
||||
c = nx.group_closeness_centrality(G, [1, 2])
|
||||
c_answer = 0
|
||||
assert c == c_answer
|
||||
|
||||
def test_group_closeness_multiple_node(self):
|
||||
"""
|
||||
Group closeness centrality for a group with more than
|
||||
1 node
|
||||
"""
|
||||
G = nx.path_graph(4)
|
||||
c = nx.group_closeness_centrality(G, [1, 2])
|
||||
c_answer = 1
|
||||
assert c == c_answer
|
||||
|
||||
def test_group_closeness_node_not_in_graph(self):
|
||||
"""
|
||||
Node(s) in S not in graph, raises NodeNotFound exception
|
||||
"""
|
||||
with pytest.raises(nx.NodeNotFound):
|
||||
nx.group_closeness_centrality(nx.path_graph(5), [6, 7, 8])
|
||||
|
||||
|
||||
class TestGroupDegreeCentrality:
|
||||
def test_group_degree_centrality_single_node(self):
|
||||
"""
|
||||
Group degree centrality for a single node group
|
||||
"""
|
||||
G = nx.path_graph(4)
|
||||
d = nx.group_degree_centrality(G, [1])
|
||||
d_answer = nx.degree_centrality(G)[1]
|
||||
assert d == d_answer
|
||||
|
||||
def test_group_degree_centrality_multiple_node(self):
|
||||
"""
|
||||
Group degree centrality for group with more than
|
||||
1 node
|
||||
"""
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from([1, 2, 3, 4, 5, 6, 7, 8])
|
||||
G.add_edges_from(
|
||||
[(1, 2), (1, 3), (1, 6), (1, 7), (1, 8), (2, 3), (2, 4), (2, 5)]
|
||||
)
|
||||
d = nx.group_degree_centrality(G, [1, 2])
|
||||
d_answer = 1
|
||||
assert d == d_answer
|
||||
|
||||
def test_group_in_degree_centrality(self):
|
||||
"""
|
||||
Group in-degree centrality in a DiGraph
|
||||
"""
|
||||
G = nx.DiGraph()
|
||||
G.add_nodes_from([1, 2, 3, 4, 5, 6, 7, 8])
|
||||
G.add_edges_from(
|
||||
[(1, 2), (1, 3), (1, 6), (1, 7), (1, 8), (2, 3), (2, 4), (2, 5)]
|
||||
)
|
||||
d = nx.group_in_degree_centrality(G, [1, 2])
|
||||
d_answer = 0
|
||||
assert d == d_answer
|
||||
|
||||
def test_group_out_degree_centrality(self):
|
||||
"""
|
||||
Group out-degree centrality in a DiGraph
|
||||
"""
|
||||
G = nx.DiGraph()
|
||||
G.add_nodes_from([1, 2, 3, 4, 5, 6, 7, 8])
|
||||
G.add_edges_from(
|
||||
[(1, 2), (1, 3), (1, 6), (1, 7), (1, 8), (2, 3), (2, 4), (2, 5)]
|
||||
)
|
||||
d = nx.group_out_degree_centrality(G, [1, 2])
|
||||
d_answer = 1
|
||||
assert d == d_answer
|
||||
|
||||
def test_group_degree_centrality_node_not_in_graph(self):
|
||||
"""
|
||||
Node(s) in S not in graph, raises NetworkXError
|
||||
"""
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
nx.group_degree_centrality(nx.path_graph(5), [6, 7, 8])
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
"""
|
||||
Tests for degree centrality.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.centrality import harmonic_centrality
|
||||
|
||||
|
||||
class TestClosenessCentrality:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.P3 = nx.path_graph(3)
|
||||
cls.P4 = nx.path_graph(4)
|
||||
cls.K5 = nx.complete_graph(5)
|
||||
|
||||
cls.C4 = nx.cycle_graph(4)
|
||||
cls.C4_directed = nx.cycle_graph(4, create_using=nx.DiGraph)
|
||||
|
||||
cls.C5 = nx.cycle_graph(5)
|
||||
|
||||
cls.T = nx.balanced_tree(r=2, h=2)
|
||||
|
||||
cls.Gb = nx.DiGraph()
|
||||
cls.Gb.add_edges_from([(0, 1), (0, 2), (0, 4), (2, 1), (2, 3), (4, 3)])
|
||||
|
||||
def test_p3_harmonic(self):
|
||||
c = harmonic_centrality(self.P3)
|
||||
d = {0: 1.5, 1: 2, 2: 1.5}
|
||||
for n in sorted(self.P3):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_p4_harmonic(self):
|
||||
c = harmonic_centrality(self.P4)
|
||||
d = {0: 1.8333333, 1: 2.5, 2: 2.5, 3: 1.8333333}
|
||||
for n in sorted(self.P4):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_clique_complete(self):
|
||||
c = harmonic_centrality(self.K5)
|
||||
d = {0: 4, 1: 4, 2: 4, 3: 4, 4: 4}
|
||||
for n in sorted(self.P3):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_cycle_C4(self):
|
||||
c = harmonic_centrality(self.C4)
|
||||
d = {0: 2.5, 1: 2.5, 2: 2.5, 3: 2.5}
|
||||
for n in sorted(self.C4):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_cycle_C5(self):
|
||||
c = harmonic_centrality(self.C5)
|
||||
d = {0: 3, 1: 3, 2: 3, 3: 3, 4: 3, 5: 4}
|
||||
for n in sorted(self.C5):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_bal_tree(self):
|
||||
c = harmonic_centrality(self.T)
|
||||
d = {0: 4.0, 1: 4.1666, 2: 4.1666, 3: 2.8333, 4: 2.8333, 5: 2.8333, 6: 2.8333}
|
||||
for n in sorted(self.T):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_exampleGraph(self):
|
||||
c = harmonic_centrality(self.Gb)
|
||||
d = {0: 0, 1: 2, 2: 1, 3: 2.5, 4: 1}
|
||||
for n in sorted(self.Gb):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_weighted_harmonic(self):
|
||||
XG = nx.DiGraph()
|
||||
XG.add_weighted_edges_from(
|
||||
[
|
||||
("a", "b", 10),
|
||||
("d", "c", 5),
|
||||
("a", "c", 1),
|
||||
("e", "f", 2),
|
||||
("f", "c", 1),
|
||||
("a", "f", 3),
|
||||
]
|
||||
)
|
||||
c = harmonic_centrality(XG, distance="weight")
|
||||
d = {"a": 0, "b": 0.1, "c": 2.533, "d": 0, "e": 0, "f": 0.83333}
|
||||
for n in sorted(XG):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_empty(self):
|
||||
G = nx.DiGraph()
|
||||
c = harmonic_centrality(G, distance="weight")
|
||||
d = {}
|
||||
assert c == d
|
||||
|
||||
def test_singleton(self):
|
||||
G = nx.DiGraph()
|
||||
G.add_node(0)
|
||||
c = harmonic_centrality(G, distance="weight")
|
||||
d = {0: 0}
|
||||
assert c == d
|
||||
|
||||
def test_cycle_c4_directed(self):
|
||||
c = harmonic_centrality(self.C4_directed, nbunch=[0, 1], sources=[1, 2])
|
||||
d = {0: 0.833, 1: 0.333}
|
||||
for n in [0, 1]:
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_cycle_c4_directed_subset(self):
|
||||
c = harmonic_centrality(self.C4_directed, nbunch=[0, 1])
|
||||
d = 1.833
|
||||
for n in [0, 1]:
|
||||
assert c[n] == pytest.approx(d, abs=1e-3)
|
||||
|
||||
def test_p3_harmonic_subset(self):
|
||||
c = harmonic_centrality(self.P3, sources=[0, 1])
|
||||
d = {0: 1, 1: 1, 2: 1.5}
|
||||
for n in self.P3:
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_p4_harmonic_subset(self):
|
||||
c = harmonic_centrality(self.P4, nbunch=[2, 3], sources=[0, 1])
|
||||
d = {2: 1.5, 3: 0.8333333}
|
||||
for n in [2, 3]:
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
|
||||
|
||||
class TestKatzCentrality:
|
||||
def test_K5(self):
|
||||
"""Katz centrality: K5"""
|
||||
G = nx.complete_graph(5)
|
||||
alpha = 0.1
|
||||
b = nx.katz_centrality(G, alpha)
|
||||
v = math.sqrt(1 / 5.0)
|
||||
b_answer = dict.fromkeys(G, v)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
nstart = {n: 1 for n in G}
|
||||
b = nx.katz_centrality(G, alpha, nstart=nstart)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
|
||||
def test_P3(self):
|
||||
"""Katz centrality: P3"""
|
||||
alpha = 0.1
|
||||
G = nx.path_graph(3)
|
||||
b_answer = {0: 0.5598852584152165, 1: 0.6107839182711449, 2: 0.5598852584152162}
|
||||
b = nx.katz_centrality(G, alpha)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-4)
|
||||
|
||||
def test_maxiter(self):
|
||||
with pytest.raises(nx.PowerIterationFailedConvergence):
|
||||
nx.katz_centrality(nx.path_graph(3), 0.1, max_iter=0)
|
||||
|
||||
def test_beta_as_scalar(self):
|
||||
alpha = 0.1
|
||||
beta = 0.1
|
||||
b_answer = {0: 0.5598852584152165, 1: 0.6107839182711449, 2: 0.5598852584152162}
|
||||
G = nx.path_graph(3)
|
||||
b = nx.katz_centrality(G, alpha, beta)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-4)
|
||||
|
||||
def test_beta_as_dict(self):
|
||||
alpha = 0.1
|
||||
beta = {0: 1.0, 1: 1.0, 2: 1.0}
|
||||
b_answer = {0: 0.5598852584152165, 1: 0.6107839182711449, 2: 0.5598852584152162}
|
||||
G = nx.path_graph(3)
|
||||
b = nx.katz_centrality(G, alpha, beta)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-4)
|
||||
|
||||
def test_multiple_alpha(self):
|
||||
alpha_list = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]
|
||||
for alpha in alpha_list:
|
||||
b_answer = {
|
||||
0.1: {
|
||||
0: 0.5598852584152165,
|
||||
1: 0.6107839182711449,
|
||||
2: 0.5598852584152162,
|
||||
},
|
||||
0.2: {
|
||||
0: 0.5454545454545454,
|
||||
1: 0.6363636363636365,
|
||||
2: 0.5454545454545454,
|
||||
},
|
||||
0.3: {
|
||||
0: 0.5333964609104419,
|
||||
1: 0.6564879518897746,
|
||||
2: 0.5333964609104419,
|
||||
},
|
||||
0.4: {
|
||||
0: 0.5232045649263551,
|
||||
1: 0.6726915834767423,
|
||||
2: 0.5232045649263551,
|
||||
},
|
||||
0.5: {
|
||||
0: 0.5144957746691622,
|
||||
1: 0.6859943117075809,
|
||||
2: 0.5144957746691622,
|
||||
},
|
||||
0.6: {
|
||||
0: 0.5069794004195823,
|
||||
1: 0.6970966755769258,
|
||||
2: 0.5069794004195823,
|
||||
},
|
||||
}
|
||||
G = nx.path_graph(3)
|
||||
b = nx.katz_centrality(G, alpha)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[alpha][n], abs=1e-4)
|
||||
|
||||
def test_multigraph(self):
|
||||
with pytest.raises(nx.NetworkXException):
|
||||
nx.katz_centrality(nx.MultiGraph(), 0.1)
|
||||
|
||||
def test_empty(self):
|
||||
e = nx.katz_centrality(nx.Graph(), 0.1)
|
||||
assert e == {}
|
||||
|
||||
def test_bad_beta(self):
|
||||
with pytest.raises(nx.NetworkXException):
|
||||
G = nx.Graph([(0, 1)])
|
||||
beta = {0: 77}
|
||||
nx.katz_centrality(G, 0.1, beta=beta)
|
||||
|
||||
def test_bad_beta_number(self):
|
||||
with pytest.raises(nx.NetworkXException):
|
||||
G = nx.Graph([(0, 1)])
|
||||
nx.katz_centrality(G, 0.1, beta="foo")
|
||||
|
||||
|
||||
class TestKatzCentralityNumpy:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
global np
|
||||
np = pytest.importorskip("numpy")
|
||||
pytest.importorskip("scipy")
|
||||
|
||||
def test_K5(self):
|
||||
"""Katz centrality: K5"""
|
||||
G = nx.complete_graph(5)
|
||||
alpha = 0.1
|
||||
b = nx.katz_centrality(G, alpha)
|
||||
v = math.sqrt(1 / 5.0)
|
||||
b_answer = dict.fromkeys(G, v)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
b = nx.eigenvector_centrality_numpy(G)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-3)
|
||||
|
||||
def test_P3(self):
|
||||
"""Katz centrality: P3"""
|
||||
alpha = 0.1
|
||||
G = nx.path_graph(3)
|
||||
b_answer = {0: 0.5598852584152165, 1: 0.6107839182711449, 2: 0.5598852584152162}
|
||||
b = nx.katz_centrality_numpy(G, alpha)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-4)
|
||||
|
||||
def test_beta_as_scalar(self):
|
||||
alpha = 0.1
|
||||
beta = 0.1
|
||||
b_answer = {0: 0.5598852584152165, 1: 0.6107839182711449, 2: 0.5598852584152162}
|
||||
G = nx.path_graph(3)
|
||||
b = nx.katz_centrality_numpy(G, alpha, beta)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-4)
|
||||
|
||||
def test_beta_as_dict(self):
|
||||
alpha = 0.1
|
||||
beta = {0: 1.0, 1: 1.0, 2: 1.0}
|
||||
b_answer = {0: 0.5598852584152165, 1: 0.6107839182711449, 2: 0.5598852584152162}
|
||||
G = nx.path_graph(3)
|
||||
b = nx.katz_centrality_numpy(G, alpha, beta)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-4)
|
||||
|
||||
def test_multiple_alpha(self):
|
||||
alpha_list = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6]
|
||||
for alpha in alpha_list:
|
||||
b_answer = {
|
||||
0.1: {
|
||||
0: 0.5598852584152165,
|
||||
1: 0.6107839182711449,
|
||||
2: 0.5598852584152162,
|
||||
},
|
||||
0.2: {
|
||||
0: 0.5454545454545454,
|
||||
1: 0.6363636363636365,
|
||||
2: 0.5454545454545454,
|
||||
},
|
||||
0.3: {
|
||||
0: 0.5333964609104419,
|
||||
1: 0.6564879518897746,
|
||||
2: 0.5333964609104419,
|
||||
},
|
||||
0.4: {
|
||||
0: 0.5232045649263551,
|
||||
1: 0.6726915834767423,
|
||||
2: 0.5232045649263551,
|
||||
},
|
||||
0.5: {
|
||||
0: 0.5144957746691622,
|
||||
1: 0.6859943117075809,
|
||||
2: 0.5144957746691622,
|
||||
},
|
||||
0.6: {
|
||||
0: 0.5069794004195823,
|
||||
1: 0.6970966755769258,
|
||||
2: 0.5069794004195823,
|
||||
},
|
||||
}
|
||||
G = nx.path_graph(3)
|
||||
b = nx.katz_centrality_numpy(G, alpha)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[alpha][n], abs=1e-4)
|
||||
|
||||
def test_multigraph(self):
|
||||
with pytest.raises(nx.NetworkXException):
|
||||
nx.katz_centrality(nx.MultiGraph(), 0.1)
|
||||
|
||||
def test_empty(self):
|
||||
e = nx.katz_centrality(nx.Graph(), 0.1)
|
||||
assert e == {}
|
||||
|
||||
def test_bad_beta(self):
|
||||
with pytest.raises(nx.NetworkXException):
|
||||
G = nx.Graph([(0, 1)])
|
||||
beta = {0: 77}
|
||||
nx.katz_centrality_numpy(G, 0.1, beta=beta)
|
||||
|
||||
def test_bad_beta_numbe(self):
|
||||
with pytest.raises(nx.NetworkXException):
|
||||
G = nx.Graph([(0, 1)])
|
||||
nx.katz_centrality_numpy(G, 0.1, beta="foo")
|
||||
|
||||
def test_K5_unweighted(self):
|
||||
"""Katz centrality: K5"""
|
||||
G = nx.complete_graph(5)
|
||||
alpha = 0.1
|
||||
b = nx.katz_centrality(G, alpha, weight=None)
|
||||
v = math.sqrt(1 / 5.0)
|
||||
b_answer = dict.fromkeys(G, v)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-7)
|
||||
b = nx.eigenvector_centrality_numpy(G, weight=None)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-3)
|
||||
|
||||
def test_P3_unweighted(self):
|
||||
"""Katz centrality: P3"""
|
||||
alpha = 0.1
|
||||
G = nx.path_graph(3)
|
||||
b_answer = {0: 0.5598852584152165, 1: 0.6107839182711449, 2: 0.5598852584152162}
|
||||
b = nx.katz_centrality_numpy(G, alpha, weight=None)
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-4)
|
||||
|
||||
|
||||
class TestKatzCentralityDirected:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
G = nx.DiGraph()
|
||||
edges = [
|
||||
(1, 2),
|
||||
(1, 3),
|
||||
(2, 4),
|
||||
(3, 2),
|
||||
(3, 5),
|
||||
(4, 2),
|
||||
(4, 5),
|
||||
(4, 6),
|
||||
(5, 6),
|
||||
(5, 7),
|
||||
(5, 8),
|
||||
(6, 8),
|
||||
(7, 1),
|
||||
(7, 5),
|
||||
(7, 8),
|
||||
(8, 6),
|
||||
(8, 7),
|
||||
]
|
||||
G.add_edges_from(edges, weight=2.0)
|
||||
cls.G = G.reverse()
|
||||
cls.G.alpha = 0.1
|
||||
cls.G.evc = [
|
||||
0.3289589783189635,
|
||||
0.2832077296243516,
|
||||
0.3425906003685471,
|
||||
0.3970420865198392,
|
||||
0.41074871061646284,
|
||||
0.272257430756461,
|
||||
0.4201989685435462,
|
||||
0.34229059218038554,
|
||||
]
|
||||
|
||||
H = nx.DiGraph(edges)
|
||||
cls.H = G.reverse()
|
||||
cls.H.alpha = 0.1
|
||||
cls.H.evc = [
|
||||
0.3289589783189635,
|
||||
0.2832077296243516,
|
||||
0.3425906003685471,
|
||||
0.3970420865198392,
|
||||
0.41074871061646284,
|
||||
0.272257430756461,
|
||||
0.4201989685435462,
|
||||
0.34229059218038554,
|
||||
]
|
||||
|
||||
def test_katz_centrality_weighted(self):
|
||||
G = self.G
|
||||
alpha = self.G.alpha
|
||||
p = nx.katz_centrality(G, alpha, weight="weight")
|
||||
for a, b in zip(list(p.values()), self.G.evc):
|
||||
assert a == pytest.approx(b, abs=1e-7)
|
||||
|
||||
def test_katz_centrality_unweighted(self):
|
||||
H = self.H
|
||||
alpha = self.H.alpha
|
||||
p = nx.katz_centrality(H, alpha, weight="weight")
|
||||
for a, b in zip(list(p.values()), self.H.evc):
|
||||
assert a == pytest.approx(b, abs=1e-7)
|
||||
|
||||
|
||||
class TestKatzCentralityDirectedNumpy(TestKatzCentralityDirected):
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
global np
|
||||
np = pytest.importorskip("numpy")
|
||||
pytest.importorskip("scipy")
|
||||
super().setup_class()
|
||||
|
||||
def test_katz_centrality_weighted(self):
|
||||
G = self.G
|
||||
alpha = self.G.alpha
|
||||
p = nx.katz_centrality_numpy(G, alpha, weight="weight")
|
||||
for a, b in zip(list(p.values()), self.G.evc):
|
||||
assert a == pytest.approx(b, abs=1e-7)
|
||||
|
||||
def test_katz_centrality_unweighted(self):
|
||||
H = self.H
|
||||
alpha = self.H.alpha
|
||||
p = nx.katz_centrality_numpy(H, alpha, weight="weight")
|
||||
for a, b in zip(list(p.values()), self.H.evc):
|
||||
assert a == pytest.approx(b, abs=1e-7)
|
||||
|
||||
|
||||
class TestKatzEigenvectorVKatz:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
global np
|
||||
np = pytest.importorskip("numpy")
|
||||
pytest.importorskip("scipy")
|
||||
|
||||
def test_eigenvector_v_katz_random(self):
|
||||
G = nx.gnp_random_graph(10, 0.5, seed=1234)
|
||||
l = max(np.linalg.eigvals(nx.adjacency_matrix(G).todense()))
|
||||
e = nx.eigenvector_centrality_numpy(G)
|
||||
k = nx.katz_centrality_numpy(G, 1.0 / l)
|
||||
for n in G:
|
||||
assert e[n] == pytest.approx(k[n], abs=1e-7)
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
|
||||
np = pytest.importorskip("numpy")
|
||||
sp = pytest.importorskip("scipy")
|
||||
|
||||
|
||||
def test_laplacian_centrality_null_graph():
|
||||
G = nx.Graph()
|
||||
with pytest.raises(nx.NetworkXPointlessConcept):
|
||||
d = nx.laplacian_centrality(G, normalized=False)
|
||||
|
||||
|
||||
def test_laplacian_centrality_single_node():
|
||||
"""See gh-6571"""
|
||||
G = nx.empty_graph(1)
|
||||
assert nx.laplacian_centrality(G, normalized=False) == {0: 0}
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
nx.laplacian_centrality(G, normalized=True)
|
||||
|
||||
|
||||
def test_laplacian_centrality_unconnected_nodes():
|
||||
"""laplacian_centrality on a unconnected node graph should return 0
|
||||
|
||||
For graphs without edges, the Laplacian energy is 0 and is unchanged with
|
||||
node removal, so::
|
||||
|
||||
LC(v) = LE(G) - LE(G - v) = 0 - 0 = 0
|
||||
"""
|
||||
G = nx.empty_graph(3)
|
||||
assert nx.laplacian_centrality(G, normalized=False) == {0: 0, 1: 0, 2: 0}
|
||||
|
||||
|
||||
def test_laplacian_centrality_empty_graph():
|
||||
G = nx.empty_graph(3)
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
d = nx.laplacian_centrality(G, normalized=True)
|
||||
|
||||
|
||||
def test_laplacian_centrality_E():
|
||||
E = nx.Graph()
|
||||
E.add_weighted_edges_from(
|
||||
[(0, 1, 4), (4, 5, 1), (0, 2, 2), (2, 1, 1), (1, 3, 2), (1, 4, 2)]
|
||||
)
|
||||
d = nx.laplacian_centrality(E)
|
||||
exact = {
|
||||
0: 0.700000,
|
||||
1: 0.900000,
|
||||
2: 0.280000,
|
||||
3: 0.220000,
|
||||
4: 0.260000,
|
||||
5: 0.040000,
|
||||
}
|
||||
|
||||
for n, dc in d.items():
|
||||
assert exact[n] == pytest.approx(dc, abs=1e-7)
|
||||
|
||||
# Check not normalized
|
||||
full_energy = 200
|
||||
dnn = nx.laplacian_centrality(E, normalized=False)
|
||||
for n, dc in dnn.items():
|
||||
assert exact[n] * full_energy == pytest.approx(dc, abs=1e-7)
|
||||
|
||||
# Check unweighted not-normalized version
|
||||
duw_nn = nx.laplacian_centrality(E, normalized=False, weight=None)
|
||||
exact_uw_nn = {
|
||||
0: 18,
|
||||
1: 34,
|
||||
2: 18,
|
||||
3: 10,
|
||||
4: 16,
|
||||
5: 6,
|
||||
}
|
||||
for n, dc in duw_nn.items():
|
||||
assert exact_uw_nn[n] == pytest.approx(dc, abs=1e-7)
|
||||
|
||||
# Check unweighted version
|
||||
duw = nx.laplacian_centrality(E, weight=None)
|
||||
full_energy = 42
|
||||
for n, dc in duw.items():
|
||||
assert exact_uw_nn[n] / full_energy == pytest.approx(dc, abs=1e-7)
|
||||
|
||||
|
||||
def test_laplacian_centrality_KC():
|
||||
KC = nx.karate_club_graph()
|
||||
d = nx.laplacian_centrality(KC)
|
||||
exact = {
|
||||
0: 0.2543593,
|
||||
1: 0.1724524,
|
||||
2: 0.2166053,
|
||||
3: 0.0964646,
|
||||
4: 0.0350344,
|
||||
5: 0.0571109,
|
||||
6: 0.0540713,
|
||||
7: 0.0788674,
|
||||
8: 0.1222204,
|
||||
9: 0.0217565,
|
||||
10: 0.0308751,
|
||||
11: 0.0215965,
|
||||
12: 0.0174372,
|
||||
13: 0.118861,
|
||||
14: 0.0366341,
|
||||
15: 0.0548712,
|
||||
16: 0.0172772,
|
||||
17: 0.0191969,
|
||||
18: 0.0225564,
|
||||
19: 0.0331147,
|
||||
20: 0.0279955,
|
||||
21: 0.0246361,
|
||||
22: 0.0382339,
|
||||
23: 0.1294193,
|
||||
24: 0.0227164,
|
||||
25: 0.0644697,
|
||||
26: 0.0281555,
|
||||
27: 0.075188,
|
||||
28: 0.0364742,
|
||||
29: 0.0707087,
|
||||
30: 0.0708687,
|
||||
31: 0.131019,
|
||||
32: 0.2370821,
|
||||
33: 0.3066709,
|
||||
}
|
||||
for n, dc in d.items():
|
||||
assert exact[n] == pytest.approx(dc, abs=1e-7)
|
||||
|
||||
# Check not normalized
|
||||
full_energy = 12502
|
||||
dnn = nx.laplacian_centrality(KC, normalized=False)
|
||||
for n, dc in dnn.items():
|
||||
assert exact[n] * full_energy == pytest.approx(dc, abs=1e-3)
|
||||
|
||||
|
||||
def test_laplacian_centrality_K():
|
||||
K = nx.krackhardt_kite_graph()
|
||||
d = nx.laplacian_centrality(K)
|
||||
exact = {
|
||||
0: 0.3010753,
|
||||
1: 0.3010753,
|
||||
2: 0.2258065,
|
||||
3: 0.483871,
|
||||
4: 0.2258065,
|
||||
5: 0.3870968,
|
||||
6: 0.3870968,
|
||||
7: 0.1935484,
|
||||
8: 0.0752688,
|
||||
9: 0.0322581,
|
||||
}
|
||||
for n, dc in d.items():
|
||||
assert exact[n] == pytest.approx(dc, abs=1e-7)
|
||||
|
||||
# Check not normalized
|
||||
full_energy = 186
|
||||
dnn = nx.laplacian_centrality(K, normalized=False)
|
||||
for n, dc in dnn.items():
|
||||
assert exact[n] * full_energy == pytest.approx(dc, abs=1e-3)
|
||||
|
||||
|
||||
def test_laplacian_centrality_P3():
|
||||
P3 = nx.path_graph(3)
|
||||
d = nx.laplacian_centrality(P3)
|
||||
exact = {0: 0.6, 1: 1.0, 2: 0.6}
|
||||
for n, dc in d.items():
|
||||
assert exact[n] == pytest.approx(dc, abs=1e-7)
|
||||
|
||||
|
||||
def test_laplacian_centrality_K5():
|
||||
K5 = nx.complete_graph(5)
|
||||
d = nx.laplacian_centrality(K5)
|
||||
exact = {0: 0.52, 1: 0.52, 2: 0.52, 3: 0.52, 4: 0.52}
|
||||
for n, dc in d.items():
|
||||
assert exact[n] == pytest.approx(dc, abs=1e-7)
|
||||
|
||||
|
||||
def test_laplacian_centrality_FF():
|
||||
FF = nx.florentine_families_graph()
|
||||
d = nx.laplacian_centrality(FF)
|
||||
exact = {
|
||||
"Acciaiuoli": 0.0804598,
|
||||
"Medici": 0.4022989,
|
||||
"Castellani": 0.1724138,
|
||||
"Peruzzi": 0.183908,
|
||||
"Strozzi": 0.2528736,
|
||||
"Barbadori": 0.137931,
|
||||
"Ridolfi": 0.2183908,
|
||||
"Tornabuoni": 0.2183908,
|
||||
"Albizzi": 0.1954023,
|
||||
"Salviati": 0.1149425,
|
||||
"Pazzi": 0.0344828,
|
||||
"Bischeri": 0.1954023,
|
||||
"Guadagni": 0.2298851,
|
||||
"Ginori": 0.045977,
|
||||
"Lamberteschi": 0.0574713,
|
||||
}
|
||||
for n, dc in d.items():
|
||||
assert exact[n] == pytest.approx(dc, abs=1e-7)
|
||||
|
||||
|
||||
def test_laplacian_centrality_DG():
|
||||
DG = nx.DiGraph([(0, 5), (1, 5), (2, 5), (3, 5), (4, 5), (5, 6), (5, 7), (5, 8)])
|
||||
d = nx.laplacian_centrality(DG)
|
||||
exact = {
|
||||
0: 0.2123352,
|
||||
5: 0.515391,
|
||||
1: 0.2123352,
|
||||
2: 0.2123352,
|
||||
3: 0.2123352,
|
||||
4: 0.2123352,
|
||||
6: 0.2952031,
|
||||
7: 0.2952031,
|
||||
8: 0.2952031,
|
||||
}
|
||||
for n, dc in d.items():
|
||||
assert exact[n] == pytest.approx(dc, abs=1e-7)
|
||||
|
||||
# Check not normalized
|
||||
full_energy = 9.50704
|
||||
dnn = nx.laplacian_centrality(DG, normalized=False)
|
||||
for n, dc in dnn.items():
|
||||
assert exact[n] * full_energy == pytest.approx(dc, abs=1e-4)
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
|
||||
|
||||
class TestLoadCentrality:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
G = nx.Graph()
|
||||
G.add_edge(0, 1, weight=3)
|
||||
G.add_edge(0, 2, weight=2)
|
||||
G.add_edge(0, 3, weight=6)
|
||||
G.add_edge(0, 4, weight=4)
|
||||
G.add_edge(1, 3, weight=5)
|
||||
G.add_edge(1, 5, weight=5)
|
||||
G.add_edge(2, 4, weight=1)
|
||||
G.add_edge(3, 4, weight=2)
|
||||
G.add_edge(3, 5, weight=1)
|
||||
G.add_edge(4, 5, weight=4)
|
||||
cls.G = G
|
||||
cls.exact_weighted = {0: 4.0, 1: 0.0, 2: 8.0, 3: 6.0, 4: 8.0, 5: 0.0}
|
||||
cls.K = nx.krackhardt_kite_graph()
|
||||
cls.P3 = nx.path_graph(3)
|
||||
cls.P4 = nx.path_graph(4)
|
||||
cls.K5 = nx.complete_graph(5)
|
||||
cls.P2 = nx.path_graph(2)
|
||||
|
||||
cls.C4 = nx.cycle_graph(4)
|
||||
cls.T = nx.balanced_tree(r=2, h=2)
|
||||
cls.Gb = nx.Graph()
|
||||
cls.Gb.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (4, 5), (3, 5)])
|
||||
cls.F = nx.florentine_families_graph()
|
||||
cls.LM = nx.les_miserables_graph()
|
||||
cls.D = nx.cycle_graph(3, create_using=nx.DiGraph())
|
||||
cls.D.add_edges_from([(3, 0), (4, 3)])
|
||||
|
||||
def test_not_strongly_connected(self):
|
||||
b = nx.load_centrality(self.D)
|
||||
result = {0: 5.0 / 12, 1: 1.0 / 4, 2: 1.0 / 12, 3: 1.0 / 4, 4: 0.000}
|
||||
for n in sorted(self.D):
|
||||
assert result[n] == pytest.approx(b[n], abs=1e-3)
|
||||
assert result[n] == pytest.approx(nx.load_centrality(self.D, n), abs=1e-3)
|
||||
|
||||
def test_P2_normalized_load(self):
|
||||
G = self.P2
|
||||
c = nx.load_centrality(G, normalized=True)
|
||||
d = {0: 0.000, 1: 0.000}
|
||||
for n in sorted(G):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_weighted_load(self):
|
||||
b = nx.load_centrality(self.G, weight="weight", normalized=False)
|
||||
for n in sorted(self.G):
|
||||
assert b[n] == self.exact_weighted[n]
|
||||
|
||||
def test_k5_load(self):
|
||||
G = self.K5
|
||||
c = nx.load_centrality(G)
|
||||
d = {0: 0.000, 1: 0.000, 2: 0.000, 3: 0.000, 4: 0.000}
|
||||
for n in sorted(G):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_p3_load(self):
|
||||
G = self.P3
|
||||
c = nx.load_centrality(G)
|
||||
d = {0: 0.000, 1: 1.000, 2: 0.000}
|
||||
for n in sorted(G):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
c = nx.load_centrality(G, v=1)
|
||||
assert c == pytest.approx(1.0, abs=1e-7)
|
||||
c = nx.load_centrality(G, v=1, normalized=True)
|
||||
assert c == pytest.approx(1.0, abs=1e-7)
|
||||
|
||||
def test_p2_load(self):
|
||||
G = nx.path_graph(2)
|
||||
c = nx.load_centrality(G)
|
||||
d = {0: 0.000, 1: 0.000}
|
||||
for n in sorted(G):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_krackhardt_load(self):
|
||||
G = self.K
|
||||
c = nx.load_centrality(G)
|
||||
d = {
|
||||
0: 0.023,
|
||||
1: 0.023,
|
||||
2: 0.000,
|
||||
3: 0.102,
|
||||
4: 0.000,
|
||||
5: 0.231,
|
||||
6: 0.231,
|
||||
7: 0.389,
|
||||
8: 0.222,
|
||||
9: 0.000,
|
||||
}
|
||||
for n in sorted(G):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_florentine_families_load(self):
|
||||
G = self.F
|
||||
c = nx.load_centrality(G)
|
||||
d = {
|
||||
"Acciaiuoli": 0.000,
|
||||
"Albizzi": 0.211,
|
||||
"Barbadori": 0.093,
|
||||
"Bischeri": 0.104,
|
||||
"Castellani": 0.055,
|
||||
"Ginori": 0.000,
|
||||
"Guadagni": 0.251,
|
||||
"Lamberteschi": 0.000,
|
||||
"Medici": 0.522,
|
||||
"Pazzi": 0.000,
|
||||
"Peruzzi": 0.022,
|
||||
"Ridolfi": 0.117,
|
||||
"Salviati": 0.143,
|
||||
"Strozzi": 0.106,
|
||||
"Tornabuoni": 0.090,
|
||||
}
|
||||
for n in sorted(G):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_les_miserables_load(self):
|
||||
G = self.LM
|
||||
c = nx.load_centrality(G)
|
||||
d = {
|
||||
"Napoleon": 0.000,
|
||||
"Myriel": 0.177,
|
||||
"MlleBaptistine": 0.000,
|
||||
"MmeMagloire": 0.000,
|
||||
"CountessDeLo": 0.000,
|
||||
"Geborand": 0.000,
|
||||
"Champtercier": 0.000,
|
||||
"Cravatte": 0.000,
|
||||
"Count": 0.000,
|
||||
"OldMan": 0.000,
|
||||
"Valjean": 0.567,
|
||||
"Labarre": 0.000,
|
||||
"Marguerite": 0.000,
|
||||
"MmeDeR": 0.000,
|
||||
"Isabeau": 0.000,
|
||||
"Gervais": 0.000,
|
||||
"Listolier": 0.000,
|
||||
"Tholomyes": 0.043,
|
||||
"Fameuil": 0.000,
|
||||
"Blacheville": 0.000,
|
||||
"Favourite": 0.000,
|
||||
"Dahlia": 0.000,
|
||||
"Zephine": 0.000,
|
||||
"Fantine": 0.128,
|
||||
"MmeThenardier": 0.029,
|
||||
"Thenardier": 0.075,
|
||||
"Cosette": 0.024,
|
||||
"Javert": 0.054,
|
||||
"Fauchelevent": 0.026,
|
||||
"Bamatabois": 0.008,
|
||||
"Perpetue": 0.000,
|
||||
"Simplice": 0.009,
|
||||
"Scaufflaire": 0.000,
|
||||
"Woman1": 0.000,
|
||||
"Judge": 0.000,
|
||||
"Champmathieu": 0.000,
|
||||
"Brevet": 0.000,
|
||||
"Chenildieu": 0.000,
|
||||
"Cochepaille": 0.000,
|
||||
"Pontmercy": 0.007,
|
||||
"Boulatruelle": 0.000,
|
||||
"Eponine": 0.012,
|
||||
"Anzelma": 0.000,
|
||||
"Woman2": 0.000,
|
||||
"MotherInnocent": 0.000,
|
||||
"Gribier": 0.000,
|
||||
"MmeBurgon": 0.026,
|
||||
"Jondrette": 0.000,
|
||||
"Gavroche": 0.164,
|
||||
"Gillenormand": 0.021,
|
||||
"Magnon": 0.000,
|
||||
"MlleGillenormand": 0.047,
|
||||
"MmePontmercy": 0.000,
|
||||
"MlleVaubois": 0.000,
|
||||
"LtGillenormand": 0.000,
|
||||
"Marius": 0.133,
|
||||
"BaronessT": 0.000,
|
||||
"Mabeuf": 0.028,
|
||||
"Enjolras": 0.041,
|
||||
"Combeferre": 0.001,
|
||||
"Prouvaire": 0.000,
|
||||
"Feuilly": 0.001,
|
||||
"Courfeyrac": 0.006,
|
||||
"Bahorel": 0.002,
|
||||
"Bossuet": 0.032,
|
||||
"Joly": 0.002,
|
||||
"Grantaire": 0.000,
|
||||
"MotherPlutarch": 0.000,
|
||||
"Gueulemer": 0.005,
|
||||
"Babet": 0.005,
|
||||
"Claquesous": 0.005,
|
||||
"Montparnasse": 0.004,
|
||||
"Toussaint": 0.000,
|
||||
"Child1": 0.000,
|
||||
"Child2": 0.000,
|
||||
"Brujon": 0.000,
|
||||
"MmeHucheloup": 0.000,
|
||||
}
|
||||
for n in sorted(G):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_unnormalized_k5_load(self):
|
||||
G = self.K5
|
||||
c = nx.load_centrality(G, normalized=False)
|
||||
d = {0: 0.000, 1: 0.000, 2: 0.000, 3: 0.000, 4: 0.000}
|
||||
for n in sorted(G):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_unnormalized_p3_load(self):
|
||||
G = self.P3
|
||||
c = nx.load_centrality(G, normalized=False)
|
||||
d = {0: 0.000, 1: 2.000, 2: 0.000}
|
||||
for n in sorted(G):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_unnormalized_krackhardt_load(self):
|
||||
G = self.K
|
||||
c = nx.load_centrality(G, normalized=False)
|
||||
d = {
|
||||
0: 1.667,
|
||||
1: 1.667,
|
||||
2: 0.000,
|
||||
3: 7.333,
|
||||
4: 0.000,
|
||||
5: 16.667,
|
||||
6: 16.667,
|
||||
7: 28.000,
|
||||
8: 16.000,
|
||||
9: 0.000,
|
||||
}
|
||||
|
||||
for n in sorted(G):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_unnormalized_florentine_families_load(self):
|
||||
G = self.F
|
||||
c = nx.load_centrality(G, normalized=False)
|
||||
|
||||
d = {
|
||||
"Acciaiuoli": 0.000,
|
||||
"Albizzi": 38.333,
|
||||
"Barbadori": 17.000,
|
||||
"Bischeri": 19.000,
|
||||
"Castellani": 10.000,
|
||||
"Ginori": 0.000,
|
||||
"Guadagni": 45.667,
|
||||
"Lamberteschi": 0.000,
|
||||
"Medici": 95.000,
|
||||
"Pazzi": 0.000,
|
||||
"Peruzzi": 4.000,
|
||||
"Ridolfi": 21.333,
|
||||
"Salviati": 26.000,
|
||||
"Strozzi": 19.333,
|
||||
"Tornabuoni": 16.333,
|
||||
}
|
||||
for n in sorted(G):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_load_betweenness_difference(self):
|
||||
# Difference Between Load and Betweenness
|
||||
# --------------------------------------- The smallest graph
|
||||
# that shows the difference between load and betweenness is
|
||||
# G=ladder_graph(3) (Graph B below)
|
||||
|
||||
# Graph A and B are from Tao Zhou, Jian-Guo Liu, Bing-Hong
|
||||
# Wang: Comment on "Scientific collaboration
|
||||
# networks. II. Shortest paths, weighted networks, and
|
||||
# centrality". https://arxiv.org/pdf/physics/0511084
|
||||
|
||||
# Notice that unlike here, their calculation adds to 1 to the
|
||||
# betweenness of every node i for every path from i to every
|
||||
# other node. This is exactly what it should be, based on
|
||||
# Eqn. (1) in their paper: the eqn is B(v) = \sum_{s\neq t,
|
||||
# s\neq v}{\frac{\sigma_{st}(v)}{\sigma_{st}}}, therefore,
|
||||
# they allow v to be the target node.
|
||||
|
||||
# We follow Brandes 2001, who follows Freeman 1977 that make
|
||||
# the sum for betweenness of v exclude paths where v is either
|
||||
# the source or target node. To agree with their numbers, we
|
||||
# must additionally, remove edge (4,8) from the graph, see AC
|
||||
# example following (there is a mistake in the figure in their
|
||||
# paper - personal communication).
|
||||
|
||||
# A = nx.Graph()
|
||||
# A.add_edges_from([(0,1), (1,2), (1,3), (2,4),
|
||||
# (3,5), (4,6), (4,7), (4,8),
|
||||
# (5,8), (6,9), (7,9), (8,9)])
|
||||
B = nx.Graph() # ladder_graph(3)
|
||||
B.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (4, 5), (3, 5)])
|
||||
c = nx.load_centrality(B, normalized=False)
|
||||
d = {0: 1.750, 1: 1.750, 2: 6.500, 3: 6.500, 4: 1.750, 5: 1.750}
|
||||
for n in sorted(B):
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_c4_edge_load(self):
|
||||
G = self.C4
|
||||
c = nx.edge_load_centrality(G)
|
||||
d = {(0, 1): 6.000, (0, 3): 6.000, (1, 2): 6.000, (2, 3): 6.000}
|
||||
for n in G.edges():
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_p4_edge_load(self):
|
||||
G = self.P4
|
||||
c = nx.edge_load_centrality(G)
|
||||
d = {(0, 1): 6.000, (1, 2): 8.000, (2, 3): 6.000}
|
||||
for n in G.edges():
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_k5_edge_load(self):
|
||||
G = self.K5
|
||||
c = nx.edge_load_centrality(G)
|
||||
d = {
|
||||
(0, 1): 5.000,
|
||||
(0, 2): 5.000,
|
||||
(0, 3): 5.000,
|
||||
(0, 4): 5.000,
|
||||
(1, 2): 5.000,
|
||||
(1, 3): 5.000,
|
||||
(1, 4): 5.000,
|
||||
(2, 3): 5.000,
|
||||
(2, 4): 5.000,
|
||||
(3, 4): 5.000,
|
||||
}
|
||||
for n in G.edges():
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
|
||||
def test_tree_edge_load(self):
|
||||
G = self.T
|
||||
c = nx.edge_load_centrality(G)
|
||||
d = {
|
||||
(0, 1): 24.000,
|
||||
(0, 2): 24.000,
|
||||
(1, 3): 12.000,
|
||||
(1, 4): 12.000,
|
||||
(2, 5): 12.000,
|
||||
(2, 6): 12.000,
|
||||
}
|
||||
for n in G.edges():
|
||||
assert c[n] == pytest.approx(d[n], abs=1e-3)
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
|
||||
|
||||
def example1a_G():
|
||||
G = nx.Graph()
|
||||
G.add_node(1, percolation=0.1)
|
||||
G.add_node(2, percolation=0.2)
|
||||
G.add_node(3, percolation=0.2)
|
||||
G.add_node(4, percolation=0.2)
|
||||
G.add_node(5, percolation=0.3)
|
||||
G.add_node(6, percolation=0.2)
|
||||
G.add_node(7, percolation=0.5)
|
||||
G.add_node(8, percolation=0.5)
|
||||
G.add_edges_from([(1, 4), (2, 4), (3, 4), (4, 5), (5, 6), (6, 7), (6, 8)])
|
||||
return G
|
||||
|
||||
|
||||
def example1b_G():
|
||||
G = nx.Graph()
|
||||
G.add_node(1, percolation=0.3)
|
||||
G.add_node(2, percolation=0.5)
|
||||
G.add_node(3, percolation=0.5)
|
||||
G.add_node(4, percolation=0.2)
|
||||
G.add_node(5, percolation=0.3)
|
||||
G.add_node(6, percolation=0.2)
|
||||
G.add_node(7, percolation=0.1)
|
||||
G.add_node(8, percolation=0.1)
|
||||
G.add_edges_from([(1, 4), (2, 4), (3, 4), (4, 5), (5, 6), (6, 7), (6, 8)])
|
||||
return G
|
||||
|
||||
|
||||
def test_percolation_example1a():
|
||||
"""percolation centrality: example 1a"""
|
||||
G = example1a_G()
|
||||
p = nx.percolation_centrality(G)
|
||||
p_answer = {4: 0.625, 6: 0.667}
|
||||
for n, k in p_answer.items():
|
||||
assert p[n] == pytest.approx(k, abs=1e-3)
|
||||
|
||||
|
||||
def test_percolation_example1b():
|
||||
"""percolation centrality: example 1a"""
|
||||
G = example1b_G()
|
||||
p = nx.percolation_centrality(G)
|
||||
p_answer = {4: 0.825, 6: 0.4}
|
||||
for n, k in p_answer.items():
|
||||
assert p[n] == pytest.approx(k, abs=1e-3)
|
||||
|
||||
|
||||
def test_converge_to_betweenness():
|
||||
"""percolation centrality: should converge to betweenness
|
||||
centrality when all nodes are percolated the same"""
|
||||
# taken from betweenness test test_florentine_families_graph
|
||||
G = nx.florentine_families_graph()
|
||||
b_answer = {
|
||||
"Acciaiuoli": 0.000,
|
||||
"Albizzi": 0.212,
|
||||
"Barbadori": 0.093,
|
||||
"Bischeri": 0.104,
|
||||
"Castellani": 0.055,
|
||||
"Ginori": 0.000,
|
||||
"Guadagni": 0.255,
|
||||
"Lamberteschi": 0.000,
|
||||
"Medici": 0.522,
|
||||
"Pazzi": 0.000,
|
||||
"Peruzzi": 0.022,
|
||||
"Ridolfi": 0.114,
|
||||
"Salviati": 0.143,
|
||||
"Strozzi": 0.103,
|
||||
"Tornabuoni": 0.092,
|
||||
}
|
||||
|
||||
# If no initial state is provided, state for
|
||||
# every node defaults to 1
|
||||
p_answer = nx.percolation_centrality(G)
|
||||
assert p_answer == pytest.approx(b_answer, abs=1e-3)
|
||||
|
||||
p_states = {k: 0.3 for k, v in b_answer.items()}
|
||||
p_answer = nx.percolation_centrality(G, states=p_states)
|
||||
assert p_answer == pytest.approx(b_answer, abs=1e-3)
|
||||
|
||||
|
||||
def test_default_percolation():
|
||||
G = nx.erdos_renyi_graph(42, 0.42, seed=42)
|
||||
assert nx.percolation_centrality(G) == pytest.approx(nx.betweenness_centrality(G))
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
"""Unit tests for the :mod:`networkx.algorithms.centrality.reaching` module."""
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
|
||||
|
||||
class TestGlobalReachingCentrality:
|
||||
"""Unit tests for the global reaching centrality function."""
|
||||
|
||||
def test_non_positive_weights(self):
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
G = nx.DiGraph()
|
||||
nx.global_reaching_centrality(G, weight="weight")
|
||||
|
||||
def test_negatively_weighted(self):
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
G = nx.Graph()
|
||||
G.add_weighted_edges_from([(0, 1, -2), (1, 2, +1)])
|
||||
nx.global_reaching_centrality(G, weight="weight")
|
||||
|
||||
def test_directed_star(self):
|
||||
G = nx.DiGraph()
|
||||
G.add_weighted_edges_from([(1, 2, 0.5), (1, 3, 0.5)])
|
||||
grc = nx.global_reaching_centrality
|
||||
assert grc(G, normalized=False, weight="weight") == 0.5
|
||||
assert grc(G) == 1
|
||||
|
||||
def test_undirected_unweighted_star(self):
|
||||
G = nx.star_graph(2)
|
||||
grc = nx.global_reaching_centrality
|
||||
assert grc(G, normalized=False, weight=None) == 0.25
|
||||
|
||||
def test_undirected_weighted_star(self):
|
||||
G = nx.Graph()
|
||||
G.add_weighted_edges_from([(1, 2, 1), (1, 3, 2)])
|
||||
grc = nx.global_reaching_centrality
|
||||
assert grc(G, normalized=False, weight="weight") == 0.375
|
||||
|
||||
def test_cycle_directed_unweighted(self):
|
||||
G = nx.DiGraph()
|
||||
G.add_edge(1, 2)
|
||||
G.add_edge(2, 1)
|
||||
assert nx.global_reaching_centrality(G, weight=None) == 0
|
||||
|
||||
def test_cycle_undirected_unweighted(self):
|
||||
G = nx.Graph()
|
||||
G.add_edge(1, 2)
|
||||
assert nx.global_reaching_centrality(G, weight=None) == 0
|
||||
|
||||
def test_cycle_directed_weighted(self):
|
||||
G = nx.DiGraph()
|
||||
G.add_weighted_edges_from([(1, 2, 1), (2, 1, 1)])
|
||||
assert nx.global_reaching_centrality(G) == 0
|
||||
|
||||
def test_cycle_undirected_weighted(self):
|
||||
G = nx.Graph()
|
||||
G.add_edge(1, 2, weight=1)
|
||||
grc = nx.global_reaching_centrality
|
||||
assert grc(G, normalized=False) == 0
|
||||
|
||||
def test_directed_weighted(self):
|
||||
G = nx.DiGraph()
|
||||
G.add_edge("A", "B", weight=5)
|
||||
G.add_edge("B", "C", weight=1)
|
||||
G.add_edge("B", "D", weight=0.25)
|
||||
G.add_edge("D", "E", weight=1)
|
||||
|
||||
denom = len(G) - 1
|
||||
A_local = sum([5, 3, 2.625, 2.0833333333333]) / denom
|
||||
B_local = sum([1, 0.25, 0.625]) / denom
|
||||
C_local = 0
|
||||
D_local = sum([1]) / denom
|
||||
E_local = 0
|
||||
|
||||
local_reach_ctrs = [A_local, C_local, B_local, D_local, E_local]
|
||||
max_local = max(local_reach_ctrs)
|
||||
expected = sum(max_local - lrc for lrc in local_reach_ctrs) / denom
|
||||
grc = nx.global_reaching_centrality
|
||||
actual = grc(G, normalized=False, weight="weight")
|
||||
assert expected == pytest.approx(actual, abs=1e-7)
|
||||
|
||||
def test_single_node_with_cycle(self):
|
||||
G = nx.DiGraph([(1, 1)])
|
||||
with pytest.raises(nx.NetworkXError, match="local_reaching_centrality"):
|
||||
nx.global_reaching_centrality(G)
|
||||
|
||||
def test_single_node_with_weighted_cycle(self):
|
||||
G = nx.DiGraph()
|
||||
G.add_weighted_edges_from([(1, 1, 2)])
|
||||
with pytest.raises(nx.NetworkXError, match="local_reaching_centrality"):
|
||||
nx.global_reaching_centrality(G, weight="weight")
|
||||
|
||||
|
||||
class TestLocalReachingCentrality:
|
||||
"""Unit tests for the local reaching centrality function."""
|
||||
|
||||
def test_non_positive_weights(self):
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
G = nx.DiGraph()
|
||||
G.add_weighted_edges_from([(0, 1, 0)])
|
||||
nx.local_reaching_centrality(G, 0, weight="weight")
|
||||
|
||||
def test_negatively_weighted(self):
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
G = nx.Graph()
|
||||
G.add_weighted_edges_from([(0, 1, -2), (1, 2, +1)])
|
||||
nx.local_reaching_centrality(G, 0, weight="weight")
|
||||
|
||||
def test_undirected_unweighted_star(self):
|
||||
G = nx.star_graph(2)
|
||||
grc = nx.local_reaching_centrality
|
||||
assert grc(G, 1, weight=None, normalized=False) == 0.75
|
||||
|
||||
def test_undirected_weighted_star(self):
|
||||
G = nx.Graph()
|
||||
G.add_weighted_edges_from([(1, 2, 1), (1, 3, 2)])
|
||||
centrality = nx.local_reaching_centrality(
|
||||
G, 1, normalized=False, weight="weight"
|
||||
)
|
||||
assert centrality == 1.5
|
||||
|
||||
def test_undirected_weighted_normalized(self):
|
||||
G = nx.Graph()
|
||||
G.add_weighted_edges_from([(1, 2, 1), (1, 3, 2)])
|
||||
centrality = nx.local_reaching_centrality(
|
||||
G, 1, normalized=True, weight="weight"
|
||||
)
|
||||
assert centrality == 1.0
|
||||
|
||||
def test_single_node_with_cycle(self):
|
||||
G = nx.DiGraph([(1, 1)])
|
||||
with pytest.raises(nx.NetworkXError, match="local_reaching_centrality"):
|
||||
nx.local_reaching_centrality(G, 1)
|
||||
|
||||
def test_single_node_with_weighted_cycle(self):
|
||||
G = nx.DiGraph()
|
||||
G.add_weighted_edges_from([(1, 1, 2)])
|
||||
with pytest.raises(nx.NetworkXError, match="local_reaching_centrality"):
|
||||
nx.local_reaching_centrality(G, 1, weight="weight")
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Tests for second order centrality.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("numpy")
|
||||
pytest.importorskip("scipy")
|
||||
|
||||
import networkx as nx
|
||||
|
||||
|
||||
def test_empty():
|
||||
with pytest.raises(nx.NetworkXException):
|
||||
G = nx.empty_graph()
|
||||
nx.second_order_centrality(G)
|
||||
|
||||
|
||||
def test_non_connected():
|
||||
with pytest.raises(nx.NetworkXException):
|
||||
G = nx.Graph()
|
||||
G.add_node(0)
|
||||
G.add_node(1)
|
||||
nx.second_order_centrality(G)
|
||||
|
||||
|
||||
def test_non_negative_edge_weights():
|
||||
with pytest.raises(nx.NetworkXException):
|
||||
G = nx.path_graph(2)
|
||||
G.add_edge(0, 1, weight=-1)
|
||||
nx.second_order_centrality(G)
|
||||
|
||||
|
||||
def test_weight_attribute():
|
||||
G = nx.Graph()
|
||||
G.add_weighted_edges_from([(0, 1, 1.0), (1, 2, 3.5)], weight="w")
|
||||
expected = {0: 3.431, 1: 3.082, 2: 5.612}
|
||||
b = nx.second_order_centrality(G, weight="w")
|
||||
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(expected[n], abs=1e-2)
|
||||
|
||||
|
||||
def test_one_node_graph():
|
||||
"""Second order centrality: single node"""
|
||||
G = nx.Graph()
|
||||
G.add_node(0)
|
||||
G.add_edge(0, 0)
|
||||
assert nx.second_order_centrality(G)[0] == 0
|
||||
|
||||
|
||||
def test_P3():
|
||||
"""Second order centrality: line graph, as defined in paper"""
|
||||
G = nx.path_graph(3)
|
||||
b_answer = {0: 3.741, 1: 1.414, 2: 3.741}
|
||||
|
||||
b = nx.second_order_centrality(G)
|
||||
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-2)
|
||||
|
||||
|
||||
def test_K3():
|
||||
"""Second order centrality: complete graph, as defined in paper"""
|
||||
G = nx.complete_graph(3)
|
||||
b_answer = {0: 1.414, 1: 1.414, 2: 1.414}
|
||||
|
||||
b = nx.second_order_centrality(G)
|
||||
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-2)
|
||||
|
||||
|
||||
def test_ring_graph():
|
||||
"""Second order centrality: ring graph, as defined in paper"""
|
||||
G = nx.cycle_graph(5)
|
||||
b_answer = {0: 4.472, 1: 4.472, 2: 4.472, 3: 4.472, 4: 4.472}
|
||||
|
||||
b = nx.second_order_centrality(G)
|
||||
|
||||
for n in sorted(G):
|
||||
assert b[n] == pytest.approx(b_answer[n], abs=1e-2)
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("numpy")
|
||||
pytest.importorskip("scipy")
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.centrality.subgraph_alg import (
|
||||
communicability_betweenness_centrality,
|
||||
estrada_index,
|
||||
subgraph_centrality,
|
||||
subgraph_centrality_exp,
|
||||
)
|
||||
|
||||
|
||||
class TestSubgraph:
|
||||
def test_subgraph_centrality(self):
|
||||
answer = {0: 1.5430806348152433, 1: 1.5430806348152433}
|
||||
result = subgraph_centrality(nx.path_graph(2))
|
||||
for k, v in result.items():
|
||||
assert answer[k] == pytest.approx(v, abs=1e-7)
|
||||
|
||||
answer1 = {
|
||||
"1": 1.6445956054135658,
|
||||
"Albert": 2.4368257358712189,
|
||||
"Aric": 2.4368257358712193,
|
||||
"Dan": 3.1306328496328168,
|
||||
"Franck": 2.3876142275231915,
|
||||
}
|
||||
G1 = nx.Graph(
|
||||
[
|
||||
("Franck", "Aric"),
|
||||
("Aric", "Dan"),
|
||||
("Dan", "Albert"),
|
||||
("Albert", "Franck"),
|
||||
("Dan", "1"),
|
||||
("Franck", "Albert"),
|
||||
]
|
||||
)
|
||||
result1 = subgraph_centrality(G1)
|
||||
for k, v in result1.items():
|
||||
assert answer1[k] == pytest.approx(v, abs=1e-7)
|
||||
result1 = subgraph_centrality_exp(G1)
|
||||
for k, v in result1.items():
|
||||
assert answer1[k] == pytest.approx(v, abs=1e-7)
|
||||
|
||||
def test_subgraph_centrality_big_graph(self):
|
||||
g199 = nx.complete_graph(199)
|
||||
g200 = nx.complete_graph(200)
|
||||
|
||||
comm199 = nx.subgraph_centrality(g199)
|
||||
comm199_exp = nx.subgraph_centrality_exp(g199)
|
||||
|
||||
comm200 = nx.subgraph_centrality(g200)
|
||||
comm200_exp = nx.subgraph_centrality_exp(g200)
|
||||
|
||||
def test_communicability_betweenness_centrality_small(self):
|
||||
result = communicability_betweenness_centrality(nx.path_graph(2))
|
||||
assert result == {0: 0, 1: 0}
|
||||
|
||||
result = communicability_betweenness_centrality(nx.path_graph(1))
|
||||
assert result == {0: 0}
|
||||
|
||||
result = communicability_betweenness_centrality(nx.path_graph(0))
|
||||
assert result == {}
|
||||
|
||||
answer = {0: 0.1411224421177313, 1: 1.0, 2: 0.1411224421177313}
|
||||
result = communicability_betweenness_centrality(nx.path_graph(3))
|
||||
for k, v in result.items():
|
||||
assert answer[k] == pytest.approx(v, abs=1e-7)
|
||||
|
||||
result = communicability_betweenness_centrality(nx.complete_graph(3))
|
||||
for k, v in result.items():
|
||||
assert 0.49786143366223296 == pytest.approx(v, abs=1e-7)
|
||||
|
||||
def test_communicability_betweenness_centrality(self):
|
||||
answer = {
|
||||
0: 0.07017447951484615,
|
||||
1: 0.71565598701107991,
|
||||
2: 0.71565598701107991,
|
||||
3: 0.07017447951484615,
|
||||
}
|
||||
result = communicability_betweenness_centrality(nx.path_graph(4))
|
||||
for k, v in result.items():
|
||||
assert answer[k] == pytest.approx(v, abs=1e-7)
|
||||
|
||||
answer1 = {
|
||||
"1": 0.060039074193949521,
|
||||
"Albert": 0.315470761661372,
|
||||
"Aric": 0.31547076166137211,
|
||||
"Dan": 0.68297778678316201,
|
||||
"Franck": 0.21977926617449497,
|
||||
}
|
||||
G1 = nx.Graph(
|
||||
[
|
||||
("Franck", "Aric"),
|
||||
("Aric", "Dan"),
|
||||
("Dan", "Albert"),
|
||||
("Albert", "Franck"),
|
||||
("Dan", "1"),
|
||||
("Franck", "Albert"),
|
||||
]
|
||||
)
|
||||
result1 = communicability_betweenness_centrality(G1)
|
||||
for k, v in result1.items():
|
||||
assert answer1[k] == pytest.approx(v, abs=1e-7)
|
||||
|
||||
def test_estrada_index(self):
|
||||
answer = 1041.2470334195475
|
||||
result = estrada_index(nx.karate_club_graph())
|
||||
assert answer == pytest.approx(result, abs=1e-7)
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
"""Test trophic levels, trophic differences and trophic coherence"""
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
|
||||
np = pytest.importorskip("numpy")
|
||||
pytest.importorskip("scipy")
|
||||
|
||||
|
||||
def test_trophic_levels():
|
||||
"""Trivial example"""
|
||||
G = nx.DiGraph()
|
||||
G.add_edge("a", "b")
|
||||
G.add_edge("b", "c")
|
||||
|
||||
d = nx.trophic_levels(G)
|
||||
assert d == {"a": 1, "b": 2, "c": 3}
|
||||
|
||||
|
||||
def test_trophic_levels_levine():
|
||||
"""Example from Figure 5 in Stephen Levine (1980) J. theor. Biol. 83,
|
||||
195-207
|
||||
"""
|
||||
S = nx.DiGraph()
|
||||
S.add_edge(1, 2, weight=1.0)
|
||||
S.add_edge(1, 3, weight=0.2)
|
||||
S.add_edge(1, 4, weight=0.8)
|
||||
S.add_edge(2, 3, weight=0.2)
|
||||
S.add_edge(2, 5, weight=0.3)
|
||||
S.add_edge(4, 3, weight=0.6)
|
||||
S.add_edge(4, 5, weight=0.7)
|
||||
S.add_edge(5, 4, weight=0.2)
|
||||
|
||||
# save copy for later, test intermediate implementation details first
|
||||
S2 = S.copy()
|
||||
|
||||
# drop nodes of in-degree zero
|
||||
z = [nid for nid, d in S.in_degree if d == 0]
|
||||
for nid in z:
|
||||
S.remove_node(nid)
|
||||
|
||||
# find adjacency matrix
|
||||
q = nx.linalg.graphmatrix.adjacency_matrix(S).T
|
||||
|
||||
# fmt: off
|
||||
expected_q = np.array([
|
||||
[0, 0, 0., 0],
|
||||
[0.2, 0, 0.6, 0],
|
||||
[0, 0, 0, 0.2],
|
||||
[0.3, 0, 0.7, 0]
|
||||
])
|
||||
# fmt: on
|
||||
assert np.array_equal(q.todense(), expected_q)
|
||||
|
||||
# must be square, size of number of nodes
|
||||
assert len(q.shape) == 2
|
||||
assert q.shape[0] == q.shape[1]
|
||||
assert q.shape[0] == len(S)
|
||||
|
||||
nn = q.shape[0]
|
||||
|
||||
i = np.eye(nn)
|
||||
n = np.linalg.inv(i - q)
|
||||
y = np.asarray(n) @ np.ones(nn)
|
||||
|
||||
expected_y = np.array([1, 2.07906977, 1.46511628, 2.3255814])
|
||||
assert np.allclose(y, expected_y)
|
||||
|
||||
expected_d = {1: 1, 2: 2, 3: 3.07906977, 4: 2.46511628, 5: 3.3255814}
|
||||
|
||||
d = nx.trophic_levels(S2)
|
||||
|
||||
for nid, level in d.items():
|
||||
expected_level = expected_d[nid]
|
||||
assert expected_level == pytest.approx(level, abs=1e-7)
|
||||
|
||||
|
||||
def test_trophic_levels_simple():
|
||||
matrix_a = np.array([[0, 0], [1, 0]])
|
||||
G = nx.from_numpy_array(matrix_a, create_using=nx.DiGraph)
|
||||
d = nx.trophic_levels(G)
|
||||
assert d[0] == pytest.approx(2, abs=1e-7)
|
||||
assert d[1] == pytest.approx(1, abs=1e-7)
|
||||
|
||||
|
||||
def test_trophic_levels_more_complex():
|
||||
# fmt: off
|
||||
matrix = np.array([
|
||||
[0, 1, 0, 0],
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 0, 1],
|
||||
[0, 0, 0, 0]
|
||||
])
|
||||
# fmt: on
|
||||
G = nx.from_numpy_array(matrix, create_using=nx.DiGraph)
|
||||
d = nx.trophic_levels(G)
|
||||
expected_result = [1, 2, 3, 4]
|
||||
for ind in range(4):
|
||||
assert d[ind] == pytest.approx(expected_result[ind], abs=1e-7)
|
||||
|
||||
# fmt: off
|
||||
matrix = np.array([
|
||||
[0, 1, 1, 0],
|
||||
[0, 0, 1, 1],
|
||||
[0, 0, 0, 1],
|
||||
[0, 0, 0, 0]
|
||||
])
|
||||
# fmt: on
|
||||
G = nx.from_numpy_array(matrix, create_using=nx.DiGraph)
|
||||
d = nx.trophic_levels(G)
|
||||
|
||||
expected_result = [1, 2, 2.5, 3.25]
|
||||
for ind in range(4):
|
||||
assert d[ind] == pytest.approx(expected_result[ind], abs=1e-7)
|
||||
|
||||
|
||||
def test_trophic_levels_even_more_complex():
|
||||
# fmt: off
|
||||
# Another, bigger matrix
|
||||
matrix = np.array([
|
||||
[0, 0, 0, 0, 0],
|
||||
[0, 1, 0, 1, 0],
|
||||
[1, 0, 0, 0, 0],
|
||||
[0, 1, 0, 0, 0],
|
||||
[0, 0, 0, 1, 0]
|
||||
])
|
||||
# Generated this linear system using pen and paper:
|
||||
K = np.array([
|
||||
[1, 0, -1, 0, 0],
|
||||
[0, 0.5, 0, -0.5, 0],
|
||||
[0, 0, 1, 0, 0],
|
||||
[0, -0.5, 0, 1, -0.5],
|
||||
[0, 0, 0, 0, 1],
|
||||
])
|
||||
# fmt: on
|
||||
result_1 = np.ravel(np.linalg.inv(K) @ np.ones(5))
|
||||
G = nx.from_numpy_array(matrix, create_using=nx.DiGraph)
|
||||
result_2 = nx.trophic_levels(G)
|
||||
|
||||
for ind in range(5):
|
||||
assert result_1[ind] == pytest.approx(result_2[ind], abs=1e-7)
|
||||
|
||||
|
||||
def test_trophic_levels_singular_matrix():
|
||||
"""Should raise an error with graphs with only non-basal nodes"""
|
||||
matrix = np.identity(4)
|
||||
G = nx.from_numpy_array(matrix, create_using=nx.DiGraph)
|
||||
with pytest.raises(nx.NetworkXError, match="no basal nodes"):
|
||||
nx.trophic_levels(G)
|
||||
|
||||
|
||||
def test_trophic_levels_singular_with_basal():
|
||||
"""Should fail to compute if there are any parts of the graph which are not
|
||||
reachable from any basal node (with in-degree zero).
|
||||
"""
|
||||
G = nx.DiGraph()
|
||||
# a has in-degree zero
|
||||
G.add_edge("a", "b")
|
||||
|
||||
# b is one level above a, c and d
|
||||
G.add_edge("c", "b")
|
||||
G.add_edge("d", "b")
|
||||
|
||||
# c and d form a loop, neither are reachable from a
|
||||
G.add_edge("c", "d")
|
||||
G.add_edge("d", "c")
|
||||
|
||||
with pytest.raises(nx.NetworkXError) as e:
|
||||
nx.trophic_levels(G)
|
||||
msg = (
|
||||
"Trophic levels are only defined for graphs where every node "
|
||||
+ "has a path from a basal node (basal nodes are nodes with no "
|
||||
+ "incoming edges)."
|
||||
)
|
||||
assert msg in str(e.value)
|
||||
|
||||
# if self-loops are allowed, smaller example:
|
||||
G = nx.DiGraph()
|
||||
G.add_edge("a", "b") # a has in-degree zero
|
||||
G.add_edge("c", "b") # b is one level above a and c
|
||||
G.add_edge("c", "c") # c has a self-loop
|
||||
with pytest.raises(nx.NetworkXError) as e:
|
||||
nx.trophic_levels(G)
|
||||
msg = (
|
||||
"Trophic levels are only defined for graphs where every node "
|
||||
+ "has a path from a basal node (basal nodes are nodes with no "
|
||||
+ "incoming edges)."
|
||||
)
|
||||
assert msg in str(e.value)
|
||||
|
||||
|
||||
def test_trophic_differences():
|
||||
matrix_a = np.array([[0, 1], [0, 0]])
|
||||
G = nx.from_numpy_array(matrix_a, create_using=nx.DiGraph)
|
||||
diffs = nx.trophic_differences(G)
|
||||
assert diffs[(0, 1)] == pytest.approx(1, abs=1e-7)
|
||||
|
||||
# fmt: off
|
||||
matrix_b = np.array([
|
||||
[0, 1, 1, 0],
|
||||
[0, 0, 1, 1],
|
||||
[0, 0, 0, 1],
|
||||
[0, 0, 0, 0]
|
||||
])
|
||||
# fmt: on
|
||||
G = nx.from_numpy_array(matrix_b, create_using=nx.DiGraph)
|
||||
diffs = nx.trophic_differences(G)
|
||||
|
||||
assert diffs[(0, 1)] == pytest.approx(1, abs=1e-7)
|
||||
assert diffs[(0, 2)] == pytest.approx(1.5, abs=1e-7)
|
||||
assert diffs[(1, 2)] == pytest.approx(0.5, abs=1e-7)
|
||||
assert diffs[(1, 3)] == pytest.approx(1.25, abs=1e-7)
|
||||
assert diffs[(2, 3)] == pytest.approx(0.75, abs=1e-7)
|
||||
|
||||
|
||||
def test_trophic_incoherence_parameter_no_cannibalism():
|
||||
matrix_a = np.array([[0, 1], [0, 0]])
|
||||
G = nx.from_numpy_array(matrix_a, create_using=nx.DiGraph)
|
||||
q = nx.trophic_incoherence_parameter(G, cannibalism=False)
|
||||
assert q == pytest.approx(0, abs=1e-7)
|
||||
|
||||
# fmt: off
|
||||
matrix_b = np.array([
|
||||
[0, 1, 1, 0],
|
||||
[0, 0, 1, 1],
|
||||
[0, 0, 0, 1],
|
||||
[0, 0, 0, 0]
|
||||
])
|
||||
# fmt: on
|
||||
G = nx.from_numpy_array(matrix_b, create_using=nx.DiGraph)
|
||||
q = nx.trophic_incoherence_parameter(G, cannibalism=False)
|
||||
assert q == pytest.approx(np.std([1, 1.5, 0.5, 0.75, 1.25]), abs=1e-7)
|
||||
|
||||
# fmt: off
|
||||
matrix_c = np.array([
|
||||
[0, 1, 1, 0],
|
||||
[0, 1, 1, 1],
|
||||
[0, 0, 0, 1],
|
||||
[0, 0, 0, 1]
|
||||
])
|
||||
# fmt: on
|
||||
G = nx.from_numpy_array(matrix_c, create_using=nx.DiGraph)
|
||||
q = nx.trophic_incoherence_parameter(G, cannibalism=False)
|
||||
# Ignore the -link
|
||||
assert q == pytest.approx(np.std([1, 1.5, 0.5, 0.75, 1.25]), abs=1e-7)
|
||||
|
||||
# no self-loops case
|
||||
# fmt: off
|
||||
matrix_d = np.array([
|
||||
[0, 1, 1, 0],
|
||||
[0, 0, 1, 1],
|
||||
[0, 0, 0, 1],
|
||||
[0, 0, 0, 0]
|
||||
])
|
||||
# fmt: on
|
||||
G = nx.from_numpy_array(matrix_d, create_using=nx.DiGraph)
|
||||
q = nx.trophic_incoherence_parameter(G, cannibalism=False)
|
||||
# Ignore the -link
|
||||
assert q == pytest.approx(np.std([1, 1.5, 0.5, 0.75, 1.25]), abs=1e-7)
|
||||
|
||||
|
||||
def test_trophic_incoherence_parameter_cannibalism():
|
||||
matrix_a = np.array([[0, 1], [0, 0]])
|
||||
G = nx.from_numpy_array(matrix_a, create_using=nx.DiGraph)
|
||||
q = nx.trophic_incoherence_parameter(G, cannibalism=True)
|
||||
assert q == pytest.approx(0, abs=1e-7)
|
||||
|
||||
# fmt: off
|
||||
matrix_b = np.array([
|
||||
[0, 0, 0, 0, 0],
|
||||
[0, 1, 0, 1, 0],
|
||||
[1, 0, 0, 0, 0],
|
||||
[0, 1, 0, 0, 0],
|
||||
[0, 0, 0, 1, 0]
|
||||
])
|
||||
# fmt: on
|
||||
G = nx.from_numpy_array(matrix_b, create_using=nx.DiGraph)
|
||||
q = nx.trophic_incoherence_parameter(G, cannibalism=True)
|
||||
assert q == pytest.approx(2, abs=1e-7)
|
||||
|
||||
# fmt: off
|
||||
matrix_c = np.array([
|
||||
[0, 1, 1, 0],
|
||||
[0, 0, 1, 1],
|
||||
[0, 0, 0, 1],
|
||||
[0, 0, 0, 0]
|
||||
])
|
||||
# fmt: on
|
||||
G = nx.from_numpy_array(matrix_c, create_using=nx.DiGraph)
|
||||
q = nx.trophic_incoherence_parameter(G, cannibalism=True)
|
||||
# Ignore the -link
|
||||
assert q == pytest.approx(np.std([1, 1.5, 0.5, 0.75, 1.25]), abs=1e-7)
|
||||
|
||||
|
||||
def test_no_basal_node():
|
||||
G = nx.DiGraph([(1, 2), (2, 3), (3, 1)]) # No basal node, should raise an error
|
||||
with pytest.raises(nx.NetworkXError, match="no basal node"):
|
||||
nx.trophic_levels(G)
|
||||
G.add_node(4) # add basal node, but not connected
|
||||
with pytest.raises(nx.NetworkXError, match="every node .* path from a basal node"):
|
||||
nx.trophic_levels(G)
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Unit tests for VoteRank.
|
||||
"""
|
||||
|
||||
import networkx as nx
|
||||
|
||||
|
||||
class TestVoteRankCentrality:
|
||||
# Example Graph present in reference paper
|
||||
def test_voterank_centrality_1(self):
|
||||
G = nx.Graph()
|
||||
G.add_edges_from(
|
||||
[
|
||||
(7, 8),
|
||||
(7, 5),
|
||||
(7, 9),
|
||||
(5, 0),
|
||||
(0, 1),
|
||||
(0, 2),
|
||||
(0, 3),
|
||||
(0, 4),
|
||||
(1, 6),
|
||||
(2, 6),
|
||||
(3, 6),
|
||||
(4, 6),
|
||||
]
|
||||
)
|
||||
assert [0, 7, 6] == nx.voterank(G)
|
||||
|
||||
def test_voterank_emptygraph(self):
|
||||
G = nx.Graph()
|
||||
assert [] == nx.voterank(G)
|
||||
|
||||
# Graph unit test
|
||||
def test_voterank_centrality_2(self):
|
||||
G = nx.florentine_families_graph()
|
||||
d = nx.voterank(G, 4)
|
||||
exact = ["Medici", "Strozzi", "Guadagni", "Castellani"]
|
||||
assert exact == d
|
||||
|
||||
# DiGraph unit test
|
||||
def test_voterank_centrality_3(self):
|
||||
G = nx.gnc_graph(10, seed=7)
|
||||
d = nx.voterank(G, 4)
|
||||
exact = [3, 6, 8]
|
||||
assert exact == d
|
||||
|
||||
# MultiGraph unit test
|
||||
def test_voterank_centrality_4(self):
|
||||
G = nx.MultiGraph()
|
||||
G.add_edges_from(
|
||||
[(0, 1), (0, 1), (1, 2), (2, 5), (2, 5), (5, 6), (5, 6), (2, 4), (4, 3)]
|
||||
)
|
||||
exact = [2, 1, 5, 4]
|
||||
assert exact == nx.voterank(G)
|
||||
|
||||
# MultiDiGraph unit test
|
||||
def test_voterank_centrality_5(self):
|
||||
G = nx.MultiDiGraph()
|
||||
G.add_edges_from(
|
||||
[(0, 1), (0, 1), (1, 2), (2, 5), (2, 5), (5, 6), (5, 6), (2, 4), (4, 3)]
|
||||
)
|
||||
exact = [2, 0, 5, 4]
|
||||
assert exact == nx.voterank(G)
|
||||
@@ -0,0 +1,181 @@
|
||||
"""Trophic levels"""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import not_implemented_for
|
||||
|
||||
__all__ = ["trophic_levels", "trophic_differences", "trophic_incoherence_parameter"]
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def trophic_levels(G, weight="weight"):
|
||||
r"""Compute the trophic levels of nodes.
|
||||
|
||||
The trophic level of a node $i$ is
|
||||
|
||||
.. math::
|
||||
|
||||
s_i = 1 + \frac{1}{k^{in}_i} \sum_{j} a_{ij} s_j
|
||||
|
||||
where $k^{in}_i$ is the in-degree of i
|
||||
|
||||
.. math::
|
||||
|
||||
k^{in}_i = \sum_{j} a_{ij}
|
||||
|
||||
and nodes with $k^{in}_i = 0$ have $s_i = 1$ by convention.
|
||||
|
||||
These are calculated using the method outlined in Levine [1]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : DiGraph
|
||||
A directed networkx graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dict
|
||||
Dictionary of nodes with trophic level as the value.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Stephen Levine (1980) J. theor. Biol. 83, 195-207
|
||||
"""
|
||||
|
||||
basal_nodes = [n for n, deg in G.in_degree if deg == 0]
|
||||
if not basal_nodes:
|
||||
raise nx.NetworkXError(
|
||||
"This graph has no basal nodes (nodes with no incoming edges)."
|
||||
"Trophic levels are not defined without at least one basal node."
|
||||
)
|
||||
|
||||
reachable_nodes = {
|
||||
node for layer in nx.bfs_layers(G, sources=basal_nodes) for node in layer
|
||||
}
|
||||
|
||||
if len(reachable_nodes) != len(G.nodes):
|
||||
raise nx.NetworkXError(
|
||||
"Trophic levels are only defined for graphs where every node has a path "
|
||||
"from a basal node (basal nodes are nodes with no incoming edges)."
|
||||
)
|
||||
|
||||
import numpy as np
|
||||
|
||||
# find adjacency matrix
|
||||
a = nx.adjacency_matrix(G, weight=weight).T.toarray()
|
||||
|
||||
# drop rows/columns where in-degree is zero
|
||||
rowsum = np.sum(a, axis=1)
|
||||
p = a[rowsum != 0][:, rowsum != 0]
|
||||
# normalise so sum of in-degree weights is 1 along each row
|
||||
p = p / rowsum[rowsum != 0][:, np.newaxis]
|
||||
|
||||
# calculate trophic levels
|
||||
nn = p.shape[0]
|
||||
i = np.eye(nn)
|
||||
try:
|
||||
n = np.linalg.inv(i - p)
|
||||
except np.linalg.LinAlgError as err:
|
||||
# LinAlgError is raised when there is a non-basal node
|
||||
msg = (
|
||||
"Trophic levels are only defined for graphs where every "
|
||||
+ "node has a path from a basal node (basal nodes are nodes "
|
||||
+ "with no incoming edges)."
|
||||
)
|
||||
raise nx.NetworkXError(msg) from err
|
||||
y = n.sum(axis=1) + 1
|
||||
|
||||
levels = {}
|
||||
|
||||
# all nodes with in-degree zero have trophic level == 1
|
||||
zero_node_ids = (node_id for node_id, degree in G.in_degree if degree == 0)
|
||||
for node_id in zero_node_ids:
|
||||
levels[node_id] = 1
|
||||
|
||||
# all other nodes have levels as calculated
|
||||
nonzero_node_ids = (node_id for node_id, degree in G.in_degree if degree != 0)
|
||||
for i, node_id in enumerate(nonzero_node_ids):
|
||||
levels[node_id] = y.item(i)
|
||||
|
||||
return levels
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def trophic_differences(G, weight="weight"):
|
||||
r"""Compute the trophic differences of the edges of a directed graph.
|
||||
|
||||
The trophic difference $x_ij$ for each edge is defined in Johnson et al.
|
||||
[1]_ as:
|
||||
|
||||
.. math::
|
||||
x_ij = s_j - s_i
|
||||
|
||||
Where $s_i$ is the trophic level of node $i$.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : DiGraph
|
||||
A directed networkx graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
diffs : dict
|
||||
Dictionary of edges with trophic differences as the value.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Samuel Johnson, Virginia Dominguez-Garcia, Luca Donetti, Miguel A.
|
||||
Munoz (2014) PNAS "Trophic coherence determines food-web stability"
|
||||
"""
|
||||
levels = trophic_levels(G, weight=weight)
|
||||
diffs = {}
|
||||
for u, v in G.edges:
|
||||
diffs[(u, v)] = levels[v] - levels[u]
|
||||
return diffs
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def trophic_incoherence_parameter(G, weight="weight", cannibalism=False):
|
||||
r"""Compute the trophic incoherence parameter of a graph.
|
||||
|
||||
Trophic coherence is defined as the homogeneity of the distribution of
|
||||
trophic distances: the more similar, the more coherent. This is measured by
|
||||
the standard deviation of the trophic differences and referred to as the
|
||||
trophic incoherence parameter $q$ by [1].
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : DiGraph
|
||||
A directed networkx graph
|
||||
|
||||
cannibalism: Boolean
|
||||
If set to False, self edges are not considered in the calculation
|
||||
|
||||
Returns
|
||||
-------
|
||||
trophic_incoherence_parameter : float
|
||||
The trophic coherence of a graph
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Samuel Johnson, Virginia Dominguez-Garcia, Luca Donetti, Miguel A.
|
||||
Munoz (2014) PNAS "Trophic coherence determines food-web stability"
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
if cannibalism:
|
||||
diffs = trophic_differences(G, weight=weight)
|
||||
else:
|
||||
# If no cannibalism, remove self-edges
|
||||
self_loops = list(nx.selfloop_edges(G))
|
||||
if self_loops:
|
||||
# Make a copy so we do not change G's edges in memory
|
||||
G_2 = G.copy()
|
||||
G_2.remove_edges_from(self_loops)
|
||||
else:
|
||||
# Avoid copy otherwise
|
||||
G_2 = G
|
||||
diffs = trophic_differences(G_2, weight=weight)
|
||||
return float(np.std(list(diffs.values())))
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
"""Algorithm to select influential nodes in a graph using VoteRank."""
|
||||
|
||||
import networkx as nx
|
||||
|
||||
__all__ = ["voterank"]
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def voterank(G, number_of_nodes=None):
|
||||
"""Select a list of influential nodes in a graph using VoteRank algorithm
|
||||
|
||||
VoteRank [1]_ computes a ranking of the nodes in a graph G based on a
|
||||
voting scheme. With VoteRank, all nodes vote for each of its in-neighbors
|
||||
and the node with the highest votes is elected iteratively. The voting
|
||||
ability of out-neighbors of elected nodes is decreased in subsequent turns.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph.
|
||||
|
||||
number_of_nodes : integer, optional
|
||||
Number of ranked nodes to extract (default all nodes).
|
||||
|
||||
Returns
|
||||
-------
|
||||
voterank : list
|
||||
Ordered list of computed seeds.
|
||||
Only nodes with positive number of votes are returned.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.Graph([(0, 1), (0, 2), (0, 3), (1, 4)])
|
||||
>>> nx.voterank(G)
|
||||
[0, 1]
|
||||
|
||||
The algorithm can be used both for undirected and directed graphs.
|
||||
However, the directed version is different in two ways:
|
||||
(i) nodes only vote for their in-neighbors and
|
||||
(ii) only the voting ability of elected node and its out-neighbors are updated:
|
||||
|
||||
>>> G = nx.DiGraph([(0, 1), (2, 1), (2, 3), (3, 4)])
|
||||
>>> nx.voterank(G)
|
||||
[2, 3]
|
||||
|
||||
Notes
|
||||
-----
|
||||
Each edge is treated independently in case of multigraphs.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Zhang, J.-X. et al. (2016).
|
||||
Identifying a set of influential spreaders in complex networks.
|
||||
Sci. Rep. 6, 27823; doi: 10.1038/srep27823.
|
||||
"""
|
||||
influential_nodes = []
|
||||
vote_rank = {}
|
||||
if len(G) == 0:
|
||||
return influential_nodes
|
||||
if number_of_nodes is None or number_of_nodes > len(G):
|
||||
number_of_nodes = len(G)
|
||||
if G.is_directed():
|
||||
# For directed graphs compute average out-degree
|
||||
avgDegree = sum(deg for _, deg in G.out_degree()) / len(G)
|
||||
else:
|
||||
# For undirected graphs compute average degree
|
||||
avgDegree = sum(deg for _, deg in G.degree()) / len(G)
|
||||
# step 1 - initiate all nodes to (0,1) (score, voting ability)
|
||||
for n in G.nodes():
|
||||
vote_rank[n] = [0, 1]
|
||||
# Repeat steps 1b to 4 until num_seeds are elected.
|
||||
for _ in range(number_of_nodes):
|
||||
# step 1b - reset rank
|
||||
for n in G.nodes():
|
||||
vote_rank[n][0] = 0
|
||||
# step 2 - vote
|
||||
for n, nbr in G.edges():
|
||||
# In directed graphs nodes only vote for their in-neighbors
|
||||
vote_rank[n][0] += vote_rank[nbr][1]
|
||||
if not G.is_directed():
|
||||
vote_rank[nbr][0] += vote_rank[n][1]
|
||||
for n in influential_nodes:
|
||||
vote_rank[n][0] = 0
|
||||
# step 3 - select top node
|
||||
n = max(G.nodes, key=lambda x: vote_rank[x][0])
|
||||
if vote_rank[n][0] == 0:
|
||||
return influential_nodes
|
||||
influential_nodes.append(n)
|
||||
# weaken the selected node
|
||||
vote_rank[n] = [0, 0]
|
||||
# step 4 - update voterank properties
|
||||
for _, nbr in G.edges(n):
|
||||
vote_rank[nbr][1] -= 1 / avgDegree
|
||||
vote_rank[nbr][1] = max(vote_rank[nbr][1], 0)
|
||||
return influential_nodes
|
||||
Reference in New Issue
Block a user