Excess rentals in TfL bike sharing
url <- "https://data.london.gov.uk/download/number-bicycle-hires/ac29363e-e0cb-47cc-a97a-e216d900a6b0/tfl-daily-cycle-hires.xlsx"
# Download TFL data to temporary file
httr::GET(url, write_disk(bike.temp <- tempfile(fileext = ".xlsx")))
## Response [https://airdrive-secure.s3-eu-west-1.amazonaws.com/london/dataset/number-bicycle-hires/2022-09-06T12%3A41%3A48/tfl-daily-cycle-hires.xlsx?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJJDIMAIVZJDICKHA%2F20220919%2Feu-west-1%2Fs3%2Faws4_request&X-Amz-Date=20220919T132703Z&X-Amz-Expires=300&X-Amz-Signature=94560235fddb8abec5c25ae5cdd2b43e63fab1f8b962621790db52ef86c16da8&X-Amz-SignedHeaders=host]
## Date: 2022-09-19 13:27
## Status: 200
## Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
## Size: 180 kB
## <ON DISK> C:\Users\GABRIE~1\AppData\Local\Temp\Rtmp4KXTLW\file601c11951c51.xlsx
# Use read_excel to read it as dataframe
bike0 <- read_excel(bike.temp,
sheet = "Data",
range = cell_cols("A:B"))
# change dates to get year, month, and week
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 one looks at percentage changes from the expected level of monthly rentals.
bike_monthly_avg<-bike%>%
filter(year %in% c(2017, 2018, 2019, 2020, 2021, 2022))%>%
group_by(year, month)%>%
summarise(monthly_avg=mean(bikes_hired))%>%
ungroup()%>%
add_row(year=2022, month="Sep")%>%
add_row(year=2022, month="Oct")%>%
add_row(year=2022, month="Nov")%>%
add_row(year=2022, month="Dec")%>%
group_by(year, month)%>%
mutate(date=as.Date(paste("01", month, toString(year), sep="/"), "%d/%b/%Y"))
bike_monthly_avg_across_years<-bike%>%
filter(year %in% c(2016, 2017, 2018, 2019))%>%
group_by(month)%>%
summarise(monthly_avg_across_years=mean(bikes_hired))
bike_monthly_avg <-bike_monthly_avg%>%
inner_join(bike_monthly_avg_across_years, by = "month")%>%
mutate(monthly_avg_across_years=ifelse(year==2022 & (month %in% c("Sep", "Oct", "Nov", "Dec")), NA, monthly_avg_across_years))
bike_monthly_avg
## # A tibble: 72 × 5
## # Groups: year, month [72]
## year month monthly_avg date monthly_avg_across_years
## <dbl> <chr> <dbl> <date> <dbl>
## 1 2017 Jan 20596. 2017-01-01 20617.
## 2 2017 Feb 22091. 2017-02-01 22049.
## 3 2017 Mar 26444. 2017-03-01 23237.
## 4 2017 Apr 30591. 2017-04-01 28299.
## 5 2017 May 32019. 2017-05-01 33270.
## 6 2017 Jun 36610. 2017-06-01 35413.
## 7 2017 Jul 36511. 2017-07-01 38109.
## 8 2017 Aug 32071. 2017-08-01 34393.
## 9 2017 Sep 31158. 2017-09-01 33013.
## 10 2017 Oct 31409. 2017-10-01 30235.
## # … with 62 more rows
library(ggbraid)
ggplot(bike_monthly_avg,aes(x=date))+geom_line(aes(y=monthly_avg))+geom_line(aes(y=monthly_avg_across_years), color="blue", size=1)+facet_wrap(~year, scales="free_x", nrow=2) + scale_x_date(date_breaks = "1 month", date_labels = "%b")+
geom_braid(aes(ymin=monthly_avg, ymax=monthly_avg_across_years, fill=monthly_avg>monthly_avg_across_years))+theme(legend.position="none")+
labs(title = "Monthly changes in TfL bike rentals",
subtitle = "Change from monthly average shown in blue and calculated between 2016-2019",
caption = "Source: TfL, London Data Store",y="Bike rentals", x = element_blank())

This graph looks at percentage changes from the expected level of weekly rentals. The two grey shaded rectangles correspond to Q2 (weeks 14-26) and Q4 (weeks 40-52).
bike_weekly_avg<-bike%>%
mutate(year_corrected=ifelse(month=="Jan"&(week==52|week==53), year-1, ifelse(month=="Dec"&(week==1), year+1, year)))%>%
filter(year_corrected %in% c(2017, 2018, 2019, 2020, 2021, 2022))%>%
group_by(year_corrected, week)%>%
summarise(weekly_avg=mean(bikes_hired))%>%
ungroup()
bike_weekly_avg_across_years<-bike%>%
mutate(year_corrected=ifelse(month=="Jan"&(week==52|week==53), year-1, ifelse(month=="Dec"&(week==1), year+1, year)))%>%
filter(year %in% c(2016, 2017, 2018, 2019))%>%
group_by(week)%>%
summarise(weekly_avg_across_years=mean(bikes_hired))%>%
ungroup()
bike_weekly_avg <- bike_weekly_avg %>%
inner_join(bike_weekly_avg_across_years, by = "week")%>%
mutate(weekly_change=weekly_avg/weekly_avg_across_years-1)
bike_weekly_avg
## # A tibble: 296 × 5
## year_corrected week weekly_avg weekly_avg_across_years weekly_change
## <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 2017 1 17662. 17202. 0.0267
## 2 2017 2 21858. 22056. -0.00899
## 3 2017 3 22691. 21892. 0.0365
## 4 2017 4 21269. 21470. -0.00933
## 5 2017 5 21643. 21194. 0.0212
## 6 2017 6 19782. 20226. -0.0219
## 7 2017 7 24019. 23254. 0.0329
## 8 2017 8 23326. 23575. -0.0106
## 9 2017 9 21642. 20167 0.0731
## 10 2017 10 26597. 23403. 0.136
## # … with 286 more rows
ggplot(bike_weekly_avg, aes(x=week))+geom_line(aes(y=weekly_change))+facet_wrap(~year_corrected, nrow=2)+scale_y_continuous(labels = scales::percent)+scale_x_continuous(breaks=c(13, 26, 39, 53))+theme(legend.position="none")+
geom_braid(aes(ymin=0, ymax=weekly_change, fill=weekly_change>0))+
geom_rect(data=bike_weekly_avg, aes(xmin=13, xmax=26, ymin=-Inf, ymax=Inf), color="transparent", fill="grey20", alpha=0.005)+
geom_rect(data=bike_weekly_avg, aes(xmin=39, xmax=53, ymin=-Inf, ymax=Inf), color="transparent", fill="grey20", alpha=0.005)+geom_rug(mapping = aes(color = weekly_change>0), sides="b")+
labs(title = "Weekly change in TfL bike rentals",
subtitle = "% change from weekly averages calculated between 2016-2019",
caption = "Source: TfL, London Data Store",y=element_blank(), x = "Week")

I use the mean to calculate expected rentals because according to the law of large numbers the sample mean approaches the expected value of a variable as the sample size grows infinitely large. This property makes the mean superior to median when it comes to calculating expected values.