Assignment UT3-8: Encoding Avanzado y Target Encoding - Fill in the Blanks¶
📋 Lo que necesitas saber ANTES de empezar¶
- Conceptos básicos de encoding categórico (label, one-hot)
- Idea general de target encoding y data leakage
- Uso de pipelines de scikit-learn
- Conceptos de cross-validation
📦 Paso 0: Instalación de Dependencias¶
IMPORTANTE: Ejecuta esta celda PRIMERO para instalar las librerías necesarias.
# === INSTALACIÓN DE DEPENDENCIAS ===
print("📦 Instalando dependencias necesarias...")
print("-" * 60)
# Instalar category_encoders (necesario para TargetEncoder)
!pip install shap category-encoders --quiet
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, StratifiedKFold, cross_val_score
from sklearn.preprocessing import LabelEncoder, OneHotEncoder, StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, roc_auc_score, f1_score
from category_encoders import TargetEncoder
import time
import warnings
print("\n🎉 Setup completado. Puedes continuar con el assignment.")
# Importar librerías necesarias
warnings.filterwarnings('ignore')
# Configuración
np.random.seed(42)
plt.style.use('_______') # establecer estilo visual (ej: 'seaborn-v0_8', 'default')
sns.set_palette("_______") # definir paleta de colores (ej: 'Set2', 'husl')
print("✅ Entorno configurado para encoding avanzado")
💰 Paso 2: Cargar Dataset Real - Adult Income (Census)¶
📋 CONTEXTO DE NEGOCIO (CRISP-DM: Business Understanding)
🔗 Referencias oficiales:
- Kaggle Feature Engineering - Target Encoding
- Feature Engineering for ML - Cap. 5 (Categorical Variables)
- Scikit-learn Preprocessing
- Category Encoders Library
💰 Caso de negocio:
- Problema: Predecir si el ingreso anual de una persona supera $50K basándose en datos del censo
- Desafío: Variables categóricas con alta cardinalidad (occupation: 15 valores, native-country: 42 valores)
- Objetivo: Comparar diferentes técnicas de encoding para maximizar la precisión del modelo de clasificación
- Restricción: One-hot encoding genera 100+ columnas → curse of dimensionality
- Valor: Modelos de segmentación de clientes, análisis de equidad salarial, políticas públicas
- Contexto: Dataset real del US Census (1994) con 48,842 registros - clásico de ML
# === CARGAR DATASET REAL: ADULT INCOME ===
print("💰 CARGANDO DATASET: ADULT INCOME (US CENSUS)")
print("=" * 60)
# Este dataset es del UCI ML Repository - clásico para benchmarking
# Predice si el ingreso anual supera $50K basándose en datos del censo de 1994
# OPCIÓN 1: Cargar desde URL (si tienes conexión a internet)
url = "https://archive.ics.uci.edu/ml/machine-learning-databases/adult/adult.data"
# Nombres de columnas (el dataset no tiene header)
column_names = [
'age', 'workclass', 'fnlwgt', 'education', 'education-num',
'marital-status', 'occupation', 'relationship', 'race', 'sex',
'capital-gain', 'capital-loss', 'hours-per-week', 'native-country', 'income'
]
df = pd._______(url, names=column_names, na_values=' ?', skipinitialspace=True) # función para leer CSV
# 1. Limpiar datos
print("\n🧹 Limpiando datos...")
# Remover espacios en blanco de las categorías
for col in df.select_dtypes(include=['object']).columns:
df[col] = df[col].str.______() # método para eliminar espacios en blanco
# Manejar valores faltantes
print(f" Valores faltantes antes: {df.isnull().sum().sum()}")
df = df._______(how='any') # método para eliminar filas con NaN
print(f" Valores faltantes después: {df.isnull().sum().sum()}")
print(f" Registros después de limpieza: {len(df):,}")
# 2. Crear target binario
df['target'] = (df['income'] == '>50K').astype(int)
print(f"\n📊 Dataset shape: {df.shape}")
print(f"📊 Distribución del target:")
print(f" <=50K: {(df['target']==0).sum():,} ({(df['target']==0).mean():.1%})")
print(f" >50K: {(df['target']==1).sum():,} ({(df['target']==1).mean():.1%})")
# 3. Identificar columnas categóricas (excluir target e income)
categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
if 'income' in categorical_cols:
categorical_cols.remove('income')
if 'target' in categorical_cols:
categorical_cols.remove('target')
print(f"\n🔍 Variables categóricas encontradas: {len(categorical_cols)}")
# 4. Analizar cardinalidad
print("\n🔍 ANÁLISIS DE CARDINALIDAD:")
for col in categorical_cols:
n_unique = df[col].nunique()
cardinality_type = 'BAJA' if n_unique <= 10 else ('MEDIA' if n_unique <= 50 else 'ALTA')
print(f" {col}: {n_unique} categorías únicas ({cardinality_type})")
print("\n🔍 Primeras 5 filas:")
print(df._____()) # método para mostrar primeras filas
print("\n💡 CONTEXTO DEL DATASET:")
print(" Dataset del US Census (1994) - clásico de Machine Learning")
print(" Target: Ingreso >50K/año (clasificación binaria)")
print(" Variables categóricas: workclass, education, occupation, etc.")
print(" Alta cardinalidad: native-country (42 países)")
print(" Accuracy típica: 80-85% (más desafiante que hoteles)")
💡 PISTAS:
- 📚 ¿Qué función de pandas lee CSV desde URL? Documentación
- 🧹 ¿Qué método elimina espacios en blanco? Documentación
- 🗑️ ¿Qué método elimina filas con NaN? Documentación
- 👀 ¿Qué método muestra las primeras filas? Documentación
📖 REFERENCIAS DEL DATASET:
- Fuente: UCI ML Repository - Adult Dataset
- Paper: Kohavi, R. (1996). Scaling Up the Accuracy of Naive-Bayes Classifiers
- Tamaño: 48,842 instancias (32,561 train + 16,281 test)
- Variables: 14 (6 continuas, 8 categóricas)
- Target: Ingreso binario (<=50K vs >50K)
- Baseline: ~76% mayoría, ~85% con buenos modelos
🔢 Paso 3: Análisis de Cardinalidad¶
# === ANÁLISIS DE CARDINALIDAD Y PROBLEMAS DE ONE-HOT ===
print("\n🔍 ANÁLISIS DE CARDINALIDAD")
print("=" * 60)
# 1. Clasificar columnas por cardinalidad
def classify_cardinality(df, categorical_cols):
"""Clasificar columnas por cardinalidad"""
low_card = []
medium_card = []
high_card = []
for col in categorical_cols:
n_unique = df[col].nunique()
if n_unique <= 10:
low_card.append(col)
elif n_unique <= 50:
medium_card.append(col)
else:
high_card.append(col)
return low_card, medium_card, high_card
low_card_cols, medium_card_cols, high_card_cols = classify_cardinality(df, categorical_cols)
print("📊 CLASIFICACIÓN POR CARDINALIDAD:")
print(f"✅ Baja cardinalidad (≤10): {len(low_card_cols)} columnas")
print(f" {low_card_cols}")
print(f"⚠️ Media cardinalidad (11-50): {len(medium_card_cols)} columnas")
print(f" {medium_card_cols}")
print(f"🚨 Alta cardinalidad (>50): {len(high_card_cols)} columnas")
print(f" {high_card_cols}")
# 2. Calcular dimensionalidad con One-Hot
print("\n🚨 PROBLEMA DE DIMENSIONALIDAD CON ONE-HOT:")
total_onehot_columns = 0
for col in categorical_cols:
n_categories = df[col].nunique()
n_onehot_cols = n_categories - 1 # drop='first'
total_onehot_columns += n_onehot_cols
print(f" {col}: {n_categories} categorías → {n_onehot_cols} columnas one-hot")
print(f"\n❌ Total columnas con one-hot: {total_onehot_columns}")
print(f"❌ Original: {len(categorical_cols)} columnas → {total_onehot_columns} columnas")
print(f"❌ Explosión dimensional: {total_onehot_columns / len(categorical_cols):.1f}x")
# 3. Visualizar distribución de cardinalidad
fig, ax = plt.subplots(figsize=(12, 6))
cardinalities = [df[col].nunique() for col in categorical_cols]
colors = ['green' if c <= 10 else ('orange' if c <= 50 else 'red') for c in cardinalities]
ax.bar(categorical_cols, cardinalities, color=colors, alpha=0.7)
ax.axhline(y=10, color='green', linestyle='--', label='Baja cardinalidad (≤10)')
ax.axhline(y=50, color='orange', linestyle='--', label='Media cardinalidad (≤50)')
ax.set_xlabel('Variables Categóricas')
ax.set_ylabel('Número de Categorías Únicas')
ax.set_title('Cardinalidad de Variables Categóricas')
ax.legend()
plt.xticks(rotation=45, ha='right')
plt.tight_layout()
plt.show()
print("\n💡 CONCLUSIÓN:")
print(" One-hot encoding NO es viable para variables de alta cardinalidad")
print(" Necesitamos técnicas alternativas: Label, Target, Hash, Binary encoding")
💡 PISTAS:
- 📊 ¿Por qué one-hot explota la dimensionalidad?
- 🎨 ¿Qué colores usar para representar niveles de riesgo?
- 🚨 ¿Cuál es el límite práctico de columnas para one-hot?
🏷️ Paso 4: Experimento 1 - Label Encoding¶
# === EXPERIMENTO 1: LABEL ENCODING ===
print("\n🏷️ EXPERIMENTO 1: LABEL ENCODING")
print("=" * 60)
def experiment_label_encoding(df, categorical_cols, target_col='target'):
"""
Implementar Label Encoding y evaluar performance
"""
# 1. Preparar datos
# Seleccionar variables numéricas del dataset Adult Income
numeric_cols = ['age', 'fnlwgt', 'education-num', 'capital-gain',
'capital-loss', 'hours-per-week']
X = df[categorical_cols + numeric_cols].copy()
y = df[target_col]
# Split train-test
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
# 2. Aplicar Label Encoding
print("🔄 Aplicando Label Encoding...")
X_train_encoded = X_train.copy()
X_test_encoded = X_test.copy()
label_encoders = {}
for col in categorical_cols:
le = LabelEncoder()
# Fit en train
X_train_encoded[col] = le._______(X_train[col]) # método para fit y transform
# Transform en test (manejar categorías no vistas)
# TODO: ¿Cómo manejar categorías en test que no aparecen en train?
le_dict = dict(zip(le.classes_, le.transform(le.classes_)))
X_test_encoded[col] = X_test[col].map(le_dict).fillna(-1).astype(int)
label_encoders[col] = le
# 3. Entrenar modelo
print("🌲 Entrenando Random Forest...")
start_time = time.time()
model = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
model._______(X_train_encoded, y_train) # método para entrenar modelo
training_time = time.time() - start_time
# 4. Evaluar
y_pred = model._______(X_test_encoded) # método para hacer predicciones
y_pred_proba = model.predict_proba(X_test_encoded)[:, 1]
accuracy = accuracy_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_pred_proba)
f1 = f1_score(y_test, y_pred)
results = {
'encoding': 'Label Encoding',
'accuracy': accuracy,
'auc': auc,
'f1_score': f1,
'training_time': training_time,
'n_features': X_train_encoded.shape[1]
}
print(f"✅ Label Encoding completado")
print(f" 📊 Accuracy: {accuracy:.4f}")
print(f" 📊 AUC-ROC: {auc:.4f}")
print(f" 📊 F1-Score: {f1:.4f}")
print(f" ⏱️ Training time: {training_time:.2f}s")
print(f" 📏 Features: {X_train_encoded.shape[1]}")
return results, model, label_encoders
# Ejecutar experimento
results_label, model_label, label_encoders = experiment_label_encoding(df, categorical_cols)
💡 PISTAS:
- 🏷️ ¿Qué método de LabelEncoder hace fit y transform juntos? Documentación
- 🌲 ¿Qué método de RandomForest entrena el modelo? Documentación
- 🔮 ¿Qué método hace predicciones? Documentación
- ⚠️ ¿Por qué label encoding puede ser problemático con modelos lineales?
🔥 Paso 5: Experimento 2 - One-Hot Encoding (Solo Baja Cardinalidad)¶
# === EXPERIMENTO 2: ONE-HOT ENCODING (SOLO BAJA CARDINALIDAD) ===
print("\n🔥 EXPERIMENTO 2: ONE-HOT ENCODING (BAJA CARDINALIDAD)")
print("=" * 60)
def experiment_onehot_encoding(df, low_card_cols, numeric_cols, target_col='target'):
"""
Implementar One-Hot Encoding solo para variables de baja cardinalidad
"""
# 1. Preparar datos (solo baja cardinalidad + numéricas)
feature_cols = low_card_cols + numeric_cols
X = df[feature_cols].copy()
y = df[target_col]
# Split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
# 2. Aplicar One-Hot Encoding
print("🔄 Aplicando One-Hot Encoding...")
encoder = OneHotEncoder(drop='first', sparse_output=False, handle_unknown='ignore')
# Separar categóricas y numéricas
X_train_cat = X_train[low_card_cols]
X_train_num = X_train[numeric_cols]
X_test_cat = X_test[low_card_cols]
X_test_num = X_test[numeric_cols]
# Encode categóricas
X_train_cat_encoded = encoder._______(X_train_cat) # método para fit y transform
X_test_cat_encoded = encoder._______(X_test_cat) # método para solo transform
# Combinar con numéricas
X_train_encoded = np.hstack([X_train_cat_encoded, X_train_num.values])
X_test_encoded = np.hstack([X_test_cat_encoded, X_test_num.values])
print(f" 📊 Features after one-hot: {X_train_encoded.shape[1]}")
print(f" 📊 Categóricas: {low_card_cols}")
print(f" 📊 Columnas one-hot: {X_train_cat_encoded.shape[1]}")
# 3. Entrenar modelo
print("🌲 Entrenando Random Forest...")
start_time = time.time()
model = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
model.fit(X_train_encoded, y_train)
training_time = time.time() - start_time
# 4. Evaluar
y_pred = model.predict(X_test_encoded)
y_pred_proba = model.predict_proba(X_test_encoded)[:, 1]
accuracy = accuracy_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_pred_proba)
f1 = f1_score(y_test, y_pred)
results = {
'encoding': 'One-Hot (low card only)',
'accuracy': accuracy,
'auc': auc,
'f1_score': f1,
'training_time': training_time,
'n_features': X_train_encoded.shape[1]
}
print(f"✅ One-Hot Encoding completado")
print(f" 📊 Accuracy: {accuracy:.4f}")
print(f" 📊 AUC-ROC: {auc:.4f}")
print(f" 📊 F1-Score: {f1:.4f}")
print(f" ⏱️ Training time: {training_time:.2f}s")
print(f" 📏 Features: {X_train_encoded.shape[1]}")
return results, model, encoder
# Ejecutar experimento
# Definir variables numéricas del Adult Income dataset
numeric_cols = ['age', 'fnlwgt', 'education-num', 'capital-gain',
'capital-loss', 'hours-per-week']
results_onehot, model_onehot, onehot_encoder = experiment_onehot_encoding(df, low_card_cols, numeric_cols)
💡 PISTAS:
- 🔥 ¿Qué método de OneHotEncoder hace fit y transform? Documentación
- 🔄 ¿Qué método solo transforma sin hacer fit? Documentación
- 📊 ¿Por qué solo usamos variables de baja cardinalidad?
- 🤔 ¿Qué pasa si una categoría en test no apareció en train?
🎯 Paso 6: Experimento 3 - Target Encoding (Alta Cardinalidad)¶
# === EXPERIMENTO 3: TARGET ENCODING (ALTA CARDINALIDAD) ===
print("\n🎯 EXPERIMENTO 3: TARGET ENCODING (ALTA CARDINALIDAD)")
print("=" * 60)
def experiment_target_encoding(df, high_card_cols, numeric_cols, target_col='target'):
"""
Implementar Target Encoding con cross-validation para prevenir leakage
"""
# 1. Preparar datos
feature_cols = high_card_cols + numeric_cols
X = df[feature_cols].copy()
y = df[target_col]
# Split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
# 2. Aplicar Target Encoding
print("🔄 Aplicando Target Encoding...")
print("⚠️ IMPORTANTE: Usando cross-validation para prevenir DATA LEAKAGE")
# TODO: ¿Por qué es importante usar CV para target encoding?
# PISTA: ¿Qué pasa si calculamos el promedio del target usando el mismo registro?
# Crear encoder de category_encoders
encoder = TargetEncoder(cols=high_card_cols, smoothing=________) # parámetro de smoothing (ej: 1.0, 10.0, 100.0)
# Separar categóricas y numéricas
X_train_cat = X_train[high_card_cols]
X_train_num = X_train[numeric_cols]
X_test_cat = X_test[high_card_cols]
X_test_num = X_test[numeric_cols]
# Encode categóricas (TargetEncoder necesita el target)
X_train_cat_encoded = encoder._______(X_train_cat, y_train) # método para fit y transform con target
X_test_cat_encoded = encoder._______(X_test_cat) # método para solo transform
# Combinar con numéricas
X_train_encoded = pd.concat([X_train_cat_encoded.reset_index(drop=True),
X_train_num.reset_index(drop=True)], axis=1)
X_test_encoded = pd.concat([X_test_cat_encoded.reset_index(drop=True),
X_test_num.reset_index(drop=True)], axis=1)
print(f" 📊 Features after target encoding: {X_train_encoded.shape[1]}")
print(f" 📊 Categóricas codificadas: {high_card_cols}")
print(f" 📊 Ejemplo de encoding:")
for col in high_card_cols[:2]: # mostrar primeras 2 columnas
print(f" {col}: min={X_train_cat_encoded[col].min():.3f}, "
f"max={X_train_cat_encoded[col].max():.3f}, "
f"mean={X_train_cat_encoded[col].mean():.3f}")
# 3. Entrenar modelo
print("🌲 Entrenando Random Forest...")
start_time = time.time()
model = RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1)
model.fit(X_train_encoded, y_train)
training_time = time.time() - start_time
# 4. Evaluar
y_pred = model.predict(X_test_encoded)
y_pred_proba = model.predict_proba(X_test_encoded)[:, 1]
accuracy = accuracy_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_pred_proba)
f1 = f1_score(y_test, y_pred)
results = {
'encoding': 'Target Encoding (high card)',
'accuracy': accuracy,
'auc': auc,
'f1_score': f1,
'training_time': training_time,
'n_features': X_train_encoded.shape[1]
}
print(f"✅ Target Encoding completado")
print(f" 📊 Accuracy: {accuracy:.4f}")
print(f" 📊 AUC-ROC: {auc:.4f}")
print(f" 📊 F1-Score: {f1:.4f}")
print(f" ⏱️ Training time: {training_time:.2f}s")
print(f" 📏 Features: {X_train_encoded.shape[1]}")
return results, model, encoder
# Ejecutar experimento
results_target, model_target, target_encoder = experiment_target_encoding(df, high_card_cols, numeric_cols)
💡 PISTAS:
- 🎯 ¿Qué es el smoothing en target encoding? Documentación
- 🔄 ¿Qué método de TargetEncoder necesita el target? Documentación
- ⚠️ ¿Por qué es crítico usar CV para target encoding?
- 📊 ¿Qué valores típicos usar para smoothing? (1.0, 10.0, 100.0)
🌳 Paso 7: Pipeline con Branching - ColumnTransformer¶
# === PIPELINE CON BRANCHING: COLUMNTRANSFORMER ===
print("\n🌳 PIPELINE CON BRANCHING: COLUMNTRANSFORMER")
print("=" * 60)
def create_branched_pipeline(low_card_cols, high_card_cols, numeric_cols):
"""
Crear pipeline con múltiples ramas para diferentes tipos de encoding
"""
print("🔧 Creando pipeline con branching...")
print(f" 🌿 Rama 1: One-Hot para baja cardinalidad ({len(low_card_cols)} cols)")
print(f" 🌿 Rama 2: Target Encoding para alta cardinalidad ({len(high_card_cols)} cols)")
print(f" 🌿 Rama 3: StandardScaler para numéricas ({len(numeric_cols)} cols)")
# TODO: Definir transformadores para cada rama
# RAMA 1: One-Hot para baja cardinalidad
onehot_transformer = Pipeline(steps=[
('onehot', OneHotEncoder(drop='first', sparse_output=False, handle_unknown='ignore'))
])
# RAMA 2: Target Encoding para alta cardinalidad
target_transformer = Pipeline(steps=[
('target', TargetEncoder(smoothing=10.0))
])
# RAMA 3: Scaling para numéricas
numeric_transformer = Pipeline(steps=[
('scaler', StandardScaler())
])
# COLUMNTRANSFORMER: Combina todas las ramas
preprocessor = ColumnTransformer(
transformers=[
('low_card', onehot_transformer, low_card_cols),
('high_card', target_transformer, high_card_cols),
('num', numeric_transformer, numeric_cols)
],
remainder='_______' # qué hacer con columnas no especificadas ('drop', 'passthrough')
)
# PIPELINE COMPLETO: Preprocessor + Modelo
pipeline = Pipeline(steps=[
('preprocessor', preprocessor),
('classifier', RandomForestClassifier(n_estimators=100, random_state=42, n_jobs=-1))
])
print("✅ Pipeline creado con éxito")
return pipeline, preprocessor
def experiment_branched_pipeline(df, low_card_cols, high_card_cols, numeric_cols, target_col='target'):
"""
Evaluar pipeline con branching
"""
# 1. Preparar datos
all_features = low_card_cols + high_card_cols + numeric_cols
X = df[all_features].copy()
y = df[target_col]
# Split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
# 2. Crear pipeline
pipeline, preprocessor = create_branched_pipeline(low_card_cols, high_card_cols, numeric_cols)
# 3. Entrenar pipeline completo
print("\n🔄 Entrenando pipeline completo...")
start_time = time.time()
pipeline._______(X_train, y_train) # método para entrenar pipeline
training_time = time.time() - start_time
# 4. Evaluar
y_pred = pipeline._______(X_test) # método para hacer predicciones
y_pred_proba = pipeline.predict_proba(X_test)[:, 1]
accuracy = accuracy_score(y_test, y_pred)
auc = roc_auc_score(y_test, y_pred_proba)
f1 = f1_score(y_test, y_pred)
# 5. Analizar features transformadas
print("\n📊 ANÁLISIS DE FEATURES TRANSFORMADAS:")
X_train_transformed = preprocessor.fit_transform(X_train, y_train)
print(f" 📏 Features originales: {X_train.shape[1]}")
print(f" 📏 Features después de transformación: {X_train_transformed.shape[1]}")
# TODO: ¿Cuántas columnas one-hot se crearon?
# PISTA: Usar get_feature_names_out() del preprocessor
results = {
'encoding': 'Branched Pipeline (mixed)',
'accuracy': accuracy,
'auc': auc,
'f1_score': f1,
'training_time': training_time,
'n_features': X_train_transformed.shape[1]
}
print(f"\n✅ Pipeline con branching completado")
print(f" 📊 Accuracy: {accuracy:.4f}")
print(f" 📊 AUC-ROC: {auc:.4f}")
print(f" 📊 F1-Score: {f1:.4f}")
print(f" ⏱️ Training time: {training_time:.2f}s")
print(f" 📏 Features: {X_train_transformed.shape[1]}")
return results, pipeline, X_test, y_test
# Ejecutar experimento
results_pipeline, pipeline, X_test_pipeline, y_test_pipeline = experiment_branched_pipeline(df, low_card_cols, high_card_cols, numeric_cols)
💡 PISTAS:
- 🌳 ¿Qué hace ColumnTransformer? Documentación
- 🔄 ¿Qué hacer con columnas no especificadas en remainder?
- 📊 ¿Cómo obtener nombres de features después de transformación?
- 🎯 ¿Por qué es útil el branching en pipelines?
🔍 Paso 7.5: Explicabilidad - Feature Importance y SHAP¶
# === EXPLICABILIDAD: ANÁLISIS DE FEATURE IMPORTANCE ===
print("\n🔍 EXPLICABILIDAD: FEATURE IMPORTANCE")
print("=" * 60)
# 1. Feature Importance del Random Forest
print("🌲 1. FEATURE IMPORTANCE - RANDOM FOREST")
print("-" * 60)
def analyze_feature_importance(model, feature_names):
"""
Analizar y visualizar feature importance del Random Forest
"""
# Obtener importancia de features
importances = model.feature_importances_ # atributo que contiene las importancias
# Crear DataFrame para ordenar
importance_df = pd.DataFrame({
'feature': feature_names,
'importance': importances
}).sort_values('importance', ascending=False)
print(f"🔝 Top Features más importantes:")
print(importance_df.to_string(index=False))
# Visualización
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
# Top N features
top_features = importance_df
ax1.barh(range(len(top_features)), top_features['importance'], color='skyblue', alpha=0.7)
ax1.set_yticks(range(len(top_features)))
ax1.set_yticklabels(top_features['feature'])
ax1.set_xlabel('Importance')
ax1.set_title(f'Top Features - Random Forest')
ax1.invert_yaxis()
ax1.grid(True, alpha=0.3)
# Distribución de importancias
ax2.hist(importances, bins=50, alpha=0.7, color='lightgreen')
ax2.set_xlabel('Importance Value')
ax2.set_ylabel('Frequency')
ax2.set_title('Distribución de Feature Importances')
ax2.axvline(importances.mean(), color='red', linestyle='--', label=f'Mean: {importances.mean():.4f}')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
return importance_df
# Analizar importance del mejor modelo (Pipeline con branching)
print("\n📊 Analizando modelo con Pipeline Branching...")
# Obtener nombres de features después de transformación
feature_names_out = pipeline.named_steps['preprocessor'].get_feature_names_out()
print(f"✅ Features extraídas: {len(feature_names_out)}")
# Analizar importancia
importance_df = analyze_feature_importance(
pipeline.named_steps['classifier'],
feature_names_out
)
# 2. Comparar importancia entre métodos de encoding
print("\n📊 2. COMPARACIÓN DE IMPORTANCIA POR MÉTODO")
print("-" * 60)
def compare_importance_by_encoding(models_dict, feature_names_dict):
"""
Comparar cuáles features son importantes en cada método de encoding
"""
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
axes = axes.ravel()
for idx, (name, model) in enumerate(models_dict.items()):
if idx >= 4:
break
# Obtener importancias
importances = model.feature_importances_
features = feature_names_dict[name]
# Top 10
importance_df = pd.DataFrame({
'feature': features,
'importance': importances
}).sort_values('importance', ascending=False)
# Visualizar
axes[idx].barh(range(len(importance_df)), importance_df['importance'], alpha=0.7)
axes[idx].set_yticks(range(len(importance_df)))
axes[idx].set_yticklabels(importance_df['feature'], fontsize=8)
axes[idx].set_xlabel('Importance')
axes[idx].set_title(f'{name}\nTop Features')
axes[idx].invert_yaxis()
axes[idx].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Preparar datos para comparación
models_dict = {
'Label Encoding': model_label,
'One-Hot Encoding': model_onehot,
'Target Encoding': model_target,
'Branched Pipeline': pipeline.named_steps['classifier']
}
# TODO: Definir feature names para cada modelo
# PISTA: Necesitas saber qué features tiene cada modelo después del encoding
feature_names_dict = {
'Label Encoding': categorical_cols + numeric_cols,
'One-Hot Encoding': list(onehot_encoder.get_feature_names_out(low_card_cols)) + numeric_cols,
'Target Encoding': high_card_cols + numeric_cols,
'Branched Pipeline': feature_names_out
}
print("📊 Comparando importancia entre métodos...")
compare_importance_by_encoding(models_dict, feature_names_dict)
# 4. Análisis de Features Codificadas
print("\n🔍 4. ANÁLISIS DE FEATURES CODIFICADAS")
print("-" * 60)
def analyze_encoded_features(importance_df, encoding_type='mixed'):
"""
Analizar qué tipos de features codificadas son más importantes
"""
print(f"\n📊 Análisis para encoding: {encoding_type}")
# Identificar tipo de feature por nombre
feature_types = []
for feat in importance_df['feature']:
if any(num_col in str(feat) for num_col in numeric_cols):
feature_types.append('Numérica')
elif 'target_enc' in str(feat).lower() or any(hc in str(feat) for hc in high_card_cols):
feature_types.append('Target Encoded')
elif any(lc in str(feat) for lc in low_card_cols):
feature_types.append('One-Hot Encoded')
else:
feature_types.append('Otra')
importance_df['type'] = feature_types
# Agrupar por tipo
type_importance = importance_df.groupby('type')['importance'].agg(['sum', 'mean', 'count'])
type_importance = type_importance.sort_values('sum', ascending=False)
print("\n📊 Importancia por tipo de feature:")
print(type_importance.round(4))
# Visualizar
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
# Importancia total por tipo
ax1.bar(type_importance.index, type_importance['sum'], alpha=0.7, color='coral')
ax1.set_xlabel('Tipo de Feature')
ax1.set_ylabel('Importancia Total')
ax1.set_title('Importancia Total por Tipo de Feature')
ax1.tick_params(axis='x', rotation=45)
ax1.grid(True, alpha=0.3)
# Importancia promedio por tipo
ax2.bar(type_importance.index, type_importance['mean'], alpha=0.7, color='lightblue')
ax2.set_xlabel('Tipo de Feature')
ax2.set_ylabel('Importancia Promedio')
ax2.set_title('Importancia Promedio por Tipo de Feature')
ax2.tick_params(axis='x', rotation=45)
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
return type_importance
# Analizar features del mejor modelo
type_importance = analyze_encoded_features(importance_df, 'Branched Pipeline')
print("\n💡 CONCLUSIONES DE EXPLICABILIDAD:")
print("=" * 60)
print("""
🔍 PREGUNTAS PARA REFLEXIONAR:
1. ¿Qué features son más importantes para el modelo?
- ¿Son variables numéricas o categóricas codificadas?
- ¿Las features de alta cardinalidad (target encoded) son importantes?
2. ¿Cómo afecta el encoding a la importancia?
- ¿One-hot encoding captura bien la información?
- ¿Target encoding genera features más predictivas?
3. ¿Qué tipo de features dominan el modelo?
- ¿Variables numéricas originales?
- ¿Variables categóricas codificadas?
- ¿Hay diferencias entre métodos de encoding?
4. ¿Los resultados de SHAP confirman la importancia del Random Forest?
- ¿Hay features que SHAP identifica como importantes pero RF no?
- ¿Las interacciones entre features son significativas?
5. ¿Qué implicaciones tiene esto para el negocio?
- ¿Qué factores realmente predicen el ingreso?
- ¿Hay insights accionables de las features importantes?
""")
💡 PISTAS:
- 🌲 ¿Qué atributo del Random Forest contiene las importancias? Documentación
- 🎯 ¿Qué es SHAP y cómo funciona? Documentación
- 📊 ¿Cómo interpretar SHAP values? Valores positivos → aumentan probabilidad de clase positiva
- 🔍 ¿Por qué es importante la explicabilidad en ML? Confianza, debugging, insights de negocio
📚 RECURSOS ADICIONALES:
📊 Paso 8: Comparación de Resultados¶
# === COMPARACIÓN DE TODOS LOS MÉTODOS ===
print("\n📊 COMPARACIÓN DE MÉTODOS DE ENCODING")
print("=" * 60)
# 1. Consolidar resultados
all_results = [
results_label,
results_onehot,
results_target,
results_pipeline
]
results_df = pd.DataFrame(all_results)
# 2. Mostrar tabla comparativa
print("\n🔝 TABLA COMPARATIVA:")
print(results_df.to_string(index=False))
# 3. Identificar mejor método por métrica
print("\n🏆 MEJORES MÉTODOS POR MÉTRICA:")
print(f" 🎯 Mejor Accuracy: {results_df.loc[results_df['accuracy'].idxmax(), 'encoding']} "
f"({results_df['accuracy'].max():.4f})")
print(f" 🎯 Mejor AUC-ROC: {results_df.loc[results_df['auc'].idxmax(), 'encoding']} "
f"({results_df['auc'].max():.4f})")
print(f" 🎯 Mejor F1-Score: {results_df.loc[results_df['f1_score'].idxmax(), 'encoding']} "
f"({results_df['f1_score'].max():.4f})")
print(f" ⚡ Más rápido: {results_df.loc[results_df['training_time'].idxmin(), 'encoding']} "
f"({results_df['training_time'].min():.2f}s)")
print(f" 📏 Menos features: {results_df.loc[results_df['n_features'].idxmin(), 'encoding']} "
f"({results_df['n_features'].min()} features)")
# 4. Visualización comparativa
fig, axes = plt.subplots(2, 3, figsize=(18, 10))
# Accuracy
axes[0, 0].bar(results_df['encoding'], results_df['accuracy'], color='skyblue', alpha=0.7)
axes[0, 0].set_title('Accuracy Comparison')
axes[0, 0].set_ylabel('Accuracy')
axes[0, 0].tick_params(axis='x', rotation=45)
axes[0, 0].grid(True, alpha=0.3)
# AUC-ROC
axes[0, 1].bar(results_df['encoding'], results_df['auc'], color='lightgreen', alpha=0.7)
axes[0, 1].set_title('AUC-ROC Comparison')
axes[0, 1].set_ylabel('AUC-ROC')
axes[0, 1].tick_params(axis='x', rotation=45)
axes[0, 1].grid(True, alpha=0.3)
# F1-Score
axes[0, 2].bar(results_df['encoding'], results_df['f1_score'], color='lightcoral', alpha=0.7)
axes[0, 2].set_title('F1-Score Comparison')
axes[0, 2].set_ylabel('F1-Score')
axes[0, 2].tick_params(axis='x', rotation=45)
axes[0, 2].grid(True, alpha=0.3)
# Training Time
axes[1, 0].bar(results_df['encoding'], results_df['training_time'], color='orange', alpha=0.7)
axes[1, 0].set_title('Training Time Comparison')
axes[1, 0].set_ylabel('Time (seconds)')
axes[1, 0].tick_params(axis='x', rotation=45)
axes[1, 0].grid(True, alpha=0.3)
# Number of Features
axes[1, 1].bar(results_df['encoding'], results_df['n_features'], color='purple', alpha=0.7)
axes[1, 1].set_title('Number of Features Comparison')
axes[1, 1].set_ylabel('# Features')
axes[1, 1].tick_params(axis='x', rotation=45)
axes[1, 1].grid(True, alpha=0.3)
# Trade-off: Accuracy vs Features
axes[1, 2].scatter(results_df['n_features'], results_df['accuracy'], s=200, alpha=0.6, c=range(len(results_df)))
for i, txt in enumerate(results_df['encoding']):
axes[1, 2].annotate(txt, (results_df.iloc[i]['n_features'], results_df.iloc[i]['accuracy']),
fontsize=8, ha='center')
axes[1, 2].set_xlabel('Number of Features')
axes[1, 2].set_ylabel('Accuracy')
axes[1, 2].set_title('Trade-off: Accuracy vs Dimensionality')
axes[1, 2].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# 5. Análisis de trade-offs
print("\n📊 ANÁLISIS DE TRADE-OFFS:")
print("-" * 60)
# TODO: Completa el análisis
print("🔍 Accuracy vs Dimensionalidad:")
print(f" Label Encoding: {results_df[results_df['encoding']=='Label Encoding']['accuracy'].values[0]:.4f} accuracy "
f"con {results_df[results_df['encoding']=='Label Encoding']['n_features'].values[0]} features")
print(f" Target Encoding: {results_df[results_df['encoding']=='Target Encoding (high card)']['accuracy'].values[0]:.4f} accuracy "
f"con {results_df[results_df['encoding']=='Target Encoding (high card)']['n_features'].values[0]} features")
print("\n🔍 Accuracy vs Tiempo:")
# TODO: Comparar qué método da mejor balance accuracy/tiempo
print("\n🔍 Recomendación para Producción:")
# TODO: Basándote en los resultados, ¿qué método recomendarías y por qué?
💡 PISTAS:
- 📊 ¿Qué métrica es más importante: accuracy, AUC, o F1?
- ⚡ ¿Vale la pena sacrificar velocidad por mejor accuracy?
- 📏 ¿Cuántas features son "demasiadas" para un modelo?
- 🎯 ¿Qué trade-offs considerar en producción?
🧪 Paso 9: Investigación Libre - Técnicas Adicionales¶
# === INVESTIGACIÓN LIBRE: TÉCNICAS AVANZADAS ===
print("\n🧪 INVESTIGACIÓN LIBRE")
print("=" * 60)
# 🎯 DESAFÍO 1: Frequency Encoding
print("🎯 DESAFÍO 1: Frequency Encoding")
print("-" * 40)
# TODO: Implementa frequency encoding
# PISTA: Codificar cada categoría con su frecuencia de aparición
def frequency_encoding(df, column):
"""
Codificar categorías por su frecuencia
"""
freq = df[column].value_counts(normalize=True).to_dict()
return df[column].map(freq)
# TODO: Aplica frequency encoding a 'city' y evalúa performance
# df_freq = df.copy()
# df_freq['city_freq'] = frequency_encoding(df_freq, 'city')
# ¿Cómo se compara con target encoding?
print("💡 PREGUNTAS:")
print("- ¿Frequency encoding captura información predictiva?")
print("- ¿Tiene riesgo de data leakage?")
print("- ¿Cuándo usar frequency vs target encoding?")
# 🎯 DESAFÍO 2: Ordinal Encoding
print("\n🎯 DESAFÍO 2: Ordinal Encoding")
print("-" * 40)
# TODO: Implementa ordinal encoding para variables con orden natural
# PISTA: 'education' y 'satisfaction' tienen orden natural
from sklearn.preprocessing import OrdinalEncoder
education_order = ['High School', 'Bachelor', 'Master', 'PhD']
satisfaction_order = ['Very Low', 'Low', 'Medium', 'High', 'Very High']
# TODO: Crea un encoder ordinal
# ordinal_encoder = OrdinalEncoder(categories=[education_order, satisfaction_order])
# ¿Mejora el performance vs label encoding?
print("💡 PREGUNTAS:")
print("- ¿Por qué preservar el orden natural es importante?")
print("- ¿Qué modelos se benefician más de ordinal encoding?")
print("- ¿Cómo manejar categorías con orden parcial?")
# 🎯 DESAFÍO 3: Leave-One-Out Encoding
print("\n🎯 DESAFÍO 3: Leave-One-Out Target Encoding")
print("-" * 40)
# TODO: Implementa leave-one-out encoding manualmente
# PISTA: Para cada registro, calcula el promedio del target excluyendo ese registro
def leave_one_out_encoding(X, y, column):
"""
Leave-one-out target encoding
"""
# Para cada registro, calcular promedio excluyendo ese registro
global_mean = y.mean()
# Calcular suma y conteo por categoría
agg = pd.DataFrame({'sum': y, 'count': y}).groupby(X[column]).agg({
'sum': 'sum',
'count': 'count'
})
# TODO: Implementa la lógica de leave-one-out
# Para cada registro i:
# encoded_value_i = (suma_categoria - y_i) / (count_categoria - 1)
pass
print("💡 PREGUNTAS:")
print("- ¿Por qué leave-one-out previene overfitting?")
print("- ¿Es más costoso computacionalmente?")
print("- ¿Cuándo usar LOO vs cross-validation?")
# 🎯 DESAFÍO 4: Binary Encoding
print("\n🎯 DESAFÍO 4: Binary Encoding")
print("-" * 40)
# TODO: Explora binary encoding de category_encoders
# PISTA: Convierte entero a binario y descompone en bits
from category_encoders import BinaryEncoder
# TODO: Aplica binary encoding a 'city' (alta cardinalidad)
# binary_encoder = BinaryEncoder(cols=['city'])
# ¿Cuántas columnas crea? (log2(n_categories))
# ¿Cómo se compara con one-hot y target?
print("💡 PREGUNTAS:")
print("- ¿Por qué binary encoding reduce dimensionalidad?")
print("- ¿Cuántas columnas crea para N categorías?")
print("- ¿En qué escenarios es útil binary encoding?")
# 🎯 DESAFÍO 5: Smoothing en Target Encoding
print("\n🎯 DESAFÍO 5: Experimentar con Smoothing")
print("-" * 40)
# TODO: Prueba diferentes valores de smoothing (1, 10, 100, 1000)
# ¿Cómo afecta el smoothing al performance?
smoothing_values = [1, 10, 100, 1000]
print("💡 PREGUNTAS:")
print("- ¿Qué hace el parámetro smoothing?")
print("- ¿Cuándo usar smoothing alto vs bajo?")
print("- ¿Cómo afecta a categorías raras?")
💡 PISTAS PARA INVESTIGACIÓN:
- 📚 Category Encoders Documentation
- 🎯 Target Encoding Best Practices
- 📊 Feature Engineering for ML - Cap. 5
- 🧪 Experimenta con diferentes configuraciones y documenta resultados
📝 Paso 10: Reflexión y Documentación¶
# === REFLEXIÓN FINAL ===
print("\n📝 REFLEXIÓN Y CONCLUSIONES")
print("=" * 60)
print("""
🎯 PREGUNTAS DE REFLEXIÓN OBLIGATORIAS:
1. COMPARACIÓN DE MÉTODOS:
- ¿Cuál método de encoding funcionó mejor en tu dataset?
- ¿Por qué crees que ese método fue superior?
- ¿Los resultados coinciden con tu intuición inicial?
2. TRADE-OFFS:
- ¿Qué trade-offs identificaste entre accuracy, tiempo y dimensionalidad?
- ¿Qué método recomendarías para producción y por qué?
- ¿Cómo balancearías performance vs complejidad?
3. DATA LEAKAGE:
- ¿Qué técnicas usaste para prevenir data leakage en target encoding?
- ¿Por qué es crítico usar cross-validation?
- ¿Qué pasaría si calcularas target encoding sin CV?
4. ALTA CARDINALIDAD:
- ¿Por qué one-hot encoding falla con alta cardinalidad?
- ¿Qué estrategias alternativas exploraste?
- ¿Cuándo usarías cada técnica?
5. PIPELINE BRANCHING:
- ¿Qué ventajas ofrece ColumnTransformer?
- ¿Cómo estructurarías un pipeline para producción?
- ¿Qué consideraciones adicionales incluirías?
6. APRENDIZAJES:
- ¿Qué fue lo más desafiante del assignment?
- ¿Qué técnica te sorprendió más?
- ¿Qué aplicaciones prácticas ves para estos métodos?
7. PRÓXIMOS PASOS:
- ¿Qué otras técnicas de encoding investigarías?
- ¿Cómo aplicarías esto a un proyecto real?
- ¿Qué experimentos adicionales te gustaría probar?
""")
# TODO: Escribe tus respuestas aquí
print("\n📝 MIS RESPUESTAS:")
print("-" * 60)
respuestas = {
'mejor_metodo': "_______", # ¿Cuál método funcionó mejor?
'razon': "_______", # ¿Por qué?
'trade_off_critico': "_______", # ¿Qué trade-off es más importante?
'recomendacion_produccion': "_______", # ¿Qué método para producción?
'leccion_clave': "_______", # ¿Qué aprendiste?
'proximos_pasos': "_______" # ¿Qué harías diferente?
}
for pregunta, respuesta in respuestas.items():
print(f"{pregunta}: {respuesta}")
print("\n✅ ASSIGNMENT COMPLETADO")
print("=" * 60)
🎯 Resumen y Checklist Final¶
✅ Lo que aprendiste:¶
- Analizar cardinalidad y clasificar variables categóricas
- Implementar múltiples técnicas de encoding (label, one-hot, target)
- Prevenir data leakage usando cross-validation en target encoding
- Crear pipelines con branching usando ColumnTransformer
- Comparar métodos con métricas cuantitativas
- Evaluar trade-offs entre accuracy, dimensionalidad y tiempo
- Aplicar técnicas avanzadas (smoothing, frequency encoding, ordinal)
🔍 Checklist de Completitud:¶
- Dataset con alta cardinalidad creado y analizado
- Label encoding implementado y evaluado
- One-hot encoding (solo baja cardinalidad) implementado
- Target encoding con CV implementado correctamente
- Pipeline con branching (ColumnTransformer) creado
- Comparación cuantitativa de todos los métodos realizada
- Trade-offs identificados y documentados
- Investigación libre completada (al menos 2 técnicas adicionales)
- Reflexión final documentada con respuestas completas
🤔 Preguntas conceptuales clave:¶
- ¿Qué es data leakage en target encoding y cómo prevenirlo?
- ¿Por qué one-hot encoding falla con alta cardinalidad?
- ¿Cuál es el propósito del smoothing en target encoding?
- ¿Cuándo usar label encoding vs target encoding vs one-hot?
- ¿Qué ventajas ofrece ColumnTransformer para pipelines?
- ¿Cómo manejar categorías no vistas en el conjunto de test?
- ¿Qué trade-offs considerar entre accuracy y dimensionalidad?