← Back to list

Case Study-Design Cyclistic’s Marketing Strategy

The cleaned Dataset to the Case Study is available below: https://www.kaggle.com/datasets/julianachaudhari/case-study-cyclistic

Juliana Chaudhari · 2026-03-06 12:57 · 0 claps · 25.5 min read
#google-data-analytics #tableau #data-analysis #rstudio #data-driven-decisions
Open on Medium ↗
Wiki topics: ML · Machine Learning ECO · Economy · General DIG · Digital Marketing GRW · Growth & Analytics

Case Study-Design Cyclistic’s Marketing Strategy

The cleaned Dataset to the Case Study is available below: https://www.kaggle.com/datasets/julianachaudhari/case-study-cyclistic

How does a bike-share speedy success

Background Cyclistic is focusing on success depending on maximizing annual bike subscriptions. The company decides to design a marketing strategy on conversion of casual riders to annual riders. The data analytics team is given the task to analyse how do annual members and casual riders use Cyclistic bikes differently. This will lead to Cyclistic bikes Executive team and Marketing Director making data driven decision for increased subscriptions of annual riders and increased profitability. Study Target Audience — Two wheeler bikers both leisure and and people riding to work. People with disabilities and riding 3-wheeler bikes. In this case study, we will be using the Google analytics Data analysis phases of Ask, Prepare, Process, Analyze, Share and Act

Ask phase

PROBLEM DEFINITION The company has asked the following below:

  1. How do annual members and casual riders use Cyclistic bikes differently?
  2. Why would casual riders buy Cyclistic annual memberships?
  3. How can Cyclistic use digital media to influence casual riders to become members?

Based on the above given information we have to analyse and report the following: 1. Analyse causes for casual riders to become Cyclistic annual members 2. Find out which location or trip stations can be targeted to increase the sales. 3. What can be inputs to design advertising strategy to increase annual riders.

Key Stakeholders -Marketing Director, Data Analytics team and Executive team. Bike riders are the focus audience and end user customer of Cyclistic.

DATA AND METRICS

Cyclistic have shared the historical trip data of 2019 and 2020 of bikers. The metrics will help analyze and identify trends of the trip stations, bike members etc. This analysis will help the executive team to make data driven marketing strategy decisions to increase annual subscriptions and future success.

Prepare phase The data has been made available by ( Motivate International Inc. under this license.) This is public data that will be used to explore how different customer types are using Cyclistic bikes. Data security and privacy issues have been addressed so we can’t use riders’ personally identifiable information. This means that you won’t be able to connect pass purchases to credit card numbers to determine if casual riders live in the Cyclistic service area or if they have purchased multiple single passes.

The data is reliable and original. Data used for analysis is of the year 2019 and 2020 which is not current and analysis results may vary. However, the results may vary, but can be best prepared with data available.

We have collected data, downloaded and organized in .CSV files and organized below

CSV file structure of the data

CSV file structure of the data

Process phase The following tools is used for the analysis: R programming through RStudio on cloud (Posit) is used as the data is huge and has integrated development design to collaborate data. We will be using Tableau for visualizations. STEP 1. After we import the data, we use the library below to analyse, clean and aggregate data.

# Load Packages
library(tidyverse)
library(lubridate) 
library(tidyr)
library(skimr)
library(janitor)
library(data.table)
library(ggplot2)
library(tidygeocoder)

> install.packages (“tidyverse”)
── Attaching core tidyverse packages ──────────────────────────── tidyverse 2.0.0 ──
✔ forcats   1.0.1     ✔ readr     2.1.6
✔ ggplot2   4.0.1     ✔ stringr   1.6.0
✔ lubridate 1.9.4     ✔ tibble    3.3.0
✔ purrr     1.2.0     ✔ tidyr     1.3.1
── Conflicts ────────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package to force all conflicts to become errors
> detach("package:tidyverse", unload = TRUE)
> library(tidyverse)
── Conflicts ────────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package to force all conflicts to become errors
Restarting R session...> install.packages("lubridate")
Installing package into ‘/cloud/lib/x86_64-pc-linux-gnu-library/4.5’
(as ‘lib’ is unspecified)
trying URL 'http://rspm/default/__linux__/focal/latest/src/contrib/lubridate_1.9.4.tar.gz'
Content type 'application/x-gzip' length 990322 bytes (967 KB)
==================================================
downloaded 967 KB
* installing *binary* package ‘lubridate’ ...
* DONE (lubridate)
The downloaded source packages are in
 ‘/tmp/Rtmpz7iaoA/downloaded_packages’

> install.packages("janitor")
Installing package into ‘/cloud/lib/x86_64-pc-linux-gnu-library/4.5’
(as ‘lib’ is unspecified)trying URL 'http://rspm/default/__linux__/focal/latest/src/contrib/janitor_2.2.1.tar.gz'
Content type 'application/x-gzip' length 285327 bytes (278 KB)
==================================================
downloaded 278 KB
* installing *binary* package ‘janitor’ ...
* DONE (janitor)
The downloaded source packages are in
 ‘/tmp/Rtmpz7iaoA/downloaded_packages’
> library(janitor)
Attaching package: ‘janitor’
The following objects are masked from ‘package:stats’:
    chisq.test, fisher.

Now packages are loaded, we will aggregate all the data in our Divvy Trips folder. 
We first import the data in Posit cloud.

Global Environment files in R

Global Environment files in R

STEP 2-In this phase, we wrangle the data and combine it into one file. Using the following function gives the column names of the table Divvy_Trips_2019_Q1 which shows the table specification.

1.
#Data check
Colnames(Divvy_Trips_2019_Q1)
Colnames(Divvy_Trips_2020_Q1)

[1] "trip_id"          [2]"start_time"      [3]"end_time"      [4] "bikeid"           
 [5] "tripduration"    [6] "from_station_id"  [7]"from_station_name" [8]"to_station_id"    
 [9] "to_station_name"   [10]"usertype"         [11] "gender"           [12] "birthyear" 

[1] "ride_id"            "rideable_type"      "started_at"        
[4] "ended_at"           "start_station_name" "start_station_id"  
 [7] "end_station_name"   "end_station_id"     "start_lat"         
