Cyclistic is a bike-share company in Chicago that launched in 2016. It has a fleet of 5,824 bicycles that are geotracked and locked into a network of 692 stations across Chicago. These bikes can be used from one station and returned at another.
Cyclistic has three different pricing plans: single-day passes, full-day passes, and annual memberships. Customers that use single-day passes and full-day passes are referred to as “casual riders”; customers that use annual memberships are referred to as “members”.
Lily Moreno, the director of marketing has assigned the Cyclistic marketing analytics team with designing marketing strategies aimed at converting casual riders into annual members. The Cyclistic executive team must then approve the recommended marketing program.
Identify how annual members and casual riders use Cyclistic bikes differently.
The data is located on a cloud here
The license for this data is available here
I have pulled a year’s worth of data for 2023 for this analysis.
Note: The datasets have a different name because Cyclistic is a fictional company.
The data is organized via comma-separated values (CSV) format. The columns are named as: “ride_id”, “rideable_type”, “started_at”, “ended_at”, “start_station_name”, “start_station_id”, “end_station_name”, “end_station_id”, “start_lat”, “start_lng”, “end_lat”, “end_lng”, “member_casual”.
The data is not biased as it has collected information from all users of Cyclistic. It is an accurate reputation of the population of Cyclistic users.
Also, he data is formatted and stored correctly as mentioned by the columns above. Although the data is missing some values in “end_station_name”, “end_station_id”, “end_lat”, and “end_lng”, these entries can be ignored due to the volume of data available.
This data is a primary data that is directly pulled from the company’s database.
The data is comprehensive as it contains a year’s worth of information.
This data is current as it is for 2023.
The data is cited as it is pulled directly from the company’s public database.
library(tidyverse)
library(janitor)
library(lubridate)
library(dplyr)
Jan_2023 <- read_csv("202301-divvy-tripdata.csv")
Feb_2023 <- read_csv("202302-divvy-tripdata.csv")
Mar_2023 <- read_csv("202303-divvy-tripdata.csv")
Apr_2023 <- read_csv("202304-divvy-tripdata.csv")
May_2023 <- read_csv("202305-divvy-tripdata.csv")
Jun_2023 <- read_csv("202306-divvy-tripdata.csv")
Jul_2023 <- read_csv("202307-divvy-tripdata.csv")
Aug_2023 <- read_csv("202308-divvy-tripdata.csv")
Sep_2023 <- read_csv("202309-divvy-tripdata.csv")
Oct_2023 <- read_csv("202310-divvy-tripdata.csv")
Nov_2023 <- read_csv("202311-divvy-tripdata.csv")
Dec_2023 <- read_csv("202312-divvy-tripdata.csv")
# Check that the datasets are mergeable
compare_df_cols_same(Jan_2023, Feb_2023, Mar_2023, Apr_2023, May_2023, Jun_2023, Jul_2023, Aug_2023, Sep_2023, Oct_2023, Nov_2023, Dec_2023)
## [1] TRUE
# Merge the datasets
trips_2023 <- rbind(Jan_2023, Feb_2023, Mar_2023, Apr_2023, May_2023, Jun_2023, Jul_2023, Aug_2023, Sep_2023, Oct_2023, Nov_2023, Dec_2023)
# View the new dataframe
str(trips_2023)
## spc_tbl_ [5,719,877 × 13] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
## $ ride_id : chr [1:5719877] "F96D5A74A3E41399" "13CB7EB698CEDB88" "BD88A2E670661CE5" "C90792D034FED968" ...
## $ rideable_type : chr [1:5719877] "electric_bike" "classic_bike" "electric_bike" "classic_bike" ...
## $ started_at : POSIXct[1:5719877], format: "2023-01-21 20:05:42" "2023-01-10 15:37:36" ...
## $ ended_at : POSIXct[1:5719877], format: "2023-01-21 20:16:33" "2023-01-10 15:46:05" ...
## $ start_station_name: chr [1:5719877] "Lincoln Ave & Fullerton Ave" "Kimbark Ave & 53rd St" "Western Ave & Lunt Ave" "Kimbark Ave & 53rd St" ...
## $ start_station_id : chr [1:5719877] "TA1309000058" "TA1309000037" "RP-005" "TA1309000037" ...
## $ end_station_name : chr [1:5719877] "Hampden Ct & Diversey Ave" "Greenwood Ave & 47th St" "Valli Produce - Evanston Plaza" "Greenwood Ave & 47th St" ...
## $ end_station_id : chr [1:5719877] "202480.0" "TA1308000002" "599" "TA1308000002" ...
## $ start_lat : num [1:5719877] 41.9 41.8 42 41.8 41.8 ...
## $ start_lng : num [1:5719877] -87.6 -87.6 -87.7 -87.6 -87.6 ...
## $ end_lat : num [1:5719877] 41.9 41.8 42 41.8 41.8 ...
## $ end_lng : num [1:5719877] -87.6 -87.6 -87.7 -87.6 -87.6 ...
## $ member_casual : chr [1:5719877] "member" "member" "casual" "member" ...
## - attr(*, "spec")=
## .. cols(
## .. ride_id = col_character(),
## .. rideable_type = col_character(),
## .. started_at = col_datetime(format = ""),
## .. ended_at = col_datetime(format = ""),
## .. start_station_name = col_character(),
## .. start_station_id = col_character(),
## .. end_station_name = col_character(),
## .. end_station_id = col_character(),
## .. start_lat = col_double(),
## .. start_lng = col_double(),
## .. end_lat = col_double(),
## .. end_lng = col_double(),
## .. member_casual = col_character()
## .. )
## - attr(*, "problems")=<externalptr>
# Keep only unique values ride_id
trips_2023 %>% distinct(ride_id)
# Check that only valid string values are in 'rideable_type' & 'member_casual'
valid_types <- c('electric_bike', 'classic_bike', 'docked_bike')
valid_members <- c('member', 'casual')
all(trips_2023$rideable_type %in% valid_types)
## [1] TRUE
all(trips_2023$member_casual %in% valid_members)
## [1] TRUE
# Convert date and time format
trips_2023$started_at = ymd_hms(trips_2023$started_at)
## Warning: 23 failed to parse.
trips_2023$ended_at = ymd_hms(trips_2023$ended_at)
## Warning: 26 failed to parse.
# Remove any null values
trips_2023 <- trips_2023 %>% drop_na()
# Create new columns
trips_2023$date <- as.Date(trips_2023$started_at) # Create column for date
trips_2023$year <- format(as.Date(trips_2023$date), format = "%Y") # Create column for year
trips_2023$month <- format(as.Date(trips_2023$date), format = "%m") # Create column for month
trips_2023$week <- format(as.Date(trips_2023$date), format = '%W') # Create column for week
trips_2023$day <- format(as.Date(trips_2023$date), format = '%A') # Create column for day
trips_2023$start_time <- format(as.POSIXct(trips_2023$started_at), format = '%H:%M') # Create column for start time
trips_2023$start_time_hour <- format(as.POSIXct(trips_2023$started_at), format = '%H') # Create column for start time hour
trips_2023$end_time <- format(as.POSIXct(trips_2023$ended_at), format = '%H:%M') # Create column for end time
trips_2023$length <- difftime(trips_2023$ended_at, trips_2023$started_at, units='mins') # Create for ride length in minutes
trips_2023$length <- as.numeric(as.character(trips_2023$length)) # Change value into numeric
trips_2023 <- trips_2023 %>% # remove trips with a ride length of less than 0
filter(!(length <= 0))
# Order the data by the day of the week
trips_2023$day <- as.factor(trips_2023$day)
trips_2023$day <- ordered(trips_2023$day, levels = c("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"))
# View summary
summary(trips_2023$length)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.017 5.617 9.800 15.954 17.483 12136.300
# Compare members and casual users
aggregate(trips_2023$length ~ trips_2023$member_casual, FUN = mean)
## trips_2023$member_casual trips_2023$length
## 1 casual 22.94025
## 2 member 12.13261
aggregate(trips_2023$length ~ trips_2023$member_casual, FUN = median)
## trips_2023$member_casual trips_2023$length
## 1 casual 12.750000
## 2 member 8.616667
aggregate(trips_2023$length ~ trips_2023$member_casual, FUN = max)
## trips_2023$member_casual trips_2023$length
## 1 casual 12136.300
## 2 member 1497.867
aggregate(trips_2023$length ~ trips_2023$member_casual, FUN = min)
## trips_2023$member_casual trips_2023$length
## 1 casual 0.01666667
## 2 member 0.01666667
# See the average ride time by each day for members vs casual users
aggregate(trips_2023$length ~ trips_2023$member_casual + trips_2023$day,
FUN = mean)
## trips_2023$member_casual trips_2023$day trips_2023$length
## 1 casual Sunday 26.56861
## 2 member Sunday 13.61448
## 3 casual Monday 22.53651
## 4 member Monday 11.55101
## 5 casual Tuesday 20.51477
## 6 member Tuesday 11.64996
## 7 casual Wednesday 19.60096
## 8 member Wednesday 11.58707
## 9 casual Thursday 19.99994
## 10 member Thursday 11.60285
## 11 casual Friday 22.32192
## 12 member Friday 12.04024
## 13 casual Saturday 25.91937
## 14 member Saturday 13.58460
# Create CSV for Tableau
average_length <- aggregate(trips_2023$length ~ trips_2023$member_casual + trips_2023$day,
FUN = mean)
write.csv(average_length, 'C:\\Users\\terry\\Desktop\\bike_share\\average_length.csv')
trips_2023 %>%
group_by(member_casual, day) %>% #groups by usertype and weekday
summarise(number_of_rides = n(), #calculates the number of rides and average duration
average_duration = mean(length)) %>% # calculates the average duration
arrange(member_casual, day)
## `summarise()` has grouped output by 'member_casual'. You can override using the
## `.groups` argument.
## # A tibble: 14 × 4
## # Groups: member_casual [2]
## member_casual day number_of_rides average_duration
## <chr> <ord> <int> <dbl>
## 1 casual Sunday 254710 26.6
## 2 casual Monday 175381 22.5
## 3 casual Tuesday 181510 20.5
## 4 casual Wednesday 183063 19.6
## 5 casual Thursday 198904 20.0
## 6 casual Friday 227826 22.3
## 7 casual Saturday 310123 25.9
## 8 member Sunday 307818 13.6
## 9 member Monday 386648 11.6
## 10 member Tuesday 448778 11.6
## 11 member Wednesday 452677 11.6
## 12 member Thursday 452609 11.6
## 13 member Friday 400467 12.0
## 14 member Saturday 350592 13.6
# Create CSV for Tableau
count <- trips_2023 %>%
group_by(member_casual) %>%
summarise(number_of_rides = n())
write.csv(count, 'C:\\Users\\terry\\Desktop\\bike_share\\count.csv')
count_per_day <- trips_2023 %>%
group_by(member_casual, day) %>%
summarise(number_of_rides = n())
## `summarise()` has grouped output by 'member_casual'. You can override using the
## `.groups` argument.
write.csv(count_per_day, 'C:\\Users\\terry\\Desktop\\bike_share\\count_per_day.csv')
# Visualization of the number of rides by rider type per day
trips_2023 %>%
group_by(member_casual, day) %>%
summarise(number_of_rides = n()) %>%
ggplot(aes(x = day, y = number_of_rides, fill = member_casual)) +
geom_col(position = "dodge") +
labs(title ="Number of Rides per Day ")
## `summarise()` has grouped output by 'member_casual'. You can override using the
## `.groups` argument.
# Visualization for average duration per day
trips_2023 %>%
group_by(member_casual, day) %>%
summarise(average_duration_minutes = mean(length)) %>%
arrange(member_casual, day) %>%
ggplot(aes(x = day, y = average_duration_minutes, fill = member_casual)) +
geom_col(position = "dodge") +
labs(title ="Average Duration of Ride Length")
## `summarise()` has grouped output by 'member_casual'. You can override using the
## `.groups` argument.
# Visualization for rides by month
trips_2023 %>%
group_by(member_casual, month) %>%
summarise(number_of_rides = n()) %>%
arrange(member_casual, month) %>%
ggplot(aes(x = month, y = number_of_rides, fill = member_casual)) +
geom_col(position = "dodge") +
labs(title ="Number of Rides per Month")
## `summarise()` has grouped output by 'member_casual'. You can override using the
## `.groups` argument.
# Create CSV
rides_per_month <- trips_2023 %>%
group_by(member_casual, month) %>%
summarise(number_of_rides = n()) %>%
arrange(member_casual, month)
## `summarise()` has grouped output by 'member_casual'. You can override using the
## `.groups` argument.
write.csv(rides_per_month, 'C:\\Users\\terry\\Desktop\\bike_share\\rides_per_month.csv')
# Number of Rides per Time of Day
trips_2023 %>%
group_by(member_casual, start_time_hour) %>%
summarise(number_of_rides = n()) %>%
ggplot(aes(x = start_time_hour, y = number_of_rides, fill = member_casual)) +
geom_col(position = "dodge") +
labs(title ="Number of Rides per Time of Day")
## `summarise()` has grouped output by 'member_casual'. You can override using the
## `.groups` argument.
# Create CSV
rides_per_time <- trips_2023 %>%
group_by(member_casual, start_time_hour) %>%
summarise(number_of_rides = n())
## `summarise()` has grouped output by 'member_casual'. You can override using the
## `.groups` argument.
write.csv(rides_per_time, 'C:\\Users\\terry\\Desktop\\bike_share\\rides_per_time.csv')
# Number of Rides Per Bike Type
trips_2023 %>%
group_by(rideable_type, member_casual) %>%
filter(rideable_type != 'docked_bike') %>%
summarise(number_of_rides =n()) %>%
ggplot(aes(x = rideable_type, y = number_of_rides, fill = member_casual)) +
geom_col(position = "dodge") +
labs(title ="Number of Rides per Bike Type")
## `summarise()` has grouped output by 'rideable_type'. You can override using the
## `.groups` argument.
# Create CSV
rides_per_type <- trips_2023 %>%
group_by(rideable_type, member_casual) %>%
filter(rideable_type != 'docked_bike') %>%
summarise(number_of_rides =n())
## `summarise()` has grouped output by 'rideable_type'. You can override using the
## `.groups` argument.
write.csv(rides_per_type, 'C:\\Users\\terry\\Desktop\\bike_share\\rides_per_type.csv')
# Create CSV
most_used_station_map <- trips_2023 %>%
group_by(start_station_name, start_lat, start_lng, member_casual) %>%
summarise(total_count = length(na.omit(start_station_name))) %>%
arrange(desc(total_count))
## `summarise()` has grouped output by 'start_station_name', 'start_lat',
## 'start_lng'. You can override using the `.groups` argument.
write.csv(most_used_station_map, 'C:\\Users\\terry\\Desktop\\bike_share\\most_used_station_map.csv')
most_used_stations_member <- trips_2023 %>%
filter(member_casual == 'member')
most_used_stations_member <- most_used_stations_member %>%
group_by(start_station_name) %>%
filter(start_station_name != 'NA') %>%
summarise(total_count = length(start_station_name)) %>%
arrange(desc(total_count)) %>%
top_n(10)
## Selecting by total_count
write.csv(most_used_stations_member, 'C:\\Users\\terry\\Desktop\\bike_share\\most_used_station_member.csv') # Create CSV
most_used_stations_member %>%
ggplot(aes(x = reorder(start_station_name, -total_count), y = total_count, fill =
start_station_name)) +
geom_col(position = "dodge") +
theme(axis.text.x = element_text(angle=90, vjust=.5, hjust=1)) +
theme(legend.position = "none") +
labs(title ="Most Used Stations by Member Riders", x = 'start_station_name')
most_used_stations_casual <- trips_2023 %>%
filter(member_casual == 'casual')
most_used_stations_casual <- most_used_stations_casual %>%
group_by(start_station_name) %>%
filter(start_station_name != 'NA') %>%
summarise(total_count = length(start_station_name)) %>%
arrange(desc(total_count)) %>%
top_n(10)
## Selecting by total_count
write.csv(most_used_stations_casual, 'C:\\Users\\terry\\Desktop\\bike_share\\most_used_stations_casual.csv') # Create CSV
most_used_stations_casual %>%
ggplot(aes(x = reorder(start_station_name, -total_count), y = total_count, fill =
start_station_name)) +
geom_col(position = "dodge") +
theme(axis.text.x = element_text(angle=90, vjust=.5, hjust=1)) +
theme(legend.position = "none") +
labs(title ="Most Used Stations by Casual Riders", x = 'start_station_name')
### Findings
Members make up 64.64% of riders and casuals make up 35.36% of riders.
Both types of riders prefer classic bikes.
Members prefer to ride during the weekdays and casuals like to ride over the weekends.
Casuals take much longer rides than members.
The peak hours of rides is during 8:00 AM and 5:00 PM, especially for members. This suggests that members mostly used the bikes for commuting to work. Most number of rides are also taken in between this time.
The number of rides peak during summer months.
Offer promotions during peak summer months to enroll more casual riders.
Increase the volume of classic bikes compared to electric bikes so there is no shortage of bikes once more casual riders enroll in membership.
Offer memberships based on ride duration rather than a time based membership. This will allow casual riders to feel more value for their membership if they do not ride often but take longer rides.
Offer weekend memberships at a discounted price since casual riders are more likely to use the bikes only on the weekends.
Focus marketing strategies on the top used stations to reach more riders.