Ya sabemos qué tipo de problema tenemos (04-02) y cómo dejar los datos listos (04-03). Falta abrir la caja negra de fit: ¿qué hace exactamente cada algoritmo cuando "aprende"? En esta lección recorreremos los algoritmos clásicos que resuelven la inmensa mayoría de los problemas de negocio: la regresión lineal, la regresión logística, los k vecinos más cercanos, los árboles de decisión, los bosques aleatorios (y la idea de ensamblado), las máquinas de vectores de soporte, Naive Bayes y k-means. Para cada uno daremos la intuición, la pizca de matemática necesaria para entender qué se optimiza (recordando que "aprender es optimizar", 03-04) y código sobre los datos de NovaMarket: la demanda del NovaClean para la regresión, los pedidos devueltos para la clasificación y los clientes para el agrupamiento. Terminaremos con una tabla comparativa para elegir algoritmo y una comparación en código de varios clasificadores sobre los mismos pedidos. Es importante porque elegir y entender el algoritmo es lo que permite interpretar sus resultados, explicárselos a Diego, saber cuándo desconfiar y, más adelante, ajustarlo (04-06). Un aviso: la comparación final usa solo la exactitud, medida provisional que en 04-05 sustituiremos por otras mejores.

Contenido

  1. Regresión lineal: la recta que menos se equivoca
  2. Regresión logística: de la suma ponderada a la probabilidad
  3. k vecinos más cercanos: predecir por semejanza
  4. Árboles de decisión: preguntas encadenadas
  5. Bosques aleatorios y la idea de ensamblado
  6. Máquinas de vectores de soporte: el margen más ancho
  7. Naive Bayes: probabilidad con independencia
  8. k-means por dentro: centroides y método del codo
  9. Tabla comparativa y cómo elegir
  10. Comparación en código de varios clasificadores
  11. Errores Comunes y Consejos
  12. Ejercicios
  13. Conclusión

  1. Regresión lineal: la recta que menos se equivoca

1.1 Intuición y matemática mínima

La regresión lineal supone que la etiqueta es una suma ponderada de las características más una constante: con una sola característica, una recta y = a + b·x; con varias, y = a + b₁·x₁ + b₂·x₂ + .... Los parámetros son a (ordenada) y los coeficientes b. ¿Qué recta elegir? La que minimiza la suma de errores al cuadrado (mínimos cuadrados): para cada ejemplo se calcula la diferencia entre el valor real y el predicho, se eleva al cuadrado (para que los errores por exceso y por defecto no se cancelen y para penalizar más los grandes) y se suman.

Ejemplo numérico pequeño: cuatro semanas con ventas 350, 340, 380, 390. Comparamos tres rectas candidatas:

Recta Predicciones Errores Suma de cuadrados
330 + 10·semana 340, 350, 360, 370 10, −10, 20, 20 1.000
335 + 15·semana 350, 365, 380, 395 0, −25, 0, −5 650
330 + 15·semana 345, 360, 375, 390 5, −20, 5, 0 450
325 + 16·semana (la óptima) 341, 357, 373, 389 9, −17, 7, 1 420

LinearRegression().fit encuentra la última sin probar candidatas: para este problema existe una fórmula cerrada (las llamadas ecuaciones normales) que da directamente los coeficientes óptimos. Es uno de los pocos algoritmos con solución exacta; la mayoría de los siguientes optimizan por búsqueda iterativa.

La gran virtud de la regresión lineal es la interpretabilidad: cada coeficiente dice cuánto cambia la predicción por cada unidad de la característica, con las demás fijas.

1.2 Código: la demanda del NovaClean con estacionalidad

En 04-02 una recta sobre semana daba un error medio de 47 unidades porque ignoraba la estacionalidad y el Black Friday. Añadimos características que los representen (como anunciamos en 04-03) y el mismo algoritmo mejora mucho:

import numpy as np
from novamarket_ml import generar_demanda_semanal
from sklearn.linear_model import LinearRegression

d = generar_demanda_semanal(104, 42)
d["sen"] = np.sin(2 * np.pi * d["semana"] / 52)      # la estacionalidad anual como onda
d["cos"] = np.cos(2 * np.pi * d["semana"] / 52)
d["black_friday"] = ((d["semana"] - 1) % 52 == 47).astype(int)

