Introduction

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.

Business Task

Identify how annual members and casual riders use Cyclistic bikes differently.

Data Information

Location

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.

Organization

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”.

ROCCC

Reliable

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.

Original

This data is a primary data that is directly pulled from the company’s database.

Comprehensive

The data is comprehensive as it contains a year’s worth of information.

Current

This data is current as it is for 2023.

Cited

The data is cited as it is pulled directly from the company’s public database.

Process

Import Data into R

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")

Combine Datasets

# 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>

Clean and Manipulate Data

# 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")) 

Analyze

# 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

Share

Tableau

Act

  1. Offer promotions during peak summer months to enroll more casual riders.

  2. Increase the volume of classic bikes compared to electric bikes so there is no shortage of bikes once more casual riders enroll in membership.

  3. 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.

  4. Offer weekend memberships at a discounted price since casual riders are more likely to use the bikes only on the weekends.

  5. Focus marketing strategies on the top used stations to reach more riders.