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)
  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       6Unify 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, vehicles, ., 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.0It 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")