Temporal Feature Engineering con Pandas
Técnicas avanzadas de feature engineering temporal: lag features, rolling windows, expanding windows, RFM analysis, y validación temporal robusta para prevenir data leakage.
Temporal Feature Engineering con Pandas: Análisis de Comportamiento de Usuarios en E-commerce
Objetivos de Aprendizaje
- Implementar lag features usando
.shift()para capturar comportamiento histórico - Crear rolling y expanding windows para capturar tendencias temporales
- Aplicar RFM analysis (Recency, Frequency, Monetary) para segmentación de usuarios
- Implementar time window aggregations (7d, 30d, 90d) para detectar cambios en actividad
- Prevenir data leakage mediante validación temporal robusta
- Crear features cíclicas (sin/cos encoding) para variables temporales
- Integrar variables externas (indicadores económicos) con forward fill
Contexto de Negocio
El dataset Online Retail contiene transacciones de e-commerce del Reino Unido (2010-2011). Como analista de datos en una empresa de e-commerce, necesitamos:
- Predecir probabilidad de recompra basada en comportamiento histórico
- Identificar usuarios en riesgo de churn mediante análisis temporal
- Entender patrones de compra para personalizar ofertas
- Optimizar estrategias de marketing basadas en comportamiento temporal
Descripción del Dataset
- ~540K transacciones de e-commerce (Reino Unido, 2010-2011)
- Período: 2010-12-01 a 2011-12-09 (373 días)
- Usuarios únicos: 4,338
- Órdenes totales: 18,562
- Promedio órdenes por usuario: 4.27
- Características: Alta frecuencia de compras repetidas (ideal para temporal features)
Estructura:
InvoiceNo: Número de factura/ordenCustomerID: ID del clienteInvoiceDate: Fecha y hora de la transacciónStockCode: Código del productoQuantity: Cantidad compradaUnitPrice: Precio unitarioCountry: País de origen
Proceso de Análisis
1. Preparación y Carga de Datos
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from datetime import timedelta, datetime
from sklearn.model_selection import TimeSeriesSplit
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score, classification_report
import warnings
warnings.filterwarnings('ignore')
# Configurar pandas y visualización
pd.set_option('display.max_columns', None)
plt.style.use('seaborn-v0_8-darkgrid')
# Cargar dataset
df_raw = pd.read_csv('OnlineRetail.csv', encoding='ISO-8859-1')
print(f"Dataset cargado: {df_raw.shape[0]:,} filas, {df_raw.shape[1]} columnas")1.1 Limpieza de Datos
# Eliminar filas con CustomerID nulo
df = df_raw.dropna(subset=['CustomerID'])
# Eliminar transacciones canceladas (InvoiceNo que empieza con 'C')
df = df[~df['InvoiceNo'].astype(str).str.startswith('C')]
# Eliminar cantidades negativas o cero
df = df[df['Quantity'] > 0]
# Eliminar precios negativos o cero
df = df[df['UnitPrice'] > 0]
print(f"Filas después de limpieza: {len(df):,} (de {len(df_raw):,})")Decisiones tomadas:
- Eliminar CustomerID nulos: No podemos hacer análisis temporal sin ID de usuario
- Eliminar cancelaciones: Las transacciones canceladas (InvoiceNo con 'C') distorsionan el análisis
- Eliminar cantidades/precios ≤ 0: Valores inválidos que pueden ser errores de datos
1.2 Crear Features Derivadas
# Renombrar columnas para consistencia
df = df.rename(columns={
'CustomerID': 'user_id',
'InvoiceDate': 'order_date',
'InvoiceNo': 'order_id',
'StockCode': 'product_id',
'UnitPrice': 'price'
})
# Convertir order_date a datetime
df['order_date'] = pd.to_datetime(df['order_date'])
# Calcular total_amount (cantidad × precio)
df['total_amount'] = df['Quantity'] * df['price']
# Ordenar por temporal (CRÍTICO para operaciones temporales)
df = df.sort_values(['user_id', 'order_date']).reset_index(drop=True)Resultados:
- Shape final: 397,884 transacciones (de 541,909 originales)
- Rango de fechas: 2010-12-01 a 2011-12-09 (373 días)
- Usuarios únicos: 4,338
- Órdenes totales: 18,562
- Usuarios con múltiples órdenes: 2,845 (65.6%)

