Indice / pandas: fundamentos / 4.8
Ordenar y desempatar
Ordenar es fácil. Lo que casi nadie escribe - y lo que hace que dos personas que resuelven bien el mismo ejercicio entreguen archivos distintos - es qué pasa cuando dos filas empatan.
Para qué sirve
Ordenar aparece en tres momentos distintos, y en el tercero es obligatorio:
- Para mirar: los diez más caros, los últimos cargados.
- Para deduplicar por recencia: sin ordenar antes,
keep="last"significa «la última del archivo», que no tiene nada que ver con la más reciente (lección 5.5). - Para entregar un archivo: si el orden de las filas no está fijado, dos soluciones correctas producen archivos distintos y ninguna se puede comparar con la otra.
ELI5Explicado facil
Ordenás una fila de gente por altura y hay dos que miden exactamente lo mismo. ¿Cuál va primero?
Si no lo decidís vos, lo decide el azar - y si volvés a ordenar la fila mañana, puede quedar al revés. La solución no es elegir mejor: es agregar una segunda regla («si miden igual, por orden alfabético») que no pueda empatar.
El ejemplo mínimo
import pandas as pd
inventario = pd.read_csv("inventario_productos.csv")
caros = inventario.sort_values("precio_unitario", ascending=False)
print("los 4 mas caros:")
print(caros[["codigo", "categoria", "precio_unitario"]].head(4)
.to_string(index=False))
print()
# Varias columnas: se ordena por la primera y, dentro de cada grupo, por la
# segunda. ascending tambien puede ser una lista, una por columna.
por_grupo = inventario.sort_values(
["categoria", "precio_unitario"], ascending=[True, False])
print("por categoria, y adentro del mas caro al mas barato:")
print(por_grupo[["categoria", "codigo", "precio_unitario"]].head(6)
.to_string(index=False))
los 4 mas caros:
codigo categoria precio_unitario
P033 Energia 918.40
P028 Equipos 907.42
P010 Componentes 876.30
P003 Pantallas 872.23
por categoria, y adentro del mas caro al mas barato:
categoria codigo precio_unitario
Accesorios P018 817.57
Accesorios P017 734.66
Accesorios P019 495.38
Accesorios P035 71.60
Accesorios P020 34.85
Almacenamiento P009 848.60
sort_values(["categoria", "precio"], ascending=[True, False]) ordena
por categoría de la A a la Z y, dentro de cada categoría, del más caro al más barato.
ascending acepta un valor por columna: si le pasás uno solo, vale para
todas.
Los ausentes
import pandas as pd
libros = pd.read_csv("libros_catalogo.csv")
print("precios ausentes:", int(libros["precio"].isna().sum()))
por_defecto = libros.sort_values("precio")
print()
print("sort_values('precio') -> las ultimas 3 filas:")
print(por_defecto[["isbn", "precio"]].tail(3).to_string(index=False))
primero = libros.sort_values("precio", na_position="first")
print()
print("con na_position='first' -> las primeras 3:")
print(primero[["isbn", "precio"]].head(3).to_string(index=False))
print()
print("El default es 'last' y vale para ascendente y descendente por igual:")
descendente = libros.sort_values("precio", ascending=False)
print(descendente[["isbn", "precio"]].tail(3).to_string(index=False))
print()
print("Ojo con eso: si tomas las 10 primeras de un orden descendente")
print("los ausentes no molestan, pero si tomas las ultimas, si.")
precios ausentes: 2
sort_values('precio') -> las ultimas 3 filas:
isbn precio
978-9461-024 88.6
978-2045-006 NaN
978-4222-048 NaN
con na_position='first' -> las primeras 3:
isbn precio
978-2045-006 NaN
978-4222-048 NaN
978-4612-013 -5.0
El default es 'last' y vale para ascendente y descendente por igual:
isbn precio
978-4612-013 -5.0
978-2045-006 NaN
978-4222-048 NaN
Ojo con eso: si tomas las 10 primeras de un orden descendente
los ausentes no molestan, pero si tomas las ultimas, si.
El default es na_position="last", y vale igual para ascendente y
descendente: los ausentes van al final en los dos casos. Eso importa
cuando el orden decide qué fila sobrevive: en un
sort_values(...).drop_duplicates(keep="last"), con el default una fila
sin fecha le gana a una con fecha, que rara vez es lo que se quiere.
El empate
import pandas as pd
libros = pd.read_csv("libros_catalogo.csv")
repetidos = int((libros["anio"].value_counts() > 1).sum())
print("anios que se repiten:", repetidos, "-> hay empates de sobra")
por_defecto = libros.sort_values("anio")["isbn"].tolist()
estable = libros.sort_values("anio", kind="stable")["isbn"].tolist()
print()
print("mismo criterio, dos algoritmos:")
print("sort_values('anio') ->", por_defecto[:4])
print("sort_values('anio', kind='stable') ->", estable[:4])
print("son iguales:", por_defecto == estable)
print()
print("Los dos estan BIEN ordenados por anio. Lo que cambia es que hace")
print("cada uno con las filas que empatan, y eso no lo decide el criterio.")
print()
# La solucion no es elegir el algoritmo: es que no haya empates.
total = libros.sort_values(["anio", "isbn"], kind="stable")["isbn"].tolist()
otro = libros.sort_values(["anio", "isbn"])["isbn"].tolist()
print("agregando isbn como desempate, los dos algoritmos coinciden:",
total == otro)
anios que se repiten: 16 -> hay empates de sobra
mismo criterio, dos algoritmos:
sort_values('anio') -> ['978-2002-019', '978-9835-050', '978-6846-022', '978-7632-012']
sort_values('anio', kind='stable') -> ['978-2002-019', '978-9835-050', '978-7632-012', '978-6846-022']
son iguales: False
Los dos estan BIEN ordenados por anio. Lo que cambia es que hace
cada uno con las filas que empatan, y eso no lo decide el criterio.
agregando isbn como desempate, los dos algoritmos coinciden: True
El algoritmo por defecto de sort_values no es
estable: cuando dos filas empatan, no garantiza conservar el orden en que
venían. Y el catálogo de libros tiene 16 años repetidos, así que la diferencia es
real, no teórica.
kind="stable" resuelve la mitad del problema: hace el resultado
reproducible en tu máquina. Pero sigue dependiendo del orden del archivo de
entrada, que también puede cambiar.
Orden total
import pandas as pd
libros = pd.read_csv("libros_catalogo.csv")
salida = libros.sort_values(["anio", "isbn"], ascending=[False, True],
kind="stable")
print(salida[["anio", "isbn", "titulo"]].head(4).to_string(index=False))
print()
print("Parece un orden total: anio, y si empatan, isbn.")
print("isbn.is_unique:", libros["isbn"].is_unique)
print()
repetido = libros[libros["isbn"].duplicated(keep=False)]
print("y por eso queda UN empate sin resolver:")
print(repetido[["isbn", "anio", "titulo"]].to_string(index=False))
print()
print("Mismo isbn y mismo anio: las dos filas empatan en las dos columnas,")
print("y cual va primero lo sigue decidiendo el algoritmo.")
print()
# Un orden es total cuando la ultima columna es unica EN LA SALIDA. Aca hay
# dos caminos: resolver el duplicado antes, o agregar algo que si sea unico.
con_posicion = libros.assign(fila=range(len(libros)))
total = con_posicion.sort_values(["anio", "isbn", "fila"], kind="stable")
otro = con_posicion.sort_values(["anio", "isbn", "fila"], kind="quicksort")
print("agregando la posicion original como ultimo desempate:")
print("los dos algoritmos coinciden:",
total["fila"].tolist() == otro["fila"].tolist())
anio isbn titulo
2199.0 978-3226-020 Criptografía vol. 2
2026.0 978-5379-011 Algoritmos vol. 2
2026.0 978-6715-047 Álgebra lineal vol. 5
2025.0 978-4347-010 Criptografía vol. 1
Parece un orden total: anio, y si empatan, isbn.
isbn.is_unique: False
y por eso queda UN empate sin resolver:
isbn anio titulo
978-1000-001 2003.0 Algoritmos vol. 1
978-1000-001 2003.0 Sistemas operativos vol. 4
Mismo isbn y mismo anio: las dos filas empatan en las dos columnas,
y cual va primero lo sigue decidiendo el algoritmo.
agregando la posicion original como ultimo desempate:
los dos algoritmos coinciden: True
La regla del curso, y la que fija todo contrato de práctica: la lista de columnas del orden tiene que terminar en una columna que sea única en la salida. Con eso ningún par de filas puede empatar, y el algoritmo deja de tener opinión.
Y fijate el detalle del ejemplo: isbn parece único y no lo
es. Un orden total no se declara mirando el nombre de la columna - se comprueba
con is_unique.
El top N
import pandas as pd
inventario = pd.read_csv("inventario_productos.csv")
top = inventario.sort_values("cantidad", ascending=False).head(5)
print("los 5 productos con mas stock:")
print(top[["codigo", "producto", "cantidad"]].to_string(index=False))
print()
# nlargest hace lo mismo en una linea, y es mas claro cuando es lo unico
# que queres.
print("lo mismo con nlargest:")
print(inventario.nlargest(5, "cantidad")[["codigo", "cantidad"]]
.to_string(index=False))
print()
# El problema del top N: si hay empate en el borde, quien entra lo decide
# el algoritmo, no vos.
en_el_borde = inventario["cantidad"].value_counts()
empatados = en_el_borde[en_el_borde > 1]
print("cantidades que aparecen en mas de un producto:")
print(empatados.to_string())
print()
print("Si una de esas cae justo en el puesto 5, el 'top 5' cambia segun")
print("como se haya ordenado. Por eso el enunciado tiene que decir el")
print("desempate: 'top 5 por cantidad, y si empatan, por codigo'.")
los 5 productos con mas stock:
codigo producto cantidad
P023 Papel A4 (resma) 999999.0
P028 Notebook 15 pulgadas 117.0
P008 Disco SSD 1TB 114.0
P030 Docking station 113.0
P001 Teclado mecánico 112.0
lo mismo con nlargest:
codigo cantidad
P023 999999.0
P028 117.0
P008 114.0
P030 113.0
P001 112.0
cantidades que aparecen en mas de un producto:
cantidad
26.0 2
87.0 2
103.0 2
37.0 2
Si una de esas cae justo en el puesto 5, el 'top 5' cambia segun
como se haya ordenado. Por eso el enunciado tiene que decir el
desempate: 'top 5 por cantidad, y si empatan, por codigo'.
Un «top 5» tiene el mismo problema concentrado en el borde: si el quinto y el sexto empatan, cuál entra lo decide el algoritmo. Por eso los enunciados dicen «top 20 por importe, y si empatan, por id ascendente» en vez de «top 20 por importe».
Dónde se rompe
Causa: hay empates y el orden no es total. El algoritmo por defecto no es estable.
Arreglo: agregar una columna única al final de la lista de orden. Y
kind="stable", que no cuesta nada.
Causa: no se guardó el resultado. Como todo en pandas,
sort_values devuelve una copia.
Arreglo: df = df.sort_values(...).
Causa: la columna es str, casi siempre porque tiene un
valor no numérico adentro. Alfabéticamente, "10" va antes que
"9".
Arreglo: mirá dtypes y convertí con
to_numeric antes de ordenar. Es el mismo diagnóstico de siempre.
Causa: drop_duplicates(keep="last") sin ordenar antes, o
con na_position por defecto dejando ganar a una fila sin fecha.
Arreglo: ordenar por la fecha con
na_position="first" y kind="stable", y recién ahí
deduplicar.
Causa: sort_values reordena las filas pero se lleva las
etiquetas del índice con ellas.
Arreglo: reset_index(drop=True) antes de escribir, y
to_csv(index=False).
Acá hay que elegir
Un contrato de salida tiene que poder contestar estas tres preguntas:
- Por qué columnas y en qué sentido.
importe DESC, cliente_id ASC. - Dónde van los ausentes.
na_position, aunque hoy no haya ninguno: el archivo del mes que viene puede traerlos. - Qué pasa si empatan. Y la mejor respuesta es «no pueden»: terminar la lista en una columna única.
Las tres se escriben en una línea del enunciado y ahorran la discusión entera. «Ordenado por importe» no es una especificación; es una intención.
Recetario
| Expresión | Qué hace | Cuándo |
|---|---|---|
df.sort_values("c") | Ordena ascendente por esa columna | Mirar |
df.sort_values("c", ascending=False) | Descendente | Los más grandes primero |
df.sort_values(["a", "b"]) | Por a y, dentro, por b | Agrupar visualmente |
ascending=[True, False] | Un sentido por columna | Categoría A-Z, precio de mayor a menor |
na_position="first" | Los ausentes al principio | Para que nunca ganen al deduplicar |
kind="stable" | Los empates conservan su orden | Siempre que haya empates posibles |
df.sort_index() | Ordena por el índice | Después de un groupby |
df.nlargest(5, "c") | Las 5 más grandes | Top N, más claro que ordenar y cortar |
df.nsmallest(5, "c") | Las 5 más chicas | Ídem |
df["c"].is_unique | bool | Antes de declarar que un orden es total |
df.reset_index(drop=True) | Renumera después de ordenar | Última línea, antes de escribir |