Repository: martinfleis/momepy
Pull request #178 (2020-06-20 19:49:37)

REF: consolidate MCA
Deprecate `local_closeness_centrality`, `local_betweenness_centrality` and `local_straightness_centrality` and include the ability to compute local centrality to `closeness_centrality`, `betweenness_centrality` and `straightness_centrality`. Separate local options will be removed in 0.4.0.

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

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

```diff
@@            Coverage Diff             @@
##           master     #178      +/-   ##
==========================================
+ Coverage   94.11%   94.22%   +0.10%     
==========================================
  Files          10       11       +1     
  Lines        2652     2665      +13     
==========================================
+ Hits         2496     2511      +15     
+ Misses        156      154       -2     
```


| [Impacted Files](https://codecov.io/gh/martinfleis/momepy/pull/178?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [momepy/graph.py](https://codecov.io/gh/martinfleis/momepy/pull/178/diff?src=pr&el=tree#diff-bW9tZXB5L2dyYXBoLnB5) | `98.09% <100.00%> (+0.74%)` | :arrow_up: |
| [momepy/\_\_init\_\_.py](https://codecov.io/gh/martinfleis/momepy/pull/178/diff?src=pr&el=tree#diff-bW9tZXB5L19faW5pdF9fLnB5) | `100.00% <0.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/martinfleis/momepy/pull/178?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/178?src=pr&el=footer). Last update [6c27024...ed2b3c4](https://codecov.io/gh/martinfleis/momepy/pull/178?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/api.rst b/docs/api.rst
index 41b53a5..0ffd8fd 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -103,9 +103,6 @@ graph
    cyclomatic
    edge_node_ratio
    gamma
-   local_betweenness_centrality
-   local_closeness_centrality
-   local_straightness_centrality
    mean_node_degree
    mean_node_dist
    mean_nodes
diff --git a/docs/user_guide/graph/centrality.ipynb b/docs/user_guide/graph/centrality.ipynb
index 3157796..4a0173b 100644
--- a/docs/user_guide/graph/centrality.ipynb
+++ b/docs/user_guide/graph/centrality.ipynb
@@ -115,7 +115,7 @@
     "Closeness centrality could be simplified as average distance to every other node from each node. As such, it can be measured on the whole network (Global Closeness Centrality) or wihtin certain reach only (Local Closeness Centrality). \n",
     "\n",
     "#### Local closeness\n",
-    "To measure `local_closeness_centrality` we need to specify radius (how far we should go from each node). We can use topological distance (e.g. 5 steps, then radius=5) or metric distance (e.g. 400 metres) - then radius=400 and distance= lenght of each segment saved as a parameter of each edge. By default, momepy saves length as `mm_len`. \n",
+    "To measure local `closeness_centrality` we need to specify radius (how far we should go from each node). We can use topological distance (e.g. 5 steps, then radius=5) or metric distance (e.g. 400 metres) - then radius=400 and distance= lenght of each segment saved as a parameter of each edge. By default, momepy saves length as `mm_len`. \n",
     "\n",
     "Weight parameter is used for centrality calculation. Again, we can use metric weight (using the same attribute as above) or no weight (weight=None) at all. Or any other attribute we wish."
    ]
@@ -134,7 +134,7 @@
     }
    ],
    "source": [
-    "primal = momepy.local_closeness_centrality(primal, radius=400, name='closeness400', distance='mm_len', weight='mm_len')"
+    "primal = momepy.closeness_centrality(primal, radius=400, name='closeness400', distance='mm_len', weight='mm_len')"
    ]
   },
   {
@@ -560,20 +560,20 @@
    "source": [
     "### Topological vs metric distances\n",
     "\n",
-    "Centrality can be measure topologically (ignoring physical lenght of street segments) or metrically. Moreover, in the case of local centrality, local subgraph can be also defined topologically and metrically. In the end, you have four options how to measure centralilty, illustrated on `local_betweenness_centrality`:\n",
+    "Centrality can be measure topologically (ignoring physical lenght of street segments) or metrically. Moreover, in the case of local centrality, local subgraph can be also defined topologically and metrically. In the end, you have four options how to measure centralilty, illustrated on `betweenness_centrality`:\n",
     "\n",
     "```py\n",
     "# topologically defined subgraph (5 steps) and topologically measured shortest path\n",
-    "local_betweenness_centrality(graph, radius=5, distance=None, weight=None)\n",
+    "betweenness_centrality(graph, radius=5, distance=None, weight=None)\n",
     "\n",
     "# topologically defined subgraph (5 steps) and metrically measured shortest path\n",
-    "local_betweenness_centrality(graph, radius=5, distance=None, weight='edge_length')\n",
+    "betweenness_centrality(graph, radius=5, distance=None, weight='edge_length')\n",
     "\n",
     "# metrically defined subgraph (800 meters) and topologically measured shortest path\n",
-    "local_betweenness_centrality(graph, radius=800, distance='edge_length', weight=None)\n",
+    "betweenness_centrality(graph, radius=800, distance='edge_length', weight=None)\n",
     "\n",
     "# metrically defined subgraph (800 meters) and metrically measured shortest path\n",
-    "local_betweenness_centrality(graph, radius=800, distance='edge_length', weight='edge_length')\n",
+    "betweenness_centrality(graph, radius=800, distance='edge_length', weight='edge_length')\n",
     "```"
    ]
   },
