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()
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()
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')
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'))