caracteristicas = ["semana", "sen", "cos", "black_friday"]
train, test = d[d["semana"] <= 78], d[d["semana"] > 78]

reg = LinearRegression().fit(train[caracteristicas], train["unidades"])
print("Coeficientes:", dict(zip(caracteristicas, reg.coef_.round(2))))
print("Ordenada:", round(reg.intercept_, 1))
prevision = reg.predict(test[caracteristicas])
print("Error medio en test:", round(np.abs(prevision - test["unidades"]).mean(), 1), "unidades")

Salida:

Coeficientes: {'semana': 2.47, 'sen': -10.25, 'cos': -61.41, 'black_friday': 255.22}
Ordenada: 399.7
Error medio en test: 16.8 unidades

Explicación: la recta ahora tiene cuatro coeficientes. semana (2,47) recupera la tendencia real de 2,5 unidades/semana; sen y cos capturan la onda anual (la combinación de ambos equivale a una onda de amplitud √(10² + 61²) ≈ 62, frente a los 60 reales); black_friday suma 255 unidades esa semana (250 reales). El error medio cae de 47 a 17 unidades. El algoritmo es el mismo; lo que ha cambiado son las características. Es la lección de 04-03 confirmada con números.

  1. Regresión logística: de la suma ponderada a la probabilidad

2.1 Intuición

Pese al nombre, es un algoritmo de clasificación. Calcula la misma suma ponderada que la regresión lineal, z = a + b₁·x₁ + ... + bₙ·xₙ, pero en vez de usar z como predicción la pasa por la función sigmoide, σ(z) = 1 / (1 + e^(−z)), que convierte cualquier número en un valor entre 0 y 1 interpretable como probabilidad de la clase positiva. Si z = 0, la probabilidad es 0,5; valores muy positivos dan probabilidad cercana a 1 y muy negativos, cercana a 0. Es exactamente la fórmula que usamos en el generador de 04-01 para crear la "verdad oculta"; por eso la logística es un candidato natural para estos datos.

Después se aplica un umbral: por defecto, probabilidad ≥ 0,5 → clase 1. Ese umbral no es sagrado: en 04-05 veremos que conviene moverlo según el coste de cada error.

Los parámetros (los coeficientes) se aprenden maximizando la verosimilitud de los datos, o equivalentemente minimizando la pérdida logarítmica (04-01), que castiga mucho asignar probabilidad baja a lo que sí ocurrió. No hay fórmula cerrada: se optimiza iterativamente siguiendo el gradiente, la idea que anunciamos al cerrar 03-04 y que desarrollaremos en 05-03.

2.2 Código: leer los coeficientes y calcular una probabilidad a mano

from novamarket_ml import generar_pedidos_ml
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
import pandas as pd

pedidos = generar_pedidos_ml(3000, 42)
cols = ["importe", "num_articulos", "dias_entrega", "cliente_nuevo"]
X_train, X_test, y_train, y_test = train_test_split(
    pedidos[cols], pedidos["devuelto"], test_size=0.25, random_state=42, stratify=pedidos["devuelto"])

log = LogisticRegression(max_iter=1000).fit(X_train, y_train)
print("Coeficientes:", dict(zip(cols, log.coef_[0].round(4))), " Ordenada:", round(log.intercept_[0], 3))

pedido = pd.DataFrame({"importe": [250.0], "num_articulos": [1], "dias_entrega": [5], "cliente_nuevo": [1]})
z = log.intercept_[0] + (log.coef_[0] * pedido.values[0]).sum()
print("z =", round(z, 3), " sigmoide(z) =", round(1 / (1 + np.exp(-z)), 3))
print("predict_proba:", log.predict_proba(pedido).round(3))

Salida:

Coeficientes: {'importe': 0.012, 'num_articulos': -0.2012, 'dias_entrega': 0.3069, 'cliente_nuevo': 1.8821}  Ordenada: -4.906
z = 1.302  sigmoide(z) = 0.786
predict_proba: [[0.214 0.786]]

