Módulo 3 · Aprendizaje supervisado

Lección 8 de 9

Librerías y ensembles

En esta páginaEl problema

El problema

Los pedidos son los de «Bagging y boosting»: 3000 pedidos sintéticos de una tienda en línea, con cinco columnas numéricas, una zona de entrega y la etiqueta tarde, que vale 1 si el pedido llegó después de la hora prometida.

columnaunidadcontenido
distancia_kmkmdistancia del almacén al domicilio
peso_kgkgpeso del paquete
horahhora de salida del almacén, de 8 a 21
carga_pct%ocupación del almacén al salir
lluvia_mmmmlluvia registrada durante la entrega
zona—una de 25 zonas, de Z01 a Z25
tarde—1 si llegó tarde y 0 si llegó a tiempo

La celda plegada genera la misma tabla, con la semilla 0.

Mostrar códigoOcultar códigoPython
Python 3 · ejemplo reproducible
import numpy as np
import pandas as pd

rng = np.random.default_rng(0)
n = 3000
zonas = [f"Z{i:02d}" for i in range(1, 26)]
pedidos = pd.DataFrame(
    {
        "distancia_km": np.round(rng.gamma(2.0, 4.0, n), 1),
        "peso_kg": np.round(rng.gamma(1.5, 2.0, n), 1),
        "hora": rng.integers(8, 22, n),
        "carga_pct": rng.integers(20, 101, n),
        "lluvia_mm": np.round(
            # 70 % de los pedidos sin lluvia
            np.where(rng.random(n) < 0.7, 0, rng.exponential(6, n)), 1
        ),
        "zona": pd.Categorical(rng.choice(zonas, n), categories=zonas),
    }
)
efecto_zona = rng.normal(0, 1.2, len(zonas))
# log-momios del retraso: distancia, horas pico, almacén lleno, lluvia × distancia y zona
logit = (
    -2.5
    + 0.12 * pedidos["distancia_km"]
    + 2.0 * pedidos["hora"].isin([13, 14, 18, 19])
    + 2.5 * (pedidos["carga_pct"] > 80)
    + 0.15 * pedidos["distancia_km"] * (pedidos["lluvia_mm"] > 3)
    + efecto_zona[pedidos["zona"].cat.codes]
)
pedidos["tarde"] = (rng.random(n) < 1 / (1 + np.exp(-logit))).astype(int)
Python 3 · ejemplo reproducible
print(pedidos.head(4))
Salida esperada

La salida esperada está oculta.

Cada fila es un pedido. zona tiene dtype category y tarde vale 0 o 1.

train_test_split aparta primero 600 pedidos de prueba y después 600 de validación. stratify mantiene en cada parte la misma proporción de pedidos tarde. La prueba queda apartada hasta la última comparación.

Python 3 · ejemplo reproducible
from sklearn.model_selection import train_test_split

X = pedidos.drop(columns="tarde")
y = pedidos["tarde"]
X_resto, X_prueba, y_resto, y_prueba = train_test_split(
    X, y, test_size=600, stratify=y, random_state=0
)
X_ent, X_val, y_ent, y_val = train_test_split(
    X_resto, y_resto, test_size=600, stratify=y_resto, random_state=0
)
print(len(X_ent), len(X_val), len(X_prueba), round(y.mean(), 3))
Salida esperada

La salida esperada está oculta.

Las dos celdas siguientes reconstruyen los modelos de «Bagging y boosting» que esta lección usa. La regresión logística recibe zona en one-hot y las columnas numéricas escaladas: make_column_transformer aplica OneHotEncoder a zona y StandardScaler a las numéricas. La función perdida calcula la pérdida logarítmica con la probabilidad de la clase 1.

Python 3 · ejemplo reproducible
from sklearn.compose import make_column_transformer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import log_loss
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler

numericas = ["distancia_km", "peso_kg", "hora", "carga_pct", "lluvia_mm"]
preproceso = make_column_transformer(
    (OneHotEncoder(), ["zona"]), (StandardScaler(), numericas)
)
logistica = make_pipeline(preproceso, LogisticRegression(max_iter=1000))


def perdida(modelo, X_, y_):
    return log_loss(y_, modelo.predict_proba(X_)[:, 1])

