Introduction

Welcome to my analysis of the 2019 Cyclistic ride data set. Cyclistic is a (fictional) Chicago-based bike sharing service. It provides a fleet of geo-tracked bicycles, which can be unlocked from one station and returned to any other Cyclistic station within the Chicago metropolitan area.

There are two distinct categories of customer: susbscribers, who have obtained annual memberships, and casual users, who have purchased single-ride passes or full-day passes. The distinction between these two categories will provide the overarching theme of our analysis of this data set.

The Business Task

The Director of Marketing at Cyclistic, Lily Moreno, wants to formulate a marketing strategy geared towards converting casual users to subscribers. Previous analysis has determined that subscribers are a more profitable customer base than than the casual users. A marketing campaign designed to motivate the casual users to subscribe could yield more benefit than a less focused campaign aimed at all potential new customers.

To achieve this goal, differences need to be identified between the subscriber and casual groups of customers. Using those differences, characteristics more unique to the casual user base can be incorporated into future Cyclistic marketing campaigns, and the needs of that group can be addressed in order to make subscription more appealing to them.

This is where the data comes into play, enabling us to see in which ways the two groups differ, and how they use Cyclistic bicycles in their own unique ways.

The Data

I will be using first-party data for this analysis. It is from Cyclistic’s real world proxy, Divvy, whose 2019 .csv quarterly data sets will serve as a stand-in for Cyclistic’s data.

It fulfills the ROCCC (Reliable, Original, Comprehensive, Current, Cited) criteria.

It’s reliable and original as it is from the company themselves, with data collected from the bike’s geo-tracking devices and the company’s own customers. It’s comprehensive and current in that it spans the entirety of 2019, our chosen year of analysis.

It’s cited in that the company themselves have created their own data and accounted for their methods of collection, and provided permission for its public use at https://divvybikes.com/data-license-agreement

The data can be accessed at https://divvy-tripdata.s3.amazonaws.com/index.html

Processing the Data

For this analysis project, I’ll be using R, and remaining within the environment of RStudio.

The first step is to load the necessary libraries:

#Helps wrangle data:
library(tidyverse)  
#Use the conflicted package to manage conflicts:
library(conflicted)
#plotrix library for 3D pie chart
library(plotrix)
#gridExtra library for bike station plot grid
library(gridExtra)

#Set dplyr::filter and dplyr::lag as the default choices
conflict_prefer("filter", "dplyr")
conflict_prefer("lag", "dplyr")

#To disable scientific notation on the axes of the plots:
options(scipen = 999)

Now to import the data sets:

#Upload Divvy data sets (csv files):
q1_2019 <- read_csv("Divvy_Trips_2019_Q1.csv")
q2_2019 <- read_csv("Divvy_Trips_2019_Q2.csv")
q3_2019 <- read_csv("Divvy_Trips_2019_Q3.csv")
q4_2019 <- read_csv("Divvy_Trips_2019_Q4.csv")

We’ll inspect the four newly created data frames, to see how well they align with each other:

glimpse(q1_2019)
## Rows: 365,069
## Columns: 12
## $ trip_id           <dbl> 21742443, 21742444, 21742445, 21742446, 21742447, 21~
## $ start_time        <dttm> 2019-01-01 00:04:37, 2019-01-01 00:08:13, 2019-01-0~
## $ end_time          <dttm> 2019-01-01 00:11:07, 2019-01-01 00:15:34, 2019-01-0~
## $ bikeid            <dbl> 2167, 4386, 1524, 252, 1170, 2437, 2708, 2796, 6205,~
## $ tripduration      <dbl> 390, 441, 829, 1783, 364, 216, 177, 100, 1727, 336, ~
## $ from_station_id   <dbl> 199, 44, 15, 123, 173, 98, 98, 211, 150, 268, 299, 2~
## $ from_station_name <chr> "Wabash Ave & Grand Ave", "State St & Randolph St", ~
## $ to_station_id     <dbl> 84, 624, 644, 176, 35, 49, 49, 142, 148, 141, 295, 4~
## $ to_station_name   <chr> "Milwaukee Ave & Grand Ave", "Dearborn St & Van Bure~
## $ usertype          <chr> "Subscriber", "Subscriber", "Subscriber", "Subscribe~
## $ gender            <chr> "Male", "Female", "Female", "Male", "Male", "Female"~
## $ birthyear         <dbl> 1989, 1990, 1994, 1993, 1994, 1983, 1984, 1990, 1995~
glimpse(q2_2019)
## Rows: 1,108,163
## Columns: 12
## $ `01 - Rental Details Rental ID`                    <dbl> 22178529, 22178530,~
## $ `01 - Rental Details Local Start Time`             <dttm> 2019-04-01 00:02:2~
## $ `01 - Rental Details Local End Time`               <dttm> 2019-04-01 00:09:4~
## $ `01 - Rental Details Bike ID`                      <dbl> 6251, 6226, 5649, 4~
## $ `01 - Rental Details Duration In Seconds Uncapped` <dbl> 446, 1048, 252, 357~
## $ `03 - Rental Start Station ID`                     <dbl> 81, 317, 283, 26, 2~
## $ `03 - Rental Start Station Name`                   <chr> "Daley Center Plaza~
## $ `02 - Rental End Station ID`                       <dbl> 56, 59, 174, 133, 1~
## $ `02 - Rental End Station Name`                     <chr> "Desplaines St & Ki~
## $ `User Type`                                        <chr> "Subscriber", "Subs~
## $ `Member Gender`                                    <chr> "Male", "Female", "~
## $ `05 - Member Details Member Birthday Year`         <dbl> 1975, 1984, 1990, 1~
glimpse(q3_2019)
## Rows: 1,640,718
## Columns: 12
## $ trip_id           <dbl> 23479388, 23479389, 23479390, 23479391, 23479392, 23~
## $ start_time        <dttm> 2019-07-01 00:00:27, 2019-07-01 00:01:16, 2019-07-0~
## $ end_time          <dttm> 2019-07-01 00:20:41, 2019-07-01 00:18:44, 2019-07-0~
## $ bikeid            <dbl> 3591, 5353, 6180, 5540, 6014, 4941, 3770, 5442, 2957~
## $ tripduration      <dbl> 1214, 1048, 1554, 1503, 1213, 310, 1248, 1550, 1583,~
## $ from_station_id   <dbl> 117, 381, 313, 313, 168, 300, 168, 313, 43, 43, 511,~
## $ from_station_name <chr> "Wilton Ave & Belmont Ave", "Western Ave & Monroe St~
## $ to_station_id     <dbl> 497, 203, 144, 144, 62, 232, 62, 144, 195, 195, 84, ~
## $ to_station_name   <chr> "Kimball Ave & Belmont Ave", "Western Ave & 21st St"~
## $ usertype          <chr> "Subscriber", "Customer", "Customer", "Customer", "C~
## $ gender            <chr> "Male", NA, NA, NA, NA, "Male", NA, NA, NA, NA, NA, ~
## $ birthyear         <dbl> 1992, NA, NA, NA, NA, 1990, NA, NA, NA, NA, NA, NA, ~
glimpse(q4_2019)
## Rows: 704,054
## Columns: 12
## $ trip_id           <dbl> 25223640, 25223641, 25223642, 25223643, 25223644, 25~
## $ start_time        <dttm> 2019-10-01 00:01:39, 2019-10-01 00:02:16, 2019-10-0~
## $ end_time          <dttm> 2019-10-01 00:17:20, 2019-10-01 00:06:34, 2019-10-0~
## $ bikeid            <dbl> 2215, 6328, 3003, 3275, 5294, 1891, 1061, 1274, 6011~
## $ tripduration      <dbl> 940, 258, 850, 2350, 1867, 373, 1072, 1458, 1437, 83~
## $ from_station_id   <dbl> 20, 19, 84, 313, 210, 156, 84, 156, 156, 336, 77, 19~
## $ from_station_name <chr> "Sheffield Ave & Kingsbury St", "Throop (Loomis) St ~
## $ to_station_id     <dbl> 309, 241, 199, 290, 382, 226, 142, 463, 463, 336, 50~
## $ to_station_name   <chr> "Leavitt St & Armitage Ave", "Morgan St & Polk St", ~
## $ usertype          <chr> "Subscriber", "Subscriber", "Subscriber", "Subscribe~
## $ gender            <chr> "Male", "Male", "Female", "Male", "Male", "Female", ~
## $ birthyear         <dbl> 1987, 1998, 1991, 1990, 1987, 1994, 1991, 1995, 1993~

Upon inspection, all four data frames have 12 columns, with q1_2019, q3_2019, q4_2019 all having identical column names and data types. q2_2019 aligns with them in terms of data types, but the columns have different names, despite signifying the same variables. To make q2_2019 align with the others, we’ll rename the columns of q2_2019:

q2_2019 <- rename(q2_2019
                  ,trip_id = "01 - Rental Details Rental ID"
                  ,start_time = "01 - Rental Details Local Start Time"
                  ,end_time = "01 - Rental Details Local End Time"
                  ,bikeid = "01 - Rental Details Bike ID"
                  ,tripduration = "01 - Rental Details Duration In Seconds Uncapped"
                  ,from_station_id = "03 - Rental Start Station ID"
                  ,from_station_name = "03 - Rental Start Station Name"
                  ,to_station_id = "02 - Rental End Station ID"
                  ,to_station_name = "02 - Rental End Station Name"
                  ,usertype = "User Type"
                  ,gender = "Member Gender"
                  ,birthyear = "05 - Member Details Member Birthday Year"
)

Now we can stack the four data frames into one all-encompassing data frame called all_trips, which spans the entirety of 2019:

all_trips <- bind_rows(q1_2019, q2_2019, q3_2019, q4_2019)

