Capitulo 09
Aprendizaje No Supervisado: clustering, asociacion y reduccion de dimensionalidad
Cuando no hay etiquetas, el modelo tiene que descubrir la estructura solo
El cambio de chip fundamental: en M6-M8 tenias un target. Aca no. K-Means y eleccion de K, clustering jerarquico y DBSCAN para formas arbitrarias, reglas de asociacion (Apriori), PCA para reduccion y visualizacion, y como elegir el metodo correcto segun el problema.
9.1 El cambio de chip fundamental
En M6, M7 y M8 tuviste un y (target) que le decia al modelo que predecir. En M9 ese y NO existe. No hay “respuesta correcta” predefinida. El modelo tiene que descubrir estructura oculta en los datos: grupos naturales, dimensiones latentes, reglas de co-ocurrencia.
- Clustering (agrupamiento): “¿Hay grupos naturales en mis datos?” — Ej: segmentar clientes, agrupar documentos.
- Reglas de asociacion: “¿Que cosas suelen aparecer juntas?” — Ej: “los que compran panales tambien compran cerveza”.
- Reduccion de dimensionalidad: “¿Puedo representar mis datos en menos variables?” — Ej: comprimir 100 features a 2 para visualizar.
9.2 K-Means: teoria y eleccion de K
K-Means particiona n observaciones en K grupos, minimizando la varianza intra-cluster (inercia). Algoritmo iterativo:
- Inicializar K centroides (al azar o con K-Means++, mas estable)
- Asignar: cada punto va al centroide mas cercano
- Mover: cada centroide se reposiciona en el promedio de sus puntos
- Repetir hasta que los centroides no se muevan o se alcance max_iter
Encuentra un minimo LOCAL, no garantiza el optimo global. Por eso se corre con n_init > 1 y se queda con la mejor inercia.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs, make_moons
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score, davies_bouldin_score
import seaborn as sns
# Dataset con 4 clusters reales
X, y_real = make_blobs(n_samples=400, centers=4, cluster_std=1.0, random_state=42)
X_scaled = StandardScaler().fit_transform(X)
# Evaluar K=2..9 con TRES criterios
K_range = range(2, 10)
inercias, silhouettes, dbi = [], [], []
for k in K_range:
km = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = km.fit_predict(X_scaled)
inercias.append(km.inertia_)
silhouettes.append(silhouette_score(X_scaled, labels))
dbi.append(davies_bouldin_score(X_scaled, labels))
# Visualizacion
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
axes[0].plot(K_range, inercias, 'o-', color='#06B6D4', linewidth=2)
axes[0].set_title('Inercia (metodo del codo)')
axes[0].set_xlabel('K')
axes[1].plot(K_range, silhouettes, 'o-', color='#10B981', linewidth=2)
axes[1].axvline(4, color='red', linestyle='--', alpha=0.5)
axes[1].set_title('Silhouette (mayor = mejor)')
axes[1].set_xlabel('K')
axes[2].plot(K_range, dbi, 'o-', color='#8B5CF6', linewidth=2)
axes[2].axvline(4, color='red', linestyle='--', alpha=0.5)
axes[2].set_title('Davies-Bouldin (menor = mejor)')
axes[2].set_xlabel('K')
for ax in axes:
sns.despine(ax=ax)
ax.grid(alpha=0.3, linestyle='--')
plt.tight_layout()
plt.show()
# K=4 es la eleccion optima segun los 3 criterios
# Y coincide con la verdad del dataset sintetico La regla profesional: usa los TRES criterios (codo, silhouette, Davies-Bouldin) + interpretabilidad de negocio. Si los criterios se contradicen, prioriza silhouette (el mas estable) y validá con el negocio. Si ningun criterio da un pico claro, no hay estructura clusterizable: considera modelos diferentes o mas features.
9.3 Clustering jerarquico y DBSCAN: cuando K-Means no alcanza
| Concepto | Descripcion |
|---|---|
| K-Means | Asume clusters ESFERICOS y de igual tamano. Rapido, escala a millones. Falla en formas raras y outliers. |
| Jerarquico | Construye dendrograma. No requiere K. Visualmente poderoso. O(n³) → no escala > 10k muestras. |
| DBSCAN | Basado en DENSIDAD. Encuentra formas arbitrarias. Detecta outliers automaticamente. Falla con densidades variables. |
| Mean Shift | No requiere K. Encuentra modas de la distribucion. Lento. Comun en tracking de objetos. |
| Spectral | Usa el grafo de similitud. Bueno para datos no convexos. Similar costo a K-Means. |
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import DBSCAN
from sklearn.neighbors import NearestNeighbors
import seaborn as sns
X, _ = make_moons(n_samples=300, noise=0.08, random_state=42)
X_scaled = StandardScaler().fit_transform(X)
# METODO DEL k-DISTANCE PLOT para elegir eps
# Para cada punto, calculamos la distancia a su k-esimo vecino (k = min_samples)
k = 5
nn = NearestNeighbors(n_neighbors=k)
nn.fit(X_scaled)
distances, _ = nn.kneighbors(X_scaled)
kth_distances = distances[:, k-1]
kth_distances_sorted = np.sort(kth_distances)[::-1]
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(range(len(kth_distances_sorted)), kth_distances_sorted, color='#06B6D4', linewidth=1.5)
ax.set_xlabel('Puntos ordenados')
ax.set_ylabel(f'Distancia al {k}-esimo vecino')
ax.set_title(f'k-Distance Plot: buscar el "codo" para elegir eps (k = min_samples = {k})')
ax.grid(alpha=0.3, linestyle='--')
sns.despine(ax=ax)
plt.tight_layout()
plt.show()
# Si el codo esta en ~0.3, ese es tu eps.
# Luego DBSCAN(eps=0.3, min_samples=5)
dbscan = DBSCAN(eps=0.3, min_samples=5)
labels = dbscan.fit_predict(X_scaled)
n_outliers = (labels == -1).sum()
print(f"Clusters encontrados: {len(set(labels)) - (1 if -1 in labels else 0)}")
print(f"Outliers: {n_outliers} ({n_outliers/len(labels):.1%})") - Fija
min_samples = 2 * n_featurescomo punto de partida. - Usa el k-distance plot (con k = min_samples) para elegir eps.
- Si hay MUCHOS outliers, eps es muy chico. Si hay UN cluster gigante, eps es muy grande.
- HDBSCAN es la version “moderna” que no requiere elegir eps. Considerala si DBSCAN falla.
9.4 Reglas de asociacion: support, confidence, lift
Support (Soporte)
M9Fraccion de transacciones que contienen un itemset. Mide que tan FRECUENTE es la combinacion. support(A, B) = N(trans con A y B) / N(total).
Ej: Support({panal, cerveza}) = 0.05 significa que 5% de las transacciones tienen ambos.
Confidence (Confianza)
M9Probabilidad condicional P(B|A). De las transacciones que tienen A, que fraccion tiene B. Mide la FUERZA de la regla. confidence(A→B) = N(trans con A y B) / N(trans con A).
Lift (Elevacion)
M9Ratio entre la confianza observada y la esperada si A y B fueran independientes. lift(A→B) = confidence(A→B) / support(B). Lift = 1 → independientes, > 1 → correlacion positiva, < 1 → negativa.
# pip install mlxtend
import pandas as pd
from mlxtend.preprocessing import TransactionEncoder
from mlxtend.frequent_patterns import apriori, association_rules
# Dataset de transacciones de un kiosko
transacciones = [
['pan', 'manteca', 'cafe'],
['pan', 'manteca'],
['leche', 'manteca', 'galletas'],
['cafe', 'galletas'],
['pan', 'cafe'],
['leche', 'galletas'],
['pan', 'manteca', 'cafe', 'galletas'],
['leche', 'manteca'],
]
# 1) Codificar a formato one-hot
te = TransactionEncoder()
te_ary = te.fit(transacciones).transform(transacciones)
df = pd.DataFrame(te_ary, columns=te.columns_)
# 2) Itemsets frecuentes con Apriori
itemsets = apriori(df, min_support=0.4, use_colnames=True).sort_values('support', ascending=False)
# 3) Reglas de asociacion
reglas = association_rules(itemsets, metric='confidence', min_threshold=0.6)
reglas = reglas.sort_values('lift', ascending=False)
# Las 3 metricas SIEMPRE juntas
print(reglas[['antecedents', 'consequents', 'support', 'confidence', 'lift']].to_string(index=False)) Una regla con lift=2.5 puede ser:
- support=0.05, confidence=0.30 (util: patron real)
- support=0.001, confidence=0.99 (inservible: 1 de cada 1000 clientes)
- support=0.40, confidence=0.85 (sospechoso: es el patron de la mayoria, no descubre nada nuevo)
Las TRES metricas juntas cuentan la historia. Lift sin support es ruido.
9.5 PCA: reduccion de dimensionalidad
PCA encuentra los EJES ortogonales sobre los que los datos varian mas. La primera componente captura la mayor varianza posible, la segunda la siguiente (ortogonal a la primera), etc. Proyectar sobre los primeros k ejes = comprimir la informacion preservando la mayor varianza posible.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.datasets import load_wine
import seaborn as sns
# Dataset: 13 features quimicas de vinos
data = load_wine()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = data.target
class_names = data.target_names
# 1) Escalar (OBLIGATORIO antes de PCA)
X_scaled = StandardScaler().fit_transform(X)
# 2) PCA completa
pca_full = PCA()
X_pca = pca_full.fit_transform(X_scaled)
# 3) Scree plot
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
axes[0].bar(range(1, 14), pca_full.explained_variance_ratio_ * 100, color='#06B6D4')
axes[0].plot(range(1, 14), np.cumsum(pca_full.explained_variance_ratio_) * 100, 'o-', color='red', label='Acumulado')
axes[0].axhline(80, color='gray', linestyle='--', alpha=0.5, label='80% varianza')
axes[0].set_xlabel('Componente')
axes[0].set_ylabel('% Varianza explicada')
axes[0].set_title('Scree Plot')
axes[0].legend()
# 4) Visualizar en 2D con las clases
pca_2d = PCA(n_components=2)
X_pca_2d = pca_2d.fit_transform(X_scaled)
for clase in range(3):
mask = y == clase
axes[1].scatter(X_pca_2d[mask, 0], X_pca_2d[mask, 1], label=class_names[clase], s=50, alpha=0.7)
axes[1].set_xlabel(f'PC1 ({pca_2d.explained_variance_ratio_[0]:.1%})')
axes[1].set_ylabel(f'PC2 ({pca_2d.explained_variance_ratio_[1]:.1%})')
axes[1].set_title(f'Vinos en 2D (PC1+PC2 = {pca_2d.explained_variance_ratio_.sum():.1%})')
axes[1].legend()
for ax in axes:
sns.despine(ax=ax)
ax.grid(alpha=0.3, linestyle='--')
plt.tight_layout()
plt.show() - Usalo para:
- Visualizar datos de 10+ dimensiones en 2D/3D.
- Reducir multicolinealidad antes de un modelo lineal.
- Acelerar entrenamiento de modelos con muchas features.
- Eliminacion de ruido (las ultimas componentes suelen ser ruido).
- NO lo uses para:
- Explicar que features importan. Para eso, feature importance o SHAP.
- Antes de Random Forest / XGBoost (estos no se benefician del escalado).
- Datos categoricos codificados como 0/1 (no tiene sentido).
9.6 Panorama: cuando usar cada metodo
- ¿Quieres AGRUPAR observaciones?
- Conoces K, datos esfericos → K-Means
- Forma arbitraria + outliers → DBSCAN
- Dataset pequeno (< 10k), quieres jerarquia → Jerarquico + dendrograma
- ¿Quieres encontrar REGLAS (que va con que)?
- Apriori / FP-Growth con support + confidence + lift
- ¿Quieres COMPRIMIR o VISUALIZAR?
- Lineal, pocas PCs → PCA
- No lineal, solo visualizar → t-SNE o UMAP
9.7 Errores comunes
- Genera 3 datasets sinteticos: uno con clusters esfericos, uno con lunas, uno con circulos concentricos. Aplica K-Means, Jerarquico y DBSCAN a cada uno. Compara con silhouette, Davies-Bouldin y Calinski-Harabasz. ¿Cual metodo gana en cada caso?
- Carga el dataset de tu eleccion (sugiero
load_wine()o un dataset de retail con transacciones). Aplica K-Means con K=2..8 y reporta el K optimo con todos los criterios. - Para el K ganador, interpreta los clusters: calcula las medias de cada feature por cluster. Nombralos con sentido de negocio. ¿Que perfil tiene cada uno?
- Aplica PCA a 2D y visualiza los clusters en ese espacio. ¿Se separan las clases? ¿Cuanta varianza capturaste con 2 componentes?
- Bonus: si el dataset es transaccional (groceries de Kaggle), aplica Apriori con min_support=0.01 y reporta las 10 reglas con mayor lift (con sus 3 metricas).