What are the cycling patterns of the biggest cycling cities in the UK? By visualising a large quantity of travel data we can observe the spatial patterns of different cities.
Provided that travel behaviour is associated with the distance to potentially attractive destinations and that cycling in particular can be sensible not only to distance but also to the quality of cycling infrastructure, the visualisation of cycling flows can reveal interesting patterns of urban morphology. This is particularly relevant for identifying city and neighbourhood level urban adaptations and strategies that can promote more energy and space-efficient modes of travel and contribute to achieving net-zero cities.
This exercise uses origin-destination data that captures the number of trips by mode between census areas in the UK.
The following packages are necessary to run the code.
::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE)
knitrlibrary(pct)
library(stplanr)
library(tidyverse)
library(sf)
library(ggplot2)
library(extrafont)
library(patchwork)
# Get origin destination data from the 2011 Census
# ref: https://www.rdocumentation.org/packages/pct/versions/0.9.1/topics/get_od
<- get_od(
flows region = NULL,
n = NULL,
type = "within",
omit_intrazonal = FALSE,
base_url = paste0("https://s3-eu-west-1.amazonaws.com/",
"statistics.digitalresources.jisc.ac.uk", "/dkan/files/FLOW/"),
filename = "wu03ew_v2",
u = NULL
)
# Download MSOA centroids for England and Wales
<- get_centroids_ew()
nodes
# Download regions (pct defined regions)
<- pct_regions regions
%>%
flows filter(bicycle > 0) %>%
# remove O-D geo areas that are NA eg. not MSOA
filter(!is.na(geo_name2)) -> flows_bike
# create sf with unique nodes
<- flows_bike$geo_code1 %>%
x1 unique() %>%
as.data.frame()
<- flows_bike$geo_code2 %>%
x2 unique() %>%
as.data.frame()
bind_rows(x1, x2) %>%
unique() %>%
rename(nodes = ".") -> flows_nodes
%>%
nodes filter(msoa11cd %in% flows_nodes$nodes) -> nodes_bike
# Convert origin-destination data to sf lines (od2line {stplanr})
<- od2line(flows_bike, nodes_bike) bike_commute
%>%
regions st_transform(27700) -> regions
# join pct regions
%>%
bike_commute st_join(., regions, join = st_within) -> bike_commute_r
# create bicycle count breaks (categories c1)
%>%
bike_commute_r mutate(c1 = as.factor(bicycle)) -> bike_commute_r
# bike commute max and sum by region
%>%
bike_commute_r st_drop_geometry() %>%
group_by(region_name) %>%
summarise(max_br = max(bicycle),
sum_br =sum(bicycle),
# distinct values for colour palette
dv_br = n_distinct(bicycle)) %>%
arrange(desc(sum_br,max_br)) -> suta
suta#> # A tibble: 46 × 4
#> region_name max_br sum_br dv_br
#> <chr> <dbl> <dbl> <int>
#> 1 <NA> 546 148594 174
#> 2 london 109 136858 88
#> 3 cambridgeshire 613 26163 133
#> 4 hampshire 118 22513 77
#> 5 avon 311 22143 86
#> 6 greater-manchester 55 21099 39
#> 7 oxfordshire 444 17271 100
#> 8 humberside 254 15684 76
#> 9 north-east 105 13201 43
#> 10 wales 139 13018 55
#> # … with 36 more rows
# selected regions considering aggregated bicycle flows
<- c("london", "cambridgeshire", "avon", "greater-manchester", "oxfordshire", "west-midlands") regl
# get bounding boxes
<- list()
bx for (i in regl) {
<- bike_commute_r %>%
bx[[i]] filter(region_name == i) %>%
st_bbox() %>%
st_as_sfc() %>%
st_sf()
}
<- do.call(rbind, bx) %>%
bx1 mutate(rn = regl) %>%
st_sf()
# plot regions to check largest study area
%>%
regions ggplot() +
geom_sf() +
geom_sf(data = bx1, fill= NA, col= "tomato") +
theme_bw()
# compare length of largest bounding boxes
<- "cambridgeshire"
lbb
%>%
bx1 filter(rn == lbb) %>%
st_cast("MULTILINESTRING") %>%
# perimeter
st_length() -> cam_p
%>%
bx1 filter(rn == lbb) %>%
st_cast("MULTILINESTRING") %>%
# distance to nearest side
st_nearest_points(., st_centroid(.)) %>%
st_length() -> cam_n
# total length (perimeter) - radius of inscribed circle * 4, divided by 4
- (4 * cam_n)) / 4
(cam_p #> 33819.5 [m]
<- "oxfordshire"
lbb
%>%
bx1 filter(rn == lbb) %>%
st_cast("MULTILINESTRING") %>%
# perimeter
st_length() -> oxf_p
%>%
bx1 filter(rn == lbb) %>%
st_cast("MULTILINESTRING") %>%
# distance to nearest side
st_nearest_points(., st_centroid(.)) %>%
st_length() -> oxf_n
# total length (perimeter) - radius of inscribed circle * 4, divided by 4
- (4 * oxf_n)) / 4
(oxf_p #> 32356.5 [m]
# create a circle of radius = 33819.5 for each region
for (i in regl) {
<- bx1 %>%
cf filter(rn == i) %>%
st_centroid(.) %>%
st_buffer(., dist = 33819.5)
assign(paste("c", i, sep = "_"), cf)
}
# plot regions and circles to check largest study area
%>%
regions ggplot() +
geom_sf() +
geom_sf(data = bx1, fill= NA, col= "gold") +
geom_sf(data = c_avon, fill= NA, col= "tomato") +
geom_sf(data = c_london, fill= NA, col= "tomato") +
geom_sf(data = c_oxfordshire, fill= NA, col= "tomato") +
geom_sf(data = c_cambridgeshire, fill= NA, col= "tomato") +
geom_sf(data = `c_greater-manchester`, fill= NA, col= "tomato") +
geom_sf(data = `c_west-midlands`, fill= NA, col= "tomato") +
theme_bw()
<- "london"
filtered
%>%
bike_commute_r filter(region_name == filtered) %>%
ggplot() +
geom_sf(aes(col = c1, size = c1), alpha = .2, show.legend = F) +
geom_sf(data = c_london, fill=NA, col=NA) +
scale_color_manual(values = colorRampPalette(c('#233423', 'green', 'white'))(88)) +
scale_size_discrete(range = c(0.2, 0.7)) +
theme_void() +
theme(panel.background = element_rect(fill = 'black'),
plot.background = element_rect(fill = 'black'),
plot.title = element_text(color='green', size=14, family="AppleGothic", hjust = 0.5)) +
labs(title = str_to_title(filtered)) -> p1
<- "cambridgeshire"
filtered
%>%
bike_commute_r filter(region_name == filtered) %>%
ggplot() +
geom_sf(aes(col = c1, size = c1), alpha = .2, show.legend = F) +
geom_sf(data = c_cambridgeshire, fill=NA, col=NA) +
scale_color_manual(values = colorRampPalette(c('#233423', 'green', 'white'))(133)) +
scale_size_discrete(range = c(0.2, 0.7)) +
theme_void() +
theme(panel.background = element_rect(fill = 'black'),
plot.background = element_rect(fill = 'black'),
plot.title = element_text(color='green', size=14, family="AppleGothic", hjust = 0.5)) +
labs(title = str_to_title(filtered)) -> p2
<- "avon"
filtered
%>%
bike_commute_r filter(region_name == filtered) %>%
ggplot() +
geom_sf(aes(col = c1, size = c1), alpha = .2, show.legend = F) +
geom_sf(data = c_avon, fill=NA, col=NA) +
scale_color_manual(values = colorRampPalette(c('#233423', 'green', 'white'))(86)) +
scale_size_discrete(range = c(0.2, 0.7)) +
theme_void() +
theme(panel.background = element_rect(fill = 'black'),
plot.background = element_rect(fill = 'black'),
plot.title = element_text(color='green', size=14, family="AppleGothic", hjust = 0.5)) +
labs(title = str_to_title(filtered)) -> p3
<- "greater-manchester"
filtered
%>%
bike_commute_r filter(region_name == filtered) %>%
ggplot() +
geom_sf(aes(col = c1, size = c1), alpha = .2, show.legend = F) +
geom_sf(data = `c_greater-manchester`, fill=NA, col=NA) +
scale_color_manual(values = colorRampPalette(c('#233423', 'green', 'white'))(39)) +
scale_size_discrete(range = c(0.2, 0.7)) +
theme_void() +
theme(panel.background = element_rect(fill = 'black'),
plot.background = element_rect(fill = 'black'),
plot.title = element_text(color='green', size=14, family="AppleGothic", hjust = 0.5)) +
labs(title = 'Greater Manchester') -> p4
<- "oxfordshire"
filtered
%>%
bike_commute_r filter(region_name == filtered) %>%
ggplot() +
geom_sf(aes(col = c1, size = c1), alpha = .2, show.legend = F) +
geom_sf(data = c_oxfordshire, fill=NA, col=NA) +
scale_color_manual(values = colorRampPalette(c('#233423', 'green', 'white'))(100)) +
scale_size_discrete(range = c(0.2, 0.7)) +
theme_void() +
theme(panel.background = element_rect(fill = 'black'),
plot.background = element_rect(fill = 'black'),
plot.title = element_text(color='green', size=14, family="AppleGothic", hjust = 0.5)) +
labs(title = str_to_title(filtered)) -> p5
<- "west-midlands"
filtered
%>%
bike_commute_r filter(region_name == filtered) %>%
ggplot() +
geom_sf(aes(col = c1, size = c1), alpha = .2, show.legend = F) +
geom_sf(data = `c_west-midlands`, fill=NA, col=NA) +
scale_color_manual(values = colorRampPalette(c('#233423', 'green', 'white'))(28)) +
scale_size_discrete(range = c(0.2, 0.7)) +
theme_void() +
theme(panel.background = element_rect(fill = 'black'),
plot.background = element_rect(fill = 'black'),
plot.title = element_text(color='green', size=14, family="AppleGothic", hjust = 0.5)) +
labs(title = 'West Midlands') -> p6
<- "Times New Roman"
ft <- "#58685c"
ct
+ p2 + p3 ) / (p4 + p5 + p6 ) +
(p1 plot_annotation(title = 'Green Power: Cycling Flows in English Regions',
subtitle = 'Regions at the same scale organised in decreasing order',
caption = 'Data: {pct} | Graphic: @npalomin',
theme = theme(panel.background = element_rect(fill = 'black'),
plot.background = element_rect(fill = 'black'),
plot.title = element_text(color=ct, size=16, family=ft, face="bold"),
plot.subtitle = element_text(color=ct, size=10, family=ft),
plot.caption = element_text(color=ct, size=10, family=ft))) -> pgrid
pgrid
The final plot contains ~300k origin-destination lines. Often, this kind of plots tend to get cluttered making it difficult to identify meaningful patterns (the total number of links between n set of nodes is n(n-1)/2). An important contribution of this graphic is that it can provide a synoptic comparative view of flow patterns that relate to the morphology of cities. Additional quantitative analysis could be done by calculating the centrality degree distribution of the studied areas.
A great time-saver is being able to access the data using an R package which encourages reproducible research. The code in this exercise can be adapted to investigate other patterns by mode of travel or include other parameters such as travel by age groups (see data in UK Data Service).
Main references:
%>%
flows filter(car_driver > 0) %>%
# remove O-D geo areas that are NA eg. not MSOA
filter(!is.na(geo_name2)) -> flows_car
# create sf with unique nodes
<- flows_car$geo_code1 %>%
x1c unique() %>%
as.data.frame()
<- flows_car$geo_code2 %>%
x2c unique() %>%
as.data.frame()
bind_rows(x1c, x2c) %>%
unique() %>%
rename(nodes = ".") -> flows_nodes_c
%>%
nodes filter(msoa11cd %in% flows_nodes_c$nodes) -> nodes_car
# Convert origin-destination data to sf lines (od2line {stplanr})
<- od2line(flows_car, nodes_car) car_commute
%>%
regions st_transform(27700) -> regions
# join pct regions
%>%
car_commute st_join(., regions, join = st_within) -> car_commute_r
# create car_driver count breaks (categories c1)
%>%
car_commute_r mutate(c1 = as.factor(car_driver)) -> car_commute_r
# bike commute max and sum by region
%>%
car_commute_r st_drop_geometry() %>%
group_by(region_name) %>%
summarise(max_br = max(car_driver),
sum_br =sum(car_driver),
# distinct values for colour palette
dv_br = n_distinct(car_driver)) %>%
arrange(desc(sum_br,max_br)) -> suta_c
suta_c#> # A tibble: 46 × 4
#> region_name max_br sum_br dv_br
#> <chr> <dbl> <dbl> <int>
#> 1 <NA> 1926 3865415 600
#> 2 london 284 708971 156
#> 3 wales 989 608826 374
#> 4 greater-manchester 418 514229 234
#> 5 west-yorkshire 445 449770 266
#> 6 west-midlands 335 414589 195
#> 7 north-east 630 411017 306
#> 8 hampshire 472 319641 267
#> 9 kent 522 304585 271
#> 10 lancashire 524 284016 269
#> # … with 36 more rows
<- "london"
filtered
%>%
car_commute_r filter(region_name == filtered) %>%
arrange(desc(car_driver)) %>%
ggplot() +
geom_sf(aes(col = c1, size = c1), alpha = .2, show.legend = F) +
geom_sf(data = c_london, fill=NA, col=NA) +
scale_color_manual(values = colorRampPalette(c('#233423', 'tomato', 'white'))(156)) +
scale_size_discrete(range = c(0.2, 0.7)) +
theme_void() +
theme(panel.background = element_rect(fill = 'black'),
plot.background = element_rect(fill = 'black'),
plot.title = element_text(color='tomato', size=14, family="AppleGothic", hjust = 0.5)) +
labs(title = str_to_title(filtered)) -> p1c
<- "cambridgeshire"
filtered
%>%
car_commute_r filter(region_name == filtered) %>%
ggplot() +
geom_sf(aes(col = c1, size = c1), alpha = .2, show.legend = F) +
geom_sf(data = c_cambridgeshire, fill=NA, col=NA) +
scale_color_manual(values = colorRampPalette(c('#233423', 'tomato', 'white'))(259)) +
scale_size_discrete(range = c(0.2, 0.7)) +
theme_void() +
theme(panel.background = element_rect(fill = 'black'),
plot.background = element_rect(fill = 'black'),
plot.title = element_text(color='tomato', size=14, family="AppleGothic", hjust = 0.5)) +
labs(title = str_to_title(filtered)) -> p2c
<- "avon"
filtered
%>%
car_commute_r filter(region_name == filtered) %>%
ggplot() +
geom_sf(aes(col = c1, size = c1), alpha = .2, show.legend = F) +
geom_sf(data = c_avon, fill=NA, col=NA) +
scale_color_manual(values = colorRampPalette(c('#233423', 'tomato', 'white'))(230)) +
scale_size_discrete(range = c(0.2, 0.7)) +
theme_void() +
theme(panel.background = element_rect(fill = 'black'),
plot.background = element_rect(fill = 'black'),
plot.title = element_text(color='tomato', size=14, family="AppleGothic", hjust = 0.5)) +
labs(title = str_to_title(filtered)) -> p3c
<- "greater-manchester"
filtered
%>%
car_commute_r filter(region_name == filtered) %>%
ggplot() +
geom_sf(aes(col = c1, size = c1), alpha = .2, show.legend = F) +
geom_sf(data = `c_greater-manchester`, fill=NA, col=NA) +
scale_color_manual(values = colorRampPalette(c('#233423', 'tomato', 'white'))(234)) +
scale_size_discrete(range = c(0.2, 0.7)) +
theme_void() +
theme(panel.background = element_rect(fill = 'black'),
plot.background = element_rect(fill = 'black'),
plot.title = element_text(color='tomato', size=14, family="AppleGothic", hjust = 0.5)) +
labs(title = 'Greater Manchester') -> p4c
<- "oxfordshire"
filtered
%>%
car_commute_r filter(region_name == filtered) %>%
ggplot() +
geom_sf(aes(col = c1, size = c1), alpha = .2, show.legend = F) +
geom_sf(data = c_oxfordshire, fill=NA, col=NA) +
scale_color_manual(values = colorRampPalette(c('#233423', 'tomato', 'white'))(224)) +
scale_size_discrete(range = c(0.2, 0.7)) +
theme_void() +
theme(panel.background = element_rect(fill = 'black'),
plot.background = element_rect(fill = 'black'),
plot.title = element_text(color='tomato', size=14, family="AppleGothic", hjust = 0.5)) +
labs(title = str_to_title(filtered)) -> p5c
<- "west-midlands"
filtered
%>%
car_commute_r filter(region_name == filtered) %>%
ggplot() +
geom_sf(aes(col = c1, size = c1), alpha = .2, show.legend = F) +
geom_sf(data = `c_west-midlands`, fill=NA, col=NA) +
scale_color_manual(values = colorRampPalette(c('#233423', 'tomato', 'white'))(195)) +
scale_size_discrete(range = c(0.2, 0.7)) +
theme_void() +
theme(panel.background = element_rect(fill = 'black'),
plot.background = element_rect(fill = 'black'),
plot.title = element_text(color='tomato', size=14, family="AppleGothic", hjust = 0.5)) +
labs(title = 'West Midlands') -> p6c
This might take a while to plot as it shows more than 2 million lines
<- "Times New Roman"
ft <- "#58685c"
ct
+ p4c + p6c ) / (p3c + p2c + p5c ) +
(p1c plot_annotation(title = 'Car-driver Flows in English Regions',
subtitle = 'Regions at the same scale organised in decreasing order (~2 million flows)',
caption = 'Data: {pct} | Graphic: @npalomin',
theme = theme(panel.background = element_rect(fill = 'black'),
plot.background = element_rect(fill = 'black'),
plot.title = element_text(color=ct, size=16, family=ft, face="bold"),
plot.subtitle = element_text(color=ct, size=10, family=ft),
plot.caption = element_text(color=ct, size=10, family=ft))) -> pgrid_c
pgrid_c