relabel.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import networkx as nx
  2. __all__ = ["convert_node_labels_to_integers", "relabel_nodes"]
  3. @nx._dispatchable(
  4. preserve_all_attrs=True, mutates_input={"not copy": 2}, returns_graph=True
  5. )
  6. def relabel_nodes(G, mapping, copy=True):
  7. """Relabel the nodes of the graph G according to a given mapping.
  8. The original node ordering may not be preserved if `copy` is `False` and the
  9. mapping includes overlap between old and new labels.
  10. Parameters
  11. ----------
  12. G : graph
  13. A NetworkX graph
  14. mapping : dictionary
  15. A dictionary with the old labels as keys and new labels as values.
  16. A partial mapping is allowed. Mapping 2 nodes to a single node is allowed.
  17. Any non-node keys in the mapping are ignored.
  18. copy : bool (optional, default=True)
  19. If True return a copy, or if False relabel the nodes in place.
  20. Examples
  21. --------
  22. To create a new graph with nodes relabeled according to a given
  23. dictionary:
  24. >>> G = nx.path_graph(3)
  25. >>> sorted(G)
  26. [0, 1, 2]
  27. >>> mapping = {0: "a", 1: "b", 2: "c"}
  28. >>> H = nx.relabel_nodes(G, mapping)
  29. >>> sorted(H)
  30. ['a', 'b', 'c']
  31. Nodes can be relabeled with any hashable object, including numbers
  32. and strings:
  33. >>> import string
  34. >>> G = nx.path_graph(26) # nodes are integers 0 through 25
  35. >>> sorted(G)[:3]
  36. [0, 1, 2]
  37. >>> mapping = dict(zip(G, string.ascii_lowercase))
  38. >>> G = nx.relabel_nodes(G, mapping) # nodes are characters a through z
  39. >>> sorted(G)[:3]
  40. ['a', 'b', 'c']
  41. >>> mapping = dict(zip(G, range(1, 27)))
  42. >>> G = nx.relabel_nodes(G, mapping) # nodes are integers 1 through 26
  43. >>> sorted(G)[:3]
  44. [1, 2, 3]
  45. To perform a partial in-place relabeling, provide a dictionary
  46. mapping only a subset of the nodes, and set the `copy` keyword
  47. argument to False:
  48. >>> G = nx.path_graph(3) # nodes 0-1-2
  49. >>> mapping = {0: "a", 1: "b"} # 0->'a' and 1->'b'
  50. >>> G = nx.relabel_nodes(G, mapping, copy=False)
  51. >>> sorted(G, key=str)
  52. [2, 'a', 'b']
  53. A mapping can also be given as a function:
  54. >>> G = nx.path_graph(3)
  55. >>> H = nx.relabel_nodes(G, lambda x: x**2)
  56. >>> list(H)
  57. [0, 1, 4]
  58. In a multigraph, relabeling two or more nodes to the same new node
  59. will retain all edges, but may change the edge keys in the process:
  60. >>> G = nx.MultiGraph()
  61. >>> G.add_edge(0, 1, value="a") # returns the key for this edge
  62. 0
  63. >>> G.add_edge(0, 2, value="b")
  64. 0
  65. >>> G.add_edge(0, 3, value="c")
  66. 0
  67. >>> mapping = {1: 4, 2: 4, 3: 4}
  68. >>> H = nx.relabel_nodes(G, mapping, copy=True)
  69. >>> print(H[0])
  70. {4: {0: {'value': 'a'}, 1: {'value': 'b'}, 2: {'value': 'c'}}}
  71. This works for in-place relabeling too:
  72. >>> G = nx.relabel_nodes(G, mapping, copy=False)
  73. >>> print(G[0])
  74. {4: {0: {'value': 'a'}, 1: {'value': 'b'}, 2: {'value': 'c'}}}
  75. Notes
  76. -----
  77. Only the nodes specified in the mapping will be relabeled.
  78. Any non-node keys in the mapping are ignored.
  79. The keyword setting copy=False modifies the graph in place.
  80. Relabel_nodes avoids naming collisions by building a
  81. directed graph from ``mapping`` which specifies the order of
  82. relabelings. Naming collisions, such as a->b, b->c, are ordered
  83. such that "b" gets renamed to "c" before "a" gets renamed "b".
  84. In cases of circular mappings (e.g. a->b, b->a), modifying the
  85. graph is not possible in-place and an exception is raised.
  86. In that case, use copy=True.
  87. If a relabel operation on a multigraph would cause two or more
  88. edges to have the same source, target and key, the second edge must
  89. be assigned a new key to retain all edges. The new key is set
  90. to the lowest non-negative integer not already used as a key
  91. for edges between these two nodes. Note that this means non-numeric
  92. keys may be replaced by numeric keys.
  93. See Also
  94. --------
  95. convert_node_labels_to_integers
  96. """
  97. # you can pass any callable e.g. f(old_label) -> new_label or
  98. # e.g. str(old_label) -> new_label, but we'll just make a dictionary here regardless
  99. m = {n: mapping(n) for n in G} if callable(mapping) else mapping
  100. if copy:
  101. return _relabel_copy(G, m)
  102. else:
  103. return _relabel_inplace(G, m)
  104. def _relabel_inplace(G, mapping):
  105. if len(mapping.keys() & mapping.values()) > 0:
  106. # labels sets overlap
  107. # can we topological sort and still do the relabeling?
  108. D = nx.DiGraph(list(mapping.items()))
  109. D.remove_edges_from(nx.selfloop_edges(D))
  110. try:
  111. nodes = reversed(list(nx.topological_sort(D)))
  112. except nx.NetworkXUnfeasible as err:
  113. raise nx.NetworkXUnfeasible(
  114. "The node label sets are overlapping and no ordering can "
  115. "resolve the mapping. Use copy=True."
  116. ) from err
  117. else:
  118. # non-overlapping label sets, sort them in the order of G nodes
  119. nodes = [n for n in G if n in mapping]
  120. multigraph = G.is_multigraph()
  121. directed = G.is_directed()
  122. for old in nodes:
  123. # Test that old is in both mapping and G, otherwise ignore.
  124. try:
  125. new = mapping[old]
  126. G.add_node(new, **G.nodes[old])
  127. except KeyError:
  128. continue
  129. if new == old:
  130. continue
  131. if multigraph:
  132. new_edges = [
  133. (new, new if old == target else target, key, data)
  134. for (_, target, key, data) in G.edges(old, data=True, keys=True)
  135. ]
  136. if directed:
  137. new_edges += [
  138. (new if old == source else source, new, key, data)
  139. for (source, _, key, data) in G.in_edges(old, data=True, keys=True)
  140. ]
  141. # Ensure new edges won't overwrite existing ones
  142. seen = set()
  143. for i, (source, target, key, data) in enumerate(new_edges):
  144. if target in G[source] and key in G[source][target]:
  145. new_key = 0 if not isinstance(key, int | float) else key
  146. while new_key in G[source][target] or (target, new_key) in seen:
  147. new_key += 1
  148. new_edges[i] = (source, target, new_key, data)
  149. seen.add((target, new_key))
  150. else:
  151. new_edges = [
  152. (new, new if old == target else target, data)
  153. for (_, target, data) in G.edges(old, data=True)
  154. ]
  155. if directed:
  156. new_edges += [
  157. (new if old == source else source, new, data)
  158. for (source, _, data) in G.in_edges(old, data=True)
  159. ]
  160. G.remove_node(old)
  161. G.add_edges_from(new_edges)
  162. return G
  163. def _relabel_copy(G, mapping):
  164. H = G.__class__()
  165. H.add_nodes_from(mapping.get(n, n) for n in G)
  166. H._node.update((mapping.get(n, n), d.copy()) for n, d in G.nodes.items())
  167. if G.is_multigraph():
  168. new_edges = [
  169. (mapping.get(n1, n1), mapping.get(n2, n2), k, d.copy())
  170. for (n1, n2, k, d) in G.edges(keys=True, data=True)
  171. ]
  172. # check for conflicting edge-keys
  173. undirected = not G.is_directed()
  174. seen_edges = set()
  175. for i, (source, target, key, data) in enumerate(new_edges):
  176. while (source, target, key) in seen_edges:
  177. if not isinstance(key, int | float):
  178. key = 0
  179. key += 1
  180. seen_edges.add((source, target, key))
  181. if undirected:
  182. seen_edges.add((target, source, key))
  183. new_edges[i] = (source, target, key, data)
  184. H.add_edges_from(new_edges)
  185. else:
  186. H.add_edges_from(
  187. (mapping.get(n1, n1), mapping.get(n2, n2), d.copy())
  188. for (n1, n2, d) in G.edges(data=True)
  189. )
  190. H.graph.update(G.graph)
  191. return H
  192. @nx._dispatchable(preserve_all_attrs=True, returns_graph=True)
  193. def convert_node_labels_to_integers(
  194. G, first_label=0, ordering="default", label_attribute=None
  195. ):
  196. """Returns a copy of the graph G with the nodes relabeled using
  197. consecutive integers.
  198. Parameters
  199. ----------
  200. G : graph
  201. A NetworkX graph
  202. first_label : int, optional (default=0)
  203. An integer specifying the starting offset in numbering nodes.
  204. The new integer labels are numbered first_label, ..., n-1+first_label.
  205. ordering : string
  206. "default" : inherit node ordering from G.nodes()
  207. "sorted" : inherit node ordering from sorted(G.nodes())
  208. "increasing degree" : nodes are sorted by increasing degree
  209. "decreasing degree" : nodes are sorted by decreasing degree
  210. label_attribute : string, optional (default=None)
  211. Name of node attribute to store old label. If None no attribute
  212. is created.
  213. Notes
  214. -----
  215. Node and edge attribute data are copied to the new (relabeled) graph.
  216. There is no guarantee that the relabeling of nodes to integers will
  217. give the same two integers for two (even identical graphs).
  218. Use the `ordering` argument to try to preserve the order.
  219. See Also
  220. --------
  221. relabel_nodes
  222. """
  223. N = G.number_of_nodes() + first_label
  224. if ordering == "default":
  225. mapping = dict(zip(G.nodes(), range(first_label, N)))
  226. elif ordering == "sorted":
  227. nlist = sorted(G.nodes())
  228. mapping = dict(zip(nlist, range(first_label, N)))
  229. elif ordering == "increasing degree":
  230. dv_pairs = [(d, n) for (n, d) in G.degree()]
  231. dv_pairs.sort() # in-place sort from lowest to highest degree
  232. mapping = dict(zip([n for d, n in dv_pairs], range(first_label, N)))
  233. elif ordering == "decreasing degree":
  234. dv_pairs = [(d, n) for (n, d) in G.degree()]
  235. dv_pairs.sort() # in-place sort from lowest to highest degree
  236. dv_pairs.reverse()
  237. mapping = dict(zip([n for d, n in dv_pairs], range(first_label, N)))
  238. else:
  239. raise nx.NetworkXError(f"Unknown node ordering: {ordering}")
  240. H = relabel_nodes(G, mapping)
  241. # create node attribute with the old label
  242. if label_attribute is not None:
  243. nx.set_node_attributes(H, {v: k for k, v in mapping.items()}, label_attribute)
  244. return H