Image for post Ingeniería de Características para Datos Tabulares: Potenciando tus Modelos de ML con Python

Ingeniería de Características para Datos Tabulares: Potenciando tus Modelos de ML con Python


Contexto del Problema

En el mundo del Machine Learning (ML), la calidad de tus datos es tan crucial como el algoritmo que elijas. A menudo, los desarrolladores junior y mid se centran en probar diferentes modelos o ajustar hiperparámetros, pero pasan por alto un paso fundamental: la ingeniería de características. Imagina que tienes un modelo de predicción de precios de casas. Si solo le das el número de habitaciones, el modelo tendrá una visión limitada. Pero si le proporcionas características como el tamaño del jardín, la distancia a la escuela más cercana, o la antigüedad de la casa, su capacidad para aprender y predecir mejorará drásticamente.

La ingeniería de características es el proceso de transformar los datos crudos en características que los algoritmos de ML puedan entender y utilizar de manera más efectiva. Es el arte de crear nuevas variables a partir de las existentes, o de modificar las actuales, para resaltar patrones ocultos y mejorar el rendimiento del modelo. Sin una buena ingeniería de características, incluso el modelo más sofisticado puede rendir por debajo de su potencial, un concepto a menudo resumido como "garbage in, garbage out" (basura entra, basura sale).

Conceptos Clave

La ingeniería de características abarca diversas técnicas para preparar y enriquecer tus datos. Aquí te presento los conceptos fundamentales:

  • Características Numéricas: Son valores cuantitativos, como la edad, el ingreso o la temperatura. Pueden ser continuas o discretas.
  • Características Categóricas: Representan categorías o grupos, como el género, la ciudad o el tipo de producto. Pueden ser nominales (sin orden) u ordinales (con un orden inherente).
  • Características de Fecha/Hora: Datos temporales que pueden contener información valiosa sobre estacionalidad, tendencias o ciclos.
  • Imputación de Valores Faltantes: Rellenar los datos ausentes utilizando estrategias como la media, la mediana, la moda o métodos más avanzados.
  • Codificación de Variables Categóricas: Convertir categorías textuales en representaciones numéricas que los modelos puedan procesar. Las técnicas comunes incluyen One-Hot Encoding y Label Encoding.
  • Escalado de Características Numéricas: Ajustar la escala de las características numéricas para que todas contribuyan por igual al modelo, evitando que las de mayor magnitud dominen. Métodos populares son StandardScaler y MinMaxScaler.
  • Transformaciones Numéricas: Aplicar funciones matemáticas (logaritmo, raíz cuadrada, etc.) para manejar distribuciones sesgadas o relaciones no lineales.
  • Creación de Nuevas Características: Generar características a partir de combinaciones o extracciones de las existentes, a menudo utilizando el conocimiento del dominio. Por ejemplo, crear 'Precio por metro cuadrado' a partir de 'Precio' y 'Superficie'.
  • Características de Interacción: Combinar dos o más características para capturar relaciones no lineales que de otro modo pasarían desapercibidas.

Implementación Paso a Paso

Vamos a ver cómo aplicar estas técnicas utilizando Python con las librerías pandas y scikit-learn. Para este ejemplo, simularemos un dataset de ventas.

1. Preparación del Entorno y Datos

Primero, asegúrate de tener instaladas las librerías necesarias:


pip install pandas scikit-learn numpy

Crearemos un DataFrame de ejemplo:


import pandas as pd
import numpy as np
from sklearn.preprocessing import OneHotEncoder, StandardScaler, PolynomialFeatures
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

# Crear un DataFrame de ejemplo
data = {
    'ID_Cliente': range(1, 101),
    'Edad': np.random.randint(18, 70, 100),
    'Ingresos_Anuales': np.random.randint(25000, 150000, 100),
    'Tipo_Producto': np.random.choice(['Electrónica', 'Ropa', 'Alimentos', 'Hogar'], 100),
    'Ciudad': np.random.choice(['Madrid', 'Barcelona', 'Valencia', 'Sevilla', 'Bilbao'], 100),
    'Fecha_Compra': pd.to_datetime(pd.date_range(start='2023-01-01', periods=100, freq='D')),
    'Cantidad_Comprada': np.random.randint(1, 10, 100),
    'Precio_Unitario': np.random.uniform(10, 500, 100),
    'Rating_Producto': np.random.uniform(1, 5, 100).round(1)
}
df = pd.DataFrame(data)

# Introducir algunos valores faltantes para demostración
df.loc[df.sample(frac=0.05).index, 'Ingresos_Anuales'] = np.nan
df.loc[df.sample(frac=0.03).index, 'Rating_Producto'] = np.nan
df.loc[df.sample(frac=0.02).index, 'Tipo_Producto'] = np.nan

print("DataFrame original con valores faltantes:")
print(df.head())
print("\nInformación del DataFrame:")
print(df.info())

2. Manejo de Valores Faltantes

Utilizaremos SimpleImputer para rellenar los valores faltantes. Para características numéricas, usaremos la media; para categóricas, la moda.


# Imputación para características numéricas (media)
for col in ['Ingresos_Anuales', 'Rating_Producto']:
    if df[col].isnull().any():
        imputer_numeric = SimpleImputer(strategy='mean')
        df[col] = imputer_numeric.fit_transform(df[[col]])

# Imputación para características categóricas (moda)
for col in ['Tipo_Producto']:
    if df[col].isnull().any():
        imputer_categorical = SimpleImputer(strategy='most_frequent')
        df[col] = imputer_categorical.fit_transform(df[[col]])

print("\nDataFrame después de imputar valores faltantes:")
print(df.head())
print("\nValores faltantes después de imputación:")
print(df.isnull().sum())

3. Codificación de Variables Categóricas

Aplicaremos One-Hot Encoding a 'Tipo_Producto' y 'Ciudad' para convertirlas en un formato numérico binario.


# One-Hot Encoding para 'Tipo_Producto' y 'Ciudad'
encoder = OneHotEncoder(handle_unknown='ignore', sparse_output=False)
encoded_features = encoder.fit_transform(df[['Tipo_Producto', 'Ciudad']])
encoded_df = pd.DataFrame(encoded_features, columns=encoder.get_feature_names_out(['Tipo_Producto', 'Ciudad']))

df = pd.concat([df.drop(columns=['Tipo_Producto', 'Ciudad']), encoded_df], axis=1)

print("\nDataFrame después de One-Hot Encoding:")
print(df.head())

4. Creación de Características de Fecha/Hora

Extraeremos el año, mes, día de la semana y si es fin de semana de la columna 'Fecha_Compra'.


# Características de Fecha/Hora
df['Año_Compra'] = df['Fecha_Compra'].dt.year
df['Mes_Compra'] = df['Fecha_Compra'].dt.month
df['Dia_Semana_Compra'] = df['Fecha_Compra'].dt.dayofweek # Lunes=0, Domingo=6
df['Es_Fin_Semana'] = (df['Fecha_Compra'].dt.dayofweek >= 5).astype(int)

print("\nDataFrame con características de fecha/hora:")
print(df[['Fecha_Compra', 'Año_Compra', 'Mes_Compra', 'Dia_Semana_Compra', 'Es_Fin_Semana']].head())

5. Creación de Nuevas Características e Interacciones

Generaremos 'Gasto_Total' y una característica de interacción 'Edad_x_Ingresos'.


# Nueva característica: Gasto Total
df['Gasto_Total'] = df['Cantidad_Comprada'] * df['Precio_Unitario']

# Característica de interacción: Edad x Ingresos
df['Edad_x_Ingresos'] = df['Edad'] * df['Ingresos_Anuales']

print("\nDataFrame con nuevas características e interacciones:")
print(df[['Cantidad_Comprada', 'Precio_Unitario', 'Gasto_Total', 'Edad', 'Ingresos_Anuales', 'Edad_x_Ingresos']].head())

6. Escalado de Características Numéricas

Escalaremos las características numéricas para que tengan una media de 0 y una desviación estándar de 1 (estandarización).


# Seleccionar columnas numéricas para escalar (excluyendo ID y las ya transformadas/codificadas)
numeric_cols_to_scale = ['Edad', 'Ingresos_Anuales', 'Cantidad_Comprada', 'Precio_Unitario', 'Gasto_Total', 'Edad_x_Ingresos', 'Rating_Producto']

