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.
knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE)
library(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
flows <- get_od(
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
nodes <- get_centroids_ew()
# Download regions (pct defined regions)
regions <- pct_regionsflows %>%
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
x1 <- flows_bike$geo_code1 %>%
unique() %>%
as.data.frame()
x2 <- flows_bike$geo_code2 %>%
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})
bike_commute <- od2line(flows_bike, nodes_bike)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
regl <- c("london", "cambridgeshire", "avon", "greater-manchester", "oxfordshire", "west-midlands")# get bounding boxes
bx <- list()
for (i in regl) {
bx[[i]] <- bike_commute_r %>%
filter(region_name == i) %>%
st_bbox() %>%
st_as_sfc() %>%
st_sf()
}
bx1 <- do.call(rbind, bx) %>%
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
lbb <- "cambridgeshire"
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
(cam_p - (4 * cam_n)) / 4
#> 33819.5 [m]lbb <- "oxfordshire"
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
(oxf_p - (4 * oxf_n)) / 4
#> 32356.5 [m]# create a circle of radius = 33819.5 for each region
for (i in regl) {
cf <- bx1 %>%
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()filtered <- "london"
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)) -> p1filtered <- "cambridgeshire"
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)) -> p2filtered <- "avon"
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)) -> p3filtered <- "greater-manchester"
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') -> p4filtered <- "oxfordshire"
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)) -> p5filtered <- "west-midlands"
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') -> p6ft <- "Times New Roman"
ct <- "#58685c"
(p1 + p2 + p3 ) / (p4 + p5 + p6 ) +
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
pgridThe 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
x1c <- flows_car$geo_code1 %>%
unique() %>%
as.data.frame()
x2c <- flows_car$geo_code2 %>%
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})
car_commute <- od2line(flows_car, nodes_car)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 rowsfiltered <- "london"
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)) -> p1cfiltered <- "cambridgeshire"
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)) -> p2cfiltered <- "avon"
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)) -> p3cfiltered <- "greater-manchester"
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') -> p4cfiltered <- "oxfordshire"
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)) -> p5cfiltered <- "west-midlands"
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') -> p6cThis might take a while to plot as it shows more than 2 million lines
ft <- "Times New Roman"
ct <- "#58685c"
(p1c + p4c + p6c ) / (p3c + p2c + p5c ) +
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