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):
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"])
(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):
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)
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:
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())
{'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:
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))
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.
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()))
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:
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())
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:
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)
o cut foi pelo menos 3 vezes mais rápido: True
A ordem de escolha
Primeiro a aritmética direta (
df["a"] * 2). Depoispd.cutounp.selectpara faixas. Depoismappara trocar valores por um dicionário. Oapplyfica 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.