1"""One-mode (unipartite) projections of bipartite graphs."""
2
3import networkx as nx
4from networkx.exception import NetworkXAlgorithmError
5from networkx.utils import not_implemented_for
6
7__all__ = [
8 "projected_graph",
9 "weighted_projected_graph",
10 "collaboration_weighted_projected_graph",
11 "overlap_weighted_projected_graph",
12 "generic_weighted_projected_graph",
13]
14
15
16@nx._dispatchable(
17 graphs="B", preserve_node_attrs=True, preserve_graph_attrs=True, returns_graph=True
18)
19def projected_graph(B, nodes, multigraph=False):
20 r"""Returns the projection of B onto one of its node sets.
21
22 Returns the graph G that is the projection of the bipartite graph B
23 onto the specified nodes. They retain their attributes and are connected
24 in G if they have a common neighbor in B.
25
26 Parameters
27 ----------
28 B : NetworkX graph
29 The input graph should be bipartite.
30
31 nodes : list or iterable
32 Nodes to project onto (the "bottom" nodes).
33
34 multigraph: bool (default=False)
35 If True return a multigraph where the multiple edges represent multiple
36 shared neighbors. They edge key in the multigraph is assigned to the
37 label of the neighbor.
38
39 Returns
40 -------
41 Graph : NetworkX graph or multigraph
42 A graph that is the projection onto the given nodes.
43
44 Examples
45 --------
46 >>> from networkx.algorithms import bipartite
47 >>> B = nx.path_graph(4)
48 >>> G = bipartite.projected_graph(B, [1, 3])
49 >>> list(G)
50 [1, 3]
51 >>> list(G.edges())
52 [(1, 3)]
53
54 If nodes `a`, and `b` are connected through both nodes 1 and 2 then
55 building a multigraph results in two edges in the projection onto
56 [`a`, `b`]:
57
58 >>> B = nx.Graph()
59 >>> B.add_edges_from([("a", 1), ("b", 1), ("a", 2), ("b", 2)])
60 >>> G = bipartite.projected_graph(B, ["a", "b"], multigraph=True)
61 >>> print([sorted((u, v)) for u, v in G.edges()])
62 [['a', 'b'], ['a', 'b']]
63
64 Notes
65 -----
66 No attempt is made to verify that the input graph B is bipartite.
67 Returns a simple graph that is the projection of the bipartite graph B
68 onto the set of nodes given in list nodes. If multigraph=True then
69 a multigraph is returned with an edge for every shared neighbor.
70
71 Directed graphs are allowed as input. The output will also then
72 be a directed graph with edges if there is a directed path between
73 the nodes.
74
75 The graph and node properties are (shallow) copied to the projected graph.
76
77 See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
78 for further details on how bipartite graphs are handled in NetworkX.
79
80 See Also
81 --------
82 is_bipartite,
83 is_bipartite_node_set,
84 sets,
85 weighted_projected_graph,
86 collaboration_weighted_projected_graph,
87 overlap_weighted_projected_graph,
88 generic_weighted_projected_graph
89 """
90 if B.is_multigraph():
91 raise nx.NetworkXError("not defined for multigraphs")
92 if B.is_directed():
93 directed = True
94 if multigraph:
95 G = nx.MultiDiGraph()
96 else:
97 G = nx.DiGraph()
98 else:
99 directed = False
100 if multigraph:
101 G = nx.MultiGraph()
102 else:
103 G = nx.Graph()
104 G.graph.update(B.graph)
105 G.add_nodes_from((n, B.nodes[n]) for n in nodes)
106 for u in nodes:
107 nbrs2 = {v for nbr in B[u] for v in B[nbr] if v != u}
108 if multigraph:
109 for n in nbrs2:
110 if directed:
111 links = set(B[u]) & set(B.pred[n])
112 else:
113 links = set(B[u]) & set(B[n])
114 for l in links:
115 if not G.has_edge(u, n, l):
116 G.add_edge(u, n, key=l)
117 else:
118 G.add_edges_from((u, n) for n in nbrs2)
119 return G
120
121
122@not_implemented_for("multigraph")
123@nx._dispatchable(graphs="B", returns_graph=True)
124def weighted_projected_graph(B, nodes, ratio=False):
125 r"""Returns a weighted projection of B onto one of its node sets.
126
127 The weighted projected graph is the projection of the bipartite
128 network B onto the specified nodes with weights representing the
129 number of shared neighbors or the ratio between actual shared
130 neighbors and possible shared neighbors if ``ratio is True`` [1]_.
131 The nodes retain their attributes and are connected in the resulting
132 graph if they have an edge to a common node in the original graph.
133
134 Parameters
135 ----------
136 B : NetworkX graph
137 The input graph should be bipartite.
138
139 nodes : list or iterable
140 Distinct nodes to project onto (the "bottom" nodes).
141
142 ratio: Bool (default=False)
143 If True, edge weight is the ratio between actual shared neighbors
144 and maximum possible shared neighbors (i.e., the size of the other
145 node set). If False, edges weight is the number of shared neighbors.
146
147 Returns
148 -------
149 Graph : NetworkX graph
150 A graph that is the projection onto the given nodes.
151
152 Examples
153 --------
154 >>> from networkx.algorithms import bipartite
155 >>> B = nx.path_graph(4)
156 >>> G = bipartite.weighted_projected_graph(B, [1, 3])
157 >>> list(G)
158 [1, 3]
159 >>> list(G.edges(data=True))
160 [(1, 3, {'weight': 1})]
161 >>> G = bipartite.weighted_projected_graph(B, [1, 3], ratio=True)
162 >>> list(G.edges(data=True))
163 [(1, 3, {'weight': 0.5})]
164
165 Notes
166 -----
167 No attempt is made to verify that the input graph B is bipartite, or that
168 the input nodes are distinct. However, if the length of the input nodes is
169 greater than or equal to the nodes in the graph B, an exception is raised.
170 If the nodes are not distinct but don't raise this error, the output weights
171 will be incorrect.
172 The graph and node properties are (shallow) copied to the projected graph.
173
174 See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
175 for further details on how bipartite graphs are handled in NetworkX.
176
177 See Also
178 --------
179 is_bipartite,
180 is_bipartite_node_set,
181 sets,
182 collaboration_weighted_projected_graph,
183 overlap_weighted_projected_graph,
184 generic_weighted_projected_graph
185 projected_graph
186
187 References
188 ----------
189 .. [1] Borgatti, S.P. and Halgin, D. In press. "Analyzing Affiliation
190 Networks". In Carrington, P. and Scott, J. (eds) The Sage Handbook
191 of Social Network Analysis. Sage Publications.
192 """
193 if B.is_directed():
194 pred = B.pred
195 G = nx.DiGraph()
196 else:
197 pred = B.adj
198 G = nx.Graph()
199 G.graph.update(B.graph)
200 G.add_nodes_from((n, B.nodes[n]) for n in nodes)
201 n_top = len(B) - len(nodes)
202
203 if n_top < 1:
204 raise NetworkXAlgorithmError(
205 f"the size of the nodes to project onto ({len(nodes)}) is >= the graph size ({len(B)}).\n"
206 "They are either not a valid bipartite partition or contain duplicates"
207 )
208
209 for u in nodes:
210 unbrs = set(B[u])
211 nbrs2 = {n for nbr in unbrs for n in B[nbr]} - {u}
212 for v in nbrs2:
213 vnbrs = set(pred[v])
214 common = unbrs & vnbrs
215 if not ratio:
216 weight = len(common)
217 else:
218 weight = len(common) / n_top
219 G.add_edge(u, v, weight=weight)
220 return G
221
222
223@not_implemented_for("multigraph")
224@nx._dispatchable(graphs="B", returns_graph=True)
225def collaboration_weighted_projected_graph(B, nodes):
226 r"""Newman's weighted projection of B onto one of its node sets.
227
228 The collaboration weighted projection is the projection of the
229 bipartite network B onto the specified nodes with weights assigned
230 using Newman's collaboration model [1]_:
231
232 .. math::
233
234 w_{u, v} = \sum_k \frac{\delta_{u}^{k} \delta_{v}^{k}}{d_k - 1}
235
236 where `u` and `v` are nodes from the bottom bipartite node set,
237 and `k` is a node of the top node set.
238 The value `d_k` is the degree of node `k` in the bipartite
239 network and `\delta_{u}^{k}` is 1 if node `u` is
240 linked to node `k` in the original bipartite graph or 0 otherwise.
241
242 The nodes retain their attributes and are connected in the resulting
243 graph if have an edge to a common node in the original bipartite
244 graph.
245
246 Parameters
247 ----------
248 B : NetworkX graph
249 The input graph should be bipartite.
250
251 nodes : list or iterable
252 Nodes to project onto (the "bottom" nodes).
253
254 Returns
255 -------
256 Graph : NetworkX graph
257 A graph that is the projection onto the given nodes.
258
259 Examples
260 --------
261 >>> from networkx.algorithms import bipartite
262 >>> B = nx.path_graph(5)
263 >>> B.add_edge(1, 5)
264 >>> G = bipartite.collaboration_weighted_projected_graph(B, [0, 2, 4, 5])
265 >>> list(G)
266 [0, 2, 4, 5]
267 >>> for edge in sorted(G.edges(data=True)):
268 ... print(edge)
269 (0, 2, {'weight': 0.5})
270 (0, 5, {'weight': 0.5})
271 (2, 4, {'weight': 1.0})
272 (2, 5, {'weight': 0.5})
273
274 Notes
275 -----
276 No attempt is made to verify that the input graph B is bipartite.
277 The graph and node properties are (shallow) copied to the projected graph.
278
279 See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
280 for further details on how bipartite graphs are handled in NetworkX.
281
282 See Also
283 --------
284 is_bipartite,
285 is_bipartite_node_set,
286 sets,
287 weighted_projected_graph,
288 overlap_weighted_projected_graph,
289 generic_weighted_projected_graph,
290 projected_graph
291
292 References
293 ----------
294 .. [1] Scientific collaboration networks: II.
295 Shortest paths, weighted networks, and centrality,
296 M. E. J. Newman, Phys. Rev. E 64, 016132 (2001).
297 """
298 if B.is_directed():
299 pred = B.pred
300 G = nx.DiGraph()
301 else:
302 pred = B.adj
303 G = nx.Graph()
304 G.graph.update(B.graph)
305 G.add_nodes_from((n, B.nodes[n]) for n in nodes)
306 for u in nodes:
307 unbrs = set(B[u])
308 nbrs2 = {n for nbr in unbrs for n in B[nbr] if n != u}
309 for v in nbrs2:
310 vnbrs = set(pred[v])
311 common_degree = (len(B[n]) for n in unbrs & vnbrs)
312 weight = sum(1.0 / (deg - 1) for deg in common_degree if deg > 1)
313 G.add_edge(u, v, weight=weight)
314 return G
315
316
317@not_implemented_for("multigraph")
318@nx._dispatchable(graphs="B", returns_graph=True)
319def overlap_weighted_projected_graph(B, nodes, jaccard=True):
320 r"""Overlap weighted projection of B onto one of its node sets.
321
322 The overlap weighted projection is the projection of the bipartite
323 network B onto the specified nodes with weights representing
324 the Jaccard index between the neighborhoods of the two nodes in the
325 original bipartite network [1]_:
326
327 .. math::
328
329 w_{v, u} = \frac{|N(u) \cap N(v)|}{|N(u) \cup N(v)|}
330
331 or if the parameter 'jaccard' is False, the fraction of common
332 neighbors by minimum of both nodes degree in the original
333 bipartite graph [1]_:
334
335 .. math::
336
337 w_{v, u} = \frac{|N(u) \cap N(v)|}{min(|N(u)|, |N(v)|)}
338
339 The nodes retain their attributes and are connected in the resulting
340 graph if have an edge to a common node in the original bipartite graph.
341
342 Parameters
343 ----------
344 B : NetworkX graph
345 The input graph should be bipartite.
346
347 nodes : list or iterable
348 Nodes to project onto (the "bottom" nodes).
349
350 jaccard: Bool (default=True)
351
352 Returns
353 -------
354 Graph : NetworkX graph
355 A graph that is the projection onto the given nodes.
356
357 Examples
358 --------
359 >>> from networkx.algorithms import bipartite
360 >>> B = nx.path_graph(5)
361 >>> nodes = [0, 2, 4]
362 >>> G = bipartite.overlap_weighted_projected_graph(B, nodes)
363 >>> list(G)
364 [0, 2, 4]
365 >>> list(G.edges(data=True))
366 [(0, 2, {'weight': 0.5}), (2, 4, {'weight': 0.5})]
367 >>> G = bipartite.overlap_weighted_projected_graph(B, nodes, jaccard=False)
368 >>> list(G.edges(data=True))
369 [(0, 2, {'weight': 1.0}), (2, 4, {'weight': 1.0})]
370
371 Notes
372 -----
373 No attempt is made to verify that the input graph B is bipartite.
374 The graph and node properties are (shallow) copied to the projected graph.
375
376 See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
377 for further details on how bipartite graphs are handled in NetworkX.
378
379 See Also
380 --------
381 is_bipartite,
382 is_bipartite_node_set,
383 sets,
384 weighted_projected_graph,
385 collaboration_weighted_projected_graph,
386 generic_weighted_projected_graph,
387 projected_graph
388
389 References
390 ----------
391 .. [1] Borgatti, S.P. and Halgin, D. In press. Analyzing Affiliation
392 Networks. In Carrington, P. and Scott, J. (eds) The Sage Handbook
393 of Social Network Analysis. Sage Publications.
394
395 """
396 if B.is_directed():
397 pred = B.pred
398 G = nx.DiGraph()
399 else:
400 pred = B.adj
401 G = nx.Graph()
402 G.graph.update(B.graph)
403 G.add_nodes_from((n, B.nodes[n]) for n in nodes)
404 for u in nodes:
405 unbrs = set(B[u])
406 nbrs2 = {n for nbr in unbrs for n in B[nbr]} - {u}
407 for v in nbrs2:
408 vnbrs = set(pred[v])
409 if jaccard:
410 wt = len(unbrs & vnbrs) / len(unbrs | vnbrs)
411 else:
412 wt = len(unbrs & vnbrs) / min(len(unbrs), len(vnbrs))
413 G.add_edge(u, v, weight=wt)
414 return G
415
416
417@not_implemented_for("multigraph")
418@nx._dispatchable(graphs="B", preserve_all_attrs=True, returns_graph=True)
419def generic_weighted_projected_graph(B, nodes, weight_function=None):
420 r"""Weighted projection of B with a user-specified weight function.
421
422 The bipartite network B is projected on to the specified nodes
423 with weights computed by a user-specified function. This function
424 must accept as a parameter the neighborhood sets of two nodes and
425 return an integer or a float.
426
427 The nodes retain their attributes and are connected in the resulting graph
428 if they have an edge to a common node in the original graph.
429
430 Parameters
431 ----------
432 B : NetworkX graph
433 The input graph should be bipartite.
434
435 nodes : list or iterable
436 Nodes to project onto (the "bottom" nodes).
437
438 weight_function : function
439 This function must accept as parameters the same input graph
440 that this function, and two nodes; and return an integer or a float.
441 The default function computes the number of shared neighbors.
442
443 Returns
444 -------
445 Graph : NetworkX graph
446 A graph that is the projection onto the given nodes.
447
448 Examples
449 --------
450 >>> from networkx.algorithms import bipartite
451 >>> # Define some custom weight functions
452 >>> def jaccard(G, u, v):
453 ... unbrs = set(G[u])
454 ... vnbrs = set(G[v])
455 ... return float(len(unbrs & vnbrs)) / len(unbrs | vnbrs)
456 >>> def my_weight(G, u, v, weight="weight"):
457 ... w = 0
458 ... for nbr in set(G[u]) & set(G[v]):
459 ... w += G[u][nbr].get(weight, 1) + G[v][nbr].get(weight, 1)
460 ... return w
461 >>> # A complete bipartite graph with 4 nodes and 4 edges
462 >>> B = nx.complete_bipartite_graph(2, 2)
463 >>> # Add some arbitrary weight to the edges
464 >>> for i, (u, v) in enumerate(B.edges()):
465 ... B.edges[u, v]["weight"] = i + 1
466 >>> for edge in B.edges(data=True):
467 ... print(edge)
468 (0, 2, {'weight': 1})
469 (0, 3, {'weight': 2})
470 (1, 2, {'weight': 3})
471 (1, 3, {'weight': 4})
472 >>> # By default, the weight is the number of shared neighbors
473 >>> G = bipartite.generic_weighted_projected_graph(B, [0, 1])
474 >>> print(list(G.edges(data=True)))
475 [(0, 1, {'weight': 2})]
476 >>> # To specify a custom weight function use the weight_function parameter
477 >>> G = bipartite.generic_weighted_projected_graph(
478 ... B, [0, 1], weight_function=jaccard
479 ... )
480 >>> print(list(G.edges(data=True)))
481 [(0, 1, {'weight': 1.0})]
482 >>> G = bipartite.generic_weighted_projected_graph(
483 ... B, [0, 1], weight_function=my_weight
484 ... )
485 >>> print(list(G.edges(data=True)))
486 [(0, 1, {'weight': 10})]
487
488 Notes
489 -----
490 No attempt is made to verify that the input graph B is bipartite.
491 The graph and node properties are (shallow) copied to the projected graph.
492
493 See :mod:`bipartite documentation <networkx.algorithms.bipartite>`
494 for further details on how bipartite graphs are handled in NetworkX.
495
496 See Also
497 --------
498 is_bipartite,
499 is_bipartite_node_set,
500 sets,
501 weighted_projected_graph,
502 collaboration_weighted_projected_graph,
503 overlap_weighted_projected_graph,
504 projected_graph
505
506 """
507 if B.is_directed():
508 pred = B.pred
509 G = nx.DiGraph()
510 else:
511 pred = B.adj
512 G = nx.Graph()
513 if weight_function is None:
514
515 def weight_function(G, u, v):
516 # Notice that we use set(pred[v]) for handling the directed case.
517 return len(set(G[u]) & set(pred[v]))
518
519 G.graph.update(B.graph)
520 G.add_nodes_from((n, B.nodes[n]) for n in nodes)
521 for u in nodes:
522 nbrs2 = {n for nbr in set(B[u]) for n in B[nbr]} - {u}
523 for v in nbrs2:
524 weight = weight_function(B, u, v)
525 G.add_edge(u, v, weight=weight)
526 return G