Módulo 4 · Aprendizaje no supervisado

Lección 3 de 4

Clustering por densidad y jerárquico

En esta páginaEl problema

El problema

Tienes 206 puntos en dos dimensiones. 200 forman dos lunas entrelazadas de 100 puntos cada una. Los otros 6 son lecturas sueltas que no pertenecen a ninguna luna. Aplicaste k-means con k=2 y comparaste sus grupos con el origen de cada punto:

origengrupo 0grupo 1
luna 07624
luna 12575
suelto24

Cada luna quedó partida entre los dos grupos. Los 6 puntos sueltos también recibieron un grupo.

DBSCAN y puntos de ruido

DBSCAN agrupa por densidad. Usa dos parámetros:

  • eps: el radio del vecindario de cada punto, en las unidades de los datos.
  • min_samples: cuántos puntos debe haber en el vecindario, contando el propio punto, para que sea un punto núcleo.

Dos núcleos que están a distancia eps o menos quedan en el mismo grupo, y la cadena sigue de núcleo en núcleo. Un punto borde no es núcleo, pero está en el vecindario de un núcleo y se une a su grupo. Un punto que no es núcleo ni borde es ruido y recibe la etiqueta -1.

Estos ocho puntos están sobre una recta, así que la distancia es la diferencia de sus coordenadas x.

Pregunta. Con eps=0.6 y min_samples=3, anota qué índices son núcleo y la etiqueta de cada punto. Después decide qué cambia con min_samples=4.

Python 3 · ejemplo reproducible
import numpy as np
from sklearn.cluster import DBSCAN

recta = np.array([0.0, 0.5, 1.0, 1.5, 5.0, 5.4, 5.8, 9.0])
puntos = np.column_stack([recta, np.zeros(len(recta))])

for minimo in [3, 4]:
    modelo = DBSCAN(eps=0.6, min_samples=minimo).fit(puntos)
    print(f"min_samples={minimo}")
    print("  núcleos: ", modelo.core_sample_indices_)
    print("  etiquetas:", modelo.labels_)
Salida esperada

La salida esperada está oculta.

Formas no convexas

k-means asigna cada punto al centro más cercano. Cada grupo queda dentro de una región convexa alrededor de su centro, y una luna curva no cabe en esa región. DBSCAN no usa centros: un grupo es cualquier cadena de núcleos, sin importar su forma.

X reúne los 206 puntos del problema: las 200 filas de make_moons en X_lunas, con su luna en y_lunas, y los 6 puntos sueltos al final. origen guarda el nombre de cada uno: «luna 0», «luna 1» o «suelto».

Mostrar códigoOcultar códigoPython
Python 3 · ejemplo reproducible
from sklearn.datasets import make_moons

X_lunas, y_lunas = make_moons(n_samples=200, noise=0.06, random_state=0)
rng = np.random.default_rng(0)
sueltos = rng.uniform([-1.5, -1.0], [2.5, 1.5], size=(6, 2))

X = np.vstack([X_lunas, sueltos])
origen = np.concatenate([np.where(y_lunas == 0, "luna 0", "luna 1"), ["suelto"] * 6])

Pregunta. Con eps=0.2 y min_samples=5, ¿cuántos grupos encontrará DBSCAN en las lunas? ¿Qué etiqueta recibirán los 6 puntos sueltos?

Ver resultado

Cada fila cuenta los puntos de un origen: luna 0, luna 1 o suelto. Las columnas cuentan cuántos puntos de ese origen recibió cada grupo: dos columnas para k-means con k=2 y tres para DBSCAN con eps=0.2 y min_samples=5, donde -1 es ruido. Por ejemplo, la fila «suelto» con 5 en «DBSCAN -1» dice que DBSCAN marcó como ruido cinco de los seis puntos sueltos.

origenk-means 0k-means 1DBSCAN -1DBSCAN 0DBSCAN 1
luna 0762401000
luna 1257500100
suelto24501
Lectura. DBSCAN encontró dos grupos y cada uno contiene una luna completa. Cinco puntos sueltos quedaron como ruido. El sexto cayó dentro de la luna 1, con suficientes vecinos a menos de eps para ser núcleo, y se unió a ella. k-means no tiene etiqueta de ruido: obliga a cada punto a entrar en un grupo.

eps fija qué tan cerca deben estar los puntos para encadenarse.

Pregunta. Al subir eps de 0.10 a 0.40 con min_samples=5, ¿qué pasa con el número de grupos? ¿Y con el número de puntos de ruido?

Python 3 · ejemplo reproducible
print(" eps  grupos  ruido")
for eps in [0.10, 0.15, 0.20, 0.25, 0.30, 0.40]:
    etiquetas = DBSCAN(eps=eps, min_samples=5).fit_predict(X)
    n_grupos = len(set(etiquetas) - {-1})
    n_ruido = int((etiquetas == -1).sum())
    print(f"{eps:.2f}  {n_grupos:>6}  {n_ruido:>5}")
Salida esperada

La salida esperada está oculta.

Explorador

Vecindarios de DBSCAN

Mueve eps y min_samples y observa qué puntos son núcleo, borde o ruido.

Qué probar

Usa «Expandir grupo» para ver cómo un grupo crece de núcleo en núcleo.

Círculo marcado: 4 puntos dentro de eps → núcleo

Pasa el cursor sobre un punto para ver su vecindario.

El color indica el grupo; la forma, el papel del punto.

Parámetros
eps = 0.80 y min_samples = 4: 2 grupos, 65 núcleos, 7 bordes y 7 puntos de ruido. Un núcleo tiene al menos 4 puntos dentro de su círculo de radio eps, contándose a sí mismo.

Enlaces aglomerativos

El clustering jerárquico aglomerativo empieza con cada punto como un grupo propio. En cada paso une los dos grupos más cercanos, hasta que queda uno solo. El enlace define la distancia entre dos grupos:

  • average: el promedio de las distancias entre todos los pares de puntos, uno de cada grupo.
  • ward: el aumento de la suma de cuadrados dentro del grupo (distancias al cuadrado de cada punto al centro de su grupo) que produciría la unión. linkage reporta como altura √(2·aumento), que está en unidades de distancia.

scipy.cluster.hierarchy.linkage devuelve la matriz Z con una fila por unión. Las columnas son: índice del primer grupo, índice del segundo, distancia de la unión y tamaño del grupo nuevo. Los puntos originales tienen índices 0 a n-1. El grupo creado en la fila i recibe el índice n+i.

Pregunta. Con los seis valores 0, 1, 3.5, 10, 12 y 20 y enlace average, escribe las dos primeras uniones y su distancia. Calcula la distancia de la tercera unión y el índice del grupo que crea.

Python 3 · ejemplo reproducible
from scipy.cluster.hierarchy import fcluster, linkage

valores = np.array([[0.0], [1.0], [3.5], [10.0], [12.0], [20.0]])

Z_average = linkage(valores, method="average")
Z_ward = linkage(valores, method="ward")

print(np.round(Z_average, 3))
print("alturas ward:", np.round(Z_ward[:, 2], 3))
Salida esperada

La salida esperada está oculta.

Corte del dendrograma

Cortar el dendrograma en k grupos equivale a deshacer las últimas k-1 uniones. fcluster(Z, t=k, criterion="maxclust") hace ese corte y numera los grupos desde 1.

Estos datos tienen tres nubes de 60 puntos y dos puntos lejanos cerca de (9, 9).

Pregunta. Al cortar en 3 grupos, ¿qué enlace deja los dos puntos lejanos como un grupo propio? ¿Qué le pasa entonces a las tres nubes con ese enlace?

Python 3 · ejemplo reproducible
from sklearn.datasets import make_blobs

X_nubes, _ = make_blobs(
    n_samples=[60, 60, 60],
    centers=[[0, 0], [4, 0], [2, 3.5]],
    cluster_std=0.6,
    random_state=0,
)
X_jer = np.vstack([X_nubes, [[9.0, 9.0], [9.5, 8.5]]])

for metodo in ["ward", "average"]:
    Z = linkage(X_jer, method=metodo)
    grupos = fcluster(Z, t=3, criterion="maxclust")
    tamanos = np.bincount(grupos)[1:]
    alturas = np.round(Z[-4:, 2], 2)
    print(f"{metodo:>7}: tamaños {tamanos}  últimas alturas {alturas}")
Salida esperada

La salida esperada está oculta.

Pregunta. Si cortas el árbol average en 4 grupos, ¿qué tamaños esperas?

Ver resultado

La frase da los tamaños que devuelve np.bincount(grupos)[1:] al cortar el mismo árbol average con fcluster(Z, t=4, criterion="maxclust"), en el orden de los números de grupo. Por ejemplo, el 2 inicial dice que el grupo 1 tiene dos puntos.

Con t=4, los tamaños de los grupos 1 a 4 son 2, 60, 60, 60.

Lectura. Deshacer una unión más separa las dos nubes que estaban juntas. El árbol se calcula una vez. Cada valor de k es otro corte del mismo árbol, no un modelo nuevo.

Clustering espectral

El clustering espectral construye un grafo que conecta cada punto con sus vecinos más cercanos y agrupa según qué partes del grafo están conectadas entre sí. Como DBSCAN, sigue la forma de los datos. A diferencia de DBSCAN, recibe el número de grupos y asigna un grupo a todo punto.

Pregunta. Con affinity="nearest_neighbors" y 10 vecinos por punto, ¿separará las dos lunas cuando no hay puntos sueltos?

Python 3 · ejemplo reproducible
import pandas as pd
from sklearn.cluster import SpectralClustering

espectral = SpectralClustering(
    n_clusters=2, affinity="nearest_neighbors", n_neighbors=10, random_state=0
)
grupos_espectral = espectral.fit_predict(X_lunas)
origen_lunas = np.where(y_lunas == 0, "luna 0", "luna 1")

tabla = pd.crosstab(
    origen_lunas, grupos_espectral, rownames=["origen"], colnames=["espectral"]
)
print(tabla)
Salida esperada

La salida esperada está oculta.

Práctica

Traza guiada

Para estos seis puntos sobre una recta, con eps=0.5 y min_samples=2, escribe antes de ejecutar qué índices son núcleo, cuántos grupos habrá y qué puntos serán ruido.

Python 3 · ejemplo reproducible
traza = np.array([[0.0], [0.4], [0.8], [3.0], [3.3], [7.0]])
modelo_traza = DBSCAN(eps=0.5, min_samples=2)

Comprueba con modelo_traza.fit(traza), core_sample_indices_ y labels_. Si una predicción falla, cuenta de nuevo los vecinos de cada punto incluyendo al propio punto.

Parámetro min_samples

Con X y eps=0.2, repite DBSCAN para min_samples igual a 3, 5, 10 y 20. Antes de procesar, predice con qué valor de min_samples las lunas empiezan a perder puntos y cuántos puntos de ruido habrá con 20. Imprime una tabla con grupos y ruido, y explica el resultado con la definición de punto núcleo.

Comparación de enlaces

Carga load_wine(return_X_y=True) y estandariza las 13 columnas con StandardScaler. Escribe:

  1. los tamaños de los 3 grupos con enlace ward y con enlace average
  2. las cuatro últimas alturas de cada árbol y si hay un salto grande entre ellas
  3. una tabla pd.crosstab de cada corte contra las tres clases de vino
  4. cuál de los dos enlaces dejó grupos muy pequeños y qué dice eso de los vinos que contienen

Reto opcional

Aplica el mismo SpectralClustering a X, que incluye los 6 puntos sueltos. Compara con la tabla de DBSCAN: ¿qué grupo recibe cada punto suelto y por qué el método no puede marcarlos como ruido?

Errores que revisar

  • Si DBSCAN devuelve un solo grupo con casi todos los puntos, eps es demasiado grande: con eps=0.30 las dos lunas se fundieron.
  • Si aparecen decenas de grupos pequeños y mucho ruido, eps es demasiado pequeño para la densidad de los datos.
  • Si no queda ningún grupo y todos los puntos son ruido, ningún punto alcanza min_samples vecinos dentro de eps, así que no hay núcleos.
  • Si cuentas grupos con len(set(etiquetas)), el -1 del ruido cuenta como un grupo más. Quítalo antes de contar.
  • Si las columnas tienen escalas distintas, la distancia depende casi solo de la columna con mayor rango. Estandariza antes de elegir eps o de calcular linkage.
  • Si np.bincount(grupos) empieza con un 0, recuerda que fcluster numera desde 1 y descarta la primera posición.

Cierre

Explica, con las palabras centro, densidad y ruido, por qué k-means partió cada luna mientras DBSCAN la conservó completa y dejó fuera cinco puntos sueltos.

Lista de comprobación

  • ¿Qué condición debe cumplir un punto para ser núcleo y cuál distingue a un punto borde del ruido?
  • ¿Por qué un eps mayor puede reducir el número de grupos en vez de aumentarlo?
  • ¿Qué representa cada fila de la matriz Z y qué hace un corte en k grupos?
  • ¿Por qué average aisló los dos puntos lejanos y ward no?

Documentación oficial

Cargando progreso…