1"""
2Find the k-cores of a graph.
3
4The k-core is found by recursively pruning nodes with degrees less than k.
5
6See the following references for details:
7
8An O(m) Algorithm for Cores Decomposition of Networks
9Vladimir Batagelj and Matjaz Zaversnik, 2003.
10https://arxiv.org/abs/cs.DS/0310049
11
12Generalized Cores
13Vladimir Batagelj and Matjaz Zaversnik, 2002.
14https://arxiv.org/pdf/cs/0202039
15
16For directed graphs a more general notion is that of D-cores which
17looks at (k, l) restrictions on (in, out) degree. The (k, k) D-core
18is the k-core.
19
20D-cores: Measuring Collaboration of Directed Graphs Based on Degeneracy
21Christos Giatsidis, Dimitrios M. Thilikos, Michalis Vazirgiannis, ICDM 2011.
22http://www.graphdegeneracy.org/dcores_ICDM_2011.pdf
23
24Multi-scale structure and topological anomaly detection via a new network \
25statistic: The onion decomposition
26L. Hébert-Dufresne, J. A. Grochow, and A. Allard
27Scientific Reports 6, 31708 (2016)
28http://doi.org/10.1038/srep31708
29
30"""
31
32import networkx as nx
33
34__all__ = [
35 "core_number",
36 "k_core",
37 "k_shell",
38 "k_crust",
39 "k_corona",
40 "k_truss",
41 "onion_layers",
42]
43
44
45@nx.utils.not_implemented_for("multigraph")
46@nx._dispatchable
47def core_number(G):
48 """Returns the core number for each node.
49
50 A k-core is a maximal subgraph that contains nodes of degree k or more.
51
52 The core number of a node is the largest value k of a k-core containing
53 that node.
54
55 Parameters
56 ----------
57 G : NetworkX graph
58 An undirected or directed graph
59
60 Returns
61 -------
62 core_number : dictionary
63 A dictionary keyed by node to the core number.
64
65 Raises
66 ------
67 NetworkXNotImplemented
68 If `G` is a multigraph or contains self loops.
69
70 Notes
71 -----
72 For directed graphs the node degree is defined to be the
73 in-degree + out-degree.
74
75 Examples
76 --------
77 >>> degrees = [0, 1, 2, 2, 2, 2, 3]
78 >>> H = nx.havel_hakimi_graph(degrees)
79 >>> nx.core_number(H)
80 {0: 1, 1: 2, 2: 2, 3: 2, 4: 1, 5: 2, 6: 0}
81 >>> G = nx.DiGraph()
82 >>> G.add_edges_from([(1, 2), (2, 1), (2, 3), (2, 4), (3, 4), (4, 3)])
83 >>> nx.core_number(G)
84 {1: 2, 2: 2, 3: 2, 4: 2}
85
86 References
87 ----------
88 .. [1] An O(m) Algorithm for Cores Decomposition of Networks
89 Vladimir Batagelj and Matjaz Zaversnik, 2003.
90 https://arxiv.org/abs/cs.DS/0310049
91 """
92 if nx.number_of_selfloops(G) > 0:
93 msg = (
94 "Input graph has self loops which is not permitted; "
95 "Consider using G.remove_edges_from(nx.selfloop_edges(G))."
96 )
97 raise nx.NetworkXNotImplemented(msg)
98 degrees = dict(G.degree())
99 # Sort nodes by degree.
100 nodes = sorted(degrees, key=degrees.get)
101 bin_boundaries = [0]
102 curr_degree = 0
103 for i, v in enumerate(nodes):
104 if degrees[v] > curr_degree:
105 bin_boundaries.extend([i] * (degrees[v] - curr_degree))
106 curr_degree = degrees[v]
107 node_pos = {v: pos for pos, v in enumerate(nodes)}
108 # The initial guess for the core number of a node is its degree.
109 core = degrees
110 nbrs = {v: list(nx.all_neighbors(G, v)) for v in G}
111 for v in nodes:
112 for u in nbrs[v]:
113 if core[u] > core[v]:
114 nbrs[u].remove(v)
115 pos = node_pos[u]
116 bin_start = bin_boundaries[core[u]]
117 node_pos[u] = bin_start
118 node_pos[nodes[bin_start]] = pos
119 nodes[bin_start], nodes[pos] = nodes[pos], nodes[bin_start]
120 bin_boundaries[core[u]] += 1
121 core[u] -= 1
122 return core
123
124
125def _core_subgraph(G, k_filter, k=None, core=None):
126 """Returns the subgraph induced by nodes passing filter `k_filter`.
127
128 Parameters
129 ----------
130 G : NetworkX graph
131 The graph or directed graph to process
132 k_filter : filter function
133 This function filters the nodes chosen. It takes three inputs:
134 A node of G, the filter's cutoff, and the core dict of the graph.
135 The function should return a Boolean value.
136 k : int, optional
137 The order of the core. If not specified use the max core number.
138 This value is used as the cutoff for the filter.
139 core : dict, optional
140 Precomputed core numbers keyed by node for the graph `G`.
141 If not specified, the core numbers will be computed from `G`.
142
143 """
144 if core is None:
145 core = core_number(G)
146 if k is None:
147 k = max(core.values())
148 nodes = (v for v in core if k_filter(v, k, core))
149 return G.subgraph(nodes).copy()
150
151
152@nx.utils.not_implemented_for("multigraph")
153@nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
154def k_core(G, k=None, core_number=None):
155 """Returns the k-core of G.
156
157 A k-core is a maximal subgraph that contains nodes of degree `k` or more.
158
159 Parameters
160 ----------
161 G : NetworkX graph
162 A graph or directed graph
163 k : int, optional
164 The order of the core. If not specified return the main core.
165 core_number : dictionary, optional
166 Precomputed core numbers for the graph G.
167
168 Returns
169 -------
170 G : NetworkX graph
171 The k-core subgraph
172
173 Raises
174 ------
175 NetworkXNotImplemented
176 The k-core is not defined for multigraphs or graphs with self loops.
177
178 Notes
179 -----
180 The main core is the core with `k` as the largest core_number.
181
182 For directed graphs the node degree is defined to be the
183 in-degree + out-degree.
184
185 Graph, node, and edge attributes are copied to the subgraph.
186
187 Examples
188 --------
189 >>> degrees = [0, 1, 2, 2, 2, 2, 3]
190 >>> H = nx.havel_hakimi_graph(degrees)
191 >>> H.degree
192 DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0})
193 >>> nx.k_core(H).nodes
194 NodeView((1, 2, 3, 5))
195
196 See Also
197 --------
198 core_number
199
200 References
201 ----------
202 .. [1] An O(m) Algorithm for Cores Decomposition of Networks
203 Vladimir Batagelj and Matjaz Zaversnik, 2003.
204 https://arxiv.org/abs/cs.DS/0310049
205 """
206
207 def k_filter(v, k, c):
208 return c[v] >= k
209
210 return _core_subgraph(G, k_filter, k, core_number)
211
212
213@nx.utils.not_implemented_for("multigraph")
214@nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
215def k_shell(G, k=None, core_number=None):
216 """Returns the k-shell of G.
217
218 The k-shell is the subgraph induced by nodes with core number k.
219 That is, nodes in the k-core that are not in the (k+1)-core.
220
221 Parameters
222 ----------
223 G : NetworkX graph
224 A graph or directed graph.
225 k : int, optional
226 The order of the shell. If not specified return the outer shell.
227 core_number : dictionary, optional
228 Precomputed core numbers for the graph G.
229
230
231 Returns
232 -------
233 G : NetworkX graph
234 The k-shell subgraph
235
236 Raises
237 ------
238 NetworkXNotImplemented
239 The k-shell is not implemented for multigraphs or graphs with self loops.
240
241 Notes
242 -----
243 This is similar to k_corona but in that case only neighbors in the
244 k-core are considered.
245
246 For directed graphs the node degree is defined to be the
247 in-degree + out-degree.
248
249 Graph, node, and edge attributes are copied to the subgraph.
250
251 Examples
252 --------
253 >>> degrees = [0, 1, 2, 2, 2, 2, 3]
254 >>> H = nx.havel_hakimi_graph(degrees)
255 >>> H.degree
256 DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0})
257 >>> nx.k_shell(H, k=1).nodes
258 NodeView((0, 4))
259
260 See Also
261 --------
262 core_number
263 k_corona
264
265
266 References
267 ----------
268 .. [1] A model of Internet topology using k-shell decomposition
269 Shai Carmi, Shlomo Havlin, Scott Kirkpatrick, Yuval Shavitt,
270 and Eran Shir, PNAS July 3, 2007 vol. 104 no. 27 11150-11154
271 http://www.pnas.org/content/104/27/11150.full
272 """
273
274 def k_filter(v, k, c):
275 return c[v] == k
276
277 return _core_subgraph(G, k_filter, k, core_number)
278
279
280@nx.utils.not_implemented_for("multigraph")
281@nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
282def k_crust(G, k=None, core_number=None):
283 """Returns the k-crust of G.
284
285 The k-crust is the graph G with the edges of the k-core removed
286 and isolated nodes found after the removal of edges are also removed.
287
288 Parameters
289 ----------
290 G : NetworkX graph
291 A graph or directed graph.
292 k : int, optional
293 The order of the shell. If not specified return the main crust.
294 core_number : dictionary, optional
295 Precomputed core numbers for the graph G.
296
297 Returns
298 -------
299 G : NetworkX graph
300 The k-crust subgraph
301
302 Raises
303 ------
304 NetworkXNotImplemented
305 The k-crust is not implemented for multigraphs or graphs with self loops.
306
307 Notes
308 -----
309 This definition of k-crust is different than the definition in [1]_.
310 The k-crust in [1]_ is equivalent to the k+1 crust of this algorithm.
311
312 For directed graphs the node degree is defined to be the
313 in-degree + out-degree.
314
315 Graph, node, and edge attributes are copied to the subgraph.
316
317 Examples
318 --------
319 >>> degrees = [0, 1, 2, 2, 2, 2, 3]
320 >>> H = nx.havel_hakimi_graph(degrees)
321 >>> H.degree
322 DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0})
323 >>> nx.k_crust(H, k=1).nodes
324 NodeView((0, 4, 6))
325
326 See Also
327 --------
328 core_number
329
330 References
331 ----------
332 .. [1] A model of Internet topology using k-shell decomposition
333 Shai Carmi, Shlomo Havlin, Scott Kirkpatrick, Yuval Shavitt,
334 and Eran Shir, PNAS July 3, 2007 vol. 104 no. 27 11150-11154
335 http://www.pnas.org/content/104/27/11150.full
336 """
337 # Default for k is one less than in _core_subgraph, so just inline.
338 # Filter is c[v] <= k
339 if core_number is None:
340 core_number = nx.core_number(G)
341 if k is None:
342 k = max(core_number.values()) - 1
343 nodes = (v for v in core_number if core_number[v] <= k)
344 return G.subgraph(nodes).copy()
345
346
347@nx.utils.not_implemented_for("multigraph")
348@nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
349def k_corona(G, k, core_number=None):
350 """Returns the k-corona of G.
351
352 The k-corona is the subgraph of nodes in the k-core which have
353 exactly k neighbors in the k-core.
354
355 Parameters
356 ----------
357 G : NetworkX graph
358 A graph or directed graph
359 k : int
360 The order of the corona.
361 core_number : dictionary, optional
362 Precomputed core numbers for the graph G.
363
364 Returns
365 -------
366 G : NetworkX graph
367 The k-corona subgraph
368
369 Raises
370 ------
371 NetworkXNotImplemented
372 The k-corona is not defined for multigraphs or graphs with self loops.
373
374 Notes
375 -----
376 For directed graphs the node degree is defined to be the
377 in-degree + out-degree.
378
379 Graph, node, and edge attributes are copied to the subgraph.
380
381 Examples
382 --------
383 >>> degrees = [0, 1, 2, 2, 2, 2, 3]
384 >>> H = nx.havel_hakimi_graph(degrees)
385 >>> H.degree
386 DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0})
387 >>> nx.k_corona(H, k=2).nodes
388 NodeView((1, 2, 3, 5))
389
390 See Also
391 --------
392 core_number
393
394 References
395 ----------
396 .. [1] k -core (bootstrap) percolation on complex networks:
397 Critical phenomena and nonlocal effects,
398 A. V. Goltsev, S. N. Dorogovtsev, and J. F. F. Mendes,
399 Phys. Rev. E 73, 056101 (2006)
400 http://link.aps.org/doi/10.1103/PhysRevE.73.056101
401 """
402
403 def func(v, k, c):
404 return c[v] == k and k == sum(1 for w in G[v] if c[w] >= k)
405
406 return _core_subgraph(G, func, k, core_number)
407
408
409@nx.utils.not_implemented_for("directed")
410@nx.utils.not_implemented_for("multigraph")
411@nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
412def k_truss(G, k):
413 """Returns the k-truss of `G`.
414
415 The k-truss is the maximal induced subgraph of `G` which contains at least
416 three vertices where every edge is incident to at least `k-2` triangles.
417
418 Parameters
419 ----------
420 G : NetworkX graph
421 An undirected graph
422 k : int
423 The order of the truss
424
425 Returns
426 -------
427 H : NetworkX graph
428 The k-truss subgraph
429
430 Raises
431 ------
432 NetworkXNotImplemented
433 If `G` is a multigraph or directed graph or if it contains self loops.
434
435 Notes
436 -----
437 A k-clique is a (k-2)-truss and a k-truss is a (k+1)-core.
438
439 Graph, node, and edge attributes are copied to the subgraph.
440
441 K-trusses were originally defined in [2] which states that the k-truss
442 is the maximal induced subgraph where each edge belongs to at least
443 `k-2` triangles. A more recent paper, [1], uses a slightly different
444 definition requiring that each edge belong to at least `k` triangles.
445 This implementation uses the original definition of `k-2` triangles.
446
447 Examples
448 --------
449 >>> degrees = [0, 1, 2, 2, 2, 2, 3]
450 >>> H = nx.havel_hakimi_graph(degrees)
451 >>> H.degree
452 DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0})
453 >>> nx.k_truss(H, k=2).nodes
454 NodeView((0, 1, 2, 3, 4, 5))
455
456 References
457 ----------
458 .. [1] Bounds and Algorithms for k-truss. Paul Burkhardt, Vance Faber,
459 David G. Harris, 2018. https://arxiv.org/abs/1806.05523v2
460 .. [2] Trusses: Cohesive Subgraphs for Social Network Analysis. Jonathan
461 Cohen, 2005.
462 """
463 if nx.number_of_selfloops(G) > 0:
464 msg = (
465 "Input graph has self loops which is not permitted; "
466 "Consider using G.remove_edges_from(nx.selfloop_edges(G))."
467 )
468 raise nx.NetworkXNotImplemented(msg)
469
470 H = G.copy()
471
472 n_dropped = 1
473 while n_dropped > 0:
474 n_dropped = 0
475 to_drop = []
476 seen = set()
477 for u in H:
478 nbrs_u = set(H[u])
479 seen.add(u)
480 new_nbrs = [v for v in nbrs_u if v not in seen]
481 for v in new_nbrs:
482 if len(nbrs_u & set(H[v])) < (k - 2):
483 to_drop.append((u, v))
484 H.remove_edges_from(to_drop)
485 n_dropped = len(to_drop)
486 H.remove_nodes_from(list(nx.isolates(H)))
487
488 return H
489
490
491@nx.utils.not_implemented_for("multigraph")
492@nx.utils.not_implemented_for("directed")
493@nx._dispatchable
494def onion_layers(G):
495 """Returns the layer of each vertex in an onion decomposition of the graph.
496
497 The onion decomposition refines the k-core decomposition by providing
498 information on the internal organization of each k-shell. It is usually
499 used alongside the `core numbers`.
500
501 Parameters
502 ----------
503 G : NetworkX graph
504 An undirected graph without self loops.
505
506 Returns
507 -------
508 od_layers : dictionary
509 A dictionary keyed by node to the onion layer. The layers are
510 contiguous integers starting at 1.
511
512 Raises
513 ------
514 NetworkXNotImplemented
515 If `G` is a multigraph or directed graph or if it contains self loops.
516
517 Examples
518 --------
519 >>> degrees = [0, 1, 2, 2, 2, 2, 3]
520 >>> H = nx.havel_hakimi_graph(degrees)
521 >>> H.degree
522 DegreeView({0: 1, 1: 2, 2: 2, 3: 2, 4: 2, 5: 3, 6: 0})
523 >>> nx.onion_layers(H)
524 {6: 1, 0: 2, 4: 3, 1: 4, 2: 4, 3: 4, 5: 4}
525
526 See Also
527 --------
528 core_number
529
530 References
531 ----------
532 .. [1] Multi-scale structure and topological anomaly detection via a new
533 network statistic: The onion decomposition
534 L. Hébert-Dufresne, J. A. Grochow, and A. Allard
535 Scientific Reports 6, 31708 (2016)
536 http://doi.org/10.1038/srep31708
537 .. [2] Percolation and the effective structure of complex networks
538 A. Allard and L. Hébert-Dufresne
539 Physical Review X 9, 011023 (2019)
540 http://doi.org/10.1103/PhysRevX.9.011023
541 """
542 if nx.number_of_selfloops(G) > 0:
543 msg = (
544 "Input graph contains self loops which is not permitted; "
545 "Consider using G.remove_edges_from(nx.selfloop_edges(G))."
546 )
547 raise nx.NetworkXNotImplemented(msg)
548 # Dictionaries to register the k-core/onion decompositions.
549 od_layers = {}
550 # Adjacency list
551 neighbors = {v: list(nx.all_neighbors(G, v)) for v in G}
552 # Effective degree of nodes.
553 degrees = dict(G.degree())
554 # Performs the onion decomposition.
555 current_core = 1
556 current_layer = 1
557 # Sets vertices of degree 0 to layer 1, if any.
558 isolated_nodes = list(nx.isolates(G))
559 if len(isolated_nodes) > 0:
560 for v in isolated_nodes:
561 od_layers[v] = current_layer
562 degrees.pop(v)
563 current_layer = 2
564 # Finds the layer for the remaining nodes.
565 while len(degrees) > 0:
566 # Sets the order for looking at nodes.
567 nodes = sorted(degrees, key=degrees.get)
568 # Sets properly the current core.
569 min_degree = degrees[nodes[0]]
570 if min_degree > current_core:
571 current_core = min_degree
572 # Identifies vertices in the current layer.
573 this_layer = []
574 for n in nodes:
575 if degrees[n] > current_core:
576 break
577 this_layer.append(n)
578 # Identifies the core/layer of the vertices in the current layer.
579 for v in this_layer:
580 od_layers[v] = current_layer
581 for n in neighbors[v]:
582 neighbors[n].remove(v)
583 degrees[n] = degrees[n] - 1
584 degrees.pop(v)
585 # Updates the layer count.
586 current_layer = current_layer + 1
587 # Returns the dictionaries containing the onion layer of each vertices.
588 return od_layers