Lectura: cada euro de importe suma 0,012 a z; cada día de entrega, 0,31; ser cliente nuevo, 1,88; cada artículo adicional resta 0,20. Para el pedido de 250 € (1 artículo, 5 días, cliente nuevo), z = −4,906 + 0,012·250 − 0,201·1 + 0,307·5 + 1,882·1 = 1,30, y la sigmoide da 0,786: se predice devolución. El mismo pedido de un cliente habitual tendría z = −0,58 y probabilidad 0,36: no se marcaría. Diego puede seguir el cálculo con una calculadora, y esa es la fuerza de este modelo. Nota: sin escalar, los coeficientes no son comparables entre columnas (0,012 por euro frente a 1,88 por ser nuevo); para comparar la importancia hay que estandarizar primero (04-03) o mirar el rango de cada variable.

  1. k vecinos más cercanos: predecir por semejanza

3.1 Intuición

k-NN no aprende ninguna fórmula: memoriza el conjunto de entrenamiento y, para predecir un pedido nuevo, busca los k pedidos más parecidos (los más cercanos en el espacio de características, con la distancia euclídea habitual) y vota: la clase mayoritaria entre esos vecinos (o la media de sus valores, en regresión). Es el algoritmo más intuitivo ("los pedidos como este se devolvieron") y tiene dos consecuencias prácticas:

  • Necesita escalado (04-03): sin él, importe (decenas o cientos) domina la distancia y cliente_nuevo (0/1) no cuenta.
  • El hiperparámetro k controla la suavidad: k = 1 sigue al vecino más próximo (memoriza el ruido), k grande promedia mucho (pierde detalle). En 04-06 veremos que es el ejemplo de manual de sobreajuste frente a subajuste.

3.2 Código

from sklearn.neighbors import KNeighborsClassifier
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline

for k in (1, 5, 15, 51):
    knn = Pipeline([("escalar", StandardScaler()),
                    ("knn", KNeighborsClassifier(n_neighbors=k))]).fit(X_train, y_train)
    print(f"k={k:2d}  train {knn.score(X_train, y_train):.3f}  test {knn.score(X_test, y_test):.3f}")

sin_escalar = KNeighborsClassifier(n_neighbors=15).fit(X_train, y_train)
print("k=15 sin escalar: test", round(sin_escalar.score(X_test, y_test), 3))

Salida:

k= 1  train 1.000  test 0.815
k= 5  train 0.888  test 0.864
k=15  train 0.872  test 0.875
k=51  train 0.872  test 0.872
k=15 sin escalar: test 0.837

Con k = 1 el modelo acierta el 100 % del entrenamiento (cada pedido es su propio vecino) y solo el 81,5 % del test: memoria pura. Con k = 15 se estabiliza en 87,5 %. Y sin escalar cae al 83,7 %, casi el nivel de "nunca se devuelve" (83,6 %): la distancia estaba dominada por el importe.

  1. Árboles de decisión: preguntas encadenadas

4.1 Intuición y matemática mínima

Un árbol de decisión hace lo que hacía aprender_umbral en 01-02, pero repetidamente: elige la columna y el umbral que mejor separan las clases, parte los datos en dos, y repite en cada mitad hasta que las hojas son suficientemente puras o se alcanza la profundidad máxima. El resultado es un conjunto de reglas legibles.

¿Qué significa "mejor separan"? Se mide la impureza de un grupo: si todos sus pedidos son de la misma clase, impureza 0; si están al 50 %, impureza máxima. Las dos medidas habituales son la impureza de Gini, 1 − p² − (1−p)² para dos clases con proporción p de positivos, y la entropía, −p·log₂p − (1−p)·log₂(1−p). Ambas se comportan igual en la práctica. El árbol prueba todos los umbrales de todas las columnas y elige el que más reduce la impureza ponderada de los dos hijos respecto al padre.

Con números de nuestros datos: en la raíz hay 2.250 pedidos con un 16,4 % de devueltos, Gini = 1 − 0,164² − 0,836² = 0,275. La partición importe ≤ 195,65 deja a la izquierda 1.895 pedidos con 11,5 % de devueltos (Gini 0,204) y a la derecha 355 con 42,8 % (Gini 0,490); la impureza ponderada es (1.895·0,204 + 355·0,490)/2.250 = 0,249, una ganancia de 0,026. La partición alternativa por cliente_nuevo daría 0,252 (menos ganancia); por eso el árbol pregunta primero por el importe. Ese es todo el "aprendizaje": una búsqueda voraz (03-02) de la mejor pregunta en cada nodo.

