La limpieza de datos es una etapa crítica dentro de cualquier proceso de análisis estadístico, ciencia de datos o modelado predictivo. Una base de datos rara vez llega en condiciones perfectas para ser analizada. En muchos casos aparecen valores inválidos, categorías mal registradas, fechas inconsistentes, errores aritméticos, campos vacíos y variables numéricas almacenadas como texto. Por esta razón, antes de realizar análisis descriptivos, inferenciales o construir modelos, es indispensable desarrollar un flujo de depuración riguroso y verificable.
En este documento se trabaja con la base
dirty_cafe_sales.csv, la cual contiene registros de ventas
de una cafetería. El propósito es mostrar un proceso de limpieza de
datos claro, estructurado y reproducible en RStudio, explicando la
lógica y la importancia de cada bloque de código.
A lo largo del proceso se siguen normalmente tres fases fundamentales:
El valor de este enfoque es que permite conservar simultáneamente tres niveles del trabajo:
datos_raw: base original.datos_clean: base transformada y validada.datos_final: base lista para análisis.En primer lugar se carga la base de datos. El argumento
stringsAsFactors = FALSE evita que las variables de texto
se conviertan automáticamente en factores, lo que facilita la limpieza
posterior.
# -------------------------------------------------
# 1. Cargar los datos
# -------------------------------------------------
datos <- read.csv("dirty_cafe_sales.csv", stringsAsFactors = FALSE)
# Exploración básica
dim(datos)
## [1] 10000 8
names(datos)
## [1] "Transaction.ID" "Item" "Quantity" "Price.Per.Unit"
## [5] "Total.Spent" "Payment.Method" "Location" "Transaction.Date"
str(datos)
## 'data.frame': 10000 obs. of 8 variables:
## $ Transaction.ID : chr "TXN_1961373" "TXN_4977031" "TXN_4271903" "TXN_7034554" ...
## $ Item : chr "Coffee" "Cake" "Cookie" "Salad" ...
## $ Quantity : chr "2" "4" "4" "2" ...
## $ Price.Per.Unit : chr "2.0" "3.0" "1.0" "5.0" ...
## $ Total.Spent : chr "4.0" "12.0" "ERROR" "10.0" ...
## $ Payment.Method : chr "Credit Card" "Cash" "Credit Card" "UNKNOWN" ...
## $ Location : chr "Takeaway" "In-store" "In-store" "UNKNOWN" ...
## $ Transaction.Date: chr "2023-09-08" "2023-05-16" "2023-07-19" "2023-04-27" ...
head(datos)
## Transaction.ID Item Quantity Price.Per.Unit Total.Spent Payment.Method
## 1 TXN_1961373 Coffee 2 2.0 4.0 Credit Card
## 2 TXN_4977031 Cake 4 3.0 12.0 Cash
## 3 TXN_4271903 Cookie 4 1.0 ERROR Credit Card
## 4 TXN_7034554 Salad 2 5.0 10.0 UNKNOWN
## 5 TXN_3160411 Coffee 2 2.0 4.0 Digital Wallet
## 6 TXN_2602893 Smoothie 5 4.0 20.0 Credit Card
## Location Transaction.Date
## 1 Takeaway 2023-09-08
## 2 In-store 2023-05-16
## 3 In-store 2023-07-19
## 4 UNKNOWN 2023-04-27
## 5 In-store 2023-06-11
## 6 2023-03-31
Este bloque es esencial porque permite realizar un diagnóstico inicial de la base. Las funciones utilizadas tienen los siguientes propósitos:
read.csv(...): importa el archivo CSV.dim(datos): muestra el número de filas y columnas.names(datos): permite identificar los nombres de las
variables.str(datos): muestra la estructura y el tipo de dato de
cada variable.head(datos): permite observar rápidamente los primeros
registros.Sin esta revisión inicial no sería posible decidir qué variables requieren transformación.
Se crean dos objetos adicionales. Uno conserva la base tal como fue importada y el otro será el espacio de trabajo donde se aplicarán las transformaciones.
# -------------------------------------------------
# 2. Copia de seguridad y versión de trabajo
# -------------------------------------------------
datos_raw <- datos
datos_clean <- datos
Este paso responde a una buena práctica profesional: nunca limpiar
directamente la base original. Mantener datos_raw permite
auditar todo el proceso y comparar resultados. Trabajar con
datos_clean permite transformar sin perder la referencia
inicial.
Las variables Quantity, Price.Per.Unit y
Total.Spent deberían ser numéricas, pero con frecuencia
llegan almacenadas como texto. En consecuencia, primero se limpian y
luego se convierten a formato numérico.
# -------------------------------------------------
# 3. Limpieza de variables numéricas
# -------------------------------------------------
vars_num <- c("Quantity", "Price.Per.Unit", "Total.Spent")
for (v in vars_num) {
# Convertir a carácter
x <- as.character(datos_clean[[v]])
# Quitar espacios
x <- trimws(x)
# Eliminar comas
x <- gsub(",", "", x, fixed = TRUE)
# Estandarizar para detectar inválidos
x_upper <- toupper(x)
# Reemplazar valores inválidos por NA
x[x_upper == ""] <- NA
x[x_upper == "ERROR"] <- NA
x[x_upper == "UNKNOWN"] <- NA
# Convertir a numérico
datos_clean[[paste0(v, "_num")]] <- as.numeric(x)
}
# Verificación de variables numéricas
summary(datos_clean$Quantity_num)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 1.000 2.000 3.000 3.029 4.000 5.000 479
summary(datos_clean$Price.Per.Unit_num)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 1.00 2.00 3.00 2.95 4.00 5.00 533
summary(datos_clean$Total.Spent_num)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 1.000 4.000 8.000 8.924 12.000 25.000 502
sum(is.na(datos_clean$Quantity_num))
## [1] 479
sum(is.na(datos_clean$Price.Per.Unit_num))
## [1] 533
sum(is.na(datos_clean$Total.Spent_num))
## [1] 502
Este bloque es clave dentro de la fase de transformación. Cada línea cumple una función específica:
vars_num <- c(...): define las variables que deben
convertirse a formato numérico.for (v in vars_num): evita repetir tres veces el mismo
procedimiento.as.character(...): garantiza que la variable pueda
limpiarse como texto.trimws(x): elimina espacios al inicio y al final.gsub(",", "", ...): elimina comas que podrían impedir
la conversión.toupper(x): permite identificar valores inválidos sin
importar si vienen en mayúsculas o minúsculas.x[x_upper == "ERROR"] <- NA: transforma errores
textuales en valores faltantes reales.as.numeric(x): convierte la columna limpia en
numérica.paste0(v, "_num"): crea columnas auxiliares para no
alterar todavía las variables originales.Este enfoque es importante porque todavía estamos en una fase de diagnóstico y transformación, por lo que conviene mantener columnas auxiliares antes de construir la base final.
La variable Transaction.Date también requiere revisión.
Se limpia como texto, se convierten valores inválidos a NA
y luego se transforma a formato fecha.
# -------------------------------------------------
# 4. Limpieza de fecha
# -------------------------------------------------
d <- as.character(datos_clean$Transaction.Date)
d <- trimws(d)
d_upper <- toupper(d)
d[d_upper == ""] <- NA
d[d_upper == "UNKNOWN"] <- NA
d[d_upper == "ERROR"] <- NA
datos_clean$Transaction.Date <- d
datos_clean$Date <- as.Date(d)
# Verificación de fecha
summary(datos_clean$Date)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## "2023-01-01" "2023-04-01" "2023-07-02" "2023-07-01" "2023-10-02" "2023-12-31"
## NA's
## "460"
sum(is.na(datos_clean$Date))
## [1] 460
Las fechas son fundamentales en análisis de ventas, tendencias y series temporales. Por tanto:
as.character(...) evita problemas si la variable entró
con otro formato.trimws(...) elimina espacios sobrantes."", "UNKNOWN" y
"ERROR" a NA evita errores de
interpretación.as.Date(d) transforma la variable a un formato que R
puede usar para análisis temporal.La nueva columna Date es una variable auxiliar validada
que luego se conservará en la base final.
En una base de ventas, una verificación crítica consiste en comprobar si el gasto total coincide con la multiplicación entre cantidad y precio unitario.
# -------------------------------------------------
# 5. Verificación de consistencia aritmética
# -------------------------------------------------
datos_clean$Total_teorico <- datos_clean$Quantity_num * datos_clean$Price.Per.Unit_num
datos_clean$Dif_Total <- datos_clean$Total.Spent_num - datos_clean$Total_teorico
summary(datos_clean$Dif_Total)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 0 0 0 0 0 0 1456
# Filas con inconsistencias
errores_total <- datos_clean[
abs(datos_clean$Dif_Total) > 0.01 & !is.na(datos_clean$Dif_Total),
]
nrow(errores_total)
## [1] 0
errores_total[, c("Transaction.ID",
"Quantity_num",
"Price.Per.Unit_num",
"Total.Spent_num",
"Total_teorico",
"Dif_Total")]
## [1] Transaction.ID Quantity_num Price.Per.Unit_num Total.Spent_num
## [5] Total_teorico Dif_Total
## <0 rows> (or 0-length row.names)
# Corregir Total.Spent cuando haya inconsistencia
datos_clean$Total.Spent_corregido <- ifelse(
abs(datos_clean$Dif_Total) > 0.01,
datos_clean$Total_teorico,
datos_clean$Total.Spent_num
)
Este paso es central en la fase de verificación, porque evalúa la consistencia interna del dataset:
Total_teorico: calcula cuánto debería ser el total
gastado.Dif_Total: mide la diferencia entre el total registrado
y el total esperado.abs(datos_clean$Dif_Total) > 0.01: identifica
discrepancias significativas.errores_total: almacena las filas con inconsistencias
para revisión.ifelse(...): crea una versión corregida del total
cuando se detecta error.Aquí se evidencia claramente la segunda fase: verificación y validación.
Las variables categóricas clave son Item,
Payment.Method y Location. Se convierten a
minúsculas, se quitan espacios sobrantes y se reemplazan valores
inválidos por NA.
# -------------------------------------------------
# 6. Limpieza y estandarización de variables categóricas
# -------------------------------------------------
cols_cat <- c("Item", "Payment.Method", "Location")
for (col in cols_cat) {
x <- as.character(datos_clean[[col]])
x <- tolower(x)
x <- trimws(x)
# Reducir dobles espacios
x <- gsub(" ", " ", x, fixed = TRUE)
# Reemplazar inválidos por NA
x[x == ""] <- NA
x[x == "unknown"] <- NA
x[x == "error"] <- NA
datos_clean[[col]] <- x
}
# Verificación de categóricas
table(datos_clean$Item, useNA = "ifany")
##
## cake coffee cookie juice salad sandwich smoothie tea
## 1139 1165 1092 1171 1148 1131 1096 1089
## <NA>
## 969
table(datos_clean$Payment.Method, useNA = "ifany")
##
## cash credit card digital wallet <NA>
## 2258 2273 2291 3178
table(datos_clean$Location, useNA = "ifany")
##
## in-store takeaway <NA>
## 3017 3022 3961
unique(datos_clean$Item)
## [1] "coffee" "cake" "cookie" "salad" "smoothie" NA "sandwich"
## [8] "juice" "tea"
unique(datos_clean$Payment.Method)
## [1] "credit card" "cash" NA "digital wallet"
unique(datos_clean$Location)
## [1] "takeaway" "in-store" NA
Las variables categóricas suelen contener errores de registro. Este bloque busca estandarizarlas:
tolower(...): evita que "Coffee" y
"coffee" se traten como categorías diferentes.trimws(...): elimina espacios invisibles que afectan
tablas y conteos.gsub(" ", " ", ...): reduce espacios dobles
internos.NA convierte valores inválidos en
ausencias reales.table(..., useNA = "ifany"): permite revisar si todavía
quedan faltantes.unique(...): ayuda a inspeccionar categorías
finales.Con esto se completa la fase de transformación.
En este punto se realiza una revisión general para comprobar que la limpieza aplicada sea coherente.
# -------------------------------------------------
# 7. Revisión final de calidad
# -------------------------------------------------
# NA por variable
colSums(is.na(datos_clean))
## Transaction.ID Item Quantity
## 0 969 0
## Price.Per.Unit Total.Spent Payment.Method
## 0 0 3178
## Location Transaction.Date Quantity_num
## 3961 460 479
## Price.Per.Unit_num Total.Spent_num Date
## 533 502 460
## Total_teorico Dif_Total Total.Spent_corregido
## 994 1456 1456
# Porcentaje de NA
round(colMeans(is.na(datos_clean)) * 100, 2)
## Transaction.ID Item Quantity
## 0.00 9.69 0.00
## Price.Per.Unit Total.Spent Payment.Method
## 0.00 0.00 31.78
## Location Transaction.Date Quantity_num
## 39.61 4.60 4.79
## Price.Per.Unit_num Total.Spent_num Date
## 5.33 5.02 4.60
## Total_teorico Dif_Total Total.Spent_corregido
## 9.94 14.56 14.56
# Revisar inválidos en columnas de texto
cols_text <- sapply(datos_clean, is.character)
sum(datos_clean[, cols_text] == "error", na.rm = TRUE)
## [1] 0
sum(datos_clean[, cols_text] == "unknown", na.rm = TRUE)
## [1] 0
# Revisar numéricas
summary(datos_clean$Quantity_num)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 1.000 2.000 3.000 3.029 4.000 5.000 479
summary(datos_clean$Price.Per.Unit_num)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 1.00 2.00 3.00 2.95 4.00 5.00 533
summary(datos_clean$Total.Spent_num)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 1.000 4.000 8.000 8.924 12.000 25.000 502
summary(datos_clean$Total.Spent_corregido)
## Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
## 1.000 4.000 8.000 8.929 12.000 25.000 1456
# Revisar fecha
summary(datos_clean$Date)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## "2023-01-01" "2023-04-01" "2023-07-02" "2023-07-01" "2023-10-02" "2023-12-31"
## NA's
## "460"
# Revisar duplicados
sum(duplicated(datos_clean))
## [1] 0
sum(duplicated(datos_clean$Transaction.ID))
## [1] 0
Este bloque resume la fase de validación:
colSums(is.na(...)): cuantifica faltantes.colMeans(is.na(...)): muestra el porcentaje de
faltantes.sapply(..., is.character): selecciona solo columnas de
texto para buscar inválidos.summary(...): revisa rangos y distribuciones
básicas.duplicated(...): detecta filas duplicadas e
identificadores repetidos.Si este bloque arroja resultados coherentes, entonces la base ya está lista para consolidarse.
En esta fase se seleccionan solo las variables limpias y relevantes para análisis.
# -------------------------------------------------
# 8. Construcción de la base final limpia
# -------------------------------------------------
datos_final <- datos_clean[, c(
"Transaction.ID",
"Item",
"Quantity_num",
"Price.Per.Unit_num",
"Total.Spent_corregido",
"Payment.Method",
"Location",
"Date"
)]
# Renombrar variables
names(datos_final) <- c(
"Transaction_ID",
"Item",
"Quantity",
"Price_Per_Unit",
"Total_Spent",
"Payment_Method",
"Location",
"Date"
)
# Verificación inicial de datos_final
str(datos_final)
## 'data.frame': 10000 obs. of 8 variables:
## $ Transaction_ID: chr "TXN_1961373" "TXN_4977031" "TXN_4271903" "TXN_7034554" ...
## $ Item : chr "coffee" "cake" "cookie" "salad" ...
## $ Quantity : num 2 4 4 2 2 5 3 4 5 5 ...
## $ Price_Per_Unit: num 2 3 1 5 2 4 3 4 3 4 ...
## $ Total_Spent : num 4 12 NA 10 4 20 9 16 15 20 ...
## $ Payment_Method: chr "credit card" "cash" "credit card" NA ...
## $ Location : chr "takeaway" "in-store" "in-store" NA ...
## $ Date : Date, format: "2023-09-08" "2023-05-16" ...
summary(datos_final)
## Transaction_ID Item Quantity Price_Per_Unit
## Length:10000 Length:10000 Min. :1.000 Min. :1.00
## Class :character Class :character 1st Qu.:2.000 1st Qu.:2.00
## Mode :character Mode :character Median :3.000 Median :3.00
## Mean :3.029 Mean :2.95
## 3rd Qu.:4.000 3rd Qu.:4.00
## Max. :5.000 Max. :5.00
## NA's :479 NA's :533
## Total_Spent Payment_Method Location Date
## Min. : 1.000 Length:10000 Length:10000 Min. :2023-01-01
## 1st Qu.: 4.000 Class :character Class :character 1st Qu.:2023-04-01
## Median : 8.000 Mode :character Mode :character Median :2023-07-02
## Mean : 8.929 Mean :2023-07-01
## 3rd Qu.:12.000 3rd Qu.:2023-10-02
## Max. :25.000 Max. :2023-12-31
## NA's :1456 NA's :460
colSums(is.na(datos_final))
## Transaction_ID Item Quantity Price_Per_Unit Total_Spent
## 0 969 479 533 1456
## Payment_Method Location Date
## 3178 3961 460
head(datos_final)
## Transaction_ID Item Quantity Price_Per_Unit Total_Spent Payment_Method
## 1 TXN_1961373 coffee 2 2 4 credit card
## 2 TXN_4977031 cake 4 3 12 cash
## 3 TXN_4271903 cookie 4 1 NA credit card
## 4 TXN_7034554 salad 2 5 10 <NA>
## 5 TXN_3160411 coffee 2 2 4 digital wallet
## 6 TXN_2602893 smoothie 5 4 20 credit card
## Location Date
## 1 takeaway 2023-09-08
## 2 in-store 2023-05-16
## 3 in-store 2023-07-19
## 4 <NA> 2023-04-27
## 5 in-store 2023-06-11
## 6 <NA> 2023-03-31
Aquí se materializa la tercera fase del proceso: construcción de la base final.
Esta fase permite pasar de un dataset transformado
(datos_clean) a un dataset analítico
(datos_final).
Se reemplazan los valores faltantes en Quantity y
Price_Per_Unit usando la mediana. Luego se recalcula
Total_Spent.
# -------------------------------------------------
# 9. Imputación de variables numéricas con la mediana
# -------------------------------------------------
mediana_quantity <- median(datos_final$Quantity, na.rm = TRUE)
datos_final$Quantity[is.na(datos_final$Quantity)] <- mediana_quantity
mediana_price <- median(datos_final$Price_Per_Unit, na.rm = TRUE)
datos_final$Price_Per_Unit[is.na(datos_final$Price_Per_Unit)] <- mediana_price
# Recalcular Total_Spent a partir de Quantity y Price_Per_Unit
datos_final$Total_Spent <- datos_final$Quantity * datos_final$Price_Per_Unit
La imputación con la mediana es apropiada en variables numéricas cuando se desea conservar registros y reducir el efecto de valores extremos. Este paso:
Total_Spent se mantenga consistente
tras la imputación.En variables categóricas, una estrategia común consiste en reemplazar
NA por la categoría "unknown".
# -------------------------------------------------
# 10. Imputación de variables categóricas
# -------------------------------------------------
datos_final$Payment_Method[is.na(datos_final$Payment_Method)] <- "unknown"
datos_final$Location[is.na(datos_final$Location)] <- "unknown"
datos_final$Item[is.na(datos_final$Item)] <- "unknown"
Esta estrategia permite:
En este caso, se opta por eliminar filas donde no se dispone de la fecha, ya que una fecha ausente afecta análisis temporal, agregaciones por día, mes o tendencias.
# -------------------------------------------------
# 11. Eliminar filas con fecha faltante
# -------------------------------------------------
datos_final <- datos_final[!is.na(datos_final$Date), ]
La fecha es una variable estructural importante para el análisis de ventas. En lugar de imputar fechas artificialmente, se opta por eliminar esos registros para preservar la validez del análisis temporal.
Finalmente se revisa la base resultante.
# -------------------------------------------------
# 12. Revisión final de la base lista para análisis
# -------------------------------------------------
colSums(is.na(datos_final))
## Transaction_ID Item Quantity Price_Per_Unit Total_Spent
## 0 0 0 0 0
## Payment_Method Location Date
## 0 0 0
summary(datos_final)
## Transaction_ID Item Quantity Price_Per_Unit
## Length:9540 Length:9540 Min. :1.000 Min. :1.000
## Class :character Class :character 1st Qu.:2.000 1st Qu.:2.000
## Mode :character Mode :character Median :3.000 Median :3.000
## Mean :3.024 Mean :2.953
## 3rd Qu.:4.000 3rd Qu.:4.000
## Max. :5.000 Max. :5.000
## Total_Spent Payment_Method Location Date
## Min. : 1.000 Length:9540 Length:9540 Min. :2023-01-01
## 1st Qu.: 4.000 Class :character Class :character 1st Qu.:2023-04-01
## Median : 8.000 Mode :character Mode :character Median :2023-07-02
## Mean : 8.936 Mean :2023-07-01
## 3rd Qu.:12.000 3rd Qu.:2023-10-02
## Max. :25.000 Max. :2023-12-31
str(datos_final)
## 'data.frame': 9540 obs. of 8 variables:
## $ Transaction_ID: chr "TXN_1961373" "TXN_4977031" "TXN_4271903" "TXN_7034554" ...
## $ Item : chr "coffee" "cake" "cookie" "salad" ...
## $ Quantity : num 2 4 4 2 2 5 3 4 5 5 ...
## $ Price_Per_Unit: num 2 3 1 5 2 4 3 4 3 4 ...
## $ Total_Spent : num 4 12 4 10 4 20 9 16 15 20 ...
## $ Payment_Method: chr "credit card" "cash" "credit card" "unknown" ...
## $ Location : chr "takeaway" "in-store" "in-store" "unknown" ...
## $ Date : Date, format: "2023-09-08" "2023-05-16" ...
head(datos_final)
## Transaction_ID Item Quantity Price_Per_Unit Total_Spent Payment_Method
## 1 TXN_1961373 coffee 2 2 4 credit card
## 2 TXN_4977031 cake 4 3 12 cash
## 3 TXN_4271903 cookie 4 1 4 credit card
## 4 TXN_7034554 salad 2 5 10 unknown
## 5 TXN_3160411 coffee 2 2 4 digital wallet
## 6 TXN_2602893 smoothie 5 4 20 credit card
## Location Date
## 1 takeaway 2023-09-08
## 2 in-store 2023-05-16
## 3 in-store 2023-07-19
## 4 unknown 2023-04-27
## 5 in-store 2023-06-11
## 6 unknown 2023-03-31
```