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.
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
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
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
Necesitamos crear nuevas columnas que se usarán posteriormente en el análisis:
# 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
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:
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.
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.
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
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
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
Realizar campañas publicitarias donde se promueva el uso de las bicicletas no solo con fines recreativos sino también para ir y volver del trabajo. Esto puede ser beneficioso no solo en términos de economía personal sino también de sostenibilidad urbana y ambiental.
Apuntar la campaña al público másculino de edad promedio 34 años.
Apuntar la campaña a residentes en las zonas correspondientes a las estaciones de inicio más usadas.