[10] "start_lng"          "end_lat"            "end_lng"           
[13] "member_casual"     
#While the names are not in the same order, they DO need to match perfectly before we can use a command to join them into one file
2. As the columns names in both datasets match in our next step we will 
rename the column names in 2019 Q1 dataset even though the order of the 
column names do not match.

# Rename columns  to make them consistent with q1_2020 (as this will be the supposed going-forward table design for Divvy)
(Divvy_Trips_2019_Q1 <- rename(Divvy_Trips_2019_Q1
                   ,ride_id = trip_id
                   ,rideable_type = bikeid
                   ,started_at = start_time
                   ,ended_at = end_time
                   ,start_station_name = from_station_name
                   ,start_station_id = from_station_id
                   ,end_station_name = to_station_name
                   ,end_station_id = to_station_id
                   ,member_casual = usertype
                   ))

# A tibble: 365,069 × 12
    ride_id started_at         ended_at  rideable_type tripduration start_station_id
      <dbl> <chr>              <chr>             <dbl>        <dbl>            <dbl>
 1 21742443 2019-01-01 0:04:37 2019-01-…          2167          390              199
 2 21742444 2019-01-01 0:08:13 2019-01-…          4386          441               44
 3 21742445 2019-01-01 0:13:23 2019-01-…          1524          829               15
 4 21742446 2019-01-01 0:13:45 2019-01-…           252         1783              123
 5 21742447 2019-01-01 0:14:52 2019-01-…          1170          364              173
 6 21742448 2019-01-01 0:15:33 2019-01-…          2437          216               98
 7 21742449 2019-01-01 0:16:06 2019-01-…          2708          177               98
 8 21742450 2019-01-01 0:18:41 2019-01-…          2796          100              211
 9 21742451 2019-01-01 0:18:43 2019-01-…          6205         1727              150
