Repository: martinfleis/momepy
Pull request #87 (2019-09-28 15:51:59)

ENH: local straightness & betweenness centrality
Add local betweenness (nodes only) and local straightness

codecov[bot]: # [Codecov](https://codecov.io/gh/martinfleis/momepy/pull/87?src=pr&el=h1) Report
> Merging [#87](https://codecov.io/gh/martinfleis/momepy/pull/87?src=pr&el=desc) into [master](https://codecov.io/gh/martinfleis/momepy/commit/29a64a13c972a67e7761de85f51f22baf005d4b5?src=pr&el=desc) will **decrease** coverage by `0.03%`.
> The diff coverage is `90%`.

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

```diff
@@            Coverage Diff             @@
##           master      #87      +/-   ##
==========================================
- Coverage    93.3%   93.26%   -0.04%     
==========================================
  Files           9        9              
  Lines        2418     2436      +18     
==========================================
+ Hits         2256     2272      +16     
- Misses        162      164       +2
```


| [Impacted Files](https://codecov.io/gh/martinfleis/momepy/pull/87?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [momepy/graph.py](https://codecov.io/gh/martinfleis/momepy/pull/87/diff?src=pr&el=tree#diff-bW9tZXB5L2dyYXBoLnB5) | `97.5% <90%> (-0.7%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/martinfleis/momepy/pull/87?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/87?src=pr&el=footer). Last update [29a64a1...e2d1d78](https://codecov.io/gh/martinfleis/momepy/pull/87?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 73b8b9e..48d6ee6 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -98,12 +98,14 @@ graph
 
    betweenness_centrality
    cds_length
+   closeness_centrality
    clustering
    cyclomatic
    edge_node_ratio
    gamma
-   global_closeness_centrality
+   local_betweenness_centrality
    local_closeness_centrality
+   local_straightness_centrality
    mean_node_degree
    mean_node_dist
    mean_nodes
diff --git a/momepy/graph.py b/momepy/graph.py
index abcd4f2..d26df6b 100644
--- a/momepy/graph.py
+++ b/momepy/graph.py
@@ -21,8 +21,10 @@ __all__ = [
     "gamma",
     "clustering",
     "local_closeness_centrality",
-    "global_closeness_centrality",
+    "closeness_centrality",
     "betweenness_centrality",
+    "local_betweenness_centrality",
+    "local_straightness_centrality",
     "straightness_centrality",
     "subgraph",
     "mean_nodes",
@@ -737,7 +739,7 @@ def local_closeness_centrality(
     return netx
 
 
-def global_closeness_centrality(graph, name="closeness", weight="mm_len", **kwargs):
+def closeness_centrality(graph, name="closeness", weight="mm_len", **kwargs):
     """
     Calculates the closeness centrality for nodes.
 
@@ -765,7 +767,7 @@ def global_closeness_centrality(graph, name="closeness", weight="mm_len", **kwar
 
     Examples
     --------
-    >>> network_graph = mm.global_closeness_centrality(network_graph)
+    >>> network_graph = mm.closeness_centrality(network_graph)
     """
     netx = graph.copy()
 
@@ -814,7 +816,7 @@ def betweenness_centrality(
 
     Note
     ----
-    In case of angular betweenness, implementation follows "Tasos Implementation".
+    In case of angular betweenness, implementation is based on "Tasos Implementation".
     """
     netx = graph.copy()
 
@@ -848,6 +850,76 @@ def betweenness_centrality(
     return netx
 
 
+def local_betweenness_centrality(
+    graph,
+    radius=5,
+    name="betweenness",
+    distance=None,
+    weight=None,
+    normalized=False,
+    **kwargs
+):
+    """
+    Calculates the shortest-path betweenness centrality for nodes within subgraph.
+
+    Subgraph is generated around each node within set radius. If distance=None,
+    radius will define topological distance, otherwise it uses values in distance
+    attribute.
+
+    .. math::
+
+
+    Parameters
+    ----------
+    graph : networkx.Graph
+        Graph representing street network.
+        Ideally genereated from GeoDataFrame using :py:func:`momepy.gdf_to_nx`
+    radius: int
+        number of topological steps defining the extent of subgraph
+    name : str, optional
+        calculated attribute name
+    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.
+    weight : str, optional
+        Use the specified edge attribute as the edge distance in shortest
+        path calculations in closeness centrality algorithm
+    **kwargs
+        kwargs for ``networkx.betweenness_centrality_subset``
+
+    Returns
+    -------
+    Graph
+        networkx.Graph
+
+    Examples
+    --------
+    >>> 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, undirected=True, distance=distance
+        )  # define subgraph of steps=radius
+        netx.nodes[n][name] = nx.betweenness_centrality_subset(
+            G, sub.nodes(), sub.nodes(), weight=weight, normalized=normalized, **kwargs
+        )[n]
+
+    return netx
+
+
 def _euclidean(n, m):
     """helper for straightness"""
     return math.sqrt((n[0] - m[0]) ** 2 + (n[1] - m[1]) ** 2)
@@ -859,7 +931,7 @@ def _straightness_centrality(G, weight, normalized=True):
     """
     straightness_centrality = {}
 
-    for n in tqdm(G.nodes(), total=G.number_of_nodes()):
+    for n in G.nodes():
         straightness = 0
         sp = nx.single_source_dijkstra_path_length(G, n, weight=weight)
 
@@ -922,6 +994,59 @@ def straightness_centrality(
     return netx
 
 
+def local_straightness_centrality(
+    graph, radius=5, name="straightness", distance=None, weight="mm_len"
+):
+    """
+    Calculates local straightness for each node based on the defined distance.
+
+    Subgraph is generated around each node within set radius. If distance=None,
+    radius will define topological distance, otherwise it uses values in distance
+    attribute.
+
+    .. math::
+
+
+    Parameters
+    ----------
+    graph : networkx.Graph
+        Graph representing street network.
+        Ideally genereated from GeoDataFrame using :py:func:`momepy.gdf_to_nx`
+    radius: int
+        number of topological steps defining the extent of subgraph
+    name : str, optional
+        calculated attribute name
+    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.
+    weight : str, optional
+      Use the specified edge attribute as the edge distance in shortest
+      path calculations in closeness centrality algorithm
+
+    Returns
+    -------
+    Graph
+        networkx.Graph
+
+
+    Examples
+    --------
+    >>> 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, undirected=True, distance=distance
+        )  # define subgraph of steps=radius
+        netx.nodes[n][name] = _straightness_centrality(
+            sub, weight=weight, normalized=False
+        )[n]
+
+    return netx
+
+
 def subgraph(
     graph,
     radius=5,


Test patch:
diff --git a/tests/test_graph.py b/tests/test_graph.py
index f229329..e4c1c06 100644
--- a/tests/test_graph.py
+++ b/tests/test_graph.py
@@ -80,8 +80,8 @@ class TestGraph:
         check2 = 0.0015544070362478774
         assert net2.nodes[(1603650.450422848, 6464368.600601688)]["closeness"] == check2
 
-    def test_global_closeness_centrality(self):
-        net = mm.global_closeness_centrality(self.network, weight="mm_len")
+    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
 
@@ -108,6 +108,18 @@ class TestGraph:
             == ang_b
         )
 
+    def test_local_betweenness_centrality(self):
+        net = mm.local_betweenness_centrality(self.network)
+        check = 53.74999999999999
+        assert net.nodes[(1603650.450422848, 6464368.600601688)]["betweenness"] == check
+        net2 = mm.local_betweenness_centrality(
+            self.network, weight="mm_len", normalized=True
+        )
+        check2 = 0.1693121693121693
+        assert (
+            net2.nodes[(1603650.450422848, 6464368.600601688)]["betweenness"] == check2
+        )
+
     def test_straightness_centrality(self):
         net = mm.straightness_centrality(self.network)
         net2 = mm.straightness_centrality(
@@ -120,6 +132,13 @@ class TestGraph:
         )
         assert net2.nodes[(1603650.450422848, 6464368.600601688)]["nonnorm"] == nonnorm
 
+    def test_local_straightness_centrality(self):
+        net = mm.local_straightness_centrality(self.network)
+        check = 0.8614261420539604
+        assert (
+            net.nodes[(1603650.450422848, 6464368.600601688)]["straightness"] == check
+        )
+
     def test_mean_nodes(self):
         net = mm.straightness_centrality(self.network)
         mm.mean_nodes(net, "straightness")