Source code: Street network analysis with OSMnx: routes, isochrones and walking accessibility
Runnable Python scripts, requirements and console output for the tutorial “Street network analysis with OSMnx: routes, isochrones and walking…
Last updated
These are the exact scripts used to produce every figure and number in Street network analysis with OSMnx: routes, isochrones and walking accessibility. Download them, install the requirements into a fresh virtual environment and run them from this folder.
- sheffield_networks.py
- requirements.txt
- console.txt: the console output recorded when we ran it
python -m venv .venv.venv\Scripts\activate # macOS/Linux: source .venv/bin/activatepip install -r requirements.txtpython sheffield_networks.pysheffield_networks.py
"""Street network analysis with OSMnx - TechnicalGIS tutorial.
Downloads Sheffield's walkable street network from OpenStreetMap, thencomputes a shortest path, walking isochrones and a network-basedaccessibility surface (minutes to the nearest supermarket).Run with: python sheffield_networks.py"""from pathlib import Path
import contextily as cximport geopandas as gpdimport matplotlib.pyplot as pltimport networkx as nximport numpy as npimport osmnx as oximport pandas as pdfrom shapely.geometry import Point
HERE = Path(__file__).parentOUT = HERE / "output"OUT.mkdir(exist_ok=True)plt.rcParams.update({"figure.dpi": 150, "savefig.bbox": "tight", "font.size": 9})pd.set_option("display.width", 120)ox.settings.use_cache = Trueox.settings.cache_folder = str(HERE / "cache")ox.settings.log_console = False
CENTRE = (53.3801, -1.4703) # Peace Gardens, Sheffield (lat, lon)STATION = (53.3780, -1.4623) # Sheffield railway stationUNIVERSITY = (53.3817, -1.4886) # Firth Court, University of SheffieldWALK_M_PER_MIN = 4.8 * 1000 / 60 # 4.8 km/hBASEMAP = cx.providers.Esri.WorldGrayCanvas
def fill_small_holes(geom, max_area=40_000): """Remove interior rings (city blocks) smaller than max_area m².""" from shapely.geometry import MultiPolygon, Polygon polys = geom.geoms if isinstance(geom, MultiPolygon) else [geom] out = [Polygon(p.exterior, [r for r in p.interiors if Polygon(r).area > max_area]) for p in polys] return MultiPolygon(out) if len(out) > 1 else out[0]
def section(title: str) -> None: print(f"\n=== {title} ===")
# --- Step 1: download the walking network -------------------------------------G = ox.graph_from_point(CENTRE, dist=2500, network_type="walk")Gp = ox.project_graph(G) # to local UTM so lengths/areas are in metressection("graph")print(Gp)print("crs:", Gp.graph["crs"])stats = ox.basic_stats(Gp)for k in ["n", "m", "k_avg", "edge_length_total", "street_length_avg", "streets_per_node_avg", "intersection_count", "circuity_avg"]: v = stats[k] print(f"{k:22s} {v:,.2f}" if isinstance(v, float) else f"{k:22s} {v:,}")
nodes, edges = ox.graph_to_gdfs(Gp)section("edges GeoDataFrame")print(edges[["osmid", "name", "highway", "length"]].head().to_string())print("geometry types:", edges.geom_type.value_counts().to_dict())
fig, ax = ox.plot_graph(Gp, node_size=0, edge_color="#0f766e", edge_linewidth=0.5, bgcolor="white", show=False, close=False, figsize=(8, 8))ax.set_title("Sheffield walking network within 2.5 km of the Peace Gardens")fig.savefig(OUT / "01-network.png")plt.close(fig)
# --- Step 2: shortest path -----------------------------------------------------------pts = gpd.GeoSeries([Point(STATION[1], STATION[0]), Point(UNIVERSITY[1], UNIVERSITY[0])], crs="EPSG:4326").to_crs(Gp.graph["crs"])orig, dest = ox.distance.nearest_nodes(Gp, pts.x.tolist(), pts.y.tolist())route = ox.shortest_path(Gp, orig, dest, weight="length")route_gdf = ox.routing.route_to_gdf(Gp, route, weight="length")route_len = route_gdf["length"].sum()section("shortest path station -> university")print("nodes in route:", len(route))print(f"length: {route_len:,.0f} m | walking time: {route_len / WALK_M_PER_MIN:.1f} min")print("streets:", " → ".join(pd.unique(route_gdf["name"].dropna().astype(str)))[:400])
fig, ax = ox.plot_graph_route(Gp, route, route_color="#e8590c", route_linewidth=4, node_size=0, edge_color="#9aa5a1", edge_linewidth=0.4, bgcolor="white", show=False, close=False, figsize=(8, 6))ax.set_title(f"Shortest walk: station to Firth Court ({route_len / 1000:.2f} km)")fig.savefig(OUT / "02-shortest-path.png")plt.close(fig)
# --- Step 3: walking isochrones --------------------------------------------------------times = [5, 10, 15, 20]isos = []for t in sorted(times, reverse=True): sub = nx.ego_graph(Gp, orig, radius=t * WALK_M_PER_MIN, distance="length") sub_edges = ox.graph_to_gdfs(sub, nodes=False) isos.append({"minutes": t, "geometry": fill_small_holes(sub_edges.buffer(30).unary_union.buffer(-10))})iso = gpd.GeoDataFrame(isos, crs=Gp.graph["crs"])iso["area_km2"] = (iso.area / 1e6).round(2)section("isochrones from Sheffield station")print(iso[["minutes", "area_km2"]].sort_values("minutes").to_string(index=False))iso.to_crs("EPSG:4326").to_file(OUT / "isochrones.geojson", driver="GeoJSON")
fig, ax = plt.subplots(figsize=(8, 8))iso.plot(ax=ax, column="minutes", cmap="viridis_r", alpha=0.55, edgecolor="white", linewidth=0.6, legend=True, categorical=True, legend_kwds={"title": "Walk (min)", "loc": "lower right"})pts.iloc[[0]].plot(ax=ax, color="#e8590c", markersize=60, marker="*", zorder=5)cx.add_basemap(ax, crs=iso.crs, source=BASEMAP, attribution_size=6)ax.set_title("5–20 minute walking isochrones from Sheffield station")ax.set_axis_off()fig.savefig(OUT / "03-isochrones.png")plt.close(fig)
# --- Step 4: accessibility to supermarkets ------------------------------------------shops = ox.features_from_point(CENTRE, tags={"shop": "supermarket"}, dist=3000)shops = shops.to_crs(Gp.graph["crs"])shops["geometry"] = shops.geometry.representative_point()section("supermarkets from OSM")print(len(shops), "supermarkets;", shops["name"].value_counts().head(6).to_dict())
shop_nodes = set(ox.distance.nearest_nodes(Gp, shops.geometry.x.tolist(), shops.geometry.y.tolist()))dist = nx.multi_source_dijkstra_path_length(Gp, shop_nodes, weight="length")nodes["walk_min"] = nodes.index.map(dist).astype(float) / WALK_M_PER_MINedges["walk_min"] = [np.nanmean([nodes.at[u, "walk_min"], nodes.at[v, "walk_min"]]) for u, v, _ in edges.index]
bins = pd.cut(nodes["walk_min"], [0, 5, 10, 15, 20, np.inf], labels=["0–5", "5–10", "10–15", "15–20", "20+"])section("share of network nodes by walk time to nearest supermarket")print((bins.value_counts(normalize=True).sort_index() * 100).round(1).to_string())
fig, ax = plt.subplots(figsize=(8, 8))edges.plot(ax=ax, column="walk_min", cmap="plasma", vmin=0, vmax=20, linewidth=0.8, legend=True, legend_kwds={"label": "Walking minutes to nearest supermarket", "shrink": 0.7})shops.plot(ax=ax, color="black", markersize=12, marker="s")ax.set_title("Network accessibility: minutes to the nearest supermarket")ax.set_axis_off()fig.savefig(OUT / "04-supermarket-accessibility.png")plt.close(fig)
# --- Step 5: street orientation ------------------------------------------------------Gu = ox.bearing.add_edge_bearings(ox.convert.to_undirected(G))fig, ax = ox.plot_orientation(Gu, title="Sheffield centre street orientation", area=True, color="#0f766e", edgecolor="#0b4f49", title_font={"family": "DejaVu Sans", "size": 14, "weight": "bold"})fig.savefig(OUT / "05-orientation.png")plt.close(fig)section("orientation entropy")print(round(ox.bearing.orientation_entropy(Gu), 3))
ox.save_graph_geopackage(Gp, OUT / "sheffield_walk.gpkg")