Chapter 14, Basic
The complete flow, from start to finish
All the steps you learned, chained together as in a real analysis, on the same dataset. The original course has a trap in its own script, and I show it first.
The script that breaks
The original course script selects only a few columns (step 3) and then tries to fill the idade and convert the data_admissao (steps 5 to 8), which it just discarded. This order fails, and it is a good example that the order of the steps is part of the analysis:
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'
The rule I follow: select columns only at the end, or then to handle everything you will need from them.
The flow in the right order
I write the flow as a function: it receives the file path and returns the processed table and the summary. Thus, the flow is testable, repeatable, and can be called with another file:
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
All missing values have been handled (the 0 after the form), and the department "no information" got a name (Não informado) instead of disappearing from the summary. The sum of the column funcionarios closes with the 14 rows.
The business questions
From the processed table, each question is one or two lines:
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}
Which city has the most employees? The short way (value_counts().idxmax()) returns a city, and here it hides a tie:
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
Three cities tie at the top, and the idxmax chose one without warning. The honest answer is the list of the tied ones. The same lesson from chapter 12 applies to the "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
And the question "who has more experience completes more projects?" is answered with the correlation:
print(round(float(tabela["experiencia"].corr(tabela["projetos_concluidos"])), 3))
0.217
The value, 0.22, indicates a positive, but weak: in this set, those with more experience tend to complete a few more projects, but experience explains little. With 14 people, it is an indication, not a conclusion.
Exercise 1
One more business question
Using tabela, find out how many employees at each level (Iniciante, Pleno, Sênior) have a performance score of 8 or higher, and check that the total equals the number of rows with a score of 8 or higher.