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.
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.
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.
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.txttrips must be expanded for the date as well.- Time zones come from
agency.txt; a stop can override withstop_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.
Internal links
- GTFS service calendars explained β the rules behind
services_on - A GTFS query returns no trips, or the wrong trips, on a date β when this goes wrong
- GTFS times after 24:00 break time parsing β the service-day clock
- How to calculate service frequency at every stop from GTFS β the next step
- How to read a GTFS feed in Python β types and loading
- My time column sorts or joins wrongly β time zones in pandas
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.