ParamGridBuilder y Evaluadores

API de Spark ML para Tuning Avanzado

Volver

1. ParamGridBuilder: Construcción del Espacio de Búsqueda

API y Funcionamiento

ParamGridBuilder es la clase de Spark ML para construir el producto cartesiano de hiperparámetros. Permite especificar valores discretos para cada hiperparámetro y genera automáticamente todas las combinaciones.

Sintaxis Base Python + PySpark
from pyspark.ml.tuning import ParamGridBuilder

# Crear builder
builder = ParamGridBuilder()

# Agregar grids para cada hiperparámetro (método encadenado)
paramGrid = builder \
    .addGrid(estimator.param1, [value1, value2, ...]) \  # Grid para param1
    .addGrid(estimator.param2, [value1, value2, ...]) \  # Grid para param2
    .build()  # Construir producto cartesiano

# Resultado: Lista de ParamMaps (diccionarios de configuración)
# Tamaño: len(paramGrid) = len(values_param1) × len(values_param2) × ...

Ejemplo Completo: Regresión Logística

from pyspark.ml.classification import LogisticRegression
from pyspark.ml.tuning import ParamGridBuilder

# Definir modelo
lr = LogisticRegression(featuresCol="features", labelCol="label")

# Construir grid de hiperparámetros
paramGrid = ParamGridBuilder() \
    .addGrid(lr.regParam, [0.001, 0.01, 0.1, 1.0, 10.0]) \        # Regularización: 5 valores
    .addGrid(lr.elasticNetParam, [0.0, 0.25, 0.5, 0.75, 1.0]) \   # Elastic Net: 5 valores
    .addGrid(lr.maxIter, [50, 100, 200]) \                        # Iteraciones: 3 valores
    .build()

# Tamaño del grid
print(f"Total configs: {len(paramGrid)}")  # Output: 75 (5 × 5 × 3)

# Inspeccionar primera configuración
print(paramGrid[0])
# Output: {Param(parent='LogisticRegression_...', name='regParam', ...): 0.001,
#          Param(parent='LogisticRegression_...', name='elasticNetParam', ...): 0.0,
#          Param(parent='LogisticRegression_...', name='maxIter', ...): 50}

# ============================================================
# Conexión Matemática: Producto Cartesiano
# ============================================================
# Θ = Θ_regParam × Θ_elasticNet × Θ_maxIter
# |Θ| = |{0.001, 0.01, 0.1, 1.0, 10.0}| × |{0.0, 0.25, 0.5, 0.75, 1.0}| × |{50, 100, 200}|
#     = 5 × 5 × 3 = 75 configuraciones

Tipos de Valores Soportados

Tipo Ejemplo Uso
Enteros [10, 50, 100, 200] numTrees, maxIter, maxBins
Flotantes [0.01, 0.1, 1.0] regParam, stepSize, subsamplingRate
Cadenas ["gini", "entropy"] impurity (criterio de split)
Listas/Tuplas [[10,5], [20,10,5]] layers (arquitectura de red neuronal)

Estrategias para Seleccionar Valores del Grid

1. Escala Logarítmica (regParam, stepSize)

Para parámetros que varían órdenes de magnitud, usar escala log:

# Malo: escala lineal
[0.1, 0.2, 0.3, 0.4, 0.5]  # Muy concentrado

# Bueno: escala logarítmica
[0.001, 0.01, 0.1, 1.0, 10.0]  # Cubre 4 órdenes

# En Python
import numpy as np
np.logspace(-3, 1, 5)  # [0.001, 0.01, 0.1, 1, 10]
2. Escala Lineal (numTrees, maxDepth)

Para parámetros enteros con rango acotado:

# Bueno: espaciado uniforme
numTrees: [10, 50, 100, 200]
maxDepth: [5, 10, 15, 20, 25]

# En Python
list(range(5, 30, 5))  # [5, 10, 15, 20, 25]