4.2 Código: leer las reglas con export_text

Usamos las cuatro columnas numéricas más la categoría en one-hot (sin escalar: los árboles no lo necesitan):

from sklearn.tree import DecisionTreeClassifier, export_text

Xd = pd.get_dummies(pedidos[cols + ["categoria"]], columns=["categoria"], dtype=int)
Xd_train, Xd_test, yd_train, yd_test = train_test_split(
    Xd, pedidos["devuelto"], test_size=0.25, random_state=42, stratify=pedidos["devuelto"])

arbol = DecisionTreeClassifier(max_depth=3, random_state=42).fit(Xd_train, yd_train)
print(export_text(arbol, feature_names=list(Xd.columns), show_weights=True))
print("Acierto en test:", round(arbol.score(Xd_test, yd_test), 3))

Salida (resumida):

|--- importe <= 195.65
|   |--- cliente_nuevo <= 0.50
|   |   |--- dias_entrega <= 6.50           -> weights: [1075, 60]  class: 0
|   |   |--- dias_entrega >  6.50           -> weights: [159, 28]   class: 0
|   |--- cliente_nuevo >  0.50
|   |   |--- importe <= 142.31              -> weights: [379, 70]   class: 0
|   |   |--- importe >  142.31              -> weights: [64, 60]    class: 0
|--- importe >  195.65
|   |--- cliente_nuevo <= 0.50
|   |   |--- importe <= 358.52              -> weights: [179, 44]   class: 0
|   |   |--- importe >  358.52              -> weights: [8, 15]     class: 1
|   |--- cliente_nuevo >  0.50
|   |   |--- categoria_electronica <= 0.50  -> weights: [16, 56]    class: 1
|   |   |--- categoria_electronica >  0.50  -> weights: [0, 37]     class: 1
Acierto en test: 0.868

Cada hoja muestra cuántos pedidos de entrenamiento cayeron en ella [no devueltos, devueltos] y la clase mayoritaria. Se lee como un manual de reglas: "pedidos de más de 195 € de clientes nuevos: devueltos (56 de 72; y 37 de 37 si es electrónica)"; "clientes habituales solo por encima de 358 €". Diego reconoce su intuición corregida por los datos: su umbral de 300 € era razonable para clientes habituales y demasiado alto para los nuevos. La hoja [64, 60] (clientes nuevos entre 142 y 196 €) está casi al 50 %: es la zona gris de 02-04.

Si quitamos max_depth, el árbol sigue partiendo hasta hojas puras: en estos datos alcanza profundidad 21 con 414 hojas, acierta el 100 % en entrenamiento y el 82,5 % en test, peor que la profundidad 3. Es el sobreajuste que estudiaremos en 04-06; de momento, quédate con que la profundidad es un hiperparámetro que hay que limitar.

  1. Bosques aleatorios y la idea de ensamblado

Un árbol solo es inestable: cambia mucho si cambian un poco los datos. La solución es un ensamblado: entrenar muchos modelos y combinar sus votos. El bosque aleatorio (random forest) entrena cientos de árboles, cada uno con una muestra aleatoria con reemplazo de los datos (bagging, de bootstrap aggregating) y considerando en cada nodo solo un subconjunto aleatorio de columnas, y predice por mayoría (o promedio en regresión). Cada árbol se equivoca de forma distinta, y los errores se cancelan al votar: es la sabiduría de las multitudes aplicada a modelos.

from sklearn.ensemble import RandomForestClassifier

bosque = RandomForestClassifier(n_estimators=200, min_samples_leaf=5, random_state=42)
bosque.fit(Xd_train, yd_train)
print("Acierto en test:", round(bosque.score(Xd_test, yd_test), 3))
importancias = pd.Series(bosque.feature_importances_, index=Xd.columns).sort_values(ascending=False)
print(importancias.round(3).head(4))

Salida:

Acierto en test: 0.871
importe          0.528
cliente_nuevo    0.204
dias_entrega     0.132
num_articulos    0.075

n_estimators es el número de árboles y min_samples_leaf el mínimo de pedidos por hoja (evita hojas de un solo ejemplo). Se pierde la lectura de reglas de un árbol único, pero se gana robustez y las importancias de características que usamos en 04-03 (aquí, el importe explica más de la mitad de la separación).

La otra gran familia de ensamblados es el boosting (gradient boosting: GradientBoostingClassifier, y las librerías XGBoost, LightGBM y CatBoost): en lugar de árboles independientes en paralelo, se entrenan en secuencia, y cada árbol nuevo se concentra en corregir los errores del conjunto anterior. Es, hoy, el algoritmo que más competiciones y proyectos gana sobre datos tabulares; se presenta en 07-03 y basta con que sepas que existe y cuándo probarlo (cuando el bosque funciona y quieres exprimir más).

  1. Máquinas de vectores de soporte: el margen más ancho

La SVM (support vector machine) busca la frontera que separa las dos clases dejando el margen más ancho posible a ambos lados; los ejemplos que tocan el margen son los vectores de soporte, y solo ellos determinan la frontera. Cuando las clases no son separables por una recta (o un plano), el truco del kernel proyecta los datos a un espacio de más dimensiones donde sí lo son, sin calcular esa proyección explícitamente; el kernel RBF (gaussiano) es el más usado y permite fronteras curvas. Los hiperparámetros principales son C (cuánto se penalizan los puntos mal clasificados) y gamma (cómo de local es la frontera con RBF). Necesita escalado y escala mal a datasets muy grandes (cientos de miles de filas), pero es muy sólida en tamaños medios.

from sklearn.svm import SVC

svm = Pipeline([("escalar", StandardScaler()), ("svm", SVC(kernel="rbf", C=1.0))]).fit(X_train, y_train)
print("SVM (RBF) test:", round(svm.score(X_test, y_test), 3))     # 0.872

Solo intuición: la SVM no devuelve probabilidades de forma natural (existe una opción probability=True, más lenta) y sus fronteras no se leen como reglas.

  1. Naive Bayes: probabilidad con independencia

Naive Bayes aplica el teorema de Bayes (que estudiaremos con detalle en 06-03) para calcular la probabilidad de cada clase dadas las características, con la suposición "ingenua" de que las características son independientes entre sí dada la clase. Esa suposición casi nunca es cierta, pero el algoritmo funciona sorprendentemente bien, es rapidísimo y necesita pocos datos; es el clásico del filtrado de spam y una buena línea base para clasificar reseñas por palabras (caso 4). Sobre nuestros pedidos, GaussianNB().fit(X_train, y_train) acierta el 86,0 % en test.

  1. k-means por dentro: centroides y método del codo

En 04-02 usamos k-means para segmentar clientes. Por dentro es un algoritmo iterativo de dos pasos (el algoritmo de Lloyd):

  1. Elegir k centroides iniciales (puntos en el espacio de características; KMeans usa una inicialización inteligente llamada k-means++).
  2. Asignar cada cliente al centroide más cercano.
  3. Recalcular cada centroide como la media de los clientes asignados.
  4. Repetir 2-3 hasta que las asignaciones no cambien.

Es un ascenso de colina (03-04) sobre la inercia, la suma de distancias al cuadrado de cada punto a su centroide: cada iteración la reduce, y se detiene en un óptimo local; por eso n_init=10 ejecuta 10 arranques distintos y se queda con el mejor, exactamente como los reinicios aleatorios de 03-04. En nuestros clientes converge en 4 iteraciones.

El número de grupos k es un hiperparámetro que hay que decidir. El método del codo entrena k-means para varios valores de k y dibuja la inercia: siempre baja al aumentar k (con k = n sería 0), pero llega un punto en el que añadir grupos aporta poco; ese "codo" es un buen k.

from sklearn.cluster import KMeans
from novamarket_ml import generar_clientes_ml

clientes = generar_clientes_ml(600, 42)
Xc = StandardScaler().fit_transform(clientes)
for k in range(1, 9):
    km = KMeans(n_clusters=k, n_init=10, random_state=42).fit(Xc)
    print(k, round(km.inertia_, 1))

