Ordenar filas — y decidir qué ocurre con los empates
Ordenar filas con sort_values: una o varias columnas, cada una en su propia dirección, valores ausentes y texto con apariencia de números; además de nlargest para el "top N", y por qué un empate en el límite debe ser una decisión, no un accidente.
¿Cómo quieres aprender?
El problema que vamos a resolver
El dueño de la tienda envía un mensaje breve: «¿Cuáles fueron nuestros pedidos más grandes este mes? Envíame los tres primeros».
Los datos provienen del conocido orders.csv: ocho pedidos de las tres sucursales de la tienda —norte, sur y este—. Con ocho filas podrías responder a simple vista. Miras la columna quantity, encuentras el 30, luego el 20 y después el 12. Listo.
Ahora imagina el archivo real: cuatro mil pedidos. Leer una columna de arriba a abajo para encontrar los tres números más grandes ya no es un método, es una ilusión. Y el siguiente mensaje del dueño ya está en camino: «¿Puedes enviarme la lista agrupada por sucursal, con los más grandes primero dentro de cada sucursal? Y los pedidos más recientes arriba, por favor».
Cada una de esas peticiones es la misma operación: poner las filas en un orden útil y luego leer desde arriba. Eso es ordenar, y en pandas se hace con un solo método: sort_values().
Pero el método es la parte sencilla. Las verdaderas preguntas giran en torno a él, y ahí es donde los informes fallan:
- ¿«Más grande» según qué? ¿Cantidad, precio o los ingresos que generó el pedido? Cada criterio arroja unos tres primeros distintos.
- Cuando dos pedidos empatan, ¿cuál va primero? ¿Y cambia la respuesta cada vez que ejecutas el código?
- ¿Qué le ocurre a un pedido cuya cantidad está en blanco?
- Si una columna de números se leyó como texto,
"9"se ordena después de"30", y nada te advierte de ello.
Este capítulo enseña el método y, lo que es más importante, esas decisiones.
Al final de este capítulo puedes
- Ordenar una tabla por una o varias columnas con
sort_values(), cada una en su propia dirección, y conservar el resultado en lugar de perderlo - Explicar por qué las etiquetas del índice acompañan a sus filas y elegir entre conservarlas,
sort_index()yreset_index(drop=True) - Resolver empates deliberadamente y decidir a dónde van los valores ausentes con
na_position - Responder a preguntas de «top N» con
head(),nlargest()ynsmallest()—incluyendo el «top N de un subconjunto» y los empates en el límite de corte - Ordenar texto sin distinguir mayúsculas de minúsculas con
key=, y reconocer números almacenados como texto por la forma en que se ordenan
Requisitos previos: Datos ausentes: encontrar, descartar y rellenar huecos.
Crea el archivo primero
Todos los ejemplos de este capítulo leen orders.csv, el mismo archivo utilizado desde el capítulo cuatro. Si aún no lo tienes, créalo en la carpeta de tu proyecto exactamente con estas 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.0Una sección más adelante utiliza un segundo archivo, orders_gaps.csv. Es la misma tabla con dos cantidades dejadas en blanco —los pedidos 1002 y 1007—, el tipo de hueco que el capítulo seis te enseñó a encontrar:
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,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,,15.0
1008,2024-03-04,north,bottle,accessories,4,120.0Antes de escribir el código
Ordenar parece demasiado simple como para planificarlo. No lo es, porque una ordenación nunca falla: siempre produce un orden. Si ordenaste por el criterio equivocado, el resultado se ve tan ordenado como el correcto. Por tanto, la reflexión debe ocurrir antes del código.
1. Escribe la pregunta en una sola frase, incluyendo la columna. «Pedidos más grandes» no es una pregunta que pandas pueda responder. «Los tres pedidos con mayor quantity» sí lo es. También lo es «los tres pedidos con mayor price». Dan respuestas diferentes: la mochila de 850 es el artículo más caro, pero solo se vendieron dos mochilas. Decide a cuál se refiere el dueño. Si no puedes saberlo, pregunta; cuesta un mensaje, mientras que unos tres primeros incorrectos cuestan confianza.
2. Examina la entrada antes de ordenarla. Tres aspectos importan para ordenar: el número de filas, los tipos de las columnas por las que ordenarás y si esas columnas tienen huecos.
import pandas as pd
orders = pd.read_csv("orders.csv")
print(orders.shape)
print(orders.dtypes)
print(orders["quantity"].isna().sum())(8, 7)
order_id int64
date str
branch str
product str
category str
quantity int64
price float64
dtype: object
0Ocho filas, siete columnas. quantity es int64 y price es float64, por lo que se ordenarán como números. branch y product son str —texto, que se ordena alfabéticamente—. date también es str. Eso no es un problema aquí, y vale la pena conocer el motivo: las fechas escritas como YYYY-MM-DD se ordenan correctamente como texto, porque la parte más significativa va primero. Las fechas escritas como 4/3/2024 no lo harían. Y quantity no tiene huecos, por lo que todavía no necesitamos decidir a dónde van los valores ausentes.
3. Esboza el resultado esperado. Para los «tres primeros por cantidad», la respuesta deberían ser tres filas, de mayor a menor: 30 (goma de borrar, pedido 1005), luego 20 (bolígrafo, 1007) y luego 12 (bolígrafo, 1001). Anotar eso antes de ejecutar cualquier cosa te da una referencia contra la cual verificar el código.
4. Decide los detalles que necesita la ordenación.
- Clave: ¿qué columna o columnas deciden el orden? Eso se convierte en
by=. - Dirección: ¿de menor a mayor o de mayor a menor para cada clave? Eso se convierte en
ascending=. - Empates: si dos filas tienen el mismo valor, ¿cuál va primero? Eso se convierte en una segunda clave o en
kind="stable". - Huecos: ¿los valores ausentes deben ir al principio o al final? Eso se convierte en
na_position=. - Cuántos: ¿todas las filas o solo los primeros N? Eso se convierte en
.head(n)onlargest(). - Índice: ¿las etiquetas antiguas de las filas siguen teniendo significado después? Consérvalas o descártalas con
reset_index(drop=True).
5. Elige la herramienta.
- Cada fila, en el orden de una o varias columnas:
sort_values() - Solo los N primeros o últimos según una columna numérica:
nlargest()/nsmallest() - Las filas de nuevo en el orden del índice:
sort_index() - El top N dentro de un subconjunto («en la sucursal norte»): primero filtra, luego ordena y luego usa
head()
6. Ten claro cómo lo comprobarás. Después de ordenar, las primeras filas deben coincidir con tu boceto, shape no debe cambiar (ordenar nunca añade ni quita filas) y, para una clave numérica, la columna debe leerse de forma monótona a lo largo de la página.
sort_values() — una sola columna
Para que las impresiones en pantalla sean estrechas, los ejemplos muestran cinco de las siete columnas. cols es simplemente una lista de nombres utilizada con la selección de dobles corchetes del capítulo cuatro.
cols = ["order_id", "branch", "product", "quantity", "price"]
by_qty = orders.sort_values("quantity")
print(by_qty[cols])order_id branch product quantity price
5 1006 south bag 1 850.0
2 1003 north bag 2 850.0
7 1008 north bottle 4 120.0
1 1002 south notebook 5 60.0
3 1004 east bottle 7 120.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0
4 1005 north eraser 30 8.0El valor por defecto es ascendente —el más pequeño primero—. Lee la columna quantity hacia abajo: 1, 2, 4, 5, 7, 12, 20, 30. Esa es la comprobación del plan: una clave numérica solo debe ir en aumento.
Ahora mira el borde izquierdo. El índice ya no es 0, 1, 2, …. Muestra 5, 2, 7, 1, 3, 0, 6, 4.
Por qué el índice acompaña a la fila
Pandas no renumeró las filas, y eso es deliberado. Una etiqueta de índice es el nombre de la fila, no su posición. La etiqueta 5 era el pedido 1006 antes de ordenar y sigue siéndolo después. Si ordenar renumerase las filas, cada etiqueta apuntaría repentinamente a un pedido distinto, y cualquier nota que hubieras tomado sobre la «fila 5» pasaría silenciosamente a ser errónea.
Por lo tanto, después de una ordenación existen dos formas distintas de referirse a «la primera fila», y ya no coinciden:
print(by_qty.iloc[0]["order_id"])
print(by_qty.loc[0]["order_id"])1006
1001iloc[0] significa la posición cero —la primera fila en la página, el pedido 1006—. loc[0] significa la fila etiquetada como 0 —el pedido 1001, que ahora ocupa el sexto lugar en la página—. El capítulo cuatro estableció esta distinción; la ordenación es la operación que hace que importe. Cuando quieras «la primera fila después de ordenar», usa iloc o head(), ya que ambos cuentan posiciones.
Los mayores primero: ascending=False
print(orders.sort_values("quantity", ascending=False)[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0
3 1004 east bottle 7 120.0
1 1002 south notebook 5 60.0
7 1008 north bottle 4 120.0
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0Los tres primeros son 1005, 1007 y 1001 —exactamente el boceto del plan—. Para enviar únicamente esos tres, añade head(3):
top3 = orders.sort_values("quantity", ascending=False).head(3)
print(top3[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0Leer la cadena de izquierda a derecha reproduce el plan: toma orders, pon la cantidad mayor primero y conserva los tres primeros. Este patrón de «ordenar y luego head» responde a la mayoría de las preguntas sobre «top N».
El texto se ordena alfabéticamente
Una columna str se ordena según el orden del diccionario:
print(orders.sort_values("product")[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0
7 1008 north bottle 4 120.0
4 1005 north eraser 30 8.0
1 1002 south notebook 5 60.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0bag va antes que bottle porque la a va antes que la o. Dos mochilas, dos botellas y dos bolígrafos: estos son empates, y más adelante volveremos a la cuestión de cuál de los miembros del par empatado va primero.
Una ordenación devuelve una tabla nueva
Este es el fallo que casi todo el mundo comete alguna vez, y no genera ningún error:
orders.sort_values("quantity", ascending=False)
print(orders[cols].head(3))order_id branch product quantity price
0 1001 north pen 12 15.0
1 1002 south notebook 5 60.0
2 1003 north bag 2 850.0No se ordenó nada. sort_values() no modifica orders; construye y devuelve un DataFrame nuevo con el nuevo orden. La primera línea creó esa tabla ordenada y, como nadie la guardó, la descartó. orders sigue siendo exactamente lo que produjo read_csv.
¿Por qué funciona así pandas? Porque el orden original también es información. En orders.csv representa el orden en que llegaron los pedidos. Si cada ordenación reorganizara tu única copia, nunca podrías volver a «cómo estaba el archivo», y una ordenación escrita para un informe cambiaría silenciosamente los datos de entrada del siguiente. Devolver un objeto nuevo hace que siempre sea seguro probar una ordenación.
sort_values() nunca modifica la tabla sobre la que lo llamas. Si no asignas el resultado, la ordenación se ejecutó y se descartó —y nada te avisa de ello—.Por lo tanto, conservas el resultado, ya sea en un nombre nuevo o sobre el mismo nombre:
by_qty_desc = orders.sort_values("quantity", ascending=False) # keep both
print(by_qty_desc["order_id"].head(3).tolist())
print(orders["order_id"].head(3).tolist())[1005, 1007, 1001]
[1001, 1002, 1003]La copia ordenada tiene los pedidos grandes arriba; el original permanece intacto. Escribir orders = orders.sort_values(...) también es válido cuando estés seguro de que ya no necesitas el orden anterior.
Verás inplace=True en código antiguo. Modifica la tabla in situ y devuelve None, lo que provoca su propio error (consulta Cuando algo falla). Con Copy-on-Write en pandas 3, inplace=True tampoco ahorra memoria. Asignar el resultado es más claro: puedes ver en la misma línea a dónde va la tabla ordenada.
Varias columnas: prioridad y dirección
La segunda petición del dueño: «agrupado por sucursal, con los más grandes primero dentro de cada sucursal». Son dos claves, y el orden de la lista es el orden de prioridad:
by_branch = orders.sort_values(["branch", "quantity"], ascending=[True, False])
print(by_branch[cols])order_id branch product quantity price
6 1007 east pen 20 15.0
3 1004 east bottle 7 120.0
4 1005 north eraser 30 8.0
0 1001 north pen 12 15.0
7 1008 north bottle 4 120.0
2 1003 north bag 2 850.0
1 1002 south notebook 5 60.0
5 1006 south bag 1 850.0Léelo del mismo modo en que lo construyó pandas. Primero, cada fila se ubica por branch, de la A a la Z: east, north, south. La segunda clave, quantity, solo se consulta cuando la primera clave empata —dentro de east, dentro de north y dentro de south—. Dentro de north va 30, 12, 4, 2: el mayor primero, porque el segundo elemento de ascending es False.
ascending toma un True/False por clave, en el mismo orden que by. Un solo False se aplicaría a ambas claves y pondría south primero.
Invertir la lista cambia la pregunta por completo:
print(orders.sort_values(["quantity", "branch"], ascending=[False, True])[cols].head(4))order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0
3 1004 east bottle 7 120.0Ahora manda la cantidad, y branch solo importaría si dos pedidos tuvieran la misma cantidad —lo cual ninguno tiene—. Las sucursales quedan dispersas. Por lo tanto, la pregunta que debes hacerte antes de escribir la lista es: «¿en torno a qué se organiza el informe?». Esa columna va primero.
Empates: haz que el orden sea una decisión, no un accidente
Ordena por price, los más caros primero:
print(orders.sort_values("price", ascending=False)[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
7 1008 north bottle 4 120.0
3 1004 east bottle 7 120.0
1 1002 south notebook 5 60.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0
4 1005 north eraser 30 8.0Observa las dos botellas de 120. En el archivo, el pedido 1004 va antes que el 1008. En la tabla ordenada, 1008 va primero. Nada está roto: para una sola clave, el algoritmo de ordenación por defecto de pandas (quicksort) no promete mantener las filas empatadas en su orden original, y aquí no lo hizo. Las dos mochilas de 850 mantuvieron el orden del archivo, las dos botellas se intercambiaron; no había ninguna regla que hubieras podido predecir.
Normalmente eso no importa. Empieza a importar en el momento en que cortas la lista. Si el informe pide «los tres pedidos más caros», el tercer puesto es un empate entre 1004 y 1008, y cuál de ellos entra en la lista lo decidió el algoritmo, no tú. Ejecuta el mismo código con un archivo ligeramente distinto y la respuesta puede cambiar.
Hay dos formas de recuperar esa decisión.
Opción 1: una ordenación estable. kind="stable" promete que las filas empatadas conservan el orden que tenían antes de ordenar —aquí, el orden del archivo:
stable = orders.sort_values("price", ascending=False, kind="stable")
print(stable["order_id"].tolist())[1003, 1006, 1004, 1008, 1002, 1001, 1007, 1005]Opción 2 (habitualmente mejor): un criterio de desempate explícito. Indica qué debe ganar en caso de empate. Para un informe de ventas, «si el precio es el mismo, el pedido más grande primero» es una regla sensata:
tie_broken = orders.sort_values(["price", "quantity"], ascending=[False, False])
print(tie_broken[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0
7 1008 north bottle 4 120.0
1 1002 south notebook 5 60.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0
4 1005 north eraser 30 8.0Mira de nuevo las botellas: 1004 está por encima de 1008, porque 7 supera a 4. Y los dos bolígrafos de 15: antes, 1001 iba primero; ahora gana 1007, porque 20 supera a 12 —y ganará siempre, en cualquier ordenador, sin importar el orden en que llegue el archivo—. Un criterio de desempate convierte un accidente en una regla que puedes explicarle al dueño. (Cuando ordenas por más de una columna, la ordenación de pandas es estable de todos modos, por lo que una lista de claves tampoco mezcla empates reales).
Si no tienes una regla de negocio, order_id funciona muy bien como última clave: es único, por lo que después de él ya no queda ningún empate.
Valores ausentes: na_position
Lee la versión del archivo con dos cantidades en blanco:
gaps = pd.read_csv("orders_gaps.csv")
print(gaps["quantity"].isna().sum())
print(gaps.sort_values("quantity", ascending=False)[cols])2
order_id branch product quantity price
4 1005 north eraser 30.0 8.0
0 1001 north pen 12.0 15.0
3 1004 east bottle 7.0 120.0
7 1008 north bottle 4.0 120.0
2 1003 north bag 2.0 850.0
5 1006 south bag 1.0 850.0
1 1002 south notebook NaN 60.0
6 1007 east pen NaN 15.0Dos detalles que conviene notar. Primero, quantity ahora se imprime como 30.0, 12.0: los huecos convirtieron la columna en float64, como explicó el capítulo seis, porque NaN es un float. Segundo, las filas con NaN fueron a parar al final, a pesar de que pedimos de mayor a menor. Ese es el comportamiento por defecto, na_position="last", y se aplica en ambas direcciones: un valor ausente no es «pequeño» ni «grande», es desconocido, por lo que pandas lo aparta en lugar de adivinar.
Observa lo que eso le hizo a los tres primeros. El pedido 1007 realmente vendió 20 bolígrafos, pero su cantidad está en blanco en este archivo, por lo que quedó fuera de los tres primeros y el 1004 ocupó su lugar. Una ordenación no puede arreglar datos ausentes; solo puede mostrarte a dónde fueron a parar los huecos. Por eso el paso 2 del plan cuenta los huecos antes de ordenar.
Cuando estás auditando los datos, a menudo buscas lo contrario: los problemas arriba, donde puedas verlos:
print(gaps.sort_values("quantity", na_position="first")[cols].head(3))order_id branch product quantity price
1 1002 south notebook NaN 60.0
6 1007 east pen NaN 15.0
5 1006 south bag 1.0 850.0Así que na_position no es una simple preferencia de formato. "last" conviene para un informe, donde los huecos no deben desplazar a los valores reales. "first" conviene para una revisión, donde los huecos son exactamente lo que estás buscando.
De vuelta al inicio: sort_index() y reset_index()
Dado que cada fila conservó su etiqueta, el orden original nunca se pierde. Ordenar por el índice lo recupera:
restored = by_qty.sort_index()
print(restored["order_id"].tolist())[1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008]sort_index() también acepta ascending=False, que es como listarías una tabla «con la etiqueta más reciente primero».
A veces las etiquetas antiguas han dejado de tener significado. Si ordenas para generar una lista clasificada y enviarla, las etiquetas 4, 6, 0 al costado resultan confusas. reset_index(drop=True) las descarta y numera las filas 0, 1, 2, … en su nuevo orden:
ranked = orders.sort_values("quantity", ascending=False).reset_index(drop=True)
print(ranked[cols].head(3))
print(ranked.loc[0, "order_id"])order_id branch product quantity price
0 1005 north eraser 30 8.0
1 1007 east pen 20 15.0
2 1001 north pen 12 15.0
1005Ahora loc[0] e iloc[0] vuelven a coincidir: ambos son el pedido 1005, el más grande. Sin drop=True, pandas conservaría las etiquetas antiguas como una columna nueva llamada index —útil si quieres rastrear las filas de vuelta al origen, estorbo innecesario si no es así—.
¿Cuál elegir? Conserva las etiquetas mientras sigas trabajando con los datos y puedas necesitar cotejar las filas con el original. Restablécelas cuando el orden en sí sea el resultado: una clasificación, una lista de los diez primeros.
El atajo para los «top N»: nlargest() y nsmallest()
«Los tres primeros por cantidad» es tan habitual que pandas tiene un método que expresa exactamente eso:
print(orders.nlargest(3, "quantity")[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
6 1007 east pen 20 15.0
0 1001 north pen 12 15.0Las mismas filas que sort_values("quantity", ascending=False).head(3). ¿Por qué preferirlo? La línea expresa la intención —«los tres más grandes»— en lugar del mecanismo. Y en una tabla grande es más rápido, porque no necesita ordenar todas las filas simplemente para quedarse con tres.
nsmallest() es su reflejo: los tres artículos más baratos de la tienda:
print(orders.nsmallest(3, "price")[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
0 1001 north pen 12 15.0
6 1007 east pen 20 15.0Empates en el corte: keep=
Pide los tres artículos más caros y ocurrirá algo incómodo:
print(orders.nlargest(3, "price")[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0El tercer puesto es 120 —pero dos pedidos cuestan 120: 1004 y 1008—. pandas devolvió uno de ellos —con el valor por defecto keep="first", el que aparece primero en la tabla— y dejó el otro fuera silenciosamente. Si estás entregando un premio a «los tres primeros», eso resulta injusto para el 1008. keep="all" conserva cada fila que empata en el corte, incluso si eso significa más de N filas:
print(orders.nlargest(3, "price", keep="all")[cols])order_id branch product quantity price
2 1003 north bag 2 850.0
5 1006 south bag 1 850.0
3 1004 east bottle 7 120.0
7 1008 north bottle 4 120.0Cuatro filas para un «top tres». Eso no es un error: es la respuesta honesta cuando el tercer puesto es compartido. Que lo quieras o no es, una vez más, una decisión de planificación.
nlargest y nsmallest funcionan únicamente en columnas numéricas. Para texto, usa sort_values y head.
Top N de un subconjunto: filtrar, luego ordenar y luego cortar
«¿Cuáles fueron los dos pedidos más grandes en la sucursal norte?». Esto combina el capítulo cinco con este, y el orden de los pasos importa:
north = orders[orders["branch"] == "north"]
top_north = north.sort_values("quantity", ascending=False).head(2)
print(top_north[cols])order_id branch product quantity price
4 1005 north eraser 30 8.0
0 1001 north pen 12 15.0Filtra primero, porque la pregunta se refiere únicamente a la sucursal norte. Si ordenaras toda la tabla y tomaras head(2) primero, obtendrías 1005 y 1007 —y 1007 pertenece a la sucursal este—. Filtrar después dejaría entonces una sola fila y reportarías unos «dos primeros» con una sola entrada. La regla: restringe a las filas a las que se refiere la pregunta, luego clasifícalas y luego corta.
La misma cadena funciona con nlargest: orders[orders["branch"] == "north"].nlargest(2, "quantity").
Ordenar texto sin distinguir mayúsculas de minúsculas: key=
La ordenación de texto compara caracteres por su código, y cualquier letra mayúscula va antes que cualquier minúscula. Los datos introducidos por personas suelen mezclar ambas:
names = pd.DataFrame({"product": ["pen", "Notebook", "bag", "Bottle", "eraser"]})
print(names.sort_values("product"))product
3 Bottle
1 Notebook
2 bag
4 eraser
0 penBottle y Notebook se adelantan a bag simplemente porque empiezan con mayúscula. Ningún lector llamaría a eso orden alfabético.
Podrías corregir los datos, pero a menudo no deberías hacerlo: la grafía puede ser exactamente la que escribió el cliente. El argumento key= te permite ordenar según una versión transformada de la columna conservando los valores originales. Recibe una función que toma la columna completa y devuelve la versión por la cual ordenar:
print(names.sort_values("product", key=lambda col: col.str.lower()))product
2 bag
3 Bottle
4 eraser
1 Notebook
0 penLa copia en minúsculas se utiliza solo para comparar y luego se descarta. La tabla sigue mostrando Bottle y Notebook exactamente como se introdujeron. (lambda col: col.str.lower() es una función de una sola línea: dada la columna col, la devuelve en minúsculas).
Cuando los números se almacenan como texto
Esta es la trampa que el paso de planificación sobre dtypes pretende evitar. Supón que quantity llegó como texto: un "N/A" suelto en el archivo o un sistema que exporta todo como cadenas de texto. Para ver lo que produce, creamos una columna así a propósito con astype:
as_text = orders.astype({"quantity": "str"})
print(as_text["quantity"].dtype)
print(as_text.sort_values("quantity")[["order_id", "quantity"]])str
order_id quantity
5 1006 1
0 1001 12
2 1003 2
6 1007 20
4 1005 30
7 1008 4
1 1002 5
3 1004 71, 12, 2, 20, 30, 4, 5, 7. Ningún error y un orden que parece casi correcto. El texto se compara carácter por carácter: "12" empieza por 1, que es menor que 2, por lo que "12" va antes que "2" —del mismo modo que «ab» va antes que «b» en un diccionario—. La ordenación hizo exactamente lo que se le indicó. Se le indicó lo equivocado.
Dos pistas lo delatan: quantity aparece alineada a la izquierda en la salida impresa, que es como pandas muestra el texto, y dtype indica str. La solución real es convertir la columna a números (el pd.to_numeric del capítulo seis). Si debes dejar la columna como está, key= puede ordenar por el valor numérico en su lugar:
fixed = as_text.sort_values("quantity", key=pd.to_numeric)
print(fixed["quantity"].tolist())['1', '2', '4', '5', '7', '12', '20', '30']Ahora el orden es correcto, pero los valores siguen siendo cadenas de texto, por lo que no podrías sumarlos. Es preferible convertir la columna una sola vez, justo después de leer el archivo. Así, cada ordenación, filtro y suma posterior será correcto sin que tengas que acordarte.
Un ejemplo completo
La petición completa del dueño, respondida en un solo programa: (1) los tres pedidos principales por ingresos generados, (2) los pedidos más recientes primero, (3) una lista por sucursal con el pedido más grande primero en cada una.
«Ingresos generados» es cantidad multiplicada por precio. Añadir columnas se explica en detalle más adelante en el curso; por ahora una sola línea es suficiente: asignar a un nuevo nombre de columna la crea.
sorted_report.py:
import pandas as pd
orders = pd.read_csv("orders.csv")
# Check the input before ranking anything
print("Rows, columns:", orders.shape)
print("Blank quantities:", orders["quantity"].isna().sum())
print()
orders["revenue"] = orders["quantity"] * orders["price"]
cols = ["order_id", "date", "branch", "product", "revenue"]
# 1. Top three by revenue, ties broken by the larger quantity, renumbered as a ranking
top3 = (
orders.sort_values(["revenue", "quantity"], ascending=[False, False])
.head(3)
.reset_index(drop=True)
)
print("Top three orders by revenue")
print(top3[cols])
print()
# 2. Newest first; within the same day, the bigger order first
newest = orders.sort_values(["date", "revenue"], ascending=[False, False])
print("Newest first")
print(newest[cols].head(4))
print()
# 3. By branch A-Z, biggest order first inside each branch
by_branch = orders.sort_values(["branch", "revenue"], ascending=[True, False])
print("By branch")
print(by_branch[cols])
print()
print("Still eight rows:", by_branch.shape[0] == orders.shape[0])Rows, columns: (8, 7)
Blank quantities: 0
Top three orders by revenue
order_id date branch product revenue
0 1003 2024-03-02 north bag 1700.0
1 1006 2024-03-03 south bag 850.0
2 1004 2024-03-02 east bottle 840.0
Newest first
order_id date branch product revenue
7 1008 2024-03-04 north bottle 480.0
6 1007 2024-03-04 east pen 300.0
5 1006 2024-03-03 south bag 850.0
4 1005 2024-03-03 north eraser 240.0
By branch
order_id date branch product revenue
3 1004 2024-03-02 east bottle 840.0
6 1007 2024-03-04 east pen 300.0
2 1003 2024-03-02 north bag 1700.0
7 1008 2024-03-04 north bottle 480.0
4 1005 2024-03-03 north eraser 240.0
0 1001 2024-03-01 north pen 180.0
5 1006 2024-03-03 south bag 850.0
1 1002 2024-03-01 south notebook 300.0
Still eight rows: TruePor qué está escrito de esta manera:
- La comprobación va primero.
shapey el recuento de huecos se imprimen antes de cualquier clasificación. Un top tres calculado a partir de un archivo leído a medias, o con los huecos relegados al final, parecería igual de seguro de sí mismo. - «Más grande» se convirtió en una columna. Clasificar por
quantitypondría las 30 gomas de borrar arriba —240 de facturación—. Clasificar porrevenuepone arriba las dos mochilas por valor de 1700. El primer paso del plan —escribe la pregunta incluyendo la columna— es lo que decide qué respuesta recibe el dueño. - Cada ordenación tiene un criterio de desempate. El 2024-03-04 hay dos pedidos;
revenuedecide cuál se muestra primero, por lo que el informe es idéntico en cada ejecución. - Solo la clasificación se renumera.
top3es una lista terminada, por lo que sus etiquetas pasan a ser 0, 1, 2.newestyby_branchconservan sus etiquetas originales, de modo que cualquier fila aún puede rastrearse hasta el archivo. ordersnunca se vuelve a ordenar directamente. Cada informe es una tabla nueva construida a partir de la misma entrada intacta, por lo que las tres respuestas no pueden interferir entre sí.- La última línea es una aserción barata. Una ordenación nunca debe modificar el número de filas. Imprimir
Trueno cuesta nada y detectaría, por ejemplo, unhead()accidental colocado en el lugar erróneo.
Cuando algo falla
KeyError: 'Quantity'
orders.sort_values("Quantity")KeyError: 'Quantity'El nombre en by= debe coincidir exactamente con una columna: mayúsculas, minúsculas, espacios y todo. La columna es quantity, en minúsculas. Imprime list(orders.columns) para ver los nombres reales, con comillas y cualquier espacio suelto incluidos, tal como recomendaba el capítulo tres.
AttributeError: 'NoneType' object has no attribute 'head'
top = orders.sort_values("quantity", inplace=True)
top.head()AttributeError: 'NoneType' object has no attribute 'head'Con inplace=True, sort_values modifica orders y devuelve None. Por lo tanto, top es None, y None no tiene head(). Elige un solo estilo: o bien orders.sort_values("quantity", inplace=True) en su propia línea, o —mucho mejor— top = orders.sort_values("quantity") sin inplace.
ValueError: Length of ascending (1) != length of by (2)
orders.sort_values(["branch", "quantity"], ascending=[False])ValueError: Length of ascending (1) != length of by (2)Una lista para ascending necesita un elemento por clave. Dos claves, dos booleanos: ascending=[True, False]. (Un único False simple, fuera de una lista, está permitido: se aplica a todas las claves).
ValueError: For argument "ascending" expected type bool, received type str.
orders.sort_values("branch", ascending="False")ValueError: For argument "ascending" expected type bool, received type str."False" entre comillas es una cadena de texto, no el booleano False. Quita las comillas.
TypeError: Column 'branch' has dtype str, cannot use method 'nlargest' with this dtype
orders.nlargest(3, "branch")TypeError: Column 'branch' has dtype str, cannot use method 'nlargest' with this dtypenlargest y nsmallest solo clasifican números. Para «las tres últimas sucursales alfabéticamente», usa orders.sort_values("branch", ascending=False).head(3). Si obtuviste este error en una columna que esperabas que fuera numérica, el verdadero problema es que se leyó como texto; comprueba dtypes.
TypeError: '<' not supported between instances of 'str' and 'int'
mixed = pd.Series([12, "unknown", 5])
mixed.sort_values()TypeError: '<' not supported between instances of 'str' and 'int'Esta columna contiene números reales y un fragmento de texto, por lo que su dtype es object, y Python no puede determinar si "unknown" es mayor o menor que 12. Limpia la columna primero: pd.to_numeric(mixed, errors="coerce") convierte "unknown" en NaN, y entonces la ordenación funciona y na_position decide a dónde va el hueco.
Paso 4 de 6 — Predice
Comprueba lo que has entendido
Dos mochilas comparten el precio más alto, 850.0. ¿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))
top = orders.sort_values("price", ascending=False)
print(list(top.index[:3]))- A[2, 5, 7]
- B[0, 1, 2]
- C[2, 5, 3]
- D[5, 2, 7]
El objetivo es imprimir el pedido con más artículos. ¿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))
orders.sort_values("quantity", ascending=False)
print(orders.iloc[0]["order_id"])- A1001 — `sort_values` devolvió una copia ordenada y nadie la guardó
- B1007
- CUn `TypeError`: se debe asignar el resultado
- D1005
Quieres los pedidos agrupados por sucursal de la A a la Z, y dentro de cada sucursal la mayor cantidad primero. ¿Qué línea lo hace?
- Aorders.sort_values("branch").sort_values("quantity", ascending=False)
- Borders.sort_values(["branch", "quantity"], ascending=False)
- Corders.sort_values(["quantity", "branch"], ascending=[False, True])
- Dorders.sort_values(["branch", "quantity"], ascending=[True, False])
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
Un profesor te envía las notas de un examen. Guarda esto como results.csv. Dos alumnos todavía no tienen nota y algunos nombres se escribieron en minúsculas:
name,section,marks
Rafi,B,78
anika,A,91
Tanvir,A,
Mitu,B,91
zara,A,65
Karim,B,
Nadia,A,88
Sabbir,B,54Escribe ranking.py. Antes de escribir código, redacta el plan como comentarios al inicio del archivo: para cada lista, la columna clave, la dirección, cómo se resuelven los empates y a dónde van los huecos. Luego haz que el programa cumpla estas cuatro condiciones:
- Antes de ordenar nada, imprime
shape,dtypesy la cantidad de notas ausentes. - Imprime un cuadro de honor: la nota más alta primero, los alumnos sin nota al final, los empates en notas resueltos por nombre en orden alfabético ignorando mayúsculas y minúsculas, y las filas renumeradas 0, 1, 2, …
- Imprime los tres primeros con
nlargest, conservando a todos los que empaten en el tercer puesto. - Imprime los alumnos sin nota, ordenados por nombre ignorando mayúsculas y minúsculas, para que el profesor pueda reclamársela —y luego la tabla original, para demostrar que nunca se modificó—.
Tu programa debería imprimir exactamente esto:
(8, 3)
name str
section str
marks float64
dtype: object
Missing marks: 2
Merit list
name section marks
0 anika A 91.0
1 Mitu B 91.0
2 Nadia A 88.0
3 Rafi B 78.0
4 zara A 65.0
5 Sabbir B 54.0
6 Karim B NaN
7 Tanvir A NaN
Top three
name section marks
1 anika A 91.0
3 Mitu B 91.0
6 Nadia A 88.0
No mark yet
name section marks
5 Karim B NaN
2 Tanvir A NaN
Original, unchanged
name section marks
0 Rafi B 78.0
1 anika A 91.0
2 Tanvir A NaN
3 Mitu B 91.0
4 zara A 65.0
5 Karim B NaN
6 Nadia A 88.0
7 Sabbir B 54.0Luego prueba dos experimentos sobre el cuadro de honor:
- Quita
key=. ¿Cuál de los dos alumnos con 91 va primero ahora y por qué? - Quita
"name"por completo de las claves y ordena únicamente pormarks. ¿Quién va primero con 91? ¿Y está garantizado ese resultado?
Solución
El plan. El archivo tiene 8 alumnos y 3 columnas. marks tiene huecos, por lo que se leerá como float64, y cada lista que ordene por notas debe indicar a dónde van los huecos. name mezcla mayúsculas y minúsculas, por lo que cada comparación de nombres necesita key=.
- Cuadro de honor: claves
marksy luegoname; notas de mayor a menor, nombres de la A a la Z; el empate en notas se resuelve por nombre; huecos al final (por defecto); renumerado conreset_index(drop=True). - Tres primeros:
nlargest(3, "marks")conkeep="all", de modo que un tercer puesto compartido no quede fuera.nlargestexcluye los huecos por sí mismo. - Sin nota todavía: filtra las filas donde falta
marks, como en capítulos anteriores, y luego ordena por nombre. Primero filtra y luego ordena. - Original: nunca se vuelve a asignar, por lo que imprimir
resultsal final muestra el orden del archivo.
Un detalle: una función key= se aplica a cada columna clave. Para el cuadro de honor eso incluye también a marks, y .str.lower() fallaría con números. Por tanto, la función convierte a minúsculas solo las columnas de texto y devuelve cualquier otra columna sin cambios.
ranking.py:
import pandas as pd
# Merit list : marks high->low, ties by name A->Z ignoring case, blanks last, renumber
# Top three : nlargest on marks, keep="all" so a shared third place is not cut
# No mark yet : filter rows with no mark, then name A->Z ignoring case
# Original : never reassigned
results = pd.read_csv("results.csv")
print(results.shape)
print(results.dtypes)
print("Missing marks:", results["marks"].isna().sum())
print()
def lower_text(col):
return col.str.lower() if col.dtype == "str" else col
merit = results.sort_values(
["marks", "name"], ascending=[False, True], key=lower_text
).reset_index(drop=True)
print("Merit list")
print(merit)
print()
print("Top three")
print(results.nlargest(3, "marks", keep="all"))
print()
print("No mark yet")
missing = results[results["marks"].isna()]
print(missing.sort_values("name", key=lower_text))
print()
print("Original, unchanged")
print(results)(8, 3)
name str
section str
marks float64
dtype: object
Missing marks: 2
Merit list
name section marks
0 anika A 91.0
1 Mitu B 91.0
2 Nadia A 88.0
3 Rafi B 78.0
4 zara A 65.0
5 Sabbir B 54.0
6 Karim B NaN
7 Tanvir A NaN
Top three
name section marks
1 anika A 91.0
3 Mitu B 91.0
6 Nadia A 88.0
No mark yet
name section marks
5 Karim B NaN
2 Tanvir A NaN
Original, unchanged
name section marks
0 Rafi B 78.0
1 anika A 91.0
2 Tanvir A NaN
3 Mitu B 91.0
4 zara A 65.0
5 Karim B NaN
6 Nadia A 88.0
7 Sabbir B 54.0Leyendo la salida frente al plan:
marksesfloat64, noint64, yMissing marks: 2: los huecos se detectaron antes de cualquier clasificación, por lo que quien lee el cuadro de honor sabe que dos alumnos están ausentes de él, no al final de la lista.- El empate en 91 se resuelve por el nombre.
anikava antes queMituporque la comparación se realizó entre"anika"y"mitu". - Los tres primeros tienen tres filas aquí, porque el tercer puesto (88) no es compartido. Si Nadia también tuviera 91,
keep="all"devolvería cuatro filas en lugar de descartar silenciosamente a una alumna. - La lista sin nota primero filtra y luego ordena: el mismo orden de «restringir y luego clasificar» que el patrón de top N de un subconjunto.
- La última tabla es el orden del archivo. Se generaron tres listas y
resultsnunca se reasignó.
Los experimentos:
no_key = results.sort_values(["marks", "name"], ascending=[False, True])
marks_only = results.sort_values("marks", ascending=False)
print(no_key["name"].head(2).tolist())
print(marks_only["name"].head(2).tolist())['Mitu', 'anika']
['anika', 'Mitu']Sin key=, los nombres se comparan carácter por carácter, y cada letra mayúscula va antes que cualquier minúscula, por lo que "Mitu" supera a "anika" —un orden basado en cómo se escribió un nombre, no en el alfabeto—. Con marks solo, resulta que anika aparece primero porque está antes en el archivo. Esa es la misma respuesta que en el cuadro de honor correcto, pero solo por accidente: con una sola clave, la ordenación por defecto no promete conservar las filas empatadas en el orden del archivo, como mostraron antes las dos botellas de 120. Una regla que escribes está garantizada; un resultado que simplemente parece correcto no lo está.
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