Introduction

For this lab, I continued working with Citi Bike trip data from New York City. I used one file from January 2025, so these results represent only part of the month’s trips, not all January ridership. I wanted to explore how classic and electric bike use differs between members and casual riders, whether the busiest stations show similar patterns, and how ride duration varies by bike type. I also used the opportunity to practice different ways of displaying relationships with ggplot2.

My goal is to show how bike type and membership relate to ridership patterns and trip duration. These visualizations are intended for people interested in understanding Citi Bike use in New York City, including riders and transportation planners.

Load and prepare the data

The data come from Citi Bike System Data. I first loaded the data and counted trips by membership category and bike type.

library(here)
library(tidyverse)
library(janitor)
library(patchwork)

janCitiBike <- read_csv(
  here("data", "202501-citibike-tripdata_1.csv")
)
bikeTypes <- janCitiBike %>%
  group_by(member_casual, rideable_type) %>%
  summarise(total_rides = n(), .groups = "drop")

Ridership by membership and bike type

I started with a grouped bar chart to compare the number of classic and electric bike rides for casual riders and members. In this subset, electric bikes account for more rides in both categories, and members account for many more rides overall. Because the plot shows raw counts, it describes how often each bike type was used rather than the proportion of riders who prefer each type.

##First Plot
ggplot(bikeTypes,
       aes(x = member_casual,
           y = total_rides,
           fill = rideable_type)) +
  geom_col(position = "dodge") +
  labs(
    title = "Citi Bike Ridership by Membership and Bike Type",
    subtitle = "Partial January 2025 Data",
    x = "Rider Type",
    y = "Number of Rides",
    fill = "Bike Type",
    caption = "Source: Citi Bike System Data"
  ) +
  scale_x_discrete(
    labels = c("casual" = "Casual Riders",
               "member" = "Members")
  ) +
  scale_fill_discrete(
    labels = c("classic_bike" = "Classic Bike",
               "electric_bike" = "Electric Bike")
  ) +
  scale_y_continuous(
    labels = scales::label_comma()
  ) +
  theme_minimal()
Figure 1. Number of classic and electric Citi Bike rides by membership category in a partial January 2025 dataset. Electric bikes accounted for more rides in both membership categories.

Figure 1. Number of classic and electric Citi Bike rides by membership category in a partial January 2025 dataset. Electric bikes accounted for more rides in both membership categories.

##Save out the plot
ggsave(
  here("output", "ridership_by_bike_type.png"),
  width = 9,
  height = 6,
  dpi = 300
)

Exploring alternative visualizations

I also experimented with using facet_wrap() to separate classic and electric bike ridership into two panels. While this approach makes it easier to examine each bike type individually, I preferred the original grouped bar chart because it allows for a more direct comparison between bike types within each membership category.

ggplot(bikeTypes,
       aes(x = member_casual,
           y = total_rides,
           fill = rideable_type)) +
  geom_col() +
  facet_wrap(
    ~rideable_type,
    labeller = as_labeller(
      c("classic_bike" = "Classic Bikes",
        "electric_bike" = "Electric Bikes")
    )
  ) +
  labs(
    title = "Citi Bike Ridership by Membership and Bike Type",
    subtitle = "Partial January 2025 Data",
    x = "Rider Type",
    y = "Number of Rides",
    caption = "Source: Citi Bike System Data"
  ) +
  scale_x_discrete(
    labels = c("casual" = "Casual Riders",
               "member" = "Members")
  ) +
  scale_fill_manual(
    values = c("classic_bike" = "#F8766D",
               "electric_bike" = "#00BFC4")
  ) +
  scale_y_continuous(labels = scales::label_comma()) +
  theme_minimal() +
  theme(legend.position = "none")
Figure 2. Citi Bike ridership by membership category, separated into panels for classic and electric bikes. Members accounted for more rides than casual riders in both panels, while electric bikes had higher ridership overall.

Figure 2. Citi Bike ridership by membership category, separated into panels for classic and electric bikes. Members accounted for more rides than casual riders in both panels, while electric bikes had higher ridership overall.

Relationship between classic and electric bike rides

I then used a scatterplot to look at whether stations with more classic bike rides also tend to have more electric bike rides. I included member starting stations with at least 50 rides in this subset. The points and fitted line suggest a positive relationship, although the spread increases at stations with higher trip counts. This does not necessarily mean that classic bike use causes electric bike use; both may be related to overall station activity.

##Third Plot
stationComparison <- janCitiBike %>%
  filter(member_casual == "member",
         !is.na(start_station_name),
         start_station_name != "") %>%
  count(start_station_name, rideable_type) %>%
  pivot_wider(
    names_from = rideable_type,
    values_from = n,
    values_fill = 0
  ) %>%
  mutate(total_rides = classic_bike + electric_bike) %>%
  filter(total_rides >= 50)

ggplot(stationComparison,
       aes(x = classic_bike,
           y = electric_bike)) +
  geom_point(alpha = 0.5, color = "steelblue") +
  geom_smooth(method = "lm", se = FALSE) +
  labs(
    title = "Relationship Between Classic and Electric Bike Use",
    subtitle = "Citi Bike Members | Partial January 2025 Data",
    x = "Number of Classic Bike Rides",
    y = "Number of Electric Bike Rides",
    caption = "Source: Citi Bike System Data"
  ) +
  scale_x_continuous(labels = scales::label_comma()) +
  scale_y_continuous(labels = scales::label_comma()) +
  theme_minimal()
Figure 4. Relationship between classic and electric bike rides across starting stations with at least 50 member rides. Stations with more classic bike rides generally also had more electric bike rides.

Figure 4. Relationship between classic and electric bike rides across starting stations with at least 50 member rides. Stations with more classic bike rides generally also had more electric bike rides.

##Save out the plot
ggsave(
  here("output", "station_bike_comparison.png"),
  width = 9,
  height = 6,
  dpi = 300
)

Mean and median ride duration

Finally, I calculated trip duration using the start and end timestamps. I excluded nonpositive durations and rides longer than 180 minutes before calculating the mean and median for each membership and bike type combination. I used a 180-minute cutoff to limit the influence of unusually long trips that may not reflect typical bike use. Comparing both statistics helps show how longer trips influence the average. Casual rides are longer than member rides in both categories. For members, the median durations for classic and electric bikes are nearly identical, even though the mean is slightly higher for classic bikes. These are durations, not distances traveled.

##trip duration information
rideDuration <- janCitiBike %>%
  mutate(
    started_at = as.POSIXct(started_at),
    ended_at = as.POSIXct(ended_at),
    duration_min = as.numeric(
      difftime(ended_at, started_at, units = "mins")
    )
  ) %>%
  filter(duration_min > 0,
         duration_min <= 180)

avgDuration <- rideDuration %>%
  group_by(member_casual, rideable_type) %>%
  summarise(
    avg_minutes = mean(duration_min, na.rm = TRUE),
    median_minutes = median(duration_min, na.rm = TRUE),
    .groups = "drop"
  )

avgDuration
## # A tibble: 4 × 4
##   member_casual rideable_type avg_minutes median_minutes
##   <chr>         <chr>               <dbl>          <dbl>
## 1 casual        classic_bike        16.3           11.9 
## 2 casual        electric_bike       12.6            8.69
## 3 member        classic_bike         9.39           6.78
## 4 member        electric_bike        8.89           6.89

I used patchwork to place the mean and median plots side by side with a shared legend and matching y-axis scales. This makes the differences between the two summaries easier to compare.

##Mean trip Duration
meanPlot <- ggplot(avgDuration,
       aes(x = member_casual,
           y = avg_minutes,
           fill = rideable_type)) +
  geom_col(position = "dodge") +
  labs(
    title = "Average Citi Bike Ride Duration by Bike Type",
    subtitle = "Partial January 2025 Data | Rides up to 180 minutes",
    x = "Rider Type",
    y = "Average Ride Duration (minutes)",
    fill = "Bike Type"
  ) +
  scale_x_discrete(
    labels = c("casual" = "Casual Riders",
               "member" = "Members")
  ) +
  scale_fill_discrete(
    labels = c("classic_bike" = "Classic Bike",
               "electric_bike" = "Electric Bike")
  ) +
  theme_minimal()

##Median Trip Duration
medianPlot <- ggplot(avgDuration,
       aes(x = member_casual,
           y = median_minutes,
           fill = rideable_type)) +
  geom_col(position = "dodge") +
  labs(
    title = "Median Citi Bike Ride Duration by Bike Type",
    subtitle = "Partial January 2025 Data | Rides up to 180 minutes",
    x = "Rider Type",
    y = "Median Ride Duration (minutes)",
    fill = "Bike Type"
  ) +
  scale_x_discrete(
    labels = c("casual" = "Casual Riders",
               "member" = "Members")
  ) +
  scale_fill_discrete(
    labels = c("classic_bike" = "Classic Bike",
               "electric_bike" = "Electric Bike")
  ) +
  theme_minimal()

#Put the 2 graphs together
durationComparison <- (
  meanPlot +
    labs(title = "Mean Ride Duration", subtitle = NULL) |
    medianPlot +
    labs(title = "Median Ride Duration", subtitle = NULL)
) +
  plot_layout(guides = "collect") +
  plot_annotation(
    title = "Comparing Mean and Median Citi Bike Ride Durations",
    subtitle = "Partial January 2025 Data | Rides up to 180 minutes",
    caption = "Source: Citi Bike System Data"
  ) &
  scale_y_continuous(limits = c(0, 20), breaks = seq(0, 20, 5)) &
  theme(legend.position = "bottom")

durationComparison
Figure 5. Comparison of mean and median ride durations by membership and bike type. Mean durations were higher than median durations in all four groups, suggesting that longer rides influenced the averages. Rides longer than 180 minutes were excluded.

Figure 5. Comparison of mean and median ride durations by membership and bike type. Mean durations were higher than median durations in all four groups, suggesting that longer rides influenced the averages. Rides longer than 180 minutes were excluded.

##Save out the plot
ggsave(
  here("output", "duration_comparison.png"),
  plot = durationComparison,
  width = 12,
  height = 6,
  dpi = 300
)

Conclusion

These visualizations show different aspects of Citi Bike use within the January file I analyzed. Electric bikes appear more frequently in the ride counts, including at the busiest member starting stations, while ride duration tells a different story: casual rides are longer, and the difference between classic and electric bikes depends on whether I use the mean or median. The plots also gave me practice choosing chart types based on the question I wanted to answer, refining labels and legends, and combining figures to make comparisons easier. I also tried to apply some of the design principles from this week’s readings by using consistent colors for classic and electric bikes across figures, simplifying labels, and choosing graph types that make the comparisons easier to understand.

The analysis is limited to a partial January 2025 file and should not be treated as representative of all Citi Bike trips or of seasonal ridership. The duration results also depend on the decision to exclude trips longer than 180 minutes.

Data source

Citi Bike. (2025). Citi Bike system data. https://citibikenyc.com/system-data