🔍 Práctica 5: Missing Data Detective - Fill in the Blanks¶
UT2: Calidad & Ética | Práctica Guiada
🎯 Objetivos Básicos¶
- Aprender a detectar y analizar datos faltantes (MCAR, MAR, MNAR)
- Identificar outliers usando métodos estadísticos
- Implementar estrategias de imputación apropiadas
- Crear pipelines de limpieza reproducibles
- Considerar aspectos éticos en el tratamiento de datos
📋 Lo que necesitas saber ANTES de empezar¶
- Conceptos básicos de pandas y visualización
- Idea general de qué son los datos faltantes
- Curiosidad por entender patrones en la calidad de datos
🔍 Parte 1: Setup y Carga de Datos¶
📋 CONTEXTO DE NEGOCIO (CRISP-DM: Business Understanding)
🔗 Referencias oficiales:
- Kaggle Data Cleaning - Handling Missing Values
- Pandas Documentation
- Matplotlib Documentation
- Seaborn Documentation
- Scikit-learn Documentation
🏠 Caso de negocio:
- Problema: El dataset Ames Housing tiene datos faltantes y outliers que afectan las predicciones de precios
- Objetivo: Detectar patrones de missing data y outliers para limpiar el dataset
- Variables: SalePrice, LotArea, YearBuilt, GarageArea, Neighborhood, HouseStyle, etc.
- Valor para el negocio: Datos más limpios = predicciones de precios más confiables
# === SETUP DEL ENTORNO ===
# 1. Importar librerías necesarias
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
from sklearn.model_selection import train_test_split
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
import warnings
from pandas.api.types import is_numeric_dtype
warnings.filterwarnings('ignore')
print("✅ Todas las librerías importadas correctamente")
# 3. Configurar visualizaciones
plt.style.use('_______') # estilo visual (ej: 'seaborn-v0_8', 'default', 'classic')
sns.set_palette("_______") # paleta de colores (ej: 'husl', 'Set1', 'viridis')
plt.rcParams['figure.figsize'] = (12, 8)
plt.rcParams['font.size'] = 12
print("🎨 Configuración de visualizaciones lista!")
💡 PISTAS:
- 🎨 Estilos de matplotlib
- 🌈 Paletas de seaborn
- 💭 ¿Qué estilo se ve más profesional para análisis de datos?
🏠 Paso 2: Cargar y Crear Missing Data Sintético¶
# === CARGAR DATASET AMES HOUSING ===
# 1. Cargar dataset base
!curl -L -o ames-housing-dataset.zip https://www.kaggle.com/api/v1/datasets/download/shashanknecrothapa/ames-housing-dataset
!unzip ames-housing-dataset.zip
df = pd.read_csv('AmesHousing.csv')
print("🏠 DATASET: Ames Housing")
print(f" 📊 Forma original: {df.shape}")
print(f" 📋 Columnas: {list(df.columns)}")
# 2. Crear missing data sintético para práctica
np.random.seed(42) # para reproducibilidad
# Simular MCAR en Year Built (8% missing aleatorio)
# "Los valores faltan al azar: que falte un Year Built no depende de la edad ni del propio Year Built"
missing_year = np.random.random(len(df)) < 0.08
df.loc[missing_year, 'Year Built'] = np._____
# Simular MAR en Garage Area (missing relacionado con Garage Type)
# "Los faltantes de Garage Area se concentran en ciertos tipos de garaje (variable observada)"
df.loc[df['Garage Type'] == 'None', 'Garage Area'] = df.loc[df['Garage Type'] == 'None', 'Garage Area'].sample(frac=0.7, random_state=42)
# Simular MNAR en SalePrice (missing relacionado con precio alto)
# "Los faltantes dependen del propio valor: quienes tienen precios altos no reportan precio"
high_price = df['SalePrice'] > df['SalePrice'].quantile(0.85)
df.loc[high_price, 'SalePrice'] = df.loc[high_price, 'SalePrice'].sample(frac=0.2, random_state=42)
print("\n🔍 Missing data sintético creado:")
print(df._____().sum()) # método para contar valores faltantes por columna
💡 PISTAS:
- ❓ ¿Qué valor se usa para representar datos faltantes en pandas? (np.nan, None, 'missing')
- 🔢 ¿Qué método cuenta valores faltantes? Documentación
📊 Paso 3: Análisis Inicial del Dataset¶
# === EXPLORACIÓN BÁSICA ===
# 1. Información general del dataset
print("=== INFORMACIÓN GENERAL ===")
print(df._____()) # método que muestra tipos de datos, memoria y valores no nulos
# 2. Estadísticas descriptivas
print("\n=== ESTADÍSTICAS DESCRIPTIVAS ===")
print(df._____()) # método que calcula estadísticas descriptivas
# 3. Tipos de datos
print("\n=== TIPOS DE DATOS ===")
print(df._____) # atributo que muestra tipos de datos por columna
# 4. Verificar missing data
print("\n=== MISSING DATA POR COLUMNA ===")
missing_count = df._____().sum() # contar valores faltantes
missing_pct = (missing_count / len(df)) * 100 # calcular porcentaje
missing_stats = pd.DataFrame({
'Column': df.columns,
'Missing_Count': missing_count,
'Missing_Percentage': missing_pct
})
print(missing_stats[missing_stats['Missing_Count'] > 0])
# 5. Análisis de memoria
print("\n=== ANÁLISIS DE MEMORIA ===")
total_bytes = df._____(deep=True).sum() # método para memoria en bytes
print(f"Memoria total del DataFrame: {total_bytes / (1024**2):.2f} MB")
print(f"Memoria por columna:")
for col in df.columns:
memory_usage = df[col]._____() # método para memoria de una columna
print(f" {col}: {memory_usage / 1024:.2f} KB")
# 6. Análisis de duplicados
print("\n=== ANÁLISIS DE DUPLICADOS ===")
duplicates = df._____() # método para detectar filas duplicadas
print(f"Número de filas duplicadas: {duplicates.sum()}")
if duplicates.sum() > 0:
print("Primeras 5 filas duplicadas:")
print(df[df._____()].head()) # método para filtrar duplicados
💡 PISTAS:
- ℹ️ ¿Qué método da información sobre tipos y memoria? Documentación
- 📊 ¿Qué método calcula estadísticas descriptivas? Documentación
- 🔢 ¿Qué atributo muestra tipos de datos? Documentación
- ❓ ¿Qué método cuenta valores faltantes? Documentación
- 💾 ¿Qué método calcula uso de memoria? Documentación
- 🔍 ¿Qué método detecta filas duplicadas? Documentación
🔍 Paso 4: Detección de Patrones de Missing Data¶
# === ANÁLISIS DE PATRONES DE MISSING DATA ===
# 1. Filtrar solo columnas con missing data para visualización
missing_columns = df.columns[df._____().any()].tolist() # método para detectar missing
print(f"Columnas con missing data: {len(missing_columns)}")
print(f"Columnas: {missing_columns}")
# 2. Visualización mejorada sin missingno
plt.subplot(1, 1, 1)
if len(missing_columns) > 0:
# Crear estadísticas de missing solo para columnas con missing data
missing_count = df[missing_columns]._____().sum() # método para contar missing
missing_pct = (missing_count / len(df)) * 100 # calcular porcentaje
missing_stats_filtered = pd.DataFrame({
'Column': missing_columns,
'Missing_Count': missing_count,
'Missing_Percentage': missing_pct
}).sort_values('Missing_Percentage', ascending=False).head(10)
# Crear gráfico de barras más limpio
bars = plt._____(range(len(missing_stats_filtered)), missing_stats_filtered['Missing_Percentage'],
color='steelblue', alpha=0.7, edgecolor='black', linewidth=0.5) # función para barras
plt.title('Top 10: Porcentaje de Missing por Columna', fontsize=14, fontweight='bold')
plt._____(range(len(missing_stats_filtered)), missing_stats_filtered['Column'],
rotation=45, ha='right') # función para etiquetas del eje X
plt.ylabel('Porcentaje de Missing (%)')
plt.grid(True, alpha=0.3, axis='y')
# Agregar valores en las barras
for i, bar in enumerate(bars):
height = bar.get_height()
plt.text(bar.get_x() + bar.get_width()/2., height + 0.5,
f'{height:.1f}%', ha='center', va='bottom', fontsize=10)
else:
plt.text(0.5, 0.5, 'No hay missing data', ha='center', va='center', fontsize=16)
plt.title('Porcentaje de Missing por Columna', fontsize=14, fontweight='bold')
# Distribución de missing por fila
plt.show()
plt.subplot(1, 1, 1)
missing_per_row = df._____().sum(axis=1) # contar missing por fila
plt._____(missing_per_row, bins=range(0, missing_per_row.max()+2), alpha=0.7,
edgecolor='black', color='lightcoral') # función para histograma
plt.title('Distribución de Missing por Fila', fontsize=14, fontweight='bold')
plt.xlabel('Número de valores faltantes por fila')
plt.ylabel('Frecuencia')
plt.grid(True, alpha=0.3, axis='y')
plt.tight_layout()
!mkdir -p results/visualizaciones
plt.savefig('results/visualizaciones/missing_patterns.png', dpi=300, bbox_inches='tight')
plt.show()
💡 PISTAS:
- ❓ ¿Qué método detecta valores faltantes? Documentación
- 📊 ¿Qué función de matplotlib crea barras? Documentación
- 🏷️ ¿Qué función establece etiquetas del eje X? Documentación
- 📈 ¿Qué función crea histogramas? Documentación
- 🔢 ¿Cómo contar missing por fila? (axis=1 para filas, axis=0 para columnas)
🧠 Paso 5: Clasificar Tipos de Missing Data¶
# === CLASIFICACIÓN MCAR/MAR/MNAR ===
print("=== ANÁLISIS DE TIPOS DE MISSING ===")
# 1. Year Built: ¿MCAR o MAR?
print("\n1. YEAR BUILT - Análisis de patrones:")
year_missing = df['Year Built']._____() # método para detectar missing
print("Missing Year Built por Neighborhood:")
print(df.groupby('Neighborhood')['Year Built'].apply(lambda x: x._____().sum())) # contar missing por grupo
print("Missing Year Built por House Style:")
print(df.groupby('House Style')['Year Built'].apply(lambda x: x._____().sum()))
# 2. Garage Area: ¿MAR?
print("\n2. GARAGE AREA - Análisis de patrones:")
print("Missing Garage Area por Garage Type:")
print(df.groupby('Garage Type')['Garage Area'].apply(lambda x: x._____().sum()))
# 3. SalePrice: ¿MNAR?
print("\n3. SALEPRICE - Análisis de patrones:")
price_missing = df['SalePrice']._____()
print("Valores de SalePrice en registros con missing:")
print(df[price_missing]['SalePrice']._____()) # estadísticas descriptivas
💡 PISTAS:
- ❓ ¿Qué método detecta valores faltantes? Documentación
- 📊 ¿Qué método calcula estadísticas descriptivas? Documentación
- 🧠 MCAR: Missing Completely At Random (aleatorio)
- 🧠 MAR: Missing At Random (relacionado con variables observadas)
- 🧠 MNAR: Missing Not At Random (relacionado con el valor faltante mismo)
🚨 Paso 6: Detección de Outliers¶
# === DETECCIÓN DE OUTLIERS CON IQR ===
# "Detectar extremos usando mediana y cuartiles"
# "Cuándo usar: distribuciones asimétricas / colas pesadas / presencia de outliers"
if "Year Built" in df.columns:
df["Year Built"] = pd.to_numeric(df["Year Built"], errors="coerce")
# === DETECCIÓN DE OUTLIERS: IQR y Z-SCORE (robustas) ===
def detect_outliers_iqr(df, column, factor=1.5):
"""Outliers por IQR. Devuelve (df_outliers, lower, upper)."""
x = pd.to_numeric(df[column], errors="coerce")
x_no_na = x.dropna().astype(float).values
if x_no_na.size == 0:
# sin datos válidos
return df.iloc[[]], np.nan, np.nan
q1 = np.percentile(x_no_na, 25)
q3 = np.percentile(x_no_na, 75)
iqr = q3 - q1
lower = q1 - factor * iqr
upper = q3 + factor * iqr
mask = (pd.to_numeric(df[column], errors="coerce") < lower) | (pd.to_numeric(df[column], errors="coerce") > upper)
return df[mask], lower, upper
# Analizar outliers en columnas numéricas
numeric_columns = df._____(include=[np.number]).columns # método para seleccionar columnas numéricas
outlier_analysis = {}
for col in numeric_columns:
if not df[col]._____().all(): # método para verificar si hay missing data
outliers, lower, upper = detect_outliers_iqr(df, col)
outlier_analysis[col] = {
'count': len(outliers),
'percentage': (len(outliers) / len(df)) * 100,
'lower_bound': lower,
'upper_bound': upper
}
outlier_df = pd.DataFrame(outlier_analysis).T
print("=== ANÁLISIS DE OUTLIERS (IQR) ===")
print("Útil cuando la distribución está chueca o con colas largas")
print(outlier_df)
# Análisis adicional de outliers
print("\n=== RESUMEN DE OUTLIERS ===")
total_outliers = outlier_df['count']._____() # método para sumar outliers
print(f"Total de outliers detectados: {total_outliers}")
print(f"Porcentaje promedio de outliers: {outlier_df['percentage']._____():.2f}%") # método para calcular media
print(f"Columna con más outliers: {outlier_df['count']._____()}") # método para encontrar máximo
🔍 Paso 7: Detección de Outliers con Z-Score¶
# === DETECCIÓN DE OUTLIERS CON Z-SCORE ===
# "Cuándo usar: distribución aprox. campana y sin colas raras"
# "Regla: 3 pasos (desvios) desde el promedio = raro"
def detect_outliers_zscore(df, column, threshold=3):
"""Detectar outliers usando Z-Score - Regla: 3 desvios desde el promedio = raro"""
from scipy import stats
z_scores = np.abs(stats.zscore(df[column].dropna()))
outlier_indices = df[column].dropna().index[z_scores > threshold]
return df.loc[outlier_indices]
# Comparar métodos de detección
print("\n=== COMPARACIÓN DE MÉTODOS DE DETECCIÓN ===")
for col in ['SalePrice', 'Lot Area', 'Year Built', 'Garage Area']:
if col in df.columns and not df[col].isnull().all():
iqr_outliers = detect_outliers_iqr(df, col)
zscore_outliers = detect_outliers_zscore(df, col)
print(f"\n{col}:")
print(f" IQR outliers: {len(iqr_outliers[0])} ({len(iqr_outliers[0])/len(df)*100:.1f}%)")
print(f" Z-Score outliers: {len(zscore_outliers)} ({len(zscore_outliers)/len(df)*100:.1f}%)")
💡 PISTAS:
- 📊 IQR: Interquartile Range = Q3 - Q1
- 🚨 Outliers: Valores fuera de [Q1-1.5IQR, Q3+1.5IQR]
- 🔢 ¿Qué método selecciona columnas numéricas? Documentación
- ❓ ¿Qué método verifica missing data? Documentación
- ➕ ¿Qué método suma valores? Documentación
- 📊 ¿Qué método calcula la media? Documentación
- 🔝 ¿Qué método encuentra el máximo? Documentación
📊 Paso 8: Visualización de Outliers¶
# === VISUALIZAR OUTLIERS ===
os._____('results/visualizaciones', exist_ok=True)
cols = ['SalePrice', 'Lot Area', 'Year Built', 'Garage Area']
fig, axes = plt._____(2, 2, figsize=(15, 12)) # función para crear subplots
axes = axes._____() # método para aplanar array
for i, col in enumerate(cols):
if col not in df.columns:
axes[i].set_visible(False)
continue
# convertir a numérico de forma segura
y = pd.to_numeric(df[col], errors='coerce').dropna()
if y.empty:
axes[i].axis('off')
axes[i].text(0.5, 0.5, f"{col}: sin datos numéricos", ha='center', va='center', fontsize=11)
continue
# Boxplot usando el vector numérico (evita inferencias de dtype de seaborn)
sns._____(y=y, ax=axes[i]) # función para boxplot
axes[i].set_title(f'Outliers en {col}', fontweight='bold')
axes[i].set_ylabel(col)
# Outliers por IQR y bandas
iqr_df, lower, upper = detect_outliers_iqr(df, col)
out_vals = pd.to_numeric(iqr_df[col], errors='coerce').dropna()
if np.isfinite(lower):
axes[i].axhline(lower, linestyle='--', linewidth=1, label='Límite IQR')
if np.isfinite(upper):
axes[i].axhline(upper, linestyle='--', linewidth=1)
# Marcar outliers con un leve jitter en X para que se vean
if len(out_vals) > 0:
jitter_x = np.random.normal(loc=0, scale=0.02, size=len(out_vals))
axes[i]._____(jitter_x, out_vals, alpha=0.6, s=50, label=f'Outliers ({len(out_vals)})') # función para scatter
axes[i]._____() # método para mostrar leyenda
# Opcional: si la variable es muy sesgada, usar escala log
if col in ['Lot Area', 'SalePrice'] and y.skew() > 1:
axes[i].set_yscale('log')
axes[i].set_title(f'Outliers en {col} (escala log)', fontweight='bold')
plt._____() # función para ajustar layout
plt._____('results/visualizaciones/outliers_analysis.png', dpi=300, bbox_inches='tight') # función para guardar
plt._____() # función para mostrar gráfico
💡 PISTAS:
- 📁 ¿Cómo crear directorios? Documentación
- 📊 ¿Qué función crea subplots? Documentación
- 🔄 ¿Qué método aplana arrays? Documentación
- 📦 ¿Qué función crea boxplots? Documentación
- 🎯 ¿Qué función crea scatter plots? Documentación
- 🏷️ ¿Qué método muestra leyendas? Documentación
- 📐 ¿Qué función ajusta layout? Documentación
- 💾 ¿Qué función guarda gráficos? Documentación
- 👁️ ¿Qué función muestra gráficos? Documentación
- 🔴 Los puntos rojos son outliers detectados
🔧 Paso 9: Estrategias de Imputación¶
# === IMPLEMENTAR ESTRATEGIAS DE IMPUTACIÓN ===
# "Rellenar no es gratis; hacelo columna a columna y documentá"
# "Num: mediana (si cola pesada) / media (si ~normal)"
# "Cat: moda o 'Unknown' (+ flag si sospecha MNAR)"
def impute_missing_data(df, strategy='median'):
"""Implementar diferentes estrategias de imputación - Reglas simples de la clase"""
df_imputed = df.copy()
for col in df.columns:
if df[col].isnull().any():
if df[col].dtype in ['int64', 'float64']:
if strategy == 'mean':
df_imputed[col].fillna(df[col]._____(), inplace=True) # imputar con media
elif strategy == 'median':
df_imputed[col].fillna(df[col]._____(), inplace=True) # imputar con mediana
elif strategy == 'mode':
df_imputed[col].fillna(df[col]._____()[0], inplace=True) # imputar con moda
else:
# Para variables categóricas
df_imputed[col].fillna(df[col]._____()[0], inplace=True) # imputar con moda
return df_imputed
# Probar diferentes estrategias
strategies = ['mean', 'median', 'mode']
imputed_datasets = {}
for strategy in strategies:
imputed_datasets[strategy] = impute_missing_data(df, strategy)
print(f"Estrategia {strategy}: {imputed_datasets[strategy].isnull().sum().sum()} missing values restantes")
💡 PISTAS:
- 📊 ¿Qué método calcula la media? Documentación
- 📊 ¿Qué método calcula la mediana? Documentación
- 📊 ¿Qué método calcula la moda? Documentación
- 🔧 ¿Qué método imputa valores faltantes? Documentación
🧠 Paso 10: Imputación Inteligente por Tipo de Missing¶
def smart_imputation(df, *, impute_saleprice=True):
"""Imputación inteligente robusta a dtypes y NaN."""
df_imputed = df.copy()
# --- 0) Asegurar dtypes numéricos donde corresponde ---
for c in ["Year Built", "Garage Area", "SalePrice"]:
if c in df_imputed.columns:
df_imputed[c] = pd._____(df_imputed[c], errors="coerce") # to_numeric
# --- 1) Year Built: mediana por (Neighborhood, House Style) -> Neighborhood -> global ---
if {"Neighborhood", "House Style", "Year Built"}.issubset(df_imputed.columns):
grp_med = df_imputed._____(["Neighborhood", "House Style"])["Year Built"]._____("median") # groupby, transform
df_imputed["Year Built"] = df_imputed["Year Built"]._____(grp_med) # fillna
nb_med = df_imputed.groupby("Neighborhood")["Year Built"].transform("median")
df_imputed["Year Built"] = df_imputed["Year Built"].fillna(nb_med)
df_imputed["Year Built"] = df_imputed["Year Built"].fillna(df_imputed["Year Built"]._____( )) # median
# Año entero nullable
df_imputed["Year Built"] = df_imputed["Year Built"]._____( ).astype("Int64") # round
# --- 2) Garage Area: MNAR → indicador + 0; resto por mediana del barrio ---
if "Garage Area" in df_imputed.columns:
df_imputed["GarageArea_was_na"] = df_imputed["Garage Area"]._____( ).astype("Int8") # isna
# Si hay "Garage Cars", usarlo para inferir "sin garaje" (0 área)
if "Garage Cars" in df_imputed.columns:
no_garage_mask = (df_imputed["Garage Cars"].fillna(0) == 0) & df_imputed["Garage Area"].isna()
df_imputed.loc[no_garage_mask, "Garage Area"] = 0.0
# Para los NaN restantes: mediana por Neighborhood, luego global
if "Neighborhood" in df_imputed.columns:
med_gar = df_imputed.groupby("Neighborhood")["Garage Area"].transform("median")
df_imputed["Garage Area"] = df_imputed["Garage Area"].fillna(med_gar)
df_imputed["Garage Area"] = df_imputed["Garage Area"].fillna(df_imputed["Garage Area"].median())
# --- 3) SalePrice: mediana por Neighborhood (opcional) ---
if impute_saleprice and {"Neighborhood", "SalePrice"}.issubset(df_imputed.columns):
nb_price = df_imputed.groupby("Neighborhood")["SalePrice"].transform("median")
df_imputed["SalePrice"] = df_imputed["SalePrice"]._____(nb_price) # fillna
df_imputed["SalePrice"] = df_imputed["SalePrice"].fillna(df_imputed["SalePrice"].median())
# --- 4) Garage Type: moda global (MCAR); manejar categorías ---
if "Garage Type" in df_imputed.columns:
# evitar problemas si es category
if pd.api.types._____(df_imputed["Garage Type"]): # is_categorical_dtype
df_imputed["Garage Type"] = df_imputed["Garage Type"].astype("object")
mode_val = df_imputed["Garage Type"].dropna()._____( ) # mode
fill_val = mode_val.iloc[0] if not mode_val.empty else "Unknown"
df_imputed["Garage Type"] = df_imputed["Garage Type"]._____(fill_val) # fillna
return df_imputed
# Aplicar
df_smart_imputed = smart_imputation(df)
print("=== IMPUTACIÓN INTELIGENTE ===")
print(f"Missing restantes: {int(df_smart_imputed.isnull().sum().sum())}")
💡 PISTAS:
- 🔢 ¿Qué función convierte una columna a numérico manejando errores? Documentación
- 🧩 ¿Cómo agrupás por columnas para calcular estadísticas por grupo? Documentación
- 🔄 ¿Cómo propagás el resultado de una agregación (p. ej., mediana) a cada fila del grupo? Documentación
- 🧪 ¿Qué método usás para rellenar valores faltantes con un valor o serie? Documentación
- 📊 ¿Qué método calcula la mediana de una serie? Documentación
- 🧮 ¿Cómo redondeás valores en una serie antes de castear a entero anulable? Documentación
- ❓ ¿Qué método detecta valores faltantes en una serie? Documentación
- 📈 ¿Qué método obtiene la moda de una serie? Documentación
- 🏷️ ¿Cómo verificás si una serie es de tipo categórico? Documentación
🚫 Paso 11: Anti-Leakage Básico¶
# === ANTI-LEAKAGE BÁSICO ===
# "No espiés el examen: fit en TRAIN, transform en VALID/TEST"
# "Split: X_train / X_valid / X_test"
# "imputer.fit(X_train) → transform al resto"
from sklearn.model_selection import train_test_split
# 1. Split de datos (ANTES de imputar)
X = df.drop('SalePrice', axis=1) # features
y = df['SalePrice'] # target
X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.4, random_state=42)
X_valid, X_test, y_valid, y_test = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42)
print("=== SPLIT DE DATOS ===")
print(f"Train: {X_train.shape[0]} registros")
print(f"Valid: {X_valid.shape[0]} registros")
print(f"Test: {X_test.shape[0]} registros")
# 2. Imputar SOLO en train, luego transformar
from sklearn.impute import SimpleImputer
# Separar columnas numéricas y categóricas
numeric_columns = X_train.select_dtypes(include=[np.number]).columns.tolist()
categorical_columns = X_train.select_dtypes(include=['object']).columns.tolist()
print(f"Columnas numéricas: {len(numeric_columns)}")
print(f"Columnas categóricas: {len(categorical_columns)}")
# Crear imputers para cada tipo de dato
numeric_imputer = SimpleImputer(strategy='_____') # estrategia para numéricas
categorical_imputer = SimpleImputer(strategy='_____') # estrategia para categóricas
# Ajustar imputers SOLO con train
numeric_imputer._____(X_train[numeric_columns]) # ajustar numéricas
categorical_imputer._____(X_train[categorical_columns]) # ajustar categóricas
# Transformar todos los conjuntos
X_train_numeric = numeric_imputer._____(X_train[numeric_columns]) # transformar numéricas
X_train_categorical = categorical_imputer._____(X_train[categorical_columns]) # transformar categóricas
X_valid_numeric = numeric_imputer._____(X_valid[numeric_columns])
X_valid_categorical = categorical_imputer._____(X_valid[categorical_columns])
X_test_numeric = numeric_imputer._____(X_test[numeric_columns])
X_test_categorical = categorical_imputer._____(X_test[categorical_columns])
print("\n✅ Anti-leakage aplicado: fit solo en train, transform en todo")
💡 PISTAS:
- 🔢 ¿Qué estrategia usar para numéricas? ('median', 'mean', 'most_frequent')
- 📝 ¿Qué estrategia usar para categóricas? ('most_frequent', 'constant')
- 🎯 ¿Qué método ajusta el imputer? Documentación
- 🔄 ¿Qué método aplica las transformaciones? Documentación
- 📊 ¿Qué método separa tipos de datos? Documentación
📊 Paso 12: Análisis de Impacto de la Imputación¶
# 1) Crear df_imputed con imputación simple, robusta a dtypes
df_imputed = df.copy()
for col in df.columns:
s = df[col]
# Si es numérica o puede convertirse a numérica, imputar mediana
if is_numeric_dtype(s) or (s.dtype == "object"):
s_num = pd.to_numeric(s, errors="coerce")
if s_num.notna().any():
df_imputed[col] = s_num.fillna(s_num._____()) # imputar numéricas con mediana
continue
# Caso categórico: imputar con moda (si existe), sino "Unknown"
moda = s.dropna()._____() # imputar categóricas con moda
fill_val = moda.iloc[0] if not moda.empty else "Unknown"
df_imputed[col] = s.fillna(fill_val)
# 2) Comparar distribuciones (hist para numéricas, barras para categóricas)
fig, axes = plt.subplots(2, 3, figsize=(18, 12))
axes = axes.ravel()
cols_to_plot = ['SalePrice', 'Lot Area', 'Year Built', 'Garage Area', 'Neighborhood', 'House Style']
for i, col in enumerate(cols_to_plot):
if col not in df.columns:
axes[i].axis('off')
axes[i].set_title(f'{col} no existe', fontweight='bold')
continue
s_orig = df[col]
s_imp = df_imputed[col]
# Intentar tratar como numérico (coerce) para decidir el tipo de gráfico
s_orig_num = pd.to_numeric(s_orig, errors='coerce')
s_imp_num = pd.to_numeric(s_imp, errors='coerce')
if s_orig_num.notna().any() and s_imp_num.notna().any():
# NUMÉRICAS → hist
# Mismo rango/bins para una comparación justa
data_combined = pd.concat([s_orig_num.dropna(), s_imp_num.dropna()])
bins = np.histogram_bin_edges(data_combined, bins=20)
axes[i].hist(s_orig_num.dropna(), bins=bins, alpha=0.9, label='Original',
color='steelblue', edgecolor='black')
axes[i].hist(s_imp_num.dropna(), bins=bins, alpha=0.3, label='Imputado',
color='orange', edgecolor='black')
# Si está muy sesgada, te puede servir escala log
if col in ['Lot Area', 'SalePrice'] and s_orig_num.dropna().skew() > 1:
axes[i].set_yscale('log')
else:
# CATEGÓRICAS → barras (top-K categorías para legibilidad)
K = 12
vc_orig = s_orig.astype('object').fillna('Missing').value_counts().head(K)
vc_imp = s_imp.astype('object').fillna('Missing').value_counts().head(K)
cats = list(dict.fromkeys(list(vc_orig.index) + list(vc_imp.index)))[:K] # unión ordenada
vc_orig = vc_orig.reindex(cats, fill_value=0)
vc_imp = vc_imp.reindex(cats, fill_value=0)
x = np.arange(len(cats))
w = 0.4
axes[i].bar(x - w/2, vc_orig.values, width=w, label='Original')
axes[i].bar(x + w/2, vc_imp.values, width=w, label='Imputado')
axes[i].set_xticks(x)
axes[i].set_xticklabels(cats, rotation=30, ha='right')
axes[i].set_title(f'Distribución de {col}', fontweight='bold')
axes[i].legend()
axes[i].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('results/visualizaciones/distribution_comparison.png', dpi=300, bbox_inches='tight')
plt.show()
# 3) Correlaciones (solo numéricas y con coerción segura)
important_cols = ['SalePrice', 'Lot Area', 'Year Built', 'Garage Area', 'Overall Qual', 'Gr Liv Area', 'Total Bsmt SF']
available_cols = [c for c in important_cols if c in df.columns]
print(f"Columnas seleccionadas para correlaciones: {available_cols}")
df_num_original = df[available_cols].apply(pd.to_numeric, errors='coerce')
df_num_imputed = df_imputed[available_cols].apply(pd.to_numeric, errors='coerce')
fig, axes = plt.subplots(1, 2, figsize=(16, 8))
corr_original = df_num_original._____(numeric_only=True) # método para matriz de correlación
sns.heatmap(corr_original, annot=True, cmap='coolwarm', center=0, ax=axes[0],
square=True, fmt='.2f', cbar_kws={'shrink': 0.8})
axes[0].set_title('Correlaciones - Original', fontweight='bold', fontsize=14)
corr_imputed = df_num_imputed._____(numeric_only=True) # método para matriz de correlación
sns.heatmap(corr_imputed, annot=True, cmap='coolwarm', center=0, ax=axes[1],
square=True, fmt='.2f', cbar_kws={'shrink': 0.8})
axes[1].set_title('Correlaciones - Imputado', fontweight='bold', fontsize=14)
plt.tight_layout()
plt.savefig('results/visualizaciones/correlation_comparison.png', dpi=300, bbox_inches='tight')
plt.show()
# 4) Diferencias en correlaciones
print("\n=== DIFERENCIAS EN CORRELACIONES ===")
corr_diff = corr_imputed - corr_original
print("Cambios en correlaciones (Imputado - Original):")
print(corr_diff.round(3))
💡 PISTAS:
- 📊 ¿Qué método calcula la mediana? Documentación
- 📊 ¿Qué método calcula la moda? Documentación
- 📊 ¿Qué método calcula matriz de correlación? Documentación
- 📈 ¿Qué función de seaborn crea heatmaps? Documentación
🔧 Paso 13: Pipeline de Limpieza Reproducible¶
# === CREAR PIPELINE CON SKLEARN ===
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder
def create_cleaning_pipeline():
"""Crear pipeline de limpieza reproducible"""
# Definir columnas numéricas y categóricas
numeric_features = ['SalePrice', 'Lot Area', 'Year Built', 'Garage Area']
categorical_features = ['Neighborhood', 'House Style', 'Garage Type']
# Transformadores
numeric_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='_____')), # estrategia de imputación
('scaler', StandardScaler())
])
categorical_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='_____')), # estrategia de imputación
('onehot', OneHotEncoder(handle_unknown='ignore'))
])
# Combinar transformadores
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)
]
)
return preprocessor
# Crear y probar pipeline
preprocessor = create_cleaning_pipeline()
# Aplicar pipeline
X_cleaned = preprocessor._____(df) # método para aplicar transformaciones
print(f"Shape después del pipeline: {X_cleaned.shape}")
print(f"Tipo de datos: {type(X_cleaned)}")
💡 PISTAS:
- 🔧 ¿Qué estrategia usar para numéricas? ('median', 'mean', 'most_frequent')
- 🔧 ¿Qué estrategia usar para categóricas? ('most_frequent', 'constant')
- 🔧 ¿Qué método aplica las transformaciones? Documentación
🎯 Parte B: Análisis Crítico¶
📝 Preguntas de Reflexión¶
Responde estas preguntas en tu notebook:
-
¿Qué tipo de missing data identificaste en cada columna? Justifica tu clasificación.
-
¿Por qué elegiste esas estrategias de imputación específicas? ¿Qué alternativas consideraste?
-
¿Cómo podrían las decisiones de imputación afectar a diferentes grupos demográficos? (ej: barrios, tipos de vivienda)
-
¿Qué información adicional necesitarías para tomar mejores decisiones sobre los outliers?
-
¿Cómo garantizas que tu pipeline sea reproducible y transparente?
🚀 Sugerencias para Explorar Más¶
📊 Otros Datasets Interesantes¶
🏥 Datasets de Salud: - Heart Disease Dataset - Missing data en variables médicas - COVID-19 Dataset - Datos temporales con missing values - Medical Cost Dataset - Missing data en costos médicos
💰 Datasets Financieros: - Credit Card Fraud Detection - Missing data en transacciones - Loan Prediction Dataset - Missing data en información crediticia - Stock Market Data - Missing data en precios históricos
🌍 Datasets Ambientales: - Air Quality Dataset - Missing data en mediciones ambientales - Weather Dataset - Missing data en temperaturas históricas
🔧 Algoritmos Avanzados de Imputación¶
📈 Imputación Estadística Avanzada:
# KNN Imputation
from sklearn.impute import KNNImputer
knn_imputer = KNNImputer(n_neighbors=5)
# Iterative Imputation
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
iterative_imputer = IterativeImputer(random_state=42)
# Matrix Factorization
from sklearn.decomposition import TruncatedSVD
# Para datos categóricos con missing
🎯 Técnicas de Detección de Outliers Avanzadas¶
🔍 Métodos Estadísticos:
# Isolation Forest
from sklearn.ensemble import IsolationForest
isolation_forest = IsolationForest(contamination=0.1)
# Local Outlier Factor (LOF)
from sklearn.neighbors import LocalOutlierFactor
lof = LocalOutlierFactor(n_neighbors=20)
# One-Class SVM
from sklearn.svm import OneClassSVM
one_class_svm = OneClassSVM(nu=0.1)
📊 Métodos de Visualización:
# DBSCAN Clustering
from sklearn.cluster import DBSCAN
dbscan = DBSCAN(eps=0.5, min_samples=5)
# PCA para detección de outliers
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
"Los datos faltantes no son un problema técnico, son una oportunidad para entender mejor el mundo que representan."