Before and after the Metro Network Redesign

Madison (WI) transit GTFS accessibility r5r

Comparing transit accessibility with the r5r package

Harald Kliems https://haraldkliems.netlify.app/
2023-04-24
Show code

Our transit system here in Madison (WI) will see a big change in June: On June 12, the flip will be switched and the Transit Network Redesign goes into effect. Last week, Metro released GTFS data for the new network. Not only can riders now use Transit or Google Maps to explore how they get from A to B in the new system; it also allows accessibility analyses that compare the network before and after the redesign.

Accessibility analysis is a big topic in planning, and in addition to proprietary tools, there is an open source ecosystem of software products around transit analysis. For this article I use r5r, “an R package for rapid realistic routing on multimodal transport networks (walk, bike, public transport and car). It provides a simple and friendly interface to R5, the Rapid Realistic Routing on Real-world and Reimagined networks, the routing engine developed independently by Conveyal.”

Prerequisites and data

The minimum requirements for using r5r are a file with OpenStreetMap road network data and a GTFS file for the transit data. The former I downloaded from Protomaps, and the latter is available directly from Metro or via OpenMobilityData.

Comparing trip times

One key promise of the redesign was that it would decrease travel times. r5r allows us to investigate this. I created a list of points-of-interest (POIs) in different parts of town and saved their coordinated in a Google Spreadsheet. Obviously one could choose other POIs—I picked a mix of locations that are important to me, locations that are on the various edges of town, and some that have come up in public debates during the redesign process. It would be easy to add additional POIs and re-run the analysis.

Show code
#read poi coordinates
poi <- googlesheets4::read_sheet("https://docs.google.com/spreadsheets/d/1TLN77Pks1W5dl-kx55XCi55-bK--EhJ_DeHhrexGR5M/edit?usp=sharing")

poi |> gt()
id lat lon
State Capitol 43.07475 -89.38416
UW Hospital 43.07645 -89.43103
Sequoya Library 43.05378 -89.45037
Henry Vilas Zoo 43.05808 -89.41039
Mickey’s Tavern 43.08790 -89.35847
Willy St North 43.12771 -89.36300
Walmart South Madison 43.04289 -89.35068
East Towne Mall 43.12427 -89.30629
West High 43.06860 -89.42617
DMV West 43.07894 -89.52885
Catholic Multicultural Center 43.04605 -89.39332
Show code
tmap_mode("view")
poi |> 
  sf::st_as_sf(coords = c("lon", "lat")) |> 
  tm_shape() +
  tm_dots() +
  tm_text("id", auto.placement = TRUE,
          ymod = 1.5)

Another choice is about the time of the comparison: Transit frequencies vary a lot by time of day, with more trips during rush hour, fewer trips during the rest of the day, and no service at all between approximately midnight and 6 am. And less service on weekends. For this analysis I randomly picked a Wednesday afternoon, at 2 pm, right after when the redesign goes into effect and one week before. Not peak hour, but also not low-frequency late night or weekend service. One great feature of r5r is that it is possible to analyze departures within time windows. This addresses a limitation of analyzing only a single time point: If we were to choose only 2:00 pm for our analysis, the particular schedules would have a huge impact on the result. For example, if the stop closest to the origin has a bus that comes every half hour, it matters a lot for the total trip time if the bus comes at 1:59 or at 2:01 pm. With a 1:59 departure, we’d just have missed the bus and would only get to start our trip at 2:29 pm. Whereas with a 2:01 departure, there is basically no waiting time, cutting 28 minutes from the trip. r5r allows analysis of departure times for every minute within a given time window. So it will return trip times for 2:00, 2:01, 2:02, …, 2:30 departures and then we can average those times to reduce the bias of choosing only a single time. These averages, then, represent a scenario of spontaneous, unplanned trips: I know where I want to go, I don’t know anything about bus schedules, and I want to leave right now. Again, other choices for analysis are possible.

Trip time results

Here are the results from the analysis in a table. You can sort and filter the table and explore the results. Overall trip times have indeed decreased for many origin/destination pairs. Walk times are more mixed: There are many origin/destination pairs where walk time has increased. This was one of the criticisms about the redesign: For people with limited mobility, these increased walk times could be a problem. But then there are also routes where walk time has decreased. The bottom line: It’s difficult to draw conclusions from the analysis of a set of 11 locations. r5r provides additional analysis capabilities, and in a future post I may return to this topic.

Show code
Sys.setenv(JAVA_HOME="c:/Program Files/OpenJDK/jdk-11/")

options(java.parameters = "-Xmx2G")
rJava::.jinit()
#rJava::.jcall("java.lang.System", "S", "getProperty", "java.version")


# Indicate the path where OSM and GTFS data are stored
r5r_core <- setup_r5(data_path = "data/")

mode <- c("WALK", "TRANSIT")
max_walk_time <- 30 # minutes
max_trip_duration <- 150 # minutes
departure_datetime <- as.POSIXct("12-06-2023 14:00:00",
                                 format = "%d-%m-%Y %H:%M:%S")

# # extract OSM network
# street_net <- street_network_to_sf(r5r_core)
# 
# # extract public transport network
# transit_net <- r5r::transit_network_to_sf(r5r_core)


#compare trips before and after network redesign
departure_datetime_before <- as.POSIXct("05-06-2023 14:00:00",
                                 format = "%d-%m-%Y %H:%M:%S")
departure_datetime_after <- as.POSIXct("12-06-2023 14:00:00",
                                        format = "%d-%m-%Y %H:%M:%S")

# calculate a travel time matrix
ttm_before <-  expanded_travel_time_matrix(r5r_core = r5r_core,
                                    origins = poi,
                                    destinations = poi,
                                    mode = mode,
                                    departure_datetime = departure_datetime_before,
                                    max_walk_time = max_walk_time,
                                    max_trip_duration = max_trip_duration,
                                    time_window = 30,
                                    breakdown = TRUE)

ttm_before <- ttm_before |> 
  filter(from_id != to_id) |> 
  mutate(origin_destination = paste0(from_id, " to ", to_id)) |> 
  summarize(mean_traveltime_before = mean(total_time),
            mean_walk_time_before = mean(access_time + egress_time),
            mean_transfer_time_before = mean(transfer_time),
            .by = origin_destination)

ttm_after <-  expanded_travel_time_matrix(r5r_core = r5r_core,
                                           origins = poi,
                                           destinations = poi,
                                           mode = mode,
                                           departure_datetime = departure_datetime_after,
                                           max_walk_time = max_walk_time,
                                           max_trip_duration = max_trip_duration,
                                           time_window = 30,
                                          breakdown = TRUE)

ttm_after <- ttm_after |> 
  filter(from_id != to_id) |> 
  mutate(origin_destination = paste0(from_id, " to ", to_id)) |> 
  group_by(origin_destination) |> 
  summarize(from_id,
            to_id,
            mean_traveltime_after = mean(total_time),
            mean_walk_time_after = mean(access_time + egress_time),
            mean_transfer_time_after = mean(transfer_time)) |> 
  distinct(origin_destination, .keep_all = TRUE)

before_after <- ttm_before |> 
  left_join(ttm_after, by = c("origin_destination")) |> 
  mutate(change_total = (mean_traveltime_after-mean_traveltime_before)/abs(mean_traveltime_before),
         change_walk = (mean_walk_time_after-mean_walk_time_before)/abs(mean_walk_time_before),
         change_transfer = (mean_transfer_time_after - mean_transfer_time_before) / abs(mean_transfer_time_before)) |> 
  select(-change_transfer)
Show code
before_after |> 
  select(from_id,
         to_id,
         mean_traveltime_before,
         mean_traveltime_after,
         change_total,
         mean_walk_time_before,
         mean_walk_time_after,
         change_walk) |> 
  reactable(
    searchable = FALSE,
    defaultSorted = "from_id",
    columns = list(
      from_id = colDef(name = "From",
                       filterable = TRUE),
      to_id = colDef(name = "To", 
                     filterable = TRUE),
      change_total = colDef(format = colFormat(percent = TRUE, digits = 1),
                            name = "change total time"),
      change_walk = colDef(format = colFormat(percent = TRUE, digits = 1),
                           name = "change walk time"),
      mean_traveltime_after = colDef(format = colFormat(digits = 0),
                                     name = "total time after"),
      mean_traveltime_before = colDef(format = colFormat(digits = 0),
                                      name = "total time before"),
      mean_walk_time_after = colDef(format = colFormat(digits = 0),
                                    name = "walk time after"),
      mean_walk_time_before = colDef(format = colFormat(digits = 0),
                                     name = "walk time before")
  )
  )

Reuse

Text and figures are licensed under Creative Commons Attribution CC BY-SA 4.0. The figures that have been reused from other sources don't fall under this license and can be recognized by a note in their caption: "Figure from ...".

Citation

For attribution, please cite this work as

Kliems (2023, April 24). Harald Kliems: Before and after the Metro Network Redesign. Retrieved from https://haraldkliems.netlify.app/posts/before-and-after-the-metro-network-redesign/

BibTeX citation

@misc{kliems2023before,
  author = {Kliems, Harald},
  title = {Harald Kliems: Before and after the Metro Network Redesign},
  url = {https://haraldkliems.netlify.app/posts/before-and-after-the-metro-network-redesign/},
  year = {2023}
}