Limpieza de datos
En resumen: Limpiar los datos es quitar los duplicados, llevar precios, fechas y nombres a un solo formato y comprobar con código que los datos son correctos después de la limpieza.
Problema
La nueva lista de precios
Un proveedor nuevo ha enviado su lista de precios como CSV. El gerente pregunta dos cosas: cuánto pagaremos en promedio por producto y cuánto nos cuesta el café. El archivo supplier.csv está junto al código. Aquí está:
Las cuatro columnas son str. Los precios son «0,95 €» (el proveedor escribe los precios a la manera continental, con coma decimal), «1.45», vacío y «a consultar». Las fechas están en dos formatos. El queso blanco está dos veces. Los nombres tienen mayúsculas, minúsculas y espacios. El café cuesta 1190.
prices['price'].mean() ni siquiera arranca: los precios son texto. A los datos así se les llama sucios, y son la norma, no la excepción. El programa del proveedor escribe lo que escribe, y las personas rellenan cosas a mano.
La idea
Limpiar es código, no trabajo manual
La tentación es abrir el archivo y corregir las celdas a mano. Pero dentro de un mes el proveedor enviará una lista nueva con los mismos errores. Por eso limpiar es código: se ejecuta de nuevo con cada archivo nuevo.
El camino es siempre el mismo:
- Mirar qué está mal:
dtypes,describe,value_counts, ordenar. - Corregir con un paso para cada tipo de suciedad: duplicados, texto, números, fechas, valores atípicos.
- Comprobar con código que ha funcionado: el número de filas antes y después, NaN solo donde debe haberlos.
Cada paso de esta lección es para uno de los tipos de suciedad de la lista.
La idea
Duplicados: duplicated y drop_duplicates
duplicated() da una máscara: True para cada fila que repite una fila de más arriba. .sum() las cuenta. Con subset= compara solo las columnas indicadas, y keep=False marca todas las copias, no solo la segunda:
Hay una fila exactamente igual: el queso blanco, P07. Según el código, sin embargo, hay dos repeticiones. La leche fresca, P05, está dos veces, una como «LECHE FRESCA» y otra como « Leche fresca», con un espacio delante. Para pandas son textos distintos, así que duplicated() no lo detecta.
drop_duplicates() quita las repeticiones y se queda con la primera fila. Pero si lo ejecutas ahora, la leche sigue dos veces.
La idea
Nombres: str.strip y str.lower
El texto se limpia con .str, de la lección sobre tipos. str.strip() quita los espacios de los extremos, y str.lower() pone todas las letras en minúscula. Así «LECHE FRESCA» y « Leche fresca» pasan a ser el mismo texto:
De 16 filas quedan 14: se han quitado el queso y la leche. El orden de los pasos importa: primero limpia el texto y después busca duplicados.
Más precisamente, para curiosos
str.lower() sirve para comparar. Para una etiqueta o un informe, str.capitalize() pone la primera letra en mayúscula: «leche fresca» pasa a «Leche fresca». Si los nombres también se diferencian en el medio (dos espacios entre palabras), str.replace(' ', ' ') o str.split().str.join(' ') los igualan.
Pruébalo
Sin repeticiones
Limpia los nombres de la lista (sin espacios en los extremos, en minúsculas), quita las filas repetidas y guarda en rows cuántas filas quedan.
Solución
import pandas as pd
prices = pd.read_csv('supplier.csv')
prices['name'] = prices['name'].str.strip().str.lower()
prices = prices.drop_duplicates()
rows = len(prices)
print(rows)prices['name'].str.strip().str.lower() iguala los nombres, y drop_duplicates() quita entonces las dos filas repetidas: de 16 quedan rows = 14. Si quitas primero los duplicados, quedan 15: en ese momento las dos filas de la leche todavía son distintas, y drop_duplicates solo quita el queso.
La idea
Precios a partir de texto: str.replace y pd.to_numeric
«0,95 €» es texto por tres motivos: el signo €, la coma en lugar del punto y el espacio. str.replace quita cada uno. Después pd.to_numeric convierte el texto en número. Con errors='coerce', lo que aún no sea un número se vuelve NaN en lugar de detener el código:
El precio es float64, y hay dos NaN: las manzanas (un campo vacío) y el agua mineral («a consultar»). Eso es correcto: el proveedor no dio precio para ellos. Sin errors='coerce', to_numeric se detiene con ValueError: Unable to parse string "a consultar".
errors='coerce' es cómodo y peligroso: no dice qué ha tirado. Por eso cuentas los NaN antes y después. Si tras la conversión hay más que campos vacíos, algo se ha vuelto NaN por error.
Pruébalo
Precios como números
Convierte la columna price en número. Debe quedar NaN solo donde el proveedor no dio precio.
Solución
import pandas as pd
prices = pd.read_csv('supplier.csv')
text = prices['price'].str.replace('€', '').str.replace(',', '.').str.strip()
prices['price'] = pd.to_numeric(text, errors='coerce')
print(prices['price'].isna().sum())str.replace('€', ''), luego str.replace(',', '.') y str.strip() convierten «0,95 €» en «0.95», y pd.to_numeric(text, errors='coerce') lo convierte en 0.95. Quedan 2 NaN: las manzanas y el agua. Con solo to_numeric(..., errors='coerce'), 11 de 16 se vuelven NaN, porque cada precio con coma desaparece en silencio y el código no da ningún error.
Míralo
Fechas en dos formatos
La mitad de las fechas son «01.11.2026» (día, mes, año), y la otra mitad «2026-11-01». Si dejas que pandas adivine:
pandas adivina el formato por la primera fecha y la lee como %m.%d.%Y: primero el mes, a la estadounidense. Con la fecha ISO se detiene con un error. Si todas las fechas tuvieran puntos, no habría error: 01.11 se convertiría en silencio en el 11 de enero.
Por eso indicas tú el formato, con format=, una vez para cada formato. Con errors='coerce' las fechas del otro formato se vuelven NaT, y fillna une las dos lecturas:
13 filas valen desde el 1 de noviembre y 2 desde el 15 de noviembre. Una fecha no se puede leer con ninguno de los dos formatos: el 31.11.2026 de las bananas. Ese día no existe, noviembre tiene 30 días.
Míralo
Valores atípicos: describe y ordenar
Los precios ya son números. describe() del primer vistazo te dice si parecen plausibles, y ordenar muestra cuáles son los mayores:
La media es 102,70 €, y la mediana 1,62 €. Una diferencia así significa que un valor está lejos de los demás. Eso se llama valor atípico (outlier). Al ordenar se encuentra enseguida: el café en grano a 1190 €. Nosotros vendemos el café a 14,90 €, así que el precio del proveedor es 11,90 €: su programa perdió la coma decimal.
La corrección es una regla para todos los precios por encima de 100 €. La escribes en el código, no en una celda, para que detecte el mismo error también en la próxima lista:
El café cuesta 11,90 €, la media bajó a 4,53 € y la mediana se quedó en 1,62 €. La mediana no nota un solo número enorme, y la media lo nota entero.
Más precisamente, para curiosos
La regla «más de 100 € significa coma perdida» es correcta para esta lista porque conocemos los productos: nada en ella cuesta más de 100 €. En otra lista (televisores, muebles) la misma regla estropearía precios correctos. Cuando no sabes por qué un valor es raro, pregunta de dónde viene en lugar de corregirlo.
La idea
Comprobaciones como código: assert
La limpieza está terminada cuando puedes demostrarlo. assert condición, 'mensaje' no hace nada si la condición es verdadera, y detiene el código con un AssertionError y el mensaje si no lo es. Aquí está toda la limpieza con las comprobaciones al final:
De 16 filas quedan 14. Las comprobaciones dicen en voz alta lo que esperas:
- cada código aparece una vez;
- falta un precio solo en los dos productos sin precio;
- cada precio está entre 0,10 € y 100 €;
- solo queda una fecha sin leer.
Cuando llegue la próxima lista con un error nuevo, la comprobación detendrá el código con un mensaje claro, en lugar de que el informe salga mal en silencio.
Ejercicio
El precio medio del proveedor
Responde al gerente. Limpia toda la lista en prices: los nombres y los duplicados, los precios como números, las fechas en los dos formatos y el café sin su coma decimal. Los productos sin precio se quedan, con NaN. Guarda en avg_price el precio medio, redondeado a 2 decimales.
Solución
import pandas as pd
prices = pd.read_csv('supplier.csv')
prices['name'] = prices['name'].str.strip().str.lower()
prices = prices.drop_duplicates()
text = prices['price'].str.replace('€', '').str.replace(',', '.').str.strip()
prices['price'] = pd.to_numeric(text, errors='coerce')
dots = pd.to_datetime(prices['valid_from'], format='%d.%m.%Y', errors='coerce')
iso = pd.to_datetime(prices['valid_from'], format='%Y-%m-%d', errors='coerce')
prices['valid_from'] = dots.fillna(iso)
cents = prices['price'] > 100
prices.loc[cents, 'price'] = prices.loc[cents, 'price'] / 100
assert prices['code'].is_unique
assert prices['price'].isna().sum() == 2
avg_price = round(prices['price'].mean(), 2)
print(avg_price)Los cuatro pasos en este orden dan 14 filas, 2 precios NaN y 1 fecha NaT. round(prices['price'].mean(), 2) es 4,53 €, y el café cuesta 11,90 €. Sin la corrección del café, la media es 102,70 €. Con fillna(0) el agua y las manzanas «cuestan» 0 € y bajan la media, y con dropna() pierdes también las bananas, que tienen un precio correcto.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Seleccionar: [], loc e iloc · Filtrar y ordenar · Columnas nuevas · Tipos y valores faltantes
- Continúa en: Fuga de datos
- Ejercicios sobre el tema: Preparar los datos