🎯 Objetivo del Post: Aprenderás a preparar datos para problemas de clasificación, manejando valores faltantes de manera adecuada según el tipo de variable, y entenderás por qué una buena preparación de datos es crucial para el éxito del modelo.
🧹 La Importancia de Limpiar Datos
En el mundo real, los datos rara vez están perfectos. Son como una casa que necesita limpieza antes de recibir invitados:
❌ Datos sucios = Modelo ineficaz
- Valores faltantes (NaN, null, missing)
- Formatos inconsistentes
- Valores atípicos extremos
- Tipos de datos incorrectos
✅ Datos limpios = Modelo robusto
- Sin valores faltantes
- Formatos consistentes
- Outliers tratados apropiadamente
- Tipos de datos correctos
Regla de Oro: Un modelo de ML solo puede ser tan bueno como los datos con los que se entrena. ¡Garbage in, garbage out!
📥 Carga del Dataset
Comencemos cargando nuestro dataset de lead scoring:
import pandas as pd
import numpy as np
# Cargar el dataset
url = "https://raw.githubusercontent.com/alexeygrigorev/datasets/master/course_lead_scoring.csv"
df = pd.read_csv(url)
# Información básica
print(f"Forma del dataset: {df.shape}")
print(f"\nPrimeras filas:")
print(df.head())
Salida esperada:
Forma del dataset: (1462, 9)
Primeras filas:
lead_source industry number_of_courses_viewed annual_income \
0 paid_ads NaN 1 79450.0
1 social_media retail 1 46992.0
2 events healthcare 5 78796.0
employment_status location interaction_count lead_score converted
0 unemployed south_america 4 0.94 1
1 employed south_america 1 0.80 0
2 unemployed australia 3 0.69 1
🔍 Paso 1: Identificar Tipos de Variables
Antes de manejar valores faltantes, debemos identificar qué tipo de variable es cada columna:
# Información detallada del dataset
print("INFORMACIÓN DEL DATASET")
print("=" * 50)
df.info()
Salida:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 1462 entries, 0 to 1461
Data columns (total 9 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 lead_source 1334 non-null object ← CATEGÓRICA
1 industry 1328 non-null object ← CATEGÓRICA
2 number_of_courses_viewed 1462 non-null int64 ← NUMÉRICA
3 annual_income 1281 non-null float64 ← NUMÉRICA
4 employment_status 1362 non-null object ← CATEGÓRICA
5 location 1399 non-null object ← CATEGÓRICA
6 interaction_count 1462 non-null int64 ← NUMÉRICA
7 lead_score 1462 non-null float64 ← NUMÉRICA
8 converted 1462 non-null int64 ← OBJETIVO
dtypes: float64(2), int64(3), object(4)
memory usage: 102.9+ KB
Separar Variables por Tipo
# Identificar columnas categóricas y numéricas automáticamente
categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
numerical_cols = df.select_dtypes(include=['int64', 'float64']).columns.tolist()
print("\nVARIABLES CATEGÓRICAS:")
print("=" * 40)
print(categorical_cols)
print(f"Total: {len(categorical_cols)} variables")
print("\nVARIABLES NUMÉRICAS:")
print("=" * 40)
print(numerical_cols)
print(f"Total: {len(numerical_cols)} variables")
Salida:
VARIABLES CATEGÓRICAS:
========================================
['lead_source', 'industry', 'employment_status', 'location']
Total: 4 variables
VARIABLES NUMÉRICAS:
========================================
['number_of_courses_viewed', 'annual_income', 'interaction_count',
'lead_score', 'converted']
Total: 5 variables
📊 Paso 2: Analizar Valores Faltantes
Ahora identifiquemos cuántos valores faltantes tiene cada variable:
# Contar valores faltantes
print("\nVALORES FALTANTES POR COLUMNA")
print("=" * 60)
missing_info = pd.DataFrame({
'Columna': df.columns,
'Valores Faltantes': df.isnull().sum(),
'Porcentaje': (df.isnull().sum() / len(df)) * 100,
'Tipo': df.dtypes
})
# Filtrar solo columnas con valores faltantes
missing_info = missing_info[missing_info['Valores Faltantes'] > 0]
missing_info = missing_info.sort_values('Valores Faltantes', ascending=False)
print(missing_info.to_string(index=False))
Salida:
VALORES FALTANTES POR COLUMNA
============================================================
Columna Valores Faltantes Porcentaje Tipo
annual_income 181 12.380301 float64
industry 134 9.165527 object
lead_source 128 8.755130 object
employment_status 100 6.839945 object
location 63 4.309166 object
Interpretación de los Resultados
Variable | Faltantes | % | Tipo | Acción |
---|---|---|---|---|
annual_income |
181 | 12.4% | Numérica | Reemplazar con 0.0 |
industry |
134 | 9.2% | Categórica | Reemplazar con 'NA' |
lead_source |
128 | 8.8% | Categórica | Reemplazar con 'NA' |
employment_status |
100 | 6.8% | Categórica | Reemplazar con 'NA' |
location |
63 | 4.3% | Categórica | Reemplazar con 'NA' |
🔧 Paso 3: Estrategia de Manejo de Valores Faltantes
¿Por Qué Diferentes Estrategias?
Para Variables Categóricas → 'NA'
Razón: Los valores faltantes son información valiosa
# Ejemplo: industry faltante
# Lead sin industry especificada → Puede ser nuevo en el mercado laboral
# o no quiso compartir esa información
# Tratamiento:
# NaN → 'NA' (Nueva categoría)
Ventajas:
- ✅ Preserva la información de que el dato faltaba
- ✅ Permite al modelo aprender patrones de datos faltantes
- ✅ 'NA' se convierte en una categoría más durante one-hot encoding
Ejemplo:
# Antes
industry: ['technology', NaN, 'finance', NaN, 'retail']
# Después
industry: ['technology', 'NA', 'finance', 'NA', 'retail']
Para Variables Numéricas → 0.0
Razón: Representa la ausencia de información de manera neutral
# Ejemplo: annual_income faltante
# Lead no proporcionó información de ingresos
# Tratamiento:
# NaN → 0.0 (Valor neutral)
Ventajas:
- ✅ No introduce sesgos artificiales (la media sí lo haría)
- ✅ Mantiene el rango de valores [0, max]
- ✅ Compatible con algoritmos que requieren valores numéricos
Alternativas y cuándo NO usar 0:
- ❌ Media/Mediana: Puede introducir sesgo si los datos faltantes no son aleatorios
- ❌ Interpolación: Útil en series de tiempo, no en datos transversales
- ✅ 0.0: Cuando los valores faltantes son significativos y 0 no distorsiona el rango
💻 Paso 4: Implementar el Manejo de Valores Faltantes
Opción 1: Método Manual
# Crear una copia del dataframe para no modificar el original
df_clean = df.copy()
# Reemplazar valores faltantes en columnas categóricas
for col in categorical_cols:
df_clean[col] = df_clean[col].fillna('NA')
print(f"✓ Reemplazados {df[col].isnull().sum()} valores faltantes en '{col}' con 'NA'")
# Reemplazar valores faltantes en columnas numéricas (excepto 'converted')
for col in numerical_cols:
if col != 'converted': # No tocar la variable objetivo
df_clean[col] = df_clean[col].fillna(0.0)
print(f"✓ Reemplazados {df[col].isnull().sum()} valores faltantes en '{col}' con 0.0")
Salida:
✓ Reemplazados 128 valores faltantes en 'lead_source' con 'NA'
✓ Reemplazados 134 valores faltantes en 'industry' con 'NA'
✓ Reemplazados 100 valores faltantes en 'employment_status' con 'NA'
✓ Reemplazados 63 valores faltantes en 'location' con 'NA'
✓ Reemplazados 181 valores faltantes en 'annual_income' con 0.0
✓ Reemplazados 0 valores faltantes en 'number_of_courses_viewed' con 0.0
✓ Reemplazados 0 valores faltantes en 'interaction_count' con 0.0
✓ Reemplazados 0 valores faltantes en 'lead_score' con 0.0
Opción 2: Método con Función Reutilizable
def limpiar_valores_faltantes(df, categorical_cols, numerical_cols, target_col='converted'):
"""
Limpia valores faltantes del dataframe.
Args:
df: DataFrame original
categorical_cols: Lista de columnas categóricas
numerical_cols: Lista de columnas numéricas
target_col: Nombre de la variable objetivo (no se modifica)
Returns:
DataFrame limpio
"""
df_clean = df.copy()
# Categóricas: reemplazar con 'NA'
for col in categorical_cols:
n_missing = df_clean[col].isnull().sum()
if n_missing > 0:
df_clean[col] = df_clean[col].fillna('NA')
print(f"✓ {col}: {n_missing} valores → 'NA'")
# Numéricas: reemplazar con 0.0 (excepto target)
for col in numerical_cols:
if col != target_col:
n_missing = df_clean[col].isnull().sum()
if n_missing > 0:
df_clean[col] = df_clean[col].fillna(0.0)
print(f"✓ {col}: {n_missing} valores → 0.0")
return df_clean
# Usar la función
df_clean = limpiar_valores_faltantes(df, categorical_cols, numerical_cols)
✅ Paso 5: Verificar la Limpieza
# Verificar que no quedan valores faltantes
print("\nVERIFICACIÓN DE LIMPIEZA")
print("=" * 50)
print(f"Valores faltantes totales ANTES: {df.isnull().sum().sum()}")
print(f"Valores faltantes totales DESPUÉS: {df_clean.isnull().sum().sum()}")
# Detalle por columna
if df_clean.isnull().sum().sum() == 0:
print("\n✅ ¡Perfecto! No quedan valores faltantes")
else:
print("\n⚠️ Aún hay valores faltantes:")
print(df_clean.isnull().sum()[df_clean.isnull().sum() > 0])
Salida:
VERIFICACIÓN DE LIMPIEZA
==================================================
Valores faltantes totales ANTES: 606
Valores faltantes totales DESPUÉS: 0
✅ ¡Perfecto! No quedan valores faltantes
📈 Paso 6: Comparar Datos Antes y Después
# Comparar distribuciones
print("\nCOMPARACIÓN DE DISTRIBUCIONES")
print("=" * 60)
# Ejemplo con 'industry'
print("\nDistribución de 'industry' ANTES:")
print(df['industry'].value_counts(dropna=False))
print("\nDistribución de 'industry' DESPUÉS:")
print(df_clean['industry'].value_counts())
Salida:
COMPARACIÓN DE DISTRIBUCIONES
============================================================
Distribución de 'industry' ANTES:
retail 203
finance 200
other 198
healthcare 187
education 187
technology 179
manufacturing 174
NaN 134 ← Valores faltantes
Name: industry, dtype: int64
Distribución de 'industry' DESPUÉS:
retail 203
finance 200
other 198
healthcare 187
education 187
technology 179
manufacturing 174
NA 134 ← Ahora son una categoría
Name: industry, dtype: int64
🎯 Análisis de Variables Específicas
Variable Categórica: industry
# Analizar 'industry'
print("ANÁLISIS DE 'industry'")
print("=" * 50)
# Frecuencias
industry_freq = df_clean['industry'].value_counts()
print(f"\nCategorías únicas: {df_clean['industry'].nunique()}")
print(f"\nDistribución:")
print(industry_freq)
# Porcentajes
print(f"\nPorcentajes:")
print(round(df_clean['industry'].value_counts(normalize=True) * 100, 2))
Variable Numérica: annual_income
# Analizar 'annual_income'
print("\nANÁLISIS DE 'annual_income'")
print("=" * 50)
# Estadísticas descriptivas
print("\nEstadísticas (sin contar los 0.0 imputados):")
income_no_zero = df_clean[df_clean['annual_income'] > 0]['annual_income']
print(income_no_zero.describe())
# Ver cuántos son 0.0 (imputados)
n_imputados = (df_clean['annual_income'] == 0.0).sum()
print(f"\nValores imputados con 0.0: {n_imputados} ({n_imputados/len(df_clean)*100:.1f}%)")
🔍 Paso 7: Exploración de Patrones en Datos Faltantes
¿Los valores faltantes son aleatorios o hay un patrón?
# Crear columna indicadora de valores faltantes originales
df_analysis = df.copy()
df_analysis['industry_missing'] = df['industry'].isnull().astype(int)
df_analysis['income_missing'] = df['annual_income'].isnull().astype(int)
# Ver si hay relación entre datos faltantes y conversión
print("\nRELACIÓN ENTRE DATOS FALTANTES Y CONVERSIÓN")
print("=" * 60)
print("\nTasa de conversión cuando 'industry' está presente vs. faltante:")
conversion_by_industry_missing = df_analysis.groupby('industry_missing')['converted'].mean()
print(conversion_by_industry_missing)
print("\nTasa de conversión cuando 'annual_income' está presente vs. faltante:")
conversion_by_income_missing = df_analysis.groupby('income_missing')['converted'].mean()
print(conversion_by_income_missing)
Interpretación:
Tasa de conversión cuando 'industry' está presente vs. faltante:
industry_missing
0 0.51 ← Cuando industry está presente
1 0.48 ← Cuando industry falta
Name: converted, dtype: float64
Tasa de conversión cuando 'annual_income' está presente vs. faltante:
income_missing
0 0.52 ← Cuando income está presente
1 0.45 ← Cuando income falta
Name: converted, dtype: float64
Conclusión: Los leads con información faltante tienen tasas de conversión ligeramente menores, ¡por eso es importante mantener esta información con 'NA' en lugar de eliminar las filas!
💡 Mejores Prácticas en Preparación de Datos
✅ DO (Hacer)
- Entender el contexto antes de imputar
# ¿Por qué falta el dato? ¿Es significativo?
- Documentar decisiones
# Razón: annual_income faltante → lead no quiso compartir info económica
# Decisión: Imputar con 0.0 para representar ausencia de información
- Mantener datos originales
df_clean = df.copy() # Nunca modificar el original
- Verificar resultados
assert df_clean.isnull().sum().sum() == 0, "Aún hay valores faltantes"
❌ DON'T (No Hacer)
- Eliminar filas ciegamente
# ❌ MAL: Pierdes 606 registros valiosos
df_bad = df.dropna()
- Usar siempre la media
# ❌ MAL: Introduce sesgo si los faltantes no son aleatorios
df['income'] = df['income'].fillna(df['income'].mean())
- Ignorar el tipo de variable
# ❌ MAL: Tratar categóricas como numéricas
df['industry'] = df['industry'].fillna(0)
- No verificar
# ❌ MAL: Asumir que funcionó
# ✅ BIEN: Verificar siempre
assert df_clean.isnull().sum().sum() == 0
📊 Resumen del Código Completo
import pandas as pd
import numpy as np
# 1. Cargar datos
url = "https://raw.githubusercontent.com/alexeygrigorev/datasets/master/course_lead_scoring.csv"
df = pd.read_csv(url)
# 2. Identificar tipos de variables
categorical_cols = df.select_dtypes(include=['object']).columns.tolist()
numerical_cols = df.select_dtypes(include=['int64', 'float64']).columns.tolist()
# 3. Crear copia limpia
df_clean = df.copy()
# 4. Imputar valores faltantes
# Categóricas → 'NA'
for col in categorical_cols:
df_clean[col] = df_clean[col].fillna('NA')
# Numéricas → 0.0 (excepto 'converted')
for col in numerical_cols:
if col != 'converted':
df_clean[col] = df_clean[col].fillna(0.0)
# 5. Verificar
print(f"Valores faltantes: {df_clean.isnull().sum().sum()}")
assert df_clean.isnull().sum().sum() == 0, "Error: Quedan valores faltantes"
print("✅ Datos preparados correctamente")
🎯 Conclusión
La preparación de datos es el fundamento de cualquier proyecto de ML exitoso. En este post aprendimos:
- ✅ Identificar tipos de variables (categóricas vs. numéricas)
- ✅ Analizar patrones de valores faltantes
- ✅ Aplicar estrategias de imputación apropiadas
- ✅ Verificar la calidad de los datos limpios
Puntos clave:
- 🎯 Variables categóricas → 'NA' (preserva información)
- 🎯 Variables numéricas → 0.0 (valor neutral)
- 🎯 Verificar siempre el resultado
- 🎯 Mantener una copia de los datos originales
En el próximo post (MLZC25-18), exploraremos estos datos limpios con análisis exploratorio, visualizaciones y cálculo de correlaciones para entender mejor nuestras variables antes de modelar.
¿Qué otras estrategias de imputación conoces? ¿Has tenido que lidiar con datos muy sucios en tu trabajo? ¡Comparte tu experiencia!
Top comments (0)