trophic.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. """Trophic levels"""
  2. import networkx as nx
  3. from networkx.utils import not_implemented_for
  4. __all__ = ["trophic_levels", "trophic_differences", "trophic_incoherence_parameter"]
  5. @not_implemented_for("undirected")
  6. @nx._dispatchable(edge_attrs="weight")
  7. def trophic_levels(G, weight="weight"):
  8. r"""Compute the trophic levels of nodes.
  9. The trophic level of a node $i$ is
  10. .. math::
  11. s_i = 1 + \frac{1}{k^{in}_i} \sum_{j} a_{ij} s_j
  12. where $k^{in}_i$ is the in-degree of i
  13. .. math::
  14. k^{in}_i = \sum_{j} a_{ij}
  15. and nodes with $k^{in}_i = 0$ have $s_i = 1$ by convention.
  16. These are calculated using the method outlined in Levine [1]_.
  17. Parameters
  18. ----------
  19. G : DiGraph
  20. A directed networkx graph
  21. Returns
  22. -------
  23. nodes : dict
  24. Dictionary of nodes with trophic level as the value.
  25. References
  26. ----------
  27. .. [1] Stephen Levine (1980) J. theor. Biol. 83, 195-207
  28. """
  29. basal_nodes = [n for n, deg in G.in_degree if deg == 0]
  30. if not basal_nodes:
  31. raise nx.NetworkXError(
  32. "This graph has no basal nodes (nodes with no incoming edges)."
  33. "Trophic levels are not defined without at least one basal node."
  34. )
  35. reachable_nodes = {
  36. node for layer in nx.bfs_layers(G, sources=basal_nodes) for node in layer
  37. }
  38. if len(reachable_nodes) != len(G.nodes):
  39. raise nx.NetworkXError(
  40. "Trophic levels are only defined for graphs where every node has a path "
  41. "from a basal node (basal nodes are nodes with no incoming edges)."
  42. )
  43. import numpy as np
  44. # find adjacency matrix
  45. a = nx.adjacency_matrix(G, weight=weight).T.toarray()
  46. # drop rows/columns where in-degree is zero
  47. rowsum = np.sum(a, axis=1)
  48. p = a[rowsum != 0][:, rowsum != 0]
  49. # normalise so sum of in-degree weights is 1 along each row
  50. p = p / rowsum[rowsum != 0][:, np.newaxis]
  51. # calculate trophic levels
  52. nn = p.shape[0]
  53. i = np.eye(nn)
  54. try:
  55. n = np.linalg.inv(i - p)
  56. except np.linalg.LinAlgError as err:
  57. # LinAlgError is raised when there is a non-basal node
  58. msg = (
  59. "Trophic levels are only defined for graphs where every "
  60. + "node has a path from a basal node (basal nodes are nodes "
  61. + "with no incoming edges)."
  62. )
  63. raise nx.NetworkXError(msg) from err
  64. y = n.sum(axis=1) + 1
  65. levels = {}
  66. # all nodes with in-degree zero have trophic level == 1
  67. zero_node_ids = (node_id for node_id, degree in G.in_degree if degree == 0)
  68. for node_id in zero_node_ids:
  69. levels[node_id] = 1
  70. # all other nodes have levels as calculated
  71. nonzero_node_ids = (node_id for node_id, degree in G.in_degree if degree != 0)
  72. for i, node_id in enumerate(nonzero_node_ids):
  73. levels[node_id] = y.item(i)
  74. return levels
  75. @not_implemented_for("undirected")
  76. @nx._dispatchable(edge_attrs="weight")
  77. def trophic_differences(G, weight="weight"):
  78. r"""Compute the trophic differences of the edges of a directed graph.
  79. The trophic difference $x_ij$ for each edge is defined in Johnson et al.
  80. [1]_ as:
  81. .. math::
  82. x_ij = s_j - s_i
  83. Where $s_i$ is the trophic level of node $i$.
  84. Parameters
  85. ----------
  86. G : DiGraph
  87. A directed networkx graph
  88. Returns
  89. -------
  90. diffs : dict
  91. Dictionary of edges with trophic differences as the value.
  92. References
  93. ----------
  94. .. [1] Samuel Johnson, Virginia Dominguez-Garcia, Luca Donetti, Miguel A.
  95. Munoz (2014) PNAS "Trophic coherence determines food-web stability"
  96. """
  97. levels = trophic_levels(G, weight=weight)
  98. diffs = {}
  99. for u, v in G.edges:
  100. diffs[(u, v)] = levels[v] - levels[u]
  101. return diffs
  102. @not_implemented_for("undirected")
  103. @nx._dispatchable(edge_attrs="weight")
  104. def trophic_incoherence_parameter(G, weight="weight", cannibalism=False):
  105. r"""Compute the trophic incoherence parameter of a graph.
  106. Trophic coherence is defined as the homogeneity of the distribution of
  107. trophic distances: the more similar, the more coherent. This is measured by
  108. the standard deviation of the trophic differences and referred to as the
  109. trophic incoherence parameter $q$ by [1].
  110. Parameters
  111. ----------
  112. G : DiGraph
  113. A directed networkx graph
  114. cannibalism: Boolean
  115. If set to False, self edges are not considered in the calculation
  116. Returns
  117. -------
  118. trophic_incoherence_parameter : float
  119. The trophic coherence of a graph
  120. References
  121. ----------
  122. .. [1] Samuel Johnson, Virginia Dominguez-Garcia, Luca Donetti, Miguel A.
  123. Munoz (2014) PNAS "Trophic coherence determines food-web stability"
  124. """
  125. import numpy as np
  126. if cannibalism:
  127. diffs = trophic_differences(G, weight=weight)
  128. else:
  129. # If no cannibalism, remove self-edges
  130. self_loops = list(nx.selfloop_edges(G))
  131. if self_loops:
  132. # Make a copy so we do not change G's edges in memory
  133. G_2 = G.copy()
  134. G_2.remove_edges_from(self_loops)
  135. else:
  136. # Avoid copy otherwise
  137. G_2 = G
  138. diffs = trophic_differences(G_2, weight=weight)
  139. return float(np.std(list(diffs.values())))