Saltar al contenido

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:

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

EjemploOrdenar por una columna, y por varias
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
Varias columnas y varios sentidos

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

EjemploLos ausentes van al final, salvo que digas lo contrario
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

EjemploEl empate: dos ordenamientos correctos, dos archivos distintos
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
Los dos ordenamientos son correctos y dan resultados distintos

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

EjemploOrden total: no alcanza con que la ultima columna PAREZCA unica
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

EjemploEl top N, y el empate que se corta al medio
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

(sin error) corrí lo mismo dos veces y el archivo salió distinto

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.

(sin error) sort_values no ordenó nada

Causa: no se guardó el resultado. Como todo en pandas, sort_values devuelve una copia.

Arreglo: df = df.sort_values(...).

(sin error) los números se ordenaron como texto: 10 antes que 9

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.

(sin error) deduplicar por recencia se quedó con la fila vieja

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.

(sin error) el índice quedó desordenado en el archivo

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

Cómo se declara un orden que no admite discusión

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

Ordenar y desempatar
ExpresiónQué haceCuándo
df.sort_values("c")Ordena ascendente por esa columnaMirar
df.sort_values("c", ascending=False)DescendenteLos más grandes primero
df.sort_values(["a", "b"])Por a y, dentro, por bAgrupar visualmente
ascending=[True, False]Un sentido por columnaCategoría A-Z, precio de mayor a menor
na_position="first"Los ausentes al principioPara que nunca ganen al deduplicar
kind="stable"Los empates conservan su ordenSiempre que haya empates posibles
df.sort_index()Ordena por el índiceDespués de un groupby
df.nlargest(5, "c")Las 5 más grandesTop N, más claro que ordenar y cortar
df.nsmallest(5, "c")Las 5 más chicasÍdem
df["c"].is_uniqueboolAntes de declarar que un orden es total
df.reset_index(drop=True)Renumera después de ordenarÚltima línea, antes de escribir

Ponete a prueba

1. ¿Donde van los ausentes con df.sort_values("precio")?Nivel 1 · Recordar
2. ¿Que devuelve df.sort_values("precio")?Nivel 1 · Recordar
3. Corres df.sort_values("anio") dos veces sobre el mismo archivo, en la misma maquina, y el orden de dos filas cambia. ¿Es un bug?Nivel 2 · Predecir la salida
4. ¿Que agrega kind="stable"?Nivel 2 · Predecir la salida
5. ¿Cuando un orden es total?Nivel 3 · Elegir entre dos casi correctas
6. Ordenas por ["anio", "isbn"] y isbn.is_unique da False. ¿Que conviene?Nivel 3 · Elegir entre dos casi correctas
7. Este codigo tiene que quedarse con la version mas reciente de cada SKU. Corre sin error, devuelve la cantidad correcta de filas, y algunos precios estan mal. ¿Por que?
cat = pd.read_csv("catalogo_sistema_a.csv")
unico = cat.sort_values("actualizado").drop_duplicates(
    subset=["sku"], keep="last")
Nivel 4 · Encontrar el defecto

Prácticas de esta lección