10 21742452 2019-01-01 0:19:18 2019-01-…          3939          336              268
# ℹ 365,059 more rows
# ℹ 6 more variables: start_station_name <chr>, end_station_id <dbl>,
#   end_station_name <chr>, member_casual <chr>, gender <chr>, birthyear <dbl>
# ℹ Use `print(n = ...)` to see more rows
> print(x=365069)
[1] 365069
3.
# Inspect the dataframes and look for incongruencies
str(Divvy_Trips_2019_Q1)
str(Divvy_Trips_2020_Q1)
>   str(Divvy_Trips_2019_Q1) 
spc_tbl_ [365,069 × 12] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
 $ ride_id           : num [1:365069] 21742443 21742444 21742445 21742446 21742447 ...
 $ started_at        : chr [1:365069] "2019-01-01 0:04:37" "2019-01-01 0:08:13" "2019-01-01 0:13:23" "2019-01-01 0:13:45" ...
 $ ended_at          : chr [1:365069] "2019-01-01 0:11:07" "2019-01-01 0:15:34" "2019-01-01 0:27:12" "2019-01-01 0:43:28" ...
 $ rideable_type     : num [1:365069] 2167 4386 1524 252 1170 ...
 $ tripduration      : num [1:365069] 390 441 829 1783 364 ...
 $ start_station_id  : num [1:365069] 199 44 15 123 173 98 98 211 150 268 ...
 $ start_station_name: chr [1:365069] "Wabash Ave & Grand Ave" "State St & Randolph St" "Racine Ave & 18th St" "California Ave & Milwaukee Ave" ...
 $ end_station_id    : num [1:365069] 84 624 644 176 35 49 49 142 148 141 ...
 $ end_station_name  : chr [1:365069] "Milwaukee Ave & Grand Ave" "Dearborn St & Van Buren St (*)" "Western Ave & Fillmore St (*)" "Clark St & Elm St" ...
 $ member_casual     : chr [1:365069] "Subscriber" "Subscriber" "Subscriber" "Subscriber" ...
 $ gender            : chr [1:365069] "Male" "Female" "Female" "Male" ...
 $ birthyear         : num [1:365069] 1989 1990 1994 1993 1994 ...
 - attr(*, "spec")=
  .. cols(
  ..   trip_id = col_double(),
  ..   start_time = col_character(),
  ..   end_time = col_character(),
  ..   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> 

>   str(Divvy_Trips_2020_Q1) 
spc_tbl_ [426,887 × 13] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
 $ ride_id           : chr [1:426887] "EACB19130B0CDA4A" "8FED874C809DC021" "789F3C21E472CA96" "C9A388DAC6ABF313" ...
 $ rideable_type     : chr [1:426887] "docked_bike" "docked_bike" "docked_bike" "docked_bike" ...
 $ started_at        : chr [1:426887] "2020-01-21 20:06:59" "2020-01-30 14:22:39" "2020-01-09 19:29:26" "2020-01-06 16:17:07" ...
 $ ended_at          : chr [1:426887] "2020-01-21 20:14:30" "2020-01-30 14:26:22" "2020-01-09 19:32:17" "2020-01-06 16:25:56" ...
 $ start_station_name: chr [1:426887] "Western Ave & Leland Ave" "Clark St & Montrose Ave" "Broadway & Belmont Ave" "Clark St & Randolph St" ...
 $ start_station_id  : num [1:426887] 239 234 296 51 66 212 96 96 212 38 ...
 $ end_station_name  : chr [1:426887] "Clark St & Leland Ave" "Southport Ave & Irving Park Rd" "Wilton Ave & Belmont Ave" "Fairbanks Ct & Grand Ave" ...
 $ end_station_id    : num [1:426887] 326 318 117 24 212 96 212 212 96 100 ...
 $ start_lat         : num [1:426887] 42 42 41.9 41.9 41.9 ...
 $ start_lng         : num [1:426887] -87.7 -87.7 -87.6 -87.6 -87.6 ...
 $ end_lat           : num [1:426887] 42 42 41.9 41.9 41.9 ...
 $ end_lng           : num [1:426887] -87.7 -87.7 -87.7 -87.6 -87.6 ...
 $ member_casual     : chr [1:426887] "member" "member" "member" "member" ...
 - attr(*, "spec")=
  .. cols(
  ..   ride_id = col_character(),
  ..   rideable_type = col_character(),
  ..   started_at = col_character(),
  ..   ended_at = col_character(),
  ..   start_station_name = col_character(),
  ..   start_station_id = col_double(),
  ..   end_station_name = col_character(),
  ..   end_station_id = col_double(),
  ..   start_lat = col_double(),
  ..   start_lng = col_double(),
  ..   end_lat = col_double(),
  ..   end_lng = col_double(),
  ..   member_casual = col_character()
  .. )
 - attr(*, "problems")=<externalptr>
4. 
# Rename the dataset names as it is very long using below function
> q1_2019 <- Divvy_Trips_2019_Q1
> q1_2020 <- Divvy_Trips_2020_Q1
> View(q1_2019)
> View(q1_2020)
5. 
# Convert ride_id and rideable_type to character so that they can stack correctly and merge with second dataset
q1_2019 <-  mutate(q1_2019, ride_id = as.character(ride_id)
                   ,rideable_type = as.character(rideable_type))
6.  We combine both datasets into one dataframe for further analysis 
# Stack individual quarter's data frames into one big data frame
all_trips <- bind_rows(q1_2019, q1_2020)#, q3_2019)#, q4_2019, q1_2020)
7. We can drop columns which are not required in the analysis thus making it consistent
# Remove lat, long, birthyear, and gender fields as this data was dropped beginning in 2020
all_trips <- all_trips %>%  
  select(-c(start_lat, start_lng, end_lat, end_lng, birthyear, gender, "tripduration"))

We now have 9 columns with 791956 rows

We now have 9 columns with 791956 rows

STEP 3- We now clean up and add data to prepare for analysis

# Inspect the new table that has been created
colnames(all_trips)        #List of column names

[1] "ride_id"            "started_at"         "ended_at"           "rideable_type"     
[5] "start_station_id"   "start_station_name" "end_station_id"     "end_station_name"  
[9] "member_casual"

nrow(all_trips)          #How many rows are in data frame?
[1] 791956

dim(all_trips)             #Dimensions of the data frame?
[1] 791956      9 

#See the first 6 rows of data frame.  
head(all_trips)  
# A tibble: 6 × 9
  ride_id  started_at  ended_at rideable_type start_station_id start_station_name end_station_id
  <chr>    <chr>       <chr>    <chr>                    <dbl> <chr>                       <dbl>
1 21742443 2019-01-01… 2019-01… 2167                       199 Wabash Ave & Gran…             84
2 21742444 2019-01-01… 2019-01… 4386                        44 State St & Randol…            624
3 21742445 2019-01-01… 2019-01… 1524                        15 Racine Ave & 18th…            644
4 21742446 2019-01-01… 2019-01… 252                        123 California Ave & …            176
5 21742447 2019-01-01… 2019-01… 1170                       173 Mies van der Rohe…             35
6 21742448 2019-01-01… 2019-01… 2437                        98 LaSalle St & Wash…             49
# ℹ 2 more variables: end_station_name <chr>, member_casual <chr>

Also tail(all_trips)
# A tibble: 6 × 9
  ride_id   started_at ended_at rideable_type start_station_id start_station_name end_station_id
  <chr>     <chr>      <chr>    <chr>                    <dbl> <chr>                       <dbl>
1 6F4D221B… 2020-03-1… 2020-03… docked_bike                675 HQ QR                         675
2 ADDAA33C… 2020-03-1… 2020-03… docked_bike                675 HQ QR                         675
3 82B10FA3… 2020-03-0… 2020-03… docked_bike                161 Rush St & Superio…            240
4 AA0D5AAA… 2020-03-0… 2020-03… docked_bike                141 Clark St & Lincol…            210
5 3296360A… 2020-03-0… 2020-03… docked_bike                672 Franklin St & Ill…            264
6 064EC769… 2020-03-0… 2020-03… docked_bike                110 Dearborn St & Eri…             85
# ℹ 2 more variables: end_station_name <chr>, member_casual <chr>

#See list of columns and data types (numeric, character, etc)
str(all_trips)
tibble [791,956 × 9] (S3: tbl_df/tbl/data.frame)
 $ ride_id           : chr [1:791956] "21742443" "21742444" "21742445" "21742446" ...
 $ started_at        : chr [1:791956] "2019-01-01 0:04:37" "2019-01-01 0:08:13" "2019-01-01 0:13:23" "2019-01-01 0:13:45" ...
 $ ended_at          : chr [1:791956] "2019-01-01 0:11:07" "2019-01-01 0:15:34" "2019-01-01 0:27:12" "2019-01-01 0:43:28" ...
 $ rideable_type     : chr [1:791956] "2167" "4386" "1524" "252" ...
 $ start_station_id  : num [1:791956] 199 44 15 123 173 98 98 211 150 268 ...
 $ start_station_name: chr [1:791956] "Wabash Ave & Grand Ave" "State St & Randolph St" "Racine Ave & 18th St" "California Ave & Milwaukee Ave" ...
 $ end_station_id    : num [1:791956] 84 624 644 176 35 49 49 142 148 141 ...
 $ end_station_name  : chr [1:791956] "Milwaukee Ave & Grand Ave" "Dearborn St & Van Buren St (*)" "Western Ave & Fillmore St (*)" "Clark St & Elm St" ...
 $ member_casual     : chr [1:791956] "Subscriber" "Subscriber" "Subscriber" "Subscriber" ...

#Statistical summary of data. Mainly for numerics
summary(all_trips) 
   ride_id           started_at          ended_at         rideable_type      start_station_id
 Length:791956      Length:791956      Length:791956      Length:791956      Min.   :  2.0   
 Class :character   Class :character   Class :character   Class :character   1st Qu.: 77.0   
 Mode  :character   Mode  :character   Mode  :character   Mode  :character   Median :174.0   
                                                                             Mean   :204.4   
                                                                             3rd Qu.:291.0   
                                                                             Max.   :675.0   
 start_station_name end_station_id  end_station_name   member_casual     
 Length:791956      Min.   :  2.0   Length:791956      Length:791956     
 Class :character   1st Qu.: 77.0   Class :character   Class :character  
 Mode  :character   Median :174.0   Mode  :character   Mode  :character  
                    Mean   :204.4                                        
                    3rd Qu.:291.0                                        
                    Max.   :675.0                                        
                    NA's   :1

We need to go further to clean the data for consistency, and reliability. We will now check the dataset if they have “NA” values, duplicates or blank values and decide whether to drop them or substitute them.

#Count rows with “NA” values
colSums(is.na(all_trips))
           ride_id         started_at           ended_at      rideable_type   start_station_id 
                 0                  0                  0                  0                  0 
start_station_name     end_station_id   end_station_name      member_casual 
                 0                  1                  1                  0 
#Remove missing 
all_trips <- all_trips[complete.cases(all_trips), ]

# Save and export the cleaned file before dropping any rows using
> write.csv(all_trips, file = "all_trips.csv", row.names = FALSE)

To further refine and clean the data, it is necessary to remove empty, “NA”, and missing values. This can be achieved through the use of functions such as drop_na(), remove_missing().

#Remove NA
all_trips <- drop_na(all_trips)
all_trips <- remove_missing(all_trips)  # There are a few problems we will need to fix:

(1) In the “member_casual” column, there are two names for members (“member” and “Subscriber”) and two names for casual riders (“Customer” and “casual”). We will need to consolidate that from four to two labels.

# In the "member_casual" column, replace "Subscriber" with "member" and "Customer" with "casual"
# Before 2020, Divvy used different labels for these two types of riders ... we will want to make our dataframe consistent with their current nomenclature
# N.B.: "Level" is a special property of a column that is retained even if a subset does not contain any values from a specific level
# Begin by seeing how many observations fall under each usertype
table(all_trips$member_casual)
    casual   Customer     member Subscriber 
     48479      23163     378407     341906
# Reassign to the desired values (we will go with the current 2020 labels)
all_trips <-  all_trips %>% 
  mutate(member_casual = recode(member_casual
                                ,"Subscriber" = "member"
                                ,"Customer" = "casual"))

# Check to make sure the proper number of observations were reassigned
table(all_trips$member_casual)
casual member 
  71642 720313

(2) The data can only be aggregated at the ride-level, which is too granular. We will want to add some additional columns of data — such as day, month, year — that provide additional opportunities to aggregate the data.

# Add columns that list the date, month, day, and year of each ride
# This will allow us to aggregate ride data for each month, day, or year ... before completing these operations we could only aggregate at the ride level
# https://www.statmethods.net/input/dates.html more on date formats in R found at that link
#The default format is yyyy-mm-dd
all_trips$date <- as.Date(all_trips$started_at) 
all_trips$month <- format(as.Date(all_trips$date), "%m")
all_trips$day <- format(as.Date(all_trips$date), "%d")
all_trips$year <- format(as.Date(all_trips$date), "%Y")
all_trips$day_of_week <- format(as.Date(all_trips$date), "%A")

(3) We will want to add a calculated field for length of ride since the 2020Q1 data did not have the “tripduration” column. We will add “ride_length” to the entire dataframe for consistency.

# Add a "ride_length" calculation to all_trips (in seconds)
# https://stat.ethz.ch/R-manual/R-devel/library/base/html/difftime.html
all_trips$ride_length <- difftime(all_trips$ended_at,all_trips$started_at)
# Result of “ride_length”column created with time difference in seconds

# Inspect the structure of the columns
str(all_trips)
tibble [791,955 × 15] (S3: tbl_df/tbl/data.frame)
 $ ride_id           : chr [1:791955] "21742443" "21742444" "21742445" "21742446" ...
 $ started_at        : chr [1:791955] "2019-01-01 0:04:37" "2019-01-01 0:08:13" "2019-01-01 0:13:23" "2019-01-01 0:13:45" ...
 $ ended_at          : chr [1:791955] "2019-01-01 0:11:07" "2019-01-01 0:15:34" "2019-01-01 0:27:12" "2019-01-01 0:43:28" ...
 $ rideable_type     : chr [1:791955] "2167" "4386" "1524" "252" ...
 $ start_station_id  : num [1:791955] 199 44 15 123 173 98 98 211 150 268 ...
 $ start_station_name: chr [1:791955] "Wabash Ave & Grand Ave" "State St & Randolph St" "Racine Ave & 18th St" "California Ave & Milwaukee Ave" ...
 $ end_station_id    : num [1:791955] 84 624 644 176 35 49 49 142 148 141 ...
 $ end_station_name  : chr [1:791955] "Milwaukee Ave & Grand Ave" "Dearborn St & Van Buren St (*)" "Western Ave & Fillmore St (*)" "Clark St & Elm St" ...
 $ member_casual     : chr [1:791955] "member" "member" "member" "member" ...
 $ date              : Date[1:791955], format: "2019-01-01" "2019-01-01" "2019-01-01" ...
 $ month             : chr [1:791955] "01" "01" "01" "01" ...
 $ day               : chr [1:791955] "01" "01" "01" "01" ...
 $ year              : chr [1:791955] "2019" "2019" "2019" "2019" ...
 $ day_of_week       : chr [1:791955] "Tuesday" "Tuesday" "Tuesday" "Tuesday" ...
 $ ride_length       : 'difftime' num [1:791955] 390 441 829 1783 ...
  ..- attr(*, "units")= chr "secs"

# Convert "ride_length" from Factor to numeric so we can run calculations on the data
>  is.factor(all_trips$ride_length)
[1] FALSE

>  all_trips$ride_length <- as.numeric(as.character(all_trips$ride_length)) 
# “secs” which is in character removed in ride_length column

>  is.numeric(all_trips$ride_length) # to verify column is numeric
[1] TRUE

(4) There are some rides where tripduration shows up as negative, including several hundred rides where Divvy took bikes out of circulation for Quality Control reasons. We will want to delete these rides.

# Remove "bad" data
# The dataframe includes a few hundred entries when bikes were taken out of docks and checked for quality by Divvy or ride_length was negative
# We will create a new version of the dataframe (v2) since data is being removed
# https://www.datasciencemadesimple.com/delete-or-drop-rows-in-r-with-conditions-2/
all_trips_v2 <- all_trips[!(all_trips$start_station_name == "HQ QR" | all_trips$ride_length<0),]

New version of the dataset

New version of the dataset

Analyze Phase

STEP 4: Conduct Descriptive Analysis

# Descriptive analysis on ride_length (all figures in seconds)
mean(all_trips_v2$ride_length)                  #straight average (total ride length / rides)
[1] 1189.459

median(all_trips_v2$ride_length)                #midpoint number in the ascending array of ride lengths
[1] 539

max(all_trips_v2$ride_length)                     #longest ride
[1] 10632022

min(all_trips_v2$ride_length)                     #shortest ride
[1] 1

# You can condense the four lines above to one line using summary() on the specific attribute
summary(all_trips_v2$ride_length)
    Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
       1      331      539     1190      912            106

Let us now compare members and casual users for Cyclistic as the mean ride length, median, the max and minimum ride length for both the variables

# Compare members and casual users
aggregate(all_trips_v2$ride_length ~ all_trips_v2$member_casual, FUN = mean)
1                     casual                5372.7839
2                     member                 795.2523

aggregate(all_trips_v2$ride_length ~ all_trips_v2$member_casual, FUN = median)
  all_trips_v2$member_casual all_trips_v2$ride_length
1                     casual                     1393
2                     member                      508

aggregate(all_trips_v2$ride_length ~ all_trips_v2$member_casual, FUN = max)
  all_trips_v2$member_casual all_trips_v2$ride_length
1                     casual                 10632022
2                     member                  6096428

aggregate(all_trips_v2$ride_length ~ all_trips_v2$member_casual, FUN = min)
  all_trips_v2$member_casual all_trips_v2$ride_length
1                     casual                        2
2                     member                        1

# See the average ride time taken each day for members vs casual users
aggregate(all_trips_v2$ride_length ~ all_trips_v2$member_casual + all_trips_v2$day_of_week, FUN = mean)
   all_trips_v2$member_casual all_trips_v2$day_of_week all_trips_v2$ride_length
1                      casual                   Friday                6090.7373
2                      member                   Friday                 796.7338
3                      casual                   Monday                4752.0504
4                      member                   Monday                 822.3112
5                      casual                 Saturday                4950.7708
6                      member                 Saturday                 974.0730
7                      casual                   Sunday                5061.3044
8                      member                   Sunday                 972.9383
9                      casual                 Thursday                8451.6669
10                     member                 Thursday                 707.2093
11                     casual                  Tuesday                4561.8039
12                     member                  Tuesday                 769.4416
13                     casual                Wednesday                4480.3724
14                     member                Wednesday                 711.9838

# Notice that the days of the week are out of order. Let's fix that.
all_trips_v2$day_of_week <- ordered(all_trips_v2$day_of_week, levels=c("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"))
all_trips_v2$member_casual all_trips_v2$day_of_week all_trips_v2$ride_length
1                      casual                   Sunday                5061.3044
2                      member                   Sunday                 972.9383
3                      casual                   Monday                4752.0504
4                      member                   Monday                 822.3112
5                      casual                  Tuesday                4561.8039
6                      member                  Tuesday                 769.4416
7                      casual                Wednesday                4480.3724
8                      member                Wednesday                 711.9838
9                      casual                 Thursday                8451.6669
10                     member                 Thursday                 707.2093
11                     casual                   Friday                6090.7373
12                     member                   Friday                 796.7338
13                     casual                 Saturday                4950.7708
14                     member                 Saturday                 974.0730

We will now analyze ridership data by type and weekday, using the entire code below

# analyze ridership data by type and weekday
all_trips_v2 %>% mutate(weekday = wday(started_at, label = TRUE)) %>%                                                      #creates weekday field using wday()
group_by(member_casual, weekday) %>%  
#groups by usertype and weekday
summarise(number_of_rides = n()
#calculates the number of rides and average duration 
,average_duration = mean(ride_length)) %>%   
# calculates the average duration
arrange(member_casual, weekday)
#sorts
`summarise()` has grouped output by 'member_casual'. You can override using the `.groups`
argument.

# A tibble: 14 × 4
# Groups:   member_casual [2]
   member_casual weekday number_of_rides average_duration
   <chr>         <ord>             <int>            <dbl>
 1 casual        Sun               18652            5061.
 2 casual        Mon                5591            4752.
 3 casual        Tue                7311            4562.
 4 casual        Wed                7690            4480.
 5 casual        Thu                7147            8452.
 6 casual        Fri                8013            6091.
 7 casual        Sat               13473            4951.
 8 member        Sun               60197             973.
 9 member        Mon              110430             822.
10 member        Tue              127974             769.
11 member        Wed              121902             712.
12 member        Thu              125228             707.
13 member        Fri              115168             797.
14 member        Sat               59413             974.

We also need to consider riders- casual and member that have started maximum from which location and ended at which station.

Now we analyze the location from when max and min riders — casual and member have taken and max end station.

#We show below number of ride variables in start_station_name
n_distinct(all_trips_v2$start_station_name)
[1] 635
#We show below number of ride variables in end_station_name
n_distinct(all_trips_v2$end_station_name)
[1] 636
# to find number of times a location used in start_station_name and create new dataframe
location_start_station <- all_trips_v2 %>% count(start_station_name, sort=TRUE)
                            start_station_name     n
1                           Canal St & Adams St 14155
2                  Clinton St & Washington Blvd 13640
3                       Clinton St & Madison St 13362
4                     Columbus Dr & Randolph St  9080
5                      Kingsbury St & Kinzie St  9021
6                         Canal St & Madison St  8208
7                  Michigan Ave & Washington St  7525
8                       Franklin St & Monroe St  7227
9                    Larrabee St & Kingsbury St  6708
10                         Clinton St & Lake St  6674
11                    LaSalle St & Jackson Blvd  6426
12                      Dearborn St & Monroe St  6332
13                           Daley Center Plaza  6251
14                       Michigan Ave & Lake St  6113
15                        Dearborn St & Erie St  5618
16                    Desplaines St & Kinzie St  5521
17                   Franklin St & Jackson Blvd  5495
18                       Wabash Ave & Grand Ave  5322
19                          Wells St & Huron St  5222
20                       St. Clair St & Erie St  5182
21                        Wells St & Hubbard St  5127
22                            Clark St & Elm St  5059
23          Orleans St & Merchandise Mart Plaza  5048
24                          Morgan St & Lake St  4891
25                       Kingsbury St & Erie St  4876
26                    Wacker Dr & Washington St  4839
27                    Wabash Ave & Roosevelt Rd  4634
28                         State St & Kinzie St  4632
29                      Streeter Dr & Grand Ave  4533
30                       State St & Randolph St  4521
31                        Franklin St & Lake St  4460
32                       Clark St & Randolph St  4435
33                        Wells St & Concord Ln  4393
34                           Clark St & Lake St  4223
35                     Fairbanks Ct & Grand Ave  4145
36                Sheffield Ave & Fullerton Ave  4139
37                Ravenswood Ave & Lawrence Ave  4094
38                    Ashland Ave & Division St  4090
39                       Damen Ave & Pierce Ave  4042
40                   Indiana Ave & Roosevelt Rd  3993
41                     LaSalle St & Illinois St  3954
42                 Desplaines St & Jackson Blvd  3931
43                     University Ave & 57th St  3892
44                    Lake Shore Dr & Monroe St  3811
45                              Millennium Park  3796
46                                        HQ QR  3766
47                     McClurg Ct & Illinois St  3751
48                         McClurg Ct & Erie St  3746
49                    Clark St & Ida B Wells Dr  3677
50                 Marshfield Ave & Cortland St  3638
51                    Milwaukee Ave & Grand Ave  3566
52                    Franklin St & Chicago Ave  3549
53                        Green St & Madison St  3507
54                     Wilton Ave & Belmont Ave  3486
55                       Sedgwick St & Huron St  3485
56                   LaSalle St & Washington St  3484
57                 Stetson Ave & South Water St  3472
58              Cityfront Plaza Dr & Pioneer Ct  3445
59                       Dearborn St & Adams St  3395
60                  Dearborn Pkwy & Delaware Pl  3376
61                            Wells St & Elm St  3358
62                     Peoria St & Jackson Blvd  3342
63                         Broadway & Barry Ave  3336
64                      Clark St & Armitage Ave  3305
65                        Wabash Ave & Adams St  3300
66                          Morgan St & Polk St  3224
67                 Sheridan Rd & Irving Park Rd  3159
68                         Rush St & Hubbard St  3145
69                        Rush St & Superior St  3127
70                   Wilton Ave & Diversey Pkwy  3074
71                         Federal St & Polk St  3021
72                          Wabash Ave & 9th St  3008
73                               Shedd Aquarium  2899
74                     Loomis St & Lexington St  2880
75                   Aberdeen St & Jackson Blvd  2877
76                    Clark St & Wrightwood Ave  2814
77                        Michigan Ave & Oak St  2813
78          Mies van der Rohe Way & Chicago Ave  2809
79                        Wolcott Ave & Polk St  2804
80                      Canal St & Jackson Blvd  2751
81                    Bissell St & Armitage Ave  2736
82                     Wells St & Evergreen Ave  2722
83                Clinton St & Jackson Blvd (*)  2720
84                          Ellis Ave & 55th St  2719
85                       Clark St & Schiller St  2716
86                      State St & Van Buren St  2691
87                  Desplaines St & Randolph St  2676
88                      Sedgwick St & North Ave  2666
89                       Green St & Randolph St  2607
90                      Aberdeen St & Monroe St  2596
91                          Theater on the Lake  2585
92                          Ellis Ave & 60th St  2577
93                  Michigan Ave & Jackson Blvd  2559
94                  Lincoln Ave & Fullerton Ave  2524
95                    Southport Ave & Roscoe St  2520
96                    Michigan Ave & Madison St  2499
97                       Clark St & Lincoln Ave  2447
98                       Clark St & Chicago Ave  2441
99          Mies van der Rohe Way & Chestnut St  2429
100                          Wells St & Polk St  2421
max.print----omitted 536 rows

# to find number of times a location used in end_station_name and create new data frame
location_end_station <- all_trips_v2 %>% count(end_station_name, sort = TRUE)

       end_station_name     n
1                           Canal St & Adams St 15067
2                  Clinton St & Washington Blvd 14865
3                       Clinton St & Madison St 13713
4                      Kingsbury St & Kinzie St  8991
5                  Michigan Ave & Washington St  8639
6                         Canal St & Madison St  8581
7                          Clinton St & Lake St  6943
8                       Franklin St & Monroe St  6519
9                            Daley Center Plaza  6458
10                    LaSalle St & Jackson Blvd  6409
11                      Dearborn St & Monroe St  6296
12                       Michigan Ave & Lake St  6244
13                   Larrabee St & Kingsbury St  6179
14                       St. Clair St & Erie St  6108
15                        Dearborn St & Erie St  5899
16                            Clark St & Elm St  5514
17                      Streeter Dr & Grand Ave  5416
18                    Columbus Dr & Randolph St  5408
19                         State St & Kinzie St  5316
20                        Wells St & Hubbard St  5297
21                       Wabash Ave & Grand Ave  5272
22                          Morgan St & Lake St  5256
23                    Wabash Ave & Roosevelt Rd  5203
24                   Franklin St & Jackson Blvd  5110
25                    Desplaines St & Kinzie St  4846
26                          Wells St & Huron St  4841
27                              Millennium Park  4588
28                       Clark St & Randolph St  4545
29                       State St & Randolph St  4527
30                    Wacker Dr & Washington St  4456
31                        Wells St & Concord Ln  4378
32                       Kingsbury St & Erie St  4298
33                     Fairbanks Ct & Grand Ave  4295
34          Orleans St & Merchandise Mart Plaza  4294
35                        Franklin St & Lake St  4283
36                       Damen Ave & Pierce Ave  4177
37                     LaSalle St & Illinois St  4075
38                Sheffield Ave & Fullerton Ave  4035
39                    Ashland Ave & Division St  4008
40                     University Ave & 57th St  3956
41                Ravenswood Ave & Lawrence Ave  3942
42                           Clark St & Lake St  3911
43                 Desplaines St & Jackson Blvd  3865
44                        Green St & Madison St  3848
45                         Broadway & Barry Ave  3813
46                   Indiana Ave & Roosevelt Rd  3793
47                                        HQ QR  3766
48                    Clark St & Ida B Wells Dr  3762
49                     McClurg Ct & Illinois St  3712
50                  Dearborn Pkwy & Delaware Pl  3673
51                         McClurg Ct & Erie St  3657
52                 Marshfield Ave & Cortland St  3521
53                   LaSalle St & Washington St  3513
54                    Milwaukee Ave & Grand Ave  3454
55                       Sedgwick St & Huron St  3430
56                    Franklin St & Chicago Ave  3399
57                     Wilton Ave & Belmont Ave  3332
58                       Dearborn St & Adams St  3299
59                            Wells St & Elm St  3283
60                      Clark St & Armitage Ave  3267
61                    Lake Shore Dr & Monroe St  3259
62                        Michigan Ave & Oak St  3227
63                     Peoria St & Jackson Blvd  3166
64              Cityfront Plaza Dr & Pioneer Ct  3156
65                        Wabash Ave & Adams St  3156
66                          Morgan St & Polk St  3151
67                Clinton St & Jackson Blvd (*)  3137
68                         Rush St & Hubbard St  3123
69                        Rush St & Superior St  3098
70                   Wilton Ave & Diversey Pkwy  3049
71                      Canal St & Jackson Blvd  3044
72                 Sheridan Rd & Irving Park Rd  3042
73                       Green St & Randolph St  2989
74                          Wabash Ave & 9th St  2982
75                         Federal St & Polk St  2950
76                   Aberdeen St & Jackson Blvd  2863
77                    Clark St & Wrightwood Ave  2848
78                    Bissell St & Armitage Ave  2805
79                          Theater on the Lake  2794
80          Mies van der Rohe Way & Chicago Ave  2762
81                          Ellis Ave & 55th St  2704
82                        Wolcott Ave & Polk St  2698
83                  Michigan Ave & Jackson Blvd  2689
84                  Lincoln Ave & Fullerton Ave  2648
85                    Southport Ave & Roscoe St  2647
86                     Loomis St & Lexington St  2592
87                    Michigan Ave & Madison St  2589
88                      Aberdeen St & Monroe St  2570
89                     Wells St & Evergreen Ave  2549
90                          Ellis Ave & 60th St  2532
91                 Sheffield Ave & Kingsbury St  2523
92                      State St & Van Buren St  2509
93                        Kimbark Ave & 53rd St  2501
94                       Clark St & Lincoln Ave  2495
95                               Shedd Aquarium  2481
96                       Wabash Ave & Wacker Pl  2468
97                  Desplaines St & Randolph St  2461
98                      Broadway & Waveland Ave  2451
99               Sheffield Ave & Wellington Ave  2446
100                      Clark St & Schiller St  2379
[ reached 'max' / getOption("max.print") -- omitted 536 rows ]

location_start_station <- all_trips_v2 %>% count(end_station_name, sort = TRUE)


The location for start and end station is in the state of Chicago.
# We add Chicago to the start and end station name columns using the below code:
total_trip_start_station$start_station_name <- paste(total_trip_start_station$start_station_name, "Chicago", sep = ", ")
member_casual         start_station_name                      total rides
casual                           2112 W Peterson Ave, Chicago       12
casual                           63rd St Beach, Chicago                     52
casual                           900 W Harrison St, Chicago             143
etc…..
total_trip_end_station$end_station_name <- paste(total_trip_end_station$end_station_name, "Chicago", sep = ", ")
member_casual         start_station_name                      total rides
casual                           2112 W Peterson Ave, Chicago       14
casual                           63rd St Beach, Chicago                     47
casual                           900 W Harrison St, Chicago             110

Share Phase

We will have to answer the key problems, the key stakeholder-Marketing and executive committee will use to make strategic decision to increase riders and sales.

# Lets visualize comparison of members and casual users
aggregate(all_trips_v2$ride_length ~ all_trips_v2$member_casual, FUN = mean)
1                     casual                5372.7839
2                     member                 795.2523

aggregate(all_trips_v2$ride_length ~ all_trips_v2$member_casual, FUN = median)
  all_trips_v2$member_casual all_trips_v2$ride_length
1                     casual                     1393
2                     member                      508

aggregate(all_trips_v2$ride_length~all_trips_v2$member_casual, FUN = max)
  all_trips_v2$member_casual all_trips_v2$ride_length
1                     casual                 10632022
2                     member                  6096428

aggregate(all_trips_v2$ride_length ~ all_trips_v2$member_casual, FUN = min)
  all_trips_v2$member_casual all_trips_v2$ride_length
1                     casual                        2
2                     member                        1

#Lets visualize ride duration statistics by user type
ride_stats <- all_trips_v2 %>%
  group_by(member_casual) %>%
  summarize (
    mean_length = mean(ride_length),
    median_length = median(ride_length),
    max_length = max(ride_length),
    min_length = min(ride_length))

ride_stats <- data.frame(
  user_type = c("casual", "member", "casual", "member", "casual", "member", "casual", "member"),
  metric = c("Average", "Average", "Median", "Median", "Max", "Max", "Min", "Min"),
  value = c(5372.7839, 795.2523, 1393, 508, 10632022, 6096428, 2, 1))
ggplot(ride_stats, aes(x = user_type, y = value, fill = user_type)) +
  geom_col() +
  facet_wrap(~metric, scales = "free_y") +
  labs(title = "Ride Duration Statistics by User Type",
       subtitle = "Note: Y-axes differ across panels to accommodate scale differences",
       x = "User Type",
       y = "Seconds",
       fill = "User Type") +
  theme_minimal() +
  scale_fill_brewer(palette = "Paired")

Ride Duration by User type-Casual or Member

Ride Duration by User type-Casual or Member

# See the average ride time taken each day for members vs casual users
aggregate(all_trips_v2$ride_length ~ all_trips_v2$member_casual + all_trips_v2$day_of_week, FUN = mean)
   all_trips_v2$member_casual all_trips_v2$day_of_week all_trips_v2$ride_length

1                      casual                   Friday                6090.7373
2                      member                   Friday                 796.7338
3                      casual                   Monday                4752.0504
4                      member                   Monday                 822.3112
5                      casual                 Saturday                4950.7708
6                      member                 Saturday                 974.0730
7                      casual                   Sunday                5061.3044
8                      member                   Sunday                 972.9383
9                      casual                 Thursday                8451.6669
10                     member                 Thursday                 707.2093
11                     casual                  Tuesday                4561.8039
12                     member                  Tuesday                 769.4416
13                     casual                Wednesday                4480.3724
14                     member                Wednesday                 711.9838
# Let's create a sample data frame like your aggregate output
agg_data <- data.frame(
  member_casual = rep(c("casual", "member"), 7),
  day_of_week = rep(c("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"), each = 2),
  ride_length = c(5061.3044, 972.9383, 4752.0504, 822.3112, 4561.8039, 769.4416, 4480.3724, 711.9838, 8451.6669, 707.2093, 6090.7373, 796.7338, 4950.7708, 974.0730)
)
# Make sure day_of_week is a factor in the correct order for plotting
days_order <- c("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday")
agg_data$day_of_week <- factor(agg_data$day_of_week, levels = days_order)

# --- 2. Create the ggplot ---
ggplot(agg_data, aes(x = day_of_week, y = ride_length, fill = member_casual)) +
  geom_col(position = "dodge") + # 'dodge' places bars side-by-side
  labs(
    title = "Average Ride Length by Day of Week & Member Type",
    x = "Day of Week",
    y = "Average Ride Length (seconds)", # Adjust units if needed
    fill = "Member Type"
  ) +
  theme_minimal() + scale_fill_brewer(palette = "Paired") +# A clean theme
  theme(axis.text.x = element_text(angle = 45, hjust = 1)) # Rotate x-axis labels

Average ride time taken each day for members vs casual users

Average ride time taken each day for members vs casual users

# Let's visualize the number of rides by rider type
all_trips_v2 %>% 
mutate(weekday = wday(started_at, label = TRUE)) %>% 
group_by(member_casual, weekday) %>% 
summarise(number_of_rides = n()
, average_duration = mean(ride_length)) %>% 
arrange(member_casual, weekday)  %>% 
ggplot(aes(x = weekday, y = number_of_rides, fill = member_casual)) +
  geom_col(position = "dodge")

Number of Rides each day by Rider type

Number of Rides each day by Rider type

# Let's create a visualization for average duration
all_trips_v2 %>% 
  mutate(weekday = wday(started_at, label = TRUE)) %>% 
  group_by(member_casual, weekday) %>% 
  summarise(number_of_rides = n()
            ,average_duration = mean(ride_length)) %>% 
  arrange(member_casual, weekday)  %>% 
  ggplot(aes(x = weekday, y = average_duration, fill = member_casual)) +
  geom_col(position = "dodge")

Average duration of ride per day by Rider type

Average duration of ride per day by Rider type

# Lets now visualize total rides taken by manual and casual riders from start and end station. For this, we first plot the latitude and longitude for each place.  
Use the latitude and longitude from the original database to verify the locations. As we don’t have data of latitude and longitude of some locations in Q1 2019, we find them using R Studio.
We plot the location on the map, we now find the latitude and longitude for each start and end station. We thus create 2 columns for start and end trip dataframes. 
For locations that don’t have latitude and longitude-Q1 2019. We first install “tidygeocoder”
geo_trip_start_station <- total_trip_start_station %>% geocode(address = start_station_name, method = "OSM", lat = latitude, long = longitude)
member_casual      start_station_name                  total_rides     latitude          longitude
casual               2112 W Peterson Ave, Chicago        12 41.99096 -87.68284
casual               63rd St Beach, Chicago                      52 41.78249 -87.57450
casual              900 W Harrison St, Chicago               143 41.87476 -87.64981
etc….
geo_trip_start_station <- total_trip_start_station %>% geocode(address = start_station_name, method = "OSM", lat = latitude, long = longitude)
member_casual      start_station_name                  total_rides     latitude          longitude
casual               2112 W Peterson Ave, Chicago        14 41.99096 -87.68284
casual               63rd St Beach, Chicago                      47 41.78249 -87.57450
casual              900 W Harrison St, Chicago               110 41.87476 -87.64981
etc….

Create a visualization to plot total rides for start station on Tableau

Upload the start station total ride.csv with latitude and longitude. We get the below visual also showing total rides for both member and casual riders at the start station. Using Tableau created visualization for total rides start station Q1 2019 & 2020 and total rides end station Q1 2019 & 2020

Total Start Station rides from Q1 2019 & 2020 for Member and Casual riders

Total Start Station rides from Q1 2019 & 2020 for Member and Casual riders

Total End Station rides from Q1 & 2020 for Member and Casual riders

Total End Station rides from Q1 & 2020 for Member and Casual riders

From the above visualization, we can conclude that casual riders use bike for recreational purposes and start and end from hotels, parks, aquariums, harbors and member riders use bike for work, business centers, banks, schools, universities, institutes, theatres, residential areas, hospitals.

Summary

  1. Average duration of rides is more for casual riders than member riders, i.e casual riders use bikes for longer duration and member riders use bikes for shorter duration.

  2. Number of casual riders are more during weekends than weekdays as bikes used for recreational purposes. While number of member riders are more during the weekday

We also noticed that casual riders use bikes for recreational purposes and start and end hotels, parks, aquariums, harbors and member riders use bike for work, business centers, banks, schools, universities, institutes, theatres, residential areas, hospitals.

ACT

As per our analysis, we suggest the following recommendations:

  1. We can create marketing campaigns and advertising for casual riders at other recreational areas to motivate the casual riders to become members.

  2. Marketing advertising at residential areas can also increase bike riders especially during spring and summers when casual riders are at peak.

  3. Create marketing campaigns at business centers, universities, schools, hospital on weekday to increase member riders’ base as we see that member riders are more even though for short ride duration.

  4. We can provide incentive and discount to casual riders during weekend that can convert to more members

  5. Having entire year data, we can further analyze and report marketing campaigns which ca be focused monthly, quarterly or half yearly for a particular location to increase member riders.

Secondary Case study Sources:

Coursera-Using RScript, John Rama, Visualization type for location-Somia Nasir


메타데이터
post_id
90f2b6663d6b
slug
case-study-design-cyclistics-marketing-strategy-90f2b6663d6b
url
https://medium.com/@juliana_dsouza2001/case-study-design-cyclistics-marketing-strategy-90f2b6663d6b
canonical_url
https://medium.com/@juliana_dsouza2001/case-study-design-cyclistics-marketing-strategy-90f2b6663d6b
author_url
https://medium.com/@juliana_dsouza2001
status
ok
fetched_at
2026-06-09 15:37:30