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
| Pregunta | Cómo se hace | Qué encuentra |
|---|---|---|
| ¿Cuántos huecos hay? | df.isna().sum() | Ausentes que pandas ya reconoció |
| ¿De qué tipo es cada columna? | df.info() · df.dtypes | Columnas 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 propios | Números válidos pero imposibles |
El ejemplo mínimo
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 , 120 filas y 6 columnas.
Primera mirada: info()
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
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.
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.
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.
- 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") - 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 - 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 - 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() - 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() - 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("@") - 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.
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
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.
Causa: por defecto descarta los ausentes.
Arreglo: value_counts(dropna=False).
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".
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
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
| Expresión | Qué devuelve | Cuándo |
|---|---|---|
df.shape | (filas, columnas) | Lo primero, siempre |
df.info() | Imprime tipos y no nulos | Vista general en una línea |
df.dtypes | Series | Cuando querés el tipo como dato, no impreso |
df.isna().sum() | Ausentes por columna | Siempre |
df.duplicated().sum() | Entero | Duplicados exactos de fila |
df.nunique(dropna=False) | Valores distintos por columna | Detecta columnas constantes o casi |
s.value_counts(dropna=False) | Series ordenada | Ver qué hay adentro de una columna |
df.describe() | Estadísticas de las numéricas | Mínimos y máximos absurdos |
df.describe(include="all") | También las de texto | Vista completa de una sola vez |
df.sample(5) | 5 filas al azar | Mejor que head(): no siempre el archivo está desordenado |