9 Modelo DBSCAN para detectar anomalías

9.1 Introducción

En el Capítulo 8 estudiamos dos métricas para detectar anomalías con origen en los modelos de aprendizaje de máquina: la distancia kNN y LOF. La primera técnica se basa en la distancia para encontrar anomalías, lo cual le permite encontrar anomalías globales. Por otro lado, el LOF es un método basado en densidad que permite encontrar anomalías locales.

En este capítulo estudiaremos el uso del modelo DBSCAN (Density-Based Spatial Clustering of Applications with Noise) para detectar anomalías. El DBSCAN es otra técnica basada en densidades que también permite encontrar anomalías locales. Intuitivamente, los métodos basados en densidad identifican regiones de alta concentración de datos y consideran como anomalías aquellos puntos que se encuentran en regiones de baja densidad. De hecho, a diferencia del LOF, el método que estudiaremos en este capítulo permite encontrar anomalías grupales. A diferencia del LOF que busca la densidad alrededor de cada punto y por eso identifica anomalías locales individuales, esta técnica se concentrará en grupos de observaciones, lo que permitirá encontrar anomalías locales grupales.

El modelo DBSCAN es un modelo diseñado para construir clústeres que puede ser empleado en la tarea de detección de anomalías. Es decir, este modelo permite realizar tanto la tarea de clústering72 como la detección de anomalías.

Inicialmente, el DBSCAN fue propuesto por Ester et al. (1996) para identificar clústeres de formas arbitrarias y con robustez ante datos ruidosos. Es decir, datos que no siguen el patrón general o la estructura subyacente de los datos. El ruido en este contexto se refiere a los individuos que no pertenecen claramente a ningún clúster o que pueden interferir con la identificación de clústeres con mayor definición. En otras palabras, una de las características de DBSCAN es poder trabajar con datos atípicos que, como veremos, el algoritmo termina no clasificando. Una vez que se empezó a usar este algoritmo para hacer clústering, se descubrió que también podía ser empleado para detectar anomalías.

En la Sección 9.2 presentamos el algoritmo DBSCAN de manera intuitiva y formal. En la Sección 9.3 presentamos su implementación en R.

9.2 El algoritmo DBSCAN

En general, este tipo de algoritmo basado en densidad se caracteriza por identificar regiones de alta densidad de individuos separadas por regiones de baja densidad en el espacio de datos (Ver Figura 9.1). Al encontrar esas regiones de alta densidad de datos, también encuentra aquellos puntos que no pertenecen a esas zonas de alta densidad (puntos grises en la Figura 9.1). Esas observaciones de baja densidad que no se pueden clasificar en clústeres corresponderán a los datos anómalos localmente; es decir, que no se parecen a sus vecinos.



Figura 9.1: Tipo de clústeres que puede encontrar DBSCAN

Tipo de clústeres que puede encontrar DBSCAN
Fuente: Alonso, Hoyos y Largo (2024).



Así, DBSCAN, a diferencia de modelos tradicionales de clústering como los jerárquicos y k-means, puede lidiar con los valores anómalos (Alonso et al., 2025). Todos los valores anómalos serán identificados y marcados sin ser clasificados en ningún clúster. Por esta razón, DBSCAN también es empleado para la detección de anomalías (Kassambara, 2017).

Formalmente, el algoritmo de DBSCAN implica dos parámetros importantes: eps (\(\epsilon\)) y minPts. eps (\(\epsilon\)) representa el radio máximo del vecindario de una observación. Por otro lado, minPts es el número mínimo de puntos que deben estar dentro de la vecindad de un punto para que este sea considerado un punto central y, por tanto, se configure un clúster (incluyendo el mismo punto).

Intuitivamente, cada individuo en la muestra que tenga un número de vecinos (en el vecindario \(\epsilon\)) igual o mayor a minPts será marcado como un punto central (Ver Figura 9.2). Por otro lado, si un individuo \(w\) tiene un número de vecinos inferior a minPts, pero pertenece al vecindario \(\epsilon\) (\(\epsilon\)-neighborhood) de otro punto, entonces \(w\) será denominado un punto de frontera (border point) (Ver Figura 9.2). Y si un punto no es punto central (core point) ni uno en la frontera (border point), entonces será denominado ruido (noise point) o anomalía (Ver Figura 9.2). De esta manera, el algoritmo va asignando cada punto a un clúster o lo marca como anomalía. Para conocer el detalle de este algoritmo puedes ver el Capítulo 7 de Alonso et al. (2025).



Figura 9.2: Clasificación de individuos en el algoritmo DBSCAN con minPts = 6