HistGradientBoostingClassifier usa tasa 0.1, profundidad 3, tope de 1000 rondas y parada temprana de paciencia 20 sobre X_val. validation_score_ guarda el puntaje de validación después de cada ronda, con signo tal que más alto es mejor. mejor_ronda toma la posición del máximo: la ronda con menor pérdida de validación.

Python 3 · ejemplo reproducible
from sklearn.ensemble import HistGradientBoostingClassifier

parada = HistGradientBoostingClassifier(
    learning_rate=0.1,
    max_iter=1000,
    max_depth=3,
    early_stopping=True,
    n_iter_no_change=20,
    random_state=0,
).fit(X_ent, y_ent, X_val=X_val, y_val=y_val)
# la posición 0 es el puntaje antes de la primera ronda
mejor_ronda = int(np.argmax(parada.validation_score_))

print("rondas entrenadas:", parada.n_iter_)
print("mejor ronda:", mejor_ronda)
print("pérdida de validación:", round(perdida(parada, X_val, y_val), 3))
Salida esperada

La salida esperada está oculta.

Tres librerías de boosting

XGBClassifier (XGBoost) y LGBMClassifier (LightGBM) implementan el mismo gradient boosting que HistGradientBoostingClassifier, con la interfaz de scikit-learn: fit, predict y predict_proba. Las tres librerías agrupan los valores de cada columna en intervalos antes de buscar cortes. Para compararlas, las celdas fijan la misma partición, la misma semilla, tasa 0.1, profundidad 3, tope de 1000 rondas y paciencia de 20.

Cada librería recibe la validación de otra forma. XGBoost usa early_stopping_rounds en el constructor y eval_set en fit. enable_categorical=True le permite usar la columna zona de dtype category. n_jobs=1 entrena con un solo hilo, así el resultado no depende del número de núcleos.

Python 3 · ejemplo reproducible
from xgboost import XGBClassifier

xgb = XGBClassifier(
    learning_rate=0.1,
    n_estimators=1000,
    max_depth=3,
    early_stopping_rounds=20,
    enable_categorical=True,
    n_jobs=1,
    random_state=0,
).fit(X_ent, y_ent, eval_set=[(X_val, y_val)], verbose=False)

LightGBM usa eval_X y eval_y en fit, y el callback lightgbm.early_stopping para la paciencia. verbose=-1 evita que LightGBM imprima avisos.

Python 3 · ejemplo reproducible
import lightgbm
from lightgbm import LGBMClassifier

paciencia = lightgbm.early_stopping(20, verbose=False)
lgbm = LGBMClassifier(
    learning_rate=0.1,
    n_estimators=1000,
    max_depth=3,
    n_jobs=1,
    random_state=0,
    verbose=-1,
).fit(X_ent, y_ent, eval_X=X_val, eval_y=y_val, callbacks=[paciencia])

Las rondas también se leen de otra forma. get_booster().num_boosted_rounds() da las rondas que entrenó XGBoost. evals_result_ guarda la pérdida de validación de LightGBM en cada ronda, así que su longitud es el número de rondas entrenadas. best_iteration y best_iteration_ indican la ronda con la que cada una predice. roc_auc_score calcula el ROC-AUC con las probabilidades de la clase 1.

Pregunta. ¿Las tres pérdidas de validación diferirán en más de 0.01? ¿Las tres predecirán con el mismo número de rondas?

Python 3 · ejemplo reproducible
from sklearn.metrics import roc_auc_score

rondas_xgb = xgb.get_booster().num_boosted_rounds()
rondas_lgbm = len(lgbm.evals_result_["valid_0"]["binary_logloss"])
filas = [
    ("HistGradientBoosting", parada.n_iter_, parada.n_iter_, parada),
    ("XGBoost", rondas_xgb, xgb.best_iteration + 1, xgb),  # cuenta desde 0
    ("LightGBM", rondas_lgbm, lgbm.best_iteration_, lgbm),
]
print("librería              entrenadas  usadas  pérdida_val  auc_val")
for nombre, entrenadas, usadas, modelo in filas:
    auc = roc_auc_score(y_val, modelo.predict_proba(X_val)[:, 1])
    print(
        f"{nombre:<20}  {entrenadas:10d}  {usadas:6d}  "
        f"{perdida(modelo, X_val, y_val):11.3f}  {auc:7.3f}"
    )
Salida esperada

La salida esperada está oculta.

Los nombres de los hiperparámetros cambian entre librerías:

conceptoHistGradientBoostingClassifierXGBClassifierLGBMClassifier
tasa de aprendizajelearning_ratelearning_ratelearning_rate
número máximo de rondasmax_itern_estimatorsn_estimators
profundidad máximamax_depthmax_depthmax_depth
hojas máximas por árbolmax_leaf_nodesmax_leavesnum_leaves
mínimo por hojamin_samples_leaf (filas)min_child_weight (peso mínimo; con error cuadrático, filas)min_child_samples (filas)
fracción de filas por árbolno existesubsamplesubsample y subsample_freq
fracción de columnasmax_features (en cada división)colsample_bytreecolsample_bytree
regularización L2 de las hojasl2_regularizationreg_lambdareg_lambda
regularización L1 de las hojasno existereg_alphareg_alpha
parada tempranaearly_stopping=True, n_iter_no_change, X_val y y_val en fitearly_stopping_rounds, eval_setlightgbm.early_stopping, eval_X y eval_y
categóricas nativasdtype category (from_dtype)dtype category y enable_categoricaldtype category (automático)

Los valores por defecto también cambian. XGBoost usa tasa 0.3 y profundidad 6. LightGBM y HistGradientBoostingClassifier usan tasa 0.1 y hasta 31 hojas sin límite de profundidad. Comparar las tres con sus valores por defecto compara configuraciones distintas.

La columna zona tiene dtype category. Las tres librerías la tratan como categórica nativa: en cada corte agrupan las zonas en dos conjuntos, sin suponer un orden entre ellas. La alternativa es reemplazar cada zona por su código entero, de 0 a 24, y tratar la columna como numérica.

cat.codes devuelve el código entero de cada zona, y assign crea una copia de la tabla con la columna reemplazada. El modelo codigos repite la configuración de parada sobre esas copias.

Python 3 · ejemplo reproducible
X_ent_codigos = X_ent.assign(zona=X_ent["zona"].cat.codes)
X_val_codigos = X_val.assign(zona=X_val["zona"].cat.codes)
codigos = HistGradientBoostingClassifier(
    learning_rate=0.1,
    max_iter=1000,
    max_depth=3,
    early_stopping=True,
    n_iter_no_change=20,
    random_state=0,
).fit(X_ent_codigos, y_ent, X_val=X_val_codigos, y_val=y_val)

La tabla compara codigos con parada, que ya se entrenó con zona categórica.

Pregunta. La versión nativa ya está en la primera fila de la comparación de librerías: 113 rondas y pérdida de validación 0.476. Con HistGradientBoostingClassifier, la misma parada temprana y códigos enteros, ¿la pérdida será mayor o menor? ¿Necesitará más o menos rondas?

Ver resultado

Cada fila es una versión del mismo HistGradientBoostingClassifier con parada temprana. La primera columna dice cómo entra zona, «rondas» es el número de rondas que entrenó antes de parar y las dos últimas columnas se miden en los 600 pedidos de validación. Por ejemplo, la fila de códigos enteros dice que ese modelo entrenó 139 rondas y terminó con pérdida 0.498 y ROC-AUC 0.837.

zona entra comorondaspérdida de validaciónROC-AUC de validación
códigos enteros1390.4980.837
categórica nativa1130.4760.852
Lectura. La versión nativa tiene menor pérdida y mejor ROC-AUC, con menos rondas. Con códigos, un corte solo separa «código ≤ k» de «código > k», y el orden de los códigos no tiene relación con el retraso. El modelo necesita varios cortes para aislar las zonas con más retrasos, y cada corte consume filas y rondas. Con 25 zonas la diferencia es visible. Con dos categorías, un corte sobre los códigos ya separa una de otra, y las dos versiones pueden hacer los mismos cortes.

CatBoost es otra librería de boosting muy usada con datos tabulares. Convierte cada categoría en una estadística de la etiqueta calculada solo con las filas que la preceden en una permutación aleatoria, de modo que una fila nunca usa su propia etiqueta. También construye árboles simétricos, con la misma pregunta en todos los nodos de un nivel.

Complejidad y regularización

La tasa y la parada temprana controlan cuántas correcciones se suman. Otros hiperparámetros controlan cuánto puede aprender cada árbol:

  • num_leaves limita las hojas. LightGBM crece cada árbol por la hoja que más reduce la pérdida, así que num_leaves es su control principal. Su max_depth por defecto, -1, no limita la profundidad.
  • min_child_samples exige un mínimo de filas en cada hoja.
  • subsample entrena cada árbol con una fracción de las filas y colsample_bytree con una fracción de las columnas.
  • reg_lambda penaliza los valores grandes en las hojas, como la regularización L2 de los coeficientes en Ridge.

El diccionario configuraciones asocia cada nombre con los argumentos que cambian. Las configuraciones «31 + …» parten de num_leaves=31, el valor por defecto de LightGBM.

Python 3 · ejemplo reproducible
configuraciones = {
    "num_leaves=4": {"num_leaves": 4},
    "num_leaves=31": {"num_leaves": 31},
    "num_leaves=127": {"num_leaves": 127},
    "31 + min_child_samples=100": {"min_child_samples": 100},
    "31 + subsample=0.7": {"subsample": 0.7, "subsample_freq": 1},
    "31 + colsample_bytree=0.5": {"colsample_bytree": 0.5},
    "31 + reg_lambda=30": {"reg_lambda": 30},
}

La celda fija 200 rondas con tasa 0.1, sin parada temprana, para que solo cambie la complejidad. **extra pasa los argumentos de cada configuración a LGBMClassifier como argumentos con nombre.

Pregunta. ¿Qué configuración tendrá la mayor brecha entre la pérdida de entrenamiento y la de validación? ¿Cuál de las restricciones aplicadas a num_leaves=31 bajará más la pérdida de validación?

Python 3 · ejemplo reproducible
print("configuración               pérdida_ent  pérdida_val  brecha")
for nombre, extra in configuraciones.items():
    modelo = LGBMClassifier(
        learning_rate=0.1,
        n_estimators=200,
        n_jobs=1,
        random_state=0,
        verbose=-1,
        **extra,
    ).fit(X_ent, y_ent)
    ent, val = perdida(modelo, X_ent, y_ent), perdida(modelo, X_val, y_val)
    print(f"{nombre:<26}  {ent:11.3f}  {val:11.3f}  {val - ent:6.3f}")
Salida esperada

La salida esperada está oculta.

La configuración 31 + subsample=0.7 también fijó subsample_freq=1. En LightGBM, subsample_freq indica cada cuántas rondas se vuelve a sortear el subconjunto de filas.

base reúne los argumentos comunes, y p_base guarda las probabilidades de validación del modelo sin subsample.

Python 3 · ejemplo reproducible
base = {
    "learning_rate": 0.1,
    "n_estimators": 200,
    "n_jobs": 1,
    "random_state": 0,
    "verbose": -1,
}
p_base = LGBMClassifier(**base).fit(X_ent, y_ent).predict_proba(X_val)

np.allclose compara dos arreglos elemento por elemento y devuelve True si coinciden salvo diferencias de redondeo.

Pregunta. ¿subsample=0.5 sin subsample_freq produce las mismas probabilidades que el modelo sin subsample? ¿Y con subsample_freq=1?

Python 3 · ejemplo reproducible
p_sin_freq = (
    LGBMClassifier(**base, subsample=0.5).fit(X_ent, y_ent).predict_proba(X_val)
)
p_con_freq = (
    LGBMClassifier(**base, subsample=0.5, subsample_freq=1)
    .fit(X_ent, y_ent)
    .predict_proba(X_val)
)

print("sin subsample_freq, igual al base:", np.allclose(p_base, p_sin_freq))
print("con subsample_freq=1, igual al base:", np.allclose(p_base, p_con_freq))
Salida esperada

La salida esperada está oculta.

Ensembles de modelos distintos

El boosting combina árboles de la misma familia. Otra opción es combinar modelos de familias distintas. Si cometen errores diferentes, la combinación puede corregir parte de ellos.

Aquí se combinan la regresión logística, K vecinos con k=25 y el boosting con las rondas que eligió la parada temprana. clone(preproceso) crea una copia sin entrenar del preprocesamiento, para que la tubería de vecinos no comparta el suyo con la logística. El boosting fija max_iter=mejor_ronda y apaga la parada temprana.

Python 3 · ejemplo reproducible
from sklearn.base import clone
from sklearn.neighbors import KNeighborsClassifier

