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
M4La 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.
MAR - Missing At Random
M4La 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.
MNAR - Missing Not At Random
M4La 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.
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
| Concepto | Descripcion |
|---|---|
| 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. |
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}") 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
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()) 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_outliery 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
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() 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
- 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).
- 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).
- 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.
- Crea una Pipeline de imputacion (con KNN o IterativeImputer) y compara su performance contra SimpleImputer con cross-validation.
- Si hay columnas de fecha, crea al menos 3 features: dia de la semana, mes, dias_desde_inicio. Visualiza ventas/actividad por mes.