GTFS public transport

IduEdu can build a static public-transport iduedu.UrbanGraph from a local GTFS Schedule feed. The source may be a directory containing *.txt tables, a ZIP archive, an already-read iduedu.gtfs.GTFSFeed, or a list of those merged into one feed (see Merging several feeds). The GTFS public transport notebook walks through every step below on small synthetic feeds.

Building a graph

from iduedu import get_gtfs_public_transport_graph

public_transport = get_gtfs_public_transport_graph(
    "feed.zip",
    service_date="2026-08-04",
    start_time="07:00:00",
    end_time="10:00:00",
)

service_date applies calendar.txt and calendar_dates.txt. It accepts a Python date or datetime, YYYY-MM-DD, or YYYYMMDD. start_time is inclusive and end_time is exclusive; both accept GTFS time strings, Python time values, or seconds. An end time earlier than the start time selects an overnight window.

Omitting all three filters intentionally aggregates every trip in the feed. This can combine weekday, weekend, and seasonal service, so pass a date and time window when the graph should represent a particular operating period.

Required and optional tables

The builder requires agency.txt, stops.txt, routes.txt, trips.txt, and stop_times.txt. At least one of calendar.txt and calendar_dates.txt must also be present.

The builder also reads frequencies.txt, shapes.txt, pathways.txt, levels.txt, and feed_info.txt when available. transfers.txt and exact time-dependent transfer routing are not implemented yet.

Base route types are represented as tram (0), subway (1), train (2), bus (3), ferry (4), cable_tram (5), aerial_lift (6), funicular (7), trolleybus (11), and monorail (12). Google’s extended route types are mapped by explicit ranges: 100-117 train, 200-209 coach (kept apart from city buses), 400-404 and 406-499 subway, 405 monorail, 700-716 bus, 800-899 trolleybus, 900-906 tram, 1000 and 1200 ferry, 1300 aerial_lift, 1400 funicular, and 1500 taxi. Any other code, including the miscellaneous 300, 500, 600, 1600 and 1700 ranges, remains public_transport rather than a guess from neighbouring codes.

Static time model

Trips with the same route, direction, shape, stop order, and pickup/drop-off rules form a route pattern. For each pattern segment, time_min is the median scheduled time from departure at one stop to arrival at the next.

Boarding edges represent expected waiting rather than vehicle motion:

  • scheduled service uses half the mean gap between consecutive departures at the pattern stop;

  • frequencies.txt service uses headway_secs / 120 and duration-weighted averaging when frequency periods overlap the selected window;

  • a pattern stop with only one scheduled departure has no estimable headway, so its boarding edge is omitted by default. Set single_departure_wait_min to retain it with an explicit fallback.

Boarding and alighting edges always have length_meter = 0. Alighting also has time_min = 0. GTFS waiting times come from the feed and do not use iduedu.TransportRegistry defaults.

Geometry and distance

When shapes.txt supplies usable route geometry, segment geometry is cut from the matching shape and length_meter is its measured length. Otherwise the builder creates a straight line between consecutive route-stop points and assigns sqrt(2) * straight_line_length as the estimated travel distance. The square-root-of-two correction applies only to vehicle movement edges such as bus, tram, or subway; it never applies to boarding or alighting.

The graph CRS must be projected in metres. If crs is omitted, IduEdu estimates a local UTM CRS from the feed stops.

Merging several feeds

A city is often published as several feeds, and every feed numbers its own routes, trips, and stops. Pass a list of sources to merge them before the graph is built:

public_transport = get_gtfs_public_transport_graph(
    ["subway.zip", "bus.zip"],
    service_date="2026-08-04",
    start_time="07:00:00",
    end_time="10:00:00",
)

A list is merged with iduedu.merge_gtfs_feeds(), which qualifies every identifier by its source, so route 1 of one feed and route 1 of another stay distinct. Call it directly to choose the prefixes or to fuse stops that different agencies publish for the same place:

from iduedu import get_gtfs_public_transport_graph, merge_gtfs_feeds

feed = merge_gtfs_feeds(["subway.zip", "bus.zip"], merge_stops_within=20)
public_transport = get_gtfs_public_transport_graph(feed, service_date="2026-08-04")

Stops are kept distinct by default: fusing them changes the network topology, and an inferred transfer would look like a published one. Distances of about 15-25 m suit feeds from different agencies.

Stations, pathways, and intermodal joining

pathways.txt creates station-internal pathway edges. Explicit traversal_time is converted from seconds to minutes; otherwise time is estimated from pathway length and walk_speed_m_per_min. Station entrances, nodes, boarding areas, and platforms are retained when referenced by the served stops or pathways.

A stop that names a parent_station is linked to it with a zero-cost, bidirectional station_link edge. Only stops already present in the graph are linked. Without these edges, a station described down to boarding areas would leave its platforms in a separate component that keep_largest_subgraph removes.