We’ll take a look at the new data frame, to make sure the columns and data types are as they should be:

str(all_trips)
## spc_tbl_ [3,818,004 x 12] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
##  $ trip_id          : num [1:3818004] 21742443 21742444 21742445 21742446 21742447 ...
##  $ start_time       : POSIXct[1:3818004], format: "2019-01-01 00:04:37" "2019-01-01 00:08:13" ...
##  $ end_time         : POSIXct[1:3818004], format: "2019-01-01 00:11:07" "2019-01-01 00:15:34" ...
##  $ bikeid           : num [1:3818004] 2167 4386 1524 252 1170 ...
##  $ tripduration     : num [1:3818004] 390 441 829 1783 364 ...
##  $ from_station_id  : num [1:3818004] 199 44 15 123 173 98 98 211 150 268 ...
##  $ from_station_name: chr [1:3818004] "Wabash Ave & Grand Ave" "State St & Randolph St" "Racine Ave & 18th St" "California Ave & Milwaukee Ave" ...
##  $ to_station_id    : num [1:3818004] 84 624 644 176 35 49 49 142 148 141 ...
##  $ to_station_name  : chr [1:3818004] "Milwaukee Ave & Grand Ave" "Dearborn St & Van Buren St (*)" "Western Ave & Fillmore St (*)" "Clark St & Elm St" ...
##  $ usertype         : chr [1:3818004] "Subscriber" "Subscriber" "Subscriber" "Subscriber" ...
##  $ gender           : chr [1:3818004] "Male" "Female" "Female" "Male" ...
##  $ birthyear        : num [1:3818004] 1989 1990 1994 1993 1994 ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   trip_id = col_double(),
##   ..   start_time = col_datetime(format = ""),
##   ..   end_time = col_datetime(format = ""),
##   ..   bikeid = col_double(),
##   ..   tripduration = col_number(),
##   ..   from_station_id = col_double(),
##   ..   from_station_name = col_character(),
##   ..   to_station_id = col_double(),
##   ..   to_station_name = col_character(),
##   ..   usertype = col_character(),
##   ..   gender = col_character(),
##   ..   birthyear = col_double()
##   .. )
##  - attr(*, "problems")=<externalptr>

The structure is now verified. As our analysis revolves around identifying the differences between the subscriber and casual user types, we’ll ensure there are only two types of observation contained in the usertype column:

unique(all_trips$usertype)
## [1] "Subscriber" "Customer"

There are indeed only two, but casual users are referred to as “Customer” in the data frame, which is too general of a term for this analysis. To avoid ambiguity, we’ll change every observation of “Customer” to “Casual”:

all_trips$usertype <- str_replace(all_trips$usertype,"Customer","Casual")
unique(all_trips$usertype)
## [1] "Subscriber" "Casual"

The all_trips data frame is now ready for analysis.

Analyzing the Data

We’ll begin by looking at the percentage of total rides in 2019 which were taken by subscribers, and the percentage taken by casuals:

number_of_subscribers <- nrow(filter(all_trips, usertype == "Subscriber"))
number_of_casuals <- nrow(filter(all_trips, usertype == "Casual"))
total_number_trips <- nrow(all_trips)
perc_subscribers <- round(number_of_subscribers/total_number_trips * 100, 2)
perc_casuals <- round(number_of_casuals/total_number_trips * 100, 2)
print(perc_subscribers)
## [1] 76.93
print(perc_casuals)
## [1] 23.07

The average ride time (in seconds) taken by each user type:

all_trips %>%
  group_by(usertype) %>% 
  summarise(average_duration = mean(tripduration))
## # A tibble: 2 x 2
##   usertype   average_duration
##   <chr>                 <dbl>
## 1 Casual                3421.
## 2 Subscriber             859.

The total number of rides for each user type by day of the week:

all_trips %>% 
  mutate(weekday = wday(start_time, label = TRUE)) %>% 
  group_by(usertype, weekday) %>% 
  summarise(number_of_rides = n())
## # A tibble: 14 x 3
## # Groups:   usertype [2]
##    usertype   weekday number_of_rides
##    <chr>      <ord>             <int>
##  1 Casual     Sun              170179
##  2 Casual     Mon              101489
##  3 Casual     Tue               88655
##  4 Casual     Wed               89745
##  5 Casual     Thu              101372
##  6 Casual     Fri              121141
##  7 Casual     Sat              208056
##  8 Subscriber Sun              256241
##  9 Subscriber Mon              458780
## 10 Subscriber Tue              497025
## 11 Subscriber Wed              494277
## 12 Subscriber Thu              486915
## 13 Subscriber Fri              456966
## 14 Subscriber Sat              287163

The average ride duration by user type and day of the week:

all_trips %>% 
  mutate(weekday = wday(start_time, label = TRUE)) %>% 
  group_by(usertype, weekday) %>% 
  summarise(average_duration = mean(tripduration))
