Pular para o conteúdo

    Capítulo 11, Básico

    Novas colunas

    Analisar não é só ler colunas, é criar informação a partir delas. Em aprendizado de máquina isso se chama *engenharia de atributos*: pegar as colunas que existem e transformá-las nas que você precisa.

    O ponto de partida: os dados limpos

    Daqui em diante cada capítulo começa pela mesma função, que reúne o que o capítulo 10 ensinou. Ela devolve a tabela com os tipos certos, os departamentos padronizados e sem repetições, mas ainda com os ausentes (preencher é uma decisão que cada análise toma):

    basico/cap11_novas_colunas.pylinhas 10 a 29
    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["data_admissao"] = pd.to_datetime(df["data_admissao"])
        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.shape, df.dtypes["salario"], df.dtypes["data_admissao"])
    
    Saída
    (14, 10) float64 datetime64[us]
    

    Criar e alterar colunas com aritmética

    As contas valem para a coluna inteira, como no NumPy, e o NaN passa adiante sem erro. O assign cria colunas em uma tabela nova, e é a forma que combina com o encadeamento (capítulo 30):

    basico/cap11_novas_colunas.pylinhas 34 a 38
    df["salario_anual"] = df["salario"] * 12
    nova = df.assign(bonus=(df["salario"] * df["nota_desempenho"] * 0.02).round(2))
    print(df[["nome", "salario", "salario_anual"]].head(4))
    print(nova[["nome", "bonus"]].head(4))
    print("bonus" in df.columns, "bonus" in nova.columns)
    
    Saída
               nome  salario  salario_anual
    0     Ana Souza   4500.0        54000.0
    1    Bruno Lima   4650.0        55800.0
    2  Carla Mendes   4800.0        57600.0
    3   Diego Rocha      NaN            NaN
               nome  bonus
    0     Ana Souza  360.0
    1    Bruno Lima  465.0
    2  Carla Mendes  576.0
    3   Diego Rocha    NaN
    False True
    

    Os funcionários sem salário ficam sem salário anual e sem bônus (NaN), porque uma conta com um ausente é ausente. O assign não alterou o df original.

    `apply`, `lambda` e `map`

    Quando a regra é específica demais para uma conta, o apply roda uma função para cada valor de uma coluna. Aqui, a classificação do desempenho:

    basico/cap11_novas_colunas.pylinhas 43 a 53
    def categoria_desempenho(nota):
        if nota >= 8:
            return "Excelente"
        elif nota >= 6:
            return "Bom"
        else:
            return "Precisa melhorar"
    
    
    df["categoria"] = df["nota_desempenho"].apply(categoria_desempenho)
    print(df["categoria"].value_counts().to_dict())
    
    Saída
    {'Precisa melhorar': 6, 'Bom': 4, 'Excelente': 4}
    

    Uma lambda é uma função pequena, escrita no lugar, para regras de uma linha. E o map com um dicionário é a ferramenta para trocar valores:

    basico/cap11_novas_colunas.pylinhas 55 a 58
    df["faixa_salarial"] = df["salario"].apply(lambda s: "Alta" if s > 6000 else "Normal")
    codigos = {"Engenharia": "ENG", "Marketing": "MKT", "Finanças": "FIN"}
    df["codigo_depto"] = df["departamento"].map(codigos)
    print(df[["nome", "faixa_salarial", "codigo_depto"]].head(5))
    
    Saída
                  nome faixa_salarial codigo_depto
    0        Ana Souza         Normal          ENG
    1       Bruno Lima         Normal          MKT
    2     Carla Mendes         Normal          ENG
    3      Diego Rocha         Normal          NaN
    4  Elisa Fernandes         Normal          NaN
    

    A regra para escolher: o map serve para substituir valor por valor (um dicionário), e o apply, para uma lógica que precisa de uma função de verdade, com decisões. Repare em duas coisas. Quem não está no dicionário (Ciência de Dados, RH, o ausente) virou NaN. E o Diego, que não tem salário, foi classificado como Normal: o apply recebeu um NaN, a comparação NaN > 6000 deu falso, e o ausente caiu na regra "senão". A função não sabe o que é um dado ausente, e quem classifica precisa decidir isso de forma explícita.

    O erro que não dá erro: a função sem `return`

    Uma função usada no apply que calcula, mas não devolve nada não gera erro: a coluna nova simplesmente se enche de None.

    basico/cap11_novas_colunas.pylinhas 63 a 71
    def sem_return(experiencia):
        if experiencia >= 5:
            "Sênior"
        else:
            "Pleno"
    
    
    df["nivel_errado"] = df["experiencia"].apply(sem_return)
    print(bool(df["nivel_errado"].isna().all()))
    
    Saída
    True
    

    Toda a coluna ficou ausente, em silêncio. Quando uma coluna criada com apply sai toda vazia, procure o return esquecido.

    Faixas sem `apply`: `pd.cut` e `np.select`

    O apply é flexível, mas roda uma função Python por linha, e perde a velocidade da vetorização (a mesma lição do NumPy). Para transformar números em faixas, o pd.cut faz o mesmo em uma passada, e o np.select faz o mesmo para qualquer lista de condições:

    basico/cap11_novas_colunas.pylinhas 76 a 88
    def nivel_por_apply(experiencia):
        return "Sênior" if experiencia >= 5 else ("Pleno" if experiencia >= 2 else "Iniciante")
    
    
    por_apply = df["experiencia"].apply(nivel_por_apply)
    por_cut = pd.cut(df["experiencia"], bins=[0, 2, 5, np.inf], right=False,
                     labels=["Iniciante", "Pleno", "Sênior"]).astype(str)
    por_select = pd.Series(
        np.select([df["experiencia"] >= 5, df["experiencia"] >= 2], ["Sênior", "Pleno"], default="Iniciante"),
        index=df.index,
    )
    print(bool((por_apply == por_cut).all()), bool((por_apply == por_select).all()))
    print(por_cut.value_counts().to_dict())
    
    Saída
    True True
    {'Pleno': 6, 'Sênior': 6, 'Iniciante': 2}
    

    As três versões dão o mesmo resultado. No pd.cut, o right=False faz os intervalos serem [0, 2), [2, 5) e [5, ∞), que é exatamente "menos de 2", "de 2 a 4" e "5 ou mais". Em dados grandes, a diferença de tempo é grande:

    basico/cap11_novas_colunas.pylinhas 90 a 103
    import time
    
    grande = pd.Series(np.random.default_rng(0).integers(0, 20, 200_000))
    
    
    def medir(funcao):
        inicio = time.perf_counter()
        funcao()
        return time.perf_counter() - inicio
    
    
    t_apply = medir(lambda: grande.apply(nivel_por_apply))
    t_cut = medir(lambda: pd.cut(grande, bins=[0, 2, 5, np.inf], right=False, labels=["I", "P", "S"]))
    print("o cut foi pelo menos 3 vezes mais rápido:", t_apply > 3 * t_cut)
    
    Saída
    o cut foi pelo menos 3 vezes mais rápido: True
    

    A ordem de escolha

    Primeiro a aritmética direta (df["a"] * 2). Depois pd.cut ou np.select para faixas. Depois map para trocar valores por um dicionário. O apply fica para a lógica que realmente precisa de uma função. É a mesma ordem do NumPy: vetorize sempre que der.

    Exercício 1

    Classificar o desempenho sem apply

    Reescreva categoria_desempenho com np.select (Excelente se a nota for 8 ou mais, Bom se for 6 ou mais, e Precisa melhorar nos demais casos), e confira que o resultado é igual ao do apply.