Definición de la tarea de negocio

El objetivo de este análisis es proporcionar recomendaciones a Cyclistic sobre cómo aumentar la cantidad de usuarios casuales que se convierten en miembros anuales. Para ello, se analizarán patrones de uso en los viajes de bicicletas compartidas, comparando las diferencias en los comportamientos de los usuarios casuales y los miembros anuales.

Se responderán preguntas clave sobre las diferencias en los horarios de inicio y fin de los viajes, los patrones de uso a lo largo de los días de la semana, el género y edad de los usuarios y las estaciones más utilizadas.


Instalación y carga de paquetes necesarios

Para llevar a cabo el análisis, necesitamos instalar y cargar los siguientes paquetes:

# Instalar paquetes si no están presentes
if (!requireNamespace("dplyr", quietly = TRUE)) install.packages("dplyr")
if (!requireNamespace("ggplot2", quietly = TRUE)) install.packages("ggplot2")
if (!requireNamespace("lubridate", quietly = TRUE)) install.packages("lubridate")
if (!requireNamespace("tidyr", quietly = TRUE)) install.packages("tidyr")
if (!requireNamespace("gridExtra", quietly = TRUE)) install.packages("gridExtra")

# Cargar paquetes
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(ggplot2)
library(lubridate)
## 
## Attaching package: 'lubridate'
## The following objects are masked from 'package:base':
## 
##     date, intersect, setdiff, union
library(tidyr)
library(gridExtra)
## 
## Attaching package: 'gridExtra'
## The following object is masked from 'package:dplyr':
## 
##     combine

Carga y combinación de los datos

Los archivos CSV con los datos de los viajes se llaman Divvy_Trips_2019_Q1.csv y Divvy_Trips_2020_Q1.csv. A continuación, se cargan los datos y luego se combinan en un solo data frame.

# Cargar los datos
Trips_2019_Q1 <- read.csv("Divvy_Trips_2019_Q1.csv")
Trips_2020_Q1 <- read.csv("Divvy_Trips_2020_Q1.csv")

# Verificar la estructura de los datos
str(Trips_2019_Q1)
## 'data.frame':    365069 obs. of  12 variables:
##  $ trip_id          : int  21742443 21742444 21742445 21742446 21742447 21742448 21742449 21742450 21742451 21742452 ...
##  $ start_time       : chr  "2019-01-01 00:04:37" "2019-01-01 00:08:13" "2019-01-01 00:13:23" "2019-01-01 00:13:45" ...
##  $ end_time         : chr  "2019-01-01 00:11:07" "2019-01-01 00:15:34" "2019-01-01 00:27:12" "2019-01-01 00:43:28" ...
##  $ bikeid           : int  2167 4386 1524 252 1170 2437 2708 2796 6205 3939 ...
##  $ tripduration     : chr  "390.0" "441.0" "829.0" "1,783.0" ...
##  $ from_station_id  : int  199 44 15 123 173 98 98 211 150 268 ...
##  $ from_station_name: chr  "Wabash Ave & Grand Ave" "State St & Randolph St" "Racine Ave & 18th St" "California Ave & Milwaukee Ave" ...
##  $ to_station_id    : int  84 624 644 176 35 49 49 142 148 141 ...
##  $ to_station_name  : chr  "Milwaukee Ave & Grand Ave" "Dearborn St & Van Buren St (*)" "Western Ave & Fillmore St (*)" "Clark St & Elm St" ...
##  $ usertype         : chr  "Subscriber" "Subscriber" "Subscriber" "Subscriber" ...
##  $ gender           : chr  "Male" "Female" "Female" "Male" ...
##  $ birthyear        : int  1989 1990 1994 1993 1994 1983 1984 1990 1995 1996 ...
str(Trips_2020_Q1)
## 'data.frame':    426887 obs. of  13 variables:
##  $ ride_id           : chr  "EACB19130B0CDA4A" "8FED874C809DC021" "789F3C21E472CA96" "C9A388DAC6ABF313" ...
##  $ rideable_type     : chr  "docked_bike" "docked_bike" "docked_bike" "docked_bike" ...
##  $ started_at        : chr  "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  "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  "Western Ave & Leland Ave" "Clark St & Montrose Ave" "Broadway & Belmont Ave" "Clark St & Randolph St" ...
##  $ start_station_id  : int  239 234 296 51 66 212 96 96 212 38 ...
##  $ end_station_name  : chr  "Clark St & Leland Ave" "Southport Ave & Irving Park Rd" "Wilton Ave & Belmont Ave" "Fairbanks Ct & Grand Ave" ...
##  $ end_station_id    : int  326 318 117 24 212 96 212 212 96 100 ...
##  $ start_lat         : num  42 42 41.9 41.9 41.9 ...
##  $ start_lng         : num  -87.7 -87.7 -87.6 -87.6 -87.6 ...
##  $ end_lat           : num  42 42 41.9 41.9 41.9 ...
##  $ end_lng           : num  -87.7 -87.7 -87.7 -87.6 -87.6 ...
##  $ member_casual     : chr  "member" "member" "member" "member" ...
# Eliminar datos que no se usarán para evitar problemas de memoria
Trips_2019_Q1 <- Trips_2019_Q1 %>%
  select(-tripduration)
Trips_2020_Q1 <- Trips_2020_Q1 %>%
  select(-start_lat, -start_lng, -end_lat, -end_lng)

# Renombrar columnas en Trips_2019_Q1 para alinearlas con Trips_2020_Q1
Trips_2019_Q1 <- Trips_2019_Q1 %>%
  rename(
    ride_id = trip_id,
    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
  )

# Convertir la columna ride_id a character en Trips_2019_Q1
Trips_2019_Q1$ride_id <- as.character(Trips_2019_Q1$ride_id)

# Modificar valores de la columna 'member_casual' en Trips_2019_Q1
Trips_2019_Q1$member_casual <- ifelse(
  tolower(Trips_2019_Q1$member_casual) == "subscriber", "member", 
  ifelse(tolower(Trips_2019_Q1$member_casual) == "customer", "casual", NA)
  )

# Combinar ambos data frames
Trips_combined <- bind_rows(Trips_2019_Q1, Trips_2020_Q1)

# Verificar estructura del data frame combinado
str(Trips_combined)
## 'data.frame':    791956 obs. of  12 variables:
##  $ ride_id           : chr  "21742443" "21742444" "21742445" "21742446" ...
##  $ started_at        : chr  "2019-01-01 00:04:37" "2019-01-01 00:08:13" "2019-01-01 00:13:23" "2019-01-01 00:13:45" ...
##  $ ended_at          : chr  "2019-01-01 00:11:07" "2019-01-01 00:15:34" "2019-01-01 00:27:12" "2019-01-01 00:43:28" ...
##  $ bikeid            : int  2167 4386 1524 252 1170 2437 2708 2796 6205 3939 ...
##  $ start_station_id  : int  199 44 15 123 173 98 98 211 150 268 ...
##  $ start_station_name: chr  "Wabash Ave & Grand Ave" "State St & Randolph St" "Racine Ave & 18th St" "California Ave & Milwaukee Ave" ...
##  $ end_station_id    : int  84 624 644 176 35 49 49 142 148 141 ...
##  $ end_station_name  : chr  "Milwaukee Ave & Grand Ave" "Dearborn St & Van Buren St (*)" "Western Ave & Fillmore St (*)" "Clark St & Elm St" ...
##  $ member_casual     : chr  "member" "member" "member" "member" ...
##  $ gender            : chr  "Male" "Female" "Female" "Male" ...
##  $ birthyear         : int  1989 1990 1994 1993 1994 1983 1984 1990 1995 1996 ...
##  $ rideable_type     : chr  NA NA NA NA ...
# Eliminar los data frames originales
rm(Trips_2019_Q1, Trips_2020_Q1)

# Forzar la recolección de basura para liberar memoria
gc()
##            used  (Mb) gc trigger  (Mb) max used  (Mb)
## Ncells  3126847 167.0    5969888 318.9  3182574 170.0
## Vcells 19624838 149.8   32727294 249.7 32159235 245.4

Verificar calidad y consistencia de los datos

Datos que se revisan:

# Asegurarse de que no haya duplicados en 'ride_id'
sum(duplicated(Trips_combined$ride_id))
## [1] 0
# Verificar cuántos tipos de bicicleta existen
unique_bike_types <- Trips_combined %>%
  distinct(rideable_type) %>%
  pull(rideable_type)
  cat("Tipos de bicicleta únicos:\n", unique_bike_types, "\n")
## Tipos de bicicleta únicos:
##  NA docked_bike
# Confirmar que member_casual solo tenga dos categorías
unique(Trips_combined$member_casual)
## [1] "member" "casual"
# Convertir 'started_at' y 'ended_at' al formato POSIXct
Trips_combined <- Trips_combined %>%
  mutate(
    started_at = as.POSIXct(started_at, format = "%Y-%m-%d %H:%M:%S"),
    ended_at = as.POSIXct(ended_at, format = "%Y-%m-%d %H:%M:%S")
  )

# Identificar el nombre más frecuente para cada id de estación de salida
start_station_corrections <- Trips_combined %>%
  group_by(start_station_id, start_station_name) %>%
  summarize(count = n(), .groups = "drop") %>%
  group_by(start_station_id) %>%
  filter(count == max(count)) %>%
  slice(1) %>%
  select(start_station_id, official_name = start_station_name)

# Identificar el nombre más frecuente para cada id de estación de llegada
end_station_corrections <- Trips_combined %>%
  group_by(end_station_id, end_station_name) %>%
  summarize(count = n(), .groups = "drop") %>%
  group_by(end_station_id) %>%
  filter(count == max(count)) %>%
  slice(1) %>%
  select(end_station_id, official_name = end_station_name)

# Actualizar los nombres en el dataframe combinado
Trips_combined <- Trips_combined %>%
  left_join(start_station_corrections, by = "start_station_id") %>%
  mutate(start_station_name = ifelse(!is.na(official_name), official_name, start_station_name)) %>%
  select(-official_name) %>%
  left_join(end_station_corrections, by = "end_station_id") %>%
  mutate(end_station_name = ifelse(!is.na(official_name), official_name, end_station_name)) %>%
  select(-official_name)

# Verificar valores nulos
colSums(is.na(Trips_combined))
##            ride_id         started_at           ended_at             bikeid 
##                  0                  0                  0             426887 
##   start_station_id start_station_name     end_station_id   end_station_name 
##                  0                  0                  1                  0 
##      member_casual             gender          birthyear      rideable_type 
##                  0             426887             444910             365069
# Eliminar los data frames usados en la corrección para liberar memoria
rm(end_station_corrections, start_station_corrections)

# Forzar la recolección de basura para liberar memoria
gc()
##            used  (Mb) gc trigger  (Mb) max used  (Mb)
## Ncells  1780181  95.1    5969888 318.9  4227837 225.8
## Vcells 14144282 108.0   39352752 300.3 39200404 299.1

Creación de nuevas columnas

Necesitamos crear nuevas columnas que se usarán posteriormente en el análisis:

  • Una columna con el día de la semana en la que se inicia el viaje.
  • Una columna con la duración del viaje en minutos.
# Creación de una nueva columna con el día de la semana en la que se inicia el viaje
Trips_combined <- Trips_combined %>%
  mutate(    
    start_day_of_week = weekdays(started_at),  
  )

# Creación de una nueva columna con la duración de los viajes en minutos
Trips_combined$duration_minutes <- as.numeric(difftime(Trips_combined$ended_at, Trips_combined$started_at, units = "mins"))

# Verificar la estructura del data frame
head(Trips_combined)
##    ride_id          started_at            ended_at bikeid start_station_id
## 1 21742443 2019-01-01 00:04:37 2019-01-01 00:11:07   2167              199
## 2 21742444 2019-01-01 00:08:13 2019-01-01 00:15:34   4386               44
## 3 21742445 2019-01-01 00:13:23 2019-01-01 00:27:12   1524               15
## 4 21742446 2019-01-01 00:13:45 2019-01-01 00:43:28    252              123
## 5 21742447 2019-01-01 00:14:52 2019-01-01 00:20:56   1170              173
## 6 21742448 2019-01-01 00:15:33 2019-01-01 00:19:09   2437               98
##                    start_station_name end_station_id
## 1              Wabash Ave & Grand Ave             84
## 2              State St & Randolph St            624
## 3                Racine Ave & 18th St            644
## 4      California Ave & Milwaukee Ave            176
## 5 Mies van der Rohe Way & Chicago Ave             35
## 6          LaSalle St & Washington St             49
##                end_station_name member_casual gender birthyear rideable_type
## 1     Milwaukee Ave & Grand Ave        member   Male      1989          <NA>
## 2    Dearborn St & Van Buren St        member Female      1990          <NA>
## 3 Western Ave & Fillmore St (*)        member Female      1994          <NA>
## 4             Clark St & Elm St        member   Male      1993          <NA>
## 5       Streeter Dr & Grand Ave        member   Male      1994          <NA>
## 6       Dearborn St & Monroe St        member Female      1983          <NA>
##   start_day_of_week duration_minutes
## 1           Tuesday         6.500000
## 2           Tuesday         7.350000
## 3           Tuesday        13.816667
## 4           Tuesday        29.716667
## 5           Tuesday         6.066667
## 6           Tuesday         3.600000

Análisis de los datos

Promedio y desviación de la duración de viajes por tipo de usuario

Obtener el promedio y la desviación estándar de la duración de los viajes por tipo de usuario nos permitirá reconocer diferencias en los patrones de uso.

# Calcular el promedio y la desviación estándar por tipo de usuario (member_casual)
summary_stats <- Trips_combined %>%
  group_by(member_casual) %>%
  summarise(
    promedio = mean(duration_minutes, na.rm = TRUE),
    desviación = sd(duration_minutes, na.rm = TRUE)
  )

# Imprimir los resultados
print(summary_stats)
## # A tibble: 2 × 3
##   member_casual promedio desviación
##   <chr>            <dbl>      <dbl>
## 1 casual            84.8      1622.
## 2 member            13.3       273.

Este resultado indica que los usuarios casuales realizan en promedio viajes más largos y que la duración de sus viajes es más variable, en comparación con los viajes de los miembros anuales. Podemos visualizar esta diferencia de la siguiente manera:

Distribución de los viajes de acuerdo a su duración y a tipo de usuario

Vamos a crear un gráfico de barras o histograma para visualizar la distribución de los viajes de acuerdo a su duración y al tipo de usuario.

# Filtrar los datos para eliminar valores NA y fuera de rango
Trips_combined <- Trips_combined %>%
  filter(duration_minutes >= 1 & duration_minutes <= 100, !is.na(duration_minutes))

# Gráfico para usuarios casuales
plot_casual <- ggplot(Trips_combined %>% filter(member_casual == "casual"), aes(x = duration_minutes)) +
  geom_histogram(binwidth = 1, fill = "lightblue", color = "black", alpha = 0.7) +
  labs(title = "Usuarios Casuales",
       x = "Duración del viaje (minutos)", y = "Frecuencia") +
  scale_x_continuous(limits = c(1, 100)) +
  scale_y_continuous(limits = c(0, 65000)) +
  theme_minimal()

# Gráfico para usuarios miembros
plot_member <- ggplot(Trips_combined %>% filter(member_casual == "member"), aes(x = duration_minutes)) +
  geom_histogram(binwidth = 1, fill = "lightgreen", color = "black", alpha = 0.7) +
  labs(title = "Miembros Anuales",
       x = "Duración del viaje (minutos)", y = "Frecuencia") +
  scale_x_continuous(limits = c(1, 100)) +
  scale_y_continuous(limits = c(0, 65000)) +
  theme_minimal()

# Mostrar ambos gráficos en una sola vista
grid.arrange(plot_casual, plot_member, ncol = 2)
## Warning: Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).
## Removed 2 rows containing missing values or values outside the scale range
## (`geom_bar()`).

Esto sugiere que los usuarios casuales utilizan las bicicletas con fines recreativos, mientras que los miembros anuales utilizan las bicicletas para ir y volver del trabajo. Intentemos confirmar esto evaluando cómo se distribuyen los viajes por día de la semana para cada tipo de usuario.

Distribución de viajes por día de la semana

A continuación, vamos a crear un histograma para ver cómo se distribuyen los viajes por día de la semana.

# Asegurar que los días de la semana estén en el orden correcto
Trips_combined$start_day_of_week <- factor(Trips_combined$start_day_of_week, 
                                           levels = c("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"))

# Crear el histograma para visualizar la distribución de viajes por día de la semana
ggplot(Trips_combined, aes(x = start_day_of_week, fill = member_casual)) +
  geom_bar(position = "dodge") +
  labs(title = "Distribución de los viajes por día de la semana", 
       x = "Día de la semana", 
       y = "Frecuencia") +
  theme_minimal()

Esta visualización confirma que los usuarios casuales utilizan predominantemente las bicicletas con fines recreativos, ya que se aprecia una mayor frecuencia de uso en el fin de semana. Los miembros anuales utilizan predominantemente las bicicletas para ir y volver del trabajo, ya que se aprecia una mayor frecuencia de uso en los días laborables.

Predominancia de género entre usuarios casuales

Vamos a calcular cómo se distribuyen en términos de porcentaje los géneros entre los usuarios casuales.

# Calcular la distribución de géneros entre los usuarios casuales contando solo "Female" y "Male"
gender_distribution_casual <- Trips_combined %>%
  filter(member_casual == "casual", gender %in% c("Female", "Male")) %>%
  group_by(gender) %>%
  summarise(count = n()) %>%
  mutate(percentage = count / sum(count) * 100) %>%
  select(gender, percentage)

# Ver la tibble resultante
gender_distribution_casual
## # A tibble: 2 × 2
##   gender percentage
##   <chr>       <dbl>
## 1 Female       31.7
## 2 Male         68.3

Edad promedio de los usuarios casuales

Vamos a calcular la edad promedio de los usuarios casuales.

# Calcular la edad promedio de los usuarios casuales
edad_promedio_casual <- Trips_combined %>%
  filter(member_casual == "casual", !is.na(birthyear)) %>%
  mutate(edad = 2024 - birthyear) %>%  # Suponiendo que el año actual es 2024
  summarise(edad_promedio = mean(edad, na.rm = TRUE))

# Ver el resultado
edad_promedio_casual
##   edad_promedio
## 1      34.58639

Estaciones de inicio más utilizadas

Finalmente, vamos a identificar las estaciones de inicio más utilizadas.

# Identificar las 10 estaciones de inicio más utilizadas por los usuarios casuales
top_10_start_stations_casual <- Trips_combined %>%
  filter(member_casual == "casual", !is.na(start_station_name)) %>%
  group_by(start_station_name) %>%
  summarise(viajes = n()) %>%
  arrange(desc(viajes)) %>%  # Ordenar de mayor a menor cantidad de viajes
  top_n(10, viajes)  # Obtener las 10 estaciones con más viajes

# Ver el resultado
top_10_start_stations_casual
## # A tibble: 10 × 2
##    start_station_name           viajes
##    <chr>                         <int>
##  1 Lake Shore Dr & Monroe St      2626
##  2 Streeter Dr & Grand Ave        2619
##  3 Shedd Aquarium                 1805
##  4 Millennium Park                1319
##  5 Michigan Ave & Oak St           966
##  6 Dusable Harbor                  799
##  7 Adler Planetarium               798
##  8 Theater on the Lake             768
##  9 Michigan Ave & Washington St    743
## 10 Lake Shore Dr & North Blvd      581

Visualicemos el resultado anterior.

# Crear una visualización de las 10 estaciones de inicio más utilizadas por usuarios casuales
ggplot(top_10_start_stations_casual, aes(x = reorder(start_station_name, -viajes), y = viajes)) +
  geom_bar(stat = "identity", fill = "lightblue", color = "black", alpha = 0.7) +
  labs(title = "Top 10 Estaciones de Inicio Más Utilizadas por Usuarios Casuales",
       x = "Estación de Inicio", y = "Número de Viajes") +
  theme_minimal() +
  theme(axis.text.x = element_text(angle = 45, hjust = 1))  # Rotar etiquetas del eje x para mejor legibilidad


Hallazgos y recomendaciones

Hallazgos clave:

  • Los usuarios casuales utilizan las bicicletas con fines recreativos, mientras que los miembros anuales lo hacen para ir y volver del trabajo.
  • Los usuarios casuales son predominantemente de género másculino.
  • La edad promedio de los usuarios casuales es 35 años.
  • Se identificaron las 10 estaciones de inicio preferidas por los usuarios casuales.

Recomendaciones: