# Ocho modelos

URL canónica: https://missyera.com/guias/machine-learning-desde-cero/los-modelos/
Última actualización: 2026-11-14
Autora: Gera Flores (Miss Yera)

> **Respuesta corta**
>
> Aquí gana la regresión logística con 0,7214 de AUC, por delante de naive Bayes, del bosque podado, del boosting y del SVM. Y no es una anécdota de este archivo: cuando hay poca señal, el modelo complicado no tiene de dónde sacarla y solo añade formas de equivocarse. A mí me costó aceptarlo más de lo que me gustaría admitir 🏆

Capítulo 13 de 29 del libro [Machine learning desde cero](https://missyera.com/guias/machine-learning-desde-cero.md).

Hasta aquí hemos usado un solo modelo, la regresión logística, porque hacía falta uno para poder hablar de lo demás. Hoy probamos ocho 🎰

Y quiero que apuestes antes de mirar: **¿cuál crees que va a ganar?** Apúntalo mentalmente, porque la respuesta es la lección del capítulo y funciona mucho mejor si primero te mojas 🎲

Y te adelanto el final, porque es lo interesante del capítulo: **gana la regresión logística**, que es el más simple y el más viejo de todos. Los que salen en las noticias quedan por debajo.

Esto pasa mucho más de lo que te cuentan, y saber por qué es más útil que saberse los ocho algoritmos.

## El punto de partida

```
import pandas as pd
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv'

def carga_limpia(url):
    v = pd.read_csv(url).drop_duplicates()
    v['ciudad'] = (v['ciudad'].str.strip().str.lower()
                   .str.normalize('NFKD')
                   .str.encode('ascii', 'ignore').str.decode('utf-8'))
    v['monto'] = pd.to_numeric(v['monto'].str.replace(',', '.'))
    for col in ['fecha', 'fecha_ultima_compra']:
        f = pd.to_datetime(v[col], format='%Y-%m-%d', errors='coerce')
        falta = f.isna() & v[col].notna()
        f[falta] = pd.to_datetime(v.loc[falta, col], format='%d/%m/%Y', errors='coerce')
        v[col] = f
    return v

def prepara(v):
    v = v.sort_values(['cliente_id', 'fecha']).copy()
    v['sin_compra_previa'] = v['fecha_ultima_compra'].isna().astype(int)
    v['sin_descuento'] = v['descuento'].isna().astype(int)
    v['sin_satisfaccion'] = v['satisfaccion'].isna().astype(int)
    v['precio_unitario'] = v['monto'] / v['unidades']
    v['visita_numero'] = v.groupby('cliente_id').cumcount() + 1
    return v

NUMERICAS = ['unidades', 'monto', 'descuento', 'satisfaccion', 'precio_unitario',
             'sin_compra_previa', 'sin_descuento', 'sin_satisfaccion', 'visita_numero']
CATEGORICAS = ['ciudad', 'segmento', 'canal', 'categoria']

def arma(clasificador):
    """El pipeline del capítulo 11 con el modelo que le pases."""
    pre = ColumnTransformer([
        ('num', Pipeline([('r', SimpleImputer(strategy='median')),
                          ('e', StandardScaler())]), NUMERICAS),
        ('cat', Pipeline([('r', SimpleImputer(strategy='most_frequent')),
                          ('c', OneHotEncoder(handle_unknown='ignore'))]), CATEGORICAS),
    ])
    return Pipeline([('pre', pre), ('mod', clasificador)])

datos = prepara(carga_limpia(URL))
X = datos[NUMERICAS + CATEGORICAS]
y = datos['compro']
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25,
                                          random_state=42, stratify=y)
print(X_tr.shape, X_te.shape)
```

```text
(2250, 13) (750, 13)
```

![Nube de puntos de dos clases separadas por una recta: la frontera de decisión de una regresión logística. A un lado el modelo predice una clase y al otro la contraria, y lo que aprende el modelo es dónde va esa recta.](https://missyera.com/static/diagramas/webp/g-ml-frontera.webp)

*Esto es todo lo que hace una regresión logística: buscar dónde poner la raya. Y de paso explica su límite, porque si tus clases no se separan con una recta, no hay ajuste de parámetros que la salve: hace falta otro modelo.*

## 1. Regresión logística: la que hay que entender

`p = σ(z) = 1/(1 + e^(−z)), z = b + ∑_jw_jx_j`

el modelo suma las columnas con un peso cada una y esa suma la aplasta entre 0 y 1 para que se pueda leer como probabilidad

Le da un peso a cada columna, los suma y convierte esa suma en una probabilidad. Nada más. Es de 1958 y sigue ganando.

```
logistica = arma(LogisticRegression(max_iter=1000, random_state=42)).fit(X_tr, y_tr)
print('exactitud:', round(logistica.score(X_te, y_te), 4))
print('AUC      :', round(roc_auc_score(y_te, logistica.predict_proba(X_te)[:, 1]), 4))
```

```text
exactitud: 0.6733
AUC      : 0.7214
```

Sus dos ventajas son enormes y no se ven en el número: **se puede explicar** (cada peso dice cuánto suma o resta cada columna) y **no se rompe** con datos que no vio.

Su límite: solo sabe sumar. No puede aprender que "WhatsApp funciona *solo* para mayoristas", que en el capítulo 4 vimos que pasa. Esas interacciones hay que dárselas escritas.

![Los mismos puntos partidos por un árbol de decisión de profundidad 2: un primer corte vertical parte el plano en dos, y dentro de la mitad izquierda un segundo corte horizontal vuelve a partir. Quedan tres rectángulos, uno por clase.](https://missyera.com/static/diagramas/webp/g-ml-arbol.webp)

*Un árbol no traza rayas inclinadas: parte siempre por una variable a la vez, así que sus fronteras son escalones. Ahí está su ventaja y su límite en la misma imagen: cada corte se lee en voz alta como una regla, y por eso se puede auditar.*

## 2. Árbol de decisión: el que se lee

`Gini = 1−∑_kp_k^2`

cuánto se mezclan las clases dentro de una hoja: cero si todas son iguales y máximo si están mitad y mitad

```
from sklearn.tree import DecisionTreeClassifier

arbol = arma(DecisionTreeClassifier(max_depth=4, random_state=42)).fit(X_tr, y_tr)
print('exactitud:', round(arbol.score(X_te, y_te), 4))
print('AUC      :', round(roc_auc_score(y_te, arbol.predict_proba(X_te)[:, 1]), 4))
```

```text
exactitud: 0.6413
AUC      : 0.6643
```

Peor que la logística. Un árbol solo hace cortes rectos ("monto mayor que 532") y con cuatro niveles se queda corto.

Lo que sí tiene es que se puede dibujar y enseñar en una reunión, y eso a veces vale más que cinco puntos de AUC 🌳

![Los mismos puntos con tres modelos encima: a la izquierda una recta que no captura la curva, en el centro una curva suave que pasa por el medio de los puntos, y a la derecha una línea retorcida que pasa por todos los puntos incluido el ruido.](https://missyera.com/static/diagramas/webp/g-ml-sesgo-varianza.webp)

*Los dos extremos fallan y no fallan igual. El de la izquierda es demasiado simple y lo dice desde el primer día; el de la derecha acierta en entrenamiento y falla fuera, que es el que engaña. El del medio no pasa por todos los puntos a propósito.*

## 3. El mismo árbol sin límite: la lección del capítulo

`H = −∑_kp_klog_2p_k`

lo mismo medido en bits, o sea cuántas preguntas de sí o no harían falta para adivinar la clase de una fila

```
libre = arma(DecisionTreeClassifier(random_state=42)).fit(X_tr, y_tr)
print('en train:', round(libre.score(X_tr, y_tr), 4))
print('en test :', round(libre.score(X_te, y_te), 4))
print('AUC     :', round(roc_auc_score(y_te, libre.predict_proba(X_te)[:, 1]), 4))
```

```text
en train: 1.0
en test : 0.5613
AUC     : 0.5525
```

**1,0 en entrenamiento y 0,5613 en examen.** Cien por ciento perfecto en lo que ya vio, y peor que el listón en lo que no 😱

Eso es el sobreajuste, el overfitting, en su forma más pura: el árbol creció hasta que cada hoja tenía una sola venta, o sea que memorizó las 2.250 filas una por una. Un diccionario, no un modelo.

Guárdate el par de números. **1,0 en train es siempre una alarma**, nunca un logro.

## 4. Bosque aleatorio: muchos árboles votando

```
from sklearn.ensemble import RandomForestClassifier

bosque = arma(RandomForestClassifier(n_estimators=300, random_state=42,
                                     n_jobs=-1)).fit(X_tr, y_tr)
print('en train:', round(bosque.score(X_tr, y_tr), 4))
print('AUC     :', round(roc_auc_score(y_te, bosque.predict_proba(X_te)[:, 1]), 4))
```

```text
en train: 1.0
AUC     : 0.677
```

0,677. Mejor que el árbol solo (0,6643) y todavía por debajo de la logística (0,7214).

La idea del bosque es preciosa: entrena 300 árboles, cada uno con una muestra distinta de filas y de columnas, y los hace votar. Los errores de uno los corrigen los otros.

Pero fíjate en el `train`: **1,0 otra vez**. Los árboles siguen memorizando por dentro; lo que pasa es que el promedio de 300 memorizaciones distintas generaliza algo mejor. Y por defecto scikit-learn los deja crecer sin límite 😬

```
podado = arma(RandomForestClassifier(n_estimators=300, max_depth=5,
                                     random_state=42, n_jobs=-1)).fit(X_tr, y_tr)
print('en train:', round(podado.score(X_tr, y_tr), 4))
print('AUC     :', round(roc_auc_score(y_te, podado.predict_proba(X_te)[:, 1]), 4))
```

```text
en train: 0.704
AUC     : 0.7115
```

Limitando la profundidad a 5: el train baja a 0,704 y el AUC sube a **0,7115**. Casi cuatro puntos ganados por poner un freno.

Es de las cosas más rentables que existen y casi nadie la hace, porque `RandomForestClassifier()` a secas ya "funciona" 🌲

## 5 y 6. Boosting: los árboles que se corrigen

`𝔼[(y−f^)^2] = Sesgo^2_⏟_(modelo muy simple) + Varianza_⏟_(modelo muy pegado) + σ^2_⏟_(ruido, no se toca)`

el error se parte en tres, y bajar uno de los dos primeros normalmente sube el otro, mientras el tercero no se puede tocar por más modelo que le eches

```
from sklearn.ensemble import GradientBoostingClassifier, HistGradientBoostingClassifier

boosting = arma(GradientBoostingClassifier(random_state=42)).fit(X_tr, y_tr)
print('boosting     :', round(roc_auc_score(y_te, boosting.predict_proba(X_te)[:, 1]), 4))

rapido = arma(HistGradientBoostingClassifier(random_state=42)).fit(X_tr, y_tr)
print('hist boosting:', round(roc_auc_score(y_te, rapido.predict_proba(X_te)[:, 1]), 4))
```

```text
boosting     : 0.6876
hist boosting: 0.6799
```

0,6876 y 0,6799. Los dos por debajo de la logística.

El boosting es distinto del bosque: en vez de 300 árboles independientes, entrena uno detrás de otro y **cada uno se dedica a los casos que falló el anterior**. Suele ser lo que gana las competencias con datos de tabla, y aquí no gana. Ya llegaremos a por qué.

`HistGradientBoosting` es la versión rápida, la que compite con XGBoost y LightGBM, y además **acepta nulos sin imputar**, que es comodísimo.

![Dos clases separadas por un hiperplano con dos líneas punteadas paralelas a los lados que marcan el margen. Los dos puntos que tocan esas líneas están rodeados: son los vectores de soporte.](https://missyera.com/static/diagramas/webp/g-ml-svm.webp)

*De todas las rectas que separan las dos clases, la SVM elige la que deja más espacio libre a los lados. Los únicos puntos que deciden son los dos rodeados: puedes mover cualquier otro y la frontera no se entera.*

## 7 y 8. Los otros dos que hay que conocer

```
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB

vecinos = arma(KNeighborsClassifier(n_neighbors=25)).fit(X_tr, y_tr)
print('vecinos    :', round(roc_auc_score(y_te, vecinos.predict_proba(X_te)[:, 1]), 4))

bayes = arma(GaussianNB()).fit(X_tr, y_tr)
print('naive bayes:', round(roc_auc_score(y_te, bayes.predict_proba(X_te)[:, 1]), 4))
```

```text
vecinos    : 0.6766
naive bayes: 0.7145
```

**Vecinos** no aprende nada: guarda todas las filas y, cuando le preguntas, busca las 25 más parecidas y vota. Es lentísimo con datos grandes y necesita sí o sí que las columnas estén escaladas, cosa que nuestro pipeline ya hace.

**Naive Bayes** saca 0,7145, que es el segundo mejor de los ocho. Y es el más simple de todos: asume que las columnas son independientes entre sí, cosa que es falsa siempre, y aun así funciona. Se llama "naive" (ingenuo) por eso 🤓

## La tabla completa

| Modelo | AUC | Train | Para qué sirve |
|---|---|---|---|
| **Regresión logística** | **0,7214** | 0,6658 | Casi siempre el primero. Se explica y no se rompe |
| Naive Bayes | 0,7145 | 0,6480 | Rapidísimo, buen listón, muy usado en texto |
| Bosque podado | 0,7115 | 0,7040 | Cuando hay interacciones y no linealidad |
| Boosting | 0,6876 | 0,7493 | Suele ganar con más datos y con ajuste |
| SVM | 0,6826 | 0,7320 | Pocos datos y muchas columnas |
| Hist boosting | 0,6799 | 0,9387 | Como el boosting pero rápido y aguanta nulos |
| Bosque sin podar | 0,6770 | 1,0000 | Lo de arriba, mal configurado |
| Vecinos | 0,6766 | 0,6787 | Didáctico. En producción casi nunca |
| Árbol d=4 | 0,6643 | 0,6658 | Cuando hay que dibujarlo en una reunión |
| Árbol sin límite | 0,5525 | 1,0000 | Nada. Es el ejemplo de qué no hacer |

Mira la columna `train` de arriba abajo: los que están arriba tienen train parecido al test, y los que están abajo lo tienen altísimo. **Esa columna predice el orden mejor que el nombre del algoritmo** 📐

## ¿Y si los juntamos a todos?

Es la pregunta que sale sola mirando la tabla, y tiene nombre: **apilar modelos**, o *stacking*. La idea es bonita. Entrenas varios modelos distintos, y encima pones uno chiquito cuyo único trabajo es aprender **cuándo hacerle caso a cada uno**.

No es lo mismo que el bosque aleatorio, ojo. Ahí eran muchos árboles iguales votando. Acá son modelos de familias distintas, que se equivocan en cosas distintas, y alguien arriba decidiendo.

```
from sklearn.ensemble import StackingClassifier

base = [('log', arma(LogisticRegression(max_iter=1000))),
        ('bosque', arma(RandomForestClassifier(n_estimators=300, random_state=42))),
        ('boost', arma(GradientBoostingClassifier(random_state=42)))]

apilado = StackingClassifier(estimators=base, final_estimator=LogisticRegression(), cv=5)
apilado.fit(X_tr, y_tr)
print('apilado :', round(roc_auc_score(y_te, apilado.predict_proba(X_te)[:, 1]), 4))

for nombre, modelo in base:
    modelo.fit(X_tr, y_tr)
    print(f'{nombre:8}:', round(roc_auc_score(y_te, modelo.predict_proba(X_te)[:, 1]), 4))
```

```text
apilado : 0.7206
log     : 0.7214
bosque  : 0.677
boost   : 0.6876
```

Tres modelos, uno arriba coordinando, validación cruzada por dentro, bastante más tiempo de entrenamiento. Y saca **0,7206 contra los 0,7214 de la regresión logística sola**.

Pierde. Por poquito, pero pierde, y encima cuesta varias veces más de entrenar y es muchísimo más difícil de explicar en una reunión.

Y tiene su explicación, que es la misma de todo el capítulo. Apilar gana cuando los modelos de abajo son parecidos de buenos y se equivocan en sitios distintos. Acá uno saca 0,72 y los otros dos 0,68, así que el de arriba no tiene gran cosa que combinar: lo mejor que puede hacer es copiar al primero, y le sale un pelín peor por el ruido de intentarlo 📊

Guarda la técnica igual. En competencias de datos apilar es lo que gana, y en esos datos sí se cumple la condición. Lo que no hay que hacer es traerla acá por defecto: **si la complicación no se paga en el número, no se paga**.

## Por qué gana la simple, que es lo que hay que entender

Los modelos complicados ganan cuando hay algo complicado que aprender. Aquí no lo hay, y por tres razones concretas:

- 📉 **Poca señal.** El mejor AUC del archivo es 0,72. Cuando la señal es débil, lo que un modelo flexible encuentra de más suele ser ruido.
- 📊 **Pocas filas.** 2.250 para entrenar es poco. El boosting y los bosques empiezan a brillar con decenas de miles.
- ➕ **Relaciones simples.** Del capítulo 4: más monto, más cierre; más satisfacción, más cierre. Todo monótono, que es justo lo que una recta hace bien.

Y la regla que yo sigo: **empieza siempre por la logística, y solo cambia si otro modelo le gana de verdad**. Con "de verdad" quiero decir en validación cruzada, que es el capítulo 16, y no en una sola partición 💛

## Ejercicios

Siete. Intenta antes de abrir 💛

**1. El SVM, sin la trampa de las probabilidades**

Entrena una máquina de vectores de soporte y saca su AUC sin pedirle probabilidades.

```
from sklearn.svm import SVC

svm = arma(SVC(random_state=42)).fit(X_tr, y_tr)
print('exactitud:', round(svm.score(X_te, y_te), 4))
print('AUC      :', round(roc_auc_score(y_te, svm.decision_function(X_te)), 4))
```

```text
exactitud: 0.644
AUC      : 0.6826
```

Fíjate en `decision_function` en vez de `predict_proba`: el SVM no da probabilidades de forma natural, da una distancia a la frontera. Para el AUC da igual, porque el AUC solo mira el **orden** 📐

Pedirle probabilidades con `probability=True` lo hace muchísimo más lento y además está en camino de desaparecer de scikit-learn.

**2. Cuántos vecinos**

Prueba k = 1, 5, 25 y 100 y mira el patrón.

```
for k in (1, 5, 25, 100):
    m = arma(KNeighborsClassifier(n_neighbors=k)).fit(X_tr, y_tr)
    print(f'k={k:3}  train {m.score(X_tr, y_tr):.4f}  '
          f'AUC {roc_auc_score(y_te, m.predict_proba(X_te)[:, 1]):.4f}')
```

```text
k=  1  train 1.0000  AUC 0.5658
k=  5  train 0.7289  AUC 0.6201
k= 25  train 0.6787  AUC 0.6766
k=100  train 0.6520  AUC 0.6985
```

Con k=1 el train da 1,0 (cada fila es su propio vecino más cercano) y el AUC se hunde. Al subir k, el train baja y el AUC sube.

Ese `k` es el ejemplo más limpio que existe del equilibrio entre memorizar y generalizar: **es literalmente un dial** 🎚️

**3. Qué columnas usa el bosque**

Las cinco columnas más importantes según el bosque podado.

```
import numpy as np

nombres = podado.named_steps['pre'].get_feature_names_out()
peso = podado.named_steps['mod'].feature_importances_
for i in np.argsort(peso)[::-1][:5]:
    print(f'{nombres[i]:30} {peso[i]:.4f}')
```

```text
num__monto                     0.1872
num__satisfaccion              0.1281
num__precio_unitario           0.1193
cat__segmento_Bodega           0.1046
num__sin_compra_previa         0.0894
```

Ojo con esta lista, que engaña: la importancia de un bosque **favorece a las columnas con muchos valores distintos**, porque ofrecen más sitios donde cortar.

Aquí se ve: el monto se lleva 0,1872 y `sin_compra_previa`, que en el capítulo 4 medimos en veinte puntos de tasa de cierre, se queda en 0,0894, la mitad. No es que valga la mitad: es que es una columna de dos valores y solo ofrece un sitio donde cortar.

La forma honesta de medir importancia es otra y está en el capítulo 20 🔍

**4. El árbol, dibujado en palabras**

Enseña las primeras reglas del árbol de profundidad 3.

```
from sklearn.tree import export_text

corto = arma(DecisionTreeClassifier(max_depth=3, random_state=42)).fit(X_tr, y_tr)
nombres3 = corto.named_steps['pre'].get_feature_names_out()
print(export_text(corto.named_steps['mod'], feature_names=list(nombres3))[:700])
```

```text
|--- num__monto <= -0.64
|   |--- num__satisfaccion <= 1.09
|   |   |--- num__sin_compra_previa <= 0.83
|   |   |   |--- class: 0
|   |   |--- num__sin_compra_previa >  0.83
|   |   |   |--- class: 0
|   |--- num__satisfaccion >  1.09
|   |   |--- num__precio_unitario <= -0.46
|   |   |   |--- class: 0
|   |   |--- num__precio_unitario >  -0.46
|   |   |   |--- class: 1
|--- num__monto >  -0.64
|   |--- num__sin_compra_previa <= 0.83
|   |   |--- num__satisfaccion <= 0.36
|   |   |   |--- class: 1
|   |   |--- num__satisfaccion >  0.36
|   |   |   |--- class: 1
|   |--- num__sin_compra_previa >  0.83
|   |   |--- num__visita_numero <= -0.43
|   |   |   |--- class: 1
|   |   |--- num__visita_
```

Eso se puede pegar en un correo y lo entiende cualquiera. Es la razón por la que los árboles sobreviven: **en algunos sitios, poder explicar la regla vale más que acertar más**.

Un banco que rechaza un crédito tiene que decir por qué. Con esto puede; con un bosque de 300 árboles, no 🏦

**5. El boosting, cuánto le importa la tasa de aprendizaje**

Prueba tres tasas y mira el sobreajuste.

```
for tasa in (0.01, 0.1, 0.5):
    m = arma(GradientBoostingClassifier(learning_rate=tasa,
                                        random_state=42)).fit(X_tr, y_tr)
    print(f'tasa {tasa:<5} train {m.score(X_tr, y_tr):.4f}  '
          f'AUC {roc_auc_score(y_te, m.predict_proba(X_te)[:, 1]):.4f}')
```

```text
tasa 0.01  train 0.6796  AUC 0.6977
tasa 0.1   train 0.7493  AUC 0.6876
tasa 0.5   train 0.9044  AUC 0.6515
```

Con 0,01 aprende despacito y generaliza mejor; con 0,5 se lanza y memoriza. Esa es la perilla más importante del boosting, y la regla es **tasa baja y muchos árboles**.

Con 0,01 este boosting ya se acerca a la logística. Con más datos la pasaría 📈

**6. El error de pedirle probabilidades a quien no las tiene**

Pídele `predict_proba` a un SVM normal.

```
svm.predict_proba(X_te)
```

```
AttributeError: This 'Pipeline' has no attribute 'predict_proba'
```

No lo tiene. Y es un error honesto: en vez de inventarse un número, el objeto te dice que esa operación no existe para él.

Es un buen recordatorio de que **no todos los modelos dan lo mismo**. Si tu código pide `predict_proba` a ciegas, el día que alguien cambie el clasificador se rompe 🔌

**7. Los ocho de una pasada**

Un bucle que los entrena todos y los ordena por AUC. Es el que yo corro al empezar cualquier proyecto.

```
candidatos = {
    'logistica': LogisticRegression(max_iter=1000, random_state=42),
    'naive bayes': GaussianNB(),
    'bosque podado': RandomForestClassifier(n_estimators=300, max_depth=5,
                                            random_state=42, n_jobs=-1),
    'boosting': GradientBoostingClassifier(random_state=42),
    'vecinos': KNeighborsClassifier(n_neighbors=25),
    'arbol d=4': DecisionTreeClassifier(max_depth=4, random_state=42),
}

resultados = []
for nombre, clf in candidatos.items():
    m = arma(clf).fit(X_tr, y_tr)
    resultados.append((roc_auc_score(y_te, m.predict_proba(X_te)[:, 1]),
                       m.score(X_tr, y_tr), nombre))

for auc, train, nombre in sorted(resultados, reverse=True):
    print(f'{nombre:15} AUC {auc:.4f}   train {train:.4f}')
```

```text
logistica       AUC 0.7214   train 0.6658
naive bayes     AUC 0.7145   train 0.6480
bosque podado   AUC 0.7115   train 0.7040
boosting        AUC 0.6876   train 0.7493
vecinos         AUC 0.6766   train 0.6787
arbol d=4       AUC 0.6643   train 0.6658
```

Ese bucle son diez líneas y contesta en cinco segundos la pregunta que más tiempo se pierde discutiendo: *¿qué modelo usamos?*

Y ojo con lo que **no** hace: no ajusta hiperparámetros ni valida en varias particiones. Sirve para descartar rápido, no para decidir. Decidir es el capítulo 16 🎯

Comprueba que lo tienes

La regresión logística le gana a un bosque aleatorio en estos datos. ¿Qué significa?

- Que la relación es sencilla y no hace falta un modelo complicado
- Que el bosque está mal configurado
- Que hay pocos datos
- Que hay que usar redes neuronales

## El examen que dejó de serlo

La trampa

Entrenas los ocho, los ordenas por su nota en el examen y te quedas con el mejor. Es exactamente lo que hace la tabla de este capítulo.

```
for nombre, mod in modelos.items():
    mod.fit(X_tr, y_tr)
    auc[nombre] = evalúa(mod, X_te, y_te)

ganador = max(auc, key=auc.get)
```

**Qué está mal**

Acabas de usar el examen ocho veces para tomar una decisión, así que el examen dejó de ser un examen 📄 El AUC del ganador ya no es lo que va a rendir con datos nuevos: es lo que rinde el mejor de ocho intentos sobre esas filas concretas, y parte de esa ventaja es suerte.

Cuantos más modelos comparas, más se infla. Con ocho es poco y con cincuenta combinaciones de hiperparámetros ya es mucho, que es justo lo que hace una búsqueda automática.

Lo correcto son **tres trozos y no dos**: uno para entrenar, otro para elegir entre modelos y un tercero que no se toca hasta el final y se usa una sola vez. Cuando no dan los datos para tres, se elige con validación cruzada sobre el entrenamiento y el examen se reserva para el número que vas a reportar. Eso es el capítulo [[cap:validación-cruzada]].

## Lo que te llevas

- 🥇 Aquí gana la regresión logística con 0,7214, por delante de bosques y boosting. Empieza siempre por ella.
- 🚨 Un árbol sin límite da 1,0 en train y 0,5525 en test. 1,0 en train es una alarma, nunca un logro.
- 🌲 El bosque por defecto crece sin límite: limitándolo a profundidad 5, el AUC sube de 0,677 a 0,7115.
- 📈 El boosting entrena árboles que corrigen al anterior. Brilla con muchos datos; con 2.250 filas, no.
- 🎚️ El `k` de los vecinos y la tasa del boosting son diales de memorizar contra generalizar.
- 📐 La columna `train` ordena los modelos mejor que su nombre.
- 🔍 La importancia de columnas de un bosque favorece a las que tienen muchos valores. No es de fiar.

Y si de todo el capítulo te llevas una sola frase, que sea esta:

El modelo complicado no inventa señal donde no la hay. Solo añade formas de equivocarse.

Y si quieres ver estos mismos modelos con redes neuronales al lado, y cuándo ganan y cuándo no, eso es el [libro de deep learning desde cero](https://missyera.com/guias/deep-learning-desde-cero/) 🧠

En el capítulo 14 dejamos de mirar un solo número: matriz de confusión, precisión, exhaustividad y por qué un 95% de exactitud puede ser una vergüenza.

Que tengas lindo día! 🌸

## Preguntas frecuentes

### ¿Qué es el algoritmo KNN?

Vecinos más cercanos: para predecir algo mira los casos más parecidos que ya conoce y copia lo que pasó con ellos. No entrena nada, se guarda los datos y compara.

### ¿Qué es un árbol de decisión?

Una cadena de preguntas de sí o no que va partiendo los datos. Es el modelo más fácil de explicar a alguien que no es técnico, y por eso vale mucho más de lo que su precisión sugiere.

### ¿Cuál es la diferencia entre clasificación y regresión?

La clasificación predice una etiqueta, como compra o no compra. La regresión predice un número, como cuánto va a comprar.

### ¿Qué modelo elijo para empezar?

El más simple que resuelva tu caso, y compararlo siempre contra una línea base tonta. Si un modelo con árboles no le gana a "predice siempre el promedio", el problema no era el modelo.

## Seguir leyendo

- Anterior: [La fuga de información](https://missyera.com/guias/machine-learning-desde-cero/fuga-de-informacion.md)
- Siguiente: [Las métricas](https://missyera.com/guias/machine-learning-desde-cero/metricas.md)

## Contacto

- Diagnóstico de 30 minutos con Miss Yera: https://calendly.com/soymissyera/reunion-con-yera
- Correo: hola@missyera.com
- Esta página en HTML: https://missyera.com/guias/machine-learning-desde-cero/los-modelos/
- Índice de todas las páginas en texto: https://missyera.com/llms-index.txt
