Gráficos para identificar datos outliers o anómalos en R
18/6/2025
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 == TRUE
4.
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()
ageom_point_interactive()
- Pasar de
geom_jitter()
ageom_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;"))
)
Toca o posa el cursor sobre un punto para ver la información extra! ¿Puedes encontrar el punto que dice pamache? 🦝
-
El código para este gráfico está disponible en este Gist. ↩︎
-
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. ↩︎ -
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). ↩︎ -
En R, decir
variable == TRUE
es lo mismo que decir simplementevariable
, porquevariable
esTRUE
; o sea que puedes hacerfilter(variable)
en vez defilter(variable == TRUE)
, y de la misma forma,filter(!variable)
en vez defilter(variable == FALSE)
. ↩︎
- Fecha de publicación:
- June 18, 2025
- Extensión:
- 11 minute read, 2243 words