Auditoría y limpieza de datos — Ejemplo ETL

Published

June 9, 2026

Note

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.

Introducción

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.

Code
library(tidyverse)
library(readxl)
library(knitr)
Code
# 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.

Code
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:

Code
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…

Problemas encontrados

A continuación se describen los problemas de calidad identificados en el dataset crudo.

Valores faltantes

Code
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.

Fechas con formato mixto

La columna fecha_orden es de tipo texto y mezcla dos formatos:

Code
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.

Columna producto inconsistente

El mismo producto aparece con múltiples representaciones: códigos SKU, nombres descriptivos, variantes de capitalización y diferencias tipográficas.

Code
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_orden

Code
devoluciones_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.

Code
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

Verificación: total_orden vs cantidad × precio_unitario

Code
discrepancias <- 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. ✓


Transformaciones aplicadas

Paso 1 — Estandarizar fecha_orden

Se 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.

Code
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"
Code
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"
  )

Paso 2 — Tratar NAs en region

Al no existir un patrón sistemático, los NAs se etiquetan como "Sin región" para mantener trazabilidad.

Code
ETL_clean <- ETL_clean |>
  mutate(region = if_else(is.na(region), "Sin región", region))

sum(is.na(ETL_clean$region))
[1] 0
Code
# 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

Paso 3 — Normalizar producto

Se construye una tabla de equivalencias que mapea cada variante al nombre canónico del producto.

Code
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 

Paso 4 — Marcar devoluciones excesivas

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.

Code
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

Dataset resultante

Code
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                 
Code
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>

Guardar dataset limpio

Code
write_csv(ETL_clean, "Ejemplo_ETL_limpio.csv")

Próximos pasos sugeridos

Las siguientes acciones requieren revisión manual o decisiones de negocio antes de automatizarse:

  1. 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.

  2. 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.

  3. 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.