Scenario

You are a junior data analyst working on the marketing analyst team at Cyclistic, a bike-share company in Chicago. The director of marketing believes the company’s future success depends on maximizing the number of annual memberships. Therefore, your team wants to understand how casual riders and annual members use Cyclistic bikes differently. From these insights, your team will design a new marketing strategy to convert casual riders into annual members. But first, Cyclistic executives must approve your recommendations, so they must be backed up with compelling data insights and professional data visualizations.

About the company

In 2016, Cyclistic launched a successful bike-share offering. Since then, the program has grown to a fleet of 5,824 bicycles that are geotracked and locked into a network of 692 stations across Chicago. The bikes can be unlocked from one station and returned to any other station in the system anytime. Until now, Cyclistic’s marketing strategy relied on building general awareness and appealing to broad consumer segments. One approach that helped make these things possible was the flexibility of its pricing plans: single-ride passes, full-day passes, and annual memberships. Customers who purchase single-ride or full-day passes are referred to as casual riders. Customers who purchase annual memberships are Cyclistic members. Cyclistic’s finance analysts have concluded that annual members are much more profitable than casual riders. Although the pricing flexibility helps Cyclistic attract more customers, Moreno believes that maximizing the number of annual members will be key to future growth. Rather than creating a marketing campaign that targets all-new customers, Moreno believes there is a solid opportunity to convert casual riders into members. She notes that casual riders are already aware of the Cyclistic program and have chosen Cyclistic for their mobility needs. Moreno has set a clear goal: Design marketing strategies aimed at converting casual riders into annual members. In order to do that, however, the team needs to better understand how annual members and casual riders differ, why casual riders would buy a membership, and how digital media could affect their marketing tactics. Moreno and her team are interested in analyzing the Cyclistic historical bike trip data to identify trends.

A clear statement of the business task

The business task is to design marketing strategies aimed at converting casual riders into annual members. To achieve this task, there need to be understanding on the differences between casual riders and member, hence this project

A description of all data sources used