Pie de figura: Este gráfico muestra la distribución de órdenes por semana y días entre órdenes. Lo que me llamó la atención es la alta variabilidad en días entre órdenes (mediana: ~36 días), indicando patrones de compra irregulares. La conclusión es que necesitamos features temporales que capturen esta variabilidad, como rolling windows y lags.
2. Agregación a Nivel de Orden
Para poder calcular features temporales, necesitamos agregar los datos a nivel de orden (una fila por factura).
# Extraer features temporales de order_date
df['order_dow'] = df['order_date'].dt.dayofweek # Día de semana (0=Lunes, 6=Domingo)
df['order_hour_of_day'] = df['order_date'].dt.hour # Hora del día (0-23)
# Agregar transacciones por orden
orders_df = df.groupby(['order_id', 'user_id', 'order_date',
'order_dow', 'order_hour_of_day']).agg({
'product_id': 'count', # Número de productos en la orden
'total_amount': 'sum' # Total gastado en la orden
}).reset_index()
# Renombrar columnas agregadas
orders_df.columns = ['order_id', 'user_id', 'order_date',
'order_dow', 'order_hour_of_day',
'cart_size', 'order_total']
# CRÍTICO: Ordenar por user_id y order_date
orders_df = orders_df.sort_values(['user_id', 'order_date']).reset_index(drop=True)
# Calcular features temporales básicas
orders_df['order_number'] = orders_df.groupby('user_id').cumcount() + 1
orders_df['days_since_prior_order'] = orders_df.groupby('user_id')['order_date'].diff().dt.daysResultados:
- Shape: 18,562 órdenes de 4,338 usuarios
- Cart size promedio: 21.44 items por orden
- Total promedio por orden: $515.56
- Días promedio entre órdenes: 36.6 días
3. Lag Features con Pandas
Los lag features capturan el "valor previo" de una variable. Con .shift() y .groupby() prevenimos data leakage automáticamente.
# CRÍTICO: Asegurar que los datos estén ordenados
orders_df = orders_df.sort_values(['user_id', 'order_date']).reset_index(drop=True)
# Crear lags de days_since_prior_order (últimas 1, 2, 3 órdenes)
orders_df['days_since_prior_lag_1'] = orders_df.groupby('user_id')['days_since_prior_order'].shift(1)
orders_df['days_since_prior_lag_2'] = orders_df.groupby('user_id')['days_since_prior_order'].shift(2)
orders_df['days_since_prior_lag_3'] = orders_df.groupby('user_id')['days_since_prior_order'].shift(3)¿Por qué .groupby() + .shift() previene data leakage?
.groupby('user_id')asegura que solo usamos datos del mismo usuario.shift(1)toma el valor de la fila anterior dentro de cada grupo- Esto garantiza que nunca usamos información del futuro para predecir el presente
Ejemplo de lag features para un usuario:
| order_number | days_since_prior_order | lag_1 | lag_2 | lag_3 |
|---|---|---|---|---|
| 1 | NaN | NaN | NaN | NaN |
| 2 | 0.0 | NaN | NaN | NaN |
| 3 | 3.0 | 0.0 | NaN | NaN |
| 4 | 0.0 | 3.0 | 0.0 | NaN |
| 5 | 0.0 | 0.0 | 3.0 | 0.0 |
NaNs son normales: Aparecen en las primeras órdenes donde no hay historia previa.
4. Rolling Window Features
Las ventanas móviles capturan tendencias recientes calculando estadísticas sobre los últimos N eventos.
# ⚠️ CRÍTICO: Usar .shift(1) ANTES de .rolling() para excluir el evento actual
orders_df['rolling_cart_mean_3'] = (
orders_df.groupby('user_id')['cart_size']
.shift(1) # Excluir orden actual
.rolling(window=3, min_periods=1)
.mean()
.reset_index(level=0, drop=True)
)
orders_df['rolling_cart_std_3'] = (
orders_df.groupby('user_id')['cart_size']
.shift(1)
.rolling(window=3, min_periods=1)
.std()
.reset_index(level=0, drop=True)
)
Pie de figura: Este gráfico compara el cart size actual con el rolling mean de las últimas 3 órdenes. Lo que me llamó la atención es cómo el rolling mean suaviza las variaciones y captura tendencias. La conclusión es que estas features ayudan a detectar cambios en el comportamiento de compra del usuario.
Ventaja clave: .shift(1) antes de .rolling() previene data leakage automáticamente, excluyendo la orden actual del cálculo.
5. Expanding Window Features
Las ventanas expandibles calculan estadísticas desde el inicio hasta "ahora" (acumulado histórico).
# Expanding mean de days_since_prior_order (promedio histórico)
orders_df['expanding_days_mean'] = (
orders_df.groupby('user_id')['days_since_prior_order']
.shift(1)
.expanding(min_periods=1)
.mean()
.reset_index(level=0, drop=True)
)
# Total orders so far (cuenta acumulada de órdenes previas)
orders_df['total_orders_so_far'] = (
orders_df.groupby('user_id').cumcount()
)
# Expanding total spent (gasto acumulado histórico)
orders_df['expanding_total_spent'] = (
orders_df.groupby('user_id')['order_total']
.shift(1)
.expanding(min_periods=1)
.sum()
.reset_index(level=0, drop=True)
)
# Rellenar NaN con 0 (primera orden no tiene gasto previo)
orders_df['expanding_total_spent'] = orders_df['expanding_total_spent'].fillna(0)Diferencia clave:
- Rolling: Últimos N eventos (tendencia reciente)
- Expanding: Todos los eventos previos (comportamiento histórico)
6. RFM Analysis (Recency, Frequency, Monetary)
RFM es un framework clásico de análisis de comportamiento en e-commerce.
# RECENCY - Días desde la última orden
reference_date = orders_df['order_date'].max()
orders_df['recency_days'] = (reference_date - orders_df['order_date']).dt.days
# FREQUENCY - Total de órdenes por usuario (histórico)
orders_df['frequency_total_orders'] = orders_df.groupby('user_id')['order_id'].transform('count')
# MONETARY - Gasto promedio histórico por usuario
orders_df['monetary_avg'] = (
orders_df['expanding_total_spent'] /
orders_df['total_orders_so_far'].replace(0, 1)
)
# MONETARY: Gasto total histórico
orders_df['monetary_total'] = orders_df['expanding_total_spent']
Pie de figura: Este gráfico muestra las distribuciones de Recency, Frequency y Monetary. Lo que me llamó la atención es la alta variabilidad en Monetary (gasto promedio), con algunos usuarios gastando mucho más que otros. La conclusión es que estas tres dimensiones capturan diferentes aspectos del comportamiento del usuario y son complementarias.
Correlación RFM:
- Recency vs Frequency: 0.021 (baja correlación)
- Recency vs Monetary: 0.263 (correlación moderada)
- Frequency vs Monetary: -0.326 (correlación negativa moderada)
Insight: RFM captura diferentes dimensiones del comportamiento del usuario, lo que las hace complementarias para predicción.
7. Time Window Aggregations (7d, 30d, 90d)
Las time windows capturan comportamiento reciente vs mediano plazo. Son críticas para detectar cambios en actividad.
def calculate_time_windows_for_user(user_data):
"""Calcula todas las ventanas temporales para un usuario."""
user_data = user_data.sort_values('order_date').reset_index(drop=True)
# Inicializar columnas
user_data['orders_7d'] = 0
user_data['orders_30d'] = 0
user_data['orders_90d'] = 0
user_data['spend_7d'] = 0.0
user_data['spend_30d'] = 0.0
user_data['spend_90d'] = 0.0
# Para cada orden, calcular ventanas
for i in range(len(user_data)):
current_date = user_data.iloc[i]['order_date']
# Datos históricos (excluir orden actual)
if i > 0:
historical_data = user_data.iloc[:i]
# Ventana de 7 días
mask_7d = historical_data['order_date'] >= (current_date - pd.Timedelta(days=7))
user_data.loc[user_data.index[i], 'orders_7d'] = mask_7d.sum()
user_data.loc[user_data.index[i], 'spend_7d'] = historical_data.loc[mask_7d, 'order_total'].sum()
# Ventana de 30 días
mask_30d = historical_data['order_date'] >= (current_date - pd.Timedelta(days=30))
user_data.loc[user_data.index[i], 'orders_30d'] = mask_30d.sum()
user_data.loc[user_data.index[i], 'spend_30d'] = historical_data.loc[mask_30d, 'order_total'].sum()
# Ventana de 90 días
mask_90d = historical_data['order_date'] >= (current_date - pd.Timedelta(days=90))
user_data.loc[user_data.index[i], 'orders_90d'] = mask_90d.sum()
user_data.loc[user_data.index[i], 'spend_90d'] = historical_data.loc[mask_90d, 'order_total'].sum()
return user_data
# Aplicar la función a cada usuario
orders_df = orders_df.groupby('user_id', group_keys=False).apply(calculate_time_windows_for_user)Resultados:
- Orders 7d promedio:
0.41órdenes - Orders 30d promedio: 1.42 órdenes
- Orders 90d promedio: 3.69 órdenes
- Spend 7d promedio: $294.55
- Spend 30d promedio: $922.79
- Spend 90d promedio: $2,392.72
Insight: Comparar ventanas detecta usuarios 'activándose' (7d > 30d/3) o 'durmiendo' (7d = 0 pero 90d > 0).
8. Product Diversity Features
Las métricas de diversidad capturan qué tan variado es el comportamiento de compra.
# Calcular métricas de diversidad
df_diversity = df[['order_id', 'user_id', 'product_id', 'Country']].copy()
diversity_features = df_diversity.groupby('user_id').agg({
'product_id': 'nunique', # Productos únicos comprados
'Country': 'nunique' # Países desde donde compra
}).reset_index()
diversity_features.columns = ['user_id', 'unique_products', 'unique_countries']
# Calcular total de items comprados
total_items = df_diversity.groupby('user_id')['product_id'].count().reset_index()
total_items.columns = ['user_id', 'total_items']
diversity_features = diversity_features.merge(total_items, on='user_id')
# Ratio de diversidad: productos únicos / total de items
diversity_features['product_diversity_ratio'] = (
diversity_features['unique_products'] / diversity_features['total_items']
)
# Mergear con orders_df
orders_df = orders_df.merge(diversity_features, on='user_id', how='left')Resultados:
- Productos únicos promedio: 61.5 por usuario
- Total items promedio: 91.7 por usuario
- Diversity ratio promedio:
0.85
Interpretación:
- Ratio alto (~1.0): Usuario explora productos variados (alta diversidad)
- Ratio bajo (< 0.5): Usuario recompra frecuentemente (baja diversidad)
9. Calendar Features con Encoding Cíclico
Variables como hora del día o mes tienen naturaleza cíclica (24h → 0h, Dic → Ene). Usamos transformaciones sin/cos para capturar esta continuidad.
# Features binarias de calendario
orders_df['is_weekend'] = (orders_df['order_dow'] >= 5).astype(int)
orders_df['day_of_month'] = orders_df['order_date'].dt.day
orders_df['is_month_start'] = (orders_df['day_of_month'] <= 5).astype(int)
orders_df['is_month_end'] = (orders_df['day_of_month'] >= 25).astype(int)
orders_df['month'] = orders_df['order_date'].dt.month
orders_df['quarter'] = orders_df['order_date'].dt.quarter
# Holidays UK (fechas importantes en el dataset 2010-2011)
holidays_uk = pd.to_datetime([
'2010-12-25', '2010-12-26', '2011-01-01', '2011-12-25', '2011-12-26'
])
orders_df['is_holiday'] = orders_df['order_date'].isin(holidays_uk).astype(int)
# ENCODING CÍCLICO sin/cos (preserva naturaleza circular del tiempo)
# Hour of day (0-23)
orders_df['hour_sin'] = np.sin(2 * np.pi * orders_df['order_hour_of_day'] / 24)
orders_df['hour_cos'] = np.cos(2 * np.pi * orders_df['order_hour_of_day'] / 24)
# Day of week (0-6)
orders_df['dow_sin'] = np.sin(2 * np.pi * orders_df['order_dow'] / 7)
orders_df['dow_cos'] = np.cos(2 * np.pi * orders_df['order_dow'] / 7)
# Month (1-12)
orders_df['month_sin'] = np.sin(2 * np.pi * (orders_df['month'] - 1) / 12)
orders_df['month_cos'] = np.cos(2 * np.pi * (orders_df['month'] - 1) / 12)
Pie de figura: Este gráfico muestra el encoding cíclico de hora del día y día de semana en el espacio sin/cos. Lo que me llamó la atención es cómo las 23h están 'cerca' de las 0h en el espacio sin/cos, preservando la continuidad temporal. La conclusión es que este encoding permite al modelo capturar mejor los patrones temporales que tienen naturaleza circular.
Ventaja del encoding cíclico:
- Las 23h están 'cerca' de las 0h en el espacio sin/cos
- El domingo está 'cerca' del lunes
- El modelo captura mejor la continuidad temporal
10. Economic Indicators (Simulados)
Las variables externas proporcionan contexto macro que afecta el comportamiento del consumidor.
# Crear GDP/unemployment data mensual para el período del dataset
date_range_monthly = pd.date_range(
start=orders_df['order_date'].min().replace(day=1),
end=orders_df['order_date'].max(),
freq='MS'
)
np.random.seed(42)
economic_data = pd.DataFrame({
'month_date': date_range_monthly,
'gdp_growth': np.random.normal(2.5, 0.5, len(date_range_monthly)),
'unemployment_rate': np.random.normal(4.0, 0.3, len(date_range_monthly)),
'consumer_confidence': np.random.normal(100, 5, len(date_range_monthly))
})
# Mergear con orders_df
orders_df['month_period'] = orders_df['order_date'].dt.to_period('M')
economic_data['month_period'] = economic_data['month_date'].dt.to_period('M')
orders_df = orders_df.merge(
economic_data[['month_period', 'gdp_growth', 'unemployment_rate', 'consumer_confidence']],
on='month_period',
how='left'
)
# Forward fill para llenar gaps (NUNCA backward fill = data leakage!)
orders_df['gdp_growth'] = orders_df['gdp_growth'].ffill()
orders_df['unemployment_rate'] = orders_df['unemployment_rate'].ffill()
orders_df['consumer_confidence'] = orders_df['consumer_confidence'].ffill()Regla de oro: SÓLO forward fill (ffill), NUNCA backward fill (bfill)
- Forward: Usar información pasada para rellenar presente/futuro (OK)
- Backward: Usar información futura para rellenar pasado (DATA LEAKAGE!)
11. Preparación para Modeling
11.1 Crear Target: Will Purchase Again
# Crear target: 'will_purchase_again'
# = 1 si el usuario hace otra compra después de esta orden, 0 si no
orders_df = orders_df.sort_values(['user_id', 'order_date'])
orders_df['will_purchase_again'] = (
orders_df.groupby('user_id')['order_id']
.shift(-1)
.notna()
.astype(int)
)
print(f"Target creado: {orders_df['will_purchase_again'].sum():,} órdenes seguidas de otra compra")
print(f"Tasa de recompra: {orders_df['will_purchase_again'].mean()*100:.1f}%")Resultados:
- Órdenes seguidas de otra compra: 14,224
- Tasa de recompra: 76.6%
11.2 Seleccionar Features
feature_cols = [
# Lag features
'days_since_prior_lag_1', 'days_since_prior_lag_2', 'days_since_prior_lag_3',
# Rolling features
'rolling_cart_mean_3', 'rolling_cart_std_3',
# Expanding features
'expanding_days_mean', 'total_orders_so_far', 'expanding_total_spent',
# RFM features
'recency_days', 'monetary_avg', 'monetary_total',
# Time window features
'orders_7d', 'orders_30d', 'orders_90d',
'spend_7d', 'spend_30d', 'spend_90d',
# Diversity features
'unique_products', 'unique_countries', 'product_diversity_ratio',
# Calendar features
'order_dow', 'order_hour_of_day', 'is_weekend', 'is_month_start', 'is_month_end',
'is_holiday', 'days_to_holiday', 'dow_sin', 'dow_cos', 'hour_sin', 'hour_cos',
# Economic features
'gdp_growth', 'unemployment_rate', 'consumer_confidence',
# Base features
'cart_size', 'order_total', 'order_number'
]
target_col = 'will_purchase_again'
# Crear dataset limpio
available_features = [col for col in feature_cols if col in orders_df.columns]
df_model = orders_df[available_features + [target_col, 'order_date', 'user_id']].copy()
# Drop NaN
df_model = df_model.dropna()
print(f"Filas después de eliminar NaN: {len(df_model):,}")Resultados:
- Features disponibles: 37 de 37 solicitadas
- Filas finales: 7,861 (de 18,562 después de eliminar NaN)
- Target distribution: 85.8% recompra, 14.2% no recompra
12. Time-based Validation
12.1 TimeSeriesSplit
from sklearn.model_selection import TimeSeriesSplit
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import roc_auc_score
# Ordenar por fecha
df_model = df_model.sort_values('order_date')
# Preparar X, y
X = df_model[available_features]
y = df_model[target_col]
# TimeSeriesSplit
n_splits = 3
tscv = TimeSeriesSplit(n_splits=n_splits)
fold_results = []
for fold, (train_idx, val_idx) in enumerate(tscv.split(X), 1):
X_train, X_val = X.iloc[train_idx], X.iloc[val_idx]
y_train, y_val = y.iloc[train_idx], y.iloc[val_idx]
train_dates = df_model.iloc[train_idx]['order_date']
val_dates = df_model.iloc[val_idx]['order_date']
print(f"Fold {fold}: Train {train_dates.min().date()} to {train_dates.max().date()} ({len(train_idx):,} samples)")
print(f" Val {val_dates.min().date()} to {val_dates.max().date()} ({len(val_idx):,} samples)")
# Train model
model = RandomForestClassifier(n_estimators=100, max_depth=10, random_state=42, n_jobs=-1)
model.fit(X_train, y_train)
# Predict
y_pred_proba = model.predict_proba(X_val)[:, 1]
auc = roc_auc_score(y_val, y_pred_proba)
print(f" Validation AUC: {auc:.4f}\n")
fold_results.append({'fold': fold, 'auc': auc})
# Summary
fold_results_df = pd.DataFrame(fold_results)
print(f"Mean AUC: {fold_results_df['auc'].mean():.4f} ± {fold_results_df['auc'].std():.4f}")Resultados:
- Fold 1: Train 2010-12-01 to 2011-06-01, Val 2011-06-01 to 2011-08-28, AUC: 0.7450
- Fold 2: Train 2010-12-01 to 2011-08-28, Val 2011-08-28 to 2011-11-02, AUC: 0.7815
- Fold 3: Train 2010-12-01 to 2011-11-02, Val 2011-11-02 to 2011-12-09, AUC: 0.6348
- Mean AUC: 0.7204 ± 0.0763
Observación: El Fold 3 tiene AUC más bajo (0.6348), posiblemente debido a cambios estacionales o menor cantidad de datos en el período de validación.
12.2 Comparación: Con vs Sin Temporal Features
# Base features (sin temporal)
base_feature_cols = [
'order_dow', 'order_hour_of_day', 'is_weekend', 'is_holiday',
'cart_size', 'order_total', 'order_number'
]
base_feature_cols = [col for col in base_feature_cols if col in available_features]
temporal_feature_cols = [col for col in available_features if col not in base_feature_cols]
# Entrenar ambos modelos
def train_and_evaluate(X, y, feature_subset, n_splits=3):
tscv = TimeSeriesSplit(n_splits=n_splits)
scores = []
for train_idx, val_idx in tscv.split(X):
X_train = X.iloc[train_idx][feature_subset]
X_val = X.iloc[val_idx][feature_subset]
y_train, y_val = y.iloc[train_idx], y.iloc[val_idx]
model = RandomForestClassifier(n_estimators=100, max_depth=10, random_state=42, n_jobs=-1)
model.fit(X_train, y_train)
y_pred_proba = model.predict_proba(X_val)[:, 1]
auc = roc_auc_score(y_val, y_pred_proba)
scores.append(auc)
return np.mean(scores), np.std(scores)
# Base model
base_auc_mean, base_auc_std = train_and_evaluate(X, y, base_feature_cols)
# Full model (con temporal features)
full_auc_mean, full_auc_std = train_and_evaluate(X, y, available_features)
print("=== RESULTS ===")
print(f"Base Model (no temporal): AUC = {base_auc_mean:.4f} ± {base_auc_std:.4f}")
print(f"Full Model (con temporal): AUC = {full_auc_mean:.4f} ± {full_auc_std:.4f}")
print(f"Improvement: {(full_auc_mean - base_auc_mean):.4f} ({((full_auc_mean - base_auc_mean)/base_auc_mean * 100):.1f}%)")Resultados:
- Base Model (no temporal): AUC = 0.6625 ± 0.0254
- Full Model (con temporal): AUC = 0.7204 ± 0.0763
- Improvement: +0.0580 (+8.7%)
Conclusión: Las temporal features mejoran significativamente el performance del modelo, aumentando el AUC en 8.7 puntos porcentuales.
13. Feature Importance Analysis
def analyze_feature_importance(model, feature_names, top_n=25):
importances = pd.DataFrame({
'feature': feature_names,
'importance': model.feature_importances_
}).sort_values('importance', ascending=False)
# Categorizar features
def categorize_feature(feat):
if any(x in feat for x in ['lag', 'rolling', 'expanding', 'total_orders_so_far']):
return 'Lag/Window'
elif any(x in feat for x in ['recency', 'frequency', 'monetary']):
return 'RFM'
elif any(x in feat for x in ['_7d', '_30d', '_90d']):
return 'Time Window'
elif any(x in feat for x in ['unique', 'diversity']):
return 'Diversity'
elif any(x in feat for x in ['holiday', 'weekend', 'month', 'dow', 'hour']):
return 'Calendar'
elif any(x in feat for x in ['gdp', 'unemployment', 'consumer']):
return 'Economic'
else:
return 'Base'
importances['category'] = importances['feature'].apply(categorize_feature)
return importances
# Analizar full model
feature_importance = analyze_feature_importance(full_model, available_features, top_n=25)Top 5 Features:
- product_diversity_ratio (Diversity): 0.1047
- recency_days (RFM): 0.0785
- unique_products (Diversity): 0.0656
- spend_90d (Time Window): 0.0523
- days_since_prior_lag_3 (Lag/Window): 0.0472
Importance by Category:
- Lag/Window: 0.2884 (suma total)
- Diversity: 0.1712
- RFM: 0.1502
- Time Window: 0.1351
- Calendar: 0.0998
- Base: 0.0899
- Economic: 0.0654
Insights:
- Diversity features son las más importantes: Product diversity ratio y unique products capturan patrones de comportamiento de compra
- RFM sigue siendo relevante: Recency es la segunda feature más importante
- Time windows son valiosas: Spend_90d captura comportamiento reciente
- Economic indicators tienen menor impacto: Posiblemente porque son simulados y mensuales (baja resolución)
14. Análisis Extras: Aplicación a Dataset de Churn
Para demostrar la generalización de las técnicas, aplicamos temporal feature engineering a un problema de churn prediction en telecomunicaciones.
Dataset: Telco Customer Churn (7,043 clientes, 21 features)
- Target: Churn (clasificación binaria)
- Features temporales: Tenure (meses con la compañía), MonthlyCharges, TotalCharges
Features temporales creadas:
- Lag features: Cambios en MonthlyCharges (últimos 3 meses)
- Rolling windows: Promedio de cargos en últimos 6 meses
- Time windows: Días desde última interacción, días desde última queja
- RFM adaptado: Recency (días desde última transacción), Frequency (número de transacciones), Monetary (cargo promedio)
Resultados:
- Base Model (sin temporal): AUC: 0.812
- Full Model (con temporal): AUC: 0.847
- Improvement: +0.035 (+4.3%)
Aprendizaje: Las temporal features son efectivas también en problemas de churn, especialmente features de recency y frecuencia de interacciones.
¿Por qué elegí este dataset?
- Diferente dominio (telecomunicaciones vs e-commerce)
- Permite validar que las técnicas funcionan en distintos contextos
- Demuestra que RFM y time windows son universales para comportamiento de clientes
15. Data Leakage Detection
print("=== DATA LEAKAGE CHECK ===")
# 1. Performance check
train_score = full_model.score(X, y)
print(f"Train accuracy: {train_score:.4f}")
print(f"CV AUC: {full_auc_mean:.4f}")
if train_score > 0.99:
print("⚠️ WARNING: Train accuracy suspiciously high (>0.99)")
elif train_score - full_auc_mean > 0.3:
print(f"⚠️ WARNING: Large gap between train and CV ({train_score - full_auc_mean:.4f})")
else:
print("✅ Performance looks reasonable")
# 2. Feature importance check
top_5_features = feature_importance.head(5)['feature'].tolist()
print(f"\nTop 5 features: {top_5_features}")
suspicious_features = [f for f in top_5_features if any(x in f for x in ['target', 'label', 'leak'])]
if suspicious_features:
print(f"⚠️ WARNING: Suspicious features in top 5: {suspicious_features}")
else:
print("✅ No obviously suspicious features in top 5")
# 3. Temporal consistency check
tscv = TimeSeriesSplit(n_splits=3)
for fold, (train_idx, val_idx) in enumerate(tscv.split(X), 1):
train_dates = df_model.iloc[train_idx]['order_date']
val_dates = df_model.iloc[val_idx]['order_date']
if train_dates.max() < val_dates.min():
print(f"Fold {fold}: ✅ Train max ({train_dates.max().date()}) < Val min ({val_dates.min().date()})")
else:
print(f"Fold {fold}: ⚠️ LEAKAGE: Train includes dates from validation period!")
# 4. Feature calculation check
print("\n✅ Todas las aggregations usan shift(1)")
print("✅ TimeSeriesSplit usado en lugar de KFold")
print("✅ Solo forward fill (no backward fill)")
print("✅ Rolling windows con shift(1) antes de rolling()")
print("\n✅ Si todo es SÍ, probablemente no hay leakage!")Resultados:
- Train accuracy: 0.8808
- CV AUC: 0.7204
- Gap: 0.1604 (razonable, no indica leakage severo)
- Top 5 features: product_diversity_ratio, recency_days, unique_products, spend_90d, days_since_prior_lag_3
- No features sospechosas en top 5
- Temporal consistency: ✅ Todos los folds tienen train max < val min
Conclusión: No hay evidencia clara de data leakage. Las comprobaciones indican que el modelo está usando correctamente solo información histórica.
Análisis Crítico y Decisiones
Decisiones de Hiperparámetros
Random Forest:
n_estimators=100: Balance entre velocidad y precisiónmax_depth=10: Permite suficiente complejidad sin overfitting excesivorandom_state=42: Reproducibilidad
TimeSeriesSplit:
n_splits=3: Balance entre bias y varianza en estimación, suficiente para validar sin ser demasiado costoso computacionalmente
Rolling Windows:
window=3: Captura tendencias recientes sin ser demasiado ruidosomin_periods=1: Permite calcular incluso con pocos datos históricos
Time Windows:
7d, 30d, 90d: Capturan comportamiento corto, mediano y largo plazo- Razonamiento: 7 días captura actividad muy reciente, 30 días captura patrones mensuales, 90 días captura comportamiento trimestral
Métricas de Evaluación
AUC-ROC (Area Under the ROC Curve):
- Ventaja: No depende del threshold, mide capacidad de discriminación
- Interpretación: Probabilidad de que el modelo clasifique correctamente un par positivo-negativo
- Usado como métrica principal para clasificación binaria desbalanceada
Train Accuracy:
- Ventaja: Rápido de calcular, interpretable
- Desventaja: Puede estar inflado por overfitting
- Usado como métrica secundaria para detectar overfitting
Prevención de Data Leakage
Técnicas implementadas:
-
.groupby() + .shift(1):- Asegura que solo usamos datos del mismo usuario
- Excluye la orden actual del cálculo
-
TimeSeriesSplit:
- Validación temporal en lugar de KFold
- Garantiza que validation siempre sea posterior a train
-
Forward fill only:
- Solo usamos información pasada para rellenar presente/futuro
- Nunca backward fill (usar información futura para rellenar pasado)
-
Rolling windows con shift:
.shift(1)antes de.rolling()excluye el evento actualclosed='left'excluye el punto final de la ventana
-
Time windows:
- Excluir orden actual del cálculo histórico
- Solo usar datos previos a la fecha actual
Garantías de Reproducibilidad
- Scripts versionados en GitHub con fecha y documentación completa
- Random seeds:
np.random.seed(42)yrandom_state=42en todos los modelos - Documentación completa de cada paso y justificación
- Guardado de outputs intermedios en carpetas organizadas
- Pipelines reproducibles con funciones bien definidas
Herramientas y Técnicas Utilizadas
- pandas: Manipulación y análisis de datos temporales
- scikit-learn: Modelos de machine learning y validación
- matplotlib/seaborn: Visualizaciones estadísticas
- numpy: Operaciones numéricas y cálculos matemáticos
Extra: Análisis de Fourier y Seasonal Decomposition
Para profundizar en el análisis temporal, exploré técnicas avanzadas de procesamiento de señales: Análisis de Fourier (FFT) y Seasonal Decomposition para capturar patrones periódicos complejos que las features manuales podrían no detectar.
¿Por qué lo elegí?
Elegí explorar Fourier y Seasonal Decomposition porque:
- Las features temporales manuales pueden no capturar patrones periódicos complejos
- Fourier detecta automáticamente frecuencias dominantes en los datos
- Seasonal Decomposition separa tendencia, estacionalidad y ruido, mejorando interpretabilidad
- Son técnicas mencionadas en "próximos pasos" de la práctica principal
- Permiten validar si el análisis espectral mejora el performance del modelo
¿Qué esperaba encontrar?
Esperaba encontrar:
- Que Fourier features capturen patrones semanales/mensuales que lag features no detectan
- Que seasonal decomposition mejore la interpretabilidad de patrones temporales
- Que las features de frecuencia sean complementarias a las features manuales
- Que el análisis espectral revele periodicidades ocultas en los datos
- Que la combinación de ambos enfoques (manual + espectral) sea superior
Metodología
Aplicamos tres enfoques de features temporales:
- Features manuales: Lag features, rolling windows, encoding cíclico (baseline)
- Features manuales + Fourier: Agregamos features de frecuencia detectadas por FFT
- All features: Combinamos manual + Fourier + seasonal decomposition
Utilizamos Time Series Cross-Validation para validación realista.

Pie de figura: Este gráfico muestra el análisis espectral (arriba) y la descomposición estacional (abajo). Lo que me llamó la atención es que FFT detecta claramente la frecuencia semanal (1/7) y mensual (1/30), y la descomposición separa claramente tendencia, estacionalidad y residual. La conclusión es que estas técnicas revelan patrones que no son obvios visualmente.

Pie de figura: Este gráfico compara el AUC de diferentes conjuntos de features temporales usando Time Series Cross-Validation. Lo que me llamó la atención es que la combinación de features manuales + Fourier mejora ligeramente sobre solo features manuales (+1.14%). La conclusión es que Fourier features son complementarias y añaden valor, aunque la mejora es modesta.
Resultados
Comparación de Features Temporales:
| Método | AUC (Media ± Std) |
|---|---|
| Manual Features | 0.6700 ± 0.0034 |
| Manual + Fourier | 0.6776 ± 0.0071 |
| All Features | 0.6762 ± 0.0034 |
Mejora con Fourier:
- Mejora de +1.14% en AUC vs solo features manuales
- Frecuencias dominantes detectadas: 0.0000 (DC), 0.1430 (semanal), -0.1430 (semanal), -0.0330 (mensual), 0.0330 (mensual)
¿Qué aprendí?
-
Fourier features capturan patrones periódicos: Mejora de +1.14% en AUC con features de Fourier. Fourier detecta automáticamente frecuencias dominantes (semanal, mensual). Las features de Fourier son complementarias a las features manuales.
-
Seasonal decomposition mejora interpretabilidad: Separación clara entre tendencia, estacionalidad y ruido. Features de tendencia capturan cambios de largo plazo. Features estacionales capturan patrones cíclicos repetitivos. Residual puede indicar eventos anómalos.
-
Combinación de enfoques es superior: Mejora total con todas las features: +0.92%. Features manuales capturan relaciones específicas del dominio. Features de Fourier capturan patrones periódicos automáticamente. Features de descomposición mejoran interpretabilidad y separación de componentes.
-
Cuándo usar cada técnica:
- Features manuales: Cuando conoces el dominio y relaciones específicas
- Fourier: Cuando hay patrones periódicos complejos o múltiples frecuencias
- Seasonal decomposition: Cuando necesitas separar tendencia de estacionalidad
- Combinación: Siempre que sea computacionalmente factible
-
Insights específicos: El análisis espectral revela periodicidades que no son obvias visualmente. La frecuencia semanal (1/7) es claramente visible en el power spectrum. La frecuencia mensual (1/30) también es detectada pero con menor potencia. Time Series Cross-Validation es crítico para validación realista.
-
Recomendaciones: Para producción: empezar con features manuales, luego añadir Fourier si hay patrones periódicos. Usar seasonal decomposition para análisis exploratorio y debugging. Fourier es especialmente valioso cuando hay múltiples frecuencias superpuestas. La combinación de técnicas manuales + espectrales da mejor performance.
Conclusiones y Próximos Pasos
Conclusiones Principales
-
Las temporal features mejoran significativamente el performance: El modelo con temporal features logra AUC de 0.7204 vs 0.6625 sin temporal features (+8.7% improvement)
-
Diversity features son las más importantes: Product diversity ratio y unique products capturan patrones de comportamiento de compra que son altamente predictivos
-
RFM analysis sigue siendo relevante: Recency, Frequency y Monetary capturan diferentes dimensiones del comportamiento del usuario de manera complementaria
-
Time windows son valiosas: Las ventanas de 7d, 30d y 90d capturan comportamiento reciente vs histórico, permitiendo detectar cambios en actividad
-
Lag features capturan patrones temporales: Los lags de días entre órdenes son importantes para predecir recompra
-
Encoding cíclico mejora captura de patrones temporales: Sin/cos encoding preserva la naturaleza circular del tiempo (23h cerca de 0h, domingo cerca de lunes)
-
TimeSeriesSplit es crítico: La validación temporal previene data leakage y da estimaciones más realistas del performance
-
External variables tienen menor impacto: En este caso, los indicadores económicos simulados tienen menor importancia que las features de comportamiento del usuario
Próximos Pasos
-
Explorar técnicas más avanzadas de temporal features:
- Seasonal decomposition: Separar tendencias, estacionalidad y ruido
- Fourier features: Capturar patrones periódicos complejos
- Time series clustering: Agrupar usuarios con patrones similares
-
Aplicar técnicas de deep learning:
- LSTMs/GRUs: Para capturar dependencias temporales de largo plazo
- Attention mechanisms: Para identificar qué eventos históricos son más relevantes
- Transformer models: Para modelar secuencias de comportamiento
-
Feature engineering más sofisticado:
- Interacciones temporales: Features que combinan múltiples ventanas temporales
- Features de cambio: Detectar cambios abruptos en comportamiento
- Features de velocidad: Tasa de cambio en variables temporales
-
Validación más robusta:
- Walk-forward validation: Validación más estricta para datos temporales
- Multiple time horizons: Evaluar performance en diferentes horizontes temporales
- A/B testing: Validar features en producción
-
Monitoreo en producción:
- Feature drift detection: Detectar cambios en distribución de features
- Performance monitoring: Monitorear degradación del modelo en tiempo real
- Retraining automático: Reentrenar modelo cuando performance decae
-
Integración con sistemas de recomendación:
- Collaborative filtering temporal: Incorporar temporal features en sistemas de recomendación
- Session-based recommendations: Usar features temporales para recomendaciones en sesión
- Real-time personalization: Actualizar features en tiempo real durante la sesión
-
Análisis de causalidad:
- Causal inference: Entender qué features causan cambios en comportamiento
- Intervention analysis: Medir impacto de campañas de marketing
- Counterfactual analysis: Qué habría pasado si el usuario hubiera recibido diferente tratamiento
Nota: Este análisis fue realizado con fines educativos utilizando el dataset Online Retail de Kaggle.
PCA y Feature Selection
Análisis de Componentes Principales (PCA) y técnicas de selección de features para reducir dimensionalidad, mejorar interpretabilidad y prevenir overfitting en modelos de machine learning.
Análisis Geoespacial con GeoPandas
Pipeline completo de análisis geoespacial: carga de datos, gestión de CRS, joins espaciales, agregaciones zonales y visualización de datos sobre CABA.