Creating 15-Minute City Isochrones in Python
Creating 15-Minute City Isochrones in Python requires combining OpenStreetMap network extraction, time-weighted graph traversal, and spatial aggregation. The most reliable workflow uses osmnx for street network retrieval, networkx for shortest-path calculations with mode-specific travel speeds, and geopandas/shapely for polygon generation. By converting edge lengths to travel times, running a Dijkstra search from a central coordinate, and extracting reachable nodes within 900 seconds, you can generate accurate walk, bike, or drive isochrones that respect real-world routing constraints.
This approach avoids raster-based approximations and produces vector-accurate accessibility boundaries suitable for urban planning, logistics optimization, and spatial analytics. For teams evaluating alternative spatial libraries, Generating Isochrones with PySal and GeoPandas covers complementary matrix-based techniques, while the broader Python Routing Engines & Isochrone Mapping cluster details production deployment patterns.
Core Pipeline
The workflow follows a deterministic six-step sequence:
- Network Extraction: Download a localized, topologically correct street graph using
osmnx.graph_from_point(). - Impedance Assignment: Convert geometric edge lengths to temporal weights (seconds) using mode-specific velocities.
- Origin Mapping: Snap the input coordinate to the nearest valid graph node.
- Graph Traversal: Execute single-source Dijkstra with a 900-second cutoff to identify all reachable nodes.
- Spatial Aggregation: Convert reachable nodes to points, apply a metric buffer, and union geometries.
- Boundary Smoothing: Apply a concave hull to remove artificial buffer artifacts and produce a contiguous polygon.
Production-Ready Implementation
The following script implements the pipeline with modern shapely 2.x and geopandas 0.14+ APIs. It handles CRS projection for accurate metric buffering and returns a standards-compliant GeoDataFrame.
import osmnx as ox
import networkx as nx
import geopandas as gpd
import shapely
from shapely.geometry import Point
def generate_15min_isochrone(lat, lon, travel_mode="walk", speed_kmh=5.0, buffer_m=50):
"""
Generate a 15-minute (900s) isochrone polygon for a given coordinate.
Returns a GeoDataFrame in EPSG:4326.
"""
# 1. Fetch and simplify street network
G = ox.graph_from_point(
(lat, lon),
dist=3000,
network_type=travel_mode,
simplify=True
)
# 2. Calculate travel time (seconds) per edge
speed_ms = (speed_kmh * 1000) / 3600
for u, v, data in G.edges(data=True):
length_m = data.get("length", 0)
data["travel_time"] = length_m / speed_ms
# 3. Locate nearest graph node to origin
origin_node = ox.distance.nearest_nodes(G, lon, lat)
# 4. Run single-source shortest path with time cutoff
reachable = nx.single_source_dijkstra_path_length(
G, origin_node, weight="travel_time", cutoff=900
)
if len(reachable) < 4:
raise ValueError("Insufficient reachable nodes. Verify coordinates, network_type, or speed_kmh.")
# 5. Extract coordinates and build spatial object
coords = [(G.nodes[n]["x"], G.nodes[n]["y"]) for n in reachable]
points_gdf = gpd.GeoDataFrame(geometry=[Point(c) for c in coords], crs="EPSG:4326")
# 6. Project to metric CRS for accurate buffering, then generate boundary
points_metric = points_gdf.to_crs(epsg=3857)
buffered = points_metric.buffer(buffer_m)
merged = shapely.union_all(buffered.geometry)
isochrone = shapely.concave_hull(merged, ratio=0.95)
# Return to WGS84 for standard GIS compatibility
return gpd.GeoDataFrame({"geometry": [isochrone], "travel_time_max": 900}, crs="EPSG:4326")
# Usage
# iso = generate_15min_isochrone(48.8566, 2.3522, travel_mode="walk", speed_kmh=5.0)
# iso.to_file("15min_walk.gpkg", driver="GPKG")
Impedance Modeling & Network Configuration
Accurate isochrones depend on realistic impedance functions. The default speed_kmh parameter assumes uniform velocity, which rarely reflects urban mobility. For production systems, replace static speeds with dynamic impedance tables that account for:
- Topography: Elevation gain reduces cycling and walking speeds by 15–30% on gradients >5%.
- Infrastructure Quality: Dedicated bike lanes, pedestrian zones, and traffic signals introduce friction coefficients.
- Temporal Variance: Rush-hour congestion can increase drive times by 40–60%. OSMnx allows you to load
maxspeedtags and apply time-of-day multipliers.
When modeling multimodal networks, ensure network_type aligns with OSM routing profiles (walk, bike, drive, drive_service). For detailed routing engine comparisons and impedance calibration strategies, consult the official OSMnx documentation and NetworkX shortest path algorithms.
Performance Optimization & Scaling
Graph traversal scales linearly with edge count. For city-wide or regional deployments:
- Precompute Graphs: Serialize networks to
.graphmlor.gpkgto avoid repeated OSM API calls. - Limit Search Radius: A 3,000m radius is sufficient for 15-minute walk/bike isochrones. Drive isochrones may require 8,000–12,000m.
- Parallelize Origins: Use
concurrent.futuresormultiprocessingto process multiple coordinates. Each worker should maintain its own graph instance to avoid thread-safety issues innetworkx. - Memory Management: Large graphs (>500k edges) benefit from
nx.algorithms.shortest_paths.weighted.single_source_dijkstrawith explicit heap optimization.
Validation & Output Formats
Always validate generated polygons against ground-truth routing APIs (e.g., OpenRouteService, Valhalla) before deployment. Common validation checks include:
- Topology Integrity: Ensure
isochrone.is_validreturnsTrue. - Area Reasonableness: A 15-minute walk typically covers 0.8–1.5 km² in dense urban cores.
- Edge Cases: Water bodies, gated communities, and one-way restrictions should naturally constrain the boundary.
Export to GeoPackage (.gpkg) for lossless geometry storage, or GeoJSON for web visualization. When integrating with frontend mapping libraries, project to EPSG:3857 client-side to align with standard tile grids.