Filtrar filas: hacerle a cada fila la misma pregunta
Conservar solo las filas que responden que sí: máscaras booleanas, combinación con &, | y ~, isin, between y los métodos .str, filas filtradas con .loc y modificación exclusiva de esas filas sin la asignación encadenada que silenciosamente no hace nada.
¿Cómo quieres aprender?
El problema que vamos a resolver
El gerente de la tienda manda un mensaje: «¿Cuáles pedidos de la sucursal norte tenían más de tres artículos? Quiero verlos.»
Con los ocho pedidos del capítulo anterior podrías responder a simple vista. Deslizas un dedo por la columna branch, te detienes en cada north y miras la columna quantity. Tres filas. Listo.
Ahora imagina que ese es el archivo real: doce mil pedidos, un año entero. La vista ya no alcanza, y la primera idea que se le ocurre a la mayoría de los programadores Python es un bucle:
import pandas as pd
orders = pd.read_csv("orders.csv")
keep = []
for i in range(len(orders)):
row = orders.iloc[i]
if row["branch"] == "north" and row["quantity"] > 3:
keep.append(int(row["order_id"]))
print(keep)[1001, 1005, 1008]La respuesta es correcta. Pero mira lo que costó: un contador, una lista vacía, un if, un append, y la tabla desmenuzada fila por fila. Además, el resultado ya no es una tabla, sino una lista desnuda de números de pedido. Para mostrarle al gerente también los productos y los precios habría que volver a recorrer el bucle. Con doce mil filas esto también es lento, porque cada orders.iloc[i] construye una Series nueva solo para leer dos valores.
Pandas tiene otra forma de hacer esta pregunta, y es la operación más frecuente que harás con un DataFrame: hacerle a cada fila la misma pregunta de sí o no a la vez y quedarte con las que dijeron sí. Eso es el filtrado, y es todo este capítulo. El bucle anterior se convierte en una línea, el resultado sigue siendo una tabla y se lee casi igual que la frase del gerente.
Al final de este capítulo puedes
- Convertir una pregunta en una máscara booleana — una columna de
True/False, una por fila — y usarla para conservar filas, o para contarlas conmask.sum()ymask.mean() - Combinar condiciones con
&,|y~, y explicar por qué los paréntesis son obligatorios - Usar
isin,betweeny los métodos.strpara listas, rangos y patrones de texto - Filtrar filas y seleccionar columnas en un solo paso con
.loc[mask, columns] - Cambiar valores en las filas filtradas de forma segura con
.loc[mask, column] = value, y reconocer elChainedAssignmentErrorque produce la forma incorrecta en pandas 3
Requisitos previos: Seleccionar columnas y filas.
Crea el archivo primero
Este capítulo usa el mismo orders.csv que el anterior. Si no lo tienes, créalo en la carpeta de tu proyecto con exactamente estas nueve líneas:
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,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
1007,2024-03-04,east,pen,stationery,20,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0Antes de escribir el código
Un filtro es una pregunta escrita en código. La mayoría de los errores de filtrado no son errores de pandas, sino una pregunta que nunca se concretó bien o una columna que no contiene lo que suponías. Así que antes de la primera línea de filtrado, haz estas cinco cosas.
1. Escribe la pregunta como condiciones, una por columna. Desglosa la frase del gerente:
- «pedidos de la sucursal norte» → columna
branch, prueba igual a, valor"north" - «más de tres artículos» → columna
quantity, prueba mayor que, valor3 - «quiero verlos» → sin prueba; conserva la fila completa
Esta lista te obliga a tomar dos decisiones. «Más de tres» es > 3, no >= 3 — un pedido de exactamente tres artículos no cuenta. Y las dos condiciones se unen con y: un pedido debe cumplir ambas. Esas dos decisiones son todo el filtro; el código solo las escribe.
2. Revisa la entrada — forma, tipos y los valores exactos contra los que compararás.
import pandas as pd
orders = pd.read_csv("orders.csv")
print(orders.shape)
print(orders.dtypes)(8, 7)
order_id int64
date str
branch str
product str
category str
quantity int64
price float64
dtype: objectquantity es int64, así que > 3 comparará números. Si hubiera llegado como str — porque alguien escribió "3 pcs" en una celda — la comparación fallaría con un TypeError, y conviene saberlo ahora, no a mitad del proceso. branch es str, por lo que se comparará como texto, y la comparación de texto es exacta: "north", "North" y "north " son tres valores distintos. Así que mira los valores que realmente existen:
print(orders["branch"].unique())<StringArray>
['north', 'south', 'east']
Length: 3, dtype: strTres sucursales, escritas siempre igual, sin espacios sobrantes. Ahora "north" en el código está garantizado que coincidirá con algo.
3. Dibuja la salida antes de producirla. Con un archivo pequeño, responde la pregunta a mano primero. Los pedidos de la sucursal norte son 1001 (12 artículos), 1003 (2), 1005 (30) y 1008 (4). Más de tres artículos: 1001, 1005, 1008. Entonces el resultado esperado es 3 filas, las 7 columnas, y sus números de pedido son conocidos. Si el código devuelve 4 filas o 0, sabrás de inmediato que está mal, en lugar de confiar ciegamente en él. Con un archivo grande no puedes hacer todo esto a mano, pero aún puedes estimar: «la sucursal norte representa más o menos la mitad de los pedidos, así que la respuesta debería ser bastante menos de la mitad de las filas».
4. Elige la herramienta según la forma de la condición.
- una comparación → una máscara:
df["col"] > value - varias condiciones → máscaras unidas con
&(y),|(o),~(no) - «uno de estos valores» →
df["col"].isin([...]) - «entre dos valores» →
df["col"].between(low, high) - un patrón dentro de texto →
df["col"].str.contains(...),.str.startswith(...) - filas y solo algunas columnas →
df.loc[mask, ["col1", "col2"]] - cambiar valores en esas filas →
df.loc[mask, "col"] = new_value
La pregunta del gerente tiene dos comparaciones unidas por y, así que: dos máscaras y &.
5. Decide cómo verificarás la respuesta. Después de filtrar, tres comprobaciones baratas detectan casi todo: el número de filas coincide con tu boceto; la condición se cumple de verdad en todas las filas que sobrevivieron ((result["quantity"] > 3).all() debe imprimir True); y nada de lo que esperabas falta (busca 1008 en el resultado).
Ahora el código — una idea a la vez.
Una comparación produce una máscara
Compara una columna con un valor y pandas no te da un solo True o False. Compara cada fila y te devuelve una columna entera de respuestas:
big = orders["quantity"] > 5
print(big)0 True
1 False
2 False
3 True
4 True
5 False
6 True
7 False
Name: quantity, dtype: boolEsto es una Series de bool — una máscara booleana. Tiene el mismo índice que orders (0 al 7), una respuesta por fila: la fila 0 tiene 12 artículos, así que True; la fila 1 tiene 5, y 5 no es mayor que 5, así que False.
¿Por qué pandas trabaja así, en lugar de hacerte recorrer un bucle? Porque una columna se almacena como un bloque continuo de números de un solo tipo, y comparar todo ese bloque contra 5 es una operación ejecutada en código compilado rápido. Tu bucle le pedía a Python que mirara cada fila por separado, ocho veces — o doce mil veces. La máscara pregunta una sola vez.
Todos los operadores de comparación funcionan igual: ==, !=, <, <=, >, >=. También funcionan en columnas de texto — orders["branch"] == "north" es una máscara con True en las filas 0, 2, 4 y 7.
Aún no se ha filtrado nada. Una máscara es solo las respuestas. El siguiente paso es usarlas.
La máscara conserva filas
Pon la máscara dentro de corchetes y pandas conserva las filas donde la máscara es True:
print(orders[big])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
3 1004 2024-03-02 east bottle accessories 7 120.0
4 1005 2024-03-03 north eraser stationery 30 8.0
6 1007 2024-03-04 east pen stationery 20 15.0Cuatro filas de ocho. Lee el índice por la izquierda: 0, 3, 4, 6. Las filas que sobreviven conservan sus etiquetas originales. El filtrado no renumera nada — la fila 3 sigue siendo la fila 3, el mismo pedido 1004 que estaba en la tabla completa. Eso importa, y volveremos a ello.
¿Cómo alinea pandas la máscara con las filas? No por posición, sino por etiqueta de índice. La etiqueta 0 de la máscara decide la fila 0, la etiqueta 3 decide la fila 3. Aquí los dos índices son idénticos, porque la máscara se construyó a partir de orders misma, así que esto es invisible. Solo se vuelve visible cuando usas una máscara construida a partir de una tabla distinta — eso aparece en «Cuando se rompe».
Normalmente verás la máscara escrita de forma directa, sin nombre:
print(orders[orders["branch"] == "north"])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
2 1003 2024-03-02 north bag accessories 2 850.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0Léelo de adentro hacia afuera: `orders["branch"] == "north"` construye la máscara, `orders[ … ]` conserva las filas con True. Darle nombre a la máscara (big = …) vale la pena cuando la condición es larga o cuando la usas dos veces; la forma directa está bien para condiciones cortas.
Contar con una máscara, sin filtrar
A menudo el gerente no quiere las filas, solo un número: ¿cuántos pedidos tenían más de cinco artículos? Para eso no necesitas filtrar. En aritmética, True cuenta como 1 y False como 0, así que:
print(big.sum())
print(big.mean())4
0.5sum() suma los unos: 4 pedidos. mean() divide por el número de filas: 4 de 8 es 0.5, así que la mitad de los pedidos tenían más de cinco artículos. «Cuántos» es .sum() de una máscara; «qué proporción» es .mean() de una máscara. Ambos son una línea y ninguno construye una tabla nueva.
Y cuando quieres un total sobre las filas filtradas — la cantidad de artículos en esos pedidos grandes — filtra primero, luego selecciona la columna y después suma:
print(orders[big]["quantity"].sum())6912 + 7 + 30 + 20 = 69. Leer una columna de una tabla filtrada, así, está bien. Escribir en una de esta forma no lo está — eso se explica en la última sección del capítulo.
Varias condiciones: &, |, ~
La pregunta del gerente tiene dos condiciones. Pandas une máscaras con tres operadores:
&significa y — la fila se conserva cuando ambas máscaras sonTrue|significa o — la fila se conserva cuando al menos una máscara esTrue~significa no — invierte cadaTrueaFalsey viceversa
Aquí está la pregunta del gerente, por fin:
north_big = orders[(orders["branch"] == "north") & (orders["quantity"] > 3)]
print(north_big)order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0Tres filas: 1001, 1005, 1008 — exactamente el boceto del paso de planificación. Ahora la verificación del paso 5:
print(len(north_big))
print((north_big["quantity"] > 3).all())
print((north_big["branch"] == "north").all())3
True
TrueEl conteo coincide y ambas condiciones se cumplen en todas las filas supervivientes. .all() pregunta «¿es True cada valor de esta máscara?» — es la forma más barata de comprobar que un filtro hizo lo que querías.
Una pregunta con o: pedidos de la sucursal este, o cualquier pedido de algo que cueste 850 o más.
print(orders[(orders["branch"] == "east") | (orders["price"] >= 850)])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
6 1007 2024-03-04 east pen stationery 20 15.0Cuatro filas: los dos pedidos de la sucursal este y los dos pedidos de mochilas de las sucursales norte y sur. Una fila que cumpliera ambas condiciones (ninguna aquí) aparecería una sola vez, no dos — un filtro solo conserva o descarta una fila.
Una pregunta con no: todo excepto papelería.
print(orders[~(orders["category"] == "stationery")])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
7 1008 2024-03-04 north bottle accessories 4 120.0~ convierte cada True en False y viceversa. Para una igualdad simple podrías escribir != en su lugar, que es más sencillo; ~ justifica su uso cuando lo que niegas es una condición más larga o un isin (lo verás enseguida).
Por qué los paréntesis son obligatorios
Cada condición de los ejemplos anteriores está dentro de sus propios paréntesis. No es estilo. Quítalos y el código se rompe:
print(orders[orders["quantity"] > 3 & orders["price"] < 100])TypeError: Cannot perform 'rand_' with a dtyped [float64] array and scalar of type [bool]Lo que ves arriba es la última línea del traceback, la que nombra el error; las líneas anteriores solo dicen dónde ocurrió. La razón es la precedencia de operadores — el orden en que Python aplica los operadores. En Python, & se une más fuerte que > y <. Eso se decidió mucho antes de que existiera pandas, porque & fue diseñado para números enteros (el «y» a nivel de bits), donde debe ejecutarse primero. Así que Python lee la línea como:
orders["quantity"] > (3 & orders["price"]) < 100Intenta calcular primero 3 & orders["price"] — un «y» a nivel de bits entre el número 3 y una columna de decimales — y de eso trata el error. El mensaje menciona rand_ (el «and» por la derecha) y un array, pero la causa es siempre la misma: falta un par de paréntesis.
El error es el resultado afortunado. Envuelve solo la primera condición y olvida la segunda, y no hay ningún error:
print(len(orders[(orders["quantity"] > 3) & orders["price"] < 100]))
print(len(orders[(orders["quantity"] > 3) & (orders["price"] < 100)]))8
4La primera línea se lee como ((quantity > 3) & price) < 100. El & mezcla una máscara con los precios, y todos los resultados resultan ser menores que 100, así que el «filtro» conserva las ocho filas. El filtro correcto conserva cuatro. Mismas columnas, mismos números, un par de paréntesis faltante — y la versión incorrecta se ejecuta sin hacer ningún ruido. Envuelve cada condición en sus propios paréntesis, siempre. Así ni el error ni la versión silenciosa pueden ocurrir.
¿Por qué no and?
El otro instinto natural es escribir la palabra en inglés:
print(orders[(orders["branch"] == "north") and (orders["quantity"] > 3)])ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().Los operadores and, or y not de Python necesitan un solo True o False a cada lado — fueron creados para sentencias if. Para usar and, Python le pregunta a la primera máscara «¿eres verdadera?», y una columna de ocho respuestas no puede contestar con una sola. ¿Es verdadera si algún valor es True? ¿Si todos son True? Pandas se niega a adivinar, y el error lista las formas en que podrías decidirlo (a.any(), a.all()). Pero tú no querías una sola respuesta; querías el «y» fila por fila. Eso es &. La regla a recordar: dentro de un filtro, and/or/not se convierten en &/|/~.
Uno de varios valores: isin
Pedidos de la sucursal sur o este. Podrías escribir dos igualdades unidas con |. Con cinco sucursales eso se convierte en cinco comparaciones. isin recibe una lista y pregunta «¿el valor de esta fila está en la lista?»:
print(orders[orders["branch"].isin(["east", "south"])])order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
6 1007 2024-03-04 east pen stationery 20 15.0Y aquí es donde ~ justifica su uso — toda sucursal excepto norte y este es ~ delante de un isin:
print(orders[~orders["branch"].isin(["north", "east"])]["order_id"].tolist())[1002, 1006]Dos pedidos, ambos de la sucursal sur. Fíjate que ~orders["branch"].isin([...]) no necesita paréntesis extra: ~ se aplica al resultado de la llamada al método, y no hay & ni | con qué pelear. En cuanto lo combines con otra condición, los paréntesis vuelven: (~orders["branch"].isin([...])) & (orders["quantity"] > 3).
Un rango: between
Pedidos con precio entre 15 y 120. Son dos comparaciones, >= 15 y <= 120. between lo dice en una:
print(orders[orders["price"].between(15, 120)])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
6 1007 2024-03-04 east pen stationery 20 15.0
7 1008 2024-03-04 north bottle accessories 4 120.0between incluye ambos extremos por defecto — los bolígrafos a exactamente 15.0 y las botellas a exactamente 120.0 están incluidos. Eso coincide con cómo la gente suele decir «de 15 a 120», pero compruébalo contra la pregunta. Si la pregunta significa estrictamente entre, dilo así:
print(orders["price"].between(15, 120, inclusive="neither").sum())1Solo el cuaderno a 60.0 está estrictamente entre los dos valores. inclusive también acepta "left" y "right" para rangos semiabiertos — la elección habitual para bandas de precios, donde quieres 0–100, 100–200 y así sucesivamente sin que un valor límite caiga en dos bandas.
Patrones de texto: los métodos .str
La igualdad en texto es exacta. El accesor .str le da a una columna de texto los métodos de cadena que ya conoces de Python, aplicados a cada fila, y cada uno devuelve una máscara.
Productos cuyo nombre empieza por «b»:
print(orders[orders["product"].str.startswith("b")])order_id date branch product category quantity price
2 1003 2024-03-02 north bag accessories 2 850.0
3 1004 2024-03-02 east bottle accessories 7 120.0
5 1006 2024-03-03 south bag accessories 1 850.0
7 1008 2024-03-04 north bottle accessories 4 120.0Productos que contienen la letra «o», en cualquier mayúscula o minúscula:
print(orders[orders["product"].str.contains("O", case=False)])order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
3 1004 2024-03-02 east bottle accessories 7 120.0
7 1008 2024-03-04 north bottle accessories 4 120.0No hay ninguna O mayúscula en los datos, pero el cuaderno y las dos botellas aparecen, porque case=False ignora las mayúsculas. Sin él, .str.contains("O") no encontraría nada.
La igualdad exacta no tiene esa opción. Este es el error más frecuente de «mi filtro no devuelve nada»:
print(orders[orders["branch"] == "North"])Empty DataFrame
Columns: [order_id, date, branch, product, category, quantity, price]
Index: []Sin error, sin filas. La tabla no está mal y pandas no está mal — simplemente no hay ninguna sucursal escrita "North" con mayúscula. Por eso el paso 2 del plan imprimió unique(): los valores contra los que comparas deben copiarse de lo que realmente hay en la columna, no escribirse de memoria.
Las fechas de texto se comparan correctamente — cuando son ISO
La columna date llegó como str. ¿Puedes filtrarla con >=? Pedidos desde el 3 de marzo en adelante:
print(orders["date"] >= "2024-03-03")0 False
1 False
2 False
3 False
4 True
5 True
6 True
7 True
Name: date, dtype: boolFunciona, pero entiende por qué. Son cadenas, así que >= las compara como texto, carácter por carácter, tal como se ordena un diccionario. Eso da la respuesta correcta solo porque las fechas están escritas año-mes-día con ceros a la izquierda (2024-03-03), de modo que el orden de texto coincide con el orden cronológico. Una columna escrita como 3/3/2024 compararía mal — "10/1/2024" iría antes que "9/1/2024" porque "1" viene antes que "9". Para algo más allá de simples cortes, convierte a fechas reales con pd.to_datetime, que se cubre en el capítulo sobre agregar columnas.
Filas y columnas juntas: .loc[mask, columns]
Los filtros anteriores conservaban todas las columnas. El gerente quería los pedidos de la sucursal norte, pero el informe solo necesita el producto y la cantidad. Conociste .loc[rows, columns] en el capítulo anterior, con etiquetas. La parte de filas también acepta una máscara:
print(orders.loc[orders["branch"] == "north", ["product", "quantity"]])product quantity
0 pen 12
2 bag 2
4 eraser 30
7 bottle 4Un paso, un objeto, y quien lea tu código ve la pregunta completa — qué filas, qué columnas — en un solo lugar. .loc[mask, "quantity"] (un nombre de columna solo, no una lista) te da una Series, lista para operaciones aritméticas:
print(orders.loc[orders["branch"] == "north", "quantity"].sum())4812 + 2 + 30 + 4 = 48 artículos vendidos por la sucursal norte.
query(): el mismo filtro como una frase
Existe una segunda forma de escribir filtros, query(), que recibe la condición como cadena:
print(orders.query("branch == 'north' and quantity > 3"))order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
7 1008 2024-03-04 north bottle accessories 4 120.0Las mismas tres filas. Dentro de la cadena escribes nombres de columna simples, y and/or/not están permitidos, porque query analiza la cadena por su cuenta en lugar de delegarla a los operadores de Python. Para usar una variable de Python, ponle el prefijo @:
min_qty = 5
print(orders.query("quantity >= @min_qty")["order_id"].tolist())[1001, 1002, 1004, 1005, 1007]query se lee bien para condiciones largas. Sus costos: un error tipográfico dentro de la cadena solo se descubre cuando se ejecuta esa línea, tu editor no puede ayudarte dentro de ella, y los nombres de columna con espacios necesitan comillas invertidas. Este curso usa máscaras como herramienta principal, porque las máscaras son la base de todo lo demás en pandas — .loc, asignación, conteo. Conoce query para poder leerlo en el código de otras personas.
La tabla filtrada conserva sus etiquetas antiguas
Volvamos al índice. Guarda los pedidos de la sucursal norte y obsérvalos:
north = orders[orders["branch"] == "north"]
print(north.index.tolist())[0, 2, 4, 7]Etiquetas 0, 2, 4, 7. Entonces, ¿qué es north.loc[1]? No hay ninguna etiqueta 1 en esta tabla — el pedido 1002 venía de la sucursal sur y fue filtrado:
print(north.loc[1])KeyError: 1.loc busca una etiqueta, y la etiqueta 1 no existe aquí. Si querías «el segundo pedido de la sucursal norte», eso es una posición, y las posiciones son .iloc:
print(north.iloc[1])order_id 1003
date 2024-03-02
branch north
product bag
category accessories
quantity 2
price 850.0
Name: 2, dtype: objectLa segunda fila por posición es el pedido de la mochila — y su Name es 2, su etiqueta de la tabla original. Esta es exactamente la distinción loc/iloc del capítulo anterior, y el filtrado es donde duele, porque después de un filtro las etiquetas y las posiciones dejan de coincidir.
Conservar las etiquetas es intencional: te permite rastrear cualquier fila de un resultado hasta la tabla original. Cuando ya no lo necesites — digamos que la tabla filtrada es el informe final — renumérala:
print(north.reset_index(drop=True))order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
1 1003 2024-03-02 north bag accessories 2 850.0
2 1005 2024-03-03 north eraser stationery 30 8.0
3 1008 2024-03-04 north bottle accessories 4 120.0drop=True descarta las etiquetas antiguas. Sin él, se conservarían como una nueva columna llamada index, que rara vez es lo que quieres en un informe.
Un resultado vacío no es un error
none = orders[orders["branch"] == "west"]
print(none)
print(none.shape)
print(none.empty)Empty DataFrame
Columns: [order_id, date, branch, product, category, quantity, price]
Index: []
(0, 7)
TrueNo hay sucursal oeste, así que no hay filas — y ningún error. Eso es comportamiento correcto (cero es una respuesta válida), pero significa que un filtro mal escrito falla silenciosamente. Las sumas sobre un resultado vacío son 0 y las medias son nan, lo que puede propagarse por un informe sin que nadie lo note. Cuando un filtro alimenta algo importante, verifica result.empty o el número de filas, y comunícalo claramente en la salida: "No orders from the west branch." es mejor que una tabla en blanco.
Cambiar las filas filtradas: .loc[mask, column] = value
A veces filtras para cambiar algo. Los bolígrafos vendidos por la sucursal este tenían un precio incorrecto; deberían ser 13.5, no 15.0. El código tentador hace el filtro y luego selecciona la columna — dos pares de corchetes, igual que cuando leíste valores antes:
import pandas as pd
orders = pd.read_csv("orders.csv")
orders[orders["branch"] == "east"]["price"] = 13.5
print(orders["price"].tolist())[15.0, 60.0, 850.0, 120.0, 8.0, 850.0, 15.0, 120.0]
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment.
Such chained assignment 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).
Try using '.loc[row_indexer, col_indexer] = value' instead, to perform the assignment in a single step.
See the documentation for a more detailed explanation: https://pandas.pydata.org/pandas-docs/stable/user_guide/copy_on_write.html#chained-assignmentLee los precios: nada cambió. Y pandas te avisó, con detalle. (En una terminal la advertencia va a stderr, así que aparece encima de la lista, en el momento en que se ejecuta la línea de asignación, y empieza con el nombre del archivo y el número de línea. Aquí se muestra después de la salida, sin ellos.) Esto es una asignación encadenada — dos pasos de indexación, orders[mask] y luego ["price"], con la asignación al final de la cadena. El primer paso, orders[mask], construye una nueva tabla con las filas de la sucursal este. El segundo paso fija precios en esa tabla nueva. Luego la tabla nueva se descarta, porque nada la retiene. orders nunca fue tocado.
En pandas 3 este es un comportamiento garantizado, llamado Copy-on-Write: cualquier tabla que obtienes al indexar se comporta como su propia copia, de modo que escribir en ella nunca puede afectar al original. El ChainedAssignmentError es una advertencia, no una excepción — el programa sigue ejecutándose — lo cual es exactamente por qué es peligroso: un script con esta línea termina «correctamente» con los precios antiguos todavía en su lugar.
(Si lees tutoriales más antiguos, verás esta situación descrita con un SettingWithCopyWarning y una nota que dice que «puede o no funcionar». En pandas 3 la respuesta es sencilla: nunca funciona, así que nunca lo escribas.)
La solución es el propio consejo de la advertencia: un solo paso de indexación, .loc[rows, column], con la asignación sobre él:
orders.loc[orders["branch"] == "east", "price"] = 13.5
print(orders["price"].tolist())[15.0, 60.0, 850.0, 13.5, 8.0, 850.0, 13.5, 120.0]Espera — eso cambió ambos pedidos de la sucursal este, el bolígrafo y la botella. La botella no tenía el precio incorrecto. El código hizo exactamente lo que decía; la pregunta en el código era incorrecta. Esto vuelve al paso 1 del plan: «bolígrafos vendidos por la sucursal este» son dos condiciones, y solo se escribió una. Empieza de nuevo con una copia nueva del archivo y escribe ambas:
import pandas as pd
orders = pd.read_csv("orders.csv")
fix = (orders["branch"] == "east") & (orders["product"] == "pen")
print(fix.sum())
orders.loc[fix, "price"] = 13.5
print(orders.loc[orders["branch"] == "east", ["product", "price"]])1
product price
3 bottle 120.0
6 pen 13.5Aquí son visibles dos hábitos. La máscara recibe un nombre (fix) porque se usa para algo importante; y antes de escribir, fix.sum() dice cuántas filas están a punto de cambiar — una, que es lo que esperábamos. Cuenta antes de sobreescribir, siempre.
Para leer,df[mask]["col"]está bien. Para escribir, usa siempredf.loc[mask, "col"] = value. La forma encadenada no lanza ninguna excepción, y el cambio desaparece silenciosamente.
Guardar una tabla filtrada para trabajar en ella está bien
¿Qué pasa con esto?
import pandas as pd
orders = pd.read_csv("orders.csv")
east = orders[orders["branch"] == "east"]
east["price"] = 0.0
print(east["price"].tolist())
print(orders["price"].tolist())[0.0, 0.0]
[15.0, 60.0, 850.0, 120.0, 8.0, 850.0, 15.0, 120.0]Sin advertencia, y está bien. Aquí quisiste crear una tabla separada: la llamaste east, y cambiarla solo cambia east. orders queda intacto. Copy-on-Write hace que este comportamiento sea predecible — una tabla que obtuviste al filtrar siempre es independiente del original. El error solo aparece en la forma encadenada, donde la tabla intermedia no tiene nombre y el cambio desaparece con ella.
Un ejemplo completo
El gerente ahora quiere un informe breve. report.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
# Check what arrived before asking it anything.
print("Rows, columns:", orders.shape)
print("Branches:", list(orders["branch"].unique()))
print()
# 1. The original question: north-branch orders of more than three items.
north_big = orders.loc[
(orders["branch"] == "north") & (orders["quantity"] > 3),
["order_id", "product", "quantity"],
]
print("North orders over 3 items:")
print(north_big.reset_index(drop=True))
print()
# 2. Counting with masks: no new table needed.
accessories = orders["category"] == "accessories"
print("Accessory orders :", accessories.sum())
print("Share of all orders:", accessories.mean())
print("Accessory items :", orders.loc[accessories, "quantity"].sum())
print()
# 3. Outside north, cheap items (under 100).
cheap_outside = orders[
(orders["branch"] != "north") & (orders["price"] < 100)
]
print("Cheap orders outside north:", cheap_outside["order_id"].tolist())
# 4. A branch we do not have: say so instead of printing an empty table.
west = orders[orders["branch"] == "west"]
if west.empty:
print("No orders from the west branch.")Rows, columns: (8, 7)
Branches: ['north', 'south', 'east']
North orders over 3 items:
order_id product quantity
0 1001 pen 12
1 1005 eraser 30
2 1008 bottle 4
Accessory orders : 4
Share of all orders: 0.5
Accessory items : 14
Cheap orders outside north: [1002, 1007]
No orders from the west branch.Por qué está escrito así:
- Revisa primero, luego pregunta.
shapey la lista de sucursales se imprimen antes de cualquier filtro. Si el archivo hubiera llegado conNorthen mayúscula, la línea de sucursales lo mostraría antes de que el informe imprimiera una tabla vacía. .loc[mask, columns]para la pregunta principal. Las filas y las columnas de la respuesta se deciden en un solo lugar, yreset_index(drop=True)renumera el resultado porque este es el informe final, no un paso intermedio.- Una máscara con nombre, usada tres veces.
accessoriesse calcula una vez y luego se cuenta (sum), se mide (mean) y se usa para seleccionar una columna. Darle nombre a una máscara es la forma de evitar escribir la misma condición tres veces y que una de ellas sea ligeramente distinta. - Cada condición en sus propios paréntesis, incluso en las cortas, para que agregar una tercera condición más tarde no pueda provocar el error de precedencia.
- El caso vacío se maneja en voz alta. Cero filas es una respuesta válida, y el informe lo dice con palabras.
Cuando se rompe
ValueError: The truth value of a Series is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all(). Usaste and, or o not entre dos máscaras — o un if sobre una máscara completa. Las palabras clave de Python necesitan un solo True/False, y una máscara tiene uno por fila. Dentro de un filtro, escribe &, |, ~. Dentro de un if, decide qué quieres decir: if mask.any(): («¿al menos una fila es verdadera?») o if mask.all(): («¿todas las filas son verdaderas?»).
TypeError: Cannot perform 'rand_' with a dtyped [float64] array and scalar of type [bool] (o 'ror_', o [int64]) Faltan paréntesis alrededor de una condición junto a & o |. Python aplicó & antes de >, así que intentó un «y» a nivel de bits entre un número y una columna. Pon cada condición en sus propios paréntesis: (a > 3) & (b < 100).
El filtro devuelve una tabla vacía, sin error Tu valor no aparece en la columna exactamente como lo escribiste. Imprime df["col"].unique() — o mejor, list(df["col"].unique()), que muestra cada valor entre comillas, de modo que un espacio final en 'north ' se vuelve visible. Limpia los datos con df["col"].str.strip(), o busca de forma flexible con .str.contains(..., case=False).
KeyError: 1 (u otro número) justo después de filtrar Usaste .loc[n] en una tabla filtrada como si todavía tuviera etiquetas 0, 1, 2…. Conserva las etiquetas originales. Usa .iloc[n] para «la n-ésima fila», o reset_index(drop=True) primero si las etiquetas antiguas ya no te sirven.
ChainedAssignmentError: A value is being set on a copy of a DataFrame or Series through chained assignment. Escribiste df[mask]["col"] = value. En pandas 3 nunca cambia df — la advertencia te dice que esa línea no hizo nada. Escribe df.loc[mask, "col"] = value.
UserWarning: Boolean Series key will be reindexed to match DataFrame index. La máscara se construyó a partir de una tabla distinta a la que estás filtrando — típicamente una máscara del orders completo usada sobre el north filtrado. Pandas la alinea por etiqueta y descarta silenciosamente las etiquetas que no encuentra:
import pandas as pd
orders = pd.read_csv("orders.csv")
north = orders[orders["branch"] == "north"]
big = orders["quantity"] > 5
print(north[big])order_id date branch product category quantity price
0 1001 2024-03-01 north pen stationery 12 15.0
4 1005 2024-03-03 north eraser stationery 30 8.0
UserWarning: Boolean Series key will be reindexed to match DataFrame index.(En una terminal la advertencia aparece encima de la tabla.) Aquí da una respuesta razonable, pero solo por la suerte de las etiquetas. Construye la máscara a partir de la tabla que estás filtrando: north[north["quantity"] > 5].
Paso 4 de 6 — Predice
Comprueba lo que has entendido
¿Qué se imprime?
from io import StringIO
import pandas as pd
# Stands in for orders.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,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
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))
mask = orders["quantity"] > 5
print(mask.sum(), len(orders[mask]))- A5 5
- B4 8
- C4 4
- DTrue 4
El objetivo es conservar los pedidos de la sucursal norte de más de tres artículos. ¿Qué ocurre cuando se ejecuta?
from io import StringIO
import pandas as pd
# Stands in for orders.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,5,60.0
1003,2024-03-02,north,bag,accessories,2,850.0
1004,2024-03-02,east,bottle,accessories,7,120.0
1005,2024-03-03,north,eraser,stationery,30,8.0
1006,2024-03-03,south,bag,accessories,1,850.0
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))
north_big = orders[orders["branch"] == "north" & orders["quantity"] > 3]- AUn `ValueError: The truth value of a Series is ambiguous`
- BDevuelve una tabla vacía
- CFunciona y conserva los pedidos 1001, 1005 y 1008
- DUn `TypeError`: sin paréntesis, `&` se aplica antes que `==` y `>`
Necesitas reducir a la mitad el precio de cada mochila en el propio orders. ¿Qué línea lo consigue?
- Aorders[orders["product"] == "bag"]["price"] = orders["price"] / 2
- Bbags = orders[orders["product"] == "bag"] bags["price"] = bags["price"] / 2
- Corders["price"][orders["product"] == "bag"] /= 2
- Dorders.loc[orders["product"] == "bag", "price"] = orders["price"] / 2
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
Usa el mismo orders.csv. Escribe filters.py que responda cuatro preguntas, cada una impresa bajo un encabezado breve:
- ¿Cuántos pedidos fueron de 10 artículos o más? Imprime un número, no una tabla.
- Pedidos de accesorios fuera de la sucursal norte — solo
order_id,branch,productyprice, renumerados desde 0. - Pedidos de la sucursal sur o este cuyo precio sea menor de 100. Usa
isinpara las sucursales. - La sede central otorga un descuento del 10% en cada pedido de papelería de 20 artículos o más. Fija el
pricede exactamente esas filas al 90% de su precio actual, pero primero imprime cuántas filas estás a punto de cambiar. Luego imprimeorder_id,quantityypricede todos los pedidos de papelería.
Antes de escribir cualquier código, planifica cada pregunta en papel: qué columnas, qué pruebas, unidas por qué, y qué filas esperas al mirar el archivo. Cuando tu programa sea correcto, imprimirá exactamente esto:
1. Orders of 10+ items:
3
2. Accessory orders outside north:
order_id branch product price
0 1004 east bottle 120.0
1 1006 south bag 850.0
3. South or east, price under 100:
order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
6 1007 2024-03-04 east pen stationery 20 15.0
4. Stationery discount:
Rows to change: 2
order_id quantity price
0 1001 12 15.0
1 1002 5 60.0
4 1005 30 7.2
6 1007 20 13.5Luego rómpelo a propósito, una vez cada uno:
- Quita los paréntesis en la pregunta 3. ¿Qué error aparece?
- Escribe la pregunta 4 como
orders[mask]["price"] = .... ¿Qué imprime pandas, y cambió algún precio? - Cambia
"east"por"East"en la pregunta 3. ¿Qué pasa, y por qué eso es peor que un error?
Solución
Planifica primero. Cada pregunta, desglosada:
quantity >= 10, una condición. A simple vista: 12, 30 y 20, así que 3.category == "accessories"ybranch != "north". A simple vista: 1004 (este, bottle) y 1006 (sur, bag).branchen sur/este yprice < 100. A simple vista: 1002 (notebook, 60.0) y 1007 (pen, 15.0).category == "stationery"yquantity >= 20. A simple vista: 1005 (eraser, 30) y 1007 (pen, 20), así que 2 filas.
La pregunta 4 requiere cuidado: «20 artículos o más» es >= 20, así que el pedido 1007, con exactamente 20 artículos, está incluido. Y escribe, así que usa .loc[mask, "price"] con una máscara con nombre, contada primero.
filters.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
print("1. Orders of 10+ items:")
print((orders["quantity"] >= 10).sum())
print()
print("2. Accessory orders outside north:")
outside = orders.loc[
(orders["category"] == "accessories") & (orders["branch"] != "north"),
["order_id", "branch", "product", "price"],
]
print(outside.reset_index(drop=True))
print()
print("3. South or east, price under 100:")
cheap = orders[
(orders["branch"].isin(["south", "east"])) & (orders["price"] < 100)
]
print(cheap)
print()
print("4. Stationery discount:")
discount = (orders["category"] == "stationery") & (orders["quantity"] >= 20)
print("Rows to change:", discount.sum())
orders.loc[discount, "price"] = orders.loc[discount, "price"] * 0.9
print(orders.loc[orders["category"] == "stationery", ["order_id", "quantity", "price"]])1. Orders of 10+ items:
3
2. Accessory orders outside north:
order_id branch product price
0 1004 east bottle 120.0
1 1006 south bag 850.0
3. South or east, price under 100:
order_id date branch product category quantity price
1 1002 2024-03-01 south notebook stationery 5 60.0
6 1007 2024-03-04 east pen stationery 20 15.0
4. Stationery discount:
Rows to change: 2
order_id quantity price
0 1001 12 15.0
1 1002 5 60.0
4 1005 30 7.2
6 1007 20 13.5Cada respuesta coincide con el plan. Notas sobre las decisiones:
- La pregunta 1 usa
sum()de una máscara, nolen(orders[...]). Ambas dan 3, pero la máscara dice «contar» directamente y no construye ninguna tabla. - La pregunta 2 usa
!=para «fuera de la sucursal norte».~(orders["branch"] == "north")significa lo mismo;!=es más sencillo para un solo valor. - La pregunta 3 conserva las etiquetas originales
1y6, porque la tarea no pedía renumerar. Esas etiquetas indican qué filas deorderseran. - La pregunta 4 cuenta antes de escribir, y el nuevo valor se calcula a partir de las mismas filas en el lado derecho:
orders.loc[discount, "price"] * 0.9. Ambos lados usan la misma máscara, así que cada fila obtiene el 90% de su propio precio: 8.0 se convierte en 7.2, 15.0 en 13.5. El pedido de bolígrafo1001(12 artículos) y el cuaderno (5 artículos) conservan sus precios.
Y las tres formas en que se rompe:
- Sin paréntesis, la pregunta 3 lanza
TypeError: Cannot perform 'rand_' …, porque&se aplicó antes que<. orders[discount]["price"] = …imprime la advertenciaChainedAssignmentError, el script continúa, y la tabla final sigue mostrando los precios antiguos8.0y15.0. La línea no hizo nada.- Con
"East", la pregunta 3 pierde silenciosamente el bolígrafo de la sucursal este e imprime una fila en lugar de dos. Sin error, sin advertencia: solo una respuesta incorrecta que parece correcta. Por eso el plan escribe las filas esperadas antes de que se ejecute el código. Lo único que detecta este error es una persona que ya sabía que la respuesta debería tener dos filas.
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