Code
library(tidyverse)
library(readxl)
library(knitr)Este reporte fue generado usando IA bajo dirección humana general. Al momento de su generación, el contenido no ha sido revisado en forma exhaustiva por un analista humano.
Este reporte documenta el proceso de auditoría y limpieza aplicado al dataset Ejemplo_ETL, correspondiente a órdenes de venta de un retailer. El objetivo es identificar problemas de calidad en los datos crudos, aplicar las transformaciones necesarias y dejar un registro reproducible de cada decisión tomada.
library(tidyverse)
library(readxl)
library(knitr)# skip = 2 omite la fila de título del Excel antes de los encabezados reales
Ejemplo_ETL <- read_excel(
"/Users/ignacia/Downloads/caso_etl_retail.xlsx",
sheet = "Órdenes (datos crudos)",
skip = 2
)El dataset original contiene 840 filas y 11 columnas.
head(Ejemplo_ETL, 10)# A tibble: 10 × 11
orden_id cliente_id fecha_orden canal producto cantidad precio_unitario
<dbl> <chr> <chr> <chr> <chr> <dbl> <dbl>
1 10001 CLI-0328 01/09/2024 Web Polera … 3 15990
2 10002 CLI-0053 09/07/2024 Marketplace POL-AZU… 3 15990
3 10003 CLI-0309 14/12/2024 Web Calceti… 1 7990
4 10004 CLI-0230 26/12/2024 Marketplace Pantalo… 1 29990
5 10005 CLI-0080 10/06/2024 App Chaquet… 1 49990
6 10006 CLI-0177 26/10/2024 Marketplace Pantalo… 1 29990
7 10007 CLI-0194 06/12/2024 Web Calceti… 2 7990
8 10008 CLI-0361 13/09/2024 Web polera … 3 15990
9 10009 CLI-0052 02/10/2024 Web Pantalo… 2 29990
10 10010 CLI-0108 08/13/2024 App POL-AZU… 3 15990
# ℹ 4 more variables: total_orden <dbl>, region <chr>, estado <chr>,
# monto_devuelto <dbl>
Las columnas disponibles son:
glimpse(Ejemplo_ETL)Rows: 840
Columns: 11
$ orden_id <dbl> 10001, 10002, 10003, 10004, 10005, 10006, 10007, 10008…
$ cliente_id <chr> "CLI-0328", "CLI-0053", "CLI-0309", "CLI-0230", "CLI-0…
$ fecha_orden <chr> "01/09/2024", "09/07/2024", "14/12/2024", "26/12/2024"…
$ canal <chr> "Web", "Marketplace", "Web", "Marketplace", "App", "Ma…
$ producto <chr> "Polera azul talla M", "POL-AZUL-M", "Calcetines Pack …
$ cantidad <dbl> 3, 3, 1, 1, 1, 1, 2, 3, 2, 3, 2, 1, 3, 2, 1, 3, 3, 1, …
$ precio_unitario <dbl> 15990, 15990, 7990, 29990, 49990, 29990, 7990, 15990, …
$ total_orden <dbl> 47970, 47970, 7990, 29990, 49990, 29990, 15980, 47970,…
$ region <chr> NA, NA, "Coquimbo", "Los Lagos", "Valparaíso", NA, "O'…
$ estado <chr> "Entregado", "Devuelto", "Cancelado", "Entregado", "En…
$ monto_devuelto <dbl> NA, 47970, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 179…
A continuación se describen los problemas de calidad identificados en el dataset crudo.
nas_por_columna <- Ejemplo_ETL |>
summarise(across(everything(), ~sum(is.na(.)))) |>
pivot_longer(everything(), names_to = "columna", values_to = "NAs") |>
filter(NAs > 0) |>
mutate(porcentaje = round(NAs / nrow(Ejemplo_ETL) * 100, 1))
nas_por_columna# A tibble: 2 × 3
columna NAs porcentaje
<chr> <int> <dbl>
1 region 131 15.6
2 monto_devuelto 680 81
Solo dos columnas presentan valores faltantes:
region: 131 filas (15.6%). Al cruzar con canal y estado, los NAs se distribuyen de forma uniforme entre todos los grupos (12–18%), lo que sugiere ausencia aleatoria sin patrón sistemático.monto_devuelto: 680 filas. Este comportamiento es esperado: solo las órdenes con estado == "Devuelto" deben tener un monto de devolución asociado.La columna fecha_orden es de tipo texto y mezcla dos formatos:
partes_fecha <- strsplit(Ejemplo_ETL$fecha_orden, "/")
mes_posicion2 <- as.integer(sapply(partes_fecha, `[`, 2))
n_mm_dd <- sum(mes_posicion2 > 12, na.rm = TRUE)
n_dd_mm <- sum(mes_posicion2 <= 12, na.rm = TRUE)
tibble(
formato = c("DD/MM/YYYY (correcto)", "MM/DD/YYYY (a corregir)"),
filas_afectadas = c(n_dd_mm, n_mm_dd),
porcentaje = round(c(n_dd_mm, n_mm_dd) / nrow(Ejemplo_ETL) * 100, 1)
)# A tibble: 2 × 3
formato filas_afectadas porcentaje
<chr> <int> <dbl>
1 DD/MM/YYYY (correcto) 611 72.7
2 MM/DD/YYYY (a corregir) 229 27.3
Las fechas con el mes en posición 2 mayor a 12 son imposibles en formato DD/MM/YYYY (por ejemplo "08/13/2024"), lo que indica que están escritas en MM/DD/YYYY.
producto inconsistenteEl mismo producto aparece con múltiples representaciones: códigos SKU, nombres descriptivos, variantes de capitalización y diferencias tipográficas.
Ejemplo_ETL |>
count(producto, sort = TRUE) |>
rename(variante = producto, frecuencia = n)# A tibble: 19 × 2
variante frecuencia
<chr> <int>
1 polera azul M 57
2 CALC-PK3 52
3 Pantalón cargo negro L 52
4 PANT-CARGO-NG-L 51
5 Pantalon cargo negro L 49
6 POL-AZUL-M 48
7 Polera Azul M 48
8 Calcetines Pack 3 47
9 MOCH-URB-GR 46
10 CHAQ-IMP-S 44
11 Chaqueta impermeable S 43
12 Mochila Urbana Gris 42
13 Calcetines pack x3 40
14 Mochila urbana gris 39
15 Zapatilla running blanca 42 39
16 ZAP-RUN-BL-42 38
17 chaqueta impermeable s 38
18 Zapatilla Running Blanca 42 37
19 Polera azul talla M 30
Hay 19 valores únicos que corresponden a solo 6 productos reales.
monto_devuelto superior a total_ordendevoluciones_excesivas <- Ejemplo_ETL |>
filter(!is.na(monto_devuelto), monto_devuelto > total_orden)
nrow(devoluciones_excesivas)[1] 17
Hay 17 órdenes donde el monto devuelto supera el total cobrado, lo cual es lógicamente imposible. En varios casos el monto_devuelto es un múltiplo exacto del total_orden, sugiriendo un error de captura.
devoluciones_excesivas |>
select(orden_id, total_orden, monto_devuelto) |>
mutate(diferencia = monto_devuelto - total_orden)# A tibble: 17 × 4
orden_id total_orden monto_devuelto diferencia
<dbl> <dbl> <dbl> <dbl>
1 10139 89970 269910 179940
2 10167 7990 31960 23970
3 10242 179970 359940 179970
4 10245 89970 179940 89970
5 10266 149970 449910 299940
6 10316 47970 191880 143910
7 10463 7990 15980 7990
8 10579 31980 127920 95940
9 10597 59980 239920 179940
10 10600 31980 63960 31980
11 10684 23970 95880 71910
12 10694 29990 89970 59980
13 10713 47970 191880 143910
14 10732 59980 179940 119960
15 10772 31980 95940 63960
16 10785 29990 89970 59980
17 10826 31980 127920 95940
total_orden vs cantidad × precio_unitariodiscrepancias <- Ejemplo_ETL |>
filter(abs(total_orden - cantidad * precio_unitario) > 1)
nrow(discrepancias)[1] 0
No se encontraron discrepancias: todos los totales cuadran con cantidad × precio_unitario. ✓
fecha_ordenSe detectan las filas con formato MM/DD/YYYY (segundo componente > 12) y se intercambian los componentes día y mes antes de parsear a tipo Date.
ETL_clean <- Ejemplo_ETL |>
mutate(
partes = strsplit(fecha_orden, "/"),
p1 = as.integer(sapply(partes, `[`, 1)),
p2 = as.integer(sapply(partes, `[`, 2)),
anio = sapply(partes, `[`, 3),
# p2 > 12: formato MM/DD/YYYY → mes = p1, día = p2
# p2 <= 12: formato DD/MM/YYYY → día = p1, mes = p2
fecha_orden = if_else(
p2 > 12,
as.Date(paste(anio, p1, p2, sep = "-"), format = "%Y-%m-%d"),
as.Date(paste(anio, p2, p1, sep = "-"), format = "%Y-%m-%d")
)
) |>
select(-partes, -p1, -p2, -anio)
# Verificación: sin NAs y rango coherente
list(
NAs_en_fecha = sum(is.na(ETL_clean$fecha_orden)),
rango = range(ETL_clean$fecha_orden)
)$NAs_en_fecha
[1] 0
$rango
[1] "2024-01-07" "2024-12-31"
ETL_clean |>
count(mes = floor_date(fecha_orden, "month")) |>
ggplot(aes(x = mes, y = n)) +
geom_col() +
labs(
title = "Órdenes por mes tras corrección de fechas",
x = NULL,
y = "Número de órdenes"
)
regionAl no existir un patrón sistemático, los NAs se etiquetan como "Sin región" para mantener trazabilidad.
ETL_clean <- ETL_clean |>
mutate(region = if_else(is.na(region), "Sin región", region))
sum(is.na(ETL_clean$region))[1] 0
# Revisión del patrón de NAs por canal y estado
Ejemplo_ETL |>
mutate(region_faltante = is.na(region)) |>
group_by(canal) |>
summarise(
total = n(),
nas_region = sum(region_faltante),
porcentaje_nas = round(nas_region / total * 100, 1)
)# A tibble: 3 × 4
canal total nas_region porcentaje_nas
<chr> <int> <int> <dbl>
1 App 345 59 17.1
2 Marketplace 159 21 13.2
3 Web 336 51 15.2
productoSe construye una tabla de equivalencias que mapea cada variante al nombre canónico del producto.
tabla_productos <- c(
"CALC-PK3" = "Calcetines Pack 3",
"Calcetines Pack 3" = "Calcetines Pack 3",
"Calcetines pack x3" = "Calcetines Pack 3",
"CHAQ-IMP-S" = "Chaqueta impermeable S",
"chaqueta impermeable s" = "Chaqueta impermeable S",
"Chaqueta impermeable S" = "Chaqueta impermeable S",
"MOCH-URB-GR" = "Mochila urbana gris",
"Mochila urbana gris" = "Mochila urbana gris",
"Mochila Urbana Gris" = "Mochila urbana gris",
"PANT-CARGO-NG-L" = "Pantalón cargo negro L",
"Pantalon cargo negro L" = "Pantalón cargo negro L",
"Pantalón cargo negro L" = "Pantalón cargo negro L",
"POL-AZUL-M" = "Polera azul M",
"polera azul M" = "Polera azul M",
"Polera Azul M" = "Polera azul M",
"Polera azul talla M" = "Polera azul M",
"ZAP-RUN-BL-42" = "Zapatilla running blanca 42",
"Zapatilla running blanca 42" = "Zapatilla running blanca 42",
"Zapatilla Running Blanca 42" = "Zapatilla running blanca 42"
)
ETL_clean <- ETL_clean |>
mutate(producto = tabla_productos[producto])
# Verificación: 6 valores únicos
table(ETL_clean$producto)
Calcetines Pack 3 Chaqueta impermeable S
139 125
Mochila urbana gris Pantalón cargo negro L
127 152
Polera azul M Zapatilla running blanca 42
183 114
Se agrega la columna flag_devolucion_excesiva para identificar las 17 órdenes donde monto_devuelto > total_orden. Estas filas no se corrigen automáticamente ya que requieren revisión manual.
ETL_clean <- ETL_clean |>
mutate(
flag_devolucion_excesiva = !is.na(monto_devuelto) & monto_devuelto > total_orden
)
ETL_clean |>
filter(flag_devolucion_excesiva) |>
select(orden_id, total_orden, monto_devuelto) |>
mutate(diferencia = monto_devuelto - total_orden)# A tibble: 17 × 4
orden_id total_orden monto_devuelto diferencia
<dbl> <dbl> <dbl> <dbl>
1 10139 89970 269910 179940
2 10167 7990 31960 23970
3 10242 179970 359940 179970
4 10245 89970 179940 89970
5 10266 149970 449910 299940
6 10316 47970 191880 143910
7 10463 7990 15980 7990
8 10579 31980 127920 95940
9 10597 59980 239920 179940
10 10600 31980 63960 31980
11 10684 23970 95880 71910
12 10694 29990 89970 59980
13 10713 47970 191880 143910
14 10732 59980 179940 119960
15 10772 31980 95940 63960
16 10785 29990 89970 59980
17 10826 31980 127920 95940
tibble(
aspecto = c(
"Filas",
"Columnas",
"Tipo de fecha_orden",
"NAs en region",
"Valores únicos en producto",
"Fechas con formato incorrecto",
"Devoluciones excesivas (sin flag)"
),
`Antes (crudo)` = c(
nrow(Ejemplo_ETL),
ncol(Ejemplo_ETL),
class(Ejemplo_ETL$fecha_orden),
sum(is.na(Ejemplo_ETL$region)),
n_distinct(Ejemplo_ETL$producto),
229,
17
),
`Después (limpio)` = c(
nrow(ETL_clean),
ncol(ETL_clean),
class(ETL_clean$fecha_orden),
sum(is.na(ETL_clean$region)),
n_distinct(ETL_clean$producto),
0,
0
)
)# A tibble: 7 × 3
aspecto `Antes (crudo)` `Después (limpio)`
<chr> <chr> <chr>
1 Filas 840 840
2 Columnas 11 12
3 Tipo de fecha_orden character Date
4 NAs en region 131 0
5 Valores únicos en producto 19 6
6 Fechas con formato incorrecto 229 0
7 Devoluciones excesivas (sin flag) 17 0
head(ETL_clean, 10)# A tibble: 10 × 12
orden_id cliente_id fecha_orden canal producto cantidad precio_unitario
<dbl> <chr> <date> <chr> <chr> <dbl> <dbl>
1 10001 CLI-0328 2024-09-01 Web Polera … 3 15990
2 10002 CLI-0053 2024-07-09 Marketplace Polera … 3 15990
3 10003 CLI-0309 2024-12-14 Web Calceti… 1 7990
4 10004 CLI-0230 2024-12-26 Marketplace Pantaló… 1 29990
5 10005 CLI-0080 2024-06-10 App Chaquet… 1 49990
6 10006 CLI-0177 2024-10-26 Marketplace Pantaló… 1 29990
7 10007 CLI-0194 2024-12-06 Web Calceti… 2 7990
8 10008 CLI-0361 2024-09-13 Web Polera … 3 15990
9 10009 CLI-0052 2024-10-02 Web Pantaló… 2 29990
10 10010 CLI-0108 2024-08-13 App Polera … 3 15990
# ℹ 5 more variables: total_orden <dbl>, region <chr>, estado <chr>,
# monto_devuelto <dbl>, flag_devolucion_excesiva <lgl>
write_csv(ETL_clean, "Ejemplo_ETL_limpio.csv")Las siguientes acciones requieren revisión manual o decisiones de negocio antes de automatizarse:
Revisar las 17 devoluciones excesivas: varios montos son múltiplos exactos del total_orden, lo que sugiere un error de entrada de datos (p. ej., se multiplicó en lugar de copiar). Confirmar el valor correcto con la fuente original.
Investigar los 131 registros sin región: determinar si corresponden a un canal o período específico, o si se pueden recuperar desde otra fuente.
Estandarizar el catálogo de productos: la tabla de equivalencias usada en este reporte fue construida manualmente. Se recomienda mantener un catálogo maestro de productos con SKU y nombre canónico como referencia única.