Caso de Estudio: Análisis de Cyclistic

Author

Santiago Gonzalez

OBJETIVO

El objetivo de este análisis es identificar las diferencias clave entre los usuarios ocasionales y los miembros anuales de Cyclistic para diseñar una estrategia de marketing efectiva.

El periodo analizado es marzo 2025-febrero 2026.

SOBRE LOS DATOS

Cyclistic, empresa ficticia de la ciudad de Chicago proporciona los datos mensuales sobre sus usuarios en una base de datos de internet. De alli se extrajieron los datos a trabajar durante el periodo de marzo 2025 y febrero 2026.

Se analizó un conjunto de datos masivo correspondiente a 12 meses de operación, con un total de 4.034.328 de registros. La limpieza se realizo mes a mes de manera indivual mediante Excel con Macros.

  • Metodología: La limpieza se realizó mediante herramientas de procesamiento de datos para filtrar variables redundantes, normalizar formatos de fecha (Día/Mes/Año), eliminar valores nulos, inconsistentes o atipicos y asegurar la integridad de la información.

  • Métricas Creadas: Se calcularon variables críticas para el análisis, incluyendo la duración del viaje en minutos (ride_length), el día de la semana (day_of_week) y el mes del año.

  • Interfaz de codigo utilizada: una vez normalizados los datos de cada mes de manera invidual, se cargo cada una de estas tablas en R, donde se juntaron los mismos para crear la tabla viajes_final, con mas de 4 millones de registros de viajes.

PREPARACION DE LOS DATOS

Cargamos las librerías y el dataset de los 12 meses de viajes.

La tabla viajes_final es la que contiene los registros de los 12 meses estudiados. Esta tiene 8 variables.

  1. Tipo de bicicleta

  2. Estacion de inicio

  3. Estacion final

  4. Hora de comenzo del viaje

  5. Hora de finalizacion del viaje

  6. Tipo de usuario

  7. Duracion del viaje

  8. Dia de la semana del viaje

Code
#| label: carga-datos
#| message: false
#| warning: false
#| echo: false

library(tidyverse)
# Cargamos la tabla 
viajes_final <- readRDS("viajes_final.rds")

ANALISIS EXPLORATORIO

En esta etapa, el objetivo fue identificar las tendencias y comportamientos que diferencian a los miembros anuales de los usuarios ocasionales. Para ello, me enfoqué en cuatro pilares fundamentales: la duración de los viajes, la temporalidad, la ubicación geográfica y la preferencia de equipos.

1. Análisis de la Duración de los Viajes

Para entender el uso que cada segmento le da a las bicicletas, calculé el promedio y la mediana de la duración de los viajes.

Code
#| label: carga-datos
#| message: false
#| warning: false
#| echo: false
# 1. Tabla de Duración
resumen_duracion <- viajes_final %>%
  group_by(member_casual) %>%
  summarise(
    Promedio_Minutos = round(mean(ride_length), 2),
    Mediana_Minutos = round(median(ride_length), 2),
    Cantidad_Viajes = n()
  )

knitr::kable(resumen_duracion, 
             col.names = c("Tipo de Usuario", "Promedio (min)", "Mediana (min)", "Total Viajes"))
Tipo de Usuario Promedio (min) Mediana (min) Total Viajes
casual 22.04 12.55 1423491
member 12.37 8.70 2610251

Hallazgo: los usuarios ocasionales suelen realizar viajes con una duración promedio significativamente mayor (aprox. 22 min) en comparación con los miembros (aprox. 12 min). Esto sugiere que el usuario ocasional utiliza el servicio con fines recreativos, mientras que el miembro lo utiliza para trayectos funcionales.

2. Comportamiento Temporal (Días y Meses)

Analicé el volumen de viajes distribuidos por día de la semana y mes del año para detectar picos de demanda.

-Comportamiento semanal:

Code
#| label: carga-datos
#| message: false
#| warning: false
#| echo: false
#| fig-cap: "Distribución de viajes por día de la semana"

library(scales) # Esta es la que hace la magia con los ejes

resumen_semanal <- viajes_final %>%
  count(member_casual, day_of_week)


ggplot(resumen_semanal, aes(x = day_of_week, y = n, fill = member_casual)) +
  geom_col(position = "dodge") +
  scale_y_continuous(labels = label_number(big.mark = ".", decimal.mark = ",")) +
  labs(title = "Viajes por Día de la Semana",
       x = "Día",
       y = "Total de Viajes",
       fill = "Tipo de Usuario") +
  theme_minimal() +
  scale_fill_manual(values = c("#2c3e50", "#3498db"))

Días de la semana: Se observó un incremento masivo de usuarios ocasionales durante los fines de semana, mientras que los miembros mantienen un flujo constante y superior de lunes a viernes.

-Comportamiento mensual:

Code
#| label: carga-datos
#| message: false
#| warning: false
#| echo: false
#| fig-cap: "Distribución de viajes por mes"
resumen_mensual <- viajes_final %>%
  mutate(mes = month(started_at, label = TRUE, abbr = FALSE)) %>%
  count(member_casual, mes)

# Aseguramos el orden de los meses (por si acaso)
resumen_mensual$mes <- factor(resumen_mensual$mes, 
                              levels = c("enero", "febrero", "marzo", "abril", "mayo", "junio", 
                                         "julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"))

ggplot(resumen_mensual, aes(x = as.numeric(mes), y = n, group = member_casual, color = member_casual)) +
  
  # 1. El sombreado del Verano
  annotate("rect", xmin = 5.5, xmax = 8.5, ymin = 0, ymax = Inf, 
           fill = "#BDC3C7", alpha = 0.2) + 
  
  # 2. El texto "TEMPORADA ALTA" (Más chico y limpio)
  annotate("text", x = 7, y = max(resumen_mensual$n) * 1.06, 
           label = "TEMPORADA ALTA", 
           fontface = "bold", size = 2.5, color = "#7F8C8D") + # Size bajado a 2.8
  
  # 3. Líneas y puntos
  geom_line(linewidth = 1.1, alpha = 0.9) +
  geom_point(size = 3) +
  
  # 4. Colores y Negritas
  scale_color_manual(values = c("casual" = "#4E95A4", "member" = "#2C3E50"),
                     labels = c("casual" = expression(bold("casual")), 
                                "member" = expression(bold("member")))) +
  
  # 5. Ejes y etiquetas
  scale_x_continuous(breaks = 1:12, labels = levels(resumen_mensual$mes)) +
  scale_y_continuous(labels = scales::comma) +
  
  labs(title = "Comportamiento mensual", 
       subtitle = "El volumen de usuarios casuales se dispara en los meses cálidos",
       x = "Mes", y = "Total de viajes", 
       color = "TIPO DE USUARIO") + 
  
  theme_minimal() +
  
  # 6. Meses verticales y ajustes finales
  theme(
    axis.text.x = element_text(angle = 90, vjust = 0.5, hjust = 1, face = "bold"),
    legend.title = element_text(face = "bold"),
    legend.text = element_text(size = 10),
    axis.title = element_text(face = "bold"),
    plot.title = element_text(face = "bold", size = 14)
  )

Estacionalidad: El volumen de viajes de usuarios ocasionales cae drásticamente en los meses de invierno, lo que refuerza la hipótesis del uso recreativo vinculado al clima.

3. Preferencia de Estaciones y Equipamiento

Identifiqué las 10 estaciones de inicio más populares para los usuarios ocasionales para localizar puntos estratégicos de conversión. Además, analicé la preferencia entre bicicletas clásicas, eléctricas y de carga.

-Preferencias de estaciones:

Code
#| label: carga-datos
#| message: false
#| warning: false
#| echo: false
#| fig-cap: "Distribución de viajes por zona"

# 4. Tabla de Estaciones Top (Para la publicidad física)
top_estaciones <- viajes_final %>%
  filter(member_casual == "casual", !is.na(start_station_name)) %>%
  count(start_station_name, name = "Total_Viajes") %>%
  arrange(desc(Total_Viajes)) %>%
  head(10)

knitr::kable(top_estaciones, 
             col.names = c("ESTACION", "Total Viajes"))
ESTACION Total Viajes
DuSable Lake Shore Dr & Monroe St 28991
Navy Pier 25705
Streeter Dr & Grand Ave 21598
Michigan Ave & Oak St 20279
DuSable Lake Shore Dr & North Blvd 17514
Millennium Park 17338
Shedd Aquarium 15840
Dusable Harbor 14123
Theater on the Lake 14100
Michigan Ave & 8th St 10249

Hallazgo: las 10 estaciones mas demandadas por los usuarios casuales se encuentran en la zona costera de la ciudad. Puntos excelentes para la implementacion de carteleria fisica de publicidad.

-Preferencias de tipo de bicicleta:

Code
#| label: carga-datos
#| message: false
#| warning: false
#| echo: false
#| fig-cap: "Distribución de viajes por tipo"


# 5. Tabla de Preferencia de Bicis (La que me pediste recién)
preferencia_bicis <- viajes_final %>%
  group_by(member_casual, rideable_type) %>%
  summarise(cantidad_viajes = n()) %>%
  mutate(porcentaje = round((cantidad_viajes / sum(cantidad_viajes)) * 100, 2))


# QUE TIPO PREFIEREN LOS USUARIOS

# 1. Preparar datos asegurando que todos los tipos cuenten
preferencia_limpia <- viajes_final %>%
  group_by(member_casual, rideable_type) %>%
  summarise(total = n(), .groups = 'drop') %>%
  group_by(member_casual) %>%
  mutate(porcentaje = total / sum(total))

# ... (todo el proceso de datos igual)

ggplot(preferencia_limpia, aes(x = 2, y = total, fill = rideable_type)) +
  geom_bar(stat = "identity", position = "fill", width = 1, color = "white") +
  coord_polar(theta = "y") +
  geom_text(aes(label = scales::percent(porcentaje, accuracy = 1)), 
            position = position_fill(vjust = 0.5), 
            color = "white", fontface = "bold", size = 3.5) +
  
  # --- CAMBIO 1: name = NULL para que no aparezca el título de la leyenda ---
  scale_fill_manual(values = c("electric_bike" = "#4E95A4", 
                               "classic_bike" = "#002D5A", 
                               "docked_bike" = "#E6FD6F"),
                    name = NULL, 
                    labels = c("classic_bike" = "Clásica", 
                               "electric_bike" = "Eléctrica", 
                               "docked_bike" = "Anclada")) +
  
  facet_wrap(~member_casual, labeller = as_labeller(c("casual" = "USUARIOS\nCASUALES", 
                                                      "member" = "MIEMBROS\nANUALES"))) +
  xlim(0.5, 2.5) +
  
  # --- CAMBIO 2: Agregamos el título que pediste ---
  labs(title = "",
       ) +
  
  theme_void() +
  theme(
    plot.title = element_text(face = "bold", size = 18, hjust = 0.5, color = "#2C3E50", margin = margin(b = 10)),
    plot.subtitle = element_text(size = 11, hjust = 0.5, color = "#7F8C8D", margin = margin(b = 25)),
    strip.text = element_text(face = "bold", size = 11, color = "#002D5A", lineheight = 1.1, margin = margin(b = 10)), 
    legend.position = "bottom",
    plot.margin = margin(20, 20, 20, 20, "pt")
  )

Como podemos observar, no hay diferencia significativa en la eleccion de los usuarios ocacionales por un tipo especifico de bicileta. Pero si hay una leve preferencia por la biciletas electricas, lo cual puede ser una buena idea lanzar promociones sobre este tipo de bicicletas en particular.