chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
from easygraph.functions.basic import *
|
||||
from easygraph.functions.centrality import *
|
||||
from easygraph.functions.community import *
|
||||
from easygraph.functions.components import *
|
||||
from easygraph.functions.core import *
|
||||
from easygraph.functions.drawing import *
|
||||
from easygraph.functions.graph_embedding import *
|
||||
from easygraph.functions.graph_generator import *
|
||||
from easygraph.functions.isolate import *
|
||||
from easygraph.functions.path import *
|
||||
from easygraph.functions.structural_holes import *
|
||||
|
||||
|
||||
try:
|
||||
from easygraph.functions.hypergraph import *
|
||||
except:
|
||||
print(
|
||||
"Warning raise in module:model.Please install "
|
||||
"Pytorch before you use functions"
|
||||
" related to Hypergraph"
|
||||
)
|
||||
@@ -0,0 +1,4 @@
|
||||
from .avg_degree import *
|
||||
from .cluster import *
|
||||
from .localassort import *
|
||||
from .predecessor_path_based import *
|
||||
@@ -0,0 +1,31 @@
|
||||
__all__ = [
|
||||
"average_degree",
|
||||
]
|
||||
|
||||
|
||||
def average_degree(G) -> float:
|
||||
"""Returns the average degree of the graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A EasyGraph graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
average degree : float
|
||||
The average degree of the graph.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Self loops are counted twice in the total degree of a node.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
|
||||
>>> G.add_edge(1, 2)
|
||||
>>> G.add_edge(2, 3)
|
||||
>>> eg.average_degree(G)
|
||||
1.3333333333333333
|
||||
"""
|
||||
return G.number_of_edges() / G.number_of_nodes() * 2
|
||||
@@ -0,0 +1,559 @@
|
||||
from collections import Counter
|
||||
from itertools import chain
|
||||
|
||||
import numpy as np
|
||||
|
||||
from easygraph.utils.decorators import hybrid
|
||||
from easygraph.utils.decorators import not_implemented_for
|
||||
from easygraph.utils.misc import split
|
||||
from easygraph.utils.misc import split_len
|
||||
|
||||
|
||||
__all__ = ["average_clustering", "clustering"]
|
||||
|
||||
|
||||
def _local_weighted_triangles_and_degree_iter_parallel(
|
||||
nodes_nbrs, G, weight, max_weight
|
||||
):
|
||||
ret = []
|
||||
|
||||
def wt(u, v):
|
||||
return G[u][v].get(weight, 1) / max_weight
|
||||
|
||||
for i, nbrs in nodes_nbrs:
|
||||
inbrs = set(nbrs) - {i}
|
||||
weighted_triangles = 0
|
||||
seen = set()
|
||||
for j in inbrs:
|
||||
seen.add(j)
|
||||
# This avoids counting twice -- we double at the end.
|
||||
jnbrs = set(G[j]) - seen
|
||||
# Only compute the edge weight once, before the inner inner
|
||||
# loop.
|
||||
wij = wt(i, j)
|
||||
weighted_triangles += sum(
|
||||
np.cbrt([(wij * wt(j, k) * wt(k, i)) for k in inbrs & jnbrs])
|
||||
)
|
||||
ret.append((i, len(inbrs), 2 * weighted_triangles))
|
||||
return ret
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def _weighted_triangles_and_degree_iter(G, nodes=None, weight="weight", n_workers=None):
|
||||
"""Return an iterator of (node, degree, weighted_triangles).
|
||||
|
||||
Used for weighted clustering.
|
||||
Note: this returns the geometric average weight of edges in the triangle.
|
||||
Also, each triangle is counted twice (each direction).
|
||||
So you may want to divide by 2.
|
||||
|
||||
"""
|
||||
|
||||
if weight is None or G.number_of_edges() == 0:
|
||||
max_weight = 1
|
||||
else:
|
||||
max_weight = max(d.get(weight, 1) for u, v, d in G.edges)
|
||||
if nodes is None:
|
||||
nodes_nbrs = G.adj.items()
|
||||
else:
|
||||
nodes_nbrs = ((n, G[n]) for n in G.nbunch_iter(nodes))
|
||||
|
||||
def wt(u, v):
|
||||
return G[u][v].get(weight, 1) / max_weight
|
||||
|
||||
if n_workers is not None:
|
||||
import random
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
|
||||
_local_weighted_triangles_and_degree_iter_function = partial(
|
||||
_local_weighted_triangles_and_degree_iter_parallel,
|
||||
G=G,
|
||||
weight=weight,
|
||||
max_weight=max_weight,
|
||||
)
|
||||
nodes_nbrs = list(nodes_nbrs)
|
||||
random.shuffle(nodes_nbrs)
|
||||
if len(nodes_nbrs) > n_workers * 30000:
|
||||
nodes_nbrs = split_len(nodes, step=30000)
|
||||
else:
|
||||
nodes_nbrs = split(nodes_nbrs, n_workers)
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(_local_weighted_triangles_and_degree_iter_function, nodes_nbrs)
|
||||
for r in ret:
|
||||
for x in r:
|
||||
yield x
|
||||
else:
|
||||
for i, nbrs in nodes_nbrs:
|
||||
inbrs = set(nbrs) - {i}
|
||||
weighted_triangles = 0
|
||||
seen = set()
|
||||
for j in inbrs:
|
||||
seen.add(j)
|
||||
# This avoids counting twice -- we double at the end.
|
||||
jnbrs = set(G[j]) - seen
|
||||
# Only compute the edge weight once, before the inner inner
|
||||
# loop.
|
||||
wij = wt(i, j)
|
||||
weighted_triangles += sum(
|
||||
np.cbrt([(wij * wt(j, k) * wt(k, i)) for k in inbrs & jnbrs])
|
||||
)
|
||||
yield (i, len(inbrs), 2 * weighted_triangles)
|
||||
|
||||
|
||||
def _local_directed_weighted_triangles_and_degree_parallel(
|
||||
nodes_nbrs, G, weight, max_weight
|
||||
):
|
||||
ret = []
|
||||
|
||||
def wt(u, v):
|
||||
return G[u][v].get(weight, 1) / max_weight
|
||||
|
||||
for i, preds, succs in nodes_nbrs:
|
||||
ipreds = set(preds) - {i}
|
||||
isuccs = set(succs) - {i}
|
||||
|
||||
directed_triangles = 0
|
||||
for j in ipreds:
|
||||
jpreds = set(G._pred[j]) - {j}
|
||||
jsuccs = set(G._adj[j]) - {j}
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs])
|
||||
)
|
||||
|
||||
for j in isuccs:
|
||||
jpreds = set(G._pred[j]) - {j}
|
||||
jsuccs = set(G._adj[j]) - {j}
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs])
|
||||
)
|
||||
|
||||
dtotal = len(ipreds) + len(isuccs)
|
||||
dbidirectional = len(ipreds & isuccs)
|
||||
ret.append([i, dtotal, dbidirectional, directed_triangles])
|
||||
return ret
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def _directed_weighted_triangles_and_degree_iter(
|
||||
G, nodes=None, weight="weight", n_workers=None
|
||||
):
|
||||
"""Return an iterator of
|
||||
(node, total_degree, reciprocal_degree, directed_weighted_triangles).
|
||||
|
||||
Used for directed weighted clustering.
|
||||
Note that unlike `_weighted_triangles_and_degree_iter()`, this function counts
|
||||
directed triangles so does not count triangles twice.
|
||||
|
||||
"""
|
||||
|
||||
if weight is None or G.number_of_edges() == 0:
|
||||
max_weight = 1
|
||||
else:
|
||||
max_weight = max(d.get(weight, 1) for u, v, d in G.edges)
|
||||
|
||||
nodes_nbrs = ((n, G._pred[n], G._adj[n]) for n in G.nbunch_iter(nodes))
|
||||
|
||||
def wt(u, v):
|
||||
return G[u][v].get(weight, 1) / max_weight
|
||||
|
||||
if n_workers is not None:
|
||||
import random
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
|
||||
_local_directed_weighted_triangles_and_degree_function = partial(
|
||||
_local_directed_weighted_triangles_and_degree_parallel,
|
||||
G=G,
|
||||
weight=weight,
|
||||
max_weight=max_weight,
|
||||
)
|
||||
nodes_nbrs = list(nodes_nbrs)
|
||||
random.shuffle(nodes_nbrs)
|
||||
if len(nodes_nbrs) > n_workers * 30000:
|
||||
nodes_nbrs = split_len(nodes, step=30000)
|
||||
else:
|
||||
nodes_nbrs = split(nodes_nbrs, n_workers)
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(
|
||||
_local_directed_weighted_triangles_and_degree_function, nodes_nbrs
|
||||
)
|
||||
for r in ret:
|
||||
for x in r:
|
||||
yield x
|
||||
|
||||
else:
|
||||
for i, preds, succs in nodes_nbrs:
|
||||
ipreds = set(preds) - {i}
|
||||
isuccs = set(succs) - {i}
|
||||
|
||||
directed_triangles = 0
|
||||
for j in ipreds:
|
||||
jpreds = set(G._pred[j]) - {j}
|
||||
jsuccs = set(G._adj[j]) - {j}
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs])
|
||||
)
|
||||
|
||||
for j in isuccs:
|
||||
jpreds = set(G._pred[j]) - {j}
|
||||
jsuccs = set(G._adj[j]) - {j}
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs])
|
||||
)
|
||||
|
||||
dtotal = len(ipreds) + len(isuccs)
|
||||
dbidirectional = len(ipreds & isuccs)
|
||||
yield (i, dtotal, dbidirectional, directed_triangles)
|
||||
|
||||
|
||||
def average_clustering(G, nodes=None, weight=None, count_zeros=True, n_workers=None):
|
||||
r"""Compute the average clustering coefficient for the graph G.
|
||||
|
||||
The clustering coefficient for the graph is the average,
|
||||
|
||||
.. math::
|
||||
|
||||
C = \frac{1}{n}\sum_{v \in G} c_v,
|
||||
|
||||
where :math:`n` is the number of nodes in `G`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
|
||||
nodes : container of nodes, optional (default=all nodes in G)
|
||||
Compute average clustering for nodes in this container.
|
||||
|
||||
weight : string or None, optional (default=None)
|
||||
The edge attribute that holds the numerical value used as a weight.
|
||||
If None, then each edge has weight 1.
|
||||
|
||||
count_zeros : bool
|
||||
If False include only the nodes with nonzero clustering in the average.
|
||||
|
||||
Returns
|
||||
-------
|
||||
avg : float
|
||||
Average clustering
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.complete_graph(5)
|
||||
>>> print(eg.average_clustering(G))
|
||||
1.0
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is a space saving routine; it might be faster
|
||||
to use the clustering function to get a list and then take the average.
|
||||
|
||||
Self loops are ignored.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Generalizations of the clustering coefficient to weighted
|
||||
complex networks by J. Saramäki, M. Kivelä, J.-P. Onnela,
|
||||
K. Kaski, and J. Kertész, Physical Review E, 75 027105 (2007).
|
||||
http://jponnela.com/web_documents/a9.pdf
|
||||
.. [2] Marcus Kaiser, Mean clustering coefficients: the role of isolated
|
||||
nodes and leafs on clustering measures for small-world networks.
|
||||
https://arxiv.org/abs/0802.2512
|
||||
"""
|
||||
c = clustering(G, nodes, weight=weight, n_workers=n_workers).values()
|
||||
if not count_zeros:
|
||||
c = [v for v in c if abs(v) > 0]
|
||||
return sum(c) / len(c)
|
||||
|
||||
|
||||
def _local_directed_triangles_and_degree_iter_parallel(nodes_nbrs, G):
|
||||
ret = []
|
||||
for i, preds, succs in nodes_nbrs:
|
||||
ipreds = set(preds) - {i}
|
||||
isuccs = set(succs) - {i}
|
||||
|
||||
directed_triangles = 0
|
||||
for j in chain(ipreds, isuccs):
|
||||
jpreds = set(G._pred[j]) - {j}
|
||||
jsuccs = set(G._adj[j]) - {j}
|
||||
directed_triangles += sum(
|
||||
1
|
||||
for k in chain(
|
||||
(ipreds & jpreds),
|
||||
(ipreds & jsuccs),
|
||||
(isuccs & jpreds),
|
||||
(isuccs & jsuccs),
|
||||
)
|
||||
)
|
||||
dtotal = len(ipreds) + len(isuccs)
|
||||
dbidirectional = len(ipreds & isuccs)
|
||||
ret.append((i, dtotal, dbidirectional, directed_triangles))
|
||||
return ret
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def _directed_triangles_and_degree_iter(G, nodes=None, n_workers=None):
|
||||
"""Return an iterator of
|
||||
(node, total_degree, reciprocal_degree, directed_triangles).
|
||||
|
||||
Used for directed clustering.
|
||||
Note that unlike `_triangles_and_degree_iter()`, this function counts
|
||||
directed triangles so does not count triangles twice.
|
||||
|
||||
"""
|
||||
nodes_nbrs = ((n, G._pred[n], G._adj[n]) for n in G.nbunch_iter(nodes))
|
||||
|
||||
if n_workers is not None:
|
||||
import random
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
|
||||
_local_directed_triangles_and_degree_iter_parallel_function = partial(
|
||||
_local_directed_triangles_and_degree_iter_parallel, G=G
|
||||
)
|
||||
nodes_nbrs = list(nodes_nbrs)
|
||||
random.shuffle(nodes_nbrs)
|
||||
if len(nodes_nbrs) > n_workers * 30000:
|
||||
nodes_nbrs = split_len(nodes_nbrs, step=30000)
|
||||
else:
|
||||
nodes_nbrs = split(nodes_nbrs, n_workers)
|
||||
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(
|
||||
_local_directed_triangles_and_degree_iter_parallel_function, nodes_nbrs
|
||||
)
|
||||
for r in ret:
|
||||
for x in r:
|
||||
yield x
|
||||
else:
|
||||
for i, preds, succs in nodes_nbrs:
|
||||
ipreds = set(preds) - {i}
|
||||
isuccs = set(succs) - {i}
|
||||
|
||||
directed_triangles = 0
|
||||
for j in chain(ipreds, isuccs):
|
||||
jpreds = set(G._pred[j]) - {j}
|
||||
jsuccs = set(G._adj[j]) - {j}
|
||||
directed_triangles += sum(
|
||||
1
|
||||
for k in chain(
|
||||
(ipreds & jpreds),
|
||||
(ipreds & jsuccs),
|
||||
(isuccs & jpreds),
|
||||
(isuccs & jsuccs),
|
||||
)
|
||||
)
|
||||
dtotal = len(ipreds) + len(isuccs)
|
||||
dbidirectional = len(ipreds & isuccs)
|
||||
yield (i, dtotal, dbidirectional, directed_triangles)
|
||||
|
||||
|
||||
def _local_triangles_and_degree_iter_function_parallel(nodes_nbrs, G):
|
||||
ret = []
|
||||
for v, v_nbrs in nodes_nbrs:
|
||||
vs = set(v_nbrs) - {v}
|
||||
gen_degree = Counter(len(vs & (set(G[w]) - {w})) for w in vs)
|
||||
ntriangles = sum(k * val for k, val in gen_degree.items())
|
||||
ret.append((v, len(vs), ntriangles, gen_degree))
|
||||
return ret
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def _triangles_and_degree_iter(G, nodes=None, n_workers=None):
|
||||
"""Return an iterator of (node, degree, triangles, generalized degree).
|
||||
|
||||
This double counts triangles so you may want to divide by 2.
|
||||
See degree(), triangles() and generalized_degree() for definitions
|
||||
and details.
|
||||
|
||||
"""
|
||||
if nodes is None:
|
||||
nodes_nbrs = G.adj.items()
|
||||
else:
|
||||
nodes_nbrs = ((n, G[n]) for n in G.nbunch_iter(nodes))
|
||||
|
||||
if n_workers is not None:
|
||||
import random
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
|
||||
_local_triangles_and_degree_iter_function = partial(
|
||||
_local_triangles_and_degree_iter_function_parallel, G=G
|
||||
)
|
||||
nodes_nbrs = list(nodes_nbrs)
|
||||
random.shuffle(nodes_nbrs)
|
||||
if len(nodes_nbrs) > n_workers * 30000:
|
||||
nodes_nbrs = split_len(nodes_nbrs, step=30000)
|
||||
else:
|
||||
nodes_nbrs = split(nodes_nbrs, n_workers)
|
||||
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(_local_triangles_and_degree_iter_function, nodes_nbrs)
|
||||
for r in ret:
|
||||
for x in r:
|
||||
yield x
|
||||
else:
|
||||
for v, v_nbrs in nodes_nbrs:
|
||||
vs = set(v_nbrs) - {v}
|
||||
gen_degree = Counter(len(vs & (set(G[w]) - {w})) for w in vs)
|
||||
ntriangles = sum(k * val for k, val in gen_degree.items())
|
||||
yield (v, len(vs), ntriangles, gen_degree)
|
||||
|
||||
|
||||
@hybrid("cpp_clustering")
|
||||
def clustering(G, nodes=None, weight=None, n_workers=None):
|
||||
r"""Compute the clustering coefficient for nodes.
|
||||
|
||||
For unweighted graphs, the clustering of a node :math:`u`
|
||||
is the fraction of possible triangles through that node that exist,
|
||||
|
||||
.. math::
|
||||
|
||||
c_u = \frac{2 T(u)}{deg(u)(deg(u)-1)},
|
||||
|
||||
where :math:`T(u)` is the number of triangles through node :math:`u` and
|
||||
:math:`deg(u)` is the degree of :math:`u`.
|
||||
|
||||
For weighted graphs, there are several ways to define clustering [1]_.
|
||||
the one used here is defined
|
||||
as the geometric average of the subgraph edge weights [2]_,
|
||||
|
||||
.. math::
|
||||
|
||||
c_u = \frac{1}{deg(u)(deg(u)-1))}
|
||||
\sum_{vw} (\hat{w}_{uv} \hat{w}_{uw} \hat{w}_{vw})^{1/3}.
|
||||
|
||||
The edge weights :math:`\hat{w}_{uv}` are normalized by the maximum weight
|
||||
in the network :math:`\hat{w}_{uv} = w_{uv}/\max(w)`.
|
||||
|
||||
The value of :math:`c_u` is assigned to 0 if :math:`deg(u) < 2`.
|
||||
|
||||
Additionally, this weighted definition has been generalized to support negative edge weights [3]_.
|
||||
|
||||
For directed graphs, the clustering is similarly defined as the fraction
|
||||
of all possible directed triangles or geometric average of the subgraph
|
||||
edge weights for unweighted and weighted directed graph respectively [4]_.
|
||||
|
||||
.. math::
|
||||
|
||||
c_u = \frac{2}{deg^{tot}(u)(deg^{tot}(u)-1) - 2deg^{\leftrightarrow}(u)}
|
||||
T(u),
|
||||
|
||||
where :math:`T(u)` is the number of directed triangles through node
|
||||
:math:`u`, :math:`deg^{tot}(u)` is the sum of in degree and out degree of
|
||||
:math:`u` and :math:`deg^{\leftrightarrow}(u)` is the reciprocal degree of
|
||||
:math:`u`.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
|
||||
nodes : container of nodes, optional (default=all nodes in G)
|
||||
Compute clustering for nodes in this container.
|
||||
|
||||
weight : string or None, optional (default=None)
|
||||
The edge attribute that holds the numerical value used as a weight.
|
||||
If None, then each edge has weight 1.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : float, or dictionary
|
||||
Clustering coefficient at specified nodes
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.complete_graph(5)
|
||||
>>> print(eg.clustering(G, 0))
|
||||
1.0
|
||||
>>> print(eg.clustering(G))
|
||||
{0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0}
|
||||
|
||||
Notes
|
||||
-----
|
||||
Self loops are ignored.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Generalizations of the clustering coefficient to weighted
|
||||
complex networks by J. Saramäki, M. Kivelä, J.-P. Onnela,
|
||||
K. Kaski, and J. Kertész, Physical Review E, 75 027105 (2007).
|
||||
http://jponnela.com/web_documents/a9.pdf
|
||||
.. [2] Intensity and coherence of motifs in weighted complex
|
||||
networks by J. P. Onnela, J. Saramäki, J. Kertész, and K. Kaski,
|
||||
Physical Review E, 71(6), 065103 (2005).
|
||||
.. [3] Generalization of Clustering Coefficients to Signed Correlation Networks
|
||||
by G. Costantini and M. Perugini, PloS one, 9(2), e88669 (2014).
|
||||
.. [4] Clustering in complex directed networks by G. Fagiolo,
|
||||
Physical Review E, 76(2), 026107 (2007).
|
||||
"""
|
||||
|
||||
if G.is_directed():
|
||||
if weight is not None:
|
||||
td_iter = _directed_weighted_triangles_and_degree_iter(
|
||||
G, nodes, weight, n_workers=n_workers
|
||||
)
|
||||
clusterc = {
|
||||
v: 0 if t == 0 else t / ((dt * (dt - 1) - 2 * db) * 2)
|
||||
for v, dt, db, t in td_iter
|
||||
}
|
||||
else:
|
||||
td_iter = _directed_triangles_and_degree_iter(G, nodes, n_workers=n_workers)
|
||||
clusterc = {
|
||||
v: 0 if t == 0 else t / ((dt * (dt - 1) - 2 * db) * 2)
|
||||
for v, dt, db, t in td_iter
|
||||
}
|
||||
else:
|
||||
# The formula 2*T/(d*(d-1)) from docs is t/(d*(d-1)) here b/c t==2*T
|
||||
if weight is not None:
|
||||
td_iter = _weighted_triangles_and_degree_iter(
|
||||
G, nodes, weight, n_workers=n_workers
|
||||
)
|
||||
clusterc = {v: 0 if t == 0 else t / (d * (d - 1)) for v, d, t in td_iter}
|
||||
else:
|
||||
td_iter = _triangles_and_degree_iter(G, nodes, n_workers=n_workers)
|
||||
clusterc = {v: 0 if t == 0 else t / (d * (d - 1)) for v, d, t, _ in td_iter}
|
||||
if nodes in G:
|
||||
# Return the value of the sole entry in the dictionary.
|
||||
return clusterc[nodes]
|
||||
return clusterc
|
||||
@@ -0,0 +1,226 @@
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
import scipy.sparse as sparse
|
||||
|
||||
|
||||
__all__ = [
|
||||
"localAssort",
|
||||
]
|
||||
|
||||
|
||||
def localAssort(
|
||||
edgelist, node_attr, pr=np.arange(0.0, 1.0, 0.1), undir=True, missingValue=-1
|
||||
):
|
||||
"""Calculate the multiscale assortativity.
|
||||
You must ensure that the node index and node attribute index start from 0
|
||||
Parameters
|
||||
----------
|
||||
edgelist : array_like
|
||||
the network represented as an edge list,
|
||||
i.e., a E x 2 array of node pairs
|
||||
node_attr : array_like
|
||||
n length array of node attribute values
|
||||
pr : array, optional
|
||||
array of one minus restart probabilities for the random walk in
|
||||
calculating the personalised pagerank. The largest of these values
|
||||
determines the accuracy of the TotalRank vector max(pr) -> 1 is more
|
||||
accurate (default: [0, .1, .2, .3, .4, .5, .6, .7, .8, .9])
|
||||
undir : bool, optional
|
||||
indicate if network is undirected (default: True)
|
||||
missingValue : int, optional
|
||||
token to indicate missing attribute values (default: -1)
|
||||
Returns
|
||||
-------
|
||||
assortM : array_like
|
||||
n x len(pr) array of local assortativities, each column corresponds to
|
||||
a value of the input restart probabilities, pr. Note if only number of
|
||||
restart probabilties is greater than one (i.e., len(pr) > 1).
|
||||
assortT : array_like
|
||||
n length array of multiscale assortativities
|
||||
Z : array_like
|
||||
N length array of per-node confidence scores
|
||||
References
|
||||
----------
|
||||
For full details see [1]_
|
||||
.. [1] Peel, L., Delvenne, J. C., & Lambiotte, R. (2018). "Multiscale
|
||||
mixing patterns in networks.' PNAS, 115(16), 4057-4062.
|
||||
"""
|
||||
# number of nodes
|
||||
n = len(node_attr)
|
||||
|
||||
# number of nodes with complete attribute
|
||||
ncomp = (node_attr != missingValue).sum()
|
||||
# number of edges
|
||||
m = len(edgelist)
|
||||
# construct adjacency matrix and calculate degree sequence
|
||||
A, degree = createA(edgelist, n, undir)
|
||||
|
||||
# construct diagonal inverse degree matrix
|
||||
D = sparse.diags(1.0 / degree, 0, format="csc")
|
||||
|
||||
# construct transition matrix (row normalised adjacency matrix)
|
||||
W = D @ A
|
||||
|
||||
# number of distinct node categories
|
||||
c = len(np.unique(node_attr))
|
||||
if ncomp < n:
|
||||
c -= 1
|
||||
|
||||
# calculate node weights for how "complete" the
|
||||
# metadata is around the node
|
||||
Z = np.zeros(n)
|
||||
|
||||
Z[node_attr == missingValue] = 1.0
|
||||
|
||||
Z = (W @ Z) / degree
|
||||
|
||||
# indicator array if node has attribute data (or missing)
|
||||
hasAttribute = node_attr != missingValue
|
||||
|
||||
# calculate global expected values
|
||||
values = np.ones(ncomp)
|
||||
|
||||
yi = (hasAttribute).nonzero()[0]
|
||||
|
||||
yj = node_attr[hasAttribute]
|
||||
Y = sparse.coo_matrix((values, (yi, yj)), shape=(n, c)).tocsc()
|
||||
eij_glob = np.array(Y.T @ (A @ Y).todense())
|
||||
|
||||
eij_glob /= np.sum(eij_glob)
|
||||
|
||||
ab_glob = np.sum(eij_glob.sum(1) * eij_glob.sum(0))
|
||||
# initialise outputs
|
||||
assortM = np.empty((n, len(pr)))
|
||||
assortT = np.empty(n)
|
||||
WY = (W @ Y).tocsc()
|
||||
|
||||
for i in range(n):
|
||||
pis, ti, it = calculateRWRrange(W, i, pr, n)
|
||||
if len(pr) > 1:
|
||||
for ii, pri in enumerate(pr):
|
||||
pi = pis[:, ii]
|
||||
|
||||
YPI = sparse.coo_matrix(
|
||||
(
|
||||
pi[hasAttribute],
|
||||
(node_attr[hasAttribute], np.arange(n)[hasAttribute]),
|
||||
),
|
||||
shape=(c, n),
|
||||
).tocsr()
|
||||
trace_e = (YPI.dot(WY).toarray()).trace()
|
||||
assortM[i, ii] = trace_e
|
||||
YPI = sparse.coo_matrix(
|
||||
(ti[hasAttribute], (node_attr[hasAttribute], np.arange(n)[hasAttribute])),
|
||||
shape=(c, n),
|
||||
).tocsr()
|
||||
e_gh = (YPI @ WY).toarray()
|
||||
e_gh_sum = e_gh.sum()
|
||||
Z[i] = e_gh_sum
|
||||
e_gh /= e_gh_sum
|
||||
trace_e = e_gh.trace()
|
||||
assortT[i] = trace_e
|
||||
|
||||
assortT -= ab_glob
|
||||
np.divide(assortT, 1.0 - ab_glob, out=assortT, where=ab_glob != 0)
|
||||
|
||||
if len(pr) > 1:
|
||||
assortM -= ab_glob
|
||||
np.divide(assortM, 1.0 - ab_glob, out=assortM, where=ab_glob != 0)
|
||||
return assortM, assortT, Z
|
||||
return None, assortT, Z
|
||||
|
||||
|
||||
def createA(E, n, undir=True):
|
||||
"""Create adjacency matrix and degree sequence."""
|
||||
if undir:
|
||||
G = eg.Graph()
|
||||
else:
|
||||
G = eg.DiGraph()
|
||||
G.add_nodes_from(range(n))
|
||||
|
||||
for e in E:
|
||||
G.add_edge(e[0], e[1])
|
||||
|
||||
A = eg.to_scipy_sparse_matrix(G)
|
||||
|
||||
degree = np.array(A.sum(1)).flatten()
|
||||
|
||||
return A, degree
|
||||
|
||||
|
||||
def calculateRWRrange(W, i, alphas, n, maxIter=1000):
|
||||
"""
|
||||
Calculate the personalised TotalRank and personalised PageRank vectors.
|
||||
Parameters
|
||||
----------
|
||||
W : array_like
|
||||
transition matrix (row normalised adjacency matrix)
|
||||
i : int
|
||||
index of the personalisation node
|
||||
alphas : array_like
|
||||
array of (1 - restart probabilties)
|
||||
n : int
|
||||
number of nodes in the network
|
||||
maxIter : int, optional
|
||||
maximum number of interations (default: 1000)
|
||||
Returns
|
||||
-------
|
||||
pPageRank_all : array_like
|
||||
personalised PageRank for all input alpha values (only calculated if
|
||||
more than one alpha given as input, i.e., len(alphas) > 1)
|
||||
pTotalRank : array_like
|
||||
personalised TotalRank (personalised PageRank with alpha integrated
|
||||
out)
|
||||
|
||||
it : int
|
||||
number of iterations
|
||||
References
|
||||
----------
|
||||
See [2]_ and [3]_ for further details.
|
||||
.. [2] Boldi, P. (2005). "TotalRank: Ranking without damping." In Special
|
||||
interest tracks and posters of the 14th international conference on
|
||||
World Wide Web (pp. 898-899).
|
||||
.. [3] Boldi, P., Santini, M., & Vigna, S. (2007). "A deeper investigation
|
||||
of PageRank as a function of the damping factor." In Dagstuhl Seminar
|
||||
Proceedings. Schloss Dagstuhl-Leibniz-Zentrum für Informatik.
|
||||
"""
|
||||
alpha0 = alphas.max()
|
||||
WT = alpha0 * W.T
|
||||
diff = 1
|
||||
it = 1
|
||||
|
||||
# initialise PageRank vectors
|
||||
pPageRank = np.zeros(n)
|
||||
|
||||
pPageRank_all = np.zeros((n, len(alphas)))
|
||||
pPageRank[i] = 1
|
||||
|
||||
pPageRank_all[i, :] = 1
|
||||
|
||||
pPageRank_old = pPageRank.copy()
|
||||
pTotalRank = pPageRank.copy()
|
||||
|
||||
oneminusalpha0 = 1 - alpha0
|
||||
|
||||
while diff > 1e-9:
|
||||
# calculate personalised PageRank via power iteration
|
||||
pPageRank = WT @ pPageRank
|
||||
pPageRank[i] += oneminusalpha0
|
||||
# calculate difference in pPageRank from previous iteration
|
||||
delta_pPageRank = pPageRank - pPageRank_old
|
||||
# Eq. [S23] Ref. [1]
|
||||
pTotalRank += (delta_pPageRank) / ((it + 1) * (alpha0**it))
|
||||
# only calculate personalised pageranks if more than one alpha
|
||||
if len(alphas) > 1:
|
||||
pPageRank_all += np.outer((delta_pPageRank), (alphas / alpha0) ** it)
|
||||
|
||||
# calculate convergence criteria
|
||||
diff = np.sum((delta_pPageRank) ** 2) / n
|
||||
it += 1
|
||||
|
||||
if it > maxIter:
|
||||
print(i, "max iterations exceeded")
|
||||
diff = 0
|
||||
pPageRank_old = pPageRank.copy()
|
||||
|
||||
return pPageRank_all, pTotalRank, it
|
||||
@@ -0,0 +1,101 @@
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
__all__ = [
|
||||
"predecessor",
|
||||
]
|
||||
|
||||
|
||||
def predecessor(G, source, target=None, cutoff=None, return_seen=None):
|
||||
"""Returns dict of predecessors for the path from source to all nodes in G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph graph
|
||||
|
||||
source : node label
|
||||
Starting node for path
|
||||
|
||||
target : node label, optional
|
||||
Ending node for path. If provided only predecessors between
|
||||
source and target are returned
|
||||
|
||||
cutoff : integer, optional
|
||||
Depth to stop the search. Only paths of length <= cutoff are returned.
|
||||
|
||||
return_seen : bool, optional (default=None)
|
||||
Whether to return a dictionary, keyed by node, of the level (number of
|
||||
hops) to reach the node (as seen during breadth-first-search).
|
||||
|
||||
Returns
|
||||
-------
|
||||
pred : dictionary
|
||||
Dictionary, keyed by node, of predecessors in the shortest path.
|
||||
|
||||
|
||||
(pred, seen): tuple of dictionaries
|
||||
If `return_seen` argument is set to `True`, then a tuple of dictionaries
|
||||
is returned. The first element is the dictionary, keyed by node, of
|
||||
predecessors in the shortest path. The second element is the dictionary,
|
||||
keyed by node, of the level (number of hops) to reach the node (as seen
|
||||
during breadth-first-search).
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.path_graph(4)
|
||||
>>> list(G)
|
||||
[0, 1, 2, 3]
|
||||
>>> eg.predecessor(G, 0)
|
||||
{0: [], 1: [0], 2: [1], 3: [2]}
|
||||
>>> eg.predecessor(G, 0, return_seen=True)
|
||||
({0: [], 1: [0], 2: [1], 3: [2]}, {0: 0, 1: 1, 2: 2, 3: 3})
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if source not in G:
|
||||
raise eg.NodeNotFound(f"Source {source} not in G")
|
||||
level = 0 # the current level
|
||||
nextlevel = [source] # list of nodes to check at next level
|
||||
seen = {source: level} # level (number of hops) when seen in BFS
|
||||
pred = {source: []} # predecessor dictionary
|
||||
while nextlevel:
|
||||
level = level + 1
|
||||
thislevel = nextlevel
|
||||
nextlevel = []
|
||||
for v in thislevel:
|
||||
for w in list(G.neighbors(v)):
|
||||
if w not in seen:
|
||||
pred[w] = [v]
|
||||
seen[w] = level
|
||||
nextlevel.append(w)
|
||||
elif seen[w] == level: # add v to predecessor list if it
|
||||
pred[w].append(v) # is at the correct level
|
||||
if cutoff and cutoff <= level:
|
||||
break
|
||||
|
||||
if target is not None:
|
||||
if return_seen:
|
||||
if target not in pred:
|
||||
return ([], -1) # No predecessor
|
||||
return (pred[target], seen[target])
|
||||
else:
|
||||
if target not in pred:
|
||||
return [] # No predecessor
|
||||
return pred[target]
|
||||
else:
|
||||
if return_seen:
|
||||
return (pred, seen)
|
||||
else:
|
||||
return pred
|
||||
|
||||
|
||||
# def main():
|
||||
# G = eg.path_graph(4)
|
||||
# print(G.edges)
|
||||
|
||||
# print(predecessor(G, 0))
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# main()
|
||||
@@ -0,0 +1,41 @@
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
from easygraph.functions.basic import average_degree
|
||||
|
||||
|
||||
def test_average_degree_basic():
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(1, 2), (2, 3)])
|
||||
assert average_degree(G) == pytest.approx(4 / 3)
|
||||
|
||||
|
||||
def test_average_degree_empty_graph():
|
||||
G = eg.Graph()
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
average_degree(G)
|
||||
|
||||
|
||||
def test_average_degree_self_loop():
|
||||
G = eg.Graph()
|
||||
G.add_edge(1, 1) # self-loop
|
||||
# Self-loop counts as 2 towards degree of node 1
|
||||
assert average_degree(G) == pytest.approx(2.0)
|
||||
|
||||
|
||||
def test_average_degree_with_isolated_node():
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(1, 2), (2, 3)])
|
||||
G.add_node(4) # isolated node
|
||||
assert average_degree(G) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_average_degree_directed_graph():
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(1, 2), (2, 3), (3, 1)])
|
||||
assert average_degree(G) == pytest.approx(2.0)
|
||||
|
||||
|
||||
def test_average_degree_invalid_input():
|
||||
with pytest.raises(AttributeError):
|
||||
average_degree(None)
|
||||
@@ -0,0 +1,418 @@
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
|
||||
class TestClustering:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
pytest.importorskip("numpy")
|
||||
|
||||
def test_clustering(self):
|
||||
G = eg.DiGraph()
|
||||
G.add_edge("1", "2", weight=16)
|
||||
G.add_edge("2", "3", weight=16)
|
||||
G.add_edge("4", "3", weight=16)
|
||||
G.add_edge("3", "4", weight=23)
|
||||
G.add_edge("3", "5", weight=16)
|
||||
G.add_edge("4", "2", weight=20)
|
||||
print("clustering" in dir(eg))
|
||||
assert eg.clustering(G) == {
|
||||
"1": 0,
|
||||
"2": 0.3333333333333333,
|
||||
"3": 0.2,
|
||||
"4": 0.5,
|
||||
"5": 0,
|
||||
}
|
||||
|
||||
def test_path(self):
|
||||
G = eg.path_graph(10)
|
||||
assert list(eg.clustering(G).values()) == [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
assert eg.clustering(G) == {
|
||||
0: 0,
|
||||
1: 0,
|
||||
2: 0,
|
||||
3: 0,
|
||||
4: 0,
|
||||
5: 0,
|
||||
6: 0,
|
||||
7: 0,
|
||||
8: 0,
|
||||
9: 0,
|
||||
}
|
||||
|
||||
def test_k5(self):
|
||||
G = eg.complete_graph(5)
|
||||
assert list(eg.clustering(G).values()) == [1, 1, 1, 1, 1]
|
||||
assert eg.average_clustering(G) == 1
|
||||
G.remove_edge(1, 2)
|
||||
assert list(eg.clustering(G).values()) == [
|
||||
5 / 6,
|
||||
1,
|
||||
1,
|
||||
5 / 6,
|
||||
5 / 6,
|
||||
]
|
||||
assert eg.clustering(G, [1, 4]) == {1: 1, 4: 0.83333333333333337}
|
||||
|
||||
def test_k5_signed(self):
|
||||
G = eg.complete_graph(5)
|
||||
assert list(eg.clustering(G).values()) == [1, 1, 1, 1, 1]
|
||||
assert eg.average_clustering(G) == 1
|
||||
G.remove_edge(1, 2)
|
||||
G.add_edge(0, 1, weight=-1)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
1 / 6,
|
||||
-1 / 3,
|
||||
1,
|
||||
3 / 6,
|
||||
3 / 6,
|
||||
]
|
||||
|
||||
|
||||
class TestDirectedClustering:
|
||||
def test_clustering(self):
|
||||
G = eg.DiGraph()
|
||||
assert list(eg.clustering(G).values()) == []
|
||||
assert eg.clustering(G) == {}
|
||||
|
||||
def test_path(self):
|
||||
G = eg.path_graph(10, create_using=eg.DiGraph())
|
||||
assert list(eg.clustering(G).values()) == [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
assert eg.clustering(G) == {
|
||||
0: 0,
|
||||
1: 0,
|
||||
2: 0,
|
||||
3: 0,
|
||||
4: 0,
|
||||
5: 0,
|
||||
6: 0,
|
||||
7: 0,
|
||||
8: 0,
|
||||
9: 0,
|
||||
}
|
||||
assert eg.clustering(G, 0) == 0
|
||||
|
||||
def test_k5(self):
|
||||
G = eg.complete_graph(5, create_using=eg.DiGraph())
|
||||
assert list(eg.clustering(G).values()) == [1, 1, 1, 1, 1]
|
||||
assert eg.average_clustering(G) == 1
|
||||
G.remove_edge(1, 2)
|
||||
assert list(eg.clustering(G).values()) == [
|
||||
11 / 12,
|
||||
1,
|
||||
1,
|
||||
11 / 12,
|
||||
11 / 12,
|
||||
]
|
||||
assert eg.clustering(G, [1, 4]) == {1: 1, 4: 11 / 12}
|
||||
G.remove_edge(2, 1)
|
||||
assert list(eg.clustering(G).values()) == [
|
||||
5 / 6,
|
||||
1,
|
||||
1,
|
||||
5 / 6,
|
||||
5 / 6,
|
||||
]
|
||||
assert eg.clustering(G, [1, 4]) == {1: 1, 4: 0.83333333333333337}
|
||||
assert eg.clustering(G, 4) == 5 / 6
|
||||
|
||||
def test_triangle_and_edge(self):
|
||||
G = eg.empty_graph(range(3), eg.DiGraph())
|
||||
G.add_edges_from(eg.pairwise(range(3), cyclic=True))
|
||||
G.add_edge(0, 4)
|
||||
assert eg.clustering(G)[0] == 1 / 6
|
||||
|
||||
|
||||
class TestDirectedAverageClustering:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
pytest.importorskip("numpy")
|
||||
|
||||
def test_empty(self):
|
||||
G = eg.DiGraph()
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
eg.average_clustering(G)
|
||||
|
||||
def test_average_clustering(self):
|
||||
G = eg.empty_graph(range(3), eg.DiGraph())
|
||||
G.add_edges_from(eg.pairwise(range(3), cyclic=True))
|
||||
G.add_edge(2, 3)
|
||||
assert eg.average_clustering(G) == (1 + 1 + 1 / 3) / 8
|
||||
assert eg.average_clustering(G, count_zeros=True) == (1 + 1 + 1 / 3) / 8
|
||||
assert eg.average_clustering(G, count_zeros=False) == (1 + 1 + 1 / 3) / 6
|
||||
assert eg.average_clustering(G, [1, 2, 3]) == (1 + 1 / 3) / 6
|
||||
assert eg.average_clustering(G, [1, 2, 3], count_zeros=True) == (1 + 1 / 3) / 6
|
||||
assert eg.average_clustering(G, [1, 2, 3], count_zeros=False) == (1 + 1 / 3) / 4
|
||||
|
||||
|
||||
class TestAverageClustering:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
pytest.importorskip("numpy")
|
||||
|
||||
def test_empty(self):
|
||||
G = eg.Graph()
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
eg.average_clustering(G)
|
||||
|
||||
def test_average_clustering(self):
|
||||
G = eg.complete_graph(3)
|
||||
G.add_edge(2, 3)
|
||||
|
||||
assert eg.average_clustering(G) == (1 + 1 + 1 / 3) / 4
|
||||
assert eg.average_clustering(G, count_zeros=True) == (1 + 1 + 1 / 3) / 4
|
||||
assert eg.average_clustering(G, count_zeros=False) == (1 + 1 + 1 / 3) / 3
|
||||
assert eg.average_clustering(G, [1, 2, 3]) == (1 + 1 / 3) / 3
|
||||
assert eg.average_clustering(G, [1, 2, 3], count_zeros=True) == (1 + 1 / 3) / 3
|
||||
assert eg.average_clustering(G, [1, 2, 3], count_zeros=False) == (1 + 1 / 3) / 2
|
||||
|
||||
def test_average_clustering_signed(self):
|
||||
G = eg.complete_graph(3)
|
||||
G.add_edge(2, 3)
|
||||
G.add_edge(0, 1, weight=-1)
|
||||
assert eg.average_clustering(G, weight="weight") == (-1 - 1 - 1 / 3) / 4
|
||||
assert (
|
||||
eg.average_clustering(G, weight="weight", count_zeros=True)
|
||||
== (-1 - 1 - 1 / 3) / 4
|
||||
)
|
||||
assert (
|
||||
eg.average_clustering(G, weight="weight", count_zeros=False)
|
||||
== (-1 - 1 - 1 / 3) / 3
|
||||
)
|
||||
|
||||
|
||||
class TestDirectedWeightedClustering:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
global np
|
||||
np = pytest.importorskip("numpy")
|
||||
|
||||
def test_clustering(self):
|
||||
G = eg.DiGraph()
|
||||
assert list(eg.clustering(G, weight="weight").values()) == []
|
||||
assert eg.clustering(G) == {}
|
||||
|
||||
def test_path(self):
|
||||
G = eg.path_graph(10, create_using=eg.DiGraph())
|
||||
print("type:", eg.clustering(G, weight="weight"))
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
assert eg.clustering(G, weight="weight") == {
|
||||
0: 0,
|
||||
1: 0,
|
||||
2: 0,
|
||||
3: 0,
|
||||
4: 0,
|
||||
5: 0,
|
||||
6: 0,
|
||||
7: 0,
|
||||
8: 0,
|
||||
9: 0,
|
||||
}
|
||||
|
||||
def test_k5(self):
|
||||
G = eg.complete_graph(5, create_using=eg.DiGraph())
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [1, 1, 1, 1, 1]
|
||||
assert eg.average_clustering(G, weight="weight") == 1
|
||||
G.remove_edge(1, 2)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
11 / 12,
|
||||
1,
|
||||
1,
|
||||
11 / 12,
|
||||
11 / 12,
|
||||
]
|
||||
assert eg.clustering(G, [1, 4], weight="weight") == {1: 1, 4: 11 / 12}
|
||||
G.remove_edge(2, 1)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
5 / 6,
|
||||
1,
|
||||
1,
|
||||
5 / 6,
|
||||
5 / 6,
|
||||
]
|
||||
assert eg.clustering(G, [1, 4], weight="weight") == {
|
||||
1: 1,
|
||||
4: 0.83333333333333337,
|
||||
}
|
||||
|
||||
def test_triangle_and_edge(self):
|
||||
G = eg.empty_graph(range(3), create_using=eg.DiGraph())
|
||||
G.add_edges_from(eg.pairwise(range(3), cyclic=True))
|
||||
G.add_edge(0, 4, weight=2)
|
||||
assert eg.clustering(G)[0] == 1 / 6
|
||||
# Relaxed comparisons to allow graphblas-algorithms to pass tests
|
||||
np.testing.assert_allclose(eg.clustering(G, weight="weight")[0], 1 / 12)
|
||||
np.testing.assert_allclose(eg.clustering(G, 0, weight="weight"), 1 / 12)
|
||||
|
||||
|
||||
class TestWeightedClustering:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
global np
|
||||
np = pytest.importorskip("numpy")
|
||||
|
||||
def test_clustering(self):
|
||||
G = eg.Graph()
|
||||
assert list(eg.clustering(G, weight="weight").values()) == []
|
||||
assert eg.clustering(G) == {}
|
||||
|
||||
def test_path(self):
|
||||
G = eg.path_graph(10)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
assert eg.clustering(G, weight="weight") == {
|
||||
0: 0,
|
||||
1: 0,
|
||||
2: 0,
|
||||
3: 0,
|
||||
4: 0,
|
||||
5: 0,
|
||||
6: 0,
|
||||
7: 0,
|
||||
8: 0,
|
||||
9: 0,
|
||||
}
|
||||
|
||||
def test_cubical(self):
|
||||
G = eg.from_dict_of_lists(
|
||||
{
|
||||
0: [1, 3, 4],
|
||||
1: [0, 2, 7],
|
||||
2: [1, 3, 6],
|
||||
3: [0, 2, 5],
|
||||
4: [0, 5, 7],
|
||||
5: [3, 4, 6],
|
||||
6: [2, 5, 7],
|
||||
7: [1, 4, 6],
|
||||
},
|
||||
create_using=None,
|
||||
)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
assert eg.clustering(G, 1) == 0
|
||||
assert list(eg.clustering(G, [1, 2], weight="weight").values()) == [0, 0]
|
||||
assert eg.clustering(G, 1, weight="weight") == 0
|
||||
assert eg.clustering(G, [1, 2], weight="weight") == {1: 0, 2: 0}
|
||||
|
||||
def test_k5(self):
|
||||
G = eg.complete_graph(5)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [1, 1, 1, 1, 1]
|
||||
assert eg.average_clustering(G, weight="weight") == 1
|
||||
G.remove_edge(1, 2)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
5 / 6,
|
||||
1,
|
||||
1,
|
||||
5 / 6,
|
||||
5 / 6,
|
||||
]
|
||||
assert eg.clustering(G, [1, 4], weight="weight") == {
|
||||
1: 1,
|
||||
4: 0.83333333333333337,
|
||||
}
|
||||
|
||||
def test_triangle_and_edge(self):
|
||||
G = eg.empty_graph(range(3), None)
|
||||
G.add_edges_from(eg.pairwise(range(3), cyclic=True))
|
||||
G.add_edge(0, 4, weight=2)
|
||||
assert eg.clustering(G)[0] == 1 / 3
|
||||
np.testing.assert_allclose(eg.clustering(G, weight="weight")[0], 1 / 6)
|
||||
np.testing.assert_allclose(eg.clustering(G, 0, weight="weight"), 1 / 6)
|
||||
|
||||
def test_triangle_and_signed_edge(self):
|
||||
G = eg.empty_graph(range(3), None)
|
||||
G.add_edges_from(eg.pairwise(range(3), cyclic=True))
|
||||
G.add_edge(0, 1, weight=-1)
|
||||
G.add_edge(3, 0, weight=0)
|
||||
assert eg.clustering(G)[0] == 1 / 3
|
||||
assert eg.clustering(G, weight="weight")[0] == -1 / 3
|
||||
|
||||
|
||||
class TestAdditionalClusteringCases:
|
||||
def test_self_loops_ignored(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (1, 2), (2, 0)])
|
||||
G.add_edge(0, 0) # self-loop
|
||||
assert eg.clustering(G, 0) == 1.0
|
||||
|
||||
def test_isolated_node(self):
|
||||
G = eg.Graph()
|
||||
G.add_node(1)
|
||||
assert eg.clustering(G) == {1: 0}
|
||||
|
||||
def test_degree_one_node(self):
|
||||
G = eg.Graph()
|
||||
G.add_edge(1, 2)
|
||||
assert eg.clustering(G) == {1: 0, 2: 0}
|
||||
|
||||
def test_custom_weight_name(self):
|
||||
G = eg.Graph()
|
||||
G.add_edge(0, 1, strength=2)
|
||||
G.add_edge(1, 2, strength=2)
|
||||
G.add_edge(2, 0, strength=2)
|
||||
result = eg.clustering(G, weight="strength")
|
||||
assert result[0] > 0
|
||||
|
||||
def test_negative_weights_mixed(self):
|
||||
G = eg.complete_graph(3)
|
||||
G[0][1]["weight"] = -1
|
||||
G[1][2]["weight"] = 1
|
||||
G[2][0]["weight"] = 1
|
||||
assert eg.clustering(G, 0, weight="weight") < 0
|
||||
|
||||
def test_directed_reciprocal_edges(self):
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(0, 1), (1, 0), (0, 2), (2, 0), (1, 2), (2, 1)])
|
||||
result = eg.clustering(G)
|
||||
assert all(0 <= v <= 1 for v in result.values())
|
||||
@@ -0,0 +1,104 @@
|
||||
import sys
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from easygraph.functions.basic.localassort import localAssort
|
||||
|
||||
|
||||
class TestLocalAssort:
|
||||
@classmethod
|
||||
def setup_class(self):
|
||||
self.G = eg.get_graph_karateclub()
|
||||
edgelist = []
|
||||
node_num = len(self.G.nodes)
|
||||
for e in self.G.edges:
|
||||
edgelist.append([e[0] - 1, e[1] - 1])
|
||||
self.edgelist = np.int32(edgelist)
|
||||
self.valuelist = np.arange(node_num, dtype=np.int32) % 6
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info.major <= 3 and sys.version_info.minor <= 7,
|
||||
reason="python version should higher than 3.7",
|
||||
)
|
||||
def test_karateclub(self):
|
||||
assortM, assortT, Z = eg.localAssort(
|
||||
self.edgelist, self.valuelist, pr=np.arange(0, 1, 0.1)
|
||||
)
|
||||
|
||||
_, assortT, Z = eg.functions.basic.localassort.localAssort(
|
||||
self.edgelist, self.valuelist, pr=np.array([0.9])
|
||||
)
|
||||
|
||||
|
||||
def test_localassort_small_complete_graph():
|
||||
G = eg.complete_graph(4)
|
||||
edgelist = np.array(list(G.edges))
|
||||
node_attr = np.array([0, 0, 1, 1])
|
||||
assortM, assortT, Z = localAssort(edgelist, node_attr)
|
||||
assert assortM.shape == (4, 10)
|
||||
assert assortT.shape == (4,)
|
||||
assert Z.shape == (4,)
|
||||
assert np.all(Z >= 0) and np.all(Z <= 1)
|
||||
|
||||
|
||||
def test_localassort_with_missing_attributes():
|
||||
G = eg.path_graph(5)
|
||||
edgelist = np.array(list(G.edges))
|
||||
node_attr = np.array([0, -1, 1, -1, 1])
|
||||
assortM, assortT, Z = localAssort(edgelist, node_attr, pr=np.array([0.5]))
|
||||
assert assortT.shape == (5,)
|
||||
assert Z.shape == (5,)
|
||||
assert np.any(np.isnan(assortT))
|
||||
|
||||
|
||||
def test_localassort_directed_graph():
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
edgelist = np.array(list(G.edges))
|
||||
node_attr = np.array([0, 1, 0, 1])
|
||||
assortM, assortT, Z = localAssort(edgelist, node_attr, undir=False)
|
||||
assert assortM.shape == (4, 10)
|
||||
assert assortT.shape == (4,)
|
||||
assert Z.shape == (4,)
|
||||
|
||||
|
||||
def test_localassort_single_node_graph():
|
||||
edgelist = np.empty((0, 2), dtype=int)
|
||||
node_attr = np.array([0])
|
||||
assortM, assortT, Z = localAssort(edgelist, node_attr)
|
||||
assert assortM.shape == (1, 10)
|
||||
assert np.all(np.isnan(assortM)) or np.allclose(assortM, 0, atol=1e-5)
|
||||
assert np.all(np.isnan(assortT)) or np.allclose(assortT, 0, atol=1e-5)
|
||||
assert np.all(np.isnan(Z)) or np.allclose(Z, 0, atol=1e-5)
|
||||
|
||||
|
||||
def test_localassort_disconnected_graph():
|
||||
G = eg.Graph()
|
||||
G.add_nodes_from(range(5))
|
||||
edgelist = np.empty((0, 2), dtype=int)
|
||||
node_attr = np.array([0, 1, 0, 1, 1])
|
||||
assortM, assortT, Z = localAssort(edgelist, node_attr)
|
||||
assert assortM.shape == (5, 10)
|
||||
assert np.all(np.isnan(assortM)) or np.allclose(assortM, 0, atol=1e-5)
|
||||
assert np.all(np.isnan(assortT)) or np.allclose(assortT, 0, atol=1e-5)
|
||||
assert np.all(np.isnan(Z)) or np.allclose(Z, 0, atol=1e-5)
|
||||
|
||||
|
||||
def test_localassort_high_restart_probabilities():
|
||||
G = eg.path_graph(5)
|
||||
edgelist = np.array(list(G.edges))
|
||||
node_attr = np.array([1, 0, 1, 0, 1])
|
||||
pr = np.array([0.95, 0.99])
|
||||
assortM, assortT, Z = localAssort(edgelist, node_attr, pr=pr)
|
||||
assert assortM.shape == (5, 2)
|
||||
assert assortT.shape == (5,)
|
||||
assert Z.shape == (5,)
|
||||
|
||||
|
||||
def test_localassort_invalid_attribute_length():
|
||||
edgelist = np.array([[0, 1], [1, 2]])
|
||||
node_attr = np.array([0, 1]) # too short
|
||||
with pytest.raises(ValueError):
|
||||
localAssort(edgelist, node_attr)
|
||||
@@ -0,0 +1,79 @@
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
|
||||
class TestPredecessor:
|
||||
# @classmethod
|
||||
# def setup_class(self):
|
||||
# pytest.importskip("numpy")
|
||||
|
||||
def test_predecessor(self):
|
||||
G = eg.path_graph(4)
|
||||
for source in G:
|
||||
assert eg.predecessor(G, source) in [
|
||||
{0: [], 1: [0], 2: [1], 3: [2]},
|
||||
{1: [], 0: [1], 2: [1], 3: [2]},
|
||||
{2: [], 1: [2], 3: [2], 0: [1]},
|
||||
{3: [], 2: [3], 1: [2], 0: [1]},
|
||||
]
|
||||
|
||||
def test_basic_predecessor(self):
|
||||
G = eg.path_graph(4)
|
||||
result = eg.predecessor(G, 0)
|
||||
assert result == {0: [], 1: [0], 2: [1], 3: [2]}
|
||||
|
||||
def test_with_return_seen(self):
|
||||
G = eg.path_graph(4)
|
||||
pred, seen = eg.predecessor(G, 0, return_seen=True)
|
||||
assert pred == {0: [], 1: [0], 2: [1], 3: [2]}
|
||||
assert seen == {0: 0, 1: 1, 2: 2, 3: 3}
|
||||
|
||||
def test_with_target(self):
|
||||
G = eg.path_graph(4)
|
||||
assert eg.predecessor(G, 0, target=2) == [1]
|
||||
|
||||
def test_with_target_and_return_seen(self):
|
||||
G = eg.path_graph(4)
|
||||
pred, seen = eg.predecessor(G, 0, target=2, return_seen=True)
|
||||
assert pred == [1]
|
||||
assert seen == 2
|
||||
|
||||
def test_with_cutoff(self):
|
||||
G = eg.path_graph(4)
|
||||
pred = eg.predecessor(G, 0, cutoff=1)
|
||||
assert pred == {0: [], 1: [0]}
|
||||
|
||||
def test_disconnected_graph(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (2, 3)])
|
||||
pred = eg.predecessor(G, 0)
|
||||
assert 2 not in pred and 3 not in pred
|
||||
|
||||
def test_invalid_source(self):
|
||||
G = eg.path_graph(4)
|
||||
with pytest.raises(eg.NodeNotFound):
|
||||
eg.predecessor(G, 99)
|
||||
|
||||
def test_no_path_to_target(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (2, 3)])
|
||||
assert eg.predecessor(G, 0, target=3) == []
|
||||
|
||||
def test_no_path_to_target_with_return_seen(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (2, 3)])
|
||||
pred, seen = eg.predecessor(G, 0, target=3, return_seen=True)
|
||||
assert pred == []
|
||||
assert seen == -1
|
||||
|
||||
def test_cycle_graph(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)]) # cycled graph
|
||||
pred = eg.predecessor(G, 0)
|
||||
assert set(pred.keys()) == set(G.nodes)
|
||||
|
||||
def test_directed_graph(self):
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
pred = eg.predecessor(G, 0)
|
||||
assert pred == {0: [], 1: [0], 2: [1], 3: [2]}
|
||||
@@ -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()
|
||||
@@ -0,0 +1,761 @@
|
||||
import copy
|
||||
import random
|
||||
|
||||
from collections import defaultdict
|
||||
from queue import Queue
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
|
||||
from easygraph.utils import *
|
||||
|
||||
|
||||
__all__ = [
|
||||
"LPA",
|
||||
"SLPA",
|
||||
"HANP",
|
||||
"BMLPA",
|
||||
]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def LPA(G):
|
||||
"""Detect community by label propagation algorithm
|
||||
Return the detected communities. But the result is random.
|
||||
Each node in the network is initially assigned to its own community. At every iteration,nodes have
|
||||
a label that the maximum number of their neighbors have. If there are more than one nodes fit and
|
||||
available, choose a label randomly. Finally, nodes having the same labels are grouped together as
|
||||
communities. In case two or more disconnected groups of nodes have the same label, we run a simple
|
||||
breadth-first search to separate the disconnected communities
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph graph
|
||||
|
||||
Returns
|
||||
----------
|
||||
communities : dictionary
|
||||
key: serial number of community , value: nodes in the community.
|
||||
|
||||
Examples
|
||||
----------
|
||||
>>> LPA(G)
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Usha Nandini Raghavan, Réka Albert, and Soundar Kumara:
|
||||
Near linear time algorithm to detect community structures in large-scale networks
|
||||
"""
|
||||
i = 0
|
||||
label_dict = dict()
|
||||
cluster_community = dict()
|
||||
Next_label_dict = dict()
|
||||
nodes = list(G.nodes.keys())
|
||||
if len(nodes) == 1:
|
||||
return {1: [nodes[0]]}
|
||||
for node in nodes:
|
||||
label_dict[node] = i
|
||||
i = i + 1
|
||||
loop_count = 0
|
||||
while True:
|
||||
loop_count += 1
|
||||
random.shuffle(nodes)
|
||||
for node in nodes:
|
||||
labels = SelectLabels(G, node, label_dict)
|
||||
if labels == []:
|
||||
Next_label_dict[node] = label_dict[node]
|
||||
continue
|
||||
Next_label_dict[node] = random.choice(labels)
|
||||
# Asynchronous updates. If you want to use synchronous updates, comment the line below
|
||||
label_dict[node] = Next_label_dict[node]
|
||||
label_dict = Next_label_dict
|
||||
if estimate_stop_cond(G, label_dict) is True:
|
||||
break
|
||||
for node in label_dict.keys():
|
||||
label = label_dict[node]
|
||||
if label not in cluster_community.keys():
|
||||
cluster_community[label] = [node]
|
||||
else:
|
||||
cluster_community[label].append(node)
|
||||
|
||||
result_community = CheckConnectivity(G, cluster_community)
|
||||
return result_community
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def SLPA(G, T, r):
|
||||
"""Detect Overlapping Communities by Speaker-listener Label Propagation Algorithm
|
||||
Return the detected Overlapping communities. But the result is random.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph graph.
|
||||
T : int
|
||||
The number of iterations, In general, T is set greater than 20, which produces relatively stable outputs.
|
||||
r : int
|
||||
a threshold between 0 and 1.
|
||||
|
||||
Returns
|
||||
-------
|
||||
communities : dictionary
|
||||
key: serial number of community , value: nodes in the community.
|
||||
|
||||
Examples
|
||||
----------
|
||||
>>> SLPA(G,
|
||||
... T = 20,
|
||||
... r = 0.05
|
||||
... )
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Jierui Xie, Boleslaw K. Szymanski, Xiaoming Liu:
|
||||
SLPA: Uncovering Overlapping Communities in Social Networks via A Speaker-listener Interaction Dynamic Process
|
||||
"""
|
||||
nodes = list(G.nodes.keys())
|
||||
if len(nodes) == 1:
|
||||
return {1: [nodes[0]]}
|
||||
nodes = G.nodes
|
||||
adj = G.adj
|
||||
memory = {i: {i: 1} for i in nodes}
|
||||
for i in range(0, T):
|
||||
listenerslist = list(G.nodes)
|
||||
random.shuffle(listenerslist)
|
||||
for listener in listenerslist:
|
||||
speakerlist = adj[listener]
|
||||
if len(speakerlist) == 0:
|
||||
continue
|
||||
labels = defaultdict(int)
|
||||
for speaker in speakerlist:
|
||||
# Speaker Rule
|
||||
total = float(sum(memory[speaker].values()))
|
||||
keys = list(memory[speaker].keys())
|
||||
index = np.random.multinomial(
|
||||
1, [round(freq / total, 2) for freq in memory[speaker].values()]
|
||||
).argmax()
|
||||
chosen_label = keys[index]
|
||||
labels[chosen_label] += 1
|
||||
# Listener Rule
|
||||
maxlabel = max(labels.items(), key=lambda x: x[1])[0]
|
||||
if maxlabel in memory[listener]:
|
||||
memory[listener][maxlabel] += 1
|
||||
else:
|
||||
memory[listener][maxlabel] = 1
|
||||
|
||||
for node, labels in memory.items():
|
||||
name_list = []
|
||||
for label_name, label_number in labels.items():
|
||||
if round(label_number / float(T + 1), 2) < r:
|
||||
name_list.append(label_name)
|
||||
for name in name_list:
|
||||
del labels[name]
|
||||
|
||||
# Find nodes membership
|
||||
communities = {}
|
||||
for node, labels in memory.items():
|
||||
for label in labels:
|
||||
if label in communities:
|
||||
communities[label].add(node)
|
||||
else:
|
||||
communities[label] = {node}
|
||||
|
||||
# Remove nested communities
|
||||
RemoveNested(communities)
|
||||
|
||||
# Check Connectivity
|
||||
result_community = CheckConnectivity(G, communities)
|
||||
return result_community
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def HANP(G, m, delta, threshod=1, hier_open=0, combine_open=0):
|
||||
"""Detect community by Hop attenuation & node preference algorithm
|
||||
|
||||
Return the detected communities. But the result is random.
|
||||
|
||||
Implement the basic HANP algorithm and give more freedom through the parameters, e.g., you can use threshod
|
||||
to set the condition for node updating. If network are known to be Hierarchical and overlapping communities,
|
||||
it's recommended to choose geodesic distance as the measure(instead of receiving the current hop scores
|
||||
from the neighborhood and carry out a subtraction) and When an equilibrium is reached, treat newly combined
|
||||
communities as a single node.
|
||||
|
||||
For using Floyd to get the shortest distance, the time complexity is a little high.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph graph
|
||||
m : float
|
||||
Used to calculate score, when m > 0, more preference is given to node with more neighbors; m < 0, less
|
||||
delta : float
|
||||
Hop attenuation
|
||||
threshod : float
|
||||
Between 0 and 1, only update node whose number of neighbors sharing the maximal label is less than the threshod.
|
||||
e.g., threshod == 1 means updating all nodes.
|
||||
hier_open :
|
||||
1 means using geodesic distance as the score measure.
|
||||
0 means not.
|
||||
combine_open :
|
||||
this option is valid only when hier_open = 1
|
||||
1 means When an equilibrium is reached, treat newly combined communities as a single node.
|
||||
0 means not.
|
||||
|
||||
Returns
|
||||
----------
|
||||
communities : dictionary
|
||||
key: serial number of community , value: nodes in the community.
|
||||
|
||||
Examples
|
||||
----------
|
||||
>>> HANP(G,
|
||||
... m = 0.1,
|
||||
... delta = 0.05,
|
||||
... threshod = 1,
|
||||
... hier_open = 0,
|
||||
... combine_open = 0
|
||||
... )
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Ian X. Y. Leung, Pan Hui, Pietro Liò, and Jon Crowcrof:
|
||||
Towards real-time community detection in large networks
|
||||
|
||||
"""
|
||||
nodes = list(G.nodes.keys())
|
||||
if len(nodes) == 1:
|
||||
return {1: [nodes[0]]}
|
||||
label_dict = dict()
|
||||
score_dict = dict()
|
||||
node_dict = dict()
|
||||
Next_label_dict = dict()
|
||||
cluster_community = dict()
|
||||
nodes = list(G.nodes.keys())
|
||||
degrees = G.degree()
|
||||
records = []
|
||||
loop_count = 0
|
||||
i = 0
|
||||
old_score = 1
|
||||
ori_G = G
|
||||
if hier_open == 1:
|
||||
distance_dict = eg.Floyd(G)
|
||||
for node in nodes:
|
||||
label_dict[node] = i
|
||||
score_dict[i] = 1
|
||||
node_dict[i] = node
|
||||
i = i + 1
|
||||
while True:
|
||||
loop_count += 1
|
||||
random.shuffle(nodes)
|
||||
score = 1
|
||||
for node in nodes:
|
||||
labels = SelectLabels_HANP(
|
||||
G, node, label_dict, score_dict, degrees, m, threshod
|
||||
)
|
||||
if labels == []:
|
||||
Next_label_dict[node] = label_dict[node]
|
||||
continue
|
||||
old_label = label_dict[node]
|
||||
Next_label_dict[node] = random.choice(labels)
|
||||
# Asynchronous updates. If you want to use synchronous updates, comment the line below
|
||||
label_dict[node] = Next_label_dict[node]
|
||||
if hier_open == 1:
|
||||
score_dict[Next_label_dict[node]] = UpdateScore_Hier(
|
||||
G, node, label_dict, node_dict, distance_dict
|
||||
)
|
||||
score = min(score, score_dict[Next_label_dict[node]])
|
||||
else:
|
||||
if old_label == Next_label_dict[node]:
|
||||
cdelta = 0
|
||||
else:
|
||||
cdelta = delta
|
||||
score_dict[Next_label_dict[node]] = UpdateScore(
|
||||
G, node, label_dict, score_dict, cdelta
|
||||
)
|
||||
if hier_open == 1 and combine_open == 1:
|
||||
if old_score - score > 1 / 3:
|
||||
old_score = score
|
||||
(
|
||||
records,
|
||||
G,
|
||||
label_dict,
|
||||
score_dict,
|
||||
node_dict,
|
||||
Next_label_dict,
|
||||
nodes,
|
||||
degrees,
|
||||
distance_dict,
|
||||
) = CombineNodes(
|
||||
records,
|
||||
G,
|
||||
label_dict,
|
||||
score_dict,
|
||||
node_dict,
|
||||
Next_label_dict,
|
||||
nodes,
|
||||
degrees,
|
||||
distance_dict,
|
||||
)
|
||||
label_dict = Next_label_dict
|
||||
if (
|
||||
estimate_stop_cond_HANP(G, label_dict, score_dict, degrees, m, threshod)
|
||||
is True
|
||||
):
|
||||
break
|
||||
"""As mentioned in the paper, it's suggested that the number of iterations
|
||||
required is independent to the number of nodes and that after
|
||||
five iterations, 95% of their nodes are already accurately clustered
|
||||
"""
|
||||
if loop_count > 20:
|
||||
break
|
||||
print("After %d iterations, HANP complete." % loop_count)
|
||||
for node in label_dict.keys():
|
||||
label = label_dict[node]
|
||||
if label not in cluster_community.keys():
|
||||
cluster_community[label] = [node]
|
||||
else:
|
||||
cluster_community[label].append(node)
|
||||
if hier_open == 1 and combine_open == 1:
|
||||
records.append(cluster_community)
|
||||
cluster_community = ShowRecord(records)
|
||||
result_community = CheckConnectivity(ori_G, cluster_community)
|
||||
return result_community
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def BMLPA(G, p):
|
||||
"""Detect community by Balanced Multi-Label Propagation algorithm
|
||||
|
||||
Return the detected communities.
|
||||
|
||||
Firstly, initialize 'old' using cores generated by RC function, the propagate label till the number and size
|
||||
of communities stay no change, check if there are subcommunity and delete it. Finally, split discontinuous
|
||||
communities.
|
||||
|
||||
For some directed graphs lead to oscillations of labels, modify the stop condition.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph graph
|
||||
p : float
|
||||
Between 0 and 1, judge Whether a community identifier should be retained
|
||||
|
||||
Returns
|
||||
----------
|
||||
communities : dictionary
|
||||
key: serial number of community , value: nodes in the community.
|
||||
|
||||
Examples
|
||||
----------
|
||||
>>> BMLPA(G,
|
||||
... p = 0.1,
|
||||
... )
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Wu Zhihao, Lin You-Fang, Gregory Steve, Wan Huai-Yu, Tian Sheng-Feng
|
||||
Balanced Multi-Label Propagation for Overlapping Community Detection in Social Networks
|
||||
|
||||
"""
|
||||
nodes = list(G.nodes.keys())
|
||||
if len(nodes) == 1:
|
||||
return {1: [nodes[0]]}
|
||||
cores = Rough_Cores(G)
|
||||
nodes = G.nodes
|
||||
i = 0
|
||||
old_label_dict = dict()
|
||||
new_label_dict = dict()
|
||||
for core in cores:
|
||||
for node in core:
|
||||
if node not in old_label_dict:
|
||||
old_label_dict[node] = {i: 1}
|
||||
else:
|
||||
old_label_dict[node][i] = 1
|
||||
i += 1
|
||||
oldMin = dict()
|
||||
loop_count = 0
|
||||
old_label_dictx = dict()
|
||||
while True:
|
||||
loop_count += 1
|
||||
old_label_dictx = old_label_dict
|
||||
for node in nodes:
|
||||
Propagate_bbc(G, node, old_label_dict, new_label_dict, p)
|
||||
if loop_count > 50 and old_label_dict == old_label_dictx:
|
||||
break
|
||||
Min = dict()
|
||||
if Id(old_label_dict) == Id(new_label_dict):
|
||||
Min = mc(count(old_label_dict), count(new_label_dict))
|
||||
else:
|
||||
Min = count(new_label_dict)
|
||||
if loop_count > 500:
|
||||
break
|
||||
if Min != oldMin:
|
||||
old_label_dict = copy.deepcopy(new_label_dict)
|
||||
oldMin = copy.deepcopy(Min)
|
||||
else:
|
||||
break
|
||||
print("After %d iterations, BMLPA complete." % loop_count)
|
||||
communities = dict()
|
||||
for node in nodes:
|
||||
for label, _ in old_label_dict[node].items():
|
||||
if label in communities:
|
||||
communities[label].add(node)
|
||||
else:
|
||||
communities[label] = {node}
|
||||
RemoveNested(communities)
|
||||
result_community = CheckConnectivity(G, communities)
|
||||
return result_community
|
||||
|
||||
|
||||
def RemoveNested(communities):
|
||||
nestedCommunities = set()
|
||||
keys = list(communities.keys())
|
||||
for i, label0 in enumerate(keys[:-1]):
|
||||
comm0 = communities[label0]
|
||||
for label1 in keys[i + 1 :]:
|
||||
comm1 = communities[label1]
|
||||
if comm0.issubset(comm1):
|
||||
nestedCommunities.add(label0)
|
||||
elif comm0.issuperset(comm1):
|
||||
nestedCommunities.add(label1)
|
||||
for comm in nestedCommunities:
|
||||
del communities[comm]
|
||||
|
||||
|
||||
def SelectLabels(G, node, label_dict):
|
||||
adj = G.adj
|
||||
count = {}
|
||||
count_items = []
|
||||
for neighbor in adj[node]:
|
||||
neighbor_label = label_dict[neighbor]
|
||||
count[neighbor_label] = count.get(neighbor_label, 0) + 1
|
||||
count_items = sorted(count.items(), key=lambda x: x[1], reverse=True)
|
||||
labels = [k for k, v in count_items if v == count_items[0][1]]
|
||||
return labels
|
||||
|
||||
|
||||
def estimate_stop_cond(G, label_dict):
|
||||
for node in G.nodes:
|
||||
if SelectLabels(G, node, label_dict) != [] and (
|
||||
label_dict[node] not in SelectLabels(G, node, label_dict)
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def SelectLabels_HANP(G, node, label_dict, score_dict, degrees, m, threshod):
|
||||
adj = G.adj
|
||||
count = defaultdict(float)
|
||||
cnt = defaultdict(int)
|
||||
for neighbor in adj[node]:
|
||||
neighbor_label = label_dict[neighbor]
|
||||
cnt[neighbor_label] += 1
|
||||
count[neighbor_label] += (
|
||||
score_dict[neighbor_label]
|
||||
* (degrees[neighbor] ** m)
|
||||
* adj[node][neighbor].get("weight", 1)
|
||||
)
|
||||
count_items = sorted(count.items(), key=lambda x: x[1], reverse=True)
|
||||
labels = [k for k, v in count_items if v == count_items[0][1]]
|
||||
# only update node whose number of neighbors sharing the maximal label is less than a certain percentage.
|
||||
if count_items == []:
|
||||
return []
|
||||
if round(cnt[count_items[0][0]] / len(adj[node]), 2) > threshod:
|
||||
return [label_dict[node]]
|
||||
return labels
|
||||
|
||||
|
||||
def HopAttenuation_Hier(G, node, label_dict, node_dict, distance_dict):
|
||||
distance = float("inf")
|
||||
Max_distance = 0
|
||||
adj = G.adj
|
||||
label = label_dict[node]
|
||||
ori_node = node_dict[label]
|
||||
for _, distancex in distance_dict[ori_node].items():
|
||||
Max_distance = max(Max_distance, distancex)
|
||||
for neighbor in adj[node]:
|
||||
if label_dict[neighbor] == label:
|
||||
distance = min(distance, distance_dict[ori_node][neighbor])
|
||||
return round((1 + distance) / Max_distance, 2)
|
||||
|
||||
|
||||
def UpdateScore_Hier(G, node, label_dict, node_dict, distance_dict):
|
||||
return 1 - HopAttenuation_Hier(G, node, label_dict, node_dict, distance_dict)
|
||||
|
||||
|
||||
def UpdateScore(G, node, label_dict, score_dict, delta):
|
||||
adj = G.adj
|
||||
Max_score = 0
|
||||
label = label_dict[node]
|
||||
for neighbor in adj[node]:
|
||||
if label_dict[neighbor] == label:
|
||||
Max_score = max(Max_score, score_dict[label_dict[neighbor]])
|
||||
return Max_score - delta
|
||||
|
||||
|
||||
def estimate_stop_cond_HANP(G, label_dict, score_dict, degrees, m, threshod):
|
||||
for node in G.nodes:
|
||||
if SelectLabels_HANP(
|
||||
G, node, label_dict, score_dict, degrees, m, threshod
|
||||
) != [] and label_dict[node] not in SelectLabels_HANP(
|
||||
G, node, label_dict, score_dict, degrees, m, threshod
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def CombineNodes(
|
||||
records,
|
||||
G,
|
||||
label_dict,
|
||||
score_dict,
|
||||
node_dict,
|
||||
Next_label_dict,
|
||||
nodes,
|
||||
degrees,
|
||||
distance_dict,
|
||||
):
|
||||
onerecord = dict()
|
||||
for node, label in label_dict.items():
|
||||
if label in onerecord:
|
||||
onerecord[label].append(node)
|
||||
else:
|
||||
onerecord[label] = [node]
|
||||
records.append(onerecord)
|
||||
Gx = eg.Graph()
|
||||
label_dictx = dict()
|
||||
score_dictx = dict()
|
||||
node_dictx = dict()
|
||||
nodesx = []
|
||||
cnt = 0
|
||||
for record_label in onerecord:
|
||||
nodesx.append(cnt)
|
||||
label_dictx[cnt] = record_label
|
||||
score_dictx[record_label] = score_dict[record_label]
|
||||
node_dictx[record_label] = cnt
|
||||
cnt += 1
|
||||
record_labels = list(onerecord.keys())
|
||||
i = 0
|
||||
edge = dict()
|
||||
adj = G.adj
|
||||
for i in range(0, len(record_labels)):
|
||||
edge[i] = dict()
|
||||
for j in range(0, len(record_labels)):
|
||||
if i == j:
|
||||
continue
|
||||
inodes = onerecord[record_labels[i]]
|
||||
jnodes = onerecord[record_labels[j]]
|
||||
for unode in inodes:
|
||||
for vnode in jnodes:
|
||||
if unode in adj and vnode in adj[unode]:
|
||||
if j not in edge[i]:
|
||||
edge[i][j] = 0
|
||||
edge[i][j] += adj[unode][vnode].get("weight", 1)
|
||||
for unode in edge:
|
||||
for vnode, w in edge[unode].items():
|
||||
if unode < vnode:
|
||||
Gx.add_edge(unode, vnode, weight=w)
|
||||
G = Gx
|
||||
label_dict = label_dictx
|
||||
score_dict = score_dictx
|
||||
node_dict = node_dictx
|
||||
Next_label_dict = label_dictx
|
||||
nodes = nodesx
|
||||
degrees = G.degree()
|
||||
distance_dict = eg.Floyd(G)
|
||||
return (
|
||||
records,
|
||||
G,
|
||||
label_dict,
|
||||
score_dict,
|
||||
node_dict,
|
||||
Next_label_dict,
|
||||
nodes,
|
||||
degrees,
|
||||
distance_dict,
|
||||
)
|
||||
|
||||
|
||||
def ShowRecord(records):
|
||||
"""
|
||||
e.g.
|
||||
records : [ {1:[1,2,3,4],2:[5,6,7,8],3:[9],4:[10],5:[11],6:[12]},
|
||||
{2:[0,1,3],3:[2,4,5]},
|
||||
{2:[0,1]} ]
|
||||
|
||||
process : {1:[1,2,3,4],2:[5,6,7,8],3:[9],4:[10],5:[11],6:[12]} ->
|
||||
{2:[ [1,2,3,4] + [5,6,7,8] + [10] ], 3:[ [9] + [11] + [12] ]} ->
|
||||
{2:[ ([ [1,2,3,4] + [5,6,7,8] + [10] ]) + ([ [9] + [11] + [12] ] ]) } ->
|
||||
|
||||
return : {2:[1,2,3,4,5,6,7,8,10,9,11,12]}
|
||||
"""
|
||||
result = dict()
|
||||
first = records[0]
|
||||
for i in range(1, len(records)):
|
||||
keys = list(first.keys())
|
||||
onerecord = records[i]
|
||||
result = {}
|
||||
for label, nodes in onerecord.items():
|
||||
for unode in nodes:
|
||||
for vnode in first[keys[unode]]:
|
||||
if label not in result:
|
||||
result[label] = []
|
||||
result[label].append(vnode)
|
||||
first = result
|
||||
return first
|
||||
|
||||
|
||||
def CheckConnectivity(G, communities):
|
||||
result_community = dict()
|
||||
community = [list(community) for label, community in communities.items()]
|
||||
communityx = []
|
||||
for nodes in community:
|
||||
BFS(G, nodes, communityx)
|
||||
i = 0
|
||||
for com in communityx:
|
||||
i += 1
|
||||
result_community[i] = com
|
||||
return result_community
|
||||
|
||||
|
||||
def BFS(G, nodes, result):
|
||||
# check the nodes in G are connected or not. if not, desperate the nodes into different connected subgraphs.
|
||||
if len(nodes) == 0:
|
||||
return
|
||||
if len(nodes) == 1:
|
||||
result.append(nodes)
|
||||
return
|
||||
adj = G.adj
|
||||
queue = Queue()
|
||||
queue.put(nodes[0])
|
||||
seen = set()
|
||||
seen.add(nodes[0])
|
||||
count = 0
|
||||
while queue.empty() == 0:
|
||||
vertex = queue.get()
|
||||
count += 1
|
||||
for w in adj[vertex]:
|
||||
if w in nodes and w not in seen:
|
||||
queue.put(w)
|
||||
seen.add(w)
|
||||
if count != len(nodes):
|
||||
result.append([w for w in seen])
|
||||
return BFS(G, [w for w in nodes if w not in seen], result)
|
||||
else:
|
||||
result.append(nodes)
|
||||
return
|
||||
|
||||
|
||||
def Rough_Cores(G):
|
||||
nodes = G.nodes
|
||||
degrees = G.degree()
|
||||
adj = G.adj
|
||||
seen_dict = dict()
|
||||
label_dict = dict()
|
||||
cores = []
|
||||
i = 0
|
||||
for node in nodes:
|
||||
label_dict[node] = i
|
||||
seen_dict[node] = 1
|
||||
i += 1
|
||||
degree_list = sorted(degrees.items(), key=lambda x: x[1], reverse=True)
|
||||
for node, _ in degree_list:
|
||||
core = []
|
||||
if degrees[node] >= 3 and seen_dict[node] == 1:
|
||||
for neighbor in adj[node]:
|
||||
max_degree = 0
|
||||
j = node
|
||||
if seen_dict[neighbor] == 1:
|
||||
if degrees[neighbor] > max_degree:
|
||||
max_degree = degrees[neighbor]
|
||||
j = neighbor
|
||||
elif degrees[neighbor] == max_degree:
|
||||
pass
|
||||
if j != []:
|
||||
core = [node] + [j]
|
||||
commNeiber = [i for i in adj[node] if i in adj[j]]
|
||||
commNeiber = [node for node, _ in degree_list if node in commNeiber]
|
||||
commNeiber = commNeiber[::-1]
|
||||
while commNeiber != []:
|
||||
for h in commNeiber:
|
||||
core.append(h)
|
||||
for x in commNeiber:
|
||||
if x not in adj[h]:
|
||||
commNeiber.remove(x)
|
||||
if h in commNeiber:
|
||||
commNeiber.remove(h)
|
||||
if len(core) >= 3:
|
||||
for i in core:
|
||||
seen_dict[i] = 0
|
||||
cores.append(core)
|
||||
core_node = []
|
||||
for core in cores:
|
||||
core_node += core
|
||||
for node in nodes:
|
||||
if node not in core_node:
|
||||
cores.append([node])
|
||||
return cores
|
||||
|
||||
|
||||
def Normalizer(l):
|
||||
Sum = 0
|
||||
for identifier, coefficient in l.items():
|
||||
Sum += coefficient
|
||||
for identifier, coefficient in l.items():
|
||||
l[identifier] = round(coefficient / Sum, 2)
|
||||
|
||||
|
||||
def Propagate_bbc(G, x, source, dest, p):
|
||||
adj = G.adj
|
||||
dest[x] = dict()
|
||||
max_b = 0
|
||||
for y in adj[x]:
|
||||
for identifier, coefficient in source[y].items():
|
||||
b = coefficient
|
||||
if identifier in dest[x]:
|
||||
dest[x][identifier] += b
|
||||
else:
|
||||
dest[x][identifier] = b
|
||||
max_b = max(dest[x][identifier], max_b)
|
||||
if max_b == 0:
|
||||
dest[x] = source[x]
|
||||
return
|
||||
for identifier in list(dest[x].keys()):
|
||||
if dest[x][identifier] / max_b < p:
|
||||
del dest[x][identifier]
|
||||
Normalizer(dest[x])
|
||||
|
||||
|
||||
def Id(l):
|
||||
ids = dict()
|
||||
for x in l:
|
||||
ids[x] = Id1(l[x])
|
||||
return ids
|
||||
|
||||
|
||||
def Id1(x):
|
||||
ids = []
|
||||
for identifier, _ in x.items():
|
||||
if identifier not in ids:
|
||||
ids.append(identifier)
|
||||
return ids
|
||||
|
||||
|
||||
def count(l):
|
||||
counts = dict()
|
||||
for x in l:
|
||||
for identifier, _ in l[x].items():
|
||||
if identifier in counts:
|
||||
counts[identifier] += 1
|
||||
else:
|
||||
counts[identifier] = 1
|
||||
return counts
|
||||
|
||||
|
||||
def mc(cs1, cs2):
|
||||
cs = dict()
|
||||
for identifier, _ in cs1.items():
|
||||
cs[identifier] = min(cs1[identifier], cs2[identifier])
|
||||
return cs
|
||||
@@ -0,0 +1,7 @@
|
||||
from .ego_graph import *
|
||||
from .louvain import *
|
||||
from .LPA import *
|
||||
from .modularity import *
|
||||
from .modularity_max_detection import *
|
||||
from .motif import *
|
||||
from .localsearch import *
|
||||
@@ -0,0 +1,66 @@
|
||||
__all__ = ["ego_graph"]
|
||||
|
||||
# import easygraph as eg
|
||||
from easygraph.functions.path import single_source_dijkstra
|
||||
|
||||
|
||||
def ego_graph(G, n, radius=1, center=True, undirected=False, distance=None):
|
||||
"""Returns induced subgraph of neighbors centered at node n within
|
||||
a given radius.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A EasyGraph Graph or DiGraph
|
||||
|
||||
n : node
|
||||
A single node
|
||||
|
||||
radius : number, optional
|
||||
Include all neighbors of distance<=radius from n.
|
||||
|
||||
center : bool, optional
|
||||
If False, do not include center node in graph
|
||||
|
||||
undirected : bool, optional
|
||||
If True use both in- and out-neighbors of directed graphs.
|
||||
|
||||
distance : key, optional
|
||||
Use specified edge data key as distance. For example, setting
|
||||
distance='weight' will use the edge weight to measure the
|
||||
distance from the node n.
|
||||
|
||||
Notes
|
||||
-----
|
||||
For directed graphs D this produces the "out" neighborhood
|
||||
or successors. If you want the neighborhood of predecessors
|
||||
first reverse the graph with D.reverse(). If you want both
|
||||
directions use the keyword argument undirected=True.
|
||||
|
||||
Node, edge, and graph attributes are copied to the returned subgraph.
|
||||
"""
|
||||
if undirected:
|
||||
"""
|
||||
if distance is not None:
|
||||
sp, _ = eg.single_source_dijkstra(
|
||||
G.to_undirected(), n, cutoff=radius, weight=distance
|
||||
)
|
||||
else:
|
||||
sp = dict(
|
||||
eg.single_source_shortest_path_length(
|
||||
G.to_undirected(), n, cutoff=radius
|
||||
)
|
||||
)
|
||||
"""
|
||||
else:
|
||||
if distance is not None:
|
||||
sp = single_source_dijkstra(G, n, weight=distance)
|
||||
else:
|
||||
sp = single_source_dijkstra(G, n)
|
||||
nodes = [key for key, value in sp.items() if value <= radius]
|
||||
nodes = list(nodes)
|
||||
|
||||
H = G.nodes_subgraph(nodes)
|
||||
if not center:
|
||||
H.remove_node(n)
|
||||
return H
|
||||
@@ -0,0 +1,689 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Created on Tue Dec 21 11:00:36 2021
|
||||
Updated on Sun Jun 09 12:33:06 2024
|
||||
|
||||
Local Search (LS) algorithm proposed in
|
||||
Dingyi Shi, Fan Shang, Bingsheng Chen, Paul Expert, Linyuan Lv, H. Eugene Stanley, Renaud Lambiotte, Tim S. Evans, Ruiqi Li,
|
||||
Local dominance unveils clusters in networks, Communications Physics, 2024, 7:170 [PDF: https://rdcu.be/dJxY0]
|
||||
|
||||
"Hidden directionality unifies community detection and cluster analysis"
|
||||
|
||||
@authors: Fan Shang & Tim S. Evans & Ruiqi Li & Dingyi Shi
|
||||
"""
|
||||
|
||||
import os
|
||||
import random
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
from queue import Queue
|
||||
from datetime import datetime
|
||||
# from LS_other_function import plot_combination
|
||||
|
||||
font = {'family': 'Times New Roman',
|
||||
'style': 'italic',
|
||||
'weight': 'normal',
|
||||
'size': 22,
|
||||
}
|
||||
|
||||
def plot_combination(
|
||||
x,
|
||||
y,
|
||||
text,
|
||||
x1,
|
||||
y1,
|
||||
text1,
|
||||
center_id,
|
||||
subplot_location,
|
||||
xlim_start_end,
|
||||
ylim_start_end,
|
||||
font_location,
|
||||
filepath='./',
|
||||
dataname='LS_default',
|
||||
save=False,
|
||||
show=False):
|
||||
'''
|
||||
input:
|
||||
x:节点的度值(数据类型:list)k
|
||||
y:节点的最短路径(数据类型:list)l
|
||||
x1:节点按照乘积~{k_i} * ~{l_i}的rank排序 (数据类型:list)
|
||||
y1:~{k_i} * ~{l_i}(数据类型:list)
|
||||
text:节点的id(数据类型:list)
|
||||
filepath:需要存储的文件路径(数据类型:str)
|
||||
center_id: LS算法识别的社团中心节点集合(数据类型:list)
|
||||
dataname: 当前网络的名称(数据类型:str)
|
||||
save:是否需要存储文件(数据类型:boolean)
|
||||
return:
|
||||
plot
|
||||
'''
|
||||
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.ticker import MultipleLocator
|
||||
import matplotlib.colors as mc
|
||||
import colorsys
|
||||
except ImportError as exc:
|
||||
raise ImportError("plot_combination requires matplotlib to be installed") from exc
|
||||
|
||||
def adjust_lightness(color, amount=0.5):
|
||||
try:
|
||||
c = mc.cnames[color]
|
||||
except KeyError:
|
||||
c = color
|
||||
hls_color = colorsys.rgb_to_hls(*mc.to_rgb(c))
|
||||
return colorsys.hls_to_rgb(hls_color[0], max(0, min(1, amount * hls_color[1])), hls_color[2])
|
||||
|
||||
fig = plt.figure(figsize=(8, 7))
|
||||
basecolor = '#FFA900'
|
||||
edgecolor = adjust_lightness(basecolor, amount=1)
|
||||
left, bottom, width, height = 0.1, 0.1, 0.8, 0.8
|
||||
ax = fig.add_axes([left, bottom, width, height])
|
||||
for i in range(len(x)):
|
||||
# ax.scatter(x[i], y[i], c=basecolor, marker='o', s=200, edgecolor=edgecolor)
|
||||
if text[i] in center_id:
|
||||
ax.text(x[i], y[i] + font_location, str(text[i]), ha='center', fontsize=12, fontweight='bold')
|
||||
ax.scatter(x, y, c=basecolor, marker='o', s=200)
|
||||
|
||||
if np.max(np.array(x)) // 10 < 1:
|
||||
x_unit = 1
|
||||
else:
|
||||
x_unit = np.max(np.array(x)) // 10
|
||||
if np.max(np.array(y)) // 10 < 1:
|
||||
y_unit = 1
|
||||
else:
|
||||
y_unit = np.max(np.array(y)) // 10
|
||||
x_major_locator = MultipleLocator(x_unit)
|
||||
y_major_locator = MultipleLocator(y_unit)
|
||||
ax.xaxis.set_major_locator(x_major_locator)
|
||||
ax.yaxis.set_major_locator(y_major_locator)
|
||||
ax.set_xlim(xlim_start_end[0], max(x) + xlim_start_end[1])
|
||||
ax.set_ylim(ylim_start_end[0], max(y) + ylim_start_end[1])
|
||||
ax.set_xlabel(r'$k_i$', font)
|
||||
ax.set_ylabel(r'$l_i$', font)
|
||||
ax.tick_params(labelsize=16)
|
||||
|
||||
font1 = {'family': 'Times New Roman',
|
||||
'style': 'italic',
|
||||
'weight': 'normal',
|
||||
'size': 16,
|
||||
}
|
||||
basecolor = '#A73489'
|
||||
edgecolor = adjust_lightness(basecolor, amount=1)
|
||||
# left, bottom, width, height = 0.25,0.595,0.35,0.3 # darkar
|
||||
# left, bottom, width, height = 0.18,0.55,0.35,0.3 # Abidjan
|
||||
# left, bottom, width, height = 0.25,0.55,0.35,0.3 # Beijing
|
||||
# 添加子图
|
||||
left, bottom, width, height = subplot_location[0], subplot_location[1], subplot_location[2], subplot_location[3]
|
||||
ax1 = fig.add_axes([left, bottom, width, height])
|
||||
# 对x1,y1进行log-log处理
|
||||
x1_new = np.log(np.array(x1) + 1)
|
||||
y1_new = []
|
||||
y1_min = min(filter(lambda x: x > 0, y1))
|
||||
for i in range(len(y1)):
|
||||
if y1[i] != 0:
|
||||
y1_new.append(np.log(y1[i]))
|
||||
else:
|
||||
y1_new.append(np.log(y1_min / np.e))
|
||||
# for i in range(len(x1_new)):
|
||||
# # if text1[i] in center_id:
|
||||
# # ax1.scatter(x1_new[i], y1_new[i], color=basecolor, marker='^', s=20, edgecolor=edgecolor)
|
||||
# # else:
|
||||
# # ax1.scatter(x1_new[i], y1_new[i], color=basecolor, marker='o', s=2, edgecolor=edgecolor)
|
||||
# # # ax1.text(x1_new[i], y1_new[i]-0.1, str(int(text1[i])), ha='center', fontsize=10,fontweight='bold')
|
||||
ax1.scatter(x1_new, y1_new, color=basecolor, marker='o', s=2)
|
||||
center_x = []
|
||||
center_y = []
|
||||
for i in range(len(x1_new)):
|
||||
if text1[i] in center_id:
|
||||
center_x.append(x1_new[i])
|
||||
center_y.append(y1_new[i])
|
||||
ax1.scatter(center_x, center_y, color=basecolor, marker='^', s=20)
|
||||
|
||||
ax1.set_xlabel(r'$\ln \, rank$', font1)
|
||||
ax1.set_ylabel(r'$\ln \, ( \~{k_i} \times \~{l_i} ) $', font1)
|
||||
ax1.tick_params(labelsize=16)
|
||||
fig.tight_layout()
|
||||
if save:
|
||||
os.makedirs(filepath, exist_ok=True)
|
||||
filename = os.path.join(filepath, f"{dataname}.pdf")
|
||||
fig.savefig(filename, bbox_inches='tight', dpi=300)
|
||||
if show:
|
||||
plt.show()
|
||||
else:
|
||||
plt.close(fig)
|
||||
return fig
|
||||
|
||||
def max_degree_hierarchy_dag(G, selfloop_nodes=None):
|
||||
'''
|
||||
Create a maximum degree hierarchy DAG from a graph G
|
||||
|
||||
All edges present are from a source node to neighbours which have a larger degree
|
||||
and the degree of these neigthbours is is larger than or equal to than the degree
|
||||
of all the neighbours of the source vertex.
|
||||
|
||||
The difference from the full_degree_hierarchy_dag method is that this
|
||||
does not inlcude links to neighbours which have a higher degree than the source node
|
||||
but still that neighbouir has a degree which is less than the largest degree
|
||||
of all the neighbours.
|
||||
|
||||
This subroutine create the DAG in Fig.1b in the maintext of our paper
|
||||
|
||||
Input
|
||||
-----
|
||||
G -- a simple graph of one component
|
||||
|
||||
Return
|
||||
------
|
||||
D -- A directed acyclic graph
|
||||
'''
|
||||
D = eg.DiGraph()
|
||||
D.add_nodes_from(G)
|
||||
for v in G.nodes:
|
||||
# degree_list = [G.degree(nn) for nn in G.neighbors(v)]
|
||||
degree_list = []
|
||||
for nn in G.neighbors(v):
|
||||
if nn in selfloop_nodes:
|
||||
degree_list.append(G.degree()(nn) + 1)
|
||||
else:
|
||||
degree_list.append(G.degree()[nn])
|
||||
if len(degree_list) > 0:
|
||||
knnmax = max(degree_list)
|
||||
# print(G.degree()[v])
|
||||
if knnmax >= G.degree()[v]: # can also use np.argmax() here
|
||||
# has neighbours with the largest degree so add all of the edges to this neighbour
|
||||
# here edge points from low degree to high degree, points towards tree root
|
||||
e_list = [(v, nn) for nn in G.neighbors(v) if G.degree()[nn] == knnmax and (not D.has_edge(nn, v))]
|
||||
D.add_edges_from(e_list)
|
||||
else:
|
||||
continue
|
||||
# print("! With "+str(G.number_of_nodes())+" nodes, from "+str(G.number_of_edges())+" to "+str(D.number_of_edges())+" edges in Maximum Degree DAG")
|
||||
# print(D.edges())
|
||||
return D
|
||||
|
||||
|
||||
# def full_degree_hierarchy_dag(G,selfloop_nodes=None):
|
||||
# '''
|
||||
# [DEPRECATED] Create a full degree hierarchy DAG from a simple graph G,
|
||||
# which is a variant of the algorithm presented in our paper.
|
||||
# All edges are directed from lower to higher degree nodes, only edges not included are those between equal degree nodes.
|
||||
# Input: G -- a simple graph of one component
|
||||
# Return: D -- A directed acyclic graph
|
||||
# '''
|
||||
# D = eg.DiGraph()
|
||||
# D.add_nodes_from(G)
|
||||
# for v in G.nodes:
|
||||
# kv = G.degree(v)
|
||||
# if v in selfloop_nodes:
|
||||
# kv+=1
|
||||
# e_list = [(v,nn) for nn in G.neighbors(v) if G.degree(nn)>kv] # point to larger degree node
|
||||
# D.add_edges_from( e_list )
|
||||
# # print("! With "+str(G.number_of_nodes())+" nodes, from "+str(G.number_of_edges())+" to "+str(D.number_of_edges())+" edges in Full Degree DAG")
|
||||
# return D
|
||||
|
||||
def degree_hierarchy_random_tree(G, maximum_tree=True, random_seed=None, selfloop_nodes=None):
|
||||
'''
|
||||
Create a degree hierarchy tree from a graph G.
|
||||
|
||||
Unless seed=None, this uses a certain random number series to break ties
|
||||
where neighbours have same (maximum) degree and they are
|
||||
both at the same distance from a root node.
|
||||
|
||||
This subroutine create the DAG comprising all short-dahsed-arrows in Fig.1c in the maintext of our paper
|
||||
|
||||
Input
|
||||
-----
|
||||
G -- an simple graph of one component
|
||||
maximum_tree=True -- If true uses maximum dgree DAG as input, otherwise uses full degree DAG
|
||||
random_seed -- an specific integer to determine the random number series
|
||||
selfloop_nodes -- In the default setting (None), self-loops are not considered; if not None, self-loop will add influence (degree) to the node
|
||||
|
||||
Return
|
||||
------
|
||||
D, tree_edge_list
|
||||
|
||||
D --- A directed acyclic graph (DAG)
|
||||
tree_edge_list --- list of edges in terms of node ID used in G of a shortest path tree in G
|
||||
'''
|
||||
if random_seed != None:
|
||||
random.seed(random_seed)
|
||||
|
||||
if maximum_tree:
|
||||
D = max_degree_hierarchy_dag(G, selfloop_nodes)
|
||||
# D is a DAG in Fig. 1b in the main text of our paper
|
||||
# else:
|
||||
# D=full_degree_hierarchy_dag(G,selfloop_nodes)
|
||||
# This is a DEPRECATED variant DAG (not the one we used in our paper)
|
||||
|
||||
node_queue = Queue(maxsize=0)
|
||||
# start queue for BFS from all the root nodes
|
||||
# Each entry in queue is tuple (parent_node, node, shortest_distance_to_root)
|
||||
parent_node = None
|
||||
shortest_distance_to_root = 0
|
||||
for root_node in D:
|
||||
if D.out_degree()[root_node] == 0:
|
||||
# print("Adding root node "+str(root_node))
|
||||
node_queue.put((None, root_node, shortest_distance_to_root))
|
||||
number_of_ties = 0
|
||||
# now we have all local leaders in the queue
|
||||
|
||||
while not node_queue.empty():
|
||||
parent_node, next_node, shortest_distance_to_root = node_queue.get()
|
||||
|
||||
if "distancetoroot" in D.nodes[next_node]:
|
||||
if D.nodes[next_node]["distancetoroot"] < shortest_distance_to_root:
|
||||
continue # already found a quicker way from next_node to a root node
|
||||
if D.nodes[next_node]["distancetoroot"] == shortest_distance_to_root:
|
||||
number_of_ties += 1
|
||||
if random.random() < 0.5:
|
||||
continue # a simple way to implement randomness where there is a choice of shortest path roots
|
||||
|
||||
if parent_node == None: # Must be a root node (i.e., a local leader)
|
||||
D.nodes[next_node]["rootnode"] = next_node
|
||||
else:
|
||||
D.nodes[next_node]["rootnode"] = D.nodes[parent_node]["rootnode"]
|
||||
D.nodes[next_node]["parentnode"] = parent_node
|
||||
D.nodes[next_node]["distancetoroot"] = shortest_distance_to_root
|
||||
# print(next_node,parent_node,shortest_distance_to_root)
|
||||
nn_list = [(next_node, nn, shortest_distance_to_root + 1) for nn in
|
||||
D.predecessors(next_node)] # get all neighbors of the next_node
|
||||
for nn in nn_list:
|
||||
node_queue.put(nn)
|
||||
tree_edge_list = []
|
||||
|
||||
for node in D:
|
||||
parent_node = D.nodes[node]["parentnode"]
|
||||
if parent_node != None:
|
||||
tree_edge_list.append((parent_node, node))
|
||||
|
||||
# print("! In degree_hierarchy_random_tree broke "+str(number_of_ties)+" ties at random")
|
||||
return D, tree_edge_list
|
||||
|
||||
|
||||
# now we break all ties in Fig.1b (e.g., d->c,d->e; l->b,l->m), and tree_edge_list are short-dahsed-arrows in Fig.1c, and add information (rootnode,parentnode,distoroot), which are useful for community label backpropagation, of nodes in the DAG
|
||||
|
||||
|
||||
# prelimenary functions for computing normalized ki*li (see Supplementary Information)
|
||||
def get_indicator_rank(x):
|
||||
set_x = set(x)
|
||||
sorted_x = sorted(set_x, reverse=False)
|
||||
set_x_dict = {}
|
||||
k = 1
|
||||
for i in sorted_x:
|
||||
if i not in set_x_dict.keys():
|
||||
set_x_dict[i] = k
|
||||
k += 1
|
||||
rank_x = []
|
||||
for i in x:
|
||||
rank_x.append(set_x_dict[i])
|
||||
return rank_x
|
||||
|
||||
|
||||
def get_square(x):
|
||||
square_x = []
|
||||
square_x = [np.power(i, 2) for i in x]
|
||||
return square_x
|
||||
|
||||
|
||||
# min-max normalization
|
||||
def standard_data(x):
|
||||
x_max = np.max(x)
|
||||
x_min = np.min(x)
|
||||
if x_max - x_min == 0:
|
||||
trans_data = np.array([1 / len(x) for i in range(len(x))])
|
||||
else:
|
||||
trans_data = (x - x_min) / (x_max - x_min)
|
||||
return trans_data
|
||||
|
||||
|
||||
# When there are multi-scale community structure in the network, we may want to get the first-level partion automatically sometimes. Here, we present a very simple algorithm to determine the number of first-level comunity centers: we calculate the differences between consecutive candicates in the decision graph (see Fig. 1f in our paper), and if the gap below a certain candicate is larger than the mean+std, then this gap might be a notable gap (this works relatively well for real networks we tested in our paper) #在存在多尺度社团(Multi-scale community structure)的情况下,根据y之间的差值自动选择第一层级的聚类中心的个数
|
||||
# You can REPLACE this algorithm by a more rigorous and sophisticaed one, if you want to do automatic multi-scale community detection
|
||||
# Otherwise, in our default setting, we will give the community partion at the finest resolution
|
||||
def choose_center(multi_sort):
|
||||
y = multi_sort[:, 1]
|
||||
delta = []
|
||||
for i in range(len(y))[1:]:
|
||||
delta.append(abs(y[i] - y[i - 1]))
|
||||
# delta = np.array(delta) #
|
||||
delta_nozero = [i for i in delta if i != 0]
|
||||
delta_std = np.std(delta_nozero)
|
||||
center_num = 0
|
||||
for i in range(len(delta)):
|
||||
if delta[i] > delta_std + np.mean(delta_nozero):
|
||||
center_num = i + 1
|
||||
break
|
||||
return center_num
|
||||
|
||||
|
||||
# Local-BFS (LBFS) from a local leader to determine its superior along hierarchy (or termed as finding hidden directionality of a local leader)
|
||||
# This LBFS will stop right after enountering another local leader with a higher influence (e.g., influence can be measured by degree or other centrality measurements. This LBFS will not traverse the whole network, thus much less costly than normal BFS
|
||||
def BFS_from_s(G, s, roots):
|
||||
'''
|
||||
input:
|
||||
G: graph #图结构
|
||||
s: index of the source/start local leader (type:int) #[BFS开始的起始节点(数据类型:int)]
|
||||
roots: the set of all local leaders (type: list)
|
||||
return:
|
||||
w: the index of the superior local leader along the hierarchy; if no such superior, return itself #指向节点的id(数据类型:int),不存在时返回自己
|
||||
p: the shortest path from the local leader s to its superior local leader; when no superior, return -1 #最短路经长度(数据类型:int),不存在时返回-1
|
||||
'''
|
||||
queue = []
|
||||
queue.append(s)
|
||||
seen = set() # visited nodes in BFS #看是否访问过该结点
|
||||
seen.add(s)
|
||||
path_dict = {} # path length to other nodes #记录root到每个节点的距离
|
||||
path_dict[s] = 0
|
||||
while (len(queue) > 0):
|
||||
vertex = queue.pop(0) # 保存第一结点,并弹出,方便把他下面的子节点接入
|
||||
neighbors = [(neighbor, G.degree()[neighbor]) for neighbor in list(G.adj[vertex]) if
|
||||
neighbor not in seen] # 子节点的数组
|
||||
nodes = [node[0] for node in
|
||||
sorted(neighbors, key=lambda k: k[1], reverse=True)] # the sorting here is not necessary
|
||||
# print('nodes',vertex,nodes)
|
||||
for w in nodes:
|
||||
if w not in seen: # not uncessary, just to make sure w is not in seen #判断是否访问过,使用一个数组
|
||||
path_dict[w] = path_dict[vertex] + 1
|
||||
queue.append(w)
|
||||
seen.add(w)
|
||||
if w in roots and G.degree()[w] > G.degree()[s]: ###
|
||||
return w, path_dict[w]
|
||||
return s, -1
|
||||
|
||||
|
||||
def hierarchical_degree_communities(
|
||||
G,
|
||||
center_num=None,
|
||||
auto_choose_centers=False,
|
||||
maximum_tree=True,
|
||||
isdraw=False,
|
||||
seed=None,
|
||||
self_loop=False,
|
||||
plot_filepath="./",
|
||||
plot_dataname="LS_default",
|
||||
plot_show=False,
|
||||
):
|
||||
'''
|
||||
Produces hierarchical degree forest (HDF) of trees and hence communities.
|
||||
The main part of our Local Search (LS) algorithm
|
||||
|
||||
Input
|
||||
-----
|
||||
G -- simple graph for which communities are required
|
||||
maximum_tree=True -- If true uses maximum dgree DAG as input, otherwise uses full degree DAG
|
||||
seed=None -- an integer to use as a seed to break ties at random. Use None to remove random element
|
||||
self_loop -- If true means the self-loop makes sense
|
||||
plot_filepath -- directory to save the decision graph when isdraw is True
|
||||
plot_dataname -- filename (without extension) for the saved decision graph; saved as "<dataname>.pdf"
|
||||
plot_show -- whether to display the decision graph window when isdraw is True
|
||||
|
||||
Output
|
||||
------
|
||||
On screen statistics of communities
|
||||
|
||||
'''
|
||||
# Ensure we work on an EasyGraph Graph copy so downstream methods (e.g., remove_edges_from) exist
|
||||
if not hasattr(G, "remove_edges_from"):
|
||||
converted = eg.Graph()
|
||||
try:
|
||||
converted.add_nodes_from(G.nodes)
|
||||
except Exception:
|
||||
converted.add_nodes_from(G.nodes())
|
||||
try:
|
||||
converted.add_edges_from(G.edges)
|
||||
except Exception:
|
||||
converted.add_edges_from(G.edges())
|
||||
G = converted
|
||||
else:
|
||||
G = G.copy()
|
||||
|
||||
# Empty graph
|
||||
if not G.nodes:
|
||||
print("Warning: Empty graph detected. Returning empty results.")
|
||||
D = None
|
||||
center_dcd = set()
|
||||
y_dcd = set()
|
||||
y_partition = []
|
||||
grouped_dict = {}
|
||||
plot_combination_data = None
|
||||
return D, center_dcd, y_dcd, y_partition, grouped_dict, plot_combination_data
|
||||
|
||||
# Disconnected graph
|
||||
if not G.edges:
|
||||
print("Warning: Disconnected graph detected.")
|
||||
D = None
|
||||
center_dcd = set(G.nodes.keys())
|
||||
y_dcd = set()
|
||||
y_partition = []
|
||||
grouped_dict = G.nodes
|
||||
plot_combination_data = None
|
||||
return D, center_dcd, y_dcd, y_partition, grouped_dict, plot_combination_data
|
||||
|
||||
|
||||
selfloop_edges = []
|
||||
if eg.number_of_selfloops(G) > 0:
|
||||
selfloop_edges = list(eg.selfloop_edges(G))
|
||||
G.remove_edges_from(selfloop_edges)
|
||||
selfloop_nodes = []
|
||||
for item in selfloop_edges:
|
||||
selfloop_nodes.append(item[0])
|
||||
if self_loop == False:
|
||||
selfloop_nodes = []
|
||||
|
||||
start_time = datetime.now()
|
||||
treename = "Hierarchical Maximum Degree Forest"
|
||||
# treeabv="HMDF"
|
||||
if not maximum_tree:
|
||||
treename = "Hierarchical Full Degree Forest"
|
||||
# treeabv="HFDF"
|
||||
|
||||
# print ("\n===== "+treename+" seed "+str(seed)+" =====")
|
||||
print("\n====Local Search Algorithm (random seed " + str(seed) + ")==========")
|
||||
print("Network: " + str(len(G.nodes)) + " nodes," + str(len(G.edges)) + " edges")
|
||||
D, tree_edge_list = degree_hierarchy_random_tree(G, maximum_tree=maximum_tree, random_seed=seed,
|
||||
selfloop_nodes=selfloop_nodes)
|
||||
# D is the DAG comprising short-dahsed-arrows in Fig.1c in the main text of our paper
|
||||
# print("With "+str(G.number_of_nodes())+" nodes, now left with "+str(len(tree_edge_list))+" edges in tree" )
|
||||
|
||||
# Now find all the nodes with the same root_node (i.e., local leaders)
|
||||
root_to_node = {}
|
||||
for node in D:
|
||||
if "rootnode" in D.nodes[node]:
|
||||
root_node = D.nodes[node]["rootnode"]
|
||||
else:
|
||||
print("*** ERROR Node " + str(node) + " has no rootnode")
|
||||
continue
|
||||
if root_node not in root_to_node:
|
||||
root_to_node[root_node] = []
|
||||
root_to_node[root_node].append(node)
|
||||
##
|
||||
|
||||
# determine centers from root_to_node
|
||||
# (1). using Local-BFS to determine the hidden directionalilty of each local leader (i.e., finding its superior among local leaders along the hierarchy & calculate shortest path lengh between it and its superior l_i #通过local-BFS计算local leader的指向和最短路径
|
||||
root_to_node = {key: value for key, value in root_to_node.items() if len(value) > 1}
|
||||
Potential_Center = list(root_to_node.keys())
|
||||
# print("! Number of Communities (root nodes) found "+str(len(root_to_node)))
|
||||
# print(" Root Nodes: ",Potential_Center)
|
||||
|
||||
root_number = len(root_to_node)
|
||||
root_decision = {}
|
||||
avg_l = 0
|
||||
# print('Intermediate process of determining the center: ')
|
||||
for node in root_to_node.keys():
|
||||
e, p = BFS_from_s(G, node, Potential_Center) # Local-BFS, e is the superior, p is the path length to it
|
||||
root_decision[node] = [e, p, G.degree()[node]]
|
||||
|
||||
# For local leaders with the maximal degree in the network and noisy nodes (isolated ones), setting their l_i as the maximum of l_i of all other local leaders [or the diameter of the network #度值最大的节点和噪声节点的最短路径长度设置为所有节点中最短路径长度的最大值
|
||||
max_path_temp = max(np.array(list(root_decision.values()))[:, 1])
|
||||
# print("max_path_temp == ",max_path_temp," type",type(max_path_temp))
|
||||
max_path_temp = int(max_path_temp)
|
||||
max_path = max_path_temp if max_path_temp > -1 else 2
|
||||
for node in root_decision:
|
||||
if root_decision[node][1] == -1: # maximal local leader(s)
|
||||
root_decision[node] = [root_decision[node][0], max_path, root_decision[node][2]]
|
||||
|
||||
# (2). calculate normalized influence (here, degree k_i) & path length l_i of all nodes (yields result in Fig. 1f in the main text of our paper) #计算所有节点规一化后的度值ki和最短路径li
|
||||
node_plot = root_decision.copy()
|
||||
for n in G.nodes:
|
||||
if n not in node_plot:
|
||||
node_plot[n] = [D.nodes[n]['parentnode'], 1, G.degree()[n]]
|
||||
root_array = np.array(list(node_plot.values()))
|
||||
# print('degree, path',root_array[:,2],root_array[:,1])
|
||||
root_array[root_array[:, 2] <= 1, 1] = 1 # Set l_i=1 for nodes whose degree k_i=1 ###
|
||||
degree = get_indicator_rank(root_array[:, 2])
|
||||
shortest_path = get_square(root_array[:, 1])
|
||||
degree_standard = standard_data(np.array(degree))
|
||||
shortest_path_standard = standard_data(np.array(shortest_path))
|
||||
multi = degree_standard * shortest_path_standard # noralized k_i*l_i
|
||||
nodeid = list(node_plot.keys())
|
||||
multi_dict = {}
|
||||
for i in range(len(nodeid)):
|
||||
multi_dict[nodeid[i]] = multi[i]
|
||||
multi_sort = np.array(sorted(multi_dict.items(), key=lambda kv: (kv[1], kv[0]), reverse=True))
|
||||
multi_sort = np.array([[int(i[0]), i[1]] for i in multi_sort])
|
||||
multi_x = [i for i in range(len(multi_sort))]
|
||||
# print('Determine centers by muti:',multi_sort[:40])
|
||||
|
||||
# choosing the first-level community centers automatically when there is multi-scale communities
|
||||
if auto_choose_centers == True:
|
||||
auto_centernum = choose_center(multi_sort)
|
||||
center_num = auto_centernum if center_num < auto_centernum else center_num
|
||||
if not center_num:
|
||||
center_num = len(root_to_node)
|
||||
center_dcd = []
|
||||
local_cnt = 0
|
||||
# for i in multi_sort[:,1]:
|
||||
for i in multi_sort[:center_num]:
|
||||
if i[1] > 0:
|
||||
local_cnt += 1
|
||||
center_dcd.append(int(i[0]))
|
||||
print("The number of local leaders: " + str(local_cnt))
|
||||
# saving related data for visualization #保存绘图需要的数据
|
||||
plot_combination_data = [root_array[:, 2], root_array[:, 1], nodeid, multi_x, multi_sort[:, 1], multi_sort[:, 0],
|
||||
center_dcd]
|
||||
plot_process_degree_shortpath_data = [degree, shortest_path, nodeid]
|
||||
|
||||
|
||||
# (3). For local leaders, record their superior along the hierarchy in the DAG
|
||||
for node in root_to_node.keys():
|
||||
D.nodes[node]["parentnode"] = root_decision[node][0]
|
||||
D.nodes[node]["rootnode"] = D.nodes[node]["parentnode"]
|
||||
for node in D.nodes:
|
||||
recent_node = [] # prevent loop
|
||||
recent_node.append(node)
|
||||
flag = 0
|
||||
if node in center_dcd:
|
||||
D.nodes[node]["rootnode"] = node
|
||||
else:
|
||||
while D.nodes[node]["rootnode"] not in center_dcd and flag == 0:
|
||||
j = D.nodes[node]["rootnode"]
|
||||
if j not in recent_node and j != None:
|
||||
recent_node.append(j)
|
||||
D.nodes[node]["rootnode"] = D.nodes[j]["rootnode"]
|
||||
else:
|
||||
D.nodes[node]["rootnode"] = None
|
||||
flag = 1
|
||||
|
||||
# (4). get the classes and partition
|
||||
y_dcd = []
|
||||
y_partition = {}
|
||||
for node in D.nodes:
|
||||
if D.nodes[node]["rootnode"] == None:
|
||||
y_dcd.append(-1)
|
||||
y_partition[node] = -1
|
||||
else:
|
||||
y_dcd.append(D.nodes[node]["rootnode"])
|
||||
y_partition[node] = D.nodes[node]["rootnode"]
|
||||
|
||||
end_time = datetime.now()
|
||||
stamp = (end_time - start_time).total_seconds() * 1000
|
||||
print('Running Time: %d ms' % stamp)
|
||||
|
||||
# print('The number of community centers: '+str(center_dcd))
|
||||
print('The number of community centers: ' + str(len(plot_combination_data[6])))
|
||||
print('The id of the centers are: ' + str(plot_combination_data[6]))
|
||||
# print('Modularity of the partition by LS: '+str(community.modularity(y_partition, G)))
|
||||
|
||||
print("The decision graph for determining the number of centers, " +
|
||||
"where centers are nodes with both a large influence k_i and path length l_i to other local leaders with a higher influence.")
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
grouped_dict = defaultdict(list)
|
||||
for key, value in y_partition.items():
|
||||
grouped_dict[value].append(key)
|
||||
|
||||
# Print partition summary
|
||||
if grouped_dict:
|
||||
print("Communities (center: members):")
|
||||
for center, members in grouped_dict.items():
|
||||
print(f" {center}: {sorted(members)}")
|
||||
|
||||
# just for better visualization, can be safely modified
|
||||
if isdraw == True:
|
||||
subplot_location = [0.25, 0.55, 0.35, 0.3]
|
||||
xlim_start_end = [0.3, 0.7]
|
||||
ylim_start_end = [0.7, 0.3]
|
||||
font_location = -0.04
|
||||
plot_combination(plot_combination_data[0], plot_combination_data[1], plot_combination_data[2],
|
||||
plot_combination_data[3], plot_combination_data[4], plot_combination_data[5],
|
||||
plot_combination_data[6], subplot_location, xlim_start_end, ylim_start_end, font_location,
|
||||
filepath=plot_filepath, dataname=plot_dataname, save=True, show=plot_show)
|
||||
|
||||
print(
|
||||
"Note: If multi-scale community structure, which can be common in real networks, is of interest, the number of communities at different level can be explicitly set by some sophisticaed methods or simply by visual inspection for notable gaps in the decision graph. In the default setting, LS alorithm returns community partition at the finest level.")
|
||||
return D, center_dcd, y_dcd, y_partition, grouped_dict, plot_combination_data
|
||||
|
||||
|
||||
def LS_degree_communities(
|
||||
G,
|
||||
center_num=None,
|
||||
auto_choose_centers=False,
|
||||
maximum_tree=True,
|
||||
isdraw=True,
|
||||
seed=None,
|
||||
self_loop=False,
|
||||
plot_filepath="./",
|
||||
plot_dataname="LS_default",
|
||||
plot_show=False,
|
||||
):
|
||||
"""Alias for hierarchical_degree_communities with the same parameters."""
|
||||
return hierarchical_degree_communities(
|
||||
G,
|
||||
center_num=center_num,
|
||||
auto_choose_centers=auto_choose_centers,
|
||||
maximum_tree=maximum_tree,
|
||||
isdraw=isdraw,
|
||||
seed=seed,
|
||||
self_loop=self_loop,
|
||||
plot_filepath=plot_filepath,
|
||||
plot_dataname=plot_dataname,
|
||||
)
|
||||
|
||||
|
||||
# if __name__ == '__main__':
|
||||
# print("### Simple (extreme) example of network where this method does not produce a unique community ###")
|
||||
# G=eg.Graph()
|
||||
# #G.add_edges_from(EdgeList)
|
||||
# # # load the network data
|
||||
# seed = 163
|
||||
# G.add_edges_from([ (0,2), (0,3), (0,4), (0,5), (1,2), (1,3), (1,4), (1,5) ]) #here is a simple example
|
||||
# # G.add_edges_from([(0, 1), (2, 3), (4, 5)])
|
||||
# print(G.nodes)
|
||||
# print(type(G.nodes))
|
||||
# # If you want to use your own dataset, use to read and set label = "id" e.g. eg.read_gml("your dataset",label="id")
|
||||
# # G=eg.read_gml("net_SBM_compact_nb_groups_100_block_size_5_p_in_0.8_k_out_8_i_0.gml",label="id")
|
||||
|
||||
|
||||
# D, center_dcd, y_dcd, y_partition, grouped_dict, plot_combination_data = hierarchical_degree_communities(G, maximum_tree=True, seed=seed)
|
||||
# print("Key represents the community center, and Value represents the nodes within the community.")
|
||||
# print(grouped_dict)
|
||||
# # hierarchical_degree_communities(G, maximum_tree=False, seed=seed)
|
||||
# # print('If there is multi-scale community structure, you can type the number of communities:')
|
||||
# # nc = int(input())
|
||||
# # hierarchical_degree_communities(G, maximum_tree = True, isdraw = False, seed=seed, center_num=nc)
|
||||
# # print("Key represents the community center, and Value represents the nodes within the community.")
|
||||
# # print(grouped_dict)
|
||||
|
||||
# # # Other examples
|
||||
# # print("\n\n ### Karate Club Network ###")
|
||||
# # G=eg.karate_club_graph()
|
||||
# # hierarchical_degree_communities(G, maximum_tree=True, seed=seed)
|
||||
@@ -0,0 +1,355 @@
|
||||
from collections import defaultdict
|
||||
from collections import deque
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.functions.community.modularity import *
|
||||
|
||||
|
||||
__all__ = ["louvain_communities", "louvain_partitions"]
|
||||
|
||||
|
||||
def louvain_communities(G, weight="weight", threshold=0.00002):
|
||||
r"""Find the best partition of a graph using the Louvain Community Detection
|
||||
Algorithm.
|
||||
|
||||
Louvain Community Detection Algorithm is a simple method to extract the community
|
||||
structure of a network. This is a heuristic method based on modularity optimization. [1]_
|
||||
|
||||
The algorithm works in 2 steps. On the first step it assigns every node to be
|
||||
in its own community and then for each node it tries to find the maximum positive
|
||||
modularity gain by moving each node to all of its neighbor communities. If no positive
|
||||
gain is achieved the node remains in its original community.
|
||||
|
||||
The modularity gain obtained by moving an isolated node $i$ into a community $C$ can
|
||||
easily be calculated by the following formula (combining [1]_ [2]_ and some algebra):
|
||||
|
||||
.. math::
|
||||
\Delta Q = \frac{k_{i,in}}{2m} - \gamma\frac{ \Sigma_{tot} \cdot k_i}{2m^2}
|
||||
|
||||
where $m$ is the size of the graph, $k_{i,in}$ is the sum of the weights of the links
|
||||
from $i$ to nodes in $C$, $k_i$ is the sum of the weights of the links incident to node $i$,
|
||||
$\Sigma_{tot}$ is the sum of the weights of the links incident to nodes in $C$ and $\gamma$
|
||||
is the resolution parameter.
|
||||
|
||||
For the directed case the modularity gain can be computed using this formula according to [3]_
|
||||
|
||||
.. math::
|
||||
\Delta Q = \frac{k_{i,in}}{m}
|
||||
- \gamma\frac{k_i^{out} \cdot\Sigma_{tot}^{in} + k_i^{in} \cdot \Sigma_{tot}^{out}}{m^2}
|
||||
|
||||
where $k_i^{out}$, $k_i^{in}$ are the outer and inner weighted degrees of node $i$ and
|
||||
$\Sigma_{tot}^{in}$, $\Sigma_{tot}^{out}$ are the sum of in-going and out-going links incident
|
||||
to nodes in $C$.
|
||||
|
||||
The first phase continues until no individual move can improve the modularity.
|
||||
|
||||
The second phase consists in building a new network whose nodes are now the communities
|
||||
found in the first phase. To do so, the weights of the links between the new nodes are given by
|
||||
the sum of the weight of the links between nodes in the corresponding two communities. Once this
|
||||
phase is complete it is possible to reapply the first phase creating bigger communities with
|
||||
increased modularity.
|
||||
|
||||
The above two phases are executed until no modularity gain is achieved (or is less than
|
||||
the `threshold`).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
threshold
|
||||
G : easygraph
|
||||
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
|
||||
-------
|
||||
list
|
||||
A list of sets (partition of `G`). Each set represents one community and contains
|
||||
all the nodes that constitute it.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The order in which the nodes are considered can affect the final output. In the algorithm
|
||||
the ordering happens using a random shuffle.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Blondel, V.D. et al. Fast unfolding of communities in
|
||||
large networks. J. Stat. Mech 10008, 1-12(2008). https://doi.org/10.1088/1742-5468/2008/10/P10008
|
||||
.. [2] Traag, V.A., Waltman, L. & van Eck, N.J. From Louvain to Leiden: guaranteeing
|
||||
well-connected communities. Sci Rep 9, 5233 (2019). https://doi.org/10.1038/s41598-019-41695-z
|
||||
.. [3] Nicolas Dugu��, Anthony Perez. Directed Louvain : maximizing modularity in directed networks.
|
||||
[Research Report] Universit�� d��Orl��ans. 2015. hal-01231784. https://hal.archives-ouvertes.fr/hal-01231784
|
||||
|
||||
See Also
|
||||
--------
|
||||
louvain_partitions
|
||||
"""
|
||||
if len(G) == 0 or G.size(weight=weight) == 0:
|
||||
return [{n} for n in G.nodes]
|
||||
d = louvain_partitions(G, weight, threshold)
|
||||
q = deque(d, maxlen=1)
|
||||
# q.append(d)
|
||||
return q.pop()
|
||||
|
||||
|
||||
def louvain_partitions(G, weight="weight", threshold=0.0000001):
|
||||
"""Yields partitions for each level of the Louvain Community Detection Algorithm
|
||||
|
||||
Louvain Community Detection Algorithm is a simple method to extract the community
|
||||
structure of a network. This is a heuristic method based on modularity optimization. [1]_
|
||||
|
||||
The partitions at each level (step of the algorithm) form a dendogram of communities.
|
||||
A dendrogram is a diagram representing a tree and each level represents
|
||||
a partition of the G graph. The top level contains the smallest communities
|
||||
and as you traverse to the bottom of the tree the communities get bigger
|
||||
and the overall modularity increases making the partition better.
|
||||
|
||||
Each level is generated by executing the two phases of the Louvain Community
|
||||
Detection Algorithm.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
threshold
|
||||
G : easygraph
|
||||
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.
|
||||
|
||||
Yields
|
||||
------
|
||||
list
|
||||
A list of sets (partition of `G`). Each set represents one community and contains
|
||||
all the nodes that constitute it.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Blondel, V.D. et al. Fast unfolding of communities in
|
||||
large networks. J. Stat. Mech 10008, 1-12(2008)
|
||||
|
||||
See Also
|
||||
--------
|
||||
louvain_communities
|
||||
"""
|
||||
if len(G) == 0 or G.size(weight=weight) == 0:
|
||||
yield [{n} for n in G.nodes]
|
||||
return
|
||||
partition = [{u} for u in G.nodes]
|
||||
mod = modularity(G, partition)
|
||||
is_directed = G.is_directed()
|
||||
if G.is_multigraph():
|
||||
G = _convert_multigraph(G, weight, is_directed)
|
||||
else:
|
||||
graph = G.__class__()
|
||||
graph.add_nodes_from(G)
|
||||
graph.add_edges_from(G.edges, weight=1)
|
||||
G = graph
|
||||
|
||||
m = G.size(weight="weight")
|
||||
partition, inner_partition, improvement = _one_level(G, m, partition, is_directed)
|
||||
improvement = True
|
||||
while improvement:
|
||||
# gh-5901 protect the sets in the yielded list from further manipulation here
|
||||
|
||||
yield [s.copy() for s in partition]
|
||||
new_mod = modularity(G, inner_partition, weight="weight")
|
||||
if new_mod - mod <= threshold:
|
||||
return
|
||||
mod = new_mod
|
||||
"""
|
||||
for node1, node2, wt in G.edges:
|
||||
print(node1,node2,wt)
|
||||
print("\n")
|
||||
"""
|
||||
G = _gen_graph(G, inner_partition)
|
||||
"""
|
||||
for node1, node2, wt in G.edges:
|
||||
print(node1,node2,wt)
|
||||
"""
|
||||
partition, inner_partition, improvement = _one_level(
|
||||
G, m, partition, is_directed, 1
|
||||
)
|
||||
|
||||
|
||||
def _one_level(G, m, partition, resolution=1, is_directed=False, seed=None, tes=0):
|
||||
"""Calculate one level of the Louvain partitions tree
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph Graph/DiGraph
|
||||
The graph from which to detect communities
|
||||
m : number
|
||||
The size of the graph `G`.
|
||||
partition : list of sets of nodes
|
||||
A valid partition of the graph `G`
|
||||
resolution : positive number
|
||||
The resolution parameter for computing the modularity of a partition
|
||||
is_directed : bool
|
||||
True if `G` is a directed graph.
|
||||
seed : integer, random_state, or None (default)
|
||||
Indicator of random number generation state.
|
||||
See :ref:`Randomness<randomness>`.
|
||||
|
||||
"""
|
||||
node2com = {u: i for i, u in enumerate(G.nodes)}
|
||||
inner_partition = [{u} for u in G.nodes]
|
||||
"""
|
||||
if is_directed:
|
||||
in_degrees = dict(G.in_degree(weight="weight"))
|
||||
out_degrees = dict(G.out_degree(weight="weight"))
|
||||
Stot_in = list(in_degrees.values())
|
||||
Stot_out = list(out_degrees.values())
|
||||
# Calculate weights for both in and out neighbours
|
||||
nbrs = {}
|
||||
for u in G:
|
||||
nbrs[u] = defaultdict(float)
|
||||
for _, n, wt in G.out_edges(u, data="weight"):
|
||||
nbrs[u][n] += wt
|
||||
for n, _, wt in G.in_edges(u, data="weight"):
|
||||
nbrs[u][n] += wt
|
||||
pass
|
||||
else:
|
||||
"""
|
||||
degrees = dict(G.degree(weight="weight"))
|
||||
Stot = []
|
||||
for i in G:
|
||||
Stot.append(len(G[i]))
|
||||
|
||||
# for c in Stot:
|
||||
# print(c)
|
||||
|
||||
nbrs = {u: {v: data["weight"] for v, data in G[u].items() if v != u} for u in G}
|
||||
rand_nodes = list(G.nodes)
|
||||
# seed.shuffle(rand_nodes)
|
||||
nb_moves = 1
|
||||
improvement = False
|
||||
while nb_moves > 0:
|
||||
# print(nb_moves)
|
||||
|
||||
nb_moves = 0
|
||||
for u in rand_nodes:
|
||||
best_mod = 0
|
||||
best_com = node2com[u]
|
||||
weights2com = _neighbor_weights(nbrs[u], node2com)
|
||||
"""
|
||||
if is_directed:
|
||||
in_degree = in_degrees[u]
|
||||
out_degree = out_degrees[u]
|
||||
Stot_in[best_com] -= in_degree
|
||||
Stot_out[best_com] -= out_degree
|
||||
remove_cost = (
|
||||
-weights2com[best_com] / m
|
||||
+ (out_degree * Stot_in[best_com] + in_degree * Stot_out[best_com])
|
||||
/ m**2
|
||||
)
|
||||
else:
|
||||
"""
|
||||
degree = degrees[u]
|
||||
Stot[best_com] -= degree
|
||||
remove_cost = -weights2com[best_com] / m + (Stot[best_com] * degree) / (
|
||||
2 * m**2
|
||||
)
|
||||
for nbr_com, wt in weights2com.items():
|
||||
"""
|
||||
if is_directed:
|
||||
gain = (
|
||||
remove_cost
|
||||
+ wt / m
|
||||
- (
|
||||
out_degree * Stot_in[nbr_com]
|
||||
+ in_degree * Stot_out[nbr_com]
|
||||
)
|
||||
/ m**2
|
||||
)
|
||||
else:
|
||||
"""
|
||||
gain = remove_cost + wt / m - (Stot[nbr_com] * degree) / (2 * m**2)
|
||||
if gain > best_mod:
|
||||
best_mod = gain
|
||||
best_com = nbr_com
|
||||
"""
|
||||
if is_directed:
|
||||
Stot_in[best_com] += in_degree
|
||||
Stot_out[best_com] += out_degree
|
||||
else:
|
||||
"""
|
||||
Stot[best_com] += degree
|
||||
|
||||
if best_com != node2com[u]:
|
||||
com = G.nodes[u].get("nodes", {u})
|
||||
partition[node2com[u]].difference_update(com)
|
||||
inner_partition[node2com[u]].remove(u)
|
||||
partition[best_com].update(com)
|
||||
inner_partition[best_com].add(u)
|
||||
improvement = True
|
||||
nb_moves += 1
|
||||
node2com[u] = best_com
|
||||
partition = list(filter(len, partition))
|
||||
inner_partition = list(filter(len, inner_partition))
|
||||
|
||||
# for c in partition:
|
||||
# print(c)
|
||||
|
||||
return partition, inner_partition, improvement
|
||||
|
||||
|
||||
def _neighbor_weights(nbrs, node2com):
|
||||
"""Calculate weights between node and its neighbor communities.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
nbrs : dictionary
|
||||
Dictionary with nodes' neighbours as keys and their edge weight as value.
|
||||
node2com : dictionary
|
||||
Dictionary with all graph's nodes as keys and their community index as value.
|
||||
|
||||
"""
|
||||
weights = defaultdict(float)
|
||||
for nbr, wt in nbrs.items():
|
||||
weights[node2com[nbr]] += wt
|
||||
return weights
|
||||
|
||||
|
||||
def _gen_graph(G, partition):
|
||||
"""Generate a new graph based on the partitions of a given graph"""
|
||||
H = G.__class__()
|
||||
node2com = {}
|
||||
for i, part in enumerate(partition):
|
||||
nodes = set()
|
||||
for node in part:
|
||||
node2com[node] = i
|
||||
nodes.update(G.nodes[node].get("nodes", {node}))
|
||||
H.add_node(i, nodes=nodes)
|
||||
|
||||
for node1, node2, wt in G.edges:
|
||||
com1 = node2com[node1]
|
||||
com2 = node2com[node2]
|
||||
wt = wt["weight"]
|
||||
try:
|
||||
temp = H[com1][com2]["weight"]
|
||||
except KeyError:
|
||||
temp = 0
|
||||
H.add_edge(com1, com2, weight=wt + temp)
|
||||
"""
|
||||
if wt:
|
||||
wt = wt["weight"]
|
||||
H.add_edge(com1, com2, weight=wt)
|
||||
else:
|
||||
H.add_edge(com1, com2, weight=1)
|
||||
"""
|
||||
return H
|
||||
|
||||
|
||||
def _convert_multigraph(G, weight, is_directed):
|
||||
"""Convert a Multigraph to normal Graph"""
|
||||
if is_directed:
|
||||
H = eg.DiGraph()
|
||||
else:
|
||||
H = eg.Graph()
|
||||
H.add_nodes_from(G)
|
||||
for u, v, wt in G.edges(data=weight, default=1):
|
||||
if H.has_edge(u, v):
|
||||
H[u][v]["weight"] += wt
|
||||
else:
|
||||
H.add_edge(u, v, weight=wt)
|
||||
return H
|
||||
@@ -0,0 +1,75 @@
|
||||
from itertools import product
|
||||
|
||||
from easygraph.utils import *
|
||||
|
||||
|
||||
__all__ = ["modularity"]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def modularity(G, communities, weight="weight"):
|
||||
r"""
|
||||
Returns the modularity of the given partition of the graph.
|
||||
Modularity is defined in [1]_ as
|
||||
|
||||
.. math::
|
||||
|
||||
Q = \frac{1}{2m} \sum_{ij} \left( A_{ij} - \frac{k_ik_j}{2m}\right)
|
||||
\delta(c_i,c_j)
|
||||
|
||||
where m is the number of edges, A is the adjacency matrix of
|
||||
`G`,
|
||||
|
||||
.. math::
|
||||
|
||||
k_i\ is\ the\ degree\ of\ i\ and\ \delta(c_i, c_j)\ is\ 1\ if\ i\ and\ j\ are\ in\ the\ same\ community\ and\ 0\ otherwise.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
communities : list or iterable of set of nodes
|
||||
These node sets must represent a partition of G's nodes.
|
||||
|
||||
weight : string, optional (default : 'weight')
|
||||
The key for edge weight.
|
||||
|
||||
Returns
|
||||
----------
|
||||
Q : float
|
||||
The modularity of the partition.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] M. E. J. Newman *Networks: An Introduction*, page 224.
|
||||
Oxford University Press, 2011.
|
||||
|
||||
"""
|
||||
# TODO: multigraph not included.
|
||||
|
||||
if not isinstance(communities, list):
|
||||
communities = list(communities)
|
||||
|
||||
directed = G.is_directed()
|
||||
m = G.size(weight=weight)
|
||||
if directed:
|
||||
out_degree = dict(G.out_degree(weight=weight))
|
||||
in_degree = dict(G.in_degree(weight=weight))
|
||||
norm = 1 / m
|
||||
else:
|
||||
out_degree = dict(G.degree(weight=weight))
|
||||
in_degree = out_degree
|
||||
norm = 1 / (2 * m)
|
||||
|
||||
def val(u, v):
|
||||
try:
|
||||
w = G[u][v].get(weight, 1)
|
||||
except KeyError:
|
||||
w = 0
|
||||
# Double count self-loops if the graph is undirected.
|
||||
if u == v and not directed:
|
||||
w *= 2
|
||||
return w - in_degree[u] * out_degree[v] * norm
|
||||
|
||||
Q = sum(val(u, v) for c in communities for u, v in product(c, repeat=2))
|
||||
return Q * norm
|
||||
@@ -0,0 +1,200 @@
|
||||
from easygraph.functions.community.modularity import modularity
|
||||
from easygraph.utils import *
|
||||
from easygraph.utils.mapped_queue import MappedQueue
|
||||
|
||||
|
||||
__all__ = ["greedy_modularity_communities"]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def greedy_modularity_communities(G, weight="weight"):
|
||||
"""Communities detection via greedy modularity method.
|
||||
|
||||
Find communities in graph using Clauset-Newman-Moore greedy modularity
|
||||
maximization. This method currently supports the Graph class.
|
||||
|
||||
Greedy modularity maximization begins with each node in its own community
|
||||
and joins the pair of communities that most increases modularity until no
|
||||
such pair exists.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
weight : string (default : 'weight')
|
||||
The key for edge weight. For undirected graph, it will regard each edge
|
||||
weight as 1.
|
||||
|
||||
Returns
|
||||
----------
|
||||
Yields sets of nodes, one for each community.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Newman, M. E. J. "Networks: An Introduction Oxford Univ." (2010).
|
||||
.. [2] Clauset, Aaron, Mark EJ Newman, and Cristopher Moore.
|
||||
"Finding community structure in very large networks." Physical review E 70.6 (2004): 066111.
|
||||
"""
|
||||
|
||||
# Count nodes and edges
|
||||
|
||||
N = len(G.nodes)
|
||||
m = sum(d.get(weight, 1) for u, v, d in G.edges)
|
||||
if N == 0 or m == 0:
|
||||
print("Please input the graph which has at least one edge!")
|
||||
exit()
|
||||
q0 = 1.0 / (2.0 * m)
|
||||
|
||||
# Map node labels to contiguous integers
|
||||
label_for_node = {i: v for i, v in enumerate(G.nodes)}
|
||||
node_for_label = {label_for_node[i]: i for i in range(N)}
|
||||
|
||||
# Calculate degrees
|
||||
k_for_label = G.degree(weight=weight)
|
||||
k = [k_for_label[label_for_node[i]] for i in range(N)]
|
||||
|
||||
# Initialize community and merge lists
|
||||
communities = {i: frozenset([i]) for i in range(N)}
|
||||
merges = []
|
||||
|
||||
# Initial modularity
|
||||
partition = [[label_for_node[x] for x in c] for c in communities.values()]
|
||||
q_cnm = modularity(G, partition)
|
||||
|
||||
# Initialize data structures
|
||||
# CNM Eq 8-9 (Eq 8 was missing a factor of 2 (from A_ij + A_ji)
|
||||
# a[i]: fraction of edges within community i
|
||||
# dq_dict[i][j]: dQ for merging community i, j
|
||||
# dq_heap[i][n] : (-dq, i, j) for communitiy i nth largest dQ
|
||||
# H[n]: (-dq, i, j) for community with nth largest max_j(dQ_ij)
|
||||
a = [k[i] * q0 for i in range(N)]
|
||||
dq_dict = {
|
||||
i: {
|
||||
node_for_label[u]: 2 * q0 * d.get(weight, 1) - 2 * k[i] * k[node_for_label[u]] * q0 * q0
|
||||
for u, d in G.adj[label_for_node[i]].items()
|
||||
if node_for_label[u] != i
|
||||
}
|
||||
for i in range(N)
|
||||
}
|
||||
dq_heap = [
|
||||
MappedQueue([(-dq, i, j) for j, dq in dq_dict[i].items()]) for i in range(N)
|
||||
]
|
||||
H = MappedQueue([dq_heap[i].h[0] for i in range(N) if len(dq_heap[i]) > 0])
|
||||
|
||||
# Merge communities until we can't improve modularity
|
||||
while len(H) > 1:
|
||||
# Find best merge
|
||||
# Remove from heap of row maxes
|
||||
# Ties will be broken by choosing the pair with lowest min community id
|
||||
try:
|
||||
dq, i, j = H.pop()
|
||||
except IndexError:
|
||||
break
|
||||
dq = -dq
|
||||
# Remove best merge from row i heap
|
||||
dq_heap[i].pop()
|
||||
# Push new row max onto H
|
||||
if len(dq_heap[i]) > 0:
|
||||
H.push(dq_heap[i].h[0])
|
||||
# If this element was also at the root of row j, we need to remove the
|
||||
# duplicate entry from H
|
||||
if dq_heap[j].h[0] == (-dq, j, i):
|
||||
H.remove((-dq, j, i))
|
||||
# Remove best merge from row j heap
|
||||
dq_heap[j].remove((-dq, j, i))
|
||||
# Push new row max onto H
|
||||
if len(dq_heap[j]) > 0:
|
||||
H.push(dq_heap[j].h[0])
|
||||
else:
|
||||
# Duplicate wasn't in H, just remove from row j heap
|
||||
dq_heap[j].remove((-dq, j, i))
|
||||
# Stop when change is non-positive
|
||||
if dq <= 0:
|
||||
break
|
||||
|
||||
# Perform merge
|
||||
communities[j] = frozenset(communities[i] | communities[j])
|
||||
del communities[i]
|
||||
merges.append((i, j, dq))
|
||||
# New modularity
|
||||
q_cnm += dq
|
||||
# Get list of communities connected to merged communities
|
||||
i_set = set(dq_dict[i].keys())
|
||||
j_set = set(dq_dict[j].keys())
|
||||
all_set = (i_set | j_set) - {i, j}
|
||||
both_set = i_set & j_set
|
||||
# Merge i into j and update dQ
|
||||
for k in all_set:
|
||||
# Calculate new dq value
|
||||
if k in both_set:
|
||||
dq_jk = dq_dict[j][k] + dq_dict[i][k]
|
||||
elif k in j_set:
|
||||
dq_jk = dq_dict[j][k] - 2.0 * a[i] * a[k]
|
||||
else:
|
||||
# k in i_set
|
||||
dq_jk = dq_dict[i][k] - 2.0 * a[j] * a[k]
|
||||
# Update rows j and k
|
||||
for row, col in [(j, k), (k, j)]:
|
||||
# Save old value for finding heap index
|
||||
if k in j_set:
|
||||
d_old = (-dq_dict[row][col], row, col)
|
||||
else:
|
||||
d_old = None
|
||||
# Update dict for j,k only (i is removed below)
|
||||
dq_dict[row][col] = dq_jk
|
||||
# Save old max of per-row heap
|
||||
if len(dq_heap[row]) > 0:
|
||||
d_oldmax = dq_heap[row].h[0]
|
||||
else:
|
||||
d_oldmax = None
|
||||
# Add/update heaps
|
||||
d = (-dq_jk, row, col)
|
||||
if d_old is None:
|
||||
# We're creating a new nonzero element, add to heap
|
||||
dq_heap[row].push(d)
|
||||
else:
|
||||
# Update existing element in per-row heap
|
||||
dq_heap[row].update(d_old, d)
|
||||
# Update heap of row maxes if necessary
|
||||
if d_oldmax is None:
|
||||
# No entries previously in this row, push new max
|
||||
H.push(d)
|
||||
else:
|
||||
# We've updated an entry in this row, has the max changed?
|
||||
if dq_heap[row].h[0] != d_oldmax:
|
||||
H.update(d_oldmax, dq_heap[row].h[0])
|
||||
|
||||
# Remove row/col i from matrix
|
||||
i_neighbors = dq_dict[i].keys()
|
||||
for k in i_neighbors:
|
||||
# Remove from dict
|
||||
dq_old = dq_dict[k][i]
|
||||
del dq_dict[k][i]
|
||||
# Remove from heaps if we haven't already
|
||||
if k != j:
|
||||
# Remove both row and column
|
||||
for row, col in [(k, i), (i, k)]:
|
||||
# Check if replaced dq is row max
|
||||
d_old = (-dq_old, row, col)
|
||||
if dq_heap[row].h[0] == d_old:
|
||||
# Update per-row heap and heap of row maxes
|
||||
dq_heap[row].remove(d_old)
|
||||
H.remove(d_old)
|
||||
# Update row max
|
||||
if len(dq_heap[row]) > 0:
|
||||
H.push(dq_heap[row].h[0])
|
||||
else:
|
||||
# Only update per-row heap
|
||||
dq_heap[row].remove(d_old)
|
||||
|
||||
del dq_dict[i]
|
||||
# Mark row i as deleted, but keep placeholder
|
||||
dq_heap[i] = MappedQueue()
|
||||
# Merge i into j and update a
|
||||
a[j] += a[i]
|
||||
a[i] = 0
|
||||
|
||||
communities = [
|
||||
frozenset(label_for_node[i] for i in c) for c in communities.values()
|
||||
]
|
||||
return sorted(communities, key=len, reverse=True)
|
||||
@@ -0,0 +1,122 @@
|
||||
import random
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils import *
|
||||
|
||||
|
||||
__all__ = ["enumerate_subgraph", "random_enumerate_subgraph"]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def enumerate_subgraph(G, k: int):
|
||||
"""
|
||||
Returns the motifs.
|
||||
Motifs are small weakly connected induced subgraphs of a given structure in a graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph.
|
||||
|
||||
k : int
|
||||
The size of the motifs to search for.
|
||||
|
||||
Returns
|
||||
----------
|
||||
k_subgraphs : list
|
||||
The motifs.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Wernicke, Sebastian. "Efficient detection of network motifs."
|
||||
IEEE/ACM transactions on computational biology and bioinformatics 3.4 (2006): 347-359.
|
||||
|
||||
"""
|
||||
k_subgraphs = []
|
||||
for v, _ in G.nodes.items():
|
||||
Vextension = {u for u in G.adj[v] if u > v}
|
||||
extend_subgraph(G, {v}, Vextension, v, k, k_subgraphs)
|
||||
return k_subgraphs
|
||||
|
||||
|
||||
def extend_subgraph(
|
||||
G, Vsubgraph: set, Vextension: set, v: int, k: int, k_subgraphs: list
|
||||
):
|
||||
if len(Vsubgraph) == k:
|
||||
k_subgraphs.append(Vsubgraph)
|
||||
return
|
||||
while len(Vextension) > 0:
|
||||
w = random.choice(tuple(Vextension))
|
||||
Vextension.remove(w)
|
||||
NexclwVsubgraph = exclusive_neighborhood(G, w, Vsubgraph)
|
||||
VpExtension = Vextension | {u for u in NexclwVsubgraph if u > v}
|
||||
extend_subgraph(G, Vsubgraph | {w}, VpExtension, v, k, k_subgraphs)
|
||||
|
||||
|
||||
def exclusive_neighborhood(G, v: int, vp: set):
|
||||
Nv = set(G.adj[v])
|
||||
NVp = {u for n in vp for u in G.adj[n]} | vp
|
||||
return Nv - NVp
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def random_enumerate_subgraph(G, k: int, cut_prob: list):
|
||||
"""
|
||||
Returns the motifs.
|
||||
Motifs are small weakly connected induced subgraphs of a given structure in a graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph.
|
||||
|
||||
k : int
|
||||
The size of the motifs to search for.
|
||||
|
||||
cut_prob : list
|
||||
list of probabilities for cutting the search tree at a given level.
|
||||
|
||||
Returns
|
||||
----------
|
||||
k_subgraphs : list
|
||||
The motifs.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Wernicke, Sebastian. "A faster algorithm for detecting network motifs."
|
||||
International Workshop on Algorithms in Bioinformatics. Springer, Berlin, Heidelberg, 2005.
|
||||
|
||||
"""
|
||||
if len(cut_prob) != k:
|
||||
raise eg.EasyGraphError("length of cut_prob invalid, should equal to k")
|
||||
|
||||
k_subgraphs = []
|
||||
for v, _ in G.nodes.items():
|
||||
if random.random() > cut_prob[0]:
|
||||
continue
|
||||
Vextension = {u for u in G.adj[v] if u > v}
|
||||
random_extend_subgraph(G, {v}, Vextension, v, k, k_subgraphs, cut_prob)
|
||||
return k_subgraphs
|
||||
|
||||
|
||||
def random_extend_subgraph(
|
||||
G,
|
||||
Vsubgraph: set,
|
||||
Vextension: set,
|
||||
v: int,
|
||||
k: int,
|
||||
k_subgraphs: list,
|
||||
cut_prob: list,
|
||||
):
|
||||
if len(Vsubgraph) == k:
|
||||
k_subgraphs.append(Vsubgraph)
|
||||
return
|
||||
while len(Vextension) > 0:
|
||||
w = random.choice(tuple(Vextension))
|
||||
Vextension.remove(w)
|
||||
NexclwVsubgraph = exclusive_neighborhood(G, w, Vsubgraph)
|
||||
VpExtension = Vextension | {u for u in NexclwVsubgraph if u > v}
|
||||
if random.random() > cut_prob[len(Vsubgraph)]:
|
||||
continue
|
||||
random_extend_subgraph(
|
||||
G, Vsubgraph | {w}, VpExtension, v, k, k_subgraphs, cut_prob
|
||||
)
|
||||
@@ -0,0 +1,65 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
class TestLabelPropagationAlgorithms(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.graph_simple = eg.Graph()
|
||||
self.graph_simple.add_edges_from([(0, 1), (1, 2), (3, 4)])
|
||||
|
||||
self.graph_weighted = eg.Graph()
|
||||
self.graph_weighted.add_edges_from(
|
||||
[
|
||||
(0, 1, {"weight": 3}),
|
||||
(1, 2, {"weight": 2}),
|
||||
(2, 0, {"weight": 4}),
|
||||
(3, 4, {"weight": 1}),
|
||||
]
|
||||
)
|
||||
|
||||
self.graph_disconnected = eg.Graph()
|
||||
self.graph_disconnected.add_edges_from([(0, 1), (2, 3), (4, 5)])
|
||||
|
||||
self.graph_single_node = eg.Graph()
|
||||
self.graph_single_node.add_node(42)
|
||||
|
||||
self.graph_empty = eg.Graph()
|
||||
|
||||
def test_lpa(self):
|
||||
self.assertEqual(eg.functions.community.LPA(self.graph_single_node), {1: [42]})
|
||||
self.assertTrue(eg.functions.community.LPA(self.graph_simple))
|
||||
self.assertTrue(eg.functions.community.LPA(self.graph_weighted))
|
||||
self.assertTrue(eg.functions.community.LPA(self.graph_disconnected))
|
||||
|
||||
def test_slpa(self):
|
||||
self.assertEqual(
|
||||
eg.functions.community.SLPA(self.graph_single_node, T=5, r=0.01), {1: [42]}
|
||||
)
|
||||
self.assertTrue(eg.functions.community.SLPA(self.graph_simple, T=10, r=0.1))
|
||||
self.assertTrue(
|
||||
eg.functions.community.SLPA(self.graph_disconnected, T=15, r=0.1)
|
||||
)
|
||||
|
||||
def test_hanp(self):
|
||||
self.assertEqual(
|
||||
eg.functions.community.HANP(self.graph_single_node, m=0.1, delta=0.05),
|
||||
{1: [42]},
|
||||
)
|
||||
self.assertTrue(
|
||||
eg.functions.community.HANP(self.graph_simple, m=0.3, delta=0.1)
|
||||
)
|
||||
self.assertTrue(
|
||||
eg.functions.community.HANP(self.graph_weighted, m=0.5, delta=0.2)
|
||||
)
|
||||
|
||||
def test_bmlpa(self):
|
||||
self.assertEqual(
|
||||
eg.functions.community.BMLPA(self.graph_single_node, p=0.1), {1: [42]}
|
||||
)
|
||||
self.assertTrue(eg.functions.community.BMLPA(self.graph_simple, p=0.3))
|
||||
self.assertTrue(eg.functions.community.BMLPA(self.graph_weighted, p=0.2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,48 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph import LS_degree_communities
|
||||
|
||||
class TestLSDetection(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.graph_simple = eg.Graph()
|
||||
self.graph_simple.add_edges_from([(0,2), (0,3), (0,4), (0,5), (1,2), (1,3), (1,4), (1,5)])
|
||||
|
||||
self.graph_disconnected = eg.Graph()
|
||||
self.graph_disconnected.add_edges_from([(0, 1), (2, 3), (4, 5)])
|
||||
|
||||
self.graph_single_node = eg.Graph()
|
||||
self.graph_single_node.add_node(42)
|
||||
|
||||
self.graph_empty = eg.Graph()
|
||||
|
||||
def test_LS_simple(self):
|
||||
_, _, _, _, communities, _ = LS_degree_communities(self.graph_simple, maximum_tree=True, isdraw = False, seed=163)
|
||||
flat = set().union(*communities.values())
|
||||
self.assertSetEqual(flat, set(self.graph_simple.nodes))
|
||||
|
||||
def test_LS_disconnected(self):
|
||||
_, _, _, _, communities, _ = LS_degree_communities(self.graph_disconnected, maximum_tree=True, isdraw = False, seed=163)
|
||||
flat =set().union(*communities.values())
|
||||
self.assertSetEqual(flat, set(self.graph_disconnected.nodes))
|
||||
|
||||
def test_LS_single_node(self):
|
||||
_, _, _, _, communities, _ = LS_degree_communities(self.graph_single_node, maximum_tree=True, isdraw = False, seed=163)
|
||||
flat = set().union(communities.keys())
|
||||
self.assertEqual(len(flat), 1)
|
||||
self.assertSetEqual(flat, {42})
|
||||
|
||||
def test_LS_empty_graph(self):
|
||||
_, _, _, _, communities, _ = LS_degree_communities(self.graph_empty, maximum_tree=True, isdraw = False, seed=163)
|
||||
self.assertEqual(communities, {})
|
||||
|
||||
def test_LS_partitions_progressive_size(self):
|
||||
_, _, _, _, communities, _ = LS_degree_communities(self.graph_simple, maximum_tree=True, isdraw = False, seed=163)
|
||||
total_nodes = sum(len(members) for center, members in communities.items())
|
||||
self.assertEqual(total_nodes, len(self.graph_simple.nodes))
|
||||
flat = [node for _,members in communities.items() for node in members]
|
||||
self.assertEqual(len(flat), len(set(flat)))
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,65 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
class TestEgoGraph(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.simple_graph = eg.Graph()
|
||||
self.simple_graph.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4)])
|
||||
|
||||
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, {"weight": 1}), (1, 2, {"weight": 2}), (2, 3, {"weight": 3})]
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
def test_simple_graph_radius_1(self):
|
||||
ego = eg.functions.community.ego_graph(self.simple_graph, 2, radius=1)
|
||||
self.assertSetEqual(set(ego.nodes), {1, 2, 3})
|
||||
|
||||
def test_simple_graph_radius_2(self):
|
||||
ego = eg.functions.community.ego_graph(self.simple_graph, 2, radius=2)
|
||||
self.assertSetEqual(set(ego.nodes), {0, 1, 2, 3, 4})
|
||||
|
||||
def test_directed_graph(self):
|
||||
ego = eg.functions.community.ego_graph(self.directed_graph, 1, radius=1)
|
||||
self.assertSetEqual(set(ego.nodes), {1, 2})
|
||||
|
||||
def test_weighted_graph_with_distance(self):
|
||||
ego = eg.functions.community.ego_graph(
|
||||
self.weighted_graph, 0, radius=2, distance="weight"
|
||||
)
|
||||
self.assertSetEqual(set(ego.nodes), {0, 1})
|
||||
|
||||
def test_disconnected_graph(self):
|
||||
ego = eg.functions.community.ego_graph(self.disconnected_graph, 0, radius=1)
|
||||
self.assertSetEqual(set(ego.nodes), {0, 1})
|
||||
|
||||
def test_single_node_graph(self):
|
||||
ego = eg.functions.community.ego_graph(self.single_node_graph, 42, radius=1)
|
||||
self.assertSetEqual(set(ego.nodes), {42})
|
||||
|
||||
def test_center_false(self):
|
||||
ego = eg.functions.community.ego_graph(
|
||||
self.simple_graph, 2, radius=1, center=False
|
||||
)
|
||||
self.assertSetEqual(set(ego.nodes), {1, 3})
|
||||
|
||||
def test_empty_graph(self):
|
||||
G = eg.Graph()
|
||||
G.add_node("x")
|
||||
ego = eg.functions.community.ego_graph(G, "x", radius=1)
|
||||
self.assertSetEqual(set(ego.nodes), {"x"})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,65 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
class TestLouvainCommunityDetection(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.graph_simple = eg.Graph()
|
||||
self.graph_simple.add_edges_from([(0, 1), (1, 2), (3, 4)])
|
||||
|
||||
self.graph_weighted = eg.Graph()
|
||||
self.graph_weighted.add_edges_from(
|
||||
[(0, 1, {"weight": 5}), (1, 2, {"weight": 3}), (3, 4, {"weight": 2})]
|
||||
)
|
||||
|
||||
self.graph_directed = eg.DiGraph()
|
||||
self.graph_directed.add_edges_from([(0, 1), (1, 2), (2, 0), (3, 4)])
|
||||
|
||||
self.graph_disconnected = eg.Graph()
|
||||
self.graph_disconnected.add_edges_from([(0, 1), (2, 3), (4, 5)])
|
||||
|
||||
self.graph_single_node = eg.Graph()
|
||||
self.graph_single_node.add_node(42)
|
||||
|
||||
self.graph_empty = eg.Graph()
|
||||
|
||||
def test_louvain_communities_simple(self):
|
||||
communities = eg.functions.community.louvain_communities(self.graph_simple)
|
||||
flat = {node for comm in communities for node in comm}
|
||||
self.assertSetEqual(flat, set(self.graph_simple.nodes))
|
||||
|
||||
def test_louvain_communities_weighted(self):
|
||||
communities = eg.functions.community.louvain_communities(
|
||||
self.graph_weighted, weight="weight"
|
||||
)
|
||||
flat = {node for comm in communities for node in comm}
|
||||
self.assertSetEqual(flat, set(self.graph_weighted.nodes))
|
||||
|
||||
def test_louvain_communities_disconnected(self):
|
||||
communities = eg.functions.community.louvain_communities(
|
||||
self.graph_disconnected
|
||||
)
|
||||
flat = {node for comm in communities for node in comm}
|
||||
self.assertSetEqual(flat, set(self.graph_disconnected.nodes))
|
||||
|
||||
def test_louvain_communities_single_node(self):
|
||||
communities = eg.functions.community.louvain_communities(self.graph_single_node)
|
||||
self.assertEqual(len(communities), 1)
|
||||
self.assertSetEqual(communities[0], {42})
|
||||
|
||||
def test_louvain_communities_empty_graph(self):
|
||||
communities = eg.functions.community.louvain_communities(self.graph_empty)
|
||||
self.assertEqual(communities, [])
|
||||
|
||||
def test_louvain_partitions_progressive_size(self):
|
||||
partitions = list(eg.functions.community.louvain_partitions(self.graph_simple))
|
||||
for partition in partitions:
|
||||
total_nodes = sum(len(p) for p in partition)
|
||||
self.assertEqual(total_nodes, len(self.graph_simple.nodes))
|
||||
flat = [node for part in partition for node in part]
|
||||
self.assertEqual(len(flat), len(set(flat)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,71 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
class TestModularity(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G = eg.Graph()
|
||||
self.G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)])
|
||||
|
||||
self.DG = eg.DiGraph()
|
||||
self.DG.add_edges_from([(0, 1), (1, 2), (2, 0)])
|
||||
|
||||
self.G_weighted = eg.Graph()
|
||||
self.G_weighted.add_edge(0, 1, weight=2)
|
||||
self.G_weighted.add_edge(1, 2, weight=3)
|
||||
self.G_weighted.add_edge(2, 0, weight=1)
|
||||
|
||||
self.G_selfloop = eg.Graph()
|
||||
self.G_selfloop.add_edges_from([(0, 0), (1, 1), (0, 1)])
|
||||
|
||||
self.G_empty = eg.Graph()
|
||||
|
||||
def test_undirected_modularity(self):
|
||||
communities = [{0, 1}, {2, 3}]
|
||||
q = eg.functions.community.modularity(self.G, communities)
|
||||
self.assertIsInstance(q, float)
|
||||
|
||||
def test_directed_modularity(self):
|
||||
communities = [{0, 1, 2}]
|
||||
q = eg.functions.community.modularity(self.DG, communities)
|
||||
self.assertIsInstance(q, float)
|
||||
|
||||
def test_weighted_graph(self):
|
||||
communities = [{0, 1}, {2}]
|
||||
q = eg.functions.community.modularity(
|
||||
self.G_weighted, communities, weight="weight"
|
||||
)
|
||||
self.assertIsInstance(q, float)
|
||||
|
||||
def test_self_loops(self):
|
||||
communities = [{0, 1}]
|
||||
q = eg.functions.community.modularity(self.G_selfloop, communities)
|
||||
self.assertIsInstance(q, float)
|
||||
|
||||
def test_single_community(self):
|
||||
communities = [{0, 1, 2, 3}]
|
||||
q = eg.functions.community.modularity(self.G, communities)
|
||||
self.assertIsInstance(q, float)
|
||||
|
||||
def test_each_node_its_own_community(self):
|
||||
communities = [{0}, {1}, {2}, {3}]
|
||||
q = eg.functions.community.modularity(self.G, communities)
|
||||
self.assertIsInstance(q, float)
|
||||
|
||||
def test_empty_graph(self):
|
||||
with self.assertRaises(ZeroDivisionError):
|
||||
eg.functions.community.modularity(self.G_empty, [])
|
||||
|
||||
def test_empty_community_list(self):
|
||||
q = eg.functions.community.modularity(self.G, [])
|
||||
self.assertEqual(q, 0.0)
|
||||
|
||||
def test_non_list_communities(self):
|
||||
communities = (set([0, 1]), set([2, 3]))
|
||||
q = eg.functions.community.modularity(self.G, communities)
|
||||
self.assertIsInstance(q, float)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,81 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
class TestGreedyModularityCommunities(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# A simple connected graph
|
||||
self.graph_simple = eg.Graph()
|
||||
self.graph_simple.add_edges_from([(0, 1), (1, 2), (3, 4)])
|
||||
|
||||
# A weighted graph
|
||||
self.graph_weighted = eg.Graph()
|
||||
self.graph_weighted.add_edges_from(
|
||||
[(0, 1, {"weight": 3}), (1, 2, {"weight": 2}), (3, 4, {"weight": 1})]
|
||||
)
|
||||
|
||||
# A fully connected graph (clique)
|
||||
self.graph_clique = eg.Graph()
|
||||
self.graph_clique.add_edges_from([(0, 1), (0, 2), (1, 2)])
|
||||
|
||||
# A disconnected graph
|
||||
self.graph_disconnected = eg.Graph()
|
||||
self.graph_disconnected.add_edges_from([(0, 1), (2, 3), (4, 5)])
|
||||
|
||||
# A graph with a single node
|
||||
self.graph_single_node = eg.Graph()
|
||||
self.graph_single_node.add_node(42)
|
||||
|
||||
# An empty graph
|
||||
self.graph_empty = eg.Graph()
|
||||
|
||||
def test_communities_simple(self):
|
||||
result = eg.functions.community.greedy_modularity_communities(self.graph_simple)
|
||||
flat_nodes = {node for group in result for node in group}
|
||||
self.assertSetEqual(flat_nodes, set(self.graph_simple.nodes))
|
||||
|
||||
def test_communities_weighted(self):
|
||||
result = eg.functions.community.greedy_modularity_communities(
|
||||
self.graph_weighted
|
||||
)
|
||||
flat_nodes = {node for group in result for node in group}
|
||||
self.assertSetEqual(flat_nodes, set(self.graph_weighted.nodes))
|
||||
|
||||
def test_communities_clique(self):
|
||||
result = eg.functions.community.greedy_modularity_communities(self.graph_clique)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertSetEqual(result[0], set(self.graph_clique.nodes))
|
||||
|
||||
def test_communities_disconnected(self):
|
||||
result = eg.functions.community.greedy_modularity_communities(
|
||||
self.graph_disconnected
|
||||
)
|
||||
flat_nodes = {node for group in result for node in group}
|
||||
self.assertSetEqual(flat_nodes, set(self.graph_disconnected.nodes))
|
||||
|
||||
def test_communities_single_node(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
eg.functions.community.greedy_modularity_communities(self.graph_single_node)
|
||||
|
||||
def test_communities_empty_graph(self):
|
||||
with self.assertRaises(SystemExit):
|
||||
eg.functions.community.greedy_modularity_communities(self.graph_empty)
|
||||
|
||||
def test_correct_partition_disjoint(self):
|
||||
result = eg.functions.community.greedy_modularity_communities(
|
||||
self.graph_disconnected
|
||||
)
|
||||
all_nodes = [node for group in result for node in group]
|
||||
self.assertEqual(len(all_nodes), len(set(all_nodes)))
|
||||
|
||||
def test_communities_sorted_by_size(self):
|
||||
result = eg.functions.community.greedy_modularity_communities(
|
||||
self.graph_disconnected
|
||||
)
|
||||
sizes = [len(group) for group in result]
|
||||
self.assertEqual(sizes, sorted(sizes, reverse=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,89 @@
|
||||
import random
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
class TestMotif:
|
||||
@classmethod
|
||||
def setup_class(self):
|
||||
self.G = eg.Graph()
|
||||
self.G.add_nodes_from([1, 2, 3, 4, 5])
|
||||
self.G.add_edges_from([(1, 3), (2, 3), (3, 4), (4, 5), (3, 5)])
|
||||
|
||||
def test_esu(self):
|
||||
res = eg.enumerate_subgraph(self.G, 3)
|
||||
res = [list(x) for x in res]
|
||||
exp_res = [{1, 3, 4}, {1, 2, 3}, {1, 3, 5}, {2, 3, 5}, {2, 3, 4}, {3, 4, 5}]
|
||||
exp_res = [list(x) for x in exp_res]
|
||||
assert sorted(res) == sorted(exp_res)
|
||||
|
||||
|
||||
class TestMotifEnumeration(unittest.TestCase):
|
||||
def setUp(self):
|
||||
# Triangle plus a tail
|
||||
self.G = eg.Graph()
|
||||
self.G.add_edges_from(
|
||||
[(1, 2), (2, 3), (3, 1), (3, 4), (4, 5)] # triangle # tail
|
||||
)
|
||||
|
||||
def test_esu_enumeration_correct(self):
|
||||
motifs = eg.enumerate_subgraph(self.G, 3)
|
||||
motifs = [frozenset(m) for m in motifs]
|
||||
expected = [{1, 2, 3}, {2, 3, 4}, {3, 4, 5}]
|
||||
expected = [frozenset(x) for x in expected]
|
||||
self.assertTrue(all(m in motifs for m in expected))
|
||||
for m in motifs:
|
||||
self.assertEqual(len(m), 3)
|
||||
self.assertTrue(isinstance(m, frozenset))
|
||||
|
||||
def test_empty_graph(self):
|
||||
G = eg.Graph()
|
||||
motifs = eg.enumerate_subgraph(G, 3)
|
||||
self.assertEqual(motifs, [])
|
||||
|
||||
def test_graph_smaller_than_k(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(1, 2)])
|
||||
motifs = eg.enumerate_subgraph(G, 3)
|
||||
self.assertEqual(motifs, [])
|
||||
|
||||
def test_k_equals_1(self):
|
||||
G = eg.Graph()
|
||||
G.add_nodes_from([1, 2, 3])
|
||||
motifs = eg.enumerate_subgraph(G, 1)
|
||||
expected = [{1}, {2}, {3}]
|
||||
motifs = [set(m) for m in motifs]
|
||||
self.assertEqual(sorted(motifs), sorted(expected))
|
||||
|
||||
def test_random_enumerate_cut_prob_valid(self):
|
||||
random.seed(0)
|
||||
cut_prob = [1.0] * 3
|
||||
motifs = eg.random_enumerate_subgraph(self.G, 3, cut_prob)
|
||||
for m in motifs:
|
||||
self.assertEqual(len(m), 3)
|
||||
|
||||
def test_random_enumerate_cut_prob_invalid_length(self):
|
||||
cut_prob = [1.0, 0.9]
|
||||
with self.assertRaises(eg.EasyGraphError):
|
||||
eg.random_enumerate_subgraph(self.G, 3, cut_prob)
|
||||
|
||||
def test_random_enumerate_zero_cut_prob(self):
|
||||
cut_prob = [0.0, 0.0, 0.0]
|
||||
motifs = eg.random_enumerate_subgraph(self.G, 3, cut_prob)
|
||||
self.assertEqual(motifs, [])
|
||||
|
||||
def test_directed_graph_enumeration(self):
|
||||
DG = eg.DiGraph()
|
||||
DG.add_edges_from([(1, 2), (2, 3), (3, 1)])
|
||||
motifs = eg.enumerate_subgraph(DG, 3)
|
||||
motifs = [set(m) for m in motifs]
|
||||
self.assertIn({1, 2, 3}, motifs)
|
||||
|
||||
def test_multigraph_error(self):
|
||||
MG = eg.MultiGraph()
|
||||
MG.add_edges_from([(1, 2), (2, 3)])
|
||||
with self.assertRaises(eg.EasyGraphNotImplemented):
|
||||
eg.enumerate_subgraph(MG, 3)
|
||||
with self.assertRaises(eg.EasyGraphNotImplemented):
|
||||
eg.random_enumerate_subgraph(MG, 3, [1.0] * 3)
|
||||
@@ -0,0 +1,4 @@
|
||||
from .biconnected import *
|
||||
from .connected import *
|
||||
from .strongly_connected import *
|
||||
from .weakly_connected import *
|
||||
@@ -0,0 +1,247 @@
|
||||
from itertools import chain
|
||||
|
||||
from easygraph.utils import *
|
||||
|
||||
|
||||
__all__ = [
|
||||
"is_biconnected",
|
||||
"biconnected_components",
|
||||
"generator_biconnected_components_nodes",
|
||||
"generator_biconnected_components_edges",
|
||||
"generator_articulation_points",
|
||||
]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph", "directed")
|
||||
def is_biconnected(G):
|
||||
"""Returns whether the graph is biconnected or not.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_biconnected : boolean
|
||||
`True` if the graph is biconnected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> is_biconnected(G)
|
||||
|
||||
"""
|
||||
bc_nodes = list(generator_biconnected_components_nodes(G))
|
||||
if len(bc_nodes) == 1:
|
||||
return len(bc_nodes[0]) == len(
|
||||
G
|
||||
) # avoid situations where there is isolated vertex
|
||||
return False
|
||||
|
||||
|
||||
@not_implemented_for("multigraph", "directed")
|
||||
# TODO: get the subgraph of each biconnected graph
|
||||
def biconnected_components(G):
|
||||
"""Returns a list of biconnected components, each of which denotes the edges set of a biconnected component.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
Returns
|
||||
-------
|
||||
biconnected_components : list of list
|
||||
Each element list is the edges set of a biconnected component.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> connected_components(G)
|
||||
|
||||
"""
|
||||
return list(generator_biconnected_components_edges(G))
|
||||
|
||||
|
||||
@not_implemented_for("multigraph", "directed")
|
||||
def generator_biconnected_components_nodes(G):
|
||||
"""Returns a generator of nodes in each biconnected component.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
Returns
|
||||
-------
|
||||
Yields nodes set of each biconnected component.
|
||||
|
||||
See Also
|
||||
--------
|
||||
generator_biconnected_components_edges
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> generator_biconnected_components_nodes(G)
|
||||
|
||||
|
||||
"""
|
||||
for component in _biconnected_dfs_record_edges(G, need_components=True):
|
||||
# TODO: only one edge = biconnected_component?
|
||||
yield set(chain.from_iterable(component))
|
||||
|
||||
|
||||
@not_implemented_for("multigraph", "directed")
|
||||
def generator_biconnected_components_edges(G):
|
||||
"""Returns a generator of nodes in each biconnected component.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
Returns
|
||||
-------
|
||||
Yields edges set of each biconnected component.
|
||||
|
||||
See Also
|
||||
--------
|
||||
generator_biconnected_components_nodes
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> generator_biconnected_components_edges(G)
|
||||
|
||||
"""
|
||||
yield from _biconnected_dfs_record_edges(G, need_components=True)
|
||||
|
||||
|
||||
@not_implemented_for("multigraph", "directed")
|
||||
def generator_articulation_points(G):
|
||||
"""Returns a generator of articulation points.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
Returns
|
||||
-------
|
||||
Yields the articulation point in *G*.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> generator_articulation_points(G)
|
||||
|
||||
"""
|
||||
seen = set()
|
||||
for cut_vertex in _biconnected_dfs_record_edges(G, need_components=False):
|
||||
if cut_vertex not in seen:
|
||||
seen.add(cut_vertex)
|
||||
yield cut_vertex
|
||||
|
||||
|
||||
@hybrid("cpp_biconnected_dfs_record_edges")
|
||||
def _biconnected_dfs_record_edges(G, need_components=True):
|
||||
"""
|
||||
References
|
||||
----------
|
||||
https://www.cnblogs.com/nullzx/p/7968110.html
|
||||
https://blog.csdn.net/gauss_acm/article/details/43493903
|
||||
"""
|
||||
# record edges of each biconnected component in traversal
|
||||
# Copied version from EasyGraph
|
||||
# depth-first search algorithm to generate articulation points
|
||||
# and biconnected components
|
||||
visited = set()
|
||||
for start in G:
|
||||
if start in visited:
|
||||
continue
|
||||
discovery = {start: 0} # time of first discovery of node during search
|
||||
low = {start: 0}
|
||||
root_children = 0
|
||||
visited.add(start)
|
||||
edge_stack = []
|
||||
stack = [(start, start, iter(G[start]))]
|
||||
while stack:
|
||||
grandparent, parent, children = stack[-1]
|
||||
try:
|
||||
child = next(children)
|
||||
if grandparent == child:
|
||||
continue
|
||||
if child in visited:
|
||||
if discovery[child] <= discovery[parent]: # back edge
|
||||
low[parent] = min(low[parent], discovery[child])
|
||||
if need_components:
|
||||
edge_stack.append((parent, child))
|
||||
else:
|
||||
low[child] = discovery[child] = len(discovery)
|
||||
visited.add(child)
|
||||
stack.append((parent, child, iter(G[child])))
|
||||
if need_components:
|
||||
edge_stack.append((parent, child))
|
||||
except StopIteration:
|
||||
stack.pop()
|
||||
if len(stack) > 1:
|
||||
if low[parent] >= discovery[grandparent]:
|
||||
if need_components:
|
||||
ind = edge_stack.index((grandparent, parent))
|
||||
yield edge_stack[ind:]
|
||||
edge_stack = edge_stack[:ind]
|
||||
else:
|
||||
yield grandparent
|
||||
low[grandparent] = min(low[parent], low[grandparent])
|
||||
elif stack: # length 1 so grandparent is root
|
||||
root_children += 1
|
||||
if need_components:
|
||||
ind = edge_stack.index((grandparent, parent))
|
||||
yield edge_stack[ind:]
|
||||
if not need_components:
|
||||
# root node is articulation point if it has more than 1 child
|
||||
if root_children > 1:
|
||||
yield start
|
||||
|
||||
|
||||
def _biconnected_dfs_record_nodes(G, need_components=True):
|
||||
# record nodes of each biconnected component in traversal
|
||||
# Not used.
|
||||
visited = set()
|
||||
for start in G:
|
||||
if start in visited:
|
||||
continue
|
||||
discovery = {start: 0} # time of first discovery of node during search
|
||||
low = {start: 0}
|
||||
root_children = 0
|
||||
visited.add(start)
|
||||
node_stack = [start]
|
||||
stack = [(start, start, iter(G[start]))]
|
||||
while stack:
|
||||
grandparent, parent, children = stack[-1]
|
||||
try:
|
||||
child = next(children)
|
||||
if grandparent == child:
|
||||
continue
|
||||
if child in visited:
|
||||
if discovery[child] <= discovery[parent]: # back edge
|
||||
low[parent] = min(low[parent], discovery[child])
|
||||
else:
|
||||
low[child] = discovery[child] = len(discovery)
|
||||
visited.add(child)
|
||||
stack.append((parent, child, iter(G[child])))
|
||||
if need_components:
|
||||
node_stack.append(child)
|
||||
except StopIteration:
|
||||
stack.pop()
|
||||
if len(stack) > 1:
|
||||
if low[parent] >= discovery[grandparent]:
|
||||
if need_components:
|
||||
ind = node_stack.index(grandparent)
|
||||
yield node_stack[ind:]
|
||||
node_stack = node_stack[: ind + 1]
|
||||
else:
|
||||
yield grandparent
|
||||
low[grandparent] = min(low[parent], low[grandparent])
|
||||
elif stack: # length 1 so grandparent is root
|
||||
root_children += 1
|
||||
if need_components:
|
||||
ind = node_stack.index(grandparent)
|
||||
yield node_stack[ind:]
|
||||
if not need_components:
|
||||
# root node is articulation point if it has more than 1 child
|
||||
if root_children > 1:
|
||||
yield start
|
||||
@@ -0,0 +1,160 @@
|
||||
from easygraph.utils.decorators import *
|
||||
|
||||
|
||||
__all__ = [
|
||||
"is_connected",
|
||||
"number_connected_components",
|
||||
"connected_components",
|
||||
"connected_components_directed",
|
||||
"connected_component_of_node",
|
||||
]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def is_connected(G):
|
||||
"""Returns whether the graph is connected or not.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
Returns
|
||||
-------
|
||||
is_biconnected : boolean
|
||||
`True` if the graph is connected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> is_connected(G)
|
||||
|
||||
"""
|
||||
assert len(G) != 0, "No node in the graph."
|
||||
arbitrary_node = next(iter(G)) # Pick an arbitrary node to run BFS
|
||||
return len(G) == sum(1 for node in _plain_bfs(G, arbitrary_node))
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def number_connected_components(G):
|
||||
"""Returns the number of connected components.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
number_connected_components : int
|
||||
The number of connected components.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> number_connected_components(G)
|
||||
|
||||
"""
|
||||
return sum(1 for component in _generator_connected_components(G))
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@hybrid("cpp_connected_components_undirected")
|
||||
def connected_components(G):
|
||||
"""Returns a list of connected components, each of which denotes the edges set of a connected component.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph
|
||||
Returns
|
||||
-------
|
||||
connected_components : list of list
|
||||
Each element list is the edges set of a connected component.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> connected_components(G)
|
||||
|
||||
"""
|
||||
seen = set()
|
||||
for v in G:
|
||||
if v not in seen:
|
||||
c = set(_plain_bfs(G, v))
|
||||
seen.update(c)
|
||||
yield c
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@hybrid("cpp_connected_components_directed")
|
||||
def connected_components_directed(G):
|
||||
"""Returns a list of connected components, each of which denotes the edges set of a connected component.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.DiGraph
|
||||
Returns
|
||||
-------
|
||||
connected_components : list of list
|
||||
Each element list is the edges set of a connected component.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> connected_components(G)
|
||||
|
||||
"""
|
||||
seen = set()
|
||||
for v in G:
|
||||
if v not in seen:
|
||||
c = set(_plain_bfs(G, v))
|
||||
seen.update(c)
|
||||
yield c
|
||||
|
||||
|
||||
def _generator_connected_components(G):
|
||||
seen = set()
|
||||
for v in G:
|
||||
if v not in seen:
|
||||
component = set(_plain_bfs(G, v))
|
||||
yield component
|
||||
seen.update(component)
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def connected_component_of_node(G, node):
|
||||
"""Returns the connected component that *node* belongs to.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph
|
||||
|
||||
node : object
|
||||
The target node
|
||||
|
||||
Returns
|
||||
-------
|
||||
connected_component_of_node : set
|
||||
The connected component that *node* belongs to.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Returns the connected component of one node `Jack`.
|
||||
|
||||
>>> connected_component_of_node(G, node='Jack')
|
||||
|
||||
"""
|
||||
return set(_plain_bfs(G, node))
|
||||
|
||||
|
||||
@hybrid("cpp_plain_bfs")
|
||||
def _plain_bfs(G, source):
|
||||
"""
|
||||
A fast BFS node generator
|
||||
"""
|
||||
G_adj = G.adj
|
||||
seen = set()
|
||||
nextlevel = {source}
|
||||
while nextlevel:
|
||||
thislevel = nextlevel
|
||||
nextlevel = set()
|
||||
for v in thislevel:
|
||||
if v not in seen:
|
||||
yield v
|
||||
seen.add(v)
|
||||
nextlevel.update(G_adj[v])
|
||||
@@ -0,0 +1,244 @@
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.decorators import *
|
||||
|
||||
|
||||
__all__ = [
|
||||
"number_strongly_connected_components",
|
||||
"strongly_connected_components",
|
||||
"is_strongly_connected",
|
||||
"condensation",
|
||||
]
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
@hybrid("cpp_strongly_connected_components")
|
||||
def strongly_connected_components(G):
|
||||
"""Generate nodes in strongly connected components of graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph Graph
|
||||
A directed graph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
comp : generator of sets
|
||||
A generator of sets of nodes, one for each strongly connected
|
||||
component of G.
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphNotImplemented
|
||||
If G is undirected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Generate a sorted list of strongly connected components, largest first.
|
||||
|
||||
If you only want the largest component, it's more efficient to
|
||||
use max instead of sort.
|
||||
|
||||
>>> largest = max(eg.strongly_connected_components(G), key=len)
|
||||
|
||||
See Also
|
||||
--------
|
||||
connected_components
|
||||
|
||||
Notes
|
||||
-----
|
||||
Uses Tarjan's algorithm[1]_ with Nuutila's modifications[2]_.
|
||||
Nonrecursive version of algorithm.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Depth-first search and linear graph algorithms, R. Tarjan
|
||||
SIAM Journal of Computing 1(2):146-160, (1972).
|
||||
|
||||
.. [2] On finding the strongly connected components in a directed graph.
|
||||
E. Nuutila and E. Soisalon-Soinen
|
||||
Information Processing Letters 49(1): 9-14, (1994)..
|
||||
|
||||
"""
|
||||
preorder = {}
|
||||
lowlink = {}
|
||||
scc_found = set()
|
||||
scc_queue = []
|
||||
i = 0 # Preorder counter
|
||||
neighbors = {v: iter(G[v]) for v in G}
|
||||
for source in G:
|
||||
if source not in scc_found:
|
||||
queue = [source]
|
||||
while queue:
|
||||
v = queue[-1]
|
||||
if v not in preorder:
|
||||
i = i + 1
|
||||
preorder[v] = i
|
||||
done = True
|
||||
for w in neighbors[v]:
|
||||
if w not in preorder:
|
||||
queue.append(w)
|
||||
done = False
|
||||
break
|
||||
if done:
|
||||
lowlink[v] = preorder[v]
|
||||
for w in G[v]:
|
||||
if w not in scc_found:
|
||||
if preorder[w] > preorder[v]:
|
||||
lowlink[v] = min([lowlink[v], lowlink[w]])
|
||||
else:
|
||||
lowlink[v] = min([lowlink[v], preorder[w]])
|
||||
queue.pop()
|
||||
if lowlink[v] == preorder[v]:
|
||||
scc = {v}
|
||||
while scc_queue and preorder[scc_queue[-1]] > preorder[v]:
|
||||
k = scc_queue.pop()
|
||||
scc.add(k)
|
||||
scc_found.update(scc)
|
||||
yield scc
|
||||
else:
|
||||
scc_queue.append(v)
|
||||
|
||||
|
||||
def number_strongly_connected_components(G):
|
||||
"""Returns number of strongly connected components in graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : Easygraph graph
|
||||
A directed graph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
n : integer
|
||||
Number of strongly connected components
|
||||
|
||||
Raises
|
||||
------
|
||||
EasygraphNotImplemented
|
||||
If G is undirected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.DiGraph([(0, 1), (1, 2), (2, 0), (2, 3), (4, 5), (3, 4), (5, 6), (6, 3), (6, 7)])
|
||||
>>> eg.number_strongly_connected_components(G)
|
||||
3
|
||||
|
||||
See Also
|
||||
--------
|
||||
strongly_connected_components
|
||||
number_connected_components
|
||||
|
||||
Notes
|
||||
-----
|
||||
For directed graphs only.
|
||||
"""
|
||||
return sum(1 for scc in strongly_connected_components(G))
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
def is_strongly_connected(G):
|
||||
"""Test directed graph for strong connectivity.
|
||||
|
||||
A directed graph is strongly connected if and only if every vertex in
|
||||
the graph is reachable from every other vertex.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph Graph
|
||||
A directed graph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
connected : bool
|
||||
True if the graph is strongly connected, False otherwise.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.DiGraph([(0, 1), (1, 2), (2, 3), (3, 0), (2, 4), (4, 2)])
|
||||
>>> eg.is_strongly_connected(G)
|
||||
True
|
||||
>>> G.remove_edge(2, 3)
|
||||
>>> eg.is_strongly_connected(G)
|
||||
False
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphNotImplemented
|
||||
If G is undirected.
|
||||
|
||||
See Also
|
||||
--------
|
||||
is_connected
|
||||
is_biconnected
|
||||
strongly_connected_components
|
||||
|
||||
Notes
|
||||
-----
|
||||
For directed graphs only.
|
||||
"""
|
||||
if len(G) == 0:
|
||||
raise eg.EasyGraphPointlessConcept(
|
||||
"""Connectivity is undefined for the null graph."""
|
||||
)
|
||||
|
||||
return len(next(strongly_connected_components(G))) == len(G)
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@only_implemented_for_Directed_graph
|
||||
def condensation(G, scc=None):
|
||||
"""Returns the condensation of G.
|
||||
The condensation of G is the graph with each of the strongly connected
|
||||
components contracted into a single node.
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.DiGraph
|
||||
A directed graph.
|
||||
scc: list or generator (optional, default=None)
|
||||
Strongly connected components. If provided, the elements in
|
||||
`scc` must partition the nodes in `G`. If not provided, it will be
|
||||
calculated as scc=strongly_connected_components(G).
|
||||
Returns
|
||||
-------
|
||||
C : easygraph.DiGraph
|
||||
The condensation graph C of G. The node labels are integers
|
||||
corresponding to the index of the component in the list of
|
||||
strongly connected components of G. C has a graph attribute named
|
||||
'mapping' with a dictionary mapping the original nodes to the
|
||||
nodes in C to which they belong. Each node in C also has a node
|
||||
attribute 'members' with the set of original nodes in G that
|
||||
form the SCC that the node in C represents.
|
||||
Examples
|
||||
--------
|
||||
# >>> condensation(G)
|
||||
Notes
|
||||
-----
|
||||
After contracting all strongly connected components to a single node,
|
||||
the resulting graph is a directed acyclic graph.
|
||||
"""
|
||||
if scc is None:
|
||||
scc = strongly_connected_components(G)
|
||||
mapping = {}
|
||||
incoming_info = {}
|
||||
members = {}
|
||||
C = eg.DiGraph()
|
||||
# Add mapping dict as graph attribute
|
||||
C.graph["mapping"] = mapping
|
||||
if len(G) == 0:
|
||||
return C
|
||||
for i, component in enumerate(scc):
|
||||
members[i] = component
|
||||
mapping.update((n, i) for n in component)
|
||||
number_of_components = i + 1
|
||||
for i in range(number_of_components):
|
||||
C.add_node(i, member=members[i], incoming=set())
|
||||
C.add_nodes(range(number_of_components))
|
||||
for edge in G.edges:
|
||||
if mapping[edge[0]] != mapping[edge[1]]:
|
||||
C.add_edge(mapping[edge[0]], mapping[edge[1]])
|
||||
if edge[1] not in incoming_info.keys():
|
||||
incoming_info[edge[1]] = set()
|
||||
incoming_info[edge[1]].add(edge[0])
|
||||
C.graph["incoming_info"] = incoming_info
|
||||
return C
|
||||
@@ -0,0 +1,92 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
from easygraph import biconnected_components
|
||||
from easygraph import generator_articulation_points
|
||||
from easygraph import generator_biconnected_components_edges
|
||||
from easygraph import generator_biconnected_components_nodes
|
||||
from easygraph import is_biconnected
|
||||
|
||||
|
||||
class Test_biconnected(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [(1, 2), (2, 3), ("String", "Bool"), (2, 1), (0, 0), (-99, 256)]
|
||||
self.test_graphs = [eg.Graph(), eg.MultiGraph(), eg.DiGraph()]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
|
||||
def test_is_biconnected(self):
|
||||
for i in self.test_graphs:
|
||||
print(eg.is_biconnected(i))
|
||||
|
||||
def test_biconnected_components(self):
|
||||
for i in self.test_graphs:
|
||||
eg.biconnected_components(i)
|
||||
|
||||
def test_generator_biconnected_components_nodes(self):
|
||||
for i in self.test_graphs:
|
||||
eg.generator_biconnected_components_nodes(i)
|
||||
|
||||
def test_generator_biconnected_components_edges(self):
|
||||
for i in self.test_graphs:
|
||||
eg.generator_biconnected_components_edges(i)
|
||||
|
||||
def test_generator_articulation_points(self):
|
||||
for i in self.test_graphs:
|
||||
eg.generator_articulation_points(i)
|
||||
|
||||
|
||||
class TestBiconnectedFunctions(unittest.TestCase):
|
||||
def test_single_node(self):
|
||||
G = eg.Graph()
|
||||
G.add_node(1)
|
||||
self.assertFalse(is_biconnected(G))
|
||||
self.assertEqual(list(biconnected_components(G)), [])
|
||||
self.assertEqual(list(generator_articulation_points(G)), [])
|
||||
|
||||
def test_disconnected_graph(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (2, 3)])
|
||||
self.assertFalse(is_biconnected(G))
|
||||
self.assertGreaterEqual(len(list(generator_biconnected_components_edges(G))), 1)
|
||||
|
||||
def test_triangle(self):
|
||||
G = eg.Graph([(0, 1), (1, 2), (2, 0)])
|
||||
self.assertTrue(is_biconnected(G))
|
||||
comps = list(biconnected_components(G))
|
||||
self.assertEqual(len(comps), 1)
|
||||
self.assertEqual(set(comps[0]), {(0, 1), (1, 2), (2, 0)})
|
||||
self.assertEqual(list(generator_articulation_points(G)), [])
|
||||
|
||||
def test_with_articulation_point(self):
|
||||
G = eg.Graph([(0, 1), (1, 2), (1, 3)])
|
||||
self.assertFalse(is_biconnected(G))
|
||||
arts = list(generator_articulation_points(G))
|
||||
self.assertIn(1, arts)
|
||||
self.assertEqual(len(arts), 1)
|
||||
|
||||
def test_cycle_plus_leaf(self):
|
||||
G = eg.Graph([(0, 1), (1, 2), (2, 0), (2, 3)])
|
||||
self.assertFalse(is_biconnected(G))
|
||||
arts = list(generator_articulation_points(G))
|
||||
self.assertIn(2, arts)
|
||||
|
||||
def test_multiple_biconnected_components(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(1, 2), (2, 3), (3, 1)]) # triangle
|
||||
G.add_edges_from([(3, 4), (4, 5)]) # path
|
||||
components = list(generator_biconnected_components_edges(G))
|
||||
self.assertEqual(len(components), 3)
|
||||
nodes_comps = list(generator_biconnected_components_nodes(G))
|
||||
self.assertTrue(any({1, 2, 3}.issubset(comp) for comp in nodes_comps))
|
||||
self.assertTrue(any({4, 5}.issubset(comp) for comp in nodes_comps))
|
||||
|
||||
def test_articulation_points_multiple(self):
|
||||
G = eg.Graph([(0, 1), (1, 2), (2, 3), (3, 4)])
|
||||
aps = list(generator_articulation_points(G))
|
||||
self.assertEqual(aps, [3, 2, 1])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,112 @@
|
||||
import inspect
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph import connected_component_of_node
|
||||
from easygraph import connected_components
|
||||
from easygraph import connected_components_directed
|
||||
from easygraph import is_connected
|
||||
from easygraph import number_connected_components
|
||||
from easygraph.utils.exception import EasyGraphNotImplemented
|
||||
|
||||
|
||||
class TestConnected(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [(1, 2), (2, 3), (0, 4), (2, 1), (0, 0), (-99, 256)]
|
||||
self.test_graphs = [eg.Graph([(4, -4)]), eg.DiGraph([(4, -4)])]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
|
||||
def test_is_connected(self):
|
||||
for i in self.test_graphs:
|
||||
print(eg.is_connected(i))
|
||||
|
||||
def test_number_connected_components(self):
|
||||
for i in self.test_graphs:
|
||||
print(eg.number_connected_components(i))
|
||||
|
||||
def test_connected_components(self):
|
||||
for i in self.test_graphs:
|
||||
print(eg.connected_components(i))
|
||||
|
||||
def test_connected_components_directed(self):
|
||||
for i in self.test_graphs:
|
||||
print(eg.connected_components_directed(i))
|
||||
|
||||
def test_connected_component_of_node(self):
|
||||
for i in self.test_graphs:
|
||||
print(eg.connected_component_of_node(i, 4))
|
||||
|
||||
def test_empty_graph(self):
|
||||
G = eg.Graph()
|
||||
with self.assertRaises(AssertionError):
|
||||
is_connected(G)
|
||||
self.assertEqual(number_connected_components(G), 0)
|
||||
self.assertEqual(list(connected_components(G)), [])
|
||||
|
||||
def test_single_node(self):
|
||||
G = eg.Graph()
|
||||
G.add_node(1)
|
||||
self.assertTrue(is_connected(G))
|
||||
self.assertEqual(number_connected_components(G), 1)
|
||||
self.assertEqual(list(connected_components(G)), [{1}])
|
||||
self.assertEqual(connected_component_of_node(G, 1), {1})
|
||||
|
||||
def test_disconnected_graph(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (2, 3)])
|
||||
self.assertFalse(is_connected(G))
|
||||
self.assertEqual(number_connected_components(G), 2)
|
||||
comps = list(connected_components(G))
|
||||
self.assertTrue({0, 1} in comps and {2, 3} in comps)
|
||||
|
||||
def test_connected_graph(self):
|
||||
G = eg.path_graph(5)
|
||||
self.assertTrue(is_connected(G))
|
||||
self.assertEqual(number_connected_components(G), 1)
|
||||
comps = list(connected_components(G))
|
||||
self.assertEqual(len(comps), 1)
|
||||
self.assertEqual(comps[0], set(range(5)))
|
||||
|
||||
def test_node_component_lookup(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (2, 3)])
|
||||
comp = connected_component_of_node(G, 0)
|
||||
self.assertEqual(comp, {0, 1})
|
||||
with self.assertRaises(KeyError):
|
||||
connected_component_of_node(G, 999) # non-existent node
|
||||
|
||||
def test_undirected_with_self_loops(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(1, 1), (2, 2), (1, 2)])
|
||||
self.assertTrue(is_connected(G))
|
||||
self.assertEqual(number_connected_components(G), 1)
|
||||
self.assertEqual(list(connected_components(G))[0], {1, 2})
|
||||
|
||||
def test_directed_components(self):
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(0, 1), (2, 3)])
|
||||
self.assertEqual(number_connected_components(G), 2)
|
||||
components = list(connected_components_directed(G))
|
||||
self.assertTrue({0, 1} in components and {2, 3} in components)
|
||||
|
||||
def test_directed_strong_vs_weak(self):
|
||||
G = eg.DiGraph([(0, 1), (1, 0), (2, 3)])
|
||||
comps = list(connected_components_directed(G))
|
||||
self.assertTrue({0, 1} in comps)
|
||||
self.assertTrue({2, 3} in comps)
|
||||
|
||||
def test_multigraph_blocked(self):
|
||||
G = eg.MultiGraph([(1, 2), (2, 3)])
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
is_connected(G)
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
number_connected_components(G)
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
list(connected_components(G))
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
connected_component_of_node(G, 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,121 @@
|
||||
import inspect
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph import condensation
|
||||
from easygraph import is_strongly_connected
|
||||
from easygraph import number_strongly_connected_components
|
||||
from easygraph import strongly_connected_components
|
||||
from easygraph.utils.exception import EasyGraphNotImplemented
|
||||
from easygraph.utils.exception import EasyGraphPointlessConcept
|
||||
|
||||
|
||||
class Test_strongly_connected(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [(1, 2), (2, 3), ("String", "Bool"), (2, 1), (0, 0), (-99, 256)]
|
||||
self.test_graphs = [eg.Graph([(4, -4)]), eg.DiGraph([(4, False)])]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
|
||||
def test_empty_graph(self):
|
||||
G = eg.DiGraph()
|
||||
with self.assertRaises(EasyGraphPointlessConcept):
|
||||
is_strongly_connected(G)
|
||||
self.assertEqual(number_strongly_connected_components(G), 0)
|
||||
self.assertEqual(list(strongly_connected_components(G)), [])
|
||||
|
||||
def test_single_node(self):
|
||||
G = eg.DiGraph()
|
||||
G.add_node(1)
|
||||
self.assertTrue(is_strongly_connected(G))
|
||||
self.assertEqual(number_strongly_connected_components(G), 1)
|
||||
scc = list(strongly_connected_components(G))
|
||||
self.assertEqual(scc, [{1}])
|
||||
|
||||
def test_cycle_graph(self):
|
||||
G = eg.DiGraph([(1, 2), (2, 3), (3, 1)])
|
||||
self.assertTrue(is_strongly_connected(G))
|
||||
self.assertEqual(number_strongly_connected_components(G), 1)
|
||||
scc = list(strongly_connected_components(G))
|
||||
self.assertEqual(scc, [{1, 2, 3}])
|
||||
|
||||
def test_disconnected_scc(self):
|
||||
G = eg.DiGraph([(0, 1), (1, 0), (2, 3), (3, 2), (4, 5)])
|
||||
scc = list(strongly_connected_components(G))
|
||||
self.assertEqual(len(scc), 4)
|
||||
self.assertIn({0, 1}, scc)
|
||||
self.assertIn({2, 3}, scc)
|
||||
self.assertIn({4}, scc)
|
||||
self.assertIn({5}, scc)
|
||||
self.assertFalse(is_strongly_connected(G))
|
||||
self.assertEqual(number_strongly_connected_components(G), 4)
|
||||
|
||||
def test_scc_with_self_loops(self):
|
||||
G = eg.DiGraph([(1, 1), (2, 2), (3, 4), (4, 3)])
|
||||
scc = list(strongly_connected_components(G))
|
||||
self.assertEqual(len(scc), 3)
|
||||
self.assertIn({1}, scc)
|
||||
self.assertIn({2}, scc)
|
||||
self.assertIn({3, 4}, scc)
|
||||
|
||||
def test_condensation_structure(self):
|
||||
G = eg.DiGraph(
|
||||
[(0, 1), (1, 2), (2, 0), (2, 3), (4, 5), (3, 4), (5, 6), (6, 3), (6, 7)]
|
||||
)
|
||||
cond = condensation(G)
|
||||
self.assertTrue(cond.is_directed())
|
||||
self.assertIn("mapping", cond.graph)
|
||||
self.assertEqual(len(cond), number_strongly_connected_components(G))
|
||||
|
||||
def has_cycle(G):
|
||||
visited = set()
|
||||
temp_mark = set()
|
||||
|
||||
def visit(node):
|
||||
if node in temp_mark:
|
||||
return True
|
||||
if node in visited:
|
||||
return False
|
||||
temp_mark.add(node)
|
||||
for neighbor in G[node]:
|
||||
if visit(neighbor):
|
||||
return True
|
||||
temp_mark.remove(node)
|
||||
visited.add(node)
|
||||
return False
|
||||
|
||||
return any(visit(v) for v in G)
|
||||
|
||||
self.assertFalse(has_cycle(cond))
|
||||
|
||||
def test_condensation_empty_graph(self):
|
||||
G = eg.DiGraph()
|
||||
C = condensation(G)
|
||||
self.assertEqual(len(C), 0)
|
||||
|
||||
def test_undirected_raises(self):
|
||||
G = eg.Graph([(1, 2), (2, 3)])
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
list(strongly_connected_components(G))
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
is_strongly_connected(G)
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
number_strongly_connected_components(G)
|
||||
|
||||
def test_condensation_on_undirected_graph_raises(self):
|
||||
G = eg.Graph([(1, 2), (2, 3)])
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
condensation(G)
|
||||
|
||||
def test_condensation_manual_scc_input(self):
|
||||
G = eg.DiGraph([(1, 2), (2, 1), (3, 4)])
|
||||
scc = list(strongly_connected_components(G))
|
||||
C = condensation(G, scc=scc)
|
||||
self.assertEqual(len(C.nodes), len(scc))
|
||||
# Check if mapping is consistent
|
||||
all_mapped = set(C.graph["mapping"].keys())
|
||||
self.assertEqual(all_mapped, set(G.nodes))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,82 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph import is_weakly_connected
|
||||
from easygraph import number_weakly_connected_components
|
||||
from easygraph import weakly_connected_components
|
||||
from easygraph.utils.exception import EasyGraphNotImplemented
|
||||
from easygraph.utils.exception import EasyGraphPointlessConcept
|
||||
|
||||
|
||||
class Test_weakly_connected(unittest.TestCase):
|
||||
def test_empty_graph(self):
|
||||
G = eg.DiGraph()
|
||||
with self.assertRaises(EasyGraphPointlessConcept):
|
||||
is_weakly_connected(G)
|
||||
self.assertEqual(number_weakly_connected_components(G), 0)
|
||||
self.assertEqual(list(weakly_connected_components(G)), [])
|
||||
|
||||
def test_single_node(self):
|
||||
G = eg.DiGraph()
|
||||
G.add_node(1)
|
||||
self.assertTrue(is_weakly_connected(G))
|
||||
self.assertEqual(number_weakly_connected_components(G), 1)
|
||||
self.assertEqual(list(weakly_connected_components(G)), [{1}])
|
||||
|
||||
def test_connected_graph(self):
|
||||
G = eg.DiGraph([(1, 2), (2, 3), (3, 4)])
|
||||
self.assertTrue(is_weakly_connected(G))
|
||||
self.assertEqual(number_weakly_connected_components(G), 1)
|
||||
self.assertEqual(list(weakly_connected_components(G)), [{1, 2, 3, 4}])
|
||||
|
||||
def test_disconnected_graph(self):
|
||||
G = eg.DiGraph([(1, 2), (3, 4)])
|
||||
self.assertFalse(is_weakly_connected(G))
|
||||
wcc = list(weakly_connected_components(G))
|
||||
self.assertEqual(len(wcc), 2)
|
||||
self.assertIn({1, 2}, wcc)
|
||||
self.assertIn({3, 4}, wcc)
|
||||
|
||||
def test_self_loops(self):
|
||||
G = eg.DiGraph([(1, 1), (2, 2)])
|
||||
wcc = list(weakly_connected_components(G))
|
||||
self.assertEqual(len(wcc), 2)
|
||||
self.assertIn({1}, wcc)
|
||||
self.assertIn({2}, wcc)
|
||||
self.assertFalse(is_weakly_connected(G))
|
||||
|
||||
def test_multiple_components(self):
|
||||
G = eg.DiGraph([(1, 2), (3, 4), (5, 6), (6, 5)])
|
||||
wcc = list(weakly_connected_components(G))
|
||||
self.assertEqual(number_weakly_connected_components(G), 3)
|
||||
self.assertIn({1, 2}, wcc)
|
||||
self.assertIn({3, 4}, wcc)
|
||||
self.assertIn({5, 6}, wcc)
|
||||
|
||||
def test_unconnected_nodes(self):
|
||||
G = eg.DiGraph([(1, 2), (3, 4)])
|
||||
G.add_node(99) # isolated node
|
||||
wcc = list(weakly_connected_components(G))
|
||||
self.assertEqual(len(wcc), 3)
|
||||
self.assertIn({99}, wcc)
|
||||
|
||||
def test_is_weakly_connected_after_adding_edge(self):
|
||||
G = eg.DiGraph([(0, 1), (2, 1)])
|
||||
G.add_node(3)
|
||||
self.assertFalse(is_weakly_connected(G))
|
||||
G.add_edge(2, 3)
|
||||
self.assertTrue(is_weakly_connected(G))
|
||||
|
||||
def test_undirected_raises(self):
|
||||
G = eg.Graph([(1, 2), (2, 3)])
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
is_weakly_connected(G)
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
number_weakly_connected_components(G)
|
||||
with self.assertRaises(EasyGraphNotImplemented):
|
||||
list(weakly_connected_components(G))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,186 @@
|
||||
"""Weakly connected components."""
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.decorators import not_implemented_for
|
||||
|
||||
|
||||
__all__ = [
|
||||
"number_weakly_connected_components",
|
||||
"weakly_connected_components",
|
||||
"is_weakly_connected",
|
||||
]
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
def weakly_connected_components(G):
|
||||
"""Generate weakly connected components of G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph graph
|
||||
A directed graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
comp : generator of sets
|
||||
A generator of sets of nodes, one for each weakly connected
|
||||
component of G.
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphNotImplemented
|
||||
If G is undirected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Generate a sorted list of weakly connected components, largest first.
|
||||
|
||||
>>> G = eg.path_graph(4, create_using=eg.DiGraph())
|
||||
>>> eg.add_path(G, [10, 11, 12])
|
||||
>>> [
|
||||
... len(c)
|
||||
... for c in sorted(eg.weakly_connected_components(G), key=len, reverse=True)
|
||||
... ]
|
||||
[4, 3]
|
||||
|
||||
If you only want the largest component, it's more efficient to
|
||||
use max instead of sort:
|
||||
|
||||
>>> largest_cc = max(eg.weakly_connected_components(G), key=len)
|
||||
|
||||
See Also
|
||||
--------
|
||||
connected_components
|
||||
strongly_connected_components
|
||||
|
||||
Notes
|
||||
-----
|
||||
For directed graphs only.
|
||||
|
||||
"""
|
||||
seen = set()
|
||||
for v in G:
|
||||
if v not in seen:
|
||||
c = set(_plain_bfs(G, v))
|
||||
seen.update(c)
|
||||
yield c
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
def number_weakly_connected_components(G):
|
||||
"""Returns the number of weakly connected components in G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph graph
|
||||
A directed graph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
n : integer
|
||||
Number of weakly connected components
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphNotImplemented
|
||||
If G is undirected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.DiGraph([(0, 1), (2, 1), (3, 4)])
|
||||
>>> eg.number_weakly_connected_components(G)
|
||||
2
|
||||
|
||||
See Also
|
||||
--------
|
||||
weakly_connected_components
|
||||
number_connected_components
|
||||
number_strongly_connected_components
|
||||
|
||||
Notes
|
||||
-----
|
||||
For directed graphs only.
|
||||
|
||||
"""
|
||||
return sum(1 for wcc in weakly_connected_components(G))
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
def is_weakly_connected(G):
|
||||
"""Test directed graph for weak connectivity.
|
||||
|
||||
A directed graph is weakly connected if and only if the graph
|
||||
is connected when the direction of the edge between nodes is ignored.
|
||||
|
||||
Note that if a graph is strongly connected (i.e. the graph is connected
|
||||
even when we account for directionality), it is by definition weakly
|
||||
connected as well.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph Graph
|
||||
A directed graph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
connected : bool
|
||||
True if the graph is weakly connected, False otherwise.
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphNotImplemented
|
||||
If G is undirected.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.DiGraph([(0, 1), (2, 1)])
|
||||
>>> G.add_node(3)
|
||||
>>> eg.is_weakly_connected(G) # node 3 is not connected to the graph
|
||||
False
|
||||
>>> G.add_edge(2, 3)
|
||||
>>> eg.is_weakly_connected(G)
|
||||
True
|
||||
|
||||
See Also
|
||||
--------
|
||||
is_strongly_connected
|
||||
is_semiconnected
|
||||
is_connected
|
||||
is_biconnected
|
||||
weakly_connected_components
|
||||
|
||||
Notes
|
||||
-----
|
||||
For directed graphs only.
|
||||
|
||||
"""
|
||||
if len(G) == 0:
|
||||
raise eg.EasyGraphPointlessConcept(
|
||||
"""Connectivity is undefined for the null graph."""
|
||||
)
|
||||
|
||||
return len(next(weakly_connected_components(G))) == len(G)
|
||||
|
||||
|
||||
def _plain_bfs(G, source):
|
||||
"""A fast BFS node generator
|
||||
|
||||
The direction of the edge between nodes is ignored.
|
||||
|
||||
For directed graphs only.
|
||||
|
||||
"""
|
||||
Gsucc = G.adj
|
||||
Gpred = G.pred
|
||||
|
||||
seen = set()
|
||||
nextlevel = {source}
|
||||
while nextlevel:
|
||||
thislevel = nextlevel
|
||||
nextlevel = set()
|
||||
for v in thislevel:
|
||||
if v not in seen:
|
||||
seen.add(v)
|
||||
nextlevel.update(Gsucc[v])
|
||||
nextlevel.update(Gpred[v])
|
||||
yield v
|
||||
@@ -0,0 +1 @@
|
||||
from .k_core import *
|
||||
@@ -0,0 +1,72 @@
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils import *
|
||||
|
||||
|
||||
__all__ = [
|
||||
"k_core",
|
||||
]
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import List
|
||||
from typing import Union
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from easygraph import Graph
|
||||
|
||||
|
||||
@hybrid("cpp_k_core")
|
||||
def k_core(G: "Graph") -> Union["Graph", List]:
|
||||
"""
|
||||
Returns the k-core of G.
|
||||
|
||||
A k-core is a maximal subgraph that contains nodes of degree k or more.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph graph
|
||||
A graph or directed graph
|
||||
k : int, optional
|
||||
The order of the core. If not specified return the main core.
|
||||
return_graph : bool, optional
|
||||
If True, return the k-core as a graph. If False, return a list of nodes.
|
||||
|
||||
Returns
|
||||
-------
|
||||
G : EasyGraph graph, if return_graph is True, else a list of nodes
|
||||
The k-core subgraph
|
||||
"""
|
||||
# Create a shallow copy of the input graph
|
||||
H = G.copy()
|
||||
|
||||
# Initialize a dictionary to store the degrees of the nodes
|
||||
degrees = dict(G.degree())
|
||||
# Sort nodes by degree.
|
||||
nodes = sorted(degrees, key=degrees.get)
|
||||
bin_boundaries = [0]
|
||||
curr_degree = 0
|
||||
for i, v in enumerate(nodes):
|
||||
if degrees[v] > curr_degree:
|
||||
bin_boundaries.extend([i] * (degrees[v] - curr_degree))
|
||||
curr_degree = degrees[v]
|
||||
node_pos = {v: pos for pos, v in enumerate(nodes)}
|
||||
# The initial guess for the core number of a node is its degree.
|
||||
core = degrees
|
||||
nbrs = {v: list(G.neighbors(v)) for v in G}
|
||||
for v in nodes:
|
||||
for u in nbrs[v]:
|
||||
if core[u] > core[v]:
|
||||
nbrs[u].remove(v)
|
||||
pos = node_pos[u]
|
||||
bin_start = bin_boundaries[core[u]]
|
||||
node_pos[u] = bin_start
|
||||
node_pos[nodes[bin_start]] = pos
|
||||
nodes[bin_start], nodes[pos] = nodes[pos], nodes[bin_start]
|
||||
bin_boundaries[core[u]] += 1
|
||||
core[u] -= 1
|
||||
ret = [0.0 for i in range(len(G))]
|
||||
for i in range(len(ret)):
|
||||
ret[i] = core[G.index2node[i]]
|
||||
return ret
|
||||
@@ -0,0 +1,101 @@
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
from easygraph import k_core
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"edges,k",
|
||||
[
|
||||
([(1, 2), (1, 3), (2, 3), (2, 4), (3, 4), (4, 5)], 2),
|
||||
([(1, 2), (1, 3), (2, 3), (2, 4), (3, 4), (4, 5)], 3),
|
||||
([(1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 2),
|
||||
([(1, 2), (2, 3), (3, 4), (4, 5), (5, 6)], 3),
|
||||
([(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)], 1),
|
||||
],
|
||||
)
|
||||
def test_k_core(edges, k):
|
||||
nx = pytest.importorskip("networkx")
|
||||
|
||||
from easygraph import Graph
|
||||
from easygraph import k_core
|
||||
|
||||
G = Graph()
|
||||
G_nx = nx.Graph()
|
||||
G.add_edges_from(edges)
|
||||
G_nx.add_edges_from(edges)
|
||||
|
||||
H = k_core(G)
|
||||
H_nx = nx.core_number(G_nx)
|
||||
assert H == list(H_nx.values())
|
||||
|
||||
|
||||
def test_k_core_empty_graph():
|
||||
G = eg.Graph()
|
||||
result = k_core(G)
|
||||
assert result == []
|
||||
|
||||
|
||||
def test_k_core_single_node_isolated():
|
||||
G = eg.Graph()
|
||||
G.add_node(1)
|
||||
result = k_core(G)
|
||||
assert result == [0]
|
||||
|
||||
|
||||
def test_k_core_clique():
|
||||
G = eg.complete_graph(5) # Each node has degree 4
|
||||
result = k_core(G)
|
||||
assert set(result) == {4}
|
||||
|
||||
|
||||
def test_k_core_star_graph():
|
||||
nx = pytest.importorskip("networkx")
|
||||
G = eg.Graph()
|
||||
G.add_node(0)
|
||||
G.add_edges_from((0, i) for i in range(1, 6))
|
||||
result = k_core(G)
|
||||
G_nx = nx.Graph()
|
||||
G_nx.add_node(0)
|
||||
G_nx.add_edges_from((0, i) for i in range(1, 6))
|
||||
expected = list(nx.core_number(G_nx).values())
|
||||
assert sorted(result) == sorted(expected)
|
||||
|
||||
|
||||
def test_k_core_disconnected_components():
|
||||
G = eg.Graph()
|
||||
# Component 1: triangle
|
||||
G.add_edges_from([(0, 1), (1, 2), (2, 0)])
|
||||
# Component 2: line
|
||||
G.add_edges_from([(3, 4)])
|
||||
result = k_core(G)
|
||||
core_component_1 = {result[i] for i in [0, 1, 2]}
|
||||
core_component_2 = {result[i] for i in [3, 4]}
|
||||
assert core_component_1 == {2}
|
||||
assert core_component_2 == {1}
|
||||
|
||||
|
||||
def test_k_core_all_zero_core():
|
||||
G = eg.path_graph(5)
|
||||
result = k_core(G)
|
||||
assert all(isinstance(v, int) or isinstance(v, float) for v in result)
|
||||
assert max(result) <= 2
|
||||
|
||||
|
||||
def test_k_core_index_to_node_mapping_consistency():
|
||||
G = eg.Graph()
|
||||
edges = [(5, 10), (10, 15), (15, 20)]
|
||||
G.add_edges_from(edges)
|
||||
result = k_core(G)
|
||||
for i, node in enumerate(G.index2node):
|
||||
assert isinstance(result[i], (int, float))
|
||||
deg_map = dict(G.degree())
|
||||
if node in deg_map:
|
||||
assert result[i] <= deg_map[node]
|
||||
|
||||
|
||||
def test_k_core_large_k():
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(1, 2), (2, 3)])
|
||||
result = k_core(G)
|
||||
assert max(result) <= 2
|
||||
@@ -0,0 +1,3 @@
|
||||
from .drawing import *
|
||||
from .plot import *
|
||||
from .positioning import *
|
||||
@@ -0,0 +1,253 @@
|
||||
from typing import Any
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
|
||||
def default_style(
|
||||
num_v: int,
|
||||
num_e: int,
|
||||
v_color: Union[str, list] = "r",
|
||||
e_color: Union[str, list] = "gray",
|
||||
e_fill_color: Union[str, list] = "whitesmoke",
|
||||
):
|
||||
_v_color = "r"
|
||||
_e_color = "gray"
|
||||
_e_fill_color = "whitesmoke"
|
||||
|
||||
v_color = fill_color(v_color, _v_color, num_v)
|
||||
e_color = fill_color(e_color, _e_color, num_e)
|
||||
e_fill_color = fill_color(e_fill_color, _e_fill_color, num_e)
|
||||
|
||||
return v_color, e_color, e_fill_color
|
||||
|
||||
|
||||
def default_bipartite_style(
|
||||
num_u: int,
|
||||
num_v: int,
|
||||
num_e: int,
|
||||
u_color: Union[str, list] = "m",
|
||||
v_color: Union[str, list] = "r",
|
||||
e_color: Union[str, list] = "gray",
|
||||
e_fill_color: Union[str, list] = "whitesmoke",
|
||||
):
|
||||
_u_color = "m"
|
||||
_v_color = "r"
|
||||
_e_color = "gray"
|
||||
_e_fill_color = "whitesmoke"
|
||||
|
||||
u_color = fill_color(u_color, _u_color, num_u)
|
||||
v_color = fill_color(v_color, _v_color, num_v)
|
||||
e_color = fill_color(e_color, _e_color, num_e)
|
||||
e_fill_color = fill_color(e_fill_color, _e_fill_color, num_e)
|
||||
|
||||
return u_color, v_color, e_color, e_fill_color
|
||||
|
||||
|
||||
def default_hypergraph_style(
|
||||
num_v: int,
|
||||
num_e: int,
|
||||
v_color: Union[str, list] = "r",
|
||||
e_color: Union[str, list] = "gray",
|
||||
e_fill_color: Union[str, list] = "whitesmoke",
|
||||
):
|
||||
_v_color = "r"
|
||||
_e_color = "gray"
|
||||
_e_fill_color = "whitesmoke"
|
||||
|
||||
v_color = fill_color(v_color, _v_color, num_v)
|
||||
e_color = fill_color(e_color, _e_color, num_e)
|
||||
e_fill_color = fill_color(e_fill_color, _e_fill_color, num_e)
|
||||
|
||||
return v_color, e_color, e_fill_color
|
||||
|
||||
|
||||
def default_size(
|
||||
num_v: int,
|
||||
e_list: List[tuple],
|
||||
v_size: Union[float, list] = 1.0,
|
||||
v_line_width: Union[float, list] = 1.0,
|
||||
e_line_width: Union[float, list] = 1.0,
|
||||
font_size: float = None,
|
||||
):
|
||||
import numpy as np
|
||||
|
||||
_v_size = 1 / np.sqrt(num_v + 10) * 0.1
|
||||
_v_line_width = 1 * np.exp(-num_v / 50)
|
||||
_e_line_width = 1 * np.exp(-len(e_list) / 120)
|
||||
_font_size = 20 * np.exp(-num_v / 100)
|
||||
v_size = fill_sizes(v_size, _v_size, num_v)
|
||||
v_line_width = fill_sizes(v_line_width, _v_line_width, num_v)
|
||||
print("len(e_list):", e_list)
|
||||
e_line_width = fill_sizes(e_line_width, _e_line_width, len(e_list))
|
||||
|
||||
font_size = _font_size if font_size is None else font_size
|
||||
|
||||
return v_size, v_line_width, e_line_width, font_size
|
||||
|
||||
|
||||
def default_bipartite_size(
|
||||
num_u: int,
|
||||
num_v: int,
|
||||
e_list: List[tuple],
|
||||
u_size: Union[float, list] = 1.0,
|
||||
u_line_width: Union[float, list] = 1.0,
|
||||
v_size: Union[float, list] = 1.0,
|
||||
v_line_width: Union[float, list] = 1.0,
|
||||
e_line_width: Union[float, list] = 1.0,
|
||||
u_font_size: float = 1.0,
|
||||
v_font_size: float = 1.0,
|
||||
):
|
||||
import numpy as np
|
||||
|
||||
_u_size = 1 / np.sqrt(num_u + 12) * 0.08
|
||||
_u_line_width = 1 * np.exp(-num_u / 50)
|
||||
_v_size = 1 / np.sqrt(num_v + 12) * 0.08
|
||||
_v_line_width = 1 * np.exp(-num_v / 50)
|
||||
_e_line_width = 1 * np.exp(-len(e_list) / 50)
|
||||
_u_font_size = 12 * np.exp(-((num_u / num_v) ** 0.3) * (num_u + num_v) / 100)
|
||||
_v_font_size = 12 * np.exp(-((num_v / num_u) ** 0.3) * (num_u + num_v) / 100)
|
||||
|
||||
u_size = fill_sizes(u_size, _u_size, num_u)
|
||||
u_line_width = fill_sizes(u_line_width, _u_line_width, num_u)
|
||||
v_size = fill_sizes(v_size, _v_size, num_v)
|
||||
v_line_width = fill_sizes(v_line_width, _v_line_width, num_v)
|
||||
e_line_width = fill_sizes(e_line_width, _e_line_width, len(e_list))
|
||||
|
||||
u_font_size = _u_font_size if u_font_size is None else u_font_size * _u_font_size
|
||||
v_font_size = _v_font_size if v_font_size is None else v_font_size * _v_font_size
|
||||
|
||||
return (
|
||||
u_size,
|
||||
u_line_width,
|
||||
v_size,
|
||||
v_line_width,
|
||||
e_line_width,
|
||||
u_font_size,
|
||||
v_font_size,
|
||||
)
|
||||
|
||||
|
||||
def default_strength(
|
||||
num_v: int,
|
||||
e_list: List[tuple],
|
||||
push_v_strength: float = 1.0,
|
||||
push_e_strength: float = 1.0,
|
||||
pull_e_strength: float = 1.0,
|
||||
pull_center_strength: float = 1.0,
|
||||
):
|
||||
_push_v_strength = 0.006
|
||||
_push_e_strength = 0.0
|
||||
_pull_e_strength = 0.045
|
||||
_pull_center_strength = 0.01
|
||||
|
||||
push_v_strength = fill_strength(push_v_strength, _push_v_strength)
|
||||
push_e_strength = fill_strength(push_e_strength, _push_e_strength)
|
||||
pull_e_strength = fill_strength(pull_e_strength, _pull_e_strength)
|
||||
pull_center_strength = fill_strength(pull_center_strength, _pull_center_strength)
|
||||
|
||||
return push_v_strength, push_e_strength, pull_e_strength, pull_center_strength
|
||||
|
||||
|
||||
def default_bipartite_strength(
|
||||
num_u: int,
|
||||
num_v: int,
|
||||
e_list: List[tuple],
|
||||
push_u_strength: float = 1.0,
|
||||
push_v_strength: float = 1.0,
|
||||
push_e_strength: float = 1.0,
|
||||
pull_e_strength: float = 1.0,
|
||||
pull_u_center_strength: float = 1.0,
|
||||
pull_v_center_strength: float = 1.0,
|
||||
):
|
||||
_push_u_strength = 0.005
|
||||
_push_v_strength = 0.005
|
||||
_push_e_strength = 0.0
|
||||
_pull_e_strength = 0.03
|
||||
_pull_u_center_strength = 0.04
|
||||
_pull_v_center_strength = 0.04
|
||||
|
||||
push_u_strength = fill_strength(push_u_strength, _push_u_strength)
|
||||
push_v_strength = fill_strength(push_v_strength, _push_v_strength)
|
||||
push_e_strength = fill_strength(push_e_strength, _push_e_strength)
|
||||
pull_e_strength = fill_strength(pull_e_strength, _pull_e_strength)
|
||||
pull_u_center_strength = fill_strength(
|
||||
pull_u_center_strength, _pull_u_center_strength
|
||||
)
|
||||
pull_v_center_strength = fill_strength(
|
||||
pull_v_center_strength, _pull_v_center_strength
|
||||
)
|
||||
|
||||
return (
|
||||
push_u_strength,
|
||||
push_v_strength,
|
||||
push_e_strength,
|
||||
pull_e_strength,
|
||||
pull_u_center_strength,
|
||||
pull_v_center_strength,
|
||||
)
|
||||
|
||||
|
||||
def default_hypergraph_strength(
|
||||
num_v: int,
|
||||
e_list: List[tuple],
|
||||
push_v_strength: float = 1.0,
|
||||
push_e_strength: float = 1.0,
|
||||
pull_e_strength: float = 1.0,
|
||||
pull_center_strength: float = 1.0,
|
||||
):
|
||||
_push_v_strength = 0.006
|
||||
_push_e_strength = 0.008
|
||||
_pull_e_strength = 0.007
|
||||
_pull_center_strength = 0.001
|
||||
|
||||
push_v_strength = fill_strength(push_v_strength, _push_v_strength)
|
||||
push_e_strength = fill_strength(push_e_strength, _push_e_strength)
|
||||
pull_e_strength = fill_strength(pull_e_strength, _pull_e_strength)
|
||||
pull_center_strength = fill_strength(pull_center_strength, _pull_center_strength)
|
||||
|
||||
return push_v_strength, push_e_strength, pull_e_strength, pull_center_strength
|
||||
|
||||
|
||||
def fill_color(
|
||||
custom_color: Optional[Union[str, list]], default_color: Any, length: int
|
||||
):
|
||||
if custom_color is None:
|
||||
return [default_color] * length
|
||||
elif isinstance(custom_color, list):
|
||||
if (
|
||||
isinstance(custom_color[0], str)
|
||||
or isinstance(custom_color[0], tuple)
|
||||
or isinstance(custom_color[0], list)
|
||||
):
|
||||
return custom_color
|
||||
else:
|
||||
return [custom_color] * length
|
||||
elif isinstance(custom_color, str):
|
||||
return [custom_color] * length
|
||||
else:
|
||||
raise ValueError("The specified value is not a valid type.")
|
||||
|
||||
|
||||
def fill_sizes(
|
||||
custom_scales: Optional[Union[float, list]], default_value: Any, length: int
|
||||
):
|
||||
if custom_scales is None:
|
||||
return [default_value] * length
|
||||
elif isinstance(custom_scales, list):
|
||||
assert (
|
||||
len(custom_scales) == length
|
||||
), "The specified value list has the wrong length."
|
||||
return [default_value * scale for scale in custom_scales]
|
||||
elif isinstance(custom_scales, float):
|
||||
return [default_value * custom_scales] * length
|
||||
elif isinstance(custom_scales, int):
|
||||
return [default_value * float(custom_scales)] * length
|
||||
else:
|
||||
raise ValueError("The specified value is not a valid type.")
|
||||
|
||||
|
||||
def fill_strength(custom_scale: Optional[float], default_value: float):
|
||||
if custom_scale is None:
|
||||
return default_value
|
||||
return custom_scale * default_value
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,50 @@
|
||||
import math
|
||||
|
||||
from math import pi
|
||||
|
||||
|
||||
def radian_from_atan(x, y):
|
||||
if x == 0:
|
||||
return pi / 2 if y > 0 else 3 * pi / 2
|
||||
if y == 0:
|
||||
return 0 if x > 0 else pi
|
||||
r = math.atan(y / x)
|
||||
if x > 0 and y > 0:
|
||||
return r
|
||||
elif x > 0 and y < 0:
|
||||
return r + 2 * pi
|
||||
elif x < 0 and y > 0:
|
||||
return r + pi
|
||||
else:
|
||||
return r + pi
|
||||
|
||||
|
||||
def vlen(vector):
|
||||
return math.sqrt(vector[0] ** 2 + vector[1] ** 2)
|
||||
|
||||
|
||||
def common_tangent_radian(r1, r2, d):
|
||||
if r1 < 0 or r2 < 0:
|
||||
raise ValueError("Circle radii must be non-negative.")
|
||||
if d <= 0 or d < abs(r2 - r1):
|
||||
raise ValueError("No common tangent exists for the given circles.")
|
||||
value = abs(r2 - r1) / d
|
||||
if value > 1.0:
|
||||
value = 1.0
|
||||
elif value < -1.0:
|
||||
value = -1.0
|
||||
alpha = math.acos(value)
|
||||
alpha = alpha if r1 > r2 else pi - alpha
|
||||
return alpha
|
||||
|
||||
|
||||
def polar_position(r, theta, start_point):
|
||||
import numpy as np
|
||||
|
||||
x = r * math.cos(theta)
|
||||
y = r * math.sin(theta)
|
||||
return np.array([x, y]) + start_point
|
||||
|
||||
|
||||
def rad_2_deg(rad):
|
||||
return rad * 180 / pi
|
||||
@@ -0,0 +1,33 @@
|
||||
from typing import List
|
||||
|
||||
from .simulator import Simulator
|
||||
from .utils import edge_list_to_incidence_matrix
|
||||
from .utils import init_pos
|
||||
|
||||
|
||||
def force_layout(
|
||||
num_v: int,
|
||||
e_list: List[tuple],
|
||||
push_v_strength: float,
|
||||
push_e_strength: float,
|
||||
pull_e_strength: float,
|
||||
pull_center_strength: float,
|
||||
):
|
||||
import numpy as np
|
||||
|
||||
v_coor = init_pos(num_v, scale=5)
|
||||
assert v_coor.max() <= 5.0 and v_coor.min() >= -5.0
|
||||
centers = [np.array([0, 0])]
|
||||
sim = Simulator(
|
||||
nums=num_v,
|
||||
forces={
|
||||
Simulator.NODE_ATTRACTION: pull_e_strength,
|
||||
Simulator.NODE_REPULSION: push_v_strength,
|
||||
Simulator.EDGE_REPULSION: push_e_strength,
|
||||
Simulator.CENTER_GRAVITY: pull_center_strength,
|
||||
},
|
||||
centers=centers,
|
||||
)
|
||||
v_coor = sim.simulate(v_coor, edge_list_to_incidence_matrix(num_v, e_list))
|
||||
v_coor = (v_coor - v_coor.min(0)) / (v_coor.max(0) - v_coor.min(0)) * 0.8 + 0.1
|
||||
return v_coor
|
||||
@@ -0,0 +1,233 @@
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
__all__ = [
|
||||
"plot_Followers",
|
||||
"plot_Connected_Communities",
|
||||
"plot_Betweenness_Centrality",
|
||||
"plot_Neighborhood_Followers",
|
||||
]
|
||||
|
||||
|
||||
# Number of Followers
|
||||
def plot_Followers(G, SHS):
|
||||
"""
|
||||
Returns the CDF curves of "Number of Followers" of SH spanners and ordinary users in graph G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph graph.
|
||||
|
||||
SHS : list
|
||||
The SH Spanners in graph G.
|
||||
|
||||
Returns
|
||||
-------
|
||||
plt : CDF curves
|
||||
the CDF curves of "Number of Followers" of SH spanners and ordinary users in graph G.
|
||||
"""
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import statsmodels.api as sm
|
||||
|
||||
assert len(SHS) < len(
|
||||
G.nodes
|
||||
), "The number of SHS must be less than the number of nodes in the graph."
|
||||
OU = []
|
||||
for i in G:
|
||||
if i not in SHS:
|
||||
OU.append(i)
|
||||
degree = G.degree()
|
||||
sample1 = []
|
||||
sample2 = []
|
||||
for i in degree.keys():
|
||||
if i in OU:
|
||||
sample1.append(degree[i])
|
||||
elif i in SHS:
|
||||
sample2.append(degree[i])
|
||||
X1 = np.linspace(min(sample1), max(sample1))
|
||||
ecdf = sm.distributions.ECDF(sample1)
|
||||
Y1 = ecdf(X1)
|
||||
X2 = np.linspace(min(sample2), max(sample2))
|
||||
ecdf = sm.distributions.ECDF(sample2)
|
||||
Y2 = ecdf(X2)
|
||||
plt.plot(X1, Y1, "b--", label="Ordinary User")
|
||||
plt.plot(X2, Y2, "r", label="SH Spanner")
|
||||
plt.title("Number of Followers")
|
||||
plt.xlabel("Number of Followers")
|
||||
plt.ylabel("Cumulative Distribution Function")
|
||||
plt.legend(loc="lower right")
|
||||
plt.show()
|
||||
|
||||
|
||||
# Number of Connected Communities
|
||||
def plot_Connected_Communities(G, SHS):
|
||||
"""
|
||||
Returns the CDF curves of "Number of Connected Communities" of SH spanners and ordinary users in graph G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph graph.
|
||||
|
||||
SHS : list
|
||||
The SH Spanners in graph G.
|
||||
|
||||
Returns
|
||||
-------
|
||||
plt : CDF curves
|
||||
the CDF curves of "Number of Connected Communities" of SH spanners and ordinary users in graph G.
|
||||
"""
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import statsmodels.api as sm
|
||||
|
||||
OU = []
|
||||
for i in G:
|
||||
if i not in SHS:
|
||||
OU.append(i)
|
||||
sample1 = []
|
||||
sample2 = []
|
||||
cmts = eg.LPA(G)
|
||||
for i in OU:
|
||||
s = set()
|
||||
neighbors = G.neighbors(node=i)
|
||||
for j in neighbors:
|
||||
for k in cmts:
|
||||
if j in cmts[k]:
|
||||
s.add(k)
|
||||
sample1.append(len(s))
|
||||
for i in SHS:
|
||||
s = set()
|
||||
neighbors = G.neighbors(node=i)
|
||||
for j in neighbors:
|
||||
for k in cmts:
|
||||
if j in cmts[k]:
|
||||
s.add(k)
|
||||
sample2.append(len(s))
|
||||
print(len(cmts))
|
||||
print(sample1)
|
||||
print(sample2)
|
||||
X1 = np.linspace(min(sample1), max(sample1))
|
||||
ecdf = sm.distributions.ECDF(sample1)
|
||||
Y1 = ecdf(X1)
|
||||
X2 = np.linspace(min(sample2), max(sample2))
|
||||
ecdf = sm.distributions.ECDF(sample2)
|
||||
Y2 = ecdf(X2)
|
||||
plt.plot(X1, Y1, "b--", label="Ordinary User")
|
||||
plt.plot(X2, Y2, "r", label="SH Spanner")
|
||||
plt.title("Number of Connected Communities")
|
||||
plt.xlabel("Number of Connected Communities")
|
||||
plt.ylabel("Cumulative Distribution Function")
|
||||
plt.legend(loc="lower right")
|
||||
plt.show()
|
||||
|
||||
|
||||
# Betweenness Centrality
|
||||
def plot_Betweenness_Centrality(G, SHS):
|
||||
"""
|
||||
Returns the CDF curves of "Betweenness Centralitys" of SH spanners and ordinary users in graph G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph graph.
|
||||
|
||||
SHS : list
|
||||
The SH Spanners in graph G.
|
||||
|
||||
Returns
|
||||
-------
|
||||
plt : CDF curves
|
||||
the CDF curves of "Betweenness Centrality" of SH spanners and ordinary users in graph G.
|
||||
"""
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import statsmodels.api as sm
|
||||
|
||||
OU = []
|
||||
for i in G:
|
||||
if i not in SHS:
|
||||
OU.append(i)
|
||||
bc = eg.betweenness_centrality(G)
|
||||
bc = dict(zip(G.nodes, bc))
|
||||
sample1 = []
|
||||
sample2 = []
|
||||
for i in bc.keys():
|
||||
if i in OU:
|
||||
sample1.append(bc[i])
|
||||
else:
|
||||
sample2.append(bc[i])
|
||||
X1 = np.linspace(min(sample1), max(sample1))
|
||||
ecdf = sm.distributions.ECDF(sample1)
|
||||
Y1 = ecdf(X1)
|
||||
X2 = np.linspace(min(sample2), max(sample2))
|
||||
ecdf = sm.distributions.ECDF(sample2)
|
||||
Y2 = ecdf(X2)
|
||||
plt.plot(X1, Y1, "b--", label="Ordinary User")
|
||||
plt.plot(X2, Y2, "r", label="SH Spanner")
|
||||
plt.title("Betweenness Centrality")
|
||||
plt.xlabel("Betweenness Centrality")
|
||||
plt.ylabel("Cumulative Distribution Function")
|
||||
plt.legend(loc="lower right")
|
||||
plt.show()
|
||||
|
||||
|
||||
# Arg. Number of Followers of the Neighborhood Users
|
||||
def plot_Neighborhood_Followers(G, SHS):
|
||||
"""
|
||||
Returns the CDF curves of "Arg. Number of Followers of the Neighborhood Users" of SH spanners and ordinary users in graph G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A easygraph graph.
|
||||
|
||||
SHS : list
|
||||
The SH Spanners in graph G.
|
||||
|
||||
Returns
|
||||
-------
|
||||
plt : CDF curves
|
||||
the CDF curves of "Arg. Number of Followers of the Neighborhood Users
|
||||
" of SH spanners and ordinary users in graph G.
|
||||
"""
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import statsmodels.api as sm
|
||||
|
||||
OU = []
|
||||
for i in G:
|
||||
if i not in SHS:
|
||||
OU.append(i)
|
||||
sample1 = []
|
||||
sample2 = []
|
||||
degree = G.degree()
|
||||
for i in OU:
|
||||
num = 0
|
||||
sum = 0
|
||||
for neighbor in G.neighbors(node=i):
|
||||
num = num + 1
|
||||
sum = sum + degree[neighbor]
|
||||
sample1.append(sum / num)
|
||||
for i in SHS:
|
||||
num = 0
|
||||
sum = 0
|
||||
for neighbor in G.neighbors(node=i):
|
||||
num = num + 1
|
||||
sum = sum + degree[neighbor]
|
||||
sample2.append(sum / num)
|
||||
X1 = np.linspace(min(sample1), max(sample1))
|
||||
ecdf = sm.distributions.ECDF(sample1)
|
||||
Y1 = ecdf(X1)
|
||||
X2 = np.linspace(min(sample2), max(sample2))
|
||||
ecdf = sm.distributions.ECDF(sample2)
|
||||
Y2 = ecdf(X2)
|
||||
plt.plot(X1, Y1, "b--", label="Ordinary User")
|
||||
plt.plot(X2, Y2, "r", label="SH Spanner")
|
||||
plt.title("Arg. Number of Followers of the Neighborhood Users")
|
||||
plt.xlabel("Arg. Number of Followers of the Neighborhood Users")
|
||||
plt.ylabel("Cumulative Distribution Function")
|
||||
plt.legend(loc="lower right")
|
||||
plt.show()
|
||||
@@ -0,0 +1,646 @@
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
__all__ = [
|
||||
"random_position",
|
||||
"circular_position",
|
||||
"shell_position",
|
||||
"rescale_position",
|
||||
"kamada_kawai_layout",
|
||||
# "spring_layout",
|
||||
# "fruchterman_reingold_layout",
|
||||
# "_process_params",
|
||||
# "_fruchterman_reingold",
|
||||
# "_sparse_fruchterman_reingold",
|
||||
]
|
||||
|
||||
|
||||
def random_position(G, center=None, dim=2, random_seed=None):
|
||||
"""
|
||||
Returns random position for each node in graph G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
center : array-like or None, optional (default : None)
|
||||
Coordinate pair around which to center the layout
|
||||
|
||||
dim : int, optional (default : 2)
|
||||
Dimension of layout
|
||||
|
||||
random_seed : int or None, optional (default : None)
|
||||
Seed for RandomState instance
|
||||
|
||||
Returns
|
||||
----------
|
||||
pos : dict
|
||||
A dictionary of positions keyed by node
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
center = _get_center(center, dim)
|
||||
|
||||
rng = np.random.RandomState(seed=random_seed)
|
||||
pos = rng.rand(len(G), dim) + center
|
||||
pos = pos.astype(np.float32)
|
||||
pos = dict(zip(G, pos))
|
||||
|
||||
return pos
|
||||
|
||||
|
||||
def circular_position(G, center=None, scale=1):
|
||||
"""
|
||||
Position nodes on a circle, the dimension is 2.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
A position will be assigned to every node in G
|
||||
|
||||
center : array-like or None, optional (default : None)
|
||||
Coordinate pair around which to center the layout
|
||||
|
||||
scale : number, optional (default : 1)
|
||||
Scale factor for positions
|
||||
|
||||
Returns
|
||||
-------
|
||||
pos : dict
|
||||
A dictionary of positions keyed by node
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
center = _get_center(center, dim=2)
|
||||
|
||||
if len(G) == 0:
|
||||
pos = {}
|
||||
elif len(G) == 1:
|
||||
pos = {G.nodes[0]: center}
|
||||
else:
|
||||
theta = np.linspace(0, 1, len(G), endpoint=False) * 2 * np.pi
|
||||
theta = theta.astype(np.float32)
|
||||
pos = np.column_stack([np.cos(theta), np.sin(theta)])
|
||||
pos = rescale_position(pos, scale=scale) + center
|
||||
pos = dict(zip(G, pos))
|
||||
|
||||
return pos
|
||||
|
||||
|
||||
def shell_position(G, nlist=None, scale=1, center=None):
|
||||
"""
|
||||
Position nodes in concentric circles, the dimension is 2.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
nlist : list of lists or None, optional (default : None)
|
||||
List of node lists for each shell.
|
||||
|
||||
scale : number, optional (default : 1)
|
||||
Scale factor for positions.
|
||||
|
||||
center : array-like or None, optional (default : None)
|
||||
Coordinate pair around which to center the layout.
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
pos : dict
|
||||
A dictionary of positions keyed by node
|
||||
|
||||
Notes
|
||||
-----
|
||||
This algorithm currently only works in two dimensions and does not
|
||||
try to minimize edge crossings.
|
||||
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
center = _get_center(center, dim=2)
|
||||
|
||||
if len(G) == 0:
|
||||
return {}
|
||||
if len(G) == 1:
|
||||
return {G.nodes[0]: center}
|
||||
|
||||
if nlist is None:
|
||||
# draw the whole graph in one shell
|
||||
nlist = [list(G)]
|
||||
|
||||
if len(nlist[0]) == 1:
|
||||
# single node at center
|
||||
radius = 0.0
|
||||
else:
|
||||
# else start at r=1
|
||||
radius = 1.0
|
||||
|
||||
npos = {}
|
||||
for nodes in nlist:
|
||||
# Discard the extra angle since it matches 0 radians.
|
||||
theta = np.linspace(0, 1, len(nodes), endpoint=False) * 2 * np.pi
|
||||
theta = theta.astype(np.float32)
|
||||
pos = np.column_stack([np.cos(theta), np.sin(theta)])
|
||||
if len(pos) > 1:
|
||||
pos = rescale_position(pos, scale=scale * radius / len(nlist)) + center
|
||||
else:
|
||||
pos = np.array([(scale * radius + center[0], center[1])])
|
||||
npos.update(zip(nodes, pos))
|
||||
radius += 1.0
|
||||
|
||||
return npos
|
||||
|
||||
|
||||
def _get_center(center, dim):
|
||||
import numpy as np
|
||||
|
||||
if center is None:
|
||||
center = np.zeros(dim)
|
||||
else:
|
||||
center = np.asarray(center)
|
||||
|
||||
if dim < 2:
|
||||
raise ValueError("cannot handle dimensions < 2")
|
||||
|
||||
if len(center) != dim:
|
||||
msg = "length of center coordinates must match dimension of layout"
|
||||
raise ValueError(msg)
|
||||
|
||||
return center
|
||||
|
||||
|
||||
def rescale_position(pos, scale=1):
|
||||
"""
|
||||
Returns scaled position array to (-scale, scale) in all axes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pos : numpy array
|
||||
positions to be scaled. Each row is a position.
|
||||
|
||||
scale : number, optional (default : 1)
|
||||
The size of the resulting extent in all directions.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pos : numpy array
|
||||
scaled positions. Each row is a position.
|
||||
"""
|
||||
# Find max length over all dimensions
|
||||
assert (
|
||||
len(pos.shape) != 1
|
||||
), "One-dimensional ndarray is not available for rescaling."
|
||||
lim = 0 # max coordinate for all axes
|
||||
for i in range(pos.shape[1]):
|
||||
pos[:, i] -= pos[:, i].mean()
|
||||
lim = max(abs(pos[:, i]).max(), lim)
|
||||
# rescale to (-scale, scale) in all directions, preserves aspect
|
||||
if lim > 0:
|
||||
for i in range(pos.shape[1]):
|
||||
pos[:, i] *= scale / lim
|
||||
return pos
|
||||
|
||||
|
||||
def kamada_kawai_layout(
|
||||
G, dist=None, pos=None, weight="weight", scale=1, center=None, dim=2
|
||||
):
|
||||
"""Position nodes using Kamada-Kawai basic-length cost-function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph or list of nodes
|
||||
A position will be assigned to every node in G.
|
||||
|
||||
dist : dict (default=None)
|
||||
A two-level dictionary of optimal distances between nodes,
|
||||
indexed by source and destination node.
|
||||
If None, the distance is computed using shortest_path_length().
|
||||
|
||||
pos : dict or None optional (default=None)
|
||||
Initial positions for nodes as a dictionary with node as keys
|
||||
and values as a coordinate list or tuple. If None, then use
|
||||
circular_layout() for dim >= 2 and a linear layout for dim == 1.
|
||||
|
||||
weight : string or None optional (default='weight')
|
||||
The edge attribute that holds the numerical value used for
|
||||
the edge weight. If None, then all edge weights are 1.
|
||||
|
||||
scale : number (default: 1)
|
||||
Scale factor for positions.
|
||||
|
||||
center : array-like or None
|
||||
Coordinate pair around which to center the layout.
|
||||
|
||||
dim : int
|
||||
Dimension of layout.
|
||||
|
||||
Returns
|
||||
-------
|
||||
pos : dict
|
||||
A dictionary of positions keyed by node
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> pos = eg.kamada_kawai_layout(G)
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
nNodes = len(G)
|
||||
if nNodes == 0:
|
||||
return {}
|
||||
|
||||
if dist is None:
|
||||
dist = dict(eg.Floyd(G))
|
||||
dist_mtx = 1e6 * np.ones((nNodes, nNodes))
|
||||
for row, nr in enumerate(G):
|
||||
if nr not in dist:
|
||||
continue
|
||||
rdist = dist[nr]
|
||||
for col, nc in enumerate(G):
|
||||
if nc not in rdist:
|
||||
continue
|
||||
dist_mtx[row][col] = rdist[nc]
|
||||
|
||||
if pos is None:
|
||||
if dim >= 3:
|
||||
pos = eg.random_position(G, dim=dim)
|
||||
elif dim == 2:
|
||||
pos = eg.circular_position(G)
|
||||
else:
|
||||
pos = {n: pt for n, pt in zip(G, np.linspace(0, 1, len(G)))}
|
||||
|
||||
pos_arr = np.array([pos[n] for n in G])
|
||||
|
||||
pos = _kamada_kawai_solve(dist_mtx, pos_arr, dim)
|
||||
|
||||
if center is None:
|
||||
center = np.zeros(dim)
|
||||
else:
|
||||
center = np.asarray(center)
|
||||
|
||||
if len(center) != dim:
|
||||
msg = "length of center coordinates must match dimension of layout"
|
||||
raise ValueError(msg)
|
||||
|
||||
pos = eg.rescale_position(pos, scale=scale) + center
|
||||
return dict(zip(G, pos))
|
||||
|
||||
|
||||
def _kamada_kawai_solve(dist_mtx, pos_arr, dim):
|
||||
# Anneal node locations based on the Kamada-Kawai cost-function,
|
||||
# using the supplied matrix of preferred inter-node distances,
|
||||
# and starting locations.
|
||||
|
||||
import numpy as np
|
||||
|
||||
from scipy.optimize import minimize
|
||||
|
||||
meanwt = 1e-3
|
||||
costargs = (np, 1 / (dist_mtx + np.eye(dist_mtx.shape[0]) * 1e-3), meanwt, dim)
|
||||
|
||||
optresult = minimize(
|
||||
_kamada_kawai_costfn,
|
||||
pos_arr.ravel(),
|
||||
method="L-BFGS-B",
|
||||
args=costargs,
|
||||
jac=True,
|
||||
)
|
||||
|
||||
return optresult.x.reshape((-1, dim))
|
||||
|
||||
|
||||
def _kamada_kawai_costfn(pos_vec, np, invdist, meanweight, dim):
|
||||
# Cost-function and gradient for Kamada-Kawai layout algorithm
|
||||
nNodes = invdist.shape[0]
|
||||
pos_arr = pos_vec.reshape((nNodes, dim))
|
||||
|
||||
delta = pos_arr[:, np.newaxis, :] - pos_arr[np.newaxis, :, :]
|
||||
nodesep = np.linalg.norm(delta, axis=-1)
|
||||
direction = np.einsum("ijk,ij->ijk", delta, 1 / (nodesep + np.eye(nNodes) * 1e-3))
|
||||
|
||||
offset = nodesep * invdist - 1.0
|
||||
offset[np.diag_indices(nNodes)] = 0
|
||||
|
||||
cost = 0.5 * np.sum(offset**2)
|
||||
grad = np.einsum("ij,ij,ijk->ik", invdist, offset, direction) - np.einsum(
|
||||
"ij,ij,ijk->jk", invdist, offset, direction
|
||||
)
|
||||
|
||||
# Additional parabolic term to encourage mean position to be near origin:
|
||||
sumpos = np.sum(pos_arr, axis=0)
|
||||
cost += 0.5 * meanweight * np.sum(sumpos**2)
|
||||
grad += meanweight * sumpos
|
||||
|
||||
return (cost, grad.ravel())
|
||||
|
||||
|
||||
# @np_random_state(10)
|
||||
# def spring_layout(
|
||||
# G,
|
||||
# k=None,
|
||||
# pos=None,
|
||||
# fixed=None,
|
||||
# iterations=50,
|
||||
# threshold=1e-4,
|
||||
# weight="weight",
|
||||
# scale=1,
|
||||
# center=None,
|
||||
# dim=2,
|
||||
# seed=None,
|
||||
# ):
|
||||
# """Position nodes using Fruchterman-Reingold force-directed algorithm.
|
||||
#
|
||||
# The algorithm simulates a force-directed representation of the network
|
||||
# treating edges as springs holding nodes close, while treating nodes
|
||||
# as repelling objects, sometimes called an anti-gravity force.
|
||||
# Simulation continues until the positions are close to an equilibrium.
|
||||
#
|
||||
# There are some hard-coded values: minimal distance between
|
||||
# nodes (0.01) and "temperature" of 0.1 to ensure nodes don't fly away.
|
||||
# During the simulation, `k` helps determine the distance between nodes,
|
||||
# though `scale` and `center` determine the size and place after
|
||||
# rescaling occurs at the end of the simulation.
|
||||
#
|
||||
# Fixing some nodes doesn't allow them to move in the simulation.
|
||||
# It also turns off the rescaling feature at the simulation's end.
|
||||
# In addition, setting `scale` to `None` turns off rescaling.
|
||||
#
|
||||
# Parameters
|
||||
# ----------
|
||||
# G : EasyGraph graph or list of nodes
|
||||
# A position will be assigned to every node in G.
|
||||
#
|
||||
# k : float (default=None)
|
||||
# Optimal distance between nodes. If None the distance is set to
|
||||
# 1/sqrt(n) where n is the number of nodes. Increase this value
|
||||
# to move nodes farther apart.
|
||||
#
|
||||
# pos : dict or None optional (default=None)
|
||||
# Initial positions for nodes as a dictionary with node as keys
|
||||
# and values as a coordinate list or tuple. If None, then use
|
||||
# random initial positions.
|
||||
#
|
||||
# fixed : list or None optional (default=None)
|
||||
# Nodes to keep fixed at initial position.
|
||||
# Nodes not in ``G.nodes`` are ignored.
|
||||
# ValueError raised if `fixed` specified and `pos` not.
|
||||
#
|
||||
# iterations : int optional (default=50)
|
||||
# Maximum number of iterations taken
|
||||
#
|
||||
# threshold: float optional (default = 1e-4)
|
||||
# Threshold for relative error in node position changes.
|
||||
# The iteration stops if the error is below this threshold.
|
||||
#
|
||||
# weight : string or None optional (default='weight')
|
||||
# The edge attribute that holds the numerical value used for
|
||||
# the edge weight. Larger means a stronger attractive force.
|
||||
# If None, then all edge weights are 1.
|
||||
#
|
||||
# scale : number or None (default: 1)
|
||||
# Scale factor for positions. Not used unless `fixed is None`.
|
||||
# If scale is None, no rescaling is performed.
|
||||
#
|
||||
# center : array-like or None
|
||||
# Coordinate pair around which to center the layout.
|
||||
# Not used unless `fixed is None`.
|
||||
#
|
||||
# dim : int
|
||||
# Dimension of layout.
|
||||
#
|
||||
# seed : int, RandomState instance or None optional (default=None)
|
||||
# Set the random state for deterministic node layouts.
|
||||
# If int, `seed` is the seed used by the random number generator,
|
||||
# if numpy.random.RandomState instance, `seed` is the random
|
||||
# number generator,
|
||||
# if None, the random number generator is the RandomState instance used
|
||||
# by numpy.random.
|
||||
#
|
||||
# Returns
|
||||
# -------
|
||||
# pos : dict
|
||||
# A dictionary of positions keyed by node
|
||||
#
|
||||
# Examples
|
||||
# --------
|
||||
# >>> G = eg.path_graph(4)
|
||||
# >>> pos = eg.spring_layout(G)
|
||||
#
|
||||
#
|
||||
# """
|
||||
# import numpy as np
|
||||
#
|
||||
# G, center = _process_params(G, center, dim)
|
||||
#
|
||||
# if fixed is not None:
|
||||
# if pos is None:
|
||||
# raise ValueError("nodes are fixed without positions given")
|
||||
# for node in fixed:
|
||||
# if node not in pos:
|
||||
# raise ValueError("nodes are fixed without positions given")
|
||||
# nfixed = {node: i for i, node in enumerate(G)}
|
||||
# fixed = np.asarray([nfixed[node] for node in fixed if node in nfixed])
|
||||
#
|
||||
# if pos is not None:
|
||||
# # Determine size of existing domain to adjust initial positions
|
||||
# dom_size = max(coord for pos_tup in pos.values() for coord in pos_tup)
|
||||
# if dom_size == 0:
|
||||
# dom_size = 1
|
||||
# pos_arr = seed.rand(len(G), dim) * dom_size + center
|
||||
#
|
||||
# for i, n in enumerate(G):
|
||||
# if n in pos:
|
||||
# pos_arr[i] = np.asarray(pos[n])
|
||||
# else:
|
||||
# pos_arr = None
|
||||
# dom_size = 1
|
||||
#
|
||||
# if len(G) == 0:
|
||||
# return {}
|
||||
# if len(G) == 1:
|
||||
# return {eg.utils.arbitrary_element(G.nodes()): center}
|
||||
#
|
||||
# try:
|
||||
# # Sparse matrix
|
||||
# if len(G) < 500: # sparse solver for large graphs
|
||||
# raise ValueError
|
||||
# A = eg.to_scipy_sparse_array(G, weight=weight, dtype="f")
|
||||
# if k is None and fixed is not None:
|
||||
# # We must adjust k by domain size for layouts not near 1x1
|
||||
# nnodes, _ = A.shape
|
||||
# k = dom_size / np.sqrt(nnodes)
|
||||
# pos = _sparse_fruchterman_reingold(
|
||||
# A, k, pos_arr, fixed, iterations, threshold, dim, seed
|
||||
# )
|
||||
# except ValueError:
|
||||
# A = eg.to_numpy_array(G, weight=weight)
|
||||
# if k is None and fixed is not None:
|
||||
# # We must adjust k by domain size for layouts not near 1x1
|
||||
# nnodes, _ = A.shape
|
||||
# k = dom_size / np.sqrt(nnodes)
|
||||
# pos = _fruchterman_reingold(
|
||||
# A, k, pos_arr, fixed, iterations, threshold, dim, seed
|
||||
# )
|
||||
# if fixed is None and scale is not None:
|
||||
# pos = rescale_position(pos, scale=scale) + center
|
||||
# pos = dict(zip(G, pos))
|
||||
# return pos
|
||||
#
|
||||
# fruchterman_reingold_layout = spring_layout
|
||||
#
|
||||
# def _process_params(G, center, dim):
|
||||
# # Some boilerplate code.
|
||||
# import numpy as np
|
||||
#
|
||||
# if not isinstance(G, eg.Graph):
|
||||
# empty_graph = eg.Graph()
|
||||
# empty_graph.add_nodes_from(G)
|
||||
# G = empty_graph
|
||||
#
|
||||
# if center is None:
|
||||
# center = np.zeros(dim)
|
||||
# else:
|
||||
# center = np.asarray(center)
|
||||
#
|
||||
# if len(center) != dim:
|
||||
# msg = "length of center coordinates must match dimension of layout"
|
||||
# raise ValueError(msg)
|
||||
#
|
||||
# return G, center
|
||||
#
|
||||
# @np_random_state(7)
|
||||
# def _fruchterman_reingold(
|
||||
# A, k=None, pos=None, fixed=None, iterations=50, threshold=1e-4, dim=2, seed=None
|
||||
# ):
|
||||
# # Position nodes in adjacency matrix A using Fruchterman-Reingold
|
||||
# # Entry point for NetworkX graph is fruchterman_reingold_layout()
|
||||
# import numpy as np
|
||||
#
|
||||
# try:
|
||||
# nnodes, _ = A.shape
|
||||
# except AttributeError as err:
|
||||
# msg = "fruchterman_reingold() takes an adjacency matrix as input"
|
||||
# raise EasyGraphError(msg) from err
|
||||
#
|
||||
# if pos is None:
|
||||
# # random initial positions
|
||||
# pos = np.asarray(seed.rand(nnodes, dim), dtype=A.dtype)
|
||||
# else:
|
||||
# # make sure positions are of same type as matrix
|
||||
# pos = pos.astype(A.dtype)
|
||||
#
|
||||
# # optimal distance between nodes
|
||||
# if k is None:
|
||||
# k = np.sqrt(1.0 / nnodes)
|
||||
# # the initial "temperature" is about .1 of domain area (=1x1)
|
||||
# # this is the largest step allowed in the dynamics.
|
||||
# # We need to calculate this in case our fixed positions force our domain
|
||||
# # to be much bigger than 1x1
|
||||
# t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1
|
||||
# # simple cooling scheme.
|
||||
# # linearly step down by dt on each iteration so last iteration is size dt.
|
||||
# dt = t / (iterations + 1)
|
||||
# delta = np.zeros((pos.shape[0], pos.shape[0], pos.shape[1]), dtype=A.dtype)
|
||||
# # the inscrutable (but fast) version
|
||||
# # this is still O(V^2)
|
||||
# # could use multilevel methods to speed this up significantly
|
||||
# for iteration in range(iterations):
|
||||
# # matrix of difference between points
|
||||
# delta = pos[:, np.newaxis, :] - pos[np.newaxis, :, :]
|
||||
# # distance between points
|
||||
# distance = np.linalg.norm(delta, axis=-1)
|
||||
# # enforce minimum distance of 0.01
|
||||
# np.clip(distance, 0.01, None, out=distance)
|
||||
# # displacement "force"
|
||||
# displacement = np.einsum(
|
||||
# "ijk,ij->ik", delta, (k * k / distance**2 - A * distance / k)
|
||||
# )
|
||||
# # update positions
|
||||
# length = np.linalg.norm(displacement, axis=-1)
|
||||
# length = np.where(length < 0.01, 0.1, length)
|
||||
# delta_pos = np.einsum("ij,i->ij", displacement, t / length)
|
||||
# if fixed is not None:
|
||||
# # don't change positions of fixed nodes
|
||||
# delta_pos[fixed] = 0.0
|
||||
# pos += delta_pos
|
||||
# # cool temperature
|
||||
# t -= dt
|
||||
# if (np.linalg.norm(delta_pos) / nnodes) < threshold:
|
||||
# break
|
||||
# return pos
|
||||
#
|
||||
# @np_random_state(7)
|
||||
# def _sparse_fruchterman_reingold(
|
||||
# A, k=None, pos=None, fixed=None, iterations=50, threshold=1e-4, dim=2, seed=None
|
||||
# ):
|
||||
# # Position nodes in adjacency matrix A using Fruchterman-Reingold
|
||||
# # Entry point for NetworkX graph is fruchterman_reingold_layout()
|
||||
# # Sparse version
|
||||
# import numpy as np
|
||||
# import scipy as sp
|
||||
# import scipy.sparse # call as sp.sparse
|
||||
#
|
||||
# try:
|
||||
# nnodes, _ = A.shape
|
||||
# except AttributeError as err:
|
||||
# msg = "fruchterman_reingold() takes an adjacency matrix as input"
|
||||
# raise EasyGraphError(msg) from err
|
||||
# # make sure we have a LIst of Lists representation
|
||||
# try:
|
||||
# A = A.tolil()
|
||||
# except AttributeError:
|
||||
# A = (sp.sparse.coo_array(A)).tolil()
|
||||
#
|
||||
# if pos is None:
|
||||
# # random initial positions
|
||||
# pos = np.asarray(seed.rand(nnodes, dim), dtype=A.dtype)
|
||||
# else:
|
||||
# # make sure positions are of same type as matrix
|
||||
# pos = pos.astype(A.dtype)
|
||||
#
|
||||
# # no fixed nodes
|
||||
# if fixed is None:
|
||||
# fixed = []
|
||||
#
|
||||
# # optimal distance between nodes
|
||||
# if k is None:
|
||||
# k = np.sqrt(1.0 / nnodes)
|
||||
# # the initial "temperature" is about .1 of domain area (=1x1)
|
||||
# # this is the largest step allowed in the dynamics.
|
||||
# t = max(max(pos.T[0]) - min(pos.T[0]), max(pos.T[1]) - min(pos.T[1])) * 0.1
|
||||
# # simple cooling scheme.
|
||||
# # linearly step down by dt on each iteration so last iteration is size dt.
|
||||
# dt = t / (iterations + 1)
|
||||
#
|
||||
# displacement = np.zeros((dim, nnodes))
|
||||
# for iteration in range(iterations):
|
||||
# displacement *= 0
|
||||
# # loop over rows
|
||||
# for i in range(A.shape[0]):
|
||||
# if i in fixed:
|
||||
# continue
|
||||
# # difference between this row's node position and all others
|
||||
# delta = (pos[i] - pos).T
|
||||
# # distance between points
|
||||
# distance = np.sqrt((delta**2).sum(axis=0))
|
||||
# # enforce minimum distance of 0.01
|
||||
# distance = np.where(distance < 0.01, 0.01, distance)
|
||||
# # the adjacency matrix row
|
||||
# Ai = A.getrowview(i).toarray() # TODO: revisit w/ sparse 1D container
|
||||
# # displacement "force"
|
||||
# displacement[:, i] += (
|
||||
# delta * (k * k / distance**2 - Ai * distance / k)
|
||||
# ).sum(axis=1)
|
||||
# # update positions
|
||||
# length = np.sqrt((displacement**2).sum(axis=0))
|
||||
# length = np.where(length < 0.01, 0.1, length)
|
||||
# delta_pos = (displacement * t / length).T
|
||||
# pos += delta_pos
|
||||
# # cool temperature
|
||||
# t -= dt
|
||||
# if (np.linalg.norm(delta_pos) / nnodes) < threshold:
|
||||
# break
|
||||
# return pos
|
||||
@@ -0,0 +1,195 @@
|
||||
from copy import deepcopy
|
||||
|
||||
from .utils import safe_div
|
||||
|
||||
|
||||
class Simulator:
|
||||
NODE_ATTRACTION = 0
|
||||
NODE_REPULSION = 1
|
||||
EDGE_REPULSION = 2
|
||||
CENTER_GRAVITY = 3
|
||||
|
||||
def __init__(self, nums, forces, centers=1, damping_factor=0.999) -> None:
|
||||
self.nums = [nums] if isinstance(nums, int) else nums
|
||||
|
||||
self.node_attraction = forces.get(self.NODE_ATTRACTION, None)
|
||||
self.node_repulsion = forces.get(self.NODE_REPULSION, None)
|
||||
self.edge_repulsion = forces.get(self.EDGE_REPULSION, None)
|
||||
self.center_gravity = forces.get(self.CENTER_GRAVITY, None)
|
||||
|
||||
self.n_centers = len(centers)
|
||||
self.centers = centers
|
||||
|
||||
if self.node_repulsion is not None and isinstance(self.node_repulsion, float):
|
||||
self.node_repulsion = [self.node_repulsion] * self.n_centers
|
||||
if self.center_gravity is not None and isinstance(self.center_gravity, float):
|
||||
self.center_gravity = [self.center_gravity] * self.n_centers
|
||||
|
||||
self.damping_factor = damping_factor
|
||||
|
||||
def simulate(self, init_position, H, max_iter=400, epsilon=0.001, dt=2.0) -> None:
|
||||
import numpy as np
|
||||
|
||||
"""
|
||||
Simulate the force-directed layout algorithm.
|
||||
"""
|
||||
position = init_position.copy()
|
||||
velocity = np.zeros_like(position)
|
||||
damping = 1.0
|
||||
for it in range(max_iter):
|
||||
position, velocity, stop = self._step(
|
||||
position, velocity, H, epsilon, damping, dt
|
||||
)
|
||||
if stop:
|
||||
break
|
||||
damping *= self.damping_factor
|
||||
return position
|
||||
|
||||
def _step(self, position, velocity, H, epsilon, damping, dt):
|
||||
import numpy as np
|
||||
|
||||
from sklearn.metrics import euclidean_distances
|
||||
|
||||
"""
|
||||
One step of the simulation.
|
||||
"""
|
||||
v2v_dist = euclidean_distances(position)
|
||||
e_center = np.matmul(H.T, position) / H.sum(axis=0).reshape(-1, 1)
|
||||
v2e_dist = euclidean_distances(position, e_center) * H
|
||||
e2e_dist = euclidean_distances(e_center)
|
||||
|
||||
centers = self.centers
|
||||
|
||||
force = np.zeros_like(position)
|
||||
if self.node_attraction is not None:
|
||||
f = (
|
||||
self._node_attraction(position, e_center, v2e_dist)
|
||||
* self.node_attraction
|
||||
)
|
||||
assert np.isnan(f).sum() == 0
|
||||
force += f
|
||||
if self.node_repulsion is not None:
|
||||
f = self._node_repulsion(position, v2v_dist)
|
||||
if self.n_centers == 1:
|
||||
f *= self.node_repulsion[0]
|
||||
else:
|
||||
masks = np.zeros((position.shape[0], 1))
|
||||
masks[: self.nums[0]] = self.node_repulsion[0]
|
||||
masks[self.nums[0] :] = self.node_repulsion[1]
|
||||
f *= masks
|
||||
assert np.isnan(f).sum() == 0
|
||||
force += f
|
||||
if self.edge_repulsion is not None:
|
||||
f = self._edge_repulsion(e_center, H, e2e_dist) * self.edge_repulsion
|
||||
assert np.isnan(f).sum() == 0
|
||||
force += f
|
||||
if self.center_gravity is not None:
|
||||
masks = [np.zeros((position.shape[0], 1)), np.zeros((position.shape[0], 1))]
|
||||
masks[0][: self.nums[0]] = 1
|
||||
masks[1][self.nums[0] :] = 1
|
||||
for center, gravity, mask in zip(centers, self.center_gravity, masks):
|
||||
v2c_dist = euclidean_distances(position, center.reshape(1, -1)).reshape(
|
||||
-1, 1
|
||||
)
|
||||
f = self._center_gravity(position, center, v2c_dist) * gravity * mask
|
||||
assert np.isnan(f).sum() == 0
|
||||
force += f
|
||||
|
||||
force *= damping
|
||||
|
||||
force = np.clip(force, -0.1, 0.1)
|
||||
position += force * dt
|
||||
velocity = force
|
||||
|
||||
return position, velocity, self._stop_condition(velocity, epsilon)
|
||||
|
||||
def _node_attraction(self, position, e_center, v2e_dist, x0=0.1, k=1.0):
|
||||
import numpy as np
|
||||
|
||||
"""
|
||||
Node attracted by edge center.
|
||||
"""
|
||||
x = deepcopy(v2e_dist)
|
||||
x[v2e_dist > 0] -= x0
|
||||
f_scale = k * x # (n, m)
|
||||
f_dir = (
|
||||
e_center[np.newaxis, :, :] - position[:, np.newaxis, :]
|
||||
) # (1, m, 2) - (n, 1, 2) -> (n, m, 2)
|
||||
f_dir_len = np.linalg.norm(f_dir, axis=2) # (n, m)
|
||||
# f_dir = f_dir / f_dir_len[:, :, np.newaxis] # (n, m, 2)
|
||||
f_dir = safe_div(f_dir, f_dir_len[:, :, np.newaxis]) # (n, m, 2)
|
||||
f = f_scale[:, :, np.newaxis] * f_dir # (n, m, 2)
|
||||
f = f.sum(axis=1) # (n, 2)
|
||||
return f
|
||||
|
||||
def _node_repulsion(self, position, v2v_dist, k=1.0):
|
||||
import numpy as np
|
||||
|
||||
"""
|
||||
Node repulsed by other nodes.
|
||||
"""
|
||||
dist = v2v_dist.copy()
|
||||
r, c = np.diag_indices_from(dist)
|
||||
dist[r, c] = np.inf
|
||||
|
||||
f_scale = k / (dist**2) # (n, n) with diag 0
|
||||
f_dir = (
|
||||
position[:, np.newaxis, :] - position[np.newaxis, :, :]
|
||||
) # (n, 1, 2) - (1, n, 2) -> (n, n, 2)
|
||||
f_dir_len = np.linalg.norm(f_dir, axis=2) # (n, n)
|
||||
f_dir_len[r, c] = np.inf
|
||||
# f_dir = f_dir / f_dir_len[:, :, np.newaxis] # (n, n, 2)
|
||||
f_dir = safe_div(f_dir, f_dir_len[:, :, np.newaxis]) # (n, n, 2)
|
||||
f = f_scale[:, :, np.newaxis] * f_dir # (n, n, 2)
|
||||
f[r, c] = 0
|
||||
f = f.sum(axis=1) # (n, 2)
|
||||
return f
|
||||
|
||||
def _edge_repulsion(self, e_center, H, e2e_dist, k=1.0, min_dist=1e-6):
|
||||
import numpy as np
|
||||
|
||||
"""
|
||||
Edge repulsed by other edges.
|
||||
"""
|
||||
dist = e2e_dist.copy()
|
||||
r, c = np.diag_indices_from(dist)
|
||||
dist[r, c] = np.inf
|
||||
|
||||
f_scale = k / (dist**2) # (m, m)
|
||||
f_dir = (
|
||||
e_center[:, np.newaxis, :] - e_center[np.newaxis, :, :]
|
||||
) # (m, 1, 2) - (1, m, 2) -> (m, m, 2)
|
||||
f_dir_len = np.linalg.norm(f_dir, axis=2) # (m, m)
|
||||
f_dir_len[r, c] = np.inf
|
||||
# 使用最小距离阈值
|
||||
f_dir = safe_div(f_dir, f_dir_len[:, :, np.newaxis]) # (m, m, 2)
|
||||
f = f_scale[:, :, np.newaxis] * f_dir # (m, m, 2)
|
||||
f[r, c] = 0
|
||||
f = f.sum(axis=1) # (m, 2)
|
||||
return np.matmul(H, f)
|
||||
|
||||
def _center_gravity(self, position, center, v2c_dist, k=1):
|
||||
import numpy as np
|
||||
|
||||
"""
|
||||
Node attracted by center.
|
||||
"""
|
||||
f_scale = v2c_dist # (n, 1)
|
||||
f_dir = (
|
||||
center[np.newaxis, np.newaxis, :] - position[:, np.newaxis, :]
|
||||
) # (1, 1, 2) - (n, 1, 2) -> (n, 1, 2)
|
||||
f_dir_len = np.linalg.norm(f_dir, axis=2) # (n, 1)
|
||||
# f_dir = f_dir / f_dir_len[:, :, np.newaxis] # (n, 1, 2)
|
||||
f_dir = safe_div(f_dir, f_dir_len[:, :, np.newaxis]) # (n, 1, 2)
|
||||
f = f_scale[:, :, np.newaxis] * f_dir # (n, 1, 2)
|
||||
# f = jitter(f)
|
||||
f = f.sum(axis=1) * k
|
||||
return f
|
||||
|
||||
def _stop_condition(self, velocity, epsilon):
|
||||
import numpy as np
|
||||
|
||||
"""
|
||||
Stop condition.
|
||||
"""
|
||||
return np.linalg.norm(velocity) < epsilon
|
||||
@@ -0,0 +1,27 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
class TestGeometry(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G = eg.datasets.get_graph_karateclub()
|
||||
|
||||
def test_overall(self):
|
||||
eg.draw_SHS_center(self.G, [1, 33, 34], style="side")
|
||||
eg.draw_SHS_center(self.G, [1, 33, 34], style="center")
|
||||
eg.draw_SHS_center_kk(self.G, [1, 33, 34], style="side")
|
||||
eg.draw_SHS_center_kk(self.G, [1, 33, 34], style="center")
|
||||
eg.draw_kamada_kawai(self.G, style="side")
|
||||
eg.draw_kamada_kawai(self.G, style="center")
|
||||
eg.draw_SHS_center(self.G, [1, 33, 34], rate=0.8, style="side")
|
||||
eg.draw_SHS_center(self.G, [1, 33, 34], rate=0.8, style="center")
|
||||
eg.draw_SHS_center_kk(self.G, [1, 33, 34], rate=0.8, style="side")
|
||||
eg.draw_SHS_center_kk(self.G, [1, 33, 34], rate=0.8, style="center")
|
||||
eg.draw_kamada_kawai(self.G, rate=0.8, style="side")
|
||||
eg.draw_kamada_kawai(self.G, rate=0.8, style="center")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
# pretty awesome images
|
||||
@@ -0,0 +1,78 @@
|
||||
import math
|
||||
import unittest
|
||||
|
||||
import numpy as np
|
||||
|
||||
from easygraph.functions.drawing.geometry import common_tangent_radian
|
||||
from easygraph.functions.drawing.geometry import polar_position
|
||||
from easygraph.functions.drawing.geometry import rad_2_deg
|
||||
from easygraph.functions.drawing.geometry import radian_from_atan
|
||||
from easygraph.functions.drawing.geometry import vlen
|
||||
|
||||
|
||||
class TestGeometryUtils(unittest.TestCase):
|
||||
def test_radian_from_atan_axes(self):
|
||||
self.assertAlmostEqual(radian_from_atan(0, 1), math.pi / 2)
|
||||
self.assertAlmostEqual(radian_from_atan(0, -1), 3 * math.pi / 2)
|
||||
self.assertAlmostEqual(radian_from_atan(1, 0), 0)
|
||||
self.assertAlmostEqual(radian_from_atan(-1, 0), math.pi)
|
||||
|
||||
def test_radian_from_atan_quadrants(self):
|
||||
# Q1
|
||||
self.assertAlmostEqual(radian_from_atan(1, 1), math.atan(1))
|
||||
# Q4
|
||||
self.assertAlmostEqual(radian_from_atan(1, -1), math.atan(-1) + 2 * math.pi)
|
||||
# Q2
|
||||
self.assertAlmostEqual(radian_from_atan(-1, 1), math.atan(-1) + math.pi)
|
||||
# Q3
|
||||
self.assertAlmostEqual(radian_from_atan(-1, -1), math.atan(1) + math.pi)
|
||||
|
||||
def test_radian_from_atan_zero_vector(self):
|
||||
result = radian_from_atan(0, 0)
|
||||
self.assertAlmostEqual(result, 3 * math.pi / 2)
|
||||
|
||||
def test_vlen(self):
|
||||
self.assertEqual(vlen((3, 4)), 5.0)
|
||||
self.assertEqual(vlen((0, 0)), 0.0)
|
||||
self.assertAlmostEqual(vlen((-3, -4)), 5.0)
|
||||
|
||||
def test_common_tangent_radian_basic(self):
|
||||
r1, r2, d = 3, 2, 5
|
||||
angle = common_tangent_radian(r1, r2, d)
|
||||
expected = math.acos(abs(r2 - r1) / d)
|
||||
self.assertAlmostEqual(angle, expected)
|
||||
|
||||
def test_common_tangent_radian_reversed(self):
|
||||
r1, r2, d = 2, 3, 5
|
||||
angle = common_tangent_radian(r1, r2, d)
|
||||
expected = math.pi - math.acos(abs(r2 - r1) / d)
|
||||
self.assertAlmostEqual(angle, expected)
|
||||
|
||||
def test_common_tangent_radian_touching(self):
|
||||
self.assertAlmostEqual(common_tangent_radian(3, 3, 5), math.pi / 2)
|
||||
|
||||
def test_common_tangent_radian_invalid(self):
|
||||
with self.assertRaises(ValueError):
|
||||
common_tangent_radian(5, 1, 2)
|
||||
|
||||
def test_polar_position_origin(self):
|
||||
pos = polar_position(0, 0, np.array([5, 5]))
|
||||
np.testing.assert_array_almost_equal(pos, np.array([5, 5]))
|
||||
|
||||
def test_polar_position_90deg(self):
|
||||
pos = polar_position(1, math.pi / 2, np.array([0, 0]))
|
||||
np.testing.assert_array_almost_equal(pos, np.array([0, 1]))
|
||||
|
||||
def test_polar_position_negative_angle(self):
|
||||
pos = polar_position(1, -math.pi / 2, np.array([1, 1]))
|
||||
np.testing.assert_array_almost_equal(pos, np.array([1, 0]))
|
||||
|
||||
def test_rad_2_deg(self):
|
||||
self.assertEqual(rad_2_deg(0), 0)
|
||||
self.assertEqual(rad_2_deg(math.pi), 180)
|
||||
self.assertEqual(rad_2_deg(2 * math.pi), 360)
|
||||
self.assertEqual(rad_2_deg(-math.pi / 2), -90)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,39 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import statsmodels.api as sm
|
||||
|
||||
|
||||
class TestPlot(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.ds = eg.datasets.get_graph_karateclub()
|
||||
self.edges = [
|
||||
(1, 4),
|
||||
(2, 4),
|
||||
("String", "Bool"),
|
||||
(4, 1),
|
||||
(0, 4),
|
||||
(4, 256),
|
||||
((1, 2), (3, 4)),
|
||||
]
|
||||
self.test_graphs = [eg.Graph(), eg.DiGraph()]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
self.shs = eg.common_greedy(self.ds, int(len(self.ds.nodes) / 3))
|
||||
|
||||
def test_plot_Followers(self):
|
||||
eg.functions.plot_Followers(self.ds, self.shs)
|
||||
|
||||
def test_plot_Connected_Communities(self):
|
||||
eg.functions.plot_Connected_Communities(self.ds, self.shs)
|
||||
|
||||
def test_plot_Neighborhood_Followers(self):
|
||||
eg.functions.plot_Neighborhood_Followers(self.ds, self.shs)
|
||||
|
||||
def test_plot_Betweenness_Centrality(self):
|
||||
eg.functions.plot_Betweenness_Centrality(self.ds, self.shs)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,56 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
|
||||
class TestPositioning(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.ds = eg.datasets.get_graph_karateclub()
|
||||
self.edges = [
|
||||
(1, 4),
|
||||
(2, 4),
|
||||
("String", "Bool"),
|
||||
(4, 1),
|
||||
(0, 4),
|
||||
(4, 256),
|
||||
((1, 2), (3, 4)),
|
||||
]
|
||||
self.test_graphs = [eg.Graph(), eg.DiGraph()]
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
self.shs = eg.common_greedy(self.ds, int(len(self.ds.nodes) / 3))
|
||||
|
||||
def test_random_position(self):
|
||||
print()
|
||||
for i in self.test_graphs:
|
||||
print(eg.random_position(i))
|
||||
|
||||
def test_circular_position(self):
|
||||
print()
|
||||
for i in self.test_graphs:
|
||||
print(eg.circular_position(i))
|
||||
|
||||
def test_shell_position(self):
|
||||
print()
|
||||
for i in self.test_graphs:
|
||||
print(eg.shell_position(i))
|
||||
|
||||
def test_rescale_position(self):
|
||||
print()
|
||||
for i in self.test_graphs:
|
||||
try:
|
||||
pos = eg.random_position(i)
|
||||
obj = np.array(list(pos.values()))
|
||||
print(eg.rescale_position(obj))
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def test_kamada_kawai_layout(self):
|
||||
print()
|
||||
for i in self.test_graphs:
|
||||
print(eg.kamada_kawai_layout(i))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,596 @@
|
||||
from itertools import chain
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
|
||||
import matplotlib
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from matplotlib.axes import Axes
|
||||
from matplotlib.collections import LineCollection
|
||||
from matplotlib.collections import PatchCollection
|
||||
from matplotlib.patches import Circle
|
||||
from matplotlib.patches import PathPatch
|
||||
from matplotlib.path import Path
|
||||
from scipy.spatial import ConvexHull
|
||||
|
||||
from .geometry import common_tangent_radian
|
||||
from .geometry import polar_position
|
||||
from .geometry import rad_2_deg
|
||||
from .geometry import radian_from_atan
|
||||
from .geometry import vlen
|
||||
|
||||
|
||||
# from fa2 import ForceAtlas2
|
||||
# import bezier
|
||||
# import numpy as np
|
||||
# from easygraph import to_networkx
|
||||
# from easygraph.utils.exception import EasyGraphError
|
||||
# import easygraph as eg
|
||||
|
||||
|
||||
def safe_div(a: np.ndarray, b: np.ndarray, jitter_scale: float = 0.000001):
|
||||
mask = b == 0
|
||||
b[mask] = 1
|
||||
eps = 1e-10
|
||||
inv_b = np.divide(1.0, np.maximum(b, eps))
|
||||
res = a * inv_b
|
||||
if mask.sum() > 0:
|
||||
res[mask.repeat(2, 2)] = np.random.randn(mask.sum() * 2) * jitter_scale
|
||||
return res
|
||||
|
||||
|
||||
def init_pos(num_v: int, center: Tuple[float, float] = (0, 0), scale: float = 1.0):
|
||||
return (np.random.rand(num_v, 2) * 2 - 1) * scale + center
|
||||
|
||||
|
||||
def draw_line_edge(
|
||||
ax: Axes,
|
||||
v_coor: np.array,
|
||||
v_size: list,
|
||||
e_list: List[Tuple[int, int]],
|
||||
show_arrow: bool,
|
||||
e_color: list,
|
||||
e_line_width: list,
|
||||
):
|
||||
arrow_head_width = (
|
||||
[0.015 * w for w in e_line_width] if show_arrow else [0] * len(e_list)
|
||||
)
|
||||
|
||||
for eidx, e in enumerate(e_list):
|
||||
start_pos = v_coor[e[0]]
|
||||
end_pos = v_coor[e[1]]
|
||||
|
||||
dir = end_pos - start_pos
|
||||
dir = dir / np.linalg.norm(dir)
|
||||
|
||||
start_pos = start_pos + dir * v_size[e[0]]
|
||||
end_pos = end_pos - dir * v_size[e[1]]
|
||||
|
||||
x, y = start_pos[0], start_pos[1]
|
||||
dx, dy = end_pos[0] - x, end_pos[1] - y
|
||||
|
||||
ax.arrow(
|
||||
x,
|
||||
y,
|
||||
dx,
|
||||
dy,
|
||||
head_width=arrow_head_width[eidx],
|
||||
color=e_color[eidx],
|
||||
linewidth=e_line_width[eidx],
|
||||
length_includes_head=True,
|
||||
)
|
||||
|
||||
|
||||
def draw_circle_edge(
|
||||
ax: Axes,
|
||||
v_coor: List[Tuple[float, float]],
|
||||
v_size: list,
|
||||
e_list: List[Tuple[int, int]],
|
||||
e_color: list,
|
||||
e_fill_color: list,
|
||||
e_line_width: list,
|
||||
):
|
||||
n_v = len(v_coor)
|
||||
line_paths, arc_paths, vertices = hull_layout(n_v, e_list, v_coor, v_size)
|
||||
for eidx, lines in enumerate(line_paths):
|
||||
pathdata = []
|
||||
for line in lines:
|
||||
if len(line) == 0:
|
||||
continue
|
||||
start_pos, end_pos = line
|
||||
pathdata.append((Path.MOVETO, start_pos.tolist()))
|
||||
pathdata.append((Path.LINETO, end_pos.tolist()))
|
||||
|
||||
if len(list(zip(*pathdata))) == 0:
|
||||
continue
|
||||
codes, verts = zip(*pathdata)
|
||||
path = Path(verts, codes)
|
||||
|
||||
ax.add_patch(
|
||||
PathPatch(
|
||||
path,
|
||||
linewidth=e_line_width[eidx],
|
||||
facecolor=e_fill_color[eidx],
|
||||
edgecolor=e_color[eidx],
|
||||
)
|
||||
)
|
||||
|
||||
for eidx, arcs in enumerate(arc_paths):
|
||||
for arc in arcs:
|
||||
center, theta1, theta2, radius = arc
|
||||
x, y = center[0], center[1]
|
||||
|
||||
patcjes_arc = matplotlib.patches.Arc(
|
||||
(x, y),
|
||||
2 * radius,
|
||||
2 * radius,
|
||||
theta1=theta1,
|
||||
theta2=theta2,
|
||||
color=e_color[eidx],
|
||||
linewidth=e_line_width[eidx],
|
||||
# edgecolor=e_color[eidx],
|
||||
edgecolor=e_color[eidx],
|
||||
facecolor=e_fill_color[eidx],
|
||||
)
|
||||
|
||||
ax.add_patch(
|
||||
matplotlib.patches.Arc(
|
||||
(x, y),
|
||||
2 * radius,
|
||||
2 * radius,
|
||||
theta1=theta1,
|
||||
theta2=theta2,
|
||||
color=e_color[eidx],
|
||||
linewidth=e_line_width[eidx],
|
||||
# edgecolor=e_color[eidx],
|
||||
edgecolor=e_color[eidx],
|
||||
facecolor=e_fill_color[eidx],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def edge_list_to_incidence_matrix(num_v: int, e_list: List[tuple]) -> np.ndarray:
|
||||
v_idx = list(chain(*e_list))
|
||||
e_idx = [[idx] * len(e) for idx, e in enumerate(e_list)]
|
||||
e_idx = list(chain(*e_idx))
|
||||
H = np.zeros((num_v, len(e_list)))
|
||||
H[v_idx, e_idx] = 1
|
||||
return H
|
||||
|
||||
|
||||
def draw_vertex(
|
||||
ax: Axes,
|
||||
v_coor: List[Tuple[float, float]],
|
||||
v_label: Optional[List[str]],
|
||||
font_size: int,
|
||||
font_family: str,
|
||||
v_size: list,
|
||||
v_color: list,
|
||||
edgecolors,
|
||||
v_line_width: list,
|
||||
):
|
||||
patches = []
|
||||
n = v_coor.shape[0]
|
||||
if v_label is None:
|
||||
v_label = [""] * n
|
||||
for coor, label, size, width in zip(v_coor.tolist(), v_label, v_size, v_line_width):
|
||||
circle = Circle(coor, size)
|
||||
circle.lineWidth = width
|
||||
# circle.label = label
|
||||
if label != "":
|
||||
x, y = coor[0], coor[1]
|
||||
offset = 0, -1.3 * size
|
||||
x += offset[0]
|
||||
y += offset[1]
|
||||
ax.text(
|
||||
x,
|
||||
y,
|
||||
label,
|
||||
fontsize=font_size,
|
||||
fontfamily=font_family,
|
||||
ha="center",
|
||||
va="top",
|
||||
)
|
||||
patches.append(circle)
|
||||
edgecolors = "black" if edgecolors == None else edgecolors
|
||||
p = PatchCollection(patches, facecolors=v_color, edgecolors=edgecolors)
|
||||
ax.add_collection(p)
|
||||
|
||||
|
||||
def hull_layout(n_v, e_list, pos, v_size, radius_increment=0.3):
|
||||
line_paths = [None] * len(e_list)
|
||||
arc_paths = [None] * len(e_list)
|
||||
|
||||
polygons_vertices_index = []
|
||||
vertices_radius = np.array(v_size)
|
||||
vertices_increased_radius = vertices_radius * radius_increment
|
||||
vertices_radius += vertices_increased_radius
|
||||
|
||||
e_degree = [len(e) for e in e_list]
|
||||
e_idxs = np.argsort(np.array(e_degree))
|
||||
|
||||
# for edge in e_list:
|
||||
for e_idx in e_idxs:
|
||||
edge = list(e_list[e_idx])
|
||||
|
||||
line_path_for_e = []
|
||||
arc_path_for_e = []
|
||||
|
||||
if len(edge) == 1:
|
||||
arc_path_for_e.append([pos[edge[0]], 0, 360, vertices_radius[edge[0]]])
|
||||
|
||||
vertices_radius[edge] += vertices_increased_radius[edge]
|
||||
|
||||
line_paths[e_idx] = line_path_for_e
|
||||
arc_paths[e_idx] = arc_path_for_e
|
||||
continue
|
||||
|
||||
pos_in_edge = pos[edge]
|
||||
if len(edge) == 2:
|
||||
vertices_index = np.array((0, 1), dtype=np.int64)
|
||||
else:
|
||||
hull = ConvexHull(pos_in_edge)
|
||||
vertices_index = hull.vertices
|
||||
|
||||
n_vertices = vertices_index.shape[0]
|
||||
|
||||
vertices_index = np.append(vertices_index, vertices_index[0]) # close the loop
|
||||
|
||||
thetas = []
|
||||
|
||||
for i in range(n_vertices):
|
||||
# line
|
||||
i1 = edge[vertices_index[i]]
|
||||
i2 = edge[vertices_index[i + 1]]
|
||||
|
||||
r1 = vertices_radius[i1]
|
||||
r2 = vertices_radius[i2]
|
||||
|
||||
p1 = pos[i1]
|
||||
p2 = pos[i2]
|
||||
|
||||
dp = p2 - p1
|
||||
dp_len = vlen(dp)
|
||||
|
||||
beta = radian_from_atan(dp[0], dp[1])
|
||||
alpha = common_tangent_radian(r1, r2, dp_len)
|
||||
|
||||
theta = beta - alpha
|
||||
start_point = polar_position(r1, theta, p1)
|
||||
end_point = polar_position(r2, theta, p2)
|
||||
|
||||
line_path_for_e.append((start_point, end_point))
|
||||
thetas.append(theta)
|
||||
|
||||
for i in range(n_vertices):
|
||||
# arcs
|
||||
theta_1 = thetas[i - 1]
|
||||
theta_2 = thetas[i]
|
||||
|
||||
arc_center = pos[edge[vertices_index[i]]]
|
||||
radius = vertices_radius[edge[vertices_index[i]]]
|
||||
|
||||
theta_1, theta_2 = rad_2_deg(theta_1), rad_2_deg(theta_2)
|
||||
arc_path_for_e.append((arc_center, theta_1, theta_2, radius))
|
||||
|
||||
vertices_radius[edge] += vertices_increased_radius[edge]
|
||||
|
||||
polygons_vertices_index.append(vertices_index.copy())
|
||||
|
||||
# line_paths.append(line_path_for_e)
|
||||
# arc_paths.append(arc_path_for_e)
|
||||
line_paths[e_idx] = line_path_for_e
|
||||
arc_paths[e_idx] = arc_path_for_e
|
||||
|
||||
return line_paths, arc_paths, polygons_vertices_index
|
||||
|
||||
|
||||
def apply_alpha(colors, alpha, elem_list, cmap=None, vmin=None, vmax=None):
|
||||
"""Apply an alpha (or list of alphas) to the colors provided.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
colors : color string or array of floats (default='r')
|
||||
Color of element. Can be a single color format string,
|
||||
or a sequence of colors with the same length as nodelist.
|
||||
If numeric values are specified they will be mapped to
|
||||
colors using the cmap and vmin,vmax parameters. See
|
||||
matplotlib.scatter for more details.
|
||||
|
||||
alpha : float or array of floats
|
||||
Alpha values for elements. This can be a single alpha value, in
|
||||
which case it will be applied to all the elements of color. Otherwise,
|
||||
if it is an array, the elements of alpha will be applied to the colors
|
||||
in order (cycling through alpha multiple times if necessary).
|
||||
|
||||
elem_list : array of networkx objects
|
||||
The list of elements which are being colored. These could be nodes,
|
||||
edges or labels.
|
||||
|
||||
cmap : matplotlib colormap
|
||||
Color map for use if colors is a list of floats corresponding to points
|
||||
on a color mapping.
|
||||
|
||||
vmin, vmax : float
|
||||
Minimum and maximum values for normalizing colors if a colormap is used
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
rgba_colors : numpy ndarray
|
||||
Array containing RGBA format values for each of the node colours.
|
||||
|
||||
"""
|
||||
from itertools import cycle
|
||||
from itertools import islice
|
||||
from numbers import Number
|
||||
|
||||
import matplotlib as mpl
|
||||
import matplotlib.cm # call as mpl.cm
|
||||
import matplotlib.colors # call as mpl.colors
|
||||
import numpy as np
|
||||
|
||||
# If we have been provided with a list of numbers as long as elem_list,
|
||||
# apply the color mapping.
|
||||
if len(colors) == len(elem_list) and isinstance(colors[0], Number):
|
||||
mapper = mpl.cm.ScalarMappable(cmap=cmap)
|
||||
mapper.set_clim(vmin, vmax)
|
||||
rgba_colors = mapper.to_rgba(colors)
|
||||
# Otherwise, convert colors to matplotlib's RGB using the colorConverter
|
||||
# object. These are converted to numpy ndarrays to be consistent with the
|
||||
# to_rgba method of ScalarMappable.
|
||||
else:
|
||||
try:
|
||||
rgba_colors = np.array([mpl.colors.colorConverter.to_rgba(colors)])
|
||||
except ValueError:
|
||||
rgba_colors = np.array(
|
||||
[mpl.colors.colorConverter.to_rgba(color) for color in colors]
|
||||
)
|
||||
# Set the final column of the rgba_colors to have the relevant alpha values
|
||||
try:
|
||||
# If alpha is longer than the number of colors, resize to the number of
|
||||
# elements. Also, if rgba_colors.size (the number of elements of
|
||||
# rgba_colors) is the same as the number of elements, resize the array,
|
||||
# to avoid it being interpreted as a colormap by scatter()
|
||||
if len(alpha) > len(rgba_colors) or rgba_colors.size == len(elem_list):
|
||||
rgba_colors = np.resize(rgba_colors, (len(elem_list), 4))
|
||||
rgba_colors[1:, 0] = rgba_colors[0, 0]
|
||||
rgba_colors[1:, 1] = rgba_colors[0, 1]
|
||||
rgba_colors[1:, 2] = rgba_colors[0, 2]
|
||||
rgba_colors[:, 3] = list(islice(cycle(alpha), len(rgba_colors)))
|
||||
except TypeError:
|
||||
rgba_colors[:, -1] = alpha
|
||||
return rgba_colors
|
||||
|
||||
|
||||
# def draw_easygraph_nodes(
|
||||
# G,
|
||||
# pos,
|
||||
# nodelist=None,
|
||||
# node_size=300,
|
||||
# node_color="#1f78b4",
|
||||
# node_shape="o",
|
||||
# alpha=None,
|
||||
# cmap=None,
|
||||
# vmin=None,
|
||||
# vmax=None,
|
||||
# ax=None,
|
||||
# linewidths=None,
|
||||
# edgecolors=None,
|
||||
# label=None,
|
||||
# margins=None,
|
||||
# ):
|
||||
# """Draw the nodes of the graph G.
|
||||
|
||||
# This draws only the nodes of the graph G.
|
||||
|
||||
# Parameters
|
||||
# ----------
|
||||
# G : graph
|
||||
# A easygraph graph
|
||||
|
||||
# pos : dictionary
|
||||
# A dictionary with nodes as keys and positions as values.
|
||||
# Positions should be sequences of length 2.
|
||||
|
||||
# ax : Matplotlib Axes object, optional
|
||||
# Draw the graph in the specified Matplotlib axes.
|
||||
|
||||
# nodelist : list (default list(G))
|
||||
# Draw only specified nodes
|
||||
|
||||
# node_size : scalar or array (default=300)
|
||||
# Size of nodes. If an array it must be the same length as nodelist.
|
||||
|
||||
# node_color : color or array of colors (default='#1f78b4')
|
||||
# Node color. Can be a single color or a sequence of colors with the same
|
||||
# length as nodelist. Color can be string or rgb (or rgba) tuple of
|
||||
# floats from 0-1. If numeric values are specified they will be
|
||||
# mapped to colors using the cmap and vmin,vmax parameters. See
|
||||
# matplotlib.scatter for more details.
|
||||
|
||||
# node_shape : string (default='o')
|
||||
# The shape of the node. Specification is as matplotlib.scatter
|
||||
# marker, one of 'so^>v<dph8'.
|
||||
|
||||
# alpha : float or array of floats (default=None)
|
||||
# The node transparency. This can be a single alpha value,
|
||||
# in which case it will be applied to all the nodes of color. Otherwise,
|
||||
# if it is an array, the elements of alpha will be applied to the colors
|
||||
# in order (cycling through alpha multiple times if necessary).
|
||||
|
||||
# cmap : Matplotlib colormap (default=None)
|
||||
# Colormap for mapping intensities of nodes
|
||||
|
||||
# vmin,vmax : floats or None (default=None)
|
||||
# Minimum and maximum for node colormap scaling
|
||||
|
||||
# linewidths : [None | scalar | sequence] (default=1.0)
|
||||
# Line width of symbol border
|
||||
|
||||
# edgecolors : [None | scalar | sequence] (default = node_color)
|
||||
# Colors of node borders
|
||||
|
||||
# label : [None | string]
|
||||
# Label for legend
|
||||
|
||||
# margins : float or 2-tuple, optional
|
||||
# Sets the padding for axis autoscaling. Increase margin to prevent
|
||||
# clipping for nodes that are near the edges of an image. Values should
|
||||
# be in the range ``[0, 1]``. See :meth:`matplotlib.axes.Axes.margins`
|
||||
# for details. The default is `None`, which uses the Matplotlib default.
|
||||
|
||||
# Returns
|
||||
# -------
|
||||
# matplotlib.collections.PathCollection
|
||||
# `PathCollection` of the nodes.
|
||||
|
||||
# Examples
|
||||
# --------
|
||||
# >>> from easygraph.datasets import get_graph_karateclub
|
||||
# >>> import easygraph as eg
|
||||
# >>> G = get_graph_karateclub()
|
||||
# >>> nodes = eg.draw_easygraph_nodes(G, pos=eg.circular_position(G))
|
||||
|
||||
|
||||
# """
|
||||
# from collections.abc import Iterable
|
||||
|
||||
# import matplotlib as mpl
|
||||
# import matplotlib.collections # call as mpl.collections
|
||||
# import matplotlib.pyplot as plt
|
||||
# import numpy as np
|
||||
|
||||
# if ax is None:
|
||||
# ax = plt.gca()
|
||||
|
||||
# if nodelist is None:
|
||||
# nodelist = list(G)
|
||||
|
||||
# if len(nodelist) == 0: # empty nodelist, no drawing
|
||||
# return mpl.collections.PathCollection(None)
|
||||
|
||||
# try:
|
||||
# xy = np.asarray([pos[v] for v in nodelist])
|
||||
# except KeyError as err:
|
||||
# raise EasyGraphError(f"Node {err} has no position.") from err
|
||||
|
||||
# if isinstance(alpha, Iterable):
|
||||
# node_color = apply_alpha(node_color, alpha, nodelist, cmap, vmin, vmax)
|
||||
# alpha = None
|
||||
|
||||
# node_collection = ax.scatter(
|
||||
# xy[:, 0],
|
||||
# xy[:, 1],
|
||||
# s=node_size,
|
||||
# c=node_color,
|
||||
# marker=node_shape,
|
||||
# cmap=cmap,
|
||||
# vmin=vmin,
|
||||
# vmax=vmax,
|
||||
# alpha=alpha,
|
||||
# linewidths=linewidths,
|
||||
# edgecolors=edgecolors,
|
||||
# label=label,
|
||||
# )
|
||||
# ax.tick_params(
|
||||
# axis="both",
|
||||
# which="both",
|
||||
# bottom=False,
|
||||
# left=False,
|
||||
# labelbottom=False,
|
||||
# labelleft=False,
|
||||
# )
|
||||
|
||||
# if margins is not None:
|
||||
# if isinstance(margins, Iterable):
|
||||
# ax.margins(*margins)
|
||||
# else:
|
||||
# ax.margins(margins)
|
||||
|
||||
# node_collection.set_zorder(2)
|
||||
# return node_collection
|
||||
|
||||
|
||||
# def draw_curved_edges(G, pos, dist_ratio=0.2, bezier_precision=20, polarity='random'):
|
||||
# # Get nodes into np array
|
||||
# edges = np.array(G.edges())
|
||||
# l = edges.shape[0]
|
||||
|
||||
# if polarity == 'random':
|
||||
# # Random polarity of curve
|
||||
# rnd = np.where(np.random.randint(2, size=l)==0, -1, 1)
|
||||
# else:
|
||||
# # Create a fixed (hashed) polarity column in the case we use fixed polarity
|
||||
# # This is useful, e.g., for animations
|
||||
# rnd = np.where(np.mod(np.vectorize(hash)(edges[:,0])+np.vectorize(hash)(edges[:,1]),2)==0,-1,1)
|
||||
|
||||
# # Coordinates (x,y) of both nodes for each edge
|
||||
# # e.g., https://stackoverflow.com/questions/16992713/translate-every-element-in-numpy-array-according-to-key
|
||||
# # Note the np.vectorize method doesn't work for all node position dictionaries for some reason
|
||||
# u, inv = np.unique(edges, return_inverse = True)
|
||||
# coords = np.array([pos[x] for x in u])[inv].reshape([edges.shape[0], 2, edges.shape[1]])
|
||||
# coords_node1 = coords[:,0,:]
|
||||
# coords_node2 = coords[:,1,:]
|
||||
|
||||
# # Swap node1/node2 allocations to make sure the directionality works correctly
|
||||
# should_swap = coords_node1[:,0] > coords_node2[:,0]
|
||||
# coords_node1[should_swap], coords_node2[should_swap] = coords_node2[should_swap], coords_node1[should_swap]
|
||||
|
||||
# # Distance for control points
|
||||
# dist = dist_ratio * np.sqrt(np.sum((coords_node1-coords_node2)**2, axis=1))
|
||||
|
||||
# # Gradients of line connecting node & perpendicular
|
||||
# m1 = (coords_node2[:,1]-coords_node1[:,1])/(coords_node2[:,0]-coords_node1[:,0])
|
||||
# m2 = -1/m1
|
||||
|
||||
# # Temporary points along the line which connects two nodes
|
||||
# # e.g., https://math.stackexchange.com/questions/656500/given-a-point-slope-and-a-distance-along-that-slope-easily-find-a-second-p
|
||||
# t1 = dist/np.sqrt(1+m1**2)
|
||||
# v1 = np.array([np.ones(l),m1])
|
||||
# coords_node1_displace = coords_node1 + (v1*t1).T
|
||||
# coords_node2_displace = coords_node2 - (v1*t1).T
|
||||
|
||||
# # Control points, same distance but along perpendicular line
|
||||
# # rnd gives the 'polarity' to determine which side of the line the curve should arc
|
||||
# t2 = dist/np.sqrt(1+m2**2)
|
||||
# v2 = np.array([np.ones(len(edges)),m2])
|
||||
# coords_node1_ctrl = coords_node1_displace + (rnd*v2*t2).T
|
||||
# coords_node2_ctrl = coords_node2_displace + (rnd*v2*t2).T
|
||||
|
||||
# # Combine all these four (x,y) columns into a 'node matrix'
|
||||
# node_matrix = np.array([coords_node1, coords_node1_ctrl, coords_node2_ctrl, coords_node2])
|
||||
|
||||
# # Create the Bezier curves and store them in a list
|
||||
# curveplots = []
|
||||
# for i in range(l):
|
||||
# nodes = node_matrix[:,i,:].T
|
||||
# curveplots.append(bezier.Curve(nodes, degree=3).evaluate_multi(np.linspace(0,1,bezier_precision)).T)
|
||||
# # Return an array of these curves
|
||||
# curves = np.array(curveplots)
|
||||
# return curves
|
||||
|
||||
# def draw_curved_graph(G, colors, ax):
|
||||
# #G = to_networkx(G)
|
||||
# # layout
|
||||
# pos = eg.spring_layout(G, iterations=50)
|
||||
# eg.draw_networkx_nodes(G, pos, ax=ax, node_size=200, node_color=colors[0], alpha=0.5)
|
||||
|
||||
# # 绘制标签
|
||||
# eg.draw_networkx_labels(G, pos, ax=ax, font_size=8, font_family='Arial', font_color='black')
|
||||
|
||||
# # Produce the curves
|
||||
# curves = draw_curved_edges(G, pos)
|
||||
# lc = LineCollection(curves, color=colors[1], alpha=0.4)
|
||||
|
||||
# # 添加连线
|
||||
# ax.add_collection(lc)
|
||||
|
||||
# # 设置坐标轴参数
|
||||
# ax.tick_params(axis='both', which='both', bottom=False, left=False, labelbottom=False, labelleft=False)
|
||||
|
||||
# plt.savefig('Figure.pdf')
|
||||
# plt.show()
|
||||
@@ -0,0 +1,185 @@
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
|
||||
from easygraph.utils import *
|
||||
|
||||
|
||||
__all__ = ["NOBE", "NOBE_GA"]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def NOBE(G, K):
|
||||
"""Graph embedding via NOBE[1].
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph
|
||||
An unweighted and undirected graph.
|
||||
|
||||
K : int
|
||||
Embedding dimension k
|
||||
|
||||
Returns
|
||||
-------
|
||||
Y : list
|
||||
list of embedding vectors (y1, y2, · · · , yn)
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> NOBE(G,K=15)
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] https://www.researchgate.net/publication/325004496_On_Spectral_Graph_Embedding_A_Non-Backtracking_Perspective_and_Graph_Approximation
|
||||
|
||||
"""
|
||||
dict = {}
|
||||
a = 0
|
||||
for i in G.nodes:
|
||||
dict[i] = a
|
||||
a += 1
|
||||
LG = graph_to_d_atleast2(G)
|
||||
N = len(G)
|
||||
P, pair = Transition(LG)
|
||||
V = eigs_nodes(P, K)
|
||||
Y = embedding(V, pair, K, N, dict, G)
|
||||
return Y
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@only_implemented_for_UnDirected_graph
|
||||
def NOBE_GA(G, K):
|
||||
"""Graph embedding via NOBE-GA[1].
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph
|
||||
An unweighted and undirected graph.
|
||||
|
||||
K : int
|
||||
Embedding dimension k
|
||||
|
||||
Returns
|
||||
-------
|
||||
Y : list
|
||||
list of embedding vectors (y1, y2, · · · , yn)
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> NOBE_GA(G,K=15)
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] https://www.researchgate.net/publication/325004496_On_Spectral_Graph_Embedding_A_Non-Backtracking_Perspective_and_Graph_Approximation
|
||||
|
||||
"""
|
||||
from scipy.sparse.linalg import eigs
|
||||
|
||||
N = len(G)
|
||||
A = np.eye(N, N)
|
||||
for i in G.edges:
|
||||
(u, v, t) = i
|
||||
u = int(u) - 1
|
||||
v = int(v) - 1
|
||||
A[u, v] = 1
|
||||
degree = G.degree()
|
||||
D_inv = np.zeros([N, N])
|
||||
a = 0
|
||||
for i in degree:
|
||||
D_inv[a, a] = 1 / degree[i]
|
||||
a += 1
|
||||
D_I_inv = np.zeros([N, N])
|
||||
b = 0
|
||||
for i in degree:
|
||||
if degree[i] > 1:
|
||||
D_I_inv[b, b] = 1 / (degree[i] - 1)
|
||||
b += 1
|
||||
I = np.identity(N)
|
||||
M_D = 0.5 * A * D_I_inv * (I - D_inv)
|
||||
D_D = 0.5 * I
|
||||
T_ua = np.zeros([2 * N, 2 * N])
|
||||
T_ua[0:N, 0:N] = M_D
|
||||
T_ua[N : 2 * N, N : 2 * N] = M_D
|
||||
T_ua[N : 2 * N, 0:N] = D_D
|
||||
T_ua[0:N, N : 2 * N] = D_D
|
||||
Y1, Y = eigs(T_ua, K + 1, which="LR")
|
||||
Y = Y[0:N, :-1]
|
||||
return Y
|
||||
|
||||
|
||||
def graph_to_d_atleast2(G):
|
||||
n = len(G)
|
||||
LG = eg.Graph()
|
||||
LG = G.copy()
|
||||
new_node = n
|
||||
degree = LG.degree()
|
||||
node = LG.nodes.copy()
|
||||
for i in node:
|
||||
if degree[i] == 1:
|
||||
for neighbors in LG.neighbors(node=i):
|
||||
LG.add_edge(i, new_node)
|
||||
LG.add_edge(new_node, neighbors)
|
||||
break
|
||||
new_node = new_node + 1
|
||||
return LG
|
||||
|
||||
|
||||
def Transition(LG):
|
||||
N = len(LG)
|
||||
M = LG.size()
|
||||
LLG = eg.DiGraph()
|
||||
for i in LG.edges:
|
||||
(u, v, t) = i
|
||||
LLG.add_edge(u, v)
|
||||
LLG.add_edge(v, u)
|
||||
degree = LLG.degree()
|
||||
P = np.zeros([2 * M, 2 * M])
|
||||
pair = []
|
||||
k = 0
|
||||
l = 0
|
||||
for i in LLG.edges:
|
||||
l = 0
|
||||
for j in LLG.edges:
|
||||
(u, v, t) = i
|
||||
(x, y, z) = j
|
||||
if v == x and u != y:
|
||||
P[k][l] = 1 / (degree[v] - 1)
|
||||
l += 1
|
||||
k += 1
|
||||
a = 0
|
||||
for i in LLG.edges:
|
||||
(u, v, t) = i
|
||||
pair.append([u, v])
|
||||
a += 1
|
||||
return P, pair
|
||||
|
||||
|
||||
def eigs_nodes(P, K):
|
||||
from scipy.sparse.linalg import eigs
|
||||
|
||||
M = np.size(P, 0)
|
||||
L = np.zeros([M, M])
|
||||
I = np.identity(M)
|
||||
P_T = P.T
|
||||
L = I - (P + P_T) / 2
|
||||
U, D = eigs(L, K + 1, which="LR")
|
||||
D = D[:, :-1]
|
||||
V = np.zeros([M, K], dtype=complex)
|
||||
a = 0
|
||||
for i in D:
|
||||
V[a] = i
|
||||
a += 1
|
||||
return V
|
||||
|
||||
|
||||
def embedding(V, pair, K, N, dict, G):
|
||||
Y = np.zeros([N, K], dtype=complex)
|
||||
idx = 0
|
||||
for i in pair:
|
||||
[v, u] = i
|
||||
if u in G.nodes:
|
||||
t = dict[u]
|
||||
for j in range(0, len(V[idx])):
|
||||
Y[t, j] += V[idx, j]
|
||||
idx += 1
|
||||
return Y
|
||||
@@ -0,0 +1,13 @@
|
||||
from .deepwalk import *
|
||||
from .NOBE import *
|
||||
from .node2vec import *
|
||||
|
||||
|
||||
try:
|
||||
from .line import *
|
||||
from .sdne import *
|
||||
except:
|
||||
print(
|
||||
"Warning raise in module:graph_embedding. Please install packages Pytorch"
|
||||
" before you use functions related to graph_embedding"
|
||||
)
|
||||
@@ -0,0 +1,103 @@
|
||||
import random
|
||||
|
||||
from easygraph.functions.graph_embedding.node2vec import (
|
||||
_get_embedding_result_from_gensim_skipgram_model,
|
||||
)
|
||||
from easygraph.functions.graph_embedding.node2vec import learn_embeddings
|
||||
from easygraph.utils import *
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
__all__ = ["deepwalk"]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def deepwalk(G, dimensions=128, walk_length=80, num_walks=10, **skip_gram_params):
|
||||
"""Graph embedding via DeepWalk.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
dimensions : int
|
||||
Embedding dimensions, optional(default: 128)
|
||||
|
||||
walk_length : int
|
||||
Number of nodes in each walk, optional(default: 80)
|
||||
|
||||
num_walks : int
|
||||
Number of walks per node, optional(default: 10)
|
||||
|
||||
skip_gram_params : dict
|
||||
Parameters for gensim.models.Word2Vec - do not supply `size`, it is taken from the `dimensions` parameter
|
||||
|
||||
Returns
|
||||
-------
|
||||
embedding_vector : dict
|
||||
The embedding vector of each node
|
||||
|
||||
most_similar_nodes_of_node : dict
|
||||
The most similar nodes of each node and its similarity
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> deepwalk(G,
|
||||
... dimensions=128, # The graph embedding dimensions.
|
||||
... walk_length=80, # Walk length of each random walks.
|
||||
... num_walks=10, # Number of random walks.
|
||||
... skip_gram_params = dict( # The skip_gram parameters in Python package gensim.
|
||||
... window=10,
|
||||
... min_count=1,
|
||||
... batch_words=4,
|
||||
... iter=15
|
||||
... ))
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] https://arxiv.org/abs/1403.6652
|
||||
|
||||
"""
|
||||
G_index, index_of_node, node_of_index = G.to_index_node_graph()
|
||||
|
||||
walks = simulate_walks(G_index, walk_length=walk_length, num_walks=num_walks)
|
||||
model = learn_embeddings(walks=walks, dimensions=dimensions, **skip_gram_params)
|
||||
|
||||
(
|
||||
embedding_vector,
|
||||
most_similar_nodes_of_node,
|
||||
) = _get_embedding_result_from_gensim_skipgram_model(
|
||||
G=G, index_of_node=index_of_node, node_of_index=node_of_index, model=model
|
||||
)
|
||||
|
||||
del G_index
|
||||
return embedding_vector, most_similar_nodes_of_node
|
||||
|
||||
|
||||
def simulate_walks(G, walk_length, num_walks):
|
||||
walks = []
|
||||
nodes = list(G.nodes)
|
||||
print("Walk iteration:")
|
||||
for walk_iter in tqdm(range(num_walks)):
|
||||
random.shuffle(nodes)
|
||||
for node in nodes:
|
||||
walks.append(_deepwalk_walk(G, walk_length=walk_length, start_node=node))
|
||||
|
||||
return walks
|
||||
|
||||
|
||||
def _deepwalk_walk(G, walk_length, start_node):
|
||||
"""
|
||||
Simulate a random walk starting from start node.
|
||||
"""
|
||||
walk = [start_node]
|
||||
|
||||
while len(walk) < walk_length:
|
||||
cur = walk[-1]
|
||||
cur_nbrs = sorted(G.neighbors(cur))
|
||||
if len(cur_nbrs) > 0:
|
||||
pick_node = random.choice(cur_nbrs)
|
||||
walk.append(pick_node)
|
||||
else:
|
||||
break
|
||||
return walk
|
||||
@@ -0,0 +1,303 @@
|
||||
import time
|
||||
import warnings
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from easygraph.utils import alias_draw
|
||||
from easygraph.utils import alias_setup
|
||||
from sklearn import preprocessing
|
||||
|
||||
# from easygraph.functions.graph_embedding import *
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
class LINE(nn.Module):
|
||||
"""Graph embedding via LINE.
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
dimension: int
|
||||
walk_length: int
|
||||
|
||||
walk_num: int
|
||||
|
||||
negative: int
|
||||
batch_size: int
|
||||
|
||||
init_alpha: float
|
||||
order: int
|
||||
Returns
|
||||
-------
|
||||
embedding_vector : dict
|
||||
The embedding vector of each node
|
||||
Examples
|
||||
--------
|
||||
>>> model = LINE(
|
||||
... dimension=128,
|
||||
... walk_length=80,
|
||||
... walk_num=20,
|
||||
... negative=5,
|
||||
... batch_size=128,
|
||||
... init_alpha=0.025,
|
||||
... order=3 )
|
||||
>>> model.train()
|
||||
>>> emb = model(g, return_dict=True) # g: easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
References
|
||||
----------
|
||||
|
||||
.. [1] Tang, J., Qu, M., Wang, M., Zhang, M., Yan, J., & Mei, Q. (2015, May). Line: Large-scale information network embedding. In Proceedings of the 24th international conference on world wide web (pp. 1067-1077).
|
||||
|
||||
https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/frp0228-Tang.pdf
|
||||
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def add_args(parser):
|
||||
"""Add model-specific arguments to the parser."""
|
||||
parser.add_argument(
|
||||
"--walk-length",
|
||||
type=int,
|
||||
default=80,
|
||||
help="Length of walk per source. Default is 80.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--walk-num",
|
||||
type=int,
|
||||
default=20,
|
||||
help="Number of walks per source. Default is 20.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--negative",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Number of negative node in sampling. Default is 5.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--batch-size",
|
||||
type=int,
|
||||
default=1000,
|
||||
help="Batch size in SGD training process. Default is 1000.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--alpha",
|
||||
type=float,
|
||||
default=0.025,
|
||||
help="Initial learning rate of SGD. Default is 0.025.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--order",
|
||||
type=int,
|
||||
default=3,
|
||||
help="Order of proximity in LINE. Default is 3 for 1+2.",
|
||||
)
|
||||
parser.add_argument("--hidden-size", type=int, default=128)
|
||||
|
||||
@classmethod
|
||||
def build_model_from_args(cls, args):
|
||||
return cls(
|
||||
args.hidden_size,
|
||||
args.walk_length,
|
||||
args.walk_num,
|
||||
args.negative,
|
||||
args.batch_size,
|
||||
args.alpha,
|
||||
args.order,
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dimension=128,
|
||||
walk_length=80,
|
||||
walk_num=20,
|
||||
negative=5,
|
||||
batch_size=128,
|
||||
init_alpha=0.025,
|
||||
order=3,
|
||||
):
|
||||
super(LINE, self).__init__()
|
||||
self.dimension = dimension
|
||||
self.walk_length = walk_length
|
||||
self.walk_num = walk_num
|
||||
self.negative = negative
|
||||
self.batch_size = batch_size
|
||||
self.init_alpha = init_alpha
|
||||
self.order = order
|
||||
|
||||
def forward(self, g, return_dict=True):
|
||||
# run LINE algorithm, 1-order, 2-order or 3(1-order + 2-order)
|
||||
|
||||
self.G = g
|
||||
self.is_directed = g.is_directed()
|
||||
self.num_node = len(g.nodes)
|
||||
self.num_edge = g.number_of_edges()
|
||||
self.num_sampling_edge = self.walk_length * self.walk_num * self.num_node
|
||||
|
||||
node2id = dict([(node, vid) for vid, node in enumerate(g.nodes)])
|
||||
self.edges = [[node2id[e[0]], node2id[e[1]]] for e in self.G.edges]
|
||||
|
||||
self.edges_prob = np.asarray([1.0 for e in g.edges])
|
||||
self.edges_prob /= np.sum(self.edges_prob)
|
||||
self.edges_table, self.edges_prob = alias_setup(self.edges_prob)
|
||||
|
||||
degree_weight = np.asarray([0] * self.num_node)
|
||||
degree_weight = np.array(list(g.degree(node2id[u] for u in g.nodes).values()))
|
||||
# for u,v in g.edges:
|
||||
|
||||
# degree_weight[node2id[u]] += 1.0
|
||||
# if not self.is_directed:
|
||||
# degree_weight[node2id[v]] += 1.0
|
||||
self.node_prob = np.power(degree_weight, 0.75)
|
||||
self.node_prob /= np.sum(self.node_prob)
|
||||
self.node_table, self.node_prob = alias_setup(self.node_prob)
|
||||
|
||||
if self.order == 3:
|
||||
self.dimension = int(self.dimension / 2)
|
||||
if self.order == 1 or self.order == 3:
|
||||
print("train line with 1-order")
|
||||
print(type(self.dimension))
|
||||
self.emb_vertex = (
|
||||
np.random.random((self.num_node, self.dimension)) - 0.5
|
||||
) / self.dimension
|
||||
self._train_line(order=1)
|
||||
embedding1 = preprocessing.normalize(self.emb_vertex, "l2")
|
||||
|
||||
if self.order == 2 or self.order == 3:
|
||||
print("train line with 2-order")
|
||||
self.emb_vertex = (
|
||||
np.random.random((self.num_node, self.dimension)) - 0.5
|
||||
) / self.dimension
|
||||
self.emb_context = self.emb_vertex
|
||||
self._train_line(order=2)
|
||||
embedding2 = preprocessing.normalize(self.emb_vertex, "l2")
|
||||
|
||||
if self.order == 1:
|
||||
embeddings = embedding1
|
||||
elif self.order == 2:
|
||||
embeddings = embedding2
|
||||
else:
|
||||
print("concatenate two embedding...")
|
||||
embeddings = np.hstack((embedding1, embedding2))
|
||||
|
||||
if return_dict:
|
||||
features_matrix = dict()
|
||||
for vid, node in enumerate(g.nodes):
|
||||
features_matrix[node] = embeddings[vid]
|
||||
else:
|
||||
features_matrix = np.zeros((len(g.nodes), embeddings.shape[1]))
|
||||
nx_nodes = list(g.nodes)
|
||||
features_matrix[nx_nodes] = embeddings[np.arange(len(g.nodes))]
|
||||
return features_matrix
|
||||
|
||||
def _update(self, vec_u, vec_v, vec_error, label):
|
||||
# update vetex embedding and vec_error
|
||||
f = 1 / (1 + np.exp(-np.sum(vec_u * vec_v, axis=1)))
|
||||
g = (self.alpha * (label - f)).reshape((len(label), 1))
|
||||
vec_error += g * vec_v
|
||||
vec_v += g * vec_u
|
||||
|
||||
def _train_line(self, order):
|
||||
# train Line model with order
|
||||
self.alpha = self.init_alpha
|
||||
batch_size = self.batch_size
|
||||
t0 = time.time()
|
||||
num_batch = int(self.num_sampling_edge / batch_size)
|
||||
epoch_iter = tqdm(range(num_batch))
|
||||
for b in epoch_iter:
|
||||
if b % 100 == 0:
|
||||
epoch_iter.set_description(
|
||||
# f"Progress: {b * 1.0 / num_batch * 100:.4f}, alpha: {self.alpha:.6f}, time: {time.time() - t0:.4f}"
|
||||
)
|
||||
self.alpha = self.init_alpha * max((1 - b * 1.0 / num_batch), 0.0001)
|
||||
u, v = [0] * batch_size, [0] * batch_size
|
||||
for i in range(batch_size):
|
||||
edge_id = alias_draw(self.edges_table, self.edges_prob)
|
||||
u[i], v[i] = self.edges[edge_id]
|
||||
if not self.is_directed and np.random.rand() > 0.5:
|
||||
v[i], u[i] = self.edges[edge_id]
|
||||
|
||||
vec_error = np.zeros((batch_size, self.dimension))
|
||||
label, target = np.asarray([1 for i in range(batch_size)]), np.asarray(v)
|
||||
for j in range(1 + self.negative):
|
||||
if j != 0:
|
||||
label = np.asarray([0 for i in range(batch_size)])
|
||||
for i in range(batch_size):
|
||||
target[i] = alias_draw(self.node_table, self.node_prob)
|
||||
if order == 1:
|
||||
self._update(
|
||||
self.emb_vertex[u], self.emb_vertex[target], vec_error, label
|
||||
)
|
||||
else:
|
||||
self._update(
|
||||
self.emb_vertex[u], self.emb_context[target], vec_error, label
|
||||
)
|
||||
self.emb_vertex[u] += vec_error
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
dataset = eg.CiteseerGraphDataset(
|
||||
force_reload=True
|
||||
) # Download CiteseerGraphDataset contained in EasyGraph
|
||||
num_classes = dataset.num_classes
|
||||
g = dataset[0]
|
||||
labels = g.ndata["label"]
|
||||
edge_list = []
|
||||
for i in g.edges:
|
||||
edge_list.append((i[0], i[1]))
|
||||
g1 = eg.Graph()
|
||||
g1.add_edges_from(edge_list)
|
||||
# print(g.edges)
|
||||
# print(g.__dir__())
|
||||
|
||||
model = LINE(
|
||||
dimension=128,
|
||||
walk_length=80,
|
||||
walk_num=20,
|
||||
negative=5,
|
||||
batch_size=128,
|
||||
init_alpha=0.025,
|
||||
order=3,
|
||||
)
|
||||
print(model)
|
||||
|
||||
model.train()
|
||||
out = model(g1, return_dict=True)
|
||||
|
||||
keylist = sorted(out)
|
||||
tmp = torch.cat(
|
||||
(
|
||||
torch.unsqueeze(torch.tensor(out[keylist[0]]), -2),
|
||||
torch.unsqueeze(torch.tensor(out[keylist[1]]), -2),
|
||||
),
|
||||
0,
|
||||
)
|
||||
|
||||
for i in range(2, len(keylist)):
|
||||
tmp = torch.cat((tmp, torch.unsqueeze(torch.tensor(out[keylist[i]]), -2)), 0)
|
||||
torch.save(tmp, "line.emb")
|
||||
print(tmp, tmp.shape)
|
||||
|
||||
line_emb = []
|
||||
for i in range(0, len(tmp)):
|
||||
line_emb.append(list(tmp[i]))
|
||||
line_emb = np.array(line_emb)
|
||||
|
||||
# tsne = TSNE(n_components=2)
|
||||
# z = tsne.fit_transform(line_emb)
|
||||
# z_data = np.vstack((z.T, labels)).T
|
||||
# df_tsne = pd.DataFrame(z_data, columns=['Dim1', 'Dim2', 'class'])
|
||||
# df_tsne['class'] = df_tsne['class'].astype(int)
|
||||
# df_tsne.head()
|
||||
#
|
||||
# plt.figure(figsize=(8, 8))
|
||||
# sns.scatterplot(data=df_tsne, hue='class', x='Dim1', y='Dim2', palette=['green','orange','brown','red', 'blue','black'])
|
||||
# plt.savefig('torch_line_citeseer.pdf', bbox_inches='tight')
|
||||
# plt.show()
|
||||
#
|
||||
#
|
||||
Binary file not shown.
@@ -0,0 +1,175 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import time
|
||||
import warnings
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import easygraph as eg
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import seaborn as sns
|
||||
import torch
|
||||
|
||||
from easygraph.datasets.citation_graph import CiteseerGraphDataset
|
||||
from easygraph.functions.community import greedy_modularity_communities
|
||||
from easygraph.functions.community import modularity
|
||||
from easygraph.functions.graph_embedding import *
|
||||
from mpl_toolkits.mplot3d import Axes3D
|
||||
from sklearn.decomposition import PCA
|
||||
from sklearn.manifold import TSNE
|
||||
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
|
||||
dataset = CiteseerGraphDataset(
|
||||
force_reload=True
|
||||
) # Download CiteseerGraphDataset contained in EasyGraph
|
||||
num_classes = dataset.num_classes
|
||||
g = dataset[0]
|
||||
labels = g.ndata["label"]
|
||||
print(labels, labels.shape, len(g.nodes))
|
||||
|
||||
print("Graph embedding via DeepWalk...........")
|
||||
deepwalk_emb, _ = deepwalk(g, dimensions=128, walk_length=80, num_walks=10)
|
||||
# print(deepwalk_emb, len(deepwalk_emb))
|
||||
|
||||
dw_emb = []
|
||||
for i in range(0, len(deepwalk_emb)):
|
||||
dw_emb.append(list(deepwalk_emb[i]))
|
||||
# print(len(dw_emb))
|
||||
dw_emb = np.array(dw_emb)
|
||||
print(dw_emb)
|
||||
|
||||
tsne = TSNE(n_components=2, verbose=1, random_state=0)
|
||||
z = tsne.fit_transform(dw_emb)
|
||||
z_data = np.vstack((z.T, labels)).T
|
||||
df_tsne = pd.DataFrame(z_data, columns=["Dim1", "Dim2", "class"])
|
||||
df_tsne["class"] = df_tsne["class"].astype(int)
|
||||
plt.figure(figsize=(8, 8))
|
||||
sns.scatterplot(
|
||||
data=df_tsne,
|
||||
hue="class",
|
||||
x="Dim1",
|
||||
y="Dim2",
|
||||
palette=["green", "orange", "brown", "red", "blue", "black"],
|
||||
)
|
||||
plt.savefig(
|
||||
"figs/dw_citeseer.pdf", bbox_inches="tight"
|
||||
) # save embeddings if needed
|
||||
plt.savefig("figs/dw_citeseer.png", bbox_inches="tight")
|
||||
plt.show()
|
||||
|
||||
print("Graph embedding via Node2Vec..............")
|
||||
node2vec_emb, _ = node2vec(
|
||||
g, dimensions=128, walk_length=80, num_walks=10, p=4, q=0.25
|
||||
)
|
||||
# print(node2vec_emb, len(node2vec_emb))
|
||||
|
||||
n2v_emb = []
|
||||
for i in range(0, len(node2vec_emb)):
|
||||
n2v_emb.append(list(node2vec_emb[i]))
|
||||
# print(len(n2v_emb))
|
||||
n2v_emb = np.array(n2v_emb)
|
||||
print(n2v_emb)
|
||||
|
||||
tsne = TSNE(n_components=2, verbose=1, random_state=0)
|
||||
z = tsne.fit_transform(n2v_emb)
|
||||
z_data = np.vstack((z.T, labels)).T
|
||||
df_tsne = pd.DataFrame(z_data, columns=["Dim1", "Dim2", "class"])
|
||||
df_tsne["class"] = df_tsne["class"].astype(int)
|
||||
plt.figure(figsize=(8, 8))
|
||||
sns.scatterplot(
|
||||
data=df_tsne,
|
||||
hue="class",
|
||||
x="Dim1",
|
||||
y="Dim2",
|
||||
palette=["green", "orange", "brown", "red", "blue", "black"],
|
||||
)
|
||||
|
||||
plt.savefig("figs/n2v_citeseer.pdf", bbox_inches="tight")
|
||||
plt.savefig("figs/n2v_citeseer.png", bbox_inches="tight")
|
||||
plt.show()
|
||||
|
||||
print("Graph embedding via LINE........")
|
||||
|
||||
model = LINE(
|
||||
dimension=128,
|
||||
walk_length=80,
|
||||
walk_num=10,
|
||||
negative=5,
|
||||
batch_size=128,
|
||||
init_alpha=0.025,
|
||||
order=2,
|
||||
)
|
||||
|
||||
model.train()
|
||||
line_emb = model(g, return_dict=True)
|
||||
|
||||
l_emb = []
|
||||
for i in range(0, len(line_emb)):
|
||||
l_emb.append(list(line_emb[i]))
|
||||
# print(len(l_emb))
|
||||
l_emb = np.array(l_emb)
|
||||
print(l_emb)
|
||||
|
||||
tsne = TSNE(n_components=2, verbose=1, random_state=0)
|
||||
z = tsne.fit_transform(l_emb)
|
||||
z_data = np.vstack((z.T, labels)).T
|
||||
df_tsne = pd.DataFrame(z_data, columns=["Dim1", "Dim2", "class"])
|
||||
df_tsne["class"] = df_tsne["class"].astype(int)
|
||||
plt.figure(figsize=(8, 8))
|
||||
sns.scatterplot(
|
||||
data=df_tsne,
|
||||
hue="class",
|
||||
x="Dim1",
|
||||
y="Dim2",
|
||||
palette=["green", "orange", "brown", "red", "blue", "black"],
|
||||
)
|
||||
|
||||
plt.savefig("figs/line_citeseer.pdf", bbox_inches="tight")
|
||||
plt.savefig("figs/line_citeseer.png", bbox_inches="tight")
|
||||
plt.show()
|
||||
|
||||
print("Graph embedding via SDNE...........")
|
||||
model = eg.SDNE(
|
||||
g,
|
||||
node_size=len(g.nodes),
|
||||
nhid0=256,
|
||||
nhid1=32,
|
||||
dropout=0.025,
|
||||
alpha=5e-4,
|
||||
beta=10,
|
||||
)
|
||||
sdne_emb = model.train(model)
|
||||
|
||||
sd_emb = []
|
||||
for i in range(0, len(sdne_emb)):
|
||||
sd_emb.append(list(sdne_emb[i]))
|
||||
# print(len(sd_emb))
|
||||
sd_emb = np.array(sd_emb)
|
||||
print(sd_emb)
|
||||
|
||||
tsne = TSNE(n_components=2, verbose=1, random_state=0)
|
||||
z = tsne.fit_transform(sd_emb)
|
||||
z_data = np.vstack((z.T, labels)).T
|
||||
df_tsne = pd.DataFrame(z_data, columns=["Dim1", "Dim2", "class"])
|
||||
df_tsne["class"] = df_tsne["class"].astype(int)
|
||||
plt.figure(figsize=(8, 8))
|
||||
sns.scatterplot(
|
||||
data=df_tsne,
|
||||
hue="class",
|
||||
x="Dim1",
|
||||
y="Dim2",
|
||||
palette=["green", "orange", "brown", "red", "blue", "black"],
|
||||
)
|
||||
|
||||
plt.savefig("figs/sdne_citeseer2.pdf", bbox_inches="tight")
|
||||
plt.savefig("figs/sdne_citeseer2.png", bbox_inches="tight")
|
||||
plt.show()
|
||||
@@ -0,0 +1,308 @@
|
||||
import random
|
||||
|
||||
import numpy as np
|
||||
|
||||
from easygraph.utils import *
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
__all__ = ["node2vec"]
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def node2vec(
|
||||
G,
|
||||
dimensions=128,
|
||||
walk_length=80,
|
||||
num_walks=10,
|
||||
p=1.0,
|
||||
q=1.0,
|
||||
weight_key=None,
|
||||
workers=None,
|
||||
**skip_gram_params,
|
||||
):
|
||||
"""Graph embedding via Node2Vec.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
dimensions : int
|
||||
Embedding dimensions, optional(default: 128)
|
||||
|
||||
walk_length : int
|
||||
Number of nodes in each walk, optional(default: 80)
|
||||
|
||||
num_walks : int
|
||||
Number of walks per node, optional(default: 10)
|
||||
|
||||
p : float
|
||||
The return hyper parameter, optional(default: 1.0)
|
||||
|
||||
q : float
|
||||
The input parameter, optional(default: 1.0)
|
||||
|
||||
weight_key : string or None (default: None)
|
||||
On weighted graphs, this is the key for the weight attribute
|
||||
|
||||
workers : int or None, optional(default : None)
|
||||
The number of workers generating random walks (default: None). None if not using only one worker.
|
||||
|
||||
skip_gram_params : dict
|
||||
Parameters for gensim.models.Word2Vec - do not supply 'size', it is taken from the 'dimensions' parameter
|
||||
|
||||
Returns
|
||||
-------
|
||||
embedding_vector : dict
|
||||
The embedding vector of each node
|
||||
|
||||
most_similar_nodes_of_node : dict
|
||||
The most similar nodes of each node and its similarity
|
||||
|
||||
Examples
|
||||
--------
|
||||
|
||||
>>> node2vec(G,
|
||||
... dimensions=128, # The graph embedding dimensions.
|
||||
... walk_length=80, # Walk length of each random walks.
|
||||
... num_walks=10, # Number of random walks.
|
||||
... p=1.0, # The `p` possibility in random walk in [1]_
|
||||
... q=1.0, # The `q` possibility in random walk in [1]_
|
||||
... weight_key='weight',
|
||||
... skip_gram_params=dict( # The skip_gram parameters in Python package gensim.
|
||||
... window=10,
|
||||
... min_count=1,
|
||||
... batch_words=4
|
||||
... ))
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] https://arxiv.org/abs/1607.00653
|
||||
|
||||
"""
|
||||
G_index, index_of_node, node_of_index = G.to_index_node_graph()
|
||||
|
||||
if workers is None:
|
||||
walks = simulate_walks(
|
||||
G_index,
|
||||
walk_length=walk_length,
|
||||
num_walks=num_walks,
|
||||
p=p,
|
||||
q=q,
|
||||
weight_key=weight_key,
|
||||
)
|
||||
else:
|
||||
from joblib import Parallel
|
||||
from joblib import delayed
|
||||
|
||||
num_walks_lists = np.array_split(range(num_walks), workers)
|
||||
walks = Parallel(n_jobs=workers)(
|
||||
delayed(simulate_walks)(
|
||||
G_index, walk_length, len(num_walks), p, q, weight_key
|
||||
)
|
||||
for num_walks in num_walks_lists
|
||||
)
|
||||
# Change multidimensional array to one dimensional array
|
||||
walks = [walk for walk_group in walks for walk in walk_group]
|
||||
|
||||
model = learn_embeddings(walks=walks, dimensions=dimensions, **skip_gram_params)
|
||||
|
||||
(
|
||||
embedding_vector,
|
||||
most_similar_nodes_of_node,
|
||||
) = _get_embedding_result_from_gensim_skipgram_model(
|
||||
G=G, index_of_node=index_of_node, node_of_index=node_of_index, model=model
|
||||
)
|
||||
|
||||
del G_index
|
||||
return embedding_vector, most_similar_nodes_of_node
|
||||
|
||||
|
||||
def _get_embedding_result_from_gensim_skipgram_model(
|
||||
G, index_of_node, node_of_index, model
|
||||
):
|
||||
embedding_vector = dict()
|
||||
most_similar_nodes_of_node = dict()
|
||||
|
||||
def change_string_to_node_from_gensim_return_value(value_including_str):
|
||||
# As the return value of gensim model.wv.most_similar includes string index in G_index,
|
||||
# the string index should be changed to the original node element in G.
|
||||
result = []
|
||||
for node_index, value in value_including_str:
|
||||
node_index = int(node_index)
|
||||
node = node_of_index[node_index]
|
||||
result.append((node, value))
|
||||
return result
|
||||
|
||||
for node in G.nodes:
|
||||
# Output node names are always strings in gensim
|
||||
embedding_vector[node] = model.wv[str(index_of_node[node])]
|
||||
|
||||
most_similar_nodes = model.wv.most_similar(str(index_of_node[node]))
|
||||
most_similar_nodes_of_node[
|
||||
node
|
||||
] = change_string_to_node_from_gensim_return_value(most_similar_nodes)
|
||||
|
||||
return embedding_vector, most_similar_nodes_of_node
|
||||
|
||||
|
||||
def simulate_walks(G, walk_length, num_walks, p, q, weight_key=None):
|
||||
alias_nodes, alias_edges = _preprocess_transition_probs(G, p, q, weight_key)
|
||||
walks = []
|
||||
nodes = list(G.nodes)
|
||||
for walk_iter in tqdm(range(num_walks)):
|
||||
random.shuffle(nodes)
|
||||
for node in nodes:
|
||||
walks.append(
|
||||
_node2vec_walk(
|
||||
G,
|
||||
walk_length=walk_length,
|
||||
start_node=node,
|
||||
alias_nodes=alias_nodes,
|
||||
alias_edges=alias_edges,
|
||||
)
|
||||
)
|
||||
|
||||
return walks
|
||||
|
||||
|
||||
def _preprocess_transition_probs(G, p, q, weight_key=None):
|
||||
is_directed = G.is_directed()
|
||||
alias_nodes = {}
|
||||
|
||||
for node in G.nodes:
|
||||
if weight_key is None:
|
||||
unnormalized_probs = [1.0 for nbr in sorted(G.neighbors(node))]
|
||||
else:
|
||||
unnormalized_probs = [
|
||||
G[node][nbr][weight_key] for nbr in sorted(G.neighbors(node))
|
||||
]
|
||||
norm_const = sum(unnormalized_probs)
|
||||
normalized_probs = [float(u_prob) / norm_const for u_prob in unnormalized_probs]
|
||||
alias_nodes[node] = _alias_setup(normalized_probs)
|
||||
|
||||
alias_edges = {}
|
||||
triads = {}
|
||||
|
||||
if is_directed:
|
||||
for edge in G.edges:
|
||||
alias_edges[(edge[0], edge[1])] = _get_alias_edge(
|
||||
G, edge[0], edge[1], p, q, weight_key
|
||||
)
|
||||
else:
|
||||
for edge in G.edges:
|
||||
alias_edges[(edge[0], edge[1])] = _get_alias_edge(
|
||||
G, edge[0], edge[1], p, q, weight_key
|
||||
)
|
||||
alias_edges[(edge[1], edge[0])] = _get_alias_edge(
|
||||
G, edge[1], edge[0], p, q, weight_key
|
||||
)
|
||||
|
||||
return alias_nodes, alias_edges
|
||||
|
||||
|
||||
def _get_alias_edge(G, src, dst, p, q, weight_key=None):
|
||||
unnormalized_probs = []
|
||||
|
||||
if weight_key is None:
|
||||
for dst_nbr in sorted(G.neighbors(dst)):
|
||||
if dst_nbr == src:
|
||||
unnormalized_probs.append(1.0 / p)
|
||||
elif G.has_edge(dst_nbr, src):
|
||||
unnormalized_probs.append(1.0)
|
||||
else:
|
||||
unnormalized_probs.append(1.0 / q)
|
||||
else:
|
||||
for dst_nbr in sorted(G.neighbors(dst)):
|
||||
if dst_nbr == src:
|
||||
unnormalized_probs.append(G[dst][dst_nbr][weight_key] / p)
|
||||
elif G.has_edge(dst_nbr, src):
|
||||
unnormalized_probs.append(G[dst][dst_nbr][weight_key])
|
||||
else:
|
||||
unnormalized_probs.append(G[dst][dst_nbr][weight_key] / q)
|
||||
|
||||
norm_const = sum(unnormalized_probs)
|
||||
normalized_probs = [float(u_prob) / norm_const for u_prob in unnormalized_probs]
|
||||
|
||||
return _alias_setup(normalized_probs)
|
||||
|
||||
|
||||
def _alias_setup(probs):
|
||||
K = len(probs)
|
||||
q = np.zeros(K)
|
||||
J = np.zeros(K, dtype=int)
|
||||
|
||||
smaller = []
|
||||
larger = []
|
||||
for kk, prob in enumerate(probs):
|
||||
q[kk] = K * prob
|
||||
if q[kk] < 1.0:
|
||||
smaller.append(kk)
|
||||
else:
|
||||
larger.append(kk)
|
||||
|
||||
while len(smaller) > 0 and len(larger) > 0:
|
||||
small = smaller.pop()
|
||||
large = larger.pop()
|
||||
|
||||
J[small] = large
|
||||
q[large] = q[large] + q[small] - 1.0
|
||||
if q[large] < 1.0:
|
||||
smaller.append(large)
|
||||
else:
|
||||
larger.append(large)
|
||||
|
||||
return J, q
|
||||
|
||||
|
||||
def _node2vec_walk(G, walk_length, start_node, alias_nodes, alias_edges):
|
||||
"""
|
||||
Simulate a random walk starting from start node.
|
||||
"""
|
||||
walk = [start_node]
|
||||
|
||||
while len(walk) < walk_length:
|
||||
cur = walk[-1]
|
||||
cur_nbrs = sorted(G.neighbors(cur))
|
||||
if len(cur_nbrs) > 0:
|
||||
if len(walk) == 1:
|
||||
walk.append(
|
||||
cur_nbrs[_alias_draw(alias_nodes[cur][0], alias_nodes[cur][1])]
|
||||
)
|
||||
else:
|
||||
prev = walk[-2]
|
||||
next_node = cur_nbrs[
|
||||
_alias_draw(
|
||||
alias_edges[(prev, cur)][0], alias_edges[(prev, cur)][1]
|
||||
)
|
||||
]
|
||||
walk.append(next_node)
|
||||
else:
|
||||
break
|
||||
|
||||
return walk
|
||||
|
||||
|
||||
def _alias_draw(J, q):
|
||||
K = len(J)
|
||||
kk = int(np.floor(np.random.rand() * K))
|
||||
if np.random.rand() < q[kk]:
|
||||
return kk
|
||||
else:
|
||||
return J[kk]
|
||||
|
||||
|
||||
def learn_embeddings(walks, dimensions, **skip_gram_params):
|
||||
"""
|
||||
Learn embeddings with Word2Vec.
|
||||
"""
|
||||
from gensim.models import Word2Vec
|
||||
|
||||
walks = [list(map(str, walk)) for walk in walks]
|
||||
|
||||
if "vector_size" not in skip_gram_params:
|
||||
skip_gram_params["vector_size"] = dimensions
|
||||
|
||||
model = Word2Vec(walks, **skip_gram_params)
|
||||
|
||||
return model
|
||||
@@ -0,0 +1,280 @@
|
||||
from argparse import ArgumentDefaultsHelpFormatter
|
||||
from argparse import ArgumentParser
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
import torch.optim as optim
|
||||
|
||||
from torch.utils import data
|
||||
from torch.utils.data.dataloader import DataLoader
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = ArgumentParser(
|
||||
formatter_class=ArgumentDefaultsHelpFormatter, conflict_handler="resolve"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output", default="node.emb", help="Output representation file"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--workers", default=8, type=int, help="Number of parallel processes."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--weighted", action="store_true", default=False, help="Treat graph as weighted"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--epochs", default=400, type=int, help="The training epochs of SDNE"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dropout",
|
||||
default=0.05,
|
||||
type=float,
|
||||
help="Dropout rate (1 - keep probability)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--weight-decay",
|
||||
type=float,
|
||||
default=5e-4,
|
||||
help="Weight for L2 loss on embedding matrix",
|
||||
)
|
||||
parser.add_argument("--lr", default=0.006, type=float, help="learning rate")
|
||||
parser.add_argument(
|
||||
"--alpha", default=1e-2, type=float, help="alhpa is a hyperparameter in SDNE"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--beta", default=5.0, type=float, help="beta is a hyperparameter in SDNE"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--nu1", default=1e-5, type=float, help="nu1 is a hyperparameter in SDNE"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--nu2", default=1e-4, type=float, help="nu2 is a hyperparameter in SDNE"
|
||||
)
|
||||
parser.add_argument("--bs", default=100, type=int, help="batch size of SDNE")
|
||||
parser.add_argument("--nhid0", default=1000, type=int, help="The first dim")
|
||||
parser.add_argument("--nhid1", default=128, type=int, help="The second dim")
|
||||
parser.add_argument(
|
||||
"--step_size", default=10, type=int, help="The step size for lr"
|
||||
)
|
||||
parser.add_argument("--gamma", default=0.9, type=int, help="The gamma for lr")
|
||||
args = parser.parse_args()
|
||||
|
||||
return args
|
||||
|
||||
|
||||
class Dataload(data.Dataset):
|
||||
def __init__(self, Adj, Node):
|
||||
self.Adj = Adj
|
||||
self.Node = Node
|
||||
|
||||
def __getitem__(self, index):
|
||||
return index
|
||||
# adj_batch = self.Adj[index]
|
||||
# adj_mat = adj_batch[index]
|
||||
# b_mat = torch.ones_like(adj_batch)
|
||||
# b_mat[adj_batch != 0] = self.Beta
|
||||
# return adj_batch, adj_mat, b_mat
|
||||
|
||||
def __len__(self):
|
||||
return self.Node
|
||||
|
||||
|
||||
def get_adj(g):
|
||||
edges = list(g.edges)
|
||||
edges = [(edges[i][0], edges[i][1]) for i in range(len(edges))]
|
||||
# print(edges)
|
||||
edges = np.array([np.array(i) for i in edges])
|
||||
min_node, max_node = edges.min(), edges.max()
|
||||
if min_node == 0:
|
||||
Node = max_node + 1
|
||||
else:
|
||||
Node = max_node
|
||||
|
||||
Adj = np.zeros([Node, Node], dtype=int)
|
||||
for i in range(edges.shape[0]):
|
||||
g.add_edge(edges[i][0], edges[i][1])
|
||||
if min_node == 0:
|
||||
Adj[edges[i][0], edges[i][1]] = 1
|
||||
Adj[edges[i][1], edges[i][0]] = 1
|
||||
|
||||
else:
|
||||
Adj[edges[i][0] - 1, edges[i][1] - 1] = 1
|
||||
Adj[edges[i][1] - 1, edges[i][0] - 1] = 1
|
||||
Adj = torch.FloatTensor(Adj)
|
||||
return Adj, Node
|
||||
|
||||
|
||||
class SDNE(nn.Module):
|
||||
"""
|
||||
Graph embedding via SDNE.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
graph : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
node: Size of nodes
|
||||
|
||||
nhid0, nhid1: Two dimensions of two hiddenlayers, default: 128, 64
|
||||
|
||||
dropout: One parameter for regularization, default: 0.025
|
||||
|
||||
alpha, beta: Twe parameters
|
||||
graph=g: : easygraph.Graph or easygraph.DiGraph
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> import easygraph as eg
|
||||
>>> model = eg.SDNE(graph=g, node_size= len(g.nodes), nhid0=128, nhid1=64, dropout=0.025, alpha=2e-2, beta=10)
|
||||
>>> emb = model.train(model, epochs, lr, bs, step_size, gamma, nu1, nu2, device, output)
|
||||
|
||||
|
||||
epochs, "--epochs", default=400, type=int, help="The training epochs of SDNE"
|
||||
|
||||
alpha, "--alpha", default=2e-2, type=float, help="alhpa is a hyperparameter in SDNE"
|
||||
|
||||
beta, "--beta", default=10.0, type=float, help="beta is a hyperparameter in SDNE"
|
||||
|
||||
lr, "--lr", default=0.006, type=float, help="learning rate"
|
||||
|
||||
bs, "--bs", default=100, type=int, help="batch size of SDNE"
|
||||
|
||||
step_size, "--step_size", default=10, type=int, help="The step size for lr"
|
||||
|
||||
gamma, # "--gamma", default=0.9, type=int, help="The gamma for lr"
|
||||
|
||||
step_size, "--step_size", default=10, type=int, help="The step size for lr"
|
||||
|
||||
nu1, # "--nu1", default=1e-5, type=float, help="nu1 is a hyperparameter in SDNE"
|
||||
|
||||
nu2, "--nu2", default=1e-4, type=float, help="nu2 is a hyperparameter in SDNE"
|
||||
|
||||
device, "-- device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") "
|
||||
|
||||
output "--output", default="node.emb", help="Output representation file"
|
||||
|
||||
|
||||
Reference
|
||||
----------
|
||||
.. [1] Wang, D., Cui, P., & Zhu, W. (2016, August). Structural deep network embedding. In Proceedings of the 22nd ACM SIGKDD international conference on Knowledge discovery and data mining (pp. 1225-1234).
|
||||
|
||||
https://www.kdd.org/kdd2016/papers/files/rfp0191-wangAemb.pdf
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, graph, node_size, nhid0, nhid1, dropout=0.06, alpha=2e-2, beta=10.0
|
||||
):
|
||||
super(SDNE, self).__init__()
|
||||
self.encode0 = nn.Linear(node_size, nhid0)
|
||||
self.encode1 = nn.Linear(nhid0, nhid1)
|
||||
self.decode0 = nn.Linear(nhid1, nhid0)
|
||||
self.decode1 = nn.Linear(nhid0, node_size)
|
||||
self.droput = dropout
|
||||
self.alpha = alpha
|
||||
self.beta = beta
|
||||
self.graph = graph
|
||||
|
||||
def forward(self, adj_batch, adj_mat, b_mat):
|
||||
t0 = F.leaky_relu(self.encode0(adj_batch))
|
||||
t0 = F.leaky_relu(self.encode1(t0))
|
||||
embedding = t0
|
||||
t0 = F.leaky_relu(self.decode0(t0))
|
||||
t0 = F.leaky_relu(self.decode1(t0))
|
||||
embedding_norm = torch.sum(embedding * embedding, dim=1, keepdim=True)
|
||||
L_1st = torch.sum(
|
||||
adj_mat
|
||||
* (
|
||||
embedding_norm
|
||||
- 2 * torch.mm(embedding, torch.transpose(embedding, dim0=0, dim1=1))
|
||||
+ torch.transpose(embedding_norm, dim0=0, dim1=1)
|
||||
)
|
||||
)
|
||||
L_2nd = torch.sum(((adj_batch - t0) * b_mat) * ((adj_batch - t0) * b_mat))
|
||||
return L_1st, self.alpha * L_2nd, L_1st + self.alpha * L_2nd
|
||||
|
||||
def train(
|
||||
self,
|
||||
model,
|
||||
epochs=100,
|
||||
lr=0.006,
|
||||
bs=100,
|
||||
step_size=10,
|
||||
gamma=0.9,
|
||||
nu1=1e-5,
|
||||
nu2=1e-4,
|
||||
device="cpu",
|
||||
output="out.emb",
|
||||
):
|
||||
Adj, Node = get_adj(self.graph)
|
||||
model = model.to(device)
|
||||
|
||||
opt = optim.Adam(model.parameters(), lr=lr)
|
||||
scheduler = torch.optim.lr_scheduler.StepLR(
|
||||
opt, step_size=step_size, gamma=gamma
|
||||
)
|
||||
Data = Dataload(Adj, Node)
|
||||
Data = DataLoader(
|
||||
Data,
|
||||
batch_size=bs,
|
||||
shuffle=True,
|
||||
)
|
||||
|
||||
for epoch in range(1, epochs + 1):
|
||||
loss_sum, loss_L1, loss_L2, loss_reg = 0, 0, 0, 0
|
||||
for index in Data:
|
||||
adj_batch = Adj[index]
|
||||
adj_mat = adj_batch[:, index]
|
||||
b_mat = torch.ones_like(adj_batch)
|
||||
b_mat[adj_batch != 0] = self.beta
|
||||
|
||||
opt.zero_grad()
|
||||
L_1st, L_2nd, L_all = model(adj_batch, adj_mat, b_mat)
|
||||
L_reg = 0
|
||||
for param in model.parameters():
|
||||
L_reg += nu1 * torch.sum(torch.abs(param)) + nu2 * torch.sum(
|
||||
param * param
|
||||
)
|
||||
Loss = L_all + L_reg
|
||||
Loss.backward()
|
||||
opt.step()
|
||||
loss_sum += Loss
|
||||
loss_L1 += L_1st
|
||||
loss_L2 += L_2nd
|
||||
loss_reg += L_reg
|
||||
scheduler.step(epoch)
|
||||
# print("The lr for epoch %d is %f" %(epoch, scheduler.get_lr()[0]))
|
||||
print("loss for epoch %d is:" % epoch)
|
||||
print("loss_sum is %f" % loss_sum)
|
||||
print("loss_L1 is %f" % loss_L1)
|
||||
print("loss_L2 is %f" % loss_L2)
|
||||
print("loss_reg is %f" % loss_reg)
|
||||
|
||||
# model.eval()
|
||||
embedding = model.savector(Adj)
|
||||
outVec = embedding.detach().numpy()
|
||||
np.savetxt(output, outVec)
|
||||
|
||||
return outVec
|
||||
|
||||
def savector(self, adj):
|
||||
t0 = self.encode0(adj)
|
||||
t0 = self.encode1(t0)
|
||||
return t0
|
||||
|
||||
|
||||
# if __name__ == '__main__':
|
||||
# args = parse_args()
|
||||
# print(args)
|
||||
# dataset = eg.CiteseerGraphDataset(force_reload=True) # Download CiteseerGraphDataset contained in EasyGraph
|
||||
# num_classes = dataset.num_classes
|
||||
# g = dataset[0]
|
||||
# print(g)
|
||||
# device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
|
||||
# adj, node = get_adj(g)
|
||||
# # labels = g.ndata['label']
|
||||
# nhid0, nhid1, dropout, alpha = args.nhid0, args.nhid1, args.dropout, args.alpha
|
||||
# model = SDNE(node, nhid0, nhid1, dropout, alpha, graph=g)
|
||||
# print(model)
|
||||
#
|
||||
# emb = model.train(args, device)
|
||||
@@ -0,0 +1,4 @@
|
||||
-1.765814423561096191e-01 2.083084881305694580e-01 -1.271556913852691650e-01 -1.702362895011901855e-01 8.119292855262756348e-01 -3.134809732437133789e-01 -9.992567449808120728e-02 -1.093881502747535706e-01
|
||||
-2.064122706651687622e-01 -1.475724577903747559e-01 -1.439859867095947266e-01 -7.331190109252929688e-01 6.787545084953308105e-01 -3.651908636093139648e-01 -9.232180565595626831e-02 -8.407155275344848633e-01
|
||||
-1.765814423561096191e-01 2.083084881305694580e-01 -1.271556913852691650e-01 -1.702362895011901855e-01 8.119292855262756348e-01 -3.134809732437133789e-01 -9.992567449808120728e-02 -1.093881502747535706e-01
|
||||
-2.064122706651687622e-01 -1.475724577903747559e-01 -1.439859867095947266e-01 -7.331190109252929688e-01 6.787545084953308105e-01 -3.651908636093139648e-01 -9.232180565595626831e-02 -8.407155275344848633e-01
|
||||
@@ -0,0 +1,101 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
|
||||
|
||||
class Test_Deepwalk(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.ds = eg.datasets.get_graph_karateclub()
|
||||
self.edges = [(1, 4), (2, 4)]
|
||||
self.test_graphs = []
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
self.shs = eg.common_greedy(self.ds, int(len(self.ds.nodes) / 3))
|
||||
|
||||
self.graph = eg.Graph()
|
||||
self.graph.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)])
|
||||
|
||||
self.empty_graph = eg.Graph()
|
||||
|
||||
self.single_node_graph = eg.Graph()
|
||||
self.single_node_graph.add_node(0)
|
||||
|
||||
def test_deepwalk(self):
|
||||
for i in self.test_graphs:
|
||||
print(eg.deepwalk(i))
|
||||
|
||||
def test_deepwalk_output_structure(self):
|
||||
emb, sim = eg.deepwalk(
|
||||
self.graph,
|
||||
dimensions=16,
|
||||
walk_length=5,
|
||||
num_walks=3,
|
||||
window=2,
|
||||
min_count=1,
|
||||
batch_words=4,
|
||||
epochs=5,
|
||||
)
|
||||
self.assertIsInstance(emb, dict)
|
||||
self.assertIsInstance(sim, dict)
|
||||
for k, v in emb.items():
|
||||
self.assertEqual(len(v), 16)
|
||||
self.assertTrue(isinstance(v, np.ndarray))
|
||||
|
||||
def test_deepwalk_similarity_keys_match_nodes(self):
|
||||
emb, sim = eg.deepwalk(
|
||||
self.graph,
|
||||
dimensions=8,
|
||||
walk_length=3,
|
||||
num_walks=2,
|
||||
window=2,
|
||||
min_count=1,
|
||||
batch_words=2,
|
||||
epochs=3,
|
||||
)
|
||||
self.assertEqual(set(emb.keys()), set(sim.keys()))
|
||||
self.assertEqual(set(emb.keys()), set(self.graph.nodes))
|
||||
|
||||
def test_deepwalk_on_single_node(self):
|
||||
emb, sim = eg.deepwalk(
|
||||
self.single_node_graph,
|
||||
dimensions=4,
|
||||
walk_length=2,
|
||||
num_walks=1,
|
||||
window=1,
|
||||
min_count=1,
|
||||
batch_words=2,
|
||||
epochs=2,
|
||||
)
|
||||
self.assertEqual(len(emb), 1)
|
||||
self.assertEqual(list(emb.keys()), [0])
|
||||
self.assertEqual(len(emb[0]), 4)
|
||||
|
||||
def test_deepwalk_on_empty_graph(self):
|
||||
with self.assertRaises(RuntimeError):
|
||||
eg.deepwalk(
|
||||
self.empty_graph,
|
||||
dimensions=4,
|
||||
walk_length=2,
|
||||
num_walks=1,
|
||||
window=1,
|
||||
min_count=1,
|
||||
batch_words=2,
|
||||
epochs=2,
|
||||
)
|
||||
|
||||
def test_deepwalk_walk_length_zero(self):
|
||||
emb, sim = eg.deepwalk(
|
||||
self.graph,
|
||||
dimensions=4,
|
||||
walk_length=0,
|
||||
num_walks=2,
|
||||
window=1,
|
||||
min_count=1,
|
||||
batch_words=2,
|
||||
epochs=2,
|
||||
)
|
||||
self.assertEqual(len(emb), len(self.graph.nodes))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,77 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
|
||||
|
||||
class Test_LINE(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.edges = [(0, 1), (1, 2), (2, 3), (3, 4)]
|
||||
self.graph = eg.Graph()
|
||||
self.graph.add_edges_from(self.edges)
|
||||
|
||||
def test_output_is_dict_with_correct_dim(self):
|
||||
model = eg.functions.graph_embedding.LINE(
|
||||
dimension=16, walk_length=10, walk_num=5, order=1
|
||||
)
|
||||
emb = model(self.graph, return_dict=True)
|
||||
self.assertIsInstance(emb, dict)
|
||||
for v in emb.values():
|
||||
self.assertEqual(len(v), 16)
|
||||
|
||||
def test_output_as_matrix(self):
|
||||
model = eg.functions.graph_embedding.LINE(
|
||||
dimension=8, walk_length=5, walk_num=3, order=1
|
||||
)
|
||||
emb = model(self.graph, return_dict=False)
|
||||
self.assertEqual(emb.shape, (len(self.graph.nodes), 8))
|
||||
|
||||
def test_output_with_order_2(self):
|
||||
model = eg.functions.graph_embedding.LINE(
|
||||
dimension=16, walk_length=10, walk_num=5, order=2
|
||||
)
|
||||
emb = model(self.graph)
|
||||
for vec in emb.values():
|
||||
self.assertEqual(len(vec), 16)
|
||||
|
||||
def test_output_with_order_3_combination(self):
|
||||
model = eg.functions.graph_embedding.LINE(
|
||||
dimension=16, walk_length=10, walk_num=5, order=3
|
||||
)
|
||||
emb = model(self.graph)
|
||||
for vec in emb.values():
|
||||
self.assertEqual(len(vec), 16)
|
||||
|
||||
def test_directed_graph(self):
|
||||
g = eg.DiGraph()
|
||||
g.add_edges_from(self.edges)
|
||||
model = eg.functions.graph_embedding.LINE(
|
||||
dimension=8, walk_length=5, walk_num=3, order=1
|
||||
)
|
||||
emb = model(g)
|
||||
self.assertEqual(len(emb), len(g.nodes))
|
||||
|
||||
def test_empty_graph_raises(self):
|
||||
g = eg.Graph()
|
||||
model = eg.functions.graph_embedding.LINE(
|
||||
dimension=8, walk_length=5, walk_num=3, order=1
|
||||
)
|
||||
with self.assertRaises(Exception):
|
||||
_ = model(g)
|
||||
|
||||
def test_embeddings_are_normalized(self):
|
||||
model = eg.functions.graph_embedding.LINE(
|
||||
dimension=16, walk_length=10, walk_num=5, order=1
|
||||
)
|
||||
emb = model(self.graph)
|
||||
for vec in emb.values():
|
||||
norm = np.linalg.norm(vec)
|
||||
self.assertTrue(np.isclose(norm, 1.0, atol=1e-5))
|
||||
|
||||
def test_embedding_value_finiteness(self):
|
||||
model = eg.functions.graph_embedding.LINE(
|
||||
dimension=16, walk_length=10, walk_num=5, order=1
|
||||
)
|
||||
emb = model(self.graph)
|
||||
for vec in emb.values():
|
||||
self.assertTrue(np.all(np.isfinite(vec)))
|
||||
@@ -0,0 +1,57 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
import easygraph.functions.graph_embedding as fn
|
||||
import numpy as np
|
||||
|
||||
|
||||
class Test_Nobe(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.ds = eg.datasets.get_graph_karateclub()
|
||||
self.edges = [(1, 4), (2, 4), (4, 1), (0, 4), (4, 3)]
|
||||
self.test_directed_graphs = [eg.DiGraph()]
|
||||
self.test_undirected_graphs = [eg.Graph(self.edges)]
|
||||
self.test_directed_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
self.shs = eg.common_greedy(self.ds, int(len(self.ds.nodes) / 3))
|
||||
|
||||
self.valid_graph = eg.Graph([(0, 1), (1, 2), (2, 0), (2, 3), (3, 4)])
|
||||
self.directed_graph = eg.DiGraph([(0, 1), (1, 2)])
|
||||
self.graph_with_isolated = eg.Graph()
|
||||
self.graph_with_isolated.add_edges_from([(0, 1), (1, 2)])
|
||||
self.graph_with_isolated.add_node(3)
|
||||
self.graph_with_isolated.add_node(4)
|
||||
|
||||
def test_NOBE(self):
|
||||
fn.NOBE(self.test_undirected_graphs[0], 1)
|
||||
|
||||
def test_NOBE_GA(self):
|
||||
"""
|
||||
for i in self.test_graphs:
|
||||
eg.functions.NOBE_GA(i, K=1)
|
||||
print(i)
|
||||
"""
|
||||
fn.NOBE_GA(self.test_directed_graphs[1], 1)
|
||||
|
||||
def test_nobe_output_shape(self):
|
||||
emb = fn.NOBE(self.valid_graph, K=2)
|
||||
self.assertIsInstance(emb, np.ndarray)
|
||||
self.assertEqual(emb.shape[1], 2)
|
||||
|
||||
def test_nobe_ga_output_shape(self):
|
||||
undirected_graph = eg.Graph([(0, 1), (1, 2), (2, 3)])
|
||||
emb = fn.NOBE_GA(undirected_graph, K=2)
|
||||
self.assertIsInstance(emb, np.ndarray)
|
||||
self.assertEqual(emb.shape[1], 2)
|
||||
|
||||
def test_nobe_on_graph_with_isolated_nodes(self):
|
||||
emb = fn.NOBE(self.graph_with_isolated, K=2)
|
||||
self.assertEqual(emb.shape[0], len(self.graph_with_isolated))
|
||||
|
||||
def test_nobe_invalid_K_zero(self):
|
||||
emb = fn.NOBE(self.valid_graph, 0)
|
||||
self.assertIsInstance(emb, np.ndarray)
|
||||
self.assertEqual(emb.shape, (len(self.valid_graph), 0))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,58 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
|
||||
from easygraph.functions.graph_embedding.NOBE import NOBE
|
||||
from easygraph.functions.graph_embedding.NOBE import NOBE_GA
|
||||
|
||||
|
||||
class Test_Nobe(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.ds = eg.datasets.get_graph_karateclub()
|
||||
self.edges = [(1, 4), (2, 4), (4, 1), (0, 4)]
|
||||
self.test_graphs = [eg.classes.DiGraph(self.edges)]
|
||||
self.test_undirected_graphs = [eg.classes.Graph(self.edges)]
|
||||
self.shs = eg.common_greedy(self.ds, int(len(self.ds.nodes) / 3))
|
||||
|
||||
self.valid_graph = eg.Graph([(0, 1), (1, 2), (2, 3)])
|
||||
self.directed_graph = eg.DiGraph([(0, 1), (1, 2)])
|
||||
self.graph_with_isolated = eg.Graph([(0, 1), (1, 2)])
|
||||
self.graph_with_isolated.add_node(5) # isolated node
|
||||
|
||||
#
|
||||
def test_NOBE(self):
|
||||
for i in self.test_graphs:
|
||||
NOBE(i, K=1)
|
||||
|
||||
def test_NOBE_GA(self):
|
||||
for i in self.test_undirected_graphs:
|
||||
NOBE_GA(i, K=1)
|
||||
|
||||
def test_nobe_embedding_shape(self):
|
||||
emb = NOBE(self.valid_graph, K=2)
|
||||
self.assertIsInstance(emb, np.ndarray)
|
||||
self.assertEqual(emb.shape, (len(self.valid_graph.nodes), 2))
|
||||
|
||||
def test_nobe_ga_embedding_shape(self):
|
||||
emb = NOBE_GA(self.valid_graph, K=2)
|
||||
self.assertIsInstance(emb, np.ndarray)
|
||||
self.assertEqual(emb.shape, (len(self.valid_graph.nodes), 2))
|
||||
|
||||
def test_nobe_invalid_k_zero(self):
|
||||
emb = NOBE(self.valid_graph, 0)
|
||||
self.assertIsInstance(emb, np.ndarray)
|
||||
self.assertEqual(emb.shape, (len(self.valid_graph), 0))
|
||||
|
||||
def test_nobe_ga_invalid_k_zero(self):
|
||||
emb = NOBE_GA(self.valid_graph, 0)
|
||||
self.assertIsInstance(emb, np.ndarray)
|
||||
self.assertEqual(emb.shape, (len(self.valid_graph), 0))
|
||||
|
||||
def test_nobe_with_isolated_node(self):
|
||||
emb = NOBE(self.graph_with_isolated, K=2)
|
||||
self.assertEqual(emb.shape[0], len(self.graph_with_isolated))
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# unittest.main()
|
||||
@@ -0,0 +1,107 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
|
||||
class Test_Sdne(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.ds = eg.datasets.get_graph_karateclub()
|
||||
self.edges = [
|
||||
(1, 4),
|
||||
(2, 4),
|
||||
(4, 1),
|
||||
(0, 4),
|
||||
(4, 3),
|
||||
]
|
||||
self.test_graphs = []
|
||||
self.test_graphs.append(eg.classes.DiGraph(self.edges))
|
||||
self.shs = eg.common_greedy(self.ds, int(len(self.ds.nodes) / 3))
|
||||
self.graph = eg.DiGraph()
|
||||
self.graph.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)])
|
||||
self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
|
||||
|
||||
def test_sdne(self):
|
||||
sdne = eg.SDNE(
|
||||
graph=self.test_graphs[0],
|
||||
node_size=len(self.test_graphs[0].nodes),
|
||||
nhid0=128,
|
||||
nhid1=64,
|
||||
dropout=0.025,
|
||||
alpha=2e-2,
|
||||
beta=10,
|
||||
)
|
||||
# todo add test
|
||||
# emb = sdne.train(sdne)
|
||||
|
||||
def test_sdne_model_instantiation(self):
|
||||
model = eg.SDNE(
|
||||
graph=self.graph,
|
||||
node_size=len(self.graph.nodes),
|
||||
nhid0=32,
|
||||
nhid1=16,
|
||||
dropout=0.05,
|
||||
alpha=0.01,
|
||||
beta=5.0,
|
||||
)
|
||||
self.assertIsInstance(model, eg.SDNE)
|
||||
|
||||
def test_sdne_training_embedding_output(self):
|
||||
model = eg.SDNE(
|
||||
graph=self.graph,
|
||||
node_size=len(self.graph.nodes),
|
||||
nhid0=16,
|
||||
nhid1=8,
|
||||
dropout=0.05,
|
||||
alpha=0.01,
|
||||
beta=5.0,
|
||||
)
|
||||
embedding = model.train(
|
||||
model=model,
|
||||
epochs=5,
|
||||
lr=0.01,
|
||||
bs=2,
|
||||
step_size=2,
|
||||
gamma=0.9,
|
||||
nu1=1e-5,
|
||||
nu2=1e-4,
|
||||
device=self.device,
|
||||
output="test.emb",
|
||||
)
|
||||
self.assertIsInstance(embedding, np.ndarray)
|
||||
self.assertEqual(embedding.shape, (len(self.graph.nodes), 8))
|
||||
|
||||
def test_savector_output_shape(self):
|
||||
adj, _ = eg.get_adj(self.graph)
|
||||
model = eg.SDNE(
|
||||
graph=self.graph,
|
||||
node_size=len(self.graph.nodes),
|
||||
nhid0=16,
|
||||
nhid1=8,
|
||||
dropout=0.05,
|
||||
alpha=0.01,
|
||||
beta=5.0,
|
||||
)
|
||||
with torch.no_grad():
|
||||
emb = model.savector(adj)
|
||||
self.assertEqual(emb.shape, (len(self.graph.nodes), 8))
|
||||
|
||||
def test_get_adj_shape_and_symmetry(self):
|
||||
adj, node_count = eg.get_adj(self.graph)
|
||||
self.assertEqual(adj.shape[0], node_count)
|
||||
self.assertTrue(torch.equal(adj, adj.T)) # check symmetry for undirected
|
||||
|
||||
def test_training_on_empty_graph(self):
|
||||
empty_graph = eg.Graph()
|
||||
model = eg.SDNE(
|
||||
graph=empty_graph,
|
||||
node_size=0,
|
||||
nhid0=8,
|
||||
nhid1=4,
|
||||
dropout=0.05,
|
||||
alpha=0.01,
|
||||
beta=5.0,
|
||||
)
|
||||
with self.assertRaises(ValueError):
|
||||
model.train(model=model, epochs=5, device=self.device)
|
||||
@@ -0,0 +1,411 @@
|
||||
import math
|
||||
import random
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
from easygraph.classes.graph import Graph
|
||||
|
||||
|
||||
__all__ = [
|
||||
"erdos_renyi_M",
|
||||
"erdos_renyi_P",
|
||||
"fast_erdos_renyi_P",
|
||||
"WS_Random",
|
||||
"graph_Gnm",
|
||||
]
|
||||
|
||||
|
||||
def erdos_renyi_M(n, edge, directed=False, FilePath=None):
|
||||
"""Given the number of nodes and the number of edges, return an Erdős-Rényi random graph, and store the graph in a document.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n : int
|
||||
The number of nodes.
|
||||
edge : int
|
||||
The number of edges.
|
||||
directed : bool, optional (default=False)
|
||||
If True, this function returns a directed graph.
|
||||
FilePath : string
|
||||
The file for storing the output graph G.
|
||||
|
||||
Returns
|
||||
-------
|
||||
G : graph
|
||||
an Erdős-Rényi random graph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Returns an Erdős-Rényi random graph G.
|
||||
|
||||
>>> erdos_renyi_M(100,180,directed=False,FilePath="/users/fudanmsn/downloads/RandomNetwork.txt")
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] P. Erdős and A. Rényi, On Random Graphs, Publ. Math. 6, 290 (1959).
|
||||
.. [2] E. N. Gilbert, Random Graphs, Ann. Math. Stat., 30, 1141 (1959).
|
||||
"""
|
||||
if directed:
|
||||
G = eg.DiGraph()
|
||||
adjacent = {}
|
||||
mmax = n * (n - 1)
|
||||
if edge >= mmax:
|
||||
for i in range(n):
|
||||
for j in range(n):
|
||||
if i != j:
|
||||
G.add_edge(i, j)
|
||||
if i not in adjacent:
|
||||
adjacent[i] = []
|
||||
adjacent[i].append(j)
|
||||
else:
|
||||
adjacent[i].append(j)
|
||||
return G
|
||||
count = 0
|
||||
while count < edge:
|
||||
i = random.randint(0, n - 1)
|
||||
j = random.randint(0, n - 1)
|
||||
if i == j or G.has_edge(i, j):
|
||||
continue
|
||||
else:
|
||||
count = count + 1
|
||||
if i not in adjacent:
|
||||
adjacent[i] = []
|
||||
adjacent[i].append(j)
|
||||
else:
|
||||
adjacent[i].append(j)
|
||||
G.add_edge(i, j)
|
||||
else:
|
||||
G = eg.Graph()
|
||||
adjacent = {}
|
||||
mmax = n * (n - 1) / 2
|
||||
if edge >= mmax:
|
||||
for i in range(n):
|
||||
for j in range(n):
|
||||
if i != j:
|
||||
G.add_edge(i, j)
|
||||
if i not in adjacent:
|
||||
adjacent[i] = []
|
||||
adjacent[i].append(j)
|
||||
else:
|
||||
adjacent[i].append(j)
|
||||
if j not in adjacent:
|
||||
adjacent[j] = []
|
||||
adjacent[j].append(i)
|
||||
else:
|
||||
adjacent[j].append(i)
|
||||
return G
|
||||
count = 0
|
||||
while count < edge:
|
||||
i = random.randint(0, n - 1)
|
||||
j = random.randint(0, n - 1)
|
||||
if i == j or G.has_edge(i, j):
|
||||
continue
|
||||
else:
|
||||
count = count + 1
|
||||
if i not in adjacent:
|
||||
adjacent[i] = []
|
||||
adjacent[i].append(j)
|
||||
else:
|
||||
adjacent[i].append(j)
|
||||
if j not in adjacent:
|
||||
adjacent[j] = []
|
||||
adjacent[j].append(i)
|
||||
else:
|
||||
adjacent[j].append(i)
|
||||
G.add_edge(i, j)
|
||||
|
||||
writeRandomNetworkToFile(n, adjacent, FilePath)
|
||||
return G
|
||||
|
||||
|
||||
def erdos_renyi_P(n, p, directed=False, FilePath=None):
|
||||
"""Given the number of nodes and the probability of edge creation, return an Erdős-Rényi random graph, and store the graph in a document.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n : int
|
||||
The number of nodes.
|
||||
p : float
|
||||
Probability for edge creation.
|
||||
directed : bool, optional (default=False)
|
||||
If True, this function returns a directed graph.
|
||||
FilePath : string
|
||||
The file for storing the output graph G.
|
||||
|
||||
Returns
|
||||
-------
|
||||
G : graph
|
||||
an Erdős-Rényi random graph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Returns an Erdős-Rényi random graph G
|
||||
|
||||
>>> erdos_renyi_P(100,0.5,directed=False,FilePath="/users/fudanmsn/downloads/RandomNetwork.txt")
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] P. Erdős and A. Rényi, On Random Graphs, Publ. Math. 6, 290 (1959).
|
||||
.. [2] E. N. Gilbert, Random Graphs, Ann. Math. Stat., 30, 1141 (1959).
|
||||
"""
|
||||
if directed:
|
||||
G = eg.DiGraph()
|
||||
adjacent = {}
|
||||
probability = 0.0
|
||||
for i in range(n):
|
||||
for j in range(i + 1, n):
|
||||
probability = random.random()
|
||||
if probability < p:
|
||||
if i not in adjacent:
|
||||
adjacent[i] = []
|
||||
adjacent[i].append(j)
|
||||
else:
|
||||
adjacent[i].append(j)
|
||||
G.add_edge(i, j)
|
||||
else:
|
||||
G = eg.Graph()
|
||||
adjacent = {}
|
||||
probability = 0.0
|
||||
for i in range(n):
|
||||
for j in range(i + 1, n):
|
||||
probability = random.random()
|
||||
if probability < p:
|
||||
if i not in adjacent:
|
||||
adjacent[i] = []
|
||||
adjacent[i].append(j)
|
||||
else:
|
||||
adjacent[i].append(j)
|
||||
if j not in adjacent:
|
||||
adjacent[j] = []
|
||||
adjacent[j].append(i)
|
||||
else:
|
||||
adjacent[j].append(i)
|
||||
G.add_edge(i, j)
|
||||
|
||||
writeRandomNetworkToFile(n, adjacent, FilePath)
|
||||
return G
|
||||
|
||||
|
||||
def fast_erdos_renyi_P(n, p, directed=False, FilePath=None):
|
||||
"""Given the number of nodes and the probability of edge creation, return an Erdős-Rényi random graph, and store the graph in a document. Use this function for generating a huge scale graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n : int
|
||||
The number of nodes.
|
||||
p : float
|
||||
Probability for edge creation.
|
||||
directed : bool, optional (default=False)
|
||||
If True, this function returns a directed graph.
|
||||
FilePath : string
|
||||
The file for storing the output graph G.
|
||||
|
||||
Returns
|
||||
-------
|
||||
G : graph
|
||||
an Erdős-Rényi random graph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Returns an Erdős-Rényi random graph G
|
||||
|
||||
>>> erdos_renyi_P(100,0.5,directed=False,FilePath="/users/fudanmsn/downloads/RandomNetwork.txt")
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] P. Erdős and A. Rényi, On Random Graphs, Publ. Math. 6, 290 (1959).
|
||||
.. [2] E. N. Gilbert, Random Graphs, Ann. Math. Stat., 30, 1141 (1959).
|
||||
"""
|
||||
if directed:
|
||||
G = eg.DiGraph()
|
||||
w = -1
|
||||
lp = math.log(1.0 - p)
|
||||
v = 0
|
||||
adjacent = {}
|
||||
while v < n:
|
||||
lr = math.log(1.0 - random.random())
|
||||
w = w + 1 + int(lr / lp)
|
||||
if v == w: # avoid self loops
|
||||
w = w + 1
|
||||
while v < n <= w:
|
||||
w = w - n
|
||||
v = v + 1
|
||||
if v == w: # avoid self loops
|
||||
w = w + 1
|
||||
if v < n:
|
||||
G.add_edge(v, w)
|
||||
if v not in adjacent:
|
||||
adjacent[v] = []
|
||||
adjacent[v].append(w)
|
||||
else:
|
||||
adjacent[v].append(w)
|
||||
else:
|
||||
G = eg.Graph()
|
||||
w = -1
|
||||
lp = math.log(1.0 - p)
|
||||
v = 1
|
||||
adjacent = {}
|
||||
while v < n:
|
||||
lr = math.log(1.0 - random.random())
|
||||
w = w + 1 + int(lr / lp)
|
||||
while w >= v and v < n:
|
||||
w = w - v
|
||||
v = v + 1
|
||||
if v < n:
|
||||
G.add_edge(v, w)
|
||||
if v not in adjacent:
|
||||
adjacent[v] = []
|
||||
adjacent[v].append(w)
|
||||
else:
|
||||
adjacent[v].append(w)
|
||||
if w not in adjacent:
|
||||
adjacent[w] = []
|
||||
adjacent[w].append(v)
|
||||
else:
|
||||
adjacent[w].append(v)
|
||||
|
||||
writeRandomNetworkToFile(n, adjacent, FilePath)
|
||||
return G
|
||||
|
||||
|
||||
def WS_Random(n, k, p, FilePath=None):
|
||||
"""Returns a small-world graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n : int
|
||||
The number of nodes
|
||||
k : int
|
||||
Each node is joined with its `k` nearest neighbors in a ring
|
||||
topology.
|
||||
p : float
|
||||
The probability of rewiring each edge
|
||||
FilePath : string
|
||||
The file for storing the output graph G
|
||||
|
||||
Returns
|
||||
-------
|
||||
G : graph
|
||||
a small-world graph
|
||||
|
||||
Examples
|
||||
--------
|
||||
Returns a small-world graph G
|
||||
|
||||
>>> WS_Random(100,10,0.3,"/users/fudanmsn/downloads/RandomNetwork.txt")
|
||||
|
||||
"""
|
||||
if k >= n:
|
||||
print("k>=n, choose smaller k or larger n")
|
||||
return
|
||||
adjacent = {}
|
||||
G = eg.Graph()
|
||||
NUM1 = n
|
||||
NUM2 = NUM1 - 1
|
||||
K = k
|
||||
K1 = K + 1
|
||||
N = list(range(NUM1))
|
||||
G.add_nodes(N)
|
||||
|
||||
for i in range(NUM1):
|
||||
for j in range(1, K1):
|
||||
K_add = NUM1 - K
|
||||
i_add_j = i + j + 1
|
||||
if i >= K_add and i_add_j > NUM1:
|
||||
i_add = i + j - NUM1
|
||||
G.add_edge(i, i_add)
|
||||
else:
|
||||
i_add = i + j
|
||||
G.add_edge(i, i_add)
|
||||
if i not in adjacent:
|
||||
adjacent[i] = []
|
||||
adjacent[i].append(i_add)
|
||||
else:
|
||||
adjacent[i].append(i_add)
|
||||
if i_add not in adjacent:
|
||||
adjacent[i_add] = []
|
||||
adjacent[i_add].append(i)
|
||||
else:
|
||||
adjacent[i_add].append(i)
|
||||
for i in range(NUM1):
|
||||
for e_del in range(i + 1, i + K1):
|
||||
if e_del >= NUM1:
|
||||
e_del = e_del - NUM1
|
||||
P_random = random.random()
|
||||
if P_random < p:
|
||||
G.remove_edge(i, e_del)
|
||||
adjacent[i].remove(e_del)
|
||||
if adjacent[i] == []:
|
||||
adjacent.pop(i)
|
||||
adjacent[e_del].remove(i)
|
||||
if adjacent[e_del] == []:
|
||||
adjacent.pop(e_del)
|
||||
e_add = random.randint(0, NUM2)
|
||||
while e_add == i or G.has_edge(i, e_add) == True:
|
||||
e_add = random.randint(0, NUM2)
|
||||
G.add_edge(i, e_add)
|
||||
if i not in adjacent:
|
||||
adjacent[i] = []
|
||||
adjacent[i].append(e_add)
|
||||
else:
|
||||
adjacent[i].append(e_add)
|
||||
if e_add not in adjacent:
|
||||
adjacent[e_add] = []
|
||||
adjacent[e_add].append(i)
|
||||
else:
|
||||
adjacent[e_add].append(i)
|
||||
writeRandomNetworkToFile(n, adjacent, FilePath)
|
||||
return G
|
||||
|
||||
|
||||
def writeRandomNetworkToFile(n, adjacent, FilePath):
|
||||
if FilePath != None:
|
||||
f = open(FilePath, "w+")
|
||||
else:
|
||||
f = open("RandomNetwork.txt", "w+")
|
||||
adjacent = sorted(adjacent.items(), key=lambda d: d[0])
|
||||
for i in adjacent:
|
||||
i[1].sort()
|
||||
for j in i[1]:
|
||||
f.write(str(i[0]))
|
||||
f.write(" ")
|
||||
f.write(str(j))
|
||||
f.write("\n")
|
||||
f.close()
|
||||
|
||||
|
||||
def graph_Gnm(num_v: int, num_e: int):
|
||||
r"""Return a random graph with ``num_v`` vertices and ``num_e`` edges. Edges are drawn uniformly from the set of possible edges.
|
||||
|
||||
Args:
|
||||
``num_v`` (``int``): The Number of vertices.
|
||||
``num_e`` (``int``): The Number of edges.
|
||||
|
||||
Examples:
|
||||
>>> import easygraph.randomhypergraph as rh
|
||||
>>> g = rh.graph_Gnm(4, 5)
|
||||
>>> g.e
|
||||
([(1, 2), (0, 3), (2, 3), (0, 2), (1, 3)], [1.0, 1.0, 1.0, 1.0, 1.0])
|
||||
"""
|
||||
assert num_v > 1, "num_v must be greater than 1"
|
||||
assert (
|
||||
num_e < num_v * (num_v - 1) // 2
|
||||
), "the specified num_e is larger than the possible number of edges"
|
||||
|
||||
v_list = list(range(num_v))
|
||||
cur_num_e, e_set = 0, set()
|
||||
while cur_num_e < num_e:
|
||||
v = random.choice(v_list)
|
||||
w = random.choice(v_list)
|
||||
if v > w:
|
||||
v, w = w, v
|
||||
if v == w or (v, w) in e_set:
|
||||
continue
|
||||
e_set.add((v, w))
|
||||
cur_num_e += 1
|
||||
g = Graph()
|
||||
g.add_nodes(list(range(0, num_v)))
|
||||
for ee in list(e_set):
|
||||
g.add_edge(ee[0], ee[1], weight=1.0)
|
||||
|
||||
return g
|
||||
@@ -0,0 +1,2 @@
|
||||
from .classic import *
|
||||
from .RandomNetwork import *
|
||||
@@ -0,0 +1,73 @@
|
||||
import itertools
|
||||
|
||||
from easygraph.classes.graph import Graph
|
||||
from easygraph.utils import nodes_or_number
|
||||
from easygraph.utils import pairwise
|
||||
|
||||
|
||||
__all__ = ["empty_graph", "path_graph", "complete_graph"]
|
||||
|
||||
|
||||
@nodes_or_number(0)
|
||||
def empty_graph(n=0, create_using=None, default=Graph):
|
||||
if create_using is None:
|
||||
G = default()
|
||||
elif hasattr(create_using, "_adj"):
|
||||
# create_using is a EasyGraph style Graph
|
||||
G = create_using
|
||||
else:
|
||||
# try create_using as constructor
|
||||
G = create_using()
|
||||
|
||||
n_name, nodes = n
|
||||
G.add_nodes_from(nodes)
|
||||
return G
|
||||
|
||||
|
||||
@nodes_or_number(0)
|
||||
def path_graph(n, create_using=None):
|
||||
n_name, nodes = n
|
||||
G = empty_graph(nodes, create_using)
|
||||
G.add_edges_from(pairwise(nodes))
|
||||
return G
|
||||
|
||||
|
||||
@nodes_or_number(0)
|
||||
def complete_graph(n, create_using=None):
|
||||
"""Return the complete graph `K_n` with n nodes.
|
||||
|
||||
A complete graph on `n` nodes means that all pairs
|
||||
of distinct nodes have an edge connecting them.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n : int or iterable container of nodes
|
||||
If n is an integer, nodes are from range(n).
|
||||
If n is a container of nodes, those nodes appear in the graph.
|
||||
create_using : EasyGraph graph constructor, optional (default=eg.Graph)
|
||||
Graph type to create. If graph instance, then cleared before populated.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.complete_graph(9)
|
||||
>>> len(G)
|
||||
9
|
||||
>>> G.size()
|
||||
36
|
||||
>>> G = eg.complete_graph(range(11, 14))
|
||||
>>> list(G.nodes())
|
||||
[11, 12, 13]
|
||||
>>> G = eg.complete_graph(4, eg.DiGraph())
|
||||
>>> G.is_directed()
|
||||
True
|
||||
|
||||
"""
|
||||
n_name, nodes = n
|
||||
G = empty_graph(n_name, create_using)
|
||||
if len(nodes) > 1:
|
||||
if G.is_directed():
|
||||
edges = itertools.permutations(nodes, 2)
|
||||
else:
|
||||
edges = itertools.combinations(nodes, 2)
|
||||
G.add_edges_from(edges)
|
||||
return G
|
||||
@@ -0,0 +1,63 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
class test_random_network(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G = eg.datasets.get_graph_karateclub()
|
||||
|
||||
def test_erdos_renyi_M(self):
|
||||
print(eg.erdos_renyi_M(8, 5).edges)
|
||||
|
||||
def test_erdos_renyi_P(self):
|
||||
print(eg.erdos_renyi_P(8, 0.2).nodes)
|
||||
|
||||
def test_fast_erdos_renyi_P(self):
|
||||
print(eg.fast_erdos_renyi_P(8, 0.2).nodes)
|
||||
|
||||
def test_WS_Random(self):
|
||||
print(eg.WS_Random(8, 1, 0.5).nodes)
|
||||
|
||||
def test_graph_Gnm(self):
|
||||
print(eg.graph_Gnm(8, 5).nodes)
|
||||
|
||||
def test_erdos_renyi_M_max_edges(self):
|
||||
n = 5
|
||||
max_edges = n * (n - 1) // 2
|
||||
G = eg.erdos_renyi_M(n, max_edges)
|
||||
self.assertEqual(len(G.edges), max_edges)
|
||||
|
||||
def test_erdos_renyi_P_extreme_p(self):
|
||||
G0 = eg.erdos_renyi_P(10, 0.0)
|
||||
G1 = eg.erdos_renyi_P(10, 1.0)
|
||||
self.assertEqual(len(G0.edges), 0)
|
||||
self.assertEqual(len(G1.edges), 45) # 10 * 9 / 2
|
||||
|
||||
def test_fast_erdos_renyi_P_large_p(self):
|
||||
G = eg.fast_erdos_renyi_P(10, 0.9)
|
||||
self.assertEqual(len(G.nodes), 10)
|
||||
|
||||
def test_WS_Random_structure(self):
|
||||
G = eg.WS_Random(10, 2, 0.1)
|
||||
self.assertEqual(len(G.nodes), 10)
|
||||
self.assertTrue(all(0 <= u < 10 and 0 <= v < 10 for u, v, *_ in G.edges))
|
||||
|
||||
def test_WS_Random_invalid_k(self):
|
||||
G = eg.WS_Random(5, 5, 0.1)
|
||||
self.assertIsNone(G)
|
||||
|
||||
def test_graph_Gnm_basic(self):
|
||||
G = eg.graph_Gnm(10, 15)
|
||||
self.assertEqual(len(G.nodes), 10)
|
||||
self.assertEqual(len(G.edges), 15)
|
||||
|
||||
def test_graph_Gnm_invalid_inputs(self):
|
||||
with self.assertRaises(AssertionError):
|
||||
eg.graph_Gnm(1, 1)
|
||||
with self.assertRaises(AssertionError):
|
||||
eg.graph_Gnm(5, 11) # 5*4/2 = 10 max
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,87 @@
|
||||
import unittest
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
class test_classic(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.G = eg.datasets.get_graph_karateclub()
|
||||
|
||||
def test_empty_graph(self):
|
||||
# print(eg.empty_graph(-1).nodes)
|
||||
print(eg.empty_graph(10).nodes)
|
||||
|
||||
def test_path_graph(self):
|
||||
eg.path_graph(10, eg.DiGraph)
|
||||
|
||||
def test_complete_graph(self):
|
||||
eg.complete_graph(10, eg.DiGraph)
|
||||
|
||||
def test_empty_graph_default(self):
|
||||
G = eg.empty_graph()
|
||||
self.assertEqual(len(G.nodes), 0)
|
||||
self.assertEqual(len(G.edges), 0)
|
||||
|
||||
def test_empty_graph_with_n(self):
|
||||
G = eg.empty_graph(5)
|
||||
self.assertEqual(set(G.nodes), set(range(5)))
|
||||
self.assertEqual(len(G.edges), 0)
|
||||
|
||||
def test_empty_graph_with_custom_nodes(self):
|
||||
G = eg.empty_graph(["a", "b", "c"])
|
||||
self.assertEqual(set(G.nodes), {"a", "b", "c"})
|
||||
self.assertEqual(len(G.edges), 0)
|
||||
|
||||
def test_empty_graph_with_existing_graph(self):
|
||||
existing = eg.Graph()
|
||||
existing.add_node(999)
|
||||
G = eg.empty_graph(3, create_using=existing)
|
||||
self.assertIn(0, G.nodes) # node 0 added
|
||||
self.assertEqual(len(G.nodes), 4) # 999 is retained
|
||||
self.assertEqual(len(G.edges), 0)
|
||||
|
||||
def test_path_graph_basic(self):
|
||||
G = eg.path_graph(4)
|
||||
self.assertEqual(len(G.nodes), 4)
|
||||
self.assertEqual(len(G.edges), 3)
|
||||
edges = {(u, v) for u, v, _ in G.edges}
|
||||
self.assertTrue((0, 1) in edges and (1, 2) in edges and (2, 3) in edges)
|
||||
|
||||
def test_path_graph_with_custom_nodes(self):
|
||||
G = eg.path_graph(["x", "y", "z"])
|
||||
self.assertEqual(len(G.nodes), 3)
|
||||
actual_edges = {(u, v) for u, v, _ in G.edges}
|
||||
expected_edges = {("x", "y"), ("y", "z")}
|
||||
self.assertEqual(actual_edges, expected_edges)
|
||||
|
||||
def test_complete_graph_basic(self):
|
||||
G = eg.complete_graph(4)
|
||||
self.assertEqual(len(G.nodes), 4)
|
||||
self.assertEqual(len(G.edges), 6) # n*(n-1)/2 for undirected
|
||||
|
||||
def test_complete_graph_directed(self):
|
||||
G = eg.complete_graph(3, create_using=eg.DiGraph())
|
||||
self.assertTrue(G.is_directed())
|
||||
self.assertEqual(len(G.nodes), 3)
|
||||
self.assertEqual(len(G.edges), 6) # n*(n-1) for directed
|
||||
|
||||
def test_complete_graph_custom_nodes(self):
|
||||
G = eg.complete_graph(["a", "b", "c"])
|
||||
self.assertEqual(set(G.nodes), {"a", "b", "c"})
|
||||
actual_edges = {(u, v) for u, v, _ in G.edges}
|
||||
expected_edges = {("a", "b"), ("a", "c"), ("b", "c")}
|
||||
self.assertEqual(actual_edges, expected_edges)
|
||||
|
||||
def test_complete_graph_one_node(self):
|
||||
G = eg.complete_graph(1)
|
||||
self.assertEqual(len(G.nodes), 1)
|
||||
self.assertEqual(len(G.edges), 0)
|
||||
|
||||
def test_complete_graph_zero_nodes(self):
|
||||
G = eg.complete_graph(0)
|
||||
self.assertEqual(len(G.nodes), 0)
|
||||
self.assertEqual(len(G.edges), 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,5 @@
|
||||
from .assortativity import *
|
||||
from .centrality import *
|
||||
from .hypergraph_clustering import *
|
||||
from .hypergraph_operation import *
|
||||
from .null_model import *
|
||||
@@ -0,0 +1,185 @@
|
||||
"""Algorithms for finding the degree assortativity of a hypergraph."""
|
||||
|
||||
import random
|
||||
|
||||
from itertools import combinations
|
||||
|
||||
import numpy
|
||||
import numpy as np
|
||||
|
||||
from easygraph.utils.exception import EasyGraphError
|
||||
|
||||
|
||||
__all__ = ["dynamical_assortativity", "degree_assortativity"]
|
||||
|
||||
|
||||
def dynamical_assortativity(H):
|
||||
"""Computes the dynamical assortativity of a uniform hypergraph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
H : eg.Hypergraph
|
||||
Hypergraph of interest
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
The dynamical assortativity
|
||||
|
||||
See Also
|
||||
--------
|
||||
degree_assortativity
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphError
|
||||
If the hypergraph is not uniform, or if there are no nodes
|
||||
or no edges
|
||||
|
||||
References
|
||||
----------
|
||||
Nicholas Landry and Juan G. Restrepo,
|
||||
Hypergraph assortativity: A dynamical systems perspective,
|
||||
Chaos 2022.
|
||||
DOI: 10.1063/5.0086905
|
||||
|
||||
"""
|
||||
if len(H.v) == 0:
|
||||
raise EasyGraphError("Hypergraph must contain nodes")
|
||||
elif len(H.e[0]) == 0:
|
||||
raise EasyGraphError("Hypergraph must contain edges!")
|
||||
|
||||
if not H.is_uniform():
|
||||
raise EasyGraphError("Hypergraph must be uniform!")
|
||||
|
||||
if 1 in H.unique_edge_sizes():
|
||||
raise EasyGraphError("No singleton edges!")
|
||||
|
||||
degs = H.deg_v
|
||||
k1 = sum(degs) / len(degs)
|
||||
k2 = np.mean(numpy.array(degs) ** 2)
|
||||
kk1 = np.mean(
|
||||
[degs[n1] * degs[n2] for e in H.e[0] for n1, n2 in combinations(e, 2)]
|
||||
)
|
||||
|
||||
return kk1 * k1**2 / k2**2 - 1
|
||||
|
||||
|
||||
def degree_assortativity(H, kind="uniform", exact=False, num_samples=1000):
|
||||
"""Computes the degree assortativity of a hypergraph
|
||||
|
||||
Parameters
|
||||
----------
|
||||
H : Hypergraph
|
||||
The hypergraph of interest
|
||||
kind : str, optional
|
||||
the type of degree assortativity. valid choices are
|
||||
"uniform", "top-2", and "top-bottom". By default, "uniform".
|
||||
exact : bool, optional
|
||||
whether to compute over all edges or sample randomly from the
|
||||
set of edges. By default, False.
|
||||
num_samples : int, optional
|
||||
if not exact, specify the number of samples for the computation.
|
||||
By default, 1000.
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
the degree assortativity
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphError
|
||||
If there are no nodes or no edges
|
||||
|
||||
See Also
|
||||
--------
|
||||
dynamical_assortativity
|
||||
|
||||
References
|
||||
----------
|
||||
Phil Chodrow,
|
||||
Configuration models of random hypergraphs,
|
||||
Journal of Complex Networks 2020.
|
||||
DOI: 10.1093/comnet/cnaa018
|
||||
"""
|
||||
|
||||
if len(H.v) == 0:
|
||||
raise EasyGraphError("Hypergraph must contain nodes")
|
||||
elif len(H.e[0]) == 0:
|
||||
raise EasyGraphError("Hypergraph must contain edges!")
|
||||
|
||||
degs = H.deg_v
|
||||
if exact:
|
||||
k1k2 = [_choose_degrees(e, degs, kind) for e in H.e[0] if len(e) > 1]
|
||||
else:
|
||||
edges = [e for e in H.e[0] if len(e) > 1]
|
||||
k1k2 = [
|
||||
_choose_degrees(random.choice(H.e[0]), degs, kind)
|
||||
for _ in range(num_samples)
|
||||
]
|
||||
|
||||
rho = np.corrcoef(np.array(k1k2).T)[0, 1]
|
||||
if np.isnan(rho):
|
||||
return 0
|
||||
return rho
|
||||
|
||||
|
||||
def _choose_degrees(e, k, kind="uniform"):
|
||||
"""Choose the degrees of two nodes in a hyperedge.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
e : iterable
|
||||
the members in a hyperedge
|
||||
k : dict
|
||||
the degrees where keys are node IDs and values are degrees
|
||||
kind : str, optional
|
||||
the type of degree assortativity, options are "uniform", "top-2",
|
||||
and "top-bottom". By default, "uniform".
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple
|
||||
two degrees selected from the edge
|
||||
|
||||
Raises
|
||||
------
|
||||
EasyGraphError
|
||||
if invalid assortativity function chosen
|
||||
|
||||
See Also
|
||||
--------
|
||||
degree_assortativity
|
||||
|
||||
References
|
||||
----------
|
||||
Phil Chodrow,
|
||||
Configuration models of random hypergraphs,
|
||||
Journal of Complex Networks 2020.
|
||||
DOI: 10.1093/comnet/cnaa018
|
||||
"""
|
||||
e = list(e)
|
||||
if len(e) > 1:
|
||||
if kind == "uniform":
|
||||
i = np.random.randint(len(e))
|
||||
j = i
|
||||
while i == j:
|
||||
j = np.random.randint(len(e))
|
||||
return (k[e[i]], k[e[j]])
|
||||
|
||||
elif kind == "top-2":
|
||||
degs = sorted([k[i] for i in e])[-2:]
|
||||
random.shuffle(degs)
|
||||
return degs
|
||||
|
||||
elif kind == "top-bottom":
|
||||
# this selects the largest and smallest degrees in one line
|
||||
degs = sorted([k[i] for i in e])[:: len(e) - 1]
|
||||
random.shuffle(degs)
|
||||
return degs
|
||||
|
||||
else:
|
||||
raise EasyGraphError("Invalid choice function!")
|
||||
else:
|
||||
raise EasyGraphError("Edge must have more than one member!")
|
||||
@@ -0,0 +1,5 @@
|
||||
from .cycle_ratio import *
|
||||
from .degree import *
|
||||
from .hypercoreness import *
|
||||
from .s_centrality import *
|
||||
from .vector_centrality import *
|
||||
@@ -0,0 +1,193 @@
|
||||
import copy
|
||||
import itertools
|
||||
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
__all__ = [
|
||||
"my_all_shortest_paths",
|
||||
"getandJudgeSimpleCircle",
|
||||
"getSmallestCycles",
|
||||
"StatisticsAndCalculateIndicators",
|
||||
"cycle_ratio_centrality",
|
||||
]
|
||||
|
||||
|
||||
def my_all_shortest_paths(G, source, target):
|
||||
pred = eg.predecessor(G, source)
|
||||
if target not in pred:
|
||||
raise eg.EasyGraphNoPath(
|
||||
f"Target {target} cannot be reached from given sources"
|
||||
)
|
||||
sources = {source}
|
||||
seen = {target}
|
||||
stack = [[target, 0]]
|
||||
top = 0
|
||||
while top >= 0:
|
||||
node, i = stack[top]
|
||||
if node in sources:
|
||||
yield [p for p, n in reversed(stack[: top + 1])]
|
||||
if len(pred[node]) > i:
|
||||
stack[top][1] = i + 1
|
||||
next = pred[node][i]
|
||||
if next in seen:
|
||||
continue
|
||||
else:
|
||||
seen.add(next)
|
||||
top += 1
|
||||
if top == len(stack):
|
||||
stack.append([next, 0])
|
||||
else:
|
||||
stack[top][:] = [next, 0]
|
||||
else:
|
||||
seen.discard(node)
|
||||
top -= 1
|
||||
|
||||
|
||||
def getandJudgeSimpleCircle(objectList, G): # 这里添加 G 作为参数
|
||||
numEdge = 0
|
||||
for eleArr in list(itertools.combinations(objectList, 2)):
|
||||
if G.has_edge(eleArr[0], eleArr[1]):
|
||||
numEdge += 1
|
||||
if numEdge != len(objectList):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def getSmallestCycles(G, NodeGirth, Coreness, DEF_IMPOSSLEN):
|
||||
NodeList = list(G.nodes)
|
||||
NodeList.sort()
|
||||
# setp 1
|
||||
curCyc = list()
|
||||
for ix in NodeList[:-2]: # v1
|
||||
if NodeGirth[ix] == 0:
|
||||
continue
|
||||
curCyc.append(ix)
|
||||
for jx in NodeList[NodeList.index(ix) + 1 : -1]: # v2
|
||||
if NodeGirth[jx] == 0:
|
||||
continue
|
||||
curCyc.append(jx)
|
||||
if G.has_edge(ix, jx):
|
||||
for kx in NodeList[NodeList.index(jx) + 1 :]: # v3
|
||||
if NodeGirth[kx] == 0:
|
||||
continue
|
||||
if G.has_edge(kx, ix):
|
||||
curCyc.append(kx)
|
||||
if G.has_edge(kx, jx):
|
||||
yield tuple(curCyc) # 这里改为 yield
|
||||
for i in curCyc:
|
||||
NodeGirth[i] = 3
|
||||
curCyc.pop()
|
||||
curCyc.pop()
|
||||
curCyc.pop()
|
||||
|
||||
# setp 2
|
||||
ResiNodeList = [] # Residual Node List
|
||||
for nod in NodeList:
|
||||
if NodeGirth[nod] == DEF_IMPOSSLEN:
|
||||
ResiNodeList.append(nod)
|
||||
if len(ResiNodeList) == 0:
|
||||
return
|
||||
else:
|
||||
visitedNodes = dict.fromkeys(ResiNodeList, set())
|
||||
for nod in ResiNodeList:
|
||||
if Coreness[nod] == 2 and NodeGirth[nod] < DEF_IMPOSSLEN:
|
||||
continue
|
||||
for nei in list(G.neighbors(nod)):
|
||||
if Coreness[nei] == 2 and NodeGirth[nei] < DEF_IMPOSSLEN:
|
||||
continue
|
||||
if not nei in visitedNodes.keys() or not nod in visitedNodes[nei]:
|
||||
visitedNodes[nod].add(nei)
|
||||
if nei not in visitedNodes.keys():
|
||||
visitedNodes[nei] = set([nod])
|
||||
else:
|
||||
visitedNodes[nei].add(nod)
|
||||
if Coreness[nei] == 2 and NodeGirth[nei] < DEF_IMPOSSLEN:
|
||||
continue
|
||||
G.remove_edge(nod, nei)
|
||||
if eg.single_source_dijkstra(G, nod, nei):
|
||||
for path in my_all_shortest_paths(G, nod, nei):
|
||||
lenPath = len(path)
|
||||
path.sort()
|
||||
yield tuple(path) # 这里改为 yield
|
||||
for i in path:
|
||||
if NodeGirth[i] > lenPath:
|
||||
NodeGirth[i] = lenPath
|
||||
G.add_edge(nod, nei)
|
||||
|
||||
|
||||
def StatisticsAndCalculateIndicators(SmallestCyclesOfNodes, CycLenDict, SmallestCycles):
|
||||
NumSmallCycles = len(SmallestCycles)
|
||||
for cyc in SmallestCycles:
|
||||
lenCyc = len(cyc)
|
||||
CycLenDict[lenCyc] += 1
|
||||
for nod in cyc:
|
||||
SmallestCyclesOfNodes[nod].add(cyc)
|
||||
CycleRatio = {} # 这里将 CycleRatio 作为局部变量
|
||||
for objNode, SmaCycs in SmallestCyclesOfNodes.items():
|
||||
if len(SmaCycs) == 0:
|
||||
continue
|
||||
cycleNeighbors = set()
|
||||
NeiOccurTimes = {}
|
||||
for cyc in SmaCycs:
|
||||
for n in cyc:
|
||||
if n in NeiOccurTimes.keys():
|
||||
NeiOccurTimes[n] += 1
|
||||
else:
|
||||
NeiOccurTimes[n] = 1
|
||||
cycleNeighbors = cycleNeighbors.union(cyc)
|
||||
cycleNeighbors.remove(objNode)
|
||||
del NeiOccurTimes[objNode]
|
||||
sum = 0
|
||||
for nei in cycleNeighbors:
|
||||
sum += float(NeiOccurTimes[nei]) / len(SmallestCyclesOfNodes[nei])
|
||||
CycleRatio[objNode] = sum + 1
|
||||
return CycleRatio
|
||||
|
||||
|
||||
def cycle_ratio_centrality(G):
|
||||
"""
|
||||
Parameters
|
||||
----------
|
||||
G : eg.Graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
cycle ratio centrality of each node in G : dict
|
||||
|
||||
Example
|
||||
-------
|
||||
>>> G = eg.Graph()
|
||||
>>> G.add_edges([(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4), (1, 5), (2, 5)])
|
||||
>>> cycle_ratio_centrality(G)
|
||||
{1: 4.083333333333333, 2: 4.083333333333333, 3: 2.6666666666666665, 4: 2.6666666666666665, 5: 1.5}
|
||||
|
||||
"""
|
||||
NumNode = G.number_of_nodes() # update
|
||||
DEF_IMPOSSLEN = NumNode + 1 # Impossible simple cycle length
|
||||
NodeGirth = dict()
|
||||
CycLenDict = dict()
|
||||
|
||||
SmallestCyclesOfNodes = {}
|
||||
removeNodes = set()
|
||||
Coreness = dict(zip(list(G.nodes), eg.k_core(G)))
|
||||
for i in list(G.nodes):
|
||||
SmallestCyclesOfNodes[i] = set()
|
||||
if G.degree()[i] <= 1 or Coreness[i] <= 1:
|
||||
NodeGirth[i] = 0
|
||||
removeNodes.add(i)
|
||||
else:
|
||||
NodeGirth[i] = DEF_IMPOSSLEN
|
||||
|
||||
G.remove_nodes_from(removeNodes)
|
||||
|
||||
NodeNum = G.number_of_nodes()
|
||||
for i in range(3, NodeNum + 2):
|
||||
CycLenDict[i] = 0
|
||||
|
||||
SmallestCycles = set(getSmallestCycles(G, NodeGirth, Coreness, DEF_IMPOSSLEN))
|
||||
cycle_ratio = StatisticsAndCalculateIndicators(
|
||||
SmallestCyclesOfNodes, CycLenDict, SmallestCycles
|
||||
)
|
||||
return cycle_ratio
|
||||
@@ -0,0 +1,28 @@
|
||||
__all__ = ["hyepergraph_degree_centrality"]
|
||||
|
||||
|
||||
def hyepergraph_degree_centrality(G):
|
||||
"""
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : eg.Hypergraph
|
||||
The target hypergraph
|
||||
|
||||
Returns
|
||||
----------
|
||||
degree centrality of each node in G : dict
|
||||
|
||||
"""
|
||||
res = {}
|
||||
node_list = G.v
|
||||
# Get hyperedge list
|
||||
edge_list = G.e[0]
|
||||
for node in node_list:
|
||||
res[node] = 0
|
||||
|
||||
for e in edge_list:
|
||||
for n in e:
|
||||
res[n] += 1
|
||||
|
||||
return res
|
||||
@@ -0,0 +1,351 @@
|
||||
from itertools import compress
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
|
||||
|
||||
__all__ = ["size_independent_hypercoreness", "frequency_based_hypercoreness"]
|
||||
|
||||
|
||||
def size_independent_hypercoreness(h):
|
||||
"""The size_independent_hypercoreness of nodes in hypergraph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
h : eg.Hypergraph.
|
||||
|
||||
|
||||
Returns
|
||||
----------
|
||||
dict
|
||||
Centrality, where keys are node IDs and values are lists of centralities.
|
||||
|
||||
References
|
||||
----------
|
||||
Mancastroppa, M., Iacopini, I., Petri, G. et al. Hyper-cores promote localization and efficient seeding in higher-order processes. Nat Commun 14, 6223 (2023). https://doi.org/10.1038/s41467-023-41887-2.
|
||||
|
||||
"""
|
||||
e_list = h.e[0]
|
||||
initial_node_num = h.num_v
|
||||
data = [e_list[i] for i in range(len(e_list)) if len(e_list[i]) > 1]
|
||||
|
||||
data.sort(key=len)
|
||||
L = len(data)
|
||||
size_max = len(data[L - 1])
|
||||
|
||||
size = list([len(data[j]) for j in range(L)])
|
||||
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=data)
|
||||
IDX = list(range(0, X.num_v))
|
||||
|
||||
M = range(2, size_max + 1)
|
||||
k_step = 1
|
||||
K = range(1, 1200, k_step)
|
||||
k_shell_dict = {}
|
||||
idx_orig = IDX
|
||||
|
||||
IDX_size = range(len(size))
|
||||
k_max = np.zeros(len(M))
|
||||
|
||||
for j in idx_orig:
|
||||
k_shell_dict[j] = np.zeros(len(M))
|
||||
|
||||
for x in range(len(M)):
|
||||
m = M[x]
|
||||
|
||||
D = np.zeros(len(K))
|
||||
|
||||
# consider only hyperedges of size >=m
|
||||
idx_size = list(
|
||||
compress(IDX_size, np.greater_equal(size, m * np.ones(len(size))))
|
||||
)
|
||||
int_sel = list([data[i] for i in idx_size])
|
||||
# build hypergraph with only interactions of size >=m
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=int_sel)
|
||||
node_set = set()
|
||||
for sublist in int_sel:
|
||||
for element in sublist:
|
||||
node_set.add(element)
|
||||
IDX = list(node_set)
|
||||
# IDX_e = list(X.e[0])
|
||||
|
||||
for y in range(len(K)):
|
||||
kk = K[y]
|
||||
|
||||
d_tot_m = np.zeros(len(IDX))
|
||||
prev_shell = IDX
|
||||
|
||||
for i in range(len(IDX)):
|
||||
d_tot_m[i] = X.degree_node[IDX[i]]
|
||||
|
||||
idx_n_remove = list(
|
||||
compress(IDX, np.greater(kk * np.ones(len(d_tot_m)), d_tot_m))
|
||||
) # nodes with degree<k are removed
|
||||
# X.remove_nodes_from(idx_n_remove)
|
||||
now_e_list = X.e[0]
|
||||
new_e_list = []
|
||||
for e in now_e_list:
|
||||
new_e = []
|
||||
for n in e:
|
||||
if n not in idx_n_remove:
|
||||
new_e.append(n)
|
||||
if len(new_e) > 0:
|
||||
new_e_list.append(new_e)
|
||||
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list)
|
||||
|
||||
IDX_e = list(range(0, len(X.e[0])))
|
||||
|
||||
sizes = [
|
||||
len(X.e[0][i]) for i in IDX_e
|
||||
] # hyperedges with size <m are removed
|
||||
idx_e_remove = [IDX_e[i] for i in range(len(IDX_e)) if sizes[i] < m]
|
||||
now_e_list = X.e[0]
|
||||
new_e_list = []
|
||||
for i in range(len(now_e_list)):
|
||||
if i not in idx_e_remove:
|
||||
new_e_list.append(now_e_list[i])
|
||||
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list)
|
||||
|
||||
node_set = set()
|
||||
for sublist in X.e[0]:
|
||||
for element in sublist:
|
||||
node_set.add(element)
|
||||
IDX = list(node_set)
|
||||
|
||||
while len(idx_n_remove) > 0 or len(idx_e_remove) > 0:
|
||||
d_tot_m = np.zeros(len(IDX))
|
||||
|
||||
for i in range(len(IDX)):
|
||||
d_tot_m[i] = X.degree_node[IDX[i]]
|
||||
|
||||
idx_n_remove = list(
|
||||
compress(IDX, np.greater(kk * np.ones(len(d_tot_m)), d_tot_m))
|
||||
) # nodes with degree<k are removed
|
||||
# X.remove_nodes_from(idx_n_remove)
|
||||
now_e_list = X.e[0]
|
||||
new_e_list = []
|
||||
for e in now_e_list:
|
||||
new_e = []
|
||||
for n in e:
|
||||
if n not in idx_n_remove:
|
||||
new_e.append(n)
|
||||
if len(new_e) > 0:
|
||||
new_e_list.append(new_e)
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list)
|
||||
|
||||
IDX_e = list(range(len(X.e[0])))
|
||||
sizes = [
|
||||
len(X.e[0][i]) for i in IDX_e
|
||||
] # hyperedges with size <m are removed
|
||||
|
||||
idx_e_remove = [IDX_e[i] for i in range(len(IDX_e)) if sizes[i] < m]
|
||||
now_e_list = X.e[0]
|
||||
new_e_list = []
|
||||
for i in range(len(now_e_list)):
|
||||
if i not in idx_e_remove:
|
||||
new_e_list.append(now_e_list[i])
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list)
|
||||
|
||||
node_set = set()
|
||||
for sublist in X.e[0]:
|
||||
for element in sublist:
|
||||
node_set.add(element)
|
||||
IDX = list(node_set)
|
||||
|
||||
shell_kk = list(sorted(set(prev_shell) - set(IDX)))
|
||||
for j in shell_kk:
|
||||
# if j not in idx_n_remove:
|
||||
# continue
|
||||
k_shell_dict[j][x] = kk - k_step
|
||||
|
||||
node_set = set()
|
||||
for sublist in X.e[0]:
|
||||
for element in sublist:
|
||||
node_set.add(element)
|
||||
IDX = list(node_set)
|
||||
|
||||
D[y] = len(node_set)
|
||||
if y > 0:
|
||||
if D[y] == 0 and D[y - 1] != 0:
|
||||
# maximum connectivity at order m
|
||||
k_max[x] = kk - k_step
|
||||
# stop the decomposition when the (k,m)-core is empty
|
||||
if D[y] == 0:
|
||||
break
|
||||
|
||||
# size-independent hypercoreness
|
||||
R_dict = {}
|
||||
for y in k_shell_dict:
|
||||
R_dict[y] = sum(np.array(k_shell_dict[y]) / np.array(k_max))
|
||||
|
||||
return R_dict
|
||||
|
||||
|
||||
def frequency_based_hypercoreness(h):
|
||||
r"""The frequency-based hypercoreness of nodes in hypergraph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
h : easygraph.Hypergraph
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict : Centrality, where keys are node IDs and values are lists of centralities.
|
||||
|
||||
References
|
||||
----------
|
||||
Mancastroppa, M., Iacopini, I., Petri, G. et al. Hyper-cores promote localization and efficient seeding in higher-order processes. Nat Commun 14, 6223 (2023). https://doi.org/10.1038/s41467-023-41887-2
|
||||
|
||||
"""
|
||||
e_list = h.e[0]
|
||||
initial_node_num = h.num_v
|
||||
data = [e_list[i] for i in range(len(e_list)) if len(e_list[i]) > 1]
|
||||
|
||||
data.sort(key=len)
|
||||
L = len(data)
|
||||
size_max = len(data[L - 1])
|
||||
|
||||
size = list([len(data[j]) for j in range(L)])
|
||||
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=data)
|
||||
IDX = list(range(0, X.num_v))
|
||||
|
||||
M = range(2, size_max + 1)
|
||||
k_step = 1
|
||||
K = range(1, 1200, k_step)
|
||||
k_shell_dict = {}
|
||||
idx_orig = IDX
|
||||
|
||||
IDX_size = range(len(size))
|
||||
k_max = np.zeros(len(M))
|
||||
|
||||
for j in idx_orig:
|
||||
k_shell_dict[j] = np.zeros(len(M))
|
||||
|
||||
for x in range(len(M)):
|
||||
m = M[x]
|
||||
|
||||
D = np.zeros(len(K))
|
||||
# consider only hyperedges of size >=m
|
||||
idx_size = list(
|
||||
compress(IDX_size, np.greater_equal(size, m * np.ones(len(size))))
|
||||
)
|
||||
int_sel = list([data[i] for i in idx_size])
|
||||
# build hypergraph with only interactions of size >=m
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=int_sel)
|
||||
node_set = set()
|
||||
for sublist in int_sel:
|
||||
for element in sublist:
|
||||
node_set.add(element)
|
||||
IDX = list(node_set)
|
||||
|
||||
for y in range(len(K)):
|
||||
kk = K[y]
|
||||
|
||||
d_tot_m = np.zeros(len(IDX))
|
||||
prev_shell = IDX
|
||||
|
||||
for i in range(len(IDX)):
|
||||
d_tot_m[i] = X.degree_node[IDX[i]]
|
||||
|
||||
idx_n_remove = list(
|
||||
compress(IDX, np.greater(kk * np.ones(len(d_tot_m)), d_tot_m))
|
||||
) # nodes with degree<k are removed
|
||||
now_e_list = X.e[0]
|
||||
new_e_list = []
|
||||
for e in now_e_list:
|
||||
new_e = []
|
||||
for n in e:
|
||||
if n not in idx_n_remove:
|
||||
new_e.append(n)
|
||||
if len(new_e) > 0:
|
||||
new_e_list.append(new_e)
|
||||
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list)
|
||||
|
||||
IDX_e = list(range(0, len(X.e[0])))
|
||||
|
||||
# hyperedges with size <m are removed
|
||||
sizes = [len(X.e[0][i]) for i in IDX_e]
|
||||
idx_e_remove = [IDX_e[i] for i in range(len(IDX_e)) if sizes[i] < m]
|
||||
now_e_list = X.e[0]
|
||||
new_e_list = []
|
||||
for i in range(len(now_e_list)):
|
||||
if i not in idx_e_remove:
|
||||
new_e_list.append(now_e_list[i])
|
||||
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list)
|
||||
|
||||
node_set = set()
|
||||
for sublist in X.e[0]:
|
||||
for element in sublist:
|
||||
node_set.add(element)
|
||||
IDX = list(node_set)
|
||||
|
||||
while len(idx_n_remove) > 0 or len(idx_e_remove) > 0:
|
||||
d_tot_m = np.zeros(len(IDX))
|
||||
|
||||
for i in range(len(IDX)):
|
||||
d_tot_m[i] = X.degree_node[IDX[i]]
|
||||
# nodes with degree<k are removed
|
||||
idx_n_remove = list(
|
||||
compress(IDX, np.greater(kk * np.ones(len(d_tot_m)), d_tot_m))
|
||||
)
|
||||
now_e_list = X.e[0]
|
||||
new_e_list = []
|
||||
for e in now_e_list:
|
||||
new_e = []
|
||||
for n in e:
|
||||
if n not in idx_n_remove:
|
||||
new_e.append(n)
|
||||
if len(new_e) > 0:
|
||||
new_e_list.append(new_e)
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list)
|
||||
|
||||
IDX_e = list(range(len(X.e[0])))
|
||||
# hyperedges with size <m are removed
|
||||
sizes = [len(X.e[0][i]) for i in IDX_e]
|
||||
|
||||
idx_e_remove = [IDX_e[i] for i in range(len(IDX_e)) if sizes[i] < m]
|
||||
now_e_list = X.e[0]
|
||||
new_e_list = []
|
||||
for i in range(len(now_e_list)):
|
||||
if i not in idx_e_remove:
|
||||
new_e_list.append(now_e_list[i])
|
||||
X = eg.Hypergraph(num_v=initial_node_num, e_list=new_e_list)
|
||||
|
||||
node_set = set()
|
||||
for sublist in X.e[0]:
|
||||
for element in sublist:
|
||||
node_set.add(element)
|
||||
IDX = list(node_set)
|
||||
|
||||
shell_kk = list(sorted(set(prev_shell) - set(IDX)))
|
||||
for j in shell_kk:
|
||||
k_shell_dict[j][x] = kk - k_step
|
||||
|
||||
node_set = set()
|
||||
for sublist in X.e[0]:
|
||||
for element in sublist:
|
||||
node_set.add(element)
|
||||
IDX = list(node_set)
|
||||
|
||||
D[y] = len(node_set)
|
||||
if y > 0:
|
||||
if D[y] == 0 and D[y - 1] != 0:
|
||||
k_max[x] = kk - k_step # maximum connectivity at order m
|
||||
if D[y] == 0:
|
||||
break # stop the decomposition when the (k,m)-core is empty
|
||||
|
||||
# Psi(m) distribution of hyperedges size
|
||||
Psi = []
|
||||
for m in range(2, size_max + 1):
|
||||
Psi.append(size.count(m) / len(size))
|
||||
# frequency-based hypercoreness
|
||||
R_w_dict = {}
|
||||
for y in k_shell_dict:
|
||||
R_w_dict[y] = sum(np.array(Psi) * np.array(k_shell_dict[y]) / np.array(k_max))
|
||||
return R_w_dict
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user