Semana 06 — Limpieza de Datos y Resumen Analítico con dplyr

Author

Jogeysi Carrasco

Published

June 28, 2026

1 Introducción

En esta práctica trabajamos con un dataset de ventas que contiene problemas típicos del mundo real: valores faltantes, duplicados, inconsistencias de formato y outliers. El objetivo es aplicar un flujo completo de limpieza y transformación usando dplyr y tidyr, y cerrar con un resumen analítico por vendedor.

Pregunta central: ¿Cómo se limpia, transforma y resume un dataset con problemas reales para dejarlo listo para el análisis?


2 Dataset de práctica

Code
library(tidyverse)

ventas_crudas <- tibble(
  id_venta   = c(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 3),
  vendedor   = c("ana", "PEDRO", "María ", NA, "carmen", "ana", "PEDRO", "luis", "carmen", "luis", "María "),
  region     = c("norte", "SUR", "Norte", "sur", "NORTE", "norte", "sur", "Norte", "sur", "norte", "Norte"),
  monto      = c(15000, 22000, 18500, NA, 31000, 16000, 19500, 9500000, 21000, 17500, 18500),
  mes        = c(1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 1),
  completada = c("SI", "SI", "NO", "SI", NA, "SI", "NO", "SI", "SI", "NO", "NO")
)

ventas_crudas
# A tibble: 11 × 6
   id_venta vendedor region   monto   mes completada
      <dbl> <chr>    <chr>    <dbl> <dbl> <chr>     
 1        1 "ana"    norte    15000     1 SI        
 2        2 "PEDRO"  SUR      22000     1 SI        
 3        3 "María " Norte    18500     1 NO        
 4        4  <NA>    sur         NA     2 SI        
 5        5 "carmen" NORTE    31000     2 <NA>      
 6        6 "ana"    norte    16000     2 SI        
 7        7 "PEDRO"  sur      19500     3 NO        
 8        8 "luis"   Norte  9500000     3 SI        
 9        9 "carmen" sur      21000     3 SI        
10       10 "luis"   norte    17500     3 NO        
11        3 "María " Norte    18500     1 NO        

3 Parte 1 — Diagnóstico inicial

Antes de limpiar, exploramos el estado del dataset para entender exactamente qué necesita atención.

3.1 Estructura y tipos de datos

Code
glimpse(ventas_crudas)
Rows: 11
Columns: 6
$ id_venta   <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 3
$ vendedor   <chr> "ana", "PEDRO", "María ", NA, "carmen", "ana", "PEDRO", "lu…
$ region     <chr> "norte", "SUR", "Norte", "sur", "NORTE", "norte", "sur", "N…
$ monto      <dbl> 15000, 22000, 18500, NA, 31000, 16000, 19500, 9500000, 2100…
$ mes        <dbl> 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 1
$ completada <chr> "SI", "SI", "NO", "SI", NA, "SI", "NO", "SI", "SI", "NO", "…

glimpse() revela que todas las columnas numéricas (id_venta, monto, mes) son <dbl> y las de texto son <chr>. Notamos que completada debería ser un factor y que mes podría ser un factor ordinal, pero por ahora se mantiene numérico.


3.2 Estadísticas descriptivas

Code
summary(ventas_crudas)
    id_venta           vendedor        region       monto              mes   
 Min.   : 1.000   Length   :11   Length   :11   Min.   :  15000   Min.   :1  
 1st Qu.: 3.000   N.unique : 5   N.unique : 5   1st Qu.:  17750   1st Qu.:1  
 Median : 5.000   N.blank  : 0   N.blank  : 0   Median :  19000   Median :2  
 Mean   : 5.273   Min.nchar: 3   Min.nchar: 3   Mean   : 967900   Mean   :2  
 3rd Qu.: 7.500   Max.nchar: 6   Max.nchar: 5   3rd Qu.:  21750   3rd Qu.:3  
 Max.   :10.000   NAs      : 1                  Max.   :9500000   Max.   :3  
                                                NAs    :1                    
     completada
 Length   :11  
 N.unique : 2  
 N.blank  : 0  
 Min.nchar: 2  
 Max.nchar: 2  
 NAs      : 1  
               

El summary() muestra que monto tiene un máximo de 9,500,000, muy alejado del resto de los valores (que rondan entre 15,000 y 31,000). Esto ya señala un outlier claro. También se confirma la presencia de NA en vendedor, monto y completada.


3.3 Conteo de NAs por columna

Code
colSums(is.na(ventas_crudas))
  id_venta   vendedor     region      monto        mes completada 
         0          1          0          1          0          1 

Diagnóstico de NAs:

  • vendedor: 1 NA (fila 4, id_venta = 4)
  • monto: 1 NA (fila 4, id_venta = 4 — la misma fila)
  • completada: 1 NA (fila 5, id_venta = 5)

El resto de columnas están completas.


3.4 Duplicados

Code
# Filas con id_venta repetido
ventas_crudas |>
  group_by(id_venta) |>
  filter(n() > 1) |>
  arrange(id_venta)
# A tibble: 2 × 6
# Groups:   id_venta [1]
  id_venta vendedor region monto   mes completada
     <dbl> <chr>    <chr>  <dbl> <dbl> <chr>     
1        3 "María " Norte  18500     1 NO        
2        3 "María " Norte  18500     1 NO        

Diagnóstico de duplicados: El id_venta = 3 aparece dos veces con exactamente los mismos valores. Es un duplicado exacto que debe eliminarse conservando solo una de las filas.


3.5 Outliers en monto

Code
# Cálculo visual del rango intercuartílico
Q1  <- quantile(ventas_crudas$monto, 0.25, na.rm = TRUE)
Q3  <- quantile(ventas_crudas$monto, 0.75, na.rm = TRUE)
IQR_val <- IQR(ventas_crudas$monto, na.rm = TRUE)

cat("Q1:", Q1, "\nQ3:", Q3, "\nIQR:", IQR_val,
    "\nLímite superior:", Q3 + 1.5 * IQR_val)
Q1: 17750 
Q3: 21750 
IQR: 4000 
Límite superior: 27750

Con el criterio de Tukey (1.5 × IQR), el límite superior es aproximadamente $31,750. El valor de $9,500,000 en la fila de luis (id_venta = 8) supera ampliamente ese umbral. Probablemente es un error de digitación (quizás $9,500 o $95,000).


4 Parte 2 — Limpieza

Aplicamos cada corrección de forma ordenada, construyendo el dataset limpio paso a paso.

Code
ventas_limpias <- ventas_crudas |>

  # 2.1 — Eliminar duplicados por id_venta (conserva la primera ocurrencia)
  distinct(id_venta, .keep_all = TRUE) |>

  # 2.2a — Limpiar vendedor: quitar espacios y aplicar Title Case
  mutate(
    vendedor = str_trim(vendedor),
    vendedor = str_to_title(vendedor),

    # 2.2b — Limpiar region: quitar espacios y convertir a minúsculas
    region = str_trim(region),
    region = str_to_lower(region),

    # 2.4 — Imputar NA de vendedor con "Desconocido"
    vendedor = replace_na(vendedor, "Desconocido"),

    # 2.5 — Convertir completada a factor, imputando NA como "NO"
    completada = replace_na(completada, "NO"),
    completada = factor(completada, levels = c("SI", "NO"))
  ) |>

  # 2.3 — Imputar NA de monto con la mediana del mes correspondiente
  group_by(mes) |>
  mutate(
    monto = if_else(
      is.na(monto),
      median(monto, na.rm = TRUE),
      monto
    )
  ) |>
  ungroup() |>

  # 2.6 — Marcar outliers con criterio IQR de Tukey
  mutate(
    Q1_tmp     = quantile(monto, 0.25),
    Q3_tmp     = quantile(monto, 0.75),
    IQR_tmp    = Q3_tmp - Q1_tmp,
    es_outlier = monto < (Q1_tmp - 1.5 * IQR_tmp) | monto > (Q3_tmp + 1.5 * IQR_tmp)
  ) |>
  select(-Q1_tmp, -Q3_tmp, -IQR_tmp)   # eliminar columnas auxiliares

ventas_limpias
# A tibble: 10 × 7
   id_venta vendedor    region   monto   mes completada es_outlier
      <dbl> <chr>       <chr>    <dbl> <dbl> <fct>      <lgl>     
 1        1 Ana         norte    15000     1 SI         FALSE     
 2        2 Pedro       sur      22000     1 SI         FALSE     
 3        3 María       norte    18500     1 NO         FALSE     
 4        4 Desconocido sur      23500     2 SI         FALSE     
 5        5 Carmen      norte    31000     2 NO         FALSE     
 6        6 Ana         norte    16000     2 SI         FALSE     
 7        7 Pedro       sur      19500     3 NO         FALSE     
 8        8 Luis        norte  9500000     3 SI         TRUE      
 9        9 Carmen      sur      21000     3 SI         FALSE     
10       10 Luis        norte    17500     3 NO         FALSE     

Resumen de la limpieza:

  • Se eliminó la fila duplicada de id_venta = 3, pasando de 11 a 10 filas.
  • vendedor ahora tiene formato consistente en Title Case (ej. "Ana", "Pedro").
  • region está en minúsculas consistentes ("norte", "sur").
  • El NA de monto (mes 2) se imputó con la mediana del mes 2, que es $(23,500).
  • El NA de vendedor se reemplazó por "Desconocido".
  • completada es ahora un factor con niveles SI / NO.
  • La venta de Luis (id_venta = 8) queda marcada como es_outlier = TRUE.

5 Parte 3 — Transformación con dplyr

Con el dataset limpio, creamos nuevas variables derivadas.

Code
ventas_transformadas <- ventas_limpias |>
  mutate(
    # 3.1 — Comisión: 5% si completada == "SI", 0 si no
    comision = if_else(completada == "SI", monto * 0.05, 0),

    # 3.2 — Categoría de venta según monto
    categoria_venta = case_when(
      monto <  15000             ~ "Baja",
      monto >= 15000 & monto <= 25000 ~ "Media",
      monto >  25000             ~ "Alta"
    ),
    categoria_venta = factor(categoria_venta, levels = c("Baja", "Media", "Alta"))
  ) |>

  # 3.3 — Ranking de vendedor por monto dentro de cada región
  group_by(region) |>
  mutate(
    ranking_region = rank(-monto, ties.method = "min")
  ) |>
  ungroup()

ventas_transformadas |>
  select(id_venta, vendedor, region, monto, completada,
         es_outlier, comision, categoria_venta, ranking_region)
# A tibble: 10 × 9
   id_venta vendedor    region   monto completada es_outlier comision
      <dbl> <chr>       <chr>    <dbl> <fct>      <lgl>         <dbl>
 1        1 Ana         norte    15000 SI         FALSE           750
 2        2 Pedro       sur      22000 SI         FALSE          1100
 3        3 María       norte    18500 NO         FALSE             0
 4        4 Desconocido sur      23500 SI         FALSE          1175
 5        5 Carmen      norte    31000 NO         FALSE             0
 6        6 Ana         norte    16000 SI         FALSE           800
 7        7 Pedro       sur      19500 NO         FALSE             0
 8        8 Luis        norte  9500000 SI         TRUE         475000
 9        9 Carmen      sur      21000 SI         FALSE          1050
10       10 Luis        norte    17500 NO         FALSE             0
# ℹ 2 more variables: categoria_venta <fct>, ranking_region <int>
  • comision: solo las ventas completadas generan comisión. El outlier ($9.5M) genera una comisión enorme, lo que refuerza la importancia de haberlo marcado.
  • categoria_venta: la mayoría cae en la categoría Media (entre $15,000 y $25,000), que es el rango típico del dataset.
  • ranking_region: el ranking es independiente por región — el #1 en “norte” no compite con el #1 en “sur”.

6 Parte 4 — Resumen analítico por vendedor

Code
resumen_vendedores <- ventas_transformadas |>
  group_by(vendedor) |>
  summarise(
    ventas_completadas = sum(completada == "SI"),
    monto_total        = sum(monto),
    monto_promedio     = round(mean(monto), 0),
    comision_total     = sum(comision),
    .groups = "drop"
  ) |>
  arrange(desc(monto_total))

resumen_vendedores
# A tibble: 6 × 5
  vendedor    ventas_completadas monto_total monto_promedio comision_total
  <chr>                    <int>       <dbl>          <dbl>          <dbl>
1 Luis                         1     9517500        4758750         475000
2 Carmen                       1       52000          26000           1050
3 Pedro                        1       41500          20750           1100
4 Ana                          2       31000          15500           1550
5 Desconocido                  1       23500          23500           1175
6 María                        0       18500          18500              0

Interpretación del resumen:

  • Luis encabeza el monto total gracias al outlier de $9,500,000, pero esa cifra no es confiable — sus métricas deben interpretarse con cautela.
  • Carmen tiene el monto total más alto entre los vendedores sin outlier, con dos ventas completadas.
  • Desconocido (vendedor con NA) aparece con una sola venta completada y el monto imputado por mediana.
  • La comisión total refleja directamente qué vendedores cierran más ventas marcadas como "SI".

7 Conclusiones

Problema encontrado Solución aplicada
Fila duplicada (id_venta = 3) distinct(id_venta, .keep_all = TRUE)
Formato inconsistente en vendedor str_trim() + str_to_title()
Formato inconsistente en region str_trim() + str_to_lower()
NA en monto Imputación con mediana del mes (group_by + if_else)
NA en vendedor replace_na("Desconocido")
NA en completada replace_na("NO") + conversión a factor
Outlier en monto ($9.5M) Marcado con columna es_outlier usando criterio IQR

Un dataset bien limpio no solo evita errores en el análisis — también hace que los resúmenes y visualizaciones sean confiables. La limpieza no es opcional: es el primer paso de cualquier análisis serio.