Saltar al contenido

Indice / pandas: calidad y limpieza / 5.1

Diagnóstico antes de tocar nada

La primera pregunta ante un archivo nuevo no es «cómo lo arreglo» sino «qué tiene». Un rato midiendo ahorra una tarde corrigiendo lo que no estaba roto.

Para qué sirve

La tentación, cuando abrís un archivo, es empezar a limpiarlo enseguida: convertir esta columna, tirar aquellas filas. El problema es que no sabés qué estás tirando. Después no vas a poder contestar cuántas filas se fueron, ni por qué, ni si el resultado tiene sentido.

Diagnosticar es hacerle al archivo una serie de preguntas concretas y anotar las respuestas sin corregir nada. Recién con esos números en la mano se puede decidir. Y son varias preguntas distintas: cada una encuentra una clase de problema que las otras no ven.

ELI5Explicado facil

Es como revisar un auto usado antes de comprarlo. No empezás cambiando piezas: primero mirás todo y anotás qué encontraste - la puerta no cierra, falta la rueda de auxilio, el tablero marca 300.000 km.

Recién con esa lista decidís qué arreglar, qué dejar así y si conviene comprarlo. Si empezás a desarmar sin la lista, no sabés qué tocaste ni por qué.

Las cinco preguntas

Cada pregunta encuentra algo que las otras no
PreguntaCómo se haceQué encuentra
¿Cuántos huecos hay?df.isna().sum()Ausentes que pandas ya reconoció
¿De qué tipo es cada columna?df.info() · df.dtypesColumnas que deberían ser números y no lo son
¿Qué valores distintos hay?value_counts(dropna=False)Ausentes disfrazados y errores de carga
¿Hay filas repetidas?df.duplicated().sum()Duplicados exactos
¿Los valores tienen sentido?describe() · rangos propiosNúmeros válidos pero imposibles

El ejemplo mínimo

EjemploEl diagnostico minimo: cuantos huecos y cuantos repetidos
import pandas as pd

ventas = pd.DataFrame({
    "producto": ["Teclado", "Mouse", "Teclado", None],
    "precio": [4500.0, None, 4500.0, 900.0],
})
print(ventas)

print()
print("huecos por columna:")
print(ventas.isna().sum())

print()
print("filas duplicadas:", ventas.duplicated().sum())
print("valores distintos por columna:")
print(ventas.nunique(dropna=False))
  producto  precio
0  Teclado  4500.0
1    Mouse     NaN
2  Teclado  4500.0
3      NaN   900.0

huecos por columna:
producto    1
precio      1
dtype: int64

filas duplicadas: 1
valores distintos por columna:
producto    3
precio      3
dtype: int64

Sobre datos de verdad

Vamos con clientes_crudos.csv (8.2 kB), 120 filas y 6 columnas.

Primera mirada: info()

EjemploLo primero que se mira de un archivo real: info() y dtypes
import pandas as pd

clientes = pd.read_csv("clientes_crudos.csv")
clientes.info()

print()
print("Cada columna que deberia ser numerica y aparece como str")
print("es una alarma: hay algo que no es un numero adentro.")
<class 'pandas.DataFrame'>
RangeIndex: 120 entries, 0 to 119
Data columns (total 6 columns):
 #   Column      Non-Null Count  Dtype
---  ------      --------------  -----
 0   id          120 non-null    str  
 1   nombre      118 non-null    str  
 2   correo      118 non-null    str  
 3   edad        117 non-null    str  
 4   ciudad      119 non-null    str  
 5   fecha_alta  120 non-null    str  
dtypes: str(6)
memory usage: 5.8 KB

Cada columna que deberia ser numerica y aparece como str
es una alarma: hay algo que no es un numero adentro.

Hay dos cosas acá. Los Non-Null Count muestran que faltan datos en cuatro columnas. Y el Dtype de edad es texto: si la edad fuera un número limpio, pandas la habría leído como número. Que una columna numérica aparezca como texto es la alarma más barata que existe, y aparece sin que la busques.

Segunda mirada: contar

EjemploCuantos huecos hay, y cuantos valores distintos
import pandas as pd

clientes = pd.read_csv("clientes_crudos.csv")

resumen = pd.DataFrame({
    "nulos": clientes.isna().sum(),
    "unicos": clientes.nunique(dropna=False),
    "dtype": clientes.dtypes.astype(str),
})
print(resumen)