scaler = StandardScaler()
df[numeric_cols_to_scale] = scaler.fit_transform(df[numeric_cols_to_scale])

print("\nDataFrame después de escalar características numéricas:")
print(df[numeric_cols_to_scale].head())

Mini Proyecto / Aplicación Sencilla

Para demostrar el impacto de la ingeniería de características, construiremos un modelo de regresión lineal simple para predecir el 'Gasto_Total' de un cliente. Compararemos un modelo con características crudas versus uno con características ingenierizadas.


# --- Mini Proyecto: Predicción de Gasto Total ---

# Recrear DataFrame original para comparación
df_raw = pd.DataFrame(data)
df_raw.loc[df_raw.sample(frac=0.05).index, 'Ingresos_Anuales'] = np.nan
df_raw.loc[df_raw.sample(frac=0.03).index, 'Rating_Producto'] = np.nan
df_raw.loc[df_raw.sample(frac=0.02).index, 'Tipo_Producto'] = np.nan

# Imputar valores faltantes en df_raw (solo numéricos para simplificar)
for col in ['Ingresos_Anuales', 'Rating_Producto']:
    if df_raw[col].isnull().any():
        imputer_numeric = SimpleImputer(strategy='mean')
        df_raw[col] = imputer_numeric.fit_transform(df_raw[[col]])

# Modelo con características crudas (solo numéricas imputadas)
X_raw = df_raw[['Edad', 'Ingresos_Anuales', 'Cantidad_Comprada', 'Precio_Unitario', 'Rating_Producto']]
y = (df_raw['Cantidad_Comprada'] * df_raw['Precio_Unitario'])

X_train_raw, X_test_raw, y_train_raw, y_test_raw = train_test_split(X_raw, y, test_size=0.2, random_state=42)

model_raw = LinearRegression()
model_raw.fit(X_train_raw, y_train_raw)
y_pred_raw = model_raw.predict(X_test_raw)
mse_raw = mean_squared_error(y_test_raw, y_pred_raw)

print(f"\n--- Resultados del Modelo con Características Crudas ---")
print(f"MSE (Error Cuadrático Medio): {mse_raw:.2f}")

# Modelo con características ingenierizadas
# Asegurarse de que df tenga las columnas correctas para X_engineered
# Eliminar columnas no numéricas o ya procesadas que no son features directas
X_engineered = df.drop(columns=['ID_Cliente', 'Fecha_Compra', 'Cantidad_Comprada', 'Precio_Unitario', 'Gasto_Total'])
y_engineered = df['Gasto_Total'] # Usamos Gasto_Total ya escalado como target para este ejemplo

# Si y_engineered está escalado, el MSE será diferente. Para una comparación justa, 
# podríamos usar el Gasto_Total no escalado como target y escalar solo las features.
# Para este ejemplo, vamos a predecir el Gasto_Total escalado para demostrar el proceso.

# Dividir los datos para el modelo ingenierizado
X_train_eng, X_test_eng, y_train_eng, y_test_eng = train_test_split(X_engineered, y_engineered, test_size=0.2, random_state=42)

model_engineered = LinearRegression()
model_engineered.fit(X_train_eng, y_train_eng)
y_pred_eng = model_engineered.predict(X_test_eng)
mse_eng = mean_squared_error(y_test_eng, y_pred_eng)

print(f"\n--- Resultados del Modelo con Características Ingenierizadas ---")
print(f"MSE (Error Cuadrático Medio): {mse_eng:.2f}")

# Nota: El MSE del modelo ingenierizado será sobre el 'Gasto_Total' escalado. 
# Para una comparación directa de la mejora predictiva en la escala original, 
# necesitaríamos desescalar las predicciones o escalar el target 'Gasto_Total' 
# del modelo crudo de la misma manera.

Este mini-proyecto ilustra cómo la preparación y creación de características puede impactar directamente la capacidad predictiva de un modelo. Aunque el MSE de los modelos crudos y los ingenierizados no son directamente comparables en este ejemplo debido al escalado del target en el segundo caso, el objetivo es mostrar el flujo de trabajo. En un escenario real, esperaríamos una mejora en el rendimiento predictivo del modelo con características bien ingenierizadas.

