Gráficos para identificar datos outliers o anómalos en R

18/6/2025

Temas: limpieza de datos ggplot2 gráficos visualización de datos estadística

Los datos anómalos o outliers son datos que se alejan considerablemente de los demás. Estos datos pueden resultar problemáticos para ciertos análisis, pueden ser indicio de errores en la recolección o limpieza de datos, o pueden requerir que tomemos ciertas decisiones para corregirlos o excluirlos.

En este post simularemos un dataset con datos anómalos, y luego mostraremos algunas formas de visualización de datos anómalos en {ggplot2} para tomar decisiones al respecto. Al final crearemos un gráfico interactivo con {ggiraph} que permita poner el cursor sobre las observaciones para obtener más información.


Primero creemos un conjunto de datos que contenga números al azar, 90 números entre 1 y 100, y 10 números más grandes, para que sean nuestros outliers simulados:

library(dplyr)
library(ggplot2)

set.seed(1993)

valor <- c(sample(1:100, size = 90), # números al azar
           sample(120:190, size = 10)) # outliers

Obtenemos un vector con los dos conjuntos de números al azar.

Con la función tibble() convertimos el vector de números en una columna de un dataframe, y usando nuevamente sample() crearemos dos columnas con palabras al azar para complementar estos datos simulados:

datos <- tibble(valor) |> 
  # procesar datos por fila en vez de por la columna entera (vectorización)
  rowwise() |> 
  # por fila, elegir tres sílabas al azar y unirlas en un sólo texto, que será el "nombre" de las observaciones
  mutate(nombre = sample(c("ma", "pa", "che", "cha"), 3, F) |> paste(collapse = "")) |> 
  # desagrupar y crear otra variable con trres valores posibles, y que por lo tanto distribuya los datos en tres grupos
  ungroup() |>
  mutate(grupo = sample(c("ma", "pa", "che"), n(), T))

datos
# A tibble: 100 × 3
   valor nombre   grupo
   <int> <chr>    <chr>
 1    45 chapache pa   
 2    95 chechapa ma   
 3    83 chamapa  ma   
 4    92 mapache  pa   
 5    40 chamache che  
 6    42 pachama  pa   
 7    75 mapacha  che  
 8    65 machepa  che  
 9    55 chachema che  
10    18 chapama  che  
# ℹ 90 more rows
sample(c("ma", "pa", "che", "cha"), 3, F) |> paste(collapse = "")
[1] "mapache"

Detección de datos anómalos

Para identificar los outliers, utilizaremos el criterio del rango intercuartílico. El rango intercuartílico se calcula con la función IQR() y es el rango de los datos entre el primer y tercer cuartil (el percentil 25 y el percentil 75; es decir, la diferencia entre el dato ubicado en el 75% mayor y el 25% mayor de la distribución de los datos).

En otras palabras, el IQR es una cifra que indica qué tanta distancia existe en la “mitad” de tus datos, si los esparcieras todos en una distribución, como aparece en el siguiente gráfico1.

Luego, el rango intercuartílico se multiplica por 1,5, y se suma al valor del tercer cuartil (percentil 75), de modo que se identifiquen como outliers los datos que sean mayores2 al tercer cuartil más 1,5 veces el rango intercuartílico. Puedes calcular cualquier percentil de un vector o columna con la función quantile(x, .75), donde el número identifica al percentil.

Aplicamos el cálculo a la variable valor:

datos_outliers <- datos |> 
  mutate(umbral = quantile(valor, 0.75) + 1.5 * IQR(valor))

Esto nos dará una cifra que operará como el umbral respecto del cual se clasificarán los outliers. Es conveniente calcular este umbral dentro del dataframe, porque si queremos calcular outliers desagregados por otra variable, simplemente mantenemos la fórmula y agregamos antes un group_by() para realizar el cálculo por grupos.

Crearemos una columna que simplemente nos diga si los valores son mayores o menores al umbral:

datos_outliers <- datos_outliers |> 
  mutate(outlier = valor >= umbral)

datos_outliers
# A tibble: 100 × 5
   valor nombre   grupo umbral outlier
   <int> <chr>    <chr>  <dbl> <lgl>  
 1    45 chapache pa       167 FALSE  
 2    95 chechapa ma       167 FALSE  
 3    83 chamapa  ma       167 FALSE  
 4    92 mapache  pa       167 FALSE  
 5    40 chamache che      167 FALSE  
 6    42 pachama  pa       167 FALSE  
 7    75 mapacha  che      167 FALSE  
 8    65 machepa  che      167 FALSE  
 9    55 chachema che      167 FALSE  
10    18 chapama  che      167 FALSE  
# ℹ 90 more rows

Obtenemos la variable outlier con TRUE si es outlier, y FALSE si no lo es.

Ahora vamos a visualizar estos datos con el paquete {ggplot2}. Si necesitas una introducción a esta librería de visualización de datos, te recomiendo revisar este tutorial, donde explicamos en mayor detalle varias de estas visualizaciones.

Gráfico de cajas o boxplot

El boxplot es una visualización que en su mismo diseño incluye la opción de mostrar los outliers, así que se trata de la opción natural para este tipo de visualizaciones exploratorias. Así que con esto concluye este tutorial. Bromita 🥰

datos_outliers |> 
  ggplot() +
  aes(x = valor, y = 1) +
  # gráfico de boxplot
  geom_boxplot(alpha = 0.4, fill = "black", 
               outlier.color = "#F94C6A", outlier.size = 4, outlier.alpha = 0.7) + # configuración de outliers
  # temas
  theme_minimal() +
  scale_y_continuous(expand = expansion(c(0.5, 0.5))) # aumentar margen del eje vertical

En un boxplot, los puntos al extremo de la caja representan los casos anómalos.

Pero la gracia de este tutorial es entrenar nuestras capacidades de visualización de datos, así que veamos otras formas de visualizarlos:

Gráfico de puntos

Una forma sencilla de visualizar outliers sería simplemente visualizar las observaciones del dataser como puntos, coloreando los puntos si son outliers.

datos_outliers |> 
  ggplot() +
  aes(x = valor, y = 1, color = outlier) +
  # gráfico de puntos
  geom_point(size = 4, alpha = 0.6) +
  # escala de colores
  scale_color_manual(values = c("black", "#F94C6A")) +
  # temas
  theme_minimal() +
  # ocultar leyenda
  guides(color = guide_none(),
         y =  guide_none())

Puntos con dispersión

Como existe una concentración densa en parte de la distribución, podemos usar la función geom_jitter() para que los puntos se dispersen verticalmente (width = 0, porque si se dispersan horizontalmente se ubicarían incorrectamente con respecto a su valor)

datos_outliers |> 
  ggplot() +
  aes(x = valor, y = 1, color = outlier) +
  # gráfico de puntos con dispersión
  geom_jitter(size = 4, alpha = 0.6, width = 0) +
  scale_color_manual(values = c("black", "#F94C6A")) +
  theme_minimal() +
  guides(color = guide_none(),
         y =  guide_none())

Gráfico de violín

El gráfico de violín también nos permite observar la distribución de los datos, pero por sí solo no nos muestra las observaciones exactas, por lo que no entrega información certera sobre los outliers, sino que nos da indicios de que la distribución de los datos tiene colas que podrían contener outliers.

datos_outliers |> 
  ggplot() +
  aes(x = valor, y = 1) +
  # gráfico de violín
  geom_violin(fill = "black", alpha = 0.4) +
  theme_minimal() +
  guides(y =  guide_none()) # ocultar eje y

Combinar visualizaciones

Una buena opción es combinar las visualizaciones anteriores en una sola. De fondo podemos poner la distribución de los datos con geom_violin(), y encima poner los puntos de las observaciones; para las observaciones normales podemos usar geom_jitter() para dispersar los datos, y para los outliers, como son poquitos, geom_point() para que se ubiquen en concordancia con la distribución del violín.

grafico_outliers <- datos_outliers |> 
  ggplot() +
  aes(x = valor, y = 1, color = outlier) +
  # gráfico de violín
  geom_violin(aes(y = 1, x = valor), 
              inherit.aes = F,
              alpha = 0.2, lwd = 0.1, fill = "black") +
  # puntos para los outliers
  geom_point(data = ~filter(.x, outlier), # sólo para observaciones que son outlier
             size = 4, alpha = 0.6) +
  # puntos dispersados para el resto de los datos
  geom_jitter(data = ~filter(.x, !outlier), # sólo para observaciones que no son outlier
              size = 4, alpha = 0.6) +
  # escala de colores
  scale_color_manual(values = c("black", "#F94C6A")) +
  # temas
  theme_minimal() +
  theme(axis.title = element_blank()) +
  guides(color = guide_none(),
         y =  guide_none()) +
  scale_y_continuous(expand = expansion(c(0.2, 0.2))) # aumentar margen del eje vertical

grafico_outliers

Para combinar estas visualizaciones, aprovechamos la capacidad de {ggplot2} de especificar los datos que se usan en cada capa o geometría de la visualización por medio del argumento data. Normalmente, el argumento data se rellena por defecto con los datos que entregamos a la función ggplot(), pero si especificamos el argumento podemos hacer que cada capa use datos completamente distintos. En nuestro caso, no queremos datos distintos, sino aplicar filtros a los datos de cada capa, lo que se logra con data = ~filter(.x, ...)3, donde ... sería el filtro que necesitemos. En la visualización anterior, queremos que geom_point() sólo muestre los datos que son outlier == FALSE, y que geom_jitter() sólo muestre los datos que son outlier == TRUE4.

data = ~filter(.x, outlier)

Etiquetas de texto

Al identificar outliers, una buena opción es mostrar etiquetas de texto para estos casos con geom_text(). De este modo podemos identificar exactamente a qué observaciones corresponden las anomalías. En el caso de que fueran muchas etiquetas, podemos usar geom_text_repel() del paquete {ggrepel} para que las etiquetas de texto se acomoden si es que caen encima de otras.

library(ggrepel)

grafico_outliers <- grafico_outliers + 
  # agregar texto al gráfico anterior
  ggrepel::geom_text_repel(data = ~filter(.x, outlier), # sólo para observaciones que son outlier
                           aes(label = nombre), # variable con el texto a mostrar
                           fontface = "bold", size = 4, point.padding = 4, angle = 90, hjust = 0) 

grafico_outliers

Ahora sabemos que mapacha, pachache, chepama y chachema son outliers 🤨

Dividir por grupos

Otra forma de afinar el análisis es separar la visualización en los valores de la variable de agrupación que tengamos con facet_wrap(). En este caso, como la variable grupo tiene 3 valores posibles, se multiplica la visualización por tres.

grafico_outliers <- grafico_outliers +
  # separar gráfico en facetas según la variable grupo
  facet_wrap(~grupo, ncol = 1, scales = "fixed") +
  theme(strip.text = element_text(face = "bold")) # título de facetas en negrita

grafico_outliers

Gráfico interactivo

Para mejorar la exploración de los datos podemos convertir fácilmente cualquier gráfico de {ggplot2} en gráficos interactivos gracias al paquete {ggiraph}. Principalmente, lo que agregaremos son tooltips o cajas emergentes que aparecerán cuando se pose el cursor sobre un punto del gráfico y que muestren información extra.

Con {ggiraph} solamente se requieren cambios mínimos para volver interactivo cualquier gráfico. Entre ellos es agregar _interactive a las funciones que crean las geometrías del gráfico:

  • Pasar de geom_point() a geom_point_interactive()
  • Pasar de geom_jitter() a geom_jitter_interactive()

Habiendo hecho este cambio, dentro de las geometrías geom_x_interactive() ahora podremos definir la estética tooltip dentro de aes() para que aparezca contenido cuando se ponga el cursor sobre los elementos del gráfico. En este caso, haremos que los tooltips muestren el nombre de la observación y su valor, y la palabra outlier si corresponde. También podemos agregar html para dar estilo al texto; por ejemplo, las etiquetas <b> para letra negrita.

Si queremos que los puntos del gráfico se iluminen o cambien de color al poner el cursor encima, adicionalmente tenemos que definir la estética data_id que identifique de forma única los elementos del gráfico (puede servir para hacer que varios se iluminen al mismo tiempo si elijes una variable que no identifique de forma única los valores).

Copiamos el código anterior y hacemos los cambios apropiados al gráfico:

library(ggiraph)

grafico_outliers_interactivo <- datos_outliers |> 
  ggplot() +
  aes(x = valor, y = 1, color = outlier,
      # variable que identifica de forma única a los elementos del gráfico
      data_id = nombre) + 
  geom_violin(aes(y = 1, x = valor), 
              inherit.aes = F, alpha = 0.2, lwd = 0.1, fill = "black") +
  # puntos para los outliers
  ggiraph::geom_point_interactive(data = ~filter(.x, outlier), 
                                  # texto "outlier" con el valor
                                  aes(tooltip = paste("<b>outlier:</b> ", valor, sep = "")), 
                                  size = 4, alpha = 0.6) +
  # puntos dispersados para el resto de los datos
  ggiraph::geom_jitter_interactive(data = ~filter(.x, !outlier), 
                                   # nombre de observación y valor
                                   aes(tooltip = paste("<b>", nombre, ":</b> ", valor, sep = "")), 
                                   size = 4, alpha = 0.6) +
  ggrepel::geom_text_repel(data = ~filter(.x, outlier),
                           aes(label = nombre),
                           fontface = "bold", size = 4, point.padding = 4, angle = 90, hjust = 0) +
  scale_color_manual(values = c("black", "#F94C6A")) +
  theme_minimal() +
  theme(axis.title = element_blank()) +
  guides(color = guide_none(),
         y =  guide_none()) +
  scale_y_continuous(expand = expansion(c(0.2, 0.2))) # aumentar margen del eje vertical

Finalmente, para permitir la interactividad del gráfico, debemos generarlo con la función girafe(), la cual recibe el objeto con el gráfico además de varias opciones para personalizar la visualización:

girafe(ggobj = grafico_outliers_interactivo, 
       # dimensiones del gráfico
       width_svg = 9, height_svg = 4,
       # opciones para los tooltips
       options = list(
         # ocultar barra de opciones
         opts_toolbar(hidden = "selection", saveaspng = FALSE),
         # color al poner el cursor encima
         opts_hover(css = "fill: white; stroke: black;"),
         # personalizar la apariencia del tooltip
         opts_tooltip(opacity = 0.7, use_fill = TRUE,
                      css = "font-family: sans-serif; font-size: 70%; color: white; padding: 4px; border-radius: 5px;"))
)
chepama pachache mapacha chachema 0 50 100 150

Toca o posa el cursor sobre un punto para ver la información extra! ¿Puedes encontrar el punto que dice pamache? 🦝



  1. El código para este gráfico está disponible en este Gist. ↩︎

  2. En este ejemplo solamente buscaremos outliers que estén por sobre la distribución de los datos, pero si queremos buscar datos outliers por debajo; es decir, valores pequeños anómalos, la fórmula es la misma pero restando el 1.5 * IQR al primer cuartil. ↩︎

  3. Esto funciona porque, al anteponer la colita de chancho (~) a la función, creamos una función lambda que reciba los datos directamente sin tener que especificar el nombre del objeto (conveniente, por ejemplo, si pasaste directo de modificar los datos a hacer el gráfico sin crear un objeto intermedio). ↩︎

  4. En R, decir variable == TRUE es lo mismo que decir simplemente variable, porque variable es TRUE; o sea que puedes hacer filter(variable) en vez de filter(variable == TRUE), y de la misma forma, filter(!variable) en vez de filter(variable == FALSE)↩︎

Fecha de publicación:
June 18, 2025
Extensión:
11 minute read, 2243 words
Tags:
limpieza de datos ggplot2 gráficos visualización de datos estadística
Ver también:
Feliz cumpleaños, {ggplot2}!
Limpieza y recodificación de datos de texto en R con {stringr}
Generar contenido en serie usando loops en un reporte Quarto