Repository: martinfleis/momepy
Pull request #176 (2020-06-07 15:46:11)

ENH: support network analysis for full network
Functions of `momepy.graph` module can now return a value for a whole graph instead of subgraphs if `radius=None` (where applicable). Applies to:

- `meshedness`
- `cds_length`
- `mean_node_degree`
- `proportion`
- `cyclomatic`
- `edge_node_ratio`
- `gamma`

codecov[bot]: # [Codecov](https://codecov.io/gh/martinfleis/momepy/pull/176?src=pr&el=h1) Report
> Merging [#176](https://codecov.io/gh/martinfleis/momepy/pull/176?src=pr&el=desc) into [master](https://codecov.io/gh/martinfleis/momepy/commit/b5963753e7a682fe3054a7cd2061824dc1abdbec&el=desc) will **increase** coverage by `0.04%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/martinfleis/momepy/pull/176/graphs/tree.svg?width=650&height=150&src=pr&token=i04999UxkK)](https://codecov.io/gh/martinfleis/momepy/pull/176?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #176      +/-   ##
==========================================
+ Coverage   94.06%   94.11%   +0.04%     
==========================================
  Files          10       10              
  Lines        2630     2652      +22     
==========================================
+ Hits         2474     2496      +22     
  Misses        156      156              
```


| [Impacted Files](https://codecov.io/gh/martinfleis/momepy/pull/176?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [momepy/graph.py](https://codecov.io/gh/martinfleis/momepy/pull/176/diff?src=pr&el=tree#diff-bW9tZXB5L2dyYXBoLnB5) | `97.34% <100.00%> (+0.24%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/martinfleis/momepy/pull/176?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/martinfleis/momepy/pull/176?src=pr&el=footer). Last update [b596375...3e75df4](https://codecov.io/gh/martinfleis/momepy/pull/176?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/momepy/graph.py b/momepy/graph.py
index a76c0bd..2ad491a 100644
--- a/momepy/graph.py
+++ b/momepy/graph.py
@@ -73,7 +73,8 @@ def _meshedness(graph):
 
 def meshedness(graph, radius=5, name="meshedness", distance=None):
     """
-    Calculates meshedness for subgraph around each node.
+    Calculates meshedness for subgraph around each node if radius is set, or for
+    whole graph, if ``radius=None``.
 
     Subgraph is generated around each node within set radius. If ``distance=None``,
     radius will define topological distance, otherwise it uses values in distance
@@ -92,7 +93,7 @@ def meshedness(graph, radius=5, name="meshedness", distance=None):
     graph : networkx.Graph
         Graph representing street network.
         Ideally generated from GeoDataFrame using :func:`momepy.gdf_to_nx`
-    radius: int
+    radius: int, optional
         number of topological steps defining the extent of subgraph
     name : str, optional
         calculated attribute name
@@ -104,7 +105,9 @@ def meshedness(graph, radius=5, name="meshedness", distance=None):
     Returns
     -------
     Graph
-        networkx.Graph
+        networkx.Graph if radius is set
+    float
+        meshedness for graph if ``radius=None``
 
     Examples
     --------
@@ -112,15 +115,18 @@ def meshedness(graph, radius=5, name="meshedness", distance=None):
     """
     netx = graph.copy()
 
-    for n in tqdm(netx, total=len(netx)):
-        sub = nx.ego_graph(
-            netx, n, radius=radius, distance=distance
-        )  # define subgraph of steps=radius
-        netx.nodes[n][name] = _meshedness(
-            sub
-        )  # save value calulated for subgraph to node
+    if radius:
+        for n in tqdm(netx, total=len(netx)):
+            sub = nx.ego_graph(
+                netx, n, radius=radius, distance=distance
+            )  # define subgraph of steps=radius
+            netx.nodes[n][name] = _meshedness(
+                sub
+            )  # save value calulated for subgraph to node
 
-    return netx
+        return netx
+
+    return _meshedness(netx)
 
 
 def mean_node_dist(graph, name="meanlen", length="mm_len"):
@@ -186,7 +192,8 @@ def cds_length(
     distance=None,
 ):
     """
-    Calculates length of cul-de-sacs for subgraph around each node.
+    Calculates length of cul-de-sacs for subgraph around each node if radius is set, or for
+    whole graph, if ``radius=None``.
 
     Subgraph is generated around each node within set radius. If ``distance=None``,
     radius will define topological distance, otherwise it uses values in distance
@@ -217,7 +224,9 @@ def cds_length(
     Returns
     -------
     Graph
-        networkx.Graph
+        networkx.Graph if radius is set
+    float
+        length of cul-de-sacs for graph if ``radius=None``
 
     Examples
     --------
@@ -232,15 +241,18 @@ def cds_length(
         else:
             netx[u][v][k]["cdsbool"] = False
 
-    for n in tqdm(netx, total=len(netx)):
-        sub = nx.ego_graph(
-            netx, n, radius=radius, distance=distance
-        )  # define subgraph of steps=radius
-        netx.nodes[n][name] = _cds_length(
-            sub, mode=mode, length=length
-        )  # save value calculated for subgraph to node
+    if radius:
+        for n in tqdm(netx, total=len(netx)):
+            sub = nx.ego_graph(
+                netx, n, radius=radius, distance=distance
+            )  # define subgraph of steps=radius
+            netx.nodes[n][name] = _cds_length(
+                sub, mode=mode, length=length
+            )  # save value calculated for subgraph to node
 
-    return netx
+        return netx
+
+    return _cds_length(netx, mode=mode, length=length)
 
 
 def _mean_node_degree(graph, degree):
@@ -252,7 +264,8 @@ def _mean_node_degree(graph, degree):
 
 def mean_node_degree(graph, radius=5, name="mean_nd", degree="degree", distance=None):
     """
-    Calculates mean node degree for subgraph around each node.
+    Calculates mean node degree for subgraph around each node if radius is set, or for
+    whole graph, if ``radius=None``.
 
     Subgraph is generated around each node within set radius. If ``distance=None``,
     radius will define topological distance, otherwise it uses values in ``distance``
@@ -278,7 +291,9 @@ def mean_node_degree(graph, radius=5, name="mean_nd", degree="degree", distance=
     Returns
     -------
     Graph
-        networkx.Graph
+        networkx.Graph if radius is set
+    float
+        mean node degree for graph if ``radius=None``
 
     Examples
     --------
@@ -286,13 +301,16 @@ def mean_node_degree(graph, radius=5, name="mean_nd", degree="degree", distance=
     """
     netx = graph.copy()
 
-    for n in tqdm(netx, total=len(netx)):
-        sub = nx.ego_graph(
-            netx, n, radius=radius, distance=distance
-        )  # define subgraph of steps=radius
-        netx.nodes[n][name] = _mean_node_degree(sub, degree=degree)
+    if radius:
+        for n in tqdm(netx, total=len(netx)):
+            sub = nx.ego_graph(
+                netx, n, radius=radius, distance=distance
+            )  # define subgraph of steps=radius
+            netx.nodes[n][name] = _mean_node_degree(sub, degree=degree)
 
-    return netx
+        return netx
+
+    return _mean_node_degree(netx, degree=degree)
 
 
 def _proportion(graph, degree):
@@ -310,7 +328,8 @@ def proportion(
     graph, radius=5, three=None, four=None, dead=None, degree="degree", distance=None
 ):
     """
-    Calculates the proportion of intersection types for subgraph around each node.
+    Calculates the proportion of intersection types for subgraph around each node if radius is set, or for
+    whole graph, if ``radius=None``.
 
     Subgraph is generated around each node within set radius. If ``distance=None``,
     radius will define topological distance, otherwise it uses values in ``distance``
@@ -339,7 +358,9 @@ def proportion(
     Returns
     -------
     Graph
-        networkx.Graph
+        networkx.Graph if radius is set
+    dict
+        dict with proportions for graph if ``radius=None``
 
     Examples
     --------
@@ -351,18 +372,31 @@ def proportion(
         )
     netx = graph.copy()
 
-    for n in tqdm(netx, total=len(netx)):
-        sub = nx.ego_graph(
-            netx, n, radius=radius, distance=distance
-        )  # define subgraph of steps=radius
-        counts = _proportion(sub, degree=degree)
-        if three:
-            netx.nodes[n][three] = counts[3] / len(sub)
-        if four:
-            netx.nodes[n][four] = counts[4] / len(sub)
-        if dead:
-            netx.nodes[n][dead] = counts[1] / len(sub)
-    return netx
+    if radius:
+        for n in tqdm(netx, total=len(netx)):
+            sub = nx.ego_graph(
+                netx, n, radius=radius, distance=distance
+            )  # define subgraph of steps=radius
+            counts = _proportion(sub, degree=degree)
+            if three:
+                netx.nodes[n][three] = counts[3] / len(sub)
+            if four:
+                netx.nodes[n][four] = counts[4] / len(sub)
+            if dead:
+                netx.nodes[n][dead] = counts[1] / len(sub)
+        return netx
+
+    # add example to docs explaining keys
+    counts = _proportion(netx, degree=degree)
+    result = {}
+    if three:
+        result[three] = counts[3] / len(netx)
+    if four:
+        result[four] = counts[4] / len(netx)
+    if dead:
+        result[dead] = counts[1] / len(netx)
+
+    return result
 
 
 def _cyclomatic(graph):
@@ -376,7 +410,8 @@ def _cyclomatic(graph):
 
 def cyclomatic(graph, radius=5, name="cyclomatic", distance=None):
     """
-    Calculates cyclomatic complexity for subgraph around each node.
+    Calculates cyclomatic complexity for subgraph around each node if radius is set, or for
+    whole graph, if ``radius=None``.
 
     Subgraph is generated around each node within set radius. If ``distance=None``,
     radius will define topological distance, otherwise it uses values in ``distance``
@@ -406,7 +441,9 @@ def cyclomatic(graph, radius=5, name="cyclomatic", distance=None):
     Returns
     -------
     Graph
-        networkx.Graph
+        networkx.Graph if radius is set
+    float
+        cyclomatic complexity for graph if ``radius=None``
 
     Examples
     --------
@@ -414,15 +451,18 @@ def cyclomatic(graph, radius=5, name="cyclomatic", distance=None):
     """
     netx = graph.copy()
 
-    for n in tqdm(netx, total=len(netx)):
-        sub = nx.ego_graph(
-            netx, n, radius=radius, distance=distance
-        )  # define subgraph of steps=radius
-        netx.nodes[n][name] = _cyclomatic(
-            sub
-        )  # save value calulated for subgraph to node
+    if radius:
+        for n in tqdm(netx, total=len(netx)):
+            sub = nx.ego_graph(
+                netx, n, radius=radius, distance=distance
+            )  # define subgraph of steps=radius
+            netx.nodes[n][name] = _cyclomatic(
+                sub
+            )  # save value calulated for subgraph to node
 
-    return netx
+        return netx
+
+    return _cyclomatic(netx)
 
 
 def _edge_node_ratio(graph):
@@ -436,7 +476,8 @@ def _edge_node_ratio(graph):
 
 def edge_node_ratio(graph, radius=5, name="edge_node_ratio", distance=None):
     """
-    Calculates edge / node ratio for subgraph around each node.
+    Calculates edge / node ratio for subgraph around each node if radius is set, or for
+    whole graph, if ``radius=None``.
 
     Subgraph is generated around each node within set radius. If ``distance=None``,
     radius will define topological distance, otherwise it uses values in ``distance``
@@ -466,7 +507,9 @@ def edge_node_ratio(graph, radius=5, name="edge_node_ratio", distance=None):
     Returns
     -------
     Graph
-        networkx.Graph
+        networkx.Graph if radius is set
+    float
+        edge / node ratio for graph if ``radius=None``
 
     Examples
     --------
@@ -474,15 +517,18 @@ def edge_node_ratio(graph, radius=5, name="edge_node_ratio", distance=None):
     """
     netx = graph.copy()
 
-    for n in tqdm(netx, total=len(netx)):
-        sub = nx.ego_graph(
-            netx, n, radius=radius, distance=distance
-        )  # define subgraph of steps=radius
-        netx.nodes[n][name] = _edge_node_ratio(
-            sub
-        )  # save value calulated for subgraph to node
+    if radius:
+        for n in tqdm(netx, total=len(netx)):
+            sub = nx.ego_graph(
+                netx, n, radius=radius, distance=distance
+            )  # define subgraph of steps=radius
+            netx.nodes[n][name] = _edge_node_ratio(
+                sub
+            )  # save value calulated for subgraph to node
 
-    return netx
+        return netx
+
+    return _edge_node_ratio(netx)
 
 
 def _gamma(graph):
@@ -498,7 +544,8 @@ def _gamma(graph):
 
 def gamma(graph, radius=5, name="gamma", distance=None):
     """
-    Calculates connectivity gamma index for subgraph around each node.
+    Calculates connectivity gamma index for subgraph around each node if radius is set, or for
+    whole graph, if ``radius=None``.
 
     Subgraph is generated around each node within set radius. If ``distance=None``,
     radius will define topological distance, otherwise it uses values in ``distance``
@@ -528,7 +575,9 @@ def gamma(graph, radius=5, name="gamma", distance=None):
     Returns
     -------
     Graph
-        networkx.Graph
+        networkx.Graph if radius is set
+    float
+        gamma index for graph if ``radius=None``
 
     Examples
     --------
@@ -537,13 +586,16 @@ def gamma(graph, radius=5, name="gamma", distance=None):
     """
     netx = graph.copy()
 
-    for n in tqdm(netx, total=len(netx)):
-        sub = nx.ego_graph(
-            netx, n, radius=radius, distance=distance
-        )  # define subgraph of steps=radius
-        netx.nodes[n][name] = _gamma(sub)
+    if radius:
+        for n in tqdm(netx, total=len(netx)):
+            sub = nx.ego_graph(
+                netx, n, radius=radius, distance=distance
+            )  # define subgraph of steps=radius
+            netx.nodes[n][name] = _gamma(sub)
 
-    return netx
+        return netx
+
+    return _gamma(netx)
 
 
 def clustering(graph, name="cluster"):


Test patch:
diff --git a/tests/test_graph.py b/tests/test_graph.py
index 7e7190f..b78b39d 100644
--- a/tests/test_graph.py
+++ b/tests/test_graph.py
@@ -20,6 +20,7 @@ class TestGraph:
         net = mm.meshedness(self.network)
         check = 0.14893617021276595
         assert net.nodes[(1603650.450422848, 6464368.600601688)]["meshedness"] == check
+        assert mm.meshedness(self.network, radius=None) == 0.1320754716981132
 
     def test_mean_node_dist(self):
         net = mm.mean_node_dist(self.network)
@@ -35,11 +36,13 @@ class TestGraph:
         assert net2.nodes[(1603650.450422848, 6464368.600601688)]["cds_mean"] == mean
         with pytest.raises(ValueError):
             net2 = mm.cds_length(self.network, mode="nonexistent")
+        assert mm.cds_length(self.network, radius=None) == 2291.4520621447705
 
     def test_mean_node_degree(self):
         net = mm.mean_node_degree(self.network)
         check = 2.576923076923077
         assert net.nodes[(1603650.450422848, 6464368.600601688)]["mean_nd"] == check
+        assert mm.mean_node_degree(self.network, radius=None) == 2.413793103448276
 
     def test_proportion(self):
         net = mm.proportion(self.network, three="three", four="four", dead="dead")
@@ -49,6 +52,12 @@ class TestGraph:
         assert net.nodes[(1603650.450422848, 6464368.600601688)]["dead"] == dead
         assert net.nodes[(1603650.450422848, 6464368.600601688)]["four"] == four
         assert net.nodes[(1603650.450422848, 6464368.600601688)]["three"] == three
+        glob = mm.proportion(
+            self.network, three="three", four="four", dead="dead", radius=None
+        )
+        assert glob["dead"] == 0.3793103448275862
+        assert glob["four"] == 0.2413793103448276
+        assert glob["three"] == 0.20689655172413793
 
     def test_proportion_error(self):
         with pytest.raises(ValueError):
@@ -58,6 +67,7 @@ class TestGraph:
         net = mm.cyclomatic(self.network)
         check = 7
         assert net.nodes[(1603650.450422848, 6464368.600601688)]["cyclomatic"] == check
+        assert mm.cyclomatic(self.network, radius=None) == 7
 
     def test_edge_node_ratio(self):
         net = mm.edge_node_ratio(self.network)
@@ -66,11 +76,13 @@ class TestGraph:
             net.nodes[(1603650.450422848, 6464368.600601688)]["edge_node_ratio"]
             == check
         )
+        assert mm.edge_node_ratio(self.network, radius=None) == 1.206896551724138
 
     def test_gamma(self):
         net = mm.gamma(self.network)
         check = 0.4444444444444444
         assert net.nodes[(1603650.450422848, 6464368.600601688)]["gamma"] == check
+        assert mm.gamma(self.network, radius=None) == 0.43209876543209874
 
     def test_local_closeness_centrality(self):
         net = mm.local_closeness_centrality(self.network)