@@ -814,4 +814,4 @@
  },
  "nbformat": 4,
  "nbformat_minor": 4
-}
+}
\ No newline at end of file
diff --git a/momepy/graph.py b/momepy/graph.py
index 2ad491a..497449d 100644
--- a/momepy/graph.py
+++ b/momepy/graph.py
@@ -4,6 +4,7 @@
 # connectivity.py
 # definitions of connectivity characters
 import math
+import warnings
 
 import networkx as nx
 import numpy as np
@@ -94,7 +95,7 @@ def meshedness(graph, radius=5, name="meshedness", distance=None):
         Graph representing street network.
         Ideally generated from GeoDataFrame using :func:`momepy.gdf_to_nx`
     radius: int, optional
-        number of topological steps defining the extent of subgraph
+        Include all neighbors of distance <= radius from n
     name : str, optional
         calculated attribute name
     distance : str, optional
@@ -206,7 +207,7 @@ def cds_length(
         Graph representing street network.
         Ideally generated from GeoDataFrame using :func:`momepy.gdf_to_nx`
     radius : int
-        number of topological steps defining the extent of subgraph
+        Include all neighbors of distance <= radius from n
     mode : str (default 'sum')
         if ``'sum'``, calculate total length, if ``'mean'`` calculate mean length
     name : str, optional
@@ -341,7 +342,7 @@ def proportion(
         Graph representing street network.
         Ideally generated from GeoDataFrame using :func:`momepy.gdf_to_nx`
     radius: int
-        number of topological steps defining the extent of subgraph
+        Include all neighbors of distance <= radius from n
     three : str, optional
         attribute name for 3-way intersections proportion
     four : str, optional
@@ -430,7 +431,7 @@ def cyclomatic(graph, radius=5, name="cyclomatic", distance=None):
         Graph representing street network.
         Ideally generated from GeoDataFrame using :func:`momepy.gdf_to_nx`
     radius: int
-        number of topological steps defining the extent of subgraph
+        Include all neighbors of distance <= radius from n
     name : str, optional
         calculated attribute name
     distance : str, optional
@@ -496,7 +497,7 @@ def edge_node_ratio(graph, radius=5, name="edge_node_ratio", distance=None):
         Graph representing street network.
         Ideally generated from GeoDataFrame using :func:`momepy.gdf_to_nx`
     radius: int
-        number of topological steps defining the extent of subgraph
+        Include all neighbors of distance <= radius from n
     name : str, optional
         calculated attribute name
     distance : str, optional
@@ -564,7 +565,7 @@ def gamma(graph, radius=5, name="gamma", distance=None):
         Graph representing street network.
         Ideally generated from GeoDataFrame using :func:`momepy.gdf_to_nx`
     radius: int
-        number of topological steps defining the extent of subgraph
+        Include all neighbors of distance <= radius from n
     name : str, optional
         calculated attribute name
     distance : str, optional
@@ -746,7 +747,7 @@ def local_closeness_centrality(
         Graph representing street network.
         Ideally generated from GeoDataFrame using :func:`momepy.gdf_to_nx`
     radius: int
-        number of topological steps defining the extent of subgraph
+        Include all neighbors of distance <= radius from n
     name : str, optional
         calculated attribute name
     distance : str, optional
@@ -767,20 +768,20 @@ def local_closeness_centrality(
     >>> network_graph = mm.local_closeness_centrality(network_graph, radius=400, distance='edge_length')
 
     """
-    netx = graph.copy()
-    lengraph = len(netx)
-    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] = _closeness_centrality(
-            sub, n, length=weight, len_graph=lengraph
-        )
+    warnings.warn(
+        "local_closeness_centrality() is deprecated and will be removed in momepy 0.4.0. "
+        "Use closeness_centrality() instead.",
+        FutureWarning,
+    )
 
-    return netx
+    return closeness_centrality(
+        graph=graph, radius=radius, name=name, distance=distance, weight=weight
+    )
 
 
-def closeness_centrality(graph, name="closeness", weight="mm_len", **kwargs):
+def closeness_centrality(
+    graph, name="closeness", weight="mm_len", radius=None, distance=None, **kwargs
+):
     """
     Calculates the closeness centrality for nodes.
 
@@ -806,6 +807,12 @@ def closeness_centrality(graph, name="closeness", weight="mm_len", **kwargs):
         calculated attribute name
     weight : str (default 'mm_len')
         attribute holding the weight of edge (e.g. length, angle)
+    radius: int
+        Include all neighbors of distance <= radius from n
+    distance : str, optional
+        Use specified edge data key as distance.
+        For example, setting ``distance=’weight’`` will use the edge ``weight`` to
+        measure the distance from the node n during ego_graph generation.
     **kwargs
         kwargs for ``networkx.closeness_centrality``
 
@@ -820,14 +827,32 @@ def closeness_centrality(graph, name="closeness", weight="mm_len", **kwargs):
     """
     netx = graph.copy()
 
-    vals = nx.closeness_centrality(netx, distance=weight, **kwargs)
-    nx.set_node_attributes(netx, vals, name)
+    if radius:
+        lengraph = len(netx)
+        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] = _closeness_centrality(
+                sub, n, length=weight, len_graph=lengraph
+            )
+    else:
+        vals = nx.closeness_centrality(netx, distance=weight, **kwargs)
+        nx.set_node_attributes(netx, vals, name)
 
     return netx
 
 
 def betweenness_centrality(
-    graph, name="betweenness", mode="nodes", weight="mm_len", endpoints=True, **kwargs
+    graph,
+    name="betweenness",
+    mode="nodes",
+    weight="mm_len",
+    endpoints=True,
+    radius=None,
+    distance=None,
+    normalized=False,
+    **kwargs
 ):
     """
     Calculates the shortest-path betweenness centrality for nodes.
@@ -871,6 +896,15 @@ def betweenness_centrality(
         mode of betweenness calculation. 'node' for node-based, 'edges' for edge-based
     weight : str (default 'mm_len')
         attribute holding the weight of edge (e.g. length, angle)
+    radius: int
+        Include all neighbors of distance <= radius from n
+    distance : str, optional
+        Use specified edge data key as distance.
+        For example, setting ``distance=’weight’`` will use the edge ``weight`` to
+        measure the distance from the node n during ego_graph generation.
+    normalized : bool, optional
+        If True the betweenness values are normalized by `2/((n-1)(n-2))`,
+        where n is the number of nodes in subgraph.
     **kwargs
         kwargs for ``networkx.betweenness_centrality`` or ``networkx.edge_betweenness_centrality``
 
@@ -898,7 +932,16 @@ def betweenness_centrality(
         else:
             G.add_edge(u, v, **data)
 
-    if mode == "nodes":
+    if radius:
+        for n in tqdm(G, total=len(G)):
+            sub = nx.ego_graph(
+                G, n, radius=radius, distance=distance
+            )  # define subgraph of steps=radius
+            netx.nodes[n][name] = nx.betweenness_centrality(
+                sub, weight=weight, normalized=normalized, **kwargs
+            )[n]
+
+    elif mode == "nodes":
         vals = nx.betweenness_centrality(
             G, weight=weight, endpoints=endpoints, **kwargs
         )
@@ -955,7 +998,7 @@ def local_betweenness_centrality(
         Graph representing street network.
         Ideally generated from GeoDataFrame using :func:`momepy.gdf_to_nx`
     radius: int
-        number of topological steps defining the extent of subgraph
+        Include all neighbors of distance <= radius from n
     name : str, optional
         calculated attribute name
     distance : str, optional
@@ -981,26 +1024,21 @@ def local_betweenness_centrality(
     >>> network_graph = mm.local_betweenness_centrality(network_graph, radius=800, distance='edge_length')
 
     """
-    netx = graph.copy()
-
-    # has to be Graph not MultiGraph as MG is not supported by networkx2.4
-    G = nx.Graph()
-    for u, v, k, data in netx.edges(data=True, keys=True):
-        if G.has_edge(u, v) and weight is not None:
-            if G[u][v][weight] > netx[u][v][k][weight]:
-                nx.set_edge_attributes(G, {(u, v): data})
-        else:
-            G.add_edge(u, v, **data)
-
-    for n in tqdm(G, total=len(G)):
-        sub = nx.ego_graph(
-            G, n, radius=radius, distance=distance
-        )  # define subgraph of steps=radius
-        netx.nodes[n][name] = nx.betweenness_centrality(
-            sub, weight=weight, normalized=normalized, **kwargs
-        )[n]
-
-    return netx
+    warnings.warn(
+        "local_betweenness_centrality() is deprecated and will be removed in momepy 0.4.0. "
+        "Use betweenness_centrality() instead.",
+        FutureWarning,
+    )
+
+    return betweenness_centrality(
+        graph,
+        radius=radius,
+        name=name,
+        distance=distance,
+        weight=weight,
+        normalized=normalized,
+        **kwargs
+    )
 
 
 def _euclidean(n, m):
@@ -1038,7 +1076,12 @@ def _straightness_centrality(G, weight, normalized=True):
 
 
 def straightness_centrality(
-    graph, weight="mm_len", normalized=True, name="straightness"
+    graph,
+    weight="mm_len",
+    normalized=True,
+    name="straightness",
+    radius=None,
+    distance=None,
 ):
     """
     Calculates the straightness centrality for nodes.
@@ -1059,9 +1102,16 @@ def straightness_centrality(
     weight : str (default 'mm_len')
         attribute holding length of edge
     normalized : bool
-        normalize to number of nodes-1 in connected part
+        normalize to number of nodes-1 in connected part (for local straightness
+        is recommended to set to normalized False)
     name : str, optional
         calculated attribute name
+    radius: int
+        Include all neighbors of distance <= radius from n
+    distance : str, optional
+        Use specified edge data key as distance.
+        For example, setting ``distance=’weight’`` will use the edge ``weight`` to
+        measure the distance from the node n during ego_graph generation.
 
     Returns
     -------
@@ -1074,8 +1124,17 @@ def straightness_centrality(
     """
     netx = graph.copy()
 
-    vals = _straightness_centrality(netx, weight=weight, normalized=normalized)
-    nx.set_node_attributes(netx, vals, name)
+    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] = _straightness_centrality(
+                sub, weight=weight, normalized=normalized
+            )[n]
+    else:
+        vals = _straightness_centrality(netx, weight=weight, normalized=normalized)
+        nx.set_node_attributes(netx, vals, name)
 
     return netx
 
@@ -1103,7 +1162,7 @@ def local_straightness_centrality(
         Graph representing street network.
         Ideally generated from GeoDataFrame using :func:`momepy.gdf_to_nx`
     radius: int
-        number of topological steps defining the extent of subgraph
+        Include all neighbors of distance <= radius from n
     name : str, optional
         calculated attribute name
     distance : str, optional
@@ -1125,16 +1184,15 @@ def local_straightness_centrality(
     >>> network_graph = mm.local_straightness_centrality(network_graph, radius=400, distance='edge_length')
 
     """
-    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] = _straightness_centrality(
-            sub, weight=weight, normalized=False
-        )[n]
-
-    return netx
+    warnings.warn(
+        "local_straightness_centrality() is deprecated and will be removed in momepy 0.4.0. "
+        "Use straightness_centrality() instead.",
+        FutureWarning,
+    )
+
+    return straightness_centrality(
+        graph=graph, radius=radius, name=name, distance=distance, weight=weight
+    )
 
 
 def subgraph(


Test patch:
diff --git a/tests/test_graph.py b/tests/test_graph.py
index b78b39d..67a54dd 100644
--- a/tests/test_graph.py
+++ b/tests/test_graph.py
@@ -85,18 +85,21 @@ class TestGraph:
         assert mm.gamma(self.network, radius=None) == 0.43209876543209874
 
     def test_local_closeness_centrality(self):
-        net = mm.local_closeness_centrality(self.network)
-        check = 0.27557319223985893
-        assert net.nodes[(1603650.450422848, 6464368.600601688)]["closeness"] == check
-        net2 = mm.local_closeness_centrality(self.network, weight="mm_len")
-        check2 = 0.0015544070362478774
-        assert net2.nodes[(1603650.450422848, 6464368.600601688)]["closeness"] == check2
+        with pytest.warns(FutureWarning):
+            mm.local_closeness_centrality(self.network)
 
     def test_closeness_centrality(self):
         net = mm.closeness_centrality(self.network, weight="mm_len")
         check = 0.0016066095164175716
         assert net.nodes[(1603650.450422848, 6464368.600601688)]["closeness"] == check
 
+        net = mm.closeness_centrality(self.network, radius=5, weight=None)
+        check = 0.27557319223985893
+        assert net.nodes[(1603650.450422848, 6464368.600601688)]["closeness"] == check
+        net2 = mm.closeness_centrality(self.network, weight="mm_len", radius=5)
+        check2 = 0.0015544070362478774
+        assert net2.nodes[(1603650.450422848, 6464368.600601688)]["closeness"] == check2
+
     def test_betweenness_centrality(self):
         net = mm.betweenness_centrality(self.network)
         net2 = mm.betweenness_centrality(self.network, mode="edges")
@@ -120,18 +123,21 @@ class TestGraph:
             == ang_b
         )
 
-    def test_local_betweenness_centrality(self):
-        net = mm.local_betweenness_centrality(self.network)
+        net = mm.betweenness_centrality(self.network, radius=5, weight=None)
         check = 53.74999999999999
         assert net.nodes[(1603650.450422848, 6464368.600601688)]["betweenness"] == check
-        net2 = mm.local_betweenness_centrality(
-            self.network, weight="mm_len", normalized=True
+        net2 = mm.betweenness_centrality(
+            self.network, radius=5, weight="mm_len", normalized=True
         )
         check2 = 0.21333333333333335
         assert (
             net2.nodes[(1603650.450422848, 6464368.600601688)]["betweenness"] == check2
         )
 
+    def test_local_betweenness_centrality(self):
+        with pytest.warns(FutureWarning):
+            mm.local_betweenness_centrality(self.network)
+
     def test_straightness_centrality(self):
         net = mm.straightness_centrality(self.network)
         net2 = mm.straightness_centrality(
@@ -150,13 +156,16 @@ class TestGraph:
             net3.nodes[(1603650.450422848, 6464368.600601688)]["straightness"] == check
         )
 
-    def test_local_straightness_centrality(self):
-        net = mm.local_straightness_centrality(self.network)
+        net = mm.straightness_centrality(self.network, radius=5, normalized=False)
         check = 0.8614261420539604
         assert (
             net.nodes[(1603650.450422848, 6464368.600601688)]["straightness"] == check
         )
 
+    def test_local_straightness_centrality(self):
+        with pytest.warns(FutureWarning):
+            mm.local_straightness_centrality(self.network)
+
     def test_mean_nodes(self):
         net = mm.straightness_centrality(self.network)
         mm.mean_nodes(net, "straightness")