Cuando hablamos de clustering, lo primero que viene a la mente suele ser k-means. Pero k-means tiene dos limitaciones importantes: hay que decidir cuántos clusters queremos a priori, y trata cualquier punto como parte de algún grupo, aunque sea claramente un valor atípico.
DBSCAN (Density-Based Spatial Clustering of Applications with Noise) resuelve precisamente esos dos problemas:
Pero todo tiene un precio: DBSCAN depende crucialmente de un hiperparámetro llamado ε (épsilon), y elegirlo bien es la diferencia entre un análisis útil y uno completamente inservible.
En esta entrada vamos a ver:
Tabla de contenidos
La idea central de DBSCAN es muy intuitiva:
Un cluster es una región densa de puntos rodeada de regiones poco densas.
Piensa en una fiesta vista desde el techo: los grupos de gente conversando son los clusters, las personas sueltas paseando son ruido. DBSCAN identifica exactamente estos patrones en los datos, pero formalmente.
Para definir qué significa “denso”, DBSCAN necesita dos hiperparámetros numéricos:
En base a estos dos hiperparámetro, cada punto del conjunto de datos cae en una de la siguientes tres categorías:
minPts vecinos dentro del radio ε. Son el corazón de un cluster.El algoritmo arranca en un punto núcleo, expande el cluster siguiendo cadenas de vecinos densos, y cuando se queda sin vecinos densos, salta a otro punto núcleo y empieza un cluster nuevo.
Todo este proceso se puede ver en directo en el simulador. Una vez cargados los datos, basta con cambiar el modo de ejecución a Paso a paso o Automático para observar cómo el algoritmo va clasificando cada punto como núcleo, frontera o ruido. Pulsa Iniciar para verlo correr de golpe, o avanza manualmente si quieres seguir en detalle cómo se actualizan los puntos en la gráfica a medida que se forman los clusters.
Como en k-means con el núcleo de clusteres, aquí está el desafío a la hora de entrenar DBSCAN. Si nos equivocamos con ε, el resultado es desastroso:
Y lo peor: el valor “correcto” depende totalmente de la escala y la densidad de tus datos. No existe un valor universal.
Entonces, ¿cómo lo elegimos sin lanzar una moneda?
La técnica estándar (y la que utiliza casi todo el mundo) se llama: k-distance plot (gráfico de distancias al k-ésimo vecino).
El procedimiento tiene tres pasos:
k = minPts.El resultado es siempre una curva decreciente: empieza alta (los puntos en zonas dispersas, con vecinos lejanos), cae de forma abrupta en algún momento, y luego se aplana en valores bajos (los puntos en zonas densas).
Nota: hay implementaciones que ordenan al revés (de menor a mayor), produciendo una curva creciente en espejo. La idea y el procedimiento son los mismos, solo cambia la orientación visual.
La magia está en buscar el famoso codo (elbow) de la curva:
El valor de ε se elige justo en ese punto de transición, donde la curva deja de caer abruptamente y empieza a aplanarse.
¿Por qué funciona? Porque la gráfica separa de manera natural dos regímenes muy distintos: dentro de un cluster, los vecinos están todos a distancias parecidas y pequeñas. Fuera, las distancias se disparan. El codo marca la frontera.
Si no quieres programarlo a mano, la aplicación del Laboratorio de Analytics Lane implementa este proceso de forma visual:Simulador de DBSCAN
El flujo es muy sencillo:
Esto convierte una heurística abstracta en una decisión visual: ves el codo, y eliges.
Este gráfico no es un truco arbitrario, tiene una interpretación clara:
En otras palabras, lo que el codo te dice es:
Estás estimando la densidad mínima que define un cluster en tus datos.
Es una forma de dejar que los datos hablen, en lugar de imponerles un valor a ojo.
Ahora viene lo interesante: reproducir exactamente lo que hace la herramienta. El código TypeScript de la aplicación implementa esencialmente este algoritmo:
En Python son apenas unas líneas, y vamos a seguir exactamente esa misma secuencia.
Queremos, para cada punto, la distancia a su k-ésimo vecino más cercano excluyendo el propio punto (justo lo que hace la aplicación con su filter(candidate.id !== point.id)).
En sklearn hay un detalle importante: cuando llamas a kneighbors(X) sobre los mismos datos con los que entrenaste, el primer vecino de cada punto es él mismo, con distancia 0. Por eso pedimos k + 1 vecinos: el primero (índice 0) es el propio punto, y los siguientes k son los vecinos reales.
from sklearn.neighbors import NearestNeighbors from sklearn.datasets import make_moons import numpy as np import matplotlib.pyplot as plt # Reemplaza X por tus datos X, _ = make_moons(n_samples=200, noise=0.2, random_state=42) k = 5 neigh = NearestNeighbors(n_neighbors=k + 1) neigh.fit(X) distances, _ = neigh.kneighbors(X)
kneighbors devuelve las distancias ordenadas a los k + 1 vecinos. Como el primero es el propio punto (distancia 0), nos quedamos con el último de la fila, que es el k-ésimo vecino real:
k_distances = distances[:, k]
k_distances = np.sort(k_distances)[::-1] # orden descendente
plt.plot(k_distances)
plt.xlabel("Puntos ordenados")
plt.ylabel(f"Distancia al {k}-ésimo vecino")
plt.title("K-distance plot")
plt.grid(True)
plt.show() Mira la gráfica, localiza el codo, y lee el valor en el eje Y. Ese es tu ε.
Esto funciona, pero tiene un problema: depende del ojo humano. Si quieres que el proceso sea reproducible y automático (como hace la herramienta interactiva), necesitas un método matemático para localizar el codo.
¿Cómo decide un programa dónde está el codo? Existen varios algoritmos, pero el más extendido por su simplicidad y robustez es el método de la máxima distancia a la cuerda.
La idea es elegante:
¿Por qué funciona? Porque el codo es, por definición, el punto donde la curva más se aleja de su tendencia lineal global. Si la curva fuera una recta perfecta, todos los puntos estarían a distancia cero. Cuanto más pronunciado sea el codo, mayor será la distancia máxima.
Visualmente, con la curva ordenada de mayor a menor:
primer → ●
punto ●
●
● ↕ máxima distancia a la cuerda
codo → ● ↕
●
●
● ● ● ● ● ● ● ● ● ← último punto La cuerda es la línea recta que une el primer punto (arriba a la izquierda) con el último (abajo a la derecha). El codo es el punto de la curva más alejado de esa línea.
Hay una trampa sutil. El eje X va de 0 a N (donde N puede ser miles de puntos), mientras que el eje Y son distancias pequeñas (a menudo entre 0 y 1). Si calculamos la distancia geométrica directamente, el eje X dominará por completo y la detección fallará.
Solución: normalizar ambos ejes al rango [0, 1] antes de calcular distancias. Así los dos ejes contribuyen por igual a la geometría.
Una nota antes del código: por qué la app muestra ε como entero y aquí no
En la aplicación interactiva, ε se presenta como un número entero por una razón puramente didáctica: los datos con los que trabaja están en coordenadas grandes (distancias del orden de cientos), así que mostrar
ε = 50en lugar deε = 49.83es más limpio en la interfaz y la parte decimal es irrelevante en esa escala.ε es un parámetro real que recibe DBSCAN, y si tus datos están escalados (con
StandardScaler, o sintéticos comomake_moons), las distancias serán del orden de 0.1 a 1.0. Por lo que se debe tratar como un real.
import numpy as np
def find_elbow(k_distances):
"""
Detecta el codo en la curva del k-distance plot usando el método
de la máxima distancia a la cuerda.
Funciona indistintamente con la curva ordenada de mayor a menor
(descendente, como en la aplicación) o de menor a mayor (ascendente),
porque la geometría es simétrica.
Devuelve
--------
elbow_idx : int
Índice del codo en el array.
elbow_eps : float
Valor de epsilon en ese punto, sin redondear.
"""
n = len(k_distances)
# 1. Coordenadas de la curva
x = np.arange(n, dtype=float)
y = np.asarray(k_distances, dtype=float)
# 2. Normalizar ambos ejes a [0, 1] para que sean comparables
x_norm = (x - x.min()) / (x.max() - x.min())
y_norm = (y - y.min()) / (y.max() - y.min())
# 3. Definir la cuerda (primer punto -> último punto)
p1 = np.array([x_norm[0], y_norm[0]])
p2 = np.array([x_norm[-1], y_norm[-1]])
line_vec = p2 - p1
line_len = np.linalg.norm(line_vec)
# 4. Distancia perpendicular de cada punto a la cuerda
# (fórmula del producto cruz en 2D, en valor absoluto)
points = np.column_stack([x_norm, y_norm])
cross = np.abs(
line_vec[0] * (p1[1] - points[:, 1]) -
(p1[0] - points[:, 0]) * line_vec[1]
)
distances_to_line = cross / line_len
# 5. El codo es el punto con máxima distancia a la cuerda
elbow_idx = int(np.argmax(distances_to_line))
elbow_eps = float(k_distances[elbow_idx])
return elbow_idx, elbow_eps Y se usa así, encadenado con los pasos anteriores:
elbow_idx, eps_auto = find_elbow(k_distances)
print(f"Codo detectado en el índice {elbow_idx}, ε ≈ {eps_auto:.4f}") Codo detectado en el índice 31, ε ≈ 0.2746
Para ganar confianza en el valor detectado, conviene representarlo sobre la gráfica:
plt.plot(k_distances, label="Distancia al k-ésimo vecino")
plt.axvline(elbow_idx, color="red", linestyle="--", label=f"Codo (idx={elbow_idx})")
plt.axhline(eps_auto, color="green", linestyle="--", label=f"ε ≈ {eps_auto:.4f}")
plt.xlabel("Puntos ordenados (descendente)")
plt.ylabel(f"Distancia al {k}-ésimo vecino")
plt.title("Detección automática del codo")
plt.legend()
plt.grid(True)
plt.show() Este es exactamente el proceso que hace la herramienta interactiva por debajo: calcula el k-distance plot, normaliza los ejes, busca el punto de máxima distancia a la cuerda, y te lo muestra como sugerencia.
kneedSi prefieres no implementarlo a mano, existe una librería ya hecha llamada kneed, que implementa el algoritmo Kneedle (Satopaa et al., 2011), una versión más sofisticada de la misma idea:
from kneed import KneeLocator
knee = KneeLocator(
range(len(k_distances)),
k_distances,
curve="convex",
direction="decreasing", # nuestra curva está ordenada de mayor a menor
)
eps_auto = float(k_distances[knee.knee])
print(f"ε automático (kneed) ≈ {eps_auto:.4f}") Ambos métodos suelen dar valores muy parecidos. La función casera tiene la ventaja de ser transparente: ves exactamente qué está haciendo y por qué.
Una vez tienes el valor, ejecutar DBSCAN es trivial:
from sklearn.cluster import DBSCAN eps = eps_auto # valor decimal devuelto por find_elbow, sin redondear min_samples = k # el mismo k que usaste antes model = DBSCAN(eps=eps, min_samples=min_samples) labels = model.fit_predict(X)
labels te devuelve la asignación de cada punto a un cluster. El valor -1 indica ruido.
Una regla práctica muy extendida:
minPts ≈ 2 × (número de dimensiones)
En 2D, minPts = 4 o 5 suele funcionar bien. En espacios de mayor dimensión, conviene aumentarlo.
DBSCAN se basa en distancias, así que si una variable va de 0 a 1 y otra de 0 a 1.000.000, la segunda dominará todo. Estandariza siempre:
from sklearn.preprocessing import StandardScaler X_scaled = StandardScaler().fit_transform(X)
El método del codo es una heurística, no una fórmula matemática exacta. A veces el codo es muy claro, otras veces hay varios candidatos razonables. No te obsesiones: prueba dos o tres valores cercanos y compara los resultados.
Antes de dar por bueno un clustering:
La selección de ε en DBSCAN no tiene por qué ser un misterio:
Una herramienta interactiva como la del Laboratorio de Analytics Lane aporta algo muy valioso: convierte una heurística abstracta en una decisión visual e inmediata. Y lo mejor es que ese mismo proceso es totalmente reproducible en Python con apenas unas líneas de código.
Al final, DBSCAN es un algoritmo poderoso, pero como toda herramienta potente, exige entenderla. Saber elegir ε es saber escuchar lo que los datos te están diciendo sobre su propia densidad.
Imagen de WikiImages en Pixabay
Cualquier país desarrollado tiene sus propios indicadores folclóricos. España, por motivos que tienen mucho que…
Entras a la web de tu banco. En la página principal, un banner llamativo: “Depósito…
Seguimos ampliando el laboratorio de Analytics Lane con el lanzamiento de la versión 1.3, disponible…
Cerramos la serie internacional con la categoría más estrambótica de todas: indicadores que predicen el…
Si el WOE y el IV son la base matemática del credit scoring, el binning…
Seguimos iterando sobre el laboratorio de Analytics Lane y lanzamos la versión 1.2, disponible en:https://www.analyticslane.com/lab/es…
This website uses cookies.