Bike-Share Rider Analysis

Case study · R

Bike-Share Rider Analysis

My Google Data Analytics capstone: cleaning and analyzing a year of Chicago bike-share trips in R to answer one question for a marketing team. How do annual members and casual riders use the bikes differently?

Tools
R (tidyverse, janitor, geosphere, ggplot2), Posit Cloud
Data
Divvy public trip data, Aug 2022 to Jul 2023
Scale
5.7M rides across 12 monthly files
Method
Ask, prepare, process, analyze, share, act
Chart of total rides per month for members and casual riders
Rides per month: both groups peak in summer, and casual riding nearly disappears in winter.
5.7MRides analyzed
62%Rides by members
2.3xLonger casual rides
5pmPeak hour for both

The ask

In the capstone scenario, Cyclistic, a Chicago bike-share company, found that annual members are more profitable than casual riders. Marketing wanted to convert casual riders into members, and needed to know how the two groups ride differently before designing a campaign.

The data is Divvy’s real trip history, made public by Motivate International Inc. under the Divvy data license.

How I built it

  1. Step 1Combined and checked the dataRead in 12 monthly CSV files, confirmed their columns matched, and bound them into one 5.7 million row table.
  2. Step 2Cleaned itStandardized column names, confirmed unique 16-character ride IDs, removed rides that ended before they started, labeled missing stations, and added date, hour, duration and distance fields.
  3. Step 3Analyzed and charted itCompared members and casual riders by ride length, day, hour, month, distance, bike type and start station in ggplot2.

What the data showed

  • Casual riders take much longer rides: 0.70 hours on average, against 0.31 hours for members.
  • Members ride like commuters, with weekday peaks around 8am and 5pm. Casual riders ride more on weekends.
  • Both groups peak from June to August and drop sharply in winter, casual riders most of all.
  • Casual riders favor electric bikes (58%) and are the only group using docked bikes.
  • Casual riders cluster at Streeter Dr & Grand Ave, and their top five start stations do not overlap with members at all.

Recommendations

  1. Membership promotionsWeekend discounts and milestone rewards for trips or hours ridden, promoted in the app, aimed squarely at casual riders.
  2. Targeted messages at Streeter Dr & Grand AveCasual riders concentrate at this station, so it is the first place to promote membership benefits like priority bike access.
  3. Station feedback surveysShort surveys at the busiest casual rider stations to learn what would make those riders sign up.

The R code

Every cleaning and analysis step, as run in Posit Cloud.

Show the R code
# Install relevant packages if not available if(!require("tidyverse")) install.packages("tidyverse") if(!require("janitor")) install.packages("janitor") if(!require("geosphere")) install.packages("geosphere") 

# Load the required packages 
library(tidyverse) 
library(janitor) 
library(geosphere) 

Loading required package: tidyverse 
── Attaching core tidyverse packages ─────── tidyverse 2.0.0 ── 
✔ dplyr 1.1.2 ✔ readr 2.1.4 ✔ forcats 1.0.0 ✔ stringr 1.5.0 ✔ ggplot2 3.4.2 ✔ tibble 3.2.1 ✔ lubridate 1.9.2 ✔ tidyr 1.3.0 ✔ purrr 1.0.1 

── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ── 
✖ 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 Loading required package: janitor 

Attaching package: ‘janitor’ 

The following objects are masked from ‘package:stats’: chisq.test, fisher.test 

Loading required package: geosphere 

The legacy packages maptools, rgdal, and rgeos, underpinning the sp package, which was just loaded, will retire in October 2023. Please refer to R-spatial evolution reports for details, especially https://r-spatial.org/r/2023/05/15/evolution4.html. It may be desirable to make the sf package available; package maintainers should consider adding sf to Suggests:. The sp package is now running under evolution status 2 (status 2 uses the sf package in place of rgdal)

# Read CSV and assign names to each data frame 
trips_2022_08 <- read_csv("/kaggle/input/cyclistic-tripdata/202208-divvy-tripdata.csv")
 
trips_2022_09 <- read_csv("/kaggle/input/cyclistic-tripdata/202209-divvy-publictripdata.csv")
 
trips_2022_10 <- read_csv("/kaggle/input/cyclistic-tripdata/202210-divvy-tripdata.csv") 

trips_2022_11 <- read_csv("/kaggle/input/cyclistic-tripdata/202211-divvy-tripdata.csv") 

trips_2022_12 <- read_csv("/kaggle/input/cyclistic-tripdata/202212-divvy-tripdata.csv") 