Salida:

1 1800.0
2 795.7
3 399.9
4 299.2
5 254.5
6 219.3
7 194.4
8 174.1

De 1 a 2 grupos la inercia cae 1.000; de 2 a 3, otros 400; de 3 a 4, solo 100, y después menos de 50 por grupo: el codo está en k = 3, que coincide con los tres tipos de cliente. Si lo dibujas con Matplotlib (plt.plot(range(1, 9), inercias, marker="o")) lo verás como un brazo doblado. El codo no siempre es tan claro; entonces se complementa con el coeficiente de silueta (silhouette_score) y, sobre todo, con la interpretabilidad de negocio de los grupos.

  1. Tabla comparativa y cómo elegir

Algoritmo Problema Interpretabilidad ¿Necesita escalado? Fortalezas Debilidades Cuándo elegirlo
Regresión lineal Regresión Muy alta (coeficientes) Recomendable con regularización Simple, rápida, exacta, explica el efecto de cada variable Solo relaciones lineales; sensible a atípicos Línea base de regresión; cuando importa explicar
Regresión logística Clasificación Alta (coeficientes, probabilidades) Probabilidades bien calibradas, robusta, rápida Frontera lineal Línea base de clasificación; problemas con requisitos de explicación
k vecinos Ambos Media (se ven los vecinos) Sí, imprescindible Sin suposiciones, captura formas complejas Lento al predecir con muchos datos; sufre con muchas columnas Datos pequeños o medianos y pocas columnas relevantes
Árbol de decisión Ambos Muy alta (reglas) No Reglas legibles, maneja mezclas de tipos e interacciones Inestable, sobreajusta si no se limita Cuando hay que explicar decisiones una a una
Bosque aleatorio Ambos Media (importancias) No Robusto, poco ajuste necesario, buen rendimiento general Menos interpretable, más lento, ficheros grandes Primera opción "seria" en datos tabulares
Gradient boosting Ambos Media No Suele dar el mejor rendimiento en tabulares Más hiperparámetros, fácil sobreajustar Cuando el bosque funciona y se quiere más
SVM Clasificación (y regresión) Baja Muy buena en tamaño medio, fronteras flexibles con kernel Escala mal, sin probabilidades nativas Datos medianos con fronteras complejas
Naive Bayes Clasificación Media No Rapidísimo, pocos datos, texto Suposición de independencia Texto, línea base rápida
k-means Agrupamiento Alta (centroides) Simple, escalable Hay que fijar k; grupos esféricos Segmentación con grupos compactos
Redes neuronales Ambos Baja Imágenes, texto, señales, escala masiva Muchos datos y cómputo, poco interpretables Módulo 5

Regla práctica de Marta: empezar por logística (o lineal) como línea base interpretable, probar un bosque aleatorio, y solo si compensa pasar a boosting o a redes. Y elegir según el problema completo, no solo el acierto: si Diego necesita justificar cada denegación de reembolso, el árbol o la logística ganan aunque el bosque acierte un poco más.

  1. Comparación en código de varios clasificadores

Reutilizamos la preparación de 04-03 tal como la empaquetamos en su sección 9.1 (preparar_pedidos y crear_preparacion en novamarket_ml.py) y cambiamos solo el último paso del pipeline:

from novamarket_ml import generar_pedidos_ml, ensuciar_pedidos, preparar_pedidos, crear_preparacion

X, y = preparar_pedidos(ensuciar_pedidos(generar_pedidos_ml(3000, 42), 42))
Xtr, Xte, ytr, yte = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)

modelos = {
    "Regresión logística": LogisticRegression(max_iter=1000),
    "k vecinos (k=15)":    KNeighborsClassifier(n_neighbors=15),
    "Árbol (prof. 4)":     DecisionTreeClassifier(max_depth=4, random_state=42),
    "Bosque aleatorio":    RandomForestClassifier(n_estimators=200, min_samples_leaf=5, random_state=42),
    "SVM (RBF)":           SVC(kernel="rbf"),
}
for nombre, m in modelos.items():
    p = Pipeline([("preparacion", crear_preparacion()), ("modelo", m)]).fit(Xtr, ytr)
    print(f"{nombre:22s} train {p.score(Xtr, ytr):.3f}  test {p.score(Xte, yte):.3f}")
