⟁ FeatEng
// HERRAMIENTA INTERACTIVA · CIENCIAS DE DATOS · ML

Feature
Engineering

Aprende cuándo, por qué y cómo transformar datos crudos en señales poderosas para tus modelos. Cada concepto incluye visualizaciones interactivas y ejemplos reales.

8
Módulos
20+
Técnicas
Visualizaciones vivas
¿Por qué Feature Engineering?
// La intuición fundamental antes de ver las técnicas
🎯
El principio fundamental
GARBAGE IN → GARBAGE OUT · Los modelos no crean información, la amplifican
¿Qué es Feature Engineering?

Es el proceso de transformar datos crudos en representaciones que los algoritmos de Machine Learning puedan entender y aprovechar mejor.

Un modelo de ML es básicamente una función matemática. Si le das señales pobres, aprenderá relaciones pobres. Si le das señales ricas, aprenderá relaciones poderosas.

Ejemplo: Un árbol de decisión no puede "ver" que "enero" y "diciembre" están cerca en el tiempo. Pero si creas sin(2π·mes/12), sí puede.

¿Cuándo es crítico?

Siempre, pero especialmente cuando:

• Los datos tienen distribuciones sesgadas (precios, salarios, clics)

• Hay variables categóricas que el modelo no puede leer directamente

• Tienes fechas o texto sin procesar

• El modelo tiene bajo rendimiento pese a muchos datos

• Hay relaciones no lineales entre variables

IMPACTO TÍPICO EN MÉTRICAS
*Datos ilustrativos basados en casos reales reportados en competencias Kaggle y papers académicos.
DEMO: PREDECIR PRECIO DE CASA — FEATURES CRUDAS vs ENGINEERED
DATOS CRUDOS
R² ≈ 0.41 — Relación lineal débil
CON LOG TRANSFORM
R² ≈ 0.78 — Relación lineal fuerte ✓
¿Qué pasó? Los precios de casas siguen una distribución log-normal. Al aplicar log(), la relación con el área se vuelve lineal y la regresión lineal la puede capturar perfectamente.
🏥 Medicina
  • IMC = peso/altura²
  • Cambio de presión sanguínea
  • Días desde último evento
  • Ratio LDL/HDL
💳 Finanzas
  • Ratio deuda/ingreso
  • Volatilidad 30 días
  • Días sin transacciones
  • Cambio % mes anterior
🛒 E-commerce
  • Tasa de conversión histórica
  • Días desde última compra
  • Categorías únicas compradas
  • Ticket promedio por sesión
Regla de oro: El conocimiento del dominio es tu arma más poderosa. Un experto en medicina sabe que el ratio LDL/HDL es más predictivo que cada valor por separado. Esto ningún algoritmo automático lo puede descubrir sin guía.
Variables Numéricas
// Transformar distribuciones para que los modelos trabajen mejor
📊
Transformaciones de Distribución
LOG · SQRT · BOX-COX · YEO-JOHNSON
⏰ CUÁNDO USAR
  • Variable tiene sesgo positivo fuerte (skewness > 1)
  • Datos de precios, salarios, ingresos, poblaciones
  • Conteos de eventos (clicks, compras)
  • El modelo usa distancias o gradientes (regresión, SVM, redes neuronales)
  • Residuos del modelo muestran heterocedasticidad
💡 POR QUÉ FUNCIONA
  • Comprime la cola larga → reduce influencia de outliers
  • Linealiza relaciones multiplicativas → regresión lineal las captura
  • Estabiliza varianza → mejora convergencia en redes neuronales
  • Hace la distribución más "normal" → mejora modelos que asumen normalidad
🚫 EVITAR CUANDO
  • Valores negativos o cero (usar log1p o Yeo-Johnson)
  • Distribución ya es simétrica/normal
  • Modelos basados en árboles (no lo necesitan)
  • La escala original tiene significado semántico importante
🔬 EJEMPLO REAL
  • Precio de viviendas (Kaggle: House Prices)
  • Ingresos en modelos crediticios
  • Conteo de transacciones bancarias
  • Tiempo hasta evento en survival analysis
VISUALIZACIÓN INTERACTIVA — EFECTO DE LA TRANSFORMACIÓN
DISTRIBUCIÓN:
ANTES: distribución original
DESPUÉS: log1p(x)
import numpy as np from sklearn.preprocessing import PowerTransformer import pandas as pd # ── Opción 1: Log natural (datos > 0) df['precio_log'] = np.log(df['precio']) # ── Opción 2: Log1p (acepta ceros) df['precio_log1p'] = np.log1p(df['precio']) # ── Opción 3: Box-Cox (solo positivos, optimiza lambda) pt = PowerTransformer(method='box-cox') df['precio_boxcox'] = pt.fit_transform(df[['precio']]) # ── Opción 4: Yeo-Johnson (acepta negativos) pt2 = PowerTransformer(method='yeo-johnson') df['precio_yj'] = pt2.fit_transform(df[['precio']]) # ── Verificar skewness antes y después print(f"Skewness antes: {df['precio'].skew():.2f}") print(f"Skewness después: {df['precio_log1p'].skew():.2f}")
📦
Binning / Discretización
EQUAL-WIDTH · EQUAL-FREQUENCY · CUSTOM · DECISION TREE BINNING
⏰ CUÁNDO USAR
  • Variable continua con relación no-lineal al target
  • Quieres capturar efectos de umbral (ej: menores de 18 años)
  • Datos con outliers extremos que distorsionan
  • Necesitas interpretabilidad del modelo
  • Preparar datos para modelos lineales con no-linealidades
💡 POR QUÉ FUNCIONA
  • Captura relaciones no-lineales sin transformaciones complejas
  • Robustez a outliers — extremos van a un mismo bin
  • Permite que el modelo trate rangos de forma diferente
  • Reduce sobreajuste al suavizar variaciones locales
🚫 EVITAR CUANDO
  • Relación con target es genuinamente lineal
  • Pierdes demasiada información con pocos bins
  • Usas modelos tipo gradient boosting (ya capturan no-linealidades)
🔬 EJEMPLO REAL
  • Edad → [0-18], [19-35], [36-60], [60+]
  • Ingresos → quintiles de riqueza
  • Horas de uso → mañana/tarde/noche
  • Score crediticio → bandas de riesgo
DEMO INTERACTIVO — EFECTO DEL NÚMERO DE BINS
5
TIPO:
import pandas as pd import numpy as np # ── Equal-Width: cada bin tiene el mismo rango df['edad_bin_ew'] = pd.cut(df['edad'], bins=5) # ── Equal-Frequency: cada bin tiene los mismos datos df['edad_bin_ef'] = pd.qcut(df['edad'], q=5) # ── Custom bins con etiquetas (dominio experto) bins = [0, 18, 25, 35, 50, 65, 120] labels = ['menor','joven','adulto_joven','adulto','senior','mayor'] df['edad_grupo'] = pd.cut(df['edad'], bins=bins, labels=labels) # ── Decision Tree Binning (óptimo para el target) from sklearn.tree import DecisionTreeClassifier tree = DecisionTreeClassifier(max_leaf_nodes=6) tree.fit(df[['edad']], df['target']) df['edad_dt_bin'] = tree.apply(df[['edad']])
🔢
Features Polinomiales e Interacciones
x² · x³ · x·y · RATIO · DIFERENCIA
⏰ CUÁNDO USAR
  • Relación curvilínea entre feature y target (parábola, cúbica)
  • Modelos lineales que no capturan no-linealidades
  • Sabes que dos variables interactúan (el efecto de A depende de B)
  • Ratios tienen significado físico/económico (velocidad, densidad, rentabilidad)
💡 POR QUÉ FUNCIONA
  • Extiende el espacio de features permitiendo curvas
  • Captura sinergias entre variables que por separado no predicen
  • Los ratios normalizan por escala (precio/m² vs precio absoluto)
🚫 EVITAR CUANDO
  • Grado > 3: explosión combinatoria de features
  • Datos insuficientes: alta varianza, overfitting
  • Usar con regularización si grado alto
🔬 EJEMPLO REAL
  • área² para predecir precio (rendimientos decrecientes)
  • ingreso × edad (poder adquisitivo acumulado)
  • deuda/ingreso (ratio crediticio)
  • velocidad² en modelos de física
DEMO: REGRESIÓN LINEAL VS POLINOMIAL
GRADO:
from sklearn.preprocessing import PolynomialFeatures import pandas as pd # ── Features polinomiales automáticas (grado 2) poly = PolynomialFeatures(degree=2, include_bias=False) X_poly = poly.fit_transform(df[['area', 'habitaciones']]) # Genera: area, habitaciones, area², area·hab, habitaciones² # ── Ratios manuales (mejor cuando tienes dominio) df['precio_por_m2'] = df['precio'] / df['area'] df['ratio_deuda_ingreso'] = df['deuda'] / df['ingreso'] df['diff_temp'] = df['temp_max'] - df['temp_min'] # ── Interacciones específicas de dominio df['ingreso_por_edad'] = df['ingreso'] * df['edad']
Variables Categóricas
// Convertir texto y categorías en números que los modelos entiendan
🏷️
Encodings Categóricos
OHE · LABEL · TARGET · FREQUENCY · BINARY
MÉTODO CARDINALIDAD MODELO RECOMENDADO RIESGO LEAKAGE PRESERVA ORDEN NOTAS CLAVE
One-Hot (OHE)Baja (<15)Regresión, SVM, NNBajoNoCrea columna por categoría. Curse of dimensionality si alta card.
Label EncodingCualquieraÁrboles, XGBoostBajoSi hay ordenIntroduce orden artificial en nominales. Solo usar en ordinales o tree-based.
Target EncodingAlta (>15)Lineal, NN, BoostingAltoNoMuy poderoso pero requiere cross-validation para evitar leakage.
Frequency Enc.AltaCualquieraBajoNoSimple y robusto. Pierde distinción entre categorías con igual frecuencia.
Binary Enc.Media-AltaCualquieraBajoNoCompromiso entre OHE y Label. log₂(n) columnas en vez de n.
EmbeddingsMuy AltaRedes NeuronalesBajoNoAprende representación semántica. Requiere suficientes datos.
ONE-HOT ENCODING

Usar cuando: Categorías nominales sin orden, baja cardinalidad (<15 categorías), modelos sensibles a escala (Regresión, SVM, KNN, NN).

Por qué: No introduce ninguna relación de orden falsa entre categorías. "Rojo" no es mayor ni menor que "Azul".

Cuidado: Con 100+ categorías crea 100+ columnas → memoria y tiempo de entrenamiento explotan.

TARGET ENCODING

Usar cuando: Alta cardinalidad (ciudades, IDs, SKUs), el dataset es grande, modelos de boosting o regresión.

Por qué: Reemplaza cada categoría con la media del target → captura información predictiva directa.

⚠ CRÍTICO: Siempre usar con cross-validation o smoothing para evitar data leakage hacia el target.

LABEL ENCODING

Usar cuando: Variables ordinales con orden natural (Bajo/Medio/Alto, Malo/Regular/Bueno), o con modelos basados en árboles.

Por qué: Los árboles de decisión pueden aprender el orden real aunque sea arbitrario, sin importar el número asignado.

Cuidado: NUNCA usar en nominales con modelos lineales (introduce orden falso).

FREQUENCY ENCODING

Usar cuando: Alta cardinalidad, no tienes target disponible (unsupervised), quieres simplicidad y robustez.

Por qué: La frecuencia de una categoría suele correlacionar con el target en muchos problemas reales.

Limitación: Dos categorías con la misma frecuencia se vuelven indistinguibles.

VISUALIZACIÓN — ENCODING DE "CIUDAD" CON 5 CATEGORÍAS
MOSTRAR:
import pandas as pd from sklearn.preprocessing import LabelEncoder from category_encoders import TargetEncoder, BinaryEncoder # ── One-Hot Encoding df = pd.get_dummies(df, columns=['ciudad'], drop_first=True) # ── Label Encoding (solo para ordinales o tree-based) le = LabelEncoder() df['nivel_enc'] = le.fit_transform(df['nivel']) # ── Target Encoding CON cross-validation para evitar leakage from sklearn.model_selection import KFold te = TargetEncoder(smoothing=10) df['ciudad_target'] = te.fit_transform(df['ciudad'], df['target']) # ── Frequency Encoding freq = df['ciudad'].value_counts() / len(df) df['ciudad_freq'] = df['ciudad'].map(freq) # ── Binary Encoding (alta cardinalidad) be = BinaryEncoder(cols=['ciudad']) df = be.fit_transform(df)
Variables Temporales
// Extraer señales de fechas, horas y series de tiempo
Ingeniería de Features Temporales
EXTRACCIÓN · CICLICIDAD · LAGS · ROLLING STATS
⏰ CUÁNDO USAR
  • Tienes columnas de fecha/hora en el dataset
  • El comportamiento varía por hora, día, mes o temporada
  • Predices demanda, ventas, tráfico, fraude
  • Hay patrones estacionales o de tendencia
  • El tiempo transcurrido desde un evento importa
💡 POR QUÉ FUNCIONA
  • Los modelos no interpretan fechas directamente
  • Extraer componentes revela estacionalidad oculta
  • Los lags capturan inercia y autocorrelación
  • Rolling stats suavizan ruido y capturan tendencia local
  • Sin/cos evitan discontinuidad en variables cíclicas
🚫 EVITAR CUANDO
  • Usar timestamp bruto como feature numérico
  • Lags sin verificar que no hay leakage temporal
  • Rolling sobre ventanas que incluyen datos futuros
  • Asumir que el patrón temporal se mantiene en producción
🔬 EJEMPLO REAL
  • Ventas retail: día_semana, es_fin_semana, semana_año
  • Fraude bancario: hora_del_día, días_desde_última_transacción
  • Energía eléctrica: temperatura_lag1, consumo_rolling7
  • Click-through rate: hora, día_semana, días_hasta_evento
PROBLEMA DE CICLICIDAD — ¿POR QUÉ SIN/COS?
El problema: Diciembre (12) y Enero (1) están cerca en el tiempo, pero el modelo ve una diferencia de 11. Enero (1) y Febrero (2) parecen igual de cercanos que Diciembre y Noviembre.
VER:
LAGS Y ROLLING STATISTICS
MOSTRAR:
import pandas as pd import numpy as np # ── Extraer componentes básicos df['año'] = df['fecha'].dt.year df['mes'] = df['fecha'].dt.month df['dia'] = df['fecha'].dt.day df['hora'] = df['fecha'].dt.hour df['dia_semana'] = df['fecha'].dt.dayofweek df['es_fin_semana'] = (df['dia_semana'] >= 5).astype(int) df['semana_año'] = df['fecha'].dt.isocalendar().week # ── Encoding cíclico (SIEMPRE para variables periódicas) df['mes_sin'] = np.sin(2 * np.pi * df['mes'] / 12) df['mes_cos'] = np.cos(2 * np.pi * df['mes'] / 12) df['hora_sin'] = np.sin(2 * np.pi * df['hora'] / 24) df['hora_cos'] = np.cos(2 * np.pi * df['hora'] / 24) # ── Lags (cuidado: ordenar por tiempo primero) df = df.sort_values('fecha') df['ventas_lag1'] = df['ventas'].shift(1) df['ventas_lag7'] = df['ventas'].shift(7) # ── Rolling statistics df['ventas_mean7'] = df['ventas'].rolling(7).mean() df['ventas_std7'] = df['ventas'].rolling(7).std() # ── Tiempo desde evento df['dias_desde_registro'] = (df['fecha'] - df['fecha_registro']).dt.days
Selección de Features
// Más features no siempre es mejor — eliminar ruido mejora el modelo
Métodos de Selección de Features
FILTER · WRAPPER · EMBEDDED · IMPORTANCIA
La maldición de la dimensionalidad: Agregar features irrelevantes añade ruido, aumenta el tiempo de entrenamiento y puede hacer que el modelo "memorice" el ruido en lugar de aprender patrones reales.
FILTER METHODS

Cuándo: Primera exploración rápida, datasets grandes, antes de entrenar.

Métodos: Correlación, Chi², Mutual Information, Varianza.

Ventaja: Rápido, independiente del modelo.

Limitación: No considera interacciones entre features.

WRAPPER METHODS

Cuándo: Dataset pequeño-mediano, quieres el subconjunto óptimo para un modelo específico.

Métodos: RFE, Forward/Backward Selection.

Ventaja: Considera el modelo completo.

Limitación: Muy costoso computacionalmente.

EMBEDDED METHODS

Cuándo: Siempre que uses Lasso, Ridge, XGBoost o Random Forest.

Métodos: Lasso (L1), Feature Importance, SelectFromModel.

Ventaja: La selección ocurre durante el entrenamiento.

Limitación: Específico al modelo usado.

DEMO: IMPORTANCIA DE FEATURES (RANDOM FOREST SIMULADO)
from sklearn.feature_selection import ( SelectKBest, f_classif, mutual_info_classif, RFE, SelectFromModel, VarianceThreshold ) from sklearn.ensemble import RandomForestClassifier from sklearn.linear_model import Lasso # ── Filter: Mutual Information selector = SelectKBest(mutual_info_classif, k=10) X_new = selector.fit_transform(X_train, y_train) # ── Filter: Eliminar baja varianza vt = VarianceThreshold(threshold=0.01) X_var = vt.fit_transform(X_train) # ── Wrapper: RFE con Random Forest rf = RandomForestClassifier(n_estimators=100) rfe = RFE(estimator=rf, n_features_to_select=15) X_rfe = rfe.fit_transform(X_train, y_train) # ── Embedded: Lasso (L1 - pone coeficientes a 0) lasso = Lasso(alpha=0.01) sel = SelectFromModel(lasso) X_lasso = sel.fit_transform(X_train, y_train) # ── Embedded: Feature importance de XGBoost import xgboost as xgb model = xgb.XGBClassifier() model.fit(X_train, y_train) importances = model.feature_importances_
Escalado y Normalización
// Poner todas las variables en la misma escala
Técnicas de Escalado
STANDARDSCALER · MINMAX · ROBUST · NORMALIZER · QUANTILE
¿Cuándo NO necesitas escalar? Modelos basados en árboles (Decision Tree, Random Forest, XGBoost, LightGBM, CatBoost) son invariantes a la escala. Las particiones se basan en comparaciones de orden, no distancias.
MÉTODOFÓRMULARANGO RESULTADOOUTLIERSUSAR CON
StandardScaler(x - μ) / σ≈ [-3, 3]SensibleRegresión, SVM, PCA, KNN, NN
MinMaxScaler(x - min) / (max - min)[0, 1]Muy sensibleRedes neuronales, imagen, [0,1] requerido
RobustScaler(x - Q2) / (Q3 - Q1)VariableRobustoDatos con outliers, antes de imputar
Normalizerx / ||x||||x|| = 1ParcialNLP, clustering con cosine similarity
QuantileTransformerRank → Gaussiana≈ [-3, 3]RobustoDistribuciones muy no-normales
VISUALIZACIÓN — EFECTO DE CADA SCALER
SCALER:
from sklearn.preprocessing import ( StandardScaler, MinMaxScaler, RobustScaler, Normalizer, QuantileTransformer ) from sklearn.pipeline import Pipeline # ⚠ REGLA CRÍTICA: Fit SIEMPRE en train, transform en train y test scaler = StandardScaler() X_train_sc = scaler.fit_transform(X_train) # fit + transform X_test_sc = scaler.transform(X_test) # solo transform # ── Usar dentro de un Pipeline (forma correcta) pipeline = Pipeline([ ('scaler', RobustScaler()), ('model', SVR()) ]) pipeline.fit(X_train, y_train) # ── Diferentes scalers para diferentes columnas from sklearn.compose import ColumnTransformer ct = ColumnTransformer([ ('std', StandardScaler(), ['edad', 'ingreso']), ('rob', RobustScaler(), ['precio']), ('mm', MinMaxScaler(), ['score']) ])
Data Leakage
// El error más peligroso y silencioso en Machine Learning
Data Leakage — El Error Silencioso
TRAIN-TEST CONTAMINATION · TARGET LEAKAGE · TEMPORAL LEAKAGE
⚠ DEFINICIÓN: Data Leakage ocurre cuando información que NO estaría disponible en producción (en el momento de hacer la predicción) "se filtra" al modelo durante el entrenamiento. El modelo aprende a hacer trampa.
¿POR QUÉ ES TAN PELIGROSO?

El modelo muestra métricas artificialmente excelentes en validación (accuracy, AUC, RMSE muy buenos).

Pero al deployar en producción, el rendimiento colapsa porque la información filtrada ya no está disponible.

El equipo ha "desperdiciado" semanas optimizando un modelo que no sirve en producción.

SEÑAL DE ALERTA

Si tu modelo tiene métricas demasiado buenas para ser verdad (AUC > 0.99, accuracy > 98%), probablemente hay leakage.

Si al deployar el rendimiento baja drásticamente, confirma leakage.

Si una feature tiene correlación muy alta con el target (> 0.95), investiga por qué.

TIPO 1: LEAKAGE DE DATOS DE TEST

Ejemplo: Aplicas StandardScaler.fit() sobre todo el dataset antes de hacer el split. El scaler "conoce" la media y varianza del test set.

Por qué es leakage: En producción, el scaler fue ajustado con datos de test que "vendrán en el futuro".

Solución: Siempre hacer split primero, luego fit del scaler solo en train.

TIPO 2: TARGET LEAKAGE

Ejemplo: Para predecir si un cliente abandonará (churn), incluyes "número de llamadas al servicio al cliente en el último mes". Pero esa info solo se sabe después de que el cliente ya está por irse.

Solución: Preguntarte: ¿Esta feature estaba disponible ANTES de hacer la predicción?

TIPO 3: LEAKAGE TEMPORAL

Ejemplo: En series de tiempo, calculas un rolling mean sin verificar que no incluye datos futuros. O usas datos de validación para calcular estadísticas de grupo.

Solución: TimeSeriesSplit, asegurar que lags y rolling windows sean "look-back only".

TIPO 4: TARGET ENCODING SIN CV

Ejemplo: Calculas target encoding usando todo el training set, incluyendo la observación misma → la categoría "conoce" su propio target.

Solución: Target encoding siempre con cross-validation o leave-one-out encoding.

VISUALIZACIÓN — EL EFECTO DEL LEAKAGE EN MÉTRICAS
ESCENARIO:
from sklearn.model_selection import train_test_split from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler # ✅ CORRECTO: Split PRIMERO, luego fit del preprocessor X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) scaler = StandardScaler() X_train_sc = scaler.fit_transform(X_train) # fit solo en train X_test_sc = scaler.transform(X_test) # transform en test # ✅ MEJOR: Usar Pipeline (evita errores humanos) pipe = Pipeline([('sc', StandardScaler()), ('model', LogisticRegression())]) pipe.fit(X_train, y_train) # scaler se fit solo en train automáticamente pipe.score(X_test, y_test) # ✅ Target Encoding correcto con cross-val from sklearn.model_selection import cross_val_predict encoded_oof = cross_val_predict(TargetEncoder(), X[['ciudad']], y, cv=5) # ✅ Series de tiempo: usar TimeSeriesSplit from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) # Garantiza que siempre validamos en datos FUTUROS al train