Regla Práctica: Coarse-to-Fine

  1. 1. Grid grueso: Pocos valores (3-4 por dim), amplio rango → identificar región óptima
  2. 2. Grid fino: Más valores (5-7 por dim), rango estrecho alrededor del óptimo

2. Evaluadores: Cuantificando el Rendimiento

Tipos de Evaluadores en Spark ML

Los evaluadores son objetos que calculan una métrica de rendimiento sobre predicciones. CrossValidator usa el evaluador para seleccionar la mejor configuración:

$$ \lambda^* = \underset{\lambda \in \Theta}{\text{argmax}} \, \text{Evaluator.evaluate}(\text{predictions}_\lambda) $$

(Nota: CrossValidator maximiza la métrica del evaluador, por eso usamos argmax)

Catálogo de Evaluadores

BinaryClassificationEvaluator

Para clasificación binaria (2 clases). Métricas disponibles:

areaUnderROC (default):

$$ \text{AUC-ROC} = \int_{0}^{1} \text{TPR}(t) \, d[\text{FPR}(t)] $$

Área bajo curva ROC. Mide separabilidad: AUC=1 perfecto, AUC=0.5 azar.

areaUnderPR:

$$ \text{AUC-PR} = \int_{0}^{1} \text{Precision}(r) \, dr $$

Área bajo curva Precision-Recall. Mejor para datasets desbalanceados.

from pyspark.ml.evaluation import BinaryClassificationEvaluator

evaluator = BinaryClassificationEvaluator(
    labelCol="label",                # Columna de etiquetas verdaderas
    rawPredictionCol="rawPrediction", # Scores del modelo (antes de threshold)
    metricName="areaUnderROC"        # "areaUnderROC" o "areaUnderPR"
)

auc = evaluator.evaluate(predictions)
print(f"AUC-ROC: {auc:.4f}")
MulticlassClassificationEvaluator

Para clasificación multiclase (3+ clases). Métricas disponibles:

Métrica Fórmula Interpretación
accuracy $\frac{TP + TN}{N}$ % predicciones correctas
f1 $\frac{2 \cdot P \cdot R}{P + R}$ Media armónica de precision/recall
weightedPrecision $\sum_i w_i P_i$ Precision ponderada por clase
weightedRecall $\sum_i w_i R_i$ Recall ponderado por clase
from pyspark.ml.evaluation import MulticlassClassificationEvaluator

evaluator = MulticlassClassificationEvaluator(
    labelCol="label",
    predictionCol="prediction",  # Clase predicha (no raw scores)
    metricName="f1"              # "f1", "accuracy", "weightedPrecision", etc.
)

f1_score = evaluator.evaluate(predictions)
print(f"F1-Score: {f1_score:.4f}")
RegressionEvaluator

Para regresión (predicción de valores continuos). Métricas:

rmse (default):

$$ \text{RMSE} = \sqrt{\frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2} $$

Root Mean Squared Error. Penaliza errores grandes.

mae:

$$ \text{MAE} = \frac{1}{n} \sum_{i=1}^{n} |y_i - \hat{y}_i| $$

Mean Absolute Error. Más robusto a outliers.

r2:

$$ R^2 = 1 - \frac{\sum (y_i - \hat{y}_i)^2}{\sum (y_i - \bar{y})^2} $$

Coeficiente de determinación. R²=1 ajuste perfecto.

mse:

$$ \text{MSE} = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2 $$

Mean Squared Error. Cuadrado del RMSE.

from pyspark.ml.evaluation import RegressionEvaluator

evaluator = RegressionEvaluator(
    labelCol="price",            # Valor real
    predictionCol="prediction",  # Valor predicho
    metricName="rmse"            # "rmse", "mae", "r2", "mse"
)

rmse = evaluator.evaluate(predictions)
print(f"RMSE: {rmse:.2f}")

Importante: Maximización vs Minimización

CrossValidator siempre maximiza la métrica del evaluador. Esto es intuitivo para métricas como AUC, F1, R² (mayor = mejor), pero contra-intuitivo para errores como RMSE, MSE (menor = mejor).

Solución: Spark invierte automáticamente

  • • Para RMSE/MSE/MAE: CrossValidator maximiza -RMSE internamente (equivalente a minimizar RMSE)
  • • Para AUC/F1/R²: CrossValidator maximiza directamente
  • • El usuario no necesita hacer nada, Spark maneja esto transparentemente

3. Tuning de Pipelines Completos

Optimizando Múltiples Stages Simultáneamente

Una de las capacidades más poderosas de Spark ML es tunear todo el pipeline de una vez, incluyendo transformadores (feature engineering) y estimadores (modelos). Esto permite encontrar la combinación óptima de preprocesamiento + modelo.

Arquitectura de Pipeline Tuning

graph LR
    A[Raw Data] --> B[VectorAssembler]
    B --> C[StandardScaler]
    C --> D[PCA]
    D --> E[RandomForest]
    E --> F[Predictions]

    G[ParamGrid] -.-> C
    G -.-> D
    G -.-> E

    H[CrossValidator] --> G
    H --> I[Best Pipeline Model]

    style A fill:#9333ea,stroke:#a855f7,color:#fff
    style G fill:#f59e0b,stroke:#fbbf24,color:#000
    style H fill:#10b981,stroke:#34d399,color:#fff
    style I fill:#ef4444,stroke:#f87171,color:#fff
                        

CrossValidator optimiza hiperparámetros de múltiples stages (Scaler, PCA, RF) simultáneamente

Ejemplo Completo: Pipeline con PCA + Random Forest

from pyspark.ml import Pipeline
from pyspark.ml.feature import VectorAssembler, StandardScaler, PCA
from pyspark.ml.classification import RandomForestClassifier
from pyspark.ml.evaluation import BinaryClassificationEvaluator
from pyspark.ml.tuning import CrossValidator, ParamGridBuilder

# ============================================================
# PASO 1: Construir Pipeline con múltiples stages
# ============================================================
# Stage 1: Ensamblar features
assembler = VectorAssembler(
    inputCols=["f1", "f2", "f3", "f4", "f5"],
    outputCol="raw_features"
)

# Stage 2: Estandarizar (media 0, std 1)
scaler = StandardScaler(
    inputCol="raw_features",
    outputCol="scaled_features",
    withMean=True,
    withStd=True
)

# Stage 3: Reducción dimensional con PCA
pca = PCA(
    inputCol="scaled_features",
    outputCol="features"
    # k será tuneable (número de componentes principales)
)

# Stage 4: Modelo Random Forest
rf = RandomForestClassifier(
    featuresCol="features",
    labelCol="label",
    seed=42
)

# Construir pipeline
pipeline = Pipeline(stages=[assembler, scaler, pca, rf])

# ============================================================
# PASO 2: Definir grid tuneable para MÚLTIPLES stages
# ============================================================
paramGrid = ParamGridBuilder() \
    .addGrid(pca.k, [3, 5, 7]) \                          # PCA: 3 valores (Stage 3)
    .addGrid(rf.numTrees, [50, 100, 200]) \               # RF: 3 valores (Stage 4)
    .addGrid(rf.maxDepth, [10, 15, 20]) \                 # RF: 3 valores (Stage 4)
    .addGrid(rf.subsamplingRate, [0.7, 0.85, 1.0]) \      # RF: 3 valores (Stage 4)
    .build()

# Tamaño del grid: 3 × 3 × 3 × 3 = 81 configuraciones
print(f"Configuraciones a evaluar: {len(paramGrid)}")

# ============================================================
# PASO 3: Configurar evaluador
# ============================================================
evaluator = BinaryClassificationEvaluator(
    labelCol="label",
    metricName="areaUnderROC"
)

