Build transport graphs

This notebook shows how to build drive, walk, public-transport, and intermodal UrbanGraph objects for one territory. The examples use osm_id=1114252 so the same boundary can be reused across graph builders.

# To install IduEdu in a clean environment:
# !pip install IduEdu

OSM_ID = 1114252
from iduedu import get_4326_boundary

boundary = get_4326_boundary(osm_id=OSM_ID)

Drive graph and the simplify parameter

simplify=True merges chains of raw OSM segments into longer graph edges. simplify=False keeps the detailed per-segment topology. The difference is easiest to see when edges and nodes are plotted together.

from iduedu import get_drive_graph

G_drive_simple = get_drive_graph(
    osm_id=OSM_ID,
    simplify=True,
    clip_by_territory=False,
    keep_largest_subgraph=False,
)

G_drive_detailed = get_drive_graph(
    osm_id=OSM_ID,
    simplify=False,
    clip_by_territory=False,
    keep_largest_subgraph=False,
)

print(
    "simplify=True :",
    len(G_drive_simple.nodes_gdf),
    "nodes,",
    len(G_drive_simple.edges_gdf),
    "edges",
)
print(
    "simplify=False:",
    len(G_drive_detailed.nodes_gdf),
    "nodes,",
    len(G_drive_detailed.edges_gdf),
    "edges",
)
simplify=True : 745 nodes, 1243 edges
simplify=False: 6521 nodes, 7019 edges
import matplotlib.pyplot as plt


def plot_graph_edges_and_nodes(graph, ax, title, edge_color="#496A81", node_color="#D84A3A"):
    graph.edges_gdf.plot(ax=ax, color=edge_color, linewidth=0.9, alpha=0.75)
    graph.nodes_gdf.plot(ax=ax, color=node_color, markersize=18, alpha=0.95)
    ax.set_title(title)
    ax.set_axis_off()


fig, axes = plt.subplots(1, 2, figsize=(13, 6), constrained_layout=True)
plot_graph_edges_and_nodes(G_drive_simple, axes[0], "Drive graph, simplify=True")
plot_graph_edges_and_nodes(G_drive_detailed, axes[1], "Drive graph, simplify=False")
plt.show()
../_images/d49ebfafdd9996c32ef52cf7649d4a31d6513842eb5645676033949192ddfcd6.png

Clipping by the exact territory boundary

OpenStreetMap ways can cross the requested boundary. With clip_by_territory=True, edge geometries are clipped by the projected boundary before graph construction. The red edges below are the geometries affected by clipping.

import geopandas as gpd

G_drive_unclipped = get_drive_graph(
    osm_id=OSM_ID,
    simplify=True,
    clip_by_territory=False,
    keep_largest_subgraph=False,
)
G_drive_clipped = get_drive_graph(
    osm_id=OSM_ID,
    simplify=True,
    clip_by_territory=True,
    keep_largest_subgraph=False,
)

boundary_local = gpd.GeoDataFrame(geometry=[boundary], crs=4326).to_crs(G_drive_unclipped.crs)
boundary_geom = boundary_local.geometry.iloc[0]

unclipped_edges = G_drive_unclipped.edges_gdf
affected_mask = ~unclipped_edges.geometry.within(boundary_geom)
affected_edges = unclipped_edges.loc[affected_mask]

print("Edges before clipping:", len(G_drive_unclipped.edges_gdf))
print("Edges affected by clipping:", len(affected_edges))
print("Edges after clipping:", len(G_drive_clipped.edges_gdf))
Edges before clipping: 1243
Edges affected by clipping: 14
Edges after clipping: 1243
fig, axes = plt.subplots(1, 2, figsize=(13, 6), constrained_layout=True)

boundary_local.boundary.plot(ax=axes[0], color="#222222", linewidth=1.2)
unclipped_edges.plot(ax=axes[0], color="#8FA1AB", linewidth=0.7, alpha=0.7)
if not affected_edges.empty:
    affected_edges.plot(ax=axes[0], color="#D84A3A", linewidth=1.6, alpha=0.95)
axes[0].set_title("Before clipping: affected edges in red")
axes[0].set_axis_off()

