Capítulo 14, Básico
O fluxo completo, do começo ao fim
Todos os passos que você aprendeu, encadeados como em uma análise de verdade, sobre o mesmo conjunto de dados. O curso original tem aqui uma armadilha no próprio roteiro, e eu a mostro primeiro.
O roteiro que quebra
O roteiro do curso original seleciona só algumas colunas (passo 3) e depois tenta preencher a idade e converter a data_admissao (passos 5 a 8), que ele acabou de descartar. Essa ordem falha, e é um bom exemplo de que a ordem dos passos faz parte da análise:
import numpy as np
import pandas as pd
pd.set_option("display.width", 170)
pd.set_option("display.max_columns", 20)
bruto = pd.read_csv("dados/funcionarios_15.csv")
so_essas = bruto[["id_funcionario", "nome", "departamento", "cidade", "salario",
"experiencia", "nota_desempenho", "projetos_concluidos"]]
try:
so_essas["idade"].fillna(so_essas["idade"].median())
except KeyError as erro:
print("KeyError:", erro)
KeyError: 'idade'
A regra que eu sigo: selecione colunas só no final, ou depois de tratar tudo o que você vai precisar delas.
O fluxo na ordem certa
Eu escrevo o fluxo como uma função: ela recebe o caminho do arquivo e devolve a tabela tratada e o resumo. Assim, o fluxo é testável, repetível e pode ser chamado com outro arquivo:
CANONICO = {"engenharia": "Engenharia", "marketing": "Marketing", "finanças": "Finanças",
"ciência de dados": "Ciência de Dados", "rh": "RH"}
def analisar(caminho: str) -> tuple[pd.DataFrame, pd.DataFrame]:
# 1 e 2. Carregar e inspecionar
df = pd.read_csv(caminho)
assert df["id_funcionario"].notna().all()
# 3. Corrigir os tipos (antes de qualquer conta)
df["salario"] = pd.to_numeric(df["salario"].str.replace("R$ ", "", regex=False))
df["data_admissao"] = pd.to_datetime(df["data_admissao"])
# 4. Padronizar o texto
df["departamento"] = df["departamento"].str.strip().str.lower().map(CANONICO)
df["departamento"] = df["departamento"].fillna("Não informado")
df["cidade"] = df["cidade"].fillna("Não informada")
# 5. Remover repetições pelo identificador
df = df.drop_duplicates(subset=["id_funcionario"], keep="first")
# 6. Preencher os ausentes numéricos com a mediana
df["salario"] = df["salario"].fillna(df["salario"].median())
df["idade"] = df["idade"].fillna(df["idade"].median())
# 7. Criar atributos
df["nivel"] = pd.cut(df["experiencia"], bins=[0, 2, 5, np.inf], right=False,
labels=["Iniciante", "Pleno", "Sênior"]).astype(str)
# 8. Selecionar e ordenar, só no final
df = df[["id_funcionario", "nome", "departamento", "cidade", "idade", "salario", "experiencia",
"nota_desempenho", "projetos_concluidos", "nivel"]]
df = df.sort_values(["nota_desempenho", "projetos_concluidos", "id_funcionario"],
ascending=[False, False, True]).reset_index(drop=True)
# 9. Resumir por departamento
resumo = df.groupby("departamento").agg(
salario_medio=("salario", "mean"),
nota_media=("nota_desempenho", "mean"),
funcionarios=("id_funcionario", "count"),
).reset_index()
return df, resumo
tabela, resumo = analisar("dados/funcionarios_15.csv")
print(tabela.shape, int(tabela.isna().sum().sum()))
print(resumo.round(1))
(14, 10) 0
departamento salario_medio nota_media funcionarios
0 Ciência de Dados 5962.5 6.0 2
1 Engenharia 5100.0 6.0 3
2 Finanças 5925.0 4.5 2
3 Marketing 5362.5 6.8 4
4 Não informado 5700.0 6.0 1
5 RH 5287.5 7.5 2
Todos os valores ausentes foram tratados (o 0 depois da forma), e o departamento "sem informação" ganhou um nome (Não informado) em vez de sumir do resumo. A soma da coluna funcionarios fecha com as 14 linhas.
As perguntas de negócio
A partir da tabela tratada, cada pergunta é uma ou duas linhas:
melhor_salario = resumo.loc[resumo["salario_medio"].idxmax()]
melhor_nota = resumo.loc[resumo["nota_media"].idxmax()]
print(melhor_salario["departamento"], round(float(melhor_salario["salario_medio"]), 1))
print(melhor_nota["departamento"], round(float(melhor_nota["nota_media"]), 2))
print(tabela["departamento"].value_counts().to_dict())
Ciência de Dados 5962.5
RH 7.5
{'Marketing': 4, 'Engenharia': 3, 'RH': 2, 'Ciência de Dados': 2, 'Finanças': 2, 'Não informado': 1}
Qual cidade tem mais funcionários? O jeito curto (value_counts().idxmax()) devolve uma cidade, e aqui isso esconde um empate:
por_cidade = tabela["cidade"].value_counts()
print(por_cidade.to_dict())
empatadas = por_cidade[por_cidade == por_cidade.max()].index.tolist()
print(sorted(empatadas), por_cidade.idxmax() in empatadas)
{'São Paulo': 3, 'Rio de Janeiro': 3, 'Porto Alegre': 3, 'Não informada': 2, 'Belo Horizonte': 2, 'Curitiba': 1}
['Porto Alegre', 'Rio de Janeiro', 'São Paulo'] True
Três cidades empatam no topo, e o idxmax escolheu uma sem avisar. A resposta honesta é a lista das empatadas. A mesma lição do capítulo 12 vale para o "top 5":
top5 = tabela.head(5)
print(top5[["nome", "nota_desempenho", "projetos_concluidos"]])
nome nota_desempenho projetos_concluidos
0 Felipe Araújo 9 7
1 Lucas Barbosa 9 3
2 Elisa Fernandes 8 6
3 Karina Alves 8 2
4 João Pereira 7 11
E a pergunta "quem tem mais experiência completa mais projetos?" se responde com a correlação:
print(round(float(tabela["experiencia"].corr(tabela["projetos_concluidos"])), 3))
0.217
O valor, 0,22, indica uma relação positiva, mas fraca: neste conjunto, quem tem mais experiência tende a concluir um pouco mais projetos, mas a experiência explica pouco. Com 14 pessoas, é uma indicação, e não uma conclusão.
Exercício 1
Mais uma pergunta de negócio
Usando tabela, descubra quantos funcionários de cada nível (Iniciante, Pleno, Sênior) têm nota de desempenho 8 ou mais, e confira que o total é igual ao número de linhas com nota 8 ou mais.