How to find the trips running on a given date in GTFS

On this page

Problem statement

Nearly every GTFS analysis starts with the same filter: which trips run on the date I care about? Get it wrong and every downstream number β€” frequency, travel time, coverage β€” is computed on a timetable that never ran. The usual mistakes are applying calendar.txt without calendar_dates.txt, forgetting that some trips on the evening before spill past midnight into the date, and choosing a date outside the feed.

On TriMet’s feed for Tuesday 26 January 2027, 6,144 trips run. 101 of them start after midnight by the clock, still under Tuesday's service, and 190 trips from Monday's service are still running during the first hours of Tuesday's calendar date.

Quick answer

Resolve the active services for the date, filter trips to them, and keep the service date with the result:

import pandas as pd

FEED = "trimet_gtfs"
DAYS = ["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"]
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)

def services_on(date):
    d = pd.Timestamp(date)
    ds, dow = d.strftime("%Y%m%d"), DAYS[d.dayofweek]
    weekly = set(cal.loc[(cal[dow] == "1") & (cal.start_date <= ds) &
                         (cal.end_date >= ds), "service_id"])
    ex = cd[cd.date == ds]
    return (weekly | set(ex.loc[ex.exception_type == "1", "service_id"])) \
        - set(ex.loc[ex.exception_type == "2", "service_id"])

def trips_on(date):
    out = trips[trips.service_id.isin(services_on(date))].copy()
    out["service_date"] = pd.Timestamp(date).strftime("%Y%m%d")
    return out

day = trips_on("2027-01-26")
print(len(day), "trips on", day.service_date.iloc[0], "from services",
      sorted(day.service_id.unique()))
6144 trips on 20270126 from services ['7.735', 'A.735', 'D.735', 'Q.735', 'W.735']

Carrying service_date on every row is what lets a later step turn 25:10:00 into a real timestamp.

From a requested date to the timetable for that service day. trips on a dateinside feedwindow?else raiseactiveservicescalendar +exceptionsfilter trips6,144 on 26 Janfilterstop_times301,132 rowstagservice_dateon every rowAn empty result for a date outside the feed looks exactly like a day with no service spatialworkflow.io
Five steps, and the first one β€” is the date inside the feed? β€” is the one that fails silently.

Check the window, resolve services, filter trips

1. Check the date is inside the feed

Read feed_start_date and feed_end_date from feed_info.txt, or take the minimum and maximum dates across the calendar files. Raise if the date is outside; an empty result is not an answer.

2. Resolve active services

Apply the weekly pattern, then add and remove exceptions, as in the calendar guide.

3. Filter trips, then stop times

Filter trips.txt first β€” it is small β€” and use the resulting trip IDs to filter stop_times.txt. Filtering the big table by a set membership is fast; joining it first and filtering afterwards is not.

4. Decide about the previous day's late trips

If the question is "what is running on the clock date", include trips from the previous service day whose times exceed 24:00, shifted back by 24 hours. If the question is "what is the timetable for this service day", leave them out.

5. Convert to timestamps only at the end

Keep integer seconds for all computation. When a real timestamp is needed, add them to the service day's noon minus twelve hours in the agency time zone β€” that is the definition, and it is correct on daylight-saving change days, where midnight plus the seconds is not.

6. Write the date into outputs

A table of frequencies or travel times without its service date cannot be checked against the timetable later.

Monday-service trips running past 24:00 overlapping Tuesday-service trips in the small hours of Tuesday. Tuesday 26 January, clock timeMon service stillrunning190 trips after 00:00first Tuedeparturesfrom 02:55last Mondeparture03:10 (27:10:00)Tue service allday6,144 tripsTue trips aftermidnight101 start after 24:00Service days overlap by a few hours at each end; decide which ones a question means spatialworkflow.io
Two service days overlap in the small hours; which trips you keep depends on the question.

Everything moving on one service day

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

# TriMet replaces this feed every week or two: its version and start date move, the dates below do not
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 β€” guard the feed window

info = pd.read_csv(f"{FEED}/feed_info.txt", dtype=str).iloc[0]

def checked_trips_on(date):
    ds = pd.Timestamp(date).strftime("%Y%m%d")
    if not info.feed_start_date <= ds <= info.feed_end_date:
        raise ValueError(f"{ds} is outside the feed "
                         f"({info.feed_start_date}–{info.feed_end_date})")
    return trips_on(date)

try:
    checked_trips_on("2027-03-02")
except ValueError as e:
    print(e)
20270302 is outside the feed (20260927–20270227)

Example 2 β€” the stop times for the service day

def to_secs(s):
    p = s.str.split(":", expand=True).astype(int)
    return p[0] * 3600 + p[1] * 60 + p[2]

st = pd.read_csv(f"{FEED}/stop_times.txt",
                 usecols=["trip_id", "departure_time", "stop_id", "stop_sequence"],
                 dtype={"trip_id": str, "stop_id": str})
tue = st[st.trip_id.isin(day.trip_id)].copy()
tue["dep"] = to_secs(tue.departure_time)
first = tue.groupby("trip_id").dep.min()
print(f"{len(tue):,} stop times; trips starting after 24:00: {int((first >= 86400).sum())}")
301,132 stop times; trips starting after 24:00: 101

Example 3 β€” everything moving on the clock date

mon = trips_on("2027-01-25")
late = st[st.trip_id.isin(mon.trip_id)].copy()
late["dep"] = to_secs(late.departure_time)
spill = late[late.dep >= 86400].copy()
spill["dep"] -= 86400                        # onto Tuesday's clock
print(spill.trip_id.nunique(), "Monday-service trips still running after midnight;",
      "last departure", f"{spill.dep.max() // 3600:02d}:{spill.dep.max() % 3600 // 60:02d}")
190 Monday-service trips still running after midnight; last departure 03:10

Example 4 β€” timestamps done properly

from zoneinfo import ZoneInfo

tz = ZoneInfo(pd.read_csv(f"{FEED}/agency.txt", dtype=str).agency_timezone.iloc[0])

def to_timestamp(service_date, secs):
    noon = pd.Timestamp(service_date + " 12:00").tz_localize(tz)
    return noon - pd.Timedelta(hours=12) + pd.to_timedelta(secs, unit="s")

print(to_timestamp("2027-01-26", 25 * 3600 + 10 * 60))
print(to_timestamp("2026-11-01", 3 * 3600))      # the day the clocks go back
naive = pd.Timestamp("2026-11-01").tz_localize(tz) + pd.Timedelta(hours=3)
print(naive, "<- local midnight plus three hours")
2027-01-27 01:10:00-08:00
2026-11-01 03:00:00-08:00
2026-11-01 02:00:00-08:00 <- local midnight plus three hours

1 November 2026 is 25 hours long in Portland, so noon minus twelve hours is 01:00 PDT rather than midnight, and a service-day time of 03:00:00 lands at 03:00 PST β€” the time printed on the timetable. Anchoring on local midnight gives 02:00 PST, an hour early for every trip that day.

Trips name services, not dates

Why trips are filtered through services

A trip does not name its dates; it names a service, and the service names the dates. This indirection is what lets one trip definition run on every weekday for three months without repeating itself. It also means a date filter is always a two-step lookup.

Why the late trips matter

Night service is where transit analyses are thinnest and where the most vulnerable riders travel. A clock-date filter that ignores the previous service day undercounts early-morning service; a service-day filter that is read as a clock date double-counts it at the other end. Both are defensible; mixing them is not.

Why noon minus twelve hours

The GTFS reference defines service-day times relative to "noon minus 12h" precisely so that daylight-saving changes do not shift the timetable. On the two change days, local midnight is 23 or 25 hours before the next midnight, and the only unambiguous anchor is noon.

Local midnight plus seconds against noon minus twelve hours plus seconds on 1 November 2026, when clocks go back. service-day time 03:00:00 on 1 Nov 2026, Portlandmidnight + 3 hanchor 00:00 PDTβœ•lands at 02:00 PSTβœ•an hour early all daynoon βˆ’ 12 h + 3 hanchor 01:00 PDT (25-hour day)βœ“lands at 03:00 PSTβœ“matches the printed timetableThe GTFS reference defines service-day times from noon minus 12h for exactly this reason spatialworkflow.io
On a daylight-saving change day the naive anchor is an hour out for the whole timetable.

Calendar-dates-only and merged feeds

  • Calendar-dates-only feeds work with the same function; the weekly set is simply empty.
  • Merged feeds can reuse service IDs; namespace them before merging.
  • frequencies.txt trips must be expanded for the date as well.
  • Time zones come from agency.txt; a stop can override with stop_timezone, rarely.
  • Holidays are the dates most often asked about and the ones most often wrong.
  • Keep integer seconds until you need timestamps; they are faster and unambiguous.

FAQ

How do I get the trips for one date in GTFS?

Resolve the active service IDs from calendar.txt and calendar_dates.txt, then filter trips.txt to those services.

Why does my date return no trips?

Usually because it is outside the feed window, or because only calendar.txt was applied to a feed that uses calendar_dates.txt.

Do trips after midnight belong to the next day?

No. A trip with times past 24:00 belongs to the service day on which it started.

How do I convert GTFS times to timestamps?

Add the seconds to noon minus twelve hours on the service date, in the agency time zone.

Should I include the previous day's late trips?

If the question is about the clock date, yes, shifted back 24 hours. If it is about the service day's timetable, no.

Why keep the service date on every row?

Because a time like 25:10:00 only becomes a real moment when combined with it.