Practica Machine Learning con Scikit-Learn - Soluciones

Curso Python para Ciencia de datos

Por Jose R. Zapata - https://joserzapata.github.io/

Invítame a un Café

NOTA: Realizar primero los ejercicios y luego revisar las soluciones propuestas.

En este ejercicio práctico se aplicarán los conceptos de Machine Learning con Scikit-Learn vistos en el capítulo anterior:

  • Pipelines de transformación
  • Validación cruzada (Cross-Validation)
  • Búsqueda de hiperparámetros (RandomizedSearchCV)
  • Evaluación del modelo final
  • Guardar y cargar modelos

Para realizar los ejercicios prácticos de este capitulo, hacer click en el siguiente enlace que los llevara a Google Colab, donde podrán ejecutar el código y realizar los ejercicios propuestos.

Ejercicios Machine Learning - click para abrir en colab

Descripción del Problema

Usaremos el dataset Wine Quality que contiene características fisicoquímicas de vinos tintos portugueses. El objetivo es predecir la calidad del vino (clasificación binaria: bueno o malo) a partir de sus propiedades químicas.

Las características del dataset son:

  • fixed acidity - acidez fija
  • volatile acidity - acidez volátil
  • citric acid - ácido cítrico
  • residual sugar - azúcar residual
  • chlorides - cloruros
  • free sulfur dioxide - dióxido de azufre libre
  • total sulfur dioxide - dióxido de azufre total
  • density - densidad
  • pH - pH
  • sulphates - sulfatos
  • alcohol - alcohol
  • quality - calidad (variable objetivo, valor entre 3 y 8)

Transformaremos la variable quality en una variable binaria: 1 (bueno, quality >= 7) y 0 (malo, quality < 7).


Ejercicio 1: Importar librerías

Importe las siguientes librerías: pandas, sklearn , matplotlib e imprima las versiones de pandas y sklearn.

# Copie el código aca
import pandas as pd
import matplotlib.pyplot as plt
import sklearn

print(f"Pandas version: {pd.__version__}")
print(f"Sklearn version: {sklearn.__version__}")
Pandas version: 2.2.2
Sklearn version: 1.6.1

Ejercicio 2: Cargar y explorar los datos

Cargue el dataset de vinos tintos desde la siguiente URL y explore los datos.

URL: https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv

Nota: El separador del archivo CSV es ; (punto y coma).

Realice las siguientes exploraciones:

  1. Muestre la información del DataFrame (.info())
  2. Muestre las primeras 5 filas
  3. Muestre la descripción estadística
# Copie el código aca
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/wine-quality/winequality-red.csv"
wine_df = pd.read_csv(url, sep=";")
wine_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1599 entries, 0 to 1598
Data columns (total 12 columns):
 #   Column                Non-Null Count  Dtype
---  ------                --------------  -----
 0   fixed acidity         1599 non-null   float64
 1   volatile acidity      1599 non-null   float64
 2   citric acid           1599 non-null   float64
 3   residual sugar        1599 non-null   float64
 4   chlorides             1599 non-null   float64
 5   free sulfur dioxide   1599 non-null   float64
 6   total sulfur dioxide  1599 non-null   float64
 7   density               1599 non-null   float64
 8   pH                    1599 non-null   float64
 9   sulphates             1599 non-null   float64
 10  alcohol               1599 non-null   float64
 11  quality               1599 non-null   int64
dtypes: float64(11), int64(1)
memory usage: 150.0 KB
wine_df.head()

fixed acidityvolatile aciditycitric acidresidual sugarchloridesfree sulfur dioxidetotal sulfur dioxidedensitypHsulphatesalcoholquality
07.40.700.001.90.07611.034.00.99783.510.569.45
17.80.880.002.60.09825.067.00.99683.200.689.85
27.80.760.042.30.09215.054.00.99703.260.659.85
311.20.280.561.90.07517.060.00.99803.160.589.86
47.40.700.001.90.07611.034.00.99783.510.569.45
wine_df.describe()