# ============================================================
# PASO 4: CrossValidator para pipeline completo
# ============================================================
cv = CrossValidator(
    estimator=pipeline,          # ← Pipeline completo (no solo modelo)
    estimatorParamMaps=paramGrid,
    evaluator=evaluator,
    numFolds=5,
    parallelism=8,
    seed=42
)

# ============================================================
# PASO 5: Entrenar (tunea TODOS los stages)
# ============================================================
print("Iniciando tuning de pipeline completo...")
print("Esto optimizará: PCA (k) + RF (numTrees, maxDepth, subsamplingRate)")

cvModel = cv.fit(df_train)

# ============================================================
# PASO 6: Extraer mejor pipeline
# ============================================================
bestPipeline = cvModel.bestModel  # Es un PipelineModel (no solo RF)

# Extraer componentes del mejor pipeline
best_pca = bestPipeline.stages[2]     # Stage 3: PCA
best_rf = bestPipeline.stages[3]      # Stage 4: RandomForest

print("\n========== MEJOR CONFIGURACIÓN ==========")
print(f"PCA k (componentes): {best_pca.getK()}")
print(f"RF numTrees: {best_rf.getNumTrees}")
print(f"RF maxDepth: {best_rf.getMaxDepth()}")
print(f"RF subsamplingRate: {best_rf.getSubsamplingRate()}")

# Mejor AUC
best_auc = max(cvModel.avgMetrics)
print(f"\nMejor AUC (CV): {best_auc:.4f}")

# ============================================================
# PASO 7: Usar pipeline completo para predicciones
# ============================================================
# El bestPipeline contiene TODAS las transformaciones + modelo
predictions = bestPipeline.transform(df_test)

# Evaluar en test
test_auc = evaluator.evaluate(predictions)
print(f"AUC en Test: {test_auc:.4f}")

# ============================================================
# OBSERVACIÓN CRÍTICA
# ============================================================
# Al tunear el pipeline completo:
# 1. Cada configuración entrena TODO el pipeline (assembler → scaler → PCA → RF)
# 2. PCA con k=3 puede funcionar mejor con RF(numTrees=200, maxDepth=10)
# 3. PCA con k=7 puede funcionar mejor con RF(numTrees=50, maxDepth=20)
# 4. El tuning encuentra la MEJOR COMBINACIÓN CONJUNTA de todos los hiperparámetros

Ventajas del Pipeline Tuning

Beneficios Técnicos:

  • Optimización Holística: Encuentra combinación óptima de preprocesamiento + modelo (no secuencial)
  • Evita Data Leakage: Scaler y PCA se ajustan solo en train folds, no en val fold
  • Reproducibilidad: Todo el pipeline queda encapsulado en bestModel

Casos de Uso:

  • Tunear dimensionalidad PCA + parámetros del modelo
  • Optimizar grado de PolynomialExpansion + regParam
  • Seleccionar features (ChiSqSelector k) + modelo
  • Comparar diferentes transformaciones (con/sin PCA)

4. Extracción y Análisis de Resultados

Atributos de CrossValidatorModel

Después de cv.fit(), el objeto cvModel contiene información detallada sobre todas las configuraciones evaluadas.

# Entrenar CrossValidator
cvModel = cv.fit(df_train)

# ============================================================
# Atributos principales del cvModel
# ============================================================

# 1. bestModel: Mejor modelo encontrado (ya entrenado en TODO el dataset)
bestModel = cvModel.bestModel

# 2. avgMetrics: Lista con métrica promedio de CV para cada config
avg_metrics = cvModel.avgMetrics  # Lista de floats, len = |paramGrid|

# 3. stdMetrics: Desviación estándar de la métrica para cada config (si collectSubModels=True)
# std_metrics = cvModel.stdMetrics  # Requiere collectSubModels=True en CrossValidator

# 4. getEstimatorParamMaps: Grid de configuraciones evaluadas
param_maps = cvModel.getEstimatorParamMaps()  # Lista de ParamMaps