Clasificación de individuos en el algoritmo DBSCAN con minPts = 6
Fuente: Alonso, Hoyos y Largo (2024).



Una de las características del algoritmo DBSCAN es que este no requiere la definición previa del número de clústeres, al ser este un resultado del mismo algoritmo. De hecho, el número de clústeres óptimo (\(q\)) es resultado del valor óptimo del parámetro eps (\(\epsilon\)). Chen et al. (2020) sugiere emplear la media de las distancias de cada individuo a sus \(k\) vecinos más cercanos (distancia kNN) para encontrar dicho parámetro.

El algoritmo implica calcular la distancia kNN para cada individuo y diferentes valores del parámetro eps y posteriormente calcular la suma cuadrada de las distancias kNN. Empleando ese resultado, se busca el valor de eps que minimiza dicha suma.

9.3 Implementación en R

Para detectar anomalías con DBSCAN se requiere hacer el ejercicio de clústering, veamos en detalle cómo realizar el procedimiento de clusterización con DBSCAN en R. Si ya dominas esta técnica, puedes saltarte esta subsección. Si quieres estudiar en más detalle DBSCAN puedes consultar el Capítulo 7 de Alonso et al. (2025).

En este capítulo seguiremos empleando los mismos datos que trabajamos en los Capítulos 2, 3, 4 y 5. Los datos provienen de Hofmann (1994) y se encuentran en el archivo datos_credito.RData que se puede descargar de la página web del libro (https://www.icesi.edu.co/editorial/deteccion-anomalias). La descripción de las variables se encuentra en la Sección 2.4. Carguemos los datos:

load("./datos/datos_credito.RData")

Los datos están cargados en el objeto de clase data.frame que denominamos german. Ese objeto tiene 14 variables y 1000 clientes. Seleccionemos solo las variables cuantitativas para poder aplicar DBSCAN. Empleemos la función select_if() del paquete dplyr (Wickham et al., 2021) para hacer esta tarea más sencilla.

library(dplyr)
german_cuanti <- german %>% 
     select_if(is.numeric) 

Recuerden que contamos con mil observaciones y 6 variables.

9.3.1 Algoritmo DBSCAN en R

Este algoritmo se puede implementar en R por medio de la función dbscan() del paquete dbscan (Hahsler et al., 2019). Pero antes de emplear esta función es necesario conocer cuál es el valor óptimo del parámetro eps. La propuesta de Chen et al. (2020) se puede implementar en R empleando la función n_clusters_dbscan del paquete parameters.

Esta función tiene los siguientes argumentos

n_clusters_dbscan(x, standardize, include_factors, eps_range = , distance_method)

donde:

  • x: objeto con datos de clase data.frame

  • standardize: si es TRUE se estandarizarán los datos, este es el valor por defecto.

  • incluir_factores: si es TRUE, las variables de clase factor se convierten en objetos numéricos para ser incluidas en los datos para determinar el número de clústeres. Por defecto, es igual a FALSE; es decir, se eliminan las variables que sean de clase factor. Esto se hace porque la mayoría de los métodos que determinan el número de conglomerados solo funcionan con variables cuantitativas.

  • min_size: el número mínimo de individuos (incluyendo al mismo individuo) requeridos en el vecindario \(\epsilon\) (\(\epsilon\)-neighborhood) para ser considerado como un punto central (core point). Por defecto, el valor es el 10% de la muestra (min_size = 0.1). Si se emplea un entero, este será el número de observaciones. Equivale al parámetro minPts.

  • eps_range: El rango sobre el cual se evaluarán los posibles valores del parámetro eps.

  • method: El método para escoger el parámetro eps óptimo. En nuestro caso, empleemos method = “SS” para que se calcule la suma cuadrada.

  • distance_method: El tipo de distancia a calcular. Este elemento es pasado a la función dist(). . Por defecto, el método es “euclidean”; los otros posibles valores son “maximum”, “manhattan”, “canberra”, “binary” y “minkowski”. No obstante, para el caso del algoritmo DBSCAN no se encuentra habilitada la opción para otra distancia.

El valor óptimo del parámetro eps (\(\epsilon\)) para el algoritmo DBSCAN y el método de la distancia kNN (Ver Sección 8.1) y empleando la distancia euclidiana lo podemos encontrar con el siguiente código empleando nuestros datos ya estandarizados (datos_est) y la distancia euclidiana.

library(parameters)
library(dbscan)
res_dbscan_knn <- n_clusters_dbscan(german_cuanti, 
                                standardize = TRUE,
                                eps_range = c(0.001, 3),
                                min_size = 5,
                                distance_method = "euclidean",
                                method = "SS")

Los resultados del eps óptimo los podemos visualizar imprimiendo el resultado en la consola o por medio de la Figura 9.3.

res_dbscan_knn
##           eps n_Clusters total_SS
## 1  0.00100000          0 5994.000
## 2  0.06220408          0 5994.000
## 3  0.12340816          7 5904.835
## 4  0.18461224         10 5614.148
## 5  0.24581633         21 5381.524
## 6  0.30702041         21 5013.082
## 7  0.36822449         24 4717.736
## 8  0.42942857         32 4498.607
## 9  0.49063265         33 4225.445
## 10 0.55183673         32 4011.373
## 11 0.61304082         33 3853.884
## 12 0.67424490         32 3663.358
## 13 0.73544898         33 3511.788
## 14 0.79665306         34 3429.663
## 15 0.85785714         34 3372.487
## 16 0.91906122         10 4475.440
## 17 0.98026531          4 4352.314
## 18 1.04146939          6 4236.618
## 19 1.10267347          6 4167.380
## 20 1.16387755          7 4146.265
## 21 1.22508163          7 4113.196
## 22 1.28628571          6 4100.781
## 23 1.34748980          5 4078.374
## 24 1.40869388          6 4063.361
## 25 1.46989796          6 4050.378
## 26 1.53110204          6 4035.224
## 27 1.59230612          6 4036.018
## 28 1.65351020          6 4033.557
## 29 1.71471429          6 4036.916
## 30 1.77591837          2 4797.414
## 31 1.83712245          2 4809.183
## 32 1.89832653          2 4816.140
## 33 1.95953061          2 4817.710
## 34 2.02073469          2 4838.101
## 35 2.08193878          2 4846.380
## 36 2.14314286          2 4843.118
## 37 2.20434694          2 4862.773
## 38 2.26555102          2 4878.640
## 39 2.32675510          2 4895.874
## 40 2.38795918          2 4906.858
## 41 2.44916327          2 4906.858
## 42 2.51036735          2 4926.477
## 43 2.57157143          2 4915.415
## 44 2.63277551          2 4938.087
## 45 2.69397959          2 4939.577
## 46 2.75518367          2 4939.577
## 47 2.81638776          1 5963.098
## 48 2.87759184          1 5963.098
## 49 2.93879592          1 5963.098
## 50 3.00000000          1 5963.098
plot(res_dbscan_knn)


Figura 9.3: Seleccionando el valor óptimo del parámetro eps por medio de la distancia kNN

Seleccionando el valor óptimo del parámetro eps por medio de la distancia kNN
Fuente: calculos propios
## NULL



El resultado que minimiza la suma de las distancias kNN corresponde a un 0.8578571. Nota que en este caso esto implica 34 clústeres.

El valor óptimo del parámetro eps (\(\epsilon\)) se puede extraer de la siguiente manera:

# Extraer el valor óptimo de eps

eps_opt <- attributes(res_dbscan_knn)$eps

eps_opt
## [1] 0.8578571

9.3.2 Detección de anomaías con DBSCAN en R

Ya hemos construido los clústeres, ahora podemos calcular la membrecía de cada individuo. Recuerda que aquellos individuos que no se asignen a un clúster se entenderán como anomalías.

La partición de los datos se puede realizar empleando la función dbscan() del paquete dbscan (Hahsler et al., 2019). La función típicamente incluye los siguientes argumentos:

dbscan(x, eps, minPts)

donde:

  • x: los datos estandarizados o una matriz de proximidad73.

  • eps: el parámetro (\(\epsilon\)) que representa el radio máximo del vecindario.

  • minPts: el número mínimo de individuos (incluyendo al mismo individuo) requeridos en el vecindario \(\epsilon\) (\(\epsilon\)-neighborhhood) para ser considerado como un punto central (core point).

En nuestro caso, primero debemos estandarizar los datos. Estandaricemos los datos empleando la función scale() de la base de R.

# Estandarizar los datos
german_cuanti_est <- scale(german_cuanti)

Ahora procedamos a particionar las observaciones con el siguiente código:

# Instalar el paquete si no se tiene
# install.packages("dbscan")
# Cargar el paquete 

library("dbscan")


# DBSCAN 

res_DBSCAN <- dbscan::dbscan(x = german_cuanti_est  , eps = eps_opt, 
                             minPts = 5)

Las membrecías a los clústeres se pueden encontrar fácilmente con el siguiente código:

# Resultados de la asignación
head(res_DBSCAN$cluster)
## [1] 1 2 0 0 0 0
# Calcular observaciones por clúster
table(res_DBSCAN$cluster)
## 
##   0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19 
## 273  62  32  32  45  43  11   9  71   7  15  99  28  16  37  32  12   6   7   7 
##  20  21  22  23  24  25  26  27  28  29  30  31  32  33  34 
##  22   6  10   8  15   6  23   6  14   5   6  15   9   6   5

Nota que en nuestro caso tenemos 3, 4, 5, 6, 18, 19, 20, 21, 22, 23, 28, 30, 31, 37, 43, 45, 49, 52, 57, 58, 64, 66, 73, 74, 79, 81, 88, 96, 97, 100, 106, 107, 111, 113, 117, 120, 132, 135, 137, 138, 139, 147, 157, 160, 161, 164, 171, 176, 181, 187, 188, 191, 197, 198, 205, 206, 207, 210, 211, 213, 216, 227, 228, 235, 237, 238, 240, 242, 243, 247, 251, 256, 260, 263, 264, 265, 269, 273, 275, 286, 287, 292, 293, 295, 301, 305, 317, 331, 334, 335, 341, 343, 345, 349, 350, 367, 373, 374, 375, 379, 382, 396, 398, 399, 403, 410, 412, 414, 416, 417, 418, 423, 426, 430, 431, 432, 433, 450, 451, 457, 466, 468, 470, 492, 493, 495, 497, 500, 508, 510, 518, 521, 523, 526, 528, 530, 531, 537, 540, 544, 548, 550, 553, 557, 564, 569, 581, 582, 587, 590, 591, 598, 601, 607, 612, 616, 617, 618, 630, 636, 638, 640, 651, 654, 658, 660, 669, 671, 673, 681, 684, 685, 686, 690, 694, 702, 705, 710, 715, 716, 717, 722, 724, 725, 727, 729, 732, 735, 737, 738, 739, 745, 746, 749, 751, 757, 758, 761, 764, 766, 769, 770, 772, 775, 785, 788, 792, 793, 797, 802, 806, 807, 808, 809, 812, 813, 816, 817, 818, 819, 823, 827, 828, 829, 830, 833, 838, 845, 847, 852, 855, 864, 869, 871, 872, 875, 880, 881, 882, 885, 887, 888, 890, 891, 896, 902, 903, 906, 909, 916, 918, 922, 924, 925, 928, 933, 940, 941, 942, 944, 946, 949, 951, 954, 958, 962, 970, 972, 976, 977, 981, 984, 991 observaciones que no han sido clasificadas. Es decir, que fueron asignadas al clúster 0.

Ahora extraigamos el número de los individuos que se pudieron agrupar. Esto lo podemos hacer con el siguiente código:

# Crear objeto con el índice de las observaciones  (número de la observación) que son outliers.

DBSCAN_anomal <- as.data.frame(res_DBSCAN$cluster) %>%   
        mutate(ID = row_number()) %>% 
  rename(Cluster = `res_DBSCAN$cluster`) %>% 
        filter(Cluster == 0) %>% 
        dplyr::select(ID)

dim(DBSCAN_anomal)
## [1] 273   1
head(DBSCAN_anomal, 5)
##   ID
## 1  3
## 2  4
## 3  5
## 4  6
## 5 18

En el objeto DBSCAN_anomal tenemos el índice (número) de las 3, 4, 5, 6, 18, 19, 20, 21, 22, 23, 28, 30, 31, 37, 43, 45, 49, 52, 57, 58, 64, 66, 73, 74, 79, 81, 88, 96, 97, 100, 106, 107, 111, 113, 117, 120, 132, 135, 137, 138, 139, 147, 157, 160, 161, 164, 171, 176, 181, 187, 188, 191, 197, 198, 205, 206, 207, 210, 211, 213, 216, 227, 228, 235, 237, 238, 240, 242, 243, 247, 251, 256, 260, 263, 264, 265, 269, 273, 275, 286, 287, 292, 293, 295, 301, 305, 317, 331, 334, 335, 341, 343, 345, 349, 350, 367, 373, 374, 375, 379, 382, 396, 398, 399, 403, 410, 412, 414, 416, 417, 418, 423, 426, 430, 431, 432, 433, 450, 451, 457, 466, 468, 470, 492, 493, 495, 497, 500, 508, 510, 518, 521, 523, 526, 528, 530, 531, 537, 540, 544, 548, 550, 553, 557, 564, 569, 581, 582, 587, 590, 591, 598, 601, 607, 612, 616, 617, 618, 630, 636, 638, 640, 651, 654, 658, 660, 669, 671, 673, 681, 684, 685, 686, 690, 694, 702, 705, 710, 715, 716, 717, 722, 724, 725, 727, 729, 732, 735, 737, 738, 739, 745, 746, 749, 751, 757, 758, 761, 764, 766, 769, 770, 772, 775, 785, 788, 792, 793, 797, 802, 806, 807, 808, 809, 812, 813, 816, 817, 818, 819, 823, 827, 828, 829, 830, 833, 838, 845, 847, 852, 855, 864, 869, 871, 872, 875, 880, 881, 882, 885, 887, 888, 890, 891, 896, 902, 903, 906, 909, 916, 918, 922, 924, 925, 928, 933, 940, 941, 942, 944, 946, 949, 951, 954, 958, 962, 970, 972, 976, 977, 981, 984, 991 observaciones que no se pudieron agrupar. Estas observaciones serán consideradas como anomalías grupales locales.

9.4 Comentarios finales

En este capítulo hemos estudiado cómo emplear un método de clústering particionado basado en la densidad de los datos como DBSCAN para detectar anomalías locales y multivariadas. Es importante mencionar que los resultados de este algoritmo son muy sensibles a la escogencia de del parámetro eps. Por esta razón, es importante tener una estrategia para fijar, a partir de los datos, este parámetro.

Finalmente, es importante recordar que el algoritmo DBSCAN es una herramienta más para hacer la tarea de detectar anomalías que debes tener en tu caja de herramientas. Como las otras herramientas que hemos estudiado, DBSCAN no es una solución mágica que funcione en todos los casos, pero tiene algunas ventajas sobre otros algoritmos de agrupamiento.

Por ejemplo, DBSCAN no requiere que especifiques el número de vecinos cercanos como si se requiere cuando se emplea la distancia kNN y LOF. Esto puede ser una gran ventaja, ya que a menudo es difícil saber cuántos vecinos debemos emplear.

En el siguiente capítulo continuaremos nuestro análisis de algoritmos para detectar anomalías con modelos de aprendizaje de máquina. Estudiaremos una filosofía totalmente diferente.

En este capítulo hemos estudiado cómo emplear un método de clústering particionado basado en la densidad de los datos como DBSCAN para detectar anomalías locales y multivariadas. Es importante mencionar que los resultados de este algoritmo son muy sensibles a la escogencia de del parámetro eps. Por esta razón, es importante tener una estrategia para fijar, a partir de los datos, este parámetro.

Finalmente, es importante recordar que el algoritmo DBSCAN es una herramienta más para hacer la tarea de detectar anomalías que debes tener en tu caja de herramientas. Como las otras herramientas que hemos estudiado, DBSCAN no es una solución mágica que funcione en todos los casos, pero tiene algunas ventajas sobre otros algoritmos de agrupamiento.

Por ejemplo, DBSCAN no requiere que especifiques el número de vecinos cercanos como si se requiere cuando se emplea la distancia kNN y LOF. Esto puede ser una gran ventaja, ya que a menudo es difícil saber cuántos vecinos debemos emplear.

En el Capítulo 10 estudiaremos un modelo de ML que sigue una filosofía totalmente diferente: el modelo de Isolation Forest.



Referencias

Alonso, J. C., Hoyos, C. C., & Largo, M. F. (2025). Una introducción a los modelos de clústering empleando r. Universidad Icesi. https://doi.org/https://doi.org/10.18046/EUI/bda.h.6
Chen, Y., Ruys, W., & Biros, G. (2020). KNN-DBSCAN: A DBSCAN in high dimensions. arXiv Preprint arXiv:2009.04552.
Ester, M., Kriegel, H.-P., Sander, J., Xu, X., et al. (1996). A density-based algorithm for discovering clusters in large spatial databases with noise. Kdd, 96, 226–231.
Hahsler, M., Piekenbrock, M., & Doran, D. (2019). dbscan: Fast density-based clustering with R. Journal of Statistical Software, 91(1), 1–30. https://doi.org/10.18637/jss.v091.i01
Hofmann, H. (1994). Statlog (German Credit Data). UCI Machine Learning Repository.
Kassambara, A. (2017). Practical guide to cluster analysis in r: Unsupervised machine learning (Vol. 1). Sthda.
Wickham, H., François, R., Henry, L., & Müller, K. (2021). Dplyr: A grammar of data manipulation. https://CRAN.R-project.org/package=dplyr

  1. En Alonso et al. (2025) se presenta una discusión detallada de la tarea de clústering y los diferentes modelos disponibles para realizarla.↩︎

  2. Si se quisiera emplear otro tipo de distancia diferente a la euclidiana, esto se puede realizar empleando una matriz de proximidad con la función dist(). ↩︎