import geopandas as gpd
import matplotlib.pyplot as plt
from cityseer.metrics import layers
from cityseer.tools import graphs, ioAdding GTFS transport data to accessibility
This notebook demonstrates how GTFS (General Transit Feed Specification) public transport data can be integrated into the street network before computing landuse accessibility metrics. By adding metro stops and segment travel times, the accessibility calculations can account for the extended reach provided by public transport.
Accessibility to retail premises is computed twice: once on the street network alone, and again after adding the Madrid Metro. The difference reveals how the metro network extends the catchment area for retail access.
streets_gpd = gpd.read_file("data/madrid_streets/street_network.gpkg")
streets_gpd = streets_gpd.explode(reset_index=True)
G = io.nx_from_generic_geopandas(streets_gpd)
G_dual = graphs.nx_to_dual(G)
nodes_gdf, _edges_gdf, network_structure = io.network_structure_from_nx(G_dual)INFO:cityseer.tools.graphs:Merging parallel edges within buffer of 1.
INFO:cityseer.tools.graphs:Converting graph to dual.
INFO:cityseer.tools.graphs:Preparing dual nodes
INFO:cityseer.tools.graphs:Preparing dual edges (splitting and welding geoms)
INFO:cityseer.tools.io:Preparing node and edge arrays from networkX graph.
INFO:cityseer.graph:Edge R-tree built successfully with 452252 items.
prems_gpd = gpd.read_file("data/madrid_premises/madrid_premises.gpkg")
prems_gpd.head()| index | local_id | local_distr_id | local_distr_desc | local_neighb_id | local_neighb_desc | local_neighb_code | local_census_section_id | local_census_section_desc | section_id | section_desc | division_id | division_desc | epigraph_id | epigraph_desc | easting | northing | geometry | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 10003324 | 1 | CENTRO | 105 | UNIVERSIDAD | 5 | 1091 | 91 | I | hospitality | 56 | food_bev | 561001 | RESTAURANTE | 440181.6 | 4475586.5 | POINT (440181.6 4475586.5) |
| 1 | 1 | 10003330 | 1 | CENTRO | 105 | UNIVERSIDAD | 5 | 1115 | 115 | R | art_rec_entert | 90 | creat_entert | 900003 | TEATRO Y ACTIVIDADES ESCENICAS REALIZADAS EN D... | 440000.6 | 4474761.5 | POINT (440000.6 4474761.5) |
| 2 | 2 | 10003356 | 1 | CENTRO | 104 | JUSTICIA | 4 | 1074 | 74 | I | hospitality | 56 | food_bev | 561004 | BAR RESTAURANTE | 440618.6 | 4474692.5 | POINT (440618.6 4474692.5) |
| 3 | 3 | 10003364 | 1 | CENTRO | 104 | JUSTICIA | 4 | 1075 | 75 | G | wholesale_retail_motor | 47 | retail | 472401 | COMERCIO AL POR MENOR DE PAN Y PRODUCTOS DE PA... | 440666.6 | 4474909.5 | POINT (440666.6 4474909.5) |
| 4 | 4 | 10003367 | 1 | CENTRO | 106 | SOL | 6 | 1119 | 119 | G | wholesale_retail_motor | 47 | retail | 477701 | COMERCIO AL POR MENOR DE JOYAS, RELOJERIA Y BI... | 440378.6 | 4474380.5 | POINT (440378.6 4474380.5) |
Compute baseline retail accessibility (without GTFS data). The _not suffix columns preserve the baseline values for later comparison.
# compute accessibility
distances = [800, 1600]
nodes_gdf, prems_gpd = layers.compute_accessibilities(
prems_gpd,
landuse_column_label="division_desc",
accessibility_keys=["retail"],
nodes_gdf=nodes_gdf,
network_structure=network_structure,
distances=distances,
)
# keep a copy before adding GTFS for comparison
for col in nodes_gdf.columns:
if col.startswith("cc_"):
nodes_gdf[f"{col}_not"] = nodes_gdf[col]INFO:cityseer.metrics.layers:Computing land-use accessibility for: retail
INFO:cityseer.metrics.layers:Assigning data to network.
INFO:cityseer.data:Assigning 117358 data entries to network nodes (max_dist: 100).
INFO:cityseer.data:Finished assigning data. 199450 assignments added to 26679 nodes.
INFO:cityseer.config:Metrics computed for:
INFO:cityseer.config:Distance: 800m, Beta: 0.005, Walking Time: 10.0 minutes.
INFO:cityseer.config:Distance: 1600m, Beta: 0.0025, Walking Time: 20.0 minutes.
Load the Madrid Metro GTFS data into the NetworkStructure using add_transport_gtfs. This adds metro stops as nodes and segment travel times as edges.
network_structure, stops, avg_stop_pairs = io.add_transport_gtfs(
"data/madrid_gtfs/madrid_metro",
network_structure,
nodes_gdf.crs,
)INFO:cityseer.tools.io:Loading GTFS data from data/madrid_gtfs/madrid_metro
INFO:cityseer.tools.io:Loaded 1272 stops and 2360 stop times
INFO:cityseer.tools.io:Adding GTFS stops to network nodes (with street linking logic).
INFO:cityseer.tools.io:Generating segment durations between stops.
INFO:cityseer.tools.io:Adding GTFS segments to network edges.
INFO:cityseer.graph:Edge R-tree built successfully with 452252 items.
# convert stops to geopandas
stops_gdf = gpd.GeoDataFrame(
stops,
geometry=gpd.points_from_xy(stops["stop_lon"], stops["stop_lat"]),
crs=4326, # Adjust the CRS to suit your data if needed
)
stops_gdf = stops_gdf.to_crs(streets_gpd.crs.to_epsg())
stops_gdf| stop_id | stop_code | stop_name | stop_desc | stop_lat | stop_lon | zone_id | stop_url | location_type | parent_station | stop_timezone | wheelchair_boarding | avg_wait_time | geometry | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | gtfs-data/madrid_gtfs/madrid_metro-par_4_1 | 1 | PLAZA DE CASTILLA | Paseo de la Castellana 189 | 40.46690 | -3.68917 | A | http://www.crtm.es | 0 | est_90_21 | NaN | 0 | 430.357143 | POINT (441575.052 4479808.7) |
| 1 | gtfs-data/madrid_gtfs/madrid_metro-acc_4_1_1 | 1 | Plaza de Castilla | Paseo de la Castellana 189 | 40.46682 | -3.68918 | NaN | http://www.crtm.es | 2 | est_90_21 | NaN | 0 | NaN | POINT (441574.135 4479799.826) |
| 2 | gtfs-data/madrid_gtfs/madrid_metro-acc_4_1_1040 | 1 | Ascensor | Plaza de Castilla 9 | 40.46555 | -3.68877 | NaN | http://www.crtm.es | 2 | est_90_21 | NaN | 0 | NaN | POINT (441607.793 4479658.584) |
| 3 | gtfs-data/madrid_gtfs/madrid_metro-acc_4_1_1043 | 1 | Intercambiador Superficie | Paseo de la Castellana 191 B | 40.46728 | -3.68915 | NaN | http://www.crtm.es | 2 | est_90_21 | NaN | 0 | NaN | POINT (441577.077 4479850.867) |
| 4 | gtfs-data/madrid_gtfs/madrid_metro-acc_4_1_1044 | 1 | Ascensor | Paseo de la Castellana 189 | 40.46702 | -3.68918 | NaN | http://www.crtm.es | 2 | est_90_21 | NaN | 0 | NaN | POINT (441574.308 4479822.026) |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 1267 | gtfs-data/madrid_gtfs/madrid_metro-est_4_234 | 234 | HOSPITAL SEVERO OCHOA | Avda Orellana 3 | 40.32177 | -3.76797 | B1 | http://www.crtm.es | 1 | NaN | Europe/Madrid | 2 | NaN | POINT (434754.721 4463754.488) |
| 1268 | gtfs-data/madrid_gtfs/madrid_metro-est_4_235 | 235 | LEGANES CENTRAL | Calle Virgen del Camino 1 | 40.32899 | -3.77154 | B1 | http://www.crtm.es | 1 | NaN | Europe/Madrid | 2 | NaN | POINT (434458.401 4464558.532) |
| 1269 | gtfs-data/madrid_gtfs/madrid_metro-est_4_236 | 236 | SAN NICASIO | Avda Mar Mediterráneo SN | 40.33616 | -3.77587 | B1 | http://www.crtm.es | 1 | NaN | Europe/Madrid | 2 | NaN | POINT (434097.544 4465357.604) |
| 1270 | gtfs-data/madrid_gtfs/madrid_metro-par_4_237 | 237 | OPERA | Plaza de Isabel II 9 | 40.41809 | -3.70928 | A | http://www.crtm.es | 0 | est_4_36 | NaN | 0 | 13.214286 | POINT (439826.687 4474404.289) |
| 1271 | gtfs-data/madrid_gtfs/madrid_metro-par_4_238 | 238 | PRINCIPE PIO | Paseo de la Florida 2 | 40.42099 | -3.72033 | A | http://www.crtm.es | 0 | est_90_18 | NaN | 1 | 13.214286 | POINT (438891.852 4474733.771) |
1272 rows × 14 columns
Recompute retail accessibility, now with the metro network included. The shortest-path algorithms can now route through metro segments, extending the effective catchment area.
nodes_gdf, prems_gpd = layers.compute_accessibilities(
prems_gpd,
landuse_column_label="division_desc",
accessibility_keys=["retail"],
nodes_gdf=nodes_gdf,
network_structure=network_structure,
distances=distances,
)INFO:cityseer.metrics.layers:Computing land-use accessibility for: retail
INFO:cityseer.metrics.layers:Assigning data to network.
INFO:cityseer.data:Assigning 117358 data entries to network nodes (max_dist: 100).
INFO:cityseer.data:Finished assigning data. 199450 assignments added to 26679 nodes.
INFO:cityseer.config:Metrics computed for:
INFO:cityseer.config:Distance: 800m, Beta: 0.005, Walking Time: 10.0 minutes.
INFO:cityseer.config:Distance: 1600m, Beta: 0.0025, Walking Time: 20.0 minutes.
Compare results
Compute the difference between the metro-integrated and baseline retail accessibility. The three-panel plot below shows weighted retail accessibility within 800m without the metro, with the metro, and the difference. Retail premises are shown in white and metro stops in red.
nodes_gdf.columnsIndex(['ns_node_idx', 'x', 'y', 'z', 'live', 'weight', 'primal_edge',
'primal_edge_node_a', 'primal_edge_node_b', 'primal_edge_idx',
'dual_node', 'cc_retail_800_nw', 'cc_retail_800_wt',
'cc_retail_1600_nw', 'cc_retail_1600_wt', 'cc_retail_nearest_max_1600',
'cc_retail_800_nw_not', 'cc_retail_800_wt_not', 'cc_retail_1600_nw_not',
'cc_retail_1600_wt_not', 'cc_retail_nearest_max_1600_not'],
dtype='object')
nodes_gdf["cc_retail_800_wt_diff"] = (
nodes_gdf["cc_retail_800_wt"] - nodes_gdf["cc_retail_800_wt_not"]
)fig, axes = plt.subplots(3, 1, figsize=(8, 20), facecolor="#1d1d1d")
nodes_gdf.plot(
column="cc_retail_800_wt_not",
cmap="magma",
legend=True,
ax=axes[0],
vmax=450,
)
axes[0].set_title(
"Retail Accessibility without Metro Stops",
fontsize=8,
color="#ddd",
)
stops_gdf.plot(ax=axes[0], color="red", markersize=1)
nodes_gdf.plot(
column="cc_retail_800_wt",
cmap="magma",
legend=True,
ax=axes[1],
vmax=450,
)
axes[1].set_title(
"Retail Accessibility with Metro Stops",
fontsize=8,
color="#ddd",
)
stops_gdf.plot(ax=axes[1], color="red", markersize=1)
nodes_gdf.plot(
column="cc_retail_800_wt_diff",
cmap="viridis",
legend=True,
ax=axes[2],
)
axes[2].set_title(
"Retail Accessibility with difference due to Metro Stops",
fontsize=8,
color="#ddd",
)
for ax in axes:
prems_gpd[prems_gpd["division_desc"] == "retail"].plot(
markersize=1,
edgecolor=None,
color="white",
legend=False,
ax=ax,
)
ax.set_xlim(438500, 438500 + 3500)
ax.set_ylim(4472500, 4472500 + 3500)
ax.axis(False)
plt.tight_layout()
Conclusion
This notebook demonstrated how to integrate GTFS public transport data (Madrid Metro) into the street network and compare retail accessibility with and without transit connectivity. The before-and-after comparison reveals how the metro extends the effective catchment area for retail access, particularly around metro stop locations.
Next steps: For centrality with GTFS data, see GTFS Centrality.