Usage | 1. Gap identification

This notebook explains how FixBikeNet finds gaps with illustrative examples.

Parameters covered: num_gaps, settings.radius, min_gap_length, max_gap_length

We start every Usage notebook with the standard way of importing FixBikeNet:

import fixbikenet as fbn

Ranking gaps through betweenness

FixBikeNet is based on the idea that any shortest path between two nodes on an existing bicycle network, where the entire path goes through the non-bicycle network, is a potential gap. Out of these potentially millions of gaps in a city, FixBikeNet identifies those that are most important to close in terms of providing the most cycled meters (per gap meter). It ranks the identified gaps and selects the top num_gaps - the first parameter for running FixBikeNet. The ranking happens through betweenness centrality, a proxy for cyclist flow. This measure assumes that cyclists move from each intersection to each other intersection in the city.

The assumption of such a global all-to-all flow is relaxed in FixBikeNet to only consider local betweenness flows within a settings.radius (that is loaded from fbn.settings). This relaxation is done for several reasons:

  1. Typical cyclists are usually not traversing the whole city

  2. global betweenness is biased towards the city center while local betweenness allows identifying gaps on neighborhood levels

  3. calculations are much more effective, especially for large cities.

See below the top 10 gaps for Frederiksberg municipality, Denmark:

gaps_ordered = fbn.fixbikenet(
    "Frederiksberg municipality",
    num_gaps=10,
)

Under the hood, FixBikeNet downloads the street network data from OpenStreetMap (OSM) via OSMnx.

Explore the results interactively:

gaps_ordered.explore(
    tiles="https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png", 
    attr="(C) Stadia Maps (C) OpenMapTiles (C) OpenStreetMap contributors",
    style_kwds={"weight": 3, "color": "#096a51"},
)
Make this Notebook Trusted to load map: File -> Trust Notebook

The content of the output gaps_ordered shows several columns:

gaps_ordered
benefit name source target ordering length geometry
0 114509.773337 n/a 929287662 576734 0 189 LINESTRING (12.5214 55.67937, 12.52136 55.6792...
1 65684.000000 n/a 8088739 369902569 1 22 LINESTRING (12.51731 55.67908, 12.51732 55.679...
2 42247.092427 n/a 1629778132 5771168117 2 153 LINESTRING (12.53946 55.67056, 12.53931 55.670...
3 37662.754856 n/a 781056 13824074631 3 210 LINESTRING (12.54403 55.68123, 12.54361 55.681...
4 25797.253923 n/a 1669421661 454565354 4 74 LINESTRING (12.50854 55.67991, 12.50933 55.679...
5 19402.679375 n/a 1645615539 1645615733 5 141 LINESTRING (12.50892 55.67335, 12.50901 55.673...
6 15757.726457 n/a 424578709 20908222 6 393 LINESTRING (12.51115 55.67531, 12.51048 55.675...
7 15728.831385 n/a 1741398202 1618234157 7 284 LINESTRING (12.54169 55.6846, 12.54174 55.6847...
8 15613.750000 n/a 282822564 928203337 8 67 LINESTRING (12.52436 55.68026, 12.52448 55.67966)
9 15570.694384 n/a 3656885645 1572650173 9 243 LINESTRING (12.52059 55.68272, 12.52046 55.682...

They are:

  • benefit: The metric to order gaps by.

  • name: Name of the gaps’ underlying street. If not found, “n/a”.

  • source, target: The OSM IDs of source and target seed points. These are nodes that can be looked up on OSM, for example for OSM ID 298719867: https://www.openstreetmap.org/node/298719867

  • ordering: The ordering of the gaps by betweenness centrality. These are consecutive integers.

  • length: Length of the gap, rounded to whole meters.

  • geometry: The geometries of the edges connecting source and target seed points, in the coordinate references system (crs) given by the setting settings.crs_result. These geometries are linestrings.

To add more context, let us download Frederiksberg’s network of protected bicycle lanes, and show it together with the gaps. To do so, we define a fitting custom filter:

Hide code cell source

PBI_CUSTOM_FILTER = [
    '["highway"~"cycleway|living_street"]',
    '["highway"~"path|pedestrian|footway"]["bicycle"~"designated|yes|permissive"]["access"!~"private"]',
    '["cyclestreet"~"yes"]',
    '["bicycle_road"~"yes"]',
    '["cycleway"~"track"]',
    '["cycleway:right"~"track|opposite_track"]',
    '["cycleway:left"~"track|opposite_track"]',
    '["cycleway:both"~"track|opposite_track"]',
]
import osmnx as ox
bike_network = ox.graph.graph_from_place("Frederiksberg municipality", custom_filter=PBI_CUSTOM_FILTER, retain_all=True)
bike_edges = ox.convert.graph_to_gdfs(bike_network, nodes=False)

To visualize the outcome, we plot first bike edges in blue, then the gaps in green. To add layer control in the top right of the map, we import folium.

Hide code cell source

import folium

viz = bike_edges.explore(
    tiles="https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png", 
    attr="(C) Stadia Maps (C) OpenMapTiles (C) OpenStreetMap contributors",
    style_kwds={"weight": 2, "color": "#9999cc"},
    name="Existing bike network",
)
viz = gaps_ordered.explore(
    m=viz, 
    style_kwds={"weight": 3, "color": "#096a51"},
    name="Identified gaps",
)
folium.LayerControl().add_to(viz)
viz
Make this Notebook Trusted to load map: File -> Trust Notebook

We generally do not recommend to change the radius, as the default value has been carefully chosen to balance typical cycling distances versus center biases versus computational efficiency. However, it makes sense to tune the gap lengths.

Tuning gap lengths

The default minimum gap length is not zero, because often very short gaps are either data artefacts of undershoots (pieces did not get connected exactly), or due to intersections or parallel lanes. However, it straightforward to change the minimum gap length to, say 0, via min_gap_length=0.

gaps_ordered_min_gap_length0 = fbn.fixbikenet(
    "Frederiksberg municipality",
    num_gaps=10,
    min_gap_length=0,
)

Let us add the outcome with min_gap_length=0 in orange to see the difference:

Hide code cell source

import folium

viz = bike_edges.explore(
    tiles="https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.png", 
    attr="(C) Stadia Maps (C) OpenMapTiles (C) OpenStreetMap contributors",
    style_kwds={"weight": 2, "color": "#9999cc"},
    name="Existing bike network",
)
viz = gaps_ordered_min_gap_length0.explore(
    m=viz, 
    style_kwds={"weight": 6, "color": "#f19730"},
    name="Identified gaps, min_gap_length=0",
)
viz = gaps_ordered.explore(
    m=viz, 
    style_kwds={"weight": 3, "color": "#096a51"},
    name="Identified gaps",
)

folium.LayerControl().add_to(viz)
viz
Make this Notebook Trusted to load map: File -> Trust Notebook

It is also possible to consider longer gaps, for example, up to 1500m via max_gap_length=1500. This is computationally expensive and takes much longer to calculate. In the case of Frederiksberg, around 3 times as many potential gaps have to be considered, increasing the runtime by a factor of around 9. The result is however identical, which is why we are not running it here. The default maximum gap is already quite high, catching most long gaps. It can be useful to lower the max_gap_length value though, to making search faster and limiting it to only shorter gaps.