Skip to contents

Introduction

Public transit analysis takes advantage of the standardized GTFS format. However, its provision by operator makes it difficult for network aggregated analysis, considering connectivity and multimodality. GTFShift::unify() proposes a simple solution to this problem, generating an aggregated GTFS file given several instances of these.

This article uses GTFS feeds from the library GTFS database for Portugal as an example. Refer to the vignette(“download”) for more details.

# Get GTFS from library GTFS database for Portugal
data = read.csv(system.file("extdata", "gtfs_sources_pt.csv", package = "GTFShift"))

gtfs_list = lapply(c("barreiro", "fertagus"), function(ID) {
  feed = GTFShift::load_feed(data$URL[data$ID == ID], create_transfers=FALSE)
  # If some GTFS does not have calendar.txt, but another yes, so we must create it to enable unification
  if (! ("calendar" %in% names(feed))) feed$calendar = GTFShift::create_calendar(feed)
  summary(feed)
  return(feed)
})
#> tidygtfs object
#> files        agency, routes, stop_times, trips, shapes, calendar, calendar_dates, feed_info, stops
#> agency       Transportes Colectivos do Barreiro
#> service      from 2025-05-01 to 2025-12-31
#> uses         stop_times (no frequencies)
#> # routes       20
#> # trips      2284
#> # stop_ids    278
#> # stop_names  161
#> # shapes       75
#> tidygtfs object
#> files        agency, routes, stop_times, trips, fare_attributes, fare_rules, shapes, vehicles, calendar, calendar_dates, feed_info, stops
#> agency       Fertagus
#> service      from 2025-06-03 to 2025-12-03
#> uses         stop_times (no frequencies)
#> # routes       3
#> # trips      260
#> # stop_ids    14
#> # stop_names  14
#> # shapes       6

Unify GTFS

The unification is performed through GTFShift::unify(), producing a single GTFS instance, saved as a ZIP file. Option create_transfers enables the generation of transfers.txt, aggregating close stops, even if from different GTFS.

# Perform unification
gtfs_united = GTFShift::unify(gtfs_list[[1]], gtfs_list[[2]], create_transfers = TRUE)

summary(gtfs_united)
#> tidygtfs object
#> files        agency, routes, stop_times, trips, fare_attributes, fare_rules, shapes, transfers, ., calendar, calendar_dates, feed_info, stops
#> agencies     Transportes Colectivos do Barreiro, Fertagus
#> service      from 2025-05-01 to 2025-12-31
#> uses         stop_times (no frequencies)
#> # routes       23
#> # trips      2544
#> # stop_ids    292
#> # stop_names  175
#> # shapes       81

summary(gtfs_united$transfers)
#>  from_stop_id        to_stop_id        transfer_type min_transfer_time
#>  Length:1126        Length:1126        Min.   :2     Min.   :120.0    
#>  Class :character   Class :character   1st Qu.:2     1st Qu.:120.0    
#>  Mode  :character   Mode  :character   Median :2     Median :255.0    
#>                                        Mean   :2     Mean   :246.9    
#>                                        3rd Qu.:2     3rd Qu.:351.8    
#>                                        Max.   :2     Max.   :416.0

It can be displayed using mapview.

Aggregated routes

shape_agency = gtfs_united$trips |>
  left_join(gtfs_united$routes, by = "route_id") |>
  left_join(gtfs_united$agency, by = "agency_id") |>
  select(shape_id, agency_id, agency_name) |>
  distinct()

shapes_sf = tidytransit::shapes_as_sf(gtfs_united$shapes) |>
  left_join(shape_agency, by = "shape_id") |>
  filter(!is.na(agency_id))
mapview::mapview(shapes_sf, zcol = "agency_name", legend = TRUE, layer.name="Agency")

Aggregated stops

stop_agency = gtfs_united$stop_times  |>
  left_join(gtfs_united$trips, by = "trip_id") |>
  left_join(gtfs_united$routes, by = "route_id") |>
  left_join(gtfs_united$agency, by = "agency_id") |>
  select(stop_id, agency_id, agency_name) |>
  distinct()

stops_sf = tidytransit::stops_as_sf(gtfs_united$stops) |>
  left_join(stop_agency, by = "stop_id") |>
  filter(!is.na(agency_id))
mapview::mapview(stops_sf, zcol = "agency_name", legend = TRUE, layer.name="Agency")