fixed acidityvolatile aciditycitric acidresidual sugarchloridesfree sulfur dioxidetotal sulfur dioxidedensitypHsulphatesalcoholquality
count1599.0000001599.0000001599.0000001599.0000001599.0000001599.0000001599.0000001599.0000001599.0000001599.0000001599.0000001599.000000
mean8.3196370.5278210.2709762.5388060.08746715.87492246.4677920.9967473.3111130.65814910.4229835.636023
std1.7410960.1790600.1948011.4099280.04706510.46015732.8953240.0018870.1543860.1695071.0656680.807569
min4.6000000.1200000.0000000.9000000.0120001.0000006.0000000.9900702.7400000.3300008.4000003.000000
25%7.1000000.3900000.0900001.9000000.0700007.00000022.0000000.9956003.2100000.5500009.5000005.000000
50%7.9000000.5200000.2600002.2000000.07900014.00000038.0000000.9967503.3100000.62000010.2000006.000000
75%9.2000000.6400000.4200002.6000000.09000021.00000062.0000000.9978353.4000000.73000011.1000006.000000
max15.9000001.5800001.00000015.5000000.61100072.000000289.0000001.0036904.0100002.00000014.9000008.000000

Ejercicio 3: Verificar valores nulos y duplicados

Verifique si hay valores nulos en el dataset y cuántos datos duplicados existen. Si hay duplicados, elimínelos.

# Copie el código aca
# Verificar valores nulos
print("Valores nulos por columna:")
print(wine_df.isna().sum())
print(f"\nTotal de valores nulos: {wine_df.isna().sum().sum()}")
Valores nulos por columna:
fixed acidity           0
volatile acidity        0
citric acid             0
residual sugar          0
chlorides               0
free sulfur dioxide     0
total sulfur dioxide    0
density                 0
pH                      0
sulphates               0
alcohol                 0
quality                 0
dtype: int64

Total de valores nulos: 0
# Verificar y eliminar duplicados
print(f"Filas antes de eliminar duplicados: {len(wine_df)}")
print(f"Filas duplicadas: {wine_df.duplicated().sum()}")

wine_df = wine_df.drop_duplicates()
print(f"Filas después de eliminar duplicados: {len(wine_df)}")
Filas antes de eliminar duplicados: 1599
Filas duplicadas: 240
Filas después de eliminar duplicados: 1359

Ejercicio 4: Crear la variable objetivo binaria

Cree una nueva columna llamada good_quality que sea 1 si quality >= 7 y 0 en caso contrario. Luego elimine la columna quality original.

Verifique la distribución de clases de la nueva variable objetivo. ¿Hay desbalance de clases?

# Copie el código aca
# Crear variable objetivo binaria
wine_df["good_quality"] = (wine_df["quality"] >= 7).astype(int)

# Eliminar la columna quality original
wine_df = wine_df.drop(columns=["quality"])

# Verificar distribución de clases
print("Distribución de clases:")
print(wine_df["good_quality"].value_counts())
print(f"\nProporción de clase positiva: {wine_df['good_quality'].mean():.2%}")
Distribución de clases:
good_quality
0    1175
1     184
Name: count, dtype: int64

Proporción de clase positiva: 13.54%
  • Se puede observar que hay un desbalance de clases, ya que la clase positiva (vino bueno) representa una proporción mucho menor que la clase negativa.
  • Por esta razón, usar accuracy como métrica no sería lo más adecuado. Es mejor usar F1-Score que balancea precision y recall.

Ejercicio 5: Separar características y variable objetivo

Separe el DataFrame en X (características) y y (variable objetivo good_quality).

# Copie el código aca
X = wine_df.drop(columns=["good_quality"])
y = wine_df["good_quality"]

print(f"Forma de X: {X.shape}")
print(f"Forma de y: {y.shape}")
print(f"\nColumnas de X: {list(X.columns)}")
Forma de X: (1359, 11)
Forma de y: (1359,)

Columnas de X: ['fixed acidity', 'volatile acidity', 'citric acid', 'residual sugar', 'chlorides', 'free sulfur dioxide', 'total sulfur dioxide', 'density', 'pH', 'sulphates', 'alcohol']

Ejercicio 6: Dividir datos en entrenamiento y test

Divida los datos en conjuntos de entrenamiento (80%) y test (20%) usando train_test_split.