## # A tibble: 14 x 3
## # Groups:   usertype [2]
##    usertype   weekday average_duration
##    <chr>      <ord>              <dbl>
##  1 Casual     Sun                3371.
##  2 Casual     Mon                3270.
##  3 Casual     Tue                3445.
##  4 Casual     Wed                3620.
##  5 Casual     Thu                3597.
##  6 Casual     Fri                3610.
##  7 Casual     Sat                3244.
##  8 Subscriber Sun                 924.
##  9 Subscriber Mon                 855.
## 10 Subscriber Tue                 849.
## 11 Subscriber Wed                 828.
## 12 Subscriber Thu                 827.
## 13 Subscriber Fri                 834.
## 14 Subscriber Sat                 978.

The number of trips by user type and month. For this we’ll create a new column called cyc_date:

all_trips$cyc_date <- as.Date(all_trips$start_time)

all_trips %>%
  mutate(cyc_month = format(cyc_date, "%m %b")) %>%
  group_by(cyc_month, usertype) %>% 
  summarise(number_of_rides = n()) %>%
  print(n = 24)
## # A tibble: 24 x 3
## # Groups:   cyc_month [12]
##    cyc_month usertype   number_of_rides
##    <chr>     <chr>                <int>
##  1 01 Jan    Casual                4602
##  2 01 Jan    Subscriber           98670
##  3 02 Feb    Casual                2638
##  4 02 Feb    Subscriber           93548
##  5 03 Mar    Casual               15923
##  6 03 Mar    Subscriber          149688
##  7 04 Apr    Casual               47744
##  8 04 Apr    Subscriber          217566
##  9 05 May    Casual               81624
## 10 05 May    Subscriber          285834
## 11 06 Jun    Casual              130218
## 12 06 Jun    Subscriber          345177
## 13 07 Jul    Casual              175632
## 14 07 Jul    Subscriber          381683
## 15 08 Aug    Casual              186889
## 16 08 Aug    Subscriber          403295
## 17 09 Sep    Casual              129173
## 18 09 Sep    Subscriber          364046
## 19 10 Oct    Casual               71035
## 20 10 Oct    Subscriber          300751
## 21 11 Nov    Casual               18729
## 22 11 Nov    Subscriber          158447
## 23 12 Dec    Casual               16430
## 24 12 Dec    Subscriber          138662

The proportion of male, female, and N/A among subscribers:

number_of_maleSUB <- nrow(filter(all_trips, gender == "Male" & usertype == "Subscriber"))
number_of_femaleSUB <- nrow(filter(all_trips, gender == "Female" & usertype == "Subscriber"))
number_of_NAgenderSUB <- number_of_subscribers - (number_of_maleSUB + number_of_femaleSUB)

print(perc_maleSUB <- round(number_of_maleSUB/number_of_subscribers * 100, 2))
## [1] 74.49
print(perc_femaleSUB <- round(number_of_femaleSUB/number_of_subscribers * 100, 2))
## [1] 24.73
print(perc_NAgenderSUB <- round(number_of_NAgenderSUB/number_of_subscribers * 100, 2))
## [1] 0.77

The proportion of male, female, and N/A among casuals:

number_of_maleCAS <- nrow(filter(all_trips, gender == "Male" & usertype == "Casual"))
number_of_femaleCAS <- nrow(filter(all_trips, gender == "Female" & usertype == "Casual"))
number_of_NAgenderCAS <- number_of_casuals - (number_of_maleCAS + number_of_femaleCAS)

print(perc_maleCAS <- round(number_of_maleCAS/number_of_casuals * 100, 2))
## [1] 24.16
print(perc_femaleCAS <- round(number_of_femaleCAS/number_of_casuals * 100, 2))
## [1] 14.93
print(perc_NAgenderCAS <- round(number_of_NAgenderCAS/number_of_casuals * 100, 2))
## [1] 60.92

Breaking down the time of day into ‘morning’, ‘afternoon’ and ‘night’, and counting the number of rides during each time of day by user type:

all_trips$time_hour <- format(as.POSIXct(all_trips$start_time), format = "%H")
all_trips$time_hour <- as.numeric(all_trips$time_hour)

all_trips$time_category <- ifelse(all_trips$time_hour >= 00 & all_trips$time_hour <= 04, "Night",
                                  ifelse(all_trips$time_hour > 04 & all_trips$time_hour <= 11, "Morning",
                                         ifelse(all_trips$time_hour > 11 & all_trips$time_hour <= 17, "Afternoon", "Night")))

all_trips %>% 
  mutate(time_category_ordered = factor(time_category, levels=c("Morning", "Afternoon", "Night"))) %>%
  group_by(usertype, time_category_ordered) %>% 
  summarise(number_of_rides = n())
## # A tibble: 6 x 3
## # Groups:   usertype [2]
##   usertype   time_category_ordered number_of_rides
##   <chr>      <fct>                           <int>
## 1 Casual     Morning                        176871
## 2 Casual     Afternoon                      470699
## 3 Casual     Night                          233067
## 4 Subscriber Morning                        999882
## 5 Subscriber Afternoon                     1243450
## 6 Subscriber Night                          694035

