Validación, GridSearch y buenas prácticas avanzadas en Scikit‑Learn (Parte 3)


Scikit-Learn (Parte 3)

Validación, GridSearch y buenas prácticas avanzadas: cómo evaluar modelos sin engañarte a ti mismo

En las Partes 1 y 2 construimos los cimientos:

  • entendiste Estimators, Transformers y Predictors
  • aprendiste a encadenarlos en un Pipeline real
  • evitaste leakage y mantuviste consistencia entre train y test

Ahora toca la parte que separa un notebook bonito de un sistema de ML honesto y fiable:

la validación y el tuning de modelos.

Porque entrenar un modelo es fácil. Evaluarlo bien es lo difícil. Y casi todo el mundo lo hace mal sin darse cuenta.

Vamos paso a paso.

Diagrama de validación cruzada y GridSearch en un pipeline de Scikit‑Learn


1. El problema: la validación ingenua

Muchos tutoriales hacen esto:

python
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
accuracy_score(y_test, y_pred)

Funciona, sí. Pero tiene problemas serios:

  • depende demasiado de un único split
  • no detecta la variabilidad del modelo
  • no te dice si tu modelo es estable
  • no te dice si tu modelo generaliza
  • y si hiciste preprocesamiento fuera del Pipeline → leakage garantizado

Piénsalo así: un solo split te da un solo número. Y un solo número no tiene contexto. ¿Ese 0.91 de accuracy es real, o te tocó un test set fácil por azar? No tienes forma de saberlo.

La validación correcta no es un paso final. Es parte del diseño del sistema.


2. Cross-Validation: la base de una evaluación honesta

La validación cruzada (CV) divide los datos en varios folds y repite el entrenamiento varias veces, rotando cuál fold se usa para validar.

python
from sklearn.model_selection import cross_val_score

scores = cross_val_score(pipeline, X, y, cv=5)
scores.mean(), scores.std()

¿Por qué es tan importante?

  • reduce la varianza de la evaluación
  • detecta modelos inestables
  • evita depender de un único split
  • evalúa el preprocesamiento dentro del Pipeline
  • evita leakage automáticamente

Y fíjate en un detalle que casi nadie mira:scores.std().

La media te dice cómo de bueno es tu modelo. La desviación estándar te dice cómo de fiable es esa media. Dos modelos conmean = 0.88no son iguales si uno tienestd = 0.01y el otrostd = 0.09. El segundo es una montaña rusa: en producción te puede dar 0.79 cualquier día.

Si no usas CV, no estás evaluando tu modelo. Estás jugando a la lotería.


3. StratifiedKFold y la elección del scoring

Dos detalles que marcan la diferencia entre una validación de juguete y una seria.

Estratificación. En clasificación, si tus clases están desbalanceadas (90% A, 10% B), un KFold normal puede dejarte un fold casi sin ejemplos de la clase B. Scikit-Learn lo sabe y, para clasificadores,cross_val_scoreya usaStratifiedKFoldpor defecto. Pero conviene hacerlo explícito cuando controlas el split:

python
from sklearn.model_selection import StratifiedKFold

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(pipeline, X, y, cv=cv)

La métrica.accuracyes engañosa con clases desbalanceadas: un modelo que siempre predice la clase mayoritaria puede sacar 90% y ser inútil. Elige la métrica según el problema:

python
scores = cross_val_score(pipeline, X, y, cv=5, scoring='f1_macro')

Algunas opciones según el caso:

  • f1_macro/balanced_accuracy→ clases desbalanceadas
  • roc_auc→ ranking y probabilidades
  • neg_root_mean_squared_error→ regresión
  • precision/recall→ cuando un tipo de error duele más que otro

La pregunta correcta no es «¿qué accuracy tengo?», sino «¿qué error me cuesta dinero?».


4. GridSearchCV: tuning sin romper el Pipeline

El tuning manual es lento, frágil y propenso a errores.GridSearchCVautomatiza la búsqueda de hiperparámetros y lo hace dentro del Pipeline, así que no hay leakage.

python
from sklearn.model_selection import GridSearchCV

params = {
    'prep__num__with_mean': [True, False],
    'model__C': [0.1, 1, 10]
}

grid = GridSearchCV(pipeline, params, cv=5)
grid.fit(X, y)

La magia está en la sintaxis:

prep__num__with_mean

Que se lee así:

pipeline → paso 'prep' → bloque 'num' → parámetro 'with_mean'

Dobles guiones bajos (__) para bajar de nivel. Con esto puedes tunear el preprocesamiento, el modelo o cualquier paso del Pipeline, todo en una sola llamada y sin leakage.

Pero ojo con el coste. GridSearch prueba todas las combinaciones, y eso se multiplica rápido:

