smallworld.py 13 KB

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