Finding the top 10 most popular departure stations for subscribers:

all_trips %>%
  filter(usertype == "Subscriber") %>%
  group_by(from_station_name) %>% 
  summarise(number_of_rides = n()) %>%
  arrange(desc(number_of_rides)) %>%
  top_n(10, number_of_rides)
## # A tibble: 10 x 2
##    from_station_name            number_of_rides
##    <chr>                                  <int>
##  1 Canal St & Adams St                    50575
##  2 Clinton St & Madison St                45990
##  3 Clinton St & Washington Blvd           45378
##  4 Columbus Dr & Randolph St              31370
##  5 Franklin St & Monroe St                30832
##  6 Kingsbury St & Kinzie St               30654
##  7 Daley Center Plaza                     30423
##  8 Canal St & Madison St                  27138
##  9 Michigan Ave & Washington St           25468
## 10 LaSalle St & Jackson Blvd              23021

And the 10 most popular departure stations for casuals:

all_trips %>%
  filter(usertype == "Casual") %>%
  group_by(from_station_name) %>% 
  summarise(number_of_rides = n()) %>%
  arrange(desc(number_of_rides)) %>%
  top_n(10, number_of_rides)
## # A tibble: 10 x 2
##    from_station_name            number_of_rides
##    <chr>                                  <int>
##  1 Streeter Dr & Grand Ave                53104
##  2 Lake Shore Dr & Monroe St              39238
##  3 Millennium Park                        21749
##  4 Michigan Ave & Oak St                  21388
##  5 Shedd Aquarium                         20617
##  6 Lake Shore Dr & North Blvd             18952
##  7 Theater on the Lake                    15027
##  8 Dusable Harbor                         12546
##  9 Michigan Ave & Washington St           12228
## 10 Adler Planetarium                      11928

We’ll find the average age of subscribers and casuals, while replacing any birth date prior to 1919 with ‘NA’:

all_trips$birthyear <- replace(all_trips$birthyear, all_trips$birthyear<1919, NA)

all_trips %>% 
  mutate(age_member = (2019 - birthyear)) %>% 
  group_by(usertype) %>%
  summarise(average_age = round(mean(age_member, na.rm = TRUE), 0))
## # A tibble: 2 x 2
##   usertype   average_age
##   <chr>            <dbl>
## 1 Casual              31
## 2 Subscriber          35

Visualizing the Data

I’ll present visualizations of all the previous analyses, which will enable us to begin drawing some conclusions about the data. I’ll offer a finding after each visualization.

Firstly, the proportion of total rides taken by subscribers and casuals:

number_of_subscribers <- nrow(filter(all_trips, usertype == "Subscriber"))
number_of_casuals <- nrow(filter(all_trips, usertype == "Casual"))
total_number_trips <- nrow(all_trips)
ratio_members <- c(number_of_casuals, number_of_subscribers)
piece_color <- c("#33CC99", "#3399FF", "#FF3333")

perc_subscribers <- paste0(round(number_of_subscribers/total_number_trips * 100, 2), "%", " Subscriber")
perc_casuals <- paste0(round(number_of_casuals/total_number_trips * 100, 2), "%", " Casual")
ratio_perc <- (c(perc_casuals, perc_subscribers))

cyc_pie <- pie3D(ratio_members, main = "Proportion of Trips by Customer Type in 2019", border = "white", col = piece_color,
                 explode = 0.08, col.main = "deepskyblue4")
pie3D.labels(cyc_pie, labels = ratio_perc, theta = 0.8)

Susbscribers make up the majority of the Cyclistic rides, a little over three quarters. But the casual users still account for a significant amount of the rides, proving it would be economically beneficial to motivate those casuals, who are already familiar with the service, to subscribe.

We’ll take a look at the average ride times of subscribers and casuals:

all_trips %>%
  group_by(usertype) %>% 
  summarise(average_duration = mean(tripduration)) %>%
  ggplot(aes(x = usertype, y = average_duration, fill = usertype)) +
  geom_col(position = "dodge", colour="black") +
  scale_fill_manual(values = c("#33CC99","#3399FF")) +
  theme(panel.background = element_rect(fill = "#9999CC", colour = "NA",
                                        size = 1, linetype = "blank")) +
  labs(title = "Average Ride Durations in 2019", fontface = "bold") +
  xlab("Customer Type") +
  ylab("Average Ride Duration in Seconds") +
  theme(plot.title=element_text(face="bold",hjust = 0.5)) +
  theme(legend.title=element_blank())

The average ride duration for casual users is quite a bit longer than that of the subscribers. It can be inferred that the casuals are either traveling further, or traveling less directly. Subsequent visualizations might help us to hone in on this.

Let’s see how the number of trips for the two user types differs by day of the week:

