Visualising cycling potential with A/B Street

Nathanael Sheehan and Robin Lovelace

Introduction

The abstr package was originally developed as part of the ActDev project, which involved the development of a prototype web application that provides evidence on active travel provision and potential in and around planned and proposed development sites, as outlined in a paper describing the tool. The ActDev website demonstrates the potential for new developments to support walking and cycling by visualising data generated using reproducible R code. A key challenge was to create a simulation for each of the case study sites based on the input origin-destination datasets. To overcome this challenge, A/B Street developers were commissioned to extend the ActDev tool to enable real time simulation of scenarios of change. At the time there was no way to get the OD data we had in the R world into the A/B Street world. This was motivation for creating the abstr R package. As outlined in the package’s README, the package’s main job is to take origin-destination data from .csv files (and other tabular file types) and outputs .json files that can be imported and visualised in A/B Street.

Given you have all of the above, you are ready to start transforming data-frames into simulations! So lets set an aim for the vignette

####
####
#### AIM: Use PCT data to create scenario of change where commuting cycling levels increase and car journeys decrease which can be imported
####      into A/B Street city simulation software. This method should be fully reproducible for all other pct_regions.
####
####

While the PCT is a powerful and popular tool for strategic cycleway planning it has some key limitations that are addressed by A/B Street:

This vignette aims to demonstrate how data from the PCT, which provides evidence-based visions of how cycling could become the natural choice for urban travel, can be visualised in A/B Street to overcome the limitations outlined above. The first stage is to install the necessary packages (see the abstr vignette for a detailed introduction to software requirements).

Installing and Loading Packages

To begin with, you need to install and load the necessary packages for this vignette.

#### INSTALL PACKAGES ####
cran_pkgs = c("abstr", "pct", "osmextract", "sf", "tidyverse")
remotes::install_cran(cran_pkgs)
#### LOAD PACKAGES ####
library(abstr)
library(pct)
library(osmextract)
library(sf)
library(dplyr)

Choosing a region

Data in the PCT for England and Wales is divided into regions, which can be seen below.

pct_regions$region_name

To run the example below, replace devon with a different region from the list above (warning, this may not work for large regions).

region_name = "devon"

You can select a specific local authority of interest from those available in the region of interest. You can check which are available in your region of interest as follows:

lookup = pct::pct_regions_lookup
table(lookup$lad16nm[lookup$region_name == region_name])

For the purposes of this article we will use Exeter as the case study:

lad_name = "Exeter"

Fetching PCT Data

Next you want to fetch two types of PCT data. Firstly, zone data, which is gathered using the get_pct_zones() function and is filtered to only include a local authority of choice, in this example we use Exeter. Secondly, commute data, which is gathered using the get_pct_lines() function and is also filtered to only include trips within the local authority.

####    READ DATA ####
devon_zones = get_pct_zones(region = region_name, geography = "msoa") # get zone data
# filter for exeter
exeter_zones = devon_zones %>% filter(lad_name == lad_name) %>%
  select(geo_code) 
# get commute od data
exeter_commute_od = get_pct_lines(region = region_name, geography = "msoa") %>% 
  filter(lad_name1 == lad_name & lad_name2 == lad_name) # filter for exeter

Data Cleaning and Transformation

Now you have your data, its time to clean and transform it. In fact, you only need to transform the exeter_commute_od dataframe as the exeter_zones is already in abstr format. The first step in cleaning the data requires renaming variables so that we can clearly see the difference between the base scenario and the scenario of change. Next, you calculate the scenario of change, in this example we use the uptake_pct_godutch_2020() function which takes two arguments of distance and gradient in its model calculation. The results from this PCT function allow you to calculate the mode shift from driving to cycling. Finally, we subset the data to only include the columns which are needed to progress.

exeter_commute_od = exeter_commute_od %>%
  mutate(cycle_base = bicycle) %>%
  mutate(walk_base = foot) %>%
  mutate(transit_base = bus + train_tube) %>% # bunch of renaming -_-
  mutate(drive_base = car_driver + car_passenger + motorbike + taxi_other) %>%
  mutate(all_base = all) %>%
  mutate(
    # create new columns
    pcycle_godutch_uptake = uptake_pct_godutch_2020(distance = rf_dist_km, gradient = rf_avslope_perc),
    cycle_godutch_additional = pcycle_godutch_uptake * drive_base,
    cycle_godutch = cycle_base + cycle_godutch_additional,
    pcycle_godutch = cycle_godutch / all_base,
    drive__godutch = drive_base - cycle_godutch_additional,
    across(c(drive__godutch, cycle_godutch), round, 0),
    all_go_dutch = drive__godutch + cycle_godutch + transit_base + walk_base
  ) %>%
  select(
    # select variables for new df
    geo_code1,
    geo_code2,
    cycle_base,
    drive_base,
    walk_base,
    transit_base,
    all_base,
    all_go_dutch,
    drive__godutch,
    cycle_godutch,
    cycle_godutch_additional,
    pcycle_godutch
  )

As a quick sanity check we can make sure our model has not generated any new commutes and we still have the same base number of commuters as before.

# sanity check: ensure total remains the same
# (this is not a dynamic model where population change is factored in)
identical(exeter_commute_od$all_base, exeter_commute_od$all_go_dutch) 

Download OSM building data

Now, you need to download OSM building data to populate the AB Street simulation map. In this example we use the osmextract package to fetch a PBF (protocolbuffer binary format) file hosted on GeoFabrik. You then need to filter the contents of the PBF file to only include the defined building types and subset the data to only include osm_way_id, name, building columns. Following this, you should ensure you only include valid sf buildings and then aggregate the building data against the zone boundary.

