Aprendizaje supervisado

"El aprendizaje automático es estadística con mejor marketing." — breve y brutalmente honesto.

Qué vas a aprender en este capítulo

El aprendizaje supervisado es el tipo de IA más usado en la industria: se le da al modelo datos con respuestas conocidas (ejemplos etiquetados), y aprende a predecir la respuesta para ejemplos nuevos. Desde filtros de spam hasta diagnósticos médicos.

El proyecto: predecir si un cliente va a pedir postre basándose en su historial — así La Esquina puede ofrecer la sugerencia en el momento correcto.


2.1 El paradigma supervisado

💡 Intuición

Un niño aprende qué es un "perro" porque los adultos le muestran fotos de perros y no-perros, y le dicen cuál es cuál. Después de ver suficientes ejemplos, el niño puede clasificar fotos nuevas.

El aprendizaje supervisado funciona igual: ejemplos con etiquetas → modelo aprende patrones → predice etiquetas para ejemplos nuevos.

📐 Fundamento

Definición formal:

Dado un conjunto de entrenamiento {(xi,yi)}i=1n{(\mathbf{x}i, y_i)}{i=1}^{n} donde:

  • xiRd\mathbf{x}_i \in \mathbb{R}^d es el vector de características (features)
  • yiy_i es la etiqueta (label)

El objetivo es aprender una función f:RdYf: \mathbb{R}^d \to \mathcal{Y} tal que f(x)yf(\mathbf{x}) \approx y para ejemplos nuevos.

Tipos de problemas:

Tipo yy Ejemplo
Regresión Número continuo Precio de un pedido
Clasificación binaria {0, 1} ¿El cliente deja propina?
Clasificación multiclase {clase_1, ..., clase_k} Tipo de platillo más probable

El pipeline estándar:

import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

# 1. Cargar datos
df = pd.read_csv('historial_pedidos.csv')
X = df[['hora_dia', 'num_personas', 'gasto_previo', 'es_fin_semana']]
y = df['pidio_postre']  # 0 o 1

# 2. Dividir en entrenamiento y prueba
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 3. Escalar features (muchos modelos lo necesitan)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)  # aprende media/std del train
X_test_scaled = scaler.transform(X_test)        # aplica la misma escala

# 4. Entrenar modelo
# 5. Evaluar

Por qué dividir train/test:

Si evaluamos el modelo en los mismos datos con que entrenó, no sabemos si generalizó o simplemente memorizó. El test set simula datos nunca vistos — cómo se comportará en producción.


2.2 K-Nearest Neighbors (KNN)

💡 Intuición

KNN es el algoritmo más intuitivo: para clasificar un nuevo ejemplo, buscás los K ejemplos más similares en los datos de entrenamiento y votás por la mayoría.

"Dime con quién andas y te diré quién eres." Si los 5 clientes más similares a Ana pidieron postre, probablemente Ana también lo pida.

📐 Fundamento

Algoritmo KNN:

from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import accuracy_score, classification_report

# Entrenar (KNN no "aprende" — simplemente guarda los datos)
knn = KNeighborsClassifier(n_neighbors=5, metric='euclidean')
knn.fit(X_train_scaled, y_train)

# Predecir
y_pred = knn.predict(X_test_scaled)

# Evaluar
print(f"Accuracy: {accuracy_score(y_test, y_pred):.2%}")
print(classification_report(y_test, y_pred, target_names=['No postre', 'Sí postre']))

Distancias:

deuclidiana(x,x)=j=1d(xjxj)2d_{euclidiana}(\mathbf{x}, \mathbf{x'}) = \sqrt{\sum_{j=1}^{d}(x_j - x'_j)^2}
dmanhattan(x,x)=j=1dxjxjd_{manhattan}(\mathbf{x}, \mathbf{x'}) = \sum_{j=1}^{d}|x_j - x'_j|

Efecto del hiperparámetro K:

K pequeño K grande
Modelo más complejo Modelo más simple
Alta varianza (overfitting) Alto sesgo (underfitting)
Sensible al ruido Ignora detalles locales

Elegir K con validación cruzada:

from sklearn.model_selection import cross_val_score

for k in range(1, 21):
    knn = KNeighborsClassifier(n_neighbors=k)
    scores = cross_val_score(knn, X_train_scaled, y_train, cv=5, scoring='accuracy')
    print(f"K={k:2d}: {scores.mean():.3f} ± {scores.std():.3f}")
# Elegir K con el mejor promedio

Problemas de KNN:

  • Lento en predicción: Necesita calcular distancia a todos los puntos de entrenamiento → O(n·d).
  • Maldición de la dimensionalidad: Con muchas features, todas las distancias se vuelven similares.
  • No maneja bien features irrelevantes.

Solución: KD-Tree o Ball-Tree para acelerar la búsqueda de vecinos. Selección de features antes de KNN.


2.3 Árboles de decisión

💡 Intuición

Un árbol de decisión imita la forma en que razonamos: "¿Es fin de semana? Si sí, ¿el grupo tiene más de 3 personas? Si sí, → recomendar postre."

Ventaja sobre KNN: el árbol es interpretable — podés leer las reglas que aprendió y explicarlas a un cliente o un auditor.

📐 Fundamento

Construcción del árbol — ID3/CART:

El árbol se construye eligiendo en cada nodo el feature que mejor separa los datos según una métrica de impureza:

Entropía (ID3):

H(S)=cpclog2(pc)H(S) = -\sum_{c} p_c \log_2(p_c)

Entropía 0 = todos los ejemplos son de la misma clase (perfecto).
Entropía 1 = mitad de cada clase (impuro).

Ganancia de información:

Gain(S,A)=H(S)vvalores(A)SvSH(Sv)\text{Gain}(S, A) = H(S) - \sum_{v \in \text{valores}(A)} \frac{|S_v|}{|S|} H(S_v)

Se elige el feature A con mayor ganancia de información.

Índice Gini (CART — scikit-learn por defecto):

Gini(S)=1cpc2\text{Gini}(S) = 1 - \sum_c p_c^2
from sklearn.tree import DecisionTreeClassifier, plot_tree
import matplotlib.pyplot as plt

# Entrenar
tree = DecisionTreeClassifier(
    max_depth=4,          # limitar profundidad para evitar overfitting
    min_samples_leaf=10,  # cada hoja tiene al menos 10 ejemplos
    random_state=42
)
tree.fit(X_train, y_train)

# Visualizar
plt.figure(figsize=(15, 8))
plot_tree(tree, feature_names=X.columns, class_names=['No', 'Sí'], filled=True)
plt.savefig('arbol_decision.png')

# Ver las reglas aprendidas
from sklearn.tree import export_text
print(export_text(tree, feature_names=list(X.columns)))

Overfitting en árboles:

Un árbol sin restricciones crece hasta memorizar cada ejemplo de entrenamiento (accuracy = 100% en train, malo en test). Técnicas de regularización:

  1. Limitar max_depth — profundidad máxima.
  2. Aumentar min_samples_split — mínimo de ejemplos para dividir un nodo.
  3. Post-pruning — crecer el árbol completo y luego podar las ramas que no mejoran la validación.

2.4 El tradeoff sesgo-varianza

💡 Intuición

Underfitting (alto sesgo): El modelo es demasiado simple — no captura los patrones reales. Un árbol de profundidad 1 que predice siempre "no postre" tiene alto sesgo.

Overfitting (alta varianza): El modelo es demasiado complejo — memorizó el ruido de los datos de entrenamiento y no generaliza. Un árbol de profundidad 100 que memorizó cada fila.

El objetivo: el punto medio donde el error en datos nuevos es mínimo.

📐 Fundamento

Diagnóstico gráfico (curva de aprendizaje):

from sklearn.model_selection import learning_curve
import numpy as np

train_sizes, train_scores, val_scores = learning_curve(
    DecisionTreeClassifier(max_depth=4),
    X_train_scaled, y_train,
    cv=5, train_sizes=np.linspace(0.1, 1.0, 10)
)

# Graficar
plt.plot(train_sizes, train_scores.mean(axis=1), label='Train')
plt.plot(train_sizes, val_scores.mean(axis=1), label='Validación')
plt.xlabel('Tamaño del training set')
plt.ylabel('Accuracy')
plt.legend()
Síntoma Diagnóstico Solución
Train alto, Val bajo Overfitting Regularizar, más datos, reduce features
Train bajo, Val bajo Underfitting Modelo más complejo, más features
Train ≈ Val ≈ alto Bien ajustado

Validación cruzada (k-fold cross validation):

from sklearn.model_selection import cross_validate

resultados = cross_validate(
    DecisionTreeClassifier(max_depth=4),
    X, y,
    cv=5,
    scoring=['accuracy', 'f1', 'roc_auc'],
    return_train_score=True
)
print(f"Val accuracy: {resultados['test_accuracy'].mean():.3f}")
print(f"Train accuracy: {resultados['train_accuracy'].mean():.3f}")

Métricas de clasificación:

                  Predicho +    Predicho -
Real +    │  TP (True Pos) │  FN (False Neg) │
Real -    │  FP (False Pos) │ TN (True Neg)  │

Accuracy  = (TP + TN) / Total
Precision = TP / (TP + FP)   — de los que predije +, ¿cuántos eran +?
Recall    = TP / (TP + FN)   — de los que son +, ¿cuántos predije +?
F1        = 2 * (Precision * Recall) / (Precision + Recall)

Cuándo usar Precision vs Recall:

  • Alta Precision: cuando un FP es muy costoso (spam filter — no queremos marcar email válido como spam).
  • Alto Recall: cuando un FN es muy costoso (diagnóstico de cáncer — no queremos perder un caso real).

🛠️ En la práctica

La Esquina: predecir si el cliente pedirá postre

import pandas as pd
from sklearn.tree import DecisionTreeClassifier
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.metrics import classification_report

# Dataset simulado de pedidos de La Esquina
datos = {
    'hora_dia':        [12, 13, 19, 20, 12, 13, 20, 21, 12, 19],
    'num_personas':    [2, 4, 2, 5, 1, 3, 6, 2, 2, 4],
    'gasto_total':     [8, 18, 6, 22, 4, 12, 30, 7, 5, 20],
    'es_fin_semana':   [0, 0, 1, 1, 0, 1, 1, 0, 0, 1],
    'pidio_postre':    [0, 1, 0, 1, 0, 1, 1, 0, 0, 1]
}
df = pd.DataFrame(datos)

X = df[['hora_dia', 'num_personas', 'gasto_total', 'es_fin_semana']]
y = df['pidio_postre']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

modelo = DecisionTreeClassifier(max_depth=3, min_samples_leaf=2)
modelo.fit(X_train, y_train)

y_pred = modelo.predict(X_test)
print(classification_report(y_test, y_pred))

# La regla más importante que aprendió el árbol:
# Si gasto_total > 15 Y num_personas > 2 → recomendar postre (Precision 0.87)

2.5 Ejercicios

✏️ Ejercicio 2.1 — Elegir el algoritmo

Para cada problema, elegí el tipo de aprendizaje supervisado (regresión / clasificación binaria / clasificación multiclase) y el algoritmo más apropiado (KNN / árbol de decisión / regresión lineal):

a. Predecir el tiempo de preparación de un pedido (en minutos) basándose en el número de ítems y el horario.

b. Clasificar si un mensaje de cliente en la app es una queja, una consulta, o un elogio.

c. Predecir si un nuevo empleado pasará la prueba de atención al cliente (sí/no) basándose en su experiencia previa y puntaje de entrevista.

d. Predecir el costo total de delivery basándose en la distancia.

✏️ Ejercicio 2.2 — Interpretar resultados

Un modelo de árbol de decisión para predecir si un cliente pedirá postre muestra:

Train accuracy: 98%
Test accuracy:  61%

a. ¿Cuál es el diagnóstico? b. Propone tres cambios en los hiperparámetros del DecisionTreeClassifier para corregirlo. c. Si después de los cambios el modelo queda: Train: 72%, Test: 70% — ¿es mejor o peor? ¿Por qué?


2.6 Para profundizar

2.7 Errores comunes

⚠️ Trampa común

Olvidar stratify=y con clases desbalanceadas. Si tu dataset tiene 95 % "no-fraude" y 5 % "fraude", train_test_split sin stratify puede dejar 0 % de fraude en el set de prueba. El modelo "predice todo no-fraude" y obtiene 95 % de accuracy. Falsa victoria.

Tip: siempre train_test_split(X, y, stratify=y, random_state=42) para clasificación. Y usar F1, AUC-ROC o recall, no accuracy, cuando las clases están desbalanceadas.

⚠️ Trampa común

Data leakage al normalizar antes del split. Si hacés StandardScaler().fit_transform(X) y después train_test_split, el mean y std del scaler ya vieron los datos de prueba. El modelo recibe información del futuro. La accuracy en test es optimista; en producción, decae.

Tip: usá Pipeline de scikit-learn: Pipeline([('scaler', StandardScaler()), ('model', ...)]). El pipeline ajusta el scaler solo con train dentro de cada fold de cross-validation.


Definiciones nuevas: aprendizaje supervisado, features, labels, train/test split, KNN, distancia euclidiana, árbol de decisión, entropía, ganancia de información, índice Gini, overfitting, underfitting, sesgo-varianza, validación cruzada, accuracy, precision, recall, F1.