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

  1. Dado o DataFrame abaixo com dados de vendas, use groupby para 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
    
  2. Explique a diferença entre groupby().agg() e groupby().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.

  3. 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_prova e nota_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
    
  4. 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=True o 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=True resolve 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
    
  5. Dado o DataFrame abaixo, use apply com axis=1 para 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
    
  6. 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