Saltar a contenido

Assignment UT3-10: PCA y Feature Selection

🔁 CONTINUACIÓN DE TAREAS ANTERIORES: Este assignment es la tercera parte de una serie usando Ames Housing: - UT3-08: Feature Engineering - Creaste features derivadas, ratios, transformaciones - UT3-09: Encoding Avanzado - Aplicaste diferentes técnicas de encoding (aunque usaste Adult Income) - UT3-10 (ESTA): PCA y Feature Selection - Reducción dimensional y selección de features

🎯 Objetivos

  • Implementar PCA y analizar componentes principales
  • Aplicar Feature Selection con múltiples métodos
  • Comparar PCA vs Feature Selection
  • Evaluar trade-offs en contexto de negocio

⏱️ Tiempo Estimado: 90-110 minutos

💡 OPTIMIZACIÓN: Esta tarea asume que ya conoces el dataset Ames Housing de tareas anteriores. El preprocesamiento es mínimo (una función de ~30 líneas) para enfocarnos directamente en PCA y Feature Selection, que son los temas principales.

⚠️ NOTA IMPORTANTE: Los wrapper methods (Forward, Backward, RFE) son lentos (~2-3 min cada uno). Ten paciencia, es normal.


📊 Dataset: Ames Housing (Resumen Rápido)

Fuente: Kaggle / Dean De Cock (Iowa State University) URL: https://www.kaggle.com/c/house-prices-advanced-regression-techniques

📁 Si ya tienes el dataset: Perfecto, úsalo directamente.
📥 Si no lo tienes: Descarga train.csv del link de arriba o usa Kaggle API: kaggle competitions download -c house-prices-advanced-regression-techniques

Contexto de negocio (resumen): Eres data scientist en una empresa de bienes raíces (real estate) que necesita predecir precios de casas con precisión. La empresa tiene 80+ características de cada propiedad (desde calidad de cocina hasta año de construcción), y necesitas:

  1. Identificar qué características realmente importan para el precio de venta
  2. Reducir la complejidad del modelo para que sea más rápido y mantenible
  3. Explicar a agentes inmobiliarios qué factores considerar al tasar una propiedad
  4. Evitar overfitting eliminando features redundantes o irrelevantes

Descripción del Dataset:

  • ~2900 casas vendidas en Ames, Iowa (2006-2010)
  • Target: SalePrice - Precio de venta en dólares (regresión continua)
  • ~80 features divididas en:
  • Dimensiones: LotArea, GrLivArea, TotalBsmtSF, GarageArea, 1stFlrSF, 2ndFlrSF
  • Calidad: OverallQual, OverallCond, KitchenQual, ExterQual, BsmtQual
  • Temporales: YearBuilt, YearRemodAdd, GarageYrBlt, YrSold
  • Categóricas: Neighborhood, HouseStyle, RoofStyle, Exterior, Foundation (~40 categóricas)
  • Numéricas discretas: BedroomAbvGr, FullBath, Fireplaces, GarageCars
  • Booleanas: CentralAir, PavedDrive, Street

Parte 1: Setup Rápido - Dataset Ames Housing (10 min)

⏱️ Tiempo esperado: ~10 minutos

💡 NOTA: Este dataset ya lo usaste en tareas anteriores (Feature Engineering, Encoding). Si ya tienes el dataset descargado y procesado, esta parte te tomará solo 5 minutos.

Paso 1.1: Setup Rápido - Cargar y Preprocesar

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from sklearn.impute import SimpleImputer
import warnings
warnings.filterwarnings('ignore')

# Configuración
np.random.seed(42)
plt.style.use('default')
sns.set_palette("husl")

print("=" * 80)
print("ASSIGNMENT UT3-10: PCA y Feature Selection - Ames Housing Dataset")
print("=" * 80)

# ========== FUNCIÓN DE CARGA Y PREPROCESAMIENTO RÁPIDO ==========
def quick_load_and_preprocess_ames(filepath='train.csv'):
    """
    Carga y preprocesa Ames Housing en un solo paso
    (Ya hiciste esto en tareas anteriores, aquí es versión simplificada)
    """
    print("\n🏠 CARGANDO Y PREPROCESANDO AMES HOUSING...")

    # Cargar dataset
    df = pd.read_csv(filepath)
    print(f"✅ Dataset cargado: {df.shape[0]:,} casas, {df.shape[1]} columnas")

    # Eliminar 'Id' (no predictivo)
    df = df.drop('Id', axis=1, errors='ignore')

    # Identificar tipos de variables
    numerical_cols = df.select_dtypes(include=['int64', 'float64']).columns.tolist()
    categorical_cols = df.select_dtypes(include=['object']).columns.tolist()

    # Remover target de features
    if 'SalePrice' in numerical_cols:
        numerical_cols.remove('SalePrice')

    print(f"✅ Features numéricas: {len(numerical_cols)}")
    print(f"✅ Features categóricas: {len(categorical_cols)}")

    # Imputar valores faltantes
    num_imputer = SimpleImputer(strategy='median')
    df[numerical_cols] = num_imputer.fit_transform(df[numerical_cols])

    cat_imputer = SimpleImputer(strategy='most_frequent')
    df[categorical_cols] = cat_imputer.fit_transform(df[categorical_cols])

    print(f"✅ Missing values imputados")

    # Label encoding para categóricas
    le = LabelEncoder()
    for col in categorical_cols:
        df[col] = le.fit_transform(df[col].astype(str))

    print(f"✅ Categóricas encoded")

    # Separar X y y
    X = df.drop('SalePrice', axis=1)
    y = df['SalePrice']

    print(f"\n✅ DATASET LISTO:")
    print(f"   X shape: {X.shape} ({X.shape[1]} features)")
    print(f"   y shape: {y.shape}")
    print(f"   Precio promedio: ${y.mean():,.0f}")
    print(f"   Precio mediana: ${y.median():,.0f}")

    return X, y, X.columns.tolist()

# ========== EJECUTAR CARGA RÁPIDA ==========
# TODO: Completa con el path correcto
X, y, feature_names = quick_load_and_preprocess_ames(______)

print(f"\n📊 RESUMEN DEL DATASET:")
print(f"   Total features: {X.shape[1]}")
print(f"   Total casas: {X.shape[0]:,}")
print(f"   Ejemplos de features: {feature_names[:10]}")

💡 Pista: Pasa el nombre del archivo CSV como string. Debe estar en el mismo directorio que tu notebook/script.
📖 Documentación: pandas.read_csv()

📚 RECORDATORIO DE TAREAS ANTERIORES:

Si necesitas refrescar conceptos de preprocesamiento: - Tarea 08 (Feature Engineering): Creación de features derivadas, ratios, transformaciones - Tarea 09 (Encoding Avanzado): Label, One-Hot, Target Encoding - Esta tarea (10): Nos enfocamos en reducción dimensional (PCA) y feature selection

🤔 Pregunta Reflexiva 1:

  1. ¿Con 80+ features, esperarías que todas sean igualmente importantes para predecir precio?
  2. ¿Qué problemas puede causar tener tantas features? (Piensa en: overfitting, velocidad, interpretabilidad)
  3. ¿Conoces la diferencia entre PCA (transformar features) y Feature Selection (seleccionar features)?

Parte 2: PCA - Análisis de Componentes Principales (35 min)

⏱️ Tiempo esperado: ~35 minutos (PCA con 80 features toma más tiempo)

💡 Contexto de Negocio: Con 80+ features, el modelo es complejo, lento de entrenar, y difícil de interpretar. PCA te permitirá reducir a 10-15 componentes mientras mantienes 80-90% de la información. Esto es crítico para: - ⚡ Velocidad: Modelos más rápidos de entrenar y predecir - 🧠 Prevenir overfitting: Menos features = menos riesgo de sobreajuste - 💰 Costos computacionales: Menos features = menos recursos necesarios

Paso 2.1: Estandarización (Crítico para PCA)

from sklearn.preprocessing import StandardScaler

# ========== ESTANDARIZACIÓN ==========
print("=== ESTANDARIZACIÓN DE FEATURES ===")
print("⚠️ PCA es sensible a escala. SIEMPRE estandarizar antes de PCA.")

# TODO: Estandarizar datos
scaler = StandardScaler()
X_scaled = ______

# Verificar estandarización: mean ≈ 0, std ≈ 1
print(f"\n✅ Mean después de scaling: {X_scaled.mean():.6f} (esperado: ~0)")
print(f"✅ Std después de scaling: {X_scaled.std():.6f} (esperado: ~1)")

# Verificar shape
print(f"✅ X_scaled shape: {______}")

# Comparar antes vs después
print(f"\n=== COMPARACIÓN ANTES vs DESPUÉS ===")
print(f"Antes - Mean GrLivArea: {X['GrLivArea'].mean():.0f}, Std: {X['GrLivArea'].std():.0f}")
print(f"Después - Mean GrLivArea: {X_scaled[:, X.columns.get_loc('GrLivArea')].mean():.6f}, Std: {X_scaled[:, X.columns.get_loc('GrLivArea')].std():.6f}")

💡 Pistas:

  • Primer blank: Usa el método del scaler que transforma los datos (combina fit y transform).
  • Segundo blank: Accede al atributo que contiene las dimensiones del array resultante.

📖 Documentación: StandardScaler

🤔 Pregunta Reflexiva 2:

  1. ¿Por qué PCA requiere estandarización? ¿Qué pasaría si no estandarizas?
  2. ¿Qué features crees que dominarían PC1 si NO estandarizamos (hint: piensa en escala)?
  3. ¿Hay algún caso donde NO deberías estandarizar antes de PCA?

Paso 2.2: Aplicar PCA Completo (80 Componentes)

from sklearn.decomposition import PCA
import time

# ========== APLICAR PCA SIN RESTRICCIONES ==========
print("\n=== APLICANDO PCA ===")
print("⏱️ Esto puede tomar 10-20 segundos con 80 features...")

start_time = time.time()

# TODO: Aplicar PCA sin restricción de componentes
pca = PCA()  # Sin n_components = todos los componentes posibles
X_pca = ______

elapsed_time = time.time() - start_time
print(f"✅ PCA completado en {elapsed_time:.2f} segundos")

# ========== ANALIZAR VARIANZA EXPLICADA ==========
explained_variance = pca.explained_variance_ratio_
cumulative_variance = np.cumsum(explained_variance)

print(f"\n=== ANÁLISIS DE COMPONENTES PRINCIPALES ===")
print(f"Total de componentes generados: {______}")
print(f"\nVarianza explicada por componentes principales:")
print(f"  PC1: {explained_variance[0]:.3%} (¡la más importante!)")
print(f"  PC2: {explained_variance[1]:.3%}")
print(f"  PC3: {explained_variance[2]:.3%}")
print(f"  PC4: {explained_variance[3]:.3%}")
print(f"  PC5: {explained_variance[4]:.3%}")

# TODO: Top 10 componentes
print("\n=== TOP 10 COMPONENTES ===")
for i in range(min(10, len(explained_variance))):
    print(f"PC{i+1:2d}: Individual {explained_variance[i]:6.3%} | Acumulada {cumulative_variance[i]:6.3%}")

💡 Pistas:

  • Primer blank: Usa el método del objeto pca que ajusta y transforma en un solo paso.
  • Segundo blank: Accede al atributo del objeto pca que guarda el número de componentes resultantes.

📖 Documentación: PCA en scikit-learn

🤔 Pregunta Reflexiva 3:

  1. ¿PC1 captura más varianza que en datasets típicos? ¿Por qué crees que pasa esto?
  2. ¿Cuántos componentes capturan ~50% de la varianza? ¿Te sorprende?
  3. Si tuvieras que explicar PC1 a un agente inmobiliario, ¿cómo lo harías?

Paso 2.3: Scree Plot y Decisión de Dimensionalidad

# ========== CREAR SCREE PLOT ==========
print("\n=== SCREE PLOT: VISUALIZACIÓN DE VARIANZA ===")

# TODO: Crear scree plot con 80 componentes
plt.figure(figsize=(16, 6))

# Subplot 1: Varianza individual (primeros 30 componentes para claridad)
plt.subplot(1, 2, 1)
n_to_show = min(30, len(explained_variance))
plt.bar(range(1, n_to_show + 1), explained_variance[:n_to_show], alpha=0.7, color='steelblue')
plt.xlabel('Componente Principal', fontsize=12)
plt.ylabel('Varianza Explicada (Individual)', fontsize=12)
plt.title(f'Scree Plot - Primeros {n_to_show} Componentes', fontsize=14)
plt.grid(axis='y', alpha=0.3)

# Subplot 2: Varianza acumulada (TODOS los componentes)
plt.subplot(1, 2, 2)
plt.plot(range(1, len(cumulative_variance) + 1), cumulative_variance, 'o-', 
         color='steelblue', markersize=4, linewidth=2)

# Líneas de referencia
plt.axhline(y=______, color='r', linestyle='--', label='80% varianza', linewidth=2)
plt.axhline(y=______, color='g', linestyle='--', label='90% varianza', linewidth=2)
plt.axhline(y=______, color='orange', linestyle='--', label='95% varianza', linewidth=2)

plt.xlabel('Número de Componentes', fontsize=12)
plt.ylabel('Varianza Acumulada', fontsize=12)
plt.title('Varianza Acumulada por Número de Componentes', fontsize=14)
plt.legend(fontsize=11)
plt.grid(alpha=0.3)
plt.ylim([0, 1.05])

plt.tight_layout()
plt.show()

# ========== DECISIÓN DE DIMENSIONALIDAD ==========
print("\n=== DECISIÓN: ¿CUÁNTOS COMPONENTES NECESITAMOS? ===")

# TODO: Calcular componentes necesarios para diferentes umbrales
n_components_80 = ______
n_components_90 = ______
n_components_95 = ______

print(f"📊 Para 80% de varianza: {n_components_80} componentes")
print(f"📊 Para 90% de varianza: {n_components_90} componentes")
print(f"📊 Para 95% de varianza: {n_components_95} componentes")

# Análisis de reducción dimensional
original_features = X.shape[1]
reduction_80 = (1 - n_components_80 / original_features) * 100
reduction_90 = (1 - n_components_90 / original_features) * 100
reduction_95 = (1 - n_components_95 / original_features) * 100

print(f"\n🎯 IMPACTO DE REDUCCIÓN DIMENSIONAL:")
print(f"   Original: {original_features} features")
print(f"   80% varianza: {original_features}{n_components_80} ({reduction_80:.1f}% reducción)")
print(f"   90% varianza: {original_features}{n_components_90} ({reduction_90:.1f}% reducción)")
print(f"   95% varianza: {original_features}{n_components_95} ({reduction_95:.1f}% reducción)")

print(f"\n💡 RECOMENDACIÓN PRÁCTICA:")
print(f"   Para este assignment, usaremos {n_components_80} componentes (80% varianza)")
print(f"   Esto balancea reducción dimensional con retención de información")

💡 Pistas:

  • Para las líneas de referencia: Usa valores decimales (0.80 = 80%, 0.90 = 90%, 0.95 = 95%).
  • Para calcular componentes necesarios: Usa np.argmax() para encontrar el índice donde la varianza acumulada alcanza o supera el umbral, y suma 1 (los índices empiezan en 0).

📖 Documentación: matplotlib.pyplot.axhline | numpy.argmax

🤔 Pregunta Reflexiva 4:

  1. ¿Con 80% de varianza reduces a cuántas dimensiones? ¿Es una reducción significativa?
  2. ¿Preferirías 80%, 90% o 95% de varianza para un modelo de producción? ¿Por qué?
  3. ¿El trade-off entre simplicidad y información retenida vale la pena en este caso?

Paso 2.4: Interpretación de Loadings (¿Qué representa cada PC?)

# ========== OBTENER LOADINGS ==========
print("\n=== INTERPRETACIÓN DE COMPONENTES PRINCIPALES ===")
print("Los loadings te dicen qué features originales contribuyen a cada componente")

# TODO: Obtener loadings de PC1 y PC2
loadings = pca.components_.T * np.sqrt(pca.explained_variance_)

# Crear DataFrame de loadings para PC1 y PC2
loadings_df = pd.DataFrame(
    loadings[:, :2],
    columns=['PC1', 'PC2'],
    index=X.columns
)

# ========== FEATURES MÁS IMPORTANTES PARA PC1 ==========
print("\n=== PC1: COMPONENTE PRINCIPAL #1 ===")
print(f"Explica {explained_variance[0]:.1%} de la varianza total")
print(f"\nTop 10 features más importantes para PC1:")
pc1_top = loadings_df['PC1'].abs().nlargest(10)
for i, (feature, loading_abs) in enumerate(pc1_top.items(), 1):
    loading_val = loadings_df.loc[feature, 'PC1']
    direction = "↑ positivo" if loading_val > 0 else "↓ negativo"
    print(f"  {i:2d}. {feature:20s}: {loading_val:+7.3f} ({direction})")

# ========== FEATURES MÁS IMPORTANTES PARA PC2 ==========
print("\n=== PC2: COMPONENTE PRINCIPAL #2 ===")
print(f"Explica {explained_variance[1]:.1%} de la varianza total")
print(f"\nTop 10 features más importantes para PC2:")
pc2_top = loadings_df['PC2'].abs().nlargest(10)
for i, (feature, loading_abs) in enumerate(pc2_top.items(), 1):
    loading_val = loadings_df.loc[feature, 'PC2']
    direction = "↑ positivo" if loading_val > 0 else "↓ negativo"
    print(f"  {i:2d}. {feature:20s}: {loading_val:+7.3f} ({direction})")

# ========== VISUALIZAR LOADINGS (solo top features para claridad) ==========
print("\n=== VISUALIZACIÓN DE LOADINGS ===")

# Seleccionar top features para visualizar (top 15 de PC1 o PC2)
top_features_pc1 = set(loadings_df['PC1'].abs().nlargest(15).index)
top_features_pc2 = set(loadings_df['PC2'].abs().nlargest(15).index)
top_features = list(top_features_pc1.union(top_features_pc2))

loadings_df_viz = loadings_df.loc[top_features]

plt.figure(figsize=(14, 10))
plt.scatter(loadings_df_viz['PC1'], loadings_df_viz['PC2'], alpha=0.7, s=150, c='steelblue', edgecolors='black')

# Anotar features
for feature in loadings_df_viz.index:
    plt.annotate(feature, 
                (loadings_df_viz.loc[feature, 'PC1'], loadings_df_viz.loc[feature, 'PC2']), 
                fontsize=10, alpha=0.9, ha='center')

plt.xlabel(f'PC1 Loadings ({explained_variance[0]:.1%} varianza explicada)', fontsize=13)
plt.ylabel(f'PC2 Loadings ({explained_variance[1]:.1%} varianza explicada)', fontsize=13)
plt.title('Loadings Plot - Top Features en PC1 y PC2\n(Features más influyentes en componentes principales)', fontsize=14)
plt.axhline(y=0, color='k', linestyle='-', alpha=0.3, linewidth=1)
plt.axvline(x=0, color='k', linestyle='-', alpha=0.3, linewidth=1)
plt.grid(alpha=0.3)
plt.tight_layout()
plt.show()

# ========== INTERPRETACIÓN DE NEGOCIO ==========
print("\n=== 💼 INTERPRETACIÓN DE NEGOCIO ===")
print("\nPC1 representa principalmente (top 3):")
for i, (feature, loading_abs) in enumerate(loadings_df['PC1'].abs().nlargest(3).items(), 1):
    loading_val = loadings_df.loc[feature, 'PC1']
    direction = "positivamente" if loading_val > 0 else "negativamente"
    print(f"  {i}. {feature}: Contribuye {direction}")

print("\n💡 Interpretación: PC1 probablemente captura el 'tamaño/calidad general' de la casa")

print("\nPC2 representa principalmente (top 3):")
for i, (feature, loading_abs) in enumerate(loadings_df['PC2'].abs().nlargest(3).items(), 1):
    loading_val = loadings_df.loc[feature, 'PC2']
    direction = "positivamente" if loading_val > 0 else "negativamente"
    print(f"  {i}. {feature}: Contribuye {direction}")

print("\n💡 Interpretación: PC2 probablemente captura otra dimensión (edad, ubicación, etc.)")

🤔 Pregunta Reflexiva 5:

  1. ¿Las features que dominan PC1 tienen sentido para predecir precio de una casa?
  2. ¿PC1 y PC2 son interpretables o son "cajas negras"? ¿Por qué importa esto?
  3. ¿Cómo le explicarías a un agente inmobiliario qué significa PC1 en términos de negocio?

Paso 2.5: Feature Selection Basada en PCA Loadings (CRÍTICO)

💡 CONCEPTO CLAVE: En lugar de usar PC1, PC2... como features, vamos a identificar las features ORIGINALES que más contribuyen a los PCs importantes, y usar ESAS features originales.

# ========== FEATURE SELECTION BASADA EN PCA LOADINGS ==========
print("\n=== FEATURE SELECTION BASADA EN PCA LOADINGS ===")
print("💡 En lugar de usar PC1, PC2... usaremos las features ORIGINALES")
print("   que tienen mayor loading (peso) en los componentes principales")

# Decidir cuántos componentes considerar
n_top_components = ______

# Obtener loadings absolutos de todos los componentes importantes
print(f"\n🔍 Analizando loadings de los primeros {n_top_components} componentes...")

# Para cada componente, obtener las features con mayor loading absoluto
all_loadings = pca.components_[:n_top_components, :]  # Primeros n componentes

# Crear DataFrame con loadings de todos los componentes
loadings_all = pd.DataFrame(
    all_loadings.T,
    columns=[f'PC{i+1}' for i in range(n_top_components)],
    index=X.columns
)

# ========== ESTRATEGIA: SUMAR LOADINGS ABSOLUTOS ==========
# Para cada feature, sumar su importancia (loading absoluto) en todos los componentes
print("\n📊 ESTRATEGIA: Ranking de features por suma de loadings absolutos")

# TODO: Calcular importancia total de cada feature
feature_importance_from_pca = ______

# Ordenar por importancia
feature_importance_from_pca = feature_importance_from_pca.sort_values(ascending=False)

print(f"\n🔝 TOP 20 FEATURES POR IMPORTANCIA EN PCA:")
for i, (feature, importance) in enumerate(feature_importance_from_pca.head(20).items(), 1):
    print(f"  {i:2d}. {feature:25s}: {importance:.4f}")

# ========== SELECCIONAR TOP-K FEATURES ==========
k = n_components_80  # Mismo número que usamos con PCA reducido

print(f"\n✅ Seleccionando top {k} features originales basadas en loadings de PCA")

# TODO: Seleccionar features
selected_features_pca = ______

print(f"\n📋 Features seleccionadas ({k}):")
for i, feat in enumerate(selected_features_pca, 1):
    print(f"  {i:2d}. {feat}")

# ========== PREPARAR DATASET CON FEATURES SELECCIONADAS ==========
X_pca_selected = X_scaled[:, X.columns.isin(selected_features_pca)]

print(f"\n✅ Dataset con features seleccionadas por PCA:")
print(f"   Shape: {X_pca_selected.shape}")
print(f"   Reducción: {X.shape[1]}{X_pca_selected.shape[1]} features")

# ========== VISUALIZAR COMPARACIÓN ==========
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))

# Top 20 features por importancia PCA
ax1.barh(range(20), feature_importance_from_pca.head(20).values, color='steelblue', alpha=0.7)
ax1.set_yticks(range(20))
ax1.set_yticklabels(feature_importance_from_pca.head(20).index, fontsize=9)
ax1.set_xlabel('Importancia Total (Suma de Loadings Absolutos)', fontsize=11)
ax1.set_title('Top 20 Features por Importancia en PCA\n(Features originales con mayor peso en componentes)', fontsize=12)
ax1.invert_yaxis()
ax1.grid(axis='x', alpha=0.3)

# Distribución de importancias
ax2.hist(feature_importance_from_pca, bins=30, alpha=0.7, color='lightgreen', edgecolor='black')
ax2.set_xlabel('Importancia', fontsize=11)
ax2.set_ylabel('Frecuencia', fontsize=11)
ax2.set_title('Distribución de Importancia de Features', fontsize=12)
ax2.axvline(feature_importance_from_pca.iloc[k-1], color='red', linestyle='--', 
            label=f'Umbral (top {k})', linewidth=2)
ax2.legend(fontsize=11)
ax2.grid(alpha=0.3)

plt.tight_layout()
plt.show()

print("\n💡 INTERPRETACIÓN:")
print("   Estas features originales son las que 'explican' los componentes principales")
print("   Ventaja: Mantienen interpretabilidad (puedes decir 'GrLivArea importa')")
print("   Diferencia con PCA: Usas features originales, no combinaciones lineales")

💡 Pistas:

  • Primer blank: Usa la variable que calculaste anteriormente para 80% de varianza.
  • Segundo blank: Calcula la suma de valores absolutos del DataFrame de loadings a lo largo del eje de componentes (.abs().sum(axis=1)).
  • Tercer blank: Usa el método para obtener los k elementos más grandes y extrae sus índices como lista (.nlargest(k).index.tolist()).

📖 Documentación: pandas.DataFrame.abs | pandas.Series.nlargest

🤔 Pregunta Reflexiva 5.5 (Nueva):

  1. ¿Las features seleccionadas por loadings de PCA coinciden con tu intuición?
  2. ¿Por qué este enfoque es diferente a usar directamente PC1, PC2, etc.?
  3. ¿Cuál método preferirías para un negocio: PCA completo o features basadas en loadings? ¿Por qué?

Paso 2.6: Evaluación de Performance con REGRESIÓN (RMSE y R²)

⏱️ Tiempo esperado: ~3-5 minutos (cross-validation puede ser lento con 80 features)

from sklearn.ensemble import RandomForestRegressor  # ⚠️ REGRESSOR, no Classifier
from sklearn.model_selection import cross_val_score
from sklearn.metrics import mean_squared_error, r2_score, mean_absolute_error
import warnings
warnings.filterwarnings('ignore')  # Suprimir warnings de convergencia

# ========== MODELO BASELINE: TODAS LAS FEATURES ORIGINALES ==========
print("\n=== EVALUACIÓN DE PERFORMANCE: PCA vs ORIGINAL ===")
print("⏱️ Esto puede tomar 1-2 minutos (cross-validation con 80 features)...\n")

# TODO: Evaluar modelo con datos originales
print("🔄 Evaluando modelo con features originales...")
rf_original = RandomForestRegressor(
    random_state=42, 
    n_estimators=______,
    max_depth=______,
    n_jobs=-1             # Usar todos los cores
)

# Usar neg_mean_squared_error y neg_mean_absolute_error para CV
scores_mse_original = -cross_val_score(rf_original, X_scaled, y, cv=5, 
                                        scoring='neg_mean_squared_error', n_jobs=-1)
scores_r2_original = cross_val_score(rf_original, X_scaled, y, cv=5, 
                                     scoring='r2', n_jobs=-1)

rmse_original = np.sqrt(scores_mse_original)

print(f"\n✅ BASELINE - Features Originales ({X.shape[1]} features):")
print(f"   RMSE: ${rmse_original.mean():,.0f} ± ${rmse_original.std():,.0f}")
print(f"   R²:   {scores_r2_original.mean():.4f} ± {scores_r2_original.std():.4f}")
print(f"   Scores RMSE: {[f'${x:,.0f}' for x in rmse_original]}")

# ========== MODELO CON PCA (80% VARIANZA) ==========
print(f"\n🔄 Evaluando modelo con PCA ({n_components_80} componentes)...")

# TODO: Aplicar PCA reducido
pca_reduced = PCA(n_components=______)
X_pca_reduced = pca_reduced.fit_transform(X_scaled)

print(f"✅ PCA transformado: {X.shape[1]}{X_pca_reduced.shape[1]} features")

# Evaluar con PCA
rf_pca = RandomForestRegressor(
    random_state=42, 
    n_estimators=100, 
    max_depth=15,
    n_jobs=-1
)

scores_mse_pca = -cross_val_score(rf_pca, X_pca_reduced, y, cv=5, 
                                   scoring='neg_mean_squared_error', n_jobs=-1)
scores_r2_pca = cross_val_score(rf_pca, X_pca_reduced, y, cv=5, 
                                scoring='r2', n_jobs=-1)

rmse_pca = np.sqrt(scores_mse_pca)

print(f"\n✅ PCA - Componentes Reducidos ({n_components_80} componentes):")
print(f"   RMSE: ${rmse_pca.mean():,.0f} ± ${rmse_pca.std():,.0f}")
print(f"   R²:   {scores_r2_pca.mean():.4f} ± {scores_r2_pca.std():.4f}")
print(f"   Scores RMSE: {[f'${x:,.0f}' for x in rmse_pca]}")

# ========== MODELO CON FEATURES SELECCIONADAS POR PCA LOADINGS ==========
print(f"\n🔄 Evaluando modelo con features originales seleccionadas por PCA loadings...")

rf_pca_selected = RandomForestRegressor(
    random_state=42, 
    n_estimators=100, 
    max_depth=15,
    n_jobs=-1
)

scores_mse_pca_selected = -cross_val_score(rf_pca_selected, X_pca_selected, y, cv=5, 
                                             scoring='neg_mean_squared_error', n_jobs=-1)
scores_r2_pca_selected = cross_val_score(rf_pca_selected, X_pca_selected, y, cv=5, 
                                          scoring='r2', n_jobs=-1)

rmse_pca_selected = np.sqrt(scores_mse_pca_selected)

print(f"\n✅ PCA Loadings - Features Originales Seleccionadas ({len(selected_features_pca)} features):")
print(f"   RMSE: ${rmse_pca_selected.mean():,.0f} ± ${rmse_pca_selected.std():,.0f}")
print(f"   R²:   {scores_r2_pca_selected.mean():.4f} ± {scores_r2_pca_selected.std():.4f}")
print(f"   Scores RMSE: {[f'${x:,.0f}' for x in rmse_pca_selected]}")

# ========== ANÁLISIS COMPARATIVO ==========
print(f"\n" + "="*80)
print(f"{'COMPARACIÓN: ORIGINAL vs PCA vs PCA LOADINGS':^80}")
print(f"="*80)

reduction_pct = (1 - n_components_80 / X.shape[1]) * 100
rmse_diff_pca = rmse_pca.mean() - rmse_original.mean()
rmse_diff_pca_selected = rmse_pca_selected.mean() - rmse_original.mean()
r2_diff_pca = scores_r2_pca.mean() - scores_r2_original.mean()
r2_diff_pca_selected = scores_r2_pca_selected.mean() - scores_r2_original.mean()

print(f"\n📊 REDUCCIÓN DIMENSIONAL:")
print(f"   Original: {X.shape[1]} features")
print(f"   PCA: {X.shape[1]}{n_components_80} componentes ({reduction_pct:.1f}% reducción)")
print(f"   PCA Loadings: {X.shape[1]}{len(selected_features_pca)} features originales ({reduction_pct:.1f}% reducción)")
print(f"   Varianza retenida (PCA): {pca_reduced.explained_variance_ratio_.sum():.1%}")

print(f"\n📊 PERFORMANCE COMPARATIVO:")
print(f"\n   {'Método':<25s} {'RMSE':>15s} {'R²':>10s} {'Features':>10s}")
print(f"   {'-'*25} {'-'*15} {'-'*10} {'-'*10}")
print(f"   {'Original':<25s} ${rmse_original.mean():>14,.0f} {scores_r2_original.mean():>10.4f} {X.shape[1]:>10d}")
print(f"   {'PCA Componentes':<25s} ${rmse_pca.mean():>14,.0f} {scores_r2_pca.mean():>10.4f} {n_components_80:>10d}")
print(f"   {'PCA Loadings (Originales)':<25s} ${rmse_pca_selected.mean():>14,.0f} {scores_r2_pca_selected.mean():>10.4f} {len(selected_features_pca):>10d}")

print(f"\n📊 DIFERENCIAS VS ORIGINAL:")
print(f"   PCA Componentes:  RMSE {rmse_diff_pca:+,.0f} ({(rmse_diff_pca/rmse_original.mean())*100:+.1f}%) | R² {r2_diff_pca:+.4f}")
print(f"   PCA Loadings:     RMSE {rmse_diff_pca_selected:+,.0f} ({(rmse_diff_pca_selected/rmse_original.mean())*100:+.1f}%) | R² {r2_diff_pca_selected:+.4f}")

# Interpretación
print(f"\n💡 INTERPRETACIÓN:")
print(f"\n   🔵 PCA Componentes (PC1, PC2...):")
if rmse_pca.mean() < rmse_original.mean() * 1.05:
    print(f"      ✅ Mantiene performance similar con {reduction_pct:.0f}% reducción")
    print(f"      ⚠️ Pero: Componentes son combinaciones lineales (menos interpretables)")
else:
    print(f"      ⚠️ Pierde precisión significativa ({(rmse_diff_pca/rmse_original.mean())*100:.1f}%)")

print(f"\n   🟢 PCA Loadings (Features originales):")
if rmse_pca_selected.mean() < rmse_original.mean() * 1.05:
    print(f"      ✅ Mantiene performance similar con {reduction_pct:.0f}% reducción")
    print(f"      ✅ Plus: Usa features originales (interpretables)")
else:
    print(f"      ⚠️ Pierde precisión ({(rmse_diff_pca_selected/rmse_original.mean())*100:.1f}%)")

print(f"\n   💼 PARA NEGOCIO:")
print(f"      - PCA Componentes: Mejor para modelos 'black box' donde solo importa precisión")
print(f"      - PCA Loadings: Mejor para negocio (puedes decir 'GrLivArea es importante')")

💡 Pistas:

  • n_estimators: Número de árboles en el Random Forest. Valores típicos: 50-200. Más árboles = más lento pero más estable.
  • max_depth: Profundidad máxima de cada árbol. Valores típicos: 10-20. Mayor profundidad = más complejo.
  • Para PCA reducido: Usa la variable que calculaste para 80% de varianza.

📖 Documentación: RandomForestRegressor | PCA | cross_val_score

🤔 Pregunta Reflexiva 6:

  1. ¿El RMSE aumentó o disminuyó con PCA? ¿Es aceptable la diferencia?
  2. ¿R² bajó significativamente? ¿Qué te dice esto sobre la información perdida?
  3. ¿Vale la pena perder {reduction_pct:.0f}% de features si RMSE sube ${rmse_diff:,.0f}?
  4. ¿Para qué casos de negocio preferirías PCA sobre mantener todas las features?
  5. ¿Cómo afecta PCA al tiempo de entrenamiento e inferencia en producción?

Parte 3: Feature Selection - Filter Methods (25 min)

⏱️ Tiempo esperado: ~25 minutos

💡 Contexto de Negocio: A diferencia de PCA (que crea nuevas features abstractas), Feature Selection mantiene las features originales. Esto es CRÍTICO para: - 🗣️ Explicabilidad: Puedes decir "GrLivArea y OverallQual son las más importantes" - 📊 Monitoreo: Detectar feature drift es más fácil - 🤝 Confianza: Stakeholders entienden qué features usa el modelo

Paso 3.1: Filter Method - F-test (ANOVA F-value para Regresión)

from sklearn.feature_selection import SelectKBest, f_regression  # ⚠️ f_regression, no f_classif

# ========== F-TEST PARA REGRESIÓN ==========
print("\n=== FILTER METHOD: F-TEST (ANOVA) ===")
print("F-test mide la relación lineal entre cada feature y el target (SalePrice)")

# TODO: Seleccionar top-k features por F-test
k = n_components_80  # Mismo número que PCA para comparación justa

print(f"\nSeleccionando top {k} features con F-test...")

selector_f = SelectKBest(_______, k=k)
X_filter_f = selector_f.fit_transform(X_scaled, y)

# Identificar features seleccionadas
selected_features_f = X.columns[selector_f.get_support()]
print(f"\n✅ Features seleccionadas por F-test ({k}):")
for i, feat in enumerate(selected_features_f, 1):
    print(f"  {i:2d}. {feat}")

# ========== SCORES DE F-TEST ==========
scores_f = pd.Series(selector_f.scores_, index=X.columns).sort_values(ascending=False)
print(f"\n=== TOP 15 F-SCORES (Mayor correlación con SalePrice) ===")
for i, (feat, score) in enumerate(scores_f.head(15).items(), 1):
    print(f"  {i:2d}. {feat:20s}: {score:,.0f}")

# TODO: Visualizar scores (top 30 para claridad)
plt.figure(figsize=(14, 10))
scores_f.head(30).sort_values(ascending=True).plot(kind='barh', color='steelblue')
plt.xlabel('F-Score (ANOVA)', fontsize=12)
plt.title('Top 30 Features por F-test\n(Mayor F-score = Mayor relación lineal con SalePrice)', fontsize=14)
plt.grid(axis='x', alpha=0.3)
plt.tight_layout()
plt.show()

💡 Pista: Usa la función f_regression que calcula los F-scores de ANOVA para problemas de regresión.

📖 Documentación: SelectKBest | f_regression

🤔 Pregunta Reflexiva 7:

  1. ¿Las features con mayor F-score tienen sentido intuitivamente para predecir precio?
  2. ¿Esperabas que esas features fueran las más importantes? ¿Por qué sí/no?
  3. ¿F-test captura relaciones no-lineales? ¿Cuál es su limitación principal?

Paso 3.2: Filter Method - Mutual Information (Captura relaciones no-lineales)

from sklearn.feature_selection import mutual_info_regression  # ⚠️ mutual_info_regression, no classif

# ========== MUTUAL INFORMATION PARA REGRESIÓN ==========
print("\n=== FILTER METHOD: MUTUAL INFORMATION ===")
print("MI captura relaciones LINEALES Y NO-LINEALES (más flexible que F-test)")
print("⏱️ Esto puede tomar 30-60 segundos...")

# TODO: Seleccionar top-k features por Mutual Information
selector_mi = SelectKBest(_______, k=k)
X_filter_mi = selector_mi.fit_transform(X_scaled, y)

selected_features_mi = X.columns[selector_mi.get_support()]
print(f"\n✅ Features seleccionadas por Mutual Information ({k}):")
for i, feat in enumerate(selected_features_mi, 1):
    print(f"  {i:2d}. {feat}")

# Scores
scores_mi = pd.Series(selector_mi.scores_, index=X.columns).sort_values(ascending=False)
print(f"\n=== TOP 15 MI SCORES ===")
for i, (feat, score) in enumerate(scores_mi.head(15).items(), 1):
    print(f"  {i:2d}. {feat:20s}: {score:.4f}")

# ========== COMPARACIÓN: F-TEST vs MUTUAL INFORMATION ==========
common_features = set(selected_features_f) & set(selected_features_mi)
print(f"\n" + "="*70)
print(f"{'COMPARACIÓN: F-TEST vs MUTUAL INFORMATION':^70}")
print(f"="*70)
print(f"\n📊 Features en común: {len(common_features)} de {k} ({len(common_features)/k*100:.1f}% coincidencia)")

print(f"\n✅ Features comunes (ambos métodos las eligieron):")
for i, feat in enumerate(sorted(common_features), 1):
    print(f"  {i:2d}. {feat}")

print(f"\n🔵 Features SOLO en F-test:")
only_f = set(selected_features_f) - set(selected_features_mi)
for i, feat in enumerate(sorted(only_f), 1):
    print(f"  {i:2d}. {feat}")

print(f"\n🟢 Features SOLO en Mutual Information:")
only_mi = set(selected_features_mi) - set(selected_features_f)
for i, feat in enumerate(sorted(only_mi), 1):
    print(f"  {i:2d}. {feat}")

print(f"\n💡 INTERPRETACIÓN:")
if len(common_features) / k > 0.7:
    print(f"   Alta coincidencia ({len(common_features)/k*100:.0f}%) → Ambos métodos están de acuerdo")
else:
    print(f"   Baja coincidencia ({len(common_features)/k*100:.0f}%) → MI captura relaciones no-lineales diferentes")

💡 Pista: Usa la función mutual_info_regression que estima la información mutua entre cada feature y el target para problemas de regresión.

📖 Documentación: mutual_info_regression

🤔 Pregunta Reflexiva 8:

  1. ¿Por qué MI y F-test eligieron features diferentes? ¿Qué significa esto?
  2. ¿Cuáles features "solo en MI" crees que tienen relación no-lineal con el precio?
  3. ¿Preferirías usar F-test o MI para seleccionar features? ¿Por qué?

Paso 3.3: Evaluar Performance de Filter Methods

# ========== EVALUACIÓN: F-TEST ==========
print("\n=== EVALUANDO FILTER METHODS ===")
print("⏱️ Esto puede tomar 1-2 minutos...\n")

print("🔄 Evaluando F-test...")
rf_filter_f = RandomForestRegressor(random_state=42, n_estimators=100, max_depth=15, n_jobs=-1)

scores_mse_filter_f = -cross_val_score(rf_filter_f, X_filter_f, y, cv=5, 
                                        scoring='neg_mean_squared_error', n_jobs=-1)
scores_r2_filter_f = cross_val_score(rf_filter_f, X_filter_f, y, cv=5, 
                                     scoring='r2', n_jobs=-1)
rmse_filter_f = np.sqrt(scores_mse_filter_f)

print(f"✅ F-test ({k} features):")
print(f"   RMSE: ${rmse_filter_f.mean():,.0f} ± ${rmse_filter_f.std():,.0f}")
print(f"   R²:   {scores_r2_filter_f.mean():.4f} ± {scores_r2_filter_f.std():.4f}")

# ========== EVALUACIÓN: MUTUAL INFORMATION ==========
print(f"\n🔄 Evaluando Mutual Information...")
rf_filter_mi = RandomForestRegressor(random_state=42, n_estimators=100, max_depth=15, n_jobs=-1)

scores_mse_filter_mi = -cross_val_score(rf_filter_mi, X_filter_mi, y, cv=5, 
                                         scoring='neg_mean_squared_error', n_jobs=-1)
scores_r2_filter_mi = cross_val_score(rf_filter_mi, X_filter_mi, y, cv=5, 
                                      scoring='r2', n_jobs=-1)
rmse_filter_mi = np.sqrt(scores_mse_filter_mi)

print(f"✅ Mutual Information ({k} features):")
print(f"   RMSE: ${rmse_filter_mi.mean():,.0f} ± ${rmse_filter_mi.std():,.0f}")
print(f"   R²:   {scores_r2_filter_mi.mean():.4f} ± {scores_r2_filter_mi.std():.4f}")

# ========== COMPARACIÓN ACTUALIZADA ==========
print(f"\n" + "="*70)
print(f"{'COMPARACIÓN: BASELINE vs PCA vs FILTER METHODS':^70}")
print(f"="*70)

comparison_data = {
    'Método': ['Original', 'PCA', 'F-test', 'MI'],
    'N_Features': [X.shape[1], n_components_80, k, k],
    'RMSE': [rmse_original.mean(), rmse_pca.mean(), rmse_filter_f.mean(), rmse_filter_mi.mean()],
    'R²': [scores_r2_original.mean(), scores_r2_pca.mean(), scores_r2_filter_f.mean(), scores_r2_filter_mi.mean()]
}
comparison_df = pd.DataFrame(comparison_data)
comparison_df['Reducción%'] = (1 - comparison_df['N_Features'] / X.shape[1]) * 100

print(f"\n{comparison_df.to_string(index=False)}")

print(f"\n💡 OBSERVACIONES:")
print(f"   - PCA: Reduce a componentes abstractos (no interpretables)")
print(f"   - F-test & MI: Mantienen features originales (interpretables)")
print(f"   - ¿Cuál tiene mejor RMSE? ¿Vale la pena sacrificar interpretabilidad?")

🤔 Pregunta Reflexiva 9:

  1. ¿Cuál método (PCA, F-test, MI) tuvo mejor RMSE? ¿Por qué crees que fue así?
  2. ¿La diferencia en RMSE es suficiente para justificar perder interpretabilidad (PCA)?
  3. Si tuvieras que presentar resultados al equipo de bienes raíces, ¿qué método elegirías?

Parte 4: Feature Selection - Wrapper Methods (20 min)

⏱️ Tiempo esperado: ~20 minutos (reducido usando two-stage selection)

💡 Contexto: Wrapper Methods evalúan subconjuntos de features entrenando el modelo. Son más lentos pero más precisos que Filter Methods.

🚀 ESTRATEGIA OPTIMIZADA: En lugar de aplicar Forward/Backward sobre las ~79 features originales (muy lento), las aplicaremos sobre las features pre-seleccionadas por PCA Loadings. Esto es una estrategia "two-stage" común en la industria: 1. Stage 1: PCA Loadings reduce 79 → ~36-45 features (rápido) 2. Stage 2: Wrapper methods refinan esa selección (más rápido)

Paso 4.1: Forward Selection (Sequential Feature Selection)

from sklearn.feature_selection import SequentialFeatureSelector

# ========== TWO-STAGE SELECTION: PCA + FORWARD ==========
print("\n=== WRAPPER METHOD: FORWARD SELECTION ===")
print("💡 ESTRATEGIA: Forward Selection sobre features pre-seleccionadas por PCA")
print(f"   Stage 1 (ya hecho): PCA Loadings → {len(selected_features_pca)} features")
print(f"   Stage 2 (ahora): Forward Selection → refinar a menos features")
print("⏱️ Esto tomará ~30-60 segundos (mucho más rápido que sobre 79 features)...\n")

# Decidir cuántas features seleccionar con wrapper
k_wrapper = max(15, k // 2)  # Aproximadamente la mitad de las features PCA, o mínimo 15
print(f"🎯 Target: Seleccionar {k_wrapper} features con Forward Selection")

# TODO: Forward Selection sobre features pre-seleccionadas
estimator_forward = RandomForestRegressor(
    random_state=42, 
    n_estimators=______,
    max_depth=______,
    n_jobs=-1
)

selector_forward = SequentialFeatureSelector(
    estimator=estimator_forward, 
    n_features_to_select=k_wrapper,
    direction=______,
    cv=3,              # 3 folds para rapidez
    n_jobs=-1
)

print(f"🔄 Ejecutando Forward Selection sobre {len(selected_features_pca)} features pre-seleccionadas...")
import time
start_time = time.time()
X_forward = selector_forward.fit_transform(X_pca_selected, y)
elapsed_time = time.time() - start_time

print(f"✅ Forward Selection completado en {elapsed_time:.1f} segundos")

# Features seleccionadas (mapear índices de vuelta a nombres)
selected_indices_forward = selector_forward.get_support()
selected_features_forward = [selected_features_pca[i] for i, sel in enumerate(selected_indices_forward) if sel]

print(f"\n✅ Features seleccionadas por Forward Selection ({len(selected_features_forward)}):")
for i, feat in enumerate(selected_features_forward, 1):
    print(f"  {i:2d}. {feat}")

Paso 4.2: Backward Elimination (Sequential Feature Selection)

# ========== TWO-STAGE SELECTION: PCA + BACKWARD ==========
print("\n=== WRAPPER METHOD: BACKWARD ELIMINATION ===")
print("💡 ESTRATEGIA: Backward Elimination sobre features pre-seleccionadas por PCA")
print(f"   Stage 1 (ya hecho): PCA Loadings → {len(selected_features_pca)} features")
print(f"   Stage 2 (ahora): Backward Elimination → refinar a {k_wrapper} features")
print("⏱️ Esto tomará ~30-60 segundos...\n")

# TODO: Backward Selection sobre features pre-seleccionadas
estimator_backward = RandomForestRegressor(
    random_state=42, 
    n_estimators=50,
    max_depth=10,
    n_jobs=-1
)

selector_backward = SequentialFeatureSelector(
    estimator=estimator_backward, 
    n_features_to_select=k_wrapper,
    direction=______,
    cv=3,
    n_jobs=-1
)

print(f"🔄 Ejecutando Backward Elimination sobre {len(selected_features_pca)} features...")
start_time = time.time()
X_backward = selector_backward.fit_transform(X_pca_selected, y)
elapsed_time_backward = time.time() - start_time

print(f"✅ Backward Elimination completado en {elapsed_time_backward:.1f} segundos")

# Features seleccionadas (mapear índices de vuelta a nombres)
selected_indices_backward = selector_backward.get_support()
selected_features_backward = [selected_features_pca[i] for i, sel in enumerate(selected_indices_backward) if sel]

print(f"\n✅ Features seleccionadas por Backward Elimination ({len(selected_features_backward)}):")
for i, feat in enumerate(selected_features_backward, 1):
    print(f"  {i:2d}. {feat}")

# ========== COMPARAR FORWARD VS BACKWARD ==========
print(f"\n" + "="*70)
print(f"{'COMPARACIÓN: FORWARD vs BACKWARD':^70}")
print(f"="*70)

common_features_fb = set(selected_features_forward) & set(selected_features_backward)
print(f"\n📊 Features en común: {len(common_features_fb)} de {k_wrapper} ({len(common_features_fb)/k_wrapper*100:.1f}% coincidencia)")

print(f"\n✅ Features comunes (ambos métodos las eligieron):")
for i, feat in enumerate(sorted(common_features_fb), 1):
    print(f"  {i:2d}. {feat}")

print(f"\n🔵 Features SOLO en Forward:")
only_forward = set(selected_features_forward) - set(selected_features_backward)
for i, feat in enumerate(sorted(only_forward), 1):
    print(f"  {i:2d}. {feat}")

print(f"\n🟢 Features SOLO en Backward:")
only_backward = set(selected_features_backward) - set(selected_features_forward)
for i, feat in enumerate(sorted(only_backward), 1):
    print(f"  {i:2d}. {feat}")

print(f"\n⏱️ TIEMPO DE EJECUCIÓN:")
print(f"   Forward:  {elapsed_time:.1f}s")
print(f"   Backward: {elapsed_time_backward:.1f}s")

print(f"\n💡 OBSERVACIÓN:")
if len(common_features_fb) / k_wrapper > 0.7:
    print(f"   Alta coincidencia ({len(common_features_fb)/k_wrapper*100:.0f}%) → Ambos métodos convergen")
else:
    print(f"   Baja coincidencia ({len(common_features_fb)/k_wrapper*100:.0f}%) → Orden de selección importa")

💡 Pistas:

  • Para Forward: n_estimators=50 y max_depth=10 (valores más pequeños para rapidez), direction='forward'.
  • Para Backward: Igual que Forward pero direction='backward'.
  • Two-Stage Strategy: Aplicamos wrapper methods sobre las features pre-seleccionadas por PCA (~36-45 features) en lugar de las 79 originales. Esto reduce el tiempo de 2-3 minutos a 30-60 segundos.
  • k_wrapper se calcula automáticamente como la mitad de k (o mínimo 15) para refinar aún más la selección.

📖 Documentación: SequentialFeatureSelector

🤔 Pregunta Reflexiva (Forward vs Backward):

  1. ¿Forward o Backward fue más rápido? ¿Por qué?
  2. ¿Por qué los dos métodos eligieron features diferentes si ambos usan el mismo modelo?
  3. ¿Cuándo preferirías usar uno sobre el otro?

Paso 4.3: Recursive Feature Elimination (RFE)

from sklearn.feature_selection import RFE

# ========== TWO-STAGE SELECTION: PCA + RFE ==========
print("\n=== WRAPPER METHOD: RFE (Recursive Feature Elimination) ===")
print("💡 ESTRATEGIA: RFE sobre features pre-seleccionadas por PCA")
print(f"   Stage 1 (ya hecho): PCA Loadings → {len(selected_features_pca)} features")
print(f"   Stage 2 (ahora): RFE → refinar a {k_wrapper} features")
print("⏱️ Esto tomará ~45-90 segundos...\n")

# TODO: RFE con Random Forest Regressor sobre features pre-seleccionadas
estimator = RandomForestRegressor(
    random_state=42, 
    n_estimators=______,
    max_depth=______,
    n_jobs=-1
)
selector_rfe = RFE(estimator=estimator, n_features_to_select=k_wrapper, step=______)

print(f"🔄 Ejecutando RFE sobre {len(selected_features_pca)} features...")
import time
start_time = time.time()
X_rfe = selector_rfe.fit_transform(X_pca_selected, y)
elapsed_time = time.time() - start_time

print(f"✅ RFE completado en {elapsed_time:.1f} segundos")

# Features seleccionadas (mapear índices de vuelta a nombres)
selected_indices_rfe = selector_rfe.get_support()
selected_features_rfe = [selected_features_pca[i] for i, sel in enumerate(selected_indices_rfe) if sel]

print(f"\n✅ Features seleccionadas por RFE ({len(selected_features_rfe)}):")
for i, feat in enumerate(selected_features_rfe, 1):
    print(f"  {i:2d}. {feat}")

# Ranking de features (solo sobre las pre-seleccionadas por PCA)
ranking = pd.Series(selector_rfe.ranking_, index=selected_features_pca).sort_values()
print(f"\nRanking de features (1 = seleccionada, solo mostrando top 20):")
print(ranking.head(20))

# Visualizar ranking (top 30 para claridad)
plt.figure(figsize=(12, 8))
ranking.head(30).sort_values(ascending=False).plot(kind='barh')
plt.xlabel('Ranking (1 = mejor, números mayores = eliminadas antes)')
plt.title(f'RFE Feature Ranking - Top 30 de {len(selected_features_pca)} features pre-seleccionadas')
plt.tight_layout()
plt.show()

💡 Pistas:

  • n_estimators y max_depth: Igual que Forward/Backward (50 y 10 respectivamente para rapidez).
  • step: Número de features a eliminar en cada iteración. step=2 elimina 2 features por iteración (más rápido que step=1).

📖 Documentación: RFE

Paso 4.4: Comparación de Features Seleccionadas (Todos los Wrapper Methods)

# Comparar features seleccionadas por diferentes métodos
print("\n=== COMPARACIÓN DE FEATURES SELECCIONADAS ===")

# Crear conjunto de features por método
features_dict = {
    'F-test': set(selected_features_f),
    'Mutual Info': set(selected_features_mi),
    'Forward': set(selected_features_forward),
    'Backward': set(selected_features_backward),
    'RFE': set(selected_features_rfe)
}

# Features en al menos 2 métodos
all_features = set()
for features in features_dict.values():
    all_features.update(features)

feature_counts = {}
for feature in all_features:
    count = sum(1 for features in features_dict.values() if feature in features)
    feature_counts[feature] = count

# Features consistentes (en todos los métodos)
consistent_features = [f for f, count in feature_counts.items() if count == 3]
print(f"\nFeatures consistentes (en todos los métodos): {len(consistent_features)}")
print(consistent_features)

# Features en al menos 2 métodos
robust_features = [f for f, count in feature_counts.items() if count >= 2]
print(f"\nFeatures robustas (≥2 métodos): {len(robust_features)}")
print(robust_features)

print("\n💡 OBSERVACIÓN:")
print(f"   Forward, Backward y RFE son todos wrapper methods, pero usan estrategias diferentes")

🤔 Pregunta Reflexiva 10:

  1. ¿Cuánto tiempo tomaron los wrapper methods (Forward, Backward, RFE) vs Filter Methods?
  2. ¿Los 3 wrapper methods seleccionaron features similares? ¿Qué significa esto?
  3. ¿Cuándo usarías Forward vs Backward vs RFE en un proyecto real?

Paso 4.5: Evaluar Performance de Wrapper Methods

# ========== EVALUACIÓN: FORWARD SELECTION ==========
print("\n=== EVALUANDO WRAPPER METHODS ===")
print("⏱️ Cross-validation con features de cada método...\n")

print("🔄 Evaluando Forward Selection...")
rf_forward = RandomForestRegressor(random_state=42, n_estimators=100, max_depth=15, n_jobs=-1)

scores_mse_forward = -cross_val_score(rf_forward, X_forward, y, cv=5, 
                                       scoring='neg_mean_squared_error', n_jobs=-1)
scores_r2_forward = cross_val_score(rf_forward, X_forward, y, cv=5, 
                                    scoring='r2', n_jobs=-1)
rmse_forward = np.sqrt(scores_mse_forward)

print(f"✅ Forward Selection ({len(selected_features_forward)} features):")
print(f"   RMSE: ${rmse_forward.mean():,.0f} ± ${rmse_forward.std():,.0f}")
print(f"   R²:   {scores_r2_forward.mean():.4f} ± {scores_r2_forward.std():.4f}")

# ========== EVALUACIÓN: BACKWARD ELIMINATION ==========
print(f"\n🔄 Evaluando Backward Elimination...")
rf_backward = RandomForestRegressor(random_state=42, n_estimators=100, max_depth=15, n_jobs=-1)

scores_mse_backward = -cross_val_score(rf_backward, X_backward, y, cv=5, 
                                         scoring='neg_mean_squared_error', n_jobs=-1)
scores_r2_backward = cross_val_score(rf_backward, X_backward, y, cv=5, 
                                      scoring='r2', n_jobs=-1)
rmse_backward = np.sqrt(scores_mse_backward)

print(f"✅ Backward Elimination ({len(selected_features_backward)} features):")
print(f"   RMSE: ${rmse_backward.mean():,.0f} ± ${rmse_backward.std():,.0f}")
print(f"   R²:   {scores_r2_backward.mean():.4f} ± {scores_r2_backward.std():.4f}")

# ========== EVALUACIÓN: RFE ==========
print(f"\n🔄 Evaluando RFE...")
rf_rfe = RandomForestRegressor(random_state=42, n_estimators=100, max_depth=15, n_jobs=-1)

scores_mse_rfe = -cross_val_score(rf_rfe, X_rfe, y, cv=5, 
                                   scoring='neg_mean_squared_error', n_jobs=-1)
scores_r2_rfe = cross_val_score(rf_rfe, X_rfe, y, cv=5, 
                                scoring='r2', n_jobs=-1)
rmse_rfe = np.sqrt(scores_mse_rfe)

print(f"✅ RFE ({len(selected_features_rfe)} features):")
print(f"   RMSE: ${rmse_rfe.mean():,.0f} ± ${rmse_rfe.std():,.0f}")
print(f"   R²:   {scores_r2_rfe.mean():.4f} ± {scores_r2_rfe.std():.4f}")

# ========== COMPARACIÓN ACTUALIZADA ==========
print(f"\n" + "="*80)
print(f"{'COMPARACIÓN: TODOS LOS MÉTODOS HASTA AHORA':^80}")
print(f"="*80)

comparison_updated = {
    'Método': ['Original', 'PCA Componentes', 'PCA Loadings', 'F-test', 'MI', 'Forward', 'Backward', 'RFE'],
    'N_Features': [X.shape[1], n_components_80, k, k, k, k, k, k],
    'RMSE': [rmse_original.mean(), rmse_pca.mean(), rmse_pca_selected.mean(), rmse_filter_f.mean(), 
             rmse_filter_mi.mean(), rmse_forward.mean(), rmse_backward.mean(), rmse_rfe.mean()],
    'R²': [scores_r2_original.mean(), scores_r2_pca.mean(), scores_r2_pca_selected.mean(), scores_r2_filter_f.mean(), 
           scores_r2_filter_mi.mean(), scores_r2_forward.mean(), scores_r2_backward.mean(), scores_r2_rfe.mean()]
}
comparison_updated_df = pd.DataFrame(comparison_updated)
comparison_updated_df['Reducción%'] = (1 - comparison_updated_df['N_Features'] / X.shape[1]) * 100
comparison_updated_df = comparison_updated_df.sort_values('RMSE')

print(f"\n{comparison_updated_df.to_string(index=False)}")

print(f"\n💡 OBSERVACIÓN:")
best_method = comparison_updated_df.iloc[0]['Método']
best_rmse = comparison_updated_df.iloc[0]['RMSE']
print(f"   🏆 Mejor RMSE: {best_method} (${best_rmse:,.0f})")

Parte 5: Feature Selection - Embedded Methods (15 min)

Paso 5.1: Random Forest Feature Importance

# TODO: Entrenar Random Forest y obtener importances
print("\n=== EMBEDDED METHODS: Random Forest ===")
from sklearn.ensemble import RandomForestClassifier

rf_embedded = RandomForestClassifier(random_state=42, n_estimators=200, max_depth=10)
rf_embedded.fit(X_scaled, y)

# Feature importances
importances = pd.Series(rf_embedded.feature_importances_, index=X.columns).sort_values(ascending=False)
print("Top 10 features por importancia:")
print(importances.head(10))

# Visualizar importances
plt.figure(figsize=(12, 8))
importances.sort_values(ascending=True).plot(kind='barh')
plt.xlabel('Feature Importance')
plt.title('Random Forest Feature Importances')
plt.tight_layout()
plt.show()

# TODO: Seleccionar top-k features
top_k_features = importances.nlargest(k).index
X_rf_importance = X_scaled[:, X.columns.isin(top_k_features)]

print(f"\nFeatures seleccionadas por RF Importance ({k}):")
print(list(top_k_features))

# Evaluar
rmse_rf_importance = np.sqrt(-cross_val_score(rf_embedded, X_rf_importance, y, cv=5, scoring='neg_mean_squared_error'))
print(f"\nRMSE RF Importance ({k} features): ${rmse_rf_importance.mean():,.0f} ± ${rmse_rf_importance.std():,.0f}")
scores_rf_importance = cross_val_score(rf_embedded, X_rf_importance, y, cv=5, scoring='accuracy')
print(f"\nRF Importance ({k} features):")
print(f"  Mean: {scores_rf_importance.mean():.3f}")
print(f"  Std: {scores_rf_importance.std():.3f}")

🤔 Pregunta Reflexiva 11:

  1. ¿Qué features crees que son "realmente importantes" basándote en los 3 métodos?
  2. ¿Por qué RF Importance podría diferir de F-test/MI/RFE?
  3. ¿Confiarías más en un método o en el consenso de varios?

Paso 5.2: Lasso (L1 Regularization para Regresión)

from sklearn.linear_model import LassoCV

# ========== LASSO PARA FEATURE SELECTION ==========
print("\n=== EMBEDDED METHOD: Lasso (L1 Regularization) ===")
print("Lasso penaliza coeficientes, forzando a 0 features no importantes")
print("⏱️ Esto puede tomar 30-60 segundos...\n")

# TODO: Lasso para regresión
lasso = LassoCV(cv=5, random_state=42, max_iter=______)
lasso.fit(X_scaled, y)

print(f"✅ Lasso alpha seleccionado: {lasso.alpha_:.4f}")

# Features seleccionadas (coef != 0)
lasso_nonzero = X.columns[lasso.coef_ != 0]
print(f"\n📊 Features con coeficiente no-cero: {len(lasso_nonzero)} de {X.shape[1]}")

# Seleccionar top-k por magnitud de coeficiente
coef_abs = pd.Series(np.abs(lasso.coef_), index=X.columns).sort_values(ascending=False)
lasso_features = coef_abs.nlargest(k).index

print(f"\n✅ Top {k} features por magnitud de coeficiente Lasso:")
for i, (feat, coef) in enumerate(coef_abs.nlargest(k).items(), 1):
    print(f"  {i:2d}. {feat:20s}: |{coef:.6f}|")

# TODO: Visualizar coeficientes (top 30)
plt.figure(figsize=(14, 10))
coef_abs.head(30).sort_values(ascending=True).plot(kind='barh', color='purple')
plt.xlabel('|Coeficiente Lasso|', fontsize=12)
plt.title('Top 30 Features por Magnitud de Coeficiente Lasso\n(Mayor magnitud = Mayor importancia)', fontsize=14)
plt.grid(axis='x', alpha=0.3)
plt.tight_layout()
plt.show()

# Preparar features para evaluación
X_lasso = X_scaled[:, X.columns.isin(lasso_features)]

# ========== EVALUAR CON RANDOM FOREST ==========
print(f"\n🔄 Evaluando Lasso selection...")
rf_lasso = RandomForestRegressor(random_state=42, n_estimators=100, max_depth=15, n_jobs=-1)

scores_mse_lasso = -cross_val_score(rf_lasso, X_lasso, y, cv=5, 
                                     scoring='neg_mean_squared_error', n_jobs=-1)
scores_r2_lasso = cross_val_score(rf_lasso, X_lasso, y, cv=5, 
                                  scoring='r2', n_jobs=-1)
rmse_lasso = np.sqrt(scores_mse_lasso)

print(f"✅ Lasso selection ({k} features):")
print(f"   RMSE: ${rmse_lasso.mean():,.0f} ± ${rmse_lasso.std():,.0f}")
print(f"   R²:   {scores_r2_lasso.mean():.4f} ± {scores_r2_lasso.std():.4f}")

💡 Pista: max_iter es el número máximo de iteraciones para convergencia. Usa 10000 para asegurar que el algoritmo converja completamente (valores muy bajos pueden causar warnings de convergencia).

📖 Documentación: LassoCV

🤔 Pregunta Reflexiva (Lasso):

  1. ¿Lasso forzó muchas features a 0? ¿Qué te dice esto sobre la redundancia de features?
  2. ¿Las features que Lasso considera importantes coinciden con los otros métodos?
  3. ¿Lasso es más útil para interpretabilidad o para performance?

🤔 Pregunta Reflexiva 12 (Final de Parte 6):

  1. ¿Qué método dio mejor RMSE? ¿Es el que recomendarías para producción? ¿Por qué sí o no?
  2. ¿Por qué Feature Selection es preferible a PCA en el contexto de bienes raíces?
  3. Si tuvieras que presentar estos resultados al CEO de la empresa inmobiliaria (no técnico), ¿qué 3 puntos clave destacarías?
  4. ¿Cómo comunicarías el trade-off entre reducir features (velocidad) y mantener precisión (RMSE)?
  5. ¿El método ganador es interpretable? ¿Por qué esto es crítico para esta industria?

Parte 7: Investigación Libre (opcional, +15 min)

Explora uno de estos temas avanzados:

Opción A: Forward/Backward Selection

Implementa Sequential Feature Selection y compara con RFE:

from sklearn.feature_selection import SequentialFeatureSelector

# Forward selection
sfs_forward = SequentialFeatureSelector(
    RandomForestClassifier(random_state=42),
    n_features_to_select=k,
    direction='forward',
    cv=3
)
# Implementa y evalúa

Opción B: SHAP Values para Feature Importance

Usa SHAP para una interpretación más robusta:

import shap

# Train model
model = RandomForestClassifier(random_state=42)
model.fit(X_scaled, y)

# SHAP values
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_scaled)

# Visualize
shap.summary_plot(shap_values, X_scaled, feature_names=X.columns)

Opción C: PCA Incremental

Para datasets que no caben en memoria:

from sklearn.decomposition import IncrementalPCA

# PCA incremental (mini-batches)
ipca = IncrementalPCA(n_components=k, batch_size=1000)
# Implementa con batches

Opción D: Feature Engineering + Selection

Crea interaction features y luego aplica selection:

from sklearn.preprocessing import PolynomialFeatures

# Crear interacciones
poly = PolynomialFeatures(degree=2, include_bias=False, interaction_only=True)
X_interactions = poly.fit_transform(X_scaled)

# Aplicar feature selection en features expandidas
# Evaluar si mejora

📝 Reflexión Final Integradora (OBLIGATORIO)

⏱️ Tiempo estimado: 15-20 minutos

Responde las siguientes preguntas en una celda Markdown al final de tu notebook. Estas preguntas te ayudarán a consolidar tu comprensión y demostrar pensamiento crítico.

A. Sobre PCA

  1. Interpretabilidad: ¿Puedes explicar en términos simples qué representa PC1 en el contexto de precios de casas? ¿Es esto útil para un agente inmobiliario?

  2. Varianza Explicada: Si PC1 captura 40% de varianza, ¿qué significa esto exactamente? ¿Qué información se "pierde" en el 60% restante?

  3. Cuándo usar PCA: Menciona 3 escenarios reales donde PCA sería MÁS útil que Feature Selection (no en este dataset, sino en general).

  4. Limitaciones: ¿Cuál es la mayor desventaja de PCA para este problema de bienes raíces?

B. Sobre Feature Selection

  1. Consistencia: Si F-test, MI, RFE y Lasso eligieron features diferentes, ¿cómo decides cuál conjunto usar? ¿Qué estrategia aplicarías?

  2. Features Redundantes: Si GarageArea y GarageCars están altamente correlacionadas, ¿cuál deberías eliminar? ¿Cómo decides?

  3. Métodos Filter vs Wrapper: ¿Por qué RFE es más lento que F-test? ¿Cuándo justifica el tiempo extra?

  4. Lasso Shrinkage: Si Lasso forzó 40 features a coeficiente 0, ¿qué te dice esto sobre la redundancia en el dataset?


🏠 Trabajos Domiciliarios (Opcionales pero Recomendados)

⏱️ Tiempo estimado: 2-4 horas por trabajo

Estos trabajos te permitirán profundizar en aspectos específicos de PCA y Feature Selection. Puedes elegir uno, varios, o todos.

Trabajo Domiciliario 1: Comparación de Umbrales de Varianza en PCA

Objetivo: Entender cómo el umbral de varianza afecta performance y complejidad.

Tarea: 1. Implementa PCA con diferentes umbrales de varianza explicada: 70%, 80%, 90%, 95%, 99% 2. Para cada umbral: - Registra número de componentes - Calcula RMSE y R² con cross-validation - Mide tiempo de entrenamiento e inferencia 3. Grafica: - Gráfico 1: Varianza explicada vs Número de componentes - Gráfico 2: RMSE vs Número de componentes - Gráfico 3: Tiempo de entrenamiento vs Número de componentes 4. Responde: - ¿Dónde está el "punto óptimo" (elbow) en el trade-off varianza-performance? - ¿A partir de cuántos componentes el RMSE deja de mejorar significativamente? - ¿Cuánto tiempo ahorras en inferencia con 70% vs 99% varianza? - ¿Qué umbral recomendarías para una app móvil que necesita respuestas instantáneas?

Entregable: Notebook con código, gráficos y análisis (max 3 páginas).


Trabajo Domiciliario 2: Feature Selection - Comparación de Métodos Filter

Objetivo: Comparar diferentes métricas de importancia en Filter Methods.

Tarea: 1. Implementa los siguientes Filter Methods y selecciona top-k=15 features: - F-test (ANOVA F-value): f_regression - Mutual Information: mutual_info_regression - Correlación de Pearson: df.corr()['SalePrice'].abs() - Chi-squared: chi2 (requiere discretización de features) 2. Para cada método: - Lista las 15 features seleccionadas - Calcula RMSE y R² con Random Forest 3. Crea un diagrama de Venn mostrando overlap entre los 4 métodos 4. Identifica: - Features "robustas" (seleccionadas por los 4 métodos) - Features "controversiales" (seleccionadas solo por 1-2 métodos) 5. Responde: - ¿Por qué Correlación de Pearson eligió features diferentes a F-test si ambos miden relaciones lineales? - ¿Cuál método fue mejor en RMSE? ¿Coincide con tu intuición? - ¿Cómo decidirías qué features usar si los métodos no están de acuerdo? - ¿Preferirías usar un "ensemble" de métodos? ¿Cómo lo implementarías?

Entregable: Notebook con código, Venn diagram, tabla comparativa y análisis (max 3 páginas).


Trabajo Domiciliario 3: Wrapper Methods - Sequential Feature Selection

Objetivo: Comparar RFE con Sequential Feature Selection (SFS) y entender trade-offs de tiempo vs precisión.

Tarea: 1. Implementa los siguientes Wrapper Methods y selecciona top-k=15 features: - RFE (Recursive Feature Elimination): elimina features de peor a mejor - Forward Selection: agrega features de a una (empieza con 0) - Backward Selection: elimina features de a una (empieza con 80) - Bi-Directional Selection: combina forward y backward

Usa SequentialFeatureSelector de sklearn:

from sklearn.feature_selection import SequentialFeatureSelector

# Forward Selection
sfs_forward = SequentialFeatureSelector(
    RandomForestRegressor(random_state=42, n_estimators=50),
    n_features_to_select=15,
    direction='forward',
    cv=3,
    n_jobs=-1
)

  1. Para cada método:
  2. Registra features seleccionadas
  3. Mide tiempo de ejecución (crítico para comparar)
  4. Calcula RMSE y R² con Random Forest (100 estimators, cv=5)
  5. Compara:
  6. ¿Qué método es más rápido? ¿Por qué?
  7. ¿Qué método tiene mejor RMSE?
  8. ¿El overlap de features es alto o bajo?
  9. Experimenta con Early Stopping:
  10. Implementa Forward Selection con early stopping (para si RMSE no mejora en 3 iteraciones)
  11. Compara tiempo y features seleccionadas vs Forward Selection completo
  12. Responde:
  13. ¿Vale la pena el tiempo extra de RFE/SFS vs Filter Methods (F-test)?
  14. ¿En qué escenarios usarías Wrapper Methods en lugar de Filter Methods?
  15. ¿Cómo podrías acelerar Wrapper Methods sin sacrificar mucha precisión?

Entregable: Notebook con código, tabla de tiempos, comparación de features y análisis (max 4 páginas).


📚 Recursos Adicionales


🎉 ¡Assignment completado! Sube tu notebook al portfolio y prepárate para discutir tus hallazgos en clase.