The trips data of the Cyclistic which was made available by Motivate International Inc. and were downloaded from [https://divvy-tripdata.s3.amazonaws.com/index.html]. The downloaded data in the various months’ zip folders were unzipped into a folder named Cyclistic_unzipped .15 current datasets from 2022 to 2024 were selected into another folder named cyclistic

#Documentation of any cleaning or manipulation of data 
# Loading Packages for work.
 library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ ggplot2   3.5.1     ✔ tibble    3.2.1
## ✔ lubridate 1.9.3     ✔ tidyr     1.3.1
## ✔ purrr     1.0.2     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ purrr::%||%()   masks base::%||%()
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(dplyr)
library(lubridate)
library("hms")
## 
## Attaching package: 'hms'
## 
## The following object is masked from 'package:lubridate':
## 
##     hms
# Importing Data and combining them
setwd("C:/Users/Daniel/Desktop/Cyclistic") # setting work directory
fnames <- list.files() # listing files
print(fnames)
##  [1] "202201-divvy-tripdata.csv" "202202-divvy-tripdata.csv"
##  [3] "202203-divvy-tripdata.csv" "202204-divvy-tripdata.csv"
##  [5] "202206-divvy-tripdata.csv" "202307-divvy-tripdata.csv"
##  [7] "202308-divvy-tripdata.csv" "202309-divvy-tripdata.csv"
##  [9] "202310-divvy-tripdata.csv" "202311-divvy-tripdata.csv"
## [11] "202312-divvy-tripdata.csv" "202401-divvy-tripdata.csv"
## [13] "202402-divvy-tripdata.csv" "202403-divvy-tripdata.csv"
## [15] "202404-divvy-tripdata.csv"
csv <- lapply(fnames, read.csv) # reading files
Cyclistic_data <- do.call(rbind,csv) # Combining Files
# Data Wrangling
 Cyclistic_data$started_at <- as.POSIXct(Cyclistic_data$started_at, format = "%Y-%m-%d %H:%M:%S") # converting string started_at to POSIXct format
Cyclistic_data$ended_at <- as.POSIXct(Cyclistic_data$ended_at, format = "%Y-%m-%d %H:%M:%S") # converting string ended_at to POSIXct format
Cyclistic_data$ride_length <- difftime(Cyclistic_data$ended_at,Cyclistic_data$started_at) # finding the difference in between started_at and ended_at
Cyclistic_data$ride_length <- as.numeric(Cyclistic_data$ride_length)
# storing the data$ride_length as numeric

Cyclistic_data$ride_length <- as_hms(Cyclistic_data$ride_length) # storing the answer as hour minute and second
# Cleaning Data
Cyclistic_data <- Cyclistic_data [,- c(5,6,7,8,9,10,11,12)] # remove unwanted columns by column index
Cyclistic_data <- na.omit(Cyclistic_data) # remove empty cells
Cyclistic_data <- distinct(Cyclistic_data) # select distinct rows
Cyclistic_data <- subset(Cyclistic_data,ride_length > 0) # select only rows with ride_length greater than zero
# adding new columns 

# convert started_at to date
# convert date to text
# retrive the day of the week as numeric 
# convert date to month 
# convert date to year                                                                                                                                                         
Cyclistic_data <- Cyclistic_data  %>%                                                      mutate(date = as.Date(started_at), 
day_of_week = format( date, "%A"), 
day =  wday(date), month =  format( date, "%m"), 
year =  format( date, "%Y"))
# A summary of your analysis 
#Total Number of Cyclists
Total_number_Users <- count(Cyclistic_data)
Total_number_Users
##         n
## 1 6056186
#Mode of Week
library(DescTools)
mode_day_week <- Mode(Cyclistic_data$day_of_week)
mode_day_week
## [1] "Saturday"
## attr(,"freq")
## [1] 933677
#Total number of members and casual
Total_number_Users_pmt <- Cyclistic_data %>% group_by(member_casual)%>% count() 
Total_number_Users_pmt
## # A tibble: 2 × 2
## # Groups:   member_casual [2]
##   member_casual       n
##   <chr>           <int>
## 1 casual        2141735
## 2 member        3914451
#Mean of ride_length
mean_ride_lenght <- as_hms(mean(Cyclistic_data$ride_length))
mean_ride_lenght
## 00:18:30.402014
# Maximum and Mean ride_length by membership type
Mm_Mn_of_ride_length_mt <- Cyclistic_data %>% group_by(member_casual) %>%                   summarise( mean = as_hms(mean(ride_length)),  max = as_hms(max(ride_length)))
 Mm_Mn_of_ride_length_mt
## # A tibble: 2 × 3
##   member_casual mean          max       
##   <chr>         <time>        <time>    
## 1 casual        29'01.046615" 1641:29:04
## 2 member        12'45.353982"   25:59:56
# Average ride length of users by type of bike

Av_ride_length_users_tb <- Cyclistic_data %>% group_by(member_casual,rideable_type) %>%     summarise( mean = as_hms(mean(ride_length)))
## `summarise()` has grouped output by 'member_casual'. You can override using the
## `.groups` argument.
Av_ride_length_users_tb
## # A tibble: 5 × 3
## # Groups:   member_casual [2]
##   member_casual rideable_type mean           
##   <chr>         <chr>         <time>         
## 1 casual        classic_bike  00:32:54.814899
## 2 casual        docked_bike   02:39:27.547238
## 3 casual        electric_bike 00:14:54.371702
## 4 member        classic_bike  00:14:11.689538
## 5 member        electric_bike 00:11:09.652798
#Average ride_length for users by day of week
Av_ride_length_users_dw <- Cyclistic_data %>% group_by(member_casual,day_of_week) %>%       summarise( mean = as_hms(mean(ride_length)))
## `summarise()` has grouped output by 'member_casual'. You can override using the
## `.groups` argument.
Av_ride_length_users_dw
## # A tibble: 14 × 3
## # Groups:   member_casual [2]
##    member_casual day_of_week mean         
##    <chr>         <chr>       <time>       
##  1 casual        Friday      27'42.461208"
##  2 casual        Monday      28'34.698235"
##  3 casual        Saturday    32'00.074501"
##  4 casual        Sunday      33'50.792240"
##  5 casual        Thursday    26'05.511689"
##  6 casual        Tuesday     26'18.029221"
##  7 casual        Wednesday   25'05.929885"
##  8 member        Friday      12'29.722483"
##  9 member        Monday      12'16.505640"
## 10 member        Saturday    14'00.685026"
## 11 member        Sunday      14'16.554497"
## 12 member        Thursday    12'13.825664"
## 13 member        Tuesday     12'21.681553"
## 14 member        Wednesday   12'15.515211"
#Number of rides for users by day of the week
No_rides_users_dweek <- Cyclistic_data %>% group_by(member_casual,day_of_week) %>%      count()
No_rides_users_dweek
## # A tibble: 14 × 3
## # Groups:   member_casual, day_of_week [14]
##    member_casual day_of_week      n
##    <chr>         <chr>        <int>
##  1 casual        Friday      305177
##  2 casual        Monday      251825
##  3 casual        Saturday    431619
##  4 casual        Sunday      360272
##  5 casual        Thursday    281415
##  6 casual        Tuesday     258002
##  7 casual        Wednesday   253425
##  8 member        Friday      540139
##  9 member        Monday      556010
## 10 member        Saturday    502058
## 11 member        Sunday      437678
## 12 member        Thursday    631441
## 13 member        Tuesday     627128
## 14 member        Wednesday   619997

Supporting visualizations and key findings

You can also embed plots, for example:

# Total Number of Users by Membership Status

 ggplot(Total_number_Users_pmt, aes(x = "", y = n, fill = member_casual)) + geom_bar(stat = "identity") + geom_text(aes(label = n), position = position_stack(vjust = 0.5), color = "black", size = 5) + labs(title = "Total Number of Users by Membership Status", x = NULL, y = NULL, fill = "Membership Status") + scale_fill_manual(values = c("#0072B2", "#009E73")) + coord_polar("y", start = 0) + theme_void()

# Mean Ride Length by Membership Status

ggplot(Mm_Mn_of_ride_length_mt, aes(x = member_casual,fill = member_casual, y = mean)) +
    geom_bar(stat = "identity", alpha = 0.5) +
    labs(title = "Mean Ride Length by Membership Status",
         x = "Membership Status",
         y = "Mean Ride Length") +
    theme_minimal()

# Max Ride Length by Membership Status
ggplot(Mm_Mn_of_ride_length_mt, aes(x = member_casual,fill = member_casual, y = as_hms(max))) +
       geom_bar(stat = "identity", alpha = 0.5) +
        geom_text(aes(label = max), vjust = -0.5, color = "black", size = 4) +
        labs(title = "Max Ride Length by Membership Status",
                       x = "Membership Status",
                        y = "Max Ride Length") +
        theme_minimal()

# Average Ride Length of Users by Bike Type
ggplot(Av_ride_length_users_tb, aes(x = rideable_type, fill = member_casual, y = mean)) + geom_col(position = "dodge")  + labs(title = "Average Ride Length of Users by Bike Type", x = "Rideable Type", y = "Mean Ride Length") + theme_minimal()

# Average Ride Length of Users by Day of the Week
ggplot(Av_ride_length_users_dw, aes(x = day_of_week, fill = member_casual, y = mean)) + geom_col(position = "dodge") + labs(title = "Average Ride Length of Users by Day of the Week", x = "Day of the Week", y = "Mean Ride Length") + theme_minimal() + theme(axis.text.x = element_text(angle = 45, hjust = 1)) # Adjust angle and justification of x-axis labels

# Number of rides for users by day of the week
ggplot(No_rides_users_dweek, aes(x = day_of_week, fill = member_casual, y = n)) + geom_col(position = "dodge") + labs(title = "Number of rides for users by day of the week", x = "Day_of_the_Week", y = "count_of_Ride") + theme_minimal() + theme(axis.text.x = element_text(angle = 45, hjust = 1)) # Adjust angle and justification of x-axis labels

# Stacked graph Number of rides for users by day of the week
ggplot(No_rides_users_dweek, aes(x = day_of_week, fill = member_casual, y = n)) + 
  geom_col(position = "stack") + 
  labs(title = "Number of rides for users by day of the week", x = "Day of the Week", y = "Count of Rides") + 
  theme_minimal() + 
  theme(axis.text.x = element_text(angle = 45, hjust = 1))

#Max Ride Length by Membership Status
ggplot(Mm_Mn_of_ride_length_mt, aes(x = member_casual,fill = member_casual, y = as_hms(max))) +
       geom_bar(stat = "identity", alpha = 0.5) +
        geom_text(aes(label = max), vjust = -0.5, color = "black", size = 4) +
        labs(title = "Max Ride Length by Membership Status",
                       x = "Membership Status",
                        y = "Max Ride Length") +
        theme_minimal()

Recommendations.

  1. Saturday being the mode of day means that it records the highest number of bike rides. Cyclistic can expand services, run promotions and organize community events on Saturdays all in the bid to capitalize on the number of riders and make profit.

2.Since the maximum duration of ride is by the casual user type,the company should employ reward systems and adverts to convert more of the casuals to members.Cyclistic should also offer loyalist packages in the form of discounts to members who form the greatest chunk of users in order to maintain them.

  1. The Classic bike type is the most preferred among both user types. Cyclistic can increase the number of classic bikes available.