boundary_local.boundary.plot(ax=axes[1], color="#222222", linewidth=1.2)
G_drive_clipped.edges_gdf.plot(ax=axes[1], color="#496A81", linewidth=0.8, alpha=0.8)
G_drive_clipped.nodes_gdf.plot(ax=axes[1], color="#D84A3A", markersize=10, alpha=0.9)
axes[1].set_title("After clip_by_territory=True")
axes[1].set_axis_off()

plt.show()
../_images/15ebb113b4a51b532a147af4caae9aabe41d6fae8e9efbf54ea541f77fcd3317.png

Keeping the largest component

keep_largest_subgraph=True removes disconnected fragments. For directed drive graphs the default mode keeps the largest strongly connected component.

G_drive_all = get_drive_graph(
    osm_id=OSM_ID,
    simplify=True,
    keep_largest_subgraph=False,
)
G_drive_main = get_drive_graph(
    osm_id=OSM_ID,
    simplify=True,
    keep_largest_subgraph=True,
)

print("All components:", len(G_drive_all.nodes_gdf), "nodes,", len(G_drive_all.edges_gdf), "edges")
print("Largest component:", len(G_drive_main.nodes_gdf), "nodes,", len(G_drive_main.edges_gdf), "edges")
2026-07-07 15:17:42.948 | WARNING  | Removing 38 nodes outside the largest strongly connected component. Retaining 707 of 745 nodes.
All components: 745 nodes, 1243 edges
Largest component: 707 nodes, 1188 edges

Walk graph

Pedestrian graphs are undirected by default. Use walk_speed to control the time_min edge weight.

from iduedu import get_walk_graph

G_walk = get_walk_graph(
    osm_id=OSM_ID,
    simplify=True,
    walk_speed=5 * 1000 / 60,
    keep_largest_subgraph=True,
)

G_walk
2026-07-07 15:17:44.234 | WARNING  | Removing 332 nodes outside the largest connected component. Retaining 19588 of 19920 nodes.
UrbanGraph(nodes=19588, edges=26384, is_multigraph=True, is_directed=False, edge_direction_column=None, crs='EPSG:32636', type='walk')

Public-transport graph

transport_types accepts one type or a list of types. Travel time is computed with the active transport registry.

from iduedu import get_public_transport_graph

G_pt = get_public_transport_graph(
    osm_id=OSM_ID,
    transport_types=["bus", "tram"],
    clip_by_territory=False,
)

G_pt.edges_gdf[["type", "route", "length_meter", "time_min"]].head()
type route length_meter time_min
0 bus 220 374.030 5.786
1 bus 220 241.285 5.248
2 bus 220 362.113 3.857
3 bus 220 456.592 3.857
4 bus 220 713.562 4.876
G_pt.edges_gdf.explore(column="route",tiles='CartoDB Positron')
Make this Notebook Trusted to load map: File -> Trust Notebook

Join public transport with walking

join_pt_walk_graph connects platform-like public-transport nodes to nearby walking edges. get_intermodal_graph runs the walk and PT builders and then joins them in one call.

from iduedu import get_intermodal_graph, join_pt_walk_graph

G_joined = join_pt_walk_graph(
    G_pt,
    G_walk,
    max_dist=30,
    keep_largest_subgraph=True,
)

G_intermodal = get_intermodal_graph(
    osm_id=OSM_ID,
    max_dist=30,
    pt_kwargs={"transport_types": ["bus", "tram"]},
    walk_kwargs={"simplify": True},
)

G_joined, G_intermodal
2026-07-07 15:17:48.922 | WARNING  | Removing 603 nodes outside the largest strongly connected component. Retaining 21237 of 21840 nodes.
2026-07-07 15:17:52.027 | WARNING  | Removing 935 nodes outside the largest strongly connected component. Retaining 21237 of 22172 nodes.
(UrbanGraph(nodes=21237, edges=29794, is_multigraph=True, is_directed=True, edge_direction_column='oneway', crs='EPSG:32636', type='intermodal'),
 UrbanGraph(nodes=21237, edges=29794, is_multigraph=True, is_directed=True, edge_direction_column='oneway', crs='EPSG:32636', type='intermodal'))