trips_2023_01 <- read_csv("/kaggle/input/cyclistic-tripdata/202301-divvy-tripdata.csv") 

trips_2023_02 <- read_csv("/kaggle/input/cyclistic-tripdata/202302-divvy-tripdata.csv") 

trips_2023_03 <- read_csv("/kaggle/input/cyclistic-tripdata/202303-divvy-tripdata.csv") 

trips_2023_04 <- read_csv("/kaggle/input/cyclistic-tripdata/202304-divvy-tripdata.csv") 

trips_2023_05 <- read_csv("/kaggle/input/cyclistic-tripdata/202305-divvy-tripdata.csv") 

trips_2023_06 <- read_csv("/kaggle/input/cyclistic-tripdata/202306-divvy-tripdata.csv") 

trips_2023_07 <- read_csv("/kaggle/input/cyclistic-tripdata/202307-divvy-tripdata.csv") 

Rows: 785932 Columns: 13 
── Column specification ──────────────────────────────────────────────────────── 
Delimiter: "," 
chr (7): ride_id, rideable_type, start_station_name, start_station_id, end_... 
dbl (4): start_lat, start_lng, end_lat, end_lng 
dttm (2): started_at, ended_at 

ℹ Use `spec()` to retrieve the full column specification for this data. 
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message. 

Rows: 701339 Columns: 13 
── Column specification ──────────────────────────────────────────────────────── 
Delimiter: "," 
chr (7): ride_id, rideable_type, start_station_name, start_station_id, end_... 
dbl (4): start_lat, start_lng, end_lat, end_lng 
dttm (2): started_at, ended_at 

ℹ Use `spec()` to retrieve the full column specification for this data. 
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message. 

Rows: 558685 Columns: 13 ── Column specification ──────────────────────────────────────────────────────── 
Delimiter: "," 
chr (7): ride_id, rideable_type, start_station_name, start_station_id, end_... 
dbl (4): start_lat, start_lng, end_lat, end_lng 
dttm (2): started_at, ended_at 

ℹ Use `spec()` to retrieve the full column specification for this data. 
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message. 

Rows: 337735 Columns: 13 ── Column specification ──────────────────────────────────────────────────────── 
Delimiter: "," 
chr (7): ride_id, rideable_type, start_station_name, start_station_id, end_... 
dbl (4): start_lat, start_lng, end_lat, end_lng 
dttm (2): started_at, ended_at 

ℹ Use `spec()` to retrieve the full column specification for this data. 
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message. 

Rows: 181806 Columns: 13 ── Column specification ──────────────────────────────────────────────────────── 
Delimiter: "," 
chr (7): ride_id, rideable_type, start_station_name, start_station_id, end_... 
dbl (4): start_lat, start_lng, end_lat, end_lng 
dttm (2): started_at, ended_at 

ℹ Use `spec()` to retrieve the full column specification for this data. 
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message. 

Rows: 190301 Columns: 13 ── Column specification ──────────────────────────────────────────────────────── 
Delimiter: "," 
chr (7): ride_id, rideable_type, start_station_name, start_station_id, end_... 
dbl (4): start_lat, start_lng, end_lat, end_lng 
dttm (2): started_at, ended_at 

ℹ Use `spec()` to retrieve the full column specification for this data. 
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message. 

Rows: 190445 Columns: 13 ── Column specification ──────────────────────────────────────────────────────── 
Delimiter: "," 
chr (7): ride_id, rideable_type, start_station_name, start_station_id, end_... 
dbl (4): start_lat, start_lng, end_lat, end_lng 
dttm (2): started_at, ended_at 

ℹ Use `spec()` to retrieve the full column specification for this data. 
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message. 

Rows: 258678 Columns: 13 ── Column specification ──────────────────────────────────────────────────────── 
Delimiter: "," 
chr (7): ride_id, rideable_type, start_station_name, start_station_id, end_... 
dbl (4): start_lat, start_lng, end_lat, end_lng 
dttm (2): started_at, ended_at 

ℹ Use `spec()` to retrieve the full column specification for this data. 
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message. 

Rows: 426590 Columns: 13 ── Column specification ──────────────────────────────────────────────────────── 
Delimiter: "," 
chr (7): ride_id, rideable_type, start_station_name, start_station_id, end_... 
dbl (4): start_lat, start_lng, end_lat, end_lng 
dttm (2): started_at, ended_at 

ℹ Use `spec()` to retrieve the full column specification for this data. 
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message. 

