Introducing the pct package

Obtaining and reproducing data from the Propensity to Cycle Tool (PCT)

Robin Lovelace and Layik Hama


Set eval=TRUE to run this code when knitting:

knitr::opts_chunk$set(eval = FALSE)


The goal of the pct package is to increase the accessibility and reproducibility of the outputs from the Propensity to Cycle Tool (PCT), a research project and web application hosted at The tool is one of just ~300 central government websites exempt from the requirement to transition to the domain name, and is a recommended source of evidence in the preparation of Local Walking and Cycling plans (LCWIPs), as outlined in technical guidance,1 supporting the Cycling and Walking Infrastructure Strategy (CWIS), an amendment of the Infrastructure Act 2015. For an overview of the data provided by the PCT, clicking on the previous link and trying it out is a great place to start. An academic paper on the PCT provides detail on the motivations and methods underlying the project.2

Since work on the package began in 2015, for example during the ODI Leeds Hack my Route hackathon (see an early prototype of the tool at, the features and demand for the PCT have evolved substantially. In early 2019, for example, the School travel layer was added to the main PCT site to provide evidence nationwide on the potential benefits of scenarios of cycling uptake, and where safe routes to school should be prioritised.3 In fact, a major aim of the PCT was to enable people to extend the tool:2

We envision stakeholders in local government modifying scenarios for their own purposes, and that academics in relevant fields may add new features and develop new use cases of the PCT.

Motivated by this vision of adaptable transport planning tools, this introductory vignette demonstrates how the package works with an example from the Isle of Wight, an island just off the southern coast of Britain, with a population of ~140,000 people. Before demonstrating some of the package’s key functions, it’s worth providing a little context.

Why this package?

The Propensity to Cycle Tool was commissioned by the UK’s Department for Transport to help planners and others prioritise investment and policies to get people cycling, as outlined in the Government report National propensity to cycle: full report with annexes.4 However, the academic team leading the project had a wider sub-aim: of making transport evidence more accessible, encouraging evidence-based transport policies, and encouraging a more democratic transport planning process, and that means open transport data and open source transport modelling tools.2

The code base underlying the PCT is publicly available (see However, the code hosted there is not easy to run or reproduce, which is where this package comes in: it provides quick access to the data underlying the PCT and enables some of the key results to be reproduced quickly. It was developed primarily for educational purposes (including for upcoming PCT training courses) but it may be useful for people to build on the the methods, for example to create a scenario of cycling uptake in their town/city/region.

In summary, if you want to know how PCT works, be able to reproduce some of its results, and build scenarios of cycling uptake to inform transport policies enabling cycling in cities worldwide, this package is for you!


You can install the development version of the package as follows:


Load the package as follows:


We will also use the following packages in this tutorial:


Get PCT data

From feedback, we hear that the use of the data is critical in decision making. Therefore, one area where the package could be useful is making the data “easily” available to be processed.

To download the data within, we have added a suite of functions:

There are other get_() functions that get official data underlying the PCT, as we will see in a later section. For now, let’s see how the functions work. To get the centroids in Isle of Wight at the lower-resolution (smaller files) MSOA level (LSOA level data is returned by default or by replacing msoa with lsoa in the code below) you would run:

wight_centroids = get_pct_centroids(region = "isle-of-wight", geography = "msoa")
wight_zones = get_pct_zones(region = "isle-of-wight", geography = "msoa")

Let’s verify that the data gave us what we would expect to see:

plot(wight_centroids[, "bicycle"])
plot(wight_zones[, "bicycle"])

The results are indeed as we would expect, with the centroid data showing points and the zone data showing zones. The zones with higher cycling levels are in the more densely populated south of the island, as we would expect. Likewise, the following command downloads the desire lines for the Isle of Wight:

wight_lines_pct = get_pct_lines(region = "isle-of-wight", geography = "msoa")

The rest of the get_pct_ functions are similar to the above two examples and download data from

However, the base of these functions is get_pct(), which takes the following arguments:

Comparing downloaded data with the PCT web app

To compare the downloaded data with data in the PCT web app, we will take a subset of the wight_lines_pct dataset. The top 30 travelled desire lines by number of commuters who use cycling as their main mode is taken in the following code chunk. The reason for selecting the top 30 will become apparent (the wight_lines_30 object is provided in the PCT package):

wight_lines_30 = wight_lines_pct %>% 
  top_n(30, bicycle)

The resulting wight_lines_pct and wight_lines_30 datasets are available in the package. We’ll use the smaller one for speed. Note: these contain many variables, three of which (the number of people cycling, driving and walking along the desire lines from the 2011 Census) are shown below for the Isle of Wight:

lwd = wight_lines_30$all / mean(wight_lines_30$all) * 5
plot(wight_lines_30[c("bicycle", "car_driver", "foot")], lwd = lwd)

To provide another view of the data, focus on cycling, let’s create a leaflet map:

pal = colorNumeric(palette = "RdYlBu", domain = wight_lines_30$bicycle)
leaflet(data = wight_lines_30) %>% 
  addTiles() %>% 
  addPolylines(weight = lwd,
               color = ~ pal(bicycle)) %>% 
  addLegend(pal = pal, values = ~bicycle)

There was a reason for selecting the top 30 lines: it mirrors the view of the desire lines available from the PCT web application for the island, available at (note that Straight Lines is selected from the Cycling Flows dropdown menu in the image below, and by default shows the top 30 flows by number of bicycle trips).

Getting Census data

The previous section showed that data downloaded with get_pct*() functions get the results generated by the PCT. However, they do not reproduce the results generated by the PCT, starting from first principles and publicly available, official data. Underlying the PCT is origin-destination data from the 2011 Census. The MSOA-level data is open access, so we only provide access to this dataset. The following command gets the origin-destination data for the Isle of Wight:

wight_od_all = get_od(region = "wight")
summary(wight_od_all$geo_code1 %in% wight_centroids$geo_code)
summary(wight_od_all$geo_code2 %in% wight_centroids$geo_code)

Note that all the origin codes match the Isle of Wight centroid codes, but most of the destination zones do not. This is because many people on the island work outside the island. get_od() by default returns only OD pairs in which the commute trips originate from the area entered.

To make the dataset smaller and simpler, let’s subset it so it only contains OD pairs in which the origin and destination are in the island (the resulting wight_od data is provided in the package):

wight_od = wight_od_all %>% 
  filter(geo_code2 %in% wight_centroids$geo_code)

To convert the results to geographic desire lines, we can use the function od2line() from the stplanr package:

wight_lines = od2line(wight_od, wight_centroids)

The previous code chunk downloads and processes 324 origin-destination pairs, representing inter-zonal commuting trips made by 42,139 people on the island (population: 140,000). By default, the function includes intra-zonal flows, but these can be omitted as follows (the argument omit_intrazonal in get_od() does the same thing):

wight_lines_census = wight_lines %>% 
  filter(geo_code1 != geo_code2)

Another OD data processing step developed for the PCT was converting oneway lines into 2 way lines. This can be done as follows:

wight_lines_census1 = od_oneway(
  attrib = c("all", "bicycle")
nrow(wight_lines_census1) / nrow(wight_lines_census)
sum(wight_lines_census1$all) / sum(wight_lines_census$all)

Note that the resulting lines contain 50% of the number of lines, but the same number of trips: this is because 2 separate lines between the same zones have been converted into 1 line representing the combined number of trips in both directions, for each OD pair. This step is not essential but it has a couple of advantages: it was used in the PCT to make the routing more computationally efficient (less work computing the same route twice); and it makes visualising the lines and routes simpler.

Now that the lines data contains data on 2 way trips between zones, we can estimate routes (note: the results on the PCT website contain estimated uptake levels from intrazonal flow) Visually, this involves converting the straight desire lines shown in the previous map into routes that can be cycled, as shown in the next code chunk. Note: this code does not run dynamically, because you need an API key for this, and it takes some time:

wight_routes_fast = route(
  l = wight_lines_census1,
  route_fun = cyclestreets::journey,
  plan = "fastest")

You can download these routes as follows:

u = ""
wight_routes_fast = readRDS(url(u))

A sample of these is provided in the package as wight_routes_30_cs, which was generated as follows:

wight_lines_census_30 = wight_lines_census1 %>% 
  top_n(30, bicycle)
wight_routes_30_cs = wight_routes_fast %>% 
  group_by(geo_code1, geo_code2) %>% 
    all = mean(all),
    bicycle = mean(bicycle),
    av_incline = weighted.mean(gradient_smooth, w = distances),
    length = sum(distances),
    time = sum(time)
    ) %>% 
  ungroup() %>% 
  top_n(30, bicycle) 

A simple verification that we have the right desire lines matched to the routes involves plotting the Euclidean vs Route distance, e.g. as follows:

d = as.numeric(st_length(wight_lines_census_30)) / 1000
plot(d, wight_routes_30_cs$length / 1000, xlim = c(0, 10))
abline(a = c(0, 1))

How well does that match the route distance data downloaded from the PCT?

plot(wight_lines_30$rf_dist_km, wight_routes_30_cs$length)

Almost perfectly for most of the routes. Differences can be explained by changes in infrastructure since the PCT results were first generated (these will be updated in the Propensity to Cycle Tool on-line data later in 2019).

We now have everything needed to estimate cycling uptake for each desire lines on the Isle of Wight (we’ll do the calculation on the top 30 by current cycling levels).

Estimate cycling uptake

Functions named with uptake_*() estimate cycling uptake:

We will estimate cycling potential with uptake_pct_godutch(), using the length and av_incline from the wight_routes_30_cs object.

pcycle_govtarget = uptake_pct_govtarget(
  distance = wight_routes_30_cs$length,
  gradient = wight_routes_30_cs$av_incline * 100

In terms of cycling uptake, the results are shown below:

wight_routes_30_cs$govtarget = wight_lines_census_30$bicycle +
  pcycle_govtarget * wight_lines_census_30$all
wight_routes_30_cs$govtarget_pct = wight_lines_30$govtarget_slc

ggplot(wight_routes_30_cs) +
  geom_point(aes(length, govtarget), colour = "red") +
  geom_point(aes(length, govtarget_pct), colour = "blue") 
cor(wight_routes_30_cs$govtarget, wight_routes_30_cs$govtarget_pct)

The final computational stage is also one of the most important from a policy perspective: estimating cycling potential down to the street level, to help prioritise investment where it is most needed. This work is done by the overline2() function, as follows:

wight_routes_30_ls = sf::st_cast(wight_routes_30_cs, "LINESTRING")
rnet = overline(wight_routes_30_ls, "govtarget")

Running the same function for all routes in wight_routes_fast, generates the packaged data object wight_rnet, which was created as follows:

wight_routes_fast_gt = wight_routes_fast %>% 
  group_by(geo_code1, geo_code2) %>% 
    govtarget = uptake_pct_govtarget(sum(distances), mean(gradient_smooth)) *
      (sum(all) + sum(bicycle))
wight_routes_fast_gt = sf::st_cast(wight_routes_fast_gt, "LINESTRING")
wight_rnet = overline(wight_routes_fast_gt, "govtarget")
pal = colorNumeric(palette = "RdYlBu", domain = wight_rnet$govtarget)
leaflet(data = wight_rnet) %>% 
  addTiles() %>% 
  addPolylines(color = ~ pal(govtarget)) %>% 
  addLegend(pal = pal, values = ~govtarget)


1. Department for Transport. Local Cycling and Walking Infrastructure Plans. (Department for Transport, 2017).

2. Lovelace, R. et al. The Propensity to Cycle Tool: An open source online system for sustainable transport planning. Journal of Transport and Land Use 10, (2017).

3. Goodman, A. et al. Scenarios of cycling to school in England, and associated health and carbon impacts: Application of the ‘Propensity to Cycle Tool’. Journal of Transport & Health 12, 263–278 (2019).

4. Department for Transport. National Propensity to Cycle Tool Project: Full report with annexes. (Department for Transport, 2016).