####    DOWNLOAD OSM BUILDING DATA ####
osm_polygons = osmextract::oe_read(
  "https://download.geofabrik.de/europe/great-britain/england/devon-latest.osm.pbf",
  # download osm buildings for region using geofabrik
  layer = "multipolygons"
)

building_types = c(
  "yes",
  "house",
  "detached",
  "residential",
  "apartments",
  "commercial",
  "retail",
  "school",
  "industrial",
  "semidetached_house",
  "church",
  "hangar",
  "mobile_home",
  "warehouse",
  "office",
  "college",
  "university",
  "public",
  "garages",
  "cabin",
  "hospital",
  "dormitory",
  "hotel",
  "service",
  "parking",
  "manufactured",
  "civic",
  "farm",
  "manufacturing",
  "floating_home",
  "government",
  "bungalow",
  "transportation",
  "motel",
  "manufacture",
  "kindergarten",
  "house_boat",
  "sports_centre"
)
osm_buildings  = osm_polygons %>%
  filter(building %in% building_types) %>%
  select(osm_way_id, name, building)

osm_buildings_valid = osm_buildings[sf::st_is_valid(osm_buildings), ]

exeter_osm_buildings_all = osm_buildings_valid[exeter_zones, ]

Subsequently, you can join the OSM buildings data with the exeter_zones geography in order to create the complete building table. This table is filtered to not include any NA's and is aggregated to only include

####  JOIN OSM BUILDINGS WITH ZONE DATA ####
exeter_osm_buildings_all_joined = exeter_osm_buildings_all %>%
  sf::st_join(exeter_zones)

exeter_osm_buildings_sample = exeter_osm_buildings_all_joined %>%
  filter(!is.na(osm_way_id))

exeter_osm_buildings_tbl = exeter_osm_buildings_all %>%
  filter(osm_way_id %in% exeter_osm_buildings_sample$osm_way_id)

Using Abstr to Generate Scenarios

You now have everything in place to generate AB Street scenarios for both our base commute rate and our go_active commute rate. However, abstr takes a strict column name definition as to adhere to the AB street documentation. This means you need to rename mode columns for scenario generation. In order to make things easy, we can create a simple logic gate that renames our mode columns depending on the boolean value go_active.

set.seed(2021) # for reproducible builds
####  LOGIC GATE ####
# Logic gate for go_dutch scenario of change, where cycling levels increase to a proportion reflecting the Netherlands.
# Switch to FALSE if you want census commuting OD
go_dutch = TRUE
if (go_dutch == TRUE) {
  exeter_od = exeter_commute_od %>%
    mutate(All = all_go_dutch) %>%
    mutate(Bike = cycle_godutch) %>%
    mutate(Transit = transit_base) %>%
    mutate(Drive = drive_base) %>%
    mutate(Walk = walk_base) %>%
    select(geo_code1, geo_code2, All, Bike, Transit, Drive, Walk,geometry)
} else {
  exeter_od = exeter_commute_od %>%
    mutate(All = all_base) %>%
    mutate(Bike = cycle_base) %>%
    mutate(Drive = drive_base) %>%
    mutate(Transit = transit_base) %>%
    mutate(Walk = walk_base) %>%
    select(geo_code1, geo_code2, All, Bike, Transit, Drive, Walk, geometry)
}

Voila, you are ready to generate simulation files from your data-frames. Lets start by using the ab_scenario() function with our exeter_od, exeter_zones and exeter_osm_buildings_tbl data-frames.

####  GENERATE A/B STREET SCENARIO ####
output_sf = ab_scenario(
  od = exeter_od,
  zones = exeter_zones,
  zones_d = NULL,
  origin_buildings = exeter_osm_buildings_tbl,
  destination_buildings = exeter_osm_buildings_tbl,
  pop_var = 3,
  time_fun = ab_time_normal,
  output = "sf",
  modes = c("Walk", "Bike", "Drive", "Transit")
)

To conclude you will need to generate a JSON format using the ab_json() function and then save the json file to your local machine using the ab_save() function. (You can download this file from the repo’s releases.)

#### SAVE JSON FILE ####
output_json = ab_json(output_sf, time_fun = ab_time_normal, scenario_name = "Go Dutch")
ab_save(output_json, f = "dutch.json")
# Upload the json file for future reference
piggyback::pb_upload("dutch.json")
piggyback::pb_download_url("dutch.json")

Importing scenario files into A/B Street

Now that you’ve generated a scenario, you can simulate it. First install the latest build of A/B Street for your platform. Run the software, and choose “Sandbox” on the title screen.

You can then change the default map to other cities across the world. A/B Street contains over 40 sites in England from the ActDev project. If the local authority you have generated a scenario for is not included, you can also import a new city from the user interface.

After loading the correct map, change the traffic scenario from the default “weekday” or “none” pattern. Choose “import JSON scenario,” then select your dutch file.

After selecting your scenario, you should see something like this, a Go Dutch scenario of cycling taking place before your eyes, making the OD data frames come to life in a real time simulation!

Then let your eyes wonder on the simulation you have created and let your imagination explore the possibilities of transforming your local area into an active travel utopia.

Conclusions and next steps

This vignette is by no means simple, and if you get stuck please raise an issue in the Github. If you succeed in generating a simulation for chosen city please share it on social media tagging the authors abstr so we can see how you have used the methods/data. If you are looking to extend this work presented in this vignette, why not try: