Skip to contents

Website: https://e-kotov.github.io/gtfsrt2static/

Experimental — APIs will change without deprecation.

Turn archived GTFS-Realtime feeds (and GPS-derived observations) into standard-compliant static GTFS snapshots of the service that actually ran, ready for comparison against the planned schedule.

How it fits together

live endpoints ──rt2s_collect()──► daily .pb ZIPs
                                     │
      Trip Updates ──(gtfsrealtime)──┴──► rt2s_events_from_trip_updates() ──┐
                                                                         ├─► observed stop events ─► rt2s_assemble() ─► realized GTFS zip
      Vehicle Positions / raw GPS ──(gps2gtfs)─► rt2s_events_from_stop_times() ─┘        (baseline feed optional)
  • gtfsrealtime parses GTFS-RT protobuf into data frames (not this package’s job).
  • gps2gtfs infers trips and stop times from raw coordinates (not this package’s job either).
  • This package does everything that is neither protobuf nor spatial: collecting feeds, converging both input paths on a canonical observed stop events table, and assembling compliant feeds.

Modules

Module Functions Job
collect rt2s_collect(), rt2s_archive_rotate(), rt2s_archive_coverage(), rt2s_service_template() archive ephemeral GTFS-RT endpoints as daily .pb ZIPs (never parses; raw bytes only)
events rt2s_events_from_trip_updates(), rt2s_events_from_stop_times(), rt2s_events_validate() reduce observations to one row per trip × stop × service date, with provenance
summarise rt2s_obs_headways(), rt2s_obs_travel_times(), rt2s_obs_stop_order(), rt2s_time_window() reduce many observed runs to headway / travel-time / dwell quantiles and a cross-trip canonical stop order
baseline rt2s_baseline_patterns(), rt2s_baseline_headways(), rt2s_baseline_service_dates() reduce a planned static feed to one canonical stop pattern per route-direction, to its own scheduled headways, and to the dates a named service runs
assemble rt2s_assemble(), rt2s_scaffold(), rt2s_frequencies(), rt2s_resolved_grid() merge events into a baseline feed (official IDs preserved), scaffold a compliant feed from scratch, or collapse many runs into a frequency-based feed — and read back which headway groups were emitted, with what, and why the rest were dropped

Quick example

library(gtfsrt2static)

# Trip Updates archive -> realized feed for one day
updates <- gtfsrealtime::read_gtfsrt_trip_updates("tu/20260714.zip", "America/New_York")
events  <- rt2s_events_from_trip_updates(updates)
feed    <- rt2s_assemble(events, baseline = "planned_gtfs.zip")
gtfsio::export_gtfs(feed, "realized_20260714.zip")

# Vehicle Positions (no Trip Updates, no baseline)
positions <- gtfsrealtime::read_gtfsrt_positions("vp/20260714.zip", "America/New_York")
result <- gps2gtfs::g2g_extract_trips_and_stop_times(positions, terminals, stops, ...)
events <- rt2s_events_from_stop_times(result$stop_times)
feed   <- rt2s_assemble(
  events,
  agency = list(name = "My Agency", url = "https://...", timezone = "America/New_York"),
  stops  = gps2gtfs::g2g_stops_from_positions(positions)
)

# Collapse many runs into a frequency-based feed at three reliability quantiles
feeds <- rt2s_frequencies(
  events,
  windows = list(am_peak = c("06:00", "09:00"), midday = c("09:00", "16:00")),
  stops   = stops_with_coords,
  agency  = list(name = "My Agency", url = "https://...", timezone = "America/New_York")
)
feeds$structural; feeds$median; feeds$reliable   # p05 / p50 / p95 feeds

# If trip_ref is not per-run, estimate headways from reference-stop passages
feeds <- rt2s_frequencies(
  events,
  windows = list(am_peak = c("06:00", "09:00"), midday = c("09:00", "16:00")),
  stops   = stops_with_coords,
  agency  = list(name = "My Agency", url = "https://...", timezone = "America/New_York"),
  headway_method = "passage"
)

Supplying reference_stops restricts passage-headway output to route-directions that serve one of those stops. The same choice is available one level down as rt2s_obs_headways(method = "passage"), which is where reference_stops and min_revisit_gap_s are documented.

Anchoring the frequency feeds on a planned feed (pattern_source = "baseline", scaling=, headways=, headway_groups=, service_dates=) and mixing in individually-timed trips (extra_trips=) are covered in vignette("frequency-feeds").

Learn more

Installation

# install.packages("pak")
pak::pak("e-kotov/gtfsrt2static")

Design notes

  • Both input paths converge on the observed stop events schema (?"observed-stop-events"): trip × stop × service date × actual times × provenance (observed / predicted-last / propagated / skipped / canceled).
  • Times are absolute POSIXct internally; stop_times.txt clock strings (including >24:00:00 for post-midnight stops) are derived only at assembly.
  • The collector never parses protobuf: archive fidelity must not depend on parser versions, and malformed responses are evidence worth keeping.
  • Output follows the gtfsio convention, so gtfstools and tidytransit work on it directly.

License

MIT © Egor Kotov