Edge & Filtering Logic¶
This page covers two mixins on Feed: FeedFilteringMixin
(date/time/route-type/AOI filtering) and FeedEdgeAnalysisMixin
(stop-to-stop "edge" headway and speed).
Date filtering: day_offset and nominal vs. real dates¶
GTFS allows arrival_time/departure_time values past 24:00:00 for trips
that run past midnight. pyGTFSHandler keeps such trips attached to their
original nominal service date and stores how many days a stop_time spills
into the next day as day_offset (0 for same-day, 1 for a trip that departs
after midnight relative to its trip_id's nominal service date, etc.).
The real calendar date of a stop_time is therefore:
_max_day_offset scans the data for the largest day_offset present so that
_filter_by_date/_filter_by_date_range know how many "nominal date = real
date − day_offset" candidate service dates to check — a stop_time can only
match a requested real date if its nominal service date is active and its
day_offset bridges the gap. This lets filter_by_date and
filter_by_date_range return exactly the stop_times that actually occur (in
wall-clock terms) on the requested date(s), including the tail end of
overnight trips, without duplicating rows for same-day trips.
Time-of-day filtering and frequencies.txt¶
_filter_by_time_range filters to a [start_time, end_time] window of
seconds since midnight (via time_parsing.time_to_seconds). Trips defined by
frequencies.txt are handled differently from explicit stop_times.txt
trips: a frequency-based "trip" only has a start/end window and a headway,
not concrete departure times, so it is kept if its window overlaps
[start_time, end_time] at all (the actual departures are only materialized
later, during expansion — see Frequency expansion) — rather
than requiring a specific departure timestamp to fall inside the window.
Edge construction¶
Both get_headway_at_edges and get_speed_at_edges build "edges" (directed
stop-to-stop segments) the same way:
- Filter the feed to the requested
date/start_time/end_time/route_types. - Sort by
(trip_id, stop_sequence)and pair each stop with the previous stop on the same trip (stop_id_B = stop_id.shift(1).over("trip_id")), producing one row per consecutive stop pair per trip. - Derive a direction-independent
edge_idby ordering the two endpoint ids lexicographically ({at}_Avs{at}_B, whereatis typically"parent_station") — i.e. the same physical segment travelled in either direction gets the sameedge_id, withdirection_id(0 or 1) recording which way each row went relative to that canonical order.
Headway per edge¶
get_headway_at_edges groups rows sharing an edge (and grouping key by,
e.g. route_id or shape_direction) and applies the same RMS-style headway
statistic used for per-stop headway (see
Direction & headway methodology):
where \(\Delta t_i\) are consecutive differences between sorted departure
times on that edge/group, and initial_headway accounts for the gap before
the first and after the last departure within the requested time window
(so an edge with a single very early or very late trip is penalized rather
than scored as a perfect headway). The result is converted from seconds to
minutes before being returned.
When aggregating multiple routes/directions sharing a physical edge
(how="add"), pyGTFSHandler combines headways as combined frequencies
(reciprocal rates), matching how independent, roughly-Poisson arrival
processes combine:
how="best"/"max"/"min" instead pick the extreme value across the group
(e.g. the shortest headway among candidate routes at that edge), keeping the
associated route_ids/shape_direction/endpoint columns from that row via
sort_by("headway", ...).first()/.last().
Edges with fewer than min_trips trips are dropped from the result, since a
headway computed from 0–1 observed departures is not meaningful.
Speed per edge¶
get_speed_at_edges derives, per consecutive stop pair on a trip:
distance_weight: the difference inshape_dist_traveledbetween the two stops (meters along the shape geometry, not straight-line).time_weight: the difference indeparture_timebetween the two stops (seconds).
and computes speed in km/h:
Aggregation across trips sharing an edge (how="mean") is a weighted mean —
distance and time are separately summed weighted by n_trips per row, and
speed is recomputed from the aggregated totals, rather than averaging
per-trip speeds directly:
This avoids over-weighting infrequent trips and matches how a rider
experiences "typical" travel time better than an unweighted average of
speeds would. how="max"/"min" instead pick the fastest/slowest single
observed trip on that edge (with +inf/-inf sentinels used during sorting
so null speeds — e.g. zero time_weight — sort last, then converted back to
null afterward).