Errores Comunes y Depuración

La ingeniería de características, aunque poderosa, está llena de trampas. Aquí algunos errores comunes y cómo evitarlos:

  • Fuga de Datos (Data Leakage): Ocurre cuando se utiliza información del conjunto de prueba (o incluso del target) durante la fase de ingeniería de características en el conjunto de entrenamiento. Esto lleva a métricas de rendimiento engañosamente optimistas que no se sostienen en producción. Solución: Realiza la división entre conjuntos de entrenamiento y prueba *antes* de cualquier paso de ingeniería de características que dependa de estadísticas del dataset (como la media para imputación o el escalado). Aplica las transformaciones aprendidas del conjunto de entrenamiento al conjunto de prueba.
  • Ignorar Variables Categóricas: Descartar variables categóricas o no codificarlas correctamente. Muchos modelos no pueden trabajar directamente con texto. Solución: Utiliza técnicas de codificación apropiadas como One-Hot Encoding o Label Encoding.
  • No Escalar Características Numéricas: Algunos algoritmos (como regresión lineal, SVM, redes neuronales) son sensibles a la escala de las características, dando más peso a las de mayor magnitud. Solución: Aplica escalado (StandardScaler, MinMaxScaler) a las características numéricas, especialmente si el algoritmo lo requiere.
  • Exceso de Características (Overfitting): Crear demasiadas características o características irrelevantes puede llevar al sobreajuste, donde el modelo memoriza el ruido en lugar de los patrones reales. Solución: Utiliza técnicas de selección de características (como eliminación recursiva de características, selección basada en importancia) y valida tu modelo con validación cruzada.
  • Falta de Conocimiento del Dominio: Realizar ingeniería de características sin entender el contexto del negocio o los datos. Solución: Colabora con expertos del dominio para identificar características significativas y relevantes.
  • Manejo Incorrecto de Valores Faltantes: Imputar valores faltantes de forma ingenua puede distorsionar la distribución o introducir sesgos. Solución: Analiza el patrón de los valores faltantes. Considera imputación basada en modelos, o incluso crear una característica binaria que indique la presencia de un valor faltante.

Aprendizaje Futuro / Próximos Pasos

La ingeniería de características es un campo vasto y en constante evolución. Aquí hay algunas áreas para explorar a medida que avanzas:

  • Ingeniería de Características Automatizada (Automated Feature Engineering): Librerías como Featuretools o Autofeat pueden generar automáticamente una gran cantidad de características candidatas a partir de datos relacionales o series temporales, reduciendo el trabajo manual.
  • Feature Stores: Para proyectos de ML más grandes y en producción, los Feature Stores (como Feast) ayudan a gestionar, versionar y servir características de manera consistente para entrenamiento e inferencia.
  • Técnicas Avanzadas de Codificación Categórica: Explora métodos como Target Encoding, Binary Encoding o CatBoost Encoding, que pueden ser más efectivos que One-Hot Encoding para variables con alta cardinalidad.
  • Ingeniería de Características para Tipos de Datos Específicos: Aprende técnicas para datos de texto (TF-IDF, embeddings), imágenes (extracción de características con redes convolucionales) o series temporales (ventanas deslizantes, características de Fourier).
  • Selección de Características: Una vez que has creado muchas características, el siguiente paso es seleccionar las más relevantes para evitar el sobreajuste y mejorar la interpretabilidad. Técnicas como la importancia de características de modelos basados en árboles, RFE (Recursive Feature Elimination) o métodos basados en correlación son útiles.
  • Pipelines de Scikit-learn: Para organizar y automatizar tu flujo de trabajo de preprocesamiento y modelado, los pipelines de Scikit-learn son una herramienta invaluable. Permiten encadenar transformaciones y modelos, asegurando que las transformaciones se apliquen consistentemente.

Dominar la ingeniería de características te dará una ventaja significativa en cualquier proyecto de Machine Learning. Es una habilidad que combina la creatividad, el conocimiento del dominio y la destreza técnica para desbloquear el verdadero potencial de tus datos.