Ingeniería de Características en Datos Tabulares con Pandas y NumPy 🛠️
Este tutorial te guiará a través de las técnicas esenciales de ingeniería de características utilizando las bibliotecas Pandas y NumPy. Descubrirás cómo transformar tus datos crudos en variables significativas, mejorando drásticamente el rendimiento de tus modelos de Machine Learning.
La ingeniería de características es uno de los pasos más críticos y creativos en el pipeline de Machine Learning. No importa cuán sofisticado sea tu algoritmo, si tus características son débiles, tu modelo tendrá dificultades para aprender patrones significativos. En este tutorial, exploraremos cómo utilizar la potencia de Pandas y NumPy para construir características robustas y relevantes a partir de datos tabulares.
¿Qué es la Ingeniería de Características? 🤔
En esencia, la ingeniería de características es el proceso de transformar datos crudos en características que representen mejor el problema subyacente al modelo predictivo, resultando en un mejor rendimiento del modelo. Implica crear nuevas características a partir de las existentes o transformar las características actuales.
La Importancia Crucial de las Características ✨
Las características son los inputs que un modelo de Machine Learning utiliza para aprender y hacer predicciones. Imagina que quieres predecir el precio de una casa. Las características podrían ser el número de habitaciones, el tamaño del jardín, la ubicación, el año de construcción, etc. La forma en que presentas estas características al modelo puede cambiar radicalmente su capacidad para encontrar relaciones.
"Limpiar tus datos, crear buenas características y construir un modelo es el 80% de lo que hace un científico de datos." – Andrew Ng
Configuración del Entorno 🚀
Antes de sumergirnos en la acción, asegúrate de tener las bibliotecas necesarias instaladas. Si aún no las tienes, puedes instalarlas fácilmente usando pip.
pip install pandas numpy scikit-learn matplotlib seaborn
Una vez instaladas, importémoslas en nuestro entorno de trabajo.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
# Configuraciones para mejor visualización
sns.set_style("whitegrid")
plt.rcParams['figure.figsize'] = (10, 6)
plt.rcParams['font.size'] = 12
Preparando los Datos de Ejemplo 📊
Para ilustrar los conceptos, crearemos un DataFrame sintético que simule datos de clientes de una tienda online. Este DataFrame contendrá varios tipos de datos que nos permitirán explorar diferentes técnicas de ingeniería de características.
data = {
'CustomerID': range(1, 101),
'Age': np.random.randint(18, 70, 100),
'Gender': np.random.choice(['Male', 'Female'], 100),
'ProductCategory': np.random.choice(['Electronics', 'Apparel', 'Books', 'Home Goods'], 100),
'PurchaseAmount': np.random.normal(loc=150, scale=50, size=100).round(2),
'NumPurchases': np.random.randint(1, 15, 100),
'DaysSinceLastPurchase': np.random.randint(1, 100, 100),
'MembershipLevel': np.random.choice(['Bronze', 'Silver', 'Gold'], 100, p=[0.5, 0.3, 0.2]),
'ReviewScore': np.random.randint(1, 6, 100),
'IsPromotionClicked': np.random.choice([0, 1], 100, p=[0.7, 0.3])
}
df = pd.DataFrame(data)
# Añadir algunos valores nulos para demostración
for col in ['PurchaseAmount', 'DaysSinceLastPurchase']:
df.loc[df.sample(frac=0.05).index, col] = np.nan
print(df.head())
Visualización del DataFrame inicial
CustomerID Age Gender ProductCategory PurchaseAmount NumPurchases DaysSinceLastPurchase MembershipLevel ReviewScore IsPromotionClicked 0 1 52 Male Electronics 208.77 13 33.0 Gold 1 1 1 2 44 Female Electronics 224.77 7 50.0 Gold 2 0 2 3 51 Male Apparel 86.74 2 NaN Silver 3 0 3 4 60 Male Electronics 142.17 6 45.0 Bronze 5 1 4 5 22 Female Apparel 104.22 8 12.0 Gold 1 1
Limpieza y Preprocesamiento Inicial de Datos 🧹
Antes de crear nuevas características, es fundamental asegurar que nuestros datos estén limpios. Esto a menudo implica manejar valores nulos y convertir tipos de datos si es necesario.
Manejo de Valores Nulos 🕵️♀️
En nuestro DataFrame de ejemplo, hemos introducido algunos NaN. Una estrategia común para variables numéricas es imputar la media o la mediana.
# Imputar valores nulos con la mediana
df['PurchaseAmount'].fillna(df['PurchaseAmount'].median(), inplace=True)
df['DaysSinceLastPurchase'].fillna(df['DaysSinceLastPurchase'].median(), inplace=True)
print("Valores nulos después de imputación:")
print(df.isnull().sum())
Técnicas de Ingeniería de Características con Pandas y NumPy 💡
Ahora que nuestros datos están limpios, podemos empezar a aplicar diferentes técnicas para crear características más expresivas.
1. Combinación de Características Existentes ➕➖✖️➗
Podemos crear nuevas características combinando dos o más características existentes mediante operaciones aritméticas. Esto a menudo revela relaciones que no eran obvias individualmente.
Ejemplo: Gasto Promedio por Compra (AveragePurchaseValue)
df['AveragePurchaseValue'] = df['PurchaseAmount'] / df['NumPurchases']
print(df[['PurchaseAmount', 'NumPurchases', 'AveragePurchaseValue']].head())
2. Extracción de Información de Fechas y Horas ⏰
Aunque nuestro dataset de ejemplo no contiene fechas, es una técnica fundamental. Imagina una columna TransactionDate. Podrías extraer el día de la semana, el mes, el año, si es fin de semana, etc.
Ejemplo hipotético con fechas
```python # Si tuvieras una columna 'TransactionDate' de tipo datetime # df['TransactionDate'] = pd.to_datetime(df['TransactionDate']) # df['Year'] = df['TransactionDate'].dt.year # df['Month'] = df['TransactionDate'].dt.month # df['DayOfWeek'] = df['TransactionDate'].dt.dayofweek # df['IsWeekend'] = df['TransactionDate'].dt.dayofweek.isin([5, 6]).astype(int) ```3. Binning o Discretización (Agrupación de Características Numéricas) 📦
Convertir una característica numérica continua en una categórica dividiéndola en 'bins' o rangos. Esto puede ayudar a capturar relaciones no lineales y hacer que las características sean más robustas a los outliers.
Ejemplo: Agrupar la Edad en Categorías (AgeGroup)
bins = [18, 25, 35, 50, 70]
labels = ['Young Adult', 'Adult', 'Middle-Aged', 'Senior']
df['AgeGroup'] = pd.cut(df['Age'], bins=bins, labels=labels, right=False)
print(df[['Age', 'AgeGroup']].head())
Visualización de la Discretización
4. Codificación de Variables Categóricas 🏷️
Los modelos de Machine Learning generalmente requieren entradas numéricas. Las variables categóricas deben ser transformadas.
a) One-Hot Encoding (OHE) 🌡️
Ideal para variables categóricas nominales (sin orden intrínseco). Crea nuevas columnas binarias para cada categoría.
Ejemplo: Categoría de Producto (ProductCategory)
df_encoded = pd.get_dummies(df, columns=['ProductCategory'], prefix='ProductCat')
print(df_encoded[[col for col in df_encoded.columns if 'ProductCat_' in col]].head())
b) Label Encoding (Codificación de Etiquetas) 🔡
Adecuada para variables categóricas ordinales (con un orden intrínseco). Asigna un número entero a cada categoría.
Ejemplo: Nivel de Membresía (MembershipLevel)
membership_mapping = {'Bronze': 1, 'Silver': 2, 'Gold': 3}
df['MembershipLevel_Encoded'] = df['MembershipLevel'].map(membership_mapping)
print(df[['MembershipLevel', 'MembershipLevel_Encoded']].head())
5. Interacciones de Características 🤝
Crear una nueva característica multiplicando o combinando dos características existentes puede capturar interacciones complejas. Por ejemplo, el gasto en un producto puede depender de la edad y la categoría del producto.
Ejemplo: Gasto por Edad y Nivel de Membresía (Purchase_x_Membership)
df['Purchase_x_Membership'] = df['PurchaseAmount'] * df['MembershipLevel_Encoded']
print(df[['PurchaseAmount', 'MembershipLevel_Encoded', 'Purchase_x_Membership']].head())
6. Características Basadas en Frecuencia/Conteo 📊
Una característica puede ser el recuento de ocurrencias de un valor en otra columna. Esto puede ser útil para identificar popularidad o rareza.
Ejemplo: Frecuencia de Categoría de Producto (ProductCategory_Frequency)
category_counts = df['ProductCategory'].value_counts().to_dict()
df['ProductCategory_Frequency'] = df['ProductCategory'].map(category_counts)
print(df[['ProductCategory', 'ProductCategory_Frequency']].head())
7. Transformaciones Numéricas (Log, Raíz Cuadrada, Box-Cox) 📈
Algunas características numéricas pueden tener distribuciones sesgadas. Transformarlas puede ayudar a que se ajusten mejor a los supuestos de ciertos modelos (ej. regresión lineal) o a reducir el impacto de outliers.
Ejemplo: Transformación Logarítmica de PurchaseAmount
# Añadimos un pequeño valor para evitar log(0) si fuera posible
df['PurchaseAmount_Log'] = np.log1p(df['PurchaseAmount'])
# Visualizar la distribución antes y después
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
sns.histplot(df['PurchaseAmount'], bins=30, kde=True, ax=axes[0]).set_title('PurchaseAmount Original')
sns.histplot(df['PurchaseAmount_Log'], bins=30, kde=True, ax=axes[1]).set_title('PurchaseAmount Log-Transformado')
plt.tight_layout()
plt.show()
Pipelines de Ingeniería de Características (Scikit-learn) ⚙️
Cuando trabajamos con múltiples transformaciones, es crucial mantener un flujo de trabajo organizado y reproducible. Scikit-learn ofrece herramientas excelentes para esto, como ColumnTransformer y Pipeline.
Consideremos un subconjunto de nuestras características para demostrar un pipeline.
from sklearn.preprocessing import StandardScaler, OneHotEncoder, LabelEncoder
from sklearn.impute import SimpleImputer
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
# Definir las características para el pipeline
features_numeric = ['Age', 'PurchaseAmount', 'NumPurchases', 'DaysSinceLastPurchase']
features_categorical_ohe = ['ProductCategory']
features_categorical_label = ['MembershipLevel'] # Usaremos esto con precaución
# Creamos una copia para el pipeline
df_pipeline = df.copy()
# Primero, codificamos Label Encoding manualmente si es ordinal
membership_mapping_pipeline = {'Bronze': 1, 'Silver': 2, 'Gold': 3}
df_pipeline['MembershipLevel_Encoded'] = df_pipeline['MembershipLevel'].map(membership_mapping_pipeline)
features_numeric.append('MembershipLevel_Encoded') # Añadir al grupo numérico
# Preprocesamiento para características numéricas
numeric_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='median')), # Ya lo hicimos, pero es buena práctica
('scaler', StandardScaler()) # Escalado para normalizar las características
])
# Preprocesamiento para características categóricas (One-Hot Encoding)
categorical_transformer = Pipeline(steps=[
('onehot', OneHotEncoder(handle_unknown='ignore'))
])
# Crear un ColumnTransformer para aplicar diferentes transformaciones a diferentes columnas
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, features_numeric),
('cat', categorical_transformer, features_categorical_ohe)
],
remainder='passthrough' # Mantener otras columnas no transformadas
)
# El pipeline final podría incluir un modelo
# from sklearn.linear_model import LogisticRegression
# model_pipeline = Pipeline(steps=[
# ('preprocessor', preprocessor),
# ('classifier', LogisticRegression(solver='liblinear'))
# ])
# Aplicar solo el preprocesador para ver las características transformadas
X_transformed = preprocessor.fit_transform(df_pipeline)
print("Forma de los datos transformados:", X_transformed.shape)
print("Primeras 5 filas de los datos transformados (parcial):")
print(X_transformed[:5])
Explicación del `ColumnTransformer`
El `ColumnTransformer` es extremadamente útil porque permite aplicar diferentes transformaciones a diferentes columnas de tu DataFrame. Por ejemplo, las columnas numéricas pueden necesitar estandarización e imputación, mientras que las categóricas requieren One-Hot Encoding. `ColumnTransformer` orquesta esto perfectamente.Ejercicio Práctico: Mejorando un Modelo Sencillo 🎯
Usemos las características que hemos creado para entrenar un modelo sencillo y veamos cómo la ingeniería de características puede impactar el rendimiento.
Supongamos que queremos predecir IsPromotionClicked.
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, classification_report
# Definimos las características ingenierizadas que usaremos
# Excluyendo CustomerID, Gender (ya manejado por OHE en ProductCategory, no incluido directamente), MembershipLevel original
# y las columnas que queremos predecir o las transformadas para OHE
final_features = [
'Age', 'PurchaseAmount', 'NumPurchases', 'DaysSinceLastPurchase',
'AveragePurchaseValue', 'MembershipLevel_Encoded', 'ReviewScore',
'Purchase_x_Membership', 'ProductCategory_Frequency', 'PurchaseAmount_Log'
]
# Añadimos las columnas de One-Hot Encoding
ohe_cols = [col for col in df_encoded.columns if 'ProductCat_' in col]
final_features.extend(ohe_cols)
# Creamos una versión del DataFrame con todas las características relevantes ya preparadas
df_final = df_encoded.copy()
# Aseguramos que todas las columnas usadas en final_features existan en df_final
for col in ['AveragePurchaseValue', 'MembershipLevel_Encoded', 'Purchase_x_Membership', 'ProductCategory_Frequency', 'PurchaseAmount_Log']:
if col not in df_final.columns:
df_final[col] = df[col]
y = df_final['IsPromotionClicked']
X = df_final[final_features]
# Manejo de nulos final por si acaso (aunque ya se hizo)
X.fillna(X.median(), inplace=True)
# División de datos
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y)
# Escalado de características numéricas (importante para muchos modelos)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# Entrenamiento del modelo con características ingenierizadas
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train_scaled, y_train)
y_pred = model.predict(X_test_scaled)
print("\n--- Rendimiento del Modelo con Características Ingenierizadas ---")
print(f"Accuracy: {accuracy_score(y_test, y_pred):.4f}")
print("Classification Report:\n", classification_report(y_test, y_pred))
# Comparación: Modelo sin ingeniería de características (solo características originales relevantes)
original_features = ['Age', 'PurchaseAmount', 'NumPurchases', 'DaysSinceLastPurchase', 'ReviewScore']
# Añadimos OHE para 'ProductCategory' y Label Encoding para 'MembershipLevel' si fuera el caso, pero simplificamos
# para mostrar el impacto de la ingeniería manual
X_original = df_final[original_features]
X_original.fillna(X_original.median(), inplace=True)
X_train_orig, X_test_orig, y_train_orig, y_test_orig = train_test_split(X_original, y, test_size=0.3, random_state=42, stratify=y)
scaler_orig = StandardScaler()
X_train_orig_scaled = scaler_orig.fit_transform(X_train_orig)
X_test_orig_scaled = scaler_orig.transform(X_test_orig)
model_orig = RandomForestClassifier(n_estimators=100, random_state=42)
model_orig.fit(X_train_orig_scaled, y_train_orig)
y_pred_orig = model_orig.predict(X_test_orig_scaled)
print("\n--- Rendimiento del Modelo SIN Ingeniería de Características --- ")
print(f"Accuracy: {accuracy_score(y_test_orig, y_pred_orig):.4f}")
print("Classification Report:\n", classification_report(y_test_orig, y_pred_orig))
Consideraciones Adicionales y Mejores Prácticas ✅
- Conocimiento del Dominio: Siempre es tu mejor herramienta. Entender el negocio o el fenómeno detrás de los datos te ayudará a crear características más inteligentes.
- Exploración de Datos (EDA): Realiza un EDA exhaustivo antes y durante la ingeniería de características. Los gráficos de dispersión, histogramas y correlaciones pueden revelar relaciones ocultas.
- Selección de Características: No todas las características ingenierizadas serán útiles. Usa técnicas de selección de características (Filter, Wrapper, Embedded methods) para elegir las más relevantes y evitar la sobrecarga del modelo.
- Iteración: La ingeniería de características es un proceso iterativo. Crea, prueba, evalúa y refina.
- Evitar Data Leakage: Asegúrate de que las transformaciones (como la imputación de la media o el escalado) se calculen solo con los datos de entrenamiento y luego se apliquen a los datos de prueba y nuevos datos.
Resumen de Técnicas Vistas 📖
| Técnica | Descripción | Cuándo Usarla |
|---|---|---|
| Combinación Aritmética | Crear nuevas características combinando existentes (suma, resta, etc.) | Cuando hay una relación lógica entre características que se puede expresar aritméticamente. |
| Discretización (Binning) | Convertir numéricas continuas en categóricas por rangos | Para capturar no linealidades, reducir el impacto de outliers, o cuando un rango tiene más significado que un valor exacto. |
| One-Hot Encoding (OHE) | Convertir categóricas nominales en binarias | Para variables categóricas sin orden intrínseco. |
| Label Encoding | Convertir categóricas ordinales en enteros | Para variables categóricas con un orden claro (ej. niveles). |
| Interacciones de Caract. | Combinar dos características para capturar efectos sinérgicos | Cuando el efecto de una característica depende de otra. |
| Frecuencia/Conteo | Usar la frecuencia de aparición de un valor como característica | Para capturar la popularidad o rareza de una categoría. |
| Transformaciones Numéricas | Aplicar funciones (log, raíz) para cambiar la distribución de una variable | Cuando una variable está sesgada o tiene outliers para normalizar su distribución. |
Conclusión 🎉
La ingeniería de características es tanto un arte como una ciencia. Requiere creatividad, conocimiento del dominio y un buen dominio de herramientas como Pandas y NumPy. Al aplicar las técnicas que hemos explorado en este tutorial, estás un paso más cerca de desbloquear el verdadero potencial de tus datos y construir modelos de Machine Learning más robustos y precisos.
¡Sigue experimentando y mejorando tus habilidades en este fascinante campo!
Tutoriales relacionados
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!