chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
from .betweenness import *
|
||||
from .closeness import *
|
||||
from .degree import *
|
||||
from .ego_betweenness import *
|
||||
from .flowbetweenness import *
|
||||
from .laplacian import *
|
||||
from .pagerank import *
|
||||
from .katz_centrality import *
|
||||
from .eigenvector import *
|
||||
@@ -0,0 +1,245 @@
|
||||
from easygraph.utils import *
|
||||
from easygraph.utils.decorators import *
|
||||
|
||||
|
||||
__all__ = [
|
||||
"betweenness_centrality",
|
||||
]
|
||||
|
||||
|
||||
def betweenness_centrality_parallel(nodes, G, path_length, accumulate):
|
||||
betweenness = {node: 0.0 for node in G}
|
||||
for node in nodes:
|
||||
S, P, sigma = path_length(G, source=node)
|
||||
betweenness = accumulate(betweenness, S, P, sigma, node)
|
||||
return betweenness
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@hybrid("cpp_betweenness_centrality")
|
||||
def betweenness_centrality(
|
||||
G, weight=None, sources=None, normalized=True, endpoints=False, n_workers=None
|
||||
):
|
||||
r"""Compute the shortest-basic betweenness centrality for nodes.
|
||||
|
||||
.. math::
|
||||
|
||||
c_B(v) = \sum_{s,t \in V} \frac{\sigma(s, t|v)}{\sigma(s, t)}
|
||||
|
||||
where V is the set of nodes,
|
||||
|
||||
.. math::
|
||||
\sigma(s, t)
|
||||
|
||||
is the number of shortest (s, t)-paths, and
|
||||
|
||||
.. math::
|
||||
|
||||
\sigma(s, t|v)
|
||||
|
||||
is the number of those paths passing through some node v other than s, t.
|
||||
|
||||
.. math::
|
||||
|
||||
If\ s\ =\ t,\ \sigma(s, t) = 1, and\ if\ v \in {s, t}, \sigma(s, t|v) = 0 [2]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph 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.
|
||||
|
||||
sources : None or nodes list, optional (default=None)
|
||||
If None, all nodes are considered.
|
||||
Otherwise,the set of source vertices to consider when calculating shortest paths.
|
||||
|
||||
normalized : bool, optional
|
||||
If True the betweenness values are normalized by `2/((n-1)(n-2))`
|
||||
for graphs, and `1/((n-1)(n-2))` for directed graphs where `n`
|
||||
is the number of nodes in G.
|
||||
|
||||
endpoints : bool, optional
|
||||
If True include the endpoints in the shortest basic counts.
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with betweenness centrality as the value.
|
||||
|
||||
>>> betweenness_centrality(G,weight="weight")
|
||||
"""
|
||||
|
||||
import functools
|
||||
|
||||
if weight is not None:
|
||||
path_length = functools.partial(_single_source_dijkstra_path, weight=weight)
|
||||
else:
|
||||
path_length = functools.partial(_single_source_bfs_path)
|
||||
|
||||
if endpoints:
|
||||
accumulate = functools.partial(_accumulate_endpoints)
|
||||
else:
|
||||
accumulate = functools.partial(_accumulate_basic)
|
||||
|
||||
if sources is not None:
|
||||
nodes = sources
|
||||
else:
|
||||
nodes = G.nodes
|
||||
betweenness = dict.fromkeys(G, 0.0)
|
||||
|
||||
if n_workers is not None:
|
||||
# use the parallel version for large graph
|
||||
import random
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
|
||||
nodes = list(nodes)
|
||||
random.shuffle(nodes)
|
||||
|
||||
if len(nodes) > n_workers * 30000:
|
||||
nodes = split_len(nodes, step=30000)
|
||||
else:
|
||||
nodes = split(nodes, n_workers)
|
||||
local_function = partial(
|
||||
betweenness_centrality_parallel,
|
||||
G=G,
|
||||
path_length=path_length,
|
||||
accumulate=accumulate,
|
||||
)
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(local_function, nodes)
|
||||
for res in ret:
|
||||
for key in res:
|
||||
betweenness[key] += res[key]
|
||||
else:
|
||||
# use np-parallel version for small graph
|
||||
for node in nodes:
|
||||
S, P, sigma = path_length(G, source=node)
|
||||
betweenness = accumulate(betweenness, S, P, sigma, node)
|
||||
|
||||
betweenness = _rescale(
|
||||
betweenness,
|
||||
len(G),
|
||||
normalized=normalized,
|
||||
directed=G.is_directed(),
|
||||
endpoints=endpoints,
|
||||
)
|
||||
ret = [0.0 for i in range(len(G))]
|
||||
for i in range(len(ret)):
|
||||
ret[i] = betweenness[G.index2node[i]]
|
||||
return ret
|
||||
|
||||
|
||||
def _rescale(betweenness, n, normalized, directed=False, endpoints=False):
|
||||
if normalized:
|
||||
if endpoints:
|
||||
if n < 2:
|
||||
scale = None # no normalization
|
||||
else:
|
||||
# Scale factor should include endpoint nodes
|
||||
scale = 1 / (n * (n - 1))
|
||||
elif n <= 2:
|
||||
scale = None # no normalization b=0 for all nodes
|
||||
else:
|
||||
scale = 1 / ((n - 1) * (n - 2))
|
||||
else: # rescale by 2 for undirected graphs
|
||||
if not directed:
|
||||
scale = 0.5
|
||||
else:
|
||||
scale = None
|
||||
if scale is not None:
|
||||
for v in betweenness:
|
||||
betweenness[v] *= scale
|
||||
return betweenness
|
||||
|
||||
|
||||
def _single_source_bfs_path(G, source):
|
||||
S = []
|
||||
P = {v: [] for v in G}
|
||||
sigma = dict.fromkeys(G, 0.0)
|
||||
D = {}
|
||||
sigma[source] = 1.0
|
||||
D[source] = 0
|
||||
Q = [source]
|
||||
adj = G.adj
|
||||
while Q:
|
||||
v = Q.pop(0)
|
||||
S.append(v)
|
||||
Dv = D[v]
|
||||
sigmav = sigma[v]
|
||||
for w in adj[v]:
|
||||
if w not in D:
|
||||
Q.append(w)
|
||||
D[w] = Dv + 1
|
||||
if D[w] == Dv + 1:
|
||||
sigma[w] += sigmav
|
||||
P[w].append(v)
|
||||
return S, P, sigma
|
||||
|
||||
|
||||
def _single_source_dijkstra_path(G, source, weight="weight"):
|
||||
from heapq import heappop
|
||||
from heapq import heappush
|
||||
|
||||
push = heappush
|
||||
pop = heappop
|
||||
S = []
|
||||
P = {v: [] for v in G}
|
||||
sigma = dict.fromkeys(G, 0.0)
|
||||
D = {}
|
||||
sigma[source] = 1.0
|
||||
seen = {source: 0}
|
||||
Q = []
|
||||
from itertools import count
|
||||
|
||||
c = count()
|
||||
adj = G.adj
|
||||
push(Q, (0, next(c), source, source))
|
||||
while Q:
|
||||
(dist, _, pred, v) = pop(Q)
|
||||
if v in D:
|
||||
continue
|
||||
sigma[v] += sigma[pred]
|
||||
S.append(v)
|
||||
D[v] = dist
|
||||
for w in adj[v]:
|
||||
vw_dist = dist + adj[v][w].get(weight, 1)
|
||||
if w not in D and (w not in seen or vw_dist < seen[w]):
|
||||
seen[w] = vw_dist
|
||||
push(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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
@@ -0,0 +1,105 @@
|
||||
from easygraph.functions.basic import *
|
||||
from easygraph.functions.path import single_source_bfs
|
||||
from easygraph.functions.path import single_source_dijkstra
|
||||
from easygraph.utils import *
|
||||
|
||||
|
||||
__all__ = [
|
||||
"closeness_centrality",
|
||||
]
|
||||
|
||||
|
||||
def closeness_centrality_parallel(nodes, G, path_length):
|
||||
ret = []
|
||||
length = len(G)
|
||||
for node in nodes:
|
||||
x = path_length(G, node)
|
||||
dist = sum(x.values())
|
||||
cnt = len(x)
|
||||
if dist == 0:
|
||||
ret.append([node, 0])
|
||||
else:
|
||||
ret.append([node, (cnt - 1) * (cnt - 1) / (dist * (length - 1))])
|
||||
return ret
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@hybrid("cpp_closeness_centrality")
|
||||
def closeness_centrality(G, weight=None, sources=None, n_workers=None):
|
||||
r"""
|
||||
Compute closeness centrality for nodes.
|
||||
|
||||
.. math::
|
||||
|
||||
C_{WF}(u) = \frac{n-1}{N-1} \frac{n - 1}{\sum_{v=1}^{n-1} d(v, u)},
|
||||
|
||||
Notice that the closeness distance function computes the
|
||||
outcoming distance to `u` for directed graphs. To use
|
||||
incoming distance, act on `G.reverse()`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph 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.
|
||||
|
||||
sources : None or nodes list, optional (default=None)
|
||||
If None, all nodes are returned
|
||||
Otherwise,the set of source vertices to creturn.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with closeness centrality as the value.
|
||||
"""
|
||||
closeness = dict()
|
||||
if sources is not None:
|
||||
nodes = sources
|
||||
else:
|
||||
nodes = G.nodes
|
||||
length = len(G)
|
||||
import functools
|
||||
|
||||
if weight is not None:
|
||||
path_length = functools.partial(single_source_dijkstra, weight=weight)
|
||||
else:
|
||||
path_length = functools.partial(single_source_bfs)
|
||||
|
||||
if n_workers is not None:
|
||||
# use parallel version for large graph
|
||||
import random
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
|
||||
nodes = list(nodes)
|
||||
random.shuffle(nodes)
|
||||
|
||||
if len(nodes) > n_workers * 30000:
|
||||
nodes = split_len(nodes, step=30000)
|
||||
else:
|
||||
nodes = split(nodes, n_workers)
|
||||
local_function = partial(
|
||||
closeness_centrality_parallel, G=G, path_length=path_length
|
||||
)
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(local_function, nodes)
|
||||
res = [x for i in ret for x in i]
|
||||
closeness = dict(res)
|
||||
else:
|
||||
# use np-parallel version for small graph
|
||||
for node in nodes:
|
||||
x = path_length(G, node)
|
||||
dist = sum(x.values())
|
||||
cnt = len(x)
|
||||
if dist == 0:
|
||||
closeness[node] = 0
|
||||
else:
|
||||
closeness[node] = (cnt - 1) * (cnt - 1) / (dist * (length - 1))
|
||||
ret = [0.0 for i in range(len(G))]
|
||||
for i in range(len(ret)):
|
||||
ret[i] = closeness[G.index2node[i]]
|
||||
return ret
|
||||
@@ -0,0 +1,125 @@
|
||||
from easygraph.utils.decorators import *
|
||||
|
||||
|
||||
__all__ = ["degree_centrality", "in_degree_centrality", "out_degree_centrality"]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@hybrid("cpp_degree_centrality")
|
||||
def degree_centrality(G):
|
||||
"""Compute the degree centrality for nodes in a bipartite network.
|
||||
|
||||
The degree centrality for a node v is the fraction of nodes it
|
||||
is connected to.
|
||||
|
||||
parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with degree centrality as the value.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The degree centrality are normalized by dividing by n-1 where
|
||||
n is number of nodes in G.
|
||||
"""
|
||||
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()).items()}
|
||||
return centrality
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@only_implemented_for_Directed_graph
|
||||
@hybrid("cpp_in_degree_centrality")
|
||||
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 EasyGraph graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with in-degree centrality as values.
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphNotImplemented:
|
||||
If G is undirected.
|
||||
|
||||
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().items()}
|
||||
return centrality
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@only_implemented_for_Directed_graph
|
||||
@hybrid("cpp_out_degree_centrality")
|
||||
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 EasyGraph graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with out-degree centrality as values.
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphNotImplemented:
|
||||
If G is undirected.
|
||||
|
||||
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().items()}
|
||||
return centrality
|
||||
@@ -0,0 +1,57 @@
|
||||
__all__ = ["ego_betweenness"]
|
||||
import numpy as np
|
||||
|
||||
from easygraph.utils import *
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def ego_betweenness(G, node):
|
||||
"""
|
||||
ego networks are networks consisting of a single actor (ego) together with the actors they are connected to (alters) and all the links among those alters.[1]
|
||||
Burt (1992), in his book Structural Holes, provides ample evidence that having high betweenness centrality, which is highly correlated with having many structural holes, can bring benefits to ego.[1]
|
||||
Returns the betweenness centrality of a ego network whose ego is set
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
node : int
|
||||
|
||||
Returns
|
||||
-------
|
||||
sum : float
|
||||
the betweenness centrality of a ego network whose ego is set
|
||||
|
||||
Examples
|
||||
--------
|
||||
Returns the betwenness centrality of node 1.
|
||||
|
||||
>>> ego_betweenness(G,node=1)
|
||||
|
||||
Reference
|
||||
---------
|
||||
.. [1] Martin Everett, Stephen P. Borgatti. "Ego network betweenness." Social Networks, Volume 27, Issue 1, Pages 31-38, 2005.
|
||||
|
||||
"""
|
||||
g = G.ego_subgraph(node)
|
||||
print(g.edges)
|
||||
print(g.nodes)
|
||||
n = len(g)
|
||||
|
||||
A = np.zeros((n, n))
|
||||
|
||||
for i in range(n):
|
||||
for j in range(n):
|
||||
if g.has_edge(g.index2node[i], g.index2node[j]):
|
||||
A[i, j] = 1
|
||||
|
||||
B = A * A
|
||||
C = np.identity(n) - A
|
||||
sum = 0
|
||||
flag = G.is_directed()
|
||||
for i in range(n):
|
||||
for j in range(n):
|
||||
if i != j and C[i, j] == 1 and B[i, j] != 0:
|
||||
sum += 1.0 / B[i, j]
|
||||
if flag == False:
|
||||
sum /= 2
|
||||
return sum
|
||||
@@ -0,0 +1,154 @@
|
||||
import math
|
||||
import easygraph as eg
|
||||
from easygraph.utils import *
|
||||
from easygraph.utils.decorators import *
|
||||
from scipy import sparse
|
||||
from scipy.sparse import linalg
|
||||
import numpy as np
|
||||
from collections import defaultdict
|
||||
|
||||
__all__ = ["eigenvector_centrality"]
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@hybrid("cpp_eigenvector_centrality")
|
||||
def eigenvector_centrality(G, max_iter=100, tol=1.0e-6, nstart=None, weight=None):
|
||||
"""Calculate eigenvector centrality for nodes in the graph
|
||||
|
||||
Eigenvector centrality is based on the idea that a node's importance
|
||||
depends on the importance of its neighboring nodes.
|
||||
Specifically, a node's centrality is proportional to the sum of
|
||||
centrality values of its neighbors.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph object
|
||||
An undirected or directed graph
|
||||
|
||||
max_iter : int, optional (default=100)
|
||||
Maximum number of iterations for the power method
|
||||
|
||||
tol : float, optional (default=1.0e-6)
|
||||
Convergence threshold; algorithm terminates when the difference
|
||||
between centrality values in consecutive iterations is less than this value
|
||||
|
||||
nstart : dictionary, optional (default=None)
|
||||
Dictionary mapping nodes to initial centrality values
|
||||
If None, the ARPACK solver is used to directly compute the eigenvector
|
||||
|
||||
weight : string or None, optional (default=None)
|
||||
Name of the edge attribute to be used as edge weight
|
||||
If None, all edges are considered to have weight 1
|
||||
|
||||
Returns
|
||||
-------
|
||||
centrality : dictionary
|
||||
Dictionary mapping nodes to their eigenvector centrality values
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphPointlessConcept
|
||||
When input is an empty graph
|
||||
|
||||
EasyGraphError
|
||||
When the algorithm fails to converge within the specified maximum iterations
|
||||
|
||||
Notes
|
||||
-----
|
||||
This algorithm uses the power iteration method to find the principal eigenvector.
|
||||
When nstart is not provided, the ARPACK solver is used for efficiency.
|
||||
The returned centrality values are normalized.
|
||||
"""
|
||||
|
||||
if len(G) == 0:
|
||||
raise eg.EasyGraphPointlessConcept(
|
||||
"cannot compute centrality for the null graph"
|
||||
)
|
||||
|
||||
if len(G) == 1:
|
||||
raise eg.EasyGraphPointlessConcept(
|
||||
"cannot compute eigenvector centrality for a single node graph"
|
||||
)
|
||||
|
||||
|
||||
# Build node list and mapping
|
||||
nodelist = list(G.nodes)
|
||||
n = len(nodelist)
|
||||
node_map = {node: i for i, node in enumerate(nodelist)}
|
||||
|
||||
# Build weighted adjacency matrix
|
||||
row, col, data = [], [], []
|
||||
for u in nodelist:
|
||||
u_idx = node_map[u]
|
||||
for v, attrs in G[u].items():
|
||||
if v in node_map:
|
||||
v_idx = node_map[v]
|
||||
w = attrs.get(weight, 1.0) if weight else 1.0
|
||||
# Build transpose matrix for centrality calculation
|
||||
row.append(v_idx)
|
||||
col.append(u_idx)
|
||||
data.append(float(w))
|
||||
|
||||
# Create CSR format sparse matrix
|
||||
A = sparse.csr_matrix((data, (row, col)), shape=(n, n))
|
||||
|
||||
# Detect and handle isolated nodes
|
||||
row_sums = np.array(A.sum(axis=1)).flatten()
|
||||
col_sums = np.array(A.sum(axis=0)).flatten()
|
||||
isolated_nodes = np.where((row_sums == 0) & (col_sums == 0))[0]
|
||||
|
||||
has_isolated = len(isolated_nodes) > 0
|
||||
isolated_indices = []
|
||||
|
||||
# Add small self-loops to isolated nodes for stability
|
||||
if has_isolated:
|
||||
# Store isolated node indices
|
||||
isolated_indices = isolated_nodes.tolist()
|
||||
|
||||
# Add small self-loop weights to isolated nodes
|
||||
for idx in isolated_indices:
|
||||
A[idx, idx] = 1.0e-4 # Small enough to not affect results, but maintains numerical stability
|
||||
if nstart is not None:
|
||||
# Use custom initial vector for power iteration
|
||||
v = np.array([nstart.get(n, 1.0) for n in nodelist], dtype=float)
|
||||
v = v / np.sum(np.abs(v))
|
||||
|
||||
# Power iteration method to compute principal eigenvector
|
||||
v_last = np.zeros_like(v)
|
||||
for _ in range(max_iter):
|
||||
np.copyto(v_last, v)
|
||||
v = A @ v_last # Sparse matrix multiplication
|
||||
|
||||
norm = np.linalg.norm(v)
|
||||
if norm < 1e-10:
|
||||
v = v_last.copy()
|
||||
break
|
||||
v = v / norm # Normalization
|
||||
|
||||
# Check convergence
|
||||
if np.linalg.norm(v - v_last) < tol:
|
||||
break
|
||||
else:
|
||||
raise eg.EasyGraphError(f"Eigenvector calculation did not converge in {max_iter} iterations")
|
||||
|
||||
centrality = v
|
||||
else:
|
||||
# Use ARPACK solver to directly compute the principal eigenvector
|
||||
eigenvalues, eigenvectors = linalg.eigs(A, k=1, which='LR',
|
||||
maxiter=max_iter, tol=tol)
|
||||
centrality = np.real(eigenvectors[:,0])
|
||||
|
||||
# Ensure positive results and normalize
|
||||
if centrality.sum() < 0:
|
||||
centrality = -centrality
|
||||
|
||||
centrality = centrality / np.linalg.norm(centrality)
|
||||
# Set centrality of isolated nodes to zero
|
||||
if has_isolated:
|
||||
for idx in isolated_indices:
|
||||
centrality[idx] = 0.0
|
||||
# Renormalize if needed
|
||||
if np.sum(centrality) > 0:
|
||||
centrality = centrality / np.linalg.norm(centrality)
|
||||
|
||||
# Return dictionary of node centrality values
|
||||
return {nodelist[i]: float(centrality[i]) for i in range(n)}
|
||||
@@ -0,0 +1,146 @@
|
||||
import collections
|
||||
import copy
|
||||
|
||||
from easygraph.utils.decorators import *
|
||||
|
||||
|
||||
__all__ = [
|
||||
"flowbetweenness_centrality",
|
||||
]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def flowbetweenness_centrality(G):
|
||||
"""Compute the independent-basic betweenness centrality for nodes in a flow network.
|
||||
|
||||
.. math::
|
||||
|
||||
c_B(v) =\\sum_{s,t \\in V} \frac{\\sigma(s, t|v)}{\\sigma(s, t)}
|
||||
|
||||
where V is the set of nodes,
|
||||
|
||||
.. math::
|
||||
|
||||
\\sigma(s, t)\\ is\\ the\\ number\\ of\\ independent\\ (s, t)-paths,
|
||||
|
||||
.. math::
|
||||
|
||||
\\sigma(s, t|v)\\ is\\ the\\ maximum\\ number\\ possible\\ of\\ those\\ paths\\ passing\\ through\\ some\\ node\\ v\\ other\\ than\\ s, t.\
|
||||
|
||||
.. math::
|
||||
|
||||
If\\ s\\ =\\ t,\\ \\sigma(s, t)\\ =\\ 1,\\ and\\ if\\ v \\in \\{s, t\\},\\ \\sigma(s, t|v)\\ =\\ 0\\ [2]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph directed graph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
nodes : dictionary
|
||||
Dictionary of nodes with independent-basic betweenness centrality as the value.
|
||||
|
||||
Notes
|
||||
-----
|
||||
A flow network is a directed graph where each edge has a capacity and each edge receives a flow.
|
||||
"""
|
||||
if G.is_directed() == False:
|
||||
print("Please input a directed graph")
|
||||
return
|
||||
flow_dict = NumberOfFlow(G)
|
||||
nodes = G.nodes
|
||||
result_dict = dict()
|
||||
for node, _ in nodes.items():
|
||||
result_dict[node] = 0
|
||||
for node_v, _ in nodes.items():
|
||||
for node_s, _ in nodes.items():
|
||||
for node_t, _ in nodes.items():
|
||||
num = 1
|
||||
num_v = 0
|
||||
if node_s == node_t:
|
||||
num_v = 0
|
||||
num = 1
|
||||
if node_v in [node_s, node_t]:
|
||||
num_v = 0
|
||||
num = 1
|
||||
if node_v != node_s and node_v != node_t and node_s != node_t:
|
||||
num = flow_dict[node_s][node_t]
|
||||
num_v = min(flow_dict[node_s][node_v], flow_dict[node_v][node_t])
|
||||
if num == 0:
|
||||
pass
|
||||
else:
|
||||
result_dict[node_v] = result_dict[node_v] + num_v / num
|
||||
return result_dict
|
||||
|
||||
|
||||
# flow betweenness
|
||||
def NumberOfFlow(G):
|
||||
nodes = G.nodes
|
||||
result_dict = dict()
|
||||
for node1, _ in nodes.items():
|
||||
result_dict[node1] = dict()
|
||||
for node2, _ in nodes.items():
|
||||
if node1 == node2:
|
||||
pass
|
||||
else:
|
||||
result_dict[node1][node2] = edmonds_karp(G, node1, node2)
|
||||
return result_dict
|
||||
|
||||
|
||||
def edmonds_karp(G, source, sink):
|
||||
nodes = G.nodes
|
||||
parent = dict()
|
||||
for node, _ in nodes.items():
|
||||
parent[node] = -1
|
||||
|
||||
adj = copy.deepcopy(G.adj)
|
||||
max_flow = 0
|
||||
while bfs(G, source, sink, parent, adj):
|
||||
path_flow = float("inf")
|
||||
s = sink
|
||||
while s != source:
|
||||
path_flow = min(path_flow, adj[parent[s]][s].get("weight", 1))
|
||||
s = parent[s]
|
||||
max_flow += path_flow
|
||||
v = sink
|
||||
while v != source:
|
||||
u = parent[v]
|
||||
x = adj[u][v].get("weight", 1)
|
||||
adj[u][v].update({"weight": x})
|
||||
adj[u][v]["weight"] -= path_flow
|
||||
|
||||
flag = 0
|
||||
if v not in adj:
|
||||
adj[v] = dict()
|
||||
if u not in adj[v]:
|
||||
adj[v][u] = dict()
|
||||
flag = 1
|
||||
if flag == 1:
|
||||
x = 0
|
||||
else:
|
||||
x = adj[v][u].get("weight", 1)
|
||||
adj[v][u].update({"weight": x})
|
||||
adj[v][u]["weight"] += path_flow
|
||||
v = parent[v]
|
||||
return max_flow
|
||||
|
||||
|
||||
def bfs(G, source, sink, parent, adj):
|
||||
nodes = G.nodes
|
||||
visited = dict()
|
||||
for node, _ in nodes.items():
|
||||
visited[node] = 0
|
||||
queue = collections.deque()
|
||||
queue.append(source)
|
||||
visited[source] = True
|
||||
while queue:
|
||||
u = queue.popleft()
|
||||
if u not in adj:
|
||||
continue
|
||||
for v, attr in adj[u].items():
|
||||
if (visited[v] == False) and (attr.get("weight", 1) > 0):
|
||||
queue.append(v)
|
||||
visited[v] = True
|
||||
parent[v] = u
|
||||
return visited[sink]
|
||||
@@ -0,0 +1,105 @@
|
||||
from easygraph.utils import *
|
||||
import numpy as np
|
||||
from easygraph.utils.decorators import *
|
||||
|
||||
__all__ = ["katz_centrality"]
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@hybrid("cpp_katz_centrality")
|
||||
def katz_centrality(G, alpha=0.1, beta=1.0, max_iter=1000, tol=1e-6, normalized=True):
|
||||
r"""
|
||||
Compute the Katz centrality for nodes in a graph.
|
||||
|
||||
Katz centrality computes the influence of a node based on the total number
|
||||
of walks between nodes, attenuated by a factor of their length. It is
|
||||
defined as the solution to the linear system:
|
||||
|
||||
.. math::
|
||||
|
||||
x = \alpha A x + \beta
|
||||
|
||||
where:
|
||||
- \( A \) is the adjacency matrix of the graph,
|
||||
- \( \alpha \) is a scalar attenuation factor,
|
||||
- \( \beta \) is the bias vector (typically all ones),
|
||||
- and \( x \) is the resulting centrality vector.
|
||||
|
||||
The algorithm runs an iterative fixed-point method until convergence.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph
|
||||
An EasyGraph graph instance. Must be simple (non-multigraph).
|
||||
|
||||
alpha : float, optional (default=0.1)
|
||||
Attenuation factor, must be smaller than the reciprocal of the largest
|
||||
eigenvalue of the adjacency matrix to ensure convergence.
|
||||
|
||||
beta : float or dict, optional (default=1.0)
|
||||
Bias term. Can be a constant scalar applied to all nodes, or a dictionary
|
||||
mapping node IDs to values.
|
||||
|
||||
max_iter : int, optional (default=1000)
|
||||
Maximum number of iterations before the algorithm terminates.
|
||||
|
||||
tol : float, optional (default=1e-6)
|
||||
Convergence tolerance. Iteration stops when the L1 norm of the difference
|
||||
between successive iterations is below this threshold.
|
||||
|
||||
normalized : bool, optional (default=True)
|
||||
If True, the result vector will be normalized to unit norm (L2).
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
A dictionary mapping node IDs to Katz centrality scores.
|
||||
|
||||
Raises
|
||||
------
|
||||
RuntimeError
|
||||
If the algorithm fails to converge within `max_iter` iterations.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import easygraph as eg
|
||||
>>> from easygraph import katz_centrality
|
||||
>>> G = eg.Graph()
|
||||
>>> G.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
>>> katz_centrality(G, alpha=0.05)
|
||||
{0: 0.370..., 1: 0.447..., 2: 0.447..., 3: 0.370...}
|
||||
"""
|
||||
# Create node ordering
|
||||
nodes = list(G.nodes)
|
||||
n = len(nodes)
|
||||
node_to_index = {node: i for i, node in enumerate(nodes)}
|
||||
index_to_node = {i: node for i, node in enumerate(nodes)}
|
||||
|
||||
# Build adjacency matrix
|
||||
A = np.zeros((n, n), dtype=np.float64)
|
||||
for u in G.nodes:
|
||||
for v in G.adj[u]:
|
||||
A[node_to_index[u], node_to_index[v]] = 1.0
|
||||
|
||||
# Initialize x and beta
|
||||
x = np.ones(n, dtype=np.float64)
|
||||
if isinstance(beta, dict):
|
||||
b = np.array([beta.get(index_to_node[i], 1.0) for i in range(n)])
|
||||
else:
|
||||
b = np.ones(n, dtype=np.float64) * beta
|
||||
|
||||
# Iterative update using vectorized ops
|
||||
for _ in range(max_iter):
|
||||
x_new = alpha * A @ x + b
|
||||
if np.linalg.norm(x_new - x, ord=1) < tol:
|
||||
break
|
||||
x = x_new
|
||||
else:
|
||||
raise RuntimeError(f"Katz centrality failed to converge in {max_iter} iterations")
|
||||
|
||||
if normalized:
|
||||
norm = np.linalg.norm(x)
|
||||
if norm > 0:
|
||||
x /= norm
|
||||
|
||||
result = {index_to_node[i]: float(x[i]) for i in range(n)}
|
||||
return result
|
||||
@@ -0,0 +1,134 @@
|
||||
from easygraph.utils import *
|
||||
|
||||
|
||||
__all__ = ["laplacian"]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def laplacian(G, n_workers=None):
|
||||
"""Returns the laplacian centrality of each node in the weighted graph
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
weighted graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
CL : dict
|
||||
the laplacian centrality of each node in the weighted graph
|
||||
|
||||
Examples
|
||||
--------
|
||||
Returns the laplacian centrality of each node in the weighted graph G
|
||||
|
||||
>>> laplacian(G)
|
||||
|
||||
Reference
|
||||
---------
|
||||
.. [1] Xingqin Qi, Eddie Fuller, Qin Wu, Yezhou Wu, Cun-Quan Zhang.
|
||||
"Laplacian centrality: A new centrality measure for weighted networks."
|
||||
Information Sciences, Volume 194, Pages 240-253, 2012.
|
||||
|
||||
"""
|
||||
adj = G.adj
|
||||
from collections import defaultdict
|
||||
|
||||
X = defaultdict(int)
|
||||
W = defaultdict(int)
|
||||
CL = {}
|
||||
|
||||
if n_workers is not None:
|
||||
# use the parallel version for large graph
|
||||
import random
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
|
||||
nodes = list(G.nodes)
|
||||
random.shuffle(nodes)
|
||||
|
||||
if len(nodes) > n_workers * 30000:
|
||||
nodes = split_len(nodes, step=30000)
|
||||
else:
|
||||
nodes = split(nodes, n_workers)
|
||||
|
||||
local_function = partial(initialize_parallel, G=G, adj=adj)
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(local_function, nodes)
|
||||
resX, resW = [], []
|
||||
for i in ret:
|
||||
for x in i:
|
||||
resX.append(x[0])
|
||||
resW.append(x[1])
|
||||
X = dict(resX)
|
||||
W = dict(resW)
|
||||
ELG = sum(X[i] * X[i] for i in G) + sum(W[i] for i in G)
|
||||
local_function = partial(laplacian_parallel, G=G, X=X, W=W, adj=adj, ELG=ELG)
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(local_function, nodes)
|
||||
res = [x for i in ret for x in i]
|
||||
CL = dict(res)
|
||||
|
||||
else:
|
||||
# use np-parallel version for small graph
|
||||
for i in G:
|
||||
for j in G:
|
||||
if i in G and j in G[i]:
|
||||
X[i] += adj[i][j].get("weight", 1)
|
||||
W[i] += adj[i][j].get("weight", 1) * adj[i][j].get("weight", 1)
|
||||
ELG = sum(X[i] * X[i] for i in G) + sum(W[i] for i in G)
|
||||
for i in G:
|
||||
import copy
|
||||
|
||||
Xi = copy.deepcopy(X)
|
||||
for j in G:
|
||||
if j in adj.keys() and i in adj[j].keys():
|
||||
Xi[j] -= adj[j][i].get("weight", 1)
|
||||
Xi[i] = 0
|
||||
ELGi = sum(Xi[i] * Xi[i] for i in G) + sum(W[i] for i in G) - 2 * W[i]
|
||||
if ELG:
|
||||
CL[i] = (float)(ELG - ELGi) / ELG
|
||||
return CL
|
||||
|
||||
|
||||
def initialize_parallel(nodes, G, adj):
|
||||
ret = []
|
||||
for i in nodes:
|
||||
X = 0
|
||||
W = 0
|
||||
for j in G:
|
||||
if j in G[i]:
|
||||
X += adj[i][j].get("weight", 1)
|
||||
W += adj[i][j].get("weight", 1) * adj[i][j].get("weight", 1)
|
||||
ret.append([[i, X], [i, W]])
|
||||
return ret
|
||||
|
||||
|
||||
def laplacian_parallel(nodes, G, X, W, adj, ELG):
|
||||
ret = []
|
||||
for i in nodes:
|
||||
import copy
|
||||
|
||||
Xi = copy.deepcopy(X)
|
||||
for j in G:
|
||||
if j in adj.keys() and i in adj[j].keys():
|
||||
Xi[j] -= adj[j][i].get("weight", 1)
|
||||
Xi[i] = 0
|
||||
ELGi = sum(Xi[i] * Xi[i] for i in G) + sum(W[i] for i in G) - 2 * W[i]
|
||||
if ELG:
|
||||
ret.append([i, (float)(ELG - ELGi) / ELG])
|
||||
return ret
|
||||
|
||||
|
||||
def sort(data):
|
||||
return dict(sorted(data.items(), key=lambda x: x[0], reverse=True))
|
||||
|
||||
|
||||
def output(data, path):
|
||||
import json
|
||||
|
||||
data = sort(data)
|
||||
json_str = json.dumps(data, ensure_ascii=False, indent=4)
|
||||
with open(path, "w", encoding="utf-8") as json_file:
|
||||
json_file.write(json_str)
|
||||
@@ -0,0 +1,58 @@
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils import *
|
||||
|
||||
|
||||
__all__ = ["pagerank"]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@hybrid("cpp_pagerank")
|
||||
def pagerank(G, alpha=0.85, weight=None):
|
||||
"""
|
||||
Returns the PageRank value of each node in G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
Undirected graph will be considered as directed graph with two directed edges for each undirected edge.
|
||||
|
||||
alpha : float
|
||||
The damping factor. Default is 0.85
|
||||
|
||||
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.
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
if len(G) == 0:
|
||||
return {}
|
||||
M = google_matrix(G, alpha=alpha, weight=weight)
|
||||
|
||||
# use numpy LAPACK solver
|
||||
eigenvalues, eigenvectors = np.linalg.eig(M.T)
|
||||
ind = np.argmax(eigenvalues)
|
||||
# eigenvector of largest eigenvalue is at ind, normalized
|
||||
largest = np.array(eigenvectors[:, ind]).flatten().real
|
||||
norm = float(largest.sum())
|
||||
return dict(zip(G, map(float, largest / norm)))
|
||||
|
||||
|
||||
def google_matrix(G, alpha, weight=None):
|
||||
import numpy as np
|
||||
|
||||
M = eg.to_numpy_array(G, weight=weight).astype(float)
|
||||
N = len(G)
|
||||
if N == 0:
|
||||
return M
|
||||
|
||||
# Get dangling nodes(nodes with no out link)
|
||||
dangling_nodes = np.where(M.sum(axis=1) == 0)[0]
|
||||
dangling_weights = np.repeat(1.0 / N, N)
|
||||
for node in dangling_nodes:
|
||||
M[node] = dangling_weights
|
||||
|
||||
M /= M.sum(axis=1)[:, np.newaxis]
|
||||
|
||||
return alpha * M + (1 - alpha) * np.repeat(1.0 / N, N)
|
||||
@@ -0,0 +1,99 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
class Test_betweenness(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [
|
||||
(1, 4),
|
||||
(2, 4),
|
||||
("String", "Bool"),
|
||||
(4, 1),
|
||||
(0, 4),
|
||||
(4, 256),
|
||||
((None, None), (None, None)),
|
||||
]
|
||||
self.test_graphs = [eg.Graph(), eg.DiGraph()]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
|
||||
self.undirected = eg.Graph()
|
||||
self.undirected.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4)])
|
||||
|
||||
self.directed = eg.DiGraph()
|
||||
self.directed.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4)])
|
||||
|
||||
self.disconnected = eg.Graph()
|
||||
self.disconnected.add_edges_from([(0, 1), (2, 3)])
|
||||
|
||||
self.single_node = eg.Graph()
|
||||
self.single_node.add_node(42)
|
||||
|
||||
self.two_node = eg.Graph()
|
||||
self.two_node.add_edge("A", "B")
|
||||
|
||||
self.named_nodes = eg.Graph()
|
||||
self.named_nodes.add_edges_from([("X", "Y"), ("Y", "Z")])
|
||||
|
||||
def test_betweenness(self):
|
||||
for i in self.test_graphs:
|
||||
print(eg.functions.betweenness_centrality(i))
|
||||
|
||||
def test_basic_undirected(self):
|
||||
result = eg.functions.betweenness_centrality(self.undirected)
|
||||
self.assertEqual(len(result), len(self.undirected.nodes))
|
||||
self.assertTrue(all(isinstance(x, float) for x in result))
|
||||
|
||||
def test_basic_directed(self):
|
||||
result = eg.functions.betweenness_centrality(self.directed)
|
||||
self.assertEqual(len(result), len(self.directed.nodes))
|
||||
|
||||
def test_disconnected(self):
|
||||
result = eg.functions.betweenness_centrality(self.disconnected)
|
||||
self.assertEqual(len(result), len(self.disconnected.nodes))
|
||||
self.assertTrue(all(v == 0.0 for v in result))
|
||||
|
||||
def test_single_node_graph(self):
|
||||
result = eg.functions.betweenness_centrality(self.single_node)
|
||||
self.assertEqual(result, [0.0])
|
||||
|
||||
def test_two_node_graph(self):
|
||||
result = eg.functions.betweenness_centrality(self.two_node)
|
||||
self.assertEqual(len(result), 2)
|
||||
self.assertTrue(all(v == 0.0 for v in result))
|
||||
|
||||
def test_named_nodes_graph(self):
|
||||
result = eg.functions.betweenness_centrality(self.named_nodes)
|
||||
self.assertEqual(len(result), 3)
|
||||
|
||||
def test_with_endpoints(self):
|
||||
result = eg.functions.betweenness_centrality(self.undirected, endpoints=True)
|
||||
self.assertEqual(len(result), len(self.undirected.nodes))
|
||||
|
||||
def test_unormalized(self):
|
||||
result = eg.functions.betweenness_centrality(self.undirected, normalized=False)
|
||||
self.assertEqual(len(result), len(self.undirected.nodes))
|
||||
|
||||
def test_subset_sources(self):
|
||||
result = eg.functions.betweenness_centrality(self.undirected, sources=[1, 2])
|
||||
self.assertEqual(len(result), len(self.undirected.nodes))
|
||||
|
||||
def test_parallel_workers(self):
|
||||
result = eg.functions.betweenness_centrality(self.undirected, n_workers=2)
|
||||
self.assertEqual(len(result), len(self.undirected.nodes))
|
||||
|
||||
def test_multigraph_error(self):
|
||||
G = eg.MultiGraph()
|
||||
G.add_edges_from([(0, 1), (0, 1)])
|
||||
with self.assertRaises(eg.EasyGraphNotImplemented):
|
||||
eg.functions.betweenness_centrality(G)
|
||||
|
||||
def test_all_nodes_type_mix(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(1, 2), ("A", "B"), ((1, 2), (3, 4))])
|
||||
result = eg.functions.betweenness_centrality(G)
|
||||
self.assertEqual(len(result), len(G.nodes))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,86 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.classes.multigraph import MultiGraph
|
||||
from easygraph.functions.centrality import closeness_centrality
|
||||
|
||||
|
||||
class Test_closeness(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [
|
||||
(1, 4),
|
||||
(2, 4),
|
||||
("String", "Bool"),
|
||||
(4, 1),
|
||||
(0, 4),
|
||||
(4, 256),
|
||||
((None, None), (None, None)),
|
||||
]
|
||||
self.test_graphs = [eg.Graph(), eg.DiGraph()]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
|
||||
self.simple_graph = eg.Graph()
|
||||
self.simple_graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
|
||||
self.directed_graph = eg.DiGraph()
|
||||
self.directed_graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
|
||||
self.weighted_graph = eg.Graph()
|
||||
self.weighted_graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
for u, v, data in self.weighted_graph.edges:
|
||||
data["weight"] = 2
|
||||
|
||||
self.disconnected_graph = eg.Graph()
|
||||
self.disconnected_graph.add_edges_from([(0, 1), (2, 3)])
|
||||
|
||||
self.single_node_graph = eg.Graph()
|
||||
self.single_node_graph.add_node(42)
|
||||
|
||||
self.mixed_nodes_graph = eg.Graph()
|
||||
self.mixed_nodes_graph.add_edges_from([(1, 2), ("X", "Y"), ((1, 2), (3, 4))])
|
||||
|
||||
def test_closeness(self):
|
||||
for i in self.test_graphs:
|
||||
result = closeness_centrality(i)
|
||||
self.assertEqual(len(result), len(i))
|
||||
|
||||
def test_simple_graph(self):
|
||||
result = closeness_centrality(self.simple_graph)
|
||||
self.assertEqual(len(result), len(self.simple_graph))
|
||||
self.assertTrue(all(isinstance(x, float) for x in result))
|
||||
|
||||
def test_directed_graph(self):
|
||||
result = closeness_centrality(self.directed_graph)
|
||||
self.assertEqual(len(result), len(self.directed_graph))
|
||||
|
||||
def test_weighted_graph(self):
|
||||
result = closeness_centrality(self.weighted_graph, weight="weight")
|
||||
self.assertEqual(len(result), len(self.weighted_graph))
|
||||
|
||||
def test_disconnected_graph(self):
|
||||
result = closeness_centrality(self.disconnected_graph)
|
||||
self.assertEqual(len(result), len(self.disconnected_graph))
|
||||
self.assertTrue(all(v <= 1.0 for v in result))
|
||||
|
||||
def test_single_node_graph(self):
|
||||
result = closeness_centrality(self.single_node_graph)
|
||||
self.assertEqual(result, [0.0])
|
||||
|
||||
def test_mixed_node_types(self):
|
||||
result = closeness_centrality(self.mixed_nodes_graph)
|
||||
self.assertEqual(len(result), len(self.mixed_nodes_graph))
|
||||
|
||||
def test_parallel_workers(self):
|
||||
result = closeness_centrality(self.simple_graph, n_workers=2)
|
||||
self.assertEqual(len(result), len(self.simple_graph))
|
||||
|
||||
def test_multigraph_raises(self):
|
||||
G = MultiGraph()
|
||||
G.add_edges_from([(0, 1), (0, 1)])
|
||||
with self.assertRaises(eg.EasyGraphNotImplemented):
|
||||
closeness_centrality(G)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,78 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.exception import EasyGraphNotImplemented
|
||||
|
||||
|
||||
class Test_degree(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [
|
||||
(1, 4),
|
||||
(2, 4),
|
||||
("String", "Bool"),
|
||||
(4, 1),
|
||||
(0, 4),
|
||||
(4, 256),
|
||||
((None, None), (None, None)),
|
||||
]
|
||||
self.test_graphs = [eg.Graph(), eg.DiGraph()]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
|
||||
self.undirected_graph = eg.Graph()
|
||||
self.undirected_graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
|
||||
# Directed graph
|
||||
self.directed_graph = eg.DiGraph()
|
||||
self.directed_graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
|
||||
# Single-node graph
|
||||
self.single_node_graph = eg.Graph()
|
||||
self.single_node_graph.add_node(0)
|
||||
|
||||
# Empty graph
|
||||
self.empty_graph = eg.Graph()
|
||||
|
||||
# Multigraph
|
||||
self.multigraph = eg.MultiGraph()
|
||||
self.multigraph.add_edges_from([(0, 1), (0, 1)])
|
||||
|
||||
def test_degree(self):
|
||||
for i in self.test_graphs:
|
||||
print(i.edges)
|
||||
print(eg.functions.degree_centrality(i))
|
||||
print(eg.functions.in_degree_centrality(i))
|
||||
print(eg.functions.out_degree_centrality(i))
|
||||
|
||||
def test_degree_centrality_undirected(self):
|
||||
result = eg.functions.degree_centrality(self.undirected_graph)
|
||||
self.assertEqual(len(result), len(self.undirected_graph))
|
||||
self.assertTrue(all(isinstance(v, float) for v in result.values()))
|
||||
|
||||
def test_degree_centrality_directed(self):
|
||||
result = eg.functions.degree_centrality(self.directed_graph)
|
||||
self.assertEqual(len(result), len(self.directed_graph))
|
||||
|
||||
def test_degree_centrality_single_node(self):
|
||||
result = eg.functions.degree_centrality(self.single_node_graph)
|
||||
self.assertEqual(result, {0: 1})
|
||||
|
||||
def test_degree_centrality_empty_graph(self):
|
||||
result = eg.functions.degree_centrality(self.empty_graph)
|
||||
self.assertEqual(result, {})
|
||||
|
||||
def test_in_out_degree_centrality_directed(self):
|
||||
in_deg = eg.functions.in_degree_centrality(self.directed_graph)
|
||||
out_deg = eg.functions.out_degree_centrality(self.directed_graph)
|
||||
self.assertEqual(len(in_deg), len(self.directed_graph))
|
||||
self.assertEqual(len(out_deg), len(self.directed_graph))
|
||||
|
||||
def test_in_out_degree_centrality_single_node(self):
|
||||
G = eg.DiGraph()
|
||||
G.add_node(1)
|
||||
self.assertEqual(eg.functions.in_degree_centrality(G), {1: 1})
|
||||
self.assertEqual(eg.functions.out_degree_centrality(G), {1: 1})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,73 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.exception import EasyGraphNotImplemented
|
||||
|
||||
|
||||
class Test_egobetweenness(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [
|
||||
(1, 4),
|
||||
(2, 4),
|
||||
("String", "Bool"),
|
||||
(4, 1),
|
||||
(0, 4),
|
||||
(4, 256),
|
||||
((None, None), (None, None)),
|
||||
]
|
||||
self.test_graphs = [eg.Graph(), eg.DiGraph()]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
print(self.test_graphs[-1].edges)
|
||||
|
||||
self.graph = eg.Graph()
|
||||
self.graph.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
|
||||
self.directed_graph = eg.DiGraph()
|
||||
self.directed_graph.add_edges_from([(0, 1), (1, 2), (2, 0)])
|
||||
|
||||
self.mixed_nodes_graph = eg.Graph()
|
||||
self.mixed_nodes_graph.add_edges_from([(1, "A"), ("A", (2, 3)), ((2, 3), "B")])
|
||||
|
||||
self.single_node_graph = eg.Graph()
|
||||
self.single_node_graph.add_node(42)
|
||||
|
||||
self.disconnected_graph = eg.Graph()
|
||||
self.disconnected_graph.add_edges_from([(0, 1), (2, 3)]) # two components
|
||||
|
||||
self.multigraph = eg.MultiGraph()
|
||||
self.multigraph.add_edges_from([(0, 1), (0, 1)]) # parallel edges
|
||||
|
||||
def test_egobetweenness(self):
|
||||
print(eg.functions.ego_betweenness(self.test_graphs[-1], 4))
|
||||
|
||||
def test_small_undirected_graph(self):
|
||||
result = eg.functions.ego_betweenness(self.graph, 1)
|
||||
self.assertIsInstance(result, float)
|
||||
self.assertGreaterEqual(result, 0)
|
||||
|
||||
def test_directed_graph(self):
|
||||
result = eg.functions.ego_betweenness(self.directed_graph, 0)
|
||||
self.assertIsInstance(result, int)
|
||||
|
||||
def test_mixed_node_types(self):
|
||||
result = eg.functions.ego_betweenness(self.mixed_nodes_graph, "A")
|
||||
self.assertIsInstance(result, float)
|
||||
|
||||
def test_single_node_graph(self):
|
||||
result = eg.functions.ego_betweenness(self.single_node_graph, 42)
|
||||
self.assertEqual(result, 0.0)
|
||||
|
||||
def test_disconnected_graph_component(self):
|
||||
result_0 = eg.functions.ego_betweenness(self.disconnected_graph, 0)
|
||||
result_2 = eg.functions.ego_betweenness(self.disconnected_graph, 2)
|
||||
self.assertIsInstance(result_0, float)
|
||||
self.assertIsInstance(result_2, float)
|
||||
|
||||
def test_raises_on_multigraph(self):
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
eg.functions.ego_betweenness(self.multigraph, 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,90 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.exception import EasyGraphNotImplemented
|
||||
|
||||
|
||||
class Test_flowbetweenness(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
("String", "Bool"),
|
||||
(2, 1),
|
||||
(0, 0),
|
||||
(-99, 256),
|
||||
((None, None), (None, None)),
|
||||
]
|
||||
self.test_graphs = [eg.Graph(), eg.DiGraph()]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
|
||||
self.directed_graph = eg.DiGraph()
|
||||
self.directed_graph.add_edges_from(
|
||||
[
|
||||
(0, 1, {"weight": 3}),
|
||||
(1, 2, {"weight": 1}),
|
||||
(0, 2, {"weight": 1}),
|
||||
(2, 3, {"weight": 2}),
|
||||
(1, 3, {"weight": 4}),
|
||||
]
|
||||
)
|
||||
|
||||
self.graph_with_self_loop = eg.DiGraph()
|
||||
self.graph_with_self_loop.add_edges_from([(0, 1), (1, 2), (2, 2), (2, 3)])
|
||||
|
||||
self.disconnected_graph = eg.DiGraph()
|
||||
self.disconnected_graph.add_edges_from([(0, 1), (2, 3)])
|
||||
|
||||
self.undirected_graph = eg.Graph()
|
||||
self.undirected_graph.add_edges_from([(0, 1), (1, 2)])
|
||||
|
||||
self.single_node_graph = eg.DiGraph()
|
||||
self.single_node_graph.add_node(0)
|
||||
|
||||
self.mixed_type_graph = eg.DiGraph()
|
||||
self.mixed_type_graph.add_edges_from([(1, "A"), ("A", (2, 3)), ((2, 3), "B")])
|
||||
|
||||
self.multigraph = eg.MultiDiGraph()
|
||||
self.multigraph.add_edges_from([(0, 1), (0, 1)])
|
||||
|
||||
def test_flowbetweenness_centrality(self):
|
||||
for i in self.test_graphs:
|
||||
print(i.edges)
|
||||
print(eg.functions.flowbetweenness_centrality(i))
|
||||
|
||||
def test_flowbetweenness_on_directed(self):
|
||||
result = eg.functions.flowbetweenness_centrality(self.directed_graph)
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertTrue(
|
||||
all(isinstance(v, float) or isinstance(v, int) for v in result.values())
|
||||
)
|
||||
|
||||
def test_flowbetweenness_on_self_loop(self):
|
||||
result = eg.functions.flowbetweenness_centrality(self.graph_with_self_loop)
|
||||
self.assertIsInstance(result, dict)
|
||||
|
||||
def test_flowbetweenness_on_disconnected(self):
|
||||
result = eg.functions.flowbetweenness_centrality(self.disconnected_graph)
|
||||
self.assertIsInstance(result, dict)
|
||||
|
||||
def test_flowbetweenness_on_single_node(self):
|
||||
result = eg.functions.flowbetweenness_centrality(self.single_node_graph)
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertEqual(result, {0: 0})
|
||||
|
||||
def test_flowbetweenness_on_mixed_types(self):
|
||||
result = eg.functions.flowbetweenness_centrality(self.mixed_type_graph)
|
||||
self.assertIsInstance(result, dict)
|
||||
|
||||
def test_flowbetweenness_on_undirected_warns(self):
|
||||
result = eg.functions.flowbetweenness_centrality(self.undirected_graph)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_flowbetweenness_raises_on_multigraph(self):
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
eg.functions.flowbetweenness_centrality(self.multigraph)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,106 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.exception import EasyGraphNotImplemented
|
||||
|
||||
|
||||
class Test_laplacian(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
("String", "Bool"),
|
||||
(2, 1),
|
||||
(0, 0),
|
||||
(-99, 256),
|
||||
((None, None), (None, None)),
|
||||
]
|
||||
self.test_graphs = [eg.Graph(), eg.DiGraph()]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
self.weighted_graph = eg.Graph()
|
||||
self.weighted_graph.add_edges_from(
|
||||
[
|
||||
(0, 1, {"weight": 2}),
|
||||
(1, 2, {"weight": 3}),
|
||||
(2, 3, {"weight": 4}),
|
||||
(3, 0, {"weight": 1}),
|
||||
]
|
||||
)
|
||||
|
||||
self.unweighted_graph = eg.Graph()
|
||||
self.unweighted_graph.add_edges_from(
|
||||
[
|
||||
(0, 1),
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
]
|
||||
)
|
||||
|
||||
self.directed_graph = eg.DiGraph()
|
||||
self.directed_graph.add_edges_from(
|
||||
[
|
||||
(0, 1, {"weight": 2}),
|
||||
(1, 2, {"weight": 1}),
|
||||
(2, 0, {"weight": 3}),
|
||||
]
|
||||
)
|
||||
|
||||
self.self_loop_graph = eg.Graph()
|
||||
self.self_loop_graph.add_edges_from(
|
||||
[
|
||||
(0, 0, {"weight": 2}),
|
||||
(0, 1, {"weight": 1}),
|
||||
]
|
||||
)
|
||||
|
||||
self.mixed_type_graph = eg.Graph()
|
||||
self.mixed_type_graph.add_edges_from(
|
||||
[
|
||||
("A", "B"),
|
||||
("B", (1, 2)),
|
||||
]
|
||||
)
|
||||
|
||||
self.single_node_graph = eg.Graph()
|
||||
self.single_node_graph.add_node(42)
|
||||
|
||||
self.multigraph = eg.MultiGraph()
|
||||
self.multigraph.add_edges_from([(0, 1), (0, 1)])
|
||||
|
||||
def test_laplacian(self):
|
||||
for i in self.test_graphs:
|
||||
print(i.edges)
|
||||
print(eg.functions.laplacian(i))
|
||||
|
||||
def test_weighted_graph(self):
|
||||
result = eg.functions.laplacian(self.weighted_graph)
|
||||
self.assertEqual(set(result.keys()), set(self.weighted_graph.nodes))
|
||||
|
||||
def test_unweighted_graph(self):
|
||||
result = eg.functions.laplacian(self.unweighted_graph)
|
||||
self.assertEqual(set(result.keys()), set(self.unweighted_graph.nodes))
|
||||
|
||||
def test_directed_graph(self):
|
||||
result = eg.functions.laplacian(self.directed_graph)
|
||||
self.assertEqual(set(result.keys()), set(self.directed_graph.nodes))
|
||||
|
||||
def test_self_loop_graph(self):
|
||||
result = eg.functions.laplacian(self.self_loop_graph)
|
||||
self.assertEqual(set(result.keys()), set(self.self_loop_graph.nodes))
|
||||
|
||||
def test_mixed_node_types(self):
|
||||
result = eg.functions.laplacian(self.mixed_type_graph)
|
||||
self.assertEqual(set(result.keys()), set(self.mixed_type_graph.nodes))
|
||||
|
||||
def test_single_node_graph(self):
|
||||
result = eg.functions.laplacian(self.single_node_graph)
|
||||
self.assertEqual(result, {})
|
||||
|
||||
def test_multigraph_raises(self):
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
eg.functions.laplacian(self.multigraph)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,90 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.exception import EasyGraphNotImplemented
|
||||
|
||||
|
||||
class Test_pagerank(unittest.TestCase):
|
||||
def setUp(self):
|
||||
edges = [
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
("String", "Bool"),
|
||||
(2, 1),
|
||||
(0, 0),
|
||||
((None, None), (None, None)),
|
||||
]
|
||||
self.g = eg.classes.DiGraph(edges)
|
||||
self.directed_graph = eg.DiGraph()
|
||||
self.directed_graph.add_edges_from([(0, 1), (1, 2), (2, 0)])
|
||||
|
||||
self.undirected_graph = eg.Graph()
|
||||
self.undirected_graph.add_edges_from([(0, 1), (1, 2), (2, 0)])
|
||||
|
||||
self.disconnected_graph = eg.DiGraph()
|
||||
self.disconnected_graph.add_edges_from([(0, 1), (2, 3)])
|
||||
|
||||
self.self_loop_graph = eg.DiGraph()
|
||||
self.self_loop_graph.add_edges_from([(0, 0), (0, 1), (1, 2)])
|
||||
|
||||
self.mixed_graph = eg.DiGraph()
|
||||
self.mixed_graph.add_edges_from([("A", "B"), ("B", "C"), ("C", (1, 2))])
|
||||
|
||||
self.single_node_graph = eg.DiGraph()
|
||||
self.single_node_graph.add_node("solo")
|
||||
|
||||
self.multigraph = eg.MultiDiGraph()
|
||||
self.multigraph.add_edges_from([(0, 1), (0, 1)])
|
||||
|
||||
def test_pagerank(self):
|
||||
test_graphs = [eg.Graph(), eg.DiGraph()]
|
||||
for i in test_graphs:
|
||||
print(eg.functions.pagerank(i))
|
||||
|
||||
print(self.g.nodes)
|
||||
print(eg.functions.pagerank(self.g))
|
||||
|
||||
"""
|
||||
def test_google_matrix(self):
|
||||
test_graphs = [eg.Graph(), eg.DiGraph(), eg.MultiGraph(), eg.MultiDiGraph()]
|
||||
for g in test_graphs:
|
||||
print(eg.functions.pagerank.(g))
|
||||
"""
|
||||
|
||||
def test_directed_graph(self):
|
||||
result = eg.functions.pagerank(self.directed_graph)
|
||||
self.assertEqual(set(result.keys()), set(self.directed_graph.nodes))
|
||||
|
||||
def test_undirected_graph(self):
|
||||
result = eg.functions.pagerank(self.undirected_graph)
|
||||
self.assertEqual(set(result.keys()), set(self.undirected_graph.nodes))
|
||||
|
||||
def test_disconnected_graph(self):
|
||||
result = eg.functions.pagerank(self.disconnected_graph)
|
||||
self.assertEqual(set(result.keys()), set(self.disconnected_graph.nodes))
|
||||
|
||||
def test_self_loop_graph(self):
|
||||
result = eg.functions.pagerank(self.self_loop_graph)
|
||||
self.assertEqual(set(result.keys()), set(self.self_loop_graph.nodes))
|
||||
|
||||
def test_mixed_node_types(self):
|
||||
result = eg.functions.pagerank(self.mixed_graph)
|
||||
self.assertEqual(set(result.keys()), set(self.mixed_graph.nodes))
|
||||
|
||||
def test_single_node_graph(self):
|
||||
result = eg.functions.pagerank(self.single_node_graph)
|
||||
self.assertEqual(result, {"solo": 1.0})
|
||||
|
||||
def test_empty_graph(self):
|
||||
empty_graph = eg.DiGraph()
|
||||
result = eg.functions.pagerank(empty_graph)
|
||||
self.assertEqual(result, {})
|
||||
|
||||
def test_multigraph_raises(self):
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
eg.functions.pagerank(self.multigraph)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user