laplacianmatrix.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. """Laplacian matrix of graphs.
  2. All calculations here are done using the out-degree. For Laplacians using
  3. in-degree, use `G.reverse(copy=False)` instead of `G` and take the transpose.
  4. The `laplacian_matrix` function provides an unnormalized matrix,
  5. while `normalized_laplacian_matrix`, `directed_laplacian_matrix`,
  6. and `directed_combinatorial_laplacian_matrix` are all normalized.
  7. """
  8. import networkx as nx
  9. from networkx.utils import not_implemented_for
  10. __all__ = [
  11. "laplacian_matrix",
  12. "normalized_laplacian_matrix",
  13. "directed_laplacian_matrix",
  14. "directed_combinatorial_laplacian_matrix",
  15. ]
  16. @nx._dispatchable(edge_attrs="weight")
  17. def laplacian_matrix(G, nodelist=None, weight="weight"):
  18. """Returns the Laplacian matrix of G.
  19. The graph Laplacian is the matrix L = D - A, where
  20. A is the adjacency matrix and D is the diagonal matrix of node degrees.
  21. Parameters
  22. ----------
  23. G : graph
  24. A NetworkX graph
  25. nodelist : list, optional
  26. The rows and columns are ordered according to the nodes in nodelist.
  27. If nodelist is None, then the ordering is produced by G.nodes().
  28. weight : string or None, optional (default='weight')
  29. The edge data key used to compute each value in the matrix.
  30. If None, then each edge has weight 1.
  31. Returns
  32. -------
  33. L : SciPy sparse array
  34. The Laplacian matrix of G.
  35. Notes
  36. -----
  37. For MultiGraph, the edges weights are summed.
  38. This returns an unnormalized matrix. For a normalized output,
  39. use `normalized_laplacian_matrix`, `directed_laplacian_matrix`,
  40. or `directed_combinatorial_laplacian_matrix`.
  41. This calculation uses the out-degree of the graph `G`. To use the
  42. in-degree for calculations instead, use `G.reverse(copy=False)` and
  43. take the transpose.
  44. See Also
  45. --------
  46. :func:`~networkx.convert_matrix.to_numpy_array`
  47. normalized_laplacian_matrix
  48. directed_laplacian_matrix
  49. directed_combinatorial_laplacian_matrix
  50. :func:`~networkx.linalg.spectrum.laplacian_spectrum`
  51. Examples
  52. --------
  53. For graphs with multiple connected components, L is permutation-similar
  54. to a block diagonal matrix where each block is the respective Laplacian
  55. matrix for each component.
  56. >>> G = nx.Graph([(1, 2), (2, 3), (4, 5)])
  57. >>> print(nx.laplacian_matrix(G).toarray())
  58. [[ 1 -1 0 0 0]
  59. [-1 2 -1 0 0]
  60. [ 0 -1 1 0 0]
  61. [ 0 0 0 1 -1]
  62. [ 0 0 0 -1 1]]
  63. >>> edges = [
  64. ... (1, 2),
  65. ... (2, 1),
  66. ... (2, 4),
  67. ... (4, 3),
  68. ... (3, 4),
  69. ... ]
  70. >>> DiG = nx.DiGraph(edges)
  71. >>> print(nx.laplacian_matrix(DiG).toarray())
  72. [[ 1 -1 0 0]
  73. [-1 2 -1 0]
  74. [ 0 0 1 -1]
  75. [ 0 0 -1 1]]
  76. Notice that node 4 is represented by the third column and row. This is because
  77. by default the row/column order is the order of `G.nodes` (i.e. the node added
  78. order -- in the edgelist, 4 first appears in (2, 4), before node 3 in edge (4, 3).)
  79. To control the node order of the matrix, use the `nodelist` argument.
  80. >>> print(nx.laplacian_matrix(DiG, nodelist=[1, 2, 3, 4]).toarray())
  81. [[ 1 -1 0 0]
  82. [-1 2 0 -1]
  83. [ 0 0 1 -1]
  84. [ 0 0 -1 1]]
  85. This calculation uses the out-degree of the graph `G`. To use the
  86. in-degree for calculations instead, use `G.reverse(copy=False)` and
  87. take the transpose.
  88. >>> print(nx.laplacian_matrix(DiG.reverse(copy=False)).toarray().T)
  89. [[ 1 -1 0 0]
  90. [-1 1 -1 0]
  91. [ 0 0 2 -1]
  92. [ 0 0 -1 1]]
  93. References
  94. ----------
  95. .. [1] Langville, Amy N., and Carl D. Meyer. Google’s PageRank and Beyond:
  96. The Science of Search Engine Rankings. Princeton University Press, 2006.
  97. """
  98. import scipy as sp
  99. if nodelist is None:
  100. nodelist = list(G)
  101. A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, format="csr")
  102. n, m = A.shape
  103. D = sp.sparse.dia_array((A.sum(axis=1), 0), shape=(m, n)).tocsr()
  104. return D - A
  105. @nx._dispatchable(edge_attrs="weight")
  106. def normalized_laplacian_matrix(G, nodelist=None, weight="weight"):
  107. r"""Returns the normalized Laplacian matrix of G.
  108. The normalized graph Laplacian is the matrix
  109. .. math::
  110. N = D^{-1/2} L D^{-1/2}
  111. where `L` is the graph Laplacian and `D` is the diagonal matrix of
  112. node degrees [1]_.
  113. Parameters
  114. ----------
  115. G : graph
  116. A NetworkX graph
  117. nodelist : list, optional
  118. The rows and columns are ordered according to the nodes in nodelist.
  119. If nodelist is None, then the ordering is produced by G.nodes().
  120. weight : string or None, optional (default='weight')
  121. The edge data key used to compute each value in the matrix.
  122. If None, then each edge has weight 1.
  123. Returns
  124. -------
  125. N : SciPy sparse array
  126. The normalized Laplacian matrix of G.
  127. Notes
  128. -----
  129. For MultiGraph, the edges weights are summed.
  130. See :func:`to_numpy_array` for other options.
  131. If the Graph contains selfloops, D is defined as ``diag(sum(A, 1))``, where A is
  132. the adjacency matrix [2]_.
  133. This calculation uses the out-degree of the graph `G`. To use the
  134. in-degree for calculations instead, use `G.reverse(copy=False)` and
  135. take the transpose.
  136. For an unnormalized output, use `laplacian_matrix`.
  137. Examples
  138. --------
  139. >>> import numpy as np
  140. >>> edges = [
  141. ... (1, 2),
  142. ... (2, 1),
  143. ... (2, 4),
  144. ... (4, 3),
  145. ... (3, 4),
  146. ... ]
  147. >>> DiG = nx.DiGraph(edges)
  148. >>> print(nx.normalized_laplacian_matrix(DiG).toarray())
  149. [[ 1. -0.70710678 0. 0. ]
  150. [-0.70710678 1. -0.70710678 0. ]
  151. [ 0. 0. 1. -1. ]
  152. [ 0. 0. -1. 1. ]]
  153. Notice that node 4 is represented by the third column and row. This is because
  154. by default the row/column order is the order of `G.nodes` (i.e. the node added
  155. order -- in the edgelist, 4 first appears in (2, 4), before node 3 in edge (4, 3).)
  156. To control the node order of the matrix, use the `nodelist` argument.
  157. >>> print(nx.normalized_laplacian_matrix(DiG, nodelist=[1, 2, 3, 4]).toarray())
  158. [[ 1. -0.70710678 0. 0. ]
  159. [-0.70710678 1. 0. -0.70710678]
  160. [ 0. 0. 1. -1. ]
  161. [ 0. 0. -1. 1. ]]
  162. >>> G = nx.Graph(edges)
  163. >>> print(nx.normalized_laplacian_matrix(G).toarray())
  164. [[ 1. -0.70710678 0. 0. ]
  165. [-0.70710678 1. -0.5 0. ]
  166. [ 0. -0.5 1. -0.70710678]
  167. [ 0. 0. -0.70710678 1. ]]
  168. See Also
  169. --------
  170. laplacian_matrix
  171. normalized_laplacian_spectrum
  172. directed_laplacian_matrix
  173. directed_combinatorial_laplacian_matrix
  174. References
  175. ----------
  176. .. [1] Fan Chung-Graham, Spectral Graph Theory,
  177. CBMS Regional Conference Series in Mathematics, Number 92, 1997.
  178. .. [2] Steve Butler, Interlacing For Weighted Graphs Using The Normalized
  179. Laplacian, Electronic Journal of Linear Algebra, Volume 16, pp. 90-98,
  180. March 2007.
  181. .. [3] Langville, Amy N., and Carl D. Meyer. Google’s PageRank and Beyond:
  182. The Science of Search Engine Rankings. Princeton University Press, 2006.
  183. """
  184. import numpy as np
  185. import scipy as sp
  186. if nodelist is None:
  187. nodelist = list(G)
  188. A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, format="csr")
  189. n, _ = A.shape
  190. diags = A.sum(axis=1)
  191. D = sp.sparse.dia_array((diags, 0), shape=(n, n)).tocsr()
  192. L = D - A
  193. with np.errstate(divide="ignore"):
  194. diags_sqrt = 1.0 / np.sqrt(diags)
  195. diags_sqrt[np.isinf(diags_sqrt)] = 0
  196. DH = sp.sparse.dia_array((diags_sqrt, 0), shape=(n, n)).tocsr()
  197. return DH @ (L @ DH)
  198. ###############################################################################
  199. # Code based on work from https://github.com/bjedwards
  200. @not_implemented_for("undirected")
  201. @not_implemented_for("multigraph")
  202. @nx._dispatchable(edge_attrs="weight")
  203. def directed_laplacian_matrix(
  204. G, nodelist=None, weight="weight", walk_type=None, alpha=0.95
  205. ):
  206. r"""Returns the directed Laplacian matrix of G.
  207. The graph directed Laplacian is the matrix
  208. .. math::
  209. L = I - \frac{1}{2} \left (\Phi^{1/2} P \Phi^{-1/2} + \Phi^{-1/2} P^T \Phi^{1/2} \right )
  210. where `I` is the identity matrix, `P` is the transition matrix of the
  211. graph, and `\Phi` a matrix with the Perron vector of `P` in the diagonal and
  212. zeros elsewhere [1]_.
  213. Depending on the value of walk_type, `P` can be the transition matrix
  214. induced by a random walk, a lazy random walk, or a random walk with
  215. teleportation (PageRank).
  216. Parameters
  217. ----------
  218. G : DiGraph
  219. A NetworkX graph
  220. nodelist : list, optional
  221. The rows and columns are ordered according to the nodes in nodelist.
  222. If nodelist is None, then the ordering is produced by G.nodes().
  223. weight : string or None, optional (default='weight')
  224. The edge data key used to compute each value in the matrix.
  225. If None, then each edge has weight 1.
  226. walk_type : string or None, optional (default=None)
  227. One of ``"random"``, ``"lazy"``, or ``"pagerank"``. If ``walk_type=None``
  228. (the default), then a value is selected according to the properties of `G`:
  229. - ``walk_type="random"`` if `G` is strongly connected and aperiodic
  230. - ``walk_type="lazy"`` if `G` is strongly connected but not aperiodic
  231. - ``walk_type="pagerank"`` for all other cases.
  232. alpha : real
  233. (1 - alpha) is the teleportation probability used with pagerank
  234. Returns
  235. -------
  236. L : NumPy matrix
  237. Normalized Laplacian of G.
  238. Notes
  239. -----
  240. Only implemented for DiGraphs
  241. The result is always a symmetric matrix.
  242. This calculation uses the out-degree of the graph `G`. To use the
  243. in-degree for calculations instead, use `G.reverse(copy=False)` and
  244. take the transpose.
  245. See Also
  246. --------
  247. laplacian_matrix
  248. normalized_laplacian_matrix
  249. directed_combinatorial_laplacian_matrix
  250. References
  251. ----------
  252. .. [1] Fan Chung (2005).
  253. Laplacians and the Cheeger inequality for directed graphs.
  254. Annals of Combinatorics, 9(1), 2005
  255. """
  256. import numpy as np
  257. import scipy as sp
  258. # NOTE: P has type ndarray if walk_type=="pagerank", else csr_array
  259. P = _transition_matrix(
  260. G, nodelist=nodelist, weight=weight, walk_type=walk_type, alpha=alpha
  261. )
  262. n, m = P.shape
  263. evals, evecs = sp.sparse.linalg.eigs(P.T, k=1)
  264. v = evecs.flatten().real
  265. p = v / v.sum()
  266. # p>=0 by Perron-Frobenius Thm. Use abs() to fix roundoff across zero gh-6865
  267. sqrtp = np.sqrt(np.abs(p))
  268. Q = (
  269. sp.sparse.dia_array((sqrtp, 0), shape=(n, n)).tocsr()
  270. @ P
  271. @ sp.sparse.dia_array((1.0 / sqrtp, 0), shape=(n, n)).tocsr()
  272. )
  273. # NOTE: This could be sparsified for the non-pagerank cases
  274. I = np.identity(len(G))
  275. return I - (Q + Q.T) / 2.0
  276. @not_implemented_for("undirected")
  277. @not_implemented_for("multigraph")
  278. @nx._dispatchable(edge_attrs="weight")
  279. def directed_combinatorial_laplacian_matrix(
  280. G, nodelist=None, weight="weight", walk_type=None, alpha=0.95
  281. ):
  282. r"""Return the directed combinatorial Laplacian matrix of G.
  283. The graph directed combinatorial Laplacian is the matrix
  284. .. math::
  285. L = \Phi - \frac{1}{2} \left (\Phi P + P^T \Phi \right)
  286. where `P` is the transition matrix of the graph and `\Phi` a matrix
  287. with the Perron vector of `P` in the diagonal and zeros elsewhere [1]_.
  288. Depending on the value of walk_type, `P` can be the transition matrix
  289. induced by a random walk, a lazy random walk, or a random walk with
  290. teleportation (PageRank).
  291. Parameters
  292. ----------
  293. G : DiGraph
  294. A NetworkX graph
  295. nodelist : list, optional
  296. The rows and columns are ordered according to the nodes in nodelist.
  297. If nodelist is None, then the ordering is produced by G.nodes().
  298. weight : string or None, optional (default='weight')
  299. The edge data key used to compute each value in the matrix.
  300. If None, then each edge has weight 1.
  301. walk_type : string or None, optional (default=None)
  302. One of ``"random"``, ``"lazy"``, or ``"pagerank"``. If ``walk_type=None``
  303. (the default), then a value is selected according to the properties of `G`:
  304. - ``walk_type="random"`` if `G` is strongly connected and aperiodic
  305. - ``walk_type="lazy"`` if `G` is strongly connected but not aperiodic
  306. - ``walk_type="pagerank"`` for all other cases.
  307. alpha : real
  308. (1 - alpha) is the teleportation probability used with pagerank
  309. Returns
  310. -------
  311. L : NumPy matrix
  312. Combinatorial Laplacian of G.
  313. Notes
  314. -----
  315. Only implemented for DiGraphs
  316. The result is always a symmetric matrix.
  317. This calculation uses the out-degree of the graph `G`. To use the
  318. in-degree for calculations instead, use `G.reverse(copy=False)` and
  319. take the transpose.
  320. See Also
  321. --------
  322. laplacian_matrix
  323. normalized_laplacian_matrix
  324. directed_laplacian_matrix
  325. References
  326. ----------
  327. .. [1] Fan Chung (2005).
  328. Laplacians and the Cheeger inequality for directed graphs.
  329. Annals of Combinatorics, 9(1), 2005
  330. """
  331. import scipy as sp
  332. P = _transition_matrix(
  333. G, nodelist=nodelist, weight=weight, walk_type=walk_type, alpha=alpha
  334. )
  335. n, m = P.shape
  336. evals, evecs = sp.sparse.linalg.eigs(P.T, k=1)
  337. v = evecs.flatten().real
  338. p = v / v.sum()
  339. # NOTE: could be improved by not densifying
  340. Phi = sp.sparse.dia_array((p, 0), shape=(n, n)).toarray()
  341. return Phi - (Phi @ P + P.T @ Phi) / 2.0
  342. def _transition_matrix(G, nodelist=None, weight="weight", walk_type=None, alpha=0.95):
  343. """Returns the transition matrix of G.
  344. This is a row stochastic giving the transition probabilities while
  345. performing a random walk on the graph. Depending on the value of walk_type,
  346. P can be the transition matrix induced by a random walk, a lazy random walk,
  347. or a random walk with teleportation (PageRank).
  348. Parameters
  349. ----------
  350. G : DiGraph
  351. A NetworkX graph
  352. nodelist : list, optional
  353. The rows and columns are ordered according to the nodes in nodelist.
  354. If nodelist is None, then the ordering is produced by G.nodes().
  355. weight : string or None, optional (default='weight')
  356. The edge data key used to compute each value in the matrix.
  357. If None, then each edge has weight 1.
  358. walk_type : string or None, optional (default=None)
  359. One of ``"random"``, ``"lazy"``, or ``"pagerank"``. If ``walk_type=None``
  360. (the default), then a value is selected according to the properties of `G`:
  361. - ``walk_type="random"`` if `G` is strongly connected and aperiodic
  362. - ``walk_type="lazy"`` if `G` is strongly connected but not aperiodic
  363. - ``walk_type="pagerank"`` for all other cases.
  364. alpha : real
  365. (1 - alpha) is the teleportation probability used with pagerank
  366. Returns
  367. -------
  368. P : numpy.ndarray
  369. transition matrix of G.
  370. Raises
  371. ------
  372. NetworkXError
  373. If walk_type not specified or alpha not in valid range
  374. """
  375. import numpy as np
  376. import scipy as sp
  377. if walk_type is None:
  378. if nx.is_strongly_connected(G):
  379. if nx.is_aperiodic(G):
  380. walk_type = "random"
  381. else:
  382. walk_type = "lazy"
  383. else:
  384. walk_type = "pagerank"
  385. A = nx.to_scipy_sparse_array(G, nodelist=nodelist, weight=weight, dtype=float)
  386. n, m = A.shape
  387. if walk_type in ["random", "lazy"]:
  388. DI = sp.sparse.dia_array((1.0 / A.sum(axis=1), 0), shape=(n, n)).tocsr()
  389. if walk_type == "random":
  390. P = DI @ A
  391. else:
  392. I = sp.sparse.eye_array(n, format="csr")
  393. P = (I + DI @ A) / 2.0
  394. elif walk_type == "pagerank":
  395. if not (0 < alpha < 1):
  396. raise nx.NetworkXError("alpha must be between 0 and 1")
  397. # this is using a dense representation. NOTE: This should be sparsified!
  398. A = A.toarray()
  399. # add constant to dangling nodes' row
  400. A[A.sum(axis=1) == 0, :] = 1 / n
  401. # normalize
  402. A = A / A.sum(axis=1)[np.newaxis, :].T
  403. P = alpha * A + (1 - alpha) / n
  404. else:
  405. raise nx.NetworkXError("walk_type must be random, lazy, or pagerank")
  406. return P