Importante: Use stratify=y para mantener la proporción de clases y random_state=42 para reproducibilidad.

# Copie el código aca
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=42
)

print(f"Datos de entrenamiento: {X_train.shape[0]} filas")
print(f"Datos de test: {X_test.shape[0]} filas")
print(f"\nProporción clase positiva en entrenamiento: {y_train.mean():.2%}")
print(f"Proporción clase positiva en test: {y_test.mean():.2%}")
Datos de entrenamiento: 1087 filas
Datos de test: 272 filas

Proporción clase positiva en entrenamiento: 13.52%
Proporción clase positiva en test: 13.60%
  • Se puede verificar que la proporción de clases se mantiene similar en ambos conjuntos gracias al parámetro stratify.

Ejercicio 7: Crear un Pipeline de preprocesamiento y modelo

Cree un Pipeline que incluya los siguientes pasos:

  1. StandardScaler - para estandarizar las características
  2. RandomForestClassifier - como modelo de clasificación

Visualice el pipeline usando set_config(display="diagram").

Documentación:

# Copie el código aca
from sklearn import set_config
from sklearn.ensemble import RandomForestClassifier
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

pipeline_rf = Pipeline([
    ("scaler", StandardScaler()),
    ("modelo", RandomForestClassifier(random_state=42)),
])

