A GTFS query returns no trips, or the wrong trips, on a date

On this page

Problem statement

You filter a GTFS feed to a date and get nothing back β€” no active services, no trips, an empty map. Or you get a count that looks plausible and is wrong: the weekday timetable on a holiday, or two timetables at once. The code runs without error either way, which is what makes this failure expensive: the empty or wrong result flows into frequency maps and accessibility scores before anyone notices.

There are five common causes, and they can be reproduced on a clean, valid feed. On TriMet's feed for Thanksgiving 2026 the correct answer is 4,560 trips. A date outside the feed gives 0, a hyphenated date gives 0, exception dates read as integers give 632, and ignoring calendar_dates.txt gives 632. The fifth β€” numeric service IDs read as integers on one side of the join β€” gives 0 in feeds whose IDs are all digits; TriMet's contain letters, so it is immune.

Quick answer

Check, in this order: the date is inside the feed, the date is formatted YYYYMMDD, the tables were read as strings, both calendar files were applied, and exceptions are actually matching:

import pandas as pd

FEED = "trimet_gtfs"
cal = pd.read_csv(f"{FEED}/calendar.txt", dtype=str)
cd = pd.read_csv(f"{FEED}/calendar_dates.txt", dtype=str)
trips = pd.read_csv(f"{FEED}/trips.txt", dtype=str)
info = pd.read_csv(f"{FEED}/feed_info.txt", dtype=str).iloc[0]
DAYS = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]

def trips_on(date):
    ds = pd.Timestamp(date).strftime("%Y%m%d")                  # normalise the format
    assert info.feed_start_date <= ds <= info.feed_end_date, f"{ds} outside feed"
    dow = DAYS[pd.Timestamp(date).dayofweek]
    weekly = set(cal.loc[(cal[dow] == "1") & (cal.start_date <= ds) & (cal.end_date >= ds), "service_id"])
    ex = cd[cd.date == ds]
    active = (weekly | set(ex.loc[ex.exception_type == "1", "service_id"])) \
        - set(ex.loc[ex.exception_type == "2", "service_id"])
    return trips[trips.service_id.isin(active)]

print(len(trips_on("2026-11-26")), "trips on Thanksgiving")
4560 trips on Thanksgiving

Every table is read with dtype=str, every date is normalised to YYYYMMDD before comparing, and the window check raises instead of returning an empty set.

Five causes of empty or wrong GTFS date queries and their fixes. empty or wrong, never an errorcausefixdate outside the feed windowcheck feed_info, raise if outsidedate written 2026-11-26normalise to YYYYMMDD firstnumeric IDs read as integersdtype=str on every tablecalendar_dates.txt skippedapply both filesexception dates read as intcount exception rows foundThe first three return nothing; the last two return the wrong timetable spatialworkflow.io
Five causes, each silent; the first three give nothing, the last two give the wrong timetable.

Window, date format, strings and exceptions

1. Check the feed window

feed_info.txt gives feed_start_date and feed_end_date. If it is missing, take the smallest start_date and largest end_date in calendar.txt, and the range of calendar_dates.txt. Feeds are often published for a few months at a time; a date next quarter is outside yesterday's download.

2. Normalise the date format

GTFS dates are YYYYMMDD strings. Comparing "2026-11-26" with "20261126" as text is legal and wrong, because the hyphen sorts before every digit.

3. Read everything as strings

A service_id of 7.736 read as a float becomes 7.736, but one of 012 becomes 12, and a column of dates read as integers never equals a string date. Mixed types across tables are the most common reason a join silently returns nothing.

4. Apply calendar_dates.txt

The weekly pattern in calendar.txt is the default; exceptions add and remove services on specific dates. Skipping them gives the wrong timetable on holidays and nothing at all for services defined only by exceptions.

5. Confirm the exceptions matched

Count the exception rows for your date. Zero rows on a public holiday is a sign that the comparison is failing, not that the holiday has no special timetable.

6. Pick analysis dates that are ordinary

For typical service, choose a date with no exceptions for any service, inside the feed, and state it.

Trips returned for Thanksgiving 2026 by the correct query and by four mistakes. trips on Thursday 26 November 2026, TriMetcorrect query4,560date outside feed0hyphenated date0dates read as int632calendar.txt only632025005000For the outside-the-feed case, 4 March 2027 stands in: the feed ends on 27 February spatialworkflow.io
One correct answer and four wrong ones from the same valid feed.

Three silent failures reproduced

The data used on this page

