Capítulo 12, Básico
Ordenar e ranquear
Depois de analisar, você quase sempre quer os extremos: os maiores, os menores, os melhores, os piores. Aqui estão as ferramentas, e as duas armadilhas que viram o placar de cabeça para baixo sem avisar.
Ordenar
O sort_values ordena por uma ou mais colunas. Com várias, a primeira é a chave principal, e as seguintes só desempatam. O ascending aceita uma lista, para direções diferentes por coluna:
import numpy as np
import pandas as pd
pd.set_option("display.width", 170)
pd.set_option("display.max_columns", 20)
CANONICO = {"engenharia": "Engenharia", "marketing": "Marketing", "finanças": "Finanças",
"ciência de dados": "Ciência de Dados", "rh": "RH"}
def carregar_limpo() -> pd.DataFrame:
df = pd.read_csv("dados/funcionarios_15.csv")
df["salario"] = pd.to_numeric(df["salario"].str.replace("R$ ", "", regex=False))
df["departamento"] = df["departamento"].str.strip().str.lower().map(CANONICO)
return df.drop_duplicates(subset=["id_funcionario"], keep="first").reset_index(drop=True)
df = carregar_limpo()
print(df.sort_values("salario", ascending=False)[["nome", "salario"]].head(4))
por_depto = df.sort_values(["departamento", "salario"], ascending=[True, False])
print(por_depto[["nome", "departamento", "salario"]].head(6))
nome salario
13 Nelson Ribeiro 6450.0
12 Marina Teixeira 6300.0
11 Lucas Barbosa 6150.0
10 Karina Alves 6000.0
nome departamento salario
13 Nelson Ribeiro Ciência de Dados 6450.0
3 Diego Rocha Ciência de Dados NaN
10 Karina Alves Engenharia 6000.0
2 Carla Mendes Engenharia 4800.0
0 Ana Souza Engenharia 4500.0
12 Marina Teixeira Finanças 6300.0
No segundo resultado, os departamentos ficam em ordem alfabética, e só dentro de cada um o salário ordena do maior para o menor. Dois detalhes: os salários ausentes (NaN) vão para o fim, em qualquer direção, e dá para mudar isso com na_position="first". E o sort_values devolve uma tabela nova: o original não muda, a não ser que você reatribua.
print(df.sort_values("salario", na_position="first")["salario"].head(3).tolist())
print(df["nome"].head(3).tolist() == ["Ana Souza", "Bruno Lima", "Carla Mendes"])
print(df.sort_values("salario", ascending=False).index.tolist()[:4])
[nan, nan, 4500.0]
True
[13, 12, 11, 10]
O índice acompanha as linhas: o resultado acima, [13, 12, 11, 10], mostra que o primeiro salário da lista vem da linha 13. Com ignore_index=True, ele é renumerado de 0 em diante. Em ordenações por mais de uma passada, use kind="stable", que mantém a ordem original entre valores iguais.
Ranquear
O rank dá a posição de cada valor, e o que fazer com os empates muda o resultado. Por padrão, os empatados dividem a posição (a média das posições que ocupariam):
pontos = pd.Series([100, 90, 90, 80], index=["a", "b", "c", "d"])
for metodo in ("average", "min", "dense", "first"):
print(f"{metodo:<8}", pontos.rank(ascending=False, method=metodo).tolist())
average [1.0, 2.5, 2.5, 4.0]
min [1.0, 2.0, 2.0, 4.0]
dense [1.0, 2.0, 2.0, 3.0]
first [1.0, 2.0, 3.0, 4.0]
| Método | Dois empatados na 2ª posição recebem | Próximo valor recebe |
|---|---|---|
average (padrão) | 2,5 e 2,5 | 4 |
min | 2 e 2 | 4 |
dense | 2 e 2 | 3 (sem buracos) |
first | 2 e 3 (pela ordem em que aparecem) | 4 |
O padrão está certo quando você quer a média, mas, para um placar (1º, 2º, 2º, 4º), o que se espera quase sempre é o min.
df["posicao_nota"] = df["nota_desempenho"].rank(ascending=False, method="min").astype(int)
print(df.sort_values("posicao_nota")[["nome", "nota_desempenho", "posicao_nota"]].head(5))
nome nota_desempenho posicao_nota
5 Felipe Araújo 9 1
11 Lucas Barbosa 9 1
10 Karina Alves 8 3
4 Elisa Fernandes 8 3
3 Diego Rocha 7 5
Os maiores e os menores
O nlargest e o nsmallest devolvem os extremos sem ordenar a coluna inteira, o que é mais rápido em tabelas grandes. O resultado é o mesmo de sort_values().head():
top3 = df.nlargest(3, "salario")
print(top3[["nome", "salario"]])
print(top3["salario"].tolist() == df.sort_values("salario", ascending=False).head(3)["salario"].tolist())
nome salario
13 Nelson Ribeiro 6450.0
12 Marina Teixeira 6300.0
11 Lucas Barbosa 6150.0
True
A armadilha do empate no top N
Quando há empates, qual deles entra no top N? Observe as notas: há várias pessoas com a mesma nota, e o nlargest precisa cortar em algum lugar:
cinco = df.nlargest(5, "nota_desempenho")
todos = df.nlargest(5, "nota_desempenho", keep="all")
print(len(cinco), len(todos))
print(sorted(df["nota_desempenho"].nlargest(7).tolist(), reverse=True))
5 6
[9, 9, 8, 8, 7, 7, 6]
Pediram 5, e o keep="all" devolveu mais, porque a 5ª nota está empatada com outras. O padrão (keep="first") escolhe pela ordem em que as linhas aparecem, que é um critério acidental: se a tabela chegasse em outra ordem, o top 5 mudaria. Em qualquer ranking que importa, defina o desempate de forma explícita:
desempate = df.sort_values(
["nota_desempenho", "projetos_concluidos", "id_funcionario"], ascending=[False, False, True]
).head(5)
print(desempate[["nome", "nota_desempenho", "projetos_concluidos"]])
nome nota_desempenho projetos_concluidos
5 Felipe Araújo 9 7
11 Lucas Barbosa 9 3
4 Elisa Fernandes 8 6
10 Karina Alves 8 2
9 João Pereira 7 11
Agora o critério é: maior nota, depois mais projetos, depois o menor id. Qualquer pessoa chega ao mesmo resultado.
Decida antes se maior é melhor
A nota de desempenho: maior é melhor. Uma coluna "dias de atraso": menor é melhor. Errar a direção vira o placar de cabeça para baixo sem nenhum erro. Antes de ranquear, escreva em um comentário qual é o sentido.
Exercício 1
Os três mais experientes de cada nota
Escreva top_por_projetos(tabela, n), que devolva os n funcionários com mais projetos concluídos, com desempate por menor id_funcionario, e confira que o resultado não depende da ordem das linhas (embaralhe a tabela antes de chamar).