update
This commit is contained in:
604
.CondaPkg/env/Lib/site-packages/networkx/algorithms/triads.py
vendored
Normal file
604
.CondaPkg/env/Lib/site-packages/networkx/algorithms/triads.py
vendored
Normal file
@@ -0,0 +1,604 @@
|
||||
# See https://github.com/networkx/networkx/pull/1474
|
||||
# Copyright 2011 Reya Group <http://www.reyagroup.com>
|
||||
# Copyright 2011 Alex Levenson <alex@isnotinvain.com>
|
||||
# Copyright 2011 Diederik van Liere <diederik.vanliere@rotman.utoronto.ca>
|
||||
"""Functions for analyzing triads of a graph."""
|
||||
|
||||
from collections import defaultdict
|
||||
from itertools import combinations, permutations
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import not_implemented_for, py_random_state
|
||||
|
||||
__all__ = [
|
||||
"triadic_census",
|
||||
"is_triad",
|
||||
"all_triplets",
|
||||
"all_triads",
|
||||
"triads_by_type",
|
||||
"triad_type",
|
||||
"random_triad",
|
||||
]
|
||||
|
||||
#: The integer codes representing each type of triad.
|
||||
#:
|
||||
#: Triads that are the same up to symmetry have the same code.
|
||||
TRICODES = (
|
||||
1,
|
||||
2,
|
||||
2,
|
||||
3,
|
||||
2,
|
||||
4,
|
||||
6,
|
||||
8,
|
||||
2,
|
||||
6,
|
||||
5,
|
||||
7,
|
||||
3,
|
||||
8,
|
||||
7,
|
||||
11,
|
||||
2,
|
||||
6,
|
||||
4,
|
||||
8,
|
||||
5,
|
||||
9,
|
||||
9,
|
||||
13,
|
||||
6,
|
||||
10,
|
||||
9,
|
||||
14,
|
||||
7,
|
||||
14,
|
||||
12,
|
||||
15,
|
||||
2,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
6,
|
||||
9,
|
||||
10,
|
||||
14,
|
||||
4,
|
||||
9,
|
||||
9,
|
||||
12,
|
||||
8,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
3,
|
||||
7,
|
||||
8,
|
||||
11,
|
||||
7,
|
||||
12,
|
||||
14,
|
||||
15,
|
||||
8,
|
||||
14,
|
||||
13,
|
||||
15,
|
||||
11,
|
||||
15,
|
||||
15,
|
||||
16,
|
||||
)
|
||||
|
||||
#: The names of each type of triad. The order of the elements is
|
||||
#: important: it corresponds to the tricodes given in :data:`TRICODES`.
|
||||
TRIAD_NAMES = (
|
||||
"003",
|
||||
"012",
|
||||
"102",
|
||||
"021D",
|
||||
"021U",
|
||||
"021C",
|
||||
"111D",
|
||||
"111U",
|
||||
"030T",
|
||||
"030C",
|
||||
"201",
|
||||
"120D",
|
||||
"120U",
|
||||
"120C",
|
||||
"210",
|
||||
"300",
|
||||
)
|
||||
|
||||
|
||||
#: A dictionary mapping triad code to triad name.
|
||||
TRICODE_TO_NAME = {i: TRIAD_NAMES[code - 1] for i, code in enumerate(TRICODES)}
|
||||
|
||||
|
||||
def _tricode(G, v, u, w):
|
||||
"""Returns the integer code of the given triad.
|
||||
|
||||
This is some fancy magic that comes from Batagelj and Mrvar's paper. It
|
||||
treats each edge joining a pair of `v`, `u`, and `w` as a bit in
|
||||
the binary representation of an integer.
|
||||
|
||||
"""
|
||||
combos = ((v, u, 1), (u, v, 2), (v, w, 4), (w, v, 8), (u, w, 16), (w, u, 32))
|
||||
return sum(x for u, v, x in combos if v in G[u])
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
@nx._dispatchable
|
||||
def triadic_census(G, nodelist=None):
|
||||
"""Determines the triadic census of a directed graph.
|
||||
|
||||
The triadic census is a count of how many of the 16 possible types of
|
||||
triads are present in a directed graph. If a list of nodes is passed, then
|
||||
only those triads are taken into account which have elements of nodelist in them.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : digraph
|
||||
A NetworkX DiGraph
|
||||
nodelist : list
|
||||
List of nodes for which you want to calculate triadic census
|
||||
|
||||
Returns
|
||||
-------
|
||||
census : dict
|
||||
Dictionary with triad type as keys and number of occurrences as values.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1), (3, 4), (4, 1), (4, 2)])
|
||||
>>> triadic_census = nx.triadic_census(G)
|
||||
>>> for key, value in triadic_census.items():
|
||||
... print(f"{key}: {value}")
|
||||
003: 0
|
||||
012: 0
|
||||
102: 0
|
||||
021D: 0
|
||||
021U: 0
|
||||
021C: 0
|
||||
111D: 0
|
||||
111U: 0
|
||||
030T: 2
|
||||
030C: 2
|
||||
201: 0
|
||||
120D: 0
|
||||
120U: 0
|
||||
120C: 0
|
||||
210: 0
|
||||
300: 0
|
||||
|
||||
Notes
|
||||
-----
|
||||
This algorithm has complexity $O(m)$ where $m$ is the number of edges in
|
||||
the graph.
|
||||
|
||||
For undirected graphs, the triadic census can be computed by first converting
|
||||
the graph into a directed graph using the ``G.to_directed()`` method.
|
||||
After this conversion, only the triad types 003, 102, 201 and 300 will be
|
||||
present in the undirected scenario.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If `nodelist` contains duplicate nodes or nodes not in `G`.
|
||||
If you want to ignore this you can preprocess with `set(nodelist) & G.nodes`
|
||||
|
||||
See also
|
||||
--------
|
||||
triad_graph
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Vladimir Batagelj and Andrej Mrvar, A subquadratic triad census
|
||||
algorithm for large sparse networks with small maximum degree,
|
||||
University of Ljubljana,
|
||||
http://vlado.fmf.uni-lj.si/pub/networks/doc/triads/triads.pdf
|
||||
|
||||
"""
|
||||
nodeset = set(G.nbunch_iter(nodelist))
|
||||
if nodelist is not None and len(nodelist) != len(nodeset):
|
||||
raise ValueError("nodelist includes duplicate nodes or nodes not in G")
|
||||
|
||||
N = len(G)
|
||||
Nnot = N - len(nodeset) # can signal special counting for subset of nodes
|
||||
|
||||
# create an ordering of nodes with nodeset nodes first
|
||||
m = {n: i for i, n in enumerate(nodeset)}
|
||||
if Nnot:
|
||||
# add non-nodeset nodes later in the ordering
|
||||
not_nodeset = G.nodes - nodeset
|
||||
m.update((n, i + N) for i, n in enumerate(not_nodeset))
|
||||
|
||||
# build all_neighbor dicts for easy counting
|
||||
# After Python 3.8 can leave off these keys(). Speedup also using G._pred
|
||||
# nbrs = {n: G._pred[n].keys() | G._succ[n].keys() for n in G}
|
||||
nbrs = {n: G.pred[n].keys() | G.succ[n].keys() for n in G}
|
||||
dbl_nbrs = {n: G.pred[n].keys() & G.succ[n].keys() for n in G}
|
||||
|
||||
if Nnot:
|
||||
sgl_nbrs = {n: G.pred[n].keys() ^ G.succ[n].keys() for n in not_nodeset}
|
||||
# find number of edges not incident to nodes in nodeset
|
||||
sgl = sum(1 for n in not_nodeset for nbr in sgl_nbrs[n] if nbr not in nodeset)
|
||||
sgl_edges_outside = sgl // 2
|
||||
dbl = sum(1 for n in not_nodeset for nbr in dbl_nbrs[n] if nbr not in nodeset)
|
||||
dbl_edges_outside = dbl // 2
|
||||
|
||||
# Initialize the count for each triad to be zero.
|
||||
census = {name: 0 for name in TRIAD_NAMES}
|
||||
# Main loop over nodes
|
||||
for v in nodeset:
|
||||
vnbrs = nbrs[v]
|
||||
dbl_vnbrs = dbl_nbrs[v]
|
||||
if Nnot:
|
||||
# set up counts of edges attached to v.
|
||||
sgl_unbrs_bdy = sgl_unbrs_out = dbl_unbrs_bdy = dbl_unbrs_out = 0
|
||||
for u in vnbrs:
|
||||
if m[u] <= m[v]:
|
||||
continue
|
||||
unbrs = nbrs[u]
|
||||
neighbors = (vnbrs | unbrs) - {u, v}
|
||||
# Count connected triads.
|
||||
for w in neighbors:
|
||||
if m[u] < m[w] or (m[v] < m[w] < m[u] and v not in nbrs[w]):
|
||||
code = _tricode(G, v, u, w)
|
||||
census[TRICODE_TO_NAME[code]] += 1
|
||||
|
||||
# Use a formula for dyadic triads with edge incident to v
|
||||
if u in dbl_vnbrs:
|
||||
census["102"] += N - len(neighbors) - 2
|
||||
else:
|
||||
census["012"] += N - len(neighbors) - 2
|
||||
|
||||
# Count edges attached to v. Subtract later to get triads with v isolated
|
||||
# _out are (u,unbr) for unbrs outside boundary of nodeset
|
||||
# _bdy are (u,unbr) for unbrs on boundary of nodeset (get double counted)
|
||||
if Nnot and u not in nodeset:
|
||||
sgl_unbrs = sgl_nbrs[u]
|
||||
sgl_unbrs_bdy += len(sgl_unbrs & vnbrs - nodeset)
|
||||
sgl_unbrs_out += len(sgl_unbrs - vnbrs - nodeset)
|
||||
dbl_unbrs = dbl_nbrs[u]
|
||||
dbl_unbrs_bdy += len(dbl_unbrs & vnbrs - nodeset)
|
||||
dbl_unbrs_out += len(dbl_unbrs - vnbrs - nodeset)
|
||||
# if nodeset == G.nodes, skip this b/c we will find the edge later.
|
||||
if Nnot:
|
||||
# Count edges outside nodeset not connected with v (v isolated triads)
|
||||
census["012"] += sgl_edges_outside - (sgl_unbrs_out + sgl_unbrs_bdy // 2)
|
||||
census["102"] += dbl_edges_outside - (dbl_unbrs_out + dbl_unbrs_bdy // 2)
|
||||
|
||||
# calculate null triads: "003"
|
||||
# null triads = total number of possible triads - all found triads
|
||||
total_triangles = (N * (N - 1) * (N - 2)) // 6
|
||||
triangles_without_nodeset = (Nnot * (Nnot - 1) * (Nnot - 2)) // 6
|
||||
total_census = total_triangles - triangles_without_nodeset
|
||||
census["003"] = total_census - sum(census.values())
|
||||
|
||||
return census
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def is_triad(G):
|
||||
"""Returns True if the graph G is a triad, else False.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX Graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
istriad : boolean
|
||||
Whether G is a valid triad
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1)])
|
||||
>>> nx.is_triad(G)
|
||||
True
|
||||
>>> G.add_edge(0, 1)
|
||||
>>> nx.is_triad(G)
|
||||
False
|
||||
"""
|
||||
if isinstance(G, nx.Graph):
|
||||
if G.order() == 3 and nx.is_directed(G):
|
||||
if not any((n, n) in G.edges() for n in G.nodes()):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
@nx._dispatchable
|
||||
def all_triplets(G):
|
||||
"""Returns a generator of all possible sets of 3 nodes in a DiGraph.
|
||||
|
||||
.. deprecated:: 3.3
|
||||
|
||||
all_triplets is deprecated and will be removed in NetworkX version 3.5.
|
||||
Use `itertools.combinations` instead::
|
||||
|
||||
all_triplets = itertools.combinations(G, 3)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : digraph
|
||||
A NetworkX DiGraph
|
||||
|
||||
Returns
|
||||
-------
|
||||
triplets : generator of 3-tuples
|
||||
Generator of tuples of 3 nodes
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.DiGraph([(1, 2), (2, 3), (3, 4)])
|
||||
>>> list(nx.all_triplets(G))
|
||||
[(1, 2, 3), (1, 2, 4), (1, 3, 4), (2, 3, 4)]
|
||||
|
||||
"""
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
(
|
||||
"\n\nall_triplets is deprecated and will be rmoved in v3.5.\n"
|
||||
"Use `itertools.combinations(G, 3)` instead."
|
||||
),
|
||||
category=DeprecationWarning,
|
||||
stacklevel=4,
|
||||
)
|
||||
triplets = combinations(G.nodes(), 3)
|
||||
return triplets
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
@nx._dispatchable(returns_graph=True)
|
||||
def all_triads(G):
|
||||
"""A generator of all possible triads in G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : digraph
|
||||
A NetworkX DiGraph
|
||||
|
||||
Returns
|
||||
-------
|
||||
all_triads : generator of DiGraphs
|
||||
Generator of triads (order-3 DiGraphs)
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1), (3, 4), (4, 1), (4, 2)])
|
||||
>>> for triad in nx.all_triads(G):
|
||||
... print(triad.edges)
|
||||
[(1, 2), (2, 3), (3, 1)]
|
||||
[(1, 2), (4, 1), (4, 2)]
|
||||
[(3, 1), (3, 4), (4, 1)]
|
||||
[(2, 3), (3, 4), (4, 2)]
|
||||
|
||||
"""
|
||||
triplets = combinations(G.nodes(), 3)
|
||||
for triplet in triplets:
|
||||
yield G.subgraph(triplet).copy()
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
@nx._dispatchable
|
||||
def triads_by_type(G):
|
||||
"""Returns a list of all triads for each triad type in a directed graph.
|
||||
There are exactly 16 different types of triads possible. Suppose 1, 2, 3 are three
|
||||
nodes, they will be classified as a particular triad type if their connections
|
||||
are as follows:
|
||||
|
||||
- 003: 1, 2, 3
|
||||
- 012: 1 -> 2, 3
|
||||
- 102: 1 <-> 2, 3
|
||||
- 021D: 1 <- 2 -> 3
|
||||
- 021U: 1 -> 2 <- 3
|
||||
- 021C: 1 -> 2 -> 3
|
||||
- 111D: 1 <-> 2 <- 3
|
||||
- 111U: 1 <-> 2 -> 3
|
||||
- 030T: 1 -> 2 -> 3, 1 -> 3
|
||||
- 030C: 1 <- 2 <- 3, 1 -> 3
|
||||
- 201: 1 <-> 2 <-> 3
|
||||
- 120D: 1 <- 2 -> 3, 1 <-> 3
|
||||
- 120U: 1 -> 2 <- 3, 1 <-> 3
|
||||
- 120C: 1 -> 2 -> 3, 1 <-> 3
|
||||
- 210: 1 -> 2 <-> 3, 1 <-> 3
|
||||
- 300: 1 <-> 2 <-> 3, 1 <-> 3
|
||||
|
||||
Refer to the :doc:`example gallery </auto_examples/graph/plot_triad_types>`
|
||||
for visual examples of the triad types.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : digraph
|
||||
A NetworkX DiGraph
|
||||
|
||||
Returns
|
||||
-------
|
||||
tri_by_type : dict
|
||||
Dictionary with triad types as keys and lists of triads as values.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.DiGraph([(1, 2), (1, 3), (2, 3), (3, 1), (5, 6), (5, 4), (6, 7)])
|
||||
>>> dict = nx.triads_by_type(G)
|
||||
>>> dict["120C"][0].edges()
|
||||
OutEdgeView([(1, 2), (1, 3), (2, 3), (3, 1)])
|
||||
>>> dict["012"][0].edges()
|
||||
OutEdgeView([(1, 2)])
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Snijders, T. (2012). "Transitivity and triads." University of
|
||||
Oxford.
|
||||
https://web.archive.org/web/20170830032057/http://www.stats.ox.ac.uk/~snijders/Trans_Triads_ha.pdf
|
||||
"""
|
||||
# num_triads = o * (o - 1) * (o - 2) // 6
|
||||
# if num_triads > TRIAD_LIMIT: print(WARNING)
|
||||
all_tri = all_triads(G)
|
||||
tri_by_type = defaultdict(list)
|
||||
for triad in all_tri:
|
||||
name = triad_type(triad)
|
||||
tri_by_type[name].append(triad)
|
||||
return tri_by_type
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
@nx._dispatchable
|
||||
def triad_type(G):
|
||||
"""Returns the sociological triad type for a triad.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : digraph
|
||||
A NetworkX DiGraph with 3 nodes
|
||||
|
||||
Returns
|
||||
-------
|
||||
triad_type : str
|
||||
A string identifying the triad type
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.DiGraph([(1, 2), (2, 3), (3, 1)])
|
||||
>>> nx.triad_type(G)
|
||||
'030C'
|
||||
>>> G.add_edge(1, 3)
|
||||
>>> nx.triad_type(G)
|
||||
'120C'
|
||||
|
||||
Notes
|
||||
-----
|
||||
There can be 6 unique edges in a triad (order-3 DiGraph) (so 2^^6=64 unique
|
||||
triads given 3 nodes). These 64 triads each display exactly 1 of 16
|
||||
topologies of triads (topologies can be permuted). These topologies are
|
||||
identified by the following notation:
|
||||
|
||||
{m}{a}{n}{type} (for example: 111D, 210, 102)
|
||||
|
||||
Here:
|
||||
|
||||
{m} = number of mutual ties (takes 0, 1, 2, 3); a mutual tie is (0,1)
|
||||
AND (1,0)
|
||||
{a} = number of asymmetric ties (takes 0, 1, 2, 3); an asymmetric tie
|
||||
is (0,1) BUT NOT (1,0) or vice versa
|
||||
{n} = number of null ties (takes 0, 1, 2, 3); a null tie is NEITHER
|
||||
(0,1) NOR (1,0)
|
||||
{type} = a letter (takes U, D, C, T) corresponding to up, down, cyclical
|
||||
and transitive. This is only used for topologies that can have
|
||||
more than one form (eg: 021D and 021U).
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Snijders, T. (2012). "Transitivity and triads." University of
|
||||
Oxford.
|
||||
https://web.archive.org/web/20170830032057/http://www.stats.ox.ac.uk/~snijders/Trans_Triads_ha.pdf
|
||||
"""
|
||||
if not is_triad(G):
|
||||
raise nx.NetworkXAlgorithmError("G is not a triad (order-3 DiGraph)")
|
||||
num_edges = len(G.edges())
|
||||
if num_edges == 0:
|
||||
return "003"
|
||||
elif num_edges == 1:
|
||||
return "012"
|
||||
elif num_edges == 2:
|
||||
e1, e2 = G.edges()
|
||||
if set(e1) == set(e2):
|
||||
return "102"
|
||||
elif e1[0] == e2[0]:
|
||||
return "021D"
|
||||
elif e1[1] == e2[1]:
|
||||
return "021U"
|
||||
elif e1[1] == e2[0] or e2[1] == e1[0]:
|
||||
return "021C"
|
||||
elif num_edges == 3:
|
||||
for e1, e2, e3 in permutations(G.edges(), 3):
|
||||
if set(e1) == set(e2):
|
||||
if e3[0] in e1:
|
||||
return "111U"
|
||||
# e3[1] in e1:
|
||||
return "111D"
|
||||
elif set(e1).symmetric_difference(set(e2)) == set(e3):
|
||||
if {e1[0], e2[0], e3[0]} == {e1[0], e2[0], e3[0]} == set(G.nodes()):
|
||||
return "030C"
|
||||
# e3 == (e1[0], e2[1]) and e2 == (e1[1], e3[1]):
|
||||
return "030T"
|
||||
elif num_edges == 4:
|
||||
for e1, e2, e3, e4 in permutations(G.edges(), 4):
|
||||
if set(e1) == set(e2):
|
||||
# identify pair of symmetric edges (which necessarily exists)
|
||||
if set(e3) == set(e4):
|
||||
return "201"
|
||||
if {e3[0]} == {e4[0]} == set(e3).intersection(set(e4)):
|
||||
return "120D"
|
||||
if {e3[1]} == {e4[1]} == set(e3).intersection(set(e4)):
|
||||
return "120U"
|
||||
if e3[1] == e4[0]:
|
||||
return "120C"
|
||||
elif num_edges == 5:
|
||||
return "210"
|
||||
elif num_edges == 6:
|
||||
return "300"
|
||||
|
||||
|
||||
@not_implemented_for("undirected")
|
||||
@py_random_state(1)
|
||||
@nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
|
||||
def random_triad(G, seed=None):
|
||||
"""Returns a random triad from a directed graph.
|
||||
|
||||
.. deprecated:: 3.3
|
||||
|
||||
random_triad is deprecated and will be removed in version 3.5.
|
||||
Use random sampling directly instead::
|
||||
|
||||
G.subgraph(random.sample(list(G), 3))
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : digraph
|
||||
A NetworkX DiGraph
|
||||
seed : integer, random_state, or None (default)
|
||||
Indicator of random number generation state.
|
||||
See :ref:`Randomness<randomness>`.
|
||||
|
||||
Returns
|
||||
-------
|
||||
G2 : subgraph
|
||||
A randomly selected triad (order-3 NetworkX DiGraph)
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXError
|
||||
If the input Graph has less than 3 nodes.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.DiGraph([(1, 2), (1, 3), (2, 3), (3, 1), (5, 6), (5, 4), (6, 7)])
|
||||
>>> triad = nx.random_triad(G, seed=1)
|
||||
>>> triad.edges
|
||||
OutEdgeView([(1, 2)])
|
||||
|
||||
"""
|
||||
import warnings
|
||||
|
||||
warnings.warn(
|
||||
(
|
||||
"\n\nrandom_triad is deprecated and will be removed in NetworkX v3.5.\n"
|
||||
"Use random.sample instead, e.g.::\n\n"
|
||||
"\tG.subgraph(random.sample(list(G), 3))\n"
|
||||
),
|
||||
category=DeprecationWarning,
|
||||
stacklevel=5,
|
||||
)
|
||||
if len(G) < 3:
|
||||
raise nx.NetworkXError(
|
||||
f"G needs at least 3 nodes to form a triad; (it has {len(G)} nodes)"
|
||||
)
|
||||
nodes = seed.sample(list(G.nodes()), 3)
|
||||
G2 = G.subgraph(nodes)
|
||||
return G2
|
||||
Reference in New Issue
Block a user