chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:36:30 +08:00
commit 55ab4e4a73
473 changed files with 72932 additions and 0 deletions
+21
View File
@@ -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"
)
+4
View File
@@ -0,0 +1,4 @@
from .avg_degree import *
from .cluster import *
from .localassort import *
from .predecessor_path_based import *
+31
View File
@@ -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
+559
View File
@@ -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
+226
View File
@@ -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
+105
View File
@@ -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
+125
View File
@@ -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
+134
View File
@@ -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()
+761
View File
@@ -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)
+355
View File
@@ -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 dOrlans. 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)
+122
View File
@@ -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
+160
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
from .k_core import *
+72
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
from .drawing import *
from .plot import *
from .positioning import *
+253
View File
@@ -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
+50
View File
@@ -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
+33
View File
@@ -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
+233
View File
@@ -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()
+646
View File
@@ -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
+195
View File
@@ -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()
+596
View File
@@ -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()
+185
View File
@@ -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
+303
View File
@@ -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
+280
View File
@@ -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