Changes in Tfl Bike rentals from 2016-2022
Excess rentals in TfL bike sharing
For this task, I attempted to analyse Tfl bike data to determine how the number of bikes rented in the years 2017-2022 differed from the average rentals in 2016-2019, on a monthly and weekly basis.
First, i downloaded the data from the london.gov.uk website, then read the data into a dataframe
url <- "https://data.london.gov.uk/download/number-bicycle-hires/ac29363e-e0cb-47cc-a97a-e216d900a6b0/tfl-daily-cycle-hires.xlsx"
httr::GET(url, write_disk(bike.temp <- tempfile(fileext = ".xlsx")))
bike0 <- read_excel(bike.temp,
sheet = "Data",
range = cell_cols("A:B"))
After reading the data into the dataframe, I used the clean_names() function to convert all names to use lowercase instead of uppercase letters, and to convert all spaces into underscores. I then split the year, month and week from the day field, as it was originally stored as one date field (day).
bike <- bike0 %>%
clean_names() %>%
rename (bikes_hired = number_of_bicycle_hires) %>%
mutate (year = year(day),
month = lubridate::month(day, label = TRUE),
week = isoweek(day))
The first graph I wanted to produce was to show the changes in the average number of bikes rented on a month to month basis, from 2017-2022, in comparison to the average in years 2016-2019. In order to achieve this, I first found the mean for each month, then found the monthly mean for years 2017-2019.
After finding these, I plot the two values against one another using geom_line(), in order to have a visual representation of which was higher. Geom_line() was chosen as it would produce a line connecting all points, instead of producing a best fit line. In this way, the trend from point to point could be more closely observed. To further aid in the comparison, I shaded the area between the two lines, with green representing when the monthly average was higher than the monthly average for 2016-2019, and red representing when it was lower. This was done using two ribbons from geom_ribbon(), one where the minimum was the monthly mean for 2017-2022, and one where the minimum was the monthly mean for 2016-2019. For both of these ribbons, the maximum value was taken as the higher of the pair of values, using the pmax() function.
bike2 <- bike %>%
filter(year>2015) %>%
group_by(month,year) %>%
mutate(month=match(month,month.abb)) %>%
summarize(monthly_mean = mean(bikes_hired))
bike_2016_to_2019 <-bike %>%
filter(2015<year & year<2020) %>%
group_by(month) %>%
mutate(month=match(month,month.abb)) %>%
summarize(monthly_mean_2016_2019 = mean(bikes_hired))
bike_merged<-merge(x=bike2,y=bike_2016_to_2019,by="month") %>%
filter(year>2016)
bike_longer<-bike_merged %>%
pivot_longer(cols=3:4,
names_to="type",
values_to="n")
ggplot(data=bike_merged, aes(x=month)) +
geom_line(aes(y=monthly_mean)) +
geom_line(aes(y=monthly_mean_2016_2019), colour='blue',size=1) +
geom_ribbon(aes(x=month,
ymin = monthly_mean,
ymax = pmax(monthly_mean,monthly_mean_2016_2019),
fill = "red"),
alpha=0.1) +
geom_ribbon(aes(x=month,
ymin = monthly_mean_2016_2019,
ymax = pmax(monthly_mean,monthly_mean_2016_2019),
fill = "green"),
alpha=0.1) +
scale_x_continuous(breaks = seq_along(month.abb),
labels = month.abb) +
scale_fill_manual(values=c("green", "red"), name="fill") +
guides(linetype = "none", fill = "none") +
labs(title = "Monthly changes in Tfl bike rentals", subtitle = "Change from monthly average shown in blue and calculated between 2016-2019",
x='', y='Bike Rentals', caption = "Source: Tfl, London, Data Store") +
theme(legend.position = 'none') +
facet_wrap(~year)+
theme_minimal()

The second graph I wanted to produce was to show the changes on a week on week basis for 2017-2022, once again comparing against the average for the same week across 2016-2019. In this instance however, instead of plotting the data as two separate lines, I instead plot only the percentage change, with the weekly mean from 2016-2019 being taken as the base amount.
In this instance, I first used summarize() to summarize the data by weeks instead of months, then mutated the data to have an extra column with the calculated percentage change using mutate(). I then plot the graph as a line graph using geom_line(), and used two ribbons, one green and one red, in order to shade areas where the value was above or below 0% change respectively. In order to add legibility to the graph, I also used geom_rect() to add alternating grey and white rectangles to denote chunks of 13 weeks, and used geom_rug() to add a rug at the bottom of the graph, where the colours matched the colour of the shaded area in the graph.
bike3 <- bike %>%
mutate(month=match(month,month.abb)) %>%
filter(!(month==1 & week == 52)) %>%
filter(year>2015) %>%
group_by(week,year) %>%
summarize(weekly_mean = mean(bikes_hired))
bike_weekly_2016_to_2019 <- bike %>%
filter(2015<year & year<2020) %>%
group_by(week) %>%
summarize(weekly_mean_2016_2019 = mean(bikes_hired))
bike_weekly_merged<-merge(x=bike3,y=bike_weekly_2016_to_2019,by="week") %>%
filter(year>2016) %>%
mutate(pct_change_weekly = (weekly_mean/weekly_mean_2016_2019-1)*100)
ggplot(data=bike_weekly_merged, aes(x=week)) +
geom_rect(aes(xmin = 13, xmax = 27, ymin = -Inf, ymax = Inf), fill="light gray") +
geom_rect(aes(xmin = 39, xmax = 52, ymin = -Inf, ymax = Inf), fill="light gray") +
geom_rect(aes(xmin = 0, xmax = 13, ymin = -Inf, ymax = Inf), fill="white") +
geom_rect(aes(xmin = 27, xmax = 39, ymin = -Inf, ymax = Inf), fill="white") +
geom_line(aes(y=pct_change_weekly)) +
geom_ribbon(aes(x=week,
ymin = 0,
ymax = pmax(0, pct_change_weekly)),
alpha=0.1, fill='green') +
geom_ribbon(aes(x=week,
ymin = pct_change_weekly,
ymax = pmax(0, pct_change_weekly)),
alpha=0.1, fill='red') +
guides(linetype = "none", fill = "none") +
labs(title = "Weekly changes in Tfl bike rentals", subtitle = "Change from weekly averages calculated between 2016-2019", x='week', y='', caption = "Source: Tfl, London, Data Store") +
theme(legend.position = 'none') +
facet_wrap(~year)+
scale_fill_manual(values=c("green", "red"), name="fill") +
geom_rug(aes(color=case_when(pct_change_weekly>0~"red",
pct_change_weekly<0~"green"))
,sides="b") +
scale_color_manual(values=c("red", "green"), name="color") +
xlim(0,52) +
NULL
