chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
from .avg_degree import *
|
||||
from .cluster import *
|
||||
from .localassort import *
|
||||
from .predecessor_path_based import *
|
||||
@@ -0,0 +1,31 @@
|
||||
__all__ = [
|
||||
"average_degree",
|
||||
]
|
||||
|
||||
|
||||
def average_degree(G) -> float:
|
||||
"""Returns the average degree of the graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A EasyGraph graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
average degree : float
|
||||
The average degree of the graph.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Self loops are counted twice in the total degree of a node.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.Graph() # or DiGraph, MultiGraph, MultiDiGraph, etc
|
||||
>>> G.add_edge(1, 2)
|
||||
>>> G.add_edge(2, 3)
|
||||
>>> eg.average_degree(G)
|
||||
1.3333333333333333
|
||||
"""
|
||||
return G.number_of_edges() / G.number_of_nodes() * 2
|
||||
@@ -0,0 +1,559 @@
|
||||
from collections import Counter
|
||||
from itertools import chain
|
||||
|
||||
import numpy as np
|
||||
|
||||
from easygraph.utils.decorators import hybrid
|
||||
from easygraph.utils.decorators import not_implemented_for
|
||||
from easygraph.utils.misc import split
|
||||
from easygraph.utils.misc import split_len
|
||||
|
||||
|
||||
__all__ = ["average_clustering", "clustering"]
|
||||
|
||||
|
||||
def _local_weighted_triangles_and_degree_iter_parallel(
|
||||
nodes_nbrs, G, weight, max_weight
|
||||
):
|
||||
ret = []
|
||||
|
||||
def wt(u, v):
|
||||
return G[u][v].get(weight, 1) / max_weight
|
||||
|
||||
for i, nbrs in nodes_nbrs:
|
||||
inbrs = set(nbrs) - {i}
|
||||
weighted_triangles = 0
|
||||
seen = set()
|
||||
for j in inbrs:
|
||||
seen.add(j)
|
||||
# This avoids counting twice -- we double at the end.
|
||||
jnbrs = set(G[j]) - seen
|
||||
# Only compute the edge weight once, before the inner inner
|
||||
# loop.
|
||||
wij = wt(i, j)
|
||||
weighted_triangles += sum(
|
||||
np.cbrt([(wij * wt(j, k) * wt(k, i)) for k in inbrs & jnbrs])
|
||||
)
|
||||
ret.append((i, len(inbrs), 2 * weighted_triangles))
|
||||
return ret
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def _weighted_triangles_and_degree_iter(G, nodes=None, weight="weight", n_workers=None):
|
||||
"""Return an iterator of (node, degree, weighted_triangles).
|
||||
|
||||
Used for weighted clustering.
|
||||
Note: this returns the geometric average weight of edges in the triangle.
|
||||
Also, each triangle is counted twice (each direction).
|
||||
So you may want to divide by 2.
|
||||
|
||||
"""
|
||||
|
||||
if weight is None or G.number_of_edges() == 0:
|
||||
max_weight = 1
|
||||
else:
|
||||
max_weight = max(d.get(weight, 1) for u, v, d in G.edges)
|
||||
if nodes is None:
|
||||
nodes_nbrs = G.adj.items()
|
||||
else:
|
||||
nodes_nbrs = ((n, G[n]) for n in G.nbunch_iter(nodes))
|
||||
|
||||
def wt(u, v):
|
||||
return G[u][v].get(weight, 1) / max_weight
|
||||
|
||||
if n_workers is not None:
|
||||
import random
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
|
||||
_local_weighted_triangles_and_degree_iter_function = partial(
|
||||
_local_weighted_triangles_and_degree_iter_parallel,
|
||||
G=G,
|
||||
weight=weight,
|
||||
max_weight=max_weight,
|
||||
)
|
||||
nodes_nbrs = list(nodes_nbrs)
|
||||
random.shuffle(nodes_nbrs)
|
||||
if len(nodes_nbrs) > n_workers * 30000:
|
||||
nodes_nbrs = split_len(nodes, step=30000)
|
||||
else:
|
||||
nodes_nbrs = split(nodes_nbrs, n_workers)
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(_local_weighted_triangles_and_degree_iter_function, nodes_nbrs)
|
||||
for r in ret:
|
||||
for x in r:
|
||||
yield x
|
||||
else:
|
||||
for i, nbrs in nodes_nbrs:
|
||||
inbrs = set(nbrs) - {i}
|
||||
weighted_triangles = 0
|
||||
seen = set()
|
||||
for j in inbrs:
|
||||
seen.add(j)
|
||||
# This avoids counting twice -- we double at the end.
|
||||
jnbrs = set(G[j]) - seen
|
||||
# Only compute the edge weight once, before the inner inner
|
||||
# loop.
|
||||
wij = wt(i, j)
|
||||
weighted_triangles += sum(
|
||||
np.cbrt([(wij * wt(j, k) * wt(k, i)) for k in inbrs & jnbrs])
|
||||
)
|
||||
yield (i, len(inbrs), 2 * weighted_triangles)
|
||||
|
||||
|
||||
def _local_directed_weighted_triangles_and_degree_parallel(
|
||||
nodes_nbrs, G, weight, max_weight
|
||||
):
|
||||
ret = []
|
||||
|
||||
def wt(u, v):
|
||||
return G[u][v].get(weight, 1) / max_weight
|
||||
|
||||
for i, preds, succs in nodes_nbrs:
|
||||
ipreds = set(preds) - {i}
|
||||
isuccs = set(succs) - {i}
|
||||
|
||||
directed_triangles = 0
|
||||
for j in ipreds:
|
||||
jpreds = set(G._pred[j]) - {j}
|
||||
jsuccs = set(G._adj[j]) - {j}
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs])
|
||||
)
|
||||
|
||||
for j in isuccs:
|
||||
jpreds = set(G._pred[j]) - {j}
|
||||
jsuccs = set(G._adj[j]) - {j}
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs])
|
||||
)
|
||||
|
||||
dtotal = len(ipreds) + len(isuccs)
|
||||
dbidirectional = len(ipreds & isuccs)
|
||||
ret.append([i, dtotal, dbidirectional, directed_triangles])
|
||||
return ret
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def _directed_weighted_triangles_and_degree_iter(
|
||||
G, nodes=None, weight="weight", n_workers=None
|
||||
):
|
||||
"""Return an iterator of
|
||||
(node, total_degree, reciprocal_degree, directed_weighted_triangles).
|
||||
|
||||
Used for directed weighted clustering.
|
||||
Note that unlike `_weighted_triangles_and_degree_iter()`, this function counts
|
||||
directed triangles so does not count triangles twice.
|
||||
|
||||
"""
|
||||
|
||||
if weight is None or G.number_of_edges() == 0:
|
||||
max_weight = 1
|
||||
else:
|
||||
max_weight = max(d.get(weight, 1) for u, v, d in G.edges)
|
||||
|
||||
nodes_nbrs = ((n, G._pred[n], G._adj[n]) for n in G.nbunch_iter(nodes))
|
||||
|
||||
def wt(u, v):
|
||||
return G[u][v].get(weight, 1) / max_weight
|
||||
|
||||
if n_workers is not None:
|
||||
import random
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
|
||||
_local_directed_weighted_triangles_and_degree_function = partial(
|
||||
_local_directed_weighted_triangles_and_degree_parallel,
|
||||
G=G,
|
||||
weight=weight,
|
||||
max_weight=max_weight,
|
||||
)
|
||||
nodes_nbrs = list(nodes_nbrs)
|
||||
random.shuffle(nodes_nbrs)
|
||||
if len(nodes_nbrs) > n_workers * 30000:
|
||||
nodes_nbrs = split_len(nodes, step=30000)
|
||||
else:
|
||||
nodes_nbrs = split(nodes_nbrs, n_workers)
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(
|
||||
_local_directed_weighted_triangles_and_degree_function, nodes_nbrs
|
||||
)
|
||||
for r in ret:
|
||||
for x in r:
|
||||
yield x
|
||||
|
||||
else:
|
||||
for i, preds, succs in nodes_nbrs:
|
||||
ipreds = set(preds) - {i}
|
||||
isuccs = set(succs) - {i}
|
||||
|
||||
directed_triangles = 0
|
||||
for j in ipreds:
|
||||
jpreds = set(G._pred[j]) - {j}
|
||||
jsuccs = set(G._adj[j]) - {j}
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(j, i) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs])
|
||||
)
|
||||
|
||||
for j in isuccs:
|
||||
jpreds = set(G._pred[j]) - {j}
|
||||
jsuccs = set(G._adj[j]) - {j}
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(k, i) * wt(k, j)) for k in ipreds & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(k, i) * wt(j, k)) for k in ipreds & jsuccs])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(i, k) * wt(k, j)) for k in isuccs & jpreds])
|
||||
)
|
||||
directed_triangles += sum(
|
||||
np.cbrt([(wt(i, j) * wt(i, k) * wt(j, k)) for k in isuccs & jsuccs])
|
||||
)
|
||||
|
||||
dtotal = len(ipreds) + len(isuccs)
|
||||
dbidirectional = len(ipreds & isuccs)
|
||||
yield (i, dtotal, dbidirectional, directed_triangles)
|
||||
|
||||
|
||||
def average_clustering(G, nodes=None, weight=None, count_zeros=True, n_workers=None):
|
||||
r"""Compute the average clustering coefficient for the graph G.
|
||||
|
||||
The clustering coefficient for the graph is the average,
|
||||
|
||||
.. math::
|
||||
|
||||
C = \frac{1}{n}\sum_{v \in G} c_v,
|
||||
|
||||
where :math:`n` is the number of nodes in `G`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
|
||||
nodes : container of nodes, optional (default=all nodes in G)
|
||||
Compute average clustering for nodes in this container.
|
||||
|
||||
weight : string or None, optional (default=None)
|
||||
The edge attribute that holds the numerical value used as a weight.
|
||||
If None, then each edge has weight 1.
|
||||
|
||||
count_zeros : bool
|
||||
If False include only the nodes with nonzero clustering in the average.
|
||||
|
||||
Returns
|
||||
-------
|
||||
avg : float
|
||||
Average clustering
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.complete_graph(5)
|
||||
>>> print(eg.average_clustering(G))
|
||||
1.0
|
||||
|
||||
Notes
|
||||
-----
|
||||
This is a space saving routine; it might be faster
|
||||
to use the clustering function to get a list and then take the average.
|
||||
|
||||
Self loops are ignored.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Generalizations of the clustering coefficient to weighted
|
||||
complex networks by J. Saramäki, M. Kivelä, J.-P. Onnela,
|
||||
K. Kaski, and J. Kertész, Physical Review E, 75 027105 (2007).
|
||||
http://jponnela.com/web_documents/a9.pdf
|
||||
.. [2] Marcus Kaiser, Mean clustering coefficients: the role of isolated
|
||||
nodes and leafs on clustering measures for small-world networks.
|
||||
https://arxiv.org/abs/0802.2512
|
||||
"""
|
||||
c = clustering(G, nodes, weight=weight, n_workers=n_workers).values()
|
||||
if not count_zeros:
|
||||
c = [v for v in c if abs(v) > 0]
|
||||
return sum(c) / len(c)
|
||||
|
||||
|
||||
def _local_directed_triangles_and_degree_iter_parallel(nodes_nbrs, G):
|
||||
ret = []
|
||||
for i, preds, succs in nodes_nbrs:
|
||||
ipreds = set(preds) - {i}
|
||||
isuccs = set(succs) - {i}
|
||||
|
||||
directed_triangles = 0
|
||||
for j in chain(ipreds, isuccs):
|
||||
jpreds = set(G._pred[j]) - {j}
|
||||
jsuccs = set(G._adj[j]) - {j}
|
||||
directed_triangles += sum(
|
||||
1
|
||||
for k in chain(
|
||||
(ipreds & jpreds),
|
||||
(ipreds & jsuccs),
|
||||
(isuccs & jpreds),
|
||||
(isuccs & jsuccs),
|
||||
)
|
||||
)
|
||||
dtotal = len(ipreds) + len(isuccs)
|
||||
dbidirectional = len(ipreds & isuccs)
|
||||
ret.append((i, dtotal, dbidirectional, directed_triangles))
|
||||
return ret
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def _directed_triangles_and_degree_iter(G, nodes=None, n_workers=None):
|
||||
"""Return an iterator of
|
||||
(node, total_degree, reciprocal_degree, directed_triangles).
|
||||
|
||||
Used for directed clustering.
|
||||
Note that unlike `_triangles_and_degree_iter()`, this function counts
|
||||
directed triangles so does not count triangles twice.
|
||||
|
||||
"""
|
||||
nodes_nbrs = ((n, G._pred[n], G._adj[n]) for n in G.nbunch_iter(nodes))
|
||||
|
||||
if n_workers is not None:
|
||||
import random
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
|
||||
_local_directed_triangles_and_degree_iter_parallel_function = partial(
|
||||
_local_directed_triangles_and_degree_iter_parallel, G=G
|
||||
)
|
||||
nodes_nbrs = list(nodes_nbrs)
|
||||
random.shuffle(nodes_nbrs)
|
||||
if len(nodes_nbrs) > n_workers * 30000:
|
||||
nodes_nbrs = split_len(nodes_nbrs, step=30000)
|
||||
else:
|
||||
nodes_nbrs = split(nodes_nbrs, n_workers)
|
||||
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(
|
||||
_local_directed_triangles_and_degree_iter_parallel_function, nodes_nbrs
|
||||
)
|
||||
for r in ret:
|
||||
for x in r:
|
||||
yield x
|
||||
else:
|
||||
for i, preds, succs in nodes_nbrs:
|
||||
ipreds = set(preds) - {i}
|
||||
isuccs = set(succs) - {i}
|
||||
|
||||
directed_triangles = 0
|
||||
for j in chain(ipreds, isuccs):
|
||||
jpreds = set(G._pred[j]) - {j}
|
||||
jsuccs = set(G._adj[j]) - {j}
|
||||
directed_triangles += sum(
|
||||
1
|
||||
for k in chain(
|
||||
(ipreds & jpreds),
|
||||
(ipreds & jsuccs),
|
||||
(isuccs & jpreds),
|
||||
(isuccs & jsuccs),
|
||||
)
|
||||
)
|
||||
dtotal = len(ipreds) + len(isuccs)
|
||||
dbidirectional = len(ipreds & isuccs)
|
||||
yield (i, dtotal, dbidirectional, directed_triangles)
|
||||
|
||||
|
||||
def _local_triangles_and_degree_iter_function_parallel(nodes_nbrs, G):
|
||||
ret = []
|
||||
for v, v_nbrs in nodes_nbrs:
|
||||
vs = set(v_nbrs) - {v}
|
||||
gen_degree = Counter(len(vs & (set(G[w]) - {w})) for w in vs)
|
||||
ntriangles = sum(k * val for k, val in gen_degree.items())
|
||||
ret.append((v, len(vs), ntriangles, gen_degree))
|
||||
return ret
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
def _triangles_and_degree_iter(G, nodes=None, n_workers=None):
|
||||
"""Return an iterator of (node, degree, triangles, generalized degree).
|
||||
|
||||
This double counts triangles so you may want to divide by 2.
|
||||
See degree(), triangles() and generalized_degree() for definitions
|
||||
and details.
|
||||
|
||||
"""
|
||||
if nodes is None:
|
||||
nodes_nbrs = G.adj.items()
|
||||
else:
|
||||
nodes_nbrs = ((n, G[n]) for n in G.nbunch_iter(nodes))
|
||||
|
||||
if n_workers is not None:
|
||||
import random
|
||||
|
||||
from functools import partial
|
||||
from multiprocessing import Pool
|
||||
|
||||
_local_triangles_and_degree_iter_function = partial(
|
||||
_local_triangles_and_degree_iter_function_parallel, G=G
|
||||
)
|
||||
nodes_nbrs = list(nodes_nbrs)
|
||||
random.shuffle(nodes_nbrs)
|
||||
if len(nodes_nbrs) > n_workers * 30000:
|
||||
nodes_nbrs = split_len(nodes_nbrs, step=30000)
|
||||
else:
|
||||
nodes_nbrs = split(nodes_nbrs, n_workers)
|
||||
|
||||
with Pool(n_workers) as p:
|
||||
ret = p.imap(_local_triangles_and_degree_iter_function, nodes_nbrs)
|
||||
for r in ret:
|
||||
for x in r:
|
||||
yield x
|
||||
else:
|
||||
for v, v_nbrs in nodes_nbrs:
|
||||
vs = set(v_nbrs) - {v}
|
||||
gen_degree = Counter(len(vs & (set(G[w]) - {w})) for w in vs)
|
||||
ntriangles = sum(k * val for k, val in gen_degree.items())
|
||||
yield (v, len(vs), ntriangles, gen_degree)
|
||||
|
||||
|
||||
@hybrid("cpp_clustering")
|
||||
def clustering(G, nodes=None, weight=None, n_workers=None):
|
||||
r"""Compute the clustering coefficient for nodes.
|
||||
|
||||
For unweighted graphs, the clustering of a node :math:`u`
|
||||
is the fraction of possible triangles through that node that exist,
|
||||
|
||||
.. math::
|
||||
|
||||
c_u = \frac{2 T(u)}{deg(u)(deg(u)-1)},
|
||||
|
||||
where :math:`T(u)` is the number of triangles through node :math:`u` and
|
||||
:math:`deg(u)` is the degree of :math:`u`.
|
||||
|
||||
For weighted graphs, there are several ways to define clustering [1]_.
|
||||
the one used here is defined
|
||||
as the geometric average of the subgraph edge weights [2]_,
|
||||
|
||||
.. math::
|
||||
|
||||
c_u = \frac{1}{deg(u)(deg(u)-1))}
|
||||
\sum_{vw} (\hat{w}_{uv} \hat{w}_{uw} \hat{w}_{vw})^{1/3}.
|
||||
|
||||
The edge weights :math:`\hat{w}_{uv}` are normalized by the maximum weight
|
||||
in the network :math:`\hat{w}_{uv} = w_{uv}/\max(w)`.
|
||||
|
||||
The value of :math:`c_u` is assigned to 0 if :math:`deg(u) < 2`.
|
||||
|
||||
Additionally, this weighted definition has been generalized to support negative edge weights [3]_.
|
||||
|
||||
For directed graphs, the clustering is similarly defined as the fraction
|
||||
of all possible directed triangles or geometric average of the subgraph
|
||||
edge weights for unweighted and weighted directed graph respectively [4]_.
|
||||
|
||||
.. math::
|
||||
|
||||
c_u = \frac{2}{deg^{tot}(u)(deg^{tot}(u)-1) - 2deg^{\leftrightarrow}(u)}
|
||||
T(u),
|
||||
|
||||
where :math:`T(u)` is the number of directed triangles through node
|
||||
:math:`u`, :math:`deg^{tot}(u)` is the sum of in degree and out degree of
|
||||
:math:`u` and :math:`deg^{\leftrightarrow}(u)` is the reciprocal degree of
|
||||
:math:`u`.
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
|
||||
nodes : container of nodes, optional (default=all nodes in G)
|
||||
Compute clustering for nodes in this container.
|
||||
|
||||
weight : string or None, optional (default=None)
|
||||
The edge attribute that holds the numerical value used as a weight.
|
||||
If None, then each edge has weight 1.
|
||||
|
||||
Returns
|
||||
-------
|
||||
out : float, or dictionary
|
||||
Clustering coefficient at specified nodes
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.complete_graph(5)
|
||||
>>> print(eg.clustering(G, 0))
|
||||
1.0
|
||||
>>> print(eg.clustering(G))
|
||||
{0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0}
|
||||
|
||||
Notes
|
||||
-----
|
||||
Self loops are ignored.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Generalizations of the clustering coefficient to weighted
|
||||
complex networks by J. Saramäki, M. Kivelä, J.-P. Onnela,
|
||||
K. Kaski, and J. Kertész, Physical Review E, 75 027105 (2007).
|
||||
http://jponnela.com/web_documents/a9.pdf
|
||||
.. [2] Intensity and coherence of motifs in weighted complex
|
||||
networks by J. P. Onnela, J. Saramäki, J. Kertész, and K. Kaski,
|
||||
Physical Review E, 71(6), 065103 (2005).
|
||||
.. [3] Generalization of Clustering Coefficients to Signed Correlation Networks
|
||||
by G. Costantini and M. Perugini, PloS one, 9(2), e88669 (2014).
|
||||
.. [4] Clustering in complex directed networks by G. Fagiolo,
|
||||
Physical Review E, 76(2), 026107 (2007).
|
||||
"""
|
||||
|
||||
if G.is_directed():
|
||||
if weight is not None:
|
||||
td_iter = _directed_weighted_triangles_and_degree_iter(
|
||||
G, nodes, weight, n_workers=n_workers
|
||||
)
|
||||
clusterc = {
|
||||
v: 0 if t == 0 else t / ((dt * (dt - 1) - 2 * db) * 2)
|
||||
for v, dt, db, t in td_iter
|
||||
}
|
||||
else:
|
||||
td_iter = _directed_triangles_and_degree_iter(G, nodes, n_workers=n_workers)
|
||||
clusterc = {
|
||||
v: 0 if t == 0 else t / ((dt * (dt - 1) - 2 * db) * 2)
|
||||
for v, dt, db, t in td_iter
|
||||
}
|
||||
else:
|
||||
# The formula 2*T/(d*(d-1)) from docs is t/(d*(d-1)) here b/c t==2*T
|
||||
if weight is not None:
|
||||
td_iter = _weighted_triangles_and_degree_iter(
|
||||
G, nodes, weight, n_workers=n_workers
|
||||
)
|
||||
clusterc = {v: 0 if t == 0 else t / (d * (d - 1)) for v, d, t in td_iter}
|
||||
else:
|
||||
td_iter = _triangles_and_degree_iter(G, nodes, n_workers=n_workers)
|
||||
clusterc = {v: 0 if t == 0 else t / (d * (d - 1)) for v, d, t, _ in td_iter}
|
||||
if nodes in G:
|
||||
# Return the value of the sole entry in the dictionary.
|
||||
return clusterc[nodes]
|
||||
return clusterc
|
||||
@@ -0,0 +1,226 @@
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
import scipy.sparse as sparse
|
||||
|
||||
|
||||
__all__ = [
|
||||
"localAssort",
|
||||
]
|
||||
|
||||
|
||||
def localAssort(
|
||||
edgelist, node_attr, pr=np.arange(0.0, 1.0, 0.1), undir=True, missingValue=-1
|
||||
):
|
||||
"""Calculate the multiscale assortativity.
|
||||
You must ensure that the node index and node attribute index start from 0
|
||||
Parameters
|
||||
----------
|
||||
edgelist : array_like
|
||||
the network represented as an edge list,
|
||||
i.e., a E x 2 array of node pairs
|
||||
node_attr : array_like
|
||||
n length array of node attribute values
|
||||
pr : array, optional
|
||||
array of one minus restart probabilities for the random walk in
|
||||
calculating the personalised pagerank. The largest of these values
|
||||
determines the accuracy of the TotalRank vector max(pr) -> 1 is more
|
||||
accurate (default: [0, .1, .2, .3, .4, .5, .6, .7, .8, .9])
|
||||
undir : bool, optional
|
||||
indicate if network is undirected (default: True)
|
||||
missingValue : int, optional
|
||||
token to indicate missing attribute values (default: -1)
|
||||
Returns
|
||||
-------
|
||||
assortM : array_like
|
||||
n x len(pr) array of local assortativities, each column corresponds to
|
||||
a value of the input restart probabilities, pr. Note if only number of
|
||||
restart probabilties is greater than one (i.e., len(pr) > 1).
|
||||
assortT : array_like
|
||||
n length array of multiscale assortativities
|
||||
Z : array_like
|
||||
N length array of per-node confidence scores
|
||||
References
|
||||
----------
|
||||
For full details see [1]_
|
||||
.. [1] Peel, L., Delvenne, J. C., & Lambiotte, R. (2018). "Multiscale
|
||||
mixing patterns in networks.' PNAS, 115(16), 4057-4062.
|
||||
"""
|
||||
# number of nodes
|
||||
n = len(node_attr)
|
||||
|
||||
# number of nodes with complete attribute
|
||||
ncomp = (node_attr != missingValue).sum()
|
||||
# number of edges
|
||||
m = len(edgelist)
|
||||
# construct adjacency matrix and calculate degree sequence
|
||||
A, degree = createA(edgelist, n, undir)
|
||||
|
||||
# construct diagonal inverse degree matrix
|
||||
D = sparse.diags(1.0 / degree, 0, format="csc")
|
||||
|
||||
# construct transition matrix (row normalised adjacency matrix)
|
||||
W = D @ A
|
||||
|
||||
# number of distinct node categories
|
||||
c = len(np.unique(node_attr))
|
||||
if ncomp < n:
|
||||
c -= 1
|
||||
|
||||
# calculate node weights for how "complete" the
|
||||
# metadata is around the node
|
||||
Z = np.zeros(n)
|
||||
|
||||
Z[node_attr == missingValue] = 1.0
|
||||
|
||||
Z = (W @ Z) / degree
|
||||
|
||||
# indicator array if node has attribute data (or missing)
|
||||
hasAttribute = node_attr != missingValue
|
||||
|
||||
# calculate global expected values
|
||||
values = np.ones(ncomp)
|
||||
|
||||
yi = (hasAttribute).nonzero()[0]
|
||||
|
||||
yj = node_attr[hasAttribute]
|
||||
Y = sparse.coo_matrix((values, (yi, yj)), shape=(n, c)).tocsc()
|
||||
eij_glob = np.array(Y.T @ (A @ Y).todense())
|
||||
|
||||
eij_glob /= np.sum(eij_glob)
|
||||
|
||||
ab_glob = np.sum(eij_glob.sum(1) * eij_glob.sum(0))
|
||||
# initialise outputs
|
||||
assortM = np.empty((n, len(pr)))
|
||||
assortT = np.empty(n)
|
||||
WY = (W @ Y).tocsc()
|
||||
|
||||
for i in range(n):
|
||||
pis, ti, it = calculateRWRrange(W, i, pr, n)
|
||||
if len(pr) > 1:
|
||||
for ii, pri in enumerate(pr):
|
||||
pi = pis[:, ii]
|
||||
|
||||
YPI = sparse.coo_matrix(
|
||||
(
|
||||
pi[hasAttribute],
|
||||
(node_attr[hasAttribute], np.arange(n)[hasAttribute]),
|
||||
),
|
||||
shape=(c, n),
|
||||
).tocsr()
|
||||
trace_e = (YPI.dot(WY).toarray()).trace()
|
||||
assortM[i, ii] = trace_e
|
||||
YPI = sparse.coo_matrix(
|
||||
(ti[hasAttribute], (node_attr[hasAttribute], np.arange(n)[hasAttribute])),
|
||||
shape=(c, n),
|
||||
).tocsr()
|
||||
e_gh = (YPI @ WY).toarray()
|
||||
e_gh_sum = e_gh.sum()
|
||||
Z[i] = e_gh_sum
|
||||
e_gh /= e_gh_sum
|
||||
trace_e = e_gh.trace()
|
||||
assortT[i] = trace_e
|
||||
|
||||
assortT -= ab_glob
|
||||
np.divide(assortT, 1.0 - ab_glob, out=assortT, where=ab_glob != 0)
|
||||
|
||||
if len(pr) > 1:
|
||||
assortM -= ab_glob
|
||||
np.divide(assortM, 1.0 - ab_glob, out=assortM, where=ab_glob != 0)
|
||||
return assortM, assortT, Z
|
||||
return None, assortT, Z
|
||||
|
||||
|
||||
def createA(E, n, undir=True):
|
||||
"""Create adjacency matrix and degree sequence."""
|
||||
if undir:
|
||||
G = eg.Graph()
|
||||
else:
|
||||
G = eg.DiGraph()
|
||||
G.add_nodes_from(range(n))
|
||||
|
||||
for e in E:
|
||||
G.add_edge(e[0], e[1])
|
||||
|
||||
A = eg.to_scipy_sparse_matrix(G)
|
||||
|
||||
degree = np.array(A.sum(1)).flatten()
|
||||
|
||||
return A, degree
|
||||
|
||||
|
||||
def calculateRWRrange(W, i, alphas, n, maxIter=1000):
|
||||
"""
|
||||
Calculate the personalised TotalRank and personalised PageRank vectors.
|
||||
Parameters
|
||||
----------
|
||||
W : array_like
|
||||
transition matrix (row normalised adjacency matrix)
|
||||
i : int
|
||||
index of the personalisation node
|
||||
alphas : array_like
|
||||
array of (1 - restart probabilties)
|
||||
n : int
|
||||
number of nodes in the network
|
||||
maxIter : int, optional
|
||||
maximum number of interations (default: 1000)
|
||||
Returns
|
||||
-------
|
||||
pPageRank_all : array_like
|
||||
personalised PageRank for all input alpha values (only calculated if
|
||||
more than one alpha given as input, i.e., len(alphas) > 1)
|
||||
pTotalRank : array_like
|
||||
personalised TotalRank (personalised PageRank with alpha integrated
|
||||
out)
|
||||
|
||||
it : int
|
||||
number of iterations
|
||||
References
|
||||
----------
|
||||
See [2]_ and [3]_ for further details.
|
||||
.. [2] Boldi, P. (2005). "TotalRank: Ranking without damping." In Special
|
||||
interest tracks and posters of the 14th international conference on
|
||||
World Wide Web (pp. 898-899).
|
||||
.. [3] Boldi, P., Santini, M., & Vigna, S. (2007). "A deeper investigation
|
||||
of PageRank as a function of the damping factor." In Dagstuhl Seminar
|
||||
Proceedings. Schloss Dagstuhl-Leibniz-Zentrum für Informatik.
|
||||
"""
|
||||
alpha0 = alphas.max()
|
||||
WT = alpha0 * W.T
|
||||
diff = 1
|
||||
it = 1
|
||||
|
||||
# initialise PageRank vectors
|
||||
pPageRank = np.zeros(n)
|
||||
|
||||
pPageRank_all = np.zeros((n, len(alphas)))
|
||||
pPageRank[i] = 1
|
||||
|
||||
pPageRank_all[i, :] = 1
|
||||
|
||||
pPageRank_old = pPageRank.copy()
|
||||
pTotalRank = pPageRank.copy()
|
||||
|
||||
oneminusalpha0 = 1 - alpha0
|
||||
|
||||
while diff > 1e-9:
|
||||
# calculate personalised PageRank via power iteration
|
||||
pPageRank = WT @ pPageRank
|
||||
pPageRank[i] += oneminusalpha0
|
||||
# calculate difference in pPageRank from previous iteration
|
||||
delta_pPageRank = pPageRank - pPageRank_old
|
||||
# Eq. [S23] Ref. [1]
|
||||
pTotalRank += (delta_pPageRank) / ((it + 1) * (alpha0**it))
|
||||
# only calculate personalised pageranks if more than one alpha
|
||||
if len(alphas) > 1:
|
||||
pPageRank_all += np.outer((delta_pPageRank), (alphas / alpha0) ** it)
|
||||
|
||||
# calculate convergence criteria
|
||||
diff = np.sum((delta_pPageRank) ** 2) / n
|
||||
it += 1
|
||||
|
||||
if it > maxIter:
|
||||
print(i, "max iterations exceeded")
|
||||
diff = 0
|
||||
pPageRank_old = pPageRank.copy()
|
||||
|
||||
return pPageRank_all, pTotalRank, it
|
||||
@@ -0,0 +1,101 @@
|
||||
import easygraph as eg
|
||||
|
||||
|
||||
__all__ = [
|
||||
"predecessor",
|
||||
]
|
||||
|
||||
|
||||
def predecessor(G, source, target=None, cutoff=None, return_seen=None):
|
||||
"""Returns dict of predecessors for the path from source to all nodes in G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : EasyGraph graph
|
||||
|
||||
source : node label
|
||||
Starting node for path
|
||||
|
||||
target : node label, optional
|
||||
Ending node for path. If provided only predecessors between
|
||||
source and target are returned
|
||||
|
||||
cutoff : integer, optional
|
||||
Depth to stop the search. Only paths of length <= cutoff are returned.
|
||||
|
||||
return_seen : bool, optional (default=None)
|
||||
Whether to return a dictionary, keyed by node, of the level (number of
|
||||
hops) to reach the node (as seen during breadth-first-search).
|
||||
|
||||
Returns
|
||||
-------
|
||||
pred : dictionary
|
||||
Dictionary, keyed by node, of predecessors in the shortest path.
|
||||
|
||||
|
||||
(pred, seen): tuple of dictionaries
|
||||
If `return_seen` argument is set to `True`, then a tuple of dictionaries
|
||||
is returned. The first element is the dictionary, keyed by node, of
|
||||
predecessors in the shortest path. The second element is the dictionary,
|
||||
keyed by node, of the level (number of hops) to reach the node (as seen
|
||||
during breadth-first-search).
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = eg.path_graph(4)
|
||||
>>> list(G)
|
||||
[0, 1, 2, 3]
|
||||
>>> eg.predecessor(G, 0)
|
||||
{0: [], 1: [0], 2: [1], 3: [2]}
|
||||
>>> eg.predecessor(G, 0, return_seen=True)
|
||||
({0: [], 1: [0], 2: [1], 3: [2]}, {0: 0, 1: 1, 2: 2, 3: 3})
|
||||
|
||||
|
||||
"""
|
||||
|
||||
if source not in G:
|
||||
raise eg.NodeNotFound(f"Source {source} not in G")
|
||||
level = 0 # the current level
|
||||
nextlevel = [source] # list of nodes to check at next level
|
||||
seen = {source: level} # level (number of hops) when seen in BFS
|
||||
pred = {source: []} # predecessor dictionary
|
||||
while nextlevel:
|
||||
level = level + 1
|
||||
thislevel = nextlevel
|
||||
nextlevel = []
|
||||
for v in thislevel:
|
||||
for w in list(G.neighbors(v)):
|
||||
if w not in seen:
|
||||
pred[w] = [v]
|
||||
seen[w] = level
|
||||
nextlevel.append(w)
|
||||
elif seen[w] == level: # add v to predecessor list if it
|
||||
pred[w].append(v) # is at the correct level
|
||||
if cutoff and cutoff <= level:
|
||||
break
|
||||
|
||||
if target is not None:
|
||||
if return_seen:
|
||||
if target not in pred:
|
||||
return ([], -1) # No predecessor
|
||||
return (pred[target], seen[target])
|
||||
else:
|
||||
if target not in pred:
|
||||
return [] # No predecessor
|
||||
return pred[target]
|
||||
else:
|
||||
if return_seen:
|
||||
return (pred, seen)
|
||||
else:
|
||||
return pred
|
||||
|
||||
|
||||
# def main():
|
||||
# G = eg.path_graph(4)
|
||||
# print(G.edges)
|
||||
|
||||
# print(predecessor(G, 0))
|
||||
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# main()
|
||||
@@ -0,0 +1,41 @@
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
from easygraph.functions.basic import average_degree
|
||||
|
||||
|
||||
def test_average_degree_basic():
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(1, 2), (2, 3)])
|
||||
assert average_degree(G) == pytest.approx(4 / 3)
|
||||
|
||||
|
||||
def test_average_degree_empty_graph():
|
||||
G = eg.Graph()
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
average_degree(G)
|
||||
|
||||
|
||||
def test_average_degree_self_loop():
|
||||
G = eg.Graph()
|
||||
G.add_edge(1, 1) # self-loop
|
||||
# Self-loop counts as 2 towards degree of node 1
|
||||
assert average_degree(G) == pytest.approx(2.0)
|
||||
|
||||
|
||||
def test_average_degree_with_isolated_node():
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(1, 2), (2, 3)])
|
||||
G.add_node(4) # isolated node
|
||||
assert average_degree(G) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_average_degree_directed_graph():
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(1, 2), (2, 3), (3, 1)])
|
||||
assert average_degree(G) == pytest.approx(2.0)
|
||||
|
||||
|
||||
def test_average_degree_invalid_input():
|
||||
with pytest.raises(AttributeError):
|
||||
average_degree(None)
|
||||
@@ -0,0 +1,418 @@
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
|
||||
class TestClustering:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
pytest.importorskip("numpy")
|
||||
|
||||
def test_clustering(self):
|
||||
G = eg.DiGraph()
|
||||
G.add_edge("1", "2", weight=16)
|
||||
G.add_edge("2", "3", weight=16)
|
||||
G.add_edge("4", "3", weight=16)
|
||||
G.add_edge("3", "4", weight=23)
|
||||
G.add_edge("3", "5", weight=16)
|
||||
G.add_edge("4", "2", weight=20)
|
||||
print("clustering" in dir(eg))
|
||||
assert eg.clustering(G) == {
|
||||
"1": 0,
|
||||
"2": 0.3333333333333333,
|
||||
"3": 0.2,
|
||||
"4": 0.5,
|
||||
"5": 0,
|
||||
}
|
||||
|
||||
def test_path(self):
|
||||
G = eg.path_graph(10)
|
||||
assert list(eg.clustering(G).values()) == [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
assert eg.clustering(G) == {
|
||||
0: 0,
|
||||
1: 0,
|
||||
2: 0,
|
||||
3: 0,
|
||||
4: 0,
|
||||
5: 0,
|
||||
6: 0,
|
||||
7: 0,
|
||||
8: 0,
|
||||
9: 0,
|
||||
}
|
||||
|
||||
def test_k5(self):
|
||||
G = eg.complete_graph(5)
|
||||
assert list(eg.clustering(G).values()) == [1, 1, 1, 1, 1]
|
||||
assert eg.average_clustering(G) == 1
|
||||
G.remove_edge(1, 2)
|
||||
assert list(eg.clustering(G).values()) == [
|
||||
5 / 6,
|
||||
1,
|
||||
1,
|
||||
5 / 6,
|
||||
5 / 6,
|
||||
]
|
||||
assert eg.clustering(G, [1, 4]) == {1: 1, 4: 0.83333333333333337}
|
||||
|
||||
def test_k5_signed(self):
|
||||
G = eg.complete_graph(5)
|
||||
assert list(eg.clustering(G).values()) == [1, 1, 1, 1, 1]
|
||||
assert eg.average_clustering(G) == 1
|
||||
G.remove_edge(1, 2)
|
||||
G.add_edge(0, 1, weight=-1)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
1 / 6,
|
||||
-1 / 3,
|
||||
1,
|
||||
3 / 6,
|
||||
3 / 6,
|
||||
]
|
||||
|
||||
|
||||
class TestDirectedClustering:
|
||||
def test_clustering(self):
|
||||
G = eg.DiGraph()
|
||||
assert list(eg.clustering(G).values()) == []
|
||||
assert eg.clustering(G) == {}
|
||||
|
||||
def test_path(self):
|
||||
G = eg.path_graph(10, create_using=eg.DiGraph())
|
||||
assert list(eg.clustering(G).values()) == [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
assert eg.clustering(G) == {
|
||||
0: 0,
|
||||
1: 0,
|
||||
2: 0,
|
||||
3: 0,
|
||||
4: 0,
|
||||
5: 0,
|
||||
6: 0,
|
||||
7: 0,
|
||||
8: 0,
|
||||
9: 0,
|
||||
}
|
||||
assert eg.clustering(G, 0) == 0
|
||||
|
||||
def test_k5(self):
|
||||
G = eg.complete_graph(5, create_using=eg.DiGraph())
|
||||
assert list(eg.clustering(G).values()) == [1, 1, 1, 1, 1]
|
||||
assert eg.average_clustering(G) == 1
|
||||
G.remove_edge(1, 2)
|
||||
assert list(eg.clustering(G).values()) == [
|
||||
11 / 12,
|
||||
1,
|
||||
1,
|
||||
11 / 12,
|
||||
11 / 12,
|
||||
]
|
||||
assert eg.clustering(G, [1, 4]) == {1: 1, 4: 11 / 12}
|
||||
G.remove_edge(2, 1)
|
||||
assert list(eg.clustering(G).values()) == [
|
||||
5 / 6,
|
||||
1,
|
||||
1,
|
||||
5 / 6,
|
||||
5 / 6,
|
||||
]
|
||||
assert eg.clustering(G, [1, 4]) == {1: 1, 4: 0.83333333333333337}
|
||||
assert eg.clustering(G, 4) == 5 / 6
|
||||
|
||||
def test_triangle_and_edge(self):
|
||||
G = eg.empty_graph(range(3), eg.DiGraph())
|
||||
G.add_edges_from(eg.pairwise(range(3), cyclic=True))
|
||||
G.add_edge(0, 4)
|
||||
assert eg.clustering(G)[0] == 1 / 6
|
||||
|
||||
|
||||
class TestDirectedAverageClustering:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
pytest.importorskip("numpy")
|
||||
|
||||
def test_empty(self):
|
||||
G = eg.DiGraph()
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
eg.average_clustering(G)
|
||||
|
||||
def test_average_clustering(self):
|
||||
G = eg.empty_graph(range(3), eg.DiGraph())
|
||||
G.add_edges_from(eg.pairwise(range(3), cyclic=True))
|
||||
G.add_edge(2, 3)
|
||||
assert eg.average_clustering(G) == (1 + 1 + 1 / 3) / 8
|
||||
assert eg.average_clustering(G, count_zeros=True) == (1 + 1 + 1 / 3) / 8
|
||||
assert eg.average_clustering(G, count_zeros=False) == (1 + 1 + 1 / 3) / 6
|
||||
assert eg.average_clustering(G, [1, 2, 3]) == (1 + 1 / 3) / 6
|
||||
assert eg.average_clustering(G, [1, 2, 3], count_zeros=True) == (1 + 1 / 3) / 6
|
||||
assert eg.average_clustering(G, [1, 2, 3], count_zeros=False) == (1 + 1 / 3) / 4
|
||||
|
||||
|
||||
class TestAverageClustering:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
pytest.importorskip("numpy")
|
||||
|
||||
def test_empty(self):
|
||||
G = eg.Graph()
|
||||
with pytest.raises(ZeroDivisionError):
|
||||
eg.average_clustering(G)
|
||||
|
||||
def test_average_clustering(self):
|
||||
G = eg.complete_graph(3)
|
||||
G.add_edge(2, 3)
|
||||
|
||||
assert eg.average_clustering(G) == (1 + 1 + 1 / 3) / 4
|
||||
assert eg.average_clustering(G, count_zeros=True) == (1 + 1 + 1 / 3) / 4
|
||||
assert eg.average_clustering(G, count_zeros=False) == (1 + 1 + 1 / 3) / 3
|
||||
assert eg.average_clustering(G, [1, 2, 3]) == (1 + 1 / 3) / 3
|
||||
assert eg.average_clustering(G, [1, 2, 3], count_zeros=True) == (1 + 1 / 3) / 3
|
||||
assert eg.average_clustering(G, [1, 2, 3], count_zeros=False) == (1 + 1 / 3) / 2
|
||||
|
||||
def test_average_clustering_signed(self):
|
||||
G = eg.complete_graph(3)
|
||||
G.add_edge(2, 3)
|
||||
G.add_edge(0, 1, weight=-1)
|
||||
assert eg.average_clustering(G, weight="weight") == (-1 - 1 - 1 / 3) / 4
|
||||
assert (
|
||||
eg.average_clustering(G, weight="weight", count_zeros=True)
|
||||
== (-1 - 1 - 1 / 3) / 4
|
||||
)
|
||||
assert (
|
||||
eg.average_clustering(G, weight="weight", count_zeros=False)
|
||||
== (-1 - 1 - 1 / 3) / 3
|
||||
)
|
||||
|
||||
|
||||
class TestDirectedWeightedClustering:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
global np
|
||||
np = pytest.importorskip("numpy")
|
||||
|
||||
def test_clustering(self):
|
||||
G = eg.DiGraph()
|
||||
assert list(eg.clustering(G, weight="weight").values()) == []
|
||||
assert eg.clustering(G) == {}
|
||||
|
||||
def test_path(self):
|
||||
G = eg.path_graph(10, create_using=eg.DiGraph())
|
||||
print("type:", eg.clustering(G, weight="weight"))
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
assert eg.clustering(G, weight="weight") == {
|
||||
0: 0,
|
||||
1: 0,
|
||||
2: 0,
|
||||
3: 0,
|
||||
4: 0,
|
||||
5: 0,
|
||||
6: 0,
|
||||
7: 0,
|
||||
8: 0,
|
||||
9: 0,
|
||||
}
|
||||
|
||||
def test_k5(self):
|
||||
G = eg.complete_graph(5, create_using=eg.DiGraph())
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [1, 1, 1, 1, 1]
|
||||
assert eg.average_clustering(G, weight="weight") == 1
|
||||
G.remove_edge(1, 2)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
11 / 12,
|
||||
1,
|
||||
1,
|
||||
11 / 12,
|
||||
11 / 12,
|
||||
]
|
||||
assert eg.clustering(G, [1, 4], weight="weight") == {1: 1, 4: 11 / 12}
|
||||
G.remove_edge(2, 1)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
5 / 6,
|
||||
1,
|
||||
1,
|
||||
5 / 6,
|
||||
5 / 6,
|
||||
]
|
||||
assert eg.clustering(G, [1, 4], weight="weight") == {
|
||||
1: 1,
|
||||
4: 0.83333333333333337,
|
||||
}
|
||||
|
||||
def test_triangle_and_edge(self):
|
||||
G = eg.empty_graph(range(3), create_using=eg.DiGraph())
|
||||
G.add_edges_from(eg.pairwise(range(3), cyclic=True))
|
||||
G.add_edge(0, 4, weight=2)
|
||||
assert eg.clustering(G)[0] == 1 / 6
|
||||
# Relaxed comparisons to allow graphblas-algorithms to pass tests
|
||||
np.testing.assert_allclose(eg.clustering(G, weight="weight")[0], 1 / 12)
|
||||
np.testing.assert_allclose(eg.clustering(G, 0, weight="weight"), 1 / 12)
|
||||
|
||||
|
||||
class TestWeightedClustering:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
global np
|
||||
np = pytest.importorskip("numpy")
|
||||
|
||||
def test_clustering(self):
|
||||
G = eg.Graph()
|
||||
assert list(eg.clustering(G, weight="weight").values()) == []
|
||||
assert eg.clustering(G) == {}
|
||||
|
||||
def test_path(self):
|
||||
G = eg.path_graph(10)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
assert eg.clustering(G, weight="weight") == {
|
||||
0: 0,
|
||||
1: 0,
|
||||
2: 0,
|
||||
3: 0,
|
||||
4: 0,
|
||||
5: 0,
|
||||
6: 0,
|
||||
7: 0,
|
||||
8: 0,
|
||||
9: 0,
|
||||
}
|
||||
|
||||
def test_cubical(self):
|
||||
G = eg.from_dict_of_lists(
|
||||
{
|
||||
0: [1, 3, 4],
|
||||
1: [0, 2, 7],
|
||||
2: [1, 3, 6],
|
||||
3: [0, 2, 5],
|
||||
4: [0, 5, 7],
|
||||
5: [3, 4, 6],
|
||||
6: [2, 5, 7],
|
||||
7: [1, 4, 6],
|
||||
},
|
||||
create_using=None,
|
||||
)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
]
|
||||
assert eg.clustering(G, 1) == 0
|
||||
assert list(eg.clustering(G, [1, 2], weight="weight").values()) == [0, 0]
|
||||
assert eg.clustering(G, 1, weight="weight") == 0
|
||||
assert eg.clustering(G, [1, 2], weight="weight") == {1: 0, 2: 0}
|
||||
|
||||
def test_k5(self):
|
||||
G = eg.complete_graph(5)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [1, 1, 1, 1, 1]
|
||||
assert eg.average_clustering(G, weight="weight") == 1
|
||||
G.remove_edge(1, 2)
|
||||
assert list(eg.clustering(G, weight="weight").values()) == [
|
||||
5 / 6,
|
||||
1,
|
||||
1,
|
||||
5 / 6,
|
||||
5 / 6,
|
||||
]
|
||||
assert eg.clustering(G, [1, 4], weight="weight") == {
|
||||
1: 1,
|
||||
4: 0.83333333333333337,
|
||||
}
|
||||
|
||||
def test_triangle_and_edge(self):
|
||||
G = eg.empty_graph(range(3), None)
|
||||
G.add_edges_from(eg.pairwise(range(3), cyclic=True))
|
||||
G.add_edge(0, 4, weight=2)
|
||||
assert eg.clustering(G)[0] == 1 / 3
|
||||
np.testing.assert_allclose(eg.clustering(G, weight="weight")[0], 1 / 6)
|
||||
np.testing.assert_allclose(eg.clustering(G, 0, weight="weight"), 1 / 6)
|
||||
|
||||
def test_triangle_and_signed_edge(self):
|
||||
G = eg.empty_graph(range(3), None)
|
||||
G.add_edges_from(eg.pairwise(range(3), cyclic=True))
|
||||
G.add_edge(0, 1, weight=-1)
|
||||
G.add_edge(3, 0, weight=0)
|
||||
assert eg.clustering(G)[0] == 1 / 3
|
||||
assert eg.clustering(G, weight="weight")[0] == -1 / 3
|
||||
|
||||
|
||||
class TestAdditionalClusteringCases:
|
||||
def test_self_loops_ignored(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (1, 2), (2, 0)])
|
||||
G.add_edge(0, 0) # self-loop
|
||||
assert eg.clustering(G, 0) == 1.0
|
||||
|
||||
def test_isolated_node(self):
|
||||
G = eg.Graph()
|
||||
G.add_node(1)
|
||||
assert eg.clustering(G) == {1: 0}
|
||||
|
||||
def test_degree_one_node(self):
|
||||
G = eg.Graph()
|
||||
G.add_edge(1, 2)
|
||||
assert eg.clustering(G) == {1: 0, 2: 0}
|
||||
|
||||
def test_custom_weight_name(self):
|
||||
G = eg.Graph()
|
||||
G.add_edge(0, 1, strength=2)
|
||||
G.add_edge(1, 2, strength=2)
|
||||
G.add_edge(2, 0, strength=2)
|
||||
result = eg.clustering(G, weight="strength")
|
||||
assert result[0] > 0
|
||||
|
||||
def test_negative_weights_mixed(self):
|
||||
G = eg.complete_graph(3)
|
||||
G[0][1]["weight"] = -1
|
||||
G[1][2]["weight"] = 1
|
||||
G[2][0]["weight"] = 1
|
||||
assert eg.clustering(G, 0, weight="weight") < 0
|
||||
|
||||
def test_directed_reciprocal_edges(self):
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(0, 1), (1, 0), (0, 2), (2, 0), (1, 2), (2, 1)])
|
||||
result = eg.clustering(G)
|
||||
assert all(0 <= v <= 1 for v in result.values())
|
||||
@@ -0,0 +1,104 @@
|
||||
import sys
|
||||
|
||||
import easygraph as eg
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from easygraph.functions.basic.localassort import localAssort
|
||||
|
||||
|
||||
class TestLocalAssort:
|
||||
@classmethod
|
||||
def setup_class(self):
|
||||
self.G = eg.get_graph_karateclub()
|
||||
edgelist = []
|
||||
node_num = len(self.G.nodes)
|
||||
for e in self.G.edges:
|
||||
edgelist.append([e[0] - 1, e[1] - 1])
|
||||
self.edgelist = np.int32(edgelist)
|
||||
self.valuelist = np.arange(node_num, dtype=np.int32) % 6
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info.major <= 3 and sys.version_info.minor <= 7,
|
||||
reason="python version should higher than 3.7",
|
||||
)
|
||||
def test_karateclub(self):
|
||||
assortM, assortT, Z = eg.localAssort(
|
||||
self.edgelist, self.valuelist, pr=np.arange(0, 1, 0.1)
|
||||
)
|
||||
|
||||
_, assortT, Z = eg.functions.basic.localassort.localAssort(
|
||||
self.edgelist, self.valuelist, pr=np.array([0.9])
|
||||
)
|
||||
|
||||
|
||||
def test_localassort_small_complete_graph():
|
||||
G = eg.complete_graph(4)
|
||||
edgelist = np.array(list(G.edges))
|
||||
node_attr = np.array([0, 0, 1, 1])
|
||||
assortM, assortT, Z = localAssort(edgelist, node_attr)
|
||||
assert assortM.shape == (4, 10)
|
||||
assert assortT.shape == (4,)
|
||||
assert Z.shape == (4,)
|
||||
assert np.all(Z >= 0) and np.all(Z <= 1)
|
||||
|
||||
|
||||
def test_localassort_with_missing_attributes():
|
||||
G = eg.path_graph(5)
|
||||
edgelist = np.array(list(G.edges))
|
||||
node_attr = np.array([0, -1, 1, -1, 1])
|
||||
assortM, assortT, Z = localAssort(edgelist, node_attr, pr=np.array([0.5]))
|
||||
assert assortT.shape == (5,)
|
||||
assert Z.shape == (5,)
|
||||
assert np.any(np.isnan(assortT))
|
||||
|
||||
|
||||
def test_localassort_directed_graph():
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
edgelist = np.array(list(G.edges))
|
||||
node_attr = np.array([0, 1, 0, 1])
|
||||
assortM, assortT, Z = localAssort(edgelist, node_attr, undir=False)
|
||||
assert assortM.shape == (4, 10)
|
||||
assert assortT.shape == (4,)
|
||||
assert Z.shape == (4,)
|
||||
|
||||
|
||||
def test_localassort_single_node_graph():
|
||||
edgelist = np.empty((0, 2), dtype=int)
|
||||
node_attr = np.array([0])
|
||||
assortM, assortT, Z = localAssort(edgelist, node_attr)
|
||||
assert assortM.shape == (1, 10)
|
||||
assert np.all(np.isnan(assortM)) or np.allclose(assortM, 0, atol=1e-5)
|
||||
assert np.all(np.isnan(assortT)) or np.allclose(assortT, 0, atol=1e-5)
|
||||
assert np.all(np.isnan(Z)) or np.allclose(Z, 0, atol=1e-5)
|
||||
|
||||
|
||||
def test_localassort_disconnected_graph():
|
||||
G = eg.Graph()
|
||||
G.add_nodes_from(range(5))
|
||||
edgelist = np.empty((0, 2), dtype=int)
|
||||
node_attr = np.array([0, 1, 0, 1, 1])
|
||||
assortM, assortT, Z = localAssort(edgelist, node_attr)
|
||||
assert assortM.shape == (5, 10)
|
||||
assert np.all(np.isnan(assortM)) or np.allclose(assortM, 0, atol=1e-5)
|
||||
assert np.all(np.isnan(assortT)) or np.allclose(assortT, 0, atol=1e-5)
|
||||
assert np.all(np.isnan(Z)) or np.allclose(Z, 0, atol=1e-5)
|
||||
|
||||
|
||||
def test_localassort_high_restart_probabilities():
|
||||
G = eg.path_graph(5)
|
||||
edgelist = np.array(list(G.edges))
|
||||
node_attr = np.array([1, 0, 1, 0, 1])
|
||||
pr = np.array([0.95, 0.99])
|
||||
assortM, assortT, Z = localAssort(edgelist, node_attr, pr=pr)
|
||||
assert assortM.shape == (5, 2)
|
||||
assert assortT.shape == (5,)
|
||||
assert Z.shape == (5,)
|
||||
|
||||
|
||||
def test_localassort_invalid_attribute_length():
|
||||
edgelist = np.array([[0, 1], [1, 2]])
|
||||
node_attr = np.array([0, 1]) # too short
|
||||
with pytest.raises(ValueError):
|
||||
localAssort(edgelist, node_attr)
|
||||
@@ -0,0 +1,79 @@
|
||||
import easygraph as eg
|
||||
import pytest
|
||||
|
||||
|
||||
class TestPredecessor:
|
||||
# @classmethod
|
||||
# def setup_class(self):
|
||||
# pytest.importskip("numpy")
|
||||
|
||||
def test_predecessor(self):
|
||||
G = eg.path_graph(4)
|
||||
for source in G:
|
||||
assert eg.predecessor(G, source) in [
|
||||
{0: [], 1: [0], 2: [1], 3: [2]},
|
||||
{1: [], 0: [1], 2: [1], 3: [2]},
|
||||
{2: [], 1: [2], 3: [2], 0: [1]},
|
||||
{3: [], 2: [3], 1: [2], 0: [1]},
|
||||
]
|
||||
|
||||
def test_basic_predecessor(self):
|
||||
G = eg.path_graph(4)
|
||||
result = eg.predecessor(G, 0)
|
||||
assert result == {0: [], 1: [0], 2: [1], 3: [2]}
|
||||
|
||||
def test_with_return_seen(self):
|
||||
G = eg.path_graph(4)
|
||||
pred, seen = eg.predecessor(G, 0, return_seen=True)
|
||||
assert pred == {0: [], 1: [0], 2: [1], 3: [2]}
|
||||
assert seen == {0: 0, 1: 1, 2: 2, 3: 3}
|
||||
|
||||
def test_with_target(self):
|
||||
G = eg.path_graph(4)
|
||||
assert eg.predecessor(G, 0, target=2) == [1]
|
||||
|
||||
def test_with_target_and_return_seen(self):
|
||||
G = eg.path_graph(4)
|
||||
pred, seen = eg.predecessor(G, 0, target=2, return_seen=True)
|
||||
assert pred == [1]
|
||||
assert seen == 2
|
||||
|
||||
def test_with_cutoff(self):
|
||||
G = eg.path_graph(4)
|
||||
pred = eg.predecessor(G, 0, cutoff=1)
|
||||
assert pred == {0: [], 1: [0]}
|
||||
|
||||
def test_disconnected_graph(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (2, 3)])
|
||||
pred = eg.predecessor(G, 0)
|
||||
assert 2 not in pred and 3 not in pred
|
||||
|
||||
def test_invalid_source(self):
|
||||
G = eg.path_graph(4)
|
||||
with pytest.raises(eg.NodeNotFound):
|
||||
eg.predecessor(G, 99)
|
||||
|
||||
def test_no_path_to_target(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (2, 3)])
|
||||
assert eg.predecessor(G, 0, target=3) == []
|
||||
|
||||
def test_no_path_to_target_with_return_seen(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (2, 3)])
|
||||
pred, seen = eg.predecessor(G, 0, target=3, return_seen=True)
|
||||
assert pred == []
|
||||
assert seen == -1
|
||||
|
||||
def test_cycle_graph(self):
|
||||
G = eg.Graph()
|
||||
G.add_edges_from([(0, 1), (1, 2), (2, 3), (3, 0)]) # cycled graph
|
||||
pred = eg.predecessor(G, 0)
|
||||
assert set(pred.keys()) == set(G.nodes)
|
||||
|
||||
def test_directed_graph(self):
|
||||
G = eg.DiGraph()
|
||||
G.add_edges_from([(0, 1), (1, 2), (2, 3)])
|
||||
pred = eg.predecessor(G, 0)
|
||||
assert pred == {0: [], 1: [0], 2: [1], 3: [2]}
|
||||
Reference in New Issue
Block a user