OSM Strategies

Last update

March 23, 2024

Install and update cityseer if necessary.

# !pip install --upgrade cityseer

See the guide for a preamble.

Please also see the graph cleaning guide for additional information on the graph cleaning approach.

This notebook uses OSM data to compare three strategies for network preparation and then compares the centralities computed on each:

  1. Algorithmically cleaning and consolidating the network
  2. Using a minimally cleaned network which strips out unnecessary nodes but doesn’t apply any consolidation methods.
  3. Using a minimally cleaned network but with corrections for network distortions through edge “dissolving” and “jitter”.

Preparing the data extents

import matplotlib.pyplot as plt
from cityseer import rustalgos
from cityseer.metrics import networks
from cityseer.tools import graphs, io, plot

# download from OSM
lng, lat = -0.13396079424572427, 51.51371088849723
buffer = 5000
distances = [250, 500, 1000, 2000]
# creates a WGS shapely polygon
poly_wgs, _ = io.buffered_point_poly(lng, lat, buffer)
poly_utm, _ = io.buffered_point_poly(lng, lat, buffer, projected=True)

Automatic cleaning

This approach prepares a network using automated algorithmic cleaning methods to consolidate complex intersections and parallel roads.

G_utm = io.osm_graph_from_poly(poly_wgs, simplify=True)
# decompose for higher resolution analysis
G_decomp = graphs.nx_decompose(G_utm, 25)
# prepare data structures
nodes_gdf, _edges_gdf, network_structure = io.network_structure_from_nx(
    G_decomp, crs=32629
)
# compute centralities
# if computing wider area centralities, e.g. 20km, then use less decomposition to speed up the computation
nodes_gdf = networks.node_centrality_shortest(
    network_structure=network_structure,
    nodes_gdf=nodes_gdf,
    distances=distances,
)
INFO:cityseer.tools.io:Converting networkX graph from EPSG code 4326 to EPSG code 32630.
INFO:cityseer.tools.io:Processing node x, y coordinates.
100%|██████████| 158974/158974 [00:00<00:00, 485167.99it/s]
INFO:cityseer.tools.io:Processing edge geom coordinates, if present.
100%|██████████| 174746/174746 [00:00<00:00, 715205.05it/s]
INFO:cityseer.tools.graphs:Generating interpolated edge geometries.
100%|██████████| 174746/174746 [00:03<00:00, 57542.22it/s]
INFO:cityseer.tools.graphs:Removing filler nodes.
100%|██████████| 158974/158974 [00:22<00:00, 7148.15it/s]
INFO:cityseer.tools.graphs:Removing dangling nodes.
100%|██████████| 51042/51042 [00:00<00:00, 226303.09it/s]
INFO:cityseer.tools.graphs:Removing filler nodes.
100%|██████████| 44125/44125 [00:01<00:00, 27417.54it/s]
INFO:cityseer.tools.util:Creating nodes STR tree
100%|██████████| 40426/40426 [00:00<00:00, 98784.83it/s] 
INFO:cityseer.tools.graphs:Consolidating nodes.
100%|██████████| 40426/40426 [00:12<00:00, 3122.61it/s]
INFO:cityseer.tools.graphs:Removing filler nodes.
100%|██████████| 28513/28513 [00:00<00:00, 156665.72it/s]
INFO:cityseer.tools.graphs:Merging parallel edges within buffer of 50.
100%|██████████| 43574/43574 [00:03<00:00, 11255.06it/s]
INFO:cityseer.tools.util:Creating edges STR tree.
100%|██████████| 40139/40139 [00:00<00:00, 574704.78it/s]
INFO:cityseer.tools.graphs:Splitting opposing edges.
100%|██████████| 28135/28135 [00:12<00:00, 2301.02it/s]
INFO:cityseer.tools.graphs:Merging parallel edges within buffer of 50.
100%|██████████| 41835/41835 [00:00<00:00, 243592.19it/s]
INFO:cityseer.tools.util:Creating nodes STR tree
100%|██████████| 29831/29831 [00:00<00:00, 33420.23it/s] 
INFO:cityseer.tools.graphs:Consolidating nodes.
100%|██████████| 29831/29831 [00:06<00:00, 4934.07it/s]
INFO:cityseer.tools.graphs:Removing filler nodes.
100%|██████████| 25797/25797 [00:00<00:00, 48234.10it/s]
INFO:cityseer.tools.graphs:Merging parallel edges within buffer of 50.
100%|██████████| 37635/37635 [00:02<00:00, 17380.70it/s]
INFO:cityseer.tools.graphs:Removing filler nodes.
100%|██████████| 24356/24356 [00:00<00:00, 110880.43it/s]
INFO:cityseer.tools.graphs:Ironing edges.
100%|██████████| 34781/34781 [00:10<00:00, 3220.05it/s]
INFO:cityseer.tools.graphs:Merging parallel edges within buffer of 1.
100%|██████████| 34781/34781 [00:00<00:00, 230764.38it/s]
INFO:cityseer.tools.util:Creating edges STR tree.
100%|██████████| 34779/34779 [00:00<00:00, 702914.31it/s]
INFO:cityseer.tools.graphs:Splitting opposing edges.
100%|██████████| 23771/23771 [00:07<00:00, 3299.72it/s]
INFO:cityseer.tools.graphs:Merging parallel edges within buffer of 50.
100%|██████████| 34994/34994 [00:00<00:00, 108882.27it/s]
INFO:cityseer.tools.util:Creating nodes STR tree
100%|██████████| 23986/23986 [00:00<00:00, 103188.63it/s]
INFO:cityseer.tools.graphs:Consolidating nodes.
100%|██████████| 23986/23986 [00:02<00:00, 10600.80it/s]
INFO:cityseer.tools.graphs:Removing filler nodes.
100%|██████████| 23634/23634 [00:00<00:00, 375303.57it/s]
INFO:cityseer.tools.graphs:Merging parallel edges within buffer of 50.
100%|██████████| 34564/34564 [00:00<00:00, 115571.65it/s]
INFO:cityseer.tools.graphs:Removing filler nodes.
100%|██████████| 23568/23568 [00:00<00:00, 459175.76it/s]
INFO:cityseer.tools.graphs:Ironing edges.
100%|██████████| 34322/34322 [00:11<00:00, 2993.86it/s]
INFO:cityseer.tools.graphs:Merging parallel edges within buffer of 1.
100%|██████████| 34322/34322 [00:00<00:00, 158898.09it/s]
INFO:cityseer.tools.graphs:Decomposing graph to maximum edge lengths of 25.
100%|██████████| 34321/34321 [00:19<00:00, 1720.73it/s]
INFO:cityseer.tools.io:Preparing node and edge arrays from networkX graph.
100%|██████████| 90339/90339 [00:01<00:00, 87013.80it/s] 
100%|██████████| 90339/90339 [00:17<00:00, 5146.21it/s]
INFO:cityseer.metrics.networks:Computing shortest path node centrality.
100%|██████████| 90339/90339 [07:48<00:00, 192.87it/s]