all_trips %>% 
  mutate(weekday = wday(start_time, label = TRUE)) %>% 
  group_by(usertype, weekday) %>% 
  summarise(number_of_rides = n()
            ,average_duration = mean(tripduration)) %>% 
  arrange(usertype, weekday)  %>% 
  ggplot(aes(x = weekday, y = number_of_rides, fill = usertype)) +
  geom_col(position = "dodge") +
  scale_fill_manual(values = c("#33CC99","#3399FF")) +
  theme(panel.background = element_rect(fill = "darkslategrey", colour = "black",
                                        size = 1, linetype = "blank")) +
  labs(title = "Total Rides by Day of Week and Customer Type", fontface = "bold") +
  theme(plot.title=element_text(face="bold",hjust = 0.5)) +
  theme(legend.title=element_blank()) +
  xlab("Day of the Week") +
  ylab("Number of Rides")

An interesting pattern is identifiable from this visual. The subscriber usage is at its maximum on the weekdays, with a significant decline on weekends. The inverse is true for casual users, with a higher usage on weekends than on weekdays. This helps to illuminate the previous visual as well, as it now appears casual users are using Cyclistic bikes more for recreational and social use on weekends, while subscribers are quite likely using them for getting to work. It would help explain the longer ride durations for casuals, as it appears their usage is more for indirect, recreational rides. Subscribers may more commonly be riding directly to and from work. This will need to be clarified with more analysis.

This is the average ride duration for the two user types by day of the week:

all_trips %>% 
  mutate(weekday = wday(start_time, label = TRUE)) %>% 
  group_by(usertype, weekday) %>% 
  summarise(number_of_rides = n()
            ,average_duration = mean(tripduration)) %>% 
  arrange(usertype, weekday)  %>% 
  ggplot(aes(x = weekday, y = average_duration, fill = usertype)) +
  geom_col(position = "dodge") +
  scale_fill_manual(values = c("#33CC99","#3399FF")) +
  theme(panel.background = element_rect(fill = "blue4", colour = "black",
                                        size = 1, linetype = "blank")) +
  labs(title = "Average Ride Duration by Day of Week and Customer Type", fontface = "bold") +
  theme(plot.title=element_text(face="bold",hjust = 0.5)) +
  theme(legend.title=element_blank()) +
  xlab("Day of the Week") +
  ylab("Average Ride Duration in Seconds")

The average ride times are quite steady throughout the week for the two groups, with a slight increase in weekdays for subscribers, and a slight increase on weekends for the casual users.

We’ll now turn our attention to the amount of rides taken by the two groups at different times of the year:

all_trips$cyc_date <- as.Date(all_trips$start_time)

all_trips %>% 
  group_by(cyc_date, usertype) %>% 
  summarise(number_of_rides = n()) %>%
  ggplot(aes(x = cyc_date, y = number_of_rides, color = usertype)) +
  geom_smooth(fill = "red", se = TRUE, level = 0.95, alpha = 0.04) +
  scale_color_manual(values = c("#33CC99","#3399FF")) +
  theme(panel.background = element_rect(fill = "azure2", colour = "black",
                                        size = 1, linetype = "blank")) +
  scale_x_date(date_breaks = "1 months",
               date_labels = "%b\n") +
  labs(title = "Number of Rides by Month and Customer Type", fontface = "bold") +
  theme(plot.title=element_text(face="bold",hjust = 0.5)) +
  theme(legend.title=element_blank()) +
  xlab("Date") +
  ylab("Number of Rides")

The movement of the trend line of the casuals maps on pretty well to that of the subscribers, both lines rising and falling at around the same times. It shows an increase in usage in the warmer months of June, July, August and September, and a decline in the colder months. Even if the subscribers are using the bikes for a work commute, it stands to reason they’d be less inclined to use the bikes for that purpose when the weather is unfavourable, and they’d find another means of getting to work during those months.

Looking now at the gender breakdown among the subscribers and casuals, with ‘N/A’ standing in for when gender hasn’t been specified by the customer:

#Variables for subscriber:
number_of_maleSUB <- nrow(filter(all_trips, gender == "Male" & usertype == "Subscriber"))
number_of_femaleSUB <- nrow(filter(all_trips, gender == "Female" & usertype == "Subscriber"))
number_of_NAgenderSUB <- number_of_subscribers - (number_of_maleSUB + number_of_femaleSUB)

ratio_genderSUB <- c(number_of_femaleSUB, number_of_NAgenderSUB, number_of_maleSUB)
perc_maleSUB <- paste0(round(number_of_maleSUB/number_of_subscribers * 100, 2), "%", " Male")
perc_femaleSUB <- paste0(round(number_of_femaleSUB/number_of_subscribers * 100, 2), "%", " Female")
perc_NAgenderSUB <- paste0(round(number_of_NAgenderSUB/number_of_subscribers * 100, 2), "%", " N/A")
ratio_perc_genderSUB <- (c(perc_femaleSUB, perc_NAgenderSUB, perc_maleSUB))