print("Línea base 'nunca se devuelve': ", round((yte == 0).mean(), 3))

Salida:

Regresión logística    train 0.881  test 0.871
k vecinos (k=15)       train 0.850  test 0.841
Árbol (prof. 4)        train 0.886  test 0.855
Bosque aleatorio       train 0.908  test 0.859
SVM (RBF)              train 0.894  test 0.864
Línea base 'nunca se devuelve':  0.836

Observaciones (provisionales, porque la exactitud engaña con clases desequilibradas, como veremos en 04-05):

  • La regresión logística es la mejor aquí, lo que no sorprende: la "verdad oculta" del generador es una sigmoide de una suma ponderada, justo su familia. En datos reales, con interacciones y no linealidades, los bosques suelen adelantarla.
  • k vecinos empeora respecto a la sección 3 (0,841 frente a 0,875): con las 21 columnas del pipeline, muchas de ellas ruido, las distancias pierden sentido (la "maldición de la dimensionalidad"). Es el algoritmo más sensible a la selección de características.
  • El bosque tiene la mayor diferencia entre train (0,908) y test (0,859): un principio de sobreajuste que 04-06 tratará con hiperparámetros.
  • Ningún modelo pasa del 87 % y la línea base está en 83,6 %. Con esta medida parece poca ganancia; en 04-05 veremos que, en lo que importa (detectar devoluciones sin molestar a clientes honrados), la diferencia es mucho mayor.

Falta un algoritmo importante en esta lista: el perceptrón y las redes neuronales, que son "otro algoritmo más" de clasificación y regresión (scikit-learn tiene MLPClassifier), pero cuya arquitectura y entrenamiento merecen un módulo entero, el 5.

Errores Comunes y Consejos

  • Usar k-NN o SVM sin escalar. Los resultados caen al nivel de la línea base, como vimos con k-NN sin StandardScaler.
  • Dejar un árbol sin límite de profundidad. Memoriza (100 % en train, 82,5 % en test). Fija max_depth o min_samples_leaf, o usa un bosque.
  • Interpretar los coeficientes de la logística sin mirar las unidades. 0,012 por euro puede pesar más que 1,88 por ser cliente nuevo cuando el importe varía en cientos de euros. Estandariza antes de comparar.
  • Elegir el algoritmo por moda. El boosting o las redes no son mejores por definición; para 3.000 pedidos con pocas columnas, la logística gana. Prueba siempre la línea base simple.
  • Fiarse de k-means sin el codo ni interpretación. Siempre encuentra k grupos; el codo, la silueta y el sentido de negocio dicen si valen algo.
  • Comparar modelos con una sola cifra de exactitud. Es lo que acabamos de hacer, y por eso hemos insistido en que es provisional. La siguiente lección da las herramientas correctas.

Ejercicios

Ejercicio 1. Añade a la regresión de la demanda (sección 1.2) la característica unidades_semana_anterior (d["unidades"].shift(1), eliminando la primera fila con dropna()). ¿Mejora el error medio? Mira el coeficiente que recibe. ¿Por qué aporta poco cuando ya están la tendencia, la estacionalidad y el Black Friday, y en qué situación real sería más valioso?

Ejercicio 2. Calcula a mano (o con dos líneas de pandas) la impureza de Gini de la partición dias_entrega <= 4 en la raíz de los datos de entrenamiento de la sección 4, y compárala con la de importe <= 195.65 (0,249). ¿Cuál preferiría el árbol? Comprueba después qué ocurre con la profundidad y el número de hojas si entrenas con min_samples_leaf=50 en lugar de max_depth=3.

Ejercicio 3. Con los clientes de la sección 8, calcula el coeficiente de silueta (from sklearn.metrics import silhouette_score; silhouette_score(Xc, km.labels_)) para k de 2 a 6. ¿Coincide el máximo con el codo? Cambia después la semilla de generar_clientes_ml a 7 y repite: ¿se mantiene la conclusión?

Soluciones

