D
Nucleo tecnico M4 intermedio 24 min

Capitulo 04

Pandas avanzado: limpieza, series temporales y datos faltantes

Donde los proyectos reales se diferencian de los tutoriales

El 80% del tiempo de un Data Scientist se va en limpiar datos. Este modulo cubre los 3 mecanismos de datos faltantes, los 4 metodos de imputacion, deteccion de outliers con IQR y Z-score, y manipulacion de series temporales con resample/rolling/shift.

4.1 El framework de los 3 mecanismos de datos faltantes

Antes de imputar un nulo, tienes que saber por que esta ahi. La eleccion del metodo de imputacion depende del mecanismo. Los 3 mecanismos canónicos (Rubin, 1976) son:

MCAR - Missing Completely At Random

M4

La probabilidad de que falte el valor NO depende de NINGUNA variable (ni observada ni no observada). Ejemplo: un sensor se cae aleatoriamente, una pregunta del formulario se salta por error de programacion. Este es el unico caso donde la imputacion con media/mediana NO introduce sesgo.

#missing #estadistica #avanzado

MAR - Missing At Random

M4

La probabilidad de que falte depende de variables OBSERVADAS, pero no del valor faltante mismo. Ejemplo: los hombres reportan menos ingresos que las mujeres en una encuesta. La falta depende del sexo (observado), no del ingreso faltante. Imputar con KNN o modelos predictivos es razonable.

#missing #estadistica #avanzado

MNAR - Missing Not At Random

M4

La probabilidad de que falte depende del valor faltante mismo. Ejemplo: personas con ingresos muy altos no los reportan. La falta depende del ingreso (no observado). Imputar es muy dificil: puede requerir modelos de seleccion (Heckman) o analisis de sensibilidad. ⚠️ Comun en datos reales, pero raramente testeable.

#missing #estadistica #avanzado
La pregunta que cambia todo

Antes de cualquier imputacion, pregúntale al negocio o al equipo que genero los datos: ¿Por que hay nulos en este campo? La respuesta cambia la estrategia. Si es MCAR (sensor fallando aleatoriamente), imputar es seguro. Si es MNAR (los que ganan mas no reportan), imputar introduce sesgo y DEBES dejarlo explicito en el modelo.

4.2 Metodos de imputacion: cuando usar cada uno

ConceptoDescripcion
media Reemplaza con el valor central. Simple, rapido, no introduce correlacion espuria. Sesga varianza hacia abajo. Solo si MCAR. Usar mediana si hay outliers.
moda Reemplaza con la categoria mas comun. Para variables categoricas. Sesga si hay categorias raras valiosas.
ffill / bfill Para series temporales: propaga el ultimo valor conocido. Asume estabilidad temporal. Cuidado en datos con tendencia.
interpolacion Lineal, spline, polinomial entre puntos conocidos. Para series temporales regularmente espaciadas.
KNN Para cada valor faltante, busca los K vecinos mas cercanos (por otras features) y promedia sus valores. Mas robusto que media, pero costoso computacionalmente.
Iterative Entrena un modelo por columna con nulos usando las demas como features. Itera hasta converger. El mas sofisticado. Captura relaciones entre variables.
python Comparativa de imputadores con Pipeline (sin leakage)
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer, KNNImputer
from sklearn.experimental import enable_iterative_imputer  # noqa
from sklearn.impute import IterativeImputer
from sklearn.ensemble import RandomForestRegressor
from sklearn.compose import ColumnTransformer

np.random.seed(42)
N = 1000

# Generar dataset con nulos MCAR
df = pd.DataFrame({
    'edad':      np.random.normal(35, 12, N).clip(0, 90),
    'ingreso':   np.random.lognormal(10.5, 0.8, N),
    'antiguedad':np.random.uniform(0, 20, N),
})
# Introducir 15% de nulos MCAR en cada columna
for col in df.columns:
    mask = np.random.random(N) < 0.15
    df.loc[mask, col] = np.nan

# Comparar metodos de imputacion con el mismo modelo
y = np.random.binomial(1, 0.3, N)  # target sintetico
X = df.copy()

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

metodos = {
    'Media':          SimpleImputer(strategy='mean'),
    'Mediana':        SimpleImputer(strategy='median'),
    'KNN (k=5)':      KNNImputer(n_neighbors=5),
    'Iterative (MICE)': IterativeImputer(random_state=42),
}

for nombre, imputer in metodos.items():
    # CLAVE: el imputer va DENTRO del pipeline → sin leakage
    pipe = Pipeline([
        ('imputer', imputer),
        ('model', RandomForestRegressor(n_estimators=50, random_state=42, n_jobs=-1))
    ])
    scores = cross_val_score(pipe, X_train, y_train, cv=5, scoring='r2')
    print(f"{nombre:25s} R²: {scores.mean():.3f} ± {scores.std():.3f}")
Regla de oro: imputador DENTRO del Pipeline

El error mas comun y mas grave: imputar los nulos antes del train/test split usando el promedio de TODO el dataset. Eso es data leakage: la media de test “se filtra” al train a traves del valor imputado. La unica forma correcta: imputar DENTRO del Pipeline, ajustado solo en train. El codigo de arriba lo hace bien.

4.3 Outliers: IQR vs Z-score vs Isolation Forest

python Tres metodos de deteccion de outliers
import numpy as np
import pandas as pd
from sklearn.ensemble import IsolationForest

np.random.seed(42)
N = 1000
data = np.concatenate([
    np.random.normal(100, 15, N),  # datos normales
    [-50, 200, 250, 300]          # outliers extremos
])
df = pd.DataFrame({'valor': data})

# 1) IQR: cuartiles
Q1, Q3 = df['valor'].quantile([0.25, 0.75])
IQR = Q3 - Q1
limite_inf = Q1 - 1.5 * IQR
limite_sup = Q3 + 1.5 * IQR
outliers_iqr = df[(df['valor'] < limite_inf) | (df['valor'] > limite_sup)]
print(f"IQR outliers: {len(outliers_iqr)} (limites: [{limite_inf:.1f}, {limite_sup:.1f}])")

# 2) Z-score: asume distribucion normal
from scipy import stats
z_scores = np.abs(stats.zscore(df['valor']))
outliers_z = df[z_scores > 3]
print(f"Z-score outliers (|z|>3): {len(outliers_z)}")

# 3) Isolation Forest: detecta anomalias multivariadas, NO asume distribucion
iso = IsolationForest(contamination=0.05, random_state=42)
df['outlier_if'] = iso.fit_predict(df[['valor']])  # -1 = outlier
n_outliers_if = (df['outlier_if'] == -1).sum()
print(f"Isolation Forest outliers: {n_outliers_if}")

# Comparacion
print("\nPrimeros outliers detectados por cada metodo:")
print("IQR:", outliers_iqr.head().values.flatten().tolist())
print("Z-score:", outliers_z.head().values.flatten().tolist())
print("IF:", df[df['outlier_if']==-1].head().values.flatten().tolist())
Outliers: ¿borrar, transformar o etiquetar?

Tres estrategias validas, una peligrosa:

  • Transformar (log, sqrt, Box-Cox): reduce la influencia de outliers sin perder informacion. La mas recomendada.
  • Etiquetar como feature: crea una columna es_outlier y deja que el modelo decida si le importa. Util cuando el outlier puede ser senal (fraude, evento raro).
  • Winsorizar (cap a percentil 1/99): reemplaza outliers con el percentil extremo. Comun en estadistica clasica, menos en ML.
  • Borrar: ⚠️ ultimo recurso. Solo si estas seguro de que son errores de medicion.

4.4 Series temporales: el arsenal de Pandas

python Series temporales: resample, rolling, shift, diff
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# Generar serie diaria de 1 anio con tendencia y estacionalidad
np.random.seed(42)
fechas = pd.date_range('2023-01-01', '2023-12-31', freq='D')
N = len(fechas)
tendencia = np.linspace(100, 200, N)
estacionalidad = 20 * np.sin(2 * np.pi * np.arange(N) / 365)
ruido = np.random.normal(0, 5, N)
ventas = tendencia + estacionalidad + ruido

ts = pd.Series(ventas, index=fechas, name='ventas')

# 1) Resample: cambiar frecuencia
semanal = ts.resample('W').sum()
mensual = ts.resample('ME').mean()
print("Primeras semanas:", semanal.head(3).round(1).tolist())

# 2) Rolling: media/mediana movil
ts_ma7  = ts.rolling(window=7,   min_periods=1).mean()
ts_ma30 = ts.rolling(window=30,  min_periods=1).mean()
ts_ma90 = ts.rolling(window=90,  min_periods=1).mean()

# 3) Shift: lag features (predictoras del pasado)
ts_lag1  = ts.shift(1)
ts_lag7  = ts.shift(7)
ts_diff1 = ts.diff(1)  # diferencia dia a dia
ts_pct   = ts.pct_change()  # cambio porcentual

# 4) Expanding: acumulado hasta hoy
ts_cumsum = ts.expanding().sum()

# 5) Visualizacion
fig, axes = plt.subplots(2, 2, figsize=(14, 8))

axes[0,0].plot(ts.index, ts, alpha=0.4, label='Original')
axes[0,0].plot(ts.index, ts_ma7, label='MA 7d', linewidth=2)
axes[0,0].plot(ts.index, ts_ma30, label='MA 30d', linewidth=2)
axes[0,0].set_title('Medias moviles (suavizado)')
axes[0,0].legend()

axes[0,1].plot(mensual.index, mensual.values, marker='o', color='teal')
axes[0,1].set_title('Resample mensual (media)')

axes[1,0].plot(ts_diff1.index, ts_diff1.values, alpha=0.6)
axes[1,0].axhline(0, color='red', linestyle='--', alpha=0.5)
axes[1,0].set_title('Diff(1): cambio dia a dia')

axes[1,1].plot(ts_pct.index, ts_pct.values * 100, alpha=0.6, color='orange')
axes[1,1].set_title('Cambio porcentual diario (%)')

plt.tight_layout()
plt.show()
Decomposicion clasica: trend + seasonality + residual

Una serie temporal se descompone en: tendencia (direccion de largo plazo), estacionalidad (patron que se repite, ej: cada 7 dias / 12 meses), y residual (lo que queda). Modelos como Prophet, SARIMA, y ExponentialSmoothing (Holt-Winters) hacen esto automaticamente. statsmodels.tsa.seasonal_decompose() es el primer paso.

4.5 Errores comunes

Imputar nulos con la media del dataset completo (antes del split)
✗ df['edad'].fillna(df['edad'].mean()); train_test_split(df, y)
✓ Pipeline([('imputer', SimpleImputer()), ('model', ...)]); pipeline.fit(X_train)
Sortear por indice y olvidar que DatetimeIndex no es un sort estable
✗ ts.sort_index() # OK, pero no resuelve duplicados
✓ ts = ts[~ts.index.duplicated()].sort_index() # primero eliminar duplicados
Borrar nulos sin entender el mecanismo
✗ df.dropna() # borra todas las filas con cualquier nulo. Sesga los datos si MNAR.
✓ Investigar el mecanismo (MCAR/MAR/MNAR) ANTES. Imputar es generalmente mejor que borrar.
Usar resample('W') sin saber qué dia empieza la semana
✗ ts.resample('W').sum() # la semana puede empezar en domingo o lunes segun tu locale
✓ ts.resample('W-MON').sum() # explicito: semana empezando en lunes
Libro: "Forecasting: Principles and Practice" (3ra ed) - Hyndman & Athanasopoulos. Gratis online. Cap 2-7 son el canon de series temporales.
Libro: "Practical Time Series Analysis" - Aileen Nielsen. Cap 1-4 cubren lo basico en Python.
Video: StatQuest: "Time Series" (YouTube, 4 videos, ~1h total, muy didactico)
Articulo: Pandas docs: "Time Series / Date functionality" (la documentacion oficial es muy buena)
Herramienta: Prophet (Meta): forecasting automatico, robusto, documentacion excelente
Herramienta: sktime: scikit-learn para series temporales, integra Prophet, ARIMA, exponential smoothing
Mini-proyecto 4: limpieza completa de un dataset real intermedio
  1. Tomá un dataset publico con nulos, outliers y columnas de fecha (sugerencia: Airbnb listings de una ciudad, House Prices, o datos de COVID por pais).
  2. Analiza los nulos: porcentaje por columna. Investiga si parecen MCAR, MAR o MNAR. Documenta tu hipotesis con evidencia (ej: si los nulos estan concentrados en una region o en cierta clase).
  3. Para outliers numericos, prueba los 3 metodos (IQR, Z-score, Isolation Forest). Compara cuantos detecta cada uno. Para los que aparecen en multiples metodos, decide si son errores o senales.
  4. Crea una Pipeline de imputacion (con KNN o IterativeImputer) y compara su performance contra SimpleImputer con cross-validation.
  5. Si hay columnas de fecha, crea al menos 3 features: dia de la semana, mes, dias_desde_inicio. Visualiza ventas/actividad por mes.