Saltar al contenido

lección 8

EDA: el primer vistazo a un dataset

Los primeros diez minutos con un dataset desconocido: shape, dtypes, describe, nulos e histograma. El checklist que te dice si puedes confiar en los datos antes de analizarlos.

45 min

### El médico que ausculta antes de operar

Imagina que llegas a un hospital y el cirujano te dice: "Perfecto, vamos al quirófano". Sin mirarte, sin hacerte preguntas, sin una radiografía. Te levantarías de la camilla y saldrías corriendo. Pues exactamente eso es lo que hacen muchos analistas novatos con los datos: reciben un fichero (archivo), abren el editor, y empiezan a calcular métricas de negocio sin haber comprobado primero si los datos están sanos.

EDA son las siglas de Exploratory Data Analysis, o análisis exploratorio de datos: el reconocimiento médico que le haces a un dataset antes de confiar en él. No es un informe bonito para enseñárselo a nadie. Es un proceso privado, rápido y sistemático que responde a una sola pregunta: ¿puedo confiar en estos datos para tomar una decisión, o hay algo que debería preocuparme?

El término lo popularizó John Tukey, que repartió su carrera entre Bell Labs y la Universidad de Princeton — donde dirigió el departamento de estadística que ayudó a crear — y que estaba harto de que sus colegas saltaran directamente a la hipótesis formal sin mirar primero los datos con sus propios ojos. Llevaba defendiendo esa forma de trabajar desde 1962, y en 1977 lo dejó por escrito en un libro cuyo primer capítulo se abre así: "el análisis exploratorio de datos es trabajo de detective: trabajo de detective numérico, o de detective de recuentos, o de detective gráfico". Primero miras, exploras, te sorprendes. Solo después confirmas. Esa filosofía es la que vas a aprender hoy, traducida a los diez minutos de pandas que ejecutas cada vez que te llega un dataset nuevo.

Y hay una razón práctica para esto: si no haces el EDA primero, el error lo descubrirás más tarde — cuando ya hayas montado un informe, se lo hayas enseñado a tu jefa, y ella te pregunte "¿por qué el ticket medio de la tienda de Cuenca sale en 45.000 euros?" Y entonces tendrás que volver al principio, avergonzado, y descubrir que una columna tenía el precio en céntimos en vez de euros. El EDA te ahorra esa vergüenza.

### El checklist de los 10 minutos

Lo que vas a aprender no es una herramienta ni un método sofisticado. Es una RUTINA: un checklist que ejecutas siempre igual, como un piloto antes de despegar. No importa si el dataset tiene 500 filas o 5 millones; si viene de un CSV exportado de un ERP o de una API; si lo pide el director financiero o la becaria de marketing. El checklist es el mismo, y se ejecuta en este orden porque cada paso depende del anterior.

Estos seis comandos se ejecutan en orden. Lo que descubres en uno te prepara para interpretar el siguiente.

### Paso 1: .shape — ¿cuánto hay aquí?

Es lo primero que miras: cuántas filas y cuántas columnas tiene el dataset. Parece trivial, pero la respuesta ya te dice cosas. Si esperabas un fichero con las ventas de todo 2024 y shape te dice que hay 200 filas, algo falta. Si esperabas 10 columnas y hay 47, alguien te ha dado un volcado completo sin filtrar. shape es la primera pista de que estás ante lo que crees que estás.

1import pandas as pd
2
3df = pd.DataFrame({
4 'cliente_id': [1, 2, 3, 4, 5],
5 'nombre': ['Ana Garcia', 'Luis Martinez', 'Marta Lopez', 'Pedro Ruiz', 'Sara Diaz'],
6 'email': ['ana@mail.com', 'luis@mail.com', None, 'pedro@mail.com', 'sara@mail.com'],
7 'edad': [28, 35, 42, None, 31],
8 'gasto_total': ['1200.50', '340.00', '5600.75', '120.30', '890.00']
9})
10
11print(df.shape)
12print(f"Filas: {df.shape[0]}, Columnas: {df.shape[1]}")

.shape se escribe sin paréntesis porque es un atributo, no un método. Devuelve una tupla: (filas, columnas)

### Paso 2: .dtypes y .info() — ¿los tipos dicen la verdad?

Después de saber cuánto hay, miras DE QUÉ TIPO es cada columna. Aquí es donde aparece el error más común y más peligroso del análisis de datos: una columna que parece numérica pero que pandas ha leído como texto. Si gasto_total dice str — o object en versiones anteriores de pandas — en vez de float64, cualquier suma, media o comparación que hagas sobre ella será INCORRECTA o directamente dará error. Y lo peor: a veces no da error — simplemente concatena textos en vez de sumar números, y el resultado parece un número pero no lo es.

1import pandas as pd
2
3df = pd.DataFrame({
4 'cliente_id': [1, 2, 3, 4, 5],
5 'nombre': ['Ana Garcia', 'Luis Martinez', 'Marta Lopez', 'Pedro Ruiz', 'Sara Diaz'],
6 'email': ['ana@mail.com', 'luis@mail.com', None, 'pedro@mail.com', 'sara@mail.com'],
7 'edad': [28, 35, 42, None, 31],
8 'gasto_total': ['1200.50', '340.00', '5600.75', '120.30', '890.00']
9})
10
11print(df.dtypes)
12print()
13print("--- INFO ---")
14df.info()

.dtypes muestra el tipo de cada columna; .info() añade el conteo de no-nulos y el uso de memoria

El error más caro del EDA: no comprobar los tipos. Si una columna de precios llega como texto (str, o object en versiones anteriores), pandas NO te avisa al hacer operaciones sobre ella — simplemente devuelve resultados absurdos o lanza un TypeError inesperado tres pasos más adelante. Siempre comprueba dtypes ANTES de hacer cualquier cálculo. Si ves str u object donde esperabas un número, hay que convertir con .astype(float) o pd.to_numeric() antes de seguir.

### Paso 3: .isna().sum() — ¿dónde están los huecos?

Un dataset sin nulos es como un piso sin goteras: existe en teoría, pero en la práctica siempre hay algún hueco. Lo importante no es que haya nulos — es CUÁNTOS hay y DÓNDE están. Una columna con el 2% de nulos es normal y manejable. Una columna con el 80% de nulos es prácticamente inútil. Y una columna que DEBERÍA estar completa y tiene nulos (como el email de un cliente registrado) te está contando una historia sobre un proceso de captura que falla.

1import pandas as pd
2
3df = pd.DataFrame({
4 'cliente_id': [1, 2, 3, 4, 5],
5 'nombre': ['Ana Garcia', 'Luis Martinez', 'Marta Lopez', 'Pedro Ruiz', 'Sara Diaz'],
6 'email': ['ana@mail.com', 'luis@mail.com', None, 'pedro@mail.com', 'sara@mail.com'],
7 'edad': [28, 35, 42, None, 31],
8 'gasto_total': ['1200.50', '340.00', '5600.75', '120.30', '890.00']
9})
10
11# Conteo de nulos por columna
12nulos = df.isna().sum()
13print(nulos)
14print()
15
16# Porcentaje de nulos (mas util con datasets grandes)
17porcentaje = (df.isna().sum() / len(df) * 100).round(1)
18print("Porcentaje de nulos:")
19print(porcentaje)

.isna().sum() cuenta los huecos; dividir entre len(df) da el porcentaje

Consejo de senior: cuando encuentro nulos en un EDA, me hago tres preguntas. Primera: ¿son nulos "legítimos" (el dato no existe, como la fecha de baja de un cliente activo) o son nulos "accidentales" (el dato existe pero no se capturó)? Segunda: ¿el porcentaje es tan alto que la columna no sirve? Más del 50% y ya es sospechosa. Tercera: ¿si borro las filas con nulos, me queda una muestra representativa o estoy eliminando un segmento entero? Nunca borres nulos sin responder estas tres.

### Paso 4: .describe() — ¿los números tienen sentido?

Aquí es donde empieza el trabajo de detective. .describe() te da los estadísticos básicos de cada columna numérica: la media, la desviación estándar, el mínimo, los cuartiles y el máximo. Y lo que buscas no es memorizar esos números — es encontrar el que NO cuadra. Un máximo de 350 años en la columna edad. Un mínimo negativo en la columna precio. Una media que está lejísimos de la mediana (señal de valores extremos que tiran del promedio). Esos son los sospechosos.

1import pandas as pd
2
3ventas = pd.DataFrame({
4 'pedido_id': [1, 2, 3, 4, 5, 6, 7, 8],
5 'importe': [45.0, 120.0, 89.0, 34.0, 9999.0, 67.0, 52.0, 73.0],
6 'unidades': [2, 5, 3, 1, 1, 4, 2, 3],
7 'descuento': [0.0, 0.1, 0.05, 0.0, 0.0, 0.15, 0.0, 0.1]
8})
9
10print(ventas.describe())

.describe() resume las columnas numéricas: cuenta, media, desviación, min, cuartiles y max

Un describe sano tiene media y mediana cercanas. Si se separan mucho, busca el culpable en el min o el max.

### Paso 5: .nunique() — ¿cuánta variedad tiene cada columna?

La cardinalidad de una columna (cuántos valores distintos tiene) te dice CÓMO se comporta ese campo. Una columna con 3 valores únicos es categórica: "estado_pedido" con valores pendiente, enviado, entregado. Una columna con tantos valores únicos como filas es un identificador: "pedido_id". Y una columna que debería tener 47 provincias pero tiene 847 valores únicos tiene un problema de normalización — alguien escribió "Madrid", "madrid", "MADRID" y "Madird" como cuatro cosas distintas.

1import pandas as pd
2
3df = pd.DataFrame({
4 'pedido_id': [101, 102, 103, 104, 105, 106],
5 'cliente': ['Ana', 'Luis', 'Ana', 'Marta', 'Luis', 'Ana'],
6 'provincia': ['Madrid', 'madrid', 'MADRID', 'Barcelona', 'Valencia', 'Madird'],
7 'estado': ['enviado', 'pendiente', 'entregado', 'enviado', 'pendiente', 'enviado']
8})
9
10print(df.nunique())

.nunique() revela que provincia tiene 5 valores únicos cuando debería tener 3 (variantes del mismo nombre)

### Paso 6: .head() y .sample() — mirar filas con tus propios ojos

Los pasos anteriores te dan resúmenes estadísticos. Pero hay cosas que ningún resumen te cuenta: que el campo "nombre" tiene datos como "TEST_USER_001", que el email dice "no-email@fake.com", o que las fechas están en formato americano (mes/día/año) cuando las esperabas en europeo. Para eso necesitas MIRAR filas reales. head() te da las primeras cinco (que pueden no ser representativas si el fichero está ordenado) y sample() te da un subconjunto aleatorio.

1import pandas as pd
2
3df = pd.DataFrame({
4 'cliente': ['TEST_USER', 'Ana Garcia', 'Luis Martinez', 'Marta Lopez', 'no-responde'],
5 'email': ['test@test.com', 'ana@mail.com', 'luis@mail.com', 'marta@mail.com', 'no-email@fake.com'],
6 'fecha_registro': ['01/15/2024', '15/03/2024', '22/06/2024', '08/11/2024', '00/00/0000'],
7 'gasto': [0.0, 1200.50, 340.00, 5600.75, 0.01]
8})
9
10print("Primeras filas:")
11print(df.head())
12print()
13print("Muestra aleatoria (2 filas):")
14print(df.sample(2, random_state=42))

head() y sample() te enseñan lo que los números ocultan: datos de prueba, formatos incorrectos, placeholders

Consejo de senior: nunca te quedes solo con head(). Las primeras filas pueden ser las de un lote de prueba que alguien metió al principio del fichero. Usa sample(10) para ver filas de distintas zonas del dataset. Y si el fichero está ordenado por fecha, un head() te enseña solo el pasado más remoto — que puede ser la época con peor calidad de datos.

### EDA no es limpieza: es el diagnóstico

Este punto es crítico y muchos analistas novatos lo confunden. El EDA es la radiografía, no la operación. Cuando encuentras que gasto_total es texto, ANOTAS "hay que convertir a numérico". Cuando ves un 9999 en importe, ANOTAS "posible placeholder, investigar". Cuando detectas 5 variantes de Madrid, ANOTAS "normalizar provincia antes de agrupar". Pero NO corriges todavía. Primero terminas el diagnóstico completo. Porque a veces un problema que parece grave en el paso 3 se explica solo al llegar al paso 6 — y si ya lo hubieras "arreglado", habrías borrado la evidencia.

La analogía médica funciona de nuevo: el médico no opera la primera cosa rara que ve en la radiografía. Primero mira TODO el cuerpo, anota todas las anomalías, y solo entonces decide el plan de intervención. Porque a veces lo que parece un tumor en el pulmón es una sombra del corazón — y para saberlo necesitas ver las dos cosas a la vez.

Nunca limpies sin haber diagnosticado primero. El orden importa: primero miras todo, luego decides qué tocas.

### El ejemplo completo: EDA de principio a fin

Vamos a juntar los seis pasos en un ejemplo continuo. El escenario: trabajas en una empresa de suscripciones online y te llega un CSV con los datos de los suscriptores del último trimestre. La directora de producto quiere calcular el ingreso medio por usuario (ARPU, por sus siglas en inglés), pero antes de hacer ese cálculo necesitas saber si puedes confiar en los datos.

1import pandas as pd
2
3# Dataset de suscriptores (simulado con errores reales)
4subs = pd.DataFrame({
5 'user_id': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
6 'plan': ['pro', 'basic', 'pro', 'enterprise', 'basic', 'pro', 'basic', 'pro', 'enterprise', 'basic'],
7 'pago_mensual': ['29.99', '9.99', '29.99', '99.99', '9.99', '29.99', '9.99', '29.99', '99.99', '9.99'],
8 'meses_activo': [12, 3, None, 24, 1, 8, None, 15, 36, 2],
9 'soporte_tickets': [2, 0, 5, 12, 0, 3, 0, 1, 350, 0]
10})
11
12# PASO 1: shape
13print("=== SHAPE ===")
14print(f"Filas: {subs.shape[0]}, Columnas: {subs.shape[1]}")
15print()
16
17# PASO 2: dtypes
18print("=== DTYPES ===")
19print(subs.dtypes)
20print()
21
22# PASO 3: nulos
23print("=== NULOS ===")
24print(subs.isna().sum())
25print()
26
27# PASO 4: describe
28print("=== DESCRIBE ===")
29print(subs.describe())
30print()
31
32# PASO 5: nunique
33print("=== NUNIQUE ===")
34print(subs.nunique())

Los seis pasos en secuencia: cada uno revela algo que el anterior no mostraba

### Resumen: lo que buscas en cada paso

  1. 01..shape — ¿El tamaño cuadra con lo que esperabas? Si no, falta o sobra algo.
  2. 02..dtypes — ¿Hay columnas numéricas leídas como texto (object)? Es el error más común y más peligroso.
  3. 03..isna().sum() — ¿Qué porcentaje de nulos tiene cada columna? Más del 50% es sospechoso. Menos del 5% suele ser manejable.
  4. 04..describe() — ¿La media y la mediana están cerca? Si no, busca valores extremos en el min y el max. Un max físicamente imposible es un error de captura.
  5. 05..nunique() — ¿Una columna categórica tiene más valores de los esperados? Posible problema de normalización (mayúsculas, typos).
  6. 06..head()/.sample() — ¿Las filas reales tienen pinta de datos de producción o hay filas de prueba, placeholders o formatos mezclados?

Regístrate para guardar tu progreso.

## comentarios

Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.

Inicia sesión para comentar y responder.

cargando comentarios...