Lección 9 de 29•Mó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.