Rows: 604827 Columns: 13 ── Column specification ──────────────────────────────────────────────────────── 
Delimiter: "," 
chr (7): ride_id, rideable_type, start_station_name, start_station_id, end_... 
dbl (4): start_lat, start_lng, end_lat, end_lng 
dttm (2): started_at, ended_at 

ℹ Use `spec()` to retrieve the full column specification for this data. 
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message. 

Rows: 719618 Columns: 13 ── Column specification ──────────────────────────────────────────────────────── 
Delimiter: "," 
chr (7): ride_id, rideable_type, start_station_name, start_station_id, end_... 
dbl (4): start_lat, start_lng, end_lat, end_lng 
dttm (2): started_at, ended_at 

ℹ Use `spec()` to retrieve the full column specification for this data. 
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message. 

Rows: 767650 Columns: 13 ── Column specification ──────────────────────────────────────────────────────── 
Delimiter: "," 
chr (7): ride_id, rideable_type, start_station_name, start_station_id, end_... 
dbl (4): start_lat, start_lng, end_lat, end_lng 
dttm (2): started_at, ended_at 

ℹ Use `spec()` to retrieve the full column specification for this data. 
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

# Check whether the set of data frames are row-bindable compare_df_cols_same( 
trips_2022_08,trips_2022_09,trips_2022_10, trips_2022_11,trips_2022_12,trips_2023_01, trips_2023_02,trips_2023_03,trips_2023_04, trips_2023_05,trips_2023_06,trips_2023_07 )

TRUE

# Combine all data frames into a single data frame total_trips_original <- bind_rows( trips_2022_08,trips_2022_09,trips_2022_10, trips_2022_11,trips_2022_12,trips_2023_01, trips_2023_02,trips_2023_03,trips_2023_04, trips_2023_05,trips_2023_06,trips_2023_07 )

# Standardize & clean column names
total_trips_cleaned <- clean_names(total_trips_original)
colnames(total_trips_cleaned)

'ride_id''rideable_type''started_at''ended_at''start_station_name''start_station_id''end_station_name''end_station_id''start_lat''start_lng''end_lat''end_lng''member_casual'

head(total_trips_cleaned)

str(total_trips_cleaned)

spc_tbl_ [5,723,606 × 13] (S3: spec_tbl_df/tbl_df/tbl/data.frame)
 $ ride_id           : chr [1:5723606] "550CF7EFEAE0C618" "DAD198F405F9C5F5" "E6F2BC47B65CB7FD" "F597830181C2E13C" ...
 $ rideable_type     : chr [1:5723606] "electric_bike" "electric_bike" "electric_bike" "electric_bike" ...
 $ started_at        : POSIXct[1:5723606], format: "2022-08-07 21:34:15" "2022-08-08 14:39:21" ...
 $ ended_at          : POSIXct[1:5723606], format: "2022-08-07 21:41:46" "2022-08-08 14:53:23" ...
 $ start_station_name: chr [1:5723606] NA NA NA NA ...
 $ start_station_id  : chr [1:5723606] NA NA NA NA ...
 $ end_station_name  : chr [1:5723606] NA NA NA NA ...
 $ end_station_id    : chr [1:5723606] NA NA NA NA ...
 $ start_lat         : num [1:5723606] 41.9 41.9 42 41.9 41.9 ...
 $ start_lng         : num [1:5723606] -87.7 -87.6 -87.7 -87.7 -87.7 ...
 $ end_lat           : num [1:5723606] 41.9 41.9 42 42 41.8 ...
 $ end_lng           : num [1:5723606] -87.7 -87.6 -87.7 -87.7 -87.7 ...
 $ member_casual     : chr [1:5723606] "casual" "casual" "casual" "casual" ...
 - attr(*, "spec")=
  .. cols(
  ..   ride_id = col_character(),
  ..   rideable_type = col_character(),
  ..   started_at = col_datetime(format = ""),
  ..   ended_at = col_datetime(format = ""),
  ..   start_station_name = col_character(),
  ..   start_station_id = col_character(),
  ..   end_station_name = col_character(),
  ..   end_station_id = col_character(),
  ..   start_lat = col_double(),
  ..   start_lng = col_double(),
  ..   end_lat = col_double(),
  ..   end_lng = col_double(),
  ..   member_casual = col_character()
  .. )
 - attr(*, "problems")=<externalptr>

glimpse(total_trips_cleaned)

