Lección 9 de 29Módulo 2: Pandas - Manipulación de Datos

09. Limpieza de Datos

Maneja valores faltantes, duplicados y datos inconsistentes

25 minutos

Aprende a manejar valores faltantes, duplicados y datos inconsistentes. La limpieza de datos consume el 60-80% del tiempo de un analista profesional.

Valores faltantes (NaN/None)

Detectar valores nulos

import pandas as pd
import numpy as np

# Dataset con valores faltantes
df = pd.DataFrame({
    'producto': ['Laptop', 'Mouse', 'Teclado', None, 'Monitor'],
    'precio': [800, 25, np.nan, 300, 450],
    'stock': [15, np.nan, 30, 20, None],
    'categoria': ['Computadoras', 'Accesorios', 'Accesorios', 'Monitores', 'Monitores']
})

print("Dataset original:")
print(df)

# Detectar nulos por columna
print("\nValores nulos por columna:")
print(df.isnull().sum())

# Porcentaje de nulos
print("\nPorcentaje de nulos:")
print((df.isnull().sum() / len(df)) * 100)

# Verificar si hay algún nulo
print(f"\n¿Hay valores nulos?: {df.isnull().any().any()}")

# Filas con al menos un nulo
filas_con_nulos = df[df.isnull().any(axis=1)]
print(f"\nFilas con valores nulos:\n{filas_con_nulos}")

Eliminar valores nulos

# Eliminar filas con ANY nulo
df_sin_nulos = df.dropna()
print(f"Filas después de dropna(): {len(df_sin_nulos)}")

# Eliminar filas solo si TODAS las columnas son nulas
df_limpio = df.dropna(how='all')

# Eliminar filas con nulos en columnas específicas
df_limpio = df.dropna(subset=['precio'])

# Eliminar columnas con nulos
df_sin_col = df.dropna(axis=1)

# Eliminar filas con más de N nulos
df_limpio = df.dropna(thresh=3)  # Al menos 3 valores no-nulos

Rellenar valores nulos

# Rellenar con un valor específico
df_filled = df.fillna(0)

# Rellenar por columna
df['precio'] = df['precio'].fillna(df['precio'].mean())
df['stock'] = df['stock'].fillna(df['stock'].median())
df['producto'] = df['producto'].fillna('Desconocido')

# Forward fill (usar valor anterior)
df['precio'] = df['precio'].ffill()

# Backward fill (usar valor siguiente)
df['precio'] = df['precio'].bfill()

# Rellenar con diccionario (valores diferentes por columna)
valores_relleno = {
    'precio': 0,
    'stock': 10,
    'producto': 'N/A'
}
df_filled = df.fillna(valores_relleno)

Interpolación

# Interpolar valores numéricos
ventas = pd.Series([100, np.nan, np.nan, 150, np.nan, 200])
ventas_interpoladas = ventas.interpolate()
print(ventas_interpoladas)
# 0    100.0
# 1    116.7  # Interpolado
# 2    133.3  # Interpolado
# 3    150.0
# 4    175.0  # Interpolado
# 5    200.0

Datos duplicados

Detectar duplicados

# Dataset con duplicados
ventas = pd.DataFrame({
    'fecha': ['2024-01-15', '2024-01-16', '2024-01-15', '2024-01-17'],
    'producto': ['Laptop', 'Mouse', 'Laptop', 'Teclado'],
    'cantidad': [2, 10, 2, 5],
    'precio': [800, 25, 800, 60]
})

# Detectar filas duplicadas
duplicados = ventas.duplicated()
print(f"Filas duplicadas:\n{duplicados}")

# Ver las filas duplicadas
print(f"\nFilas duplicadas completas:\n{ventas[duplicados]}")

# Número de duplicados
print(f"\nTotal duplicados: {duplicados.sum()}")

# Duplicados basados en columnas específicas
duplicados_producto = ventas.duplicated(subset=['producto', 'precio'])

Eliminar duplicados

# Eliminar duplicados (mantener primer ocurrencia)
df_unique = ventas.drop_duplicates()

# Mantener última ocurrencia
df_unique = ventas.drop_duplicates(keep='last')

# Eliminar todas las ocurrencias (incluyendo la primera)
df_unique = ventas.drop_duplicates(keep=False)

# Duplicados basados en columnas específicas
df_unique = ventas.drop_duplicates(subset=['producto'])

Limpieza de strings

Métodos str de Pandas

# Dataset con strings sucios
productos = pd.DataFrame({
    'nombre': [
        '  Laptop Pro  ',
        'mouse wireless',
        'TECLADO MECÁNICO',
        'Monitor 27"',
        '  tablet  10"  '
    ],
    'categoria': [
        'Computadoras ',
        ' accesorios',
        'ACCESORIOS',
        'monitores',
        'Tablets '
    ]
})

# Eliminar espacios al inicio/final
productos['nombre'] = productos['nombre'].str.strip()
productos['categoria'] = productos['categoria'].str.strip()

# Convertir a minúsculas
productos['categoria'] = productos['categoria'].str.lower()

# Convertir a mayúsculas
# productos['categoria'] = productos['categoria'].str.upper()

