DEV Community

Jesus Oviedo Riquelme
Jesus Oviedo Riquelme

Posted on

MLZC25-17. Preparación de Datos para Clasificación: Limpieza y Manejo de Valores Faltantes

🎯 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())
Enter fullscreen mode Exit fullscreen mode

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  
Enter fullscreen mode Exit fullscreen mode

🔍 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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

📊 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))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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']
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

✅ 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])
Enter fullscreen mode Exit fullscreen mode

Salida:

VERIFICACIÓN DE LIMPIEZA
==================================================
Valores faltantes totales ANTES: 606
Valores faltantes totales DESPUÉS: 0

✅ ¡Perfecto! No quedan valores faltantes
Enter fullscreen mode Exit fullscreen mode

📈 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())
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

🎯 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))
Enter fullscreen mode Exit fullscreen mode

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}%)")
Enter fullscreen mode Exit fullscreen mode

🔍 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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)

  1. Entender el contexto antes de imputar
   # ¿Por qué falta el dato? ¿Es significativo?
Enter fullscreen mode Exit fullscreen mode
  1. 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
Enter fullscreen mode Exit fullscreen mode
  1. Mantener datos originales
   df_clean = df.copy()  # Nunca modificar el original
Enter fullscreen mode Exit fullscreen mode
  1. Verificar resultados
   assert df_clean.isnull().sum().sum() == 0, "Aún hay valores faltantes"
Enter fullscreen mode Exit fullscreen mode

❌ DON'T (No Hacer)

  1. Eliminar filas ciegamente
   # ❌ MAL: Pierdes 606 registros valiosos
   df_bad = df.dropna()
Enter fullscreen mode Exit fullscreen mode
  1. Usar siempre la media
   # ❌ MAL: Introduce sesgo si los faltantes no son aleatorios
   df['income'] = df['income'].fillna(df['income'].mean())
Enter fullscreen mode Exit fullscreen mode
  1. Ignorar el tipo de variable
   # ❌ MAL: Tratar categóricas como numéricas
   df['industry'] = df['industry'].fillna(0)
Enter fullscreen mode Exit fullscreen mode
  1. No verificar
   # ❌ MAL: Asumir que funcionó
   # ✅ BIEN: Verificar siempre
   assert df_clean.isnull().sum().sum() == 0
Enter fullscreen mode Exit fullscreen mode

📊 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")
Enter fullscreen mode Exit fullscreen mode

🎯 Conclusión

La preparación de datos es el fundamento de cualquier proyecto de ML exitoso. En este post aprendimos:

  1. ✅ Identificar tipos de variables (categóricas vs. numéricas)
  2. ✅ Analizar patrones de valores faltantes
  3. ✅ Aplicar estrategias de imputación apropiadas
  4. ✅ 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)