De datos en bruto a información útil para decidir con tidyverse
Dpto. de Fundamentos del Análisis Económico. Universidad de Alicante
RetailCorp, cadena con 12 tiendas en España, nos manda las mismas ventas de enero y febrero de 2023 en tres formas distintas:
(A)
| id_venta | id_tienda | mes | total |
|---|---|---|---|
| 222 | 1 | 1 | 252,6 |
| 83 | 2 | 1 | 757,3 |
| 54 | 2 | 2 | 232,1 |
| 406 | 2 | 2 | 325,6 |
(B)
| id_tienda | mes | ingresos |
|---|---|---|
| 1 | 1 | 14.674 |
| 1 | 2 | 7.668 |
| 2 | 1 | 10.502 |
| 2 | 2 | 10.355 |
(C)
| id_tienda | mes_1 | mes_2 |
|---|---|---|
| 1 | 14.674 | 7.668 |
| 2 | 10.502 | 10.355 |
En parejas, 3 minutos:
¿Qué es una fila en (A)? ¿y en (B)? ¿y en (C)?: ¿una venta? ¿una tienda-mes? ¿una tienda?
¿Con cuál se responde “¿cuánto vendió la tienda 1 en febrero?” sin calcular nada?
En (C), ¿mes_1 es una variable o es un valor de la variable mes?
Un gerente nos envía este Excel:
| tienda | ventas_ene | ventas_feb |
|---|---|---|
| Madrid Centro | 14.674 € | 7.668 € |
| Barcelona Diagonal | 10.502 € | 10.355 € |
| Total | 25.176 € | 18.023 € |
En parejas, 3 minutos: ¿qué falla en cada punto?
La fila Total: ¿es una observación del mismo tipo que las otras dos?
La columna ventas_ene: ¿es una variable o son dos cosas pegadas (mes y valor)?
La celda 14.674 €: ¿es un número o un texto? ¿podemos sumarla?
tidyverse está pensado para trabajar con datos ordenadosLa 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: las funciones auxiliares (elegir columnas por patrón, contar, recodificar, fechas, etc.) están en el Anexo: chuleta de funciones auxiliares; se consulta cuando hace falta, no se estudia.
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
mutate()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%
ventas |>
group_by(id_tienda) |>
mutate(ventas_tienda = sum(total)) |>
ungroup() |>
mutate(porcentaje = ventas_tienda / sum(total) * 100)
# `.groups = "drop"` es de summarize(), NO de mutate()
ventas |>
group_by(id_tienda) |>
summarize(ventas_tienda = sum(total), .groups = "drop") |>
mutate(porcentaje = ventas_tienda / sum(total) * 100)Con la tabla ventas y solo con lo visto hasta aquí:
Calcula los ingresos de 2023 de cada tienda y el porcentaje que representan sobre los ingresos totales de la cadena en 2023
¿Qué tienda tiene el porcentaje mayor? ¿Cuánto es?
Un compañero propone este código para (a). Ejecútalo: ¿qué devuelve? ¿por qué?
separate() divide, unite() combinainforme <- tibble(tienda_region = c("Madrid Centro/Centro",
"Alicante Playa/Levante"),
ventas = c(14674, 9310))
informe |>
separate(tienda_region, into = c("tienda", "region"), sep = "/")
informe |>
separate(tienda_region, into = c("tienda", "region"), sep = "/") |>
unite(etiqueta, region, tienda, sep = " - ")Formato ANCHO (informe de los gestores)
| tienda | Q1 | Q2 | Q3 | Q4 |
|---|---|---|---|---|
| Madrid | 145 | 158 | 151 | 169 |
| Barcelona | 152 | 164 | 156 | 175 |
| Valencia | 138 | 151 | 149 | 162 |
Formato LARGO (los mismos datos)
| tienda | trimestre | ventas |
|---|---|---|
| Madrid | Q1 | 145 |
| Madrid | Q2 | 158 |
| Madrid | Q3 | 151 |
| Madrid | Q4 | 169 |
| Barcelona | Q1 | 152 |
| … | … | … |
En parejas, 3 minutos:
¿Qué es una fila en la tabla ancha? ¿y en la larga? ¿Cuántas filas tiene cada una?
En la ancha, Q1 ¿es una variable o un valor? ¿Dónde está el trimestre en cada tabla?
¿Con cuál se puede responder “¿qué trimestres superan 160 en ventas?” con un filter()?
pivot_longer(): girar de ancho a largotabla a cambiar de forma
nombres o índices de las columnas a girar (representan valores)
nombre para la nueva variable que tendrá, como valores, las columnas a girar
nombre para la nueva variable que tendrá como valores las antiguas celdas
pivot_wider(): girar de largo a anchoPartimos de ventas_ancho (3 tiendas, columnas Q1 a Q4):
¿En qué trimestre vendió más la cadena entera? ¿Cuánto?
Prepara la tabla de presentación para el informe: una fila por tienda, los trimestres en columnas y una última columna Total con la venta anual de cada tienda
¿Con qué formato has hecho (a)? ¿Y (b)? ¿Por qué no al revés?
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
Una clave primaria y su externa asociada en otra tabla forman una relación: de uno-a-muchos, de uno-a-uno, de muchos-a-muchos, de muchos-a-uno
Antes de unir hay que saber qué es una fila en cada tabla: es lo que decide si la unión añade información o inventa filas
| tabla | una fila es… | filas | clave primaria |
|---|---|---|---|
ventas |
una venta | 5.000 | id_venta |
precios |
un producto en un periodo de vigencia | 276 | id_producto + desde |
objetivos |
una tienda en un mes de 2023 | 144 | id_tienda + año + mes |
ventas una fila con una fila. Lo que dice si la unión está bien no es el nombre de la función: es el número de filas antes y despuésTenemos 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 ordenNAQuiero la región de cada venta y no perder ni una venta (hay tiendas sin ventas, me da igual)
Quiero solo las ventas atendidas por un empleado (las ventas online no tienen id_empleado)
Quiero el margen medio de TODAS las categorías del catálogo, aunque alguna no tenga productos
ventas registra unidades; precio_unitario es el precio de hoy del catálogo. El del producto 7 subió el 1-sep-2023| id_producto | precio | desde | hasta |
|---|---|---|---|
| 7 | 174,58 | 2020-01-01 | 2021-12-31 |
| 7 | 188,55 | 2022-01-01 | 2023-08-31 |
| 7 | 211,18 | 2023-09-01 | 2024-12-31 |
id_producto, y contamos las filas antes y despuésid_producto y el periodorelationship detiene la unión en vez de duplicar| id_tienda | facturación (€) |
|---|---|
| 4 | 126.255,61 |
| 2 | 114.491,31 |
| 9 | 113.042,98 |
| … | … |
| 11 | 90.444,44 |
Total de la cadena: 1.251.667 €
Con el precio del catálogo habrían salido 1.268.551 €
La diferencia, 16.884 €, es un 1,3%: poco como porcentaje, bastante en la cuenta de resultados
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: deben coincidir todas
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")
objetivos no se identifica con una sola variable: hace falta tienda y mes
promociones_empleado no guarda el identificador global del empleado: guarda su número dentro de su tienda, que es como numeran las cadenas de verdad. El 3 de Madrid y el 3 de Valencia son dos personas distintasplantilla <- empleados |> filter(!is.na(id_tienda))
nrow(plantilla) # 85 empleados con tienda
# une con los bonos de otras tiendas
plantilla |>
left_join(promociones_empleado, by = "cod_empleado") |>
nrow() # 387
# la clave son las dos columnas
plantilla |>
left_join(promociones_empleado,
by = c("id_tienda" = "cod_tienda", "cod_empleado")) |>
nrow() # 85objetivos a las ventas y sumarY sin embargo la unión no ha duplicado ni una fila: 2.000 antes, 2.000 después
El objetivo de cada mes se ha copiado en todas las ventas de ese mes y sum() lo cuenta una vez por venta: la tienda 1 hizo 141 ventas en 2023, así que su objetivo sale casi 13 veces mayor
facturacion_mes <- ventas_2023_precio |>
group_by(id_tienda, año, mes) |>
summarize(facturacion = sum(cantidad * precio), .groups = "drop")
nrow(facturacion_mes) # 144 = 12 tiendas x 12 meses, el nivel de objetivos
cumplimiento <- facturacion_mes |>
left_join(objetivos, by = c("id_tienda", "año", "mes")) |>
mutate(cumplimiento = round(facturacion / objetivo * 100, 1))| id_tienda | mes | facturación | objetivo | cumplimiento |
|---|---|---|---|---|
| 1 | 1 | 15.336,91 | 15.600 | 98,3 |
| 1 | 2 | 7.806,36 | 8.500 | 91,8 |
| 1 | 3 | 6.366,60 | 5.700 | 111,7 |
En el año, 7 de las 12 tiendas cumplen: la 5 es la mejor (105,0%) y la 10 la peor (96,4%)
La regla: agregar hasta el nivel de la otra tabla y unir después. Al revés no da error, da un número sin sentido
Si una coincidencia no es única, se generan todas las combinaciones posibles (producto cartesiano) de las observaciones coincidentes
id_producto no es clave primaria de precios: 97 de los 150 productos tienen más de un periodo de precio
Comprobar la clave de la tabla que se une cuesta una línea; explicar después por qué la facturación salía un 77% mayor (2.219.322 € en vez de 1.251.667 €) cuesta bastante más
NOTA: sólo unir cuando sea necesario y siempre usar solo filas y variables estrictamente necesarias
Con ventas, precios y objetivos:
¿Cuántas filas tiene precios y cuántos productos tienen más de un periodo de precio? ¿Cuál es su clave primaria? Compruébalo con count()
Facturación real de diciembre de 2023 de cada tienda, a los precios vigentes, junto a su objetivo de diciembre y el porcentaje de cumplimiento, de mayor a menor. ¿Cuántas tiendas cumplieron?
Un compañero hace (b) uniendo objetivos antes de agregar. Ejecútalo: ¿por qué la tienda 1 cumple el 7,0% de su objetivo?
# el código del compañero, para el apartado c)
ventas |> filter(año == 2023, mes == 12) |>
left_join(precios, by = "id_producto") |>
filter(fecha >= desde, fecha <= hasta) |>
left_join(objetivos, by = c("id_tienda", "año", "mes")) |>
group_by(id_tienda) |>
summarize(facturacion = sum(cantidad * precio),
objetivo = sum(objetivo)) |>
mutate(cumplimiento = round(facturacion / objetivo * 100, 1))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 yinner_join(), full_join(), etc. pero es ineficientePueden 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
Con ventas, tiendas y clientes:
Ingresos de 2023 por región, con el número de ventas de cada una, ordenados de mayor a menor
¿Cuántos clientes de la tabla clientes no han comprado nunca? ¿Y cuántos sí?
En (a) has unido ventas con tiendas. Si por un error de carga la tienda 1 apareciera dos veces en tiendas, ¿qué le pasaría a tu respuesta? Compruébalo
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.
Datos Ordenados
Código Legible y reproducible
Scripts ordenados y comentados
Encadenar operación 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 en uniones (¿qué variables? (¿hay claves dupolicadas?))
Orden de operaciones incorrecto: ej., filtrar después de summarize() por una variable que ya no existe