vecinos = make_pipeline(clone(preproceso), KNeighborsClassifier(n_neighbors=25))
boosting = HistGradientBoostingClassifier(
    learning_rate=0.1,
    max_iter=mejor_ronda,
    max_depth=3,
    early_stopping=False,
    random_state=0,
)
bases = [("logística", logistica), ("vecinos", vecinos), ("boosting", boosting)]

VotingClassifier recibe la lista bases de pares nombre y modelo, entrena cada modelo y combina sus predicciones. Con voting="hard" cada modelo vota una clase y gana la mayoría. Con voting="soft" promedia las probabilidades y elige la clase con el promedio más alto. La comparación usa validación cruzada sobre X_ent: StratifiedKFold reparte las filas en 5 folds con la misma proporción de retrasos, y cross_val_score devuelve la exactitud en cada fold.

Pregunta. ¿El voto suave superará al boosting solo? ¿El voto duro quedará por encima o por debajo del voto suave?

Python 3 · ejemplo reproducible
from sklearn.ensemble import VotingClassifier
from sklearn.model_selection import StratifiedKFold, cross_val_score

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=0)
candidatos = dict(bases)
candidatos["voto duro"] = VotingClassifier(bases, voting="hard")
candidatos["voto suave"] = VotingClassifier(bases, voting="soft")
for nombre, modelo in candidatos.items():
    exactitud = cross_val_score(modelo, X_ent, y_ent, cv=cv)
    print(f"{nombre:<10}  {exactitud.mean():.3f} ± {exactitud.std():.3f}")
Salida esperada

La salida esperada está oculta.

El stacking reemplaza el promedio fijo por un meta-modelo que aprende cuánto confiar en cada modelo base. Sus entradas son las probabilidades de los modelos base y su etiqueta es y. La pregunta es con qué probabilidades se entrena.

Una opción es entrenar los modelos base con X_ent y usar sus probabilidades sobre esas mismas filas: predicciones dentro de la muestra. La comparación usa tres modelos base; uno de ellos es K vecinos con k=1. np.column_stack pone las probabilidades de cada modelo en una columna, así que dentro tiene una fila por pedido y una columna por modelo.

Python 3 · ejemplo reproducible
memoria = make_pipeline(clone(preproceso), KNeighborsClassifier(n_neighbors=1))
trio = [("logística", logistica), ("1 vecino", memoria), ("boosting", boosting)]

dentro = np.column_stack(
    [m.fit(X_ent, y_ent).predict_proba(X_ent)[:, 1] for _, m in trio]
)

Otra es cross_val_predict, que da predicciones fuera de fold: cada fila se predice con un modelo que no la vio. method="predict_proba" le pide probabilidades en lugar de clases. en_val guarda las probabilidades sobre X_val de los modelos entrenados con todo X_ent, que son las entradas del meta-modelo en validación.

Python 3 · ejemplo reproducible
from sklearn.model_selection import cross_val_predict

fuera = np.column_stack(
    [
        cross_val_predict(m, X_ent, y_ent, cv=cv, method="predict_proba")[:, 1]
        for _, m in trio
    ]
)
en_val = np.column_stack(
    [m.fit(X_ent, y_ent).predict_proba(X_val)[:, 1] for _, m in trio]
)

La celda siguiente mide la exactitud de cada modelo base con umbral 0.5 en las dos versiones. Después entrena un meta-modelo LogisticRegression con cada versión. meta.coef_[0] da un peso por modelo base, en el orden de trio.

Pregunta. ¿Qué peso dará el meta-modelo al vecino más cercano en cada versión? ¿Qué versión tendrá menor pérdida en validación?

Python 3 · ejemplo reproducible
print("exactitud en entrenamiento, dentro y fuera de fold")
for i, (nombre, _) in enumerate(trio):
    acierto_dentro = ((dentro[:, i] > 0.5) == y_ent).mean()
    acierto_fuera = ((fuera[:, i] > 0.5) == y_ent).mean()
    print(f"  {nombre:<10} {acierto_dentro:.3f}  {acierto_fuera:.3f}")
for nombre, entradas in [("dentro", dentro), ("fuera", fuera)]:
    meta = LogisticRegression().fit(entradas, y_ent)
    p_val = meta.predict_proba(en_val)[:, 1]
    pesos = meta.coef_[0].round(2)
    print(f"meta {nombre:<6} pesos {pesos}  pérdida_val {log_loss(y_val, p_val):.3f}")
