D
Practica aplicada M9 avanzado 32 min

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.

Tres familias, tres preguntas
  • 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

El algoritmo de Lloyd

K-Means particiona n observaciones en K grupos, minimizando la varianza intra-cluster (inercia). Algoritmo iterativo:

  1. Inicializar K centroides (al azar o con K-Means++, mas estable)
  2. Asignar: cada punto va al centroide mas cercano
  3. Mover: cada centroide se reposiciona en el promedio de sus puntos
  4. 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.

python K-Means con eleccion rigurosa de K
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
3 criterios juntos, no uno solo

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

ConceptoDescripcion
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.
python DBSCAN: eps y min_samples
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%})")
Regla para DBSCAN
  1. Fija min_samples = 2 * n_features como punto de partida.
  2. Usa el k-distance plot (con k = min_samples) para elegir eps.
  3. Si hay MUCHOS outliers, eps es muy chico. Si hay UN cluster gigante, eps es muy grande.
  4. HDBSCAN es la version “moderna” que no requiere elegir eps. Considerala si DBSCAN falla.

9.4 Reglas de asociacion: support, confidence, lift

Support (Soporte)

M9

Fraccion 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.

#no-supervisado #asociacion #core

Confidence (Confianza)

M9

Probabilidad 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).

#no-supervisado #asociacion #core

Lift (Elevacion)

M9

Ratio 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.

#no-supervisado #asociacion #core
python Reglas de asociacion con mlxtend
# 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))
Por que lift solo no alcanza

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: direcciones de maxima varianza

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.

python PCA: cuando y cuando NO
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()
PCA: cuando SI, cuando NO
  • 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

Arbol de decision para elegir tecnica
  1. ¿Quieres AGRUPAR observaciones?
    • Conoces K, datos esfericos → K-Means
    • Forma arbitraria + outliers → DBSCAN
    • Dataset pequeno (< 10k), quieres jerarquia → Jerarquico + dendrograma
  2. ¿Quieres encontrar REGLAS (que va con que)?
    • Apriori / FP-Growth con support + confidence + lift
  3. ¿Quieres COMPRIMIR o VISUALIZAR?
    • Lineal, pocas PCs → PCA
    • No lineal, solo visualizar → t-SNE o UMAP

9.7 Errores comunes

Olvidar escalar antes de K-Means / PCA / DBSCAN
✗ KMeans().fit(df) # 'monto' (0-500k) aplasta 'edad' (0-100)
✓ KMeans().fit(StandardScaler().fit_transform(df)) # SIEMPRE primero escalar
Elegir K a ojo sin criterio objetivo
✗ '3 clusters suena razonable' sin metricas
✓ Silhouette + Davies-Bouldin + codo + interpretacion de negocio, todos juntos
K-Means en datos no esfericos (lunas, anillos)
✗ KMeans(n_clusters=2).fit(make_moons()) # corta por la mitad, no por la forma
✓ DBSCAN o Spectral Clustering para formas arbitrarias
Reportar lift sin support ni confidence
✗ 'La regla A→B tiene lift 2.5' sin contexto
✓ 'A→B: support=0.05, confidence=0.30, lift=2.5' (los 3 juntos SIEMPRE)
Validar clusters solo con metricas internas
✗ Silhouette=0.7, listo. Los clusters pueden no significar nada en el dominio.
✓ Presentar al negocio y validar que cada cluster tenga un NOMBRE y un perfil accionable.
Libro: "An Introduction to Statistical Learning" (ISLR) - Cap 12 (clustering), 10.3 (PCA). Gratuito online.
Libro: "Hands-On Machine Learning" (3ra ed) - Cap 8 (PCA, t-SNE), 9 (clustering, DBSCAN, Apriori).
Paper: Ester et al: "A Density-Based Algorithm for Discovering Clusters in Large Spatial Databases with Noise" (KDD-96, DBSCAN)
Paper: Agrawal, Srikant: "Fast Algorithms for Mining Association Rules" (VLDB 1994, Apriori)
Video: StatQuest: "K-means", "PCA in 5 minutes", "DBSCAN" (YouTube)
Articulo: "A Comprehensive Guide to Association Rules" (Towards Data Science)
Herramienta: mlxtend: Apriori, FP-Growth y association rules en Python
Herramienta: HDBSCAN: alternativa moderna a DBSCAN que no requiere elegir eps
Mini-proyecto 9: clustering + asociacion + PCA end-to-end avanzado
  1. 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?
  2. 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.
  3. 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?
  4. Aplica PCA a 2D y visualiza los clusters en ese espacio. ¿Se separan las clases? ¿Cuanta varianza capturaste con 2 componentes?
  5. 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).