# Visualizar el pipeline
set_config(display="diagram")
pipeline_rf
Pipeline(steps=[('scaler', StandardScaler()),
                ('modelo', RandomForestClassifier(random_state=42))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Ejercicio 8: Evaluar el modelo con Validación Cruzada

Evalúe el pipeline usando validación cruzada con 10 segmentos (folds).

Use StratifiedKFold en lugar de KFold para mantener la proporción de clases en cada fold (importante cuando hay desbalance de clases).

Use la métrica F1-Score (scoring='f1') ya que hay desbalance de clases.

Imprima el resultado promedio y la desviación estándar.

Documentación:

# Copie el código aca
from sklearn.model_selection import StratifiedKFold, cross_validate

skfold = StratifiedKFold(n_splits=10, shuffle=True, random_state=42)

cv_results = cross_validate(pipeline_rf, X_train, y_train, cv=skfold, scoring="f1")

print(f"F1-Score por fold: {cv_results['test_score']}")
print(f"F1-Score Promedio: {cv_results['test_score'].mean():.4f}")
print(f"F1-Score STD: {cv_results['test_score'].std():.4f}")
F1-Score por fold: [0.58333333 0.36363636 0.45454545 0.27272727 0.60869565 0.5
 0.10526316 0.5        0.3        0.33333333]
F1-Score Promedio: 0.4022
F1-Score STD: 0.1480

Ejercicio 9: Comparar múltiples modelos con Cross-Validation

Compare al menos 3 modelos diferentes usando validación cruzada con la misma métrica (F1-Score) y el mismo esquema de folds.

Modelos sugeridos:

  1. RandomForestClassifier
  2. GradientBoostingClassifier
  3. LogisticRegression

Cree un pipeline para cada modelo (con StandardScaler) y compare los resultados.

Documentación:

# Copie el código aca
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression

Definir los pipelines para random forest

# Copie el código aca
Random_Forest = Pipeline([
    ("scaler", StandardScaler()),
    ("modelo", RandomForestClassifier(random_state=42)),
])
Random_Forest
Pipeline(steps=[('scaler', StandardScaler()),
                ('modelo', RandomForestClassifier(random_state=42))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Definir los pipelines para gradient boosting

# Copie el código aca
Gradient_Boosting = Pipeline([
    ("scaler", StandardScaler()),
    ("modelo", GradientBoostingClassifier(random_state=42)),
])
Gradient_Boosting
Pipeline(steps=[('scaler', StandardScaler()),
                ('modelo', GradientBoostingClassifier(random_state=42))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Definir los pipelines para logistic regression

# Copie el código aca
Logistic_Regression = Pipeline([
    ("scaler", StandardScaler()),
    ("modelo", LogisticRegression(random_state=42, max_iter=1000)),
])
Logistic_Regression
Pipeline(steps=[('scaler', StandardScaler()),
                ('modelo', LogisticRegression(max_iter=1000, random_state=42))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Evaluar cada modelo con cross-validation

# Copie el código aca
# Definir los pipelines para cada modelo
modelos = {
    "Random Forest": Random_Forest,
    "Gradient Boosting": Gradient_Boosting,
    "Logistic Regression": Logistic_Regression,
}

# Evaluar cada modelo con cross-validation
resultados = {}
cv_scores = {}  # Almacenar todos los scores de cada fold para boxplots

for nombre, pipeline in modelos.items():
    cv_result = cross_validate(pipeline, X_train, y_train, cv=skfold, scoring="f1")
    media = cv_result["test_score"].mean()
    std = cv_result["test_score"].std()
    resultados[nombre] = {"media": media, "std": std}
    cv_scores[nombre] = cv_result["test_score"]  # Guardar los scores de todos los folds
    print(f"{nombre:25s} -> F1-Score: {media:.4f} ± {std:.4f}")
Random Forest             -> F1-Score: 0.4022 ± 0.1480
Gradient Boosting         -> F1-Score: 0.4513 ± 0.1745
Logistic Regression       -> F1-Score: 0.3787 ± 0.1165

Crear una visualizacion de boxplot de los resultados de los modelos

# Copie el código aca
# Crear boxplots de los resultados de cross-validation

# Preparar los datos para el boxplot
datos_boxplot = [cv_scores[nombre] for nombre in cv_scores.keys()]
nombres_modelos = list(cv_scores.keys())

# Crear la figura
plt.boxplot(datos_boxplot, tick_labels=nombres_modelos)
plt.title("Comparación de Modelos - F1-Score (Cross-Validation)")
plt.ylabel("F1-Score")
plt.xlabel("Modelo");

png


Ejercicio 10: Búsqueda de Hiperparámetros con RandomizedSearchCV

Tome el mejor modelo del ejercicio anterior y realice una búsqueda de hiperparámetros usando RandomizedSearchCV.

RandomizedSearchCV es más eficiente que GridSearchCV cuando el espacio de hiperparámetros es grande, ya que prueba combinaciones aleatorias en lugar de todas las posibles.

Para RandomForestClassifier, defina los siguientes rangos de hiperparámetros:

param_distributions = {
    "modelo__n_estimators": [50, 100, 200, 300, 500],
    "modelo__max_depth": [3, 5, 10, 15, 20, None],
    "modelo__min_samples_split": [2, 5, 10],
    "modelo__min_samples_leaf": [1, 2, 4],
    "modelo__max_features": ["sqrt", "log2", None],
}

Use n_iter=50 para probar 50 combinaciones aleatorias, scoring='f1' y cv=skfold.

Documentación: RandomizedSearchCV

# Copie el código aca
from sklearn.model_selection import RandomizedSearchCV

# Definir el espacio de hiperparámetros
# Nota: se usa "modelo__" como prefijo porque el modelo está dentro del pipeline
param_distributions = {
    "modelo__n_estimators": [50, 100, 200, 300, 500],
    "modelo__max_depth": [3, 5, 10, 15, 20, None],
    "modelo__min_samples_split": [2, 5, 10],
    "modelo__min_samples_leaf": [1, 2, 4],
    "modelo__max_features": ["sqrt", "log2", None],
}

# Crear el RandomizedSearchCV
random_search = RandomizedSearchCV(
    pipeline_rf,
    param_distributions=param_distributions,
    n_iter=50,
    cv=skfold,
    scoring="f1",
    random_state=42,
    n_jobs=-1,  # Usar todos los cores disponibles
)

# Ejecutar la búsqueda
random_search.fit(X_train, y_train)
RandomizedSearchCV(cv=StratifiedKFold(n_splits=10, random_state=42, shuffle=True),
                   estimator=Pipeline(steps=[('scaler', StandardScaler()),
                                             ('modelo',
                                              RandomForestClassifier(random_state=42))]),
                   n_iter=50, n_jobs=-1,
                   param_distributions={'modelo__max_depth': [3, 5, 10, 15, 20,
                                                              None],
                                        'modelo__max_features': ['sqrt', 'log2',
                                                                 None],
                                        'modelo__min_samples_leaf': [1, 2, 4],
                                        'modelo__min_samples_split': [2, 5, 10],
                                        'modelo__n_estimators': [50, 100, 200,
                                                                 300, 500]},
                   random_state=42, scoring='f1')
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Ejercicio 11: Analizar los resultados de la búsqueda de hiperparámetros

Imprima los mejores hiperparámetros encontrados, el mejor F1-Score de cross-validation y su desviación estándar.

Además, muestre los 5 mejores resultados de la búsqueda en un DataFrame ordenado por el score.

# Copie el código aca
# Mejores hiperparámetros
print("Mejores hiperparámetros encontrados:")
for param, valor in random_search.best_params_.items():
    print(f"  {param}: {valor}")

print(f"\nMejor F1-Score CV: {random_search.best_score_:.4f}")
print(
    f"STD del mejor resultado: "
    f"{random_search.cv_results_['std_test_score'][random_search.best_index_]:.4f}"
)
Mejores hiperparámetros encontrados:
  modelo__n_estimators: 200
  modelo__min_samples_split: 2
  modelo__min_samples_leaf: 1
  modelo__max_features: sqrt
  modelo__max_depth: 20

Mejor F1-Score CV: 0.4168
STD del mejor resultado: 0.1716
# Top 5 resultados de la búsqueda
cv_results_df = pd.DataFrame(random_search.cv_results_)
top_5 = cv_results_df.nsmallest(5, "rank_test_score")[
    ["params", "mean_test_score", "std_test_score", "rank_test_score"]
]
top_5

paramsmean_test_scorestd_test_scorerank_test_score
48{'modelo__n_estimators': 200, 'modelo__min_sam...0.4168350.1716401
18{'modelo__n_estimators': 300, 'modelo__min_sam...0.4075820.1630162
37{'modelo__n_estimators': 100, 'modelo__min_sam...0.4054170.1551203
15{'modelo__n_estimators': 100, 'modelo__min_sam...0.4046920.1540094
47{'modelo__n_estimators': 100, 'modelo__min_sam...0.4046920.1540094

Ejercicio 12: Evaluar el modelo final con datos de Test

Configure el pipeline con los mejores hiperparámetros, entrénelo con todos los datos de entrenamiento y evalúe con los datos de test.

Calcule las siguientes métricas en los datos de test:

  • Accuracy
  • Precision
  • Recall
  • F1-Score

Además, muestre el reporte de clasificación completo usando classification_report.

Documentación: classification_report

# Copie el código aca
from sklearn.metrics import (
    accuracy_score,
    classification_report,
    f1_score,
    precision_score,
    recall_score,
)

# Configurar el pipeline con los mejores hiperparámetros
mejor_pipeline = pipeline_rf.set_params(**random_search.best_params_)

# Entrenar con todos los datos de entrenamiento
mejor_pipeline.fit(X_train, y_train)

# Predecir con datos de test
y_pred = mejor_pipeline.predict(X_test)

# Calcular métricas
print("Métricas en datos de Test:")
print(f"  Accuracy:  {accuracy_score(y_test, y_pred):.4f}")
print(f"  Precision: {precision_score(y_test, y_pred):.4f}")
print(f"  Recall:    {recall_score(y_test, y_pred):.4f}")
print(f"  F1-Score:  {f1_score(y_test, y_pred):.4f}")
Métricas en datos de Test:
  Accuracy:  0.8934
  Precision: 0.7222
  Recall:    0.3514
  F1-Score:  0.4727

Crear un Reporte de clasificación con el método classification_report

# Copie el código aca
# Reporte de clasificación completo
print("\nReporte de Clasificación:")
print(classification_report(y_test, y_pred, target_names=["Malo", "Bueno"]))
Reporte de Clasificación:
              precision    recall  f1-score   support

        Malo       0.91      0.98      0.94       235
       Bueno       0.72      0.35      0.47        37

    accuracy                           0.89       272
   macro avg       0.81      0.67      0.71       272
weighted avg       0.88      0.89      0.88       272

Ejercicio 13: Matriz de Confusión

Visualice la matriz de confusión del modelo usando ConfusionMatrixDisplay.

Documentación: ConfusionMatrixDisplay

# Copie el código aca
import matplotlib.pyplot as plt
from sklearn.metrics import ConfusionMatrixDisplay

fig, ax = plt.subplots(figsize=(6, 5))
ConfusionMatrixDisplay.from_predictions(
    y_test, y_pred, display_labels=["Malo", "Bueno"], ax=ax, cmap="Blues"
)
ax.set_title("Matriz de Confusión - Datos de Test")
plt.tight_layout()
plt.show()

png


Ejercicio 14: Pipeline con ColumnTransformer (Ejercicio Avanzado)

Cree un pipeline más complejo que aplique diferentes transformaciones a diferentes columnas usando ColumnTransformer.

Suponga que las primeras 7 columnas (fixed acidity hasta total sulfur dioxide) son características que necesitan estandarización con StandardScaler, y las últimas 4 columnas (density hasta alcohol) necesitan escalamiento con MinMaxScaler.

Pasos:

  1. Defina las listas de columnas para cada tipo de transformación
  2. Cree un ColumnTransformer con los transformadores apropiados
  3. Cree un Pipeline completo con el ColumnTransformer y un GradientBoostingClassifier
  4. Evalúe con cross-validation

Documentación:

# Copie el código aca
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import MinMaxScaler

# Definir las columnas para cada transformación
cols_standard = [
    "fixed acidity",
    "volatile acidity",
    "citric acid",
    "residual sugar",
    "chlorides",
    "free sulfur dioxide",
    "total sulfur dioxide",
]

cols_minmax = ["density", "pH", "sulphates", "alcohol"]

# Crear el ColumnTransformer
preprocessor = ColumnTransformer(
    transformers=[
        ("standard", StandardScaler(), cols_standard),
        ("minmax", MinMaxScaler(), cols_minmax),
    ]
)

# Pipeline completo: preprocesamiento + modelo
pipeline_gb = Pipeline([
    ("preprocessor", preprocessor),
    ("modelo", GradientBoostingClassifier(random_state=42)),
])

# Visualizar el pipeline
set_config(display="diagram")
pipeline_gb
Pipeline(steps=[('preprocessor',
                 ColumnTransformer(transformers=[('standard', StandardScaler(),
                                                  ['fixed acidity',
                                                   'volatile acidity',
                                                   'citric acid',
                                                   'residual sugar',
                                                   'chlorides',
                                                   'free sulfur dioxide',
                                                   'total sulfur dioxide']),
                                                 ('minmax', MinMaxScaler(),
                                                  ['density', 'pH', 'sulphates',
                                                   'alcohol'])])),
                ('modelo', GradientBoostingClassifier(random_state=42))])
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.

Evaluar con cross-validation

# Copie el código aca
# Evaluar con cross-validation
cv_results_gb = cross_validate(pipeline_gb, X_train, y_train, cv=skfold, scoring="f1")

print(f"F1-Score Promedio: {cv_results_gb['test_score'].mean():.4f}")
print(f"F1-Score STD: {cv_results_gb['test_score'].std():.4f}")
F1-Score Promedio: 0.4513
F1-Score STD: 0.1745

Crear un Boxplot de los resultados del F1-Score de los 10 folds

# Copie el código aca
# boxplot cv_results_gb
plt.figure(figsize=(10, 6))
plt.boxplot(cv_results_gb["test_score"])
plt.title("F1-Score de los 10 folds")
plt.xlabel("F1-Score")
plt.show()

png


Ejercicio 15: RandomizedSearchCV con el Pipeline avanzado

Realice una búsqueda de hiperparámetros con RandomizedSearchCV para el pipeline con ColumnTransformer y GradientBoostingClassifier.

Defina los siguientes rangos de hiperparámetros:

param_distributions_gb = {
    "modelo__n_estimators": [50, 100, 200, 300],
    "modelo__max_depth": [3, 5, 7, 10],
    "modelo__learning_rate": [0.01, 0.05, 0.1, 0.2],
    "modelo__min_samples_split": [2, 5, 10],
    "modelo__min_samples_leaf": [1, 2, 4],
    "modelo__subsample": [0.8, 0.9, 1.0],
}

Use n_iter=50, scoring='f1' y cv=skfold.

# Copie el código aca
param_distributions_gb = {
    "modelo__n_estimators": [50, 100, 200, 300],
    "modelo__max_depth": [3, 5, 7, 10],
    "modelo__learning_rate": [0.01, 0.05, 0.1, 0.2],
    "modelo__min_samples_split": [2, 5, 10],
    "modelo__min_samples_leaf": [1, 2, 4],
    "modelo__subsample": [0.8, 0.9, 1.0],
}

random_search_gb = RandomizedSearchCV(
    pipeline_gb,
    param_distributions=param_distributions_gb,
    n_iter=50,
    cv=skfold,
    scoring="f1",
    random_state=42,
    n_jobs=-1,
)

random_search_gb.fit(X_train, y_train)

print("Mejores hiperparámetros encontrados:")
for param, valor in random_search_gb.best_params_.items():
    print(f"  {param}: {valor}")

print(f"\nMejor F1-Score CV: {random_search_gb.best_score_:.4f}")
print(
    f"STD del mejor resultado: "
    f"{random_search_gb.cv_results_['std_test_score'][random_search_gb.best_index_]:.4f}"
)
Mejores hiperparámetros encontrados:
  modelo__subsample: 1.0
  modelo__n_estimators: 200
  modelo__min_samples_split: 2
  modelo__min_samples_leaf: 2
  modelo__max_depth: 3
  modelo__learning_rate: 0.2

Mejor F1-Score CV: 0.4681
STD del mejor resultado: 0.1643

Ejercicio 16: Evaluación final del mejor modelo

Entrene el mejor modelo (con los mejores hiperparámetros) con todos los datos de entrenamiento y evalúe con los datos de test.

Muestre las métricas y el reporte de clasificación.

# Copie el código aca
# Configurar el pipeline con los mejores hiperparámetros
mejor_pipeline_gb = pipeline_gb.set_params(**random_search_gb.best_params_)

# Entrenar con datos de entrenamiento
mejor_pipeline_gb.fit(X_train, y_train)

# Predecir con datos de test
y_pred_gb = mejor_pipeline_gb.predict(X_test)

# Métricas
print("Métricas en datos de Test (Gradient Boosting optimizado):")
print(f"  Accuracy:  {accuracy_score(y_test, y_pred_gb):.4f}")
print(f"  Precision: {precision_score(y_test, y_pred_gb):.4f}")
print(f"  Recall:    {recall_score(y_test, y_pred_gb):.4f}")
print(f"  F1-Score:  {f1_score(y_test, y_pred_gb):.4f}")

print("\nReporte de Clasificación:")
print(classification_report(y_test, y_pred_gb, target_names=["Malo", "Bueno"]))
Métricas en datos de Test (Gradient Boosting optimizado):
  Accuracy:  0.8971
  Precision: 0.6957
  Recall:    0.4324
  F1-Score:  0.5333

Reporte de Clasificación:
              precision    recall  f1-score   support

        Malo       0.92      0.97      0.94       235
       Bueno       0.70      0.43      0.53        37

    accuracy                           0.90       272
   macro avg       0.81      0.70      0.74       272
weighted avg       0.89      0.90      0.89       272
# Matriz de confusión
fig, ax = plt.subplots(figsize=(6, 5))
ConfusionMatrixDisplay.from_predictions(
    y_test, y_pred_gb, display_labels=["Malo", "Bueno"], ax=ax, cmap="Blues"
)
ax.set_title("Matriz de Confusión - Gradient Boosting Optimizado")
plt.tight_layout()
plt.show()

png


Ejercicio 17: Entrenar modelo final y guardarlo

Entrene el mejor modelo con TODOS los datos disponibles (entrenamiento + test) y guárdelo usando joblib.

Esto se hace para aprovechar al máximo la información disponible antes de desplegar el modelo en producción.

Documentación: Model Persistence

# Copie el código aca
from joblib import dump

# Entrenar con todos los datos
modelo_final = pipeline_gb.set_params(**random_search_gb.best_params_)
modelo_final.fit(X, y)

# Verificación: accuracy sobre todos los datos
y_pred_all = modelo_final.predict(X)
print(f"F1-Score (todos los datos): {f1_score(y, y_pred_all):.4f}")

# Guardar el pipeline completo
dump(modelo_final, "pipeline_wine_quality.joblib")
print("\nModelo guardado exitosamente en 'pipeline_wine_quality.joblib'")
F1-Score (todos los datos): 0.9973

Modelo guardado exitosamente en 'pipeline_wine_quality.joblib'

Ejercicio 18: Cargar el modelo y hacer predicciones

Cargue el modelo guardado y realice predicciones con nuevos datos.

Use los siguientes datos de ejemplo para hacer predicciones:

nuevos_datos = pd.DataFrame({
    "fixed acidity": [7.4, 11.2],
    "volatile acidity": [0.70, 0.28],
    "citric acid": [0.00, 0.56],
    "residual sugar": [1.9, 1.9],
    "chlorides": [0.076, 0.075],
    "free sulfur dioxide": [11.0, 17.0],
    "total sulfur dioxide": [34.0, 60.0],
    "density": [0.9978, 0.9980],
    "pH": [3.51, 3.16],
    "sulphates": [0.56, 0.58],
    "alcohol": [9.4, 9.8],
})
# Copie el código aca
from joblib import load

# Cargar el modelo
modelo_cargado = load("pipeline_wine_quality.joblib")

# Datos de ejemplo
nuevos_datos = pd.DataFrame({
    "fixed acidity": [7.4, 11.2],
    "volatile acidity": [0.70, 0.28],
    "citric acid": [0.00, 0.56],
    "residual sugar": [1.9, 1.9],
    "chlorides": [0.076, 0.075],
    "free sulfur dioxide": [11.0, 17.0],
    "total sulfur dioxide": [34.0, 60.0],
    "density": [0.9978, 0.9980],
    "pH": [3.51, 3.16],
    "sulphates": [0.56, 0.58],
    "alcohol": [9.4, 9.8],
})

# Hacer predicciones
predicciones = modelo_cargado.predict(nuevos_datos)
etiquetas = ["Malo" if p == 0 else "Bueno" for p in predicciones]

print("Predicciones para nuevos datos:")
for i, (pred, etiqueta) in enumerate(zip(predicciones, etiquetas)):
    print(f"  Vino {i + 1}: {pred} ({etiqueta})")
Predicciones para nuevos datos:
  Vino 1: 0 (Malo)
  Vino 2: 0 (Malo)

Ejercicio 19 (Bono): Importancia de las características

Extraiga la importancia de las características del mejor modelo y visualícelas en un gráfico de barras horizontal.

Tip: Para acceder al modelo dentro del pipeline, use pipeline.named_steps['modelo'] y luego .feature_importances_.

# Copie el código aca
# Obtener la importancia de las características
importances = modelo_final.named_steps["modelo"].feature_importances_
feature_names = cols_standard + cols_minmax

# Crear DataFrame y ordenar
importance_df = pd.DataFrame({
    "feature": feature_names,
    "importance": importances,
}).sort_values("importance", ascending=True)

# Visualizar
fig, ax = plt.subplots(figsize=(8, 6))
ax.barh(importance_df["feature"], importance_df["importance"], color="steelblue")
ax.set_xlabel("Importancia")
ax.set_title("Importancia de las Características - Gradient Boosting")
plt.tight_layout()
plt.show()

png


Resumen

En esta práctica se aplicaron los siguientes conceptos de Machine Learning con Scikit-Learn:

  1. Exploración y preparación de datos: carga, limpieza, eliminación de duplicados y creación de variable objetivo binaria
  2. Pipelines: creación de pipelines simples y con ColumnTransformer para diferentes transformaciones por columna
  3. Validación cruzada: uso de StratifiedKFold y cross_validate para evaluar modelos de forma robusta
  4. Comparación de modelos: evaluación de múltiples algoritmos con la misma metodología
  5. Búsqueda de hiperparámetros: uso de RandomizedSearchCV para encontrar la mejor configuración
  6. Evaluación final: métricas de clasificación, reporte de clasificación y matriz de confusión
  7. Persistencia del modelo: guardar y cargar el pipeline completo con joblib
  8. Importancia de características: análisis de qué variables son más relevantes para el modelo

Phd. Jose R. Zapata