Módulo 3 · Aprendizaje supervisado
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.
| columna | unidad | contenido |
|---|---|---|
distancia_km | km | distancia del almacén al domicilio |
peso_kg | kg | peso del paquete |
hora | h | hora de salida del almacén, de 8 a 21 |
carga_pct | % | ocupación del almacén al salir |
lluvia_mm | mm | lluvia 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ódigo
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)
print(pedidos.head(4))
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.
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))
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.
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.
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))
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.
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.
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?
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}"
)
La salida esperada está oculta.
Los nombres de los hiperparámetros cambian entre librerías:
| concepto | HistGradientBoostingClassifier | XGBClassifier | LGBMClassifier |
|---|---|---|---|
| tasa de aprendizaje | learning_rate | learning_rate | learning_rate |
| número máximo de rondas | max_iter | n_estimators | n_estimators |
| profundidad máxima | max_depth | max_depth | max_depth |
| hojas máximas por árbol | max_leaf_nodes | max_leaves | num_leaves |
| mínimo por hoja | min_samples_leaf (filas) | min_child_weight (peso mínimo; con error cuadrático, filas) | min_child_samples (filas) |
| fracción de filas por árbol | no existe | subsample | subsample y subsample_freq |
| fracción de columnas | max_features (en cada división) | colsample_bytree | colsample_bytree |
| regularización L2 de las hojas | l2_regularization | reg_lambda | reg_lambda |
| regularización L1 de las hojas | no existe | reg_alpha | reg_alpha |
| parada temprana | early_stopping=True, n_iter_no_change, X_val y y_val en fit | early_stopping_rounds, eval_set | lightgbm.early_stopping, eval_X y eval_y |
| categóricas nativas | dtype category (from_dtype) | dtype category y enable_categorical | dtype 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.
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 como | rondas | pérdida de validación | ROC-AUC de validación |
|---|---|---|---|
| códigos enteros | 139 | 0.498 | 0.837 |
| categórica nativa | 113 | 0.476 | 0.852 |
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_leaveslimita las hojas. LightGBM crece cada árbol por la hoja que más reduce la pérdida, así quenum_leaveses su control principal. Sumax_depthpor defecto, -1, no limita la profundidad.min_child_samplesexige un mínimo de filas en cada hoja.subsampleentrena cada árbol con una fracción de las filas ycolsample_bytreecon una fracción de las columnas.reg_lambdapenaliza los valores grandes en las hojas, como la regularización L2 de los coeficientes enRidge.
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.
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?
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}")
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.
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?
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))
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.
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?
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}")
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.
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.
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?
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}")
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?
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))
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.
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?
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}"
)
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.
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:
- el número de aciertos del baseline en prueba, que debe ser 72 de 114
HistGradientBoostingClassifier,XGBClassifieryLGBMClassifiercon tasa 0.1, profundidad 3 y parada temprana sobre validación, con sus rondas usadas- un
StackingClassifiercon regresión logística escalada, K vecinos escalado y el mejor boosting con sus rondas usadas fijas y sin parada temprana - la tabla de aciertos, ROC-AUC y pérdida logarítmica en prueba
- 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
subsampleno cambia nada en LightGBM, faltasubsample_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
categoryy 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_predictoStackingClassifierconcv.
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_nodesymin_samples_leaf? - ¿Por qué una categórica nativa necesita menos cortes que la misma columna en códigos enteros?
- ¿Qué limita
num_leavesen 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
- Ensembles en scikit-learn:
VotingClassifier,StackingClassifiery las categóricas nativas deHistGradientBoostingClassifier. - Parámetros de XGBoost:
eta(learning_rate),max_depth,min_child_weight,subsample,colsample_bytreey regularización. - Ajuste de parámetros de LightGBM:
num_leaves,min_data_in_leaf(min_child_samples), submuestreo y control del sobreajuste. - Categóricas en CatBoost: estadísticas ordenadas de la etiqueta para variables categóricas.