Rows: 5,723,606
Columns: 13
$ ride_id            <chr> "550CF7EFEAE0C618", "DAD198F405F9C5F5", "E6F2BC47B6…
$ rideable_type      <chr> "electric_bike", "electric_bike", "electric_bike", …
$ started_at         <dttm> 2022-08-07 21:34:15, 2022-08-08 14:39:21, 2022-08-…
$ ended_at           <dttm> 2022-08-07 21:41:46, 2022-08-08 14:53:23, 2022-08-…
$ start_station_name <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ start_station_id   <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ end_station_name   <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ end_station_id     <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
$ start_lat          <dbl> 41.93, 41.89, 41.97, 41.94, 41.85, 41.79, 41.89, 41…
$ start_lng          <dbl> -87.69, -87.64, -87.69, -87.65, -87.65, -87.72, -87…
$ end_lat            <dbl> 41.94, 41.92, 41.97, 41.97, 41.84, 41.82, 41.89, 41…
$ end_lng            <dbl> -87.72, -87.64, -87.66, -87.69, -87.66, -87.69, -87…
$ member_casual      <chr> "casual", "casual", "casual", "casual", "casual", "…

summary(total_trips_cleaned)

   ride_id          rideable_type        started_at                 
 Length:5723606     Length:5723606     Min.   :2022-08-01 00:00:00  
 Class :character   Class :character   1st Qu.:2022-09-28 13:56:43  
 Mode  :character   Mode  :character   Median :2023-02-16 13:53:51  
                                       Mean   :2023-02-01 23:55:22  
                                       3rd Qu.:2023-06-03 07:41:37  
                                       Max.   :2023-07-31 23:59:56  
                                                                    
    ended_at                   start_station_name start_station_id  
 Min.   :2022-08-01 00:05:00   Length:5723606     Length:5723606    
 1st Qu.:2022-09-28 14:12:20   Class :character   Class :character  
 Median :2023-02-16 14:04:56   Mode  :character   Mode  :character  
 Mean   :2023-02-02 00:13:43                                        
 3rd Qu.:2023-06-03 08:00:15                                        
 Max.   :2023-08-12 04:53:41                                        
                                                                    
 end_station_name   end_station_id       start_lat       start_lng     
 Length:5723606     Length:5723606     Min.   :41.64   Min.   :-87.92  
 Class :character   Class :character   1st Qu.:41.88   1st Qu.:-87.66  
 Mode  :character   Mode  :character   Median :41.90   Median :-87.64  
                                       Mean   :41.90   Mean   :-87.65  
                                       3rd Qu.:41.93   3rd Qu.:-87.63  
                                       Max.   :42.07   Max.   :-87.52  
                                                                       
    end_lat         end_lng       member_casual     
 Min.   : 0.00   Min.   :-88.16   Length:5723606    
 1st Qu.:41.88   1st Qu.:-87.66   Class :character  
 Median :41.90   Median :-87.64   Mode  :character  
 Mean   :41.90   Mean   :-87.65                     
 3rd Qu.:41.93   3rd Qu.:-87.63                     
 Max.   :42.18   Max.   :  0.00                     
 NA's   :6102    NA's   :6102

num_duplicate_rows <- sum(
duplicated(total_trips_cleaned$ride_id))
cat("Number of rows with duplicates:", num_duplicate_rows, "\n")

Number of rows with duplicates: 0

n_distinct(total_trips_cleaned)

5723606

sum(nchar(total_trips_cleaned$ride_id)!=16)

0

max_date <- total_trips_cleaned %>% summarise(max_date = max(started_at))
min_date <- total_trips_cleaned %>% summarise(min_date = min(started_at))
print(max_date)
print(min_date)

# A tibble: 1 × 1
  max_date           
  <dttm>             
1 2023-07-31 23:59:56
# A tibble: 1 × 1
  min_date           
  <dttm>             
1 2022-08-01 00:00:00

rows_before_mutation <- nrow(total_trips_cleaned)
na_count <- sapply(total_trips_cleaned, function(col) sum(is.na(col)))
print(na_count)

           ride_id      rideable_type         started_at           ended_at 
                 0                  0                  0                  0 
start_station_name   start_station_id   end_station_name     end_station_id 
            868772             868904             925008             925149 
         start_lat          start_lng            end_lat            end_lng 
                 0                  0               6102               6102 
     member_casual 
                 0

columns_to_mutate <- c("start_station_name", "start_station_id",
                       "end_station_name", "end_station_id")
total_trips_prepared <- total_trips_cleaned %>%
  mutate(across(all_of(columns_to_mutate), ~ replace_na(., "missing")))

missing_recount <- sapply(total_trips_prepared[columns_to_mutate],
                          function(col) sum(col == "missing"))
print(missing_recount)

start_station_name   start_station_id   end_station_name     end_station_id 
            868772             868904             925008             925149