# ============================================================
# Encontrar índice de mejor configuración
# ============================================================
best_idx = avg_metrics.index(max(avg_metrics))
best_params = param_maps[best_idx]
best_avg_metric = avg_metrics[best_idx]

print(f"Mejor configuración (índice {best_idx}):")
print(f"Parámetros: {best_params}")
print(f"Métrica promedio (CV): {best_avg_metric:.4f}")

Análisis Completo de Resultados

import pandas as pd
import matplotlib.pyplot as plt

# ============================================================
# Convertir resultados a DataFrame para análisis
# ============================================================
results = []
for idx, (params, avg_metric) in enumerate(zip(cvModel.getEstimatorParamMaps(), cvModel.avgMetrics)):
    config = {"config_id": idx, "avg_metric": avg_metric}

    # Extraer cada hiperparámetro del ParamMap
    for param, value in params.items():
        param_name = param.name  # Nombre del parámetro (ej: "numTrees")
        config[param_name] = value

    results.append(config)

results_df = pd.DataFrame(results)

# ============================================================
# Top 10 configuraciones
# ============================================================
print("========== TOP 10 CONFIGURACIONES ==========")
top10 = results_df.nlargest(10, "avg_metric")
print(top10)

# ============================================================
# Estadísticas por hiperparámetro individual
# ============================================================
print("\n========== IMPACTO POR HIPERPARÁMETRO ==========")

for col in results_df.columns:
    if col not in ["config_id", "avg_metric"]:
        print(f"\n{col}:")
        stats = results_df.groupby(col)["avg_metric"].agg(["mean", "std", "min", "max"])
        print(stats.sort_values("mean", ascending=False))

# Ejemplo Output:
#             mean       std       min       max
# numTrees
# 200         0.8623    0.0076    0.8456    0.8712  ← Mejor promedio
# 100         0.8589    0.0081    0.8423    0.8689
# 50          0.8534    0.0093    0.8312    0.8656
# 10          0.8234    0.0123    0.8012    0.8445

# ============================================================
# Visualización: Heatmap 2D (si grid es 2D)
# ============================================================
# Supongamos grid con solo numTrees y maxDepth
if "numTrees" in results_df.columns and "maxDepth" in results_df.columns:
    pivot = results_df.pivot(index="maxDepth", columns="numTrees", values="avg_metric")

    plt.figure(figsize=(10, 6))
    plt.imshow(pivot, cmap="viridis", aspect="auto")
    plt.colorbar(label="AUC-ROC")
    plt.xlabel("numTrees")
    plt.ylabel("maxDepth")
    plt.title("Heatmap de Rendimiento: Grid Search")
    plt.xticks(range(len(pivot.columns)), pivot.columns)
    plt.yticks(range(len(pivot.index)), pivot.index)
    plt.show()

# ============================================================
# Identificar configuraciones Pareto-óptimas
# ============================================================
# Configs que no son dominadas por ninguna otra en todas las métricas
# (útil si tienes múltiples métricas: AUC, tiempo, complejidad)

Ejemplo de Heatmap de Rendimiento

Grid Search: numTrees vs maxDepth

         numTrees →
maxDepth ↓    50      100     200
    5      0.8234   0.8345   0.8412
   10      0.8456   0.8567   0.8623  ← max
   15      0.8389   0.8512   0.8589
   20      0.8312   0.8445   0.8534

🔥 Mejor: (numTrees=200, maxDepth=10) → AUC=0.8623

5. TrainValidationSplit: Alternativa Rápida

Comparación con CrossValidator

TrainValidationSplit es una alternativa más rápida a CrossValidator que usa una sola división train/val en lugar de K-fold CV.

CrossValidator (K-fold CV)
$$ CV(\lambda) = \frac{1}{K} \sum_{k=1}^{K} L(f_\lambda^{(-k)}, D_k) $$
  • K entrenamientos por configuración
  • • Estimador más robusto (menos varianza)
  • Más costoso (K veces más lento)
  • • Recomendado para datasets pequeños/medianos