piece_color2 <- c("#33FF99", "#CCFFFF", "#FFFF33")

#Variables for casual:
number_of_maleCAS <- nrow(filter(all_trips, gender == "Male" & usertype == "Casual"))
number_of_femaleCAS <- nrow(filter(all_trips, gender == "Female" & usertype == "Casual"))
number_of_NAgenderCAS <- number_of_casuals - (number_of_maleCAS + number_of_femaleCAS)

ratio_genderCAS <- c(number_of_NAgenderCAS, number_of_femaleCAS, number_of_maleCAS)
perc_maleCAS <- paste0(round(number_of_maleCAS/number_of_casuals * 100, 2), "%", " Male")
perc_femaleCAS <- paste0(round(number_of_femaleCAS/number_of_casuals * 100, 2), "%", " Female")
perc_NAgenderCAS <- paste0(round(number_of_NAgenderCAS/number_of_casuals * 100, 2), "%", " N/A")
ratio_perc_genderCAS <- (c(perc_NAgenderCAS, perc_femaleCAS, perc_maleCAS))

piece_color3 <- c("lightslateblue", "cyan4", "indianred1" )

#combine the two pie charts using par:
par(mfrow=c(1,2))   

cyc_pie <- pie3D(ratio_genderSUB, main = "Subscriber", border = "white",
                 col.main = "#FF6633", col = piece_color2) #explode = 0.01)
pie3D.labels(cyc_pie, labels = ratio_perc_genderSUB, theta = 0.8, labelcex = 1.2)

cyc_pie <- pie3D(ratio_genderCAS, main = "Casual", border = "white",
                 col.main = "blue", col = piece_color3) #explode = 0.01)
pie3D.labels(cyc_pie, labels = ratio_perc_genderCAS, theta = 0.8, labelcex = 1.2)

From this combined visual, we can see that among the susbscribers, males account for about three quaters of the group, females the remaining quarter, and only a tiny sliver are N/A. With the casuals, a very large contingent, well over half, are N/A, so it may be hard to draw too many conclusions about trends. But from those who have specified gender, there is a much more even split between the males and females in the casual group. This certainly indicates, with females under-represented in the subscriber group, that it would be beneficial to motivate females as much as males in the casual group to subscribe.

We’ll investigate the number of rides for the two groups according to the time of day, by splitting the day into ‘morning’, ‘afternoon’ and ‘night’:

all_trips$time_hour <- format(as.POSIXct(all_trips$start_time), format = "%H")
all_trips$time_hour <- as.numeric(all_trips$time_hour)

all_trips$time_category <- ifelse(all_trips$time_hour >= 00 & all_trips$time_hour <= 04, "Night",
                                  ifelse(all_trips$time_hour > 04 & all_trips$time_hour <= 11, "Morning",
                                         ifelse(all_trips$time_hour > 11 & all_trips$time_hour <= 17, "Afternoon", "Night")))

all_trips %>% 
  mutate(time_category_ordered = factor(time_category, levels=c("Morning", "Afternoon", "Night"))) %>%
  group_by(usertype, time_category_ordered) %>% 
  summarise(number_of_rides = n()) %>%
  ggplot(aes(x = time_category_ordered, y = number_of_rides, fill = usertype)) +
  geom_col(position = "dodge") +
  scale_fill_manual(values = c("#33CC99","#3399FF")) +
  theme(panel.background = element_rect(fill = "orangered", colour = "black",
                                        size = 1, linetype = "blank")) +
  labs(title = "Total Rides by Time of Day and Customer Type", fontface = "bold") +
  theme(plot.title=element_text(face="bold",hjust = 0.5)) +
  theme(legend.title=element_blank()) +
  xlab("Time of Day") +
  ylab("Number of Rides")

According to this visualization, both groups have the highest usage in the afternoon. But the contrast between the groups exists in the other time periods, with the lowest usage for subscribers being at night, and the lowest for casuals being in the morning. This is consistent with the premise that casual users are using the bikes more for recreational purposes than the subscribers, with recreational and social activities often occurring in the afternoon and night.

Now we’ll investigate what the 10 most popular stations of departure are for the subscribers and casual users:

#top 10 stations for Subscriber users
subscriber_station_plot <- all_trips %>%
  filter(usertype == "Subscriber") %>%
  group_by(from_station_name) %>% 
  summarise(number_of_rides = n()) %>%
  top_n(10, number_of_rides) %>%
  ggplot(aes(fill = from_station_name, x = number_of_rides, y = reorder(from_station_name, number_of_rides))) +
  geom_col(position = "dodge", show.legend = FALSE) +
  theme(panel.background = element_rect(fill = "powderblue", colour = "black",
                                        size = 1, linetype = "blank")) +
  labs(title = "Most Popular Departure Stations for Subscribers", fontface = "bold") +
  theme(plot.title=element_text(face="bold",hjust = 0.5)) +
  theme(legend.title=element_blank()) +
  xlab("Number of Rides") +
  ylab("Station")

#top 10 stations for Casual users
casual_station_plot <- all_trips %>%
  filter(usertype == "Casual") %>%
  group_by(from_station_name) %>% 
  summarise(number_of_rides = n()) %>%
  top_n(10, number_of_rides) %>%
  ggplot(aes(fill = from_station_name, x = number_of_rides, y = reorder(from_station_name, number_of_rides))) +
  geom_col(position = "dodge", show.legend = FALSE) +
  theme(panel.background = element_rect(fill = "aquamarine", colour = "black",
                                        size = 1, linetype = "blank")) +
  labs(title = "Most Popular Departure Stations for Casual Customers", fontface = "bold") +
  theme(plot.title=element_text(face="bold",hjust = 0.5)) +
  theme(legend.title=element_blank()) +
  xlab("Number of Rides") +
  ylab("Station")

#trying out grid.arrange
grid.arrange(subscriber_station_plot, casual_station_plot, ncol=1)

The top 10 lists of stations reveal quite a divergence in the stations that subscribers and casuals commonly depart from, with only one station in common between the two lists. The list of popular casual stations provides potential locations for extra marketing material promoting the benefits of subscription.

We can now examine the average age of the two user groups taking Cyclistic rides in 2019:

all_trips$birthyear <- replace(all_trips$birthyear, all_trips$birthyear<1919, NA)

all_trips %>% 
  mutate(age_member = (2019 - birthyear)) %>% 
  group_by(usertype) %>%
  summarise(average_age = round(mean(age_member, na.rm = TRUE), 0)) %>%
  ggplot(aes(x = usertype, y = average_age, fill = usertype)) +
  geom_col(position = "dodge") +
  geom_text(aes(label = paste0(average_age, " years old")), position=position_dodge(width=0.9), fontface = "bold", vjust=-0.25)+
  scale_fill_manual(values = c("#33CC99","#3399FF")) +
  theme(panel.background = element_rect(fill = "snow3", colour = "black",
                                        size = 1, linetype = "blank")) +
  labs(title = "Average Age of Customers", fontface = "bold") +
  theme(plot.title=element_text(face="bold",hjust = 0.5)) +
  theme(legend.title=element_blank()) +
  xlab("Customer Type") +
  ylab("Average Age")

The average age of the casual group veers slightly younger than the subscriber group. This also provides a focus for a marketing strategy.

Recommendations

Now that the data has been analyzed and visualized, a picture has formed showing the distinctions between the two main categories of users of Cyclistic bikes. Those distinctions will inform a marketing strategy designed to encourage casual users to subscribe to the Cyclistic service. Here are the three main recommendations, based on the current data:

Strategy 1

Firstly, several of the facets of the analysis gave a strong indication that the casual user base, more so than the current subscribers, are using Cyclistic bikes for social and recreational purposes. To incentivize them to subscribe, an emphasis could be made on ensuring subscription is still amenable to irregular or sporadic use, that subscribers will accumulate rewards points even if they don’t use the bikes every day. It will still be worth a yearly subscription to them even if they use the bikes once every couple of weeks, or at irregular intervals. They may also receive rewards for recommending subscription to friends who will also be using the bikes recreationally.

Promotional materials can really emphasise the freedom and ease of using a Cyclistic bike for sight-seeing around Chicago, visiting friends’ places for gatherings, attending concerts and public events of any kind. And how as a subscriber using the bikes in that way is even easier, and can yield the benefit of discounts on various products, which could be tied into Cyclistic through cross-promotion. This would include many of the kinds of products that could be sold at the public events they’ll be attending.

Strategy 2

Females are under-represented in the subscriber user base, while the casual users have a much more even split among males and females. This indicates that females are using the bikes but not subscribing at the same rate, and so they should be featured more prominently in the promotional materials, and the rewards offered by subscription should appeal just as much to females as they do to males.

The casual user base appears to be about 4 years younger on average than the subscribers. Not a massive difference, but marketing should reflect the slightly younger demographic that it is being aimed at. An emphasis could be made on the environmental benefits of taking bikes over other forms of transport, as younger generations tend to be more environmentally conscious.

Strategy 3

The top 10 list of departure stations for subscribers is almost completely different from the top 10 for casuals. The two groups are favouring different parts of town. Marketing materials could be ramped up in the vicinity of the 10 top stations for casuals, be it in the form of posters, brochures, or business cards placed in those areas, or in the form of online social media content tied to Cyclistic which features some of those familiar locations in some way. The marketing materials in those locations will espouse the benefits of subscription, how it’s more convenient than getting single-ride passes when the customers are out and about.

Conclusion

Thank you for taking the time to read through this report. I hope the analysis has helped provide some insight into the ways in which Cyclistic’s main customer groups differ, and how a marketing strategy may be tailored to appeal to the idiosyncracies and preferences of the casual user base of Cyclistic bikes.