GTFS and GTFS-RT for beginners
Bus timetables are available in TransXChange and GTFS formats. Live location data is available in SiriVM and GTFS-RT. The different formats have their merits and limitations. In our previous work on buses, we'd used the SiriVM feed for live location data. This was because BODS hadn't yet started publishing the live location data in GTFS-RT format. However, in this latest work, we made the decision to switch our source for live location to GTFS-RT because it is an open standard, which means that the work we do on this project will be applicable to anywhere in the world that uses GTFS and GTFS-RT.
Through this project, we have learnt a lot about both GTFS and GTFS-RT and wanted to share what we've learnt to help others avoid some of the difficulty getting to grips with the data.
Comprehensive documentation is available on the GTFS website. For specific information, that is your best bet. However, when you're first starting out, it can be difficult to know where or what to look for. So this is a short guide for beginners on how to use the General Transit Feed Specification (GTFS) and its real time equivalent (GTFS-RT).

GTFS
At its most basic form, a GTFS Schedule dataset is composed of 7 files: agency.txt, routes.txt, trips.txt, stops.txt, stop_times.txt, calendar.txt and calendar_dates.txt. These are zipped together, so when you download a GTFS file, it will just be a .zip file that you'll need to extract the individual files from.
agency.txt:
Contains information about the companies that operate the services. It has a field agency_id which you can use to link to routes.txt.
routes.txt:
A route is a collection of trips – equivalent to a bus number, e.g. the number 12 bus. It can be linked to agency.txt using the agency_id field.
trips.txt
A "trip" is an individual bus, running on a specific route, at a specific time of day. Each trip has a unique trip_id. Trips are unique per day, but not across multiple days. An example trip could be: the number 12 bus, run by First Leeds, that departs at 7am, which runs Monday–Friday. It can be linked to agency.txt using agency_id.
stops.txt
Information about all the stops on all trips specified in the timetable. Each stop has a unique stop_id, along with its coordinates and a name. It can be linked to stop_times.txt using stop_id.
stop_times.txt
The timetabled arrival and departure time for each stop on every trip. Can be linked to trips.txt using trip_id.
calendar.txt
The start and end dates for services, and the days of the week that they run on. Contains a service_id, which can be linked to trips.txt.
calendar_dates.txt
Tells you about services that have been added or removed for a specified date e.g. a bus service isn't running on Christmas day or New Year's Day. Can be linked to calendar.txt and trips.txt using service_id.
Example
- You could find a bus by looking in agency.txt and searching for the
agency_name, e.g. "First Leeds". - You could then see the
agency_idfor that agency, e.g. "FLDS". - Using routes.txt, you could use that
agency_idto find all theroute_idvalues for that agency. - Using the
route_short_name, you could find a specific bus, e.g. "12". - With the
route_idfor that bus, you would then look in trips.txt to find all thetrip_idvalues associated with that route. - Each
trip_idis a unique identifier of a particular time and date for a bus. Eachtrip_idhas an associatedservice_id. - Using the
service_idyou could find dates, specified by calendar.txt, and exceptions using calendar_dates.txt. - Finally, using a
trip_idyou can find all everystop_idand it's arrival /departure time for that trip. - Information about stops, including their location, are specified in stops.txt.
GTFS-RT
GTFS Realtime is a little more involved. The data is encoded and decoded using Protocol Buffers, which is a "compact binary representation designed for fast and efficient processing". However, this makes the raw data, stored in a binary (.bin or .pb) file, unintelligible to humans. You therefore need to use language specific bindings to load them into data model objects in your language of choice. There's a list of currently supported languages. We used Python.
The gtfs-realtime-bindings package can be installed using pip.
From there, you'll need the following lines of code:
# Import the moduleWhere PATH_TO_FILE is the path to a GTFS-RT binary file (not a zip file). feed is now a class containing all the data in that file, which has a header and entities. It can contain different things depending on the publisher. Each entity (each vehicle position) should have a
from google.transit import gtfs_realtime_pb2
# Initialise the feed class
feed = gtfs_realtime_pb2.FeedMessage()
# Read the file
with open(PATH_TO_FILE, 'rb') as file:
feed.ParseFromString(file.read())
trip_id. This is what enables you to match that vehicle to the timetable.
Understanding the data
Once you've parsed a GTFS-RT binary file, an example feed entity might look something like this:

Credit: Open Innovations
For the details on what each of the field names mean, it's best to read the documentation.
Once you're able to parse the real time data, you may wish to write some code to take the data you need and store it in a more friendly format, like a CSV file. This will be a much larger file, but easier to use and debug across multiple languages.
Using the data
In using BODS, we've noticed that there are lots of duplicate bus location reports. If you don't need the duplicates, it may be a useful step to remove any duplicate data that has the same trip_id, vehicle_id, timestamp, latitude, longitude, and bearing. In addition, you may wish to round coordinates to 5 decimal places, which is equivalent to around 1.1m precision at the equator. This will reduce the file size further.
The fields current_stop_sequence and current_status are particularly useful for matching buses to the timetable. current_stop_sequence will enable you to match the bus to a single stop on the given trip. The current_status will let you know if the bus is STOPPED_AT the stop, IN_TRANSIT_TO the stop, or INCOMING_AT (just about to stop at) the stop.
To know the real arrival time for a stop on a particular trip, we need the closest time to when the bus first arrived at the bus stop. The simplest way this can happen is if you have a timestamp with the stop status as STOPPED_AT for that stop.
But across a whole bus journey, you may not get a report with each of the three statuses for each stop. So the priority to check in the data for when a bus first arrived at a stop would be:
- Stopped at - chose the earliest reported timestamp (smallest value).
- Incoming at - chose the most recent reported timestamp(largest value).
- In transit to - chose the most recent reported timestamp (largest value).
