smallworld.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. """Functions for estimating the small-world-ness of graphs.
  2. A small world network is characterized by a small average shortest path length,
  3. and a large clustering coefficient.
  4. Small-worldness is commonly measured with the coefficient sigma or omega.
  5. Both coefficients compare the average clustering coefficient and shortest path
  6. length of a given graph against the same quantities for an equivalent random
  7. or lattice graph.
  8. For more information, see the Wikipedia article on small-world network [1]_.
  9. .. [1] Small-world network:: https://en.wikipedia.org/wiki/Small-world_network
  10. """
  11. import networkx as nx
  12. from networkx.utils import not_implemented_for, py_random_state
  13. __all__ = ["random_reference", "lattice_reference", "sigma", "omega"]
  14. @not_implemented_for("directed")
  15. @not_implemented_for("multigraph")
  16. @py_random_state(3)
  17. @nx._dispatchable(returns_graph=True)
  18. def random_reference(G, niter=1, connectivity=True, seed=None):
  19. """Compute a random graph by swapping edges of a given graph.
  20. Parameters
  21. ----------
  22. G : graph
  23. An undirected graph with 4 or more nodes.
  24. niter : integer (optional, default=1)
  25. An edge is rewired approximately `niter` times.
  26. connectivity : boolean (optional, default=True)
  27. When True, ensure connectivity for the randomized graph.
  28. seed : integer, random_state, or None (default)
  29. Indicator of random number generation state.
  30. See :ref:`Randomness<randomness>`.
  31. Returns
  32. -------
  33. G : graph
  34. The randomized graph.
  35. Raises
  36. ------
  37. NetworkXError
  38. If there are fewer than 4 nodes or 2 edges in `G`
  39. Notes
  40. -----
  41. The implementation is adapted from the algorithm by Maslov and Sneppen
  42. (2002) [1]_.
  43. References
  44. ----------
  45. .. [1] Maslov, Sergei, and Kim Sneppen.
  46. "Specificity and stability in topology of protein networks."
  47. Science 296.5569 (2002): 910-913.
  48. """
  49. if len(G) < 4:
  50. raise nx.NetworkXError("Graph has fewer than four nodes.")
  51. if len(G.edges) < 2:
  52. raise nx.NetworkXError("Graph has fewer that 2 edges")
  53. from networkx.utils import cumulative_distribution, discrete_sequence
  54. local_conn = nx.connectivity.local_edge_connectivity
  55. G = G.copy()
  56. keys, degrees = zip(*G.degree()) # keys, degree
  57. cdf = cumulative_distribution(degrees) # cdf of degree
  58. nnodes = len(G)
  59. nedges = nx.number_of_edges(G)
  60. niter = niter * nedges
  61. ntries = int(nnodes * nedges / (nnodes * (nnodes - 1) / 2))
  62. swapcount = 0
  63. for i in range(niter):
  64. n = 0
  65. while n < ntries:
  66. # pick two random edges without creating edge list
  67. # choose source node indices from discrete distribution
  68. (ai, ci) = discrete_sequence(2, cdistribution=cdf, seed=seed)
  69. if ai == ci:
  70. continue # same source, skip
  71. a = keys[ai] # convert index to label
  72. c = keys[ci]
  73. # choose target uniformly from neighbors
  74. b = seed.choice(list(G.neighbors(a)))
  75. d = seed.choice(list(G.neighbors(c)))
  76. if b in [a, c, d] or d in [a, b, c]:
  77. continue # all vertices should be different
  78. # don't create parallel edges
  79. if (d not in G[a]) and (b not in G[c]):
  80. G.add_edge(a, d)
  81. G.add_edge(c, b)
  82. G.remove_edge(a, b)
  83. G.remove_edge(c, d)
  84. # Check if the graph is still connected
  85. if connectivity and local_conn(G, a, b) == 0:
  86. # Not connected, revert the swap
  87. G.remove_edge(a, d)
  88. G.remove_edge(c, b)
  89. G.add_edge(a, b)
  90. G.add_edge(c, d)
  91. else:
  92. swapcount += 1
  93. break
  94. n += 1
  95. return G
  96. @not_implemented_for("directed")
  97. @not_implemented_for("multigraph")
  98. @py_random_state(4)
  99. @nx._dispatchable(returns_graph=True)
  100. def lattice_reference(G, niter=5, D=None, connectivity=True, seed=None):
  101. """Latticize the given graph by swapping edges.
  102. Parameters
  103. ----------
  104. G : graph
  105. An undirected graph.
  106. niter : integer (optional, default=1)
  107. An edge is rewired approximately niter times.
  108. D : numpy.array (optional, default=None)
  109. Distance to the diagonal matrix.
  110. connectivity : boolean (optional, default=True)
  111. Ensure connectivity for the latticized graph when set to True.
  112. seed : integer, random_state, or None (default)
  113. Indicator of random number generation state.
  114. See :ref:`Randomness<randomness>`.
  115. Returns
  116. -------
  117. G : graph
  118. The latticized graph.
  119. Raises
  120. ------
  121. NetworkXError
  122. If there are fewer than 4 nodes or 2 edges in `G`
  123. Notes
  124. -----
  125. The implementation is adapted from the algorithm by Sporns et al. [1]_.
  126. which is inspired from the original work by Maslov and Sneppen(2002) [2]_.
  127. References
  128. ----------
  129. .. [1] Sporns, Olaf, and Jonathan D. Zwi.
  130. "The small world of the cerebral cortex."
  131. Neuroinformatics 2.2 (2004): 145-162.
  132. .. [2] Maslov, Sergei, and Kim Sneppen.
  133. "Specificity and stability in topology of protein networks."
  134. Science 296.5569 (2002): 910-913.
  135. """
  136. import numpy as np
  137. from networkx.utils import cumulative_distribution, discrete_sequence
  138. local_conn = nx.connectivity.local_edge_connectivity
  139. if len(G) < 4:
  140. raise nx.NetworkXError("Graph has fewer than four nodes.")
  141. if len(G.edges) < 2:
  142. raise nx.NetworkXError("Graph has fewer that 2 edges")
  143. # Instead of choosing uniformly at random from a generated edge list,
  144. # this algorithm chooses nonuniformly from the set of nodes with
  145. # probability weighted by degree.
  146. G = G.copy()
  147. keys, degrees = zip(*G.degree()) # keys, degree
  148. cdf = cumulative_distribution(degrees) # cdf of degree
  149. nnodes = len(G)
  150. nedges = nx.number_of_edges(G)
  151. if D is None:
  152. D = np.zeros((nnodes, nnodes))
  153. un = np.arange(1, nnodes)
  154. um = np.arange(nnodes - 1, 0, -1)
  155. u = np.append((0,), np.where(un < um, un, um))
  156. for v in range(int(np.ceil(nnodes / 2))):
  157. D[nnodes - v - 1, :] = np.append(u[v + 1 :], u[: v + 1])
  158. D[v, :] = D[nnodes - v - 1, :][::-1]
  159. niter = niter * nedges
  160. # maximal number of rewiring attempts per 'niter'
  161. max_attempts = int(nnodes * nedges / (nnodes * (nnodes - 1) / 2))
  162. for _ in range(niter):
  163. n = 0
  164. while n < max_attempts:
  165. # pick two random edges without creating edge list
  166. # choose source node indices from discrete distribution
  167. (ai, ci) = discrete_sequence(2, cdistribution=cdf, seed=seed)
  168. if ai == ci:
  169. continue # same source, skip
  170. a = keys[ai] # convert index to label
  171. c = keys[ci]
  172. # choose target uniformly from neighbors
  173. b = seed.choice(list(G.neighbors(a)))
  174. d = seed.choice(list(G.neighbors(c)))
  175. bi = keys.index(b)
  176. di = keys.index(d)
  177. if b in [a, c, d] or d in [a, b, c]:
  178. continue # all vertices should be different
  179. # don't create parallel edges
  180. if (d not in G[a]) and (b not in G[c]):
  181. if D[ai, bi] + D[ci, di] >= D[ai, ci] + D[bi, di]:
  182. # only swap if we get closer to the diagonal
  183. G.add_edge(a, d)
  184. G.add_edge(c, b)
  185. G.remove_edge(a, b)
  186. G.remove_edge(c, d)
  187. # Check if the graph is still connected
  188. if connectivity and local_conn(G, a, b) == 0:
  189. # Not connected, revert the swap
  190. G.remove_edge(a, d)
  191. G.remove_edge(c, b)
  192. G.add_edge(a, b)
  193. G.add_edge(c, d)
  194. else:
  195. break
  196. n += 1
  197. return G
  198. @not_implemented_for("directed")
  199. @not_implemented_for("multigraph")
  200. @py_random_state(3)
  201. @nx._dispatchable
  202. def sigma(G, niter=100, nrand=10, seed=None):
  203. """Returns the small-world coefficient (sigma) of the given graph.
  204. The small-world coefficient is defined as:
  205. sigma = C/Cr / L/Lr
  206. where C and L are respectively the average clustering coefficient and
  207. average shortest path length of G. Cr and Lr are respectively the average
  208. clustering coefficient and average shortest path length of an equivalent
  209. random graph.
  210. A graph is commonly classified as small-world if sigma>1.
  211. Parameters
  212. ----------
  213. G : NetworkX graph
  214. An undirected graph.
  215. niter : integer (optional, default=100)
  216. Approximate number of rewiring per edge to compute the equivalent
  217. random graph.
  218. nrand : integer (optional, default=10)
  219. Number of random graphs generated to compute the average clustering
  220. coefficient (Cr) and average shortest path length (Lr).
  221. seed : integer, random_state, or None (default)
  222. Indicator of random number generation state.
  223. See :ref:`Randomness<randomness>`.
  224. Returns
  225. -------
  226. sigma : float
  227. The small-world coefficient of G.
  228. Notes
  229. -----
  230. The implementation is adapted from Humphries et al. [1]_ [2]_.
  231. References
  232. ----------
  233. .. [1] The brainstem reticular formation is a small-world, not scale-free,
  234. network M. D. Humphries, K. Gurney and T. J. Prescott,
  235. Proc. Roy. Soc. B 2006 273, 503-511, doi:10.1098/rspb.2005.3354.
  236. .. [2] Humphries and Gurney (2008).
  237. "Network 'Small-World-Ness': A Quantitative Method for Determining
  238. Canonical Network Equivalence".
  239. PLoS One. 3 (4). PMID 18446219. doi:10.1371/journal.pone.0002051.
  240. """
  241. import numpy as np
  242. # Compute the mean clustering coefficient and average shortest path length
  243. # for an equivalent random graph
  244. randMetrics = {"C": [], "L": []}
  245. for i in range(nrand):
  246. Gr = random_reference(G, niter=niter, seed=seed)
  247. randMetrics["C"].append(nx.transitivity(Gr))
  248. randMetrics["L"].append(nx.average_shortest_path_length(Gr))
  249. C = nx.transitivity(G)
  250. L = nx.average_shortest_path_length(G)
  251. Cr = np.mean(randMetrics["C"])
  252. Lr = np.mean(randMetrics["L"])
  253. sigma = (C / Cr) / (L / Lr)
  254. return float(sigma)
  255. @not_implemented_for("directed")
  256. @not_implemented_for("multigraph")
  257. @py_random_state(3)
  258. @nx._dispatchable
  259. def omega(G, niter=5, nrand=10, seed=None):
  260. """Returns the small-world coefficient (omega) of a graph
  261. The small-world coefficient of a graph G is:
  262. omega = Lr/L - C/Cl
  263. where C and L are respectively the average clustering coefficient and
  264. average shortest path length of G. Lr is the average shortest path length
  265. of an equivalent random graph and Cl is the average clustering coefficient
  266. of an equivalent lattice graph.
  267. The small-world coefficient (omega) measures how much G is like a lattice
  268. or a random graph. Negative values mean G is similar to a lattice whereas
  269. positive values mean G is a random graph.
  270. Values close to 0 mean that G has small-world characteristics.
  271. Parameters
  272. ----------
  273. G : NetworkX graph
  274. An undirected graph.
  275. niter: integer (optional, default=5)
  276. Approximate number of rewiring per edge to compute the equivalent
  277. random graph.
  278. nrand: integer (optional, default=10)
  279. Number of random graphs generated to compute the maximal clustering
  280. coefficient (Cr) and average shortest path length (Lr).
  281. seed : integer, random_state, or None (default)
  282. Indicator of random number generation state.
  283. See :ref:`Randomness<randomness>`.
  284. Returns
  285. -------
  286. omega : float
  287. The small-world coefficient (omega)
  288. Notes
  289. -----
  290. The implementation is adapted from the algorithm by Telesford et al. [1]_.
  291. References
  292. ----------
  293. .. [1] Telesford, Joyce, Hayasaka, Burdette, and Laurienti (2011).
  294. "The Ubiquity of Small-World Networks".
  295. Brain Connectivity. 1 (0038): 367-75. PMC 3604768. PMID 22432451.
  296. doi:10.1089/brain.2011.0038.
  297. """
  298. import numpy as np
  299. # Compute the mean clustering coefficient and average shortest path length
  300. # for an equivalent random graph
  301. randMetrics = {"C": [], "L": []}
  302. # Calculate initial average clustering coefficient which potentially will
  303. # get replaced by higher clustering coefficients from generated lattice
  304. # reference graphs
  305. Cl = nx.average_clustering(G)
  306. niter_lattice_reference = niter
  307. niter_random_reference = niter * 2
  308. for _ in range(nrand):
  309. # Generate random graph
  310. Gr = random_reference(G, niter=niter_random_reference, seed=seed)
  311. randMetrics["L"].append(nx.average_shortest_path_length(Gr))
  312. # Generate lattice graph
  313. Gl = lattice_reference(G, niter=niter_lattice_reference, seed=seed)
  314. # Replace old clustering coefficient, if clustering is higher in
  315. # generated lattice reference
  316. Cl_temp = nx.average_clustering(Gl)
  317. if Cl_temp > Cl:
  318. Cl = Cl_temp
  319. C = nx.average_clustering(G)
  320. L = nx.average_shortest_path_length(G)
  321. Lr = np.mean(randMetrics["L"])
  322. omega = (Lr / L) - (C / Cl)
  323. return float(omega)