De datos en bruto a información útil para decidir con tidyverse
Dpto. de Fundamentos del Análisis Económico. Universidad de Alicante
El desafío real del análisis de datos:
80% del tiempo de trabajo “sucio” : limpieza y preparación
20% del tiempo: análisis y modelización
tidyverse incluye una colección de bibliotecas con herramientes eficientes para el proceso de “tratamiento de datos” (“data wrangling”)
El objetivo es obtener un conjunto de datos ordenado y limpio para realizar el análisis eficientemente y obtener información útil para la toma de decisiones
1.- Cada columna es una variable: mide el mismo atributo entre unidades
2.- Cada fila es una observación (caso): misma unidad a través de atributos
3.- Cada celda es un valor
¿Qué es una unidad en la tabla? ¿a qué nivel medimos la información?
a nivel de cada compra realizada por un cliente en cada fecha, o
a nivel de cliente (todas sus compras), de tienda (todas sus ventas), etc.
Tenemos información similar y no redundante en una misma tabla
Otras estructuras como esta pueden tener sentido para mostrar información (o por convenciones)
La visualización es atractiva, PERO sobran filas para analizar los datos: ej., total de personas con hijos y sin pareja entre 30 y 39 años
Contexto: Cadena de venta al por menor con 12 tiendas en España
Objetivo: Analizar rendimiento de ventas para toma de decisiones estratégicas
La mayoría de operaciones pueden realizarse combinando 5 “verbos”:
select(): selecciona columnas (variables)
filter(): filtra (extraer) filas
mutate(): crea nuevas columnas
arrange(): ordena filas
summarize(): crea resúmenes de la tabla
Más la tubería %>% o |>
y group_by()
NOTA: existe una colección de “chuletas” de R, p.e., para transformación.
Todos tienen como primer argumento un data frame, los siguientes describen qué hacer (con columnas o filas) y devuelven otro data frame
select()filter()%>% o |>datos %>% filter(condicion) equivale a filter(datos, condicion)
El anidamiento con tuberías sigue el flujo natural de lectura
# Por rango de columnas
ventas |> select(id_venta:id_tienda)
# Excluir columnas
ventas |> select(-descuento_porcentaje, -descuento_aplicado)
# Por patrón de nombre
ventas |> select(starts_with("id_"))
ventas |> select(ends_with("_porcentaje"))
ventas |> select(contains("descuento"))
# Por tipo de dato
ventas |> select(where(is.numeric))
ventas |> select(where(is.character))pull(): extrae una única columna, como vectormutate()lubridate)arrange()re-ordena las filas de un data frame (todas las columnas)
desc()summarize()KPIs <- ventas |>
summarize(
total_ventas = n(), # Volumen (núm. de filas)
ingresos_totales = sum(total), # Ingresos
ingresos_promedio = mean(total),
ingresos_mediano = median(total),
descuento_promedio = mean(descuento_porcentaje), # Descuentos
descuento_total = sum(descuento_aplicado),
unidades_vendidas = sum(cantidad), # Productos
clientes_unicos = n_distinct(id_cliente) # Clientes
# (núm. de filas distintas)
)group_by(): Análisis por GruposCambia el alcance: cada función actúa dentro de grupos, no sobre toda la tabla
Ejemplo: encontrar la fecha con la mayor venta por tienda
group_by() + summarize() = cambia el nivel de análisis, agregando, p.e., de transacciones a tiendas, productos, etc.
group_by() (cont.)group_by() + summarize() reduce filas: nueva tabla agregada a nivel de los gruposgroup_by() + mutate() mantiene filas: añade columnas calculadas por grupo a nivel de la tabla originalungroup()IMPORTANTE: No olvidar ungroup() o .groups = "drop" después de terminar operaciones agrupadas
Cálculo de Porcentajes Globales: sin desagrupar, el segundo sum(total) suma por tienda → siempre da 100%
slice(), slice_head()), aleatoriamente (slice_sample()), etc.distinct(): extrae sólo las filas únicas (una o varias variables)drop_na() y replace_na(): elimina/reemplaza filas con valores ausentestidyverseMuchas funciones son equivalentes a otras de R base:
parse_number(), parse_factor(), etc. por as.numeric(), as.factor(), etc.
bind_cols() y bind_rows() por cbind() y rbind()
if_else() y case_when() para ejecución condicional (ifelse())
cut_interval(), cut_number(), cut_width()lubridate: year(), month(), day(), quarter(), week()rename(): cambiar el nombre de una columnaacross(): aplica la misma transformación a múltiples columnasOperadores aritméticos (+, -, *, /, ^, %/%, %%) y lógicos (<, <=, >, >=, !=)
Funciones como log(), lag(), lead(), cumsum(), row_number() etc.
count(): cuenta los valores únicos de una o más variablesMedidas de centralidad y de dispersión: mean(x), median(x), sd(x), IQR(x)
Medidas de rango: min(x), quantile(x, 0.25), max(x)
Medidas de posición: first(x), nth(x, 2), last(x).
Sumas, productos, etc.
Conteos:
n(): observaciones totales (tamaño del grupo)
n_distinct(x): filas distintas en x
separate(): dividir una columna por caracter o posiciónunite(): combinar columnasFormato ANCHO:
| tienda | Q1 | Q2 | Q3 | Q4 |
|---|---|---|---|---|
| Madrid | 145 | 158 | 151 | 169 |
| Barcelona | 152 | 164 | 156 | 175 |
| Valencia | 138 | 151 | 149 | 162 |
(datos del informe enviado por gestores)
Formato LARGO:
| tienda | trimestre | ventas |
|---|---|---|
| Madrid | Q1 | 145 |
| Madrid | Q2 | 158 |
| Madrid | Q3 | 151 |
| Madrid | Q4 | 169 |
| Barcelona | Q1 | 152 |
| … | … | … |
Los trimestres son columnas
Cada tienda = 1 fila
Análisis de datos a menudo complicado
Los trimestres son valores
Cada tienda-trimestre = 1 fila
No adecuado para tablas de presentación final
pivot_longer(): girar de ancho a largotabla a cambiar de forma
nombres o índices (numéricos) de las columnas a girar: representan valores, no variables
nombre para la nueva variable que tendrá, como valores, esas antiguas columnas a girar
nombre para la nueva variable que tendrá como valores las antiguas celdas
pivot_wider(): girar de largo a anchotabla a cambiar de forma
nombre de la variable cuyos valores dan nombre a las nuevas columnas
nombre de la variable de cuyas celdas toman los valores las nuevas columnas
Según nuestro objetivos, podemos preferir formato ancho o largo
Problemas prácticos con formato ancho para analizar datos:
ggplot(ventas_ancho) +
geom_line(aes(x = 1:4, y = c(Q1[1], Q2[1], Q3[1], Q4[1])), color = "red") +
geom_line(aes(x = 1:4, y = c(Q1[2], Q2[2], Q3[2], Q4[2])), color = "blue") +
geom_line(aes(x = 1:4, y = c(Q1[3], Q2[3], Q3[3], Q4[3])), color = "green")
ggplot(ventas_largo, aes(x = trimestre, y = ventas,
color = tienda, group = tienda)) +
geom_line() + geom_point()Analizar datos suele implicar múltiples tablas
diferentes orígenes: ej., dptos. de empresa (personal, ventas, almacén)
almacenamiento más eficiente: elementos “similares” dentro de una tabla y diferentes entre ellas
Para poder combinar información los datos deben ser relacionales: cada par de tablas están relacionadas mediante identificadores comunes llamados claves
Primaria (o interna): identifican de forma única cada observación en una tabla. Puede ser una sola variable o múltiples
Secundaria (o externa): señala a la clave primaria de otra tabla
Tenemos estos datos.
¿Cómo conseguimos unirlos adecuadamente en R y en Excel?
cbind() o bind_columns() o copiando y pegando en Excel: nuevas columnas para filas en el mismo ordeninner_join(x, y) sólo incluye observaciones que coincidan en x y y.left_join(x, y)
right_join(x, y)
full_join(x, y)
NAs# Evolución de ventas por región
tabla_regional |>
mutate(periodo = ym(paste(año, mes, sep = "-"))) |>
ggplot() +
geom_line(aes(x = periodo, y = valor_ventas, color = region))
# Los dos periodos con más ventas medias de cada región
tabla_regional |>
group_by(region) |>
arrange(desc(ticket_medio)) |> slice(1:2)byLas claves, es decir, las variables que relacionan ambas tablas, se indican con
by = "varX", cuando la clave es una variable
by = c("varX", "varY"), cuando son varias variables
Si los nombre son distintos en cada tabla, se empareja x1 en la primera con y1 en la segunda con by = c("x1" = "y1", "x2" = "y2")
Si una coincidencia no es única, se generan todas las combinaciones posibles (producto cartesiano) de las observaciones coincidentes
Un uso habitual de las uniones: los nombres completos de categorías se ponen una tabla aparte y solo se une cuando se necesitan
Filtran las observaciones de la tabla de la izquierda basándose en si coinciden o no con una observación de la otra tabla
Se tiene un subconjunto de las filas de la tabla de la izquierda
semi_join(x, y) mantiene las observaciones en x que están en yanti_join(x, y) elimina las observaciones en x que están en yPueden ser una alternativa simple a usar filter() con filtrados complejos (que involucran varias variables), creando previamente tablas resumidas
-Ej., venta promedio de los 10 productos con más cantidad vendida
Trabajan con filas completas, comparando valores de cada variable.
Esperan que x e y tengan las mismas variables, y tratan las observaciones (filas) como elementos de un conjunto.
Útil cuando se quiere dividir un filtro complejo en piezas más simples.
df1 <- tibble(x = 1:2, y = c(1, 1))
df2 <- tibble(x = c(1,1), y = 1:2)
intersect(df1, df2) # solo filas tanto en df1 como en df2
union(df1, df2) # filas únicas en ambas tablas df1 y df2`
union_all(df1, df2) # todas las filas de df1 y df2, manteniendo duplicados
setdiff(df1, df2) # filas en df1, pero no en df2
setdiff(df2, df1) Datos Ordenados
Código Legible y reproducible
Scripts ordenados y comentados
Encadenar operacion con |> en un paso por línea
Inspeccionar resultados intermedios
Documentar decisiones de limpieza
Nombres descriptivos de variables
Guardar datos procesados
Errores comunes
No verificar claves duplicadas en uniones
Orden de operaciones incorrecto: ej., filtar después de summarize() por una variable que ya no existe