GTFS stop_times.txt: arrival_time goes past 24:00:00 and strptime refuses it
Checked 2026-09-08 · Health, environment and transport datasets
GTFS times are counted from noon minus twelve hours of the service day, not clock times, so a trip that runs after midnight keeps counting: 24:01:00, 25:14:00, 26:57:00. In the current MBTA feed 217,407 of 4,494,139 stop_times rows — 4.84 percent — have an hour of 24 or more, and datetime.strptime with %H:%M:%S raises ValueError on every one of them. Parse the field as three integers and add the hours to the service date; do not treat it as a time of day.
What we saw
217,407 of 4,494,139 rows have an arrival hour of 24 or more; the largest is 26:57:00. Source (stop_times.txt, arrival_time, 2026-09-08).
rows: 4494139 arrival_time hour>=24: 217407 (4.84%) max: 26:57:00
hours >=24 histogram: [24, 25, 26] {24: 148584, 25: 61926, 26: 6897}The affected rows are ordinary consecutive stops on ordinary trips, not a few bad records. Source (stop_times.txt, first three rows with hour >= 24, 2026-09-08).
{'trip_id': '76132147', 'arrival_time': '24:01:00', 'departure_time': '24:01:00', 'stop_id': '70264', 'stop_sequence': '7'}
{'trip_id': '76132147', 'arrival_time': '24:02:00', 'departure_time': '24:02:00', 'stop_id': '70261', 'stop_sequence': '8'}
{'trip_id': '76132148', 'arrival_time': '24:03:00', 'departure_time': '24:03:00', 'stop_id': '70261', 'stop_sequence': '1'}9,796 of 168,145 trips have at least one stop past 24:00, so dropping the bad rows silently truncates one trip in seventeen. Source (stop_times.txt, distinct trip_id, 2026-09-08).
trips with any >=24:00 stop: 9796 of 168145
The standard parser refuses the value outright. Source (datetime.strptime with %H:%M:%S, 2026-09-08).
strptime ValueError: time data '26:57:00' does not match format '%H:%M:%S'
The feed under test is the current published one and identifies itself in feed_info.txt. Source (feed_info.txt, 2026-09-08).
feed_publisher_name,feed_publisher_url,feed_lang,feed_start_date,feed_end_date,feed_version,feed_contact_email,feed_id MBTA,http://www.mbta.com,EN,20260830,20261212,"Fall 2026, 2026-09-06T00:56:24+00:00, version D",developer@mbta.com,mbta-ma-us
The command
curl -sSL -o mbta.zip -w 'http=%{http_code} bytes=%{size_download}\n' 'https://cdn.mbta.com/MBTA_GTFS.zip'gtfs http=200 bytes=34398324 t=0.561817
Checked 2026-09-08.
python3 -c "
import zipfile,csv,io,collections
z=zipfile.ZipFile('mbta.zip'); n=over=0; mx='00:00:00'; hours=collections.Counter()
for row in csv.DictReader(io.TextIOWrapper(z.open('stop_times.txt'),'utf-8-sig')):
n+=1; a=row['arrival_time']
if not a: continue
h=int(a.split(':')[0]); hours[h]+=1
if h>=24: over+=1; mx=max(mx,a)
print(n, over, round(100*over/n,2), mx, {k:hours[k] for k in sorted(hours) if k>=24})"rows: 4494139 arrival_time hour>=24: 217407 (4.84%) max: 26:57:00
hours >=24 histogram: [24, 25, 26] {24: 148584, 25: 61926, 26: 6897}Checked 2026-09-08.
python3 -c "from datetime import datetime; datetime.strptime('26:57:00','%H:%M:%S')"strptime ValueError: time data '26:57:00' does not match format '%H:%M:%S'
Checked 2026-09-08.
| arrival hour | rows | share of 4,494,139 |
|---|---|---|
| 24 | 148584 | 3.31% |
| 25 | 61926 | 1.38% |
| 26 | 6897 | 0.15% |
| 24 or more, total | 217407 | 4.84% |
Limits
- One feed. 4.84 percent is MBTA's number and says nothing about any other agency; a commuter operator with no night service would show zero.
- Only arrival_time was counted. departure_time has the same rule and I did not tabulate it separately, though the sample rows show the two agreeing.
- pandas is not installed on this box, so I did not check how pandas.to_datetime handles these values — only the standard library parser.
Open question
The hours here stop at 26. Is there a value above which feeds in practice never go, or is 24:00 simply the point past which nothing is guaranteed?