In [1]:
Copied!
import sys, os
if "google.colab" in sys.modules:
if not os.path.exists("pyGTFSHandler"):
!git clone --depth 1 https://github.com/CityScope/pyGTFSHandler.git
%cd pyGTFSHandler
!pip install -q -e ".[plot,osm,geocoding]"
%cd examples
import sys, os
if "google.colab" in sys.modules:
if not os.path.exists("pyGTFSHandler"):
!git clone --depth 1 https://github.com/CityScope/pyGTFSHandler.git
%cd pyGTFSHandler
!pip install -q -e ".[plot,osm,geocoding]"
%cd examples
Quickstart¶
pyGTFSHandler loads one or more GTFS feeds into a single Feed object and
gives you:
- An interactive route/timetable map (
route_map) - Stop-level and edge-level speed and headway analysis
- Polars DataFrames for your own analysis
This notebook uses small local GTFS feeds bundled in the repo
(examples/test_files/sevilla), so it runs offline with no downloads.
For a full real-world workflow (downloading feeds, computing service
intensity, exporting GIS layers, etc.) see
cambridge_massachusetts_usa_example.ipynb.
In [2]:
Copied!
from pathlib import Path
import datetime
from pyGTFSHandler import Feed
from pyGTFSHandler.maps import route_map
OUTPUT_DIR = Path("outputs/quickstart")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
DATA_DIR = Path("test_files/sevilla")
from pathlib import Path
import datetime
from pyGTFSHandler import Feed
from pyGTFSHandler.maps import route_map
OUTPUT_DIR = Path("outputs/quickstart")
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
DATA_DIR = Path("test_files/sevilla")
1. Load a feed¶
Feedaccepts one path or a list of paths (one per GTFS zip/folder)stop_group_distancemerges stops within N meters into a sharedparent_station(e.g. platforms on either side of a road)
In [3]:
Copied!
feed = Feed(
[DATA_DIR / "Metro_Sevilla", DATA_DIR / "TUSSAM"],
stop_group_distance=100,
)
feed
feed = Feed(
[DATA_DIR / "Metro_Sevilla", DATA_DIR / "TUSSAM"],
stop_group_distance=100,
)
feed
/home/miguel/Documents/Proyectos/pyGTFSHandler/pyGTFSHandler/models/stop_times.py:182: UserWarning: Some departure times are null and have been interpolated
warnings.warn("Some departure times are null and have been interpolated")
/home/miguel/Documents/Proyectos/pyGTFSHandler/pyGTFSHandler/models/frequencies.py:336: UserWarning: Reconciled frequencies.txt windows against headway_secs: 460 end_time(s) adjusted, 268 start_time(s) pulled forward, 0 window(s) fully covered by a preceding window dropped. warnings.warn(summary)
/home/miguel/Documents/Proyectos/pyGTFSHandler/pyGTFSHandler/models/shapes.py:1312: RuntimeWarning: direction_id assignment: 10 of 491 shape_ids (2.0%) had at least one stop where the geometry disagreed with their reported direction_id (direction_conflict=True), across 10 of 1062 stop(s) (0.9%). warnings.warn(
Out[3]:
<pyGTFSHandler.feed.Feed at 0x7b2064e54140>
2. Interactive route map¶
route_map builds one self-contained Leaflet map for a given service date:
- Every stop is shown as its route-type emoji (bus/rail/subway/...)
- Click a stop to open its timetable
- Click a timetable row to open the full trip itinerary
- "Filter lines…" and per-mode checkboxes narrow the map down live
- "Color by" -> Speed or Headway recolors stops and segments
In [4]:
Copied!
service_date = datetime.date(2026, 7, 6) # any date within the feed's calendar
m = route_map(feed, service_date)
m.save(str(OUTPUT_DIR / "map.html"))
m
service_date = datetime.date(2026, 7, 6) # any date within the feed's calendar
m = route_map(feed, service_date)
m.save(str(OUTPUT_DIR / "map.html"))
m
[output too large to embed in the docs site -- run this notebook locally or in Colab to view it]
3. Speed and headway as DataFrames¶
Every get_* method returns a Polars DataFrame (or LazyFrame), grouped by
whichever key you pass via by= and at=.
In [5]:
Copied!
start_time = datetime.time(7, 0)
end_time = datetime.time(9, 0)
stop_speed = feed.get_speed_at_stops(
date=service_date,
start_time=start_time,
end_time=end_time,
by="route_id", # group individual trip speeds by this column
at="parent_station", # compute for every 'parent_station', 'stop_id' or 'route_id'
how="mean", # 'mean', 'max' or 'min'
)
stop_speed.head()
start_time = datetime.time(7, 0)
end_time = datetime.time(9, 0)
stop_speed = feed.get_speed_at_stops(
date=service_date,
start_time=start_time,
end_time=end_time,
by="route_id", # group individual trip speeds by this column
at="parent_station", # compute for every 'parent_station', 'stop_id' or 'route_id'
how="mean", # 'mean', 'max' or 'min'
)
stop_speed.head()
/home/miguel/Documents/Proyectos/pyGTFSHandler/pyGTFSHandler/analysis/stops.py:1022: UserWarning: Sortedness of columns cannot be checked when 'by' groups provided
return gtfs_lf.filter(pl.col("isin_aoi") == True).drop("isin_aoi").collect()
sys:1: UserWarning: Sortedness of columns cannot be checked when 'by' groups provided
Out[5]:
shape: (5, 6)
| parent_station | route_id | distance_weight | time_weight | n_trips | speed |
|---|---|---|---|---|---|
| str | str | f64 | f64 | u32 | f64 |
| "580_file_1" | "87_file_1" | 3985.35083 | 1512.0 | 7 | 9.488931 |
| "1120_file_1" | "2_file_1" | 5436.412875 | 1800.0 | 16 | 10.872826 |
| "785_file_1" | "20_file_1" | 2577.427805 | 900.0 | 17 | 10.309711 |
| "445_file_1" | "15_file_1" | 2129.916176 | 821.25 | 16 | 9.336619 |
| "491_file_1" | "119_file_1" | 4237.941214 | 1359.2 | 5 | 11.224682 |
In [6]:
Copied!
stop_headway = feed.get_headway_at_stops(
date=service_date,
start_time=start_time,
end_time=end_time,
by="shape_direction", # group trips by geometric direction, no direction_id needed
at="parent_station",
how="best", # 'best', 'mean' or 'all'
n_divisions=1, # 1 -> 2 direction bins per stop (outbound/inbound)
)
stop_headway.head()
stop_headway = feed.get_headway_at_stops(
date=service_date,
start_time=start_time,
end_time=end_time,
by="shape_direction", # group trips by geometric direction, no direction_id needed
at="parent_station",
how="best", # 'best', 'mean' or 'all'
n_divisions=1, # 1 -> 2 direction bins per stop (outbound/inbound)
)
stop_headway.head()
Out[6]:
shape: (5, 5)
| parent_station | shape_direction | shape_ids | route_ids | headway |
|---|---|---|---|---|
| str | f64 | list[str] | list[str] | f64 |
| "762_file_1" | 208.222244 | ["5001301006_file_1", "5001201006_file_1", "4901301006_file_1"] | ["13_file_1", "12_file_1"] | 4.733333 |
| "494_file_1" | 15.23995 | ["4902901015_file_1", "4902901025_file_1", "4902901007_file_1"] | ["29_file_1"] | 14.65 |
| "51_file_1" | 319.149759 | ["5003401004_file_1"] | ["34_file_1"] | 13.766667 |
| "68_file_1" | 343.439382 | ["5003701004_file_1", "4900101020_file_1", … "5003701014_file_1"] | ["37_file_1", "1_file_1"] | 6.540741 |
| "100_file_1" | 219.323809 | ["5002101004_file_1", "4908701010_file_1", "5008701006_file_1"] | ["21_file_1", "87_file_1"] | 15.388963 |
Next steps¶
cambridge_massachusetts_usa_example.ipynb-- a complete real-world workflow: downloading feeds, service intensity, GIS exports, and the full DataFrame column referencedirection_and_headway_methodology.ipynb-- howdirection_idand headway are actually computed, including thedirection_id-conflict inspector (conflict_map), with synthetic and real-feed examples