OSMnx Street Networks — Deep Dive

OSMnx abstracts considerable complexity behind simple function calls. Understanding its Overpass API interaction, graph cleaning pipeline, projection handling, and advanced analysis capabilities lets you tackle research-grade urban network problems.

Under the hood: the Overpass API

OSMnx constructs Overpass QL queries to fetch data from OpenStreetMap’s Overpass API. When you call graph_from_place("Manhattan"):

  1. Geocodes “Manhattan” via Nominatim → gets a polygon boundary
  2. Builds an Overpass query for way["highway"] within that polygon
  3. Downloads the XML response
  4. Parses ways and nodes into a NetworkX graph
  5. Simplifies the topology (more on this below)

Custom Overpass filters

# Only fetch motorways and trunk roads
cf = '["highway"~"motorway|trunk"]'
G = ox.graph_from_place("Los Angeles, CA", custom_filter=cf)

# Fetch railways instead of roads
G = ox.graph_from_place(
    "Tokyo, Japan",
    custom_filter='["railway"~"rail|subway"]',
    retain_all=True,
)

The custom_filter parameter passes directly to Overpass, giving you full control over what gets downloaded.

Graph simplification and cleaning

Raw OSM data has intermediate nodes along curved roads that are not intersections. OSMnx simplifies the graph by:

  1. Removing interstitial nodes — a straight road with 50 shape-defining nodes becomes a single edge with a geometry attribute storing the LineString
  2. Consolidating intersections — complex junctions with multiple OSM nodes get merged into single graph nodes
# Control simplification
G = ox.graph_from_place("Portland, OR", simplify=True)  # default

# Access the original geometry of a simplified edge
edge = G.edges[(u, v, 0)]
print(edge.get("geometry"))  # Shapely LineString or None (straight line)

Manual simplification

# Consolidate nearby intersections within a tolerance (meters)
G_clean = ox.consolidate_intersections(
    ox.project_graph(G),
    tolerance=15,
    rebuild_graph=True,
)

This is essential for comparing network metrics across cities — without consolidation, complex intersections inflate node counts.

Projection and coordinate systems

OSMnx stores graphs in WGS-84 (EPSG:4326) by default. For metric analysis, project to UTM:

G_proj = ox.project_graph(G)  # auto-selects appropriate UTM zone
print(G_proj.graph["crs"])     # e.g., EPSG:32618

All edge length attributes are in meters regardless of projection, because OSMnx computes great-circle distances during graph construction.

Isochrone analysis

An isochrone shows how far you can travel from a point within a given time. Combine OSMnx routing with Shapely:

import networkx as nx
from shapely.geometry import Point, MultiPoint
from shapely.ops import unary_union

G = ox.graph_from_point((40.748, -73.985), dist=3000, network_type="walk")
G = ox.add_edge_speeds(G)
G = ox.add_edge_travel_times(G)

center_node = ox.nearest_nodes(G, -73.985, 40.748)

# All nodes reachable within 10 minutes (600 seconds)
subgraph = nx.ego_graph(G, center_node, radius=600, distance="travel_time")

node_points = [Point(data["x"], data["y"]) for _, data in subgraph.nodes(data=True)]
isochrone = unary_union(node_points).convex_hull

For more accurate isochrones, use alpha shapes or Voronoi tessellation instead of convex hull.

Multi-ring isochrones

trip_times = [5, 10, 15, 20]  # minutes
isochrones = []

for minutes in sorted(trip_times, reverse=True):
    subgraph = nx.ego_graph(G, center_node, radius=minutes * 60, distance="travel_time")
    nodes = [Point(d["x"], d["y"]) for _, d in subgraph.nodes(data=True)]
    isochrones.append(unary_union(nodes).convex_hull)

Centrality analysis

Network centrality metrics identify the most important nodes and edges:

import networkx as nx

# Betweenness centrality: nodes that appear on the most shortest paths
bc = nx.betweenness_centrality(G, weight="length")
max_node = max(bc, key=bc.get)