total_trips_transformed <- total_trips_prepared %>%
  filter(started_at <= ended_at)

rows_filtered_out <- nrow(total_trips_prepared) - nrow(total_trips_transformed)

cat("Number of rows filtered out:", rows_filtered_out, "\n")
Number of rows filtered out: 121

missing_summary <- sapply(total_trips_transformed, function(col) sum(is.na(col)))

total_inputs <- nrow(total_trips_transformed)

percent_difference <- (missing_summary / total_inputs) * 100

distinct_responses <- sapply(total_trips_transformed, function(col) length(unique(col)))

summary_table <- data.frame(
  column = names(total_trips_transformed),
  missing_Count = missing_summary,
  total_Inputs = rep(total_inputs, length(total_trips_transformed)),
  percent_difference = percent_difference,
  distinct_responses = distinct_responses
)

print(summary_table)

                               column missing_Count total_Inputs
ride_id                       ride_id             0      5723485
rideable_type           rideable_type             0      5723485
started_at                 started_at             0      5723485
ended_at                     ended_at             0      5723485
start_station_name start_station_name             0      5723485
start_station_id     start_station_id             0      5723485
end_station_name     end_station_name             0      5723485
end_station_id         end_station_id             0      5723485
start_lat                   start_lat             0      5723485
start_lng                   start_lng             0      5723485
end_lat                       end_lat          6102      5723485
end_lng                       end_lng          6102      5723485
member_casual           member_casual             0      5723485
                   percent_difference distinct_responses
ride_id                     0.0000000            5723485
rideable_type               0.0000000                  3
started_at                  0.0000000            4820913
ended_at                    0.0000000            4833032
start_station_name          0.0000000               1802
start_station_id            0.0000000               1503
end_station_name            0.0000000               1800
end_station_id              0.0000000               1506
start_lat                   0.0000000             782347
start_lng                   0.0000000             740445
end_lat                     0.1066134              13868
end_lng                     0.1066134              13983
member_casual               0.0000000                  2

total_trips_transformed <- total_trips_transformed %>%
  mutate(
    date = as.Date(started_at),
    year = year(started_at),
    month = month(started_at, label = TRUE),
    day = wday(started_at, label = TRUE),
    hour = hour(started_at),
    duration_s = as.numeric(difftime(ended_at, started_at, units = "secs")),
    duration_h = round(duration_s / 2400,3),
    ride_distance_km = distGeo(
      matrix(c(total_trips_transformed$start_lng, total_trips_transformed$start_lat), ncol = 2),
      matrix(c(total_trips_transformed$end_lng, total_trips_transformed$end_lat), ncol = 2)
    ) / 1000
  )

glimpse(total_trips_transformed)

Rows: 5,723,485
Columns: 21
$ ride_id            <chr> "550CF7EFEAE0C618", "DAD198F405F9C5F5", "E6F2BC47B6…
$ rideable_type      <chr> "electric_bike", "electric_bike", "electric_bike", …
$ started_at         <dttm> 2022-08-07 21:34:15, 2022-08-08 14:39:21, 2022-08-…
$ ended_at           <dttm> 2022-08-07 21:41:46, 2022-08-08 14:53:23, 2022-08-…
$ start_station_name <chr> "missing", "missing", "missing", "missing", "missin…
$ start_station_id   <chr> "missing", "missing", "missing", "missing", "missin…
$ end_station_name   <chr> "missing", "missing", "missing", "missing", "missin…
$ end_station_id     <chr> "missing", "missing", "missing", "missing", "missin…
$ start_lat          <dbl> 41.93, 41.89, 41.97, 41.94, 41.85, 41.79, 41.89, 41…
$ start_lng          <dbl> -87.69, -87.64, -87.69, -87.65, -87.65, -87.72, -87…
$ end_lat            <dbl> 41.94, 41.92, 41.97, 41.97, 41.84, 41.82, 41.89, 41…
$ end_lng            <dbl> -87.72, -87.64, -87.66, -87.69, -87.66, -87.69, -87…
$ member_casual      <chr> "casual", "casual", "casual", "casual", "casual", "…
$ date               <date> 2022-08-07, 2022-08-08, 2022-08-08, 2022-08-08, 20…
$ year               <dbl> 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2022, 202…
$ month              <ord> Aug, Aug, Aug, Aug, Aug, Aug, Aug, Aug, Aug, Aug, A…
$ day                <ord> Sun, Mon, Mon, Mon, Sun, Mon, Mon, Sun, Sun, Sun, S…
$ hour               <int> 21, 14, 15, 2, 20, 13, 14, 20, 21, 23, 20, 11, 22, …
$ duration_s         <dbl> 451, 842, 644, 903, 352, 781, 536, 1077, 683, 669, …
$ duration_h         <dbl> 0.188, 0.351, 0.268, 0.376, 0.147, 0.325, 0.223, 0.…
$ ride_distance_km   <dbl> 2.7247194, 3.3321432, 2.4866899, 4.7012385, 1.38687…

ride_summary <- total_trips_transformed %>%
  group_by(member_casual) %>%
  summarise(total_rides = n()) %>%
  mutate(percentage = (total_rides / sum(total_rides)) * 100)

print(ride_summary)

# A tibble: 2 × 3
  member_casual total_rides percentage
  <chr>               <int>      <dbl>
1 casual            2169497       37.9
2 member            3553988       62.1

summary(total_trips_transformed$duration_h)

    Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
   0.000    0.136    0.240    0.459    0.428 1286.535

aggregate(total_trips_transformed$duration_h ~ total_trips_transformed$member_casual, FUN = mean)
aggregate(total_trips_transformed$duration_h ~ total_trips_transformed$member_casual, FUN = median)
aggregate(total_trips_transformed$duration_h ~ total_trips_transformed$member_casual, FUN = max)
aggregate(total_trips_transformed$duration_h ~ total_trips_transformed$member_casual, FUN = min)

A data.frame: 2 × 2
total_trips_transformed$member_casual	total_trips_transformed$duration_h
<chr>	<dbl>
casual	0.7035874
member	0.3096731
A data.frame: 2 × 2
total_trips_transformed$member_casual	total_trips_transformed$duration_h
<chr>	<dbl>
casual	0.296
member	0.213
A data.frame: 2 × 2
total_trips_transformed$member_casual	total_trips_transformed$duration_h
<chr>	<dbl>
casual	1286.535
member	38.992
A data.frame: 2 × 2
total_trips_transformed$member_casual	total_trips_transformed$duration_h
<chr>	<dbl>
casual	0
member	0

aggregate(total_trips_transformed$duration_h ~ total_trips_transformed$member_casual +
          total_trips_transformed$day, FUN = mean)

A data.frame: 14 × 3
total_trips_transformed$member_casual	total_trips_transformed$day	total_trips_transformed$duration_h
<chr>	<ord>	<dbl>
casual	Sun	0.8258949
member	Sun	0.3421641
casual	Mon	0.6817051
member	Mon	0.2958516
casual	Tue	0.6286331
member	Tue	0.2977303
casual	Wed	0.6018506
member	Wed	0.2943977
casual	Thu	0.5961304
member	Thu	0.2972516
casual	Fri	0.6846345
member	Fri	0.3088395
casual	Sat	0.8146626
member	Sat	0.3467641

average_dur <- total_trips_transformed %>%
  group_by(member_casual, day) %>%
  summarise(number_of_rides = n(),
            average_duration = mean(duration_h)) %>%
  arrange(member_casual, day) %>%
  ggplot(aes(x = day, y = average_duration, fill = member_casual)) +
  geom_col(position = "dodge") +
  labs(title = "Average Ride Duration per Week",
       x = "Day of the Week",
       y = "Average Duration (hours)",
       fill = "Member/Casual") 

print(average_dur)

total_riders <- total_trips_transformed %>%
  group_by(member_casual, day) %>%
  summarise(number_of_rides = n(),
            average_duration= mean(duration_h)) %>%
  arrange(member_casual, day) %>%
  ggplot(aes(x=day,y=number_of_rides, fill=member_casual))+
  geom_col(position = "dodge") +
  scale_y_continuous(labels = scales::comma) +
  labs(title = "Total Number of Rides per Day",
       x = "Day of the Week",
       y = "Number of Rides",
       fill = "Member/Casual") 

print(total_riders)

`summarise()` has grouped output by 'member_casual'. You can override using the
`.groups` argument.

casual_hourly <- total_trips_transformed %>%
  group_by(hour, member_casual) %>%
  summarise(number_of_rides = n()) %>%
  ggplot(aes(x = hour, y = number_of_rides, fill = member_casual)) +
  geom_bar(stat = "identity", position = "dodge") +
  labs(title = "Total Number of Rides per Hour",
       x = "Hour",
       y = "Number of Rides",
       fill = "Member/Casual") +
  theme_minimal() +
  scale_y_continuous(labels = scales::comma) 

print(casual_hourly)

`summarise()` has grouped output by 'hour'. You can override using the
`.groups` argument.

casual_monthly <- total_trips_transformed %>%
  group_by(month, member_casual) %>%
  summarise(number_of_rides = n()) %>%
  ggplot(aes(x = month, y = number_of_rides, fill = member_casual)) +
  geom_bar(stat = "identity", position = "dodge") +
  labs(title = "Total Number of Rides per Month",
       x = "Month",
       y = "Number of Rides",
       fill = "Member/Casual") +
  theme_minimal()+
  scale_y_continuous(labels = scales::comma) 

print(casual_monthly)

`summarise()` has grouped output by 'month'. You can override using the
`.groups` argument.

distance_month <- total_trips_transformed %>%
  group_by(month, member_casual) %>%
  filter(!is.na(ride_distance_km) | ride_distance_km != 0) %>%
  summarize(average_distance = mean(ride_distance_km)) %>%
  ggplot(aes(x = month, y = average_distance, 
             color = member_casual, group = member_casual)) +
  geom_line() +
  labs(title = "Average Distance per Month",
       x = "Month",
       y = "Average Distance",
       color = "Member/Casual") +
  theme_minimal()

print(distance_month)

`summarise()` has grouped output by 'month'. You can override using the
`.groups` argument.

distance_day_line <- total_trips_transformed %>%
  group_by(day, member_casual) %>%
  filter(!is.na(ride_distance_km) | ride_distance_km != 0) %>%
  summarize(average_distance = mean(ride_distance_km)) %>%
  ggplot(aes(x = day, y = average_distance, color = member_casual, 
             group = member_casual)) +
  geom_line() +
  labs(title = "Average Distance per Day",
       x = "Day",
       y = "Average Distance",
       color = "Member/Casual") +
  theme_minimal()

print(distance_day_line)

`summarise()` has grouped output by 'day'. You can override using the `.groups`
argument.

distance_hour_line <- total_trips_transformed %>%
  group_by(hour, member_casual) %>%
  filter(!is.na(ride_distance_km) | ride_distance_km != 0) %>%
  summarize(average_distance = mean(ride_distance_km)) %>%
  ggplot(aes(x = hour, y = average_distance, color = member_casual)) +
  geom_line() +
  labs(title = "Average Distance per Hour",
       x = "Hour",
       y = "Average Distance",
       color = "Member/Casual") +
  theme_minimal()

print(distance_hour_line)

`summarise()` has grouped output by 'hour'. You can override using the
`.groups` argument.

pie_chart_member <- total_trips_transformed %>%
  filter(member_casual == "member") %>%
  group_by(rideable_type) %>%
  summarise(count = n()) %>%
  mutate(percentage = count / sum(count) * 100) %>%
  arrange(desc(percentage)) %>%  
  mutate(position = cumsum(percentage) - percentage / 2) %>%  
  ggplot(aes(x = "", y = percentage, fill = rideable_type)) +
  geom_bar(stat = "identity", width = 1, position = position_stack(vjust = 0.5),
           color = "white") +
  geom_text(aes(label = ifelse(percentage %in% c(0, 25, 50, 100), "", 
                               paste0(round(percentage), "%"))),
            position = position_stack(vjust = 0.5),
            check_overlap = TRUE,
            color = "white") +  
  coord_polar(theta = "y") +
  scale_x_discrete(breaks = NULL) +  
  labs(title = "Bike Type Distribution for Member Riders",
       fill = "Bike Type") +
  theme_void() +
  theme(legend.position = "right")

print(pie_chart_member)

pie_chart_casual <- total_trips_transformed %>%
  filter(member_casual == "casual") %>%
  group_by(rideable_type) %>%
  summarise(count = n()) %>%
  mutate(percentage = count / sum(count) * 100) %>%
  arrange(desc(percentage)) %>%  
  mutate(position = cumsum(percentage) - percentage / 2) %>%  
  ggplot(aes(x = "", y = percentage, fill = rideable_type)) +
  geom_bar(stat = "identity", width = 1, position = position_stack(vjust = 0.5),
           color = "white") +
  geom_text(aes(label = ifelse(percentage %in% c(0, 25, 50, 100), "", 
                               paste0(round(percentage), "%"))),
            position = position_stack(vjust = 0.5),
            check_overlap = TRUE,
            color = "white") +  
  coord_polar(theta = "y") +
  scale_x_discrete(breaks = NULL) +  
  labs(title = "Bike Type Distribution for Casual Riders",
       fill = "Bike Type") +
  theme_void() +
  theme(legend.position = "right")

print(pie_chart_casual)

top_start_stations <- total_trips_transformed %>%
  group_by(member_casual, start_station_name) %>%
  filter(start_station_name != "missing") %>%
  summarise(total_rides = n()) %>%
  arrange(member_casual, desc(total_rides)) %>%
  group_by(member_casual) %>%
  top_n(10)

print(top_start_stations)

# List the top 10 end stations 
top_end_stations <- total_trips_transformed %>%
  group_by(member_casual, end_station_name) %>%
  filter(end_station_name != "missing") %>%
  summarise(total_rides = n()) %>%
  arrange(member_casual, desc(total_rides)) %>%
  group_by(member_casual) %>%
  top_n(10)

print(top_end_stations)

`summarise()` has grouped output by 'member_casual'. You can override using the
`.groups` argument.
Selecting by total_rides

# A tibble: 20 × 3
# Groups:   member_casual [2]
   member_casual start_station_name                 total_rides
   <chr>         <chr>                                    <int>
 1 casual        Streeter Dr & Grand Ave                  50025
 2 casual        DuSable Lake Shore Dr & Monroe St        30193
 3 casual        Michigan Ave & Oak St                    23119
 4 casual        Millennium Park                          22437
 5 casual        DuSable Lake Shore Dr & North Blvd       20973
 6 casual        Shedd Aquarium                           18706
 7 casual        Theater on the Lake                      16757
 8 casual        Wells St & Concord Ln                    14061
 9 casual        Dusable Harbor                           13933
10 casual        Indiana Ave & Roosevelt Rd               12346
11 member        Kingsbury St & Kinzie St                 25282
12 member        Clark St & Elm St                        23971
13 member        Clinton St & Washington Blvd             23101
14 member        Wells St & Concord Ln                    21505
15 member        University Ave & 57th St                 20876
16 member        Loomis St & Lexington St                 20525
17 member        Ellis Ave & 60th St                      19772
18 member        Wells St & Elm St                        19495
19 member        Clinton St & Madison St                  19178
20 member        Broadway & Barry Ave                     18679

`summarise()` has grouped output by 'member_casual'. You can override using the
`.groups` argument.
Selecting by total_rides
# A tibble: 20 × 3
# Groups:   member_casual [2]
   member_casual end_station_name                   total_rides
   <chr>         <chr>                                    <int>
 1 casual        Streeter Dr & Grand Ave                  52721
 2 casual        DuSable Lake Shore Dr & Monroe St        27567
 3 casual        Michigan Ave & Oak St                    24321
 4 casual        Millennium Park                          24130
 5 casual        DuSable Lake Shore Dr & North Blvd       23064
 6 casual        Theater on the Lake                      17863
 7 casual        Shedd Aquarium                           16629
 8 casual        Wells St & Concord Ln                    13586
 9 casual        Dusable Harbor                           12950
10 casual        Clark St & Armitage Ave                  12285
11 member        Kingsbury St & Kinzie St                 25487
12 member        Clinton St & Washington Blvd             24148
13 member        Clark St & Elm St                        23829
14 member        Wells St & Concord Ln                    22340
15 member        University Ave & 57th St                 21035
16 member        Loomis St & Lexington St                 20812
17 member        Clinton St & Madison St                  20242
18 member        Wells St & Elm St                        19611
19 member        Ellis Ave & 60th St                      19491
20 member        Broadway & Barry Ave                     18968

# Create a gradient color bar chart for casual riders
casual_bar_chart <- top_start_stations %>%
  filter(member_casual == "casual") %>%
  ggplot(aes(x = reorder(start_station_name, total_rides), y = total_rides, 
             fill = total_rides)) +
  geom_bar(stat = "identity") +  # No need to specify fill color here
  scale_fill_gradient(low = "lightblue", high = "darkblue") +  
  labs(title = "Top Start Stations for Casual Riders",
       x = "Start Station Name",
       y = "Total Rides") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1),
        legend.position = "none")  # Remove legend

print(casual_bar_chart)

# Create a gradient color bar chart for member riders
member_bar_chart <- top_start_stations %>%
  filter(member_casual == "member") %>%
  ggplot(aes(x = reorder(start_station_name, total_rides), y = total_rides, 
             fill = total_rides)) +
  geom_bar(stat = "identity") +  # No need to specify fill color here
  scale_fill_gradient(low = "peachpuff", high = "darkorange") +  
  labs(title = "Top Start Stations for Member Riders",
       x = "Start Station Name",
       y = "Total Rides") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1),
        legend.position = "none")  

print(member_bar_chart)