TriMet's GTFS feed for Portland, Oregon, downloaded from developer.trimet.org and unzipped to trimet_gtfs/. TriMet republishes it every few weeks, so counts of trips and stop times move between versions; the setup prints the version the numbers on this page came from.

import io
import os
import zipfile

import pandas as pd
import requests

if not os.path.exists("trimet_gtfs"):
    feed = requests.get("https://developer.trimet.org/schedule/gtfs.zip", timeout=600)
    zipfile.ZipFile(io.BytesIO(feed.content)).extractall("trimet_gtfs")
info = pd.read_csv("trimet_gtfs/feed_info.txt", dtype=str).iloc[0]
print(f"TriMet feed {info.feed_version}, valid {info.feed_start_date} to {info.feed_end_date}")
TriMet feed 20260927-20261007-0900, valid 20260927 to 20270227

Example 1 β€” the date format

ds_wrong = "2026-11-26"
weekly = cal[(cal.thursday == "1") & (cal.start_date <= ds_wrong) & (cal.end_date >= ds_wrong)]
print("services matched with a hyphenated date:", len(weekly))
print("'2026-11-26' >= '20260914'?", "2026-11-26" >= "20260914")
services matched with a hyphenated date: 0
'2026-11-26' >= '20260914'? False

Example 2 β€” integer types on one side of a join

trips_int = pd.read_csv(f"{FEED}/trips.txt")                  # default inference
cd_int = pd.read_csv(f"{FEED}/calendar_dates.txt")
print(trips_int.service_id.dtype, cd_int.date.dtype)
print("exception rows found for '20261126' in the inferred table:",
      int((cd_int.date == "20261126").sum()))
str int64
exception rows found for '20261126' in the inferred table: 0

The dates became integers, so the string comparison finds no exception rows at all. The weekly pattern alone then gives the weekday timetable on Thanksgiving.

Example 3 β€” the other three results

d = "20261126"
weekly = set(cal.loc[(cal.thursday == "1") & (cal.start_date <= d) & (cal.end_date >= d), "service_id"])
print("calendar.txt only:", trips.service_id.isin(weekly).sum())
try:
    trips_on("2027-03-04")
except AssertionError as e:
    print("outside the window:", e)
calendar.txt only: 632
outside the window: 20270304 outside feed

Why none of these raise

Why none of these raise

Each mistake produces a valid pandas operation. A string comparison between two differently formatted dates returns a boolean; a comparison between an integer column and a string returns all False; an isin against an empty set returns an empty frame. The pipeline carries on with zero rows, or with the wrong rows, and the first visible symptom is usually a map.

Why holidays expose it

On an ordinary weekday, applying calendar.txt alone gives the right answer for most feeds, so the missing exception step goes unnoticed through testing. Holidays are when exceptions do their work, and they are the dates most often chosen for "what happens on Thanksgiving?" questions β€” or silently included in a month-long average.

Why the window check should raise

An empty result is indistinguishable from "no service". Raising on a date outside the feed turns a silent, plausible zero into an error at the line that caused it.

Checks before trusting a GTFS date query. before trusting a date filterdate inside feed_start–feed_endraise if notdate formatted YYYYMMDDno hyphensevery table read with dtype=strids and datescalendar and calendar_dates both appliedexception rows found on holidayszero is suspiciousa non-empty resultproves none of theseRun once per pipeline; each check replaces a silent failure with a loud one spatialworkflow.io
Five checks, run once, prevent the failures that would otherwise be found on a map.

Missing feed info, merged feeds and validators

  • Feeds without feed_info.txt need the window derived from the calendars.
  • Merged feeds can have overlapping service IDs; prefix them per feed.
  • Seasonal service sometimes ends mid-feed; a route with no trips on a date may simply be off-season.
  • Time zones do not affect dates in GTFS; all dates are local to the agency.
  • Trips past midnight belong to the previous service date; see the past-midnight fix.
  • Validators such as the MobilityData GTFS validator flag expired feeds.

FAQ

Why does my GTFS date filter return no trips?

Most often the date is outside the feed, formatted with hyphens, or compared against columns read as integers.

Why do I get the weekday timetable on a holiday?

Because calendar_dates.txt was not applied, or its dates were read as integers and never matched.

What format are GTFS dates?

YYYYMMDD strings, such as 20261126.

How do I find the feed's date range?

feed_start_date and feed_end_date in feed_info.txt, or the extremes of the calendar files.

Should an out-of-range date return an empty result?

It should raise. An empty result looks like a day without service.

How do I choose a typical weekday?

Pick one inside the feed with no exception rows for any service.