From 7831be5520d8ecacd7ab7ebf1433b4fa624ae8a9 Mon Sep 17 00:00:00 2001 From: Manuel-Knepper Date: Fri, 6 Mar 2026 09:56:10 +0100 Subject: [PATCH 1/8] added find_potential_gaps to functions.py and implemented it in FixBikeMVP.ipynb --- FixBikeMVP.ipynb | 87 +++++++++++++++++++---------------------- fixbikenet/functions.py | 34 +++++++++++++++- 2 files changed, 73 insertions(+), 48 deletions(-) diff --git a/FixBikeMVP.ipynb b/FixBikeMVP.ipynb index f6e51f6..6b9f66c 100644 --- a/FixBikeMVP.ipynb +++ b/FixBikeMVP.ipynb @@ -15,8 +15,8 @@ "id": "1b6a69660258f567", "metadata": { "ExecuteTime": { - "end_time": "2026-03-05T14:44:35.560984Z", - "start_time": "2026-03-05T14:44:34.251482Z" + "end_time": "2026-03-06T08:48:12.024606Z", + "start_time": "2026-03-06T08:48:10.488873Z" } }, "source": [ @@ -48,8 +48,8 @@ "id": "9173dd015d3280f3", "metadata": { "ExecuteTime": { - "end_time": "2026-03-05T14:44:43.976210Z", - "start_time": "2026-03-05T14:44:43.954651Z" + "end_time": "2026-03-06T08:48:20.299845Z", + "start_time": "2026-03-06T08:48:20.285621Z" } }, "source": [ @@ -79,8 +79,8 @@ "id": "a9a51035deb92425", "metadata": { "ExecuteTime": { - "end_time": "2026-03-05T14:45:07.394672Z", - "start_time": "2026-03-05T14:44:45.668432Z" + "end_time": "2026-03-06T08:48:42.905367Z", + "start_time": "2026-03-06T08:48:22.252411Z" } }, "source": [ @@ -101,8 +101,8 @@ "id": "a8b4eed5", "metadata": { "ExecuteTime": { - "end_time": "2026-03-05T14:45:15.988833Z", - "start_time": "2026-03-05T14:45:13.924067Z" + "end_time": "2026-03-06T08:48:54.125749Z", + "start_time": "2026-03-06T08:48:52.130077Z" } }, "source": [ @@ -137,8 +137,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2026-03-05T14:45:22.101582Z", - "start_time": "2026-03-05T14:45:21.982420Z" + "end_time": "2026-03-06T08:48:55.768235Z", + "start_time": "2026-03-06T08:48:55.661421Z" } }, "cell_type": "code", @@ -164,8 +164,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2026-03-05T14:47:06.726898Z", - "start_time": "2026-03-05T14:45:23.501207Z" + "end_time": "2026-03-06T08:50:42.720331Z", + "start_time": "2026-03-06T08:48:57.361618Z" } }, "cell_type": "code", @@ -179,8 +179,8 @@ "id": "e2670fe4", "metadata": { "ExecuteTime": { - "end_time": "2026-03-05T14:47:09.810662Z", - "start_time": "2026-03-05T14:47:09.732303Z" + "end_time": "2026-03-06T08:50:44.800228Z", + "start_time": "2026-03-06T08:50:44.711959Z" } }, "source": [ @@ -222,8 +222,8 @@ "id": "ac84d5bc", "metadata": { "ExecuteTime": { - "end_time": "2026-03-05T14:47:11.808025Z", - "start_time": "2026-03-05T14:47:11.457935Z" + "end_time": "2026-03-06T08:50:47.242578Z", + "start_time": "2026-03-06T08:50:46.846460Z" } }, "source": [ @@ -248,8 +248,8 @@ "id": "c6bc1fa5", "metadata": { "ExecuteTime": { - "end_time": "2026-03-05T14:47:13.884562Z", - "start_time": "2026-03-05T14:47:13.788483Z" + "end_time": "2026-03-06T08:50:49.530849Z", + "start_time": "2026-03-06T08:50:49.411693Z" } }, "source": "G = weigh_edges(G, penalty)", @@ -271,8 +271,8 @@ "id": "602e50f5", "metadata": { "ExecuteTime": { - "end_time": "2026-03-05T14:47:22.674995Z", - "start_time": "2026-03-05T14:47:22.515390Z" + "end_time": "2026-03-06T08:50:51.774494Z", + "start_time": "2026-03-06T08:50:51.618176Z" } }, "source": [ @@ -288,7 +288,7 @@ ] } ], - "execution_count": 11 + "execution_count": 10 }, { "cell_type": "markdown", @@ -297,39 +297,32 @@ "source": [ "### Create list of all potential gaps\n", "\n", - "Here defined as: contact node pair combinations **within `maxgap` euclidean distance from each other** " - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "a02eeca1", - "metadata": {}, - "outputs": [], - "source": [ - "potential_gaps = []\n", - "\n", - "for node in contact_nodes:\n", - " node_buffer = nodes_gdf.loc[node, \"geometry\"].buffer(maxgap)\n", - " q = nodes_gdf.sindex.query(node_buffer, predicate=\"intersects\")\n", - " neighbours = list(nodes_gdf.iloc[q].index)\n", - " # convention: sort by ascending OSMID...\n", - " node_pairs = [tuple(sorted(z)) for z in zip([node]*len(neighbours), neighbours)]\n", - " potential_gaps += node_pairs\n", - "\n", - "# ... so that we can easily deduplicate\n", - "potential_gaps = list(set(potential_gaps))" + "Here defined as: contact node pair combinations **within `maxgap` euclidean distance from each other**" ] }, { "cell_type": "code", - "execution_count": null, "id": "a01da088", - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-06T08:51:01.682482Z", + "start_time": "2026-03-06T08:50:59.188608Z" + } + }, "source": [ + "potential_gaps = find_potential_gaps(contact_nodes, nodes_gdf, maxgap)\n", "print(\"potential gaps found:\", len(potential_gaps))" - ] + ], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "potential gaps found: 157026\n" + ] + } + ], + "execution_count": 11 }, { "cell_type": "markdown", diff --git a/fixbikenet/functions.py b/fixbikenet/functions.py index fb6336a..e010fa5 100644 --- a/fixbikenet/functions.py +++ b/fixbikenet/functions.py @@ -106,4 +106,36 @@ def find_contact_nodes(G): pbis = set([G.edges[edge]["pbi"] for edge in G.edges(node)]) if len(pbis) == 2: contact_nodes.append(node) - return contact_nodes \ No newline at end of file + return contact_nodes + +def find_potential_gaps(contact_nodes, nodes_gdf, maxgap): + """ + finds potential gaps in protected bicycle network, corresponding to two contact nodes that are within maxgap euclidean distance of each other + + Parameters + ---------- + contact_nodes: list + list of all nodes that fulfill criteria to be a contact node + nodes_gdf: geopandas.GeoDataFrame + all nodes in street network + maxgap: int + user defined maximal euclidean distance between two contact nodes + + Returns + ------- + potential_gaps: list + all unique potential gaps in protected bicycle network + """ + potential_gaps = [] + + for node in contact_nodes: + node_buffer = nodes_gdf.loc[node, "geometry"].buffer(maxgap) + q = nodes_gdf.sindex.query(node_buffer, predicate="intersects") + neighbours = list(nodes_gdf.iloc[q].index) + # convention: sort by ascending OSMID... + node_pairs = [tuple(sorted(z)) for z in zip([node] * len(neighbours), neighbours)] + potential_gaps += node_pairs + + # ... so that we can easily deduplicate + potential_gaps = list(set(potential_gaps)) + return potential_gaps \ No newline at end of file From 4737d329cb2c490f5e9861b329955327592ea1fd Mon Sep 17 00:00:00 2001 From: Manuel-Knepper Date: Fri, 6 Mar 2026 10:41:37 +0100 Subject: [PATCH 2/8] added test_find_potential_gaps to test_functions.py and optimized find_potential_gaps slightly by removing self-neighbours --- fixbikenet/functions.py | 1 + tests/test_functions.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/fixbikenet/functions.py b/fixbikenet/functions.py index e010fa5..4cb4815 100644 --- a/fixbikenet/functions.py +++ b/fixbikenet/functions.py @@ -132,6 +132,7 @@ def find_potential_gaps(contact_nodes, nodes_gdf, maxgap): node_buffer = nodes_gdf.loc[node, "geometry"].buffer(maxgap) q = nodes_gdf.sindex.query(node_buffer, predicate="intersects") neighbours = list(nodes_gdf.iloc[q].index) + neighbours.remove(node) # convention: sort by ascending OSMID... node_pairs = [tuple(sorted(z)) for z in zip([node] * len(neighbours), neighbours)] potential_gaps += node_pairs diff --git a/tests/test_functions.py b/tests/test_functions.py index 802587a..4c82d8d 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -1,6 +1,8 @@ import pytest import networkx as nx from networkx.utils.misc import graphs_equal +import geopandas as gpd +from shapely.geometry import Point from fixbikenet.functions import * @@ -83,4 +85,29 @@ def create_validation_contact_nodes(): return contact_nodes def test_find_contact_nodes(create_weighted_graph, create_validation_contact_nodes): - assert find_contact_nodes(create_weighted_graph) == create_validation_contact_nodes \ No newline at end of file + assert find_contact_nodes(create_weighted_graph) == create_validation_contact_nodes + +@pytest.fixture +def create_test_contact_nodes(): + contact_nodes = [1,3,4] + return contact_nodes + +@pytest.fixture +def create_maxgap(): + maxgap = 50 + return maxgap + +@pytest.fixture +def create_test_nodes_gdf(): + nodes ={'osmid':[1,2,3,4],'geometry':[Point(1,1), Point(1000,1000), Point(3,3), Point(2000,2000)]} + nodes_gdf = gpd.GeoDataFrame(nodes) + nodes_gdf.set_index('osmid',inplace=True) + return nodes_gdf + +@pytest.fixture +def create_validation_gaps(): + gaps = [(1,3)] + return gaps + +def test_find_potential_gaps(create_test_contact_nodes, create_maxgap, create_test_nodes_gdf, create_validation_gaps): + assert find_potential_gaps(create_test_contact_nodes,create_test_nodes_gdf, create_maxgap) == create_validation_gaps \ No newline at end of file From 08dfd28afc31dc329d1da5f94f0e3162c06b05c0 Mon Sep 17 00:00:00 2001 From: Manuel-Knepper Date: Fri, 6 Mar 2026 10:57:11 +0100 Subject: [PATCH 3/8] added find_actual_gaps to functions.py and implemented it in FixBikeMVP.ipynb --- FixBikeMVP.ipynb | 82 ++++++++++++++++------------------------- fixbikenet/functions.py | 42 ++++++++++++++++++++- 2 files changed, 72 insertions(+), 52 deletions(-) diff --git a/FixBikeMVP.ipynb b/FixBikeMVP.ipynb index 6b9f66c..140a6ad 100644 --- a/FixBikeMVP.ipynb +++ b/FixBikeMVP.ipynb @@ -15,15 +15,14 @@ "id": "1b6a69660258f567", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T08:48:12.024606Z", - "start_time": "2026-03-06T08:48:10.488873Z" + "end_time": "2026-03-06T09:53:18.761878Z", + "start_time": "2026-03-06T09:53:17.342071Z" } }, "source": [ "# import packages\n", "import pandas as pd\n", "import osmnx as ox\n", - "import networkx as nx\n", "import random\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", @@ -48,8 +47,8 @@ "id": "9173dd015d3280f3", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T08:48:20.299845Z", - "start_time": "2026-03-06T08:48:20.285621Z" + "end_time": "2026-03-06T09:53:20.120595Z", + "start_time": "2026-03-06T09:53:20.108213Z" } }, "source": [ @@ -79,8 +78,8 @@ "id": "a9a51035deb92425", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T08:48:42.905367Z", - "start_time": "2026-03-06T08:48:22.252411Z" + "end_time": "2026-03-06T09:53:42.569977Z", + "start_time": "2026-03-06T09:53:21.836863Z" } }, "source": [ @@ -101,8 +100,8 @@ "id": "a8b4eed5", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T08:48:54.125749Z", - "start_time": "2026-03-06T08:48:52.130077Z" + "end_time": "2026-03-06T09:54:07.221253Z", + "start_time": "2026-03-06T09:54:04.981611Z" } }, "source": [ @@ -137,8 +136,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T08:48:55.768235Z", - "start_time": "2026-03-06T08:48:55.661421Z" + "end_time": "2026-03-06T09:54:08.675029Z", + "start_time": "2026-03-06T09:54:08.553649Z" } }, "cell_type": "code", @@ -164,8 +163,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T08:50:42.720331Z", - "start_time": "2026-03-06T08:48:57.361618Z" + "end_time": "2026-03-06T09:55:56.773442Z", + "start_time": "2026-03-06T09:54:10.249270Z" } }, "cell_type": "code", @@ -179,8 +178,8 @@ "id": "e2670fe4", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T08:50:44.800228Z", - "start_time": "2026-03-06T08:50:44.711959Z" + "end_time": "2026-03-06T09:55:58.859078Z", + "start_time": "2026-03-06T09:55:58.777509Z" } }, "source": [ @@ -222,8 +221,8 @@ "id": "ac84d5bc", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T08:50:47.242578Z", - "start_time": "2026-03-06T08:50:46.846460Z" + "end_time": "2026-03-06T09:56:01.289535Z", + "start_time": "2026-03-06T09:56:00.886262Z" } }, "source": [ @@ -248,8 +247,8 @@ "id": "c6bc1fa5", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T08:50:49.530849Z", - "start_time": "2026-03-06T08:50:49.411693Z" + "end_time": "2026-03-06T09:56:02.706621Z", + "start_time": "2026-03-06T09:56:02.597190Z" } }, "source": "G = weigh_edges(G, penalty)", @@ -271,8 +270,8 @@ "id": "602e50f5", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T08:50:51.774494Z", - "start_time": "2026-03-06T08:50:51.618176Z" + "end_time": "2026-03-06T09:56:04.438268Z", + "start_time": "2026-03-06T09:56:04.279504Z" } }, "source": [ @@ -305,8 +304,8 @@ "id": "a01da088", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T08:51:01.682482Z", - "start_time": "2026-03-06T08:50:59.188608Z" + "end_time": "2026-03-06T09:56:08.870572Z", + "start_time": "2026-03-06T09:56:06.345078Z" } }, "source": [ @@ -318,7 +317,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "potential gaps found: 157026\n" + "potential gaps found: 142482\n" ] } ], @@ -338,35 +337,16 @@ }, { "cell_type": "code", - "execution_count": null, "id": "43c578c3", - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-06T09:56:20.747913Z", + "start_time": "2026-03-06T09:56:15.793309Z" + } + }, + "source": "found_gaps, found_gaps_nsp = find_actual_gaps(G, potential_gaps)", "outputs": [], - "source": [ - "pbi_dict = nx.get_edge_attributes(G, \"pbi\")\n", - "\n", - "found_gaps = []\n", - "found_gaps_nsp = [] # naive shortest paths (by length, in node list format)\n", - "\n", - "for i, gap in enumerate(potential_gaps):\n", - " u, v = gap\n", - " nodelist = nx.shortest_path(\n", - " G=G,\n", - " source=u,\n", - " target=v, \n", - " weight=\"length\"\n", - " )\n", - " pbis = set([pbi_dict[tuple(sorted(z))] for z in zip(nodelist, nodelist[1:])])\n", - " \n", - " # confirm that it is an actual gap if it consists only of pbi==0 infra:\n", - " if pbis==set([0]): \n", - " found_gaps.append(gap)\n", - " found_gaps_nsp.append(nodelist)\n", - " \n", - " # # (manual timer)\n", - " # if i % 100000 == 0:\n", - " # print(i)\n" - ] + "execution_count": 12 }, { "cell_type": "markdown", diff --git a/fixbikenet/functions.py b/fixbikenet/functions.py index 4cb4815..1641c7d 100644 --- a/fixbikenet/functions.py +++ b/fixbikenet/functions.py @@ -1,4 +1,5 @@ import yaml +import networkx as nx def map_edges_to_bike_infrastructure(g): """ @@ -139,4 +140,43 @@ def find_potential_gaps(contact_nodes, nodes_gdf, maxgap): # ... so that we can easily deduplicate potential_gaps = list(set(potential_gaps)) - return potential_gaps \ No newline at end of file + return potential_gaps + +def find_actual_gaps(G, potential_gaps): + """ + determines which potential gaps are actual gaps by finding paths between all contact nodes and only keeping the gaps that have no protected bike infrastructure + + Parameters + ---------- + G: networkx.Graph + undirected simple graph representing the street network with weighted edges + potential_gaps: list + all unique potential gaps in protected bicycle network + + Returns + ------- + found_gaps: list + list of all gaps in protected bicycle network + found_gaps_nsp: list + list of paths in network for all gaps in protected bicycle networ + """ + pbi_dict = nx.get_edge_attributes(G, "pbi") + + found_gaps = [] + found_gaps_nsp = [] # naive shortest paths (by length, in node list format) + + for i, gap in enumerate(potential_gaps): + u, v = gap + nodelist = nx.shortest_path( + G=G, + source=u, + target=v, + weight="length" + ) + pbis = set([pbi_dict[tuple(sorted(z))] for z in zip(nodelist, nodelist[1:])]) + + # confirm that it is an actual gap if it consists only of pbi==0 infra: + if pbis == set([0]): + found_gaps.append(gap) + found_gaps_nsp.append(nodelist) + return found_gaps, found_gaps_nsp \ No newline at end of file From ccdedcaeb1e5195e93dc2564d4c9065e66cbb15f Mon Sep 17 00:00:00 2001 From: Manuel-Knepper Date: Fri, 6 Mar 2026 11:18:51 +0100 Subject: [PATCH 4/8] added test_find_actual_gaps to test_functions.py --- fixbikenet/functions.py | 2 +- tests/test_functions.py | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/fixbikenet/functions.py b/fixbikenet/functions.py index 1641c7d..b49f44e 100644 --- a/fixbikenet/functions.py +++ b/fixbikenet/functions.py @@ -158,7 +158,7 @@ def find_actual_gaps(G, potential_gaps): found_gaps: list list of all gaps in protected bicycle network found_gaps_nsp: list - list of paths in network for all gaps in protected bicycle networ + list of paths in network for all gaps in protected bicycle network """ pbi_dict = nx.get_edge_attributes(G, "pbi") diff --git a/tests/test_functions.py b/tests/test_functions.py index 4c82d8d..5dbc8d6 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -110,4 +110,28 @@ def create_validation_gaps(): return gaps def test_find_potential_gaps(create_test_contact_nodes, create_maxgap, create_test_nodes_gdf, create_validation_gaps): - assert find_potential_gaps(create_test_contact_nodes,create_test_nodes_gdf, create_maxgap) == create_validation_gaps \ No newline at end of file + assert find_potential_gaps(create_test_contact_nodes,create_test_nodes_gdf, create_maxgap) == create_validation_gaps + +@pytest.fixture +def create_graph_for_routing(): + G = nx.Graph() + G.add_edges_from([(1, 2), (1, 3), (2, 3), (3,4)]) + attributes = {(1, 2): {'length': 10, 'pbi': True}, + (1, 3): {'length': 4, 'pbi': False}, + (2, 3): {'length': 5, 'pbi': True}, + (3, 4): {'length': 6, 'pbi': False}} + nx.set_edge_attributes(G, attributes) + return G + +@pytest.fixture +def create_potential_gaps(): + potential_gaps = [(1,4), (2,3), (3,4)] + return potential_gaps + +@pytest.fixture +def create_validation_found_gap_paths(): + found_gap_paths = ([(1,4), (3,4)], [[1,3,4],[3,4]]) + return found_gap_paths + +def test_find_actual_gaps(create_graph_for_routing, create_potential_gaps, create_validation_found_gap_paths): + assert find_actual_gaps(create_graph_for_routing, create_potential_gaps) == create_validation_found_gap_paths \ No newline at end of file From 819231fc5f215d4004249a4be8beaca65834de08 Mon Sep 17 00:00:00 2001 From: Manuel-Knepper Date: Fri, 6 Mar 2026 11:35:21 +0100 Subject: [PATCH 5/8] added compute_local_betweenness_centrality to functions.py and implemented it in FixBikeMVP.ipynb --- FixBikeMVP.ipynb | 100 ++++++++++++++++------------------------ fixbikenet/functions.py | 49 +++++++++++++++++++- tests/test_functions.py | 1 - 3 files changed, 89 insertions(+), 61 deletions(-) diff --git a/FixBikeMVP.ipynb b/FixBikeMVP.ipynb index 140a6ad..502962f 100644 --- a/FixBikeMVP.ipynb +++ b/FixBikeMVP.ipynb @@ -15,15 +15,14 @@ "id": "1b6a69660258f567", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T09:53:18.761878Z", - "start_time": "2026-03-06T09:53:17.342071Z" + "end_time": "2026-03-06T10:30:52.577353Z", + "start_time": "2026-03-06T10:30:51.304365Z" } }, "source": [ "# import packages\n", "import pandas as pd\n", "import osmnx as ox\n", - "import random\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import os\n", @@ -47,8 +46,8 @@ "id": "9173dd015d3280f3", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T09:53:20.120595Z", - "start_time": "2026-03-06T09:53:20.108213Z" + "end_time": "2026-03-06T10:30:53.899717Z", + "start_time": "2026-03-06T10:30:53.885353Z" } }, "source": [ @@ -78,8 +77,8 @@ "id": "a9a51035deb92425", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T09:53:42.569977Z", - "start_time": "2026-03-06T09:53:21.836863Z" + "end_time": "2026-03-06T10:31:16.923555Z", + "start_time": "2026-03-06T10:30:55.992926Z" } }, "source": [ @@ -100,8 +99,8 @@ "id": "a8b4eed5", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T09:54:07.221253Z", - "start_time": "2026-03-06T09:54:04.981611Z" + "end_time": "2026-03-06T10:31:22.616118Z", + "start_time": "2026-03-06T10:31:20.483032Z" } }, "source": [ @@ -136,8 +135,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T09:54:08.675029Z", - "start_time": "2026-03-06T09:54:08.553649Z" + "end_time": "2026-03-06T10:31:24.281676Z", + "start_time": "2026-03-06T10:31:24.158324Z" } }, "cell_type": "code", @@ -163,8 +162,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T09:55:56.773442Z", - "start_time": "2026-03-06T09:54:10.249270Z" + "end_time": "2026-03-06T10:33:12.573669Z", + "start_time": "2026-03-06T10:31:26.009329Z" } }, "cell_type": "code", @@ -178,8 +177,8 @@ "id": "e2670fe4", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T09:55:58.859078Z", - "start_time": "2026-03-06T09:55:58.777509Z" + "end_time": "2026-03-06T10:33:15.380761Z", + "start_time": "2026-03-06T10:33:15.279423Z" } }, "source": [ @@ -221,8 +220,8 @@ "id": "ac84d5bc", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T09:56:01.289535Z", - "start_time": "2026-03-06T09:56:00.886262Z" + "end_time": "2026-03-06T10:33:18.179984Z", + "start_time": "2026-03-06T10:33:17.806067Z" } }, "source": [ @@ -247,8 +246,8 @@ "id": "c6bc1fa5", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T09:56:02.706621Z", - "start_time": "2026-03-06T09:56:02.597190Z" + "end_time": "2026-03-06T10:33:19.858819Z", + "start_time": "2026-03-06T10:33:19.757305Z" } }, "source": "G = weigh_edges(G, penalty)", @@ -270,8 +269,8 @@ "id": "602e50f5", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T09:56:04.438268Z", - "start_time": "2026-03-06T09:56:04.279504Z" + "end_time": "2026-03-06T10:33:21.909637Z", + "start_time": "2026-03-06T10:33:21.725905Z" } }, "source": [ @@ -304,8 +303,8 @@ "id": "a01da088", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T09:56:08.870572Z", - "start_time": "2026-03-06T09:56:06.345078Z" + "end_time": "2026-03-06T10:33:26.418652Z", + "start_time": "2026-03-06T10:33:24.024549Z" } }, "source": [ @@ -340,8 +339,8 @@ "id": "43c578c3", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T09:56:20.747913Z", - "start_time": "2026-03-06T09:56:15.793309Z" + "end_time": "2026-03-06T10:33:34.773751Z", + "start_time": "2026-03-06T10:33:30.252367Z" } }, "source": "found_gaps, found_gaps_nsp = find_actual_gaps(G, potential_gaps)", @@ -358,17 +357,22 @@ }, { "cell_type": "code", - "execution_count": null, "id": "b5ca9fdf440935d8", - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-06T10:33:39.195731Z", + "start_time": "2026-03-06T10:33:39.103249Z" + } + }, "source": [ "edgelist = []\n", "for nodelist in found_gaps_nsp:\n", " edgelist += [tuple(sorted(z)) for z in zip(nodelist, nodelist[1:])]\n", "# deduplicate\n", "edgelist = list(set(edgelist))" - ] + ], + "outputs": [], + "execution_count": 13 }, { "cell_type": "markdown", @@ -380,38 +384,16 @@ }, { "cell_type": "code", - "execution_count": null, "id": "b1f0c683", - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-06T10:34:16.805167Z", + "start_time": "2026-03-06T10:33:44.738796Z" + } + }, + "source": "ebc = compute_local_betweenness_centrality(G, nodes_gdf, radius)", "outputs": [], - "source": [ - "# set current ebc value of all G edges to 0\n", - "for edge in G.edges:\n", - " G.edges[edge][\"ebc\"] = 0\n", - "\n", - "# create dict that will be updated at each step\n", - "ebc = nx.get_edge_attributes(G, \"ebc\")\n", - "\n", - "# for each node, compute \"local\" ebc (buffered with radius!)\n", - "# for comp feas, now only subset of randomly drawn 100 nodes\n", - "random.seed(1312)\n", - "random_nodes = random.choices(list(G.nodes), k = 100)\n", - "for node in random_nodes:\n", - " node_buffer = nodes_gdf.loc[node, \"geometry\"].buffer(radius)\n", - " q = nodes_gdf.sindex.query(node_buffer, predicate=\"intersects\")\n", - " neighbours = list(nodes_gdf.iloc[q].index)\n", - " local_ebc = nx.edge_betweenness_centrality_subset(\n", - " G=G,\n", - " sources=[node],\n", - " targets=neighbours,\n", - " normalized=False, # important! otherwise the addition makes no sense\n", - " weight=\"weight\"# using penalty for non-pbi\n", - " )\n", - "\n", - " # update ebc dictionary\n", - " for k, v in local_ebc.items():\n", - " ebc[k] += v # updating ebc!!" - ] + "execution_count": 14 }, { "cell_type": "markdown", diff --git a/fixbikenet/functions.py b/fixbikenet/functions.py index b49f44e..4c393fa 100644 --- a/fixbikenet/functions.py +++ b/fixbikenet/functions.py @@ -1,5 +1,6 @@ import yaml import networkx as nx +import random def map_edges_to_bike_infrastructure(g): """ @@ -179,4 +180,50 @@ def find_actual_gaps(G, potential_gaps): if pbis == set([0]): found_gaps.append(gap) found_gaps_nsp.append(nodelist) - return found_gaps, found_gaps_nsp \ No newline at end of file + return found_gaps, found_gaps_nsp + +def compute_local_betweenness_centrality(G, nodes_gdf, radius): + """ + computes weighted betweenness centrality for paths within radius + + Parameters + ---------- + G: networkx.Graph + undirected simple graph representing the street network with weighted edges + nodes_gdf: geopandas.GeoDataFrame + all nodes in street network + radius: int + maximum length of path for betweennessn centrality calculation, set by user + + Returns + ------- + ebc: dict + local betweenness centrality values for all edges in network + """ + # set current ebc value of all G edges to 0 + for edge in G.edges: + G.edges[edge]["ebc"] = 0 + + # create dict that will be updated at each step + ebc = nx.get_edge_attributes(G, "ebc") + + # for each node, compute "local" ebc (buffered with radius!) + # for comp feas, now only subset of randomly drawn 100 nodes + random.seed(1312) + random_nodes = random.choices(list(G.nodes), k=100) + for node in random_nodes: + node_buffer = nodes_gdf.loc[node, "geometry"].buffer(radius) + q = nodes_gdf.sindex.query(node_buffer, predicate="intersects") + neighbours = list(nodes_gdf.iloc[q].index) + local_ebc = nx.edge_betweenness_centrality_subset( + G=G, + sources=[node], + targets=neighbours, + normalized=False, # important! otherwise the addition makes no sense + weight="weight" # using penalty for non-pbi + ) + + # update ebc dictionary + for k, v in local_ebc.items(): + ebc[k] += v # updating ebc!! + return ebc \ No newline at end of file diff --git a/tests/test_functions.py b/tests/test_functions.py index 5dbc8d6..6eae408 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -1,5 +1,4 @@ import pytest -import networkx as nx from networkx.utils.misc import graphs_equal import geopandas as gpd from shapely.geometry import Point From 9b0bb5c1cc049fc124cc346a3e2995e8098b8444 Mon Sep 17 00:00:00 2001 From: Manuel-Knepper Date: Fri, 6 Mar 2026 14:17:17 +0100 Subject: [PATCH 6/8] added test_compute_local_betweenness_centrality to test_functions.py --- tests/test_functions.py | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/tests/test_functions.py b/tests/test_functions.py index 6eae408..362ef07 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -115,10 +115,10 @@ def test_find_potential_gaps(create_test_contact_nodes, create_maxgap, create_te def create_graph_for_routing(): G = nx.Graph() G.add_edges_from([(1, 2), (1, 3), (2, 3), (3,4)]) - attributes = {(1, 2): {'length': 10, 'pbi': True}, - (1, 3): {'length': 4, 'pbi': False}, - (2, 3): {'length': 5, 'pbi': True}, - (3, 4): {'length': 6, 'pbi': False}} + attributes = {(1, 2): {'length': 10, 'pbi': True, 'weight': 10}, + (1, 3): {'length': 4, 'pbi': False, 'weight': 20}, + (2, 3): {'length': 5, 'pbi': True, 'weight': 5}, + (3, 4): {'length': 6, 'pbi': False, 'weight': 30}} nx.set_edge_attributes(G, attributes) return G @@ -133,4 +133,24 @@ def create_validation_found_gap_paths(): return found_gap_paths def test_find_actual_gaps(create_graph_for_routing, create_potential_gaps, create_validation_found_gap_paths): - assert find_actual_gaps(create_graph_for_routing, create_potential_gaps) == create_validation_found_gap_paths \ No newline at end of file + assert find_actual_gaps(create_graph_for_routing, create_potential_gaps) == create_validation_found_gap_paths + +@pytest.fixture +def create_betweenness_nodes(): + nodes = {'osmid': [1, 2, 3, 4], 'geometry': [Point(1, 1), Point(5000, 5000), Point(3, 3), Point(1000, 1000)]} + nodes_gdf = gpd.GeoDataFrame(nodes) + nodes_gdf.set_index('osmid', inplace=True) + return nodes_gdf + +@pytest.fixture +def create_radius(): + radius = 2000 + return radius + +@pytest.fixture +def create_validation_ebc(): + ebc = {(1,2):54.5, (1,3): 0, (2,3): 54.5, (3,4): 56.5} + return ebc + +def test_compute_local_betweenness_centrality(create_graph_for_routing, create_betweenness_nodes, create_radius, create_validation_ebc): + assert compute_local_betweenness_centrality(create_graph_for_routing, create_betweenness_nodes, create_radius) == create_validation_ebc \ No newline at end of file From 55382291fe1414a321d19d71af8aed9f8cfce327 Mon Sep 17 00:00:00 2001 From: Manuel-Knepper Date: Fri, 6 Mar 2026 14:33:04 +0100 Subject: [PATCH 7/8] added rank_gaps_by_b to functions.py and implemented it in FixBikeMVP.ipynb --- FixBikeMVP.ipynb | 76 +++++++++++++++++++---------------------- fixbikenet/functions.py | 30 +++++++++++++++- 2 files changed, 65 insertions(+), 41 deletions(-) diff --git a/FixBikeMVP.ipynb b/FixBikeMVP.ipynb index 502962f..c927ef9 100644 --- a/FixBikeMVP.ipynb +++ b/FixBikeMVP.ipynb @@ -15,15 +15,14 @@ "id": "1b6a69660258f567", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T10:30:52.577353Z", - "start_time": "2026-03-06T10:30:51.304365Z" + "end_time": "2026-03-06T13:24:09.401596Z", + "start_time": "2026-03-06T13:24:08.014186Z" } }, "source": [ "# import packages\n", "import pandas as pd\n", "import osmnx as ox\n", - "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import os\n", "\n", @@ -46,8 +45,8 @@ "id": "9173dd015d3280f3", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T10:30:53.899717Z", - "start_time": "2026-03-06T10:30:53.885353Z" + "end_time": "2026-03-06T13:24:10.609718Z", + "start_time": "2026-03-06T13:24:10.596152Z" } }, "source": [ @@ -77,8 +76,8 @@ "id": "a9a51035deb92425", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T10:31:16.923555Z", - "start_time": "2026-03-06T10:30:55.992926Z" + "end_time": "2026-03-06T13:24:34.025385Z", + "start_time": "2026-03-06T13:24:12.224519Z" } }, "source": [ @@ -99,8 +98,8 @@ "id": "a8b4eed5", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T10:31:22.616118Z", - "start_time": "2026-03-06T10:31:20.483032Z" + "end_time": "2026-03-06T13:24:46.446529Z", + "start_time": "2026-03-06T13:24:44.423659Z" } }, "source": [ @@ -135,8 +134,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T10:31:24.281676Z", - "start_time": "2026-03-06T10:31:24.158324Z" + "end_time": "2026-03-06T13:24:47.841310Z", + "start_time": "2026-03-06T13:24:47.728797Z" } }, "cell_type": "code", @@ -162,8 +161,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T10:33:12.573669Z", - "start_time": "2026-03-06T10:31:26.009329Z" + "end_time": "2026-03-06T13:26:35.471171Z", + "start_time": "2026-03-06T13:24:49.019530Z" } }, "cell_type": "code", @@ -177,8 +176,8 @@ "id": "e2670fe4", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T10:33:15.380761Z", - "start_time": "2026-03-06T10:33:15.279423Z" + "end_time": "2026-03-06T13:31:14.903844Z", + "start_time": "2026-03-06T13:31:14.725758Z" } }, "source": [ @@ -220,8 +219,8 @@ "id": "ac84d5bc", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T10:33:18.179984Z", - "start_time": "2026-03-06T10:33:17.806067Z" + "end_time": "2026-03-06T13:31:16.805584Z", + "start_time": "2026-03-06T13:31:16.458067Z" } }, "source": [ @@ -246,8 +245,8 @@ "id": "c6bc1fa5", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T10:33:19.858819Z", - "start_time": "2026-03-06T10:33:19.757305Z" + "end_time": "2026-03-06T13:31:18.314233Z", + "start_time": "2026-03-06T13:31:18.226835Z" } }, "source": "G = weigh_edges(G, penalty)", @@ -269,8 +268,8 @@ "id": "602e50f5", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T10:33:21.909637Z", - "start_time": "2026-03-06T10:33:21.725905Z" + "end_time": "2026-03-06T13:31:20.049312Z", + "start_time": "2026-03-06T13:31:19.891469Z" } }, "source": [ @@ -303,8 +302,8 @@ "id": "a01da088", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T10:33:26.418652Z", - "start_time": "2026-03-06T10:33:24.024549Z" + "end_time": "2026-03-06T13:31:24.244710Z", + "start_time": "2026-03-06T13:31:21.777852Z" } }, "source": [ @@ -339,8 +338,8 @@ "id": "43c578c3", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T10:33:34.773751Z", - "start_time": "2026-03-06T10:33:30.252367Z" + "end_time": "2026-03-06T13:31:31.246886Z", + "start_time": "2026-03-06T13:31:26.417817Z" } }, "source": "found_gaps, found_gaps_nsp = find_actual_gaps(G, potential_gaps)", @@ -360,8 +359,8 @@ "id": "b5ca9fdf440935d8", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T10:33:39.195731Z", - "start_time": "2026-03-06T10:33:39.103249Z" + "end_time": "2026-03-06T13:31:33.093955Z", + "start_time": "2026-03-06T13:31:33.006586Z" } }, "source": [ @@ -387,8 +386,8 @@ "id": "b1f0c683", "metadata": { "ExecuteTime": { - "end_time": "2026-03-06T10:34:16.805167Z", - "start_time": "2026-03-06T10:33:44.738796Z" + "end_time": "2026-03-06T13:32:08.385205Z", + "start_time": "2026-03-06T13:31:34.994480Z" } }, "source": "ebc = compute_local_betweenness_centrality(G, nodes_gdf, radius)", @@ -405,19 +404,16 @@ }, { "cell_type": "code", - "execution_count": null, "id": "4c920f49e57db5c", - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2026-03-06T13:32:13.235834Z", + "start_time": "2026-03-06T13:32:12.972818Z" + } + }, + "source": "Bs = rank_gaps_by_b(found_gaps_nsp, G, ebc)", "outputs": [], - "source": [ - "Bs = []\n", - "for nodelist in found_gaps_nsp:\n", - " edgelist = [tuple(sorted(z)) for z in zip(nodelist, nodelist[1:])]\n", - " lengths = np.array([G.edges[edge][\"length\"] for edge in edgelist])\n", - " ebcs = np.array([ebc[edge] for edge in edgelist])\n", - " B = sum(lengths * ebcs)/sum(lengths)\n", - " Bs.append(B)" - ] + "execution_count": 15 }, { "cell_type": "markdown", diff --git a/fixbikenet/functions.py b/fixbikenet/functions.py index 4c393fa..f7b47eb 100644 --- a/fixbikenet/functions.py +++ b/fixbikenet/functions.py @@ -1,6 +1,7 @@ import yaml import networkx as nx import random +import numpy as np def map_edges_to_bike_infrastructure(g): """ @@ -226,4 +227,31 @@ def compute_local_betweenness_centrality(G, nodes_gdf, radius): # update ebc dictionary for k, v in local_ebc.items(): ebc[k] += v # updating ebc!! - return ebc \ No newline at end of file + return ebc + +def rank_gaps_by_b(found_gaps_nsp, G, ebc): + """ + calculates b for all gaps + + Parameters + ---------- + found_gaps_nsp: list + list of paths in network for all gaps in protected bicycle network + G: networkx.Graph + undirected simple graph representing the street network with weighted edges + ebc: dict + local betweenness centrality values for all edges in network + + Returns + ------- + Bs: list + list of values of b for all gaps in protected bicycle network + """ + Bs = [] + for nodelist in found_gaps_nsp: + edgelist = [tuple(sorted(z)) for z in zip(nodelist, nodelist[1:])] + lengths = np.array([G.edges[edge]["length"] for edge in edgelist]) + ebcs = np.array([ebc[edge] for edge in edgelist]) + B = sum(lengths * ebcs) / sum(lengths) + Bs.append(B) + return Bs \ No newline at end of file From f8c7134a4d4893c052a61f90ba372e2261d5945a Mon Sep 17 00:00:00 2001 From: Manuel-Knepper Date: Fri, 6 Mar 2026 14:57:38 +0100 Subject: [PATCH 8/8] added test_rank_gaps_by_b to test_functions.py --- tests/test_functions.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/test_functions.py b/tests/test_functions.py index 362ef07..a34e9fd 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -153,4 +153,17 @@ def create_validation_ebc(): return ebc def test_compute_local_betweenness_centrality(create_graph_for_routing, create_betweenness_nodes, create_radius, create_validation_ebc): - assert compute_local_betweenness_centrality(create_graph_for_routing, create_betweenness_nodes, create_radius) == create_validation_ebc \ No newline at end of file + assert compute_local_betweenness_centrality(create_graph_for_routing, create_betweenness_nodes, create_radius) == create_validation_ebc + +@pytest.fixture +def create_found_paths(): + found_paths = [(1,3,4),(3,4)] + return found_paths + +@pytest.fixture +def create_validation_bs(): + validation_bs = [33.9,56.5] + return validation_bs + +def test_rank_gaps_by_b(create_found_paths, create_graph_for_routing, create_validation_ebc, create_validation_bs): + assert rank_gaps_by_b(create_found_paths, create_graph_for_routing, create_validation_ebc) == create_validation_bs \ No newline at end of file