Minimal cleaning

This method performs minimal cleaning and is used for reference point for the other two methods.

# generate OSM graph from polygon - note no automatic simplification applied
G_utm_minimal = io.osm_graph_from_poly(poly_wgs, simplify=False)
# decompose for higher resolution analysis
G_decomp_minimal = graphs.nx_decompose(G_utm_minimal, 25)
# prepare data structures
(
    nodes_gdf_minimal,
    _edges_gdf_minimal,
    network_structure_minimal,
) = io.network_structure_from_nx(G_decomp_minimal, crs=32629)
# compute centrality
nodes_gdf_minimal = networks.node_centrality_shortest(
    network_structure=network_structure_minimal,
    nodes_gdf=nodes_gdf_minimal,
    distances=distances,
)
INFO:cityseer.tools.io:Converting networkX graph from EPSG code 4326 to EPSG code 32630.
INFO:cityseer.tools.io:Processing node x, y coordinates.
100%|██████████| 158974/158974 [00:00<00:00, 424142.35it/s]
INFO:cityseer.tools.io:Processing edge geom coordinates, if present.
100%|██████████| 174746/174746 [00:00<00:00, 726609.08it/s]
INFO:cityseer.tools.graphs:Generating interpolated edge geometries.
100%|██████████| 174746/174746 [00:02<00:00, 60276.66it/s]
INFO:cityseer.tools.graphs:Removing filler nodes.
100%|██████████| 158974/158974 [00:27<00:00, 5679.47it/s]
INFO:cityseer.tools.graphs:Decomposing graph to maximum edge lengths of 25.
100%|██████████| 69824/69824 [00:34<00:00, 2053.08it/s]
INFO:cityseer.tools.io:Preparing node and edge arrays from networkX graph.
100%|██████████| 123978/123978 [00:01<00:00, 115800.08it/s]
100%|██████████| 123978/123978 [00:20<00:00, 5945.17it/s]
INFO:cityseer.metrics.networks:Computing shortest path node centrality.
100%|██████████| 123978/123978 [10:49<00:00, 190.90it/s]

Dissolving network weights

This approach doesn’t attempt to consolidate the network. Instead, it uses techniques to control for messy network representations:

  • It “dissolves” network weights - meaning that nodes representing street segments which are likely duplicitous are weighted less heavily.
  • It injects “jitter” to derive more intuitively consistent network routes.