TrainValidationSplit
$$ \text{Val}(\lambda) = L(f_\lambda^{\text{train}}, D_{\text{val}}) $$
  • 1 entrenamiento por configuración
  • • Más rápido (K veces más que CV)
  • • Mayor varianza (depende del split)
  • • Recomendado para datasets grandes (>100K)

Implementación con TrainValidationSplit

from pyspark.ml.tuning import TrainValidationSplit, ParamGridBuilder
from pyspark.ml.classification import RandomForestClassifier
from pyspark.ml.evaluation import BinaryClassificationEvaluator

# ============================================================
# Configuración (similar a CrossValidator)
# ============================================================
rf = RandomForestClassifier(featuresCol="features", labelCol="label", seed=42)

paramGrid = ParamGridBuilder() \
    .addGrid(rf.numTrees, [50, 100, 200]) \
    .addGrid(rf.maxDepth, [10, 15, 20]) \
    .build()

evaluator = BinaryClassificationEvaluator(
    labelCol="label",
    metricName="areaUnderROC"
)

# ============================================================
# TrainValidationSplit en lugar de CrossValidator
# ============================================================
tvs = TrainValidationSplit(
    estimator=rf,
    estimatorParamMaps=paramGrid,
    evaluator=evaluator,
    trainRatio=0.8,          # 80% train, 20% val (parámetro clave)
    parallelism=4,
    seed=42
)

# Complejidad: 9 configs × 1 split = 9 entrenamientos
# (vs CrossValidator con K=5: 9 × 5 = 45 entrenamientos)

# ============================================================
# Entrenar
# ============================================================
print("Iniciando TrainValidationSplit...")
print(f"Configuraciones: {len(paramGrid)}")
print("Split: 80% train / 20% val")

tvsModel = tvs.fit(df_train)

# ============================================================
# Extraer resultados (API idéntica a CrossValidator)
# ============================================================
bestModel = tvsModel.bestModel
avg_metrics = tvsModel.validationMetrics  # Nota: "validationMetrics", no "avgMetrics"

best_idx = avg_metrics.index(max(avg_metrics))
best_auc = avg_metrics[best_idx]

print(f"\nMejor AUC (validación): {best_auc:.4f}")
print(f"Mejor numTrees: {bestModel.getNumTrees}")
print(f"Mejor maxDepth: {bestModel.getMaxDepth()}")

# ============================================================
# Evaluación en test
# ============================================================
predictions = bestModel.transform(df_test)
test_auc = evaluator.evaluate(predictions)
print(f"AUC en test: {test_auc:.4f}")

# ============================================================
# Comparación de tiempos (ejemplo hipotético)
# ============================================================
# TrainValidationSplit: 15 minutos (9 entrenamientos)
# CrossValidator (K=5): 75 minutos (45 entrenamientos)
# → 5x más rápido con TrainValidationSplit

¿Cuándo usar cada uno?

Escenario Recomendación Razón
N < 10K CrossValidator Dataset pequeño necesita estimador robusto
10K < N < 100K CrossValidator (K=5) Balance entre robustez y tiempo
N > 100K TrainValidationSplit Dataset grande, single split suficiente
Grid muy grande (>500 configs) TrainValidationSplit Reduce tiempo drásticamente
Producción final CrossValidator Mayor confianza en el estimador

6. Visualización Avanzada de Resultados

Análisis Visual del Grid Search

Visualizar los resultados ayuda a entender el landscape del espacio de hiperparámetros y detectar patrones de interacción.

import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np

# Supongamos results_df ya construido (ver Bloque 4)

# ============================================================
# 1. Distribución de Métricas
# ============================================================
plt.figure(figsize=(10, 6))
plt.hist(results_df["avg_metric"], bins=30, color="#a855f7", alpha=0.7, edgecolor="black")
plt.axvline(max(results_df["avg_metric"]), color="red", linestyle="--",
            label=f"Mejor: {max(results_df['avg_metric']):.4f}")