# Title case (primera letra mayúscula)
productos['nombre'] = productos['nombre'].str.title()

print("Productos limpios:")
print(productos)

# Reemplazar texto
productos['nombre'] = productos['nombre'].str.replace('"', ' pulgadas')

# Eliminar caracteres especiales
productos['nombre'] = productos['nombre'].str.replace(r'[^\w\s]', '', regex=True)

Conversión de tipos de datos

# Dataset con tipos incorrectos
datos = pd.DataFrame({
    'fecha': ['2024-01-15', '2024-01-16', '2024-01-17'],
    'precio': ['100.50', '200.75', '150.25'],
    'cantidad': ['10', '20', '15'],
    'activo': ['True', 'False', 'True']
})

print("Tipos originales:")
print(datos.dtypes)

# Convertir a datetime
datos['fecha'] = pd.to_datetime(datos['fecha'])

# Convertir a numérico
datos['precio'] = pd.to_numeric(datos['precio'])
datos['cantidad'] = pd.to_numeric(datos['cantidad'], errors='coerce')

# Convertir a booleano
datos['activo'] = datos['activo'] == 'True'

# Convertir a categorical (eficiente para datos repetitivos)
# datos['categoria'] = datos['categoria'].astype('category')

print("\nTipos convertidos:")
print(datos.dtypes)

Manejo de outliers

# Dataset con outliers
ventas = pd.DataFrame({
    'producto': ['A']*100,
    'precio': np.concatenate([
        np.random.normal(100, 10, 95),  # Datos normales
        [500, 600, 700, 800, 900]       # Outliers
    ])
})

# Método 1: IQR (Interquartile Range)
Q1 = ventas['precio'].quantile(0.25)
Q3 = ventas['precio'].quantile(0.75)
IQR = Q3 - Q1

limite_inferior = Q1 - 1.5 * IQR
limite_superior = Q3 + 1.5 * IQR

outliers = ventas[
    (ventas['precio'] < limite_inferior) |
    (ventas['precio'] > limite_superior)
]
print(f"Outliers detectados: {len(outliers)}")

# Eliminar outliers
ventas_limpio = ventas[
    (ventas['precio'] >= limite_inferior) &
    (ventas['precio'] <= limite_superior)
]

# Método 2: Z-score
from scipy import stats
z_scores = np.abs(stats.zscore(ventas['precio']))
ventas_limpio = ventas[z_scores < 3]  # Mantener solo |z| < 3

# Método 3: Percentiles
p95 = ventas['precio'].quantile(0.95)
ventas_limpio = ventas[ventas['precio'] <= p95]

Proyecto práctico: Limpieza completa

# Dataset realista con problemas de calidad
np.random.seed(42)

datos_sucios = pd.DataFrame({
    'cliente_id': ['C001', 'C002', 'C001', None, 'C003', 'C002', 'C004'],
    'nombre': [
        '  Juan Pérez  ',
        'MARÍA GARCÍA',
        '  Juan Pérez  ',
        'carlos lópez',
        'Ana Martínez',
        'MARÍA GARCÍA',
        None
    ],
    'email': [
        'juan@email.com',
        'MARIA@EMAIL.COM',
        'juan@email.com',
        'carlos@',
        'ana@email.com',
        'maria@email.com',
        'luis@email.com'
    ],
    'edad': ['28', '32', '28', 'N/A', '45', '32', '150'],
    'compra': [100.50, np.nan, 100.50, 80.00, None, 95.00, 250.00],
    'fecha': [
        '2024-01-15',
        '15/01/2024',
        '2024-01-15',
        '2024-01-16',
        '01-17-2024',
        '2024-01-18',
        '2024-01-19'
    ]
})

print("="*70)
print("DATOS ORIGINALES (SUCIOS)")
print("="*70)
print(datos_sucios)
print(f"\nProblemas detectados:")
print(f"  - Valores nulos: {datos_sucios.isnull().sum().sum()}")
print(f"  - Duplicados: {datos_sucios.duplicated().sum()}")

# Proceso de limpieza
print("\n" + "="*70)
print("PROCESO DE LIMPIEZA")
print("="*70)

# 1. Copiar dataset original
datos_limpios = datos_sucios.copy()

# 2. Limpiar strings
print("\n1️⃣ Limpiando strings...")
datos_limpios['nombre'] = datos_limpios['nombre'].str.strip()
datos_limpios['nombre'] = datos_limpios['nombre'].str.title()
datos_limpios['email'] = datos_limpios['email'].str.lower().str.strip()

# 3. Manejar valores inválidos en edad
print("2️⃣ Limpiando edades...")
datos_limpios['edad'] = pd.to_numeric(datos_limpios['edad'], errors='coerce')
datos_limpios.loc[datos_limpios['edad'] > 120, 'edad'] = np.nan
datos_limpios['edad'] = datos_limpios['edad'].fillna(datos_limpios['edad'].median())

# 4. Validar emails
print("3️⃣ Validando emails...")
email_valido = datos_limpios['email'].str.contains('@.*\.', na=False, regex=True)
datos_limpios.loc[~email_valido, 'email'] = np.nan

