Skip to main content
Open Innovations is winding down and will no longer be commencing new work from May 2026.

GTFS and GTFS-RT for beginners

We've been working on transport accessibility for Liverpool City Region Combined Authority. In particular, we've been using the UK Bus Open Data Service (BODS) to track the live locations of every bus in England, and compare the buses that actually ran to the timetable.

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.org homepage
General Transit Feed Specification home page

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

  1. You could find a bus by looking in agency.txt and searching for the agency_name, e.g. "First Leeds".
  2. You could then see the agency_id for that agency, e.g. "FLDS".
  3. Using routes.txt, you could use that agency_id to find all the route_id values for that agency.
  4. Using the route_short_name, you could find a specific bus, e.g. "12".
  5. With the route_id for that bus, you would then look in trips.txt to find all the trip_id values associated with that route.
  6. Each trip_id is a unique identifier of a particular time and date for a bus. Each trip_id has an associated service_id.
  7. Using the service_id you could find dates, specified by calendar.txt, and exceptions using calendar_dates.txt.
  8. Finally, using a trip_id you can find all every stop_id and it's arrival /departure time for that trip.
  9. Information about stops, including their location, are specified in stops.txt.
There are many additional files, which can be included optionally to provide further detail. In BODS, for example, we've noticed they usually include the shapes.txt file, which gives coordinate pairs that describe the shape of the route a bus takes. This is useful when mapping each bus route, for example.

Finally, a useful tool we've found is the MobilityData GTFS validator. This lets you check if your GTFS schedule meets the correct format.

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 module
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())
Where 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 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:

A dict structure showing an 'id' and 'vehicle'. The id is a unique string of numbers. The vehicle contains further objects: trip, position, current_stop_sequence, current_status, timestamp, and vehicle.
Example feed entity from a GTFS-RT file downloaded from BODS.
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:

  1. Stopped at - chose the earliest reported timestamp (smallest value).
  2. Incoming at - chose the most recent reported timestamp(largest value).
  3. In transit to - chose the most recent reported timestamp (largest value).
By iterating through all your data and following the above priority for when a bus arrived at each stop, you should be able to update the timetabled arrival times with the real arrival time.

It may be the case that for certain combinations of stops and trips, you don't have any data. In that case, an option for filling in the gaps may be to use an interpolation method, to estimate when the bus arrived at the stop.

Final thoughts

GTFS is complex, but not complicated. You'll need patience and continual reference to the documentation. One of the main challenges with GTFS and GTFS-RT is that the quality of the schedules and live data can vary greatly depending on the publisher. We hope that as more people move from using TransXChange and SiriVM, to GTFS and GTFS-RT, general awareness and understanding of the latter will improve, and so too will the data.

By working in the open, we hope it will encourage others to work with us to improve the data and build open tools. You can see how we've codified some of the ideas in this blog on our bus tracking GitHub repository.

We encourage anyone who's working on this to get in touch and share ideas. We may be able to help you, or you may teach us something we didn't know.