# Edge betweenness: roads that carry the most through-traffic
ebc = nx.edge_betweenness_centrality(G, weight="length")

# Closeness centrality: nodes with shortest average distance to all others
cc = nx.closeness_centrality(G, distance="length")

Visualizing centrality

nc = ox.plot.get_node_colors_by_attr(G, bc, cmap="hot")
fig, ax = ox.plot_graph(G, node_color=nc, node_size=8, edge_linewidth=0.3)

High betweenness nodes are critical junctions — disrupting them (construction, closures) has outsized impact on network flow.

Multi-city comparative analysis

cities = [
    "Barcelona, Spain",
    "Manhattan, New York, USA",
    "Phoenix, Arizona, USA",
    "Tokyo, Japan",
]

results = []
for city in cities:
    G = ox.graph_from_place(city, network_type="drive")
    G = ox.project_graph(G)
    stats = ox.basic_stats(G)
    stats["city"] = city
    results.append(stats)

import pandas as pd
df = pd.DataFrame(results)[["city", "street_density_km", "intersection_density_km", "circuity_avg"]]
print(df)

This reveals structural differences: Barcelona’s grid has high intersection density and low circuity, while Phoenix’s suburban sprawl shows the opposite.

Graph export and interoperability

# To GeoDataFrames (nodes and edges)
nodes, edges = ox.graph_to_gdfs(G)

# To GeoPackage
edges.to_file("streets.gpkg", driver="GPKG")

# To GraphML (preserves all attributes)
ox.save_graphml(G, "network.graphml")

# To/from OSM XML
ox.save_graph_xml(G, "network.osm")

GeoDataFrame export is the bridge to GeoPandas spatial analysis, web mapping (Folium, Kepler.gl), and database storage (PostGIS).

Performance considerations

OperationSmall city (10K edges)Large metro (500K edges)
Download3-5s30-90s
Simplification<1s5-15s
Shortest path<1ms5-50ms
Betweenness centrality2s15-60 min
All-pairs shortest paths10sOut of memory

For large networks:

  • Use weight parameter in centrality functions to limit computation
  • Sample nodes for betweenness estimation: nx.betweenness_centrality(G, k=500)
  • Cache downloaded graphs: ox.save_graphml(G, "cache.graphml") and reload with ox.load_graphml

Caching and rate limiting

OSMnx caches Overpass responses by default in ~/.cache/osmnx/. For batch processing:

ox.settings.cache_folder = "/data/osmnx_cache"
ox.settings.use_cache = True
ox.settings.timeout = 300  # seconds for large queries

Respect Overpass API rate limits — for heavy batch jobs, consider running a local Overpass instance.

Edge cases and gotchas

IssueCauseFix
Disconnected graph componentsIslands, ferry-only areasG = ox.truncate.largest_component(G)
Missing speed dataOSM tags incompleteox.add_edge_speeds infers from highway type
One-way streets ignoredUsing undirected graphEnsure MultiDiGraph (default) for routing
Stale dataOSM edits not reflectedOverpass data updates every ~1 minute
Query timeoutArea too largeSplit into tiles or increase ox.settings.timeout

The one thing to remember: OSMnx’s power lies in combining geographic data retrieval with graph-theoretic analysis — the same library that downloads the street map also computes centrality, isochrones, and network statistics, making it a complete toolkit for computational urban science.

pythonosmnxstreet-networksgeospatial

See Also

  • Python Adaptive Learning Systems How Python builds learning apps that adjust to each student like a personal tutor who knows exactly what you need next.
  • Python Airflow Learn Airflow as a timetable manager that makes sure data tasks run in the right order every day.
  • Python Altair Learn Altair through the idea of drawing charts by describing rules, not by hand-placing every visual element.
  • Python Automated Grading How Python grades homework and exams automatically, from simple answer keys to understanding written essays.
  • Python Batch Vs Stream Processing Batch processing is like doing laundry once a week; stream processing is like a self-cleaning shirt that cleans itself constantly.