# 5. Unificar formato de fechas
print("4️⃣ Normalizando fechas...")
datos_limpios['fecha'] = pd.to_datetime(datos_limpios['fecha'], format='mixed', errors='coerce')

# 6. Rellenar valores nulos en compra
print("5️⃣ Rellenando valores de compra...")
datos_limpios['compra'] = datos_limpios['compra'].fillna(datos_limpios['compra'].median())

# 7. Rellenar nombres y cliente_id nulos
print("6️⃣ Manejando nulos en identificadores...")
datos_limpios['nombre'] = datos_limpios['nombre'].fillna('Cliente Desconocido')
datos_limpios['cliente_id'] = datos_limpios['cliente_id'].fillna('CXXX')

# 8. Eliminar duplicados exactos
print("7️⃣ Eliminando duplicados...")
datos_limpios = datos_limpios.drop_duplicates()

print("\n" + "="*70)
print("DATOS LIMPIOS")
print("="*70)
print(datos_limpios)
print(f"\nMejoras logradas:")
print(f"  - Valores nulos restantes: {datos_limpios.isnull().sum().sum()}")
print(f"  - Duplicados eliminados: {len(datos_sucios) - len(datos_limpios)}")
print(f"  - Formatos estandarizados: ✅")

# 9. Reporte de calidad de datos
print("\n" + "="*70)
print("REPORTE DE CALIDAD DE DATOS")
print("="*70)

print(f"\nCompletitud por columna:")
for col in datos_limpios.columns:
    completitud = (1 - datos_limpios[col].isnull().sum() / len(datos_limpios)) * 100
    print(f"  {col}: {completitud:.1f}%")

print(f"\nEstadísticas de edad:")
print(datos_limpios['edad'].describe())

print(f"\nEstadísticas de compra:")
print(datos_limpios['compra'].describe())

Función reutilizable de limpieza

def limpiar_dataset(df):
    """
    Función genérica para limpieza básica de datos.
    """
    print("🧹 Iniciando limpieza de datos...")

    df_limpio = df.copy()
    reporte = {}

    # 1. Eliminar duplicados
    duplicados_antes = df_limpio.duplicated().sum()
    df_limpio = df_limpio.drop_duplicates()
    reporte['duplicados_eliminados'] = duplicados_antes

    # 2. Limpiar strings
    columnas_string = df_limpio.select_dtypes(include=['object']).columns
    for col in columnas_string:
        df_limpio[col] = df_limpio[col].str.strip()

    # 3. Convertir tipos numéricos donde sea posible
    for col in df_limpio.columns:
        try:
            df_limpio[col] = pd.to_numeric(df_limpio[col], errors='ignore')
        except:
            pass

    # 4. Reportar nulos
    nulos_por_col = df_limpio.isnull().sum()
    reporte['columnas_con_nulos'] = nulos_por_col[nulos_por_col > 0].to_dict()

    print("✅ Limpieza completada")
    print(f"\nReporte:")
    print(f"  Duplicados eliminados: {reporte['duplicados_eliminados']}")
    print(f"  Columnas con nulos: {len(reporte['columnas_con_nulos'])}")

    return df_limpio, reporte

# Usar la función
df_limpio, reporte = limpiar_dataset(datos_sucios)

Buenas prácticas

1. Siempre trabajar con una copia

# ❌ Mal: modificar original
df_original.fillna(0, inplace=True)

# ✅ Bien: trabajar con copia
df_limpio = df_original.copy()
df_limpio = df_limpio.fillna(0)

2. Documentar decisiones de limpieza

# Crear log de limpieza
limpieza_log = {
    'fecha_limpieza': pd.Timestamp.now(),
    'registros_originales': len(df_original),
    'registros_finales': len(df_limpio),
    'duplicados_eliminados': len(df_original) - len(df_limpio),
    'estrategia_nulos': 'Median para numéricos, Unknown para categóricos',
    'outliers_removidos': 5
}

import json
with open('limpieza_log.json', 'w') as f:
    json.dump(limpieza_log, f, indent=2, default=str)

3. Validar después de limpiar

def validar_limpieza(df):
    """Validar que la limpieza fue exitosa"""
    assert df.isnull().sum().sum() == 0, "❌ Aún hay valores nulos"
    assert df.duplicated().sum() == 0, "❌ Aún hay duplicados"
    assert len(df) > 0, "❌ DataFrame vacío"
    print("✅ Validación exitosa")

validar_limpieza(df_limpio)

Resumen de la lección

dropna() elimina valores nulos, fillna() los rellena ✅ drop_duplicates() elimina filas duplicadas ✅ Métodos str limpian y estandarizan strings ✅ Detectar outliers con IQR, Z-score o percentiles ✅ Siempre trabajar con copias del dataset original ✅ Documentar decisiones de limpieza para reproducibilidad


🎯 Próxima lección: 10. Transformación de Datos

En la siguiente lección aprenderás apply, map, merge y reshaping de DataFrames. 🚀

¿Completaste esta lección?

Marca esta lección como completada. Tu progreso se guardará en tu navegador.