Datos ausentes: encontrar los huecos y luego decidir qué significan
Encontrar cada hueco en una tabla, incluidos los ocultos en forma de texto como - o ?, entender cómo NaN cambia sumas, promedios y tipos, y elegir a propósito —columna por columna— si descartar, rellenar o conservar cada hueco.
¿Cómo quieres aprender?
El problema que vamos a resolver
Los pedidos de la tienda correspondientes a la primera semana de marzo se introdujeron a mano en tres sucursales y se exportaron como orders_raw.csv. El dueño quiere dos cifras para la tarde: cuántas unidades se vendieron y cuánto dinero ingresó.
Lees el archivo y, por costumbre de los capítulos anteriores, sumas primero la columna de precios solo para ver la escala:
import pandas as pd
orders = pd.read_csv("orders_raw.csv")
print(orders["price"].sum())15.060.0850.08.0-15.0120.0Eso no es un número. Es cada precio pegado junto al siguiente como texto —y pandas no lanzó ningún error para avisarte—. Ahora la columna de cantidades:
print(orders["quantity"].sum())
print(orders["quantity"].mean())76.0
10.857142857142858Esta parece estar bien, y eso es peor. Hay ocho pedidos en el archivo, pero 76 / 8 es 9.5, no 10.86. Una cantidad está en blanco, y pandas la dejó fuera silenciosamente tanto del total como de la media. Nadie te avisó de eso tampoco.
Así que el archivo tiene huecos, y los huecos se presentan de dos formas. Algunos son visibles: una celda vacía, que pandas convierte en un indicador especial de «dato ausente» y luego omite silenciosamente en las operaciones aritméticas. Otros están ocultos: alguien escribió - en lugar de dejar la celda vacía, pandas lo tomó al pie de la letra y toda la columna de precios se convirtió en texto.
Ninguno de los dos tipos lanza un error. Ambos cambian la respuesta. Este capítulo trata sobre cómo encontrar cada hueco, entender lo que pandas hace con él y luego tomar una decisión deliberada —descartarlo, rellenarlo o conservarlo— en lugar de dejar que el comportamiento por defecto tome esa decisión por ti.
Al final de este capítulo puedes
- Explicar qué es
NaN, por quéNaN == NaNesFalsey por qué una columna de números enteros se convierte en decimales cuando una celda está vacía - Contar valores ausentes por columna con
isna().sum()y extraer las filas incompletas - Convertir indicadores ocultos como
-o?en valores ausentes reales, conna_values=opd.to_numeric(errors="coerce") - Predecir cómo
sum,mean,county la aritmética entre columnas tratan los valores ausentes - Eliminar valores ausentes con
dropna(), usandosubset=yhow=para descartar únicamente lo que pretendes - Reemplazar valores ausentes con
fillna()—un solo valor, un valor diferente por columna o un estadístico— - Decidir, columna por columna, si descartar, rellenar o conservar —y explicar por qué—
- Evitar la trampa de
inplace=Trueque no hace nada en pandas 3
Requisitos previos: Filtrar filas.
Crea el archivo primero
En la carpeta de tu proyecto, crea orders_raw.csv con exactamente estas líneas. Los huecos son deliberados; observa con atención las líneas tercera, quinta y séptima:
order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,,bottle,accessories,7,N/A
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,-
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0Son los mismos ocho pedidos que orders.csv de los capítulos anteriores, con cuatro problemas: el pedido 1002 no tiene cantidad, el pedido 1004 no tiene sucursal y su precio dice N/A, y el pedido 1006 tiene - como precio.
Antes de escribir el código
Los datos ausentes son el único tema donde escribir código primero resulta activamente peligroso: tanto dropna() como fillna() se ejecutan sin quejarse y ambos cambian la respuesta. Así que el trabajo empieza observando y decidiendo.
1. Formula la pregunta en una sola frase. «¿Cuál fue la cantidad total vendida y los ingresos totales (cantidad × precio) de la semana, y cuántos pedidos no se pudieron contabilizar?» La última parte importa. Un total que deja fuera silenciosamente dos pedidos no es la misma respuesta que un total que dice «6 de 8 pedidos contabilizados».
2. Observa lo que llegó, antes de cualquier otra cosa. shape e info() juntos te informan sobre cada columna a la vez:
print(orders.shape)
orders.info()(8, 7)
<class 'pandas.DataFrame'>
RangeIndex: 8 entries, 0 to 7
Data columns (total 7 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 order_id 8 non-null int64
1 date 8 non-null str
2 branch 7 non-null str
3 product 8 non-null str
4 category 8 non-null str
5 quantity 7 non-null float64
6 price 7 non-null str
dtypes: float64(1), int64(1), str(5)
memory usage: 580.0 bytesLéelo comparándolo con lo que esperabas, columna por columna:
branch— se esperaban 8strno nulos, se obtuvieron 7 no nulos. Falta una sucursal.quantity— se esperaban 8int64no nulos, se obtuvieron 7float64no nulos. Falta una cantidad, y esa es la razón por la que pasó a serfloat64(se explica más adelante).price— se esperaban 8float64no nulos, se obtuvieron 7strno nulos. Falta un precio y se coló texto en la columna.
Dos aspectos de esa lista son pistas más que hechos definitivos. Una columna de números enteros que muestra float64 casi siempre indica un hueco. Una columna numérica que muestra str siempre significa que hay texto mezclado —y el texto en una columna numérica suele ser un indicador oculto de «dato ausente»—.
3. Esboza la salida que deseas. Algo como:
Missing values per column: branch 1, quantity 1, price 2
Orders counted for revenue: 6 of 8
Total quantity (known): 76
Total revenue (counted): ...Observa que informa de los huecos así como de los totales. Quien lea el informe debe saber que los ingresos son un límite inferior.
4. Decide la herramienta para cada hueco, antes de escribirla. Para cada columna, pregúntate: ¿qué significa una celda vacía aquí, y se puede responder a la pregunta sin ella?
branch— un hueco significa que el pedido existió pero no se registró la sucursal. Decisión: rellenar con"Unknown", porque el pedido es real y su dinero sigue contando.quantity— un hueco significa que no sabemos cuántas unidades se vendieron. Decisión: conservarlo como ausente y excluir la fila de los ingresos, porque inventar un número (¿0? ¿la media?) sería inventar ventas.price— un hueco significa que no sabemos por cuánto se vendió. Decisión: conservarlo como ausente y excluir la fila de los ingresos, por la misma razón —e informar de cuántas filas se excluyeron—.
5. Decide qué comprobarás después. Después de cada paso que elimine o reemplace valores, vuelve a imprimir los recuentos de ausentes y el shape. Si un dropna() te pasa de 8 filas a 2, querrás verlo inmediatamente, no después de haber enviado el informe.
El resto del capítulo te presenta cada herramienta por turno, y al final reunimos este plan en un solo programa.
Cómo se ven los datos ausentes: NaN
Cuando pandas encuentra una celda vacía, no almacena una cadena vacía ni un cero. Almacena un valor especial de coma flotante llamado NaN —«not a number» (no es un número)—. Puedes crear uno tú mismo colocando None en una Series:
import pandas as pd
quantity = pd.Series([12, None, 2])
print(quantity)0 12.0
1 NaN
2 2.0
dtype: float64Observa el dtype. Le diste dos números enteros y un hueco, y obtuviste de vuelta float64, con 12.0 y 2.0. Esta es la razón detrás de la pista en info(): NaN es un valor de tipo float, y una columna clásica de enteros no tiene forma de contenerlo. Así que en el momento en que falta una celda, pandas asciende toda la columna a float64 para hacerle hueco a NaN. Por eso quantity en nuestro archivo se imprimió como 12.0, 2.0, etcétera.
Las columnas de texto se comportan de otra manera. Una columna de tipo str puede albergar un valor ausente sin cambiar de tipo:
branch = pd.Series(["north", None, "east"])
print(branch)0 north
1 NaN
2 east
dtype: strSe sigue imprimiendo como NaN, y el dtype permanece como str. De modo que en pandas 3 encontrarás NaN tanto en columnas numéricas como de texto, y las mismas herramientas lo encuentran en ambas.
NaN no es igual a nada, ni siquiera a sí mismo
Este es el hecho más sorprendente sobre los valores ausentes, y arruina la forma más natural de buscarlos:
import numpy as np
import pandas as pd
orders = pd.read_csv("orders_raw.csv")
print(np.nan == np.nan)
print((orders["branch"] == np.nan).sum())False
0NaN significa «un valor que desconocemos». De dos valores desconocidos no se sabe si son iguales, por lo que la comparación dice False —siempre—. Eso convierte a == np.nan en una prueba que nunca coincide con nada: la columna de sucursales tiene un hueco y el recuento dice 0. En su lugar, pregúntale directamente a pandas.
Nunca busques valores ausentes con==.== np.nanse ejecuta sin errores y nunca encuentra nada, por lo que una columna llena de huecos parecerá completa. Usaisna().
Encontrar valores ausentes: isna()
isna() formula la pregunta que == no puede: «¿está ausente esta celda?». Sobre una columna devuelve una columna de True/False —una máscara, exactamente como las del capítulo de filtrado—:
print(orders["branch"].isna())0 False
1 False
2 False
3 True
4 False
5 False
6 False
7 False
Name: branch, dtype: boolSobre todo el DataFrame hace lo mismo para cada celda. Sin embargo, rara vez querrás leer ocho filas de True/False. Quieres un recuento, y como True cuenta como 1, .sum() te lo da:
print(orders.isna().sum())order_id 0
date 0
branch 1
product 0
category 0
quantity 1
price 1
dtype: int64Esta es la línea que conviene ejecutar tras info() en cualquier archivo nuevo. Es la misma información que el Non-Null Count, planteada al revés para contar los huecos directamente.
Pero fíjate en price: dice 1. Sabemos que dos precios están mal —N/A y -—. Tenlo presente; volveremos a ello en la siguiente sección.
¿Qué filas están incompletas?
Un recuento te dice cuántas; para ver cuáles, usa la máscara como filtro. isna().any(axis=1) pregunta, para cada fila, «¿falta algo en esta fila?»:
incomplete = orders[orders.isna().any(axis=1)]
print(incomplete)order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery NaN 60.0
3 1004 2024-03-02 NaN bottle accessories 7.0 NaNaxis=1 significa «mirar a lo largo de cada fila» en lugar de bajar por cada columna. Y fíjate en quién no está en la lista: el pedido 1006, cuyo precio es -. Hasta donde sabe pandas, esa fila está completa. Guarda esa idea. Mirar las filas reales merece la pena esa línea extra: así es como notas, por ejemplo, que todas las cantidades ausentes provienen de la misma sucursal, lo cual es una pregunta para el personal de esa sucursal más que para pandas.
Como proporción, no como recuento
Con ocho filas, los recuentos están bien. Con ochenta mil, «312 ausentes» dice poco hasta que sabes que es el 0.4%. .mean() en una columna de True/False es la fracción de valores True:
print(orders.isna().mean())order_id 0.000
date 0.000
branch 0.125
product 0.000
category 0.000
quantity 0.125
price 0.125
dtype: float64Una de cada ocho celdas de branch, quantity y price está ausente —un 12.5% cada una—. Una columna que está vacía al 90% y otra que está vacía al 0.1% exigen decisiones completamente distintas, y así es como las distingues.
Valores ausentes ocultos: cuando el hueco está escrito como texto
isna() contó un precio ausente, pero dos precios son inutilizables. El segundo es el - en el pedido 1006. Para pandas, - no es un dato ausente: es un fragmento de texto perfectamente válido. Y un solo fragmento de texto en una columna de números basta para que toda la columna sea str, que es exactamente lo que mostró info().
Así es como averiguas qué hay realmente en una columna:
print(orders["price"].unique())<StringArray>
['15.0', '60.0', '850.0', nan, '8.0', '-', '120.0']
Length: 7, dtype: strCada valor está entre comillas: todos son texto, incluido '15.0'. Y hay dos tipos de huecos uno al lado del otro: nan, que pandas reconoció, y '-', que no reconoció.
¿Por qué N/A se convirtió en nan pero - no? Porque read_csv incluye una lista incorporada de cadenas que trata como ausentes: una celda vacía, NA, N/A, n/a, NaN, null, None y algunas otras. -, ?, missing, 0 y unknown no están en esa lista. Lo que la persona que introdujo los datos haya utilizado para indicar «no se sabe» determina qué tipo de hueco obtienes.
Hay dos formas de convertir los ocultos en un NaN real.
Solución 1: avisar a read_csv sobre el indicador — na_values=
Si conoces el indicador, la solución más limpia es en el momento de la lectura. na_values= añade tus cadenas a la lista incorporada:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
print(orders["price"].dtype)
print(orders.isna().sum())float64
order_id 0
date 0
branch 1
product 0
category 0
quantity 1
price 2
dtype: int64Compáralo con el recuento anterior. branch y quantity no han cambiado: na_values= añade a la lista, no la sustituye, por lo que las celdas vacías y N/A se siguen reconociendo. Lo que cambió es price: ahora es float64 e informa de 2 ausentes —tanto N/A como - son ahora NaN reales—. La columna vuelve a ser numérica, por lo que las operaciones aritméticas sobre ella significan realmente operaciones aritméticas.
Ten en cuenta que na_values=["-"] se aplica a todas las columnas. Aquí eso es lo que queremos. Si un - pudiera ser un valor real en otro lugar —un código de producto, por ejemplo—, pasa un diccionario en su lugar, na_values={"price": ["-"]}, y solo afectará a la columna price.
Solución 2: convertir después — pd.to_numeric(errors="coerce")
A veces no sabes de antemano qué cadenas extrañas hay en una columna, o el archivo fue leído por el código de otra persona. En ese caso, conviertes la columna después de leerla. Primero, observa qué hace una conversión simple:
raw = pd.read_csv("orders_raw.csv")
price = pd.to_numeric(raw["price"])ValueError: Unable to parse string "-" at position 5Ese error es útil: nombra el valor conflictivo y su posición. Añadir errors="coerce" dice «cualquier cosa que no puedas convertir en un número, conviértela en NaN»:
price = pd.to_numeric(raw["price"], errors="coerce")
print(price)0 15.0
1 60.0
2 850.0
3 NaN
4 8.0
5 NaN
6 15.0
7 120.0
Name: price, dtype: float64Para modificar realmente la tabla, reasigna el resultado a la columna —to_numeric devuelve una nueva Series y deja raw intacto—:
raw["price"] = pd.to_numeric(raw["price"], errors="coerce")
print(raw["price"].dtype, raw["price"].isna().sum())float64 2Qué solución elegir
errors="coerce" es potente y contundente. Convierte cualquier cosa no numérica en NaN —incluido un precio real mal tecleado como 1O0 (letra O) o 1,200 con coma de miles—. Esos no son valores ausentes; son valores con una errata, y forzarlos con coerce descarta información silenciosamente.
Por tanto, la costumbre es: contar antes, contar después y explicar la diferencia.
- Conoces el indicador (
-,?,missing): usana_values=enread_csv. Es preciso: solo esa cadena pasa a ser un dato ausente. - Residuos desconocidos y la columna debe ser numérica: usa
pd.to_numeric(errors="coerce"). Lo detecta todo, así que comprueba qué ha detectado. - Algunos valores son erratas que puedes corregir: examina primero
unique(), corrígelos y luego convierte. Forzarlos con coerce destruiría datos reales.
Antes de forzar con coerce, raw["price"].unique() mostraba exactamente una cadena no numérica ('-') más un nan. Tras forzar con coerce, hay exactamente 2 NaN. Los números coinciden, por lo que no se perdió nada más. Esa comprobación marca toda la diferencia entre limpiar datos y dañarlos.
A partir de aquí usamos la versión leída con na_values=["-"].
Cómo tratan los cálculos a NaN
Ahora que los huecos son NaN reales, necesitas saber qué hace pandas con ellos —porque no se detiene a preguntar—.
Los resúmenes de columnas omiten NaN
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
q = orders["quantity"]
print("len :", len(q))
print("count:", q.count())
print("sum :", q.sum())
print("mean :", q.mean())
print("check:", q.sum() / q.count())len : 8
count: 7
sum : 76.0
mean : 10.857142857142858
check: 10.857142857142858Tres comportamientos para recordar:
lencuenta filas;count()cuenta valores. La diferencia entre ellos es el número de huecos. Es la misma idea que elNon-Null Counteninfo().sum()omiteNaN. Suma las siete cantidades conocidas. Eso es razonable, pero significa que «76» es el total de cantidades conocidas, no de todos los pedidos.mean()divide entrecount(), no entrelen. La media se calcula sobre los siete valores conocidos. Suele ser lo que deseas; tratar el hueco como 0 reduciría la media sin motivo.
Si prefieres que se te avise de que falta algo, skipna=False hace que el resultado sea NaN en cuanto cualquier valor lo sea:
print(q.sum(skipna=False))nanLa aritmética entre columnas propaga NaN
Los resúmenes omiten los huecos; la aritmética los propaga. Cualquier cosa combinada con NaN da NaN, porque «7 veces un precio desconocido» es en sí mismo desconocido:
revenue = orders["quantity"] * orders["price"]
print(revenue)
print("total:", revenue.sum())
print("rows counted:", revenue.count(), "of", len(revenue))0 180.0
1 NaN
2 1700.0
3 NaN
4 240.0
5 NaN
6 300.0
7 480.0
dtype: float64
total: 2900.0
rows counted: 5 of 8Junta ambos comportamientos y verás la trampa del principio del capítulo. revenue.sum() imprimió 2900.0 sin ninguna advertencia, pero tres de los ocho pedidos no aportaron nada. Al pedido 1002 le falta la cantidad, a 1004 y 1006 les falta el precio. El total solo es sincero si indicas «5 de 8 pedidos». Por eso el plan pedía un recuento junto a cada total.
Eliminar valores ausentes: dropna()
dropna() elimina las filas que contienen NaN. Sin argumentos elimina cada fila que tenga algún hueco en cualquier columna:
print(orders.shape)
print(orders.dropna().shape)(8, 7)
(5, 7)Tres de ocho filas han desaparecido —y el pedido 1004 se eliminó porque faltaba su sucursal, a pesar de que para una pregunta sobre cantidades era una fila perfectamente válida—. Ese es el problema de un dropna() simple: responde a «eliminar cualquier cosa imperfecta», que rara vez es la pregunta.
subset= — solo las columnas que necesita la pregunta
Indica qué columnas deben estar presentes:
known_qty = orders.dropna(subset=["quantity"])
print(known_qty.shape)
print(known_qty["quantity"].sum())(7, 7)
76.0Solo se elimina el pedido 1002 —la única fila donde la cantidad realmente es desconocida—. Para los ingresos se necesitan ambas columnas:
countable = orders.dropna(subset=["quantity", "price"])
print(countable[["order_id", "quantity", "price"]])order_id quantity price
0 1001 12.0 15.0
2 1003 2.0 850.0
4 1005 30.0 8.0
6 1007 20.0 15.0
7 1008 4.0 120.0Observa el índice: 0, 2, 4, 6, 7. dropna() conserva las etiquetas originales, igual que lo hace el filtrado, por lo que siempre puedes rastrear una fila hasta el archivo original. Si necesitas 0, 1, 2… de nuevo, añade .reset_index(drop=True).
how="all" — solo filas completamente vacías
Las hojas de cálculo exportadas a menudo terminan con unas pocas filas que están en blanco en todas las columnas. how="all" elimina únicamente esas y conserva cualquier fila que tenga al menos un valor:
print(orders.dropna(how="all").shape)(8, 7)Aquí no se eliminó nada —ninguna de nuestras filas está completamente vacía—, que es exactamente lo que quieres confirmar.
dropna() devuelve una tabla nueva
Como la mayoría de los métodos de pandas, dropna() no modifica orders; te entrega un DataFrame nuevo. Si escribes orders.dropna() en una línea aislada, no le ocurre nada a orders. Asigna el resultado —a un nombre nuevo si vas a seguir necesitando la tabla completa, como harás en un informe como el nuestro (para indicar cuántos pedidos se descartaron)—.
Reemplazar valores ausentes: fillna()
A veces un hueco tiene un reemplazo razonable. fillna() coloca un valor donde había un NaN.
Un solo valor para una columna
La sucursal ausente es el caso más claro. El pedido existió y su dinero es real; simplemente no sabemos qué sucursal lo atendió. Rellenarlo con una etiqueta conserva la fila y hace que el hueco sea visible en cualquier recuento posterior por sucursal:
orders["branch"] = orders["branch"].fillna("Unknown")
print(orders["branch"].tolist())['north', 'south', 'north', 'Unknown', 'north', 'south', 'east', 'north']La forma de esa línea es importante: toma la columna, aplica fillna y asígnala de nuevo a la misma columna. fillna devuelve una nueva Series; sin orders["branch"] = delante, no se almacena nada.
Valores distintos para columnas distintas: un diccionario
Pasa un diccionario de {column: value} para rellenar varias columnas a la vez, cada una con su propio valor. Las columnas no mencionadas quedan intactas:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
filled = orders.fillna({"branch": "Unknown", "quantity": 0})
print(filled.isna().sum())order_id 0
date 0
branch 0
product 0
category 0
quantity 0
price 2
dtype: int64price sigue teniendo sus dos huecos, porque no lo mencionamos. Esa es la ventaja del formato con diccionario sobre orders.fillna(0), que rellenaría todas las columnas, incluidos los precios, con 0 —y un precio de 0 es una mochila gratis—.
Rellenar cambia la respuesta
Rellenar la cantidad con 0 parecía inofensivo. Compara las medias:
print("skip the gap :", orders["quantity"].mean())
print("fill with 0 :", orders["quantity"].fillna(0).mean())
print("fill with median:", orders["quantity"].fillna(orders["quantity"].median()).mean())skip the gap : 10.857142857142858
fill with 0 : 9.5
fill with median: 10.375Tres medias distintas de una misma columna, y la única diferencia es lo que decidiste que significa una cantidad desconocida:
- 0 dice «este pedido no vendió nada». Para el pedido 1002, un pedido de cuadernos que sin duda vendió algo, eso es falso y reduce la media.
- La mediana (7 aquí) dice «asume un pedido típico». Mantiene la media aproximadamente en su sitio, pero el total ahora incluye 7 cuadernos que nadie contó.
- Omitir dice «no lo sabemos, así que déjalo fuera». La media se calcula sobre los siete pedidos que sí conocemos.
Ninguna de estas opciones es «correcta» en general. Cada una responde a una pregunta ligeramente diferente. Por eso la decisión corresponde a tu plan, por escrito, en lugar de a cualquier comportamiento por defecto al que recurriste.
Una regla general que funciona la mayor parte del tiempo:
- Una etiqueta (sucursal, categoría, nombre): rellena con
"Unknown"o similar. Se conserva la fila y el hueco sigue visible como su propio grupo. - Un recuento donde vacío realmente significa ninguno (devoluciones, quejas): rellena con
0—pero solo cuando la fuente realmente quiera decir cero con un espacio en blanco—. - Una medición o dinero (cantidad vendida, precio, una calificación): conserva
NaN, exclúyelo del cálculo e informa de cuántos se excluyeron. Los números inventados se convierten en totales inventados. - Una columna que la pregunta necesita, mayoritariamente presente:
dropna(subset=[...])únicamente para esa pregunta. Una pequeña pérdida y una respuesta sincera.
Recuperar números enteros
Después de rellenar, quantity no tiene huecos, pero sigue siendo float64 —pandas no lo devuelve a entero por sí solo—. Una vez que no queda ningún NaN, astype(int) funciona:
filled["quantity"] = filled["quantity"].astype(int)
print(filled["quantity"].tolist())
print(filled["quantity"].dtype)[12, 0, 2, 7, 30, 1, 20, 4]
int64Pruébalo antes de rellenar y fallará, porque no hay ningún entero que signifique «desconocido» —consulta «Cuando algo falla» más abajo—.
Un ejemplo completo
clean_orders.py plasma en un único programa el plan de «Antes de escribir el código»: leer con el indicador conocido, informar de cada hueco, rellenar la sucursal, excluir las filas que no se pueden contabilizar e informar de los totales junto con la cantidad de pedidos que abarcan.
import pandas as pd
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
# 1. Look first
print("Rows, columns:", orders.shape)
missing = orders.isna().sum()
print("Missing per column:")
print(missing[missing > 0])
print()
# 2. Decide per column: branch -> label; quantity and price -> keep, exclude
orders["branch"] = orders["branch"].fillna("Unknown")
# 3. Only rows with both numbers can be counted for revenue
countable = orders.dropna(subset=["quantity", "price"])
skipped = orders[orders[["quantity", "price"]].isna().any(axis=1)]
revenue = countable["quantity"] * countable["price"]
print("Orders counted for revenue:", len(countable), "of", len(orders))
print("Total quantity (known): ", int(orders["quantity"].sum()))
print("Total revenue (counted): ", revenue.sum())
print()
print("Not counted - check these with the branch:")
print(skipped[["order_id", "branch", "quantity", "price"]])Rows, columns: (8, 7)
Missing per column:
branch 1
quantity 1
price 2
dtype: int64
Orders counted for revenue: 5 of 8
Total quantity (known): 76
Total revenue (counted): 2900.0
Not counted - check these with the branch:
order_id branch quantity price
1 1002 south NaN 60.0
3 1004 Unknown 7.0 NaN
5 1006 south 1.0 NaNPor qué está escrito de esta manera:
na_values=["-"]está en la primerísima línea. El indicador se encontró una vez, examinandounique(). Ponerlo enread_csvsignifica que la columna es numérica desde el principio y cualquiera que lea el programa ve qué indicador utiliza el archivo.missing[missing > 0]imprime solo las columnas que tienen huecos. En un archivo con cuarenta columnas, esa es la diferencia entre un informe legible y un muro de ceros.- La sucursal se rellena, los números no. Esa es la lista de decisiones del plan, escrita en código. Un lector puede estar en desacuerdo, pero puede verla con claridad.
dropna(subset=...)nombra las dos columnas que necesitan los ingresos, de modo que el pedido 1004 se excluye por su precio ausente, no por su sucursal ausente.- Las filas omitidas se imprimen, no solo se cuentan. «3 pedidos no contabilizados» invita a la pregunta «¿cuáles?»; respóndela en el informe. Esos tres pedidos son una lista que alguien realmente puede revisar y corregir.
- «Total quantity (known)» y «(counted)» especifican qué son esas cifras. Ambos son límites inferiores; las etiquetas impiden que cualquiera los trate como cifras completas.
Cuando algo falla
TypeError: Cannot perform reduction 'mean' with string dtype Pediste la media de una columna que pandas leyó como texto —por lo general porque una celda contiene un indicador como -, ? o missing—. Ejecuta df["col"].unique() para encontrar al culpable, y luego vuelve a leer con na_values=["-"] o convierte con pd.to_numeric(df["col"], errors="coerce").
raw = pd.read_csv("orders_raw.csv")
print(raw["price"].mean())TypeError: Cannot perform reduction 'mean' with string dtypesum() devolvió una larga cadena de dígitos, como 15.060.0850.0... Misma causa, peor síntoma: en una columna de texto sum() une las cadenas en lugar de sumar números, y no lanza ningún error. Cualquier total que se vea así provino de una columna str. Comprueba df.dtypes antes de sumar cualquier cosa.
ValueError: Unable to parse string "-" at position 5 pd.to_numeric encontró texto que no pudo convertir y te indicó exactamente qué y dónde. O bien añade errors="coerce" (después de comprobar con unique() que los valores extraños son realmente indicadores de «desconocido» y no erratas), o bien corrige las erratas primero.
(df["col"] == np.nan).sum() dice 0, pero hay huecos NaN nunca es igual a nada, incluido a sí mismo, por lo que == np.nan no coincide con ninguna celda. Usa df["col"].isna().
IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer Llamaste a astype(int) en una columna que todavía tiene NaN. No existe ningún entero que signifique «desconocido». Rellena o elimina los huecos primero, y luego convierte:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
orders["quantity"] = orders["quantity"].astype(int)pandas.errors.IntCastingNaNError: Cannot convert non-finite values (NA or inf) to integer.Replace or remove non-finite values or cast to an integer typethat supports these values (e.g. 'Int64')(Los espacios que faltan en «integer.Replace» y «typethat» están en el propio mensaje de pandas). El mensaje también menciona 'Int64' con I mayúscula: un tipo entero propio de pandas que sí puede contener valores ausentes. Es útil, pero rellenar o descartar primero es la solución más sencilla y a la que conviene recurrir.
ChainedAssignmentError — y el hueco sigue ahí Escribiste la forma con inplace=True que encontrarás en muchos tutoriales antiguos:
orders = pd.read_csv("orders_raw.csv", na_values=["-"])
orders["quantity"].fillna(0, inplace=True)
print(orders["quantity"].isna().sum())1
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment using an inplace method.
Such inplace method never works to update the original DataFrame or Series, because the intermediate object on which we are setting values always behaves as a copy (due to Copy-on-Write).
For example, when doing 'df[col].method(value, inplace=True)', try using 'df.method({col: value}, inplace=True)' instead, to perform the operation inplace on the original object, or try to avoid an inplace operation using 'df[col] = df[col].method(value)'.
See the documentation for a more detailed explanation: https://pandas.pydata.org/pandas-docs/stable/user_guide/copy_on_write.htmlEl recuento sigue siendo 1: no se rellenó nada. En pandas 3, orders["quantity"] siempre actúa como su propia copia (Copy-on-Write), por lo que rellenarlo «in place» rellena esa copia, que luego se descarta. Las versiones anteriores de pandas a veces permitían que esto funcionara, razón por la cual el patrón abunda en internet. Escribe la asignación completa en su lugar: funciona en todas las versiones y expresa con claridad qué cambia:
orders["quantity"] = orders["quantity"].fillna(0)
print(orders["quantity"].isna().sum())0dropna() eliminó muchas más filas de las que esperabas Un dropna() simple elimina una fila por un hueco en cualquier columna —incluida una columna casi vacía de notes o discount que ni siquiera estabas usando—. Imprime df.isna().sum() para encontrar la columna con muchos huecos y luego usa dropna(subset=[...]) con solo las columnas que tu pregunta necesita.
TypeError: can't multiply sequence by non-int of type 'float' Multiplicaste una columna numérica por una columna de texto (quantity * price mientras price es str). La solución es la misma que para el primer error: haz que price sea numérica primero.
Paso 4 de 6 — Predice
Comprueba lo que has entendido
Una cantidad está en blanco. ¿Qué se imprime?
from io import StringIO
import pandas as pd
# Stands in for orders_raw.csv, so this snippet runs on its own.
RAW = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,,bottle,accessories,7,N/A
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,-
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0
"""
orders = pd.read_csv(StringIO(RAW))
print(orders["quantity"].sum(), orders["quantity"].count(), len(orders))- A76.0 7 8
- B76.0 8 8
- C76 8 8
- Dnan 7 8
El objetivo es encontrar los pedidos sin precio. Faltan dos precios. ¿Qué se imprime?
from io import StringIO
import pandas as pd
# Stands in for orders_raw.csv, so this snippet runs on its own.
RAW = """order_id,date,branch,product,category,quantity,price
1001,2024-03-01,north,pen,stationery,12,15.0
1002,2024-03-01,south,notebook,stationery,,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,,bottle,accessories,7,N/A
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,-
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0
"""
orders = pd.read_csv(StringIO(RAW), na_values=["-"])
missing_price = orders[orders["price"] == float("nan")]
print(len(missing_price))- A0 — `NaN` no es igual a nada, ni siquiera a `NaN`, por lo que `==` nunca lo encuentra
- B1
- CUn `TypeError`: no se puede comparar con `nan`
- D2
La columna de precios se leyó como str porque una celda dice - y otra N/A. ¿Qué línea la convierte en números, dejando ambos huecos como NaN?
- Aorders["price"] = pd.to_numeric(orders["price"], errors="coerce")
- Borders["price"] = orders["price"].fillna(0)
- Corders["price"] = orders["price"].astype(float)
- Dorders["price"] = orders["price"].dropna()
Para responder necesitas una cuenta
Inicia sesión para comprobar tus respuestas
Las preguntas están arriba, y resolverlas mentalmente es lo que de verdad importa. Inicia sesión para ver las respuestas, las explicaciones y las pistas de tres niveles.
Tu turno
La tienda ha comenzado el reparto a domicilio y el registro de los repartidores para la primera semana llega como deliveries.csv. Créalo exactamente así:
order_id,branch,distance_km,fee,rider
2001,north,3.5,60,R1
2002,north,,60,R2
2003,south,8.0,?,R1
2004,east,12.0,120,
2005,north,2.0,40,R2
2006,south,6.5,90,?
2007,east,,110,R3
2008,north,4.0,60,R3El dueño pregunta: ¿cuánto se recaudó en tarifas de entrega, a cuántas entregas les falta la tarifa y cuál es la tarifa media por kilómetro?
Antes de escribir cualquier código, observa el archivo con unique() y decide cuál es el indicador oculto. Luego escribe deliveries_report.py que:
- Lea el archivo de modo que el indicador oculto se convierta en un valor ausente real en cada columna donde aparezca
- Imprima el número de valores ausentes por columna y las filas incompletas
- Rellene el
riderausente con"unassigned" - Imprima el total de tarifas conocidas, la cantidad de entregas sin tarifa y la tarifa por kilómetro —tarifa total dividida entre la distancia total— calculada únicamente a partir de las entregas donde se conozcan tanto la tarifa como la distancia
Las tres últimas líneas que imprima tu programa deben ser exactamente:
Fees collected (known): 540.0
Deliveries with no fee: 1
Fee per km: 13.21 from 5 of 8 deliveriesEscribe también, antes del código, una lista de decisiones como la de «Antes de escribir el código»: para cada una de distance_km, fee y rider, ¿qué significa un hueco y vas a rellenar, descartar o conservar?
Luego prueba un experimento: calcula la tarifa por kilómetro con un dropna() simple en lugar de dropna(subset=...). ¿Cambia la cifra? ¿Cuántas filas utilizó y por qué?
Solución
Primero el plan. La pregunta consta de tres partes: un total, un recuento de huecos y una proporción. Leer con info() mostraría fee como str —una columna de tarifas debería ser de números, por lo que hay texto oculto en ella—. Decisiones por columna:
rider— la entrega se realizó, pero no se registró el repartidor. Rellenar con"unassigned".fee— no sabemos cuánto se cobró. ConservarNaN; sumar las tarifas conocidas y contar las desconocidas.distance_km— no sabemos qué distancia hubo. ConservarNaN; excluir esas filas de la proporción por kilómetro.
Para la proporción, ambos números deben conocerse para la misma entrega; de lo contrario, dividirías las tarifas de siete entregas entre las distancias de seis.
Paso 1 — leer de forma simple y observar, para encontrar el indicador:
import pandas as pd
raw = pd.read_csv("deliveries.csv")
print(raw.shape)
print(raw.dtypes)
print(raw["fee"].unique())
print(raw["rider"].unique())(8, 5)
order_id int64
branch str
distance_km float64
fee str
rider str
dtype: object
<StringArray>
['60', '?', '120', '40', '90', '110']
Length: 6, dtype: str
<StringArray>
['R1', 'R2', nan, '?', 'R3']
Length: 5, dtype: strfee es str debido al ?. Y ? también aparece como repartidor: el mismo indicador, utilizado en dos columnas. distance_km ya es float64: sus huecos son celdas vacías, que pandas reconoció por sí solo. Como el indicador es el mismo en todas partes, na_values=["?"] en el momento de la lectura corrige ambas columnas a la vez.
El programa completo, deliveries_report.py:
import pandas as pd
deliveries = pd.read_csv("deliveries.csv", na_values=["?"])
print("Types after reading:")
print(deliveries.dtypes)
print()
print("Missing per column:")
print(deliveries.isna().sum())
print()
print("Incomplete rows:")
print(deliveries[deliveries.isna().any(axis=1)])
print()
deliveries["rider"] = deliveries["rider"].fillna("unassigned")
fee_total = deliveries["fee"].sum()
fee_missing = deliveries["fee"].isna().sum()
print("Fees collected (known):", fee_total)
print("Deliveries with no fee:", fee_missing)
both = deliveries.dropna(subset=["fee", "distance_km"])
per_km = both["fee"].sum() / both["distance_km"].sum()
print("Fee per km:", round(per_km, 2), "from", len(both), "of", len(deliveries), "deliveries")Types after reading:
order_id int64
branch str
distance_km float64
fee float64
rider str
dtype: object
Missing per column:
order_id 0
branch 0
distance_km 2
fee 1
rider 2
dtype: int64
Incomplete rows:
order_id branch distance_km fee rider
1 2002 north NaN 60.0 R2
2 2003 south 8.0 NaN R1
3 2004 east 12.0 120.0 NaN
5 2006 south 6.5 90.0 NaN
6 2007 east NaN 110.0 R3
Fees collected (known): 540.0
Deliveries with no fee: 1
Fee per km: 13.21 from 5 of 8 deliveriesComprueba los números a mano una vez, porque así es como aprendes a confiar en un total limpio. Tarifas conocidas: 60 + 60 + 120 + 40 + 90 + 110 + 60 = 540, de siete entregas —2003 no tiene ninguna—. Para la proporción, dropna(subset=["fee", "distance_km"]) descarta 2003 (sin tarifa) y 2002 y 2007 (sin distancia). Pregúntale a pandas qué filas quedaron y cuánto suman, en lugar de confiar en el cálculo mental:
print(both["order_id"].tolist())
print(both["fee"].sum(), both["distance_km"].sum())[2001, 2004, 2005, 2006, 2008]
370.0 28.0370 / 28.0 = 13.21, que es la cifra que imprimió el programa. Fíjate en lo que la proporción no hizo: no dividió los 540 de siete tarifas entre las distancias de seis entregas. Ambos números provienen de las mismas cinco filas, que es la única manera de que una cifra «por kilómetro» tenga sentido.
Dos decisiones más que conviene destacar. rider se rellenó antes del cálculo, pero nunca influyó en él: subset= solo examinó fee y distance_km. Y el total de tarifas está etiquetado como «(known)», con el recuento de tarifas ausentes impreso justo debajo, para que nadie confunda 540 con todo lo que se recaudó.
El experimento. Un dropna() simple también elimina 2004 y 2006 —cuya tarifa y distancia estaban ambas bien— porque faltaba su repartidor. Si lo ejecutas sobre la tabla antes de rellenar rider:
deliveries = pd.read_csv("deliveries.csv", na_values=["?"])
strict = deliveries.dropna()
print(strict["order_id"].tolist())
print(round(strict["fee"].sum() / strict["distance_km"].sum(), 2))[2001, 2005, 2008]
16.84Solo sobreviven tres entregas y la proporción salta de 13.21 a 16.84 —no porque haya cambiado ninguna tarifa, sino porque los dos trayectos más largos (12 y 6.5 km), que casualmente son más baratos por kilómetro, se descartaron por la falta de un nombre—. La columna del repartidor no tiene nada que ver con la pregunta, por lo que no debería decidir qué filas cuentan. Para eso está subset=.
Paso 6 de 6
Reto — el quiz del capítulo
Diez preguntas de fácil a difícil. Las últimas son difíciles a propósito.
Inicia sesión para hacer el quiz