Pular para o conteúdo

    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:

    basico/cap12_ordenar_ranquear.pylinhas 10 a 30
    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))
    
    Saída
                   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.

    basico/cap12_ordenar_ranquear.pylinhas 32 a 34
    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])
    
    Saída
    [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):

    basico/cap12_ordenar_ranquear.pylinhas 39 a 41
    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())
    
    Saída
    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étodoDois empatados na 2ª posição recebemPróximo valor recebe
    average (padrão)2,5 e 2,54
    min2 e 24
    dense2 e 23 (sem buracos)
    first2 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.

    basico/cap12_ordenar_ranquear.pylinhas 43 a 44
    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))
    
    Saída
                   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():

    basico/cap12_ordenar_ranquear.pylinhas 49 a 51
    top3 = df.nlargest(3, "salario")
    print(top3[["nome", "salario"]])
    print(top3["salario"].tolist() == df.sort_values("salario", ascending=False).head(3)["salario"].tolist())
    
    Saída
                   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:

    basico/cap12_ordenar_ranquear.pylinhas 56 a 59
    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))
    
    Saída
    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:

    basico/cap12_ordenar_ranquear.pylinhas 61 a 64
    desempate = df.sort_values(
        ["nota_desempenho", "projetos_concluidos", "id_funcionario"], ascending=[False, False, True]
    ).head(5)
    print(desempate[["nome", "nota_desempenho", "projetos_concluidos"]])
    
    Saída
                   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).