AulaML Python · C++
Python intermedio · 42 min · Python para ciencia de datos

Limpieza de datos

Nulos, duplicados, tipos y valores extremos: decidir con criterio y dejar constancia

Limpiar datos es decidir qué hacer con lo que falta, lo que sobra y lo que no encaja, y dejar registro de cada decisión. Aprenderás a perfilar un archivo, tratar nulos y duplicados, corregir tipos y fechas, detectar valores extremos y empaquetar todo en un pipeline que puedas repetir.

Lección 3 de 5 del módulo
Al terminar esta lección podrás
  • Perfilar una tabla y cuantificar nulos, duplicados y tipos incorrectos
  • Elegir entre eliminar, imputar o marcar valores ausentes según el contexto
  • Convertir columnas de texto a números, fechas y categorías sin perder datos
  • Detectar valores extremos con el rango intercuartílico y decidir qué hacer con ellos
  • Escribir una función de limpieza reproducible que registre sus decisiones

Antes necesitas: Pandas: Series y DataFrames

En esta lección
  1. ¿Qué es la limpieza de datos?
  2. ¿Para qué sirve?
  3. ¿Cómo funciona por dentro?
  4. Sintaxis: las operaciones esenciales
  5. Ejemplo básico: perfilar antes de tocar
  6. Ejemplo intermedio: tipos, texto y nulos
  7. Caso práctico: un pipeline que deja rastro
  8. Visualización: un valor extremo en la tabla
  9. Errores comunes
  10. Buenas prácticas
  11. Ejercicios
  12. Soluciones explicadas
  13. Resumen
  14. Evaluación

Abres el archivo de pedidos del mes y encuentras tres problemas en la primera pantalla: una fecha escrita como texto, un monto vacío y dos filas que parecen la misma compra. Nada de eso impide que pandas cargue la tabla, y ahí está el peligro: el programa sigue adelante y produce un promedio que nadie debería creer.

Limpiar no es borrar filas hasta que el código deje de quejarse. Es decidir qué representa cada hueco, qué filas son duplicados reales y qué valores son errores de captura, y anotar esas decisiones para que otro pueda revisarlas.

¿Qué es la limpieza de datos?

La limpieza de datos es el conjunto de operaciones que convierten una tabla cruda en una tabla utilizable: tipos correctos, identificadores sin repetir, fechas comparables, texto con un formato uniforme y valores ausentes tratados de forma explícita. No es un paso estético; cada transformación cambia lo que el análisis puede afirmar.

La idea que ordena todo el proceso es la unidad de observación. Antes de tocar nada, decide qué representa una fila: un pedido, un cliente, un día de ventas. Si una fila es un pedido, dos pedidos idénticos en monto y fecha pueden ser legítimos; si es un cliente, el mismo nombre dos veces es un problema. Sin esa respuesta, drop_duplicates es una apuesta.

¿Para qué sirve?

  • Evitar conclusiones falsas: un promedio calculado sobre montos en texto o con ceros de relleno no mide lo que crees.
  • Preparar Machine Learning: los modelos no aceptan NaN ni fechas como texto, y una categoría escrita de tres formas distintas se convierte en tres columnas inútiles.
  • Unir tablas: si una clave tiene espacios o mayúsculas inconsistentes, el merge pierde filas en silencio.
  • Cumplir reglas de negocio: montos negativos, edades de 200 años o correos sin arroba son errores que hay que aislar.
  • Repetir el análisis: un pipeline escrito permite volver a limpiar el archivo del mes siguiente sin empezar de cero.

¿Cómo funciona por dentro?

Pandas representa lo ausente con NaN en columnas numéricas y con None o NaT en texto y fechas. Para contar y tratar huecos de forma uniforme existen isna, dropna y fillna, que reconocen todas esas formas. Un detalle importante: NaN nunca es igual a sí mismo, así que df[df["monto"] == np.nan] devuelve una tabla vacía; hay que preguntar con isna().

No todas las ausencias significan lo mismo. Un monto vacío porque el cliente no pagó es distinto de un monto vacío porque el sistema no lo registró. La estadística distingue tres casos: ausencia totalmente aleatoria, ausencia explicada por otra variable y ausencia ligada al propio valor. Solo en el primer caso imputar la media es defendible; en los otros, conviene añadir una columna que marque el hueco antes de rellenarlo.

El orden de las operaciones también importa, y casi siempre es el mismo: normalizar texto, convertir tipos, tratar duplicados, decidir nulos, revisar extremos y verificar. Si deduplicas antes de normalizar, "Lima " y "lima" seguirán siendo dos clientes distintos. Si imputas antes de convertir a número, el relleno se calculó sobre texto y no representa ninguna cantidad.

ProblemaHerramientaQué comprobar antes
Huecos aisladosdropna(subset=[...])¿Cuántas filas se pierden y de qué columna?
Huecos con valor de rellenofillna(valor)¿La media o mediana se calculó antes de imputar?
Filas repetidasdrop_duplicates(subset=[...])¿Qué columnas identifican una observación?
Texto inconsistente.str.strip().str.lower()¿Hay tildes, guiones o abreviaturas?
Extremosquantile y clip¿Es un error de captura o un caso real?

Sintaxis: las operaciones esenciales

La limpieza cabe en un puñado de métodos de pandas, que se instala con pip install pandas. La clave está en el argumento que los hace específicos: subset decide qué columnas cuentan para un duplicado o un nulo.

PYTHON operaciones.py
import pandas as pd

# Nulos: contar, eliminar por columna clave, rellenar
df.isna().sum()
df = df.dropna(subset=["monto"])
df["ciudad"] = df["ciudad"].fillna("Sin dato")

# Duplicados: detectar y conservar el primero
df.duplicated(subset=["pedido"]).sum()
df = df.drop_duplicates(subset=["pedido"], keep="first")

# Tipos y fechas
df["monto"] = pd.to_numeric(df["monto"], errors="coerce")
df["fecha"] = pd.to_datetime(df["fecha"], errors="coerce", dayfirst=True)

# Texto
df["ciudad"] = df["ciudad"].str.strip().str.title()

Ejemplo básico: perfilar antes de tocar

El primer paso siempre es medir. Este DataFrame pequeño imita lo que suele traer un archivo exportado: un monto vacío, una fila repetida y una fecha en texto.

PYTHON perfil.py
import pandas as pd

df = pd.DataFrame({
    "cliente": ["A", "B", "C", "A"],
    "monto": [120.0, None, 95.0, 120.0],
    "fecha": ["2026-01-05", "2026-01-07", "2026-02-11", "2026-01-05"],
})

print(df.shape)
print(df.isna().sum().to_dict())
print(df.duplicated().sum())
print(df["fecha"].dtype)

shape devuelve (4, 3): cuatro filas y tres columnas. isna().sum() cuenta un único nulo, en monto. duplicated() marca una fila como repetida, la última, porque es idéntica a la primera. Y dtype de la fecha es object, es decir texto: aunque parezca una fecha, pandas no puede ordenarla ni calcular diferencias con ella.

Con esos cuatro números ya tienes el diagnóstico. Lo que sigue no es una receta automática: cada línea responde a una pregunta concreta sobre este archivo, no sobre los archivos en general.

Ejemplo intermedio: tipos, texto y nulos

Ahora corrige el archivo anterior. Primero convierte, después normaliza, luego decide qué hacer con el hueco y por último elimina la fila repetida. Observa que el orden no es casual.

PYTHON limpiar_archivo.py
import pandas as pd

df["fecha"] = pd.to_datetime(df["fecha"], errors="coerce")
df["cliente"] = df["cliente"].str.strip().str.upper()
df["monto"] = df["monto"].fillna(df["monto"].median())
df = df.drop_duplicates(subset=["cliente", "fecha"], keep="first")

print(df["monto"].tolist())
print(df["monto"].sum())
print(df.shape)

La mediana de 120 y 95 es 107.5, así que el hueco se rellena con ese valor. Tras eliminar la fila repetida de A quedan tres filas con montos [120.0, 107.5, 95.0] y un total de 322.5. La fecha ya es de tipo datetime64, lo que permite agrupar por mes sin trucos de texto.

Hay una decisión discutible en la línea 5: imputar la mediana supone que el pedido de B valía algo parecido a los demás. Si el monto faltante fuera el total de una compra grande, esa suposición sería falsa y el total quedaría subestimado. Imputar siempre es una hipótesis; lo mínimo es escribirla.

Caso práctico: un pipeline que deja rastro

Un informe se repite cada mes. En lugar de recordar las decisiones, escríbelas en una función que reciba la tabla, devuelva una nueva y añada cada decisión a una lista de registro. Así el número final viaja con su explicación.

PYTHON pipeline.py
import pandas as pd

def limpiar(df, registro):
    df = df.copy()
    df["ciudad"] = df["ciudad"].str.strip().str.title()
    nulos = int(df["ciudad"].isna().sum())
    df["ciudad"] = df["ciudad"].fillna("Sin dato")
    registro.append(f"ciudad: {nulos} nulos imputados")

    antes = len(df)
    df = df.drop_duplicates(subset=["pedido"], keep="first")
    registro.append(f"duplicados: {antes - len(df)} filas eliminadas")
    return df

registro = []
pedidos = pd.read_csv("pedidos.csv")
limpio = limpiar(pedidos, registro)
print(limpio.shape)
print(registro)

Con seis pedidos donde una ciudad está vacía y dos filas repiten el mismo identificador, el resultado tiene cinco filas y el registro contiene dos líneas: un nulo imputado y una fila eliminada. La función no imprime nada por dentro; devuelve el resultado y deja que quien la llama decida qué mostrar. Esa separación permite probarla con datos pequeños y reutilizarla en el informe del mes siguiente.

Visualización: un valor extremo en la tabla

La matriz muestra el gasto mensual de cinco clientes. A simple vista hay una celda que no encaja: 4200 soles en enero del cliente C, cuando todos los demás montos están entre 95 y 210. Ese es el tipo de valor que arrastra una media y arruina un gráfico de barras.

Mapa de datos ·Gasto mensual por cliente (soles)
5 × 3 rango [95, 4200] Quince montos. El de C en enero rompe el patrón del resto.
Ene
Feb
Mar
cliente A
120
135
128
cliente B
95
110
102
cliente C
4200
130
125
cliente D
210
198
205
cliente E
150
160
155

Recorre los pasos para ver cómo cambian las celdas.

Antes de recortar, pregúntate qué es ese 4200. Si fue una compra corporativa real, recortarla borra información valiosa; si fue un cero de más al digitar, corregirlo es obligatorio. La regla del rango intercuartílico solo señala candidatos, no decide por ti.

Ejecución visual ·Convertir, normalizar, rellenar
1 / 6
1 import pandas as pd
2
3 df = pd.DataFrame({"monto": ["120", "95", None, "4200"], "ciudad": ["Lima", "lima ", "Arequipa", None]})
4 df["monto"] = pd.to_numeric(df["monto"], errors="coerce")
5 df["ciudad"] = df["ciudad"].str.strip().str.title()
6 df["ciudad"] = df["ciudad"].fillna("Sin dato")
7 print(df["monto"].median())
Se carga pandas con el alias habitual.
Variables
pd = módulo pandas
Salida
—

Avanza paso a paso: observa la línea resaltada, las variables y la salida.

Errores comunes

SíntomaCausaSolución
fillna no cambia la tablaNo asignaste el resultadodf["x"] = df["x"].fillna(v)
SettingWithCopyWarningEscribes sobre una selecciónTrabaja con .copy() o con .loc
Quedan duplicados tras limpiarEspacios o mayúsculas distintasNormaliza el texto antes de deduplicar
ValueError: Cannot mask with non-boolean array...str.contains encuentra nulosAñade na=False
Las fechas se ordenan malSe leyeron como texto o con formato ambiguoUsa parse_dates y, si hace falta, dayfirst=True

El error que más caro sale no produce ninguna excepción: eliminar filas de más. dropna() sin argumentos borra toda fila con al menos un hueco en cualquier columna, incluidas las que no usas para el análisis.

Eliminar nulos sin matar el informe
Incorrecto
PYTHON Borra demasiado
df = df.dropna()
print(len(df))
Correcto
PYTHON Decide qué columna importa
df = df.dropna(subset=["monto"])
print(len(df))

Sin `subset`, cualquier nulo en cualquier columna elimina la fila, aunque ese campo sea opcional. Con `subset` decides qué dato es imprescindible y conservas el resto. Antes de ejecutarlo, compara el número de filas con el original.

Buenas prácticas

  • Perfila primero: shape, dtypes, isna().sum() y duplicated().sum() antes de transformar.
  • Normaliza el texto antes de deduplicar o de unir tablas; los espacios invisibles rompen las claves.
  • Prefiere fillna con un valor explicable y documentado antes que con la media por defecto.
  • Separa «valor ausente» de «valor cero»: un monto desconocido no es un cliente que no gastó.
  • Antes de recortar extremos, cuéntalos y míralos uno por uno; muchos son la parte interesante del negocio.
  • Devuelve siempre tablas nuevas y anota filas de entrada y de salida en cada paso.

Ejercicios

Ejercicio 1

Diagnóstico y limpieza mínima

Con una tabla de seis pedidos donde ciudad tiene dos nulos y hay una fila repetida, muestra cuántas filas, nulos y duplicados hay. Después normaliza ciudad con strip y title, rellena los nulos con "Sin dato" y elimina los duplicados por la columna pedido. Comprueba que quedan cinco filas y ningún nulo.
Pista

Cuenta los nulos con isna().sum().sum() y los duplicados con duplicated().sum(). Para quedarte con la primera aparición usa keep="first".

Completar código Completa la limpieza
df["ciudad"] = df["ciudad"].str.strip().str.()
df["monto"] = df["monto"].fillna(df["monto"].())
df = df.(subset=["pedido"])

El método que pone la inicial en mayúscula es el mismo que usarías con un título. Para el relleno, elige una medida que no se deje arrastrar por los extremos.

Ejercicio 2

Detectar extremos con el rango intercuartílico

Usa los quince montos de la matriz: 120, 135, 128, 95, 110, 102, 4200, 130, 125, 210, 198, 205, 150, 160 y 155. Calcula el primer cuartil, el tercero, el rango intercuartílico y el límite superior. Recorta los valores por encima de ese límite con clip y compara el total antes y después.
Pista

quantile(0.25) y quantile(0.75) usan interpolación lineal por defecto. clip(upper=limite) no elimina filas: cambia el valor de las que se pasan.

Soluciones explicadas

En el primer ejercicio, el orden importa. Deduplicar por pedido funciona porque el identificador es un número, pero si la clave fuera el nombre del cliente, "Ana Pérez" y "ana perez" pasarían como dos clientes distintos. Normalizar antes de comparar cierra ese hueco y deja categorías comparables.

En el segundo, el IQR no dice que 4200 sea un error, solo que es atípico respecto al resto. La decisión depende del negocio: una venta corporativa se conserva y se analiza aparte; un cero de más se corrige con el valor original. En ambos casos el paso obligatorio es el mismo: mirar el valor, no solo el número que devuelve la fórmula.

Resumen

  • Limpiar es decidir y documentar, no borrar filas hasta que el código no proteste.
  • Perfila con shape, dtypes, isna().sum() y duplicated().sum() antes de transformar.
  • dropna(subset=...) elimina solo cuando falta un dato imprescindible; sin subset borra de más.
  • fillna imputa, y toda imputación es una hipótesis que debe quedar escrita.
  • Normaliza texto antes de deduplicar o unir: espacios y mayúsculas crean duplicados falsos.
  • El IQR señala candidatos a extremo; la decisión de recortar o conservar es del negocio.
  • Un pipeline en funciones con registro permite repetir el informe y auditar cada cambio.

Evaluación

Evaluación 1

¿Qué hace df.dropna() sin argumentos?

Evaluación 2

Tienes df["monto"].fillna(0) sin asignar el resultado. ¿Qué ocurre?

Evaluación 3

¿Por qué conviene normalizar el texto antes de eliminar duplicados?

Tus notas

¿Ya lo entendiste?

Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.