Agrupar sin etiquetas: clustering
Los 11 bloques de código de la teoría, en el mismo orden. Cópialos y córrelos en tu Python o en Colab; en la ficha Teoría está la explicación de cada uno.
Aprender sin etiquetas
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
# 300 puntos sintéticos alrededor de 4 centros
X, y_verdad = make_blobs(n_samples=300, centers=4, cluster_std=0.9,
random_state=7)
print("Forma de X:", X.shape)
print("Primeros puntos:", X[:3].round(1).tolist())
plt.scatter(X[:, 0], X[:, 1], s=16)
plt.xlabel("Característica 1")
plt.ylabel("Característica 2")
plt.title("300 puntos sin etiqueta (sintéticos)")
plt.show()K-means: asignar y mover
def kmeans(X, centroides, pasos=100):
"""K-means desde unos centroides iniciales.
Devuelve el grupo de cada punto, los centroides finales
y la lista de centroides de cada vuelta.
"""
k = len(centroides)
historial = [centroides]
for _ in range(pasos):
# 1) Asignar: distancia de cada punto a cada centroide, (n, k)
d = np.linalg.norm(X[:, None, :] - centroides[None, :, :], axis=2)
grupo = d.argmin(axis=1)
# 2) Mover: cada centroide va al promedio de su grupo
# (si un grupo quedó vacío, su centroide no se mueve)
nuevos = np.array([X[grupo == j].mean(axis=0) if np.any(grupo == j)
else centroides[j] for j in range(k)])
if np.allclose(nuevos, centroides): # ya no se mueven: listo
break
centroides = nuevos
historial.append(centroides)
return grupo, centroides, historial
def inercia(X, grupo, centroides):
"""Suma de las distancias al cuadrado de cada punto a su centroide."""
return np.sum((X - centroides[grupo]) ** 2)
rng = np.random.default_rng(6)
# Cuatro puntos de los datos, al azar, como centroides iniciales
iniciales = X[rng.choice(len(X), size=4, replace=False)]
grupo, centroides, historial = kmeans(X, iniciales)
print("Vueltas hasta que los centroides se quedaron quietos:",
len(historial) - 1)
print("Centroides finales:")
print(centroides.round(2))
print(f"Inercia final: {inercia(X, grupo, centroides):.1f}")
print("Puntos por grupo:", np.bincount(grupo))FORMAS = ["o", "s", "^", "D"] # círculo, cuadrado, triángulo, rombo
COLORES = ["C0", "C2", "C3", "C0"]
def dibujar_grupos(eje, X, grupo, s=14):
"""Un color y una forma por grupo; el ruido (grupo -1) va como x."""
for g in np.unique(grupo):
parte = X[grupo == g]
if g == -1:
eje.scatter(parte[:, 0], parte[:, 1], s=s, marker="x",
color="C2", linewidths=1)
else:
eje.scatter(parte[:, 0], parte[:, 1], s=s, marker=FORMAS[g % 4],
color=COLORES[g % 4], edgecolors="C0",
linewidths=0.6)
fig, ejes = plt.subplots(2, 2, figsize=(4.8, 4.8),
sharex=True, sharey=True)
for vuelta, eje in enumerate(ejes.flat):
c = historial[vuelta]
# Asignar cada punto al centroide de esta vuelta
g = np.linalg.norm(X[:, None, :] - c[None, :, :], axis=2).argmin(axis=1)
dibujar_grupos(eje, X, g, s=12)
final = vuelta == len(historial) - 1
eje.scatter(c[:, 0], c[:, 1], marker="X", s=150, linewidths=1.5,
color="C1" if final else "C0", edgecolors="white")
nombre = "Inicio" if vuelta == 0 else f"Vuelta {vuelta}"
eje.set_title(f"{nombre}\ninercia {inercia(X, g, c):.0f}")
plt.show()Mínimos locales y k-means++
for semilla in range(6):
rng = np.random.default_rng(semilla)
inicio = X[rng.choice(len(X), size=4, replace=False)]
g, c, h = kmeans(X, inicio)
print(f"Semilla {semilla}: {len(h) - 1:2d} vueltas "
f"inercia {inercia(X, g, c):7.1f} "
f"tamaños {np.bincount(g, minlength=4)}")from sklearn.cluster import KMeans
modelo = KMeans(n_clusters=4, n_init=10, random_state=0)
grupos_km = modelo.fit_predict(X)
print(f"Inercia: {modelo.inertia_:.1f}")
print("Centroides:")
print(modelo.cluster_centers_.round(2))
print("Grupo de los 10 primeros puntos:", grupos_km[:10])¿Cuántos grupos? El codo y la silueta
from sklearn.metrics import silhouette_score
ks = range(2, 9)
inercias, siluetas = [], []
for k in ks:
m = KMeans(n_clusters=k, n_init=10, random_state=0).fit(X)
inercias.append(m.inertia_)
siluetas.append(silhouette_score(X, m.labels_))
print(f"k = {k}: inercia {m.inertia_:7.1f} "
f"silueta {siluetas[-1]:.3f}")
mejor_k = ks[np.argmax(siluetas)]
fig, (arriba, abajo) = plt.subplots(2, 1, figsize=(4.8, 4.8), sharex=True)
arriba.plot(ks, inercias, marker="o", color="C0")
arriba.set_ylabel("Inercia")
arriba.set_title("El codo y la silueta según k")
abajo.plot(ks, siluetas, marker="o", color="C2")
abajo.scatter([mejor_k], [max(siluetas)], s=160, color="C1", zorder=3,
label=f"La más alta: k = {mejor_k}")
abajo.set_ylabel("Silueta")
abajo.set_xlabel("Cantidad de grupos k")
abajo.legend()
plt.show()DBSCAN: grupos por densidad
from sklearn.datasets import make_moons
from sklearn.cluster import DBSCAN
X_lunas, y_lunas = make_moons(n_samples=300, noise=0.08, random_state=0)
db = DBSCAN(eps=0.2, min_samples=5).fit(X_lunas)
grupos_db = db.labels_
nucleo = np.zeros(len(X_lunas), dtype=bool)
nucleo[db.core_sample_indices_] = True
ruido = grupos_db == -1
borde = ~nucleo & ~ruido
print("Grupos encontrados:", len(set(grupos_db) - {-1}))
print(f"Núcleo: {nucleo.sum()} borde: {borde.sum()} "
f"ruido: {ruido.sum()}")Clustering jerárquico y el dendrograma
from scipy.cluster.hierarchy import linkage, dendrogram
# Cada fila de Z es una unión: grupo, grupo, distancia, tamaño
Z = linkage(X, method="ward")
print("Forma de Z:", Z.shape)
print("Altura de las 4 últimas uniones:", Z[-4:, 2].round(1))
# Un corte entre la 4.ª y la 3.ª unión desde arriba deja 4 grupos
corte = (Z[-4, 2] + Z[-3, 2]) / 2
dendrogram(Z, truncate_mode="lastp", p=12, leaf_font_size=11,
link_color_func=lambda k: "C0")
plt.axhline(corte, color="C1", linestyle="--", label="Corte: 4 grupos")
plt.ylim(0, 210) # espacio arriba para la leyenda
plt.ylabel("Distancia de unión")
plt.title("Dendrograma (enlace ward)")
plt.legend(loc="upper left")
plt.show()from sklearn.cluster import AgglomerativeClustering
from sklearn.metrics import adjusted_rand_score
grupos_ag = AgglomerativeClustering(n_clusters=4,
linkage="ward").fit_predict(X)
print("Puntos por grupo:", np.bincount(grupos_ag))
print("Acuerdo con K-means:", adjusted_rand_score(grupos_km, grupos_ag))
print("Acuerdo con la verdad:", adjusted_rand_score(y_verdad, grupos_ag))Los cuatro métodos sobre las lunas
from sklearn.cluster import SpectralClustering
metodos = {
"K-means": KMeans(n_clusters=2, n_init=10, random_state=0),
"DBSCAN": DBSCAN(eps=0.2, min_samples=5),
"Jerárquico simple": AgglomerativeClustering(n_clusters=2,
linkage="single"),
"Espectral": SpectralClustering(n_clusters=2,
affinity="nearest_neighbors",
random_state=0),
}
fig, ejes = plt.subplots(2, 2, figsize=(4.8, 4.8),
sharex=True, sharey=True)
for eje, (nombre, metodo) in zip(ejes.flat, metodos.items()):
grupos = metodo.fit_predict(X_lunas)
acuerdo = adjusted_rand_score(y_lunas, grupos)
print(f"{nombre:18s} acuerdo con las lunas: {acuerdo:.2f}")
dibujar_grupos(eje, X_lunas, grupos, s=12)
eje.set_title(nombre)
plt.show()Comprimir una foto con K-means
from sklearn.datasets import load_sample_image
foto = load_sample_image("china.jpg")
# Una fila por píxel, con sus tres canales entre 0 y 1
pixeles = foto.reshape(-1, 3) / 255
print("Píxeles:", pixeles.shape)
print("Colores distintos en la foto:", len(np.unique(pixeles, axis=0)))
rng = np.random.default_rng(0)
muestra = pixeles[rng.choice(len(pixeles), size=2000, replace=False)]
fig, ejes = plt.subplots(2, 2, figsize=(4.8, 4.8))
ejes[0, 0].imshow(foto)
ejes[0, 0].set_title("Original")
for eje, k in zip(ejes.flat[1:], [2, 8, 32]):
colores = KMeans(n_clusters=k, n_init=4, random_state=0).fit(muestra)
# Cada píxel se reemplaza por el centroide (el color) de su grupo
grupo = colores.predict(pixeles)
reducida = colores.cluster_centers_[grupo].reshape(foto.shape)
eje.imshow(reducida)
eje.set_title(f"{k} colores")
for eje in ejes.flat:
eje.axis("off")
plt.show()