Módulo 4 · Aprendizaje no supervisado

Lección 1 de 4

K-means y PCA

En esta páginaEl problema

El problema

Un laboratorio midió 13 variables químicas en 178 vinos: alcohol, magnesio, intensidad de color, prolina y otras. Las mediciones no dicen de qué cultivar viene cada vino. Quieres saber si los vinos forman grupos y si bastan pocos números para describir cada vino.

Estas cuatro columnas muestran lo distintas que son las escalas:

variablemínimomáximodesviación estándar
alcohol11.0314.830.81
magnesium70.00162.0014.28
hue0.481.710.23
proline278.001680.00314.91

Los datos son load_wine de scikit-learn. El conjunto trae además el cultivar de cada vino (0, 1 o 2). Aquí el cultivar no entra a ningún modelo. Solo se usa después, para comparar los grupos encontrados con un dato que el algoritmo no vio.

Centroides e inercia

K-means recibe un número de grupos k y repite dos pasos. Primero asigna cada punto al centroide más cercano con distancia euclidiana. Después mueve cada centroide al promedio de los puntos que recibió. Se detiene cuando las asignaciones dejan de cambiar.

La inercia es la suma de las distancias al cuadrado entre cada punto y su centroide. K-means busca los centroides con la menor inercia.

Explorador

Asignar y mover, paso a paso

Avanza un paso a la vez: cada punto toma el color del centroide más cercano y después cada centroide se mueve al promedio de sus puntos.

Qué probar

Observa la inercia después de cada paso.

123

Arrastra un centroide o haz clic en un espacio vacío para agregar un punto.

Parámetros
k = 3. Los centroides están en su posición inicial. Siguiente paso: asignar cada punto al centroide más cercano.

Pregunta. Con estos ocho puntos y k=2, escribe las coordenadas de los dos centroides y la inercia total.

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

puntos = np.array(
    [[1, 1], [1, 3], [3, 1], [3, 3], [8, 8], [8, 10], [10, 8], [10, 10]],
    dtype=float,
)
modelo = KMeans(n_clusters=2, n_init=10, random_state=0).fit(puntos)

print(modelo.labels_)
print(modelo.cluster_centers_)
print(modelo.inertia_)

centro_de_cada_punto = modelo.cluster_centers_[modelo.labels_]
print(((puntos - centro_de_cada_punto) ** 2).sum())
Salida esperada

La salida esperada está oculta.

Escala de las variables

La distancia euclidiana suma diferencias al cuadrado de todas las variables. En los vinos, esas diferencias tienen unidades y tamaños muy distintos. StandardScaler resta la media de cada columna y divide entre su desviación estándar, así que cada variable queda en unidades de desviación estándar.

Pregunta. Ajusta K-means con k=3 a la tabla original y a la tabla escalada. ¿Qué variable tendrá centroides más separados en la tabla original? ¿En cuál de los dos casos coincidirán mejor los grupos con los cultivares?

Python 3 · ejemplo reproducible
import pandas as pd
from sklearn.datasets import load_wine
from sklearn.preprocessing import StandardScaler

X, cultivar = load_wine(return_X_y=True, as_frame=True)
X_esc = StandardScaler().fit_transform(X)

km_crudo = KMeans(n_clusters=3, n_init=10, random_state=0).fit(X)
km_esc = KMeans(n_clusters=3, n_init=10, random_state=0).fit(X_esc)

centros_crudos = pd.DataFrame(km_crudo.cluster_centers_, columns=X.columns)
print(centros_crudos[["alcohol", "magnesium", "hue", "proline"]].round(1))
print()
print(pd.crosstab(km_crudo.labels_, cultivar, rownames=["grupo"], colnames=["cultivar"]))
print()
print(pd.crosstab(km_esc.labels_, cultivar, rownames=["grupo"], colnames=["cultivar"]))
Salida esperada

La salida esperada está oculta.

Número de grupos

K-means empieza con centroides elegidos al azar. La inicialización por defecto, k-means++, los elige alejados entre sí. Aun así, dos inicios distintos pueden terminar en soluciones distintas. random_state fija el azar. n_init repite el ajuste con varios inicios y conserva el de menor inercia.

Pregunta. Con seis semillas distintas, ¿qué variará más entre semillas: la inercia con k=3 o con k=5? ¿Qué efecto tendrá n_init=10?

Ver resultado

Cada fila es una semilla de random_state: la primera columna es la semilla y las otras tres dan la inercia final de K-means sobre X_esc con un k y un n_init. Por ejemplo, la fila 5 dice que, con esa semilla, k=5 y un solo inicio termina en 1168.5, y con diez inicios en 1110.3.

semillak=3, n_init=1k=5, n_init=1k=5, n_init=10
01277.91121.31110.4
11278.81124.71124.7
21277.91105.71105.7
31277.91112.71104.1
41280.01120.11109.1
51278.81168.51110.3
Lectura. Con k=3, las seis semillas terminan entre 1277.9 y 1280.0. Con k=5 y un solo inicio, la inercia va de 1105.7 a 1168.5: cada semilla cae en un mínimo local distinto. n_init=10 reduce ese rango, pero no lo elimina. Fijar random_state hace que el resultado se repita. No garantiza que sea el mejor posible.

El coeficiente de silueta compara, para cada punto, a, la distancia media a los puntos de su grupo, con b, la distancia media a los puntos del grupo vecino más cercano. Su valor es (b - a) / max(a, b), entre −1 y 1. silhouette_score promedia ese valor en todos los puntos. Un valor cercano a 1 indica grupos compactos y separados; cercano a 0, puntos en la frontera entre dos grupos.

Pregunta. Para k de 2 a 7, ¿la inercia puede subir en algún paso? ¿Qué k tendrá la silueta más alta?

Python 3 · ejemplo reproducible
from sklearn.metrics import silhouette_score

print("k  inercia  silueta")
for k in range(2, 8):
    km = KMeans(n_clusters=k, n_init=10, random_state=0).fit(X_esc)
    silueta = silhouette_score(X_esc, km.labels_)
    print(f"{k}  {km.inertia_:>7.1f}  {silueta:.3f}")
Salida esperada

La salida esperada está oculta.

El método del codo también usa la inercia, pero no busca la menor. Mira la curva de inercia contra k y busca el punto donde se dobla: antes de ese punto, cada grupo nuevo reduce mucho la inercia; después, poco. La tabla anterior tiene la inercia de k=2 a k=7 sobre X_esc. Para la curva de 1 a 8 faltan dos valores.

Pregunta. ¿Cuánto vale la inercia con k=1, cuando el único centroide es el promedio de cada columna escalada? ¿La bajada de k=7 a k=8 será menor que la de k=6 a k=7?

Ver resultado

Cada fila es un valor de k que faltaba en la curva, y la columna da la inercia de K-means sobre X_esc con ese k. Por ejemplo, la fila con 8 y 944.6 dice que, con ocho centroides, la suma de distancias al cuadrado de cada vino a su centroide vale 944.6. La frase de abajo cuenta los valores de X_esc.

kinercia
12314.0
8944.6

X_esc tiene 178 filas × 13 columnas = 2314 valores escalados.

Lectura. Con k=1, el centroide es la media de cada columna escalada, que vale 0. La inercia es la suma de los cuadrados de todos los valores escalados. Cada columna tiene varianza 1 y aporta 178, así que las 13 columnas dan 2314. Con las dos tablas, la inercia baja 655.0 de k=1 a k=2 y 381.1 de k=2 a k=3. De k=3 a k=4 baja solo 97.2, y ningún paso posterior baja más de 100: la curva se dobla en k=3. La bajada de k=7 a k=8 no es menor: vale 51.4, y la de k=6 a k=7 valía 48.5.

El codo no siempre es tan claro. No tiene una definición única: suele leerse en una gráfica, y cambiar la escala del eje vertical cambia dónde parece doblarse. Cuanto más se traslapan los grupos, más gradual es la curva y menos marcado el ángulo. Las bajadas tampoco se reducen siempre, como muestra el paso de k=7 a k=8. La inercia solo suma distancias al cuadrado de cada punto a su centroide, es decir, mide cuán compactos son los grupos. La silueta compara la compacidad (a) con la separación respecto al grupo vecino (b), y da un máximo numérico en lugar de una lectura visual. La silueta no se define con k=1, porque no hay grupo vecino; la curva del codo sí incluye ese punto. En los vinos, los dos métodos señalan k=3.

Componentes principales

PCA busca direcciones en el espacio de las variables. La primera componente es la dirección en la que los datos varían más. La segunda es perpendicular a la primera y recoge la mayor varianza restante, y así sucesivamente. explained_variance_ratio_ da la fracción de la varianza total que recoge cada componente.

Explorador

Dirección de máxima varianza

Gira la dirección candidata y observa cuánta varianza conservan las proyecciones.

Qué probar

Arrastra los puntos para cambiar la nube.

Varianza según la dirección

Dirección a 150°. Las proyecciones conservan 0.43 de 2.68 de varianza (16 %); se pierden 2.25. La primera componente está a 68° de aquí.

Pregunta. Ajusta PCA a la tabla original y a la escalada. ¿Qué fracción de la varianza recogerá la primera componente en cada caso? ¿Cuántas componentes hacen falta en la tabla escalada para pasar del 80 %?

Python 3 · ejemplo reproducible
from sklearn.decomposition import PCA

pca_crudo = PCA().fit(X)
pca = PCA().fit(X_esc)

print("sin escalar:", pca_crudo.explained_variance_ratio_[:3].round(3))
print("peso de proline en PC1 sin escalar:", abs(pca_crudo.components_[0, -1]).round(3))
print("escalada:   ", pca.explained_variance_ratio_[:3].round(3))
print("acumulada:  ", pca.explained_variance_ratio_.cumsum().round(3))
Salida esperada

La salida esperada está oculta.

Cada componente es una combinación lineal de las variables escaladas. components_ guarda sus pesos. PCA resta primero la media de cada variable, guardada en mean_; aquí esa media ya es 0 porque los datos están escalados. Por eso la coordenada de un vino en la primera componente es la suma de sus 13 valores escalados, cada uno multiplicado por su peso.

Pregunta. ¿Qué variables tendrán los pesos más grandes en valor absoluto en la primera componente? Comprueba después que el producto punto reproduce la coordenada que calcula transform.

Python 3 · ejemplo reproducible
pesos = pd.DataFrame(pca.components_[:2].T, index=X.columns, columns=["PC1", "PC2"])
print(pesos.round(2))

coordenadas = pca.transform(X_esc)
print(round(X_esc[0] @ pca.components_[0], 3))
print(round(coordenadas[0, 0], 3))
Salida esperada

La salida esperada está oculta.

Compresión y agrupamiento

PCA comprime: convierte cada vino de 13 números a menos números y no asigna ningún grupo. K-means agrupa: asigna una etiqueta a cada vino y no reduce columnas. Un pipeline puede encadenar los dos: escalar, comprimir a dos componentes y agrupar en ese espacio.

Pregunta. Las dos componentes conservan el 55 % de la varianza. ¿Cuántos de los 178 vinos cambiarán de grupo respecto a K-means con las 13 variables escaladas? ¿La silueta calculada con dos componentes será comparable con la de 13 variables?

Python 3 · ejemplo reproducible
from sklearn.pipeline import make_pipeline

comprimir_y_agrupar = make_pipeline(
    StandardScaler(),
    PCA(n_components=2),
    KMeans(n_clusters=3, n_init=10, random_state=0),
)
grupos_pca = comprimir_y_agrupar.fit_predict(X)
Z = comprimir_y_agrupar[:-1].transform(X)

print(Z.shape)
print(Z[:3].round(2))
print(pd.crosstab(km_esc.labels_, grupos_pca, rownames=["13 variables"], colnames=["2 componentes"]))
print("silueta en 13 variables, grupos de 13 variables:", round(silhouette_score(X_esc, km_esc.labels_), 3))
print("silueta en 13 variables, grupos de 2 componentes:", round(silhouette_score(X_esc, grupos_pca), 3))
print("silueta en 2 componentes, grupos de 2 componentes:", round(silhouette_score(Z, grupos_pca), 3))
Salida esperada

La salida esperada está oculta.

Práctica

Traza guiada

Estos seis valores tienen una sola variable. Los centroides iniciales son 3 y 4.

Python 3 · ejemplo reproducible
valores = np.array([[1.0], [2.0], [3.0], [8.0], [9.0], [13.0]])
inicio = np.array([[3.0], [4.0]])
traza = KMeans(n_clusters=2, init=inicio, n_init=1)

Antes de ejecutar traza.fit(valores), escribe a mano: los grupos de la primera asignación, los centroides después de la primera actualización, los grupos de la segunda asignación, los centroides finales y la inercia. Comprueba con labels_, cluster_centers_ e inertia_.

Vinos sin escalar

Repite la tabla de inercia y silueta para k de 2 a 7 con X en lugar de X_esc. Antes de procesar, predice si la silueta será mayor o menor que 0.285 y qué k tendrá la más alta. Después explica, con la escala de proline, si esa silueta indica grupos más útiles. Usa la tabla cruzada con cultivar como evidencia.

Dígitos comprimidos

Con load_digits(return_X_y=True) tienes 1797 imágenes de 8 × 8 píxeles como vectores de 64 valores. Escribe:

  1. cuántas componentes necesita PCA para conservar al menos el 80 % de la varianza
  2. los grupos de KMeans(n_clusters=10, n_init=10, random_state=0) sobre los 64 píxeles
  3. los grupos del mismo K-means dentro de un pipeline con PCA(n_components=0.8) antes
  4. la tabla cruzada entre ambas particiones y cuántas imágenes cambian de grupo
  5. la silueta de las dos particiones, calculada en ambos casos sobre los 64 píxeles

Reto opcional

Programa K-means con NumPy: asigna cada fila de X_esc al centroide más cercano, recalcula cada centroide como promedio y repite hasta que las asignaciones no cambien. Empieza con las filas 0, 60 y 130 como centroides. Compara tus centroides y tu inercia con KMeans(n_clusters=3, init=X_esc[[0, 60, 130]], n_init=1).

Errores que revisar

  • Si una variable con valores grandes decide sola los grupos, escala antes de K-means o PCA. Sin escalar, la prolina domina los centroides y el 99.8 % de la varianza.
  • Si eliges k con la menor inercia, gana el k más grande. Compara la silueta y la estabilidad entre semillas.
  • Si dos ejecuciones dan grupos distintos, revisa random_state y n_init. Si el resultado cambia con la semilla aun con n_init=10, la estructura con ese k es débil.
  • Si comparas etiquetas entre dos ajustes, usa una tabla cruzada. El grupo 0 de un ajuste puede ser el grupo 2 de otro.
  • Si una silueta sube después de PCA, revisa en qué espacio se calculó. Compara siluetas solo sobre la misma matriz.

Cierre

Con los resultados de los vinos, explica por qué PCA a dos componentes no es un agrupamiento y por qué K-means sobre esas dos componentes casi reproduce los grupos de las 13 variables escaladas.

Lista de comprobación

  • ¿Qué calcula la inercia y por qué baja al aumentar k?
  • ¿Por qué una variable con desviación estándar 314.91 domina la distancia euclidiana?
  • ¿Qué comparan a y b en el coeficiente de silueta?
  • ¿Cómo se obtiene la coordenada de un vino en la primera componente a partir de sus variables?
  • ¿Qué produce PCA y qué produce K-means?

Documentación oficial

Cargando progreso…