Solución 1. El error medio queda prácticamente igual (17,0 frente a 16,8 unidades) y el coeficiente del lag es casi cero (0,04): las otras cuatro características ya explican la serie, así que las ventas de la semana anterior no añaden información nueva. Sería valioso en una serie real, donde la demanda tiene inercia que ni la tendencia ni la estacionalidad fijas capturan (una campaña que arranca, un producto que se pone de moda, una rotura de stock): ahí el lag suele ser la característica más importante. También es la única característica que exige cuidado con la fuga (04-03): siempre shift(1).

Solución 2. Para dias_entrega <= 4: la mitad izquierda (días 1-4, 1.293 pedidos) tiene un 11,7 % de devueltos y la derecha (5-7, 957 pedidos) un 22,9 %; los Gini son 0,206 y 0,353 y la impureza ponderada 0,269, una ganancia de solo 0,006 frente a los 0,026 de la partición por importe. El árbol prefiere el importe. Con min_samples_leaf=50 (sin max_depth) el árbol crece más que con profundidad 3 (profundidad 8 y 32 hojas), pero ninguna hoja tiene menos de 50 pedidos, lo que limita la memorización: acierta 0,876 en entrenamiento y 0,864 en test, parecido al árbol de profundidad 3. Son dos formas distintas de frenar el mismo problema, y en 04-06 las compararemos con método.

Solución 3. Con la semilla 42, la silueta es máxima en k = 3 (0,57, frente a 0,55 con k = 2 y 0,53 con k = 4) y cae a 0,42 con k = 6: coincide con el codo y con los tres tipos de cliente. Con la semilla 7 los valores cambian ligeramente (0,58 en k = 3) pero el máximo sigue en k = 3, porque la estructura oculta (tres grupos) es la misma; solo cambian los clientes concretos. Si en un dataset real la silueta fuera baja para todo k (por debajo de 0,25) o el codo no apareciera, la conclusión honesta sería que no hay grupos claros, y forzar una segmentación sería inventar estructura.

Conclusión

En esta lección hemos abierto la caja de fit para los algoritmos clásicos: la regresión lineal minimiza los errores al cuadrado y, con las características adecuadas, redujo el error de la demanda del NovaClean de 47 a 17 unidades; la regresión logística pasa una suma ponderada por la sigmoide para dar probabilidades legibles; k vecinos predice por semejanza y exige escalado; los árboles encadenan preguntas eligiendo en cada nodo la que más reduce la impureza de Gini, y producen reglas que Diego puede leer; los bosques aleatorios promedian muchos árboles (bagging) y el boosting los encadena corrigiendo errores; las SVM buscan el margen más ancho con ayuda de kernels; Naive Bayes aplica Bayes con independencia; y k-means alterna asignar y recalcular centroides, con el método del codo para elegir k. La tabla comparativa y la comparación en código nos han dejado una primera clasificación de modelos sobre los pedidos de NovaMarket, medida solo con la exactitud.

Y ahí está el problema pendiente: la exactitud dice que la logística acierta el 87,1 % y la línea base "nunca se devuelve" el 83,6 %, una diferencia que a Diego le parece pequeña. En la siguiente lección, Evaluación y Validación de Modelos, veremos por qué esa cifra engaña con clases desequilibradas, aprenderemos a leer la matriz de confusión y a traducir cada tipo de error a euros, mediremos precisión, sensibilidad, F1 y AUC, elegiremos el umbral según el coste, usaremos las métricas de regresión para la demanda y validaremos con métodos que no se dejen engañar por una única división afortunada, incluida la validación temporal para las series.

Fundamentos de Inteligencia Artificial (IA)

Módulo 1: Introducción a la Inteligencia Artificial

Módulo 2: Principios Básicos de la IA

Módulo 3: Algoritmos en IA

Módulo 4: Aprendizaje Automático (Machine Learning)

Módulo 5: Redes Neuronales y Deep Learning

Módulo 6: Lógica y Sistemas Expertos

Módulo 7: Herramientas y Lenguajes de Programación en IA

Módulo 8: Proyectos y Casos de Estudio

Módulo 9: Ejercicios y Prácticas

Módulo 10: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados