Estrategias de Ingeniería de Features para Machine Learning
Resumen
Buenos features superan a modelos complejos. Enfócate en conocimiento del dominio para crear features, codificación sistemática para categóricas, features de lag para series temporales y embeddings para texto. Siempre valida la importancia de features y vigila el data leakage.
La ingeniería de features sigue siendo la habilidad más impactante en machine learning aplicado. Mientras el deep learning ha automatizado algo del trabajo de features, la mayoría del ML del mundo real todavía depende de datos tabulares donde la ingeniería de features domina. Esta guía cubre técnicas prácticas que consistentemente mejoran modelos.
La Mentalidad de Ingeniería de Features
El objetivo no es crear muchas features—es crear features informativas que capturen patrones que el modelo no puede descubrir por sí mismo.
┌─────────────────────────────────────────────────────────────────┐
│ Pipeline de Ingeniería de Features │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Datos Crudos → Limpieza → Transformación → Creación → Selección│
│ │
│ ┌─────────┐ ┌──────────┐ ┌───────────┐ ┌──────────┐ │
│ │ Valores │ │ Encoding │ │ Features │ │ Selección│ │
│ │ Faltantes│→│ Escalado │→ │ de Dominio│→ │ de │ │
│ │ Outliers│ │ Binning │ │Interacciones│ │ Features │ │
│ └─────────┘ └──────────┘ └───────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Insight Clave
Dedica tiempo a entender el dominio antes de ingeniar features. La intuición de un experto del dominio sobre qué importa es frecuentemente más valiosa que la generación automatizada de features.
Features Numéricas
Escalado y Normalización
Diferentes modelos tienen diferentes requisitos:
import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
# Datos de ejemplo con outliers
df = pd.DataFrame({
'age': [25, 30, 35, 40, 45, 100], # 100 es un outlier
'income': [30000, 45000, 60000, 75000, 90000, 500000]
})
# StandardScaler: Media=0, Std=1 (sensible a outliers)
standard_scaler = StandardScaler()
df['age_standard'] = standard_scaler.fit_transform(df[['age']])
# MinMaxScaler: Rango [0,1] (sensible a outliers)
minmax_scaler = MinMaxScaler()
df['income_minmax'] = minmax_scaler.fit_transform(df[['income']])
# RobustScaler: Usa mediana e IQR (robusto a outliers)
robust_scaler = RobustScaler()
df['income_robust'] = robust_scaler.fit_transform(df[['income']])Transformaciones para Distribuciones Sesgadas
from scipy import stats
# Transformación log (para datos sesgados a la derecha)
df['income_log'] = np.log1p(df['income']) # log1p maneja ceros
# Transformación Box-Cox (requiere valores positivos)
df['income_boxcox'], lambda_param = stats.boxcox(df['income'] + 1)
# Transformación Yeo-Johnson (maneja valores negativos)
from sklearn.preprocessing import PowerTransformer
pt = PowerTransformer(method='yeo-johnson')
df['income_yeojohnson'] = pt.fit_transform(df[['income']])Estrategias de Binning
# Binning de ancho igual
df['age_bins_equal'] = pd.cut(df['age'], bins=5, labels=['muy_joven', 'joven', 'medio', 'senior', 'anciano'])
# Binning basado en cuantiles (frecuencia igual)
df['income_quartiles'] = pd.qcut(df['income'], q=4, labels=['Q1', 'Q2', 'Q3', 'Q4'])
# Binning personalizado basado en dominio
age_bins = [0, 18, 35, 50, 65, 120]
age_labels = ['menor', 'adulto_joven', 'mediana_edad', 'senior', 'anciano']
df['age_category'] = pd.cut(df['age'], bins=age_bins, labels=age_labels)Error Común
No ajustes los escaladores en todo tu conjunto de datos antes de dividir. Ajusta solo en datos de entrenamiento, luego transforma tanto train como test para prevenir data leakage.
Features Categóricas
Estrategias de Encoding
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
import category_encoders as ce
df = pd.DataFrame({
'color': ['rojo', 'azul', 'verde', 'rojo', 'azul'],
'size': ['S', 'M', 'L', 'XL', 'M'],
'city': ['CDMX', 'GDL', 'MTY', 'CDMX', 'TIJ']
})
# Label Encoding (categorías ordinales)
size_order = {'S': 1, 'M': 2, 'L': 3, 'XL': 4}
df['size_encoded'] = df['size'].map(size_order)
# One-Hot Encoding (categorías nominales con pocos valores)
df_onehot = pd.get_dummies(df, columns=['color'], prefix='color')
# Target Encoding (alta cardinalidad + relación con target)
target_encoder = ce.TargetEncoder(cols=['city'])
df['city_target_encoded'] = target_encoder.fit_transform(df['city'], y)
# Frequency Encoding (útil para modelos basados en árboles)
city_freq = df['city'].value_counts(normalize=True)
df['city_frequency'] = df['city'].map(city_freq)Manejando Alta Cardinalidad
def reduce_cardinality(series: pd.Series, threshold: float = 0.01) -> pd.Series:
"""Agrupa categorías raras en 'Otro'."""
value_counts = series.value_counts(normalize=True)
rare_categories = value_counts[value_counts < threshold].index
return series.replace(rare_categories, 'Otro')
# Ejemplo: Reducir categorías con menos del 1% de frecuencia
df['city_reduced'] = reduce_cardinality(df['city'], threshold=0.01)Features Temporales
Las features basadas en tiempo frecuentemente contienen señales ricas. La investigación de Zheng y Casari (2018) muestra que las features temporales consistentemente mejoran modelos de pronóstico.
Descomposición de Fecha/Hora
df = pd.DataFrame({
'timestamp': pd.date_range('2024-01-01', periods=100, freq='H')
})
# Extraer componentes
df['hour'] = df['timestamp'].dt.hour
df['day_of_week'] = df['timestamp'].dt.dayofweek
df['day_of_month'] = df['timestamp'].dt.day
df['month'] = df['timestamp'].dt.month
df['quarter'] = df['timestamp'].dt.quarter
df['year'] = df['timestamp'].dt.year
df['is_weekend'] = df['day_of_week'].isin([5, 6]).astype(int)
# Encoding cíclico para features periódicas
df['hour_sin'] = np.sin(2 * np.pi * df['hour'] / 24)
df['hour_cos'] = np.cos(2 * np.pi * df['hour'] / 24)
df['month_sin'] = np.sin(2 * np.pi * df['month'] / 12)
df['month_cos'] = np.cos(2 * np.pi * df['month'] / 12)Features de Lag y Estadísticas Móviles
def create_lag_features(df: pd.DataFrame, column: str, lags: list[int]) -> pd.DataFrame:
"""Crear versiones retrasadas de una columna."""
for lag in lags:
df[f'{column}_lag_{lag}'] = df[column].shift(lag)
return df
def create_rolling_features(df: pd.DataFrame, column: str, windows: list[int]) -> pd.DataFrame:
"""Crear estadísticas móviles."""
for window in windows:
df[f'{column}_rolling_mean_{window}'] = df[column].rolling(window).mean()
df[f'{column}_rolling_std_{window}'] = df[column].rolling(window).std()
df[f'{column}_rolling_min_{window}'] = df[column].rolling(window).min()
df[f'{column}_rolling_max_{window}'] = df[column].rolling(window).max()
return df
# Aplicar a datos de ventas
df = create_lag_features(df, 'sales', lags=[1, 7, 14, 28])
df = create_rolling_features(df, 'sales', windows=[7, 14, 28])Features de Texto
Features Básicas de Texto
import re
from collections import Counter
def extract_text_features(text: str) -> dict:
"""Extraer features estadísticas básicas del texto."""
words = text.split()
sentences = re.split(r'[.!?]+', text)
return {
'char_count': len(text),
'word_count': len(words),
'sentence_count': len([s for s in sentences if s.strip()]),
'avg_word_length': np.mean([len(w) for w in words]) if words else 0,
'unique_word_ratio': len(set(words)) / len(words) if words else 0,
'uppercase_ratio': sum(1 for c in text if c.isupper()) / len(text) if text else 0,
'digit_ratio': sum(1 for c in text if c.isdigit()) / len(text) if text else 0,
'punctuation_count': sum(1 for c in text if c in '.,!?;:'),
}TF-IDF y Embeddings
from sklearn.feature_extraction.text import TfidfVectorizer
from sentence_transformers import SentenceTransformer
# TF-IDF para ML tradicional
tfidf = TfidfVectorizer(max_features=1000, ngram_range=(1, 2))
text_features = tfidf.fit_transform(df['text_column'])
# Embeddings de oraciones para similitud semántica
model = SentenceTransformer('all-MiniLM-L6-v2')
embeddings = model.encode(df['text_column'].tolist())
embedding_df = pd.DataFrame(embeddings, columns=[f'emb_{i}' for i in range(embeddings.shape[1])])Interacciones de Features
Features Polinomiales
from sklearn.preprocessing import PolynomialFeatures
# Crear features de interacción
poly = PolynomialFeatures(degree=2, interaction_only=True, include_bias=False)
interaction_features = poly.fit_transform(df[['age', 'income']])
# Interacciones manuales significativas
df['income_per_age'] = df['income'] / df['age']
df['income_age_product'] = df['income'] * df['age']Interacciones Específicas del Dominio
# Ejemplo e-commerce
df['conversion_rate'] = df['purchases'] / df['visits']
df['avg_order_value'] = df['revenue'] / df['purchases']
df['pages_per_session'] = df['page_views'] / df['sessions']
# Ejemplo healthcare
df['bmi'] = df['weight_kg'] / (df['height_m'] ** 2)
df['pulse_pressure'] = df['systolic_bp'] - df['diastolic_bp']
df['map'] = df['diastolic_bp'] + (df['pulse_pressure'] / 3) # Presión arterial mediaSelección de Features
Métodos de Filtro
from sklearn.feature_selection import SelectKBest, f_classif, mutual_info_classif
# Pruebas estadísticas
selector = SelectKBest(score_func=f_classif, k=10)
X_selected = selector.fit_transform(X, y)
# Obtener scores de features
feature_scores = pd.DataFrame({
'feature': X.columns,
'score': selector.scores_
}).sort_values('score', ascending=False)
# Selección basada en correlación
def remove_correlated_features(df: pd.DataFrame, threshold: float = 0.95) -> list[str]:
"""Eliminar una de cada par de features altamente correlacionadas."""
corr_matrix = df.corr().abs()
upper = corr_matrix.where(np.triu(np.ones(corr_matrix.shape), k=1).astype(bool))
to_drop = [column for column in upper.columns if any(upper[column] > threshold)]
return to_dropSelección Basada en Modelos
from sklearn.ensemble import RandomForestClassifier
from sklearn.feature_selection import RFECV
# Importancia de features de modelos de árboles
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)
importance_df = pd.DataFrame({
'feature': X_train.columns,
'importance': rf.feature_importances_
}).sort_values('importance', ascending=False)
# Eliminación Recursiva de Features con CV
rfecv = RFECV(
estimator=rf,
step=1,
cv=5,
scoring='accuracy',
min_features_to_select=5
)
rfecv.fit(X_train, y_train)
selected_features = X_train.columns[rfecv.support_]Previniendo Data Leakage
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
# Crear pipeline de preprocesamiento
numeric_transformer = Pipeline([
('scaler', StandardScaler()),
])
categorical_transformer = Pipeline([
('encoder', OneHotEncoder(handle_unknown='ignore')),
])
preprocessor = ColumnTransformer([
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features),
])
# Pipeline completo asegura separación apropiada fit/transform
full_pipeline = Pipeline([
('preprocessor', preprocessor),
('classifier', RandomForestClassifier()),
])
# Esto previene leakage: ajusta solo en train, transforma ambos
full_pipeline.fit(X_train, y_train)
predictions = full_pipeline.predict(X_test)Conclusión
La ingeniería efectiva de features sigue estos principios:
- Entiende el dominio - El conocimiento del dominio guía la creación de features
- Transforma apropiadamente - Empareja transformaciones con distribuciones de datos y requisitos del modelo
- Crea interacciones significativas - Combina features que tienen significado en el dominio
- Selecciona rigurosamente - Elimina features redundantes e irrelevantes
- Previene leakage - Usa pipelines y separación apropiada train/test
Las mejores features cuentan una historia sobre tus datos que el modelo puede entender.
Referencias
Zheng, A., & Casari, A. (2018). Feature engineering for machine learning: Principles and techniques for data scientists. O'Reilly Media.
Kuhn, M., & Johnson, K. (2019). Feature engineering and selection: A practical approach for predictive models. CRC Press. http://www.feat.engineering/
Ng, A. (2018). Machine learning yearning. https://www.deeplearning.ai/programs/machine-learning-specialization/
Scikit-learn developers. (2024). Scikit-learn user guide: Preprocessing data. https://scikit-learn.org/stable/modules/preprocessing.html
¿Trabajando en un proyecto de machine learning? Contáctame para discutir estrategias de ingeniería de features.
Frequently Asked Questions
Osvaldo Restrepo
Senior Full Stack AI & Software Engineer. Building production AI systems that solve real problems.