Introducción

Este documento es mi primer proyecto para poner en práctica mis habilidades como Data Analyst. Los ejercicios fueron entregados por la plataforma de estudios Coursera.

¿Qué veremos?

  1. Descripción del proyecto
  2. Carga de archivos
  3. Limpieza de datos
  4. Explorando los datos
  5. Visualización
  6. Conclusiones

Proyecto

Cyclistic es un servicio de renta ficticio de bicicletas públicas en Chicago que incluye 5.800 bicicletas y 600 estaciones. La directora de marketing cree que el éxito futuro de la empresa depende de incrementar la cantidad de membresías anuales. Para esto, necesitan conocer a los ciclistas ocasionales y cómo abordarlos publicitariamente.

¿Cómo lograr el éxito de un negocio de arriendo de bicicletas?

Preguntas fundamentales

  1. ¿Cuál es la diferencia entre usuarios casuales y miembros?

  2. ¿Por qué los ciclistas casuales querrían ser miembros?

  3. ¿Cómo puede Cyclistic usar los medios digitales para incitar a los ciclistas casuales a convertirse en miembros?

Paquetes necesarios

if(!require(tidyverse)) install.packages("tidyverse")
## Loading required package: tidyverse
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.4     ✔ readr     2.1.5
## ✔ forcats   1.0.0     ✔ stringr   1.5.1
## ✔ ggplot2   3.5.1     ✔ tibble    3.2.1
## ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
## ✔ purrr     1.0.4     
## ── 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
if(!require(ggplot2)) install.packages("ggplot2")
library(tidyverse)
library(ggplot2)
library(skimr)
library(lubridate)

Carga de archivos

# Establecemos la carpeta que contiene los archivos
setwd("/Users/buger/Documents/Análisis_de_datos/Caso_Cyclistic")  

# Obtener lista de archivos CSV
archivos <- list.files(pattern = "\\.csv$")

# Acá queremos verificar si las columnas de nuestros archivos son iguales

# Extraer nombres de columnas de cada archivo
columnas_por_archivo <- map(archivos, ~names(read_csv(.x, n_max = 0)))

# Crear un resumen
resumen_columnas <- tibble(
  archivo = archivos,
  columnas = map(columnas_por_archivo, ~paste(sort(.x), collapse = ", "))
)

# Mostrar diferencias entre columnas. Si indica nª 12, son iguales.
resumen_columnas %>% 
  count(columnas) %>% 
  arrange(desc(n))
## # A tibble: 1 × 2
##   columnas      n
##   <list>    <int>
## 1 <chr [1]>    12
# Unimos los archivos
# Una vez comprobado que son compatibles, unimos los archivos.
datos_combinados <- list.files(pattern = "\\d+-divvy-tripdata\\.csv$") %>% 
  map_dfr(~read_csv(.x, show_col_types = FALSE))

# Comprobamos nuestros datos
glimpse(datos_combinados)
## Rows: 5,779,568
## Columns: 13
## $ ride_id            <chr> "743252713F32516B", "BE90D33D2240C614", "D47BBDDE7C…
## $ rideable_type      <chr> "classic_bike", "electric_bike", "classic_bike", "c…
## $ started_at         <dttm> 2024-04-22 19:08:21, 2024-04-11 06:19:24, 2024-04-…
## $ ended_at           <dttm> 2024-04-22 19:12:56, 2024-04-11 06:22:21, 2024-04-…
## $ start_station_name <chr> "Aberdeen St & Jackson Blvd", "Aberdeen St & Jackso…
## $ start_station_id   <chr> "13157", "13157", "TA1307000107", "13157", "TA13070…
## $ end_station_name   <chr> "Desplaines St & Jackson Blvd", "Desplaines St & Ja…
## $ end_station_id     <chr> "15539", "15539", "13249", "15539", "TA1308000029",…
## $ start_lat          <dbl> 41.87773, 41.87772, 41.96167, 41.87773, 41.96161, 4…
## $ start_lng          <dbl> -87.65479, -87.65496, -87.65464, -87.65479, -87.654…
## $ end_lat            <dbl> 41.87812, 41.87812, 41.95606, 41.87812, 41.88683, 4…
## $ end_lng            <dbl> -87.64395, -87.64395, -87.66884, -87.64395, -87.622…
## $ member_casual      <chr> "member", "member", "member", "member", "member", "…

Limpieza y preparación de datos

# Creamos una columna para indicar el día, mes y hora del viaje
datos_combinados <- datos_combinados %>% 
  mutate(
  hour_of_day = hour(started_at),
  day_of_week = wday(started_at, label = TRUE),
  month_name = month(started_at, label = TRUE, abbr = FALSE))
# Agregamos una columna nueva con el tiempo de recorrido
datos_combinados <- datos_combinados %>%
  mutate(ride_length = as.numeric(difftime(ended_at, started_at, units = "mins"))) %>% 
  mutate(ride_length = round(ride_length, 2))
# Eliminamos los datos que pertenescan a la estación que no es real "HQ QR"
# además de los que tienen tiempo de recorrido negativo
datos_combinados <- datos_combinados[!(datos_combinados$start_station_name == "HQ QR" | datos_combinados$ride_length < 0),]

# Eliminamos las columnas que no sirven
datos_combinados <- datos_combinados %>% 
  select(-c(start_lat, start_lng, end_lat, end_lng))

Ya que tenemos listos nuestros datos, comenzamos con la observación.

# Revisamos si se incluyeron las columnas
colnames(datos_combinados)
##  [1] "ride_id"            "rideable_type"      "started_at"        
##  [4] "ended_at"           "start_station_name" "start_station_id"  
##  [7] "end_station_name"   "end_station_id"     "member_casual"     
## [10] "hour_of_day"        "day_of_week"        "month_name"        
## [13] "ride_length"
glimpse(datos_combinados)
## Rows: 5,779,391
## Columns: 13
## $ ride_id            <chr> "743252713F32516B", "BE90D33D2240C614", "D47BBDDE7C…
## $ rideable_type      <chr> "classic_bike", "electric_bike", "classic_bike", "c…
## $ started_at         <dttm> 2024-04-22 19:08:21, 2024-04-11 06:19:24, 2024-04-…
## $ ended_at           <dttm> 2024-04-22 19:12:56, 2024-04-11 06:22:21, 2024-04-…
## $ start_station_name <chr> "Aberdeen St & Jackson Blvd", "Aberdeen St & Jackso…
## $ start_station_id   <chr> "13157", "13157", "TA1307000107", "13157", "TA13070…
## $ end_station_name   <chr> "Desplaines St & Jackson Blvd", "Desplaines St & Ja…
## $ end_station_id     <chr> "15539", "15539", "13249", "15539", "TA1308000029",…
## $ member_casual      <chr> "member", "member", "member", "member", "member", "…
## $ hour_of_day        <int> 19, 6, 11, 18, 19, 16, 7, 18, 20, 15, 18, 17, 22, 1…
## $ day_of_week        <ord> Mon, Thu, Sat, Thu, Fri, Wed, Wed, Thu, Tue, Sat, T…
## $ month_name         <ord> April, April, April, April, April, April, April, Ap…
## $ ride_length        <dbl> 4.58, 2.95, 16.30, 3.77, 37.37, 5.13, 16.90, 6.03, …
summary(datos_combinados$ride_length)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max.    NA's 
##     0.0     5.7    10.0    18.3    17.9  1559.9 1091133
table(datos_combinados$day_of_week)
## 
##    Sun    Mon    Tue    Wed    Thu    Fri    Sat 
## 619107 641757 641223 695859 661645 691107 737560
# Son 5,779,391 de viajes registrados.
# El promedio de tiempo de viajes es de 18 minutos.
# Los sábados es cuando más se utiliza el servicio
# Frecuencia de tipos de bicicleta

datos_combinados %>% 
  count(rideable_type)
## # A tibble: 4 × 2
##   rideable_type          n
##   <chr>              <int>
## 1 classic_bike     2583585
## 2 electric_bike    2027985
## 3 electric_scooter   76688
## 4 <NA>             1091133
# La bicicleta clásica es la que más se utiliza, 
# seguida de la bicicleta eléctrica

# Frecuencia de tipos de usuarios

datos_combinados %>% 
  count(member_casual) %>% 
  mutate(porcentaje = n / sum(n) * 100)
## # A tibble: 3 × 3
##   member_casual       n porcentaje
##   <chr>           <int>      <dbl>
## 1 casual        1720682       29.8
## 2 member        2967576       51.3
## 3 <NA>          1091133       18.9
# Los miembros equivalen al 51.3%
# Los usuarios casuales 29.8%
# Promedio de viajes por miembros y no miembros

datos_combinados %>% 
  group_by(member_casual) %>% 
  summarise(mean(ride_length))
## # A tibble: 3 × 2
##   member_casual `mean(ride_length)`
##   <chr>                       <dbl>
## 1 casual                       27.7
## 2 member                       12.8
## 3 <NA>                         NA
# Tiempo promedio de uso por usuario
# Casual: 27 min
# Miembro: 12 min

# Promedio de viaje por tipo de usuario y día

datos_combinados %>% 
  group_by(member_casual, day_of_week) %>%  
  summarise(
    duracion_promedio = mean(ride_length, na.rm = TRUE),  
    .groups = "drop"  
  ) %>% 
  arrange(member_casual, day_of_week)
## # A tibble: 15 × 3
##    member_casual day_of_week duracion_promedio
##    <chr>         <ord>                   <dbl>
##  1 casual        Sun                      32.5
##  2 casual        Mon                      26.2
##  3 casual        Tue                      23.3
##  4 casual        Wed                      24.6
##  5 casual        Thu                      24.3
##  6 casual        Fri                      27.1
##  7 casual        Sat                      31.2
##  8 member        Sun                      14.4
##  9 member        Mon                      12.1
## 10 member        Tue                      12.2
## 11 member        Wed                      12.3
## 12 member        Thu                      12.3
## 13 member        Fri                      12.6
## 14 member        Sat                      14.3
## 15 <NA>          <NA>                    NaN
# Qué día se movilizan más

datos_combinados %>%
  mutate(dia_semana = wday(started_at, label = TRUE, week_start = 1)) %>%
  count(dia_semana, member_casual) %>%
  drop_na() %>% 
  arrange(desc(n))
## # A tibble: 14 × 3
##    dia_semana member_casual      n
##    <ord>      <chr>          <int>
##  1 Wed        member        483654
##  2 Tue        member        458193
##  3 Thu        member        453324
##  4 Mon        member        437917
##  5 Fri        member        429697
##  6 Sat        member        378504
##  7 Sat        casual        359056
##  8 Sun        member        326287
##  9 Sun        casual        292820
## 10 Fri        casual        261410
## 11 Wed        casual        212205
## 12 Thu        casual        208321
## 13 Mon        casual        203840
## 14 Tue        casual        183030
# Usuarios casuales: sábados.
# Miembros: miércoles.


# Frecuencia de viajes diarios
datos_combinados %>% 
  group_by(member_casual == "member", day_of_week) %>% 
  count(ride_length) %>% 
  drop_na() %>% 
  arrange(desc(n))
## # A tibble: 152,486 × 4
## # Groups:   member_casual == "member", day_of_week [14]
##    `member_casual == "member"` day_of_week ride_length     n
##    <lgl>                       <ord>             <dbl> <int>
##  1 TRUE                        Wed                4.88   505
##  2 TRUE                        Wed                4.9    500
##  3 TRUE                        Tue                4.17   493
##  4 TRUE                        Wed                4.87   490
##  5 TRUE                        Wed                4.92   483
##  6 TRUE                        Wed                5.72   483
##  7 TRUE                        Wed                4.63   477
##  8 TRUE                        Wed                4.45   476
##  9 TRUE                        Wed                4.7    475
## 10 TRUE                        Wed                5.02   473
## # ℹ 152,476 more rows
# Rutas frecuentes
datos_combinados %>% 
  group_by(start_station_name, end_station_name) %>% 
  count(member_casual) %>% 
  arrange(desc(n)) %>% 
  drop_na()
## # A tibble: 274,158 × 4
## # Groups:   start_station_name, end_station_name [179,410]
##    start_station_name                end_station_name        member_casual     n
##    <chr>                             <chr>                   <chr>         <int>
##  1 Streeter Dr & Grand Ave           Streeter Dr & Grand Ave casual         8705
##  2 DuSable Lake Shore Dr & Monroe St DuSable Lake Shore Dr … casual         7099
##  3 DuSable Lake Shore Dr & Monroe St Streeter Dr & Grand Ave casual         5203
##  4 State St & 33rd St                Calumet Ave & 33rd St   member         5104
##  5 Calumet Ave & 33rd St             State St & 33rd St      member         5083
##  6 Michigan Ave & Oak St             Michigan Ave & Oak St   casual         4597
##  7 Ellis Ave & 60th St               Ellis Ave & 55th St     member         3991
##  8 Ellis Ave & 55th St               Ellis Ave & 60th St     member         3758
##  9 Ellis Ave & 60th St               University Ave & 57th … member         3718
## 10 University Ave & 57th St          Ellis Ave & 60th St     member         3710
## # ℹ 274,148 more rows
# Los usuarios casuales parten y terminan en la misma estación.
# Posiblemente la usan solo para pasear

# Horas en las que más se usa la bicicleta
# Todos los usuarios
datos_combinados %>%
  group_by(member_casual) %>% 
  count(hour_of_day) %>%
  arrange(desc(n)) %>% 
  drop_na()
## # A tibble: 48 × 3
## # Groups:   member_casual [2]
##    member_casual hour_of_day      n
##    <chr>               <int>  <int>
##  1 member                 17 321298
##  2 member                 16 278068
##  3 member                 18 246997
##  4 member                  8 212871
##  5 member                 15 198519
##  6 member                 19 170537
##  7 member                  7 167112
##  8 casual                 17 166717
##  9 member                 12 162369
## 10 member                 14 161308
## # ℹ 38 more rows
# La hora pic es a las 17:00hrs

# Usuarios casuales
datos_combinados %>%
  filter(member_casual == "casual") %>% 
  group_by(member_casual) %>% 
  count(hour_of_day) %>%
  arrange(desc(n)) %>% 
  drop_na()
## # A tibble: 24 × 3
## # Groups:   member_casual [1]
##    member_casual hour_of_day      n
##    <chr>               <int>  <int>
##  1 casual                 17 166717
##  2 casual                 16 154186
##  3 casual                 18 140442
##  4 casual                 15 135513
##  5 casual                 14 124006
##  6 casual                 13 119889
##  7 casual                 12 115692
##  8 casual                 19 100338
##  9 casual                 11  99447
## 10 casual                 10  78101
## # ℹ 14 more rows
# Los miembros casuales también tienen como hora pic las 17:00 hrs.

# Uso del servicio mensualmente
# Miembros
datos_combinados %>% 
  group_by(month_name) %>% 
  count(member_casual) %>% 
  filter(member_casual == "member") %>% 
  arrange(member_casual, desc(n))
## # A tibble: 12 × 3
## # Groups:   month_name [12]
##    month_name member_casual      n
##    <ord>      <chr>          <int>
##  1 September  member        373058
##  2 August     member        356122
##  3 July       member        349961
##  4 October    member        329547
##  5 June       member        327342
##  6 May        member        310372
##  7 April      member        231991
##  8 November   member        201053
##  9 March      member        172144
## 10 December   member        116932
## 11 February   member        103125
## 12 January    member         95929
# Mayormente en Septiembre y poco en Enero

# Usuarios casuales
datos_combinados %>% 
  group_by(month_name) %>% 
  count(member_casual) %>% 
  filter(member_casual == "casual") %>% 
  arrange(member_casual, desc(n))
## # A tibble: 12 × 3
## # Groups:   month_name [12]
##    month_name member_casual      n
##    <ord>      <chr>          <int>
##  1 July       casual        262097
##  2 August     casual        259014
##  3 September  casual        258743
##  4 June       casual        239369
##  5 May        casual        190237
##  6 October    casual        179598
##  7 April      casual        108215
##  8 November   casual         77763
##  9 March      casual         70643
## 10 December   casual         32287
## 11 February   casual         22845
## 12 January    casual         19871
# Julio, agosto y Septiembre.

Explorando los datos

En total tenemos 5,779,391 viajes registrados.

Cómo se dividen los clientes:
Los miembros equivalen al 51.3%
Los usuarios casuales 29.8%

Los usuarios casuales parten y terminan en la misma estación. Posiblemente utilizan la bicicleta solo para pasear

Los usuarios casuales se mueven mayormente los sábados, mientras que los miembros, los miércoles.

Tiempo de uso promedio:
Usuarios casuales: 27 min.
Miembros: 12 min.

La hora pic del servicio es a las 17:00hrs. por parte de ambos usuarios.

Julio, agosto y Septiembre, son los meses donde más se usa el servicio, es decir, en verano.

Visualización

datos_combinados %>%
  filter(!is.na(member_casual) & !is.na(rideable_type)) %>%  # Excluye NA en ambas variables
  ggplot(aes(x = member_casual, fill = rideable_type)) +
  geom_bar(position = "dodge") +
  labs(
    title = "Uso de bicicletas por tipo de usuario",
    x = "Tipo de usuario", 
    y = "Número de viajes",
    fill = "Tipo de bicicleta"
  ) +
  theme_minimal() +
  labs(caption = "Fuente: Divvy Bikes, 2024")

datos_combinados %>%
  mutate(mes = floor_date(started_at, "month")) %>%
  count(mes, member_casual) %>%
  drop_na() %>%
  ggplot(aes(x = mes, y = n, color = member_casual)) +
  geom_line(linewidth = 1) +
  geom_point() +
  labs(title = "Uso anual por tipo de usuario",
       x = "Mes", y = "Viajes") +
  labs(caption = "Fuente: Divvy Bikes, 2024")

datos_combinados %>%
  mutate(dia_semana = wday(started_at, label = TRUE, week_start = 1)) %>%
  count(dia_semana, member_casual) %>%
  drop_na() %>% 
  ggplot(aes(x = dia_semana, y = n, fill = member_casual)) +
  geom_col(position = "dodge") +
  labs(title = "Uso semanal por tipo de usuario",
       x = "Día de la semana", y = "Viajes") +
  labs(caption = "Fuente: Divvy Bikes, 2024")

Conclusiones

Las principales caracteristicas de los usuarios casuales, a quienes queremos alcanzar, son:

Equivalen al 29% de los usuarios totales, usan en su mayoría la bicicleta para pasear y mayormente los sábados, a las 17:00hrs, entre Julio y Septiembre.

Se les puede ofrecer planes más flexibles para motivar el paseo durante el fin de semana o la movilización durante esos 3 meses.

Los datos han sido proporcionados por Motivate International Inc. bajo esta licencia.