Restore 0.1.5 version from stash
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
r"""This module provides functions and operations for bipartite
|
||||
graphs. Bipartite graphs `B = (U, V, E)` have two node sets `U,V` and edges in
|
||||
`E` that only connect nodes from opposite sets. It is common in the literature
|
||||
to use an spatial analogy referring to the two node sets as top and bottom nodes.
|
||||
|
||||
The bipartite algorithms are not imported into the networkx namespace
|
||||
at the top level so the easiest way to use them is with:
|
||||
|
||||
>>> from networkx.algorithms import bipartite
|
||||
|
||||
NetworkX does not have a custom bipartite graph class but the Graph()
|
||||
or DiGraph() classes can be used to represent bipartite graphs. However,
|
||||
you have to keep track of which set each node belongs to, and make
|
||||
sure that there is no edge between nodes of the same set. The convention used
|
||||
in NetworkX is to use a node attribute named `bipartite` with values 0 or 1 to
|
||||
identify the sets each node belongs to. This convention is not enforced in
|
||||
the source code of bipartite functions, it's only a recommendation.
|
||||
|
||||
For example:
|
||||
|
||||
>>> B = nx.Graph()
|
||||
>>> # Add nodes with the node attribute "bipartite"
|
||||
>>> B.add_nodes_from([1, 2, 3, 4], bipartite=0)
|
||||
>>> B.add_nodes_from(["a", "b", "c"], bipartite=1)
|
||||
>>> # Add edges only between nodes of opposite node sets
|
||||
>>> B.add_edges_from([(1, "a"), (1, "b"), (2, "b"), (2, "c"), (3, "c"), (4, "a")])
|
||||
|
||||
Many algorithms of the bipartite module of NetworkX require, as an argument, a
|
||||
container with all the nodes that belong to one set, in addition to the bipartite
|
||||
graph `B`. The functions in the bipartite package do not check that the node set
|
||||
is actually correct nor that the input graph is actually bipartite.
|
||||
If `B` is connected, you can find the two node sets using a two-coloring
|
||||
algorithm:
|
||||
|
||||
>>> nx.is_connected(B)
|
||||
True
|
||||
>>> bottom_nodes, top_nodes = bipartite.sets(B)
|
||||
|
||||
However, if the input graph is not connected, there are more than one possible
|
||||
colorations. This is the reason why we require the user to pass a container
|
||||
with all nodes of one bipartite node set as an argument to most bipartite
|
||||
functions. In the face of ambiguity, we refuse the temptation to guess and
|
||||
raise an :exc:`AmbiguousSolution <networkx.AmbiguousSolution>`
|
||||
Exception if the input graph for
|
||||
:func:`bipartite.sets <networkx.algorithms.bipartite.basic.sets>`
|
||||
is disconnected.
|
||||
|
||||
Using the `bipartite` node attribute, you can easily get the two node sets:
|
||||
|
||||
>>> top_nodes = {n for n, d in B.nodes(data=True) if d["bipartite"] == 0}
|
||||
>>> bottom_nodes = set(B) - top_nodes
|
||||
|
||||
So you can easily use the bipartite algorithms that require, as an argument, a
|
||||
container with all nodes that belong to one node set:
|
||||
|
||||
>>> print(round(bipartite.density(B, bottom_nodes), 2))
|
||||
0.5
|
||||
>>> G = bipartite.projected_graph(B, top_nodes)
|
||||
|
||||
All bipartite graph generators in NetworkX build bipartite graphs with the
|
||||
`bipartite` node attribute. Thus, you can use the same approach:
|
||||
|
||||
>>> RB = bipartite.random_graph(5, 7, 0.2)
|
||||
>>> RB_top = {n for n, d in RB.nodes(data=True) if d["bipartite"] == 0}
|
||||
>>> RB_bottom = set(RB) - RB_top
|
||||
>>> list(RB_top)
|
||||
[0, 1, 2, 3, 4]
|
||||
>>> list(RB_bottom)
|
||||
[5, 6, 7, 8, 9, 10, 11]
|
||||
|
||||
For other bipartite graph generators see
|
||||
:mod:`Generators <networkx.algorithms.bipartite.generators>`.
|
||||
|
||||
"""
|
||||
|
||||
from networkx.algorithms.bipartite.basic import *
|
||||
from networkx.algorithms.bipartite.centrality import *
|
||||
from networkx.algorithms.bipartite.cluster import *
|
||||
from networkx.algorithms.bipartite.covering import *
|
||||
from networkx.algorithms.bipartite.edgelist import *
|
||||
from networkx.algorithms.bipartite.matching import *
|
||||
from networkx.algorithms.bipartite.matrix import *
|
||||
from networkx.algorithms.bipartite.projection import *
|
||||
from networkx.algorithms.bipartite.redundancy import *
|
||||
from networkx.algorithms.bipartite.spectral import *
|
||||
from networkx.algorithms.bipartite.generators import *
|
||||
from networkx.algorithms.bipartite.extendability import *
|
||||
from networkx.algorithms.bipartite.link_analysis import *
|
||||
@@ -0,0 +1,322 @@
|
||||
"""
|
||||
==========================
|
||||
Bipartite Graph Algorithms
|
||||
==========================
|
||||
"""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.components import connected_components
|
||||
from networkx.exception import AmbiguousSolution
|
||||
|
||||
__all__ = [
|
||||
"is_bipartite",
|
||||
"is_bipartite_node_set",
|
||||
"color",
|
||||
"sets",
|
||||
"density",
|
||||
"degrees",
|
||||
]
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def color(G):
|
||||
"""Returns a two-coloring of the graph.
|
||||
|
||||
Raises an exception if the graph is not bipartite.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
color : dictionary
|
||||
A dictionary keyed by node with a 1 or 0 as data for each node color.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXError
|
||||
If the graph is not two-colorable.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> G = nx.path_graph(4)
|
||||
>>> c = bipartite.color(G)
|
||||
>>> print(c)
|
||||
{0: 1, 1: 0, 2: 1, 3: 0}
|
||||
|
||||
You can use this to set a node attribute indicating the bipartite set:
|
||||
|
||||
>>> nx.set_node_attributes(G, c, "bipartite")
|
||||
>>> print(G.nodes[0]["bipartite"])
|
||||
1
|
||||
>>> print(G.nodes[1]["bipartite"])
|
||||
0
|
||||
"""
|
||||
if G.is_directed():
|
||||
import itertools
|
||||
|
||||
def neighbors(v):
|
||||
return itertools.chain.from_iterable([G.predecessors(v), G.successors(v)])
|
||||
|
||||
else:
|
||||
neighbors = G.neighbors
|
||||
|
||||
color = {}
|
||||
for n in G: # handle disconnected graphs
|
||||
if n in color or len(G[n]) == 0: # skip isolates
|
||||
continue
|
||||
queue = [n]
|
||||
color[n] = 1 # nodes seen with color (1 or 0)
|
||||
while queue:
|
||||
v = queue.pop()
|
||||
c = 1 - color[v] # opposite color of node v
|
||||
for w in neighbors(v):
|
||||
if w in color:
|
||||
if color[w] == color[v]:
|
||||
raise nx.NetworkXError("Graph is not bipartite.")
|
||||
else:
|
||||
color[w] = c
|
||||
queue.append(w)
|
||||
# color isolates with 0
|
||||
color.update(dict.fromkeys(nx.isolates(G), 0))
|
||||
return color
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def is_bipartite(G):
|
||||
"""Returns True if graph G is bipartite, False if not.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> G = nx.path_graph(4)
|
||||
>>> print(bipartite.is_bipartite(G))
|
||||
True
|
||||
|
||||
See Also
|
||||
--------
|
||||
color, is_bipartite_node_set
|
||||
"""
|
||||
try:
|
||||
color(G)
|
||||
return True
|
||||
except nx.NetworkXError:
|
||||
return False
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def is_bipartite_node_set(G, nodes):
|
||||
"""Returns True if nodes and G/nodes are a bipartition of G.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
nodes: list or container
|
||||
Check if nodes are a one of a bipartite set.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> G = nx.path_graph(4)
|
||||
>>> X = set([1, 3])
|
||||
>>> bipartite.is_bipartite_node_set(G, X)
|
||||
True
|
||||
|
||||
Notes
|
||||
-----
|
||||
An exception is raised if the input nodes are not distinct, because in this
|
||||
case some bipartite algorithms will yield incorrect results.
|
||||
For connected graphs the bipartite sets are unique. This function handles
|
||||
disconnected graphs.
|
||||
"""
|
||||
S = set(nodes)
|
||||
|
||||
if len(S) < len(nodes):
|
||||
# this should maybe just return False?
|
||||
raise AmbiguousSolution(
|
||||
"The input node set contains duplicates.\n"
|
||||
"This may lead to incorrect results when using it in bipartite algorithms.\n"
|
||||
"Consider using set(nodes) as the input"
|
||||
)
|
||||
|
||||
for CC in (G.subgraph(c).copy() for c in connected_components(G)):
|
||||
X, Y = sets(CC)
|
||||
if not (
|
||||
(X.issubset(S) and Y.isdisjoint(S)) or (Y.issubset(S) and X.isdisjoint(S))
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def sets(G, top_nodes=None):
|
||||
"""Returns bipartite node sets of graph G.
|
||||
|
||||
Raises an exception if the graph is not bipartite or if the input
|
||||
graph is disconnected and thus more than one valid solution exists.
|
||||
See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
|
||||
for further details on how bipartite graphs are handled in NetworkX.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
top_nodes : container, optional
|
||||
Container with all nodes in one bipartite node set. If not supplied
|
||||
it will be computed. But if more than one solution exists an exception
|
||||
will be raised.
|
||||
|
||||
Returns
|
||||
-------
|
||||
X : set
|
||||
Nodes from one side of the bipartite graph.
|
||||
Y : set
|
||||
Nodes from the other side.
|
||||
|
||||
Raises
|
||||
------
|
||||
AmbiguousSolution
|
||||
Raised if the input bipartite graph is disconnected and no container
|
||||
with all nodes in one bipartite set is provided. When determining
|
||||
the nodes in each bipartite set more than one valid solution is
|
||||
possible if the input graph is disconnected.
|
||||
NetworkXError
|
||||
Raised if the input graph is not bipartite.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> G = nx.path_graph(4)
|
||||
>>> X, Y = bipartite.sets(G)
|
||||
>>> list(X)
|
||||
[0, 2]
|
||||
>>> list(Y)
|
||||
[1, 3]
|
||||
|
||||
See Also
|
||||
--------
|
||||
color
|
||||
|
||||
"""
|
||||
if G.is_directed():
|
||||
is_connected = nx.is_weakly_connected
|
||||
else:
|
||||
is_connected = nx.is_connected
|
||||
if top_nodes is not None:
|
||||
X = set(top_nodes)
|
||||
Y = set(G) - X
|
||||
else:
|
||||
if not is_connected(G):
|
||||
msg = "Disconnected graph: Ambiguous solution for bipartite sets."
|
||||
raise nx.AmbiguousSolution(msg)
|
||||
c = color(G)
|
||||
X = {n for n, is_top in c.items() if is_top}
|
||||
Y = {n for n, is_top in c.items() if not is_top}
|
||||
return (X, Y)
|
||||
|
||||
|
||||
@nx._dispatchable(graphs="B")
|
||||
def density(B, nodes):
|
||||
"""Returns density of bipartite graph B.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
B : NetworkX graph
|
||||
|
||||
nodes: list or container
|
||||
Nodes in one node set of the bipartite graph.
|
||||
|
||||
Returns
|
||||
-------
|
||||
d : float
|
||||
The bipartite density
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> G = nx.complete_bipartite_graph(3, 2)
|
||||
>>> X = set([0, 1, 2])
|
||||
>>> bipartite.density(G, X)
|
||||
1.0
|
||||
>>> Y = set([3, 4])
|
||||
>>> bipartite.density(G, Y)
|
||||
1.0
|
||||
|
||||
Notes
|
||||
-----
|
||||
The container of nodes passed as argument must contain all nodes
|
||||
in one of the two bipartite node sets to avoid ambiguity in the
|
||||
case of disconnected graphs.
|
||||
See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
|
||||
for further details on how bipartite graphs are handled in NetworkX.
|
||||
|
||||
See Also
|
||||
--------
|
||||
color
|
||||
"""
|
||||
n = len(B)
|
||||
m = nx.number_of_edges(B)
|
||||
nb = len(nodes)
|
||||
nt = n - nb
|
||||
if m == 0: # includes cases n==0 and n==1
|
||||
d = 0.0
|
||||
else:
|
||||
if B.is_directed():
|
||||
d = m / (2 * nb * nt)
|
||||
else:
|
||||
d = m / (nb * nt)
|
||||
return d
|
||||
|
||||
|
||||
@nx._dispatchable(graphs="B", edge_attrs="weight")
|
||||
def degrees(B, nodes, weight=None):
|
||||
"""Returns the degrees of the two node sets in the bipartite graph B.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
B : NetworkX graph
|
||||
|
||||
nodes: list or container
|
||||
Nodes in one node set of the bipartite graph.
|
||||
|
||||
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.
|
||||
The degree is the sum of the edge weights adjacent to the node.
|
||||
|
||||
Returns
|
||||
-------
|
||||
(degX,degY) : tuple of dictionaries
|
||||
The degrees of the two bipartite sets as dictionaries keyed by node.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> G = nx.complete_bipartite_graph(3, 2)
|
||||
>>> Y = set([3, 4])
|
||||
>>> degX, degY = bipartite.degrees(G, Y)
|
||||
>>> dict(degX)
|
||||
{0: 2, 1: 2, 2: 2}
|
||||
|
||||
Notes
|
||||
-----
|
||||
The container of nodes passed as argument must contain all nodes
|
||||
in one of the two bipartite node sets to avoid ambiguity in the
|
||||
case of disconnected graphs.
|
||||
See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
|
||||
for further details on how bipartite graphs are handled in NetworkX.
|
||||
|
||||
See Also
|
||||
--------
|
||||
color, density
|
||||
"""
|
||||
bottom = set(nodes)
|
||||
top = set(B) - bottom
|
||||
return (B.degree(top, weight), B.degree(bottom, weight))
|
||||
@@ -0,0 +1,290 @@
|
||||
import networkx as nx
|
||||
|
||||
__all__ = ["degree_centrality", "betweenness_centrality", "closeness_centrality"]
|
||||
|
||||
|
||||
@nx._dispatchable(name="bipartite_degree_centrality")
|
||||
def degree_centrality(G, nodes):
|
||||
r"""Compute the degree centrality for nodes in a bipartite network.
|
||||
|
||||
The degree centrality for a node `v` is the fraction of nodes
|
||||
connected to it.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A bipartite network
|
||||
|
||||
nodes : list or container
|
||||
Container with all nodes in one bipartite node set.
|
||||
|
||||
Returns
|
||||
-------
|
||||
centrality : dictionary
|
||||
Dictionary keyed by node with bipartite degree centrality as the value.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.wheel_graph(5)
|
||||
>>> top_nodes = {0, 1, 2}
|
||||
>>> nx.bipartite.degree_centrality(G, nodes=top_nodes)
|
||||
{0: 2.0, 1: 1.5, 2: 1.5, 3: 1.0, 4: 1.0}
|
||||
|
||||
See Also
|
||||
--------
|
||||
betweenness_centrality
|
||||
closeness_centrality
|
||||
:func:`~networkx.algorithms.bipartite.basic.sets`
|
||||
:func:`~networkx.algorithms.bipartite.basic.is_bipartite`
|
||||
|
||||
Notes
|
||||
-----
|
||||
The nodes input parameter must contain all nodes in one bipartite node set,
|
||||
but the dictionary returned contains all nodes from both bipartite node
|
||||
sets. See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
|
||||
for further details on how bipartite graphs are handled in NetworkX.
|
||||
|
||||
For unipartite networks, the degree centrality values are
|
||||
normalized by dividing by the maximum possible degree (which is
|
||||
`n-1` where `n` is the number of nodes in G).
|
||||
|
||||
In the bipartite case, the maximum possible degree of a node in a
|
||||
bipartite node set is the number of nodes in the opposite node set
|
||||
[1]_. The degree centrality for a node `v` in the bipartite
|
||||
sets `U` with `n` nodes and `V` with `m` nodes is
|
||||
|
||||
.. math::
|
||||
|
||||
d_{v} = \frac{deg(v)}{m}, \mbox{for} v \in U ,
|
||||
|
||||
d_{v} = \frac{deg(v)}{n}, \mbox{for} v \in V ,
|
||||
|
||||
|
||||
where `deg(v)` is the degree of node `v`.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Borgatti, S.P. and Halgin, D. In press. "Analyzing Affiliation
|
||||
Networks". In Carrington, P. and Scott, J. (eds) The Sage Handbook
|
||||
of Social Network Analysis. Sage Publications.
|
||||
https://dx.doi.org/10.4135/9781446294413.n28
|
||||
"""
|
||||
top = set(nodes)
|
||||
bottom = set(G) - top
|
||||
s = 1.0 / len(bottom)
|
||||
centrality = {n: d * s for n, d in G.degree(top)}
|
||||
s = 1.0 / len(top)
|
||||
centrality.update({n: d * s for n, d in G.degree(bottom)})
|
||||
return centrality
|
||||
|
||||
|
||||
@nx._dispatchable(name="bipartite_betweenness_centrality")
|
||||
def betweenness_centrality(G, nodes):
|
||||
r"""Compute betweenness centrality for nodes in a bipartite network.
|
||||
|
||||
Betweenness centrality of a node `v` is the sum of the
|
||||
fraction of all-pairs shortest paths that pass through `v`.
|
||||
|
||||
Values of betweenness are normalized by the maximum possible
|
||||
value which for bipartite graphs is limited by the relative size
|
||||
of the two node sets [1]_.
|
||||
|
||||
Let `n` be the number of nodes in the node set `U` and
|
||||
`m` be the number of nodes in the node set `V`, then
|
||||
nodes in `U` are normalized by dividing by
|
||||
|
||||
.. math::
|
||||
|
||||
\frac{1}{2} [m^2 (s + 1)^2 + m (s + 1)(2t - s - 1) - t (2s - t + 3)] ,
|
||||
|
||||
where
|
||||
|
||||
.. math::
|
||||
|
||||
s = (n - 1) \div m , t = (n - 1) \mod m ,
|
||||
|
||||
and nodes in `V` are normalized by dividing by
|
||||
|
||||
.. math::
|
||||
|
||||
\frac{1}{2} [n^2 (p + 1)^2 + n (p + 1)(2r - p - 1) - r (2p - r + 3)] ,
|
||||
|
||||
where,
|
||||
|
||||
.. math::
|
||||
|
||||
p = (m - 1) \div n , r = (m - 1) \mod n .
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A bipartite graph
|
||||
|
||||
nodes : list or container
|
||||
Container with all nodes in one bipartite node set.
|
||||
|
||||
Returns
|
||||
-------
|
||||
betweenness : dictionary
|
||||
Dictionary keyed by node with bipartite betweenness centrality
|
||||
as the value.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.cycle_graph(4)
|
||||
>>> top_nodes = {1, 2}
|
||||
>>> nx.bipartite.betweenness_centrality(G, nodes=top_nodes)
|
||||
{0: 0.25, 1: 0.25, 2: 0.25, 3: 0.25}
|
||||
|
||||
See Also
|
||||
--------
|
||||
degree_centrality
|
||||
closeness_centrality
|
||||
:func:`~networkx.algorithms.bipartite.basic.sets`
|
||||
:func:`~networkx.algorithms.bipartite.basic.is_bipartite`
|
||||
|
||||
Notes
|
||||
-----
|
||||
The nodes input parameter must contain all nodes in one bipartite node set,
|
||||
but the dictionary returned contains all nodes from both node sets.
|
||||
See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
|
||||
for further details on how bipartite graphs are handled in NetworkX.
|
||||
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Borgatti, S.P. and Halgin, D. In press. "Analyzing Affiliation
|
||||
Networks". In Carrington, P. and Scott, J. (eds) The Sage Handbook
|
||||
of Social Network Analysis. Sage Publications.
|
||||
https://dx.doi.org/10.4135/9781446294413.n28
|
||||
"""
|
||||
top = set(nodes)
|
||||
bottom = set(G) - top
|
||||
n = len(top)
|
||||
m = len(bottom)
|
||||
s, t = divmod(n - 1, m)
|
||||
bet_max_top = (
|
||||
((m**2) * ((s + 1) ** 2))
|
||||
+ (m * (s + 1) * (2 * t - s - 1))
|
||||
- (t * ((2 * s) - t + 3))
|
||||
) / 2.0
|
||||
p, r = divmod(m - 1, n)
|
||||
bet_max_bot = (
|
||||
((n**2) * ((p + 1) ** 2))
|
||||
+ (n * (p + 1) * (2 * r - p - 1))
|
||||
- (r * ((2 * p) - r + 3))
|
||||
) / 2.0
|
||||
betweenness = nx.betweenness_centrality(G, normalized=False, weight=None)
|
||||
for node in top:
|
||||
betweenness[node] /= bet_max_top
|
||||
for node in bottom:
|
||||
betweenness[node] /= bet_max_bot
|
||||
return betweenness
|
||||
|
||||
|
||||
@nx._dispatchable(name="bipartite_closeness_centrality")
|
||||
def closeness_centrality(G, nodes, normalized=True):
|
||||
r"""Compute the closeness centrality for nodes in a bipartite network.
|
||||
|
||||
The closeness of a node is the distance to all other nodes in the
|
||||
graph or in the case that the graph is not connected to all other nodes
|
||||
in the connected component containing that node.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A bipartite network
|
||||
|
||||
nodes : list or container
|
||||
Container with all nodes in one bipartite node set.
|
||||
|
||||
normalized : bool, optional
|
||||
If True (default) normalize by connected component size.
|
||||
|
||||
Returns
|
||||
-------
|
||||
closeness : dictionary
|
||||
Dictionary keyed by node with bipartite closeness centrality
|
||||
as the value.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.wheel_graph(5)
|
||||
>>> top_nodes = {0, 1, 2}
|
||||
>>> nx.bipartite.closeness_centrality(G, nodes=top_nodes)
|
||||
{0: 1.5, 1: 1.2, 2: 1.2, 3: 1.0, 4: 1.0}
|
||||
|
||||
See Also
|
||||
--------
|
||||
betweenness_centrality
|
||||
degree_centrality
|
||||
:func:`~networkx.algorithms.bipartite.basic.sets`
|
||||
:func:`~networkx.algorithms.bipartite.basic.is_bipartite`
|
||||
|
||||
Notes
|
||||
-----
|
||||
The nodes input parameter must contain all nodes in one bipartite node set,
|
||||
but the dictionary returned contains all nodes from both node sets.
|
||||
See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
|
||||
for further details on how bipartite graphs are handled in NetworkX.
|
||||
|
||||
|
||||
Closeness centrality is normalized by the minimum distance possible.
|
||||
In the bipartite case the minimum distance for a node in one bipartite
|
||||
node set is 1 from all nodes in the other node set and 2 from all
|
||||
other nodes in its own set [1]_. Thus the closeness centrality
|
||||
for node `v` in the two bipartite sets `U` with
|
||||
`n` nodes and `V` with `m` nodes is
|
||||
|
||||
.. math::
|
||||
|
||||
c_{v} = \frac{m + 2(n - 1)}{d}, \mbox{for} v \in U,
|
||||
|
||||
c_{v} = \frac{n + 2(m - 1)}{d}, \mbox{for} v \in V,
|
||||
|
||||
where `d` is the sum of the distances from `v` to all
|
||||
other nodes.
|
||||
|
||||
Higher values of closeness indicate higher centrality.
|
||||
|
||||
As in the unipartite case, setting normalized=True causes the
|
||||
values to normalized further to n-1 / size(G)-1 where n is the
|
||||
number of nodes in the connected part of graph containing the
|
||||
node. If the graph is not completely connected, this algorithm
|
||||
computes the closeness centrality for each connected part
|
||||
separately.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Borgatti, S.P. and Halgin, D. In press. "Analyzing Affiliation
|
||||
Networks". In Carrington, P. and Scott, J. (eds) The Sage Handbook
|
||||
of Social Network Analysis. Sage Publications.
|
||||
https://dx.doi.org/10.4135/9781446294413.n28
|
||||
"""
|
||||
closeness = {}
|
||||
path_length = nx.single_source_shortest_path_length
|
||||
top = set(nodes)
|
||||
bottom = set(G) - top
|
||||
n = len(top)
|
||||
m = len(bottom)
|
||||
for node in top:
|
||||
sp = dict(path_length(G, node))
|
||||
totsp = sum(sp.values())
|
||||
if totsp > 0.0 and len(G) > 1:
|
||||
closeness[node] = (m + 2 * (n - 1)) / totsp
|
||||
if normalized:
|
||||
s = (len(sp) - 1) / (len(G) - 1)
|
||||
closeness[node] *= s
|
||||
else:
|
||||
closeness[node] = 0.0
|
||||
for node in bottom:
|
||||
sp = dict(path_length(G, node))
|
||||
totsp = sum(sp.values())
|
||||
if totsp > 0.0 and len(G) > 1:
|
||||
closeness[node] = (n + 2 * (m - 1)) / totsp
|
||||
if normalized:
|
||||
s = (len(sp) - 1) / (len(G) - 1)
|
||||
closeness[node] *= s
|
||||
else:
|
||||
closeness[node] = 0.0
|
||||
return closeness
|
||||
@@ -0,0 +1,289 @@
|
||||
"""Functions for computing clustering of pairs"""
|
||||
|
||||
import itertools
|
||||
|
||||
import networkx as nx
|
||||
|
||||
__all__ = [
|
||||
"clustering",
|
||||
"average_clustering",
|
||||
"latapy_clustering",
|
||||
"robins_alexander_clustering",
|
||||
]
|
||||
|
||||
|
||||
def cc_dot(nu, nv):
|
||||
return len(nu & nv) / len(nu | nv)
|
||||
|
||||
|
||||
def cc_max(nu, nv):
|
||||
return len(nu & nv) / max(len(nu), len(nv))
|
||||
|
||||
|
||||
def cc_min(nu, nv):
|
||||
return len(nu & nv) / min(len(nu), len(nv))
|
||||
|
||||
|
||||
modes = {"dot": cc_dot, "min": cc_min, "max": cc_max}
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def latapy_clustering(G, nodes=None, mode="dot"):
|
||||
r"""Compute a bipartite clustering coefficient for nodes.
|
||||
|
||||
The bipartite clustering coefficient is a measure of local density
|
||||
of connections defined as [1]_:
|
||||
|
||||
.. math::
|
||||
|
||||
c_u = \frac{\sum_{v \in N(N(u))} c_{uv} }{|N(N(u))|}
|
||||
|
||||
where `N(N(u))` are the second order neighbors of `u` in `G` excluding `u`,
|
||||
and `c_{uv}` is the pairwise clustering coefficient between nodes
|
||||
`u` and `v`.
|
||||
|
||||
The mode selects the function for `c_{uv}` which can be:
|
||||
|
||||
`dot`:
|
||||
|
||||
.. math::
|
||||
|
||||
c_{uv}=\frac{|N(u)\cap N(v)|}{|N(u) \cup N(v)|}
|
||||
|
||||
`min`:
|
||||
|
||||
.. math::
|
||||
|
||||
c_{uv}=\frac{|N(u)\cap N(v)|}{min(|N(u)|,|N(v)|)}
|
||||
|
||||
`max`:
|
||||
|
||||
.. math::
|
||||
|
||||
c_{uv}=\frac{|N(u)\cap N(v)|}{max(|N(u)|,|N(v)|)}
|
||||
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A bipartite graph
|
||||
|
||||
nodes : list or iterable (optional)
|
||||
Compute bipartite clustering for these nodes. The default
|
||||
is all nodes in G.
|
||||
|
||||
mode : string
|
||||
The pairwise bipartite clustering method to be used in the computation.
|
||||
It must be "dot", "max", or "min".
|
||||
|
||||
Returns
|
||||
-------
|
||||
clustering : dictionary
|
||||
A dictionary keyed by node with the clustering coefficient value.
|
||||
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> G = nx.path_graph(4) # path graphs are bipartite
|
||||
>>> c = bipartite.clustering(G)
|
||||
>>> c[0]
|
||||
0.5
|
||||
>>> c = bipartite.clustering(G, mode="min")
|
||||
>>> c[0]
|
||||
1.0
|
||||
|
||||
See Also
|
||||
--------
|
||||
robins_alexander_clustering
|
||||
average_clustering
|
||||
networkx.algorithms.cluster.square_clustering
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Latapy, Matthieu, Clémence Magnien, and Nathalie Del Vecchio (2008).
|
||||
Basic notions for the analysis of large two-mode networks.
|
||||
Social Networks 30(1), 31--48.
|
||||
"""
|
||||
if not nx.algorithms.bipartite.is_bipartite(G):
|
||||
raise nx.NetworkXError("Graph is not bipartite")
|
||||
|
||||
try:
|
||||
cc_func = modes[mode]
|
||||
except KeyError as err:
|
||||
raise nx.NetworkXError(
|
||||
"Mode for bipartite clustering must be: dot, min or max"
|
||||
) from err
|
||||
|
||||
if nodes is None:
|
||||
nodes = G
|
||||
ccs = {}
|
||||
for v in nodes:
|
||||
cc = 0.0
|
||||
nbrs2 = {u for nbr in G[v] for u in G[nbr]} - {v}
|
||||
for u in nbrs2:
|
||||
cc += cc_func(set(G[u]), set(G[v]))
|
||||
if cc > 0.0: # len(nbrs2)>0
|
||||
cc /= len(nbrs2)
|
||||
ccs[v] = cc
|
||||
return ccs
|
||||
|
||||
|
||||
clustering = latapy_clustering
|
||||
|
||||
|
||||
@nx._dispatchable(name="bipartite_average_clustering")
|
||||
def average_clustering(G, nodes=None, mode="dot"):
|
||||
r"""Compute the average bipartite clustering coefficient.
|
||||
|
||||
A clustering coefficient for the whole graph is the average,
|
||||
|
||||
.. math::
|
||||
|
||||
C = \frac{1}{n}\sum_{v \in G} c_v,
|
||||
|
||||
where `n` is the number of nodes in `G`.
|
||||
|
||||
Similar measures for the two bipartite sets can be defined [1]_
|
||||
|
||||
.. math::
|
||||
|
||||
C_X = \frac{1}{|X|}\sum_{v \in X} c_v,
|
||||
|
||||
where `X` is a bipartite set of `G`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
a bipartite graph
|
||||
|
||||
nodes : list or iterable, optional
|
||||
A container of nodes to use in computing the average.
|
||||
The nodes should be either the entire graph (the default) or one of the
|
||||
bipartite sets.
|
||||
|
||||
mode : string
|
||||
The pairwise bipartite clustering method.
|
||||
It must be "dot", "max", or "min"
|
||||
|
||||
Returns
|
||||
-------
|
||||
clustering : float
|
||||
The average bipartite clustering for the given set of nodes or the
|
||||
entire graph if no nodes are specified.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> G = nx.star_graph(3) # star graphs are bipartite
|
||||
>>> bipartite.average_clustering(G)
|
||||
0.75
|
||||
>>> X, Y = bipartite.sets(G)
|
||||
>>> bipartite.average_clustering(G, X)
|
||||
0.0
|
||||
>>> bipartite.average_clustering(G, Y)
|
||||
1.0
|
||||
|
||||
See Also
|
||||
--------
|
||||
clustering
|
||||
|
||||
Notes
|
||||
-----
|
||||
The container of nodes passed to this function must contain all of the nodes
|
||||
in one of the bipartite sets ("top" or "bottom") in order to compute
|
||||
the correct average bipartite clustering coefficients.
|
||||
See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
|
||||
for further details on how bipartite graphs are handled in NetworkX.
|
||||
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Latapy, Matthieu, Clémence Magnien, and Nathalie Del Vecchio (2008).
|
||||
Basic notions for the analysis of large two-mode networks.
|
||||
Social Networks 30(1), 31--48.
|
||||
"""
|
||||
if nodes is None:
|
||||
nodes = G
|
||||
ccs = latapy_clustering(G, nodes=nodes, mode=mode)
|
||||
return sum(ccs[v] for v in nodes) / len(nodes)
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def robins_alexander_clustering(G):
|
||||
r"""Compute the bipartite clustering of G.
|
||||
|
||||
Robins and Alexander [1]_ defined bipartite clustering coefficient as
|
||||
four times the number of four cycles `C_4` divided by the number of
|
||||
three paths `L_3` in a bipartite graph:
|
||||
|
||||
.. math::
|
||||
|
||||
CC_4 = \frac{4 * C_4}{L_3}
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
a bipartite graph
|
||||
|
||||
Returns
|
||||
-------
|
||||
clustering : float
|
||||
The Robins and Alexander bipartite clustering for the input graph.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> G = nx.davis_southern_women_graph()
|
||||
>>> print(round(bipartite.robins_alexander_clustering(G), 3))
|
||||
0.468
|
||||
|
||||
See Also
|
||||
--------
|
||||
latapy_clustering
|
||||
networkx.algorithms.cluster.square_clustering
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Robins, G. and M. Alexander (2004). Small worlds among interlocking
|
||||
directors: Network structure and distance in bipartite graphs.
|
||||
Computational & Mathematical Organization Theory 10(1), 69–94.
|
||||
|
||||
"""
|
||||
if G.order() < 4 or G.size() < 3:
|
||||
return 0
|
||||
L_3 = _threepaths(G)
|
||||
if L_3 == 0:
|
||||
return 0
|
||||
C_4 = _four_cycles(G)
|
||||
return (4.0 * C_4) / L_3
|
||||
|
||||
|
||||
def _four_cycles(G):
|
||||
# Also see `square_clustering` which counts squares in a similar way
|
||||
cycles = 0
|
||||
seen = set()
|
||||
G_adj = G._adj
|
||||
for v in G:
|
||||
seen.add(v)
|
||||
v_neighbors = set(G_adj[v])
|
||||
if len(v_neighbors) < 2:
|
||||
# Can't form a square without at least two neighbors
|
||||
continue
|
||||
two_hop_neighbors = set().union(*(G_adj[u] for u in v_neighbors))
|
||||
two_hop_neighbors -= seen
|
||||
for x in two_hop_neighbors:
|
||||
p2 = len(v_neighbors.intersection(G_adj[x]))
|
||||
cycles += p2 * (p2 - 1)
|
||||
return cycles / 4
|
||||
|
||||
|
||||
def _threepaths(G):
|
||||
paths = 0
|
||||
for v in G:
|
||||
for u in G[v]:
|
||||
for w in set(G[u]) - {v}:
|
||||
paths += len(set(G[w]) - {v, u})
|
||||
# Divide by two because we count each three path twice
|
||||
# one for each possible starting point
|
||||
return paths / 2
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Functions related to graph covers."""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.bipartite.matching import hopcroft_karp_matching
|
||||
from networkx.algorithms.covering import min_edge_cover as _min_edge_cover
|
||||
from networkx.utils import not_implemented_for
|
||||
|
||||
__all__ = ["min_edge_cover"]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable(name="bipartite_min_edge_cover")
|
||||
def min_edge_cover(G, matching_algorithm=None):
|
||||
"""Returns a set of edges which constitutes
|
||||
the minimum edge cover of the graph.
|
||||
|
||||
The smallest edge cover can be found in polynomial time by finding
|
||||
a maximum matching and extending it greedily so that all nodes
|
||||
are covered.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
An undirected bipartite graph.
|
||||
|
||||
matching_algorithm : function
|
||||
A function that returns a maximum cardinality matching in a
|
||||
given bipartite graph. The function must take one input, the
|
||||
graph ``G``, and return a dictionary mapping each node to its
|
||||
mate. If not specified,
|
||||
:func:`~networkx.algorithms.bipartite.matching.hopcroft_karp_matching`
|
||||
will be used. Other possibilities include
|
||||
:func:`~networkx.algorithms.bipartite.matching.eppstein_matching`,
|
||||
|
||||
Returns
|
||||
-------
|
||||
set
|
||||
A set of the edges in a minimum edge cover of the graph, given as
|
||||
pairs of nodes. It contains both the edges `(u, v)` and `(v, u)`
|
||||
for given nodes `u` and `v` among the edges of minimum edge cover.
|
||||
|
||||
Notes
|
||||
-----
|
||||
An edge cover of a graph is a set of edges such that every node of
|
||||
the graph is incident to at least one edge of the set.
|
||||
A minimum edge cover is an edge covering of smallest cardinality.
|
||||
|
||||
Due to its implementation, the worst-case running time of this algorithm
|
||||
is bounded by the worst-case running time of the function
|
||||
``matching_algorithm``.
|
||||
"""
|
||||
if G.order() == 0: # Special case for the empty graph
|
||||
return set()
|
||||
if matching_algorithm is None:
|
||||
matching_algorithm = hopcroft_karp_matching
|
||||
return _min_edge_cover(G, matching_algorithm=matching_algorithm)
|
||||
@@ -0,0 +1,360 @@
|
||||
"""
|
||||
********************
|
||||
Bipartite Edge Lists
|
||||
********************
|
||||
Read and write NetworkX graphs as bipartite edge lists.
|
||||
|
||||
Format
|
||||
------
|
||||
You can read or write three formats of edge lists with these functions.
|
||||
|
||||
Node pairs with no data::
|
||||
|
||||
1 2
|
||||
|
||||
Python dictionary as data::
|
||||
|
||||
1 2 {'weight':7, 'color':'green'}
|
||||
|
||||
Arbitrary data::
|
||||
|
||||
1 2 7 green
|
||||
|
||||
For each edge (u, v) the node u is assigned to part 0 and the node v to part 1.
|
||||
"""
|
||||
|
||||
__all__ = ["generate_edgelist", "write_edgelist", "parse_edgelist", "read_edgelist"]
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import not_implemented_for, open_file
|
||||
|
||||
|
||||
@open_file(1, mode="wb")
|
||||
def write_edgelist(G, path, comments="#", delimiter=" ", data=True, encoding="utf-8"):
|
||||
"""Write a bipartite graph as a list of edges.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : Graph
|
||||
A NetworkX bipartite graph
|
||||
path : file or string
|
||||
File or filename to write. If a file is provided, it must be
|
||||
opened in 'wb' mode. Filenames ending in .gz or .bz2 will be compressed.
|
||||
comments : string, optional
|
||||
The character used to indicate the start of a comment
|
||||
delimiter : string, optional
|
||||
The string used to separate values. The default is whitespace.
|
||||
data : bool or list, optional
|
||||
If False write no edge data.
|
||||
If True write a string representation of the edge data dictionary..
|
||||
If a list (or other iterable) is provided, write the keys specified
|
||||
in the list.
|
||||
encoding: string, optional
|
||||
Specify which encoding to use when writing file.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.path_graph(4)
|
||||
>>> G.add_nodes_from([0, 2], bipartite=0)
|
||||
>>> G.add_nodes_from([1, 3], bipartite=1)
|
||||
>>> nx.write_edgelist(G, "test.edgelist")
|
||||
>>> fh = open("test.edgelist_open", "wb")
|
||||
>>> nx.write_edgelist(G, fh)
|
||||
>>> nx.write_edgelist(G, "test.edgelist.gz")
|
||||
>>> nx.write_edgelist(G, "test.edgelist_nodata.gz", data=False)
|
||||
|
||||
>>> G = nx.Graph()
|
||||
>>> G.add_edge(1, 2, weight=7, color="red")
|
||||
>>> nx.write_edgelist(G, "test.edgelist_bigger_nodata", data=False)
|
||||
>>> nx.write_edgelist(G, "test.edgelist_color", data=["color"])
|
||||
>>> nx.write_edgelist(G, "test.edgelist_color_weight", data=["color", "weight"])
|
||||
|
||||
See Also
|
||||
--------
|
||||
write_edgelist
|
||||
generate_edgelist
|
||||
"""
|
||||
for line in generate_edgelist(G, delimiter, data):
|
||||
line += "\n"
|
||||
path.write(line.encode(encoding))
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
def generate_edgelist(G, delimiter=" ", data=True):
|
||||
"""Generate a single line of the bipartite graph G in edge list format.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
The graph is assumed to have node attribute `part` set to 0,1 representing
|
||||
the two graph parts
|
||||
|
||||
delimiter : string, optional
|
||||
Separator for node labels
|
||||
|
||||
data : bool or list of keys
|
||||
If False generate no edge data. If True use a dictionary
|
||||
representation of edge data. If a list of keys use a list of data
|
||||
values corresponding to the keys.
|
||||
|
||||
Returns
|
||||
-------
|
||||
lines : string
|
||||
Lines of data in adjlist format.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> G = nx.path_graph(4)
|
||||
>>> G.add_nodes_from([0, 2], bipartite=0)
|
||||
>>> G.add_nodes_from([1, 3], bipartite=1)
|
||||
>>> G[1][2]["weight"] = 3
|
||||
>>> G[2][3]["capacity"] = 12
|
||||
>>> for line in bipartite.generate_edgelist(G, data=False):
|
||||
... print(line)
|
||||
0 1
|
||||
2 1
|
||||
2 3
|
||||
|
||||
>>> for line in bipartite.generate_edgelist(G):
|
||||
... print(line)
|
||||
0 1 {}
|
||||
2 1 {'weight': 3}
|
||||
2 3 {'capacity': 12}
|
||||
|
||||
>>> for line in bipartite.generate_edgelist(G, data=["weight"]):
|
||||
... print(line)
|
||||
0 1
|
||||
2 1 3
|
||||
2 3
|
||||
"""
|
||||
try:
|
||||
part0 = [n for n, d in G.nodes.items() if d["bipartite"] == 0]
|
||||
except BaseException as err:
|
||||
raise AttributeError("Missing node attribute `bipartite`") from err
|
||||
if data is True or data is False:
|
||||
for n in part0:
|
||||
for edge in G.edges(n, data=data):
|
||||
yield delimiter.join(map(str, edge))
|
||||
else:
|
||||
for n in part0:
|
||||
for u, v, d in G.edges(n, data=True):
|
||||
edge = [u, v]
|
||||
try:
|
||||
edge.extend(d[k] for k in data)
|
||||
except KeyError:
|
||||
pass # missing data for this edge, should warn?
|
||||
yield delimiter.join(map(str, edge))
|
||||
|
||||
|
||||
@nx._dispatchable(name="bipartite_parse_edgelist", graphs=None, returns_graph=True)
|
||||
def parse_edgelist(
|
||||
lines, comments="#", delimiter=None, create_using=None, nodetype=None, data=True
|
||||
):
|
||||
"""Parse lines of an edge list representation of a bipartite graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lines : list or iterator of strings
|
||||
Input data in edgelist format
|
||||
comments : string, optional
|
||||
Marker for comment lines
|
||||
delimiter : string, optional
|
||||
Separator for node labels
|
||||
create_using: NetworkX graph container, optional
|
||||
Use given NetworkX graph for holding nodes or edges.
|
||||
nodetype : Python type, optional
|
||||
Convert nodes to this type.
|
||||
data : bool or list of (label,type) tuples
|
||||
If False generate no edge data or if True use a dictionary
|
||||
representation of edge data or a list tuples specifying dictionary
|
||||
key names and types for edge data.
|
||||
|
||||
Returns
|
||||
-------
|
||||
G: NetworkX Graph
|
||||
The bipartite graph corresponding to lines
|
||||
|
||||
Examples
|
||||
--------
|
||||
Edgelist with no data:
|
||||
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> lines = ["1 2", "2 3", "3 4"]
|
||||
>>> G = bipartite.parse_edgelist(lines, nodetype=int)
|
||||
>>> sorted(G.nodes())
|
||||
[1, 2, 3, 4]
|
||||
>>> sorted(G.nodes(data=True))
|
||||
[(1, {'bipartite': 0}), (2, {'bipartite': 0}), (3, {'bipartite': 0}), (4, {'bipartite': 1})]
|
||||
>>> sorted(G.edges())
|
||||
[(1, 2), (2, 3), (3, 4)]
|
||||
|
||||
Edgelist with data in Python dictionary representation:
|
||||
|
||||
>>> lines = ["1 2 {'weight':3}", "2 3 {'weight':27}", "3 4 {'weight':3.0}"]
|
||||
>>> G = bipartite.parse_edgelist(lines, nodetype=int)
|
||||
>>> sorted(G.nodes())
|
||||
[1, 2, 3, 4]
|
||||
>>> sorted(G.edges(data=True))
|
||||
[(1, 2, {'weight': 3}), (2, 3, {'weight': 27}), (3, 4, {'weight': 3.0})]
|
||||
|
||||
Edgelist with data in a list:
|
||||
|
||||
>>> lines = ["1 2 3", "2 3 27", "3 4 3.0"]
|
||||
>>> G = bipartite.parse_edgelist(lines, nodetype=int, data=(("weight", float),))
|
||||
>>> sorted(G.nodes())
|
||||
[1, 2, 3, 4]
|
||||
>>> sorted(G.edges(data=True))
|
||||
[(1, 2, {'weight': 3.0}), (2, 3, {'weight': 27.0}), (3, 4, {'weight': 3.0})]
|
||||
|
||||
See Also
|
||||
--------
|
||||
"""
|
||||
from ast import literal_eval
|
||||
|
||||
G = nx.empty_graph(0, create_using)
|
||||
for line in lines:
|
||||
p = line.find(comments)
|
||||
if p >= 0:
|
||||
line = line[:p]
|
||||
if not len(line):
|
||||
continue
|
||||
# split line, should have 2 or more
|
||||
s = line.rstrip("\n").split(delimiter)
|
||||
if len(s) < 2:
|
||||
continue
|
||||
u = s.pop(0)
|
||||
v = s.pop(0)
|
||||
d = s
|
||||
if nodetype is not None:
|
||||
try:
|
||||
u = nodetype(u)
|
||||
v = nodetype(v)
|
||||
except BaseException as err:
|
||||
raise TypeError(
|
||||
f"Failed to convert nodes {u},{v} to type {nodetype}."
|
||||
) from err
|
||||
|
||||
if len(d) == 0 or data is False:
|
||||
# no data or data type specified
|
||||
edgedata = {}
|
||||
elif data is True:
|
||||
# no edge types specified
|
||||
try: # try to evaluate as dictionary
|
||||
edgedata = dict(literal_eval(" ".join(d)))
|
||||
except BaseException as err:
|
||||
raise TypeError(
|
||||
f"Failed to convert edge data ({d}) to dictionary."
|
||||
) from err
|
||||
else:
|
||||
# convert edge data to dictionary with specified keys and type
|
||||
if len(d) != len(data):
|
||||
raise IndexError(
|
||||
f"Edge data {d} and data_keys {data} are not the same length"
|
||||
)
|
||||
edgedata = {}
|
||||
for (edge_key, edge_type), edge_value in zip(data, d):
|
||||
try:
|
||||
edge_value = edge_type(edge_value)
|
||||
except BaseException as err:
|
||||
raise TypeError(
|
||||
f"Failed to convert {edge_key} data "
|
||||
f"{edge_value} to type {edge_type}."
|
||||
) from err
|
||||
edgedata.update({edge_key: edge_value})
|
||||
G.add_node(u, bipartite=0)
|
||||
G.add_node(v, bipartite=1)
|
||||
G.add_edge(u, v, **edgedata)
|
||||
return G
|
||||
|
||||
|
||||
@open_file(0, mode="rb")
|
||||
@nx._dispatchable(name="bipartite_read_edgelist", graphs=None, returns_graph=True)
|
||||
def read_edgelist(
|
||||
path,
|
||||
comments="#",
|
||||
delimiter=None,
|
||||
create_using=None,
|
||||
nodetype=None,
|
||||
data=True,
|
||||
edgetype=None,
|
||||
encoding="utf-8",
|
||||
):
|
||||
"""Read a bipartite graph from a list of edges.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path : file or string
|
||||
File or filename to read. If a file is provided, it must be
|
||||
opened in 'rb' mode.
|
||||
Filenames ending in .gz or .bz2 will be decompressed.
|
||||
comments : string, optional
|
||||
The character used to indicate the start of a comment.
|
||||
delimiter : string, optional
|
||||
The string used to separate values. The default is whitespace.
|
||||
create_using : Graph container, optional,
|
||||
Use specified container to build graph. The default is networkx.Graph,
|
||||
an undirected graph.
|
||||
nodetype : int, float, str, Python type, optional
|
||||
Convert node data from strings to specified type
|
||||
data : bool or list of (label,type) tuples
|
||||
Tuples specifying dictionary key names and types for edge data
|
||||
edgetype : int, float, str, Python type, optional OBSOLETE
|
||||
Convert edge data from strings to specified type and use as 'weight'
|
||||
encoding: string, optional
|
||||
Specify which encoding to use when reading file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
G : graph
|
||||
A networkx Graph or other type specified with create_using
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> G = nx.path_graph(4)
|
||||
>>> G.add_nodes_from([0, 2], bipartite=0)
|
||||
>>> G.add_nodes_from([1, 3], bipartite=1)
|
||||
>>> bipartite.write_edgelist(G, "test.edgelist")
|
||||
>>> G = bipartite.read_edgelist("test.edgelist")
|
||||
|
||||
>>> fh = open("test.edgelist", "rb")
|
||||
>>> G = bipartite.read_edgelist(fh)
|
||||
>>> fh.close()
|
||||
|
||||
>>> G = bipartite.read_edgelist("test.edgelist", nodetype=int)
|
||||
|
||||
Edgelist with data in a list:
|
||||
|
||||
>>> textline = "1 2 3"
|
||||
>>> fh = open("test.edgelist", "w")
|
||||
>>> d = fh.write(textline)
|
||||
>>> fh.close()
|
||||
>>> G = bipartite.read_edgelist(
|
||||
... "test.edgelist", nodetype=int, data=(("weight", float),)
|
||||
... )
|
||||
>>> list(G)
|
||||
[1, 2]
|
||||
>>> list(G.edges(data=True))
|
||||
[(1, 2, {'weight': 3.0})]
|
||||
|
||||
See parse_edgelist() for more examples of formatting.
|
||||
|
||||
See Also
|
||||
--------
|
||||
parse_edgelist
|
||||
|
||||
Notes
|
||||
-----
|
||||
Since nodes must be hashable, the function nodetype must return hashable
|
||||
types (e.g. int, float, str, frozenset - or tuples of those, etc.)
|
||||
"""
|
||||
lines = (line.decode(encoding) for line in path)
|
||||
return parse_edgelist(
|
||||
lines,
|
||||
comments=comments,
|
||||
delimiter=delimiter,
|
||||
create_using=create_using,
|
||||
nodetype=nodetype,
|
||||
data=data,
|
||||
)
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
"""Provides a function for computing the extendability of a graph which is
|
||||
undirected, simple, connected and bipartite and contains at least one perfect matching."""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import not_implemented_for
|
||||
|
||||
__all__ = ["maximal_extendability"]
|
||||
|
||||
|
||||
@not_implemented_for("directed")
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable
|
||||
def maximal_extendability(G):
|
||||
"""Computes the extendability of a graph.
|
||||
|
||||
The extendability of a graph is defined as the maximum $k$ for which `G`
|
||||
is $k$-extendable. Graph `G` is $k$-extendable if and only if `G` has a
|
||||
perfect matching and every set of $k$ independent edges can be extended
|
||||
to a perfect matching in `G`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX Graph
|
||||
A fully-connected bipartite graph without self-loops
|
||||
|
||||
Returns
|
||||
-------
|
||||
extendability : int
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXError
|
||||
If the graph `G` is disconnected.
|
||||
If the graph `G` is not bipartite.
|
||||
If the graph `G` does not contain a perfect matching.
|
||||
If the residual graph of `G` is not strongly connected.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Definition:
|
||||
Let `G` be a simple, connected, undirected and bipartite graph with a perfect
|
||||
matching M and bipartition (U,V). The residual graph of `G`, denoted by $G_M$,
|
||||
is the graph obtained from G by directing the edges of M from V to U and the
|
||||
edges that do not belong to M from U to V.
|
||||
|
||||
Lemma [1]_ :
|
||||
Let M be a perfect matching of `G`. `G` is $k$-extendable if and only if its residual
|
||||
graph $G_M$ is strongly connected and there are $k$ vertex-disjoint directed
|
||||
paths between every vertex of U and every vertex of V.
|
||||
|
||||
Assuming that input graph `G` is undirected, simple, connected, bipartite and contains
|
||||
a perfect matching M, this function constructs the residual graph $G_M$ of G and
|
||||
returns the minimum value among the maximum vertex-disjoint directed paths between
|
||||
every vertex of U and every vertex of V in $G_M$. By combining the definitions
|
||||
and the lemma, this value represents the extendability of the graph `G`.
|
||||
|
||||
Time complexity O($n^3$ $m^2$)) where $n$ is the number of vertices
|
||||
and $m$ is the number of edges.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] "A polynomial algorithm for the extendability problem in bipartite graphs",
|
||||
J. Lakhal, L. Litzler, Information Processing Letters, 1998.
|
||||
.. [2] "On n-extendible graphs", M. D. Plummer, Discrete Mathematics, 31:201–210, 1980
|
||||
https://doi.org/10.1016/0012-365X(80)90037-0
|
||||
|
||||
"""
|
||||
if not nx.is_connected(G):
|
||||
raise nx.NetworkXError("Graph G is not connected")
|
||||
|
||||
if not nx.bipartite.is_bipartite(G):
|
||||
raise nx.NetworkXError("Graph G is not bipartite")
|
||||
|
||||
U, V = nx.bipartite.sets(G)
|
||||
|
||||
maximum_matching = nx.bipartite.hopcroft_karp_matching(G)
|
||||
|
||||
if not nx.is_perfect_matching(G, maximum_matching):
|
||||
raise nx.NetworkXError("Graph G does not contain a perfect matching")
|
||||
|
||||
# list of edges in perfect matching, directed from V to U
|
||||
pm = [(node, maximum_matching[node]) for node in V & maximum_matching.keys()]
|
||||
|
||||
# Direct all the edges of G, from V to U if in matching, else from U to V
|
||||
directed_edges = [
|
||||
(x, y) if (x in V and (x, y) in pm) or (x in U and (y, x) not in pm) else (y, x)
|
||||
for x, y in G.edges
|
||||
]
|
||||
|
||||
# Construct the residual graph of G
|
||||
residual_G = nx.DiGraph()
|
||||
residual_G.add_nodes_from(G)
|
||||
residual_G.add_edges_from(directed_edges)
|
||||
|
||||
if not nx.is_strongly_connected(residual_G):
|
||||
raise nx.NetworkXError("The residual graph of G is not strongly connected")
|
||||
|
||||
# For node-pairs between V & U, keep min of max number of node-disjoint paths
|
||||
# Variable $k$ stands for the extendability of graph G
|
||||
k = float("inf")
|
||||
for u in U:
|
||||
for v in V:
|
||||
num_paths = sum(1 for _ in nx.node_disjoint_paths(residual_G, u, v))
|
||||
k = k if k < num_paths else num_paths
|
||||
return k
|
||||
@@ -0,0 +1,603 @@
|
||||
"""
|
||||
Generators and functions for bipartite graphs.
|
||||
"""
|
||||
|
||||
import math
|
||||
import numbers
|
||||
from functools import reduce
|
||||
|
||||
import networkx as nx
|
||||
from networkx.utils import nodes_or_number, py_random_state
|
||||
|
||||
__all__ = [
|
||||
"configuration_model",
|
||||
"havel_hakimi_graph",
|
||||
"reverse_havel_hakimi_graph",
|
||||
"alternating_havel_hakimi_graph",
|
||||
"preferential_attachment_graph",
|
||||
"random_graph",
|
||||
"gnmk_random_graph",
|
||||
"complete_bipartite_graph",
|
||||
]
|
||||
|
||||
|
||||
@nx._dispatchable(graphs=None, returns_graph=True)
|
||||
@nodes_or_number([0, 1])
|
||||
def complete_bipartite_graph(n1, n2, create_using=None):
|
||||
"""Returns the complete bipartite graph `K_{n_1,n_2}`.
|
||||
|
||||
The graph is composed of two partitions with nodes 0 to (n1 - 1)
|
||||
in the first and nodes n1 to (n1 + n2 - 1) in the second.
|
||||
Each node in the first is connected to each node in the second.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n1, n2 : integer or iterable container of nodes
|
||||
If integers, nodes are from `range(n1)` and `range(n1, n1 + n2)`.
|
||||
If a container, the elements are the nodes.
|
||||
create_using : NetworkX graph instance, (default: nx.Graph)
|
||||
Return graph of this type.
|
||||
|
||||
Notes
|
||||
-----
|
||||
Nodes are the integers 0 to `n1 + n2 - 1` unless either n1 or n2 are
|
||||
containers of nodes. If only one of n1 or n2 are integers, that
|
||||
integer is replaced by `range` of that integer.
|
||||
|
||||
The nodes are assigned the attribute 'bipartite' with the value 0 or 1
|
||||
to indicate which bipartite set the node belongs to.
|
||||
|
||||
This function is not imported in the main namespace.
|
||||
To use it use nx.bipartite.complete_bipartite_graph
|
||||
"""
|
||||
G = nx.empty_graph(0, create_using)
|
||||
if G.is_directed():
|
||||
raise nx.NetworkXError("Directed Graph not supported")
|
||||
|
||||
n1, top = n1
|
||||
n2, bottom = n2
|
||||
if isinstance(n1, numbers.Integral) and isinstance(n2, numbers.Integral):
|
||||
bottom = [n1 + i for i in bottom]
|
||||
G.add_nodes_from(top, bipartite=0)
|
||||
G.add_nodes_from(bottom, bipartite=1)
|
||||
if len(G) != len(top) + len(bottom):
|
||||
raise nx.NetworkXError("Inputs n1 and n2 must contain distinct nodes")
|
||||
G.add_edges_from((u, v) for u in top for v in bottom)
|
||||
G.graph["name"] = f"complete_bipartite_graph({len(top)}, {len(bottom)})"
|
||||
return G
|
||||
|
||||
|
||||
@py_random_state(3)
|
||||
@nx._dispatchable(name="bipartite_configuration_model", graphs=None, returns_graph=True)
|
||||
def configuration_model(aseq, bseq, create_using=None, seed=None):
|
||||
"""Returns a random bipartite graph from two given degree sequences.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
aseq : list
|
||||
Degree sequence for node set A.
|
||||
bseq : list
|
||||
Degree sequence for node set B.
|
||||
create_using : NetworkX graph instance, optional
|
||||
Return graph of this type.
|
||||
seed : integer, random_state, or None (default)
|
||||
Indicator of random number generation state.
|
||||
See :ref:`Randomness<randomness>`.
|
||||
|
||||
The graph is composed of two partitions. Set A has nodes 0 to
|
||||
(len(aseq) - 1) and set B has nodes len(aseq) to (len(bseq) - 1).
|
||||
Nodes from set A are connected to nodes in set B by choosing
|
||||
randomly from the possible free stubs, one in A and one in B.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The sum of the two sequences must be equal: sum(aseq)=sum(bseq)
|
||||
If no graph type is specified use MultiGraph with parallel edges.
|
||||
If you want a graph with no parallel edges use create_using=Graph()
|
||||
but then the resulting degree sequences might not be exact.
|
||||
|
||||
The nodes are assigned the attribute 'bipartite' with the value 0 or 1
|
||||
to indicate which bipartite set the node belongs to.
|
||||
|
||||
This function is not imported in the main namespace.
|
||||
To use it use nx.bipartite.configuration_model
|
||||
"""
|
||||
G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
|
||||
if G.is_directed():
|
||||
raise nx.NetworkXError("Directed Graph not supported")
|
||||
|
||||
# length and sum of each sequence
|
||||
lena = len(aseq)
|
||||
lenb = len(bseq)
|
||||
suma = sum(aseq)
|
||||
sumb = sum(bseq)
|
||||
|
||||
if not suma == sumb:
|
||||
raise nx.NetworkXError(
|
||||
f"invalid degree sequences, sum(aseq)!=sum(bseq),{suma},{sumb}"
|
||||
)
|
||||
|
||||
G = _add_nodes_with_bipartite_label(G, lena, lenb)
|
||||
|
||||
if len(aseq) == 0 or max(aseq) == 0:
|
||||
return G # done if no edges
|
||||
|
||||
# build lists of degree-repeated vertex numbers
|
||||
stubs = [[v] * aseq[v] for v in range(lena)]
|
||||
astubs = [x for subseq in stubs for x in subseq]
|
||||
|
||||
stubs = [[v] * bseq[v - lena] for v in range(lena, lena + lenb)]
|
||||
bstubs = [x for subseq in stubs for x in subseq]
|
||||
|
||||
# shuffle lists
|
||||
seed.shuffle(astubs)
|
||||
seed.shuffle(bstubs)
|
||||
|
||||
G.add_edges_from([astubs[i], bstubs[i]] for i in range(suma))
|
||||
|
||||
G.name = "bipartite_configuration_model"
|
||||
return G
|
||||
|
||||
|
||||
@nx._dispatchable(name="bipartite_havel_hakimi_graph", graphs=None, returns_graph=True)
|
||||
def havel_hakimi_graph(aseq, bseq, create_using=None):
|
||||
"""Returns a bipartite graph from two given degree sequences using a
|
||||
Havel-Hakimi style construction.
|
||||
|
||||
The graph is composed of two partitions. Set A has nodes 0 to
|
||||
(len(aseq) - 1) and set B has nodes len(aseq) to (len(bseq) - 1).
|
||||
Nodes from the set A are connected to nodes in the set B by
|
||||
connecting the highest degree nodes in set A to the highest degree
|
||||
nodes in set B until all stubs are connected.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
aseq : list
|
||||
Degree sequence for node set A.
|
||||
bseq : list
|
||||
Degree sequence for node set B.
|
||||
create_using : NetworkX graph instance, optional
|
||||
Return graph of this type.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The sum of the two sequences must be equal: sum(aseq)=sum(bseq)
|
||||
If no graph type is specified use MultiGraph with parallel edges.
|
||||
If you want a graph with no parallel edges use create_using=Graph()
|
||||
but then the resulting degree sequences might not be exact.
|
||||
|
||||
The nodes are assigned the attribute 'bipartite' with the value 0 or 1
|
||||
to indicate which bipartite set the node belongs to.
|
||||
|
||||
This function is not imported in the main namespace.
|
||||
To use it use nx.bipartite.havel_hakimi_graph
|
||||
"""
|
||||
G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
|
||||
if G.is_directed():
|
||||
raise nx.NetworkXError("Directed Graph not supported")
|
||||
|
||||
# length of the each sequence
|
||||
naseq = len(aseq)
|
||||
nbseq = len(bseq)
|
||||
|
||||
suma = sum(aseq)
|
||||
sumb = sum(bseq)
|
||||
|
||||
if not suma == sumb:
|
||||
raise nx.NetworkXError(
|
||||
f"invalid degree sequences, sum(aseq)!=sum(bseq),{suma},{sumb}"
|
||||
)
|
||||
|
||||
G = _add_nodes_with_bipartite_label(G, naseq, nbseq)
|
||||
|
||||
if len(aseq) == 0 or max(aseq) == 0:
|
||||
return G # done if no edges
|
||||
|
||||
# build list of degree-repeated vertex numbers
|
||||
astubs = [[aseq[v], v] for v in range(naseq)]
|
||||
bstubs = [[bseq[v - naseq], v] for v in range(naseq, naseq + nbseq)]
|
||||
astubs.sort()
|
||||
while astubs:
|
||||
(degree, u) = astubs.pop() # take of largest degree node in the a set
|
||||
if degree == 0:
|
||||
break # done, all are zero
|
||||
# connect the source to largest degree nodes in the b set
|
||||
bstubs.sort()
|
||||
for target in bstubs[-degree:]:
|
||||
v = target[1]
|
||||
G.add_edge(u, v)
|
||||
target[0] -= 1 # note this updates bstubs too.
|
||||
if target[0] == 0:
|
||||
bstubs.remove(target)
|
||||
|
||||
G.name = "bipartite_havel_hakimi_graph"
|
||||
return G
|
||||
|
||||
|
||||
@nx._dispatchable(graphs=None, returns_graph=True)
|
||||
def reverse_havel_hakimi_graph(aseq, bseq, create_using=None):
|
||||
"""Returns a bipartite graph from two given degree sequences using a
|
||||
Havel-Hakimi style construction.
|
||||
|
||||
The graph is composed of two partitions. Set A has nodes 0 to
|
||||
(len(aseq) - 1) and set B has nodes len(aseq) to (len(bseq) - 1).
|
||||
Nodes from set A are connected to nodes in the set B by connecting
|
||||
the highest degree nodes in set A to the lowest degree nodes in
|
||||
set B until all stubs are connected.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
aseq : list
|
||||
Degree sequence for node set A.
|
||||
bseq : list
|
||||
Degree sequence for node set B.
|
||||
create_using : NetworkX graph instance, optional
|
||||
Return graph of this type.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The sum of the two sequences must be equal: sum(aseq)=sum(bseq)
|
||||
If no graph type is specified use MultiGraph with parallel edges.
|
||||
If you want a graph with no parallel edges use create_using=Graph()
|
||||
but then the resulting degree sequences might not be exact.
|
||||
|
||||
The nodes are assigned the attribute 'bipartite' with the value 0 or 1
|
||||
to indicate which bipartite set the node belongs to.
|
||||
|
||||
This function is not imported in the main namespace.
|
||||
To use it use nx.bipartite.reverse_havel_hakimi_graph
|
||||
"""
|
||||
G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
|
||||
if G.is_directed():
|
||||
raise nx.NetworkXError("Directed Graph not supported")
|
||||
|
||||
# length of the each sequence
|
||||
lena = len(aseq)
|
||||
lenb = len(bseq)
|
||||
suma = sum(aseq)
|
||||
sumb = sum(bseq)
|
||||
|
||||
if not suma == sumb:
|
||||
raise nx.NetworkXError(
|
||||
f"invalid degree sequences, sum(aseq)!=sum(bseq),{suma},{sumb}"
|
||||
)
|
||||
|
||||
G = _add_nodes_with_bipartite_label(G, lena, lenb)
|
||||
|
||||
if len(aseq) == 0 or max(aseq) == 0:
|
||||
return G # done if no edges
|
||||
|
||||
# build list of degree-repeated vertex numbers
|
||||
astubs = [[aseq[v], v] for v in range(lena)]
|
||||
bstubs = [[bseq[v - lena], v] for v in range(lena, lena + lenb)]
|
||||
astubs.sort()
|
||||
bstubs.sort()
|
||||
while astubs:
|
||||
(degree, u) = astubs.pop() # take of largest degree node in the a set
|
||||
if degree == 0:
|
||||
break # done, all are zero
|
||||
# connect the source to the smallest degree nodes in the b set
|
||||
for target in bstubs[0:degree]:
|
||||
v = target[1]
|
||||
G.add_edge(u, v)
|
||||
target[0] -= 1 # note this updates bstubs too.
|
||||
if target[0] == 0:
|
||||
bstubs.remove(target)
|
||||
|
||||
G.name = "bipartite_reverse_havel_hakimi_graph"
|
||||
return G
|
||||
|
||||
|
||||
@nx._dispatchable(graphs=None, returns_graph=True)
|
||||
def alternating_havel_hakimi_graph(aseq, bseq, create_using=None):
|
||||
"""Returns a bipartite graph from two given degree sequences using
|
||||
an alternating Havel-Hakimi style construction.
|
||||
|
||||
The graph is composed of two partitions. Set A has nodes 0 to
|
||||
(len(aseq) - 1) and set B has nodes len(aseq) to (len(bseq) - 1).
|
||||
Nodes from the set A are connected to nodes in the set B by
|
||||
connecting the highest degree nodes in set A to alternatively the
|
||||
highest and the lowest degree nodes in set B until all stubs are
|
||||
connected.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
aseq : list
|
||||
Degree sequence for node set A.
|
||||
bseq : list
|
||||
Degree sequence for node set B.
|
||||
create_using : NetworkX graph instance, optional
|
||||
Return graph of this type.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The sum of the two sequences must be equal: sum(aseq)=sum(bseq)
|
||||
If no graph type is specified use MultiGraph with parallel edges.
|
||||
If you want a graph with no parallel edges use create_using=Graph()
|
||||
but then the resulting degree sequences might not be exact.
|
||||
|
||||
The nodes are assigned the attribute 'bipartite' with the value 0 or 1
|
||||
to indicate which bipartite set the node belongs to.
|
||||
|
||||
This function is not imported in the main namespace.
|
||||
To use it use nx.bipartite.alternating_havel_hakimi_graph
|
||||
"""
|
||||
G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
|
||||
if G.is_directed():
|
||||
raise nx.NetworkXError("Directed Graph not supported")
|
||||
|
||||
# length of the each sequence
|
||||
naseq = len(aseq)
|
||||
nbseq = len(bseq)
|
||||
suma = sum(aseq)
|
||||
sumb = sum(bseq)
|
||||
|
||||
if not suma == sumb:
|
||||
raise nx.NetworkXError(
|
||||
f"invalid degree sequences, sum(aseq)!=sum(bseq),{suma},{sumb}"
|
||||
)
|
||||
|
||||
G = _add_nodes_with_bipartite_label(G, naseq, nbseq)
|
||||
|
||||
if len(aseq) == 0 or max(aseq) == 0:
|
||||
return G # done if no edges
|
||||
# build list of degree-repeated vertex numbers
|
||||
astubs = [[aseq[v], v] for v in range(naseq)]
|
||||
bstubs = [[bseq[v - naseq], v] for v in range(naseq, naseq + nbseq)]
|
||||
while astubs:
|
||||
astubs.sort()
|
||||
(degree, u) = astubs.pop() # take of largest degree node in the a set
|
||||
if degree == 0:
|
||||
break # done, all are zero
|
||||
bstubs.sort()
|
||||
small = bstubs[0 : degree // 2] # add these low degree targets
|
||||
large = bstubs[(-degree + degree // 2) :] # now high degree targets
|
||||
stubs = [x for z in zip(large, small) for x in z] # combine, sorry
|
||||
if len(stubs) < len(small) + len(large): # check for zip truncation
|
||||
stubs.append(large.pop())
|
||||
for target in stubs:
|
||||
v = target[1]
|
||||
G.add_edge(u, v)
|
||||
target[0] -= 1 # note this updates bstubs too.
|
||||
if target[0] == 0:
|
||||
bstubs.remove(target)
|
||||
|
||||
G.name = "bipartite_alternating_havel_hakimi_graph"
|
||||
return G
|
||||
|
||||
|
||||
@py_random_state(3)
|
||||
@nx._dispatchable(graphs=None, returns_graph=True)
|
||||
def preferential_attachment_graph(aseq, p, create_using=None, seed=None):
|
||||
"""Create a bipartite graph with a preferential attachment model from
|
||||
a given single degree sequence.
|
||||
|
||||
The graph is composed of two partitions. Set A has nodes 0 to
|
||||
(len(aseq) - 1) and set B has nodes starting with node len(aseq).
|
||||
The number of nodes in set B is random.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
aseq : list
|
||||
Degree sequence for node set A.
|
||||
p : float
|
||||
Probability that a new bottom node is added.
|
||||
create_using : NetworkX graph instance, optional
|
||||
Return graph of this type.
|
||||
seed : integer, random_state, or None (default)
|
||||
Indicator of random number generation state.
|
||||
See :ref:`Randomness<randomness>`.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Guillaume, J.L. and Latapy, M.,
|
||||
Bipartite graphs as models of complex networks.
|
||||
Physica A: Statistical Mechanics and its Applications,
|
||||
2006, 371(2), pp.795-813.
|
||||
.. [2] Jean-Loup Guillaume and Matthieu Latapy,
|
||||
Bipartite structure of all complex networks,
|
||||
Inf. Process. Lett. 90, 2004, pg. 215-221
|
||||
https://doi.org/10.1016/j.ipl.2004.03.007
|
||||
|
||||
Notes
|
||||
-----
|
||||
The nodes are assigned the attribute 'bipartite' with the value 0 or 1
|
||||
to indicate which bipartite set the node belongs to.
|
||||
|
||||
This function is not imported in the main namespace.
|
||||
To use it use nx.bipartite.preferential_attachment_graph
|
||||
"""
|
||||
G = nx.empty_graph(0, create_using, default=nx.MultiGraph)
|
||||
if G.is_directed():
|
||||
raise nx.NetworkXError("Directed Graph not supported")
|
||||
|
||||
if p > 1:
|
||||
raise nx.NetworkXError(f"probability {p} > 1")
|
||||
|
||||
naseq = len(aseq)
|
||||
G = _add_nodes_with_bipartite_label(G, naseq, 0)
|
||||
vv = [[v] * aseq[v] for v in range(naseq)]
|
||||
while vv:
|
||||
while vv[0]:
|
||||
source = vv[0][0]
|
||||
vv[0].remove(source)
|
||||
if seed.random() < p or len(G) == naseq:
|
||||
target = len(G)
|
||||
G.add_node(target, bipartite=1)
|
||||
G.add_edge(source, target)
|
||||
else:
|
||||
bb = [[b] * G.degree(b) for b in range(naseq, len(G))]
|
||||
# flatten the list of lists into a list.
|
||||
bbstubs = reduce(lambda x, y: x + y, bb)
|
||||
# choose preferentially a bottom node.
|
||||
target = seed.choice(bbstubs)
|
||||
G.add_node(target, bipartite=1)
|
||||
G.add_edge(source, target)
|
||||
vv.remove(vv[0])
|
||||
G.name = "bipartite_preferential_attachment_model"
|
||||
return G
|
||||
|
||||
|
||||
@py_random_state(3)
|
||||
@nx._dispatchable(graphs=None, returns_graph=True)
|
||||
def random_graph(n, m, p, seed=None, directed=False):
|
||||
"""Returns a bipartite random graph.
|
||||
|
||||
This is a bipartite version of the binomial (Erdős-Rényi) graph.
|
||||
The graph is composed of two partitions. Set A has nodes 0 to
|
||||
(n - 1) and set B has nodes n to (n + m - 1).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n : int
|
||||
The number of nodes in the first bipartite set.
|
||||
m : int
|
||||
The number of nodes in the second bipartite set.
|
||||
p : float
|
||||
Probability for edge creation.
|
||||
seed : integer, random_state, or None (default)
|
||||
Indicator of random number generation state.
|
||||
See :ref:`Randomness<randomness>`.
|
||||
directed : bool, optional (default=False)
|
||||
If True return a directed graph
|
||||
|
||||
Notes
|
||||
-----
|
||||
The bipartite random graph algorithm chooses each of the n*m (undirected)
|
||||
or 2*nm (directed) possible edges with probability p.
|
||||
|
||||
This algorithm is $O(n+m)$ where $m$ is the expected number of edges.
|
||||
|
||||
The nodes are assigned the attribute 'bipartite' with the value 0 or 1
|
||||
to indicate which bipartite set the node belongs to.
|
||||
|
||||
This function is not imported in the main namespace.
|
||||
To use it use nx.bipartite.random_graph
|
||||
|
||||
See Also
|
||||
--------
|
||||
gnp_random_graph, configuration_model
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Vladimir Batagelj and Ulrik Brandes,
|
||||
"Efficient generation of large random networks",
|
||||
Phys. Rev. E, 71, 036113, 2005.
|
||||
"""
|
||||
G = nx.Graph()
|
||||
G = _add_nodes_with_bipartite_label(G, n, m)
|
||||
if directed:
|
||||
G = nx.DiGraph(G)
|
||||
G.name = f"fast_gnp_random_graph({n},{m},{p})"
|
||||
|
||||
if p <= 0:
|
||||
return G
|
||||
if p >= 1:
|
||||
return nx.complete_bipartite_graph(n, m)
|
||||
|
||||
lp = math.log(1.0 - p)
|
||||
|
||||
v = 0
|
||||
w = -1
|
||||
while v < n:
|
||||
lr = math.log(1.0 - seed.random())
|
||||
w = w + 1 + int(lr / lp)
|
||||
while w >= m and v < n:
|
||||
w = w - m
|
||||
v = v + 1
|
||||
if v < n:
|
||||
G.add_edge(v, n + w)
|
||||
|
||||
if directed:
|
||||
# use the same algorithm to
|
||||
# add edges from the "m" to "n" set
|
||||
v = 0
|
||||
w = -1
|
||||
while v < n:
|
||||
lr = math.log(1.0 - seed.random())
|
||||
w = w + 1 + int(lr / lp)
|
||||
while w >= m and v < n:
|
||||
w = w - m
|
||||
v = v + 1
|
||||
if v < n:
|
||||
G.add_edge(n + w, v)
|
||||
|
||||
return G
|
||||
|
||||
|
||||
@py_random_state(3)
|
||||
@nx._dispatchable(graphs=None, returns_graph=True)
|
||||
def gnmk_random_graph(n, m, k, seed=None, directed=False):
|
||||
"""Returns a random bipartite graph G_{n,m,k}.
|
||||
|
||||
Produces a bipartite graph chosen randomly out of the set of all graphs
|
||||
with n top nodes, m bottom nodes, and k edges.
|
||||
The graph is composed of two sets of nodes.
|
||||
Set A has nodes 0 to (n - 1) and set B has nodes n to (n + m - 1).
|
||||
|
||||
Parameters
|
||||
----------
|
||||
n : int
|
||||
The number of nodes in the first bipartite set.
|
||||
m : int
|
||||
The number of nodes in the second bipartite set.
|
||||
k : int
|
||||
The number of edges
|
||||
seed : integer, random_state, or None (default)
|
||||
Indicator of random number generation state.
|
||||
See :ref:`Randomness<randomness>`.
|
||||
directed : bool, optional (default=False)
|
||||
If True return a directed graph
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> G = nx.bipartite.gnmk_random_graph(10, 20, 50)
|
||||
|
||||
See Also
|
||||
--------
|
||||
gnm_random_graph
|
||||
|
||||
Notes
|
||||
-----
|
||||
If k > m * n then a complete bipartite graph is returned.
|
||||
|
||||
This graph is a bipartite version of the `G_{nm}` random graph model.
|
||||
|
||||
The nodes are assigned the attribute 'bipartite' with the value 0 or 1
|
||||
to indicate which bipartite set the node belongs to.
|
||||
|
||||
This function is not imported in the main namespace.
|
||||
To use it use nx.bipartite.gnmk_random_graph
|
||||
"""
|
||||
G = nx.Graph()
|
||||
G = _add_nodes_with_bipartite_label(G, n, m)
|
||||
if directed:
|
||||
G = nx.DiGraph(G)
|
||||
G.name = f"bipartite_gnm_random_graph({n},{m},{k})"
|
||||
if n == 1 or m == 1:
|
||||
return G
|
||||
max_edges = n * m # max_edges for bipartite networks
|
||||
if k >= max_edges: # Maybe we should raise an exception here
|
||||
return nx.complete_bipartite_graph(n, m, create_using=G)
|
||||
|
||||
top = [n for n, d in G.nodes(data=True) if d["bipartite"] == 0]
|
||||
bottom = list(set(G) - set(top))
|
||||
edge_count = 0
|
||||
while edge_count < k:
|
||||
# generate random edge,u,v
|
||||
u = seed.choice(top)
|
||||
v = seed.choice(bottom)
|
||||
if v in G[u]:
|
||||
continue
|
||||
else:
|
||||
G.add_edge(u, v)
|
||||
edge_count += 1
|
||||
return G
|
||||
|
||||
|
||||
def _add_nodes_with_bipartite_label(G, lena, lenb):
|
||||
G.add_nodes_from(range(lena + lenb))
|
||||
b = dict(zip(range(lena), [0] * lena))
|
||||
b.update(dict(zip(range(lena, lena + lenb), [1] * lenb)))
|
||||
nx.set_node_attributes(G, b, "bipartite")
|
||||
return G
|
||||
+316
@@ -0,0 +1,316 @@
|
||||
import itertools
|
||||
|
||||
import networkx as nx
|
||||
|
||||
__all__ = ["birank"]
|
||||
|
||||
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def birank(
|
||||
G,
|
||||
nodes,
|
||||
*,
|
||||
alpha=None,
|
||||
beta=None,
|
||||
top_personalization=None,
|
||||
bottom_personalization=None,
|
||||
max_iter=100,
|
||||
tol=1.0e-6,
|
||||
weight="weight",
|
||||
):
|
||||
r"""Compute the BiRank score for nodes in a bipartite network.
|
||||
|
||||
Given the bipartite sets $U$ and $P$, the BiRank algorithm seeks to satisfy
|
||||
the following recursive relationships between the scores of nodes $j \in P$
|
||||
and $i \in U$:
|
||||
|
||||
.. math::
|
||||
|
||||
p_j = \alpha \sum_{i \in U} \frac{w_{ij}}{\sqrt{d_i}\sqrt{d_j}} u_i
|
||||
+ (1 - \alpha) p_j^0
|
||||
|
||||
u_i = \beta \sum_{j \in P} \frac{w_{ij}}{\sqrt{d_i}\sqrt{d_j}} p_j
|
||||
+ (1 - \beta) u_i^0
|
||||
|
||||
where
|
||||
|
||||
* $p_j$ and $u_i$ are the BiRank scores of nodes $j \in P$ and $i \in U$.
|
||||
* $w_{ij}$ is the weight of the edge between nodes $i \in U$ and $j \in P$
|
||||
(With a value of 0 if no edge exists).
|
||||
* $d_i$ and $d_j$ are the weighted degrees of nodes $i \in U$ and $j \in P$,
|
||||
respectively.
|
||||
* $p_j^0$ and $u_i^0$ are personalization values that can encode a priori
|
||||
weights for the nodes $j \in P$ and $i \in U$, respectively. Akin to the
|
||||
personalization vector used by PageRank.
|
||||
* $\alpha$ and $\beta$ are damping hyperparameters applying to nodes in $P$
|
||||
and $U$ respectively. They can take values in the interval $[0, 1]$, and
|
||||
are analogous to those used by PageRank.
|
||||
|
||||
Below are two use cases for this algorithm.
|
||||
|
||||
1. Personalized Recommendation System
|
||||
Given a bipartite graph representing users and items, BiRank can be used
|
||||
as a collaborative filtering algorithm to recommend items to users.
|
||||
Previous ratings are encoded as edge weights, and the specific ratings
|
||||
of an individual user on a set of items is used as the personalization
|
||||
vector over items. See the example below for an implementation of this
|
||||
on a toy dataset provided in [1]_.
|
||||
|
||||
2. Popularity Prediction
|
||||
Given a bipartite graph representing user interactions with items, e.g.
|
||||
commits to a GitHub repository, BiRank can be used to predict the
|
||||
popularity of a given item. Edge weights should encode the strength of
|
||||
the interaction signal. This could be a raw count, or weighted by a time
|
||||
decay function like that specified in Eq. (15) of [1]_. The
|
||||
personalization vectors can be used to encode existing popularity
|
||||
signals, for example, the monthly download count of a repository's
|
||||
package.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A bipartite network
|
||||
|
||||
nodes : iterable of nodes
|
||||
Container with all nodes belonging to the first bipartite node set
|
||||
('top'). The nodes in this set use the hyperparameter `alpha`, and the
|
||||
personalization dictionary `top_personalization`. The nodes in the second
|
||||
bipartite node set ('bottom') are automatically determined by taking the
|
||||
complement of 'top' with respect to the graph `G`.
|
||||
|
||||
alpha : float, optional (default=0.80 if top_personalization not empty, else 1)
|
||||
Damping factor for the 'top' nodes. Must be in the interval $[0, 1]$.
|
||||
Larger alpha and beta generally reduce the effect of the personalizations
|
||||
and increase the number of iterations before convergence. Choice of value
|
||||
is largely dependent on use case, and experimentation is recommended.
|
||||
|
||||
beta : float, optional (default=0.80 if bottom_personalization not empty, else 1)
|
||||
Damping factor for the 'bottom' nodes. Must be in the interval $[0, 1]$.
|
||||
Larger alpha and beta generally reduce the effect of the personalizations
|
||||
and increase the number of iterations before convergence. Choice of value
|
||||
is largely dependent on use case, and experimentation is recommended.
|
||||
|
||||
top_personalization : dict, optional (default=None)
|
||||
Dictionary keyed by nodes in 'top' to that node's personalization value.
|
||||
Unspecified nodes in 'top' will be assigned a personalization value of 0.
|
||||
Personalization values are used to encode a priori weights for a given node,
|
||||
and should be non-negative.
|
||||
|
||||
bottom_personalization : dict, optional (default=None)
|
||||
Dictionary keyed by nodes in 'bottom' to that node's personalization value.
|
||||
Unspecified nodes in 'bottom' will be assigned a personalization value of 0.
|
||||
Personalization values are used to encode a priori weights for a given node,
|
||||
and should be non-negative.
|
||||
|
||||
max_iter : int, optional (default=100)
|
||||
Maximum number of iterations in power method eigenvalue solver.
|
||||
|
||||
tol : float, optional (default=1.0e-6)
|
||||
Error tolerance used to check convergence in power method solver. The
|
||||
iteration will stop after a tolerance of both ``len(top) * tol`` and
|
||||
``len(bottom) * tol`` is reached for nodes in 'top' and 'bottom'
|
||||
respectively.
|
||||
|
||||
weight : string or None, optional (default='weight')
|
||||
Edge data key to use as weight.
|
||||
|
||||
Returns
|
||||
-------
|
||||
birank : dictionary
|
||||
Dictionary keyed by node to that node's BiRank score.
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXAlgorithmError
|
||||
If the parameters `alpha` or `beta` are not in the interval [0, 1],
|
||||
if either of the bipartite sets are empty, or if negative values are
|
||||
provided in the personalization dictionaries.
|
||||
|
||||
PowerIterationFailedConvergence
|
||||
If the algorithm fails to converge to the specified tolerance
|
||||
within the specified number of iterations of the power iteration
|
||||
method.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Construct a bipartite graph with user-item ratings and use BiRank to
|
||||
recommend items to a user (user 1). The example below uses the `rating`
|
||||
edge attribute as the weight of the edges. The `top_personalization` vector
|
||||
is used to encode the user's previous ratings on items.
|
||||
|
||||
Creation of graph, bipartite sets for the example.
|
||||
|
||||
>>> elist = [
|
||||
... ("u1", "p1", 5),
|
||||
... ("u2", "p1", 5),
|
||||
... ("u2", "p2", 4),
|
||||
... ("u3", "p1", 3),
|
||||
... ("u3", "p3", 2),
|
||||
... ]
|
||||
>>> G = nx.Graph()
|
||||
>>> G.add_weighted_edges_from(elist, weight="rating")
|
||||
>>> product_nodes = ("p1", "p2", "p3")
|
||||
>>> user = "u1"
|
||||
|
||||
First, we create a personalization vector for the user based on on their
|
||||
ratings of past items. In this case they have only rated one item (p1, with
|
||||
a rating of 5) in the past.
|
||||
|
||||
>>> user_personalization = {
|
||||
... product: rating
|
||||
... for _, product, rating in G.edges(nbunch=user, data="rating")
|
||||
... }
|
||||
>>> user_personalization
|
||||
{'p1': 5}
|
||||
|
||||
Calculate the BiRank score of all nodes in the graph, filter for the items
|
||||
that the user has not rated yet, and sort the results by score.
|
||||
|
||||
>>> user_birank_results = nx.bipartite.birank(
|
||||
... G, product_nodes, top_personalization=user_personalization, weight="rating"
|
||||
... )
|
||||
>>> user_birank_results = filter(
|
||||
... lambda item: item[0][0] == "p" and user not in G.neighbors(item[0]),
|
||||
... user_birank_results.items(),
|
||||
... )
|
||||
>>> user_birank_results = sorted(
|
||||
... user_birank_results, key=lambda item: item[1], reverse=True
|
||||
... )
|
||||
>>> user_recommendations = {
|
||||
... product: round(score, 5) for product, score in user_birank_results
|
||||
... }
|
||||
>>> user_recommendations
|
||||
{'p2': 1.44818, 'p3': 1.04811}
|
||||
|
||||
We find that user 1 should be recommended item p2 over item p3. This is due
|
||||
to the fact that user 2 rated also rated p1 highly, while user 3 did not.
|
||||
Thus user 2's tastes are inferred to be similar to user 1's, and carry more
|
||||
weight in the recommendation.
|
||||
|
||||
See Also
|
||||
--------
|
||||
:func:`~networkx.algorithms.link_analysis.pagerank_alg.pagerank`
|
||||
:func:`~networkx.algorithms.link_analysis.hits_alg.hits`
|
||||
:func:`~networkx.algorithms.bipartite.centrality.betweenness_centrality`
|
||||
:func:`~networkx.algorithms.bipartite.basic.sets`
|
||||
:func:`~networkx.algorithms.bipartite.basic.is_bipartite`
|
||||
|
||||
Notes
|
||||
-----
|
||||
The `nodes` input parameter must contain all nodes in one bipartite
|
||||
node set, but the dictionary returned contains all nodes from both
|
||||
bipartite node sets. See :mod:`bipartite documentation
|
||||
<networkx.algorithms.bipartite>` for further details on how
|
||||
bipartite graphs are handled in NetworkX.
|
||||
|
||||
In the case a personalization dictionary is not provided for top (bottom)
|
||||
`alpha` (`beta`) will default to 1. This is because a damping factor
|
||||
without a non-zero entry in the personalization vector will lead to the
|
||||
algorithm converging to the zero vector.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Xiangnan He, Ming Gao, Min-Yen Kan, and Dingxian Wang. 2017.
|
||||
BiRank: Towards Ranking on Bipartite Graphs. IEEE Trans. on Knowl.
|
||||
and Data Eng. 29, 1 (January 2017), 57–71.
|
||||
https://arxiv.org/pdf/1708.04396
|
||||
|
||||
"""
|
||||
import numpy as np
|
||||
import scipy as sp
|
||||
|
||||
# Initialize the sets of top and bottom nodes
|
||||
top = set(nodes)
|
||||
bottom = set(G) - top
|
||||
top_count = len(top)
|
||||
bottom_count = len(bottom)
|
||||
|
||||
if top_count == 0 or bottom_count == 0:
|
||||
raise nx.NetworkXAlgorithmError(
|
||||
"The BiRank algorithm requires a bipartite graph with at least one"
|
||||
"node in each set."
|
||||
)
|
||||
|
||||
# Clean the personalization dictionaries
|
||||
top_personalization = _clean_personalization_dict(top_personalization)
|
||||
bottom_personalization = _clean_personalization_dict(bottom_personalization)
|
||||
|
||||
# Set default values for alpha and beta if not provided
|
||||
if alpha is None:
|
||||
alpha = 0.8 if top_personalization else 1
|
||||
if beta is None:
|
||||
beta = 0.8 if bottom_personalization else 1
|
||||
|
||||
if alpha < 0 or alpha > 1:
|
||||
raise nx.NetworkXAlgorithmError("alpha must be in the interval [0, 1]")
|
||||
if beta < 0 or beta > 1:
|
||||
raise nx.NetworkXAlgorithmError("beta must be in the interval [0, 1]")
|
||||
|
||||
# Initialize query vectors
|
||||
p0 = np.array([top_personalization.get(n, 0) for n in top], dtype=float)
|
||||
u0 = np.array([bottom_personalization.get(n, 0) for n in bottom], dtype=float)
|
||||
|
||||
# Construct degree normalized biadjacency matrix `S` and its transpose
|
||||
W = nx.bipartite.biadjacency_matrix(G, bottom, top, weight=weight, dtype=float)
|
||||
p_degrees = W.sum(axis=0, dtype=float)
|
||||
# Handle case where the node is disconnected - avoids warning
|
||||
p_degrees[p_degrees == 0] = 1.0
|
||||
D_p = sp.sparse.dia_array(
|
||||
([1.0 / np.sqrt(p_degrees)], [0]),
|
||||
shape=(top_count, top_count),
|
||||
dtype=float,
|
||||
)
|
||||
u_degrees = W.sum(axis=1, dtype=float)
|
||||
u_degrees[u_degrees == 0] = 1.0
|
||||
D_u = sp.sparse.dia_array(
|
||||
([1.0 / np.sqrt(u_degrees)], [0]),
|
||||
shape=(bottom_count, bottom_count),
|
||||
dtype=float,
|
||||
)
|
||||
S = D_u.tocsr() @ W @ D_p.tocsr()
|
||||
S_T = S.T
|
||||
|
||||
# Initialize birank vectors for iteration
|
||||
p = np.ones(top_count, dtype=float) / top_count
|
||||
u = beta * (S @ p) + (1 - beta) * u0
|
||||
|
||||
# Iterate until convergence
|
||||
for _ in range(max_iter):
|
||||
p_last = p
|
||||
u_last = u
|
||||
p = alpha * (S_T @ u) + (1 - alpha) * p0
|
||||
u = beta * (S @ p) + (1 - beta) * u0
|
||||
|
||||
# Continue iterating if the error (absolute if less than 1, relative otherwise)
|
||||
# is above the tolerance threshold for either p or u
|
||||
err_u = np.absolute((u_last - u) / np.maximum(1.0, u_last)).sum()
|
||||
if err_u >= len(u) * tol:
|
||||
continue
|
||||
err_p = np.absolute((p_last - p) / np.maximum(1.0, p_last)).sum()
|
||||
if err_p >= len(p) * tol:
|
||||
continue
|
||||
|
||||
# Handle edge case where if both alpha and beta are 1, scale is
|
||||
# indeterminate, so normalization is required to return consistent results
|
||||
if alpha == 1 and beta == 1:
|
||||
p = p / np.linalg.norm(p, 1)
|
||||
u = u / np.linalg.norm(u, 1)
|
||||
|
||||
# If both error thresholds pass, return a single dictionary mapping
|
||||
# nodes to their scores
|
||||
return dict(
|
||||
zip(itertools.chain(top, bottom), map(float, itertools.chain(p, u)))
|
||||
)
|
||||
|
||||
# If we reach this point, we have not converged
|
||||
raise nx.PowerIterationFailedConvergence(max_iter)
|
||||
|
||||
|
||||
def _clean_personalization_dict(personalization):
|
||||
"""Filter out zero values from the personalization dictionary,
|
||||
handle case where None is passed, ensure values are non-negative."""
|
||||
if personalization is None:
|
||||
return {}
|
||||
if any(value < 0 for value in personalization.values()):
|
||||
raise nx.NetworkXAlgorithmError("Personalization values must be non-negative.")
|
||||
return {node: value for node, value in personalization.items() if value != 0}
|
||||
@@ -0,0 +1,590 @@
|
||||
# This module uses material from the Wikipedia article Hopcroft--Karp algorithm
|
||||
# <https://en.wikipedia.org/wiki/Hopcroft%E2%80%93Karp_algorithm>, accessed on
|
||||
# January 3, 2015, which is released under the Creative Commons
|
||||
# Attribution-Share-Alike License 3.0
|
||||
# <http://creativecommons.org/licenses/by-sa/3.0/>. That article includes
|
||||
# pseudocode, which has been translated into the corresponding Python code.
|
||||
#
|
||||
# Portions of this module use code from David Eppstein's Python Algorithms and
|
||||
# Data Structures (PADS) library, which is dedicated to the public domain (for
|
||||
# proof, see <http://www.ics.uci.edu/~eppstein/PADS/ABOUT-PADS.txt>).
|
||||
"""Provides functions for computing maximum cardinality matchings and minimum
|
||||
weight full matchings in a bipartite graph.
|
||||
|
||||
If you don't care about the particular implementation of the maximum matching
|
||||
algorithm, simply use the :func:`maximum_matching`. If you do care, you can
|
||||
import one of the named maximum matching algorithms directly.
|
||||
|
||||
For example, to find a maximum matching in the complete bipartite graph with
|
||||
two vertices on the left and three vertices on the right:
|
||||
|
||||
>>> G = nx.complete_bipartite_graph(2, 3)
|
||||
>>> left, right = nx.bipartite.sets(G)
|
||||
>>> list(left)
|
||||
[0, 1]
|
||||
>>> list(right)
|
||||
[2, 3, 4]
|
||||
>>> nx.bipartite.maximum_matching(G)
|
||||
{0: 2, 1: 3, 2: 0, 3: 1}
|
||||
|
||||
The dictionary returned by :func:`maximum_matching` includes a mapping for
|
||||
vertices in both the left and right vertex sets.
|
||||
|
||||
Similarly, :func:`minimum_weight_full_matching` produces, for a complete
|
||||
weighted bipartite graph, a matching whose cardinality is the cardinality of
|
||||
the smaller of the two partitions, and for which the sum of the weights of the
|
||||
edges included in the matching is minimal.
|
||||
|
||||
"""
|
||||
|
||||
import collections
|
||||
import itertools
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.bipartite import sets as bipartite_sets
|
||||
from networkx.algorithms.bipartite.matrix import biadjacency_matrix
|
||||
|
||||
__all__ = [
|
||||
"maximum_matching",
|
||||
"hopcroft_karp_matching",
|
||||
"eppstein_matching",
|
||||
"to_vertex_cover",
|
||||
"minimum_weight_full_matching",
|
||||
]
|
||||
|
||||
INFINITY = float("inf")
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def hopcroft_karp_matching(G, top_nodes=None):
|
||||
"""Returns the maximum cardinality matching of the bipartite graph `G`.
|
||||
|
||||
A matching is a set of edges that do not share any nodes. A maximum
|
||||
cardinality matching is a matching with the most edges possible. It
|
||||
is not always unique. Finding a matching in a bipartite graph can be
|
||||
treated as a networkx flow problem.
|
||||
|
||||
The functions ``hopcroft_karp_matching`` and ``maximum_matching``
|
||||
are aliases of the same function.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
Undirected bipartite graph
|
||||
|
||||
top_nodes : container of nodes
|
||||
|
||||
Container with all nodes in one bipartite node set. If not supplied
|
||||
it will be computed. But if more than one solution exists an exception
|
||||
will be raised.
|
||||
|
||||
Returns
|
||||
-------
|
||||
matches : dictionary
|
||||
|
||||
The matching is returned as a dictionary, `matches`, such that
|
||||
``matches[v] == w`` if node `v` is matched to node `w`. Unmatched
|
||||
nodes do not occur as a key in `matches`.
|
||||
|
||||
Raises
|
||||
------
|
||||
AmbiguousSolution
|
||||
Raised if the input bipartite graph is disconnected and no container
|
||||
with all nodes in one bipartite set is provided. When determining
|
||||
the nodes in each bipartite set more than one valid solution is
|
||||
possible if the input graph is disconnected.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This function is implemented with the `Hopcroft--Karp matching algorithm
|
||||
<https://en.wikipedia.org/wiki/Hopcroft%E2%80%93Karp_algorithm>`_ for
|
||||
bipartite graphs.
|
||||
|
||||
See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
|
||||
for further details on how bipartite graphs are handled in NetworkX.
|
||||
|
||||
See Also
|
||||
--------
|
||||
maximum_matching
|
||||
hopcroft_karp_matching
|
||||
eppstein_matching
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] John E. Hopcroft and Richard M. Karp. "An n^{5 / 2} Algorithm for
|
||||
Maximum Matchings in Bipartite Graphs" In: **SIAM Journal of Computing**
|
||||
2.4 (1973), pp. 225--231. <https://doi.org/10.1137/0202019>.
|
||||
|
||||
"""
|
||||
|
||||
# First we define some auxiliary search functions.
|
||||
#
|
||||
# If you are a human reading these auxiliary search functions, the "global"
|
||||
# variables `leftmatches`, `rightmatches`, `distances`, etc. are defined
|
||||
# below the functions, so that they are initialized close to the initial
|
||||
# invocation of the search functions.
|
||||
def breadth_first_search():
|
||||
for v in left:
|
||||
if leftmatches[v] is None:
|
||||
distances[v] = 0
|
||||
queue.append(v)
|
||||
else:
|
||||
distances[v] = INFINITY
|
||||
distances[None] = INFINITY
|
||||
while queue:
|
||||
v = queue.popleft()
|
||||
if distances[v] < distances[None]:
|
||||
for u in G[v]:
|
||||
if distances[rightmatches[u]] is INFINITY:
|
||||
distances[rightmatches[u]] = distances[v] + 1
|
||||
queue.append(rightmatches[u])
|
||||
return distances[None] is not INFINITY
|
||||
|
||||
def depth_first_search(v):
|
||||
if v is not None:
|
||||
for u in G[v]:
|
||||
if distances[rightmatches[u]] == distances[v] + 1:
|
||||
if depth_first_search(rightmatches[u]):
|
||||
rightmatches[u] = v
|
||||
leftmatches[v] = u
|
||||
return True
|
||||
distances[v] = INFINITY
|
||||
return False
|
||||
return True
|
||||
|
||||
# Initialize the "global" variables that maintain state during the search.
|
||||
left, right = bipartite_sets(G, top_nodes)
|
||||
leftmatches = {v: None for v in left}
|
||||
rightmatches = {v: None for v in right}
|
||||
distances = {}
|
||||
queue = collections.deque()
|
||||
|
||||
# Implementation note: this counter is incremented as pairs are matched but
|
||||
# it is currently not used elsewhere in the computation.
|
||||
num_matched_pairs = 0
|
||||
while breadth_first_search():
|
||||
for v in left:
|
||||
if leftmatches[v] is None:
|
||||
if depth_first_search(v):
|
||||
num_matched_pairs += 1
|
||||
|
||||
# Strip the entries matched to `None`.
|
||||
leftmatches = {k: v for k, v in leftmatches.items() if v is not None}
|
||||
rightmatches = {k: v for k, v in rightmatches.items() if v is not None}
|
||||
|
||||
# At this point, the left matches and the right matches are inverses of one
|
||||
# another. In other words,
|
||||
#
|
||||
# leftmatches == {v, k for k, v in rightmatches.items()}
|
||||
#
|
||||
# Finally, we combine both the left matches and right matches.
|
||||
return dict(itertools.chain(leftmatches.items(), rightmatches.items()))
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def eppstein_matching(G, top_nodes=None):
|
||||
"""Returns the maximum cardinality matching of the bipartite graph `G`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
Undirected bipartite graph
|
||||
|
||||
top_nodes : container
|
||||
|
||||
Container with all nodes in one bipartite node set. If not supplied
|
||||
it will be computed. But if more than one solution exists an exception
|
||||
will be raised.
|
||||
|
||||
Returns
|
||||
-------
|
||||
matches : dictionary
|
||||
|
||||
The matching is returned as a dictionary, `matching`, such that
|
||||
``matching[v] == w`` if node `v` is matched to node `w`. Unmatched
|
||||
nodes do not occur as a key in `matching`.
|
||||
|
||||
Raises
|
||||
------
|
||||
AmbiguousSolution
|
||||
Raised if the input bipartite graph is disconnected and no container
|
||||
with all nodes in one bipartite set is provided. When determining
|
||||
the nodes in each bipartite set more than one valid solution is
|
||||
possible if the input graph is disconnected.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This function is implemented with David Eppstein's version of the algorithm
|
||||
Hopcroft--Karp algorithm (see :func:`hopcroft_karp_matching`), which
|
||||
originally appeared in the `Python Algorithms and Data Structures library
|
||||
(PADS) <http://www.ics.uci.edu/~eppstein/PADS/ABOUT-PADS.txt>`_.
|
||||
|
||||
See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
|
||||
for further details on how bipartite graphs are handled in NetworkX.
|
||||
|
||||
See Also
|
||||
--------
|
||||
|
||||
hopcroft_karp_matching
|
||||
|
||||
"""
|
||||
# Due to its original implementation, a directed graph is needed
|
||||
# so that the two sets of bipartite nodes can be distinguished
|
||||
left, right = bipartite_sets(G, top_nodes)
|
||||
G = nx.DiGraph(G.edges(left))
|
||||
# initialize greedy matching (redundant, but faster than full search)
|
||||
matching = {}
|
||||
for u in G:
|
||||
for v in G[u]:
|
||||
if v not in matching:
|
||||
matching[v] = u
|
||||
break
|
||||
while True:
|
||||
# structure residual graph into layers
|
||||
# pred[u] gives the neighbor in the previous layer for u in U
|
||||
# preds[v] gives a list of neighbors in the previous layer for v in V
|
||||
# unmatched gives a list of unmatched vertices in final layer of V,
|
||||
# and is also used as a flag value for pred[u] when u is in the first
|
||||
# layer
|
||||
preds = {}
|
||||
unmatched = []
|
||||
pred = {u: unmatched for u in G}
|
||||
for v in matching:
|
||||
del pred[matching[v]]
|
||||
layer = list(pred)
|
||||
|
||||
# repeatedly extend layering structure by another pair of layers
|
||||
while layer and not unmatched:
|
||||
newLayer = {}
|
||||
for u in layer:
|
||||
for v in G[u]:
|
||||
if v not in preds:
|
||||
newLayer.setdefault(v, []).append(u)
|
||||
layer = []
|
||||
for v in newLayer:
|
||||
preds[v] = newLayer[v]
|
||||
if v in matching:
|
||||
layer.append(matching[v])
|
||||
pred[matching[v]] = v
|
||||
else:
|
||||
unmatched.append(v)
|
||||
|
||||
# did we finish layering without finding any alternating paths?
|
||||
if not unmatched:
|
||||
# TODO - The lines between --- were unused and were thus commented
|
||||
# out. This whole commented chunk should be reviewed to determine
|
||||
# whether it should be built upon or completely removed.
|
||||
# ---
|
||||
# unlayered = {}
|
||||
# for u in G:
|
||||
# # TODO Why is extra inner loop necessary?
|
||||
# for v in G[u]:
|
||||
# if v not in preds:
|
||||
# unlayered[v] = None
|
||||
# ---
|
||||
# TODO Originally, this function returned a three-tuple:
|
||||
#
|
||||
# return (matching, list(pred), list(unlayered))
|
||||
#
|
||||
# For some reason, the documentation for this function
|
||||
# indicated that the second and third elements of the returned
|
||||
# three-tuple would be the vertices in the left and right vertex
|
||||
# sets, respectively, that are also in the maximum independent set.
|
||||
# However, what I think the author meant was that the second
|
||||
# element is the list of vertices that were unmatched and the third
|
||||
# element was the list of vertices that were matched. Since that
|
||||
# seems to be the case, they don't really need to be returned,
|
||||
# since that information can be inferred from the matching
|
||||
# dictionary.
|
||||
|
||||
# All the matched nodes must be a key in the dictionary
|
||||
for key in matching.copy():
|
||||
matching[matching[key]] = key
|
||||
return matching
|
||||
|
||||
# recursively search backward through layers to find alternating paths
|
||||
# recursion returns true if found path, false otherwise
|
||||
def recurse(v):
|
||||
if v in preds:
|
||||
L = preds.pop(v)
|
||||
for u in L:
|
||||
if u in pred:
|
||||
pu = pred.pop(u)
|
||||
if pu is unmatched or recurse(pu):
|
||||
matching[v] = u
|
||||
return True
|
||||
return False
|
||||
|
||||
for v in unmatched:
|
||||
recurse(v)
|
||||
|
||||
|
||||
def _is_connected_by_alternating_path(G, v, matched_edges, unmatched_edges, targets):
|
||||
"""Returns True if and only if the vertex `v` is connected to one of
|
||||
the target vertices by an alternating path in `G`.
|
||||
|
||||
An *alternating path* is a path in which every other edge is in the
|
||||
specified maximum matching (and the remaining edges in the path are not in
|
||||
the matching). An alternating path may have matched edges in the even
|
||||
positions or in the odd positions, as long as the edges alternate between
|
||||
'matched' and 'unmatched'.
|
||||
|
||||
`G` is an undirected bipartite NetworkX graph.
|
||||
|
||||
`v` is a vertex in `G`.
|
||||
|
||||
`matched_edges` is a set of edges present in a maximum matching in `G`.
|
||||
|
||||
`unmatched_edges` is a set of edges not present in a maximum
|
||||
matching in `G`.
|
||||
|
||||
`targets` is a set of vertices.
|
||||
|
||||
"""
|
||||
|
||||
def _alternating_dfs(u, along_matched=True):
|
||||
"""Returns True if and only if `u` is connected to one of the
|
||||
targets by an alternating path.
|
||||
|
||||
`u` is a vertex in the graph `G`.
|
||||
|
||||
If `along_matched` is True, this step of the depth-first search
|
||||
will continue only through edges in the given matching. Otherwise, it
|
||||
will continue only through edges *not* in the given matching.
|
||||
|
||||
"""
|
||||
visited = set()
|
||||
# Follow matched edges when depth is even,
|
||||
# and follow unmatched edges when depth is odd.
|
||||
initial_depth = 0 if along_matched else 1
|
||||
stack = [(u, iter(G[u]), initial_depth)]
|
||||
while stack:
|
||||
parent, children, depth = stack[-1]
|
||||
valid_edges = matched_edges if depth % 2 else unmatched_edges
|
||||
try:
|
||||
child = next(children)
|
||||
if child not in visited:
|
||||
if (parent, child) in valid_edges or (child, parent) in valid_edges:
|
||||
if child in targets:
|
||||
return True
|
||||
visited.add(child)
|
||||
stack.append((child, iter(G[child]), depth + 1))
|
||||
except StopIteration:
|
||||
stack.pop()
|
||||
return False
|
||||
|
||||
# Check for alternating paths starting with edges in the matching, then
|
||||
# check for alternating paths starting with edges not in the
|
||||
# matching.
|
||||
return _alternating_dfs(v, along_matched=True) or _alternating_dfs(
|
||||
v, along_matched=False
|
||||
)
|
||||
|
||||
|
||||
def _connected_by_alternating_paths(G, matching, targets):
|
||||
"""Returns the set of vertices that are connected to one of the target
|
||||
vertices by an alternating path in `G` or are themselves a target.
|
||||
|
||||
An *alternating path* is a path in which every other edge is in the
|
||||
specified maximum matching (and the remaining edges in the path are not in
|
||||
the matching). An alternating path may have matched edges in the even
|
||||
positions or in the odd positions, as long as the edges alternate between
|
||||
'matched' and 'unmatched'.
|
||||
|
||||
`G` is an undirected bipartite NetworkX graph.
|
||||
|
||||
`matching` is a dictionary representing a maximum matching in `G`, as
|
||||
returned by, for example, :func:`maximum_matching`.
|
||||
|
||||
`targets` is a set of vertices.
|
||||
|
||||
"""
|
||||
# Get the set of matched edges and the set of unmatched edges. Only include
|
||||
# one version of each undirected edge (for example, include edge (1, 2) but
|
||||
# not edge (2, 1)). Using frozensets as an intermediary step we do not
|
||||
# require nodes to be orderable.
|
||||
edge_sets = {frozenset((u, v)) for u, v in matching.items()}
|
||||
matched_edges = {tuple(edge) for edge in edge_sets}
|
||||
unmatched_edges = {
|
||||
(u, v) for (u, v) in G.edges() if frozenset((u, v)) not in edge_sets
|
||||
}
|
||||
|
||||
return {
|
||||
v
|
||||
for v in G
|
||||
if v in targets
|
||||
or _is_connected_by_alternating_path(
|
||||
G, v, matched_edges, unmatched_edges, targets
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def to_vertex_cover(G, matching, top_nodes=None):
|
||||
"""Returns the minimum vertex cover corresponding to the given maximum
|
||||
matching of the bipartite graph `G`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
Undirected bipartite graph
|
||||
|
||||
matching : dictionary
|
||||
|
||||
A dictionary whose keys are vertices in `G` and whose values are the
|
||||
distinct neighbors comprising the maximum matching for `G`, as returned
|
||||
by, for example, :func:`maximum_matching`. The dictionary *must*
|
||||
represent the maximum matching.
|
||||
|
||||
top_nodes : container
|
||||
|
||||
Container with all nodes in one bipartite node set. If not supplied
|
||||
it will be computed. But if more than one solution exists an exception
|
||||
will be raised.
|
||||
|
||||
Returns
|
||||
-------
|
||||
vertex_cover : :class:`set`
|
||||
|
||||
The minimum vertex cover in `G`.
|
||||
|
||||
Raises
|
||||
------
|
||||
AmbiguousSolution
|
||||
Raised if the input bipartite graph is disconnected and no container
|
||||
with all nodes in one bipartite set is provided. When determining
|
||||
the nodes in each bipartite set more than one valid solution is
|
||||
possible if the input graph is disconnected.
|
||||
|
||||
Notes
|
||||
-----
|
||||
This function is implemented using the procedure guaranteed by `Konig's
|
||||
theorem
|
||||
<https://en.wikipedia.org/wiki/K%C3%B6nig%27s_theorem_%28graph_theory%29>`_,
|
||||
which proves an equivalence between a maximum matching and a minimum vertex
|
||||
cover in bipartite graphs.
|
||||
|
||||
Since a minimum vertex cover is the complement of a maximum independent set
|
||||
for any graph, one can compute the maximum independent set of a bipartite
|
||||
graph this way:
|
||||
|
||||
>>> G = nx.complete_bipartite_graph(2, 3)
|
||||
>>> matching = nx.bipartite.maximum_matching(G)
|
||||
>>> vertex_cover = nx.bipartite.to_vertex_cover(G, matching)
|
||||
>>> independent_set = set(G) - vertex_cover
|
||||
>>> print(list(independent_set))
|
||||
[2, 3, 4]
|
||||
|
||||
See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
|
||||
for further details on how bipartite graphs are handled in NetworkX.
|
||||
|
||||
"""
|
||||
# This is a Python implementation of the algorithm described at
|
||||
# <https://en.wikipedia.org/wiki/K%C3%B6nig%27s_theorem_%28graph_theory%29#Proof>.
|
||||
L, R = bipartite_sets(G, top_nodes)
|
||||
# Let U be the set of unmatched vertices in the left vertex set.
|
||||
unmatched_vertices = set(G) - set(matching)
|
||||
U = unmatched_vertices & L
|
||||
# Let Z be the set of vertices that are either in U or are connected to U
|
||||
# by alternating paths.
|
||||
Z = _connected_by_alternating_paths(G, matching, U)
|
||||
# At this point, every edge either has a right endpoint in Z or a left
|
||||
# endpoint not in Z. This gives us the vertex cover.
|
||||
return (L - Z) | (R & Z)
|
||||
|
||||
|
||||
#: Returns the maximum cardinality matching in the given bipartite graph.
|
||||
#:
|
||||
#: This function is simply an alias for :func:`hopcroft_karp_matching`.
|
||||
maximum_matching = hopcroft_karp_matching
|
||||
|
||||
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def minimum_weight_full_matching(G, top_nodes=None, weight="weight"):
|
||||
r"""Returns a minimum weight full matching of the bipartite graph `G`.
|
||||
|
||||
Let :math:`G = ((U, V), E)` be a weighted bipartite graph with real weights
|
||||
:math:`w : E \to \mathbb{R}`. This function then produces a matching
|
||||
:math:`M \subseteq E` with cardinality
|
||||
|
||||
.. math::
|
||||
\lvert M \rvert = \min(\lvert U \rvert, \lvert V \rvert),
|
||||
|
||||
which minimizes the sum of the weights of the edges included in the
|
||||
matching, :math:`\sum_{e \in M} w(e)`, or raises an error if no such
|
||||
matching exists.
|
||||
|
||||
When :math:`\lvert U \rvert = \lvert V \rvert`, this is commonly
|
||||
referred to as a perfect matching; here, since we allow
|
||||
:math:`\lvert U \rvert` and :math:`\lvert V \rvert` to differ, we
|
||||
follow Karp [1]_ and refer to the matching as *full*.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
Undirected bipartite graph
|
||||
|
||||
top_nodes : container
|
||||
|
||||
Container with all nodes in one bipartite node set. If not supplied
|
||||
it will be computed.
|
||||
|
||||
weight : string, optional (default='weight')
|
||||
|
||||
The edge data key used to provide each value in the matrix.
|
||||
If None, then each edge has weight 1.
|
||||
|
||||
Returns
|
||||
-------
|
||||
matches : dictionary
|
||||
|
||||
The matching is returned as a dictionary, `matches`, such that
|
||||
``matches[v] == w`` if node `v` is matched to node `w`. Unmatched
|
||||
nodes do not occur as a key in `matches`.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
Raised if no full matching exists.
|
||||
|
||||
ImportError
|
||||
Raised if SciPy is not available.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The problem of determining a minimum weight full matching is also known as
|
||||
the rectangular linear assignment problem. This implementation defers the
|
||||
calculation of the assignment to SciPy.
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Richard Manning Karp:
|
||||
An algorithm to Solve the m x n Assignment Problem in Expected Time
|
||||
O(mn log n).
|
||||
Networks, 10(2):143–152, 1980.
|
||||
|
||||
"""
|
||||
import numpy as np
|
||||
import scipy as sp
|
||||
|
||||
left, right = nx.bipartite.sets(G, top_nodes)
|
||||
U = list(left)
|
||||
V = list(right)
|
||||
# We explicitly create the biadjacency matrix having infinities
|
||||
# where edges are missing (as opposed to zeros, which is what one would
|
||||
# get by using toarray on the sparse matrix).
|
||||
weights_sparse = biadjacency_matrix(
|
||||
G, row_order=U, column_order=V, weight=weight, format="coo"
|
||||
)
|
||||
weights = np.full(weights_sparse.shape, np.inf)
|
||||
weights[weights_sparse.row, weights_sparse.col] = weights_sparse.data
|
||||
left_matches = sp.optimize.linear_sum_assignment(weights)
|
||||
d = {U[u]: V[v] for u, v in zip(*left_matches)}
|
||||
# d will contain the matching from edges in left to right; we need to
|
||||
# add the ones from right to left as well.
|
||||
d.update({v: u for u, v in d.items()})
|
||||
return d
|
||||
@@ -0,0 +1,168 @@
|
||||
"""
|
||||
====================
|
||||
Biadjacency matrices
|
||||
====================
|
||||
"""
|
||||
|
||||
import itertools
|
||||
|
||||
import networkx as nx
|
||||
from networkx.convert_matrix import _generate_weighted_edges
|
||||
|
||||
__all__ = ["biadjacency_matrix", "from_biadjacency_matrix"]
|
||||
|
||||
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def biadjacency_matrix(
|
||||
G, row_order, column_order=None, dtype=None, weight="weight", format="csr"
|
||||
):
|
||||
r"""Returns the biadjacency matrix of the bipartite graph G.
|
||||
|
||||
Let `G = (U, V, E)` be a bipartite graph with node sets
|
||||
`U = u_{1},...,u_{r}` and `V = v_{1},...,v_{s}`. The biadjacency
|
||||
matrix [1]_ is the `r` x `s` matrix `B` in which `b_{i,j} = 1`
|
||||
if, and only if, `(u_i, v_j) \in E`. If the parameter `weight` is
|
||||
not `None` and matches the name of an edge attribute, its value is
|
||||
used instead of 1.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A NetworkX graph
|
||||
|
||||
row_order : list of nodes
|
||||
The rows of the matrix are ordered according to the list of nodes.
|
||||
|
||||
column_order : list, optional
|
||||
The columns of the matrix are ordered according to the list of nodes.
|
||||
If column_order is None, then the ordering of columns is arbitrary.
|
||||
|
||||
dtype : NumPy data-type, optional
|
||||
A valid NumPy dtype used to initialize the array. If None, then the
|
||||
NumPy default is used.
|
||||
|
||||
weight : string or None, optional (default='weight')
|
||||
The edge data key used to provide each value in the matrix.
|
||||
If None, then each edge has weight 1.
|
||||
|
||||
format : str in {'dense', 'bsr', 'csr', 'csc', 'coo', 'lil', 'dia', 'dok'}
|
||||
The type of the matrix to be returned (default 'csr'). For
|
||||
some algorithms different implementations of sparse matrices
|
||||
can perform better. See [2]_ for details.
|
||||
|
||||
Returns
|
||||
-------
|
||||
M : SciPy sparse array
|
||||
Biadjacency matrix representation of the bipartite graph G.
|
||||
|
||||
Notes
|
||||
-----
|
||||
No attempt is made to check that the input graph is bipartite.
|
||||
|
||||
For directed bipartite graphs only successors are considered as neighbors.
|
||||
To obtain an adjacency matrix with ones (or weight values) for both
|
||||
predecessors and successors you have to generate two biadjacency matrices
|
||||
where the rows of one of them are the columns of the other, and then add
|
||||
one to the transpose of the other.
|
||||
|
||||
See Also
|
||||
--------
|
||||
adjacency_matrix
|
||||
from_biadjacency_matrix
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] https://en.wikipedia.org/wiki/Adjacency_matrix#Adjacency_matrix_of_a_bipartite_graph
|
||||
.. [2] Scipy Dev. References, "Sparse Matrices",
|
||||
https://docs.scipy.org/doc/scipy/reference/sparse.html
|
||||
"""
|
||||
import scipy as sp
|
||||
|
||||
nlen = len(row_order)
|
||||
if nlen == 0:
|
||||
raise nx.NetworkXError("row_order is empty list")
|
||||
if len(row_order) != len(set(row_order)):
|
||||
msg = "Ambiguous ordering: `row_order` contained duplicates."
|
||||
raise nx.NetworkXError(msg)
|
||||
if column_order is None:
|
||||
column_order = list(set(G) - set(row_order))
|
||||
mlen = len(column_order)
|
||||
if len(column_order) != len(set(column_order)):
|
||||
msg = "Ambiguous ordering: `column_order` contained duplicates."
|
||||
raise nx.NetworkXError(msg)
|
||||
|
||||
row_index = dict(zip(row_order, itertools.count()))
|
||||
col_index = dict(zip(column_order, itertools.count()))
|
||||
|
||||
if G.number_of_edges() == 0:
|
||||
row, col, data = [], [], []
|
||||
else:
|
||||
row, col, data = zip(
|
||||
*(
|
||||
(row_index[u], col_index[v], d.get(weight, 1))
|
||||
for u, v, d in G.edges(row_order, data=True)
|
||||
if u in row_index and v in col_index
|
||||
)
|
||||
)
|
||||
A = sp.sparse.coo_array((data, (row, col)), shape=(nlen, mlen), dtype=dtype)
|
||||
try:
|
||||
return A.asformat(format)
|
||||
except ValueError as err:
|
||||
raise nx.NetworkXError(f"Unknown sparse array format: {format}") from err
|
||||
|
||||
|
||||
@nx._dispatchable(graphs=None, returns_graph=True)
|
||||
def from_biadjacency_matrix(A, create_using=None, edge_attribute="weight"):
|
||||
r"""Creates a new bipartite graph from a biadjacency matrix given as a
|
||||
SciPy sparse array.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
A: scipy sparse array
|
||||
A biadjacency matrix representation of a graph
|
||||
|
||||
create_using: NetworkX graph
|
||||
Use specified graph for result. The default is Graph()
|
||||
|
||||
edge_attribute: string
|
||||
Name of edge attribute to store matrix numeric value. The data will
|
||||
have the same type as the matrix entry (int, float, (real,imag)).
|
||||
|
||||
Notes
|
||||
-----
|
||||
The nodes are labeled with the attribute `bipartite` set to an integer
|
||||
0 or 1 representing membership in part 0 or part 1 of the bipartite graph.
|
||||
|
||||
If `create_using` is an instance of :class:`networkx.MultiGraph` or
|
||||
:class:`networkx.MultiDiGraph` and the entries of `A` are of
|
||||
type :class:`int`, then this function returns a multigraph (of the same
|
||||
type as `create_using`) with parallel edges. In this case, `edge_attribute`
|
||||
will be ignored.
|
||||
|
||||
See Also
|
||||
--------
|
||||
biadjacency_matrix
|
||||
from_numpy_array
|
||||
|
||||
References
|
||||
----------
|
||||
[1] https://en.wikipedia.org/wiki/Adjacency_matrix#Adjacency_matrix_of_a_bipartite_graph
|
||||
"""
|
||||
G = nx.empty_graph(0, create_using)
|
||||
n, m = A.shape
|
||||
# Make sure we get even the isolated nodes of the graph.
|
||||
G.add_nodes_from(range(n), bipartite=0)
|
||||
G.add_nodes_from(range(n, n + m), bipartite=1)
|
||||
# Create an iterable over (u, v, w) triples and for each triple, add an
|
||||
# edge from u to v with weight w.
|
||||
triples = ((u, n + v, d) for (u, v, d) in _generate_weighted_edges(A))
|
||||
# If the entries in the adjacency matrix are integers and the graph is a
|
||||
# multigraph, then create parallel edges, each with weight 1, for each
|
||||
# entry in the adjacency matrix. Otherwise, create one edge for each
|
||||
# positive entry in the adjacency matrix and set the weight of that edge to
|
||||
# be the entry in the matrix.
|
||||
if A.dtype.kind in ("i", "u") and G.is_multigraph():
|
||||
chain = itertools.chain.from_iterable
|
||||
triples = chain(((u, v, 1) for d in range(w)) for (u, v, w) in triples)
|
||||
G.add_weighted_edges_from(triples, weight=edge_attribute)
|
||||
return G
|
||||
@@ -0,0 +1,526 @@
|
||||
"""One-mode (unipartite) projections of bipartite graphs."""
|
||||
|
||||
import networkx as nx
|
||||
from networkx.exception import NetworkXAlgorithmError
|
||||
from networkx.utils import not_implemented_for
|
||||
|
||||
__all__ = [
|
||||
"projected_graph",
|
||||
"weighted_projected_graph",
|
||||
"collaboration_weighted_projected_graph",
|
||||
"overlap_weighted_projected_graph",
|
||||
"generic_weighted_projected_graph",
|
||||
]
|
||||
|
||||
|
||||
@nx._dispatchable(
|
||||
graphs="B", preserve_node_attrs=True, preserve_graph_attrs=True, returns_graph=True
|
||||
)
|
||||
def projected_graph(B, nodes, multigraph=False):
|
||||
r"""Returns the projection of B onto one of its node sets.
|
||||
|
||||
Returns the graph G that is the projection of the bipartite graph B
|
||||
onto the specified nodes. They retain their attributes and are connected
|
||||
in G if they have a common neighbor in B.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
B : NetworkX graph
|
||||
The input graph should be bipartite.
|
||||
|
||||
nodes : list or iterable
|
||||
Nodes to project onto (the "bottom" nodes).
|
||||
|
||||
multigraph: bool (default=False)
|
||||
If True return a multigraph where the multiple edges represent multiple
|
||||
shared neighbors. They edge key in the multigraph is assigned to the
|
||||
label of the neighbor.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Graph : NetworkX graph or multigraph
|
||||
A graph that is the projection onto the given nodes.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> B = nx.path_graph(4)
|
||||
>>> G = bipartite.projected_graph(B, [1, 3])
|
||||
>>> list(G)
|
||||
[1, 3]
|
||||
>>> list(G.edges())
|
||||
[(1, 3)]
|
||||
|
||||
If nodes `a`, and `b` are connected through both nodes 1 and 2 then
|
||||
building a multigraph results in two edges in the projection onto
|
||||
[`a`, `b`]:
|
||||
|
||||
>>> B = nx.Graph()
|
||||
>>> B.add_edges_from([("a", 1), ("b", 1), ("a", 2), ("b", 2)])
|
||||
>>> G = bipartite.projected_graph(B, ["a", "b"], multigraph=True)
|
||||
>>> print([sorted((u, v)) for u, v in G.edges()])
|
||||
[['a', 'b'], ['a', 'b']]
|
||||
|
||||
Notes
|
||||
-----
|
||||
No attempt is made to verify that the input graph B is bipartite.
|
||||
Returns a simple graph that is the projection of the bipartite graph B
|
||||
onto the set of nodes given in list nodes. If multigraph=True then
|
||||
a multigraph is returned with an edge for every shared neighbor.
|
||||
|
||||
Directed graphs are allowed as input. The output will also then
|
||||
be a directed graph with edges if there is a directed path between
|
||||
the nodes.
|
||||
|
||||
The graph and node properties are (shallow) copied to the projected graph.
|
||||
|
||||
See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
|
||||
for further details on how bipartite graphs are handled in NetworkX.
|
||||
|
||||
See Also
|
||||
--------
|
||||
is_bipartite,
|
||||
is_bipartite_node_set,
|
||||
sets,
|
||||
weighted_projected_graph,
|
||||
collaboration_weighted_projected_graph,
|
||||
overlap_weighted_projected_graph,
|
||||
generic_weighted_projected_graph
|
||||
"""
|
||||
if B.is_multigraph():
|
||||
raise nx.NetworkXError("not defined for multigraphs")
|
||||
if B.is_directed():
|
||||
directed = True
|
||||
if multigraph:
|
||||
G = nx.MultiDiGraph()
|
||||
else:
|
||||
G = nx.DiGraph()
|
||||
else:
|
||||
directed = False
|
||||
if multigraph:
|
||||
G = nx.MultiGraph()
|
||||
else:
|
||||
G = nx.Graph()
|
||||
G.graph.update(B.graph)
|
||||
G.add_nodes_from((n, B.nodes[n]) for n in nodes)
|
||||
for u in nodes:
|
||||
nbrs2 = {v for nbr in B[u] for v in B[nbr] if v != u}
|
||||
if multigraph:
|
||||
for n in nbrs2:
|
||||
if directed:
|
||||
links = set(B[u]) & set(B.pred[n])
|
||||
else:
|
||||
links = set(B[u]) & set(B[n])
|
||||
for l in links:
|
||||
if not G.has_edge(u, n, l):
|
||||
G.add_edge(u, n, key=l)
|
||||
else:
|
||||
G.add_edges_from((u, n) for n in nbrs2)
|
||||
return G
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable(graphs="B", returns_graph=True)
|
||||
def weighted_projected_graph(B, nodes, ratio=False):
|
||||
r"""Returns a weighted projection of B onto one of its node sets.
|
||||
|
||||
The weighted projected graph is the projection of the bipartite
|
||||
network B onto the specified nodes with weights representing the
|
||||
number of shared neighbors or the ratio between actual shared
|
||||
neighbors and possible shared neighbors if ``ratio is True`` [1]_.
|
||||
The nodes retain their attributes and are connected in the resulting
|
||||
graph if they have an edge to a common node in the original graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
B : NetworkX graph
|
||||
The input graph should be bipartite.
|
||||
|
||||
nodes : list or iterable
|
||||
Distinct nodes to project onto (the "bottom" nodes).
|
||||
|
||||
ratio: Bool (default=False)
|
||||
If True, edge weight is the ratio between actual shared neighbors
|
||||
and maximum possible shared neighbors (i.e., the size of the other
|
||||
node set). If False, edges weight is the number of shared neighbors.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Graph : NetworkX graph
|
||||
A graph that is the projection onto the given nodes.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> B = nx.path_graph(4)
|
||||
>>> G = bipartite.weighted_projected_graph(B, [1, 3])
|
||||
>>> list(G)
|
||||
[1, 3]
|
||||
>>> list(G.edges(data=True))
|
||||
[(1, 3, {'weight': 1})]
|
||||
>>> G = bipartite.weighted_projected_graph(B, [1, 3], ratio=True)
|
||||
>>> list(G.edges(data=True))
|
||||
[(1, 3, {'weight': 0.5})]
|
||||
|
||||
Notes
|
||||
-----
|
||||
No attempt is made to verify that the input graph B is bipartite, or that
|
||||
the input nodes are distinct. However, if the length of the input nodes is
|
||||
greater than or equal to the nodes in the graph B, an exception is raised.
|
||||
If the nodes are not distinct but don't raise this error, the output weights
|
||||
will be incorrect.
|
||||
The graph and node properties are (shallow) copied to the projected graph.
|
||||
|
||||
See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
|
||||
for further details on how bipartite graphs are handled in NetworkX.
|
||||
|
||||
See Also
|
||||
--------
|
||||
is_bipartite,
|
||||
is_bipartite_node_set,
|
||||
sets,
|
||||
collaboration_weighted_projected_graph,
|
||||
overlap_weighted_projected_graph,
|
||||
generic_weighted_projected_graph
|
||||
projected_graph
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Borgatti, S.P. and Halgin, D. In press. "Analyzing Affiliation
|
||||
Networks". In Carrington, P. and Scott, J. (eds) The Sage Handbook
|
||||
of Social Network Analysis. Sage Publications.
|
||||
"""
|
||||
if B.is_directed():
|
||||
pred = B.pred
|
||||
G = nx.DiGraph()
|
||||
else:
|
||||
pred = B.adj
|
||||
G = nx.Graph()
|
||||
G.graph.update(B.graph)
|
||||
G.add_nodes_from((n, B.nodes[n]) for n in nodes)
|
||||
n_top = len(B) - len(nodes)
|
||||
|
||||
if n_top < 1:
|
||||
raise NetworkXAlgorithmError(
|
||||
f"the size of the nodes to project onto ({len(nodes)}) is >= the graph size ({len(B)}).\n"
|
||||
"They are either not a valid bipartite partition or contain duplicates"
|
||||
)
|
||||
|
||||
for u in nodes:
|
||||
unbrs = set(B[u])
|
||||
nbrs2 = {n for nbr in unbrs for n in B[nbr]} - {u}
|
||||
for v in nbrs2:
|
||||
vnbrs = set(pred[v])
|
||||
common = unbrs & vnbrs
|
||||
if not ratio:
|
||||
weight = len(common)
|
||||
else:
|
||||
weight = len(common) / n_top
|
||||
G.add_edge(u, v, weight=weight)
|
||||
return G
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable(graphs="B", returns_graph=True)
|
||||
def collaboration_weighted_projected_graph(B, nodes):
|
||||
r"""Newman's weighted projection of B onto one of its node sets.
|
||||
|
||||
The collaboration weighted projection is the projection of the
|
||||
bipartite network B onto the specified nodes with weights assigned
|
||||
using Newman's collaboration model [1]_:
|
||||
|
||||
.. math::
|
||||
|
||||
w_{u, v} = \sum_k \frac{\delta_{u}^{k} \delta_{v}^{k}}{d_k - 1}
|
||||
|
||||
where `u` and `v` are nodes from the bottom bipartite node set,
|
||||
and `k` is a node of the top node set.
|
||||
The value `d_k` is the degree of node `k` in the bipartite
|
||||
network and `\delta_{u}^{k}` is 1 if node `u` is
|
||||
linked to node `k` in the original bipartite graph or 0 otherwise.
|
||||
|
||||
The nodes retain their attributes and are connected in the resulting
|
||||
graph if have an edge to a common node in the original bipartite
|
||||
graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
B : NetworkX graph
|
||||
The input graph should be bipartite.
|
||||
|
||||
nodes : list or iterable
|
||||
Nodes to project onto (the "bottom" nodes).
|
||||
|
||||
Returns
|
||||
-------
|
||||
Graph : NetworkX graph
|
||||
A graph that is the projection onto the given nodes.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> B = nx.path_graph(5)
|
||||
>>> B.add_edge(1, 5)
|
||||
>>> G = bipartite.collaboration_weighted_projected_graph(B, [0, 2, 4, 5])
|
||||
>>> list(G)
|
||||
[0, 2, 4, 5]
|
||||
>>> for edge in sorted(G.edges(data=True)):
|
||||
... print(edge)
|
||||
(0, 2, {'weight': 0.5})
|
||||
(0, 5, {'weight': 0.5})
|
||||
(2, 4, {'weight': 1.0})
|
||||
(2, 5, {'weight': 0.5})
|
||||
|
||||
Notes
|
||||
-----
|
||||
No attempt is made to verify that the input graph B is bipartite.
|
||||
The graph and node properties are (shallow) copied to the projected graph.
|
||||
|
||||
See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
|
||||
for further details on how bipartite graphs are handled in NetworkX.
|
||||
|
||||
See Also
|
||||
--------
|
||||
is_bipartite,
|
||||
is_bipartite_node_set,
|
||||
sets,
|
||||
weighted_projected_graph,
|
||||
overlap_weighted_projected_graph,
|
||||
generic_weighted_projected_graph,
|
||||
projected_graph
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Scientific collaboration networks: II.
|
||||
Shortest paths, weighted networks, and centrality,
|
||||
M. E. J. Newman, Phys. Rev. E 64, 016132 (2001).
|
||||
"""
|
||||
if B.is_directed():
|
||||
pred = B.pred
|
||||
G = nx.DiGraph()
|
||||
else:
|
||||
pred = B.adj
|
||||
G = nx.Graph()
|
||||
G.graph.update(B.graph)
|
||||
G.add_nodes_from((n, B.nodes[n]) for n in nodes)
|
||||
for u in nodes:
|
||||
unbrs = set(B[u])
|
||||
nbrs2 = {n for nbr in unbrs for n in B[nbr] if n != u}
|
||||
for v in nbrs2:
|
||||
vnbrs = set(pred[v])
|
||||
common_degree = (len(B[n]) for n in unbrs & vnbrs)
|
||||
weight = sum(1.0 / (deg - 1) for deg in common_degree if deg > 1)
|
||||
G.add_edge(u, v, weight=weight)
|
||||
return G
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable(graphs="B", returns_graph=True)
|
||||
def overlap_weighted_projected_graph(B, nodes, jaccard=True):
|
||||
r"""Overlap weighted projection of B onto one of its node sets.
|
||||
|
||||
The overlap weighted projection is the projection of the bipartite
|
||||
network B onto the specified nodes with weights representing
|
||||
the Jaccard index between the neighborhoods of the two nodes in the
|
||||
original bipartite network [1]_:
|
||||
|
||||
.. math::
|
||||
|
||||
w_{v, u} = \frac{|N(u) \cap N(v)|}{|N(u) \cup N(v)|}
|
||||
|
||||
or if the parameter 'jaccard' is False, the fraction of common
|
||||
neighbors by minimum of both nodes degree in the original
|
||||
bipartite graph [1]_:
|
||||
|
||||
.. math::
|
||||
|
||||
w_{v, u} = \frac{|N(u) \cap N(v)|}{min(|N(u)|, |N(v)|)}
|
||||
|
||||
The nodes retain their attributes and are connected in the resulting
|
||||
graph if have an edge to a common node in the original bipartite graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
B : NetworkX graph
|
||||
The input graph should be bipartite.
|
||||
|
||||
nodes : list or iterable
|
||||
Nodes to project onto (the "bottom" nodes).
|
||||
|
||||
jaccard: Bool (default=True)
|
||||
|
||||
Returns
|
||||
-------
|
||||
Graph : NetworkX graph
|
||||
A graph that is the projection onto the given nodes.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> B = nx.path_graph(5)
|
||||
>>> nodes = [0, 2, 4]
|
||||
>>> G = bipartite.overlap_weighted_projected_graph(B, nodes)
|
||||
>>> list(G)
|
||||
[0, 2, 4]
|
||||
>>> list(G.edges(data=True))
|
||||
[(0, 2, {'weight': 0.5}), (2, 4, {'weight': 0.5})]
|
||||
>>> G = bipartite.overlap_weighted_projected_graph(B, nodes, jaccard=False)
|
||||
>>> list(G.edges(data=True))
|
||||
[(0, 2, {'weight': 1.0}), (2, 4, {'weight': 1.0})]
|
||||
|
||||
Notes
|
||||
-----
|
||||
No attempt is made to verify that the input graph B is bipartite.
|
||||
The graph and node properties are (shallow) copied to the projected graph.
|
||||
|
||||
See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
|
||||
for further details on how bipartite graphs are handled in NetworkX.
|
||||
|
||||
See Also
|
||||
--------
|
||||
is_bipartite,
|
||||
is_bipartite_node_set,
|
||||
sets,
|
||||
weighted_projected_graph,
|
||||
collaboration_weighted_projected_graph,
|
||||
generic_weighted_projected_graph,
|
||||
projected_graph
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Borgatti, S.P. and Halgin, D. In press. Analyzing Affiliation
|
||||
Networks. In Carrington, P. and Scott, J. (eds) The Sage Handbook
|
||||
of Social Network Analysis. Sage Publications.
|
||||
|
||||
"""
|
||||
if B.is_directed():
|
||||
pred = B.pred
|
||||
G = nx.DiGraph()
|
||||
else:
|
||||
pred = B.adj
|
||||
G = nx.Graph()
|
||||
G.graph.update(B.graph)
|
||||
G.add_nodes_from((n, B.nodes[n]) for n in nodes)
|
||||
for u in nodes:
|
||||
unbrs = set(B[u])
|
||||
nbrs2 = {n for nbr in unbrs for n in B[nbr]} - {u}
|
||||
for v in nbrs2:
|
||||
vnbrs = set(pred[v])
|
||||
if jaccard:
|
||||
wt = len(unbrs & vnbrs) / len(unbrs | vnbrs)
|
||||
else:
|
||||
wt = len(unbrs & vnbrs) / min(len(unbrs), len(vnbrs))
|
||||
G.add_edge(u, v, weight=wt)
|
||||
return G
|
||||
|
||||
|
||||
@not_implemented_for("multigraph")
|
||||
@nx._dispatchable(graphs="B", preserve_all_attrs=True, returns_graph=True)
|
||||
def generic_weighted_projected_graph(B, nodes, weight_function=None):
|
||||
r"""Weighted projection of B with a user-specified weight function.
|
||||
|
||||
The bipartite network B is projected on to the specified nodes
|
||||
with weights computed by a user-specified function. This function
|
||||
must accept as a parameter the neighborhood sets of two nodes and
|
||||
return an integer or a float.
|
||||
|
||||
The nodes retain their attributes and are connected in the resulting graph
|
||||
if they have an edge to a common node in the original graph.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
B : NetworkX graph
|
||||
The input graph should be bipartite.
|
||||
|
||||
nodes : list or iterable
|
||||
Nodes to project onto (the "bottom" nodes).
|
||||
|
||||
weight_function : function
|
||||
This function must accept as parameters the same input graph
|
||||
that this function, and two nodes; and return an integer or a float.
|
||||
The default function computes the number of shared neighbors.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Graph : NetworkX graph
|
||||
A graph that is the projection onto the given nodes.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> # Define some custom weight functions
|
||||
>>> def jaccard(G, u, v):
|
||||
... unbrs = set(G[u])
|
||||
... vnbrs = set(G[v])
|
||||
... return float(len(unbrs & vnbrs)) / len(unbrs | vnbrs)
|
||||
>>> def my_weight(G, u, v, weight="weight"):
|
||||
... w = 0
|
||||
... for nbr in set(G[u]) & set(G[v]):
|
||||
... w += G[u][nbr].get(weight, 1) + G[v][nbr].get(weight, 1)
|
||||
... return w
|
||||
>>> # A complete bipartite graph with 4 nodes and 4 edges
|
||||
>>> B = nx.complete_bipartite_graph(2, 2)
|
||||
>>> # Add some arbitrary weight to the edges
|
||||
>>> for i, (u, v) in enumerate(B.edges()):
|
||||
... B.edges[u, v]["weight"] = i + 1
|
||||
>>> for edge in B.edges(data=True):
|
||||
... print(edge)
|
||||
(0, 2, {'weight': 1})
|
||||
(0, 3, {'weight': 2})
|
||||
(1, 2, {'weight': 3})
|
||||
(1, 3, {'weight': 4})
|
||||
>>> # By default, the weight is the number of shared neighbors
|
||||
>>> G = bipartite.generic_weighted_projected_graph(B, [0, 1])
|
||||
>>> print(list(G.edges(data=True)))
|
||||
[(0, 1, {'weight': 2})]
|
||||
>>> # To specify a custom weight function use the weight_function parameter
|
||||
>>> G = bipartite.generic_weighted_projected_graph(
|
||||
... B, [0, 1], weight_function=jaccard
|
||||
... )
|
||||
>>> print(list(G.edges(data=True)))
|
||||
[(0, 1, {'weight': 1.0})]
|
||||
>>> G = bipartite.generic_weighted_projected_graph(
|
||||
... B, [0, 1], weight_function=my_weight
|
||||
... )
|
||||
>>> print(list(G.edges(data=True)))
|
||||
[(0, 1, {'weight': 10})]
|
||||
|
||||
Notes
|
||||
-----
|
||||
No attempt is made to verify that the input graph B is bipartite.
|
||||
The graph and node properties are (shallow) copied to the projected graph.
|
||||
|
||||
See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
|
||||
for further details on how bipartite graphs are handled in NetworkX.
|
||||
|
||||
See Also
|
||||
--------
|
||||
is_bipartite,
|
||||
is_bipartite_node_set,
|
||||
sets,
|
||||
weighted_projected_graph,
|
||||
collaboration_weighted_projected_graph,
|
||||
overlap_weighted_projected_graph,
|
||||
projected_graph
|
||||
|
||||
"""
|
||||
if B.is_directed():
|
||||
pred = B.pred
|
||||
G = nx.DiGraph()
|
||||
else:
|
||||
pred = B.adj
|
||||
G = nx.Graph()
|
||||
if weight_function is None:
|
||||
|
||||
def weight_function(G, u, v):
|
||||
# Notice that we use set(pred[v]) for handling the directed case.
|
||||
return len(set(G[u]) & set(pred[v]))
|
||||
|
||||
G.graph.update(B.graph)
|
||||
G.add_nodes_from((n, B.nodes[n]) for n in nodes)
|
||||
for u in nodes:
|
||||
nbrs2 = {n for nbr in set(B[u]) for n in B[nbr]} - {u}
|
||||
for v in nbrs2:
|
||||
weight = weight_function(B, u, v)
|
||||
G.add_edge(u, v, weight=weight)
|
||||
return G
|
||||
@@ -0,0 +1,112 @@
|
||||
"""Node redundancy for bipartite graphs."""
|
||||
|
||||
from itertools import combinations
|
||||
|
||||
import networkx as nx
|
||||
from networkx import NetworkXError
|
||||
|
||||
__all__ = ["node_redundancy"]
|
||||
|
||||
|
||||
@nx._dispatchable
|
||||
def node_redundancy(G, nodes=None):
|
||||
r"""Computes the node redundancy coefficients for the nodes in the bipartite
|
||||
graph `G`.
|
||||
|
||||
The redundancy coefficient of a node `v` is the fraction of pairs of
|
||||
neighbors of `v` that are both linked to other nodes. In a one-mode
|
||||
projection these nodes would be linked together even if `v` were
|
||||
not there.
|
||||
|
||||
More formally, for any vertex `v`, the *redundancy coefficient of `v`* is
|
||||
defined by
|
||||
|
||||
.. math::
|
||||
|
||||
rc(v) = \frac{|\{\{u, w\} \subseteq N(v),
|
||||
\: \exists v' \neq v,\: (v',u) \in E\:
|
||||
\mathrm{and}\: (v',w) \in E\}|}{ \frac{|N(v)|(|N(v)|-1)}{2}},
|
||||
|
||||
where `N(v)` is the set of neighbors of `v` in `G` [1]_.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : graph
|
||||
A bipartite graph
|
||||
|
||||
nodes : list or iterable (optional)
|
||||
Compute redundancy for these nodes. The default is all nodes in G.
|
||||
|
||||
Returns
|
||||
-------
|
||||
redundancy : dictionary
|
||||
A dictionary keyed by node with the node redundancy value.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Compute the redundancy coefficient of each node in a graph:
|
||||
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> G = nx.cycle_graph(4)
|
||||
>>> rc = bipartite.node_redundancy(G)
|
||||
>>> rc[0]
|
||||
1.0
|
||||
|
||||
Compute the average redundancy for the graph:
|
||||
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> G = nx.cycle_graph(4)
|
||||
>>> rc = bipartite.node_redundancy(G)
|
||||
>>> sum(rc.values()) / len(G)
|
||||
1.0
|
||||
|
||||
Compute the average redundancy for a set of nodes:
|
||||
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> G = nx.cycle_graph(4)
|
||||
>>> rc = bipartite.node_redundancy(G)
|
||||
>>> nodes = [0, 2]
|
||||
>>> sum(rc[n] for n in nodes) / len(nodes)
|
||||
1.0
|
||||
|
||||
Raises
|
||||
------
|
||||
NetworkXError
|
||||
If any of the nodes in the graph (or in `nodes`, if specified) has
|
||||
(out-)degree less than two (which would result in division by zero,
|
||||
according to the definition of the redundancy coefficient).
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] Latapy, Matthieu, Clémence Magnien, and Nathalie Del Vecchio (2008).
|
||||
Basic notions for the analysis of large two-mode networks.
|
||||
Social Networks 30(1), 31--48.
|
||||
|
||||
"""
|
||||
if nodes is None:
|
||||
nodes = G
|
||||
if any(len(G[v]) < 2 for v in nodes):
|
||||
raise NetworkXError(
|
||||
"Cannot compute redundancy coefficient for a node"
|
||||
" that has fewer than two neighbors."
|
||||
)
|
||||
# TODO This can be trivially parallelized.
|
||||
return {v: _node_redundancy(G, v) for v in nodes}
|
||||
|
||||
|
||||
def _node_redundancy(G, v):
|
||||
"""Returns the redundancy of the node `v` in the bipartite graph `G`.
|
||||
|
||||
If `G` is a graph with `n` nodes, the redundancy of a node is the ratio
|
||||
of the "overlap" of `v` to the maximum possible overlap of `v`
|
||||
according to its degree. The overlap of `v` is the number of pairs of
|
||||
neighbors that have mutual neighbors themselves, other than `v`.
|
||||
|
||||
`v` must have at least two neighbors in `G`.
|
||||
|
||||
"""
|
||||
n = len(G[v])
|
||||
overlap = sum(
|
||||
1 for (u, w) in combinations(G[v], 2) if (set(G[u]) & set(G[w])) - {v}
|
||||
)
|
||||
return (2 * overlap) / (n * (n - 1))
|
||||
@@ -0,0 +1,69 @@
|
||||
"""
|
||||
Spectral bipartivity measure.
|
||||
"""
|
||||
|
||||
import networkx as nx
|
||||
|
||||
__all__ = ["spectral_bipartivity"]
|
||||
|
||||
|
||||
@nx._dispatchable(edge_attrs="weight")
|
||||
def spectral_bipartivity(G, nodes=None, weight="weight"):
|
||||
"""Returns the spectral bipartivity.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
G : NetworkX graph
|
||||
|
||||
nodes : list or container optional(default is all nodes)
|
||||
Nodes to return value of spectral bipartivity contribution.
|
||||
|
||||
weight : string or None optional (default = 'weight')
|
||||
Edge data key to use for edge weights. If None, weights set to 1.
|
||||
|
||||
Returns
|
||||
-------
|
||||
sb : float or dict
|
||||
A single number if the keyword nodes is not specified, or
|
||||
a dictionary keyed by node with the spectral bipartivity contribution
|
||||
of that node as the value.
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from networkx.algorithms import bipartite
|
||||
>>> G = nx.path_graph(4)
|
||||
>>> bipartite.spectral_bipartivity(G)
|
||||
1.0
|
||||
|
||||
Notes
|
||||
-----
|
||||
This implementation uses Numpy (dense) matrices which are not efficient
|
||||
for storing large sparse graphs.
|
||||
|
||||
See Also
|
||||
--------
|
||||
color
|
||||
|
||||
References
|
||||
----------
|
||||
.. [1] E. Estrada and J. A. Rodríguez-Velázquez, "Spectral measures of
|
||||
bipartivity in complex networks", PhysRev E 72, 046105 (2005)
|
||||
"""
|
||||
import scipy as sp
|
||||
|
||||
nodelist = list(G) # ordering of nodes in matrix
|
||||
A = nx.to_numpy_array(G, nodelist, weight=weight)
|
||||
expA = sp.linalg.expm(A)
|
||||
expmA = sp.linalg.expm(-A)
|
||||
coshA = 0.5 * (expA + expmA)
|
||||
if nodes is None:
|
||||
# return single number for entire graph
|
||||
return float(coshA.diagonal().sum() / expA.diagonal().sum())
|
||||
else:
|
||||
# contribution for individual nodes
|
||||
index = dict(zip(nodelist, range(len(nodelist))))
|
||||
sb = {}
|
||||
for n in nodes:
|
||||
i = index[n]
|
||||
sb[n] = coshA.item(i, i) / expA.item(i, i)
|
||||
return sb
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms import bipartite
|
||||
|
||||
|
||||
class TestBipartiteBasic:
|
||||
def test_is_bipartite(self):
|
||||
assert bipartite.is_bipartite(nx.path_graph(4))
|
||||
assert bipartite.is_bipartite(nx.DiGraph([(1, 0)]))
|
||||
assert not bipartite.is_bipartite(nx.complete_graph(3))
|
||||
|
||||
def test_bipartite_color(self):
|
||||
G = nx.path_graph(4)
|
||||
c = bipartite.color(G)
|
||||
assert c == {0: 1, 1: 0, 2: 1, 3: 0}
|
||||
|
||||
def test_not_bipartite_color(self):
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
c = bipartite.color(nx.complete_graph(4))
|
||||
|
||||
def test_bipartite_directed(self):
|
||||
G = bipartite.random_graph(10, 10, 0.1, directed=True)
|
||||
assert bipartite.is_bipartite(G)
|
||||
|
||||
def test_bipartite_sets(self):
|
||||
G = nx.path_graph(4)
|
||||
X, Y = bipartite.sets(G)
|
||||
assert X == {0, 2}
|
||||
assert Y == {1, 3}
|
||||
|
||||
def test_bipartite_sets_directed(self):
|
||||
G = nx.path_graph(4)
|
||||
D = G.to_directed()
|
||||
X, Y = bipartite.sets(D)
|
||||
assert X == {0, 2}
|
||||
assert Y == {1, 3}
|
||||
|
||||
def test_bipartite_sets_given_top_nodes(self):
|
||||
G = nx.path_graph(4)
|
||||
top_nodes = [0, 2]
|
||||
X, Y = bipartite.sets(G, top_nodes)
|
||||
assert X == {0, 2}
|
||||
assert Y == {1, 3}
|
||||
|
||||
def test_bipartite_sets_disconnected(self):
|
||||
with pytest.raises(nx.AmbiguousSolution):
|
||||
G = nx.path_graph(4)
|
||||
G.add_edges_from([(5, 6), (6, 7)])
|
||||
X, Y = bipartite.sets(G)
|
||||
|
||||
def test_is_bipartite_node_set(self):
|
||||
G = nx.path_graph(4)
|
||||
|
||||
with pytest.raises(nx.AmbiguousSolution):
|
||||
bipartite.is_bipartite_node_set(G, [1, 1, 2, 3])
|
||||
|
||||
assert bipartite.is_bipartite_node_set(G, [0, 2])
|
||||
assert bipartite.is_bipartite_node_set(G, [1, 3])
|
||||
assert not bipartite.is_bipartite_node_set(G, [1, 2])
|
||||
G.add_edge(10, 20)
|
||||
assert bipartite.is_bipartite_node_set(G, [0, 2, 10])
|
||||
assert bipartite.is_bipartite_node_set(G, [0, 2, 20])
|
||||
assert bipartite.is_bipartite_node_set(G, [1, 3, 10])
|
||||
assert bipartite.is_bipartite_node_set(G, [1, 3, 20])
|
||||
|
||||
def test_bipartite_density(self):
|
||||
G = nx.path_graph(5)
|
||||
X, Y = bipartite.sets(G)
|
||||
density = len(list(G.edges())) / (len(X) * len(Y))
|
||||
assert bipartite.density(G, X) == density
|
||||
D = nx.DiGraph(G.edges())
|
||||
assert bipartite.density(D, X) == density / 2.0
|
||||
assert bipartite.density(nx.Graph(), {}) == 0.0
|
||||
|
||||
def test_bipartite_degrees(self):
|
||||
G = nx.path_graph(5)
|
||||
X = {1, 3}
|
||||
Y = {0, 2, 4}
|
||||
u, d = bipartite.degrees(G, Y)
|
||||
assert dict(u) == {1: 2, 3: 2}
|
||||
assert dict(d) == {0: 1, 2: 2, 4: 1}
|
||||
|
||||
def test_bipartite_weighted_degrees(self):
|
||||
G = nx.path_graph(5)
|
||||
G.add_edge(0, 1, weight=0.1, other=0.2)
|
||||
X = {1, 3}
|
||||
Y = {0, 2, 4}
|
||||
u, d = bipartite.degrees(G, Y, weight="weight")
|
||||
assert dict(u) == {1: 1.1, 3: 2}
|
||||
assert dict(d) == {0: 0.1, 2: 2, 4: 1}
|
||||
u, d = bipartite.degrees(G, Y, weight="other")
|
||||
assert dict(u) == {1: 1.2, 3: 2}
|
||||
assert dict(d) == {0: 0.2, 2: 2, 4: 1}
|
||||
|
||||
def test_biadjacency_matrix_weight(self):
|
||||
pytest.importorskip("scipy")
|
||||
G = nx.path_graph(5)
|
||||
G.add_edge(0, 1, weight=2, other=4)
|
||||
X = [1, 3]
|
||||
Y = [0, 2, 4]
|
||||
M = bipartite.biadjacency_matrix(G, X, weight="weight")
|
||||
assert M[0, 0] == 2
|
||||
M = bipartite.biadjacency_matrix(G, X, weight="other")
|
||||
assert M[0, 0] == 4
|
||||
|
||||
def test_biadjacency_matrix(self):
|
||||
pytest.importorskip("scipy")
|
||||
tops = [2, 5, 10]
|
||||
bots = [5, 10, 15]
|
||||
for i in range(len(tops)):
|
||||
G = bipartite.random_graph(tops[i], bots[i], 0.2)
|
||||
top = [n for n, d in G.nodes(data=True) if d["bipartite"] == 0]
|
||||
M = bipartite.biadjacency_matrix(G, top)
|
||||
assert M.shape[0] == tops[i]
|
||||
assert M.shape[1] == bots[i]
|
||||
|
||||
def test_biadjacency_matrix_order(self):
|
||||
pytest.importorskip("scipy")
|
||||
G = nx.path_graph(5)
|
||||
G.add_edge(0, 1, weight=2)
|
||||
X = [3, 1]
|
||||
Y = [4, 2, 0]
|
||||
M = bipartite.biadjacency_matrix(G, X, Y, weight="weight")
|
||||
assert M[1, 2] == 2
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms import bipartite
|
||||
|
||||
|
||||
class TestBipartiteCentrality:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.P4 = nx.path_graph(4)
|
||||
cls.K3 = nx.complete_bipartite_graph(3, 3)
|
||||
cls.C4 = nx.cycle_graph(4)
|
||||
cls.davis = nx.davis_southern_women_graph()
|
||||
cls.top_nodes = [
|
||||
n for n, d in cls.davis.nodes(data=True) if d["bipartite"] == 0
|
||||
]
|
||||
|
||||
def test_degree_centrality(self):
|
||||
d = bipartite.degree_centrality(self.P4, [1, 3])
|
||||
answer = {0: 0.5, 1: 1.0, 2: 1.0, 3: 0.5}
|
||||
assert d == answer
|
||||
d = bipartite.degree_centrality(self.K3, [0, 1, 2])
|
||||
answer = {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0, 5: 1.0}
|
||||
assert d == answer
|
||||
d = bipartite.degree_centrality(self.C4, [0, 2])
|
||||
answer = {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0}
|
||||
assert d == answer
|
||||
|
||||
def test_betweenness_centrality(self):
|
||||
c = bipartite.betweenness_centrality(self.P4, [1, 3])
|
||||
answer = {0: 0.0, 1: 1.0, 2: 1.0, 3: 0.0}
|
||||
assert c == answer
|
||||
c = bipartite.betweenness_centrality(self.K3, [0, 1, 2])
|
||||
answer = {0: 0.125, 1: 0.125, 2: 0.125, 3: 0.125, 4: 0.125, 5: 0.125}
|
||||
assert c == answer
|
||||
c = bipartite.betweenness_centrality(self.C4, [0, 2])
|
||||
answer = {0: 0.25, 1: 0.25, 2: 0.25, 3: 0.25}
|
||||
assert c == answer
|
||||
|
||||
def test_closeness_centrality(self):
|
||||
c = bipartite.closeness_centrality(self.P4, [1, 3])
|
||||
answer = {0: 2.0 / 3, 1: 1.0, 2: 1.0, 3: 2.0 / 3}
|
||||
assert c == answer
|
||||
c = bipartite.closeness_centrality(self.K3, [0, 1, 2])
|
||||
answer = {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0, 4: 1.0, 5: 1.0}
|
||||
assert c == answer
|
||||
c = bipartite.closeness_centrality(self.C4, [0, 2])
|
||||
answer = {0: 1.0, 1: 1.0, 2: 1.0, 3: 1.0}
|
||||
assert c == answer
|
||||
G = nx.Graph()
|
||||
G.add_node(0)
|
||||
G.add_node(1)
|
||||
c = bipartite.closeness_centrality(G, [0])
|
||||
assert c == {0: 0.0, 1: 0.0}
|
||||
c = bipartite.closeness_centrality(G, [1])
|
||||
assert c == {0: 0.0, 1: 0.0}
|
||||
|
||||
def test_bipartite_closeness_centrality_unconnected(self):
|
||||
G = nx.complete_bipartite_graph(3, 3)
|
||||
G.add_edge(6, 7)
|
||||
c = bipartite.closeness_centrality(G, [0, 2, 4, 6], normalized=False)
|
||||
answer = {
|
||||
0: 10.0 / 7,
|
||||
2: 10.0 / 7,
|
||||
4: 10.0 / 7,
|
||||
6: 10.0,
|
||||
1: 10.0 / 7,
|
||||
3: 10.0 / 7,
|
||||
5: 10.0 / 7,
|
||||
7: 10.0,
|
||||
}
|
||||
assert c == answer
|
||||
|
||||
def test_davis_degree_centrality(self):
|
||||
G = self.davis
|
||||
deg = bipartite.degree_centrality(G, self.top_nodes)
|
||||
answer = {
|
||||
"E8": 0.78,
|
||||
"E9": 0.67,
|
||||
"E7": 0.56,
|
||||
"Nora Fayette": 0.57,
|
||||
"Evelyn Jefferson": 0.57,
|
||||
"Theresa Anderson": 0.57,
|
||||
"E6": 0.44,
|
||||
"Sylvia Avondale": 0.50,
|
||||
"Laura Mandeville": 0.50,
|
||||
"Brenda Rogers": 0.50,
|
||||
"Katherina Rogers": 0.43,
|
||||
"E5": 0.44,
|
||||
"Helen Lloyd": 0.36,
|
||||
"E3": 0.33,
|
||||
"Ruth DeSand": 0.29,
|
||||
"Verne Sanderson": 0.29,
|
||||
"E12": 0.33,
|
||||
"Myra Liddel": 0.29,
|
||||
"E11": 0.22,
|
||||
"Eleanor Nye": 0.29,
|
||||
"Frances Anderson": 0.29,
|
||||
"Pearl Oglethorpe": 0.21,
|
||||
"E4": 0.22,
|
||||
"Charlotte McDowd": 0.29,
|
||||
"E10": 0.28,
|
||||
"Olivia Carleton": 0.14,
|
||||
"Flora Price": 0.14,
|
||||
"E2": 0.17,
|
||||
"E1": 0.17,
|
||||
"Dorothy Murchison": 0.14,
|
||||
"E13": 0.17,
|
||||
"E14": 0.17,
|
||||
}
|
||||
for node, value in answer.items():
|
||||
assert value == pytest.approx(deg[node], abs=1e-2)
|
||||
|
||||
def test_davis_betweenness_centrality(self):
|
||||
G = self.davis
|
||||
bet = bipartite.betweenness_centrality(G, self.top_nodes)
|
||||
answer = {
|
||||
"E8": 0.24,
|
||||
"E9": 0.23,
|
||||
"E7": 0.13,
|
||||
"Nora Fayette": 0.11,
|
||||
"Evelyn Jefferson": 0.10,
|
||||
"Theresa Anderson": 0.09,
|
||||
"E6": 0.07,
|
||||
"Sylvia Avondale": 0.07,
|
||||
"Laura Mandeville": 0.05,
|
||||
"Brenda Rogers": 0.05,
|
||||
"Katherina Rogers": 0.05,
|
||||
"E5": 0.04,
|
||||
"Helen Lloyd": 0.04,
|
||||
"E3": 0.02,
|
||||
"Ruth DeSand": 0.02,
|
||||
"Verne Sanderson": 0.02,
|
||||
"E12": 0.02,
|
||||
"Myra Liddel": 0.02,
|
||||
"E11": 0.02,
|
||||
"Eleanor Nye": 0.01,
|
||||
"Frances Anderson": 0.01,
|
||||
"Pearl Oglethorpe": 0.01,
|
||||
"E4": 0.01,
|
||||
"Charlotte McDowd": 0.01,
|
||||
"E10": 0.01,
|
||||
"Olivia Carleton": 0.01,
|
||||
"Flora Price": 0.01,
|
||||
"E2": 0.00,
|
||||
"E1": 0.00,
|
||||
"Dorothy Murchison": 0.00,
|
||||
"E13": 0.00,
|
||||
"E14": 0.00,
|
||||
}
|
||||
for node, value in answer.items():
|
||||
assert value == pytest.approx(bet[node], abs=1e-2)
|
||||
|
||||
def test_davis_closeness_centrality(self):
|
||||
G = self.davis
|
||||
clos = bipartite.closeness_centrality(G, self.top_nodes)
|
||||
answer = {
|
||||
"E8": 0.85,
|
||||
"E9": 0.79,
|
||||
"E7": 0.73,
|
||||
"Nora Fayette": 0.80,
|
||||
"Evelyn Jefferson": 0.80,
|
||||
"Theresa Anderson": 0.80,
|
||||
"E6": 0.69,
|
||||
"Sylvia Avondale": 0.77,
|
||||
"Laura Mandeville": 0.73,
|
||||
"Brenda Rogers": 0.73,
|
||||
"Katherina Rogers": 0.73,
|
||||
"E5": 0.59,
|
||||
"Helen Lloyd": 0.73,
|
||||
"E3": 0.56,
|
||||
"Ruth DeSand": 0.71,
|
||||
"Verne Sanderson": 0.71,
|
||||
"E12": 0.56,
|
||||
"Myra Liddel": 0.69,
|
||||
"E11": 0.54,
|
||||
"Eleanor Nye": 0.67,
|
||||
"Frances Anderson": 0.67,
|
||||
"Pearl Oglethorpe": 0.67,
|
||||
"E4": 0.54,
|
||||
"Charlotte McDowd": 0.60,
|
||||
"E10": 0.55,
|
||||
"Olivia Carleton": 0.59,
|
||||
"Flora Price": 0.59,
|
||||
"E2": 0.52,
|
||||
"E1": 0.52,
|
||||
"Dorothy Murchison": 0.65,
|
||||
"E13": 0.52,
|
||||
"E14": 0.52,
|
||||
}
|
||||
for node, value in answer.items():
|
||||
assert value == pytest.approx(clos[node], abs=1e-2)
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms import bipartite
|
||||
from networkx.algorithms.bipartite.cluster import cc_dot, cc_max, cc_min
|
||||
|
||||
|
||||
def test_pairwise_bipartite_cc_functions():
|
||||
# Test functions for different kinds of bipartite clustering coefficients
|
||||
# between pairs of nodes using 3 example graphs from figure 5 p. 40
|
||||
# Latapy et al (2008)
|
||||
G1 = nx.Graph([(0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (1, 5), (1, 6), (1, 7)])
|
||||
G2 = nx.Graph([(0, 2), (0, 3), (0, 4), (1, 3), (1, 4), (1, 5)])
|
||||
G3 = nx.Graph(
|
||||
[(0, 2), (0, 3), (0, 4), (0, 5), (0, 6), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9)]
|
||||
)
|
||||
result = {
|
||||
0: [1 / 3.0, 2 / 3.0, 2 / 5.0],
|
||||
1: [1 / 2.0, 2 / 3.0, 2 / 3.0],
|
||||
2: [2 / 8.0, 2 / 5.0, 2 / 5.0],
|
||||
}
|
||||
for i, G in enumerate([G1, G2, G3]):
|
||||
assert bipartite.is_bipartite(G)
|
||||
assert cc_dot(set(G[0]), set(G[1])) == result[i][0]
|
||||
assert cc_min(set(G[0]), set(G[1])) == result[i][1]
|
||||
assert cc_max(set(G[0]), set(G[1])) == result[i][2]
|
||||
|
||||
|
||||
def test_star_graph():
|
||||
G = nx.star_graph(3)
|
||||
# all modes are the same
|
||||
answer = {0: 0, 1: 1, 2: 1, 3: 1}
|
||||
assert bipartite.clustering(G, mode="dot") == answer
|
||||
assert bipartite.clustering(G, mode="min") == answer
|
||||
assert bipartite.clustering(G, mode="max") == answer
|
||||
|
||||
|
||||
def test_not_bipartite():
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
bipartite.clustering(nx.complete_graph(4))
|
||||
|
||||
|
||||
def test_bad_mode():
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
bipartite.clustering(nx.path_graph(4), mode="foo")
|
||||
|
||||
|
||||
def test_path_graph():
|
||||
G = nx.path_graph(4)
|
||||
answer = {0: 0.5, 1: 0.5, 2: 0.5, 3: 0.5}
|
||||
assert bipartite.clustering(G, mode="dot") == answer
|
||||
assert bipartite.clustering(G, mode="max") == answer
|
||||
answer = {0: 1, 1: 1, 2: 1, 3: 1}
|
||||
assert bipartite.clustering(G, mode="min") == answer
|
||||
|
||||
|
||||
def test_average_path_graph():
|
||||
G = nx.path_graph(4)
|
||||
assert bipartite.average_clustering(G, mode="dot") == 0.5
|
||||
assert bipartite.average_clustering(G, mode="max") == 0.5
|
||||
assert bipartite.average_clustering(G, mode="min") == 1
|
||||
|
||||
|
||||
def test_ra_clustering_davis():
|
||||
G = nx.davis_southern_women_graph()
|
||||
cc4 = round(bipartite.robins_alexander_clustering(G), 3)
|
||||
assert cc4 == 0.468
|
||||
|
||||
|
||||
def test_ra_clustering_square():
|
||||
G = nx.path_graph(4)
|
||||
G.add_edge(0, 3)
|
||||
assert bipartite.robins_alexander_clustering(G) == 1.0
|
||||
|
||||
|
||||
def test_ra_clustering_zero():
|
||||
G = nx.Graph()
|
||||
assert bipartite.robins_alexander_clustering(G) == 0
|
||||
G.add_nodes_from(range(4))
|
||||
assert bipartite.robins_alexander_clustering(G) == 0
|
||||
G.add_edges_from([(0, 1), (2, 3), (3, 4)])
|
||||
assert bipartite.robins_alexander_clustering(G) == 0
|
||||
G.add_edge(1, 2)
|
||||
assert bipartite.robins_alexander_clustering(G) == 0
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import networkx as nx
|
||||
from networkx.algorithms import bipartite
|
||||
|
||||
|
||||
class TestMinEdgeCover:
|
||||
"""Tests for :func:`networkx.algorithms.bipartite.min_edge_cover`"""
|
||||
|
||||
def test_empty_graph(self):
|
||||
G = nx.Graph()
|
||||
assert bipartite.min_edge_cover(G) == set()
|
||||
|
||||
def test_graph_single_edge(self):
|
||||
G = nx.Graph()
|
||||
G.add_edge(0, 1)
|
||||
assert bipartite.min_edge_cover(G) == {(0, 1), (1, 0)}
|
||||
|
||||
def test_bipartite_default(self):
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from([1, 2, 3, 4], bipartite=0)
|
||||
G.add_nodes_from(["a", "b", "c"], bipartite=1)
|
||||
G.add_edges_from([(1, "a"), (1, "b"), (2, "b"), (2, "c"), (3, "c"), (4, "a")])
|
||||
min_cover = bipartite.min_edge_cover(G)
|
||||
assert nx.is_edge_cover(G, min_cover)
|
||||
assert len(min_cover) == 8
|
||||
|
||||
def test_bipartite_explicit(self):
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from([1, 2, 3, 4], bipartite=0)
|
||||
G.add_nodes_from(["a", "b", "c"], bipartite=1)
|
||||
G.add_edges_from([(1, "a"), (1, "b"), (2, "b"), (2, "c"), (3, "c"), (4, "a")])
|
||||
min_cover = bipartite.min_edge_cover(G, bipartite.eppstein_matching)
|
||||
assert nx.is_edge_cover(G, min_cover)
|
||||
assert len(min_cover) == 8
|
||||
+240
@@ -0,0 +1,240 @@
|
||||
"""
|
||||
Unit tests for bipartite edgelists.
|
||||
"""
|
||||
|
||||
import io
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms import bipartite
|
||||
from networkx.utils import edges_equal, graphs_equal, nodes_equal
|
||||
|
||||
|
||||
class TestEdgelist:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.G = nx.Graph(name="test")
|
||||
e = [("a", "b"), ("b", "c"), ("c", "d"), ("d", "e"), ("e", "f"), ("a", "f")]
|
||||
cls.G.add_edges_from(e)
|
||||
cls.G.add_nodes_from(["a", "c", "e"], bipartite=0)
|
||||
cls.G.add_nodes_from(["b", "d", "f"], bipartite=1)
|
||||
cls.G.add_node("g", bipartite=0)
|
||||
cls.DG = nx.DiGraph(cls.G)
|
||||
cls.MG = nx.MultiGraph()
|
||||
cls.MG.add_edges_from([(1, 2), (1, 2), (1, 2)])
|
||||
cls.MG.add_node(1, bipartite=0)
|
||||
cls.MG.add_node(2, bipartite=1)
|
||||
|
||||
def test_read_edgelist_1(self):
|
||||
s = b"""\
|
||||
# comment line
|
||||
1 2
|
||||
# comment line
|
||||
2 3
|
||||
"""
|
||||
bytesIO = io.BytesIO(s)
|
||||
G = bipartite.read_edgelist(bytesIO, nodetype=int)
|
||||
assert edges_equal(G.edges(), [(1, 2), (2, 3)])
|
||||
|
||||
def test_read_edgelist_3(self):
|
||||
s = b"""\
|
||||
# comment line
|
||||
1 2 {'weight':2.0}
|
||||
# comment line
|
||||
2 3 {'weight':3.0}
|
||||
"""
|
||||
bytesIO = io.BytesIO(s)
|
||||
G = bipartite.read_edgelist(bytesIO, nodetype=int, data=False)
|
||||
assert edges_equal(G.edges(), [(1, 2), (2, 3)])
|
||||
|
||||
bytesIO = io.BytesIO(s)
|
||||
G = bipartite.read_edgelist(bytesIO, nodetype=int, data=True)
|
||||
assert edges_equal(
|
||||
G.edges(data=True), [(1, 2, {"weight": 2.0}), (2, 3, {"weight": 3.0})]
|
||||
)
|
||||
|
||||
def test_write_edgelist_1(self):
|
||||
fh = io.BytesIO()
|
||||
G = nx.Graph()
|
||||
G.add_edges_from([(1, 2), (2, 3)])
|
||||
G.add_node(1, bipartite=0)
|
||||
G.add_node(2, bipartite=1)
|
||||
G.add_node(3, bipartite=0)
|
||||
bipartite.write_edgelist(G, fh, data=False)
|
||||
fh.seek(0)
|
||||
assert fh.read() == b"1 2\n3 2\n"
|
||||
|
||||
def test_write_edgelist_2(self):
|
||||
fh = io.BytesIO()
|
||||
G = nx.Graph()
|
||||
G.add_edges_from([(1, 2), (2, 3)])
|
||||
G.add_node(1, bipartite=0)
|
||||
G.add_node(2, bipartite=1)
|
||||
G.add_node(3, bipartite=0)
|
||||
bipartite.write_edgelist(G, fh, data=True)
|
||||
fh.seek(0)
|
||||
assert fh.read() == b"1 2 {}\n3 2 {}\n"
|
||||
|
||||
def test_write_edgelist_3(self):
|
||||
fh = io.BytesIO()
|
||||
G = nx.Graph()
|
||||
G.add_edge(1, 2, weight=2.0)
|
||||
G.add_edge(2, 3, weight=3.0)
|
||||
G.add_node(1, bipartite=0)
|
||||
G.add_node(2, bipartite=1)
|
||||
G.add_node(3, bipartite=0)
|
||||
bipartite.write_edgelist(G, fh, data=True)
|
||||
fh.seek(0)
|
||||
assert fh.read() == b"1 2 {'weight': 2.0}\n3 2 {'weight': 3.0}\n"
|
||||
|
||||
def test_write_edgelist_4(self):
|
||||
fh = io.BytesIO()
|
||||
G = nx.Graph()
|
||||
G.add_edge(1, 2, weight=2.0)
|
||||
G.add_edge(2, 3, weight=3.0)
|
||||
G.add_node(1, bipartite=0)
|
||||
G.add_node(2, bipartite=1)
|
||||
G.add_node(3, bipartite=0)
|
||||
bipartite.write_edgelist(G, fh, data=[("weight")])
|
||||
fh.seek(0)
|
||||
assert fh.read() == b"1 2 2.0\n3 2 3.0\n"
|
||||
|
||||
def test_unicode(self, tmp_path):
|
||||
G = nx.Graph()
|
||||
name1 = chr(2344) + chr(123) + chr(6543)
|
||||
name2 = chr(5543) + chr(1543) + chr(324)
|
||||
G.add_edge(name1, "Radiohead", **{name2: 3})
|
||||
G.add_node(name1, bipartite=0)
|
||||
G.add_node("Radiohead", bipartite=1)
|
||||
|
||||
fname = tmp_path / "edgelist.txt"
|
||||
bipartite.write_edgelist(G, fname)
|
||||
H = bipartite.read_edgelist(fname)
|
||||
assert graphs_equal(G, H)
|
||||
|
||||
def test_latin1_issue(self, tmp_path):
|
||||
G = nx.Graph()
|
||||
name1 = chr(2344) + chr(123) + chr(6543)
|
||||
name2 = chr(5543) + chr(1543) + chr(324)
|
||||
G.add_edge(name1, "Radiohead", **{name2: 3})
|
||||
G.add_node(name1, bipartite=0)
|
||||
G.add_node("Radiohead", bipartite=1)
|
||||
|
||||
fname = tmp_path / "edgelist.txt"
|
||||
with pytest.raises(UnicodeEncodeError):
|
||||
bipartite.write_edgelist(G, fname, encoding="latin-1")
|
||||
|
||||
def test_latin1(self, tmp_path):
|
||||
G = nx.Graph()
|
||||
name1 = "Bj" + chr(246) + "rk"
|
||||
name2 = chr(220) + "ber"
|
||||
G.add_edge(name1, "Radiohead", **{name2: 3})
|
||||
G.add_node(name1, bipartite=0)
|
||||
G.add_node("Radiohead", bipartite=1)
|
||||
|
||||
fname = tmp_path / "edgelist.txt"
|
||||
bipartite.write_edgelist(G, fname, encoding="latin-1")
|
||||
H = bipartite.read_edgelist(fname, encoding="latin-1")
|
||||
assert graphs_equal(G, H)
|
||||
|
||||
def test_edgelist_graph(self, tmp_path):
|
||||
G = self.G
|
||||
fname = tmp_path / "edgelist.txt"
|
||||
bipartite.write_edgelist(G, fname)
|
||||
H = bipartite.read_edgelist(fname)
|
||||
H2 = bipartite.read_edgelist(fname)
|
||||
assert H is not H2 # they should be different graphs
|
||||
G.remove_node("g") # isolated nodes are not written in edgelist
|
||||
assert nodes_equal(list(H), list(G))
|
||||
assert edges_equal(list(H.edges()), list(G.edges()))
|
||||
|
||||
def test_edgelist_integers(self, tmp_path):
|
||||
G = nx.convert_node_labels_to_integers(self.G)
|
||||
fname = tmp_path / "edgelist.txt"
|
||||
bipartite.write_edgelist(G, fname)
|
||||
H = bipartite.read_edgelist(fname, nodetype=int)
|
||||
# isolated nodes are not written in edgelist
|
||||
G.remove_nodes_from(list(nx.isolates(G)))
|
||||
assert nodes_equal(list(H), list(G))
|
||||
assert edges_equal(list(H.edges()), list(G.edges()))
|
||||
|
||||
def test_edgelist_multigraph(self, tmp_path):
|
||||
G = self.MG
|
||||
fname = tmp_path / "edgelist.txt"
|
||||
bipartite.write_edgelist(G, fname)
|
||||
H = bipartite.read_edgelist(fname, nodetype=int, create_using=nx.MultiGraph())
|
||||
H2 = bipartite.read_edgelist(fname, nodetype=int, create_using=nx.MultiGraph())
|
||||
assert H is not H2 # they should be different graphs
|
||||
assert nodes_equal(list(H), list(G))
|
||||
assert edges_equal(list(H.edges()), list(G.edges()))
|
||||
|
||||
def test_empty_digraph(self):
|
||||
with pytest.raises(nx.NetworkXNotImplemented):
|
||||
bytesIO = io.BytesIO()
|
||||
bipartite.write_edgelist(nx.DiGraph(), bytesIO)
|
||||
|
||||
def test_raise_attribute(self):
|
||||
with pytest.raises(AttributeError):
|
||||
G = nx.path_graph(4)
|
||||
bytesIO = io.BytesIO()
|
||||
bipartite.write_edgelist(G, bytesIO)
|
||||
|
||||
def test_parse_edgelist(self):
|
||||
"""Tests for conditions specific to
|
||||
parse_edge_list method"""
|
||||
|
||||
# ignore strings of length less than 2
|
||||
lines = ["1 2", "2 3", "3 1", "4", " "]
|
||||
G = bipartite.parse_edgelist(lines, nodetype=int)
|
||||
assert list(G.nodes) == [1, 2, 3]
|
||||
|
||||
# Exception raised when node is not convertible
|
||||
# to specified data type
|
||||
with pytest.raises(TypeError, match=".*Failed to convert nodes"):
|
||||
lines = ["a b", "b c", "c a"]
|
||||
G = bipartite.parse_edgelist(lines, nodetype=int)
|
||||
|
||||
# Exception raised when format of data is not
|
||||
# convertible to dictionary object
|
||||
with pytest.raises(TypeError, match=".*Failed to convert edge data"):
|
||||
lines = ["1 2 3", "2 3 4", "3 1 2"]
|
||||
G = bipartite.parse_edgelist(lines, nodetype=int)
|
||||
|
||||
# Exception raised when edge data and data
|
||||
# keys are not of same length
|
||||
with pytest.raises(IndexError):
|
||||
lines = ["1 2 3 4", "2 3 4"]
|
||||
G = bipartite.parse_edgelist(
|
||||
lines, nodetype=int, data=[("weight", int), ("key", int)]
|
||||
)
|
||||
|
||||
# Exception raised when edge data is not
|
||||
# convertible to specified data type
|
||||
with pytest.raises(TypeError, match=".*Failed to convert key data"):
|
||||
lines = ["1 2 3 a", "2 3 4 b"]
|
||||
G = bipartite.parse_edgelist(
|
||||
lines, nodetype=int, data=[("weight", int), ("key", int)]
|
||||
)
|
||||
|
||||
|
||||
def test_bipartite_edgelist_consistent_strip_handling():
|
||||
"""See gh-7462
|
||||
|
||||
Input when printed looks like:
|
||||
|
||||
A B interaction 2
|
||||
B C interaction 4
|
||||
C A interaction
|
||||
|
||||
Note the trailing \\t in the last line, which indicates the existence of
|
||||
an empty data field.
|
||||
"""
|
||||
lines = io.StringIO(
|
||||
"A\tB\tinteraction\t2\nB\tC\tinteraction\t4\nC\tA\tinteraction\t"
|
||||
)
|
||||
descr = [("type", str), ("weight", str)]
|
||||
# Should not raise
|
||||
G = nx.bipartite.parse_edgelist(lines, delimiter="\t", data=descr)
|
||||
expected = [("A", "B", "2"), ("A", "C", ""), ("B", "C", "4")]
|
||||
assert sorted(G.edges(data="weight")) == expected
|
||||
+334
@@ -0,0 +1,334 @@
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
|
||||
|
||||
def test_selfloops_raises():
|
||||
G = nx.ladder_graph(3)
|
||||
G.add_edge(0, 0)
|
||||
with pytest.raises(nx.NetworkXError, match=".*not bipartite"):
|
||||
nx.bipartite.maximal_extendability(G)
|
||||
|
||||
|
||||
def test_disconnected_raises():
|
||||
G = nx.ladder_graph(3)
|
||||
G.add_node("a")
|
||||
with pytest.raises(nx.NetworkXError, match=".*not connected"):
|
||||
nx.bipartite.maximal_extendability(G)
|
||||
|
||||
|
||||
def test_not_bipartite_raises():
|
||||
G = nx.complete_graph(5)
|
||||
with pytest.raises(nx.NetworkXError, match=".*not bipartite"):
|
||||
nx.bipartite.maximal_extendability(G)
|
||||
|
||||
|
||||
def test_no_perfect_matching_raises():
|
||||
G = nx.Graph([(0, 1), (0, 2)])
|
||||
with pytest.raises(nx.NetworkXError, match=".*not contain a perfect matching"):
|
||||
nx.bipartite.maximal_extendability(G)
|
||||
|
||||
|
||||
def test_residual_graph_not_strongly_connected_raises():
|
||||
G = nx.Graph([(1, 2), (2, 3), (3, 4)])
|
||||
with pytest.raises(
|
||||
nx.NetworkXError, match="The residual graph of G is not strongly connected"
|
||||
):
|
||||
nx.bipartite.maximal_extendability(G)
|
||||
|
||||
|
||||
def test_ladder_graph_is_1():
|
||||
G = nx.ladder_graph(3)
|
||||
assert nx.bipartite.maximal_extendability(G) == 1
|
||||
|
||||
|
||||
def test_cubical_graph_is_2():
|
||||
G = nx.cubical_graph()
|
||||
assert nx.bipartite.maximal_extendability(G) == 2
|
||||
|
||||
|
||||
def test_k_is_3():
|
||||
G = nx.Graph(
|
||||
[
|
||||
(1, 6),
|
||||
(1, 7),
|
||||
(1, 8),
|
||||
(1, 9),
|
||||
(2, 6),
|
||||
(2, 7),
|
||||
(2, 8),
|
||||
(2, 10),
|
||||
(3, 6),
|
||||
(3, 8),
|
||||
(3, 9),
|
||||
(3, 10),
|
||||
(4, 7),
|
||||
(4, 8),
|
||||
(4, 9),
|
||||
(4, 10),
|
||||
(5, 6),
|
||||
(5, 7),
|
||||
(5, 9),
|
||||
(5, 10),
|
||||
]
|
||||
)
|
||||
assert nx.bipartite.maximal_extendability(G) == 3
|
||||
|
||||
|
||||
def test_k_is_4():
|
||||
G = nx.Graph(
|
||||
[
|
||||
(8, 1),
|
||||
(8, 2),
|
||||
(8, 3),
|
||||
(8, 4),
|
||||
(8, 5),
|
||||
(9, 1),
|
||||
(9, 2),
|
||||
(9, 3),
|
||||
(9, 4),
|
||||
(9, 7),
|
||||
(10, 1),
|
||||
(10, 2),
|
||||
(10, 3),
|
||||
(10, 4),
|
||||
(10, 6),
|
||||
(11, 1),
|
||||
(11, 2),
|
||||
(11, 5),
|
||||
(11, 6),
|
||||
(11, 7),
|
||||
(12, 1),
|
||||
(12, 3),
|
||||
(12, 5),
|
||||
(12, 6),
|
||||
(12, 7),
|
||||
(13, 2),
|
||||
(13, 4),
|
||||
(13, 5),
|
||||
(13, 6),
|
||||
(13, 7),
|
||||
(14, 3),
|
||||
(14, 4),
|
||||
(14, 5),
|
||||
(14, 6),
|
||||
(14, 7),
|
||||
]
|
||||
)
|
||||
assert nx.bipartite.maximal_extendability(G) == 4
|
||||
|
||||
|
||||
def test_k_is_5():
|
||||
G = nx.Graph(
|
||||
[
|
||||
(8, 1),
|
||||
(8, 2),
|
||||
(8, 3),
|
||||
(8, 4),
|
||||
(8, 5),
|
||||
(8, 6),
|
||||
(9, 1),
|
||||
(9, 2),
|
||||
(9, 3),
|
||||
(9, 4),
|
||||
(9, 5),
|
||||
(9, 7),
|
||||
(10, 1),
|
||||
(10, 2),
|
||||
(10, 3),
|
||||
(10, 4),
|
||||
(10, 6),
|
||||
(10, 7),
|
||||
(11, 1),
|
||||
(11, 2),
|
||||
(11, 3),
|
||||
(11, 5),
|
||||
(11, 6),
|
||||
(11, 7),
|
||||
(12, 1),
|
||||
(12, 2),
|
||||
(12, 4),
|
||||
(12, 5),
|
||||
(12, 6),
|
||||
(12, 7),
|
||||
(13, 1),
|
||||
(13, 3),
|
||||
(13, 4),
|
||||
(13, 5),
|
||||
(13, 6),
|
||||
(13, 7),
|
||||
(14, 2),
|
||||
(14, 3),
|
||||
(14, 4),
|
||||
(14, 5),
|
||||
(14, 6),
|
||||
(14, 7),
|
||||
]
|
||||
)
|
||||
assert nx.bipartite.maximal_extendability(G) == 5
|
||||
|
||||
|
||||
def test_k_is_6():
|
||||
G = nx.Graph(
|
||||
[
|
||||
(9, 1),
|
||||
(9, 2),
|
||||
(9, 3),
|
||||
(9, 4),
|
||||
(9, 5),
|
||||
(9, 6),
|
||||
(9, 7),
|
||||
(10, 1),
|
||||
(10, 2),
|
||||
(10, 3),
|
||||
(10, 4),
|
||||
(10, 5),
|
||||
(10, 6),
|
||||
(10, 8),
|
||||
(11, 1),
|
||||
(11, 2),
|
||||
(11, 3),
|
||||
(11, 4),
|
||||
(11, 5),
|
||||
(11, 7),
|
||||
(11, 8),
|
||||
(12, 1),
|
||||
(12, 2),
|
||||
(12, 3),
|
||||
(12, 4),
|
||||
(12, 6),
|
||||
(12, 7),
|
||||
(12, 8),
|
||||
(13, 1),
|
||||
(13, 2),
|
||||
(13, 3),
|
||||
(13, 5),
|
||||
(13, 6),
|
||||
(13, 7),
|
||||
(13, 8),
|
||||
(14, 1),
|
||||
(14, 2),
|
||||
(14, 4),
|
||||
(14, 5),
|
||||
(14, 6),
|
||||
(14, 7),
|
||||
(14, 8),
|
||||
(15, 1),
|
||||
(15, 3),
|
||||
(15, 4),
|
||||
(15, 5),
|
||||
(15, 6),
|
||||
(15, 7),
|
||||
(15, 8),
|
||||
(16, 2),
|
||||
(16, 3),
|
||||
(16, 4),
|
||||
(16, 5),
|
||||
(16, 6),
|
||||
(16, 7),
|
||||
(16, 8),
|
||||
]
|
||||
)
|
||||
assert nx.bipartite.maximal_extendability(G) == 6
|
||||
|
||||
|
||||
def test_k_is_7():
|
||||
G = nx.Graph(
|
||||
[
|
||||
(1, 11),
|
||||
(1, 12),
|
||||
(1, 13),
|
||||
(1, 14),
|
||||
(1, 15),
|
||||
(1, 16),
|
||||
(1, 17),
|
||||
(1, 18),
|
||||
(2, 11),
|
||||
(2, 12),
|
||||
(2, 13),
|
||||
(2, 14),
|
||||
(2, 15),
|
||||
(2, 16),
|
||||
(2, 17),
|
||||
(2, 19),
|
||||
(3, 11),
|
||||
(3, 12),
|
||||
(3, 13),
|
||||
(3, 14),
|
||||
(3, 15),
|
||||
(3, 16),
|
||||
(3, 17),
|
||||
(3, 20),
|
||||
(4, 11),
|
||||
(4, 12),
|
||||
(4, 13),
|
||||
(4, 14),
|
||||
(4, 15),
|
||||
(4, 16),
|
||||
(4, 17),
|
||||
(4, 18),
|
||||
(4, 19),
|
||||
(4, 20),
|
||||
(5, 11),
|
||||
(5, 12),
|
||||
(5, 13),
|
||||
(5, 14),
|
||||
(5, 15),
|
||||
(5, 16),
|
||||
(5, 17),
|
||||
(5, 18),
|
||||
(5, 19),
|
||||
(5, 20),
|
||||
(6, 11),
|
||||
(6, 12),
|
||||
(6, 13),
|
||||
(6, 14),
|
||||
(6, 15),
|
||||
(6, 16),
|
||||
(6, 17),
|
||||
(6, 18),
|
||||
(6, 19),
|
||||
(6, 20),
|
||||
(7, 11),
|
||||
(7, 12),
|
||||
(7, 13),
|
||||
(7, 14),
|
||||
(7, 15),
|
||||
(7, 16),
|
||||
(7, 17),
|
||||
(7, 18),
|
||||
(7, 19),
|
||||
(7, 20),
|
||||
(8, 11),
|
||||
(8, 12),
|
||||
(8, 13),
|
||||
(8, 14),
|
||||
(8, 15),
|
||||
(8, 16),
|
||||
(8, 17),
|
||||
(8, 18),
|
||||
(8, 19),
|
||||
(8, 20),
|
||||
(9, 11),
|
||||
(9, 12),
|
||||
(9, 13),
|
||||
(9, 14),
|
||||
(9, 15),
|
||||
(9, 16),
|
||||
(9, 17),
|
||||
(9, 18),
|
||||
(9, 19),
|
||||
(9, 20),
|
||||
(10, 11),
|
||||
(10, 12),
|
||||
(10, 13),
|
||||
(10, 14),
|
||||
(10, 15),
|
||||
(10, 16),
|
||||
(10, 17),
|
||||
(10, 18),
|
||||
(10, 19),
|
||||
(10, 20),
|
||||
]
|
||||
)
|
||||
assert nx.bipartite.maximal_extendability(G) == 7
|
||||
+407
@@ -0,0 +1,407 @@
|
||||
import numbers
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
|
||||
from ..generators import (
|
||||
alternating_havel_hakimi_graph,
|
||||
complete_bipartite_graph,
|
||||
configuration_model,
|
||||
gnmk_random_graph,
|
||||
havel_hakimi_graph,
|
||||
preferential_attachment_graph,
|
||||
random_graph,
|
||||
reverse_havel_hakimi_graph,
|
||||
)
|
||||
|
||||
"""
|
||||
Generators - Bipartite
|
||||
----------------------
|
||||
"""
|
||||
|
||||
|
||||
class TestGeneratorsBipartite:
|
||||
def test_complete_bipartite_graph(self):
|
||||
G = complete_bipartite_graph(0, 0)
|
||||
assert nx.is_isomorphic(G, nx.null_graph())
|
||||
|
||||
for i in [1, 5]:
|
||||
G = complete_bipartite_graph(i, 0)
|
||||
assert nx.is_isomorphic(G, nx.empty_graph(i))
|
||||
G = complete_bipartite_graph(0, i)
|
||||
assert nx.is_isomorphic(G, nx.empty_graph(i))
|
||||
|
||||
G = complete_bipartite_graph(2, 2)
|
||||
assert nx.is_isomorphic(G, nx.cycle_graph(4))
|
||||
|
||||
G = complete_bipartite_graph(1, 5)
|
||||
assert nx.is_isomorphic(G, nx.star_graph(5))
|
||||
|
||||
G = complete_bipartite_graph(5, 1)
|
||||
assert nx.is_isomorphic(G, nx.star_graph(5))
|
||||
|
||||
# complete_bipartite_graph(m1,m2) is a connected graph with
|
||||
# m1+m2 nodes and m1*m2 edges
|
||||
for m1, m2 in [(5, 11), (7, 3)]:
|
||||
G = complete_bipartite_graph(m1, m2)
|
||||
assert nx.number_of_nodes(G) == m1 + m2
|
||||
assert nx.number_of_edges(G) == m1 * m2
|
||||
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
complete_bipartite_graph(7, 3, create_using=nx.DiGraph)
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
complete_bipartite_graph(7, 3, create_using=nx.MultiDiGraph)
|
||||
|
||||
mG = complete_bipartite_graph(7, 3, create_using=nx.MultiGraph)
|
||||
assert mG.is_multigraph()
|
||||
assert sorted(mG.edges()) == sorted(G.edges())
|
||||
|
||||
mG = complete_bipartite_graph(7, 3, create_using=nx.MultiGraph)
|
||||
assert mG.is_multigraph()
|
||||
assert sorted(mG.edges()) == sorted(G.edges())
|
||||
|
||||
mG = complete_bipartite_graph(7, 3) # default to Graph
|
||||
assert sorted(mG.edges()) == sorted(G.edges())
|
||||
assert not mG.is_multigraph()
|
||||
assert not mG.is_directed()
|
||||
|
||||
# specify nodes rather than number of nodes
|
||||
for n1, n2 in [([1, 2], "ab"), (3, 2), (3, "ab"), ("ab", 3)]:
|
||||
G = complete_bipartite_graph(n1, n2)
|
||||
if isinstance(n1, numbers.Integral):
|
||||
if isinstance(n2, numbers.Integral):
|
||||
n2 = range(n1, n1 + n2)
|
||||
n1 = range(n1)
|
||||
elif isinstance(n2, numbers.Integral):
|
||||
n2 = range(n2)
|
||||
edges = {(u, v) for u in n1 for v in n2}
|
||||
assert edges == set(G.edges)
|
||||
assert G.size() == len(edges)
|
||||
|
||||
# raise when node sets are not distinct
|
||||
for n1, n2 in [([1, 2], 3), (3, [1, 2]), ("abc", "bcd")]:
|
||||
pytest.raises(nx.NetworkXError, complete_bipartite_graph, n1, n2)
|
||||
|
||||
def test_configuration_model(self):
|
||||
aseq = []
|
||||
bseq = []
|
||||
G = configuration_model(aseq, bseq)
|
||||
assert len(G) == 0
|
||||
|
||||
aseq = [0, 0]
|
||||
bseq = [0, 0]
|
||||
G = configuration_model(aseq, bseq)
|
||||
assert len(G) == 4
|
||||
assert G.number_of_edges() == 0
|
||||
|
||||
aseq = [3, 3, 3, 3]
|
||||
bseq = [2, 2, 2, 2, 2]
|
||||
pytest.raises(nx.NetworkXError, configuration_model, aseq, bseq)
|
||||
|
||||
aseq = [3, 3, 3, 3]
|
||||
bseq = [2, 2, 2, 2, 2, 2]
|
||||
G = configuration_model(aseq, bseq)
|
||||
assert sorted(d for n, d in G.degree()) == [2, 2, 2, 2, 2, 2, 3, 3, 3, 3]
|
||||
|
||||
aseq = [2, 2, 2, 2, 2, 2]
|
||||
bseq = [3, 3, 3, 3]
|
||||
G = configuration_model(aseq, bseq)
|
||||
assert sorted(d for n, d in G.degree()) == [2, 2, 2, 2, 2, 2, 3, 3, 3, 3]
|
||||
|
||||
aseq = [2, 2, 2, 1, 1, 1]
|
||||
bseq = [3, 3, 3]
|
||||
G = configuration_model(aseq, bseq)
|
||||
assert G.is_multigraph()
|
||||
assert not G.is_directed()
|
||||
assert sorted(d for n, d in G.degree()) == [1, 1, 1, 2, 2, 2, 3, 3, 3]
|
||||
|
||||
GU = nx.projected_graph(nx.Graph(G), range(len(aseq)))
|
||||
assert GU.number_of_nodes() == 6
|
||||
|
||||
GD = nx.projected_graph(nx.Graph(G), range(len(aseq), len(aseq) + len(bseq)))
|
||||
assert GD.number_of_nodes() == 3
|
||||
|
||||
G = reverse_havel_hakimi_graph(aseq, bseq, create_using=nx.Graph)
|
||||
assert not G.is_multigraph()
|
||||
assert not G.is_directed()
|
||||
|
||||
pytest.raises(
|
||||
nx.NetworkXError, configuration_model, aseq, bseq, create_using=nx.DiGraph()
|
||||
)
|
||||
pytest.raises(
|
||||
nx.NetworkXError, configuration_model, aseq, bseq, create_using=nx.DiGraph
|
||||
)
|
||||
pytest.raises(
|
||||
nx.NetworkXError,
|
||||
configuration_model,
|
||||
aseq,
|
||||
bseq,
|
||||
create_using=nx.MultiDiGraph,
|
||||
)
|
||||
|
||||
def test_havel_hakimi_graph(self):
|
||||
aseq = []
|
||||
bseq = []
|
||||
G = havel_hakimi_graph(aseq, bseq)
|
||||
assert len(G) == 0
|
||||
|
||||
aseq = [0, 0]
|
||||
bseq = [0, 0]
|
||||
G = havel_hakimi_graph(aseq, bseq)
|
||||
assert len(G) == 4
|
||||
assert G.number_of_edges() == 0
|
||||
|
||||
aseq = [3, 3, 3, 3]
|
||||
bseq = [2, 2, 2, 2, 2]
|
||||
pytest.raises(nx.NetworkXError, havel_hakimi_graph, aseq, bseq)
|
||||
|
||||
bseq = [2, 2, 2, 2, 2, 2]
|
||||
G = havel_hakimi_graph(aseq, bseq)
|
||||
assert sorted(d for n, d in G.degree()) == [2, 2, 2, 2, 2, 2, 3, 3, 3, 3]
|
||||
|
||||
aseq = [2, 2, 2, 2, 2, 2]
|
||||
bseq = [3, 3, 3, 3]
|
||||
G = havel_hakimi_graph(aseq, bseq)
|
||||
assert G.is_multigraph()
|
||||
assert not G.is_directed()
|
||||
assert sorted(d for n, d in G.degree()) == [2, 2, 2, 2, 2, 2, 3, 3, 3, 3]
|
||||
|
||||
GU = nx.projected_graph(nx.Graph(G), range(len(aseq)))
|
||||
assert GU.number_of_nodes() == 6
|
||||
|
||||
GD = nx.projected_graph(nx.Graph(G), range(len(aseq), len(aseq) + len(bseq)))
|
||||
assert GD.number_of_nodes() == 4
|
||||
|
||||
G = reverse_havel_hakimi_graph(aseq, bseq, create_using=nx.Graph)
|
||||
assert not G.is_multigraph()
|
||||
assert not G.is_directed()
|
||||
|
||||
pytest.raises(
|
||||
nx.NetworkXError, havel_hakimi_graph, aseq, bseq, create_using=nx.DiGraph
|
||||
)
|
||||
pytest.raises(
|
||||
nx.NetworkXError, havel_hakimi_graph, aseq, bseq, create_using=nx.DiGraph
|
||||
)
|
||||
pytest.raises(
|
||||
nx.NetworkXError,
|
||||
havel_hakimi_graph,
|
||||
aseq,
|
||||
bseq,
|
||||
create_using=nx.MultiDiGraph,
|
||||
)
|
||||
|
||||
def test_reverse_havel_hakimi_graph(self):
|
||||
aseq = []
|
||||
bseq = []
|
||||
G = reverse_havel_hakimi_graph(aseq, bseq)
|
||||
assert len(G) == 0
|
||||
|
||||
aseq = [0, 0]
|
||||
bseq = [0, 0]
|
||||
G = reverse_havel_hakimi_graph(aseq, bseq)
|
||||
assert len(G) == 4
|
||||
assert G.number_of_edges() == 0
|
||||
|
||||
aseq = [3, 3, 3, 3]
|
||||
bseq = [2, 2, 2, 2, 2]
|
||||
pytest.raises(nx.NetworkXError, reverse_havel_hakimi_graph, aseq, bseq)
|
||||
|
||||
bseq = [2, 2, 2, 2, 2, 2]
|
||||
G = reverse_havel_hakimi_graph(aseq, bseq)
|
||||
assert sorted(d for n, d in G.degree()) == [2, 2, 2, 2, 2, 2, 3, 3, 3, 3]
|
||||
|
||||
aseq = [2, 2, 2, 2, 2, 2]
|
||||
bseq = [3, 3, 3, 3]
|
||||
G = reverse_havel_hakimi_graph(aseq, bseq)
|
||||
assert sorted(d for n, d in G.degree()) == [2, 2, 2, 2, 2, 2, 3, 3, 3, 3]
|
||||
|
||||
aseq = [2, 2, 2, 1, 1, 1]
|
||||
bseq = [3, 3, 3]
|
||||
G = reverse_havel_hakimi_graph(aseq, bseq)
|
||||
assert G.is_multigraph()
|
||||
assert not G.is_directed()
|
||||
assert sorted(d for n, d in G.degree()) == [1, 1, 1, 2, 2, 2, 3, 3, 3]
|
||||
|
||||
GU = nx.projected_graph(nx.Graph(G), range(len(aseq)))
|
||||
assert GU.number_of_nodes() == 6
|
||||
|
||||
GD = nx.projected_graph(nx.Graph(G), range(len(aseq), len(aseq) + len(bseq)))
|
||||
assert GD.number_of_nodes() == 3
|
||||
|
||||
G = reverse_havel_hakimi_graph(aseq, bseq, create_using=nx.Graph)
|
||||
assert not G.is_multigraph()
|
||||
assert not G.is_directed()
|
||||
|
||||
pytest.raises(
|
||||
nx.NetworkXError,
|
||||
reverse_havel_hakimi_graph,
|
||||
aseq,
|
||||
bseq,
|
||||
create_using=nx.DiGraph,
|
||||
)
|
||||
pytest.raises(
|
||||
nx.NetworkXError,
|
||||
reverse_havel_hakimi_graph,
|
||||
aseq,
|
||||
bseq,
|
||||
create_using=nx.DiGraph,
|
||||
)
|
||||
pytest.raises(
|
||||
nx.NetworkXError,
|
||||
reverse_havel_hakimi_graph,
|
||||
aseq,
|
||||
bseq,
|
||||
create_using=nx.MultiDiGraph,
|
||||
)
|
||||
|
||||
def test_alternating_havel_hakimi_graph(self):
|
||||
aseq = []
|
||||
bseq = []
|
||||
G = alternating_havel_hakimi_graph(aseq, bseq)
|
||||
assert len(G) == 0
|
||||
|
||||
aseq = [0, 0]
|
||||
bseq = [0, 0]
|
||||
G = alternating_havel_hakimi_graph(aseq, bseq)
|
||||
assert len(G) == 4
|
||||
assert G.number_of_edges() == 0
|
||||
|
||||
aseq = [3, 3, 3, 3]
|
||||
bseq = [2, 2, 2, 2, 2]
|
||||
pytest.raises(nx.NetworkXError, alternating_havel_hakimi_graph, aseq, bseq)
|
||||
|
||||
bseq = [2, 2, 2, 2, 2, 2]
|
||||
G = alternating_havel_hakimi_graph(aseq, bseq)
|
||||
assert sorted(d for n, d in G.degree()) == [2, 2, 2, 2, 2, 2, 3, 3, 3, 3]
|
||||
|
||||
aseq = [2, 2, 2, 2, 2, 2]
|
||||
bseq = [3, 3, 3, 3]
|
||||
G = alternating_havel_hakimi_graph(aseq, bseq)
|
||||
assert sorted(d for n, d in G.degree()) == [2, 2, 2, 2, 2, 2, 3, 3, 3, 3]
|
||||
|
||||
aseq = [2, 2, 2, 1, 1, 1]
|
||||
bseq = [3, 3, 3]
|
||||
G = alternating_havel_hakimi_graph(aseq, bseq)
|
||||
assert G.is_multigraph()
|
||||
assert not G.is_directed()
|
||||
assert sorted(d for n, d in G.degree()) == [1, 1, 1, 2, 2, 2, 3, 3, 3]
|
||||
|
||||
GU = nx.projected_graph(nx.Graph(G), range(len(aseq)))
|
||||
assert GU.number_of_nodes() == 6
|
||||
|
||||
GD = nx.projected_graph(nx.Graph(G), range(len(aseq), len(aseq) + len(bseq)))
|
||||
assert GD.number_of_nodes() == 3
|
||||
|
||||
G = reverse_havel_hakimi_graph(aseq, bseq, create_using=nx.Graph)
|
||||
assert not G.is_multigraph()
|
||||
assert not G.is_directed()
|
||||
|
||||
pytest.raises(
|
||||
nx.NetworkXError,
|
||||
alternating_havel_hakimi_graph,
|
||||
aseq,
|
||||
bseq,
|
||||
create_using=nx.DiGraph,
|
||||
)
|
||||
pytest.raises(
|
||||
nx.NetworkXError,
|
||||
alternating_havel_hakimi_graph,
|
||||
aseq,
|
||||
bseq,
|
||||
create_using=nx.DiGraph,
|
||||
)
|
||||
pytest.raises(
|
||||
nx.NetworkXError,
|
||||
alternating_havel_hakimi_graph,
|
||||
aseq,
|
||||
bseq,
|
||||
create_using=nx.MultiDiGraph,
|
||||
)
|
||||
|
||||
def test_preferential_attachment(self):
|
||||
aseq = [3, 2, 1, 1]
|
||||
G = preferential_attachment_graph(aseq, 0.5)
|
||||
assert G.is_multigraph()
|
||||
assert not G.is_directed()
|
||||
|
||||
G = preferential_attachment_graph(aseq, 0.5, create_using=nx.Graph)
|
||||
assert not G.is_multigraph()
|
||||
assert not G.is_directed()
|
||||
|
||||
pytest.raises(
|
||||
nx.NetworkXError,
|
||||
preferential_attachment_graph,
|
||||
aseq,
|
||||
0.5,
|
||||
create_using=nx.DiGraph(),
|
||||
)
|
||||
pytest.raises(
|
||||
nx.NetworkXError,
|
||||
preferential_attachment_graph,
|
||||
aseq,
|
||||
0.5,
|
||||
create_using=nx.DiGraph(),
|
||||
)
|
||||
pytest.raises(
|
||||
nx.NetworkXError,
|
||||
preferential_attachment_graph,
|
||||
aseq,
|
||||
0.5,
|
||||
create_using=nx.DiGraph(),
|
||||
)
|
||||
|
||||
def test_random_graph(self):
|
||||
n = 10
|
||||
m = 20
|
||||
G = random_graph(n, m, 0.9)
|
||||
assert len(G) == 30
|
||||
assert nx.is_bipartite(G)
|
||||
X, Y = nx.algorithms.bipartite.sets(G)
|
||||
assert set(range(n)) == X
|
||||
assert set(range(n, n + m)) == Y
|
||||
|
||||
def test_random_digraph(self):
|
||||
n = 10
|
||||
m = 20
|
||||
G = random_graph(n, m, 0.9, directed=True)
|
||||
assert len(G) == 30
|
||||
assert nx.is_bipartite(G)
|
||||
X, Y = nx.algorithms.bipartite.sets(G)
|
||||
assert set(range(n)) == X
|
||||
assert set(range(n, n + m)) == Y
|
||||
|
||||
def test_gnmk_random_graph(self):
|
||||
n = 10
|
||||
m = 20
|
||||
edges = 100
|
||||
# set seed because sometimes it is not connected
|
||||
# which raises an error in bipartite.sets(G) below.
|
||||
G = gnmk_random_graph(n, m, edges, seed=1234)
|
||||
assert len(G) == n + m
|
||||
assert nx.is_bipartite(G)
|
||||
X, Y = nx.algorithms.bipartite.sets(G)
|
||||
assert set(range(n)) == X
|
||||
assert set(range(n, n + m)) == Y
|
||||
assert edges == len(list(G.edges()))
|
||||
|
||||
def test_gnmk_random_graph_complete(self):
|
||||
n = 10
|
||||
m = 20
|
||||
edges = 200
|
||||
G = gnmk_random_graph(n, m, edges)
|
||||
assert len(G) == n + m
|
||||
assert nx.is_bipartite(G)
|
||||
X, Y = nx.algorithms.bipartite.sets(G)
|
||||
assert set(range(n)) == X
|
||||
assert set(range(n, n + m)) == Y
|
||||
assert edges == len(list(G.edges()))
|
||||
|
||||
@pytest.mark.parametrize("n", (4, range(4), {0, 1, 2, 3}))
|
||||
@pytest.mark.parametrize("m", (range(4, 7), {4, 5, 6}))
|
||||
def test_complete_bipartite_graph_str(self, n, m):
|
||||
"""Ensure G.name is consistent for all inputs accepted by nodes_or_number.
|
||||
See gh-7396"""
|
||||
G = nx.complete_bipartite_graph(n, m)
|
||||
ans = "Graph named 'complete_bipartite_graph(4, 3)' with 7 nodes and 12 edges"
|
||||
assert str(G) == ans
|
||||
+218
@@ -0,0 +1,218 @@
|
||||
import itertools
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms import bipartite
|
||||
|
||||
pytest.importorskip("scipy")
|
||||
|
||||
|
||||
class TestBipartiteLinkAnalysis:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
cls.davis_southern_women_graph = nx.davis_southern_women_graph()
|
||||
cls.women_bipartite_set = {
|
||||
node
|
||||
for node, bipartite in cls.davis_southern_women_graph.nodes(
|
||||
data="bipartite"
|
||||
)
|
||||
if bipartite == 0
|
||||
}
|
||||
cls.gnmk_random_graph = nx.bipartite.generators.gnmk_random_graph(
|
||||
5 * 10**2, 10**2, 5 * 10**2, seed=27
|
||||
)
|
||||
cls.gnmk_random_graph_top_nodes = {
|
||||
node
|
||||
for node, bipartite in cls.gnmk_random_graph.nodes(data="bipartite")
|
||||
if bipartite == 0
|
||||
}
|
||||
|
||||
def test_collaborative_filtering_birank(self):
|
||||
elist = [
|
||||
("u1", "p1", 5),
|
||||
("u2", "p1", 5),
|
||||
("u2", "p2", 4),
|
||||
("u3", "p1", 3),
|
||||
("u3", "p3", 2),
|
||||
]
|
||||
item_recommendation_graph = nx.DiGraph()
|
||||
item_recommendation_graph.add_weighted_edges_from(elist, weight="rating")
|
||||
product_nodes = ("p1", "p2", "p3")
|
||||
u1_query = {
|
||||
product: rating
|
||||
for _, product, rating in item_recommendation_graph.edges(
|
||||
nbunch="u1", data="rating"
|
||||
)
|
||||
}
|
||||
u1_birank_results = bipartite.birank(
|
||||
item_recommendation_graph,
|
||||
product_nodes,
|
||||
alpha=0.8,
|
||||
beta=1.0,
|
||||
top_personalization=u1_query,
|
||||
weight="rating",
|
||||
)
|
||||
|
||||
assert u1_birank_results["p2"] > u1_birank_results["p3"]
|
||||
|
||||
u1_birank_results_unweighted = bipartite.birank(
|
||||
item_recommendation_graph,
|
||||
product_nodes,
|
||||
alpha=0.8,
|
||||
beta=1.0,
|
||||
top_personalization=u1_query,
|
||||
weight=None,
|
||||
)
|
||||
|
||||
assert u1_birank_results_unweighted["p2"] == pytest.approx(
|
||||
u1_birank_results_unweighted["p3"], rel=2e-6
|
||||
)
|
||||
|
||||
def test_davis_birank(self):
|
||||
scores = bipartite.birank(
|
||||
self.davis_southern_women_graph, self.women_bipartite_set
|
||||
)
|
||||
answer = {
|
||||
"Laura Mandeville": 0.07,
|
||||
"Olivia Carleton": 0.04,
|
||||
"Frances Anderson": 0.05,
|
||||
"Pearl Oglethorpe": 0.04,
|
||||
"Katherina Rogers": 0.06,
|
||||
"Flora Price": 0.04,
|
||||
"Dorothy Murchison": 0.04,
|
||||
"Helen Lloyd": 0.06,
|
||||
"Theresa Anderson": 0.07,
|
||||
"Eleanor Nye": 0.05,
|
||||
"Evelyn Jefferson": 0.07,
|
||||
"Sylvia Avondale": 0.07,
|
||||
"Charlotte McDowd": 0.05,
|
||||
"Verne Sanderson": 0.05,
|
||||
"Myra Liddel": 0.05,
|
||||
"Brenda Rogers": 0.07,
|
||||
"Ruth DeSand": 0.05,
|
||||
"Nora Fayette": 0.07,
|
||||
"E8": 0.11,
|
||||
"E7": 0.09,
|
||||
"E10": 0.07,
|
||||
"E9": 0.1,
|
||||
"E13": 0.05,
|
||||
"E3": 0.07,
|
||||
"E12": 0.07,
|
||||
"E11": 0.06,
|
||||
"E2": 0.05,
|
||||
"E5": 0.08,
|
||||
"E6": 0.08,
|
||||
"E14": 0.05,
|
||||
"E4": 0.06,
|
||||
"E1": 0.05,
|
||||
}
|
||||
|
||||
for node, value in answer.items():
|
||||
assert scores[node] == pytest.approx(value, abs=1e-2)
|
||||
|
||||
def test_davis_birank_with_personalization(self):
|
||||
women_personalization = {"Laura Mandeville": 1}
|
||||
scores = bipartite.birank(
|
||||
self.davis_southern_women_graph,
|
||||
self.women_bipartite_set,
|
||||
top_personalization=women_personalization,
|
||||
)
|
||||
answer = {
|
||||
"Laura Mandeville": 0.29,
|
||||
"Olivia Carleton": 0.02,
|
||||
"Frances Anderson": 0.06,
|
||||
"Pearl Oglethorpe": 0.04,
|
||||
"Katherina Rogers": 0.04,
|
||||
"Flora Price": 0.02,
|
||||
"Dorothy Murchison": 0.03,
|
||||
"Helen Lloyd": 0.04,
|
||||
"Theresa Anderson": 0.08,
|
||||
"Eleanor Nye": 0.05,
|
||||
"Evelyn Jefferson": 0.09,
|
||||
"Sylvia Avondale": 0.05,
|
||||
"Charlotte McDowd": 0.06,
|
||||
"Verne Sanderson": 0.04,
|
||||
"Myra Liddel": 0.03,
|
||||
"Brenda Rogers": 0.08,
|
||||
"Ruth DeSand": 0.05,
|
||||
"Nora Fayette": 0.05,
|
||||
"E8": 0.11,
|
||||
"E7": 0.1,
|
||||
"E10": 0.04,
|
||||
"E9": 0.07,
|
||||
"E13": 0.03,
|
||||
"E3": 0.11,
|
||||
"E12": 0.04,
|
||||
"E11": 0.03,
|
||||
"E2": 0.1,
|
||||
"E5": 0.11,
|
||||
"E6": 0.1,
|
||||
"E14": 0.03,
|
||||
"E4": 0.06,
|
||||
"E1": 0.1,
|
||||
}
|
||||
|
||||
for node, value in answer.items():
|
||||
assert scores[node] == pytest.approx(value, abs=1e-2)
|
||||
|
||||
def test_birank_empty_bipartite_set(self):
|
||||
G = nx.Graph()
|
||||
all_nodes = [1, 2, 3]
|
||||
G.add_nodes_from(all_nodes)
|
||||
|
||||
# Test with empty bipartite set
|
||||
with pytest.raises(nx.NetworkXAlgorithmError):
|
||||
bipartite.birank(G, all_nodes)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"damping_factor,value", itertools.product(["alpha", "beta"], [-0.1, 1.1])
|
||||
)
|
||||
def test_birank_invalid_alpha_beta(self, damping_factor, value):
|
||||
kwargs = {damping_factor: value}
|
||||
with pytest.raises(nx.NetworkXAlgorithmError):
|
||||
bipartite.birank(
|
||||
self.davis_southern_women_graph, self.women_bipartite_set, **kwargs
|
||||
)
|
||||
|
||||
def test_birank_power_iteration_failed_convergence(self):
|
||||
with pytest.raises(nx.PowerIterationFailedConvergence):
|
||||
bipartite.birank(
|
||||
self.davis_southern_women_graph, self.women_bipartite_set, max_iter=1
|
||||
)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"personalization,alpha,beta",
|
||||
itertools.product(
|
||||
[
|
||||
# Concentrated case
|
||||
lambda x: 1000 if x == 0 else 0,
|
||||
# Uniform case
|
||||
lambda x: 5,
|
||||
# Zero case
|
||||
lambda x: 0,
|
||||
],
|
||||
[i / 2 for i in range(3)],
|
||||
[i / 2 for i in range(3)],
|
||||
),
|
||||
)
|
||||
def test_gnmk_convergence_birank(self, personalization, alpha, beta):
|
||||
top_personalization_dict = {
|
||||
node: personalization(node) for node in self.gnmk_random_graph_top_nodes
|
||||
}
|
||||
bipartite.birank(
|
||||
self.gnmk_random_graph,
|
||||
self.gnmk_random_graph_top_nodes,
|
||||
top_personalization=top_personalization_dict,
|
||||
alpha=alpha,
|
||||
beta=beta,
|
||||
)
|
||||
|
||||
def test_negative_personalization(self):
|
||||
top_personalization_dict = {0: -1}
|
||||
with pytest.raises(nx.NetworkXAlgorithmError):
|
||||
bipartite.birank(
|
||||
self.gnmk_random_graph,
|
||||
self.gnmk_random_graph_top_nodes,
|
||||
top_personalization=top_personalization_dict,
|
||||
)
|
||||
+327
@@ -0,0 +1,327 @@
|
||||
"""Unit tests for the :mod:`networkx.algorithms.bipartite.matching` module."""
|
||||
|
||||
import itertools
|
||||
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.bipartite.matching import (
|
||||
eppstein_matching,
|
||||
hopcroft_karp_matching,
|
||||
maximum_matching,
|
||||
minimum_weight_full_matching,
|
||||
to_vertex_cover,
|
||||
)
|
||||
|
||||
|
||||
class TestMatching:
|
||||
"""Tests for bipartite matching algorithms."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Creates a bipartite graph for use in testing matching algorithms.
|
||||
|
||||
The bipartite graph has a maximum cardinality matching that leaves
|
||||
vertex 1 and vertex 10 unmatched. The first six numbers are the left
|
||||
vertices and the next six numbers are the right vertices.
|
||||
|
||||
"""
|
||||
self.simple_graph = nx.complete_bipartite_graph(2, 3)
|
||||
self.simple_solution = {0: 2, 1: 3, 2: 0, 3: 1}
|
||||
|
||||
edges = [(0, 7), (0, 8), (2, 6), (2, 9), (3, 8), (4, 8), (4, 9), (5, 11)]
|
||||
self.top_nodes = set(range(6))
|
||||
self.graph = nx.Graph()
|
||||
self.graph.add_nodes_from(range(12))
|
||||
self.graph.add_edges_from(edges)
|
||||
|
||||
# Example bipartite graph from issue 2127
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from(
|
||||
[
|
||||
(1, "C"),
|
||||
(1, "B"),
|
||||
(0, "G"),
|
||||
(1, "F"),
|
||||
(1, "E"),
|
||||
(0, "C"),
|
||||
(1, "D"),
|
||||
(1, "I"),
|
||||
(0, "A"),
|
||||
(0, "D"),
|
||||
(0, "F"),
|
||||
(0, "E"),
|
||||
(0, "H"),
|
||||
(1, "G"),
|
||||
(1, "A"),
|
||||
(0, "I"),
|
||||
(0, "B"),
|
||||
(1, "H"),
|
||||
]
|
||||
)
|
||||
G.add_edge((1, "C"), (0, "A"))
|
||||
G.add_edge((1, "B"), (0, "A"))
|
||||
G.add_edge((0, "G"), (1, "I"))
|
||||
G.add_edge((0, "G"), (1, "H"))
|
||||
G.add_edge((1, "F"), (0, "A"))
|
||||
G.add_edge((1, "F"), (0, "C"))
|
||||
G.add_edge((1, "F"), (0, "E"))
|
||||
G.add_edge((1, "E"), (0, "A"))
|
||||
G.add_edge((1, "E"), (0, "C"))
|
||||
G.add_edge((0, "C"), (1, "D"))
|
||||
G.add_edge((0, "C"), (1, "I"))
|
||||
G.add_edge((0, "C"), (1, "G"))
|
||||
G.add_edge((0, "C"), (1, "H"))
|
||||
G.add_edge((1, "D"), (0, "A"))
|
||||
G.add_edge((1, "I"), (0, "A"))
|
||||
G.add_edge((1, "I"), (0, "E"))
|
||||
G.add_edge((0, "A"), (1, "G"))
|
||||
G.add_edge((0, "A"), (1, "H"))
|
||||
G.add_edge((0, "E"), (1, "G"))
|
||||
G.add_edge((0, "E"), (1, "H"))
|
||||
self.disconnected_graph = G
|
||||
|
||||
def check_match(self, matching):
|
||||
"""Asserts that the matching is what we expect from the bipartite graph
|
||||
constructed in the :meth:`setup` fixture.
|
||||
|
||||
"""
|
||||
# For the sake of brevity, rename `matching` to `M`.
|
||||
M = matching
|
||||
matched_vertices = frozenset(itertools.chain(*M.items()))
|
||||
# Assert that the maximum number of vertices (10) is matched.
|
||||
assert matched_vertices == frozenset(range(12)) - {1, 10}
|
||||
# Assert that no vertex appears in two edges, or in other words, that
|
||||
# the matching (u, v) and (v, u) both appear in the matching
|
||||
# dictionary.
|
||||
assert all(u == M[M[u]] for u in range(12) if u in M)
|
||||
|
||||
def check_vertex_cover(self, vertices):
|
||||
"""Asserts that the given set of vertices is the vertex cover we
|
||||
expected from the bipartite graph constructed in the :meth:`setup`
|
||||
fixture.
|
||||
|
||||
"""
|
||||
# By Konig's theorem, the number of edges in a maximum matching equals
|
||||
# the number of vertices in a minimum vertex cover.
|
||||
assert len(vertices) == 5
|
||||
# Assert that the set is truly a vertex cover.
|
||||
for u, v in self.graph.edges():
|
||||
assert u in vertices or v in vertices
|
||||
# TODO Assert that the vertices are the correct ones.
|
||||
|
||||
def test_eppstein_matching(self):
|
||||
"""Tests that David Eppstein's implementation of the Hopcroft--Karp
|
||||
algorithm produces a maximum cardinality matching.
|
||||
|
||||
"""
|
||||
self.check_match(eppstein_matching(self.graph, self.top_nodes))
|
||||
|
||||
def test_hopcroft_karp_matching(self):
|
||||
"""Tests that the Hopcroft--Karp algorithm produces a maximum
|
||||
cardinality matching in a bipartite graph.
|
||||
|
||||
"""
|
||||
self.check_match(hopcroft_karp_matching(self.graph, self.top_nodes))
|
||||
|
||||
def test_to_vertex_cover(self):
|
||||
"""Test for converting a maximum matching to a minimum vertex cover."""
|
||||
matching = maximum_matching(self.graph, self.top_nodes)
|
||||
vertex_cover = to_vertex_cover(self.graph, matching, self.top_nodes)
|
||||
self.check_vertex_cover(vertex_cover)
|
||||
|
||||
def test_eppstein_matching_simple(self):
|
||||
match = eppstein_matching(self.simple_graph)
|
||||
assert match == self.simple_solution
|
||||
|
||||
def test_hopcroft_karp_matching_simple(self):
|
||||
match = hopcroft_karp_matching(self.simple_graph)
|
||||
assert match == self.simple_solution
|
||||
|
||||
def test_eppstein_matching_disconnected(self):
|
||||
with pytest.raises(nx.AmbiguousSolution):
|
||||
match = eppstein_matching(self.disconnected_graph)
|
||||
|
||||
def test_hopcroft_karp_matching_disconnected(self):
|
||||
with pytest.raises(nx.AmbiguousSolution):
|
||||
match = hopcroft_karp_matching(self.disconnected_graph)
|
||||
|
||||
def test_issue_2127(self):
|
||||
"""Test from issue 2127"""
|
||||
# Build the example DAG
|
||||
G = nx.DiGraph()
|
||||
G.add_edge("A", "C")
|
||||
G.add_edge("A", "B")
|
||||
G.add_edge("C", "E")
|
||||
G.add_edge("C", "D")
|
||||
G.add_edge("E", "G")
|
||||
G.add_edge("E", "F")
|
||||
G.add_edge("G", "I")
|
||||
G.add_edge("G", "H")
|
||||
|
||||
tc = nx.transitive_closure(G)
|
||||
btc = nx.Graph()
|
||||
|
||||
# Create a bipartite graph based on the transitive closure of G
|
||||
for v in tc.nodes():
|
||||
btc.add_node((0, v))
|
||||
btc.add_node((1, v))
|
||||
|
||||
for u, v in tc.edges():
|
||||
btc.add_edge((0, u), (1, v))
|
||||
|
||||
top_nodes = {n for n in btc if n[0] == 0}
|
||||
matching = hopcroft_karp_matching(btc, top_nodes)
|
||||
vertex_cover = to_vertex_cover(btc, matching, top_nodes)
|
||||
independent_set = set(G) - {v for _, v in vertex_cover}
|
||||
assert {"B", "D", "F", "I", "H"} == independent_set
|
||||
|
||||
def test_vertex_cover_issue_2384(self):
|
||||
G = nx.Graph([(0, 3), (1, 3), (1, 4), (2, 3)])
|
||||
matching = maximum_matching(G)
|
||||
vertex_cover = to_vertex_cover(G, matching)
|
||||
for u, v in G.edges():
|
||||
assert u in vertex_cover or v in vertex_cover
|
||||
|
||||
def test_vertex_cover_issue_3306(self):
|
||||
G = nx.Graph()
|
||||
edges = [(0, 2), (1, 0), (1, 1), (1, 2), (2, 2)]
|
||||
G.add_edges_from([((i, "L"), (j, "R")) for i, j in edges])
|
||||
|
||||
matching = maximum_matching(G)
|
||||
vertex_cover = to_vertex_cover(G, matching)
|
||||
for u, v in G.edges():
|
||||
assert u in vertex_cover or v in vertex_cover
|
||||
|
||||
def test_unorderable_nodes(self):
|
||||
a = object()
|
||||
b = object()
|
||||
c = object()
|
||||
d = object()
|
||||
e = object()
|
||||
G = nx.Graph([(a, d), (b, d), (b, e), (c, d)])
|
||||
matching = maximum_matching(G)
|
||||
vertex_cover = to_vertex_cover(G, matching)
|
||||
for u, v in G.edges():
|
||||
assert u in vertex_cover or v in vertex_cover
|
||||
|
||||
|
||||
def test_eppstein_matching():
|
||||
"""Test in accordance to issue #1927"""
|
||||
G = nx.Graph()
|
||||
G.add_nodes_from(["a", 2, 3, 4], bipartite=0)
|
||||
G.add_nodes_from([1, "b", "c"], bipartite=1)
|
||||
G.add_edges_from([("a", 1), ("a", "b"), (2, "b"), (2, "c"), (3, "c"), (4, 1)])
|
||||
matching = eppstein_matching(G)
|
||||
assert len(matching) == len(maximum_matching(G))
|
||||
assert all(x in set(matching.keys()) for x in set(matching.values()))
|
||||
|
||||
|
||||
class TestMinimumWeightFullMatching:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
pytest.importorskip("scipy")
|
||||
|
||||
def test_minimum_weight_full_matching_incomplete_graph(self):
|
||||
B = nx.Graph()
|
||||
B.add_nodes_from([1, 2], bipartite=0)
|
||||
B.add_nodes_from([3, 4], bipartite=1)
|
||||
B.add_edge(1, 4, weight=100)
|
||||
B.add_edge(2, 3, weight=100)
|
||||
B.add_edge(2, 4, weight=50)
|
||||
matching = minimum_weight_full_matching(B)
|
||||
assert matching == {1: 4, 2: 3, 4: 1, 3: 2}
|
||||
|
||||
def test_minimum_weight_full_matching_with_no_full_matching(self):
|
||||
B = nx.Graph()
|
||||
B.add_nodes_from([1, 2, 3], bipartite=0)
|
||||
B.add_nodes_from([4, 5, 6], bipartite=1)
|
||||
B.add_edge(1, 4, weight=100)
|
||||
B.add_edge(2, 4, weight=100)
|
||||
B.add_edge(3, 4, weight=50)
|
||||
B.add_edge(3, 5, weight=50)
|
||||
B.add_edge(3, 6, weight=50)
|
||||
with pytest.raises(ValueError):
|
||||
minimum_weight_full_matching(B)
|
||||
|
||||
def test_minimum_weight_full_matching_square(self):
|
||||
G = nx.complete_bipartite_graph(3, 3)
|
||||
G.add_edge(0, 3, weight=400)
|
||||
G.add_edge(0, 4, weight=150)
|
||||
G.add_edge(0, 5, weight=400)
|
||||
G.add_edge(1, 3, weight=400)
|
||||
G.add_edge(1, 4, weight=450)
|
||||
G.add_edge(1, 5, weight=600)
|
||||
G.add_edge(2, 3, weight=300)
|
||||
G.add_edge(2, 4, weight=225)
|
||||
G.add_edge(2, 5, weight=300)
|
||||
matching = minimum_weight_full_matching(G)
|
||||
assert matching == {0: 4, 1: 3, 2: 5, 4: 0, 3: 1, 5: 2}
|
||||
|
||||
def test_minimum_weight_full_matching_smaller_left(self):
|
||||
G = nx.complete_bipartite_graph(3, 4)
|
||||
G.add_edge(0, 3, weight=400)
|
||||
G.add_edge(0, 4, weight=150)
|
||||
G.add_edge(0, 5, weight=400)
|
||||
G.add_edge(0, 6, weight=1)
|
||||
G.add_edge(1, 3, weight=400)
|
||||
G.add_edge(1, 4, weight=450)
|
||||
G.add_edge(1, 5, weight=600)
|
||||
G.add_edge(1, 6, weight=2)
|
||||
G.add_edge(2, 3, weight=300)
|
||||
G.add_edge(2, 4, weight=225)
|
||||
G.add_edge(2, 5, weight=290)
|
||||
G.add_edge(2, 6, weight=3)
|
||||
matching = minimum_weight_full_matching(G)
|
||||
assert matching == {0: 4, 1: 6, 2: 5, 4: 0, 5: 2, 6: 1}
|
||||
|
||||
def test_minimum_weight_full_matching_smaller_top_nodes_right(self):
|
||||
G = nx.complete_bipartite_graph(3, 4)
|
||||
G.add_edge(0, 3, weight=400)
|
||||
G.add_edge(0, 4, weight=150)
|
||||
G.add_edge(0, 5, weight=400)
|
||||
G.add_edge(0, 6, weight=1)
|
||||
G.add_edge(1, 3, weight=400)
|
||||
G.add_edge(1, 4, weight=450)
|
||||
G.add_edge(1, 5, weight=600)
|
||||
G.add_edge(1, 6, weight=2)
|
||||
G.add_edge(2, 3, weight=300)
|
||||
G.add_edge(2, 4, weight=225)
|
||||
G.add_edge(2, 5, weight=290)
|
||||
G.add_edge(2, 6, weight=3)
|
||||
matching = minimum_weight_full_matching(G, top_nodes=[3, 4, 5, 6])
|
||||
assert matching == {0: 4, 1: 6, 2: 5, 4: 0, 5: 2, 6: 1}
|
||||
|
||||
def test_minimum_weight_full_matching_smaller_right(self):
|
||||
G = nx.complete_bipartite_graph(4, 3)
|
||||
G.add_edge(0, 4, weight=400)
|
||||
G.add_edge(0, 5, weight=400)
|
||||
G.add_edge(0, 6, weight=300)
|
||||
G.add_edge(1, 4, weight=150)
|
||||
G.add_edge(1, 5, weight=450)
|
||||
G.add_edge(1, 6, weight=225)
|
||||
G.add_edge(2, 4, weight=400)
|
||||
G.add_edge(2, 5, weight=600)
|
||||
G.add_edge(2, 6, weight=290)
|
||||
G.add_edge(3, 4, weight=1)
|
||||
G.add_edge(3, 5, weight=2)
|
||||
G.add_edge(3, 6, weight=3)
|
||||
matching = minimum_weight_full_matching(G)
|
||||
assert matching == {1: 4, 2: 6, 3: 5, 4: 1, 5: 3, 6: 2}
|
||||
|
||||
def test_minimum_weight_full_matching_negative_weights(self):
|
||||
G = nx.complete_bipartite_graph(2, 2)
|
||||
G.add_edge(0, 2, weight=-2)
|
||||
G.add_edge(0, 3, weight=0.2)
|
||||
G.add_edge(1, 2, weight=-2)
|
||||
G.add_edge(1, 3, weight=0.3)
|
||||
matching = minimum_weight_full_matching(G)
|
||||
assert matching == {0: 3, 1: 2, 2: 1, 3: 0}
|
||||
|
||||
def test_minimum_weight_full_matching_different_weight_key(self):
|
||||
G = nx.complete_bipartite_graph(2, 2)
|
||||
G.add_edge(0, 2, mass=2)
|
||||
G.add_edge(0, 3, mass=0.2)
|
||||
G.add_edge(1, 2, mass=1)
|
||||
G.add_edge(1, 3, mass=2)
|
||||
matching = minimum_weight_full_matching(G, weight="mass")
|
||||
assert matching == {0: 3, 1: 2, 2: 1, 3: 0}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms import bipartite
|
||||
from networkx.utils import edges_equal
|
||||
|
||||
np = pytest.importorskip("numpy")
|
||||
sp = pytest.importorskip("scipy")
|
||||
|
||||
|
||||
class TestBiadjacencyMatrix:
|
||||
def test_biadjacency_matrix_weight(self):
|
||||
G = nx.path_graph(5)
|
||||
G.add_edge(0, 1, weight=2, other=4)
|
||||
X = [1, 3]
|
||||
Y = [0, 2, 4]
|
||||
M = bipartite.biadjacency_matrix(G, X, weight="weight")
|
||||
assert M[0, 0] == 2
|
||||
M = bipartite.biadjacency_matrix(G, X, weight="other")
|
||||
assert M[0, 0] == 4
|
||||
|
||||
def test_biadjacency_matrix(self):
|
||||
tops = [2, 5, 10]
|
||||
bots = [5, 10, 15]
|
||||
for i in range(len(tops)):
|
||||
G = bipartite.random_graph(tops[i], bots[i], 0.2)
|
||||
top = [n for n, d in G.nodes(data=True) if d["bipartite"] == 0]
|
||||
M = bipartite.biadjacency_matrix(G, top)
|
||||
assert M.shape[0] == tops[i]
|
||||
assert M.shape[1] == bots[i]
|
||||
|
||||
def test_biadjacency_matrix_order(self):
|
||||
G = nx.path_graph(5)
|
||||
G.add_edge(0, 1, weight=2)
|
||||
X = [3, 1]
|
||||
Y = [4, 2, 0]
|
||||
M = bipartite.biadjacency_matrix(G, X, Y, weight="weight")
|
||||
assert M[1, 2] == 2
|
||||
|
||||
def test_biadjacency_matrix_empty_graph(self):
|
||||
G = nx.empty_graph(2)
|
||||
M = nx.bipartite.biadjacency_matrix(G, [0])
|
||||
assert np.array_equal(M.toarray(), np.array([[0]]))
|
||||
|
||||
def test_null_graph(self):
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
bipartite.biadjacency_matrix(nx.Graph(), [])
|
||||
|
||||
def test_empty_graph(self):
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
bipartite.biadjacency_matrix(nx.Graph([(1, 0)]), [])
|
||||
|
||||
def test_duplicate_row(self):
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
bipartite.biadjacency_matrix(nx.Graph([(1, 0)]), [1, 1])
|
||||
|
||||
def test_duplicate_col(self):
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
bipartite.biadjacency_matrix(nx.Graph([(1, 0)]), [0], [1, 1])
|
||||
|
||||
def test_format_keyword(self):
|
||||
with pytest.raises(nx.NetworkXError):
|
||||
bipartite.biadjacency_matrix(nx.Graph([(1, 0)]), [0], format="foo")
|
||||
|
||||
def test_from_biadjacency_roundtrip(self):
|
||||
B1 = nx.path_graph(5)
|
||||
M = bipartite.biadjacency_matrix(B1, [0, 2, 4])
|
||||
B2 = bipartite.from_biadjacency_matrix(M)
|
||||
assert nx.is_isomorphic(B1, B2)
|
||||
|
||||
def test_from_biadjacency_weight(self):
|
||||
M = sp.sparse.csc_array([[1, 2], [0, 3]])
|
||||
B = bipartite.from_biadjacency_matrix(M)
|
||||
assert edges_equal(B.edges(), [(0, 2), (0, 3), (1, 3)])
|
||||
B = bipartite.from_biadjacency_matrix(M, edge_attribute="weight")
|
||||
e = [(0, 2, {"weight": 1}), (0, 3, {"weight": 2}), (1, 3, {"weight": 3})]
|
||||
assert edges_equal(B.edges(data=True), e)
|
||||
|
||||
def test_from_biadjacency_multigraph(self):
|
||||
M = sp.sparse.csc_array([[1, 2], [0, 3]])
|
||||
B = bipartite.from_biadjacency_matrix(M, create_using=nx.MultiGraph())
|
||||
assert edges_equal(B.edges(), [(0, 2), (0, 3), (0, 3), (1, 3), (1, 3), (1, 3)])
|
||||
+409
@@ -0,0 +1,409 @@
|
||||
import pytest
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms import bipartite
|
||||
from networkx.utils import edges_equal, nodes_equal
|
||||
|
||||
|
||||
class TestBipartiteProject:
|
||||
def test_path_projected_graph(self):
|
||||
G = nx.path_graph(4)
|
||||
P = bipartite.projected_graph(G, [1, 3])
|
||||
assert nodes_equal(list(P), [1, 3])
|
||||
assert edges_equal(list(P.edges()), [(1, 3)])
|
||||
P = bipartite.projected_graph(G, [0, 2])
|
||||
assert nodes_equal(list(P), [0, 2])
|
||||
assert edges_equal(list(P.edges()), [(0, 2)])
|
||||
G = nx.MultiGraph([(0, 1)])
|
||||
with pytest.raises(nx.NetworkXError, match="not defined for multigraphs"):
|
||||
bipartite.projected_graph(G, [0])
|
||||
|
||||
def test_path_projected_properties_graph(self):
|
||||
G = nx.path_graph(4)
|
||||
G.add_node(1, name="one")
|
||||
G.add_node(2, name="two")
|
||||
P = bipartite.projected_graph(G, [1, 3])
|
||||
assert nodes_equal(list(P), [1, 3])
|
||||
assert edges_equal(list(P.edges()), [(1, 3)])
|
||||
assert P.nodes[1]["name"] == G.nodes[1]["name"]
|
||||
P = bipartite.projected_graph(G, [0, 2])
|
||||
assert nodes_equal(list(P), [0, 2])
|
||||
assert edges_equal(list(P.edges()), [(0, 2)])
|
||||
assert P.nodes[2]["name"] == G.nodes[2]["name"]
|
||||
|
||||
def test_path_collaboration_projected_graph(self):
|
||||
G = nx.path_graph(4)
|
||||
P = bipartite.collaboration_weighted_projected_graph(G, [1, 3])
|
||||
assert nodes_equal(list(P), [1, 3])
|
||||
assert edges_equal(list(P.edges()), [(1, 3)])
|
||||
P[1][3]["weight"] = 1
|
||||
P = bipartite.collaboration_weighted_projected_graph(G, [0, 2])
|
||||
assert nodes_equal(list(P), [0, 2])
|
||||
assert edges_equal(list(P.edges()), [(0, 2)])
|
||||
P[0][2]["weight"] = 1
|
||||
|
||||
def test_directed_path_collaboration_projected_graph(self):
|
||||
G = nx.DiGraph()
|
||||
nx.add_path(G, range(4))
|
||||
P = bipartite.collaboration_weighted_projected_graph(G, [1, 3])
|
||||
assert nodes_equal(list(P), [1, 3])
|
||||
assert edges_equal(list(P.edges()), [(1, 3)])
|
||||
P[1][3]["weight"] = 1
|
||||
P = bipartite.collaboration_weighted_projected_graph(G, [0, 2])
|
||||
assert nodes_equal(list(P), [0, 2])
|
||||
assert edges_equal(list(P.edges()), [(0, 2)])
|
||||
P[0][2]["weight"] = 1
|
||||
|
||||
def test_path_weighted_projected_graph(self):
|
||||
G = nx.path_graph(4)
|
||||
|
||||
with pytest.raises(nx.NetworkXAlgorithmError):
|
||||
bipartite.weighted_projected_graph(G, [1, 2, 3, 3])
|
||||
|
||||
P = bipartite.weighted_projected_graph(G, [1, 3])
|
||||
assert nodes_equal(list(P), [1, 3])
|
||||
assert edges_equal(list(P.edges()), [(1, 3)])
|
||||
P[1][3]["weight"] = 1
|
||||
P = bipartite.weighted_projected_graph(G, [0, 2])
|
||||
assert nodes_equal(list(P), [0, 2])
|
||||
assert edges_equal(list(P.edges()), [(0, 2)])
|
||||
P[0][2]["weight"] = 1
|
||||
|
||||
def test_digraph_weighted_projection(self):
|
||||
G = nx.DiGraph([(0, 1), (1, 2), (2, 3), (3, 4)])
|
||||
P = bipartite.overlap_weighted_projected_graph(G, [1, 3])
|
||||
assert nx.get_edge_attributes(P, "weight") == {(1, 3): 1.0}
|
||||
assert len(P) == 2
|
||||
|
||||
def test_path_weighted_projected_directed_graph(self):
|
||||
G = nx.DiGraph()
|
||||
nx.add_path(G, range(4))
|
||||
P = bipartite.weighted_projected_graph(G, [1, 3])
|
||||
assert nodes_equal(list(P), [1, 3])
|
||||
assert edges_equal(list(P.edges()), [(1, 3)], directed=True)
|
||||
P[1][3]["weight"] = 1
|
||||
P = bipartite.weighted_projected_graph(G, [0, 2])
|
||||
assert nodes_equal(list(P), [0, 2])
|
||||
assert edges_equal(list(P.edges()), [(0, 2)], directed=True)
|
||||
P[0][2]["weight"] = 1
|
||||
|
||||
def test_star_projected_graph(self):
|
||||
G = nx.star_graph(3)
|
||||
P = bipartite.projected_graph(G, [1, 2, 3])
|
||||
assert nodes_equal(list(P), [1, 2, 3])
|
||||
assert edges_equal(list(P.edges()), [(1, 2), (1, 3), (2, 3)])
|
||||
P = bipartite.weighted_projected_graph(G, [1, 2, 3])
|
||||
assert nodes_equal(list(P), [1, 2, 3])
|
||||
assert edges_equal(list(P.edges()), [(1, 2), (1, 3), (2, 3)])
|
||||
|
||||
P = bipartite.projected_graph(G, [0])
|
||||
assert nodes_equal(list(P), [0])
|
||||
assert edges_equal(list(P.edges()), [])
|
||||
|
||||
def test_project_multigraph(self):
|
||||
G = nx.Graph()
|
||||
G.add_edge("a", 1)
|
||||
G.add_edge("b", 1)
|
||||
G.add_edge("a", 2)
|
||||
G.add_edge("b", 2)
|
||||
P = bipartite.projected_graph(G, "ab")
|
||||
assert edges_equal(list(P.edges()), [("a", "b")])
|
||||
P = bipartite.weighted_projected_graph(G, "ab")
|
||||
assert edges_equal(list(P.edges()), [("a", "b")])
|
||||
P = bipartite.projected_graph(G, "ab", multigraph=True)
|
||||
assert edges_equal(list(P.edges()), [("a", "b"), ("a", "b")])
|
||||
|
||||
def test_project_collaboration(self):
|
||||
G = nx.Graph()
|
||||
G.add_edge("a", 1)
|
||||
G.add_edge("b", 1)
|
||||
G.add_edge("b", 2)
|
||||
G.add_edge("c", 2)
|
||||
G.add_edge("c", 3)
|
||||
G.add_edge("c", 4)
|
||||
G.add_edge("b", 4)
|
||||
P = bipartite.collaboration_weighted_projected_graph(G, "abc")
|
||||
assert P["a"]["b"]["weight"] == 1
|
||||
assert P["b"]["c"]["weight"] == 2
|
||||
|
||||
def test_directed_projection(self):
|
||||
G = nx.DiGraph()
|
||||
G.add_edge("A", 1)
|
||||
G.add_edge(1, "B")
|
||||
G.add_edge("A", 2)
|
||||
G.add_edge("B", 2)
|
||||
P = bipartite.projected_graph(G, "AB")
|
||||
assert edges_equal(list(P.edges()), [("A", "B")], directed=True)
|
||||
P = bipartite.weighted_projected_graph(G, "AB")
|
||||
assert edges_equal(list(P.edges()), [("A", "B")], directed=True)
|
||||
assert P["A"]["B"]["weight"] == 1
|
||||
|
||||
P = bipartite.projected_graph(G, "AB", multigraph=True)
|
||||
assert edges_equal(list(P.edges()), [("A", "B")], directed=True)
|
||||
|
||||
G = nx.DiGraph()
|
||||
G.add_edge("A", 1)
|
||||
G.add_edge(1, "B")
|
||||
G.add_edge("A", 2)
|
||||
G.add_edge(2, "B")
|
||||
P = bipartite.projected_graph(G, "AB")
|
||||
assert edges_equal(list(P.edges()), [("A", "B")], directed=True)
|
||||
P = bipartite.weighted_projected_graph(G, "AB")
|
||||
assert edges_equal(list(P.edges()), [("A", "B")], directed=True)
|
||||
assert P["A"]["B"]["weight"] == 2
|
||||
|
||||
P = bipartite.projected_graph(G, "AB", multigraph=True)
|
||||
assert edges_equal(list(P.edges()), [("A", "B"), ("A", "B")], directed=True)
|
||||
|
||||
|
||||
class TestBipartiteWeightedProjection:
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
# Tore Opsahl's example
|
||||
# http://toreopsahl.com/2009/05/01/projecting-two-mode-networks-onto-weighted-one-mode-networks/
|
||||
cls.G = nx.Graph()
|
||||
cls.G.add_edge("A", 1)
|
||||
cls.G.add_edge("A", 2)
|
||||
cls.G.add_edge("B", 1)
|
||||
cls.G.add_edge("B", 2)
|
||||
cls.G.add_edge("B", 3)
|
||||
cls.G.add_edge("B", 4)
|
||||
cls.G.add_edge("B", 5)
|
||||
cls.G.add_edge("C", 1)
|
||||
cls.G.add_edge("D", 3)
|
||||
cls.G.add_edge("E", 4)
|
||||
cls.G.add_edge("E", 5)
|
||||
cls.G.add_edge("E", 6)
|
||||
cls.G.add_edge("F", 6)
|
||||
# Graph based on figure 6 from Newman (2001)
|
||||
cls.N = nx.Graph()
|
||||
cls.N.add_edge("A", 1)
|
||||
cls.N.add_edge("A", 2)
|
||||
cls.N.add_edge("A", 3)
|
||||
cls.N.add_edge("B", 1)
|
||||
cls.N.add_edge("B", 2)
|
||||
cls.N.add_edge("B", 3)
|
||||
cls.N.add_edge("C", 1)
|
||||
cls.N.add_edge("D", 1)
|
||||
cls.N.add_edge("E", 3)
|
||||
|
||||
def test_project_weighted_shared(self):
|
||||
edges = [
|
||||
("A", "B", 2),
|
||||
("A", "C", 1),
|
||||
("B", "C", 1),
|
||||
("B", "D", 1),
|
||||
("B", "E", 2),
|
||||
("E", "F", 1),
|
||||
]
|
||||
Panswer = nx.Graph()
|
||||
Panswer.add_weighted_edges_from(edges)
|
||||
P = bipartite.weighted_projected_graph(self.G, "ABCDEF")
|
||||
assert edges_equal(list(P.edges()), Panswer.edges())
|
||||
for u, v in list(P.edges()):
|
||||
assert P[u][v]["weight"] == Panswer[u][v]["weight"]
|
||||
|
||||
edges = [
|
||||
("A", "B", 3),
|
||||
("A", "E", 1),
|
||||
("A", "C", 1),
|
||||
("A", "D", 1),
|
||||
("B", "E", 1),
|
||||
("B", "C", 1),
|
||||
("B", "D", 1),
|
||||
("C", "D", 1),
|
||||
]
|
||||
Panswer = nx.Graph()
|
||||
Panswer.add_weighted_edges_from(edges)
|
||||
P = bipartite.weighted_projected_graph(self.N, "ABCDE")
|
||||
assert edges_equal(list(P.edges()), Panswer.edges())
|
||||
for u, v in list(P.edges()):
|
||||
assert P[u][v]["weight"] == Panswer[u][v]["weight"]
|
||||
|
||||
def test_project_weighted_newman(self):
|
||||
edges = [
|
||||
("A", "B", 1.5),
|
||||
("A", "C", 0.5),
|
||||
("B", "C", 0.5),
|
||||
("B", "D", 1),
|
||||
("B", "E", 2),
|
||||
("E", "F", 1),
|
||||
]
|
||||
Panswer = nx.Graph()
|
||||
Panswer.add_weighted_edges_from(edges)
|
||||
P = bipartite.collaboration_weighted_projected_graph(self.G, "ABCDEF")
|
||||
assert edges_equal(list(P.edges()), Panswer.edges())
|
||||
for u, v in list(P.edges()):
|
||||
assert P[u][v]["weight"] == Panswer[u][v]["weight"]
|
||||
|
||||
edges = [
|
||||
("A", "B", 11 / 6.0),
|
||||
("A", "E", 1 / 2.0),
|
||||
("A", "C", 1 / 3.0),
|
||||
("A", "D", 1 / 3.0),
|
||||
("B", "E", 1 / 2.0),
|
||||
("B", "C", 1 / 3.0),
|
||||
("B", "D", 1 / 3.0),
|
||||
("C", "D", 1 / 3.0),
|
||||
]
|
||||
Panswer = nx.Graph()
|
||||
Panswer.add_weighted_edges_from(edges)
|
||||
P = bipartite.collaboration_weighted_projected_graph(self.N, "ABCDE")
|
||||
assert edges_equal(list(P.edges()), Panswer.edges())
|
||||
for u, v in list(P.edges()):
|
||||
assert P[u][v]["weight"] == Panswer[u][v]["weight"]
|
||||
|
||||
def test_project_weighted_ratio(self):
|
||||
edges = [
|
||||
("A", "B", 2 / 6.0),
|
||||
("A", "C", 1 / 6.0),
|
||||
("B", "C", 1 / 6.0),
|
||||
("B", "D", 1 / 6.0),
|
||||
("B", "E", 2 / 6.0),
|
||||
("E", "F", 1 / 6.0),
|
||||
]
|
||||
Panswer = nx.Graph()
|
||||
Panswer.add_weighted_edges_from(edges)
|
||||
P = bipartite.weighted_projected_graph(self.G, "ABCDEF", ratio=True)
|
||||
assert edges_equal(list(P.edges()), Panswer.edges())
|
||||
for u, v in list(P.edges()):
|
||||
assert P[u][v]["weight"] == Panswer[u][v]["weight"]
|
||||
|
||||
edges = [
|
||||
("A", "B", 3 / 3.0),
|
||||
("A", "E", 1 / 3.0),
|
||||
("A", "C", 1 / 3.0),
|
||||
("A", "D", 1 / 3.0),
|
||||
("B", "E", 1 / 3.0),
|
||||
("B", "C", 1 / 3.0),
|
||||
("B", "D", 1 / 3.0),
|
||||
("C", "D", 1 / 3.0),
|
||||
]
|
||||
Panswer = nx.Graph()
|
||||
Panswer.add_weighted_edges_from(edges)
|
||||
P = bipartite.weighted_projected_graph(self.N, "ABCDE", ratio=True)
|
||||
assert edges_equal(list(P.edges()), Panswer.edges())
|
||||
for u, v in list(P.edges()):
|
||||
assert P[u][v]["weight"] == Panswer[u][v]["weight"]
|
||||
|
||||
def test_project_weighted_overlap(self):
|
||||
edges = [
|
||||
("A", "B", 2 / 2.0),
|
||||
("A", "C", 1 / 1.0),
|
||||
("B", "C", 1 / 1.0),
|
||||
("B", "D", 1 / 1.0),
|
||||
("B", "E", 2 / 3.0),
|
||||
("E", "F", 1 / 1.0),
|
||||
]
|
||||
Panswer = nx.Graph()
|
||||
Panswer.add_weighted_edges_from(edges)
|
||||
P = bipartite.overlap_weighted_projected_graph(self.G, "ABCDEF", jaccard=False)
|
||||
assert edges_equal(list(P.edges()), Panswer.edges())
|
||||
for u, v in list(P.edges()):
|
||||
assert P[u][v]["weight"] == Panswer[u][v]["weight"]
|
||||
|
||||
edges = [
|
||||
("A", "B", 3 / 3.0),
|
||||
("A", "E", 1 / 1.0),
|
||||
("A", "C", 1 / 1.0),
|
||||
("A", "D", 1 / 1.0),
|
||||
("B", "E", 1 / 1.0),
|
||||
("B", "C", 1 / 1.0),
|
||||
("B", "D", 1 / 1.0),
|
||||
("C", "D", 1 / 1.0),
|
||||
]
|
||||
Panswer = nx.Graph()
|
||||
Panswer.add_weighted_edges_from(edges)
|
||||
P = bipartite.overlap_weighted_projected_graph(self.N, "ABCDE", jaccard=False)
|
||||
assert edges_equal(list(P.edges()), Panswer.edges())
|
||||
for u, v in list(P.edges()):
|
||||
assert P[u][v]["weight"] == Panswer[u][v]["weight"]
|
||||
|
||||
def test_project_weighted_jaccard(self):
|
||||
edges = [
|
||||
("A", "B", 2 / 5.0),
|
||||
("A", "C", 1 / 2.0),
|
||||
("B", "C", 1 / 5.0),
|
||||
("B", "D", 1 / 5.0),
|
||||
("B", "E", 2 / 6.0),
|
||||
("E", "F", 1 / 3.0),
|
||||
]
|
||||
Panswer = nx.Graph()
|
||||
Panswer.add_weighted_edges_from(edges)
|
||||
P = bipartite.overlap_weighted_projected_graph(self.G, "ABCDEF")
|
||||
assert edges_equal(list(P.edges()), Panswer.edges())
|
||||
for u, v in list(P.edges()):
|
||||
assert P[u][v]["weight"] == Panswer[u][v]["weight"]
|
||||
|
||||
edges = [
|
||||
("A", "B", 3 / 3.0),
|
||||
("A", "E", 1 / 3.0),
|
||||
("A", "C", 1 / 3.0),
|
||||
("A", "D", 1 / 3.0),
|
||||
("B", "E", 1 / 3.0),
|
||||
("B", "C", 1 / 3.0),
|
||||
("B", "D", 1 / 3.0),
|
||||
("C", "D", 1 / 1.0),
|
||||
]
|
||||
Panswer = nx.Graph()
|
||||
Panswer.add_weighted_edges_from(edges)
|
||||
P = bipartite.overlap_weighted_projected_graph(self.N, "ABCDE")
|
||||
assert edges_equal(list(P.edges()), Panswer.edges())
|
||||
for u, v in P.edges():
|
||||
assert P[u][v]["weight"] == Panswer[u][v]["weight"]
|
||||
|
||||
def test_generic_weighted_projected_graph_simple(self):
|
||||
def shared(G, u, v):
|
||||
return len(set(G[u]) & set(G[v]))
|
||||
|
||||
B = nx.path_graph(5)
|
||||
G = bipartite.generic_weighted_projected_graph(
|
||||
B, [0, 2, 4], weight_function=shared
|
||||
)
|
||||
assert nodes_equal(list(G), [0, 2, 4])
|
||||
assert edges_equal(
|
||||
list(G.edges(data=True)),
|
||||
[(0, 2, {"weight": 1}), (2, 4, {"weight": 1})],
|
||||
)
|
||||
|
||||
G = bipartite.generic_weighted_projected_graph(B, [0, 2, 4])
|
||||
assert nodes_equal(list(G), [0, 2, 4])
|
||||
assert edges_equal(
|
||||
list(G.edges(data=True)),
|
||||
[(0, 2, {"weight": 1}), (2, 4, {"weight": 1})],
|
||||
)
|
||||
B = nx.DiGraph()
|
||||
nx.add_path(B, range(5))
|
||||
G = bipartite.generic_weighted_projected_graph(B, [0, 2, 4])
|
||||
assert nodes_equal(list(G), [0, 2, 4])
|
||||
assert edges_equal(
|
||||
list(G.edges(data=True)),
|
||||
[(0, 2, {"weight": 1}), (2, 4, {"weight": 1})],
|
||||
directed=True,
|
||||
)
|
||||
|
||||
def test_generic_weighted_projected_graph_custom(self):
|
||||
def jaccard(G, u, v):
|
||||
unbrs = set(G[u])
|
||||
vnbrs = set(G[v])
|
||||
return len(unbrs & vnbrs) / len(unbrs | vnbrs)
|
||||
|
||||
def my_weight(G, u, v, weight="weight"):
|
||||
w = 0
|
||||
for nbr in set(G[u]) & set(G[v]):
|
||||
w += G.edges[u, nbr].get(weight, 1) + G.edges[v, nbr].get(weight, 1)
|
||||
return w
|
||||
|
||||
B = nx.bipartite.complete_bipartite_graph(2, 2)
|
||||
for i, (u, v) in enumerate(B.edges()):
|
||||
B.edges[u, v]["weight"] = i + 1
|
||||
G = bipartite.generic_weighted_projected_graph(
|
||||
B, [0, 1], weight_function=jaccard
|
||||
)
|
||||
assert edges_equal(list(G.edges(data=True)), [(0, 1, {"weight": 1.0})])
|
||||
G = bipartite.generic_weighted_projected_graph(
|
||||
B, [0, 1], weight_function=my_weight
|
||||
)
|
||||
assert edges_equal(list(G.edges(data=True)), [(0, 1, {"weight": 10})])
|
||||
G = bipartite.generic_weighted_projected_graph(B, [0, 1])
|
||||
assert edges_equal(list(G.edges(data=True)), [(0, 1, {"weight": 2})])
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
"""Unit tests for the :mod:`networkx.algorithms.bipartite.redundancy` module."""
|
||||
|
||||
import pytest
|
||||
|
||||
from networkx import NetworkXError, cycle_graph
|
||||
from networkx.algorithms.bipartite import complete_bipartite_graph, node_redundancy
|
||||
|
||||
|
||||
def test_no_redundant_nodes():
|
||||
G = complete_bipartite_graph(2, 2)
|
||||
|
||||
# when nodes is None
|
||||
rc = node_redundancy(G)
|
||||
assert all(redundancy == 1 for redundancy in rc.values())
|
||||
|
||||
# when set of nodes is specified
|
||||
rc = node_redundancy(G, (2, 3))
|
||||
assert rc == {2: 1.0, 3: 1.0}
|
||||
|
||||
|
||||
def test_redundant_nodes():
|
||||
G = cycle_graph(6)
|
||||
edge = {0, 3}
|
||||
G.add_edge(*edge)
|
||||
redundancy = node_redundancy(G)
|
||||
for v in edge:
|
||||
assert redundancy[v] == 2 / 3
|
||||
for v in set(G) - edge:
|
||||
assert redundancy[v] == 1
|
||||
|
||||
|
||||
def test_not_enough_neighbors():
|
||||
with pytest.raises(NetworkXError):
|
||||
G = complete_bipartite_graph(1, 2)
|
||||
node_redundancy(G)
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("scipy")
|
||||
|
||||
import networkx as nx
|
||||
from networkx.algorithms.bipartite import spectral_bipartivity as sb
|
||||
|
||||
# Examples from Figure 1
|
||||
# E. Estrada and J. A. Rodríguez-Velázquez, "Spectral measures of
|
||||
# bipartivity in complex networks", PhysRev E 72, 046105 (2005)
|
||||
|
||||
|
||||
class TestSpectralBipartivity:
|
||||
def test_star_like(self):
|
||||
# star-like
|
||||
|
||||
G = nx.star_graph(2)
|
||||
G.add_edge(1, 2)
|
||||
assert sb(G) == pytest.approx(0.843, abs=1e-3)
|
||||
|
||||
G = nx.star_graph(3)
|
||||
G.add_edge(1, 2)
|
||||
assert sb(G) == pytest.approx(0.871, abs=1e-3)
|
||||
|
||||
G = nx.star_graph(4)
|
||||
G.add_edge(1, 2)
|
||||
assert sb(G) == pytest.approx(0.890, abs=1e-3)
|
||||
|
||||
def test_k23_like(self):
|
||||
# K2,3-like
|
||||
G = nx.complete_bipartite_graph(2, 3)
|
||||
G.add_edge(0, 1)
|
||||
assert sb(G) == pytest.approx(0.769, abs=1e-3)
|
||||
|
||||
G = nx.complete_bipartite_graph(2, 3)
|
||||
G.add_edge(2, 4)
|
||||
assert sb(G) == pytest.approx(0.829, abs=1e-3)
|
||||
|
||||
G = nx.complete_bipartite_graph(2, 3)
|
||||
G.add_edge(2, 4)
|
||||
G.add_edge(3, 4)
|
||||
assert sb(G) == pytest.approx(0.731, abs=1e-3)
|
||||
|
||||
G = nx.complete_bipartite_graph(2, 3)
|
||||
G.add_edge(0, 1)
|
||||
G.add_edge(2, 4)
|
||||
assert sb(G) == pytest.approx(0.692, abs=1e-3)
|
||||
|
||||
G = nx.complete_bipartite_graph(2, 3)
|
||||
G.add_edge(2, 4)
|
||||
G.add_edge(3, 4)
|
||||
G.add_edge(0, 1)
|
||||
assert sb(G) == pytest.approx(0.645, abs=1e-3)
|
||||
|
||||
G = nx.complete_bipartite_graph(2, 3)
|
||||
G.add_edge(2, 4)
|
||||
G.add_edge(3, 4)
|
||||
G.add_edge(2, 3)
|
||||
assert sb(G) == pytest.approx(0.645, abs=1e-3)
|
||||
|
||||
G = nx.complete_bipartite_graph(2, 3)
|
||||
G.add_edge(2, 4)
|
||||
G.add_edge(3, 4)
|
||||
G.add_edge(2, 3)
|
||||
G.add_edge(0, 1)
|
||||
assert sb(G) == pytest.approx(0.597, abs=1e-3)
|
||||
|
||||
def test_single_nodes(self):
|
||||
# single nodes
|
||||
G = nx.complete_bipartite_graph(2, 3)
|
||||
G.add_edge(2, 4)
|
||||
sbn = sb(G, nodes=[1, 2])
|
||||
assert sbn[1] == pytest.approx(0.85, abs=1e-2)
|
||||
assert sbn[2] == pytest.approx(0.77, abs=1e-2)
|
||||
|
||||
G = nx.complete_bipartite_graph(2, 3)
|
||||
G.add_edge(0, 1)
|
||||
sbn = sb(G, nodes=[1, 2])
|
||||
assert sbn[1] == pytest.approx(0.73, abs=1e-2)
|
||||
assert sbn[2] == pytest.approx(0.82, abs=1e-2)
|
||||
Reference in New Issue
Block a user