plt.xlabel("AUC-ROC (Cross-Validation)")
plt.ylabel("Frecuencia")
plt.title("Distribución de Rendimiento: Grid Search")
plt.legend()
plt.grid(alpha=0.3)
plt.show()

# ============================================================
# 2. Boxplot por Hiperparámetro
# ============================================================
fig, axes = plt.subplots(2, 2, figsize=(14, 10))

for idx, param in enumerate(["numTrees", "maxDepth", "maxBins", "subsamplingRate"]):
    if param in results_df.columns:
        ax = axes[idx // 2, idx % 2]
        results_df.boxplot(column="avg_metric", by=param, ax=ax)
        ax.set_title(f"Impacto de {param}")
        ax.set_xlabel(param)
        ax.set_ylabel("AUC-ROC")
        ax.get_figure().suptitle("")  # Remover título general

plt.tight_layout()
plt.show()

# ============================================================
# 3. Heatmap de Interacciones (2D)
# ============================================================
# Ejemplo: numTrees vs maxDepth
if "numTrees" in results_df.columns and "maxDepth" in results_df.columns:
    pivot = results_df.pivot_table(
        values="avg_metric",
        index="maxDepth",
        columns="numTrees",
        aggfunc="mean"  # Promedio si hay múltiples valores
    )

    plt.figure(figsize=(10, 8))
    sns.heatmap(pivot, annot=True, fmt=".4f", cmap="viridis", cbar_kws={"label": "AUC-ROC"})
    plt.title("Heatmap: numTrees vs maxDepth")
    plt.xlabel("numTrees")
    plt.ylabel("maxDepth")
    plt.show()

# ============================================================
# 4. Parallel Coordinates Plot (multidimensional)
# ============================================================
from pandas.plotting import parallel_coordinates

# Seleccionar top 20 configs
top20 = results_df.nlargest(20, "avg_metric").copy()
top20["rank"] = range(1, 21)

# Normalizar valores para visualización
for col in ["numTrees", "maxDepth", "maxBins", "subsamplingRate"]:
    if col in top20.columns:
        top20[f"{col}_norm"] = (top20[col] - top20[col].min()) / (top20[col].max() - top20[col].min())

plt.figure(figsize=(12, 6))
parallel_coordinates(
    top20,
    class_column="rank",
    cols=[f"{c}_norm" for c in ["numTrees", "maxDepth", "maxBins", "subsamplingRate"] if c in top20.columns],
    colormap="viridis"
)
plt.title("Parallel Coordinates: Top 20 Configuraciones")
plt.ylabel("Valor Normalizado [0, 1]")
plt.legend(loc="upper right", fontsize=8)
plt.show()

# ============================================================
# 5. Scatter Matrix (pairwise relationships)
# ============================================================
from pandas.plotting import scatter_matrix

scatter_matrix(
    results_df[["numTrees", "maxDepth", "avg_metric"]],
    figsize=(10, 10),
    diagonal="kde",
    alpha=0.5,
    c=results_df["avg_metric"],
    cmap="viridis"
)
plt.suptitle("Scatter Matrix: Relaciones entre Hiperparámetros")
plt.show()

Insights de la Visualización

  • 1.
    Distribución de métricas: Si es bimodal, sugiere dos regímenes distintos (ej: modelos simples vs complejos)
  • 2.
    Boxplots: Identificar hiperparámetros con mayor impacto (mayor spread) vs irrelevantes (métricas similares)
  • 3.
    Heatmaps: Detectar interacciones no lineales (ej: maxDepth=20 solo funciona bien con numTrees>100)
  • 4.
    Parallel coords: Visualizar patrones en top configs (ej: todas tienen subsamplingRate>0.8)
Anterior: Grid & Random Search Siguiente: Reto Completo