2 (with_mean) × 3 (C) × 5 (folds) = 30 entrenamientos

Añade un tercer parámetro con 4 valores y ya vas por 120. Es la explosión combinatoria: con grids grandes, GridSearch se vuelve impagable.

Cuando termina, lo útil está aquí:

python
grid.best_params_      # la mejor combinación
grid.best_score_       # su score en CV
grid.best_estimator_   # el Pipeline ya reentrenado con todo X

5. RandomizedSearchCV: tuning inteligente

GridSearch prueba todas las combinaciones. RandomizedSearch prueba solo algunas, pero bien elegidas, muestreando de distribuciones que tú defines.

python
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import uniform

params = {
    'model__C': uniform(0.01, 10)
}

search = RandomizedSearchCV(pipeline, params, n_iter=20, cv=5)
search.fit(X, y)

Ventajas:

  • más rápido
  • más eficiente
  • ideal para espacios grandes
  • evita combinaciones inútiles

La clave está enn_iter: tú decides cuánto presupuesto gastar. En vez de probarlo todo, fijas 20 intentos y dejas que el azar explore. En espacios de hiperparámetros amplios, RandomizedSearch suele encontrar algo casi tan bueno como GridSearch en una fracción del tiempo.

Regla práctica: grid pequeño y discreto → GridSearch. Espacio grande o continuo → RandomizedSearch.


6. Nested Cross-Validation: la evaluación definitiva

Aquí está la trampa que casi nadie ve: si usas el mismo CV para elegir los hiperparámetros y para reportar el resultado, estás haciendo trampa sin querer. Has optimizado contra esos folds, así que ese score ya está contaminado, es optimista.

La solución es nested CV (CV anidado):

  • CV externo → evalúa el modelo
  • CV interno → hace el GridSearch
python
from sklearn.model_selection import cross_val_score, KFold

outer = KFold(n_splits=5)
scores = cross_val_score(grid, X, y, cv=outer)

Lo que pasa por dentro: en cada fold externo, elgridhace su propia búsqueda de hiperparámetros usando solo el train de ese fold, y luego se evalúa en el test externo, que nunca vio durante el tuning. Resultado: un score honesto, sin que los hiperparámetros se hayan ajustado al conjunto de evaluación.

Es más caro (un GridSearch completo por cada fold externo), pero es lo que se usa en papers, benchmarks y producción seria.


7. Buenas prácticas avanzadas

✔️ 1. Todo lo que aprende de los datos → dentro del Pipeline. Scaler, PCA, imputación, encoding… todo. Si se ajusta a los datos, va dentro.

✔️ 2. Nunca usesfit_transformfuera del Pipeline. Es la forma más común de leakage, y la más silenciosa.

✔️ 3. Evalúa siempre con CV. Un único split no es suficiente. Y mira lastd, no solo la media.

✔️ 4. Tunea hiperparámetros dentro del Pipeline. No fuera. El preprocesamiento también tiene parámetros que tunear.

✔️ 5. Usa la métrica correcta.accuracyno sirve con clases desbalanceadas.

✔️ 6. Guarda el Pipeline completo, no el modelo. El modelo sin preprocesamiento no sirve en producción.


8. Ejemplo final: evaluación + tuning + producción

python
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.pipeline import Pipeline
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import GridSearchCV

preprocess = ColumnTransformer([
    ('num', StandardScaler(), ['age', 'salary']),
    ('cat', OneHotEncoder(handle_unknown='ignore'), ['city', 'gender'])
])

pipeline = Pipeline([
    ('prep', preprocess),
    ('model', LogisticRegression())
])

params = {
    'model__C': [0.1, 1, 10]
}

grid = GridSearchCV(pipeline, params, cv=5, scoring='f1_macro')
grid.fit(X, y)

best_model = grid.best_estimator_

Y lo guardas:

python
import joblib
joblib.dump(best_model, 'pipeline.pkl')

Esto ya es nivel producción. Validado, tuneado, con la métrica correcta y listo para desplegar como un único objeto.


TL;DR

  • La validación correcta es parte del diseño, no un paso final.
  • Cross-Validation es obligatorio para evaluar modelos de forma honesta, y lastdimporta tanto como la media.
  • Usa StratifiedKFold y la métrica adecuada;accuracyengaña con clases desbalanceadas.
  • GridSearchCV prueba todo (cuidado con la explosión combinatoria); RandomizedSearchCV explora con presupuesto fijo.
  • Nested CV es la evaluación más rigurosa: separa elegir hiperparámetros de reportar resultados.
  • Todo el preprocesamiento debe vivir dentro del Pipeline.
  • Lo que despliegas no es un modelo: es un Pipeline completo.