Módulo 4 · Aprendizaje no supervisado
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:
| origen | grupo 0 | grupo 1 |
|---|---|---|
| luna 0 | 76 | 24 |
| luna 1 | 25 | 75 |
| suelto | 2 | 4 |
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.
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_)
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ódigo
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.
| origen | k-means 0 | k-means 1 | DBSCAN -1 | DBSCAN 0 | DBSCAN 1 |
|---|---|---|---|---|---|
| luna 0 | 76 | 24 | 0 | 100 | 0 |
| luna 1 | 25 | 75 | 0 | 0 | 100 |
| suelto | 2 | 4 | 5 | 0 | 1 |
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?
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}")
La salida esperada está oculta.
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.
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.linkagereporta 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.
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))
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?
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}")
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.
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?
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)
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.
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:
- los tamaños de los 3 grupos con enlace
wardy con enlaceaverage - las cuatro últimas alturas de cada árbol y si hay un salto grande entre ellas
- una tabla
pd.crosstabde cada corte contra las tres clases de vino - 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,
epses demasiado grande: coneps=0.30las dos lunas se fundieron. - Si aparecen decenas de grupos pequeños y mucho ruido,
epses 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_samplesvecinos dentro deeps, así que no hay núcleos. - Si cuentas grupos con
len(set(etiquetas)), el-1del 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
epso de calcularlinkage. - Si
np.bincount(grupos)empieza con un 0, recuerda quefclusternumera 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
epsmayor puede reducir el número de grupos en vez de aumentarlo? - ¿Qué representa cada fila de la matriz
Zy qué hace un corte enkgrupos? - ¿Por qué
averageaisló los dos puntos lejanos ywardno?
Documentación oficial
- DBSCAN en la guía de clustering: puntos núcleo, bordes, ruido y papel de
epsymin_samples. - Clustering jerárquico en scikit-learn: enlaces
ward,average,completeysingle. - scipy.cluster.hierarchy.linkage: formato de la matriz
Zy métodos de enlace. - scipy.cluster.hierarchy.fcluster: criterios
maxclustydistancepara cortar el árbol. - Clustering espectral: grafo de afinidad y
SpectralClustering.