Saltar a contenido

Práctica 10: Temporal Feature Engineering con Pandas

Parte 0: Setup 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
import platform
warnings.filterwarnings('ignore')

# Configurar pandas y visualización
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', 100)
plt.style.use('seaborn-v0_8-darkgrid')

print("✅ Librerías importadas correctamente")
print(f"Pandas version: {pd.__version__}")
print(f"Sistema operativo: {platform.system()}")
print(f"Python version: {platform.python_version()}")

print("\n" + "="*70)
print("🚀 TEMPORAL FEATURE ENGINEERING CON PANDAS")
print("="*70)
print("✅ Pandas con .groupby() + .shift() previene data leakage")
print("✅ Sintaxis clara y estudiantes ya conocen pandas")
print("✅ Compatible con cualquier entorno Python")
print("="*70 + "\n")
✅ Librerías importadas correctamente
Pandas version: 2.3.1
Sistema operativo: Windows
Python version: 3.12.10

======================================================================
🚀 TEMPORAL FEATURE ENGINEERING CON PANDAS
======================================================================
✅ Pandas con .groupby() + .shift() previene data leakage
✅ Sintaxis clara y estudiantes ya conocen pandas
✅ Compatible con cualquier entorno Python
======================================================================

Parte 1: Carga del Dataset

1.1 Cargar Online Retail Dataset desde Kaggle

print("\n=== DESCARGANDO ONLINE RETAIL DATASET DESDE KAGGLE ===\n")

# Instalar kaggle si no está instalado
from kaggle.api.kaggle_api_extended import KaggleApi

# Autenticar con API
api = KaggleApi()
api.authenticate()
print("✅ Autenticación exitosa con Kaggle API\n")

# Dataset reference para Online Retail
dataset_ref = "vijayuv/onlineretail"
download_path = "./data"

# Crear directorio si no existe
os.makedirs(download_path, exist_ok=True)

# Descargar archivos
print(f"📥 Descargando dataset: {dataset_ref}")
print(f"📂 Destino: {download_path}")
print("⏳ Esto puede tomar 1-2 minutos (~20MB)...\n")

api.dataset_download_files(
    dataset_ref,
    path=download_path,
    unzip=True  # Descomprime automáticamente
)

print("✅ Descarga completada\n")

# Listar archivos descargados
print(f"📋 Archivos disponibles en {download_path}:")
for file in sorted(os.listdir(download_path)):
    file_path = os.path.join(download_path, file)
    if os.path.isfile(file_path):
        size_mb = os.path.getsize(file_path) / (1024 * 1024)
        print(f"   ✅ {file:40s} ({size_mb:7.2f} MB)")
=== DESCARGANDO ONLINE RETAIL DATASET DESDE KAGGLE ===

✅ Autenticación exitosa con Kaggle API

📥 Descargando dataset: vijayuv/onlineretail
📂 Destino: ./data
⏳ Esto puede tomar 1-2 minutos (~20MB)...

Dataset URL: https://www.kaggle.com/datasets/vijayuv/onlineretail
✅ Descarga completada

📋 Archivos disponibles en ./data:
   ✅ OnlineRetail.csv                         (  43.47 MB)
print("\n=== CARGANDO ONLINE RETAIL EN PANDAS ===\n")

# Cargar el dataset principal
df_raw = pd.read_csv(f'{download_path}/OnlineRetail.csv', encoding='ISO-8859-1')

print("✅ Dataset cargado exitosamente\n")
print(f"📊 Shape inicial: {df_raw.shape}")

print("\n" + "=" * 70)
print("PREVIEW: Online Retail Dataset")
print("=" * 70)
print(df_raw.info())
print("\n", df_raw.head(10))

print("\n" + "=" * 70)
print("COLUMNAS DEL DATASET")
print("=" * 70)
print("\n".join([f"  - {col}: {df_raw[col].dtype}" for col in df_raw.columns]))
print("\n" + "=" * 70)
=== CARGANDO ONLINE RETAIL EN PANDAS ===

✅ Dataset cargado exitosamente

📊 Shape inicial: (541909, 8)

======================================================================
PREVIEW: Online Retail Dataset
======================================================================
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 541909 entries, 0 to 541908
Data columns (total 8 columns):
 #   Column       Non-Null Count   Dtype  
---  ------       --------------   -----  
 0   InvoiceNo    541909 non-null  object 
 1   StockCode    541909 non-null  object 
 2   Description  540455 non-null  object 
 3   Quantity     541909 non-null  int64  
 4   InvoiceDate  541909 non-null  object 
 5   UnitPrice    541909 non-null  float64
 6   CustomerID   406829 non-null  float64
 7   Country      541909 non-null  object 
dtypes: float64(2), int64(1), object(5)
memory usage: 33.1+ MB
None

   InvoiceNo StockCode                          Description  Quantity  \
0    536365    85123A   WHITE HANGING HEART T-LIGHT HOLDER         6   
1    536365     71053                  WHITE METAL LANTERN         6   
2    536365    84406B       CREAM CUPID HEARTS COAT HANGER         8   
3    536365    84029G  KNITTED UNION FLAG HOT WATER BOTTLE         6   
4    536365    84029E       RED WOOLLY HOTTIE WHITE HEART.         6   
5    536365     22752         SET 7 BABUSHKA NESTING BOXES         2   
6    536365     21730    GLASS STAR FROSTED T-LIGHT HOLDER         6   
7    536366     22633               HAND WARMER UNION JACK         6   
8    536366     22632            HAND WARMER RED POLKA DOT         6   
9    536367     84879        ASSORTED COLOUR BIRD ORNAMENT        32   

      InvoiceDate  UnitPrice  CustomerID         Country  
0  12/1/2010 8:26       2.55     17850.0  United Kingdom  
1  12/1/2010 8:26       3.39     17850.0  United Kingdom  
2  12/1/2010 8:26       2.75     17850.0  United Kingdom  
3  12/1/2010 8:26       3.39     17850.0  United Kingdom  
4  12/1/2010 8:26       3.39     17850.0  United Kingdom  
5  12/1/2010 8:26       7.65     17850.0  United Kingdom  
6  12/1/2010 8:26       4.25     17850.0  United Kingdom  
7  12/1/2010 8:28       1.85     17850.0  United Kingdom  
8  12/1/2010 8:28       1.85     17850.0  United Kingdom  
9  12/1/2010 8:34       1.69     13047.0  United Kingdom  

======================================================================
COLUMNAS DEL DATASET
======================================================================
  - InvoiceNo: object
  - StockCode: object
  - Description: object
  - Quantity: int64
  - InvoiceDate: object
  - UnitPrice: float64
  - CustomerID: float64
  - Country: object

======================================================================

1.2 Preparar y Explorar Estructura Temporal

print("\n=== PREPARANDO DATOS PARA ANÁLISIS TEMPORAL ===\n")

# 1. Limpiar datos
print("📋 Paso 1: Limpieza de datos")

# Eliminar filas con CustomerID nulo (no podemos hacer análisis temporal sin ID)
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):,})")

# 2. Crear columnas derivadas
print("\n📋 Paso 2: Creando columnas 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)

print("   ✅ Columnas renombradas y total_amount calculado")

# 3. Exploración temporal
print("\n" + "=" * 70)
print("EXPLORACIÓN TEMPORAL")
print("=" * 70)

print(f"\n📊 Shape del dataset: {df.shape}")
print(f"📅 Rango de fechas: {df['order_date'].min().date()} a {df['order_date'].max().date()}")
print(f"⏱️  Rango de días: {(df['order_date'].max() - df['order_date'].min()).days} días")

print(f"\n👥 Unique usuarios: {df['user_id'].nunique():,}")
print(f"📦 Unique productos: {df['product_id'].nunique():,}")
print(f"🛒 Total órdenes (facturas): {df['order_id'].nunique():,}")
print(f"📝 Total items/líneas: {len(df):,}")

print(f"\n💰 Promedio items por orden: {df.groupby('order_id').size().mean():.2f}")
print(f"🔁 Promedio órdenes por usuario: {df.groupby('user_id')['order_id'].nunique().mean():.2f}")
print(f"💵 Promedio precio por item: ${df['price'].mean():.2f}")
print(f"💸 Total ventas: ${df['total_amount'].sum():,.2f}")

# Identificar tipo de datos temporales
print("\n" + "=" * 70)
print("TIPO DE DATOS TEMPORALES")
print("=" * 70)
print("✅ Dataset de TRANSACCIONES (eventos irregulares con timestamps)")
print("   - No hay intervalos fijos entre eventos")
print("   - Cada usuario tiene su propio timeline de compras")
print("   - Alta frecuencia de compras repetidas (ideal para temporal features)")
print("   - Perfecto para temporal feature engineering con Pandas")

# 4. Análisis de usuarios con múltiples órdenes
multi_order_users = df.groupby('user_id')['order_id'].nunique()
users_with_multiple = (multi_order_users > 1).sum()
print(f"\n🎯 Usuarios con múltiples órdenes: {users_with_multiple:,} ({users_with_multiple/len(multi_order_users)*100:.1f}%)")
print(f"📈 Promedio órdenes (usuarios recurrentes): {multi_order_users[multi_order_users > 1].mean():.2f}")

# 5. Visualizar distribución temporal
print("\n📊 Generando visualizaciones temporales...")

fig, axes = plt.subplots(1, 2, figsize=(16, 5))

# Órdenes por semana
weekly_orders = df.groupby(df['order_date'].dt.to_period('W'))['order_id'].nunique()
axes[0].plot(range(len(weekly_orders)), weekly_orders.values, marker='o', linewidth=2, markersize=4)
axes[0].set_title('Órdenes Únicas por Semana', fontsize=14, fontweight='bold')
axes[0].set_xlabel('Semana')
axes[0].set_ylabel('Número de Órdenes')
axes[0].grid(alpha=0.3)

# Distribución de tiempo entre órdenes por usuario
time_between_list = []
for user_id, group in df.groupby('user_id')['order_date']:
    if len(group) > 1:
        diffs = group.diff().dt.days.dropna()
        time_between_list.extend([val for val in diffs if val > 0])

if len(time_between_list) > 0:
    axes[1].hist(time_between_list, bins=50, edgecolor='black', alpha=0.7, color='steelblue')
    axes[1].set_title(f'Distribución de Días entre Órdenes\n({len(time_between_list):,} transiciones)', 
                     fontsize=14, fontweight='bold')
    axes[1].set_xlabel('Días entre órdenes')
    axes[1].set_ylabel('Frecuencia')
    axes[1].axvline(np.median(time_between_list), color='red', linestyle='--', 
                   label=f'Mediana: {np.median(time_between_list):.0f} días')
    axes[1].legend()
else:
    axes[1].text(0.5, 0.5, 'No hay suficientes usuarios\ncon múltiples órdenes',
                ha='center', va='center', fontsize=12, transform=axes[1].transAxes)

axes[1].grid(alpha=0.3)
plt.tight_layout()
plt.show()

# 6. Primeras filas del dataset preparado
print("\n" + "=" * 70)
print("PRIMERAS FILAS DEL DATASET PREPARADO")
print("=" * 70)
print(df[['user_id', 'order_id', 'order_date', 'product_id', 
         'Quantity', 'price', 'total_amount', 'Country']].head(20))

print("\n✅ Dataset limpio y listo para análisis temporal con Pandas\n")
=== PREPARANDO DATOS PARA ANÁLISIS TEMPORAL ===

📋 Paso 1: Limpieza de datos
   ✅ Filas después de limpieza: 397,884 (de 541,909)

📋 Paso 2: Creando columnas derivadas
   ✅ Columnas renombradas y total_amount calculado

======================================================================
EXPLORACIÓN TEMPORAL
======================================================================

📊 Shape del dataset: (397884, 9)
📅 Rango de fechas: 2010-12-01 a 2011-12-09
⏱️  Rango de días: 373 días

👥 Unique usuarios: 4,338
📦 Unique productos: 3,665
🛒 Total órdenes (facturas): 18,532
📝 Total items/líneas: 397,884

💰 Promedio items por orden: 21.47
🔁 Promedio órdenes por usuario: 4.27
💵 Promedio precio por item: $3.12
💸 Total ventas: $8,911,407.90

======================================================================
TIPO DE DATOS TEMPORALES
======================================================================
✅ Dataset de TRANSACCIONES (eventos irregulares con timestamps)
   - No hay intervalos fijos entre eventos
   - Cada usuario tiene su propio timeline de compras
   - Alta frecuencia de compras repetidas (ideal para temporal features)
   - Perfecto para temporal feature engineering con Pandas

🎯 Usuarios con múltiples órdenes: 2,845 (65.6%)
📈 Promedio órdenes (usuarios recurrentes): 5.99

📊 Generando visualizaciones temporales...
======================================================================
PRIMERAS FILAS DEL DATASET PREPARADO
======================================================================
    user_id order_id          order_date product_id  Quantity  price  \
0   12346.0   541431 2011-01-18 10:01:00      23166     74215   1.04   
1   12347.0   537626 2010-12-07 14:57:00      85116        12   2.10   
2   12347.0   537626 2010-12-07 14:57:00      22375         4   4.25   
3   12347.0   537626 2010-12-07 14:57:00      71477        12   3.25   
4   12347.0   537626 2010-12-07 14:57:00      22492        36   0.65   
5   12347.0   537626 2010-12-07 14:57:00      22771        12   1.25   
6   12347.0   537626 2010-12-07 14:57:00      22772        12   1.25   
7   12347.0   537626 2010-12-07 14:57:00      22773        12   1.25   
8   12347.0   537626 2010-12-07 14:57:00      22774        12   1.25   
9   12347.0   537626 2010-12-07 14:57:00      22775        12   1.25   
10  12347.0   537626 2010-12-07 14:57:00      22805        12   1.25   
11  12347.0   537626 2010-12-07 14:57:00      22725         4   3.75   
12  12347.0   537626 2010-12-07 14:57:00      22726         4   3.75   
13  12347.0   537626 2010-12-07 14:57:00      22727         4   3.75   
14  12347.0   537626 2010-12-07 14:57:00      22728         4   3.75   
15  12347.0   537626 2010-12-07 14:57:00      22729         4   3.75   
16  12347.0   537626 2010-12-07 14:57:00      22212         6   2.10   
17  12347.0   537626 2010-12-07 14:57:00     85167B        30   1.25   
18  12347.0   537626 2010-12-07 14:57:00      21171        12   1.45   
19  12347.0   537626 2010-12-07 14:57:00      22195        12   1.65   

    total_amount         Country  
0        77183.6  United Kingdom  
1           25.2         Iceland  
2           17.0         Iceland  
3           39.0         Iceland  
4           23.4         Iceland  
5           15.0         Iceland  
6           15.0         Iceland  
7           15.0         Iceland  
8           15.0         Iceland  
9           15.0         Iceland  
10          15.0         Iceland  
11          15.0         Iceland  
12          15.0         Iceland  
13          15.0         Iceland  
14          15.0         Iceland  
15          15.0         Iceland  
16          12.6         Iceland  
17          37.5         Iceland  
18          17.4         Iceland  
19          19.8         Iceland  

✅ Dataset limpio y listo para análisis temporal con Pandas

Parte 1.3: Crear Features Derivadas a Nivel de Orden

print("\n=== CREANDO FEATURES DERIVADAS ===\n")

# 1. Extraer features temporales de order_date (a nivel de transacción)
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)

print("✅ Features temporales extraídas:")
print("   - order_dow: Día de semana (0=Lunes, 6=Domingo)")
print("   - order_hour_of_day: Hora del día (0-23)")

# Ver primeras transacciones con features temporales
print("\n📋 Primeras transacciones con features temporales:")
print(df[['user_id', 'order_id', 'order_date', 'order_dow', 
         'order_hour_of_day', 'Quantity', 'total_amount']].head(10))

# 2. Agregar a nivel de ORDEN (una fila por factura/orden)
print("\n" + "=" * 70)
print("AGREGANDO A NIVEL DE ORDEN")
print("=" * 70)
print(f"\n📊 Filas antes de agregar (nivel transacción): {len(df):,}")

# ⚠️ IMPORTANTE: Convertir user_id a int para consistencia
df['user_id'] = df['user_id'].astype(int)

# Agregar transacciones por orden para obtener una fila por factura
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 (necesario para features temporales)
orders_df = orders_df.sort_values(['user_id', 'order_date']).reset_index(drop=True)

# 3. Calcular features temporales a nivel de usuario
print("\n📋 Calculando features temporales por usuario...")

# order_number: número secuencial de orden para cada usuario (1, 2, 3, ...)
orders_df['order_number'] = orders_df.groupby('user_id').cumcount() + 1

# days_since_prior_order: días transcurridos desde la última orden del usuario
orders_df['days_since_prior_order'] = orders_df.groupby('user_id')['order_date'].diff().dt.days

print(f"\n✅ Filas después de agregar (nivel orden): {len(orders_df):,}")

# 4. Validación
if len(orders_df) == 0:
    raise ValueError("❌ ERROR CRÍTICO: orders_df está vacío!")

# 5. Estadísticas del dataset agregado
print("\n" + "=" * 70)
print("ORDERS DATASET (una fila por orden/factura)")
print("=" * 70)
print(f"\n📊 Shape: {orders_df.shape}")
print(f"👥 Usuarios únicos: {orders_df['user_id'].nunique():,}")
print(f"🛒 Órdenes totales: {len(orders_df):,}")

print("\n💰 Estadísticas de órdenes:")
print(f"   - Cart size promedio: {orders_df['cart_size'].mean():.2f} items")
print(f"   - Total promedio por orden: ${orders_df['order_total'].mean():.2f}")
print(f"   - Días promedio entre órdenes: {orders_df['days_since_prior_order'].mean():.1f} días")

print("\n📋 Primeras 10 órdenes:")
print(orders_df[['user_id', 'order_id', 'order_date', 'order_number', 
                 'cart_size', 'order_total', 'days_since_prior_order']].head(10))

print("\n" + "=" * 70)
print(f"✅ Dataset preparado: {len(orders_df):,} órdenes de {orders_df['user_id'].nunique():,} usuarios")
print(f"✅ Período: {(orders_df['order_date'].max() - orders_df['order_date'].min()).days} días")
print("=" * 70)
=== CREANDO FEATURES DERIVADAS ===

✅ Features temporales extraídas:
   - order_dow: Día de semana (0=Lunes, 6=Domingo)
   - order_hour_of_day: Hora del día (0-23)

📋 Primeras transacciones con features temporales:
   user_id order_id          order_date  order_dow  order_hour_of_day  \
0  12346.0   541431 2011-01-18 10:01:00          1                 10   
1  12347.0   537626 2010-12-07 14:57:00          1                 14   
2  12347.0   537626 2010-12-07 14:57:00          1                 14   
3  12347.0   537626 2010-12-07 14:57:00          1                 14   
4  12347.0   537626 2010-12-07 14:57:00          1                 14   
5  12347.0   537626 2010-12-07 14:57:00          1                 14   
6  12347.0   537626 2010-12-07 14:57:00          1                 14   
7  12347.0   537626 2010-12-07 14:57:00          1                 14   
8  12347.0   537626 2010-12-07 14:57:00          1                 14   
9  12347.0   537626 2010-12-07 14:57:00          1                 14   

   Quantity  total_amount  
0     74215       77183.6  
1        12          25.2  
2         4          17.0  
3        12          39.0  
4        36          23.4  
5        12          15.0  
6        12          15.0  
7        12          15.0  
8        12          15.0  
9        12          15.0  

======================================================================
AGREGANDO A NIVEL DE ORDEN
======================================================================

📊 Filas antes de agregar (nivel transacción): 397,884

📋 Calculando features temporales por usuario...

✅ Filas después de agregar (nivel orden): 18,562

======================================================================
ORDERS DATASET (una fila por orden/factura)
======================================================================

📊 Shape: (18562, 9)
👥 Usuarios únicos: 4,338
🛒 Órdenes totales: 18,562

💰 Estadísticas de órdenes:
   - Cart size promedio: 21.44 items
   - Total promedio por orden: $480.09
   - Días promedio entre órdenes: 39.4 días

📋 Primeras 10 órdenes:
   user_id order_id          order_date  order_number  cart_size  order_total  \
0    12346   541431 2011-01-18 10:01:00             1          1     77183.60   
1    12347   537626 2010-12-07 14:57:00             1         31       711.79   
2    12347   542237 2011-01-26 14:30:00             2         29       475.39   
3    12347   549222 2011-04-07 10:43:00             3         24       636.25   
4    12347   556201 2011-06-09 13:01:00             4         18       382.52   
5    12347   562032 2011-08-02 08:48:00             5         22       584.91   
6    12347   573511 2011-10-31 12:25:00             6         47      1294.32   
7    12347   581180 2011-12-07 15:52:00             7         11       224.82   
8    12348   539318 2010-12-16 19:09:00             1         17       892.80   
9    12348   541998 2011-01-25 10:42:00             2          6       227.44   

   days_since_prior_order  
0                     NaN  
1                     NaN  
2                    49.0  
3                    70.0  
4                    63.0  
5                    53.0  
6                    90.0  
7                    37.0  
8                     NaN  
9                    39.0  

======================================================================
✅ Dataset preparado: 18,562 órdenes de 4,338 usuarios
✅ Período: 373 días
======================================================================

Parte 2: Lag Features con Pandas

2.1 Crear Lag Features con .shift()

print("\n=== CREANDO LAG FEATURES CON PANDAS ===\n")

# CRÍTICO: Asegurar que los datos estén ordenados por user_id y order_date
orders_df = orders_df.sort_values(['user_id', 'order_date']).reset_index(drop=True)

# ⚠️ COMPLETA: Crear lags de days_since_prior_order (últimas 1, 2, 3 órdenes)
# .shift(n) toma el valor de la fila anterior DENTRO de cada grupo
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)

print("✅ Lag Features creadas con Pandas")

# IMPORTANTE: Seleccionar un usuario con MÚLTIPLES órdenes para visualizaciones
print("\n🔍 Seleccionando usuario con múltiples órdenes para ejemplos...")
user_order_counts = orders_df.groupby('user_id').size().sort_values(ascending=False)
users_with_many_orders = user_order_counts[user_order_counts >= 8].index.tolist()

if len(users_with_many_orders) > 0:
    sample_user_id = users_with_many_orders[0]  # Usuario con más órdenes
else:
    sample_user_id = user_order_counts.index[0]  # Mejor disponible

print(f"✅ Usuario seleccionado: {sample_user_id} ({user_order_counts[sample_user_id]} órdenes)\n")

# Mostrar ejemplo
print(f"Ejemplo de Lag Features para usuario {sample_user_id}:")
sample = orders_df[orders_df['user_id'] == sample_user_id][
    ['user_id', 'order_number', 'days_since_prior_order',
     'days_since_prior_lag_1', 'days_since_prior_lag_2', 'days_since_prior_lag_3']
].head(12)
print(sample)

print(f"\n✅ NaNs en lag_1: {orders_df['days_since_prior_lag_1'].isna().sum():,}")
print(f"✅ NaNs en lag_2: {orders_df['days_since_prior_lag_2'].isna().sum():,}")
print(f"✅ NaNs en lag_3: {orders_df['days_since_prior_lag_3'].isna().sum():,}")

print("\n💡 Los NaN son normales: aparecen en las primeras órdenes donde no hay historia previa")
print("💡 .groupby() + .shift() previene data leakage: cada usuario tiene sus propios lags independientes")
=== CREANDO LAG FEATURES CON PANDAS ===

✅ Lag Features creadas con Pandas

🔍 Seleccionando usuario con múltiples órdenes para ejemplos...
✅ Usuario seleccionado: 12748 (210 órdenes)

Ejemplo de Lag Features para usuario 12748:
      user_id  order_number  days_since_prior_order  days_since_prior_lag_1  \
1318    12748             1                     NaN                     NaN   
1319    12748             2                     0.0                     NaN   
1320    12748             3                     3.0                     0.0   
1321    12748             4                     0.0                     3.0   
1322    12748             5                     0.0                     0.0   
1323    12748             6                     0.0                     0.0   
1324    12748             7                     0.0                     0.0   
1325    12748             8                     0.0                     0.0   
1326    12748             9                     0.0                     0.0   
1327    12748            10                     0.0                     0.0   
1328    12748            11                     0.0                     0.0   
1329    12748            12                     0.0                     0.0   

      days_since_prior_lag_2  days_since_prior_lag_3  
1318                     NaN                     NaN  
1319                     NaN                     NaN  
1320                     NaN                     NaN  
1321                     0.0                     NaN  
1322                     3.0                     0.0  
1323                     0.0                     3.0  
1324                     0.0                     0.0  
1325                     0.0                     0.0  
1326                     0.0                     0.0  
1327                     0.0                     0.0  
1328                     0.0                     0.0  
1329                     0.0                     0.0  

✅ NaNs en lag_1: 7,185
✅ NaNs en lag_2: 9,196
✅ NaNs en lag_3: 10,701

💡 Los NaN son normales: aparecen en las primeras órdenes donde no hay historia previa
💡 .groupby() + .shift() previene data leakage: cada usuario tiene sus propios lags independientes

2.2 Rolling Window Features con Pandas

print("\n=== CREANDO ROLLING FEATURES CON PANDAS ===\n")

# ⚠️ COMPLETA: Rolling mean de cart_size (últimas 3 órdenes, EXCLUYENDO la actual)
orders_df['rolling_cart_mean_3'] = (
    orders_df.groupby('user_id')['cart_size']
    .shift(1)
    .rolling(window=3, min_periods=1)
    .mean()
    .reset_index(level=0, drop=True)
)

# ⚠️ COMPLETA: Rolling std de cart_size
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)
)

print("✅ Rolling Features creadas con Pandas")

# Mostrar ejemplo
print(f"\nEjemplo para un usuario:")
sample = orders_df[orders_df['user_id'] == sample_user_id][
    ['order_number', 'cart_size', 'rolling_cart_mean_3', 'rolling_cart_std_3']
].head(10)
print(sample)

# Visualización
fig, ax = plt.subplots(figsize=(14, 6))
user_sample = orders_df[orders_df['user_id'] == sample_user_id].head(20)

# Graficar valores reales
ax.plot(user_sample['order_number'], user_sample['cart_size'], 
        marker='x', alpha=0.7, label='Cart Size Actual', linewidth=2.5, markersize=10)

# Graficar rolling mean (solo si hay valores no-NaN)
if user_sample['rolling_cart_mean_3'].notna().any():
    ax.plot(user_sample['order_number'], user_sample['rolling_cart_mean_3'], 
            marker='o', label='Rolling Mean (3 órdenes previas)', linewidth=2.5, 
            color='coral', markersize=8)

    # Fill between con std (solo si hay valores)
    if user_sample['rolling_cart_std_3'].notna().any():
        ax.fill_between(user_sample['order_number'],
                        user_sample['rolling_cart_mean_3'] - user_sample['rolling_cart_std_3'],
                        user_sample['rolling_cart_mean_3'] + user_sample['rolling_cart_std_3'],
                        alpha=0.2, label='±1 std', color='coral')

ax.set_xlabel('Order Number', fontsize=12)
ax.set_ylabel('Cart Size', fontsize=12)
ax.set_title(f'Rolling Mean vs Actual Cart Size (User {sample_user_id})', 
             fontweight='bold', fontsize=14)
ax.legend(fontsize=11)
ax.grid(alpha=0.3)
plt.tight_layout()
plt.show()

print("\n✅ Ventaja clave: .shift(1) antes de .rolling() previene data leakage automáticamente")
=== CREANDO ROLLING FEATURES CON PANDAS ===

✅ Rolling Features creadas con Pandas

Ejemplo para un usuario:
      order_number  cart_size  rolling_cart_mean_3  rolling_cart_std_3
1318             1          1            13.000000            2.828427
1319             2          1             6.000000            7.071068
1320             3         37             1.000000            0.000000
1321             4         39            13.000000           20.784610
1322             5         15            25.666667           21.385353
1323             6         33            30.333333           13.316656
1324             7          6            29.000000           12.489996
1325             8         64            18.000000           13.747727
1326             9         38            34.333333           29.022979
1327            10         21            36.000000           29.051678
✅ Ventaja clave: .shift(1) antes de .rolling() previene data leakage automáticamente