Salida esperada

La salida esperada está oculta.

StackingClassifier aplica este procedimiento: obtiene las predicciones fuera de fold con cv, entrena el meta-modelo (final_estimator) con ellas y reentrena cada modelo base con todas las filas. Después de fit, final_estimator_ es el meta-modelo entrenado. El blending es una versión más simple: entrena los modelos base con X_ent y el meta-modelo con sus predicciones sobre un holdout, aquí X_val. Es más rápido, pero el meta-modelo solo aprende de 600 filas y esas filas no entrenan los modelos base.

Pregunta. ¿El peso de K vecinos con k=25 será mayor o menor que el 0.1 que recibió el vecino más cercano fuera de fold? ¿Qué signo tendrá ese peso en el stacking y en el blending?

Python 3 · ejemplo reproducible
from sklearn.ensemble import StackingClassifier

stacking = StackingClassifier(bases, final_estimator=LogisticRegression(), cv=cv)
stacking.fit(X_ent, y_ent)
print("stacking pesos:", stacking.final_estimator_.coef_[0].round(2))

for _, modelo in bases:
    modelo.fit(X_ent, y_ent)
p_bases_val = np.column_stack([m.predict_proba(X_val)[:, 1] for _, m in bases])
meta_blending = LogisticRegression().fit(p_bases_val, y_val)
print("blending pesos:", meta_blending.coef_[0].round(2))
Salida esperada

La salida esperada está oculta.

La comparación final usa X_prueba, que ninguna decisión tocó. Un acierto vale 1/600 ≈ 0.0017 de exactitud.

DummyClassifier es el baseline: da a cada pedido la proporción de retrasos del entrenamiento. El diccionario probabilidades guarda la probabilidad de tarde que cada modelo da a los 600 pedidos de prueba. El blending la calcula a partir de las probabilidades de los modelos base.

Python 3 · ejemplo reproducible
from sklearn.dummy import DummyClassifier

voto_suave = VotingClassifier(bases, voting="soft").fit(X_ent, y_ent)
p_bases_prueba = np.column_stack([m.predict_proba(X_prueba)[:, 1] for _, m in bases])
probabilidades = {
    "baseline": DummyClassifier().fit(X_ent, y_ent).predict_proba(X_prueba)[:, 1],
    "logística": logistica.predict_proba(X_prueba)[:, 1],
    "boosting": boosting.predict_proba(X_prueba)[:, 1],
    "voto suave": voto_suave.predict_proba(X_prueba)[:, 1],
    "stacking": stacking.predict_proba(X_prueba)[:, 1],
    "blending": meta_blending.predict_proba(p_bases_prueba)[:, 1],
}

La tabla cuenta los aciertos con umbral 0.5. accuracy_score da la misma cuenta como fracción.

Pregunta. ¿Cuántos pedidos de prueba separan al mejor ensemble del boosting solo? ¿Y al boosting solo de la regresión logística?

Python 3 · ejemplo reproducible
from sklearn.metrics import accuracy_score

print("modelo       aciertos  exactitud  auc    pérdida")
for nombre, p in probabilidades.items():
    aciertos = int(((p > 0.5) == y_prueba).sum())
    print(
        f"{nombre:<11}  {aciertos}/600   {accuracy_score(y_prueba, p > 0.5):.3f}"
        f"      {roc_auc_score(y_prueba, p):.3f}  {log_loss(y_prueba, p):.3f}"
    )
Salida esperada

La salida esperada está oculta.

Práctica

Traza guiada

Tres modelos dan estas probabilidades de tarde para cuatro pedidos. Cada fila es un modelo y cada columna, un pedido.

Python 3 · ejemplo reproducible
p_traza = np.array(
    [
        [0.90, 0.45, 0.55, 0.30],
        [0.40, 0.45, 0.60, 0.70],
        [0.45, 0.90, 0.10, 0.60],
    ]
)

Escribe para cada pedido la clase que vota cada modelo con umbral 0.5, el resultado del voto duro, el promedio de las tres probabilidades y el resultado del voto suave. Resultado esperado: el voto duro da [0, 0, 1, 1], los promedios son [0.583, 0.6, 0.417, 0.533] y el voto suave da [1, 1, 0, 1]. Los dos votos solo coinciden en el último pedido. Comprueba con (p_traza > 0.5).sum(axis=0) >= 2 y p_traza.mean(axis=0) > 0.5. Si un pedido no coincide, revisa si un solo modelo muy seguro movió el promedio al otro lado de 0.5.

