Performance
performance.RmdThis article documents the performance of the
gtfsrealtime package, using one day of bus position data
(~2.8 million observations) in New York City. To provide a baseline, it
also reads the same data with RProtoBuf, the only other
actively-maintained R package we are aware of that can read
GTFS-realtime.
Getting data
First, we download the data, as it is too large to ship with the package.
suppressMessages({
if (!file.exists("nyc-bus-demo.zip")) {
download.file(
"https://files.indicatrix.org/gtfsrealtime-r/nyc-bus-demo.zip",
"nyc-bus-demo.zip"
)
}
})Reading with gtfsrealtime
gtfsrealtime can read directly from the ZIP archive. We
use as_sf here, since that is used in the paper, but when
we go head to head with RProtoBuf below we don’t to make a fair
comparison.
system.time({data_gtfsrealtime = read_gtfsrt_positions("nyc-bus-demo.zip", "America/New_York", as_sf=T)})
#> user system elapsed
#> 11.063 3.002 15.444Confirm we read all observations:
nrow(data_gtfsrealtime)
#> [1] 2825902Comparison with RProtoBuf
Proto loading
First, we need to read the .proto file.
readProtoFiles2(here::here("src/rust/src/gtfs-realtime.proto"))Data preparation
The ZIP file contains bzipped protobuf files.
gtfsrealtime supports this natively, but RProtoBuf does
not. So we unzip and uncompress the files first.
pbdir = tempfile()
dir.create(pbdir)
unzip("nyc-bus-demo.zip", exdir=pbdir)
for (file in list.files(file.path(pbdir, "nyc-bus-demo"), full.names=T)) {
system2("bunzip2", c(file))
}The parameters of the test
- Read all of the files in the directory into a list of data frames, one for each file
input_files = list.files(file.path(pbdir, "nyc-bus-demo"), full.names=T)
stopifnot(length(input_files) == 1440) # minutely for one dayRe-reading with gtfsrealtime
We will read the uncompressed files with gtfsrealtime to
get a fair comparison, without decompression time included, but
including the additional file operations RProtoBuf will do. If anything
this tilts the field towards RProtoBuf, as the gtfsrealtime
version is loaded first so files may be e.g. cached by the operating
system or hardware.
system.time({data_gtfsrealtime_uncompressed = lapply(input_files, \(f) read_gtfsrt_positions(f, "America/New_York"))})
#> user system elapsed
#> 6.551 4.601 11.811Reading with RProtoBuf
Next, we will read them with RProtoBuf:
system.time({
data_rpbf = lapply(input_files, \(f) RProtoBuf::read(transit_realtime.FeedMessage, f))
})
#> user system elapsed
#> 2.398 0.425 3.681This is actually significantly faster, but is not a fair comparison.
The output of RProtoBuf::read is a hierarchical object,
which is notoriously slow and difficult to work with in R, and which
will need to be converted to tabular to work with with common,
performant libraries in R. The output of gtfsrealtime is
already in tabular format.
Reading and converting to tabular format with RProtoBuf
For a truly fair test, we need to also convert the RProtoBuf output
to data frames like the GTFS-rt output. We do this two ways, once with a
“row-wise” approach where each vehicle position is converted to a data
frame and then row-bound, and oncw with a “column-wise” where a single
data frame is constructed from columns that are constructed from the
hierarchical data with vapply. This is still not a
completely fair comparison, as it doesn’t have some of the features
gtfsrealtime has, such as ID deduplication or human
readable enum columns, but should be close in terms of
functionality.
Column-wise approach
flatten_position_message = function(message) {
data.frame(
id = vapply(message$entity, \(x) x$id, ""),
latitude = vapply(message$entity, \(e) e$vehicle$position$latitude, 42.0),
longitude = vapply(message$entity, \(e) e$vehicle$position$longitude, 42.0),
bearing = vapply(message$entity, \(e) e$vehicle$position$bearing, 42.0),
odometer = vapply(message$entity, \(e) e$vehicle$position$odometer, 42.0),
speed = vapply(message$entity, \(e) e$vehicle$position$speed, 42.0),
trip_id = vapply(message$entity, \(e) e$vehicle$trip$trip_id, ""),
route_id = vapply(message$entity, \(e) e$vehicle$trip$route_id, ""),
direction_id = vapply(message$entity, \(e) e$vehicle$trip$direction_id, 0),
start_time = vapply(message$entity, \(e) e$vehicle$trip$start_time, ""),
start_date = vapply(message$entity, \(e) e$vehicle$trip$start_date, ""),
schedule_relationship = vapply(message$entity, \(e) e$vehicle$trip$schedule_relationship, 0L),
stop_id = vapply(message$entity, \(e) e$vehicle$stop_id, ""),
current_stop_sequence = vapply(message$entity, \(e) e$vehicle$current_stop_sequence, 42.0),
current_status = vapply(message$entity, \(e) e$vehicle$current_status, 0L),
timestamp = as.POSIXct(vapply(message$entity, \(e) e$vehicle$timestamp, 42.0), "America/New_York"),
congestion_level = vapply(message$entity, \(e) e$vehicle$congestion_level, 0L),
occupancy_status = vapply(message$entity, \(e) e$vehicle$occupancy_status, 0L),
occupancy_percentage = vapply(message$entity, \(e) e$vehicle$occupancy_percentage, 42.0),
vehicle_id = vapply(message$entity, \(e) e$vehicle$vehicle$id, ""),
vehicle_label = vapply(message$entity, \(e) e$vehicle$vehicle$label, ""),
vehicle_license_plate = vapply(message$entity, \(e) e$vehicle$vehicle$license_plate, ""),
wheelchair_accessible = vapply(message$entity, \(e) e$vehicle$vehicle$wheelchair_accessible, 0L)
)
}
system.time({
data_rpbf_columnwise = lapply(input_files, \(f) flatten_position_message(RProtoBuf::read(transit_realtime.FeedMessage, f)))
})
#> user system elapsed
#> 1403.794 29.809 1457.412Row-wise approach
We define the function to process a single row of the data, and then apply it as part of the reading process
flatten_position_update = function(entity) {
pos = entity$vehicle
data.frame(
id = entity$id,
latitude = pos$position$latitude,
longitude = pos$position$longitude,
bearing = pos$position$bearing,
odometer = pos$position$odometer,
speed = pos$position$speed,
trip_id = pos$trip$trip_id,
route_id = pos$trip$route_id,
direction_id = pos$trip$direction_id,
start_time = pos$trip$start_time,
start_date = pos$trip$start_date,
schedule_relationship = pos$trip$schedule_relationship,
stop_id = pos$stop_id,
current_stop_sequence = pos$current_stop_sequence,
current_status = pos$current_status,
timestamp = as.POSIXct(pos$timestamp, "America/New_York"),
congestion_level = pos$congestion_level,
occupancy_status = pos$occupancy_status,
occupancy_percentage = pos$occupancy_percentage,
vehicle_id = pos$vehicle$id,
vehicle_label = pos$vehicle$label,
vehicle_license_plate = pos$vehicle$license_plate,
wheelchair_accessible = pos$vehicle$wheelchair_accessible
)
}
system.time({
data_rpbf_rowwise = lapply(input_files, function(f) {
msg = RProtoBuf::read(transit_realtime.FeedMessage, f)
lapply(msg$entity, flatten_position_update) |> rbind()
})
})
#> user system elapsed
#> 2278.537 2246.347 5154.453