spectral_graph_forge.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. """Generates graphs with a given eigenvector structure"""
  2. import networkx as nx
  3. from networkx.utils import np_random_state
  4. __all__ = ["spectral_graph_forge"]
  5. @np_random_state(3)
  6. def spectral_graph_forge(G, alpha, transformation="identity", seed=None):
  7. """Returns a random simple graph with spectrum resembling that of `G`
  8. This algorithm, called Spectral Graph Forge (SGF), computes the
  9. eigenvectors of a given graph adjacency matrix, filters them and
  10. builds a random graph with a similar eigenstructure.
  11. SGF has been proved to be particularly useful for synthesizing
  12. realistic social networks and it can also be used to anonymize
  13. graph sensitive data.
  14. Parameters
  15. ----------
  16. G : Graph
  17. alpha : float
  18. Ratio representing the percentage of eigenvectors of G to consider,
  19. values in [0,1].
  20. transformation : string, optional
  21. Represents the intended matrix linear transformation, possible values
  22. are 'identity' and 'modularity'
  23. seed : integer, random_state, or None (default)
  24. Indicator of numpy random number generation state.
  25. See :ref:`Randomness<randomness>`.
  26. Returns
  27. -------
  28. H : Graph
  29. A graph with a similar eigenvector structure of the input one.
  30. Raises
  31. ------
  32. NetworkXError
  33. If transformation has a value different from 'identity' or 'modularity'
  34. Notes
  35. -----
  36. Spectral Graph Forge (SGF) generates a random simple graph resembling the
  37. global properties of the given one.
  38. It leverages the low-rank approximation of the associated adjacency matrix
  39. driven by the *alpha* precision parameter.
  40. SGF preserves the number of nodes of the input graph and their ordering.
  41. This way, nodes of output graphs resemble the properties of the input one
  42. and attributes can be directly mapped.
  43. It considers the graph adjacency matrices which can optionally be
  44. transformed to other symmetric real matrices (currently transformation
  45. options include *identity* and *modularity*).
  46. The *modularity* transformation, in the sense of Newman's modularity matrix
  47. allows the focusing on community structure related properties of the graph.
  48. SGF applies a low-rank approximation whose fixed rank is computed from the
  49. ratio *alpha* of the input graph adjacency matrix dimension.
  50. This step performs a filtering on the input eigenvectors similar to the low
  51. pass filtering common in telecommunications.
  52. The filtered values (after truncation) are used as input to a Bernoulli
  53. sampling for constructing a random adjacency matrix.
  54. References
  55. ----------
  56. .. [1] L. Baldesi, C. T. Butts, A. Markopoulou, "Spectral Graph Forge:
  57. Graph Generation Targeting Modularity", IEEE Infocom, '18.
  58. https://arxiv.org/abs/1801.01715
  59. .. [2] M. Newman, "Networks: an introduction", Oxford university press,
  60. 2010
  61. Examples
  62. --------
  63. >>> G = nx.karate_club_graph()
  64. >>> H = nx.spectral_graph_forge(G, 0.3)
  65. >>>
  66. """
  67. import numpy as np
  68. import scipy as sp
  69. import scipy.stats # call as sp.stats
  70. available_transformations = ["identity", "modularity"]
  71. alpha = np.clip(alpha, 0, 1)
  72. A = nx.to_numpy_array(G)
  73. n = A.shape[1]
  74. level = round(n * alpha)
  75. if transformation not in available_transformations:
  76. msg = f"{transformation!r} is not a valid transformation. "
  77. msg += f"Transformations: {available_transformations}"
  78. raise nx.NetworkXError(msg)
  79. K = np.ones((1, n)) @ A
  80. B = A
  81. if transformation == "modularity":
  82. B -= K.T @ K / K.sum()
  83. # Compute low-rank approximation of B
  84. evals, evecs = np.linalg.eigh(B)
  85. k = np.argsort(np.abs(evals))[::-1] # indices of evals in descending order
  86. evecs[:, k[np.arange(level, n)]] = 0 # set smallest eigenvectors to 0
  87. B = evecs @ np.diag(evals) @ evecs.T
  88. if transformation == "modularity":
  89. B += K.T @ K / K.sum()
  90. B = np.clip(B, 0, 1)
  91. np.fill_diagonal(B, 0)
  92. for i in range(n - 1):
  93. B[i, i + 1 :] = sp.stats.bernoulli.rvs(B[i, i + 1 :], random_state=seed)
  94. B[i + 1 :, i] = np.transpose(B[i, i + 1 :])
  95. H = nx.from_numpy_array(B)
  96. return H