To join GTFS public transport to an OSM walking graph, build the walk layer first and use the same CRS:

from iduedu import get_gtfs_public_transport_graph, get_walk_graph, join_pt_walk_graph

walk = get_walk_graph(osm_id=1114252)
public_transport = get_gtfs_public_transport_graph(
    "feed.zip",
    service_date="2026-08-04",
    start_time="07:00:00",
    end_time="10:00:00",
    crs=walk.crs,
)
intermodal = join_pt_walk_graph(public_transport, walk)

API reference

iduedu.get_gtfs_public_transport_graph(feed, *, service_date=None, start_time=None, end_time=None, crs=None, single_departure_wait_min=None, walk_speed_m_per_min=83.33333333333333)[source]

Build a static public-transport UrbanGraph from a local GTFS feed.

Boarding weights use the first-iteration half-headway heuristic. Scheduled services use half the mean gap between consecutive departures of the same route pattern at the same stop. frequencies.txt uses headway_secs / 120.

None date/time values are intentionally unbounded: with all three filters omitted, every trip in the feed participates. This is an aggregate all-service graph, not the timetable for a particular real-world day.

Parameters:
  • feed (Union[str, Path, GTFSFeed, Sequence[str | Path | GTFSFeed]]) – Directory containing GTFS text files, a GTFS ZIP archive, an already-read iduedu.gtfs.GTFSFeed, or a sequence of any of those. A sequence is merged with iduedu.merge_gtfs_feeds() first, which qualifies identifiers by source; pass the merged feed yourself when you need to control prefixes or fuse stops across feeds.

  • service_date (date | datetime | str | None) – Optional service date used with calendar.txt and calendar_dates.txt. Accepts date, YYYY-MM-DD or YYYYMMDD.

  • start_time (time | str | int | float | None) – Inclusive departure-time bound. None means unbounded.

  • end_time (time | str | int | float | None) – Exclusive departure-time bound. None means unbounded.

  • crs (Any | None) – Projected target CRS. If omitted, a local UTM CRS is estimated.

  • single_departure_wait_min (float | None) – Explicit wait fallback for a pattern-stop with one scheduled departure. If None, its boarding edge is omitted.

  • walk_speed_m_per_min (float) – Fallback speed for pathways without an explicit traversal time.

Return type:

UrbanGraph

iduedu.merge_gtfs_feeds(sources, *, prefixes=None, merge_stops_within=None, crs=None, validate=True)[source]

Combine several GTFS feeds into one that the graph builder can consume.

Identifiers are qualified by their source, so a route_id of 1 in two feeds becomes two distinct routes. Definitions and references are rewritten together, and blank optional references stay blank.

Parameters:
  • sources (Sequence[str | Path | GTFSFeed]) – Paths to GTFS directories or ZIP archives, or already-read feeds. A single source is returned unchanged unless stop merging is requested, so this function is a no-op where merging is not needed.

  • prefixes (Optional[Sequence[str]]) – One prefix per source. Defaults to each source’s file name, with a numeric suffix where two sources share one.

  • merge_stops_within (float | None) – Distance in metres below which stops published by different feeds are treated as the same place and fused into one node. None (the default) keeps every stop distinct: fusing stops changes the topology of the network, and doing it silently would make an inferred transfer indistinguishable from a published one. Values around 15–25 m suit feeds from different agencies; the 1 m used for OSM platforms is far too tight here, because two agencies survey the same kerb independently.

  • crs (Any | None) – Projected CRS in metres used for the distance test. Estimated from the stops when omitted.

  • validate (bool) – Run iduedu.gtfs.validate_gtfs_feed() on the result. Leave this on: it is what makes the merge verifiable rather than merely plausible.

Return type:

GTFSFeed

Returns:

A iduedu.gtfs.GTFSFeed whose source names the merged inputs.

Raises:
  • ValueError – If no sources are given, or the number of prefixes does not match the number of sources.

  • GTFSValidationError – If the merged feed breaks a GTFS contract.

iduedu.gtfs.read_gtfs_feed(source, *, table_names=None, usecols=None)[source]

Read GTFS Schedule text tables from a directory or ZIP archive.

All fields are retained as strings. Numeric, date, time and geometry conversion belongs to the validation and graph-building layers.

Return type:

GTFSFeed

Parameters:
iduedu.gtfs.validate_gtfs_feed(feed)[source]

Validate the structural GTFS contracts required by the graph builder.

Return type:

None

Parameters:

feed (GTFSFeed)

class iduedu.gtfs.GTFSFeed(source, tables)[source]

In-memory GTFS Schedule tables loaded from a directory or ZIP archive.

Parameters:
exception iduedu.gtfs.GTFSValidationError[source]

Raised when a GTFS feed cannot be converted into an UrbanGraph.