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 : 748 nodes, 1246 edges
simplify=False: 6558 nodes, 7056 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/2c76df4896dbcf2c316a618125c4426bed2607bd4c82c14a84737ad93767e54c.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: 1246
Edges affected by clipping: 14
Edges after clipping: 1246
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/f2521e77021a222ab9b80b44439df6b95048bb762f4b70020f23315ffe2290b3.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-09-15 14:01:06.436 | WARNING  | Removing 39 nodes outside the largest strongly connected component. Retaining 709 of 748 nodes.
All components: 748 nodes, 1246 edges
Largest component: 709 nodes, 1190 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-09-15 14:01:07.332 | WARNING  | Removing 330 nodes outside the largest connected component. Retaining 19802 of 20132 nodes.
UrbanGraph(nodes=19802, edges=26658, 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.

Routes continue far beyond the district, so clip_by_territory=True keeps only the parts inside it. Without clipping, stops outside the walking network cannot be attached to it and are dropped when the layers are joined below.

from iduedu import get_public_transport_graph

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

G_pt.edges_gdf[["type", "route", "length_meter", "time_min"]].head()
type route length_meter time_min
0 bus 220 373.759 1.111
1 bus 220 241.285 0.901
2 bus 220 362.113 1.078
3 bus 220 456.592 1.216
4 bus 220 713.562 1.592
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,
    clip_by_territory=True,
    pt_kwargs={"transport_types": ["bus", "tram"]},
    walk_kwargs={"simplify": True},
)

G_joined, G_intermodal
2026-09-15 14:01:14.293 | WARNING  | Removing 332 nodes outside the largest strongly connected component. Retaining 20940 of 21272 nodes.
(UrbanGraph(nodes=20938, edges=29158, is_multigraph=True, is_directed=True, edge_direction_column='oneway', crs='EPSG:32636', type='intermodal'),
 UrbanGraph(nodes=20940, edges=29162, is_multigraph=True, is_directed=True, edge_direction_column='oneway', crs='EPSG:32636', type='intermodal'))