Reading GTFS data - team-exception-handlers/TransitSync GitHub Wiki
This page explains how the GTFS files we use are structured and how to get the information the simulator needs out of them. All examples come from the REM feed we have on hand (version 20260520).
What GTFS is
GTFS (General Transit Feed Specification) is the standard format transit agencies use to publish their schedules. A feed is a zip file of plain CSV text files. Each file is one table, the first line is the column names, and the files link to each other through ID columns, the same way tables link through foreign keys in a database.
GTFS describes the planned schedule only. It does not say where a train actually is or whether it was late. That comes from GTFS-Realtime or archived arrival data, which is a separate source.
Full reference: https://gtfs.org/documentation/schedule/reference/
Feeds we have
| Feed | Version | Valid from | Valid to | Licence |
|---|---|---|---|---|
| REM | 20260520 | 2026-05-19 | 2026-12-31 | CC BY 4.0 (credit required) |
| STM | 20260805110000_26S | 2026-06-15 | 2026-10-25 | Check the licence file in the feed |
The STM feed expires on October 25. Feeds are replaced every few months, so we always record which version a result was built on (see "Where the data lives" below).
How the files connect
routes.txt one row per line (REM A1, STM green line, ...)
│ route_id
▼
trips.txt one row per single run of a train from one end to the other
│ trip_id │ service_id
▼ ▼
stop_times.txt one row per calendar.txt / calendar_dates.txt
│ stop_id stop on a trip which days that trip runs
▼
stops.txt platforms, stations and entrances
To answer "when does a train leave McGill?", you go from the stop to stop_times, from there to trips to get the line and direction, and from trips to the calendar to check it runs that day.
The files we use
routes.txt
One row per line.
route_id,agency_id,route_short_name,route_long_name,route_type,...
S1,REM,A4-A1,A4 - Deux-Montagnes / A1 - Brossard,0,...
route_type says what kind of vehicle it is: 0 is tram or light rail, 1 is subway, 3 is bus. The REM is listed as 0 and the STM metro lines as 1. The STM feed also contains every bus route, which we filter out.
STM metro route IDs: 1 green, 2 orange, 4 yellow, 5 blue.
stops.txt
Platforms, stations and entrances, with coordinates.
stop_id,stop_name,stop_lat,stop_lon,zone_id,location_type,parent_station,...
STA_ZCD_BT_QUAI_01_MCG,Station McGill - Quai 1, 45.502695, -73.572374,1,0,ST_MCG_1,...
STA_ZCD_BT_QUAI_02_MCG,Station McGill - Quai 2, 45.502651, -73.572415,1,0,ST_MCG_1,...
ST_MCG_1,Station McGill, 45.502535, -73.572116,1,1,,...
ST_MCG_1_entree_centre_eaton,Entrée Centre Eaton,45.502368,-73.572284,1,2,ST_MCG_1,...
location_type tells you what the row is:
- 0: a platform (quai), where trains actually stop.
stop_timesonly points to these - 1: the station as a whole. Its platforms point to it through
parent_station - 2: an entrance or exit
Our three REM interchanges:
| Station | Station ID | Platform toward Brossard | Platform away from Brossard |
|---|---|---|---|
| Gare Centrale | ST_GCT_1 | STA_ZCD_BT_QUAI_02_GCT | STA_ZCD_BT_QUAI_01_GCT |
| McGill | ST_MCG_1 | STA_ZCD_BT_QUAI_02_MCG | STA_ZCD_BT_QUAI_01_MCG |
| Édouard-Montpetit | ST_EDM_1 | STA_ZCD_BT_QUAI_02_EDM | STA_ZCD_BT_QUAI_01_EDM |
trips.txt
One row per run of a train.
route_id,service_id,trip_id,direction_id,shape_id,trip_headsign,...
S3,RDS-32B220.xml,RDS-32B220.xml_0001,0,S30017,A3 - Anse-à-l'Orme,1
service_idlinks to the calendar (which days this trip runs)direction_idis 0 or 1. Its meaning is set by each agency. For the REM, 0 is away from Brossard (headsigns Deux-Montagnes, Anse-à-l'Orme, Bois-Franc) and 1 is toward Brossard. For the STM,directions.txtgives the name of each direction per line (for example route 1, direction 0 is Est)trip_headsignis the destination shown on the train
stop_times.txt
The largest file. One row per stop on each trip, in order.
trip_id,arrival_time,departure_time,stop_id,stop_sequence,...
RDS-32B220.xml_0001,05:29:15,05:30:00,STA_ZCD_BT_QUAI_01_RIV,1,...
RDS-32B220.xml_0001,05:32:02,05:32:30,STA_ZCD_BT_QUAI_01_DUQ,2,...
- The difference between
departure_timeandarrival_timeis the scheduled dwell, the time the train sits at the platform with its doors open. At our three REM stations it is fixed at 28 to 45 seconds depending on station and direction - The difference between two consecutive departures from the same platform is the headway, the time between trains. For the REM on weekdays it is 210 seconds at peak and 450 seconds otherwise
calendar.txt and calendar_dates.txt
Which days each service_id runs.
service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date
RDS-32B220.xml,1,1,1,1,1,0,0,20260519,20261224 <- weekdays
RDS-16B160.xml,0,0,0,0,0,1,0,20260523,20261231 <- Saturdays
RDS-16B156.xml,0,0,0,0,0,0,1,20260524,20261227 <- Sundays
calendar_dates.txt lists exceptions to that pattern. exception_type 1 means the service is added on that date, 2 means it is removed.
service_id,date,exception_type
RDS-32B220.xml,20261012,2 <- no weekday service on Oct 12 (Thanksgiving)
RDS-16B156.xml,20261012,1 <- Sunday service runs instead
frequencies.txt (if present)
Some feeds describe service as "a train every N seconds between two times" instead of listing every trip. Each row gives a trip_id, a start_time, an end_time and a headway_secs. The REM feed does not have this file. Check whether the STM feed uses it for the metro, since that changes how metro headways are read.
shapes.txt
The path a line follows on the map, as a list of points. Only needed for drawing the network (US-3.1), not for the simulation itself.
Reading times
- Times are local Montreal time, written
HH:MM:SS - Times can go past 24:00:00. A train leaving at 00:30 after a late evening is written
24:30:00because it belongs to the previous day's service. The REM feed goes up to26:16:39. Do not parse these with a normal time type. Convert them to seconds after midnight instead:24:30:00becomes 88200 - The database stores both the original text and the seconds value (
arrival_s,departure_s)
Finding which service runs on a given date
- Find the day of the week and check
calendar.txtfor everyservice_idwith a 1 in that column, where the date is betweenstart_dateandend_date - Remove any
service_idthat has that date withexception_type2 incalendar_dates.txt - Add any
service_idthat has that date withexception_type1
Skipping steps 2 and 3 is the most common mistake. On a holiday it gives you weekday service when the real schedule is Sunday service.
Worked example: weekday REM departures from McGill toward Brossard
select st.departure_time, t.trip_headsign
from gtfs.stop_times st
join gtfs.trips t using (feed_id, trip_id)
where st.feed_id = 'rem_20260520'
and st.stop_id = 'STA_ZCD_BT_QUAI_02_MCG'
and t.service_id = 'RDS-32B220.xml'
and st.departure_s between 8*3600 and 9*3600
order by st.departure_s;
The first rows are departures at 08:00:18, 08:03:48 and 08:07:18. Those 210 seconds between departures are the AM peak headway.
REM and STM differences
| REM | STM | |
|---|---|---|
| Feed contents | REM only | Metro and all buses |
| route_type | 0 | 1 for metro, 3 for bus |
| Stop ID style | STA_ZCD_BT_QUAI_01_MCG | Different format, check stops.txt for the metro platforms |
| Directions | Only in direction_id and headsign |
Named in directions.txt |
| Service IDs | One per day type | Many, one per garage and day type (bus garages make up most of them) |
Common mistakes
- Reading IDs as numbers. Always read every column as text first. Pandas will otherwise drop leading zeros or turn IDs into floats
- Extra spaces. Some REM coordinates have leading spaces (
45.502695). Strip every value - French characters. Files are UTF-8. Opening them in Excel without choosing UTF-8 turns "Édouard" into "Édouard"
- Platform vs station.
stop_timespoints to platforms (location_type0), never to the station row - Duplicate files. When files get uploaded twice under different names, check they are identical before loading
- Ignoring the calendar. A
trip_idon its own says nothing about which days it runs
Where the data lives
- Original zips are in Supabase Storage, bucket
gtfs-raw, underrem/<feed_version>/andstm/<feed_version>/. These are never edited - Parsed tables are in the Supabase database under the
gtfsschema. Each row has afeed_id(for examplerem_20260520) so several feed versions can be stored side by side.gtfs.feedslists every version loaded, with its dates and download date - Only rail routes are loaded. STM buses are dropped by the loader to keep the database small
- Values the simulator uses (headways per time band, dwells, platform IDs) are extracted once and committed to the repo as config. The simulator never reads GTFS at runtime
Licence and credit
The REM feed is published under CC BY 4.0. Anything we publish that uses it (the app, the report) needs a credit line:
Schedule data from the Réseau express métropolitain (REM), https://rem.info, used under CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). Data has been processed and modified for this project.