# generate dissolved weights
G_dissolved_wts = graphs.nx_weight_by_dissolved_edges(G_decomp_minimal, max_ang_diff=25)
# prepare data structures
(
    nodes_gdf_dissolved,
    _edges_gdf_dissolved,
    network_structure_dissolved,
) = io.network_structure_from_nx(G_dissolved_wts, crs=32629)
# compute centralities
nodes_gdf_dissolved = networks.node_centrality_shortest(
    network_structure=network_structure_dissolved,
    nodes_gdf=nodes_gdf_dissolved,
    distances=distances,
    jitter_scale=10,
)
INFO:cityseer.tools.graphs:Generating node weights based on locally dissolved edges using a buffer of 20m.
INFO:cityseer.tools.util:Creating edges STR tree.
100%|██████████| 139750/139750 [00:00<00:00, 420677.83it/s]
100%|██████████| 139750/139750 [03:24<00:00, 684.64it/s] 
100%|██████████| 123978/123978 [00:04<00:00, 27122.38it/s]
INFO:cityseer.tools.io:Preparing node and edge arrays from networkX graph.
100%|██████████| 123978/123978 [00:01<00:00, 87731.17it/s]
100%|██████████| 123978/123978 [00:23<00:00, 5285.59it/s]
INFO:cityseer.metrics.networks:Computing shortest path node centrality.
100%|██████████| 123978/123978 [05:11<00:00, 398.45it/s]

Plots

Compares a selection of distance thresholds for each approach.

bg_colour = "#111"
betas = rustalgos.betas_from_distances(distances)
avg_dists = rustalgos.avg_distances_for_betas(betas)
plot_bbox = poly_utm.centroid.buffer(1500).bounds
bg_colour = "#111"
text_colour = "#ddd"
font_size = 5
for d, b, avg_d in zip(distances, betas, avg_dists):
    fig, axes = plt.subplots(1, 3, figsize=(8, 3), dpi=200, facecolor=bg_colour)
    fig.suptitle(
        f"Gravity index (weighted closeness-like) at avg. walking tolerance:{avg_d:.2f}m and max tolerance of {d}m",
        color=text_colour,
        fontsize=8,
    )
    plot.plot_scatter(
        axes[0],
        network_structure.node_xs,
        network_structure.node_ys,
        nodes_gdf[f"cc_beta_{d}"],
        bbox_extents=plot_bbox,
        cmap_key="magma",
        face_colour=bg_colour,
    )
    axes[0].set_title(
        "Algorithmically cleaned network", fontsize=font_size, color=text_colour
    )
    plot.plot_scatter(
        axes[1],
        network_structure_minimal.node_xs,
        network_structure_minimal.node_ys,
        nodes_gdf_minimal[f"cc_beta_{d}"],
        bbox_extents=plot_bbox,
        cmap_key="magma",
        face_colour=bg_colour,
    )
    axes[1].set_title(
        "Minimally cleaned network", fontsize=font_size, color=text_colour
    )
    plot.plot_scatter(
        axes[2],
        network_structure_dissolved.node_xs,
        network_structure_dissolved.node_ys,
        nodes_gdf_dissolved[f"cc_beta_{d}"],
        bbox_extents=plot_bbox,
        cmap_key="magma",
        face_colour=bg_colour,
    )
    axes[2].set_title(
        "Minimal w. dissolved edge weightings and jitter",
        fontsize=font_size,
        color=text_colour,
    )
    plt.show()

for d, b, avg_d in zip(distances, betas, avg_dists):
    fig, axes = plt.subplots(1, 3, figsize=(8, 3), dpi=200, facecolor=bg_colour)
    fig.suptitle(
        f"Weighted betweenness centrality at avg. walking tolerance:{avg_d:.2f}m and max tolerance of {d}m",
        color=text_colour,
        fontsize=8,
    )
    plot.plot_scatter(
        axes[0],
        network_structure.node_xs,
        network_structure.node_ys,
        nodes_gdf[f"cc_betweenness_{d}"],
        bbox_extents=plot_bbox,
        cmap_key="magma",
        s_max=2,
        face_colour=bg_colour,
    )
    axes[0].set_title(
        "Algorithmically cleaned network", fontsize=font_size, color=text_colour
    )
    plot.plot_scatter(
        axes[1],
        network_structure_minimal.node_xs,
        network_structure_minimal.node_ys,
        nodes_gdf_minimal[f"cc_betweenness_{d}"],
        bbox_extents=plot_bbox,
        cmap_key="magma",
        s_max=2,
        face_colour=bg_colour,
    )
    axes[1].set_title(
        "Minimally cleaned network", fontsize=font_size, color=text_colour
    )
    plot.plot_scatter(
        axes[2],
        network_structure_dissolved.node_xs,
        network_structure_dissolved.node_ys,
        nodes_gdf_dissolved[f"cc_betweenness_{d}"],
        bbox_extents=plot_bbox,
        cmap_key="magma",
        s_max=2,
        face_colour=bg_colour,
    )
    axes[2].set_title(
        "Minimal w. dissolved edge weightings and jitter",
        fontsize=font_size,
        color=text_colour,
    )
    plt.tight_layout()
    plt.show()