Unidad 2 - Práctica: de la base sucia al dataset confiable

Aplicación guiada en clase · Semana 6 · 06278-ECO

Autor/a

PhD. Eduard F. Martínez-González

1 Reglas de juego

Esta sesión asume que ya estudiaste la teoría

Aquí no volvemos a explicar qué es la completitud, por qué un outlier no siempre es un error, ni qué busca la EDA: eso quedó en el podcast (y su versión escrita) y en el quiz. Hoy ese criterio se ejecuta sobre una base sucia de verdad. Si algo se te escapa, el glosario queda a un clic.

Lo que necesitas ahora mismo:

  • 💻 RStudio abierto dentro de tu proyecto intro_ba (semana 3).
  • 🌐 Internet (los datos se cargan desde la web del curso).
  • 📦 Un paquete nuevo: skimr. Si no lo tienes, install.packages("skimr") en la consola, no en el script.
  • 🧑‍🤝‍🧑 Trabaja en tu computador, pero comenta cada checkpoint con tu vecino: si sus números no coinciden, uno de los dos tiene un hallazgo.

El escenario de hoy: eres el analista de la comercializadora de tecnología de la semana 4. Pero hoy no te dan el archivo pulido: te dan la base como sale del sistema, antes de que nadie la toque. Tu trabajo es entregar, al final de la sesión, un dataset confiable, un log de decisiones y tres hallazgos. Ese es el producto — y es, casi literalmente, la Entrega 2 de tu proyecto.

Advertencia

La regla de oro de hoy

ventas_raw no se toca nunca. Todo lo que hagamos construye un objeto nuevo (ventas_clean) a partir de él. Si te equivocas limpiando, el raw es tu punto de retorno. Un analista que sobrescribe su base original no tiene a dónde volver.

2 El setup profesional ≈ 8 min

Abre tu proyecto intro_ba, crea un script nuevo y guárdalo en scripts/ como practica_semana6.R. Arranca con el encabezado de siempre — y con un paquete nuevo:

## ============================================
## Práctica semana 6 — Calidad de datos y EDA
## Nombre: _______________
## Fecha:  _______________
## ============================================

## Librerías (skimr es nuevo esta semana)
require(pacman)
p_load(dplyr, ggplot2, skimr)

## La base "tal como sale del sistema"
ventas_raw <- read.csv("https://eduard-martinez.github.io/databases/ba/ventas_raw.csv")

## Radiografía de la semana 3
str(ventas_raw)
head(ventas_raw)

Checkpoint 🎯: ¿cuántas filas y columnas reporta str()? Anótalo — al final vamos a comparar cuántas sobrevivieron. Y responde con tu vecino la pregunta que va primero que todas: ¿qué representa una fila de esta base?

3 Paso 1 — Diagnóstico ≈ 15 min

Antes de corregir nada, hay que saber qué está mal. La secuencia es siempre la misma: primero una visión general, después el zoom sobre lo que llamó la atención.

3.1 La radiografía completa: skim()

## Una sola función, una visión completa
skim(ventas_raw)

Cómo leer esta salida (léela de arriba abajo, columna por columna)

La salida viene partida en dos bloques según el tipo de variable.

Variables de texto (character):

Columna Qué te dice
n_missing Cuántos valores faltan
n_unique Cuántos valores distintos hay ← aquí saltan las inconsistencias

Variables numéricas:

Columna Qué te dice
n_missing Cuántos valores faltan
mean, sd Promedio y desviación
p0, p100 Mínimo y máximo ← aquí saltan los imposibles y los outliers

Regla práctica: cualquier cosa que no tenga sentido es un hallazgo que hay que anotar.

Checkpoint 🎯 — las tres cosas que skim() te acaba de gritar:

  1. Mira n_unique de region. La empresa tiene 3 regiones. ¿Cuántas dice que hay?
  2. Mira el tipo de precio. ¿Es numeric? ¿Debería serlo?
  3. Mira p0 (el mínimo) de cantidad. ¿Se pueden vender esas unidades?

🗣️ Discútelo con tu vecino antes de seguir. Las tres son problemas distintos: uno de consistencia, uno de validez por tipo y uno de validez por rango. Ubícalos en las cuatro dimensiones de la teoría.

3.2 Zoom 1: las categorías

skim() te dijo cuántos valores únicos hay. Ahora vamos a verlos:

## ¿Cuáles son exactamente los valores de 'region'?
unique(ventas_raw$region)

## ¿Y cuántas veces aparece cada uno?
table(ventas_raw$region)
Advertencia

El error más silencioso del análisis de datos

Ahí lo tienes: "Norte" conviviendo con "norte", y "Sur" con "SUR". Para R cada variante es una región distinta: la empresa tiene 3 regiones y la base reporta 5.

Haz este experimento mental antes de arreglarlo: si ahora mismo corrieras group_by(region) y summarise(sum(ingreso)) — el mismo código impecable de la semana 4 — te saldría una tabla con 5 filas, perfectamente formateada, con sus totales y sus decimales. Y R no diría ni una palabra. El gerente reparte el presupuesto con esa tabla, y el ingreso del Norte —partido entre "Norte" y "norte"— aparece como la mitad de lo que realmente es.

Ese es el punto entero de esta semana.

## Revisa también estas dos: ¿tienen el mismo problema?
unique(ventas_raw$trimestre)
unique(ventas_raw$producto)

3.3 Zoom 2: los duplicados

## ¿Cuántas filas son copias exactas de otra?
sum(duplicated(ventas_raw))

Qué hace duplicated()

Compara cada fila con todas las anteriores y devuelve TRUE si ya había aparecido.

  • sum(duplicated(df)) → cuántas filas son copias exactas.
  • df[duplicated(df), ] → cuáles son.

¿Por qué aparecen? Exportaciones dobles del sistema, joins mal hechos, actualizaciones que acumulan en vez de reemplazar. Siempre pregunta la causa antes de eliminar — si el sistema está duplicando, el problema no es tu base, es el sistema.

3.4 Zoom 3: el tipo de precio

## Confirmemos lo que skim() insinuó
class(ventas_raw$precio)

## ¿Por qué llegó como texto? Mira los valores
unique(ventas_raw$precio)

Ahí está el culpable: alguien dejó un "$1200" con el signo adentro. Basta un solo valor con un carácter no numérico para que R declare toda la columna como texto.

Compruébalo tú mismo (y no te asustes):

sum(ventas_raw$precio, na.rm = TRUE)
## Error: invalid 'type' (character) of argument

Este error es de los buenos: R se niega a hacer aritmética con texto y te avisa. Compáralo con el problema de "Norte"/"norte", que no avisa nada. El error que grita es menos peligroso que el que se calla.

3.5 Zoom 4: los valores imposibles y el outlier

## El mínimo de cantidad: ¿se puede vender una cantidad negativa?
summary(ventas_raw$cantidad)

Para ver el precio necesitamos convertirlo a número primero (solo para mirarlo — todavía no estamos limpiando):

## Conversión temporal, solo para poder graficar
ventas_raw <- mutate(ventas_raw, precio_num = as.numeric(gsub("\\$", "", precio)))

## El histograma como detector de outliers
ggplot(ventas_raw, aes(x = precio_num)) +
  geom_histogram(bins = 20)

Checkpoint 🎯: el eje X se estira hasta ~12.000, pero casi todas las barras están apelotonadas a la izquierda. Esa barrita solitaria a la derecha es un outlier gritando.

## ¿Quién es? Vamos a mirar la fila completa antes de juzgar
ventas_raw[!is.na(ventas_raw$precio_num) & ventas_raw$precio_num > 5000, ]

La pregunta de la teoría, ahora en vivo: ¿error o caso real?

Es un Mouse con precio 12.000, entre mouses de 20 a 30. Un mouse no cuesta 12.000: es un error de captura (casi con certeza eran 120, con un cero de más). Se corrige.

Pero fíjate en lo que acabamos de hacer, porque es el hábito que hay que aprender: fuimos a mirar la fila completa antes de decidir. Si en vez de un Mouse a 12.000 hubiera aparecido una Laptop a 12.000 vendida a un cliente corporativo, la decisión habría sido la contraria — y el “outlier” sería el dato más valioso de la base.

4 Paso 2 — Limpieza ≈ 20 min

Ya sabemos qué está mal. Ahora lo arreglamos, en orden: primero se eliminan filas (duplicados, imposibles), después se corrigen columnas (texto, tipos, outliers) y de último se ajusta la estructura.

Y desde aquí, ventas_raw no se vuelve a tocar.

4.1 2.1 — Eliminar duplicados

## distinct() elimina las filas idénticas, conservando la primera
ventas_clean <- distinct(ventas_raw)

## ¿Cuántas se fueron?
nrow(ventas_raw) - nrow(ventas_clean)

Checkpoint 🎯: el resultado debe coincidir exactamente con el sum(duplicated()) del diagnóstico. Si no coincide, algo se te fue. Esta comparación es tu forma de confirmar que la operación hizo lo que creías.

4.2 2.2 — Estandarizar las categorías

## Todo a mayúsculas: se acabaron las tres regiones fantasma
ventas_clean <- mutate(ventas_clean,
                       trimestre = toupper(trimestre),
                       region    = toupper(region),
                       producto  = toupper(producto))

## Verificar SIEMPRE después de corregir
table(ventas_clean$region)
table(ventas_clean$trimestre)

Checkpoint 🎯: ahora sí, 3 regiones y 3 trimestres. Compara este table() con el del diagnóstico: los conteos de “Norte” y “norte” se sumaron en uno solo. Ese es el presupuesto del gerente, arreglado.

Tres funciones para estandarizar texto

Función Resultado Cuándo usarla
toupper(x) "NORTE" Códigos, IDs, siglas — la que usamos hoy
tolower(x) "norte" Como paso intermedio antes de capitalizar
tools::toTitleCase(x) "Norte" Nombres propios, categorías de cara al usuario

Cuál elegir es cuestión de estilo. Que elijas una y la apliques a toda la base, no.

4.3 2.3 — Corregir el tipo de precio

## Quitar el "$" y convertir a número
ventas_clean <- mutate(ventas_clean,
                       precio = as.numeric(gsub("\\$", "", precio)))

## Verificar
class(ventas_clean$precio)
summary(ventas_clean$precio)

Qué hace gsub("\\$", "", precio)

gsub(patrón, reemplazo, texto) busca y reemplaza:

  • "\\$" → el patrón es el símbolo $. Se escribe con doble barra porque $ tiene un significado especial en expresiones regulares (significa “fin de línea”), y hay que decirle a R que esta vez es un peso, no un comodín.
  • "" → reemplázalo por nada, es decir, elimínalo.
  • Resultado: "$1200""1200"as.numeric()1200.

Ojo con la trampa: si algún valor no se puede convertir (porque tenía letras), as.numeric() lo vuelve NA con un warning fácil de ignorar. Por eso el summary() de verificación no es opcional: compara el n_missing de antes con el de ahora. Si aparecieron NA nuevos, los creaste tú.

4.4 2.4 — Eliminar los valores imposibles

## No se pueden vender cantidades negativas
ventas_clean <- filter(ventas_clean, cantidad >= 0)

## Verificar: el mínimo ya no debe ser negativo
summary(ventas_clean$cantidad)
Advertencia

¿Eliminar, convertir a NA, o corregir?

Tres opciones frente a un valor imposible:

  1. Eliminar la fila — si no tienes información para corregirla (lo que hicimos).
  2. Convertirla a NA — si quieres conservar el resto de la fila para otros análisis.
  3. Corregir el valor — si tienes evidencia. Y ojo: una devolución documentada puede tener cantidad negativa de forma perfectamente legítima. Si esta base incluyera devoluciones, -5 no sería un error: sería el dato.

La opción correcta depende del contexto de negocio, no de una regla estadística. Lo que no es opcional es documentar cuál elegiste.

4.5 2.5 — Corregir el outlier

## Ya confirmamos que es un error de captura: 12000 → 120
ventas_clean <- mutate(ventas_clean,
                       precio = ifelse(producto == "MOUSE" & precio > 5000,
                                       120,
                                       precio))

## Verificar: el máximo debe volver al rango de las Laptops (~1.250)
summary(ventas_clean$precio)

ifelse() y por qué la condición debe ser específica

ifelse(condición, valor_si_TRUE, valor_si_FALSE) decide fila por fila.

Fíjate en que la condición tiene dos partes: producto == "MOUSE" y precio > 5000. ¿Por qué no solo precio > 5000? Porque si mañana entra una Laptop legítima de 6.000, esa condición floja se la comería y la volvería 120 — un error de limpieza mucho peor que el original.

Corrige con bisturí, no con hacha. Y nota que producto ya está en mayúsculas, porque limpiamos en orden: por eso la condición dice "MOUSE" y no "Mouse".

4.6 2.6 — La estructura final

## 'observaciones' está casi toda vacía y no aporta nada analítico
ventas_clean <- select(ventas_clean, -observaciones, -precio_num)

## La variable derivada que necesita el análisis
ventas_clean <- mutate(ventas_clean, ingreso = precio * cantidad)

## El producto de la sesión
head(ventas_clean)
str(ventas_clean)

Checkpoint 🎯: acabas de pasar de raw a analysis-ready. Compara el str() de ahora con el del principio: ¿cuántas filas sobrevivieron y por qué?

5 Paso 3 — EDA ≈ 15 min

Datos confiables. Ahora sí, a explorar. Una tabla resumen y tres gráficos — el mínimo de la semana.

5.1 La radiografía, otra vez

skim(ventas_clean)

Compáralo con el skim() del Paso 1

Vuelve arriba y pon las dos salidas lado a lado. Deberías poder señalar:

  • precio pasó de character a numeric.
  • n_unique de region y trimestre bajó a 3.
  • El mínimo de cantidad ya no es negativo.
  • El máximo de precio volvió a un rango creíble.
  • Los n_missing que quedan son los que venían del origen — no los inventamos nosotros.

Esa comparación es parte de la EDA, no un trámite: es cómo confirmas que el dataset limpio quedó como creías.

5.2 Gráfico 1 — La distribución (y el hallazgo escondido)

ggplot(ventas_clean, aes(x = precio)) +
  geom_histogram(bins = 15, fill = "steelblue", color = "white") +
  labs(title = "La empresa vende dos negocios distintos, no uno",
       x = "Precio (USD)", y = "Número de transacciones") +
  theme_minimal()

Distribución bimodal: esto es un hallazgo de negocio, no una curiosidad

Dos jorobas separadas: productos baratos (Mouse, Licencia, Soporte) y productos caros (Laptop).

Consecuencia inmediata y concreta: si el gerente te pide “el precio promedio”, ese número cae en el valle entre las dos jorobas — un rango donde no existe casi ningún producto. Es técnicamente correcto y no describe a nadie.

La respuesta profesional no es el promedio: es “hay dos segmentos, y aquí está el promedio de cada uno”. Eso lo descubrió la EDA, no un modelo.

5.3 Gráfico 2 — La comparación entre grupos

## Primero la tabla (los verbos de la semana 4)
ing_region <- group_by(ventas_clean, region)
ing_region <- summarise(ing_region,
                        ingreso_total   = sum(ingreso, na.rm = TRUE),
                        n_transacciones = n(),
                        .groups = "drop")
ing_region

## Después el gráfico
ggplot(ing_region, aes(x = reorder(region, ingreso_total), y = ingreso_total)) +
  geom_col(fill = "steelblue") +
  coord_flip() +
  labs(title = "Ingreso total por región", x = NULL, y = "Ingreso (USD)") +
  theme_minimal()

Léelo con criterio de negocio (3 min con tu vecino)

  • ¿Alguna región tiene muchas transacciones pero poco ingreso? Eso no significa que le vaya mal: puede significar que vende productos baratos. Dos negocios distintos, otra vez.
  • n_transacciones te confirma cuántos registros sobrevivieron la limpieza por grupo.
  • 🚨 Y lo más importante: esta tabla, corrida sobre ventas_raw, te habría dado 5 filas. Ese era el presupuesto mal repartido. Ahora tiene 3.

5.4 Gráfico 3 — La relación entre variables

ggplot(ventas_clean, aes(x = precio, y = cantidad, color = categoria)) +
  geom_point(size = 3, alpha = 0.7, na.rm = TRUE) +
  labs(title = "¿Se vende menos de lo que cuesta más?",
       x = "Precio (USD)", y = "Unidades vendidas", color = "Categoría") +
  theme_minimal()

Tres preguntas para el gráfico

  1. ¿El Hardware tiende a tener precios más altos o más bajos que el Software?
  2. ¿Hay relación entre precio y cantidad? ¿Positiva, negativa, ninguna?
  3. ¿Ves algún punto que se salga del patrón?

El scatter plot no te da la respuesta: te da la pregunta correcta. Eso es exactamente lo que hace la EDA.

6 El producto de la sesión

Antes de cerrar el computador, tu script debe dejar cuatro cosas. Este es el estándar del curso y es el mismo de la Entrega 2:

1. El script reproducible. Comentado, que corra completo de arriba abajo. Prueba de fuego: rm(list = ls())Source. Si se cae, no es reproducible.

2. El dataset analysis-ready guardado.

write.csv(ventas_clean, "data/ventas_clean.csv", row.names = FALSE)

3. El acta de decisiones. Al final del script, en comentarios. Cinco líneas bastan, pero deben responder qué y por qué:

## ============================================
## ACTA DE LIMPIEZA
## - Eliminadas ___ filas duplicadas exactas (causa probable: ______).
## - Estandarizadas region/trimestre/producto con toupper(): de 5 a 3 regiones.
## - precio: eliminado el "$" y convertido a numeric.
## - Eliminada 1 fila con cantidad negativa (imposible en este negocio).
## - Corregido outlier Mouse 12000 → 120 (error de captura confirmado en la fila).
## - Eliminada columna 'observaciones' (___% de NA, sin valor analítico).
## ============================================

4. Tres hallazgos y tres preguntas abiertas. Los hallazgos son lo que sabes ahora; las preguntas son lo que la EDA te destapó y todavía no puedes responder. Las dos listas valen igual.

La prueba de fuego de la reproducibilidad

Pásale tu script y el link del raw a tu vecino. Si al correrlo no obtiene exactamente tu mismo ventas_clean, uno de los dos tiene un problema — y descubrir cuál es el mejor uso que le pueden dar a los últimos cinco minutos de la clase.

7 Cierre

Hoy la base pasó de sucia y peligrosa a confiable y documentada. Cuenta lo que hiciste: ni un modelo, ni una predicción. Solo diagnóstico, criterio y limpieza — el 80% del tiempo del que hablaba la semana 5, vivido en carne propia.

Y quédate con la escena de "Norte" / "norte": la tabla que salía perfecta y estaba mal, sin un solo error en la consola. Ese es el trabajo que nadie ve y que sostiene todo lo demás.

Lo que sigue: el taller de hoy te da una base nueva y sucia para que la dejes presentable sin guía. Y en la semana 7 es el parcial: el diagnóstico de calidad y la lectura de la EDA entran. 🧹