Aprendizaje no supervisado y sistemas de recomendación
"El aprendizaje no supervisado es descubrir estructura en datos que nadie etiquetó. Es el tipo de aprendizaje más cercano a cómo los humanos entendemos el mundo."
Qué vas a aprender en este capítulo
En el aprendizaje supervisado teníamos etiquetas (). En el no supervisado, no: solo tenemos los datos y queremos encontrar estructura — grupos naturales, patrones, relaciones entre ítems.
Aplicación central: construir el sistema de recomendación de platillos de La Esquina — sin etiquetas, solo con el historial de qué pidieron los clientes.
4.1 K-means clustering
💡 Intuición
Querés segmentar a los clientes de La Esquina para hacer marketing diferenciado. No sabés cuántas categorías hay ni cómo se llaman — solo tenés su historial de consumo.
K-means divide los datos en K grupos (clusters) buscando que los puntos dentro de cada grupo se parezcan entre sí y se diferencien de los otros grupos.
Es como organizar las mesas de un restaurante: agrupás a la gente que llega junta (que probablemente se parece en preferencias) sin saber de antemano cuántos grupos formarán.
📐 Fundamento
Algoritmo K-means:
1. Elegir K centroides aleatorios
2. Repetir hasta convergencia:
a. Asignar cada punto al centroide más cercano
b. Recalcular el centroide de cada cluster (promedio de sus puntos)
Implementación:
import numpy as np
import matplotlib.pyplot as plt
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
# Datos de clientes: [gasto_promedio, frecuencia_mensual]
clientes = np.array([
[5, 2], [8, 3], [4, 1], [50, 8], [60, 10], [55, 9],
[25, 4], [30, 5], [28, 6], [100, 15], [90, 12], [110, 14]
])
# Escalar (K-means es sensible a la escala)
scaler = StandardScaler()
clientes_scaled = scaler.fit_transform(clientes)
# Determinar K óptimo con el método del codo
inercias = []
for k in range(1, 9):
km = KMeans(n_clusters=k, random_state=42, n_init=10)
km.fit(clientes_scaled)
inercias.append(km.inertia_) # suma de distancias^2 a centroide
# Graficar: buscar el "codo" en la curva
plt.plot(range(1, 9), inercias, 'bx-')
plt.xlabel('K')
plt.ylabel('Inercia (WCSS)')
plt.title('Método del codo')
# Entrenar con K=3 (asumiendo que el codo está en 3)
km = KMeans(n_clusters=3, random_state=42, n_init=10)
etiquetas = km.fit_predict(clientes_scaled)
centroides = scaler.inverse_transform(km.cluster_centers_)
print("Centroides (escala original):")
for i, c in enumerate(centroides):
print(f" Cluster {i}: gasto=${c[0]:.0f}, frecuencia={c[1]:.0f}/mes")
# Posible interpretación:
# Cluster 0: "Clientes ocasionales" (gasto bajo, visitan poco)
# Cluster 1: "Clientes frecuentes" (gasto medio, visitan seguido)
# Cluster 2: "Clientes VIP" (gasto alto, muy frecuentes)
Métricas de calidad del clustering:
from sklearn.metrics import silhouette_score, davies_bouldin_score
# Silhouette score: [-1, 1], más alto es mejor
# Mide qué tan bien separados están los clusters
sil = silhouette_score(clientes_scaled, etiquetas)
print(f"Silhouette score: {sil:.3f}") # > 0.5 es bueno
# Davies-Bouldin: [0, ∞), más bajo es mejor
db = davies_bouldin_score(clientes_scaled, etiquetas)
print(f"Davies-Bouldin: {db:.3f}")
Limitaciones de K-means:
- Requiere especificar K de antemano.
- Asume clusters esféricos — falla con formas irregulares.
- Sensible a outliers.
- Sensible a la inicialización (K-means++ mejora esto).
- No funciona bien con alta dimensionalidad.
Alternativas:
- DBSCAN: detecta clusters de forma arbitraria, identifica outliers.
- Hierarchical clustering: construye un árbol de clusters (no requiere K a priori).
- Gaussian Mixture Models: versión "blanda" de K-means con probabilidades.
4.2 Reducción de dimensionalidad — PCA
💡 Intuición
Si tenés 50 features por cliente (qué platillos pidió), visualizarlos o aplicar K-means en 50 dimensiones es difícil. PCA comprime los datos a 2-3 dimensiones manteniendo la mayor variación posible.
Es como hacer una foto de un objeto 3D: la foto es 2D pero captura la esencia del objeto si la tomás desde el ángulo correcto.
📐 Fundamento
PCA — Principal Component Analysis:
from sklearn.decomposition import PCA
import matplotlib.pyplot as plt
# Supongamos que cada cliente tiene un vector de 50 features
# (cuántas veces pidió cada platillo)
X = np.random.rand(200, 50) # 200 clientes, 50 platillos
# Reducir a 2 dimensiones para visualización
pca = PCA(n_components=2)
X_2d = pca.fit_transform(X)
print(f"Varianza explicada por PC1: {pca.explained_variance_ratio_[0]:.1%}")
print(f"Varianza explicada por PC2: {pca.explained_variance_ratio_[1]:.1%}")
print(f"Total: {sum(pca.explained_variance_ratio_):.1%}")
# Visualizar
plt.scatter(X_2d[:, 0], X_2d[:, 1], alpha=0.6)
plt.xlabel('Componente Principal 1')
plt.ylabel('Componente Principal 2')
plt.title('Clientes en espacio reducido')
# Elegir n_components que expliquen el 95% de la varianza
pca_95 = PCA(n_components=0.95) # sklearn elige n automáticamente
X_reducido = pca_95.fit_transform(X)
print(f"Dimensiones para 95% varianza: {X_reducido.shape[1]}")
PCA antes de K-means:
# Pipeline: PCA + K-means (práctica recomendada)
from sklearn.pipeline import Pipeline
pipeline = Pipeline([
('scaler', StandardScaler()),
('pca', PCA(n_components=10)), # reducir a 10 dims
('kmeans', KMeans(n_clusters=4, random_state=42))
])
pipeline.fit(X)
etiquetas = pipeline.predict(X)
4.3 Sistemas de recomendación
💡 Intuición
Netflix recomienda películas que no pediste pero que te van a gustar. Lo hace analizando qué peliculas vieron personas similares a vos.
Este es collaborative filtering: "si Ana y Carlos tienen gustos similares (les gustaron las mismas 5 películas), lo que le gusta a Carlos probablemente le gustará a Ana".
📐 Fundamento
Tipos de sistemas de recomendación:
| Tipo | Basado en | Ventaja | Desventaja |
|---|---|---|---|
| Collaborative filtering | Comportamiento de usuarios similares | Descubre preferencias inesperadas | Cold start (usuarios nuevos) |
| Content-based | Atributos de los ítems | Funciona con usuarios nuevos | Recommends lo obvio |
| Híbrido | Ambos | Mejor de ambos | Más complejo |
Collaborative filtering basado en memoria:
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
# Matriz usuario-platillo: filas=clientes, columnas=platillos
# Valores: cuántas veces lo pidió (0 = nunca)
ratings = np.array([
# P1 P2 P3 P4 P5 P6
[5, 3, 0, 0, 1, 2], # Ana
[4, 0, 4, 1, 0, 0], # Beto
[0, 3, 0, 5, 3, 0], # Carlos
[0, 0, 5, 4, 0, 3], # Diana
[1, 0, 0, 0, 5, 4], # Elena
])
usuarios = ['Ana', 'Beto', 'Carlos', 'Diana', 'Elena']
platillos = ['Pupusa queso', 'Pupusa loroco', 'Tamales', 'Yuca', 'Refresco', 'Postre']
# Similitud coseno entre usuarios
sim_usuarios = cosine_similarity(ratings)
print("Similitudes:")
for i, u in enumerate(usuarios):
similares = [(usuarios[j], sim_usuarios[i,j]) for j in range(len(usuarios)) if i != j]
similares.sort(key=lambda x: -x[1])
print(f" {u}: más similar a {similares[0][0]} ({similares[0][1]:.2f})")
def recomendar(usuario_idx, n_recomendaciones=3):
usuario = ratings[usuario_idx]
similitudes = sim_usuarios[usuario_idx]
# Calcular score predicho para cada platillo no pedido
scores = np.zeros(ratings.shape[1])
for j in range(len(usuarios)):
if j != usuario_idx:
scores += similitudes[j] * ratings[j]
# Excluir platillos ya pedidos
scores[usuario > 0] = -1
# Top N recomendaciones
top_n = np.argsort(scores)[::-1][:n_recomendaciones]
return [(platillos[i], scores[i]) for i in top_n if scores[i] > 0]
print("\nRecomendaciones para Ana:")
for plato, score in recomendar(0):
print(f" {plato}: {score:.2f}")
Matrix Factorization (SVD) — el algoritmo de Netflix:
from sklearn.decomposition import TruncatedSVD
# Factorizar la matriz ratings en factores latentes
n_factors = 3
svd = TruncatedSVD(n_components=n_factors)
user_factors = svd.fit_transform(ratings)
item_factors = svd.components_.T
# Reconstruir ratings predichos
ratings_pred = user_factors @ item_factors.T
print("Ratings predichos para Ana:")
for i, (plato, pred) in enumerate(zip(platillos, ratings_pred[0])):
actual = ratings[0, i]
print(f" {plato}: real={actual}, pred={pred:.1f}")
Métricas de evaluación:
from sklearn.metrics import mean_squared_error
import numpy as np
# Dividir en train/test (ocultar algunos ratings reales)
# Evaluar con RMSE (Root Mean Squared Error)
rmse = np.sqrt(mean_squared_error(ratings_conocidos, ratings_predichos))
print(f"RMSE: {rmse:.3f}") # menor es mejor
# Precision@K: de las K recomendaciones, ¿cuántas fueron relevantes?
# Recall@K: de todos los ítems relevantes, ¿cuántos recomendé?
🛠️ En la práctica
La Esquina: sistema completo de recomendación
# Pipeline: segmentación + recomendación por cluster
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
import pandas as pd
# 1. Preparar datos: historial de pedidos
def construir_perfil_cliente(df_pedidos):
"""Convierte historial de pedidos en vector por cliente."""
return pd.pivot_table(
df_pedidos,
values='cantidad',
index='cliente_id',
columns='platillo_id',
aggfunc='sum',
fill_value=0
)
# 2. Segmentar clientes
def segmentar_clientes(perfiles, n_clusters=4):
scaler = StandardScaler()
perfiles_scaled = scaler.fit_transform(perfiles)
km = KMeans(n_clusters=n_clusters, random_state=42)
return km.fit_predict(perfiles_scaled)
# 3. Recomendar dentro del cluster
def recomendar_por_cluster(cliente_id, cluster_id, perfiles, clusters):
"""Recomienda platillos populares en el cluster que el cliente no ha pedido."""
clientes_cluster = perfiles[clusters == cluster_id]
popularidad = clientes_cluster.mean().sort_values(ascending=False)
ya_pedidos = perfiles.loc[cliente_id]
recomendaciones = popularidad[ya_pedidos == 0].head(3)
return recomendaciones.index.tolist()
# Resultado: "Ana, como a vos te gustan las pupusas y a otros clientes similares
# también les gusta la sopa de pata — ¿querés agregarla a tu próximo pedido?"
4.4 Ejercicios
✏️ Ejercicio 4.1 — K-means paso a paso
Dado el siguiente conjunto de puntos en 2D y K=2, ejecutá manualmente 2 iteraciones de K-means:
Puntos: A(1,1), B(2,1), C(8,8), D(9,8), E(1,2), F(9,9)
Centroides iniciales: C1=(1,1), C2=(9,9)
Para cada iteración: asignar puntos a centroide más cercano, calcular nuevos centroides.
Solución
Iteración 1 — Asignación:
Distancias a C1=(1,1): A=0, B=1, C=9.9, D=11.3, E=1, F=13.4 Distancias a C2=(9,9): A=11.3, B=10.6, C=1.4, D=1, E=10.6, F=0
Cluster 1: A, B, E (cercanos a C1) Cluster 2: C, D, F (cercanos a C2)
Iteración 1 — Nuevos centroides: C1 = promedio(A,B,E) = ((1+2+1)/3, (1+1+2)/3) = (1.33, 1.33) C2 = promedio(C,D,F) = ((8+9+9)/3, (8+8+9)/3) = (8.67, 8.33)
Iteración 2 — Asignación:
Verificar si algún punto cambió de cluster con los nuevos centroides... Distancias de cada punto a C1=(1.33,1.33) y C2=(8.67,8.33) — la asignación no cambia.
Convergencia: Los clusters no cambiaron, el algoritmo convergió. Cluster 1 (clientes "económicos"): A, B, E Cluster 2 (clientes "premium"): C, D, F
✏️ Ejercicio 4.2 — Diseño de sistema de recomendación
Describí cómo diseñarías un sistema de recomendación para una librería en línea:
a. ¿Qué datos usarías como input? b. ¿Content-based, collaborative filtering, o híbrido? Justificá. c. ¿Cómo manejarías el "cold start problem" (usuario nuevo sin historial)? d. ¿Cómo evaluarías si el sistema funciona bien?
Solución
a. Datos: historial de compras (rating implícito: compró/no compró), ratings explícitos (si los hay), atributos de libros (género, autor, editorial, sinopsis), datos demográficos del usuario (opcional).
b. Híbrido: Collaborative filtering para usuarios con historial (aprovecha patrones colectivos), content-based para usuarios nuevos o libros sin ratings (usa atributos del libro para recomendar similares al que está viendo).
c. Cold start: Para usuario nuevo sin historial: (1) recomendar bestsellers del género que seleccionó al registrarse, (2) hacer onboarding preguntando 3-5 libros que le gustaron, (3) usar datos demográficos para encontrar el cluster más probable.
d. Evaluación:
- Offline: RMSE en ratings reales vs predichos (split train/test en tiempo).
- Precision@10: de 10 recomendaciones, ¿cuántas compró?
- A/B testing online: grupo control (sin recomendaciones) vs grupo tratamiento (con recomendaciones) → comparar revenue y click-through rate.
4.5 Para profundizar
- Aggarwal, Recommender Systems: The Textbook — referencia completa.
- fast.ai, Practical Deep Learning, lección de collaborative filtering con embeddings.
- surprise (librería Python) — herramientas para sistemas de recomendación.
- Siguiente: Ética e IA responsable.
Definiciones nuevas: aprendizaje no supervisado, clustering, K-means, centroide, inercia, método del codo, silhouette score, PCA, componente principal, varianza explicada, sistema de recomendación, collaborative filtering, content-based filtering, cold start, matrix factorization, SVD, RMSE, precision@K.