print()
print("filas duplicadas enteras:", clientes.duplicated().sum())
print("ids repetidos           :", clientes["id"].duplicated().sum())
            nulos  unicos dtype
id              0     120   str
nombre          2     113   str
correo          2     112   str
edad            3      57   str
ciudad          1      11   str
fecha_alta      0     110   str

filas duplicadas enteras: 0
ids repetidos           : 0

Tres ausentes en edad, dos en nombre, dos en correo, uno en ciudad. Ningún duplicado exacto y ningún id repetido: la clave está sana. Ya sabemos bastante más que hace un minuto, y todavía no tocamos nada.

Tercera mirada: qué hay adentro

Los conteos dicen cuántos, no qué. Para eso está value_counts, que es el detector de basura de pandas.

Ejemplovalue_counts es el detector de basura: mostra lo que no es numero
import pandas as pd

clientes = pd.read_csv("clientes_crudos.csv")
edad = clientes["edad"]

sospechosos = edad[~edad.str.fullmatch(r"-?\d+", na=False)]
print("valores de edad que no son numeros enteros:")
print(sospechosos.value_counts(dropna=False).to_string())

print()
print("Los NaN que ya detecto read_csv:", int(edad.isna().sum()))
print("Cada uno de los de arriba es un ausente disfrazado o un error de carga.")
valores de edad que no son numeros enteros:
edad
NaN        3
treinta    3
-          1

Los NaN que ya detecto read_csv: 3
Cada uno de los de arriba es un ausente disfrazado o un error de carga.

Ahí aparecen los que estaban escondidos: tres treinta y un guion. Ninguno de esos cuatro figura en los tres ausentes que había contado isna(), y sin embargo son huecos igual. Sin esta pregunta, esos cuatro seguían disfrazados de dato válido.

Siempre dropna=False al diagnosticar

Por defecto value_counts descarta los ausentes, que es justo lo que estás buscando. Al diagnosticar, ponelo siempre.

Lo mismo, paso a paso

Seis filas reales del archivo. Fijate cómo cada pregunta enciende celdas distintas: ninguna sola pregunta encuentra todo.

Paso a pasoUn diagnostico encuentra cosas distintas segun que pregunte
  1. Seis filas del archivo, tal como se leen. A simple vista todas parecen razonables: hay nombre, hay correo, hay edad.
    clientes = pd.read_csv("clientes_crudos.csv")
  2. isna() no encuentra nada en estas seis. Ninguna celda esta vacia. Si el diagnostico terminara aca, el archivo pareceria limpio.
    clientes.isna().sum()   # 0 en estas seis filas
  3. info() avisa que edad es de tipo texto, no numero. Esa sola linea dice que adentro hay algo que no es un numero.
    clientes.info()   # edad -> str, y deberia ser un entero
  4. value_counts sobre lo que no es un numero encuentra al culpable: la palabra treinta. Es el detector de basura mas util que hay.
    edad[~edad.str.fullmatch(r"-?\d+")].value_counts()
  5. Comparar el texto con su version normalizada muestra los que tienen espacios de mas o mayusculas raras. No estan vacios ni rotos: estan sucios.
    clientes["nombre"] != clientes["nombre"].str.strip().str.title()
  6. Una regla del negocio encuentra lo que ninguna funcion generica ve: un correo sin arroba. Ninguna estadistica lo iba a marcar.
    ~clientes["correo"].str.contains("@")
  7. Y el dominio de la columna encuentra el ultimo: 17 anios es un numero perfectamente valido que este negocio no admite. Cinco preguntas distintas, cinco problemas distintos.
    ~clientes["edad"].astype(float).between(18, 100)

Un perfil reutilizable

Estas mismas preguntas se hacen en todos los archivos, así que conviene tenerlas en una función. Esta es la base de la práctica de esta lección.

EjemploUn perfil por columna, que sirve para cualquier archivo
import pandas as pd


def perfilar(tabla):
    """Una fila por columna: lo minimo para decidir por donde empezar."""
    filas = []
    for nombre in tabla.columns:
        columna = tabla[nombre]
        no_nulos = columna.dropna()
        filas.append({
            "columna": nombre,
            "dtype": str(columna.dtype),
            "nulos": int(columna.isna().sum()),
            "unicos": int(columna.nunique(dropna=False)),
            "vacios": int((no_nulos.astype(str).str.strip() == "").sum()),
            "ejemplo": no_nulos.iloc[0] if len(no_nulos) else "",
        })
    return pd.DataFrame(filas)


clientes = pd.read_csv("clientes_crudos.csv", dtype=str, keep_default_na=False)
print(perfilar(clientes).to_string(index=False))
   columna dtype  nulos  unicos  vacios              ejemplo
        id   str      0     120       0                C0001
    nombre   str      0     113       2             José Paz
    correo   str      0     112       2 jose.paz@example.com
      edad   str      0      59       1                   62
    ciudad   str      0      11       1                Salta
fecha_alta   str      0     110       0           2025-02-20

Dónde se rompe

(sin error) el diagnóstico dice que está todo bien y no lo está

Causa: preguntaste solo por ausentes. Los valores fuera de rango, los tipos incorrectos y las reglas del negocio no aparecen en isna().

Arreglo: las cinco preguntas de la tabla de arriba, siempre las cinco. Un diagnóstico incompleto es peor que ninguno, porque te da confianza falsa.

(sin error) value_counts no muestra la celda vacía

Causa: por defecto descarta los ausentes.

Arreglo: value_counts(dropna=False).

AttributeError: Can only use .str accessor with string values

Causa: estás usando .str.algo sobre una columna que pandas leyó como numérica.

Arreglo: si querés inspeccionar el texto crudo, leé con dtype=str, keep_default_na=False. Si no, convertí con .astype(str) sabiendo que los NaN se vuelven la cadena "nan".

(sin error) duplicated() no encuentra los duplicados que ves

Causa: sin subset compara la fila entera. Dos clientes con el mismo correo pero distinto id no son filas iguales.

Arreglo: decidí qué es un duplicado en este archivo y decilo: duplicated(subset=["correo"]). Se ve en detalle en 5.5.

Acá hay que elegir

Qué anotar del diagnóstico

El diagnóstico no sirve de nada si queda en la pantalla. Como mínimo, escribí en algún lado tres números por columna: cuántos ausentes, cuántos valores únicos, y qué tipo tiene. Con eso ya podés justificar cualquier decisión que tomes después, y detectar si el archivo del mes que viene cambió.

Un perfil guardado del archivo de este mes convierte «me parece que vino distinto» en «vinieron 12 ausentes más en correo».

Recetario

Diagnóstico mínimo de cualquier archivo
ExpresiónQué devuelveCuándo
df.shape(filas, columnas)Lo primero, siempre
df.info()Imprime tipos y no nulosVista general en una línea
df.dtypesSeriesCuando querés el tipo como dato, no impreso
df.isna().sum()Ausentes por columnaSiempre
df.duplicated().sum()EnteroDuplicados exactos de fila
df.nunique(dropna=False)Valores distintos por columnaDetecta columnas constantes o casi
s.value_counts(dropna=False)Series ordenadaVer qué hay adentro de una columna
df.describe()Estadísticas de las numéricasMínimos y máximos absurdos
df.describe(include="all")También las de textoVista completa de una sola vez
df.sample(5)5 filas al azarMejor que head(): no siempre el archivo está desordenado

Ponete a prueba

1. ¿Qué devuelve df.isna().sum() sobre un DataFrame?Nivel 1 · Recordar
2. En la salida de info(), la columna edad aparece como str. ¿Qué te está diciendo?Nivel 1 · Recordar
3. Tenés esta tabla:
producto  precio
Teclado   4500.0
Mouse     NaN
Teclado   4500.0
None      900.0
¿Qué imprime df.duplicated().sum()?Nivel 2 · Predecir la salida
4. Sobre clientes_crudos.csv corrés clientes["ciudad"].value_counts(). La columna tiene una celda vacía. ¿Aparece esa celda en el resultado?Nivel 2 · Predecir la salida
5. Querés saber qué valores raros esconde la columna edad, que es de tipo texto. Las cuatro corren. ¿Cuál te lo muestra?Nivel 3 · Elegir entre dos casi correctas
6. Vas a diagnosticar un archivo. ¿Cómo conviene leerlo?Nivel 3 · Elegir entre dos casi correctas
7. Este diagnóstico corre sin errores y concluye que el archivo está limpio. El archivo tiene 59 filas con problemas. ¿Qué le falta?
clientes = pd.read_csv("clientes_crudos.csv")
print(clientes.isna().sum())
print(clientes.duplicated().sum())
print("todo en orden" if clientes.isna().sum().sum() == 0 else "revisar")
Nivel 4 · Encontrar el defecto

Prácticas de esta lección