Esta aula aprofunda o Pandas nas operações que dominam o dia a dia de um projeto real: limpeza avançada, agrupamento com groupby, combinação de DataFrames com merge e join, e transformações com apply e map. É o ferramental que prepara os dados antes de qualquer modelagem.
O custo de dados sujos
Existe um ditado clássico em ciência de dados: "garbage in, garbage out". Um modelo treinado com dados sujos produz predições ruins, independente de quão sofisticado seja o algoritmo.
Na prática, cientistas de dados gastam entre 60% e 80% do tempo de um projeto limpando e preparando dados. As tarefas mais comuns são:
- Remover ou preencher valores ausentes
- Corrigir tipos de dados incorretos
- Remover duplicatas
- Tratar outliers
- Padronizar formatos (datas, strings, categorias)
- Combinar dados de múltiplas fontes
Limpeza Avançada de Dados
Duplicatas
import pandas as pd
import numpy as np
df = pd.DataFrame({
"id": [1, 2, 2, 3, 4, 4, 4],
"nome": ["Ana", "Bruno", "Bruno", "Carla", "Diego", "Diego", "Diego"],
"nota": [8.5, 7.0, 7.0, 9.2, 6.5, 6.5, 6.8]
})
# Identificar duplicatas
print(df.duplicated())
print(df.duplicated().sum()) # quantas linhas duplicadas
# Mostrar as linhas duplicadas
print(df[df.duplicated()])
# Duplicatas considerando apenas certas colunas
print(df.duplicated(subset=["id", "nome"]))
# Remover duplicatas — manter primeira ocorrência
df_unico = df.drop_duplicates()
# Manter última ocorrência
df_unico_ultimo = df.drop_duplicates(keep="last")
# Remover duplicatas por colunas específicas
df_sem_dup_nome = df.drop_duplicates(subset=["nome"])
print(f"Original: {len(df)} linhas")
print(f"Sem duplicatas: {len(df_unico)} linhas")
Outliers
np.random.seed(42)
df = pd.DataFrame({
"salario": np.concatenate([
np.random.normal(5000, 1000, 95),
[50000, 55000, 60000, -1000, 200000] # outliers artificiais
])
})
# Método 1: Z-score — remove valores a mais de N desvios padrão da média
media = df["salario"].mean()
desvio = df["salario"].std()
z_scores = (df["salario"] - media) / desvio
df_sem_outliers_z = df[z_scores.abs() < 3]
print(f"Z-score: {len(df)} → {len(df_sem_outliers_z)} linhas")
# Método 2: IQR (Interquartile Range) — mais robusto a outliers extremos
Q1 = df["salario"].quantile(0.25)
Q3 = df["salario"].quantile(0.75)
IQR = Q3 - Q1
limite_inferior = Q1 - 1.5 * IQR
limite_superior = Q3 + 1.5 * IQR
df_sem_outliers_iqr = df[
(df["salario"] >= limite_inferior) &
(df["salario"] <= limite_superior)
]
print(f"IQR: {len(df)} → {len(df_sem_outliers_iqr)} linhas")
print(f"Limites IQR: [{limite_inferior:.0f}, {limite_superior:.0f}]")
# Método 3: Clipping — em vez de remover, limitar ao intervalo
df["salario_clipado"] = df["salario"].clip(
lower=limite_inferior,
upper=limite_superior
)
Conversão e padronização de tipos
df = pd.DataFrame({
"data_cadastro": ["2024-01-15", "2024-02-20", "2024-03-10"],
"valor": ["R$ 1.500,00", "R$ 2.300,50", "R$ 890,00"],
"ativo": ["sim", "não", "sim"],
"categoria": ["A", "B", "A"]
})
# Converter strings de data para datetime
df["data_cadastro"] = pd.to_datetime(df["data_cadastro"])
print(df["data_cadastro"].dtype) # datetime64[ns]
# Extrair componentes de data
df["ano"] = df["data_cadastro"].dt.year
df["mes"] = df["data_cadastro"].dt.month
df["dia"] = df["data_cadastro"].dt.day
df["dia_semana"] = df["data_cadastro"].dt.day_name()
# Limpar e converter coluna de valor monetário
df["valor_num"] = (df["valor"]
.str.replace("R$ ", "", regex=False)
.str.replace(".", "", regex=False)
.str.replace(",", ".", regex=False)
.astype(float))
# Converter booleano textual
df["ativo_bool"] = df["ativo"].map({"sim": True, "não": False})
# Converter para categoria (economiza memória para strings repetidas)
df["categoria"] = df["categoria"].astype("category")
print(df[["data_cadastro", "valor_num", "ativo_bool", "categoria"]])
Saída:
data_cadastro valor_num ativo_bool categoria
0 2024-01-15 1500.00 True A
1 2024-02-20 2300.50 False B
2 2024-03-10 890.00 True A
Padronização de strings
df = pd.DataFrame({
"cidade": ["são paulo", "SÃO PAULO", "São Paulo ", " são paulo",
"rio de janeiro", "RIO DE JANEIRO", "Rio De Janeiro"]
})
# Padronizar: strip + title case
df["cidade_std"] = df["cidade"].str.strip().str.title()
print(df["cidade_std"].value_counts())
# São Paulo 4
# Rio De Janeiro 3
Transformações com apply e map
apply em Series
Aplica uma função a cada elemento de uma coluna:
df = pd.DataFrame({
"nome": ["Ana", "Bruno", "Carla"],
"nota": [8.5, 5.5, 9.2],
"idade": [25, 30, 22]
})
# apply com lambda
df["nota_categoria"] = df["nota"].apply(
lambda n: "Excelente" if n >= 9 else "Bom" if n >= 7 else "Regular"
)
# apply com função nomeada
def classificar_idade(idade):
if idade < 25:
return "jovem"
elif idade < 35:
return "adulto"
else:
return "sênior"
df["faixa_etaria"] = df["idade"].apply(classificar_idade)
print(df)
apply em DataFrame (axis=1)
Aplica uma função a cada linha, recebendo a linha como Series:
df = pd.DataFrame({
"nota1": [8.0, 6.0, 9.5],
"nota2": [7.5, 5.5, 8.5],
"nota3": [9.0, 7.0, 10.0]
})
# Calcular média ponderada (nota3 tem peso 2)
def media_ponderada(linha):
return (linha["nota1"] + linha["nota2"] + 2 * linha["nota3"]) / 4
df["media_pond"] = df.apply(media_ponderada, axis=1)
print(df)
map — substituição por dicionário
df = pd.DataFrame({
"cidade": ["SP", "RJ", "MG", "SP", "RJ"],
"regiao": ["SE", "SE", "SE", "SE", "SE"]
})
# Mapear abreviações para nomes completos
mapa_cidades = {
"SP": "São Paulo",
"RJ": "Rio de Janeiro",
"MG": "Minas Gerais"
}
df["cidade_completa"] = df["cidade"].map(mapa_cidades)
# Mapear labels para números (encoding manual)
mapa_regioes = {"SE": 0, "S": 1, "N": 2, "NE": 3, "CO": 4}
df["regiao_cod"] = df["regiao"].map(mapa_regioes)
print(df)
applymap / map para DataFrame inteiro
# Aplicar função a todos os elementos numéricos
df_notas = pd.DataFrame({
"mat": [8.0, 6.5, 9.0],
"por": [7.5, 7.0, 8.5],
"cie": [9.0, 5.5, 7.0]
})
# Arredondar para 1 casa decimal
df_arredondado = df_notas.map(lambda x: round(x, 1))
# Normalizar todos os valores entre 0 e 1
df_norm = df_notas.apply(lambda col: (col - col.min()) / (col.max() - col.min()))
print(df_norm.round(3))
groupby — Agrupamento de Dados
groupby é uma das operações mais poderosas do Pandas. Segue a lógica "split-apply-combine":
1. Divide o DataFrame em grupos
2. Aplica uma função a cada grupo
3. Combina os resultados
df = pd.DataFrame({
"departamento": ["TI", "RH", "TI", "Vendas", "RH", "TI", "Vendas"],
"funcionario": ["Ana", "Bruno", "Carla", "Diego", "Eva", "Fábio", "Gabi"],
"salario": [8000, 5000, 9500, 6000, 5500, 7500, 7000],
"anos": [5, 2, 8, 3, 1, 4, 6],
"avaliacao": [4, 3, 5, 4, 2, 4, 5]
})
Agregações simples
# Média de salários por departamento
print(df.groupby("departamento")["salario"].mean())
# Múltiplas agregações em uma coluna
print(df.groupby("departamento")["salario"].agg(["mean", "min", "max", "std"]))
# Múltiplas colunas com múltiplas agregações
resultado = df.groupby("departamento").agg({
"salario": ["mean", "max"],
"anos": "mean",
"avaliacao": "mean"
})
print(resultado.round(2))
Saída:
salario anos avaliacao
mean max mean mean
departamento
RH 5250.00 5500 1.5 2.5
TI 8333.33 9500 5.67 4.33
Vendas 6500.00 7000 4.5 4.5
Agregações customizadas
# Função customizada de agregação
def amplitude(serie):
return serie.max() - serie.min()
resultado = df.groupby("departamento")["salario"].agg(amplitude)
print("Amplitude salarial por departamento:")
print(resultado)
transform — manter o tamanho original
Diferente de agg (que reduz), transform retorna um resultado com o mesmo tamanho do DataFrame original. Muito útil para criar features.
# Média do departamento em cada linha
df["media_depto"] = df.groupby("departamento")["salario"].transform("mean")
# Salário normalizado dentro do departamento
df["salario_norm"] = (
df.groupby("departamento")["salario"]
.transform(lambda x: (x - x.mean()) / x.std())
)
print(df[["funcionario", "departamento", "salario", "media_depto", "salario_norm"]].round(2))
Saída:
funcionario departamento salario media_depto salario_norm
0 Ana TI 8000 8333.33 -0.33
1 Bruno RH 5000 5250.00 -0.71
2 Carla TI 9500 8333.33 1.17
3 Diego Vendas 6000 6500.00 -0.71
4 Eva RH 5500 5250.00 0.71
5 Fábio TI 7500 8333.33 -0.83
6 Gabi Vendas 7000 6500.00 0.71
filter — filtrar grupos inteiros
# Manter apenas departamentos com salário médio acima de 6000
deptos_bem_pagos = df.groupby("departamento").filter(
lambda grupo: grupo["salario"].mean() > 6000
)
print(deptos_bem_pagos)
Agrupamento por múltiplas colunas
df2 = pd.DataFrame({
"departamento": ["TI", "TI", "TI", "RH", "RH"],
"nivel": ["junior", "senior", "senior", "junior", "senior"],
"salario": [5000, 9000, 10000, 4000, 6000]
})
resultado = df2.groupby(["departamento", "nivel"])["salario"].agg(["mean", "count"])
print(resultado)
Saída:
mean count
departamento nivel
RH junior 4000.0 1
senior 6000.0 1
TI junior 5000.0 1
senior 9500.0 2
Merge e Join — Combinando DataFrames
merge — equivalente ao SQL JOIN
clientes = pd.DataFrame({
"cliente_id": [1, 2, 3, 4],
"nome": ["Ana", "Bruno", "Carla", "Diego"],
"cidade": ["SP", "RJ", "SP", "MG"]
})
pedidos = pd.DataFrame({
"pedido_id": [101, 102, 103, 104, 105],
"cliente_id": [1, 2, 1, 3, 5], # cliente 5 não existe
"valor": [150.0, 300.0, 75.0, 200.0, 100.0],
"produto": ["A", "B", "C", "A", "D"]
})
INNER JOIN — apenas registros com match em ambos:
inner = pd.merge(clientes, pedidos, on="cliente_id", how="inner")
print("INNER JOIN:")
print(inner)
Saída:
cliente_id nome cidade pedido_id valor produto
0 1 Ana SP 101 150.0 A
1 1 Ana SP 103 75.0 C
2 2 Bruno RJ 102 300.0 B
3 3 Carla SP 104 200.0 A
LEFT JOIN — todos os clientes, mesmo sem pedidos:
left = pd.merge(clientes, pedidos, on="cliente_id", how="left")
print("\nLEFT JOIN:")
print(left)
Saída:
cliente_id nome cidade pedido_id valor produto
0 1 Ana SP 101.0 150.0 A
1 1 Ana SP 103.0 75.0 C
2 2 Bruno RJ 102.0 300.0 B
3 3 Carla SP 104.0 200.0 A
4 4 Diego MG NaN NaN NaN
RIGHT JOIN — todos os pedidos, mesmo sem cliente correspondente:
right = pd.merge(clientes, pedidos, on="cliente_id", how="right")
print("\nRIGHT JOIN:")
print(right)
OUTER JOIN — todos os registros de ambos:
outer = pd.merge(clientes, pedidos, on="cliente_id", how="outer")
print("\nOUTER JOIN:")
print(outer)
Merge com chaves diferentes
df_a = pd.DataFrame({
"id_cliente": [1, 2, 3],
"nome": ["Ana", "Bruno", "Carla"]
})
df_b = pd.DataFrame({
"cod_cliente": [1, 2, 4],
"saldo": [100, 200, 300]
})
resultado = pd.merge(df_a, df_b,
left_on="id_cliente",
right_on="cod_cliente",
how="inner")
print(resultado)
Merge com suffixes para colunas duplicadas
notas_1 = pd.DataFrame({"aluno_id": [1, 2, 3], "nota": [8.0, 7.0, 9.0]})
notas_2 = pd.DataFrame({"aluno_id": [1, 2, 3], "nota": [7.5, 8.5, 8.0]})
resultado = pd.merge(notas_1, notas_2,
on="aluno_id",
suffixes=("_prova1", "_prova2"))
print(resultado)
Saída:
aluno_id nota_prova1 nota_prova2
0 1 8.0 7.5
1 2 7.0 8.5
2 3 9.0 8.0
concat — empilhar DataFrames
treino = pd.DataFrame({
"feature1": [1.0, 2.0, 3.0],
"feature2": [0.1, 0.2, 0.3],
"label": [0, 1, 0]
})
teste = pd.DataFrame({
"feature1": [4.0, 5.0],
"feature2": [0.4, 0.5],
"label": [1, 0]
})
# Empilhar verticalmente (axis=0)
tudo = pd.concat([treino, teste], ignore_index=True)
print(tudo)
# ignore_index=True reinicia o índice de 0
# Empilhar horizontalmente (axis=1)
df_a = pd.DataFrame({"A": [1, 2, 3]})
df_b = pd.DataFrame({"B": [4, 5, 6]})
lado_a_lado = pd.concat([df_a, df_b], axis=1)
print(lado_a_lado)
Pivot Tables
Pivot tables reorganizam dados de formato longo para formato largo, muito útil para análise e visualização.
df = pd.DataFrame({
"mes": ["Jan", "Jan", "Fev", "Fev", "Mar", "Mar"],
"produto": ["A", "B", "A", "B", "A", "B"],
"vendas": [100, 150, 120, 130, 140, 170],
"lucro": [20, 30, 25, 28, 30, 35]
})
# Pivot: meses como linhas, produtos como colunas
pivot = df.pivot_table(
values="vendas",
index="mes",
columns="produto",
aggfunc="sum"
)
print("Vendas por mês e produto:")
print(pivot)
# Com múltiplos valores
pivot_multi = df.pivot_table(
values=["vendas", "lucro"],
index="mes",
columns="produto",
aggfunc="mean"
)
print("\nVendas e lucro médio:")
print(pivot_multi)
Saída:
produto A B
mes
Fev 120 130
Jan 100 150
Mar 140 170
Operações com Datas
Datas são muito comuns em datasets de ML, especialmente em séries temporais.
df = pd.DataFrame({
"data": pd.date_range("2024-01-01", periods=10, freq="D"),
"vendas": np.random.randint(100, 500, 10)
})
# Extrair componentes
df["ano"] = df["data"].dt.year
df["mes"] = df["data"].dt.month
df["dia"] = df["data"].dt.day
df["dia_semana"] = df["data"].dt.dayofweek # 0=segunda, 6=domingo
df["nome_dia"] = df["data"].dt.day_name()
df["trimestre"] = df["data"].dt.quarter
# Filtrar por período
janeiro = df[df["mes"] == 1]
dias_uteis = df[df["dia_semana"] < 5] # segunda a sexta
# Diferença entre datas
df["dias_desde_inicio"] = (df["data"] - df["data"].min()).dt.days
# Resample — agregar por período de tempo
df_semanal = df.set_index("data")["vendas"].resample("W").sum()
print("Vendas por semana:")
print(df_semanal)
Encoding de Variáveis Categóricas
Modelos de ML trabalham com números, não com strings. É preciso converter variáveis categóricas.
Label Encoding (ordinal)
df = pd.DataFrame({
"nivel": ["junior", "senior", "pleno", "junior", "senior"]
})
# Mapeamento manual (quando há ordem)
mapa_nivel = {"junior": 0, "pleno": 1, "senior": 2}
df["nivel_cod"] = df["nivel"].map(mapa_nivel)
print(df)
One-Hot Encoding
Para variáveis nominais sem ordem (cidade, cor, categoria):
df = pd.DataFrame({
"cidade": ["SP", "RJ", "SP", "MG", "RJ"],
"produto": ["A", "B", "A", "C", "B"],
"vendas": [100, 150, 120, 90, 130]
})
# get_dummies: cria colunas binárias para cada categoria
df_encoded = pd.get_dummies(df, columns=["cidade", "produto"], dtype=int)
print(df_encoded)
Saída:
vendas cidade_MG cidade_RJ cidade_SP produto_A produto_B produto_C
0 100 0 0 1 1 0 0
1 150 0 1 0 0 1 0
2 120 0 0 1 1 0 0
3 90 1 0 0 0 0 1
4 130 0 1 0 0 1 0
# drop_first=True para evitar multicolinearidade
df_encoded = pd.get_dummies(df, columns=["cidade"], drop_first=True, dtype=int)
Exemplo Completo: Pipeline de Preparação de Dados para ML
import pandas as pd
import numpy as np
np.random.seed(42)
print("Criando dataset bruto...")
# Dataset simulado de empréstimos bancários
n = 300
df_raw = pd.DataFrame({
"id": range(1, n + 1),
"idade": np.random.randint(18, 70, n),
"renda_mensal": np.random.uniform(1000, 20000, n).round(2),
"tempo_emprego": np.random.randint(0, 30, n),
"valor_emprestimo": np.random.uniform(5000, 100000, n).round(2),
"educacao": np.random.choice(
["fundamental", "medio", "superior", "pos"],
n, p=[0.1, 0.4, 0.35, 0.15]
),
"estado": np.random.choice(["SP", "RJ", "MG", "RS", "BA"], n),
"inadimplente": np.random.choice([0, 1], n, p=[0.80, 0.20])
})
# Inserir problemas artificiais
idx = np.random.choice(n, 30, replace=False)
df_raw.loc[idx[:10], "renda_mensal"] = np.nan
df_raw.loc[idx[10:20], "tempo_emprego"] = np.nan
df_raw.loc[idx[20:], "educacao"] = None
df_raw.loc[np.random.choice(n, 5), "renda_mensal"] = 999999 # outliers
df_raw = pd.concat([df_raw, df_raw.iloc[:5]], ignore_index=True) # duplicatas
print(f"Dataset bruto: {df_raw.shape}")
# ─────────────────────────────────────────────
# PASSO 1: Remover duplicatas
# ─────────────────────────────────────────────
df = df_raw.drop_duplicates(subset=["id"])
print(f"\n[1] Após remover duplicatas: {df.shape}")
# ─────────────────────────────────────────────
# PASSO 2: Tratar valores ausentes
# ─────────────────────────────────────────────
print(f"\n[2] Ausentes antes:")
print(df.isna().sum()[df.isna().sum() > 0])
df["renda_mensal"] = df["renda_mensal"].fillna(df["renda_mensal"].median())
df["tempo_emprego"] = df["tempo_emprego"].fillna(df["tempo_emprego"].median())
df["educacao"] = df["educacao"].fillna(df["educacao"].mode()[0])
print(f" Ausentes depois: {df.isna().sum().sum()}")
# ─────────────────────────────────────────────
# PASSO 3: Tratar outliers com IQR
# ─────────────────────────────────────────────
for col in ["renda_mensal", "valor_emprestimo"]:
Q1 = df[col].quantile(0.25)
Q3 = df[col].quantile(0.75)
IQR = Q3 - Q1
limite_inf = Q1 - 1.5 * IQR
limite_sup = Q3 + 1.5 * IQR
df[col] = df[col].clip(lower=limite_inf, upper=limite_sup)
print(f"\n[3] Outliers tratados com clipping.")
# ─────────────────────────────────────────────
# PASSO 4: Feature engineering
# ─────────────────────────────────────────────
df["razao_emprestimo_renda"] = (df["valor_emprestimo"] /
df["renda_mensal"]).round(4)
df["faixa_etaria"] = pd.cut(
df["idade"],
bins=[17, 25, 35, 50, 70],
labels=["18-25", "26-35", "36-50", "51-70"]
)
print(f"\n[4] Features criadas: razao_emprestimo_renda, faixa_etaria")
# ─────────────────────────────────────────────
# PASSO 5: Encoding categórico
# ─────────────────────────────────────────────
mapa_educacao = {"fundamental": 0, "medio": 1, "superior": 2, "pos": 3}
df["educacao_cod"] = df["educacao"].map(mapa_educacao)
df = pd.get_dummies(df, columns=["estado", "faixa_etaria"], dtype=int)
print(f"\n[5] Após encoding: {df.shape[1]} colunas")
# ─────────────────────────────────────────────
# PASSO 6: Análise por grupo
# ─────────────────────────────────────────────
print("\n[6] Inadimplência por nível de educação:")
analise = (df.groupby("educacao")["inadimplente"]
.agg(["mean", "count"])
.rename(columns={"mean": "taxa_inadimplencia", "count": "total"})
.round(3))
print(analise)
# ─────────────────────────────────────────────
# PASSO 7: Preparar arrays para modelo
# ─────────────────────────────────────────────
colunas_para_remover = ["id", "educacao", "inadimplente"]
colunas_features = [c for c in df.columns if c not in colunas_para_remover]
X = df[colunas_features].values.astype(np.float32)
y = df["inadimplente"].values.astype(np.int64)
print(f"\n[7] Arrays prontos para o modelo:")
print(f" X: {X.shape}, dtype={X.dtype}")
print(f" y: {y.shape}, dtype={y.dtype}")
print(f" Taxa de inadimplência: {y.mean():.1%}")
Saída resumida:
Dataset bruto: (305, 10)
[1] Após remover duplicatas: (300, 10)
[2] Ausentes antes:
renda_mensal 10
tempo_emprego 10
educacao 10
Ausentes depois: 0
[3] Outliers tratados com clipping.
[4] Features criadas: razao_emprestimo_renda, faixa_etaria
[5] Após encoding: 21 colunas
[6] Inadimplência por nível de educação:
taxa_inadimplencia total
educacao
fundamental 0.241 29
medio 0.200 121
pos 0.178 45
superior 0.190 105
[7] Arrays prontos para o modelo:
X: (300, 19), dtype=float32
y: (300,), dtype=int64
Taxa de inadimplência: 20.0%
O drop_duplicates() remove linhas repetidas e aceita restringir a comparação a colunas específicas. Outliers têm três tratamentos usuais — Z-score, IQR e clipping —, cada um com seu compromisso, e a escolha depende do que o dado significa. Para transformar, apply() roda uma função por elemento ou por linha, enquanto map() substitui valores a partir de um dicionário. O groupby() segue o ciclo split-apply-combine: divide, agrega e recombina; ao lado dele vale separar agg(), que reduz, de transform(), que preserva o tamanho original do DataFrame. Para juntar dados, merge() reproduz os joins do SQL — inner, left, right e outer — e concat() empilha na vertical ou na horizontal. Variáveis categóricas viram colunas com pd.get_dummies(), mas as ordinais pedem mapeamento manual, porque a ordem carrega uma informação que o one-hot descarta. O pipeline completo, na ordem em que se aplica: remover duplicatas, tratar ausentes, tratar outliers, criar features, codificar e exportar como NumPy.
Exercícios
-
Dado o DataFrame abaixo com dados de vendas, use
groupbypara calcular: total de vendas por região, ticket médio (media de valor) por categoria de produto, e o produto mais vendido (maior soma de vendas) em cada região.df = pd.DataFrame({ "regiao": ["Sul", "Norte", "Sul", "Norte", "Sul", "Norte"], "produto": ["A", "B", "B", "A", "A", "C"], "categoria": ["Eletrônico", "Roupa", "Roupa", "Eletrônico", "Eletrônico", "Alimento"], "vendas": [1200, 800, 950, 1100, 1350, 600], "valor": [350, 120, 130, 370, 340, 45] })✓ Resposta:df = pd.DataFrame({ "regiao": ["Sul", "Norte", "Sul", "Norte", "Sul", "Norte"], "produto": ["A", "B", "B", "A", "A", "C"], "categoria": ["Eletrônico", "Roupa", "Roupa", "Eletrônico", "Eletrônico", "Alimento"], "vendas": [1200, 800, 950, 1100, 1350, 600], "valor": [350, 120, 130, 370, 340, 45] }) # Total de vendas por região print("Total de vendas por região:") print(df.groupby("regiao")["vendas"].sum()) # Ticket médio por categoria print("\nTicket médio por categoria:") print(df.groupby("categoria")["valor"].mean().round(2)) # Produto mais vendido por região print("\nProduto mais vendido por região:") mais_vendido = (df.groupby(["regiao", "produto"])["vendas"] .sum() .reset_index() .sort_values("vendas", ascending=False) .groupby("regiao") .first()) print(mais_vendido)Saída:
Total de vendas por região: regiao Norte 2500 Sul 3500 Ticket médio por categoria: categoria Alimento 45.00 Eletrônico 353.33 Roupa 125.00 Produto mais vendido por região: produto vendas regiao Norte A 1100 Sul A 2550 -
Explique a diferença entre
groupby().agg()egroupby().transform(). Dê um exemplo de quando você usaria cada um em um projeto de ML.✓ Resposta:groupby().agg()reduz o DataFrame: retorna um resultado com uma linha por grupo. Use quando quiser resumos estatísticos como médias, totais ou contagens por categoria.groupby().transform()mantém o tamanho original: retorna um valor para cada linha original, baseado no grupo ao qual ela pertence. Use quando quiser adicionar uma coluna ao DataFrame original com informações do grupo.Exemplo em ML com
agg(): calcular a taxa de inadimplência por estado para um relatório de análise.taxa_por_estado = df.groupby("estado")["inadimplente"].agg(["mean", "count"])Exemplo em ML com
transform(): criar uma feature de engenharia que mostra a diferença entre o salário de um funcionário e a média do seu departamento — sem reduzir o DataFrame.df["desvio_do_depto"] = ( df["salario"] - df.groupby("departamento")["salario"].transform("mean") )A diferença fundamental:
agg()é para análise,transform()é para criar novas features que ficam no DataFrame original. -
Dados dois DataFrames abaixo, realize um LEFT JOIN para manter todos os alunos, mesmo os sem nota de projeto. Depois calcule a nota final como média de
nota_provaenota_projeto(usando 0 quando nota_projeto for ausente).alunos = pd.DataFrame({ "aluno_id": [1, 2, 3, 4, 5], "nome": ["Ana", "Bruno", "Carla", "Diego", "Eva"], "nota_prova": [8.0, 7.5, 9.0, 6.0, 8.5] }) projetos = pd.DataFrame({ "aluno_id": [1, 3, 5], "nota_projeto": [9.0, 8.5, 7.0] })✓ Resposta:alunos = pd.DataFrame({ "aluno_id": [1, 2, 3, 4, 5], "nome": ["Ana", "Bruno", "Carla", "Diego", "Eva"], "nota_prova": [8.0, 7.5, 9.0, 6.0, 8.5] }) projetos = pd.DataFrame({ "aluno_id": [1, 3, 5], "nota_projeto": [9.0, 8.5, 7.0] }) # LEFT JOIN — todos os alunos df_merged = pd.merge(alunos, projetos, on="aluno_id", how="left") # Substituir NaN em nota_projeto por 0 df_merged["nota_projeto"] = df_merged["nota_projeto"].fillna(0) # Calcular nota final df_merged["nota_final"] = ( (df_merged["nota_prova"] + df_merged["nota_projeto"]) / 2 ) print(df_merged)Saída:
aluno_id nome nota_prova nota_projeto nota_final 0 1 Ana 8.0 9.0 8.50 1 2 Bruno 7.5 0.0 3.75 2 3 Carla 9.0 8.5 8.75 3 4 Diego 6.0 0.0 3.00 4 5 Eva 8.5 7.0 7.75 -
Explique o que é one-hot encoding e por que ele é necessário para variáveis categóricas nominais em modelos de ML. O que é o problema da "armadilha das variáveis dummy" e como o parâmetro
drop_first=Trueo resolve?✓ Resposta:One-hot encoding transforma uma variável categórica com N categorias em N colunas binárias (0 ou 1), onde cada coluna indica a presença ou ausência de uma categoria. É necessário porque modelos de ML como regressão logística, SVM e redes neurais trabalham com operações matemáticas, e não faz sentido matemático calcular "SP + RJ" ou afirmar que "MG > RJ" — categorias nominais não têm ordem nem relação numérica entre si.
A armadilha das variáveis dummy (dummy variable trap): se você criar N colunas para N categorias, há multicolinearidade perfeita — a última coluna é sempre determinada pelas outras (se as primeiras N-1 forem 0, a última é necessariamente 1). Modelos lineares têm problemas com isso pois não conseguem calcular a inversa da matriz de features.
drop_first=Trueresolve removendo a primeira categoria, deixando apenas N-1 colunas. A categoria removida se torna o grupo de referência implícito.df = pd.DataFrame({"cor": ["vermelho", "azul", "verde", "azul"]}) # Sem drop_first: 3 colunas (uma é redundante) print(pd.get_dummies(df, dtype=int)) # cor_azul cor_verde cor_vermelho # 0 0 1 # 1 0 0 # 0 1 0 # 1 0 0 # Com drop_first: 2 colunas (vermelho é o grupo de referência) print(pd.get_dummies(df, drop_first=True, dtype=int)) # cor_verde cor_vermelho # 0 1 # 0 0 # 1 0 # 0 0 -
Dado o DataFrame abaixo, use
applycomaxis=1para criar uma coluna de classificação de risco de crédito baseada em múltiplas colunas simultaneamente.df = pd.DataFrame({ "renda": [3000, 8000, 1500, 12000, 5000], "divida": [500, 6000, 1200, 1000, 4000], "historico": ["bom", "ruim", "ruim", "bom", "regular"] })Regras: risco "alto" se divida/renda > 0.5 OU historico == "ruim". Risco "médio" se divida/renda entre 0.3 e 0.5 E historico != "ruim". Risco "baixo" nos demais casos.
✓ Resposta:df = pd.DataFrame({ "renda": [3000, 8000, 1500, 12000, 5000], "divida": [500, 6000, 1200, 1000, 4000], "historico": ["bom", "ruim", "ruim", "bom", "regular"] }) def classificar_risco(linha): razao = linha["divida"] / linha["renda"] if razao > 0.5 or linha["historico"] == "ruim": return "alto" elif 0.3 <= razao <= 0.5 and linha["historico"] != "ruim": return "médio" else: return "baixo" df["risco"] = df.apply(classificar_risco, axis=1) print(df)Saída:
renda divida historico risco 0 3000 500 bom baixo 1 8000 6000 ruim alto 2 1500 1200 ruim alto 3 12000 1000 bom baixo 4 5000 4000 regular médio -
Explique o que são os quatro tipos de join (inner, left, right, outer) usando uma analogia simples. Em qual situação de ML você usaria cada tipo?
✓ Resposta:Imagine duas listas de convidados de uma festa: lista A (convidados da família) e lista B (convidados do trabalho). Algumas pessoas estão em ambas.
INNER JOIN: apenas as pessoas que estão em AMBAS as listas — os que aparecem tanto na família quanto no trabalho. Em ML: combinar features de um dataset com labels de outro, mantendo apenas os exemplos que existem nos dois.
LEFT JOIN: todos da lista A, e quando a pessoa também está em B, adiciona as informações de B. Para quem está só em A, o campo de B fica vazio. Em ML: enriquecer um dataset principal com informações opcionais de outra fonte, mantendo todos os exemplos originais mesmo sem correspondência.
RIGHT JOIN: todos da lista B, e quando a pessoa também está em A, adiciona as informações de A. É o espelho do LEFT JOIN. Em ML: garantir que todos os exemplos de uma fonte secundária apareçam no resultado, mesmo sem correspondência na fonte principal.
OUTER JOIN: todos de ambas as listas, com campos vazios onde não há correspondência. Em ML: fazer um merge completo de dois datasets para análise, identificando quais exemplos só existem em uma das fontes — útil para auditoria de dados ou detecção de inconsistências entre sistemas.
Referências
- Documentação Pandas — groupby
- Documentação Pandas — merge, join, concat
- Documentação Pandas — missing data
- Documentação Pandas — categorical data
- Real Python — Pandas GroupBy
- Real Python — Pandas merge, join, concat
- Towards Data Science — Handling Outliers in ML
- Kaggle — Data Cleaning course (gratuito)