Regularización en XGBoost

Repite la celda de complejidad con XGBClassifier(learning_rate=0.1, n_estimators=200, max_depth=6, enable_categorical=True, n_jobs=1, random_state=0), que usa la profundidad por defecto de XGBoost. Cambia una opción a la vez: max_depth=2, min_child_weight=20, subsample=0.7, colsample_bytree=0.5 y reg_lambda=30. Antes de procesar, predice qué opción baja más la pérdida de validación. Resultado esperado: con max_depth=6 la pérdida de validación es 0.618 y la brecha 0.546. Las pérdidas de validación de cada cambio son 0.482 con max_depth=2, 0.501 con min_child_weight=20, 0.507 con reg_lambda=30, 0.534 con colsample_bytree=0.5 y 0.609 con subsample=0.7. max_depth=2 da la menor pérdida de validación y la menor brecha, 0.119. Compara max_depth=2 con num_leaves=4 de LightGBM: un árbol de profundidad 2 tiene a lo más 4 hojas.

Comparación en cáncer de mama

Carga load_breast_cancer(return_X_y=True, as_frame=True). Aparta 20 % como prueba y 25 % del resto como validación, con stratify y random_state=0. Produce:

  1. el número de aciertos del baseline en prueba, que debe ser 72 de 114
  2. HistGradientBoostingClassifier, XGBClassifier y LGBMClassifier con tasa 0.1, profundidad 3 y parada temprana sobre validación, con sus rondas usadas
  3. un StackingClassifier con regresión logística escalada, K vecinos escalado y el mejor boosting con sus rondas usadas fijas y sin parada temprana
  4. la tabla de aciertos, ROC-AUC y pérdida logarítmica en prueba
  5. una frase que diga cuántos casos separan al stacking del mejor modelo solo y si esa diferencia permite elegir

Reto opcional

Repite la comparación final de los pedidos con otras cuatro particiones: cambia random_state a 1, 2, 3 y 4 en las dos llamadas a train_test_split, y vuelve a elegir mejor_ronda con la parada temprana de cada partición. Cuenta en cuántas de las cinco particiones el stacking acierta más pedidos de prueba que el boosting solo, y reporta la diferencia media en pedidos con su desviación estándar.

Errores que revisar

  • Si subsample no cambia nada en LightGBM, falta subsample_freq=1.
  • Si comparas librerías con sus valores por defecto, la diferencia mide la configuración: XGBoost usa tasa 0.3 y profundidad 6, y LightGBM usa tasa 0.1 y 31 hojas.
  • Si una columna con muchas categorías entra como códigos enteros, el modelo gasta cortes y rondas en aislar categorías. Usa el dtype category y el soporte nativo de la librería.
  • Si un voto suave queda debajo del mejor modelo solo, compara el rendimiento de cada modelo base: uno claramente peor arrastra el promedio.
  • Si el meta-modelo de un stacking da el peso mayor a un modelo que acierta 100 % en entrenamiento, lo entrenaste con predicciones dentro de la muestra. Usa cross_val_predict o StackingClassifier con cv.

Cierre

Un compañero reporta que su blending de cinco modelos supera al boosting solo por 3 pedidos (0.005 de exactitud). Eligió la ronda de parada del boosting y entrenó el meta-modelo con 600 pedidos, y midió la exactitud en esos mismos 600 pedidos. Escribe qué le falta a esa comparación y qué tabla aceptarías como evidencia.

Lista de comprobación

  • ¿Qué parámetros de XGBoost y de LightGBM corresponden a max_leaf_nodes y min_samples_leaf?
  • ¿Por qué una categórica nativa necesita menos cortes que la misma columna en códigos enteros?
  • ¿Qué limita num_leaves en LightGBM y qué cambia en la brecha entre entrenamiento y validación al subirlo?
  • ¿Por qué el meta-modelo de un stacking se entrena con predicciones fuera de fold?
  • ¿Cuándo una diferencia entre un ensemble y el mejor modelo solo es ruido de la muestra?

Documentación oficial

Cargando progreso…