Este análisis forma parte de la evaluación final del módulo Ciencia de Datos I. La idea es llevar adelante un análisis exploratorio acerca de usuarios de Ecobici durante el año 2024. Los datos se encuentran disponibles en el sitio.
(Nota: Este trabajo se encuentra publicado en mi blog y en mi sitio de GitHub)
Generar visualizaciones que permitan entender las características sociodemográficas (en este caso edad) del universo de usuarios
Análizar las altas según las fechas o momentos del dia.
El primer paso es limpiar ambiente, cargar datos y librerías y generar summary y glimpse para tener una primera aproximación a nuestros datos.
#Limpiamos ambiente
rm(list = ls())
#Cargamos librerías y datos
library(tidyverse)
library(DT)
library(scales)
#Descarga de datos
#con internet
link <- "https://cdn.buenosaires.gob.ar/datosabiertos/datasets/transporte-y-obras-publicas/bicicletas-publicas/usuarios_ecobici_2024.csv"
df <- read_csv(link)
#sin internet
#df <- read.csv("../data/usuarios_ecobici_2024.csv")
summary(df) #hacemos una primera vista de la información
## ID_usuario genero_usuario edad_usuario fecha_alta
## Min. :1083538 Length:67615 Min. : 5.00 Min. :2024-01-01
## 1st Qu.:1100442 Class :character 1st Qu.: 22.00 1st Qu.:2024-01-25
## Median :1117345 Mode :character Median : 28.00 Median :2024-02-24
## Mean :1117345 Mean : 31.34 Mean :2024-02-25
## 3rd Qu.:1134249 3rd Qu.: 38.00 3rd Qu.:2024-03-27
## Max. :1151188 Max. :949.00 Max. :2024-05-01
## NA's :3
## hora_alta Customer.Has.Dni..Yes...No.
## Length:67615 Length:67615
## Class1:hms Class :character
## Class2:difftime Mode :character
## Mode :numeric
##
##
##
glimpse(df)
## Rows: 67,615
## Columns: 6
## $ ID_usuario <dbl> 1151021, 1151039, 1150886, 1151154, 115103…
## $ genero_usuario <chr> "MALE", "FEMALE", "OTHER", "FEMALE", "FEMA…
## $ edad_usuario <dbl> 28, 19, 18, 25, 55, 27, 19, 22, 56, 18, 27…
## $ fecha_alta <date> 2024-05-01, 2024-05-01, 2024-05-01, 2024-…
## $ hora_alta <time> 15:35:18, 15:55:54, 11:33:34, 19:05:03, 1…
## $ Customer.Has.Dni..Yes...No. <chr> "No", "No", "No", "No", "No", "No", "No", …
usuarios <- nrow(unique(df)) #generamos objetos para texto y grafico
desde <- min(df$fecha_alta) #generamos objetos para texto y grafico
hasta <- max(df$fecha_alta) #generamos objetos para texto y grafico
A partir de los análisis iniciales se observa que la base contiene 67615 usuarios registrados desde 2024-01-01 hasta 2024-05-01. El paso siguiente consiste en limpiar las variables que vamos a necesitar para generar las visualizaciones. Tambien mediante summary vemos que en el campo edad_usuario hay 3 valores faltantes y al menos un caso con valores ilógicos
table(df$edad_usuario)
##
## 5 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
## 2 1 2 9 3847 4307 3743 3509 3239 3229 2863 2871 2696 2523 2347 2243
## 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
## 2090 1927 1808 1727 1580 1461 1326 1238 1178 1008 1033 974 900 867 869 821
## 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
## 736 734 693 598 559 559 487 423 1647 393 301 287 236 210 170 192
## 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
## 143 132 113 91 90 80 70 56 44 49 49 30 28 23 26 24
## 78 79 80 81 82 83 84 85 86 87 89 91 92 97 113 123
## 18 15 10 4 9 6 2 6 2 3 2 2 3 1 1 1
## 129 131 134 139 149 236 373 829 831 832 939 942 949
## 2 1 1 1 1 1 1 1 2 1 2 1 1
Para un futuro análisis podria chequearse aquellos usuarios con edades mayores a 90 en la base de recorridos para si tienen actividad, por lo pronto vamos a quitara quellos casos mayores a 97. El paso siguiente consiste en proceder a la limpieza de los datos para generar las recodificaciones necesarias. Se observa que el campo género_usuario contiene categorías en ingles, con lo cual procedemos a cambiarlas para que estén en nuestro idioma. Tambien se recodifican hora y edad usuario para obtener categorías más útiles para gráficos y tablas.
df <- df %>%
filter(edad_usuario < 98) %>%
mutate("Genero" = case_when( genero_usuario == "MALE" ~ "Masculino",
genero_usuario == "FEMALE" ~ "Femenino",
TRUE ~ "Otro"), #recod género
"Mes" = month(fecha_alta, label = TRUE), #extraigo mes
"Hora" = hour(hora_alta),
"HoraRec" = case_when(Hora >= 0 & Hora < 6 ~ "Madrugada",
Hora >= 6 & Hora < 12 ~ "Mañana",
Hora >= 12 & Hora < 18 ~ "Tarde",
TRUE ~ "Noche" ),
"Dia" = wday(fecha_alta,label = TRUE, abbr = FALSE),#agrego etiqueta para dias
"EdadRec" = case_when(edad_usuario >= 0 & edad_usuario < 15 ~ "1. Menores de 15",
edad_usuario >= 15 & edad_usuario < 20 ~ "2. de 15 hasta 20",
edad_usuario >= 20 & edad_usuario < 25 ~ "3. de 20 hasta 25",
edad_usuario >= 25 & edad_usuario < 30 ~ "4. de 25 hasta 30",
edad_usuario >= 30 & edad_usuario < 35 ~ "5. de 30 hasta 35",
edad_usuario >= 35 & edad_usuario < 40 ~ "6. de 35 hasta 40",
edad_usuario >= 40 & edad_usuario < 45 ~ "7. de 40 hasta 45",
edad_usuario >= 45 & edad_usuario < 50 ~ "8. de 45 hasta 50",
edad_usuario >= 50 & edad_usuario < 55 ~ "9. de 50 hasta 55",
TRUE ~ "Mayores de 55"))#recod edad
mujeres <- df %>%
filter(Genero == "Femenino") %>%
nrow()
varones <- df %>%
filter(Genero == "Masculino") %>%
nrow()
otro <- df %>%
filter(Genero == "Otro") %>%
nrow()
ggplot(df, aes(x = edad_usuario))+
geom_histogram(bins = 50, fill ="#d1e5f0")+
labs(title = paste0("Distribución de altas ECOBICI ",desde," - ",hasta),
subtitle = "Cantidad de casos según edad",
caption = "Datos Abiertos CABA",
y = "Edad",
x = "Casos")+
scale_y_continuous(labels = comma)+
theme_minimal()+
theme(plot.title = element_text(hjust = 0.5),
plot.subtitle = element_text(hjust = 0.5),
text = element_text(family = "Verdana"))
Un primer histograma ya con el campo edad “limpio” muestra algunas características de la distribución. Veamos ahora cuantos casos tenemos por género y edad.
# recursos:
# https://rpubs.com/xlisouski/Tuneando_Tablas_DT
# https://help.displayr.com/hc/en-us/articles/360003127476-How-to-Create-Customized-Tables-Using-the-DT-R-Package#
# https://stackoverflow.com/questions/43739218/r-datatable-formatting-with-javascript
Tabla1 <- df %>%
group_by(EdadRec) %>%
summarise(Casos = n()) %>%
mutate(Porcentaje = round(Casos/sum(Casos),4)) %>%
mutate(Etiqueta_Porcentaje = paste0(Casos," ( % ", Porcentaje*100, ")")) %>%
rename(Edad = EdadRec)
Tabla1 %>%
select(!c(Etiqueta_Porcentaje)) %>%
datatable(rownames = FALSE,caption = 'Tabla 1: Altas por rango de edad.',
options = list(paging=FALSE,
searching=FALSE,
initComplete = JS(
"function(settings, json) {",
"$(this.api().table().header()).css({'background-color': '#2c698d', 'color': '#fff','font-family': 'Verdana'});
$(this.api().table().body()).css({'background-color': 'white', 'color': 'black','font-family': 'Verdana'});
}"))) %>%
formatRound(c("Casos"),0) %>%
formatPercentage(c("Porcentaje"),2) %>%
formatStyle("Porcentaje", background = styleColorBar(c(0,max(Tabla1$Porcentaje)), '#e3f6f5'),
backgroundSize = '100% 100%',
backgroundRepeat = 'no-repeat')%>%
formatStyle(columns = c(3),width = '300px')
Tabla1a <- df %>%
group_by(Genero) %>%
summarise(total = n()) %>%
mutate(porcentaje = round(total/sum(total)*100,2)) %>%
mutate(etiqueta = paste0(total," ( % ", porcentaje, ")"))
ggplot(Tabla1a,aes(x="",y=total, fill=Genero))+
geom_bar(stat = "identity",color="white",width = 1)+
scale_fill_manual(values = c("#fb7756","#1ac0c6","#facd60"))+
labs(title = paste0("Altas ECOBICI "),
subtitle = "Proporción según Sexo",
caption = "Datos Abiertos CABA",
y = "",
x = "")+
theme_minimal()+
scale_y_continuous(labels = comma)+
theme(plot.title = element_text( hjust = 0.5 , vjust = 0.5),
plot.subtitle = element_text( hjust = 0.5 , vjust = 0.5),
legend.position = "bottom",
legend.title = element_blank(),
text = element_text(family = "Verdana"))+
guides(fill = guide_legend(reverse = TRUE))+
coord_flip()+
geom_text(aes(label = etiqueta),
position=position_stack(vjust=0.5),
color = "white")
En esta figura puede verse que una mayor proporción de varones utiliza el servicio.
#Aca iria tabla de altas por mes y género
Tabla2 <- df %>%
group_by(Mes,Genero) %>%
summarise(total = n()) %>%
pivot_wider(names_from = Genero, values_from = total)
Tabla2 %>%
datatable(rownames = FALSE,caption = paste0('Tabla 2: Altas según género y mes. ',desde," - ",hasta),
options = list(paging=FALSE,
searching=FALSE,
initComplete = JS(
"function(settings, json) {",
"$(this.api().table().header()).css({'background-color': '#2c698d', 'color': '#fff','font-family': 'Verdana'});
$(this.api().table().body()).css({'background-color': 'white', 'color': 'black','font-family': 'Verdana'});
}")))
Podemos ver la misma info en formato gráfico, pero ahora visualizando lo ocurrido por dia.
#Aca iria evolutivo de altas por mes coloreado por género
Tabla2a <- df %>%
group_by(fecha_alta,Genero) %>%
summarise(total = n())
ggplot(Tabla2a,aes(x = fecha_alta, y = total, color=Genero ))+
geom_line()+
geom_point()+
scale_color_manual(values =c("#fb7756","#1ac0c6","#facd60"))+
labs(title = paste0("Altas según fecha y género ",desde," - ",hasta),
caption = "Datos Abiertos CABA",
y = "Fecha",
x = "Casos")+
theme_minimal()+
theme(plot.title = element_text(hjust = 0.5, family = "Verdana"),
plot.subtitle = element_text(hjust = 0.5),
text = element_text(family = "Verdana"),
legend.position = "bottom",
legend.title = element_blank())
Siguiendo esta lógica, tambien podemos ver la composición de casos mediante un gráfico de proporciones con la info de la tabla 1. Para un posterior análisis sería intersante plantear que ocurre en meses con climas menos favorables para la bicicleta, para ello puede ser util replicar este mismo análisis, pero utilzando datos de 2023 completo.
Por lo pronto puede apreciarse un descenso de casos en abril tal cual se muestra en el grafico siguiente:
Tabla3 <- df %>%
group_by(Mes,Genero) %>%
summarise(total = n()) %>%
mutate(porcentaje = round(total/sum(total)*100,2)) %>%
mutate(etiqueta = paste0(total," ( % ", porcentaje, ")"))
Tablaaux <- Tabla3 %>%
select(Mes) %>%
filter(!duplicated(Mes))
filas <- nrow(Tablaaux)
ggplot(Tabla3,aes(x="",y=total, fill=Genero))+
geom_bar(stat = "identity",color="white",width = 1)+
scale_fill_manual(values = c("#fb7756","#1ac0c6","#facd60"))+
labs(title = paste0("Altas ECOBICI "),
subtitle = "Proporción según Sexo",
caption = "Datos Abiertos CABA",
y = "",
x = "")+
theme_minimal()+
theme(plot.title = element_text( hjust = 0.5 , vjust = 0.5),
plot.subtitle = element_text( hjust = 0.5 , vjust = 0.5),
legend.position = "bottom",
legend.title = element_blank(),
text = element_text(family = "Verdana"))+
guides(fill = guide_legend(reverse = TRUE))+
coord_flip()+
facet_wrap(~Mes, nrow = filas)+#se wrapea por mes
geom_text(aes(label = etiqueta),
position=position_stack(vjust=0.5),
color = "white")
A continuación veremos como se distribuye el género según la edad bajo la lógica de una piramide de edad
#Aca iria piramide de edad
TotalCasos <- nrow(df) #se arma objeto con total
Tabla4 <- df %>%
select(EdadRec, Genero) %>%
group_by(EdadRec, Genero) %>%
summarise(Casos = n()) %>%
mutate(porc = Casos/TotalCasos*100) %>% #se divide sobre el total de los casos
mutate(porc2 =case_when(Genero == "Femenino" ~ porc,
Genero == "Masculino"~ as.double(porc*(-1))))
ggplot(Tabla4, aes(x= EdadRec,
y=porc2,
fill= Genero))+
geom_col(data = Tabla4 %>%
filter(Genero == "Masculino"), width = 0.5 , fill = "#1ac0c6")+ #columnas hombres
geom_col(data = Tabla4 %>%
filter(Genero == "Femenino"), width = 0.5 , fill = "#fb7756")+ #columnas mujeres
labs(title = paste0("Altas según edad "),
caption = "Datos Abiertos CABA",
y = "Hombres Mujeres", #se agrega eje y separado para mostrar referencias
x = "Edad")+
coord_flip()+
theme_minimal()+
# scale_x_discrete(limit = c("Menores de 10","de 10 hasta 20","de 20 hasta 30","de 30 hasta 40","de 40 hasta 50"))+ #se reordena las categorias
# scale_y_continuous(breaks = c(-8,-4,0,4,8), #se especifican los cortes
# labels = c("8%","4%","0","4%","8%"))+ #se especifican los nombres de los cortes
theme(plot.title = element_text( hjust = 0.5 , vjust = 0.5),
plot.subtitle = element_text( hjust = 0.5 , vjust = 0.5),
text = element_text(family = "Verdana"))
Puede apreciarse que el sector que mas altas realiza es el de 20 a 25 años, como ya vimos en la tabla 1.
Tabla5 <- df %>%
group_by(HoraRec,Genero) %>%
summarise(total = n()) %>%
mutate(porcentaje = round(total/sum(total)*100,2)) %>%
mutate(etiqueta = paste0(total," ( % ", porcentaje, ")"))
Tablaaux <- Tabla5 %>%
select(HoraRec) %>%
filter(!duplicated(HoraRec))
filas <- nrow(Tablaaux)
ggplot(Tabla5,aes(x="",y=total, fill=Genero))+
geom_bar(stat = "identity",color="white",width = 1)+
scale_fill_manual(values = c("#fb7756","#1ac0c6","#facd60"))+
labs(title = paste0("Altas ECOBICI "),
subtitle = "Proporción según Sexo y horario",
caption = "Datos Abiertos CABA",
y = "",
x = "")+
theme_minimal()+
scale_y_continuous(labels = comma)+
theme(plot.title = element_text( hjust = 0.5 , vjust = 0.5),
plot.subtitle = element_text( hjust = 0.5 , vjust = 0.5),
legend.position = "bottom",
legend.title = element_blank(),
text = element_text(family = "Verdana"))+
guides(fill = guide_legend(reverse = TRUE))+
coord_flip()+
facet_wrap(~HoraRec, nrow = filas)+
geom_text(aes(label = total),
position=position_stack(vjust=0.5),
color = "white")#se wrapea por periodo
Se aprecia que la tarde es el momento del día con mayor cantidad de altas.
A partir de este pequeño análisis, fue posible carcaterizar la población de usuarios del Programa Ecobici. Las visualizaciones y tablas construidas permitieron comprender mejor este universo a partir de las variables de las cuales se disponía y las herramientas que dispone R para llevar adelante esta tarea dejando ver el potencial para profundizar el análisis. Se pudo ver que el grueso de altas se produce durante la tarde y en la franja etaria 20 a 25 años. Tambien se observó que el volumen de altas decae en abril en relación a meses de mayor temperatura.