Apostila Completa de Machine Learning e Inteligência Artificial
Do zero ao avançado, com Python e foco em Análise de Dados
Sumário
- Introdução à Inteligência Artificial e ao Machine Learning
- Preparando o Ambiente e Python Essencial
- NumPy: Computação Numérica
- Análise de Dados com Pandas
- Análise Exploratória de Dados (EDA) e Visualização
- Limpeza de Dados e Engenharia de Atributos
- Matemática e Estatística para ML
- Fundamentos de Machine Learning
- Regressão
- Classificação
- Métodos de Ensemble: Bagging, Random Forest e Boosting
- Aprendizado Não Supervisionado
- Pipelines, Tuning e Interpretabilidade
- Deep Learning: Redes Neurais do Zero ao PyTorch
- Redes Convolucionais (CNNs) e Visão Computacional
- Sequências: RNNs, LSTMs e Séries Temporais
- Processamento de Linguagem Natural (NLP)
- Transformers e Grandes Modelos de Linguagem (LLMs)
- Tópicos Complementares: Recomendação, RL, MLOps e Ética
- Projeto Guiado de Ponta a Ponta
- Trilha de Estudos e Próximos Passos
Capítulo 1 — Introdução à Inteligência Artificial e ao Machine Learning
1.1 O que é Inteligência Artificial?
Inteligência Artificial (IA) é o campo da computação dedicado a criar sistemas capazes de executar tarefas que, quando feitas por humanos, exigem inteligência: reconhecer imagens, entender linguagem, tomar decisões, planejar, jogar.
A IA é um guarda-chuva que contém subcampos. A hierarquia mais útil para este curso é:
Inteligência Artificial (IA)
└── Machine Learning (ML) — sistemas que aprendem a partir de dados
└── Deep Learning (DL) — ML com redes neurais profundas
└── IA Generativa — modelos que criam texto, imagem, áudio (ex.: LLMs)
- IA simbólica (clássica): regras escritas à mão ("se temperatura > 38, então febre"). Dominou dos anos 1950 aos 1980. Frágil: não generaliza para o que não foi programado.
- Machine Learning: em vez de escrever as regras, mostramos exemplos e o algoritmo descobre as regras sozinho. É a abordagem dominante desde os anos 1990.
- Deep Learning: ML usando redes neurais com muitas camadas. Explodiu a partir de 2012 (AlexNet) graças a GPUs e grandes volumes de dados.
1.2 A definição formal de Machine Learning
A definição clássica é de Tom Mitchell (1997):
"Um programa aprende com a experiência E em relação a uma classe de tarefas T e medida de desempenho P, se seu desempenho em T, medido por P, melhora com E."
Exemplo — filtro de spam: - T (tarefa): classificar e-mails como spam ou não. - E (experiência): milhares de e-mails já rotulados. - P (desempenho): percentual de e-mails classificados corretamente.
1.3 Programação tradicional vs. Machine Learning
Programação tradicional: Dados + Regras → Respostas
Machine Learning: Dados + Respostas → Regras (o "modelo")
Essa inversão é a ideia central de todo o campo. O produto do treinamento é um modelo: uma função matemática com parâmetros ajustados aos dados, capaz de gerar previsões para dados nunca vistos.
1.4 Os três paradigmas de aprendizado
Aprendizado Supervisionado
Os dados vêm com rótulos (a resposta certa). O modelo aprende a mapear entrada → saída.
- Regressão: a saída é um número contínuo (preço de imóvel, temperatura, demanda).
- Classificação: a saída é uma categoria (spam/não spam, doente/saudável, dígito 0–9).
Aprendizado Não Supervisionado
Não há rótulos. O modelo descobre estrutura nos dados.
- Clusterização: agrupar clientes com comportamento parecido.
- Redução de dimensionalidade: comprimir 1000 variáveis em 2 para visualizar.
- Detecção de anomalias: encontrar transações fraudulentas raras.
Aprendizado por Reforço (RL)
Um agente interage com um ambiente, recebe recompensas e aprende uma política de ações que maximiza a recompensa acumulada. É o paradigma por trás do AlphaGo, de robôs e do RLHF usado para alinhar LLMs.
Há ainda paradigmas híbridos importantes: - Aprendizado semi-supervisionado: poucos dados rotulados + muitos não rotulados. - Aprendizado auto-supervisionado: o rótulo é extraído do próprio dado (ex.: prever a próxima palavra de um texto — é assim que LLMs são pré-treinados).
1.5 Onde ML é usado hoje
| Área | Exemplos |
|---|---|
| Finanças | detecção de fraude, score de crédito, trading algorítmico |
| Saúde | diagnóstico por imagem, descoberta de fármacos, prognóstico |
| Varejo | recomendação de produtos, previsão de demanda, precificação |
| Indústria | manutenção preditiva, controle de qualidade visual |
| Linguagem | tradução, chatbots, sumarização, assistentes (LLMs) |
| Visão | carros autônomos, reconhecimento facial, OCR |
| Marketing | segmentação de clientes, churn, atribuição |
1.6 O fluxo de trabalho de um projeto de ML
Praticamente todo projeto segue este ciclo (metodologia CRISP-DM adaptada):
- Entendimento do problema — qual decisão o modelo vai apoiar? Qual métrica de negócio importa?
- Coleta de dados — bancos de dados, APIs, arquivos, web scraping.
- Análise exploratória (EDA) — entender distribuições, correlações, problemas.
- Limpeza e preparação — tratar faltantes, outliers, codificar categorias.
- Engenharia de atributos — criar variáveis que ajudem o modelo.
- Modelagem — treinar e comparar algoritmos.
- Avaliação — métricas honestas em dados nunca vistos.
- Implantação (deploy) — colocar o modelo em produção.
- Monitoramento — dados mudam ("drift"); o modelo precisa de manutenção.
Regra de ouro: 60–80% do tempo de um projeto real é gasto nas etapas 2–5 (dados), não na modelagem. Por isso esta apostila dá atenção especial à análise de dados.
Capítulo 2 — Preparando o Ambiente e Python Essencial
2.1 Instalação
A forma mais simples de começar:
- Instale o Python (python.org) ou a distribuição Anaconda (anaconda.com), que já traz as principais bibliotecas.
- Use o Jupyter Notebook ou o Google Colab (colab.research.google.com — roda no navegador, grátis, com GPU).
Instalando as bibliotecas centrais via pip:
pip install numpy pandas matplotlib seaborn scikit-learn jupyter
# Para deep learning e NLP (capítulos 14+):
pip install torch torchvision transformers datasets
# Boosting avançado:
pip install xgboost lightgbm
# Interpretabilidade e tuning:
pip install shap optuna
2.2 Python em 15 minutos (o essencial para ML)
# Variáveis e tipos
idade = 30 # int
preco = 19.90 # float
nome = "Ana" # str
ativo = True # bool
# Listas (mutáveis, ordenadas)
notas = [8.5, 7.0, 9.2]
notas.append(6.8)
print(notas[0]) # 8.5 (índices começam em 0)
print(notas[-1]) # 6.8 (índice negativo = do fim para o começo)
print(notas[1:3]) # [7.0, 9.2] (fatiamento: início incluso, fim excluso)
# Dicionários (pares chave: valor)
aluno = {"nome": "Ana", "nota": 8.5}
print(aluno["nome"])
# Condicionais
if aluno["nota"] >= 7:
print("Aprovado")
elif aluno["nota"] >= 5:
print("Recuperação")
else:
print("Reprovado")
# Laços
for nota in notas:
print(nota)
# List comprehension — muito usada em ciência de dados
quadrados = [x**2 for x in range(5)] # [0, 1, 4, 9, 16]
pares = [x for x in range(10) if x % 2 == 0] # [0, 2, 4, 6, 8]
# Funções
def media(valores):
"""Retorna a média aritmética de uma lista."""
return sum(valores) / len(valores)
print(media(notas))
# Funções lambda (anônimas) — comuns com Pandas
dobro = lambda x: 2 * x
2.3 Boas práticas desde o início
- Reprodutibilidade: fixe sementes aleatórias (
random_state=42) para que resultados possam ser repetidos. - Ambientes virtuais:
python -m venv venvisola as dependências de cada projeto. - Versionamento: use Git para código; considere DVC para dados.
- Notebooks para explorar, scripts para produzir: notebooks são ótimos para EDA; código de produção vive em módulos
.pytestáveis.
Capítulo 3 — NumPy: Computação Numérica
NumPy é a fundação de todo o ecossistema: Pandas, scikit-learn e PyTorch são construídos sobre (ou inspirados em) seus arrays. A razão da sua importância: operações vetorizadas em C, dezenas ou centenas de vezes mais rápidas que laços Python.
3.1 Criando arrays
import numpy as np
a = np.array([1, 2, 3, 4]) # a partir de lista
M = np.array([[1, 2], [3, 4]]) # matriz 2x2
np.zeros((2, 3)) # matriz 2x3 de zeros
np.ones(5) # vetor de uns
np.arange(0, 10, 2) # [0 2 4 6 8]
np.linspace(0, 1, 5) # [0. 0.25 0.5 0.75 1.]
np.random.seed(42) # reprodutibilidade
np.random.normal(0, 1, size=(3, 3)) # normal(média=0, desvio=1)
print(M.shape) # (2, 2) — dimensões
print(M.dtype) # int64 — tipo dos elementos
print(M.ndim) # 2 — número de eixos
3.2 Vetorização: a ideia mais importante
x = np.arange(1_000_000)
# LENTO — laço Python
total = 0
for v in x:
total += v * 2
# RÁPIDO — vetorizado (opera no array inteiro de uma vez)
total = (x * 2).sum()
Operações aritméticas são aplicadas elemento a elemento:
a = np.array([1, 2, 3])
b = np.array([10, 20, 30])
a + b # [11 22 33]
a * b # [10 40 90]
a ** 2 # [1 4 9]
np.sqrt(a) # raiz quadrada elemento a elemento
np.exp(a) # e^x
np.log(a) # logaritmo natural
3.3 Indexação, fatiamento e máscaras booleanas
M = np.arange(12).reshape(3, 4) # matriz 3x4 com valores 0..11
M[0, 0] # elemento da linha 0, coluna 0
M[1, :] # linha 1 inteira
M[:, 2] # coluna 2 inteira
M[0:2, 1:3] # submatriz
# Máscaras booleanas — filtragem, essencial em análise de dados
x = np.array([3, -1, 7, 0, -5])
mask = x > 0 # [ True False True False False]
x[mask] # [3 7]
x[x < 0] = 0 # substitui negativos por zero
np.where(x > 2, "alto", "baixo") # if/else vetorizado
3.4 Broadcasting
Broadcasting permite operar arrays de formatos diferentes, "esticando" dimensões de tamanho 1:
M = np.ones((3, 4))
v = np.array([1, 2, 3, 4])
M + v # v é somado a CADA linha de M
col = np.array([[10], [20], [30]]) # shape (3,1)
M + col # col é somado a CADA coluna
Regra: dimensões são compatíveis se forem iguais ou se uma delas for 1 (comparando da direita para a esquerda).
3.5 Agregações e álgebra linear
x = np.random.normal(50, 10, size=(1000,))
x.mean(), x.std(), x.min(), x.max()
np.median(x), np.percentile(x, [25, 50, 75])
M = np.random.rand(3, 4)
M.sum(axis=0) # soma por coluna (colapsa as linhas)
M.sum(axis=1) # soma por linha (colapsa as colunas)
# Álgebra linear — base do deep learning
A = np.random.rand(3, 2)
B = np.random.rand(2, 4)
C = A @ B # multiplicação de matrizes (3x4)
A.T # transposta
np.linalg.inv(np.eye(3)) # inversa
autoval, autovet = np.linalg.eig(np.cov(np.random.rand(5, 100)))
Conexão com ML: uma rede neural é, em essência, uma sequência de multiplicações de matrizes (
@) intercaladas com funções não lineares. Dominar NumPy é dominar o vocabulário do deep learning.
Capítulo 4 — Análise de Dados com Pandas
Pandas é a biblioteca central de análise de dados em Python. Suas duas estruturas:
- Series: coluna unidimensional com índice.
- DataFrame: tabela bidimensional (como uma planilha ou tabela SQL).
4.1 Carregando e inspecionando dados
import pandas as pd
df = pd.read_csv("vendas.csv") # também: read_excel, read_json,
# read_parquet, read_sql
df.head(10) # primeiras 10 linhas
df.tail() # últimas linhas
df.shape # (n_linhas, n_colunas)
df.info() # tipos, memória e contagem de não-nulos por coluna
df.describe() # estatísticas das colunas numéricas
df.describe(include="object") # estatísticas das colunas de texto
df.dtypes # tipo de cada coluna
df.columns # nomes das colunas
df["cidade"].value_counts() # frequência de cada categoria
df["cidade"].value_counts(normalize=True) # em proporção
df["preco"].nunique() # nº de valores distintos
Criando um DataFrame de exemplo (usaremos ao longo do capítulo):
import numpy as np
np.random.seed(42)
n = 1000
df = pd.DataFrame({
"data": pd.date_range("2025-01-01", periods=n, freq="h"),
"loja": np.random.choice(["SP", "RJ", "MG", "RS"], n),
"produto": np.random.choice(["A", "B", "C"], n, p=[0.5, 0.3, 0.2]),
"quantidade": np.random.poisson(3, n) + 1,
"preco_unit": np.round(np.random.uniform(10, 100, n), 2),
})
df["receita"] = df["quantidade"] * df["preco_unit"]
4.2 Seleção: colunas, linhas, loc e iloc
df["receita"] # uma coluna (Series)
df[["loja", "receita"]] # várias colunas (DataFrame)
# loc — seleção por RÓTULO (e máscaras booleanas)
df.loc[0] # linha de índice 0
df.loc[0:5, ["loja", "receita"]] # linhas 0 a 5 (INCLUSIVO), colunas escolhidas
# iloc — seleção por POSIÇÃO numérica
df.iloc[0] # primeira linha
df.iloc[-5:, 0:3] # últimas 5 linhas, 3 primeiras colunas
# Filtragem booleana — o dia a dia do analista
df[df["receita"] > 300]
df[(df["loja"] == "SP") & (df["produto"] == "A")] # & = E, | = OU, ~ = NÃO
df[df["loja"].isin(["SP", "RJ"])]
df[df["produto"].str.contains("A")]
df.query("receita > 300 and loja == 'SP'") # sintaxe alternativa legível
Armadilha clássica: use parênteses em cada condição com
&/|. Sem eles, a precedência dos operadores quebra a expressão.
4.3 Criando e transformando colunas
df["receita_kg"] = df["receita"] / 1000
df["faixa"] = np.where(df["receita"] > 200, "alta", "baixa")
# pd.cut — discretiza em faixas
df["faixa_preco"] = pd.cut(df["preco_unit"], bins=[0, 30, 60, 100],
labels=["barato", "medio", "caro"])
# apply / map — funções arbitrárias (use com parcimônia: mais lento que vetorizado)
df["loja_lower"] = df["loja"].str.lower() # métodos .str vetorizados
df["mes"] = df["data"].dt.month # métodos .dt para datas
df["dia_semana"] = df["data"].dt.day_name()
df["log_receita"] = np.log1p(df["receita"]) # NumPy funciona direto em Series
# assign — encadeamento funcional
df = df.assign(ticket_medio=lambda d: d["receita"] / d["quantidade"])
# Renomear, remover, ordenar
df = df.rename(columns={"preco_unit": "preco_unitario"})
df = df.drop(columns=["loja_lower"])
df = df.sort_values(["loja", "receita"], ascending=[True, False])
4.4 GroupBy: dividir, aplicar, combinar
O padrão split-apply-combine é a ferramenta analítica mais poderosa do Pandas:
# Receita total por loja
df.groupby("loja")["receita"].sum()
# Várias agregações de uma vez
df.groupby("loja").agg(
receita_total=("receita", "sum"),
ticket_medio=("receita", "mean"),
n_vendas=("receita", "size"),
maior_venda=("receita", "max"),
)
# Agrupando por mais de uma chave
df.groupby(["loja", "produto"])["receita"].sum().reset_index()
# transform — devolve o resultado no tamanho original (ótimo para features)
df["receita_media_loja"] = df.groupby("loja")["receita"].transform("mean")
df["desvio_da_loja"] = df["receita"] - df["receita_media_loja"]
# Agregação por tempo (resample) — séries temporais
receita_diaria = df.set_index("data")["receita"].resample("D").sum()
4.5 Tabelas dinâmicas e remodelagem
# pivot_table — como a tabela dinâmica do Excel
pd.pivot_table(df, values="receita", index="loja",
columns="produto", aggfunc="sum", fill_value=0, margins=True)
# crosstab — contagens cruzadas
pd.crosstab(df["loja"], df["produto"], normalize="index")
# melt — de formato largo para longo (útil para plotagem)
largo = pd.pivot_table(df, values="receita", index="loja", columns="produto")
longo = largo.reset_index().melt(id_vars="loja", var_name="produto",
value_name="receita")
# stack / unstack — movem níveis entre índice e colunas
4.6 Combinando DataFrames: merge, join e concat
clientes = pd.DataFrame({"id": [1, 2, 3], "nome": ["Ana", "Beto", "Caio"]})
pedidos = pd.DataFrame({"id_cliente": [1, 1, 2, 4], "valor": [50, 30, 70, 90]})
# merge = JOIN de SQL
pd.merge(pedidos, clientes, left_on="id_cliente", right_on="id", how="inner")
# how: "inner" (interseção), "left", "right", "outer" (união)
# indicator=True mostra a origem de cada linha — ótimo para auditar joins
pd.merge(pedidos, clientes, left_on="id_cliente", right_on="id",
how="outer", indicator=True)
# concat — empilha vertical ou horizontalmente
pd.concat([df.head(), df.tail()], axis=0)
Auditoria de merge: sempre confira
shapeantes e depois. Um join com chaves duplicadas pode multiplicar linhas silenciosamente.
4.7 Dados faltantes, duplicatas e tipos
df.isna().sum() # faltantes por coluna
df.isna().mean().sort_values(ascending=False) # em percentual
df.dropna() # remove linhas com qualquer NaN
df.dropna(subset=["receita"]) # só se faltar nessa coluna
df["preco_unitario"].fillna(df["preco_unitario"].median())
df["produto"] = df["produto"].fillna("desconhecido")
df.duplicated().sum() # conta duplicatas exatas
df = df.drop_duplicates()
# Conversão de tipos — economiza memória e evita bugs
df["loja"] = df["loja"].astype("category")
df["quantidade"] = pd.to_numeric(df["quantidade"], errors="coerce")
df["data"] = pd.to_datetime(df["data"], errors="coerce")
4.8 Encadeamento de métodos (method chaining)
Estilo moderno de Pandas, legível de cima para baixo:
resultado = (
df
.query("receita > 0")
.assign(mes=lambda d: d["data"].dt.to_period("M"))
.groupby(["mes", "loja"], observed=True)
.agg(receita=("receita", "sum"))
.reset_index()
.sort_values("receita", ascending=False)
)
4.9 Desempenho e boas práticas
- Prefira operações vetorizadas a
applycom funções Python. - Use
categorypara colunas de texto com poucos valores distintos. - Para dados grandes: formato Parquet (
df.to_parquet), leitura por partes (chunksize), ou bibliotecas como Polars e DuckDB. df.copy()ao criar sub-DataFrames que serão modificados, para evitar o famoso avisoSettingWithCopyWarning.
Capítulo 5 — Análise Exploratória de Dados (EDA) e Visualização
A EDA é a etapa em que você interroga os dados antes de modelar: distribuições, relações, anomalias, vieses de coleta. Modelar sem EDA é dirigir vendado.
5.1 O roteiro de uma EDA completa
- Visão geral:
shape,info(), amostra de linhas, colunas constantes ou quase constantes. - Qualidade: faltantes, duplicatas, tipos errados, valores impossíveis (idade negativa, datas no futuro).
- Análise univariada: distribuição de cada variável isoladamente.
- Análise bivariada: cada variável contra o alvo, e pares importantes entre si.
- Análise multivariada: correlações, interações, segmentos.
- Hipóteses e conclusões: registrar descobertas que orientarão a limpeza, as features e o modelo.
5.2 Matplotlib e Seaborn: o essencial
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_theme(style="whitegrid") # estética padrão agradável
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot([1, 2, 3], [2, 4, 3], marker="o", label="série A")
ax.set_title("Título")
ax.set_xlabel("x"); ax.set_ylabel("y")
ax.legend()
plt.tight_layout()
plt.show()
5.3 Univariada: distribuições
# Numéricas — histograma, densidade e boxplot
fig, axes = plt.subplots(1, 3, figsize=(15, 4))
sns.histplot(df["receita"], bins=40, kde=True, ax=axes[0])
sns.boxplot(x=df["receita"], ax=axes[1])
sns.ecdfplot(df["receita"], ax=axes[2]) # distribuição acumulada
plt.tight_layout()
# Estatísticas que acompanham o gráfico
df["receita"].agg(["mean", "median", "std", "skew", "kurt"])
Como ler:
- Assimetria (skew) > 0: cauda longa à direita (renda, preços). Considere transformação log.
- Boxplot: a caixa é o intervalo interquartil (IQR, 25%–75%); pontos além de 1,5×IQR são candidatos a outliers.
# Categóricas — barras
ordem = df["loja"].value_counts().index
sns.countplot(data=df, y="loja", order=ordem)
5.4 Bivariada: relações com o alvo
# numérica × numérica — dispersão e correlação
sns.scatterplot(data=df, x="preco_unitario", y="receita", alpha=0.3)
sns.regplot(data=df, x="preco_unitario", y="receita", scatter_kws={"alpha": 0.2})
df[["preco_unitario", "quantidade", "receita"]].corr()
# categórica × numérica — boxplot/violino por grupo
sns.boxplot(data=df, x="loja", y="receita")
sns.violinplot(data=df, x="produto", y="receita")
# categórica × categórica — mapa de calor de contagens
tab = pd.crosstab(df["loja"], df["produto"], normalize="index")
sns.heatmap(tab, annot=True, fmt=".2f", cmap="Blues")
5.5 Multivariada
# Matriz de correlação
num = df.select_dtypes("number")
plt.figure(figsize=(8, 6))
sns.heatmap(num.corr(), annot=True, fmt=".2f", cmap="coolwarm",
center=0, vmin=-1, vmax=1)
# Pairplot — todas as dispersões par a par, colorido por categoria
sns.pairplot(df.sample(300), vars=["preco_unitario", "quantidade", "receita"],
hue="produto")
# Séries temporais
receita_diaria = df.set_index("data")["receita"].resample("D").sum()
receita_diaria.plot(figsize=(12, 4), title="Receita diária")
receita_diaria.rolling(7).mean().plot(label="média móvel 7d")
plt.legend()
5.6 Correlação: cuidados de leitura
- Pearson mede relação linear; Spearman mede relação monótona (baseada em postos, robusta a outliers):
num.corr(method="spearman"). - Correlação não é causalidade: sorvete e afogamentos correlacionam porque ambos dependem do calor (variável de confusão).
- Correlação ≈ 0 não significa independência (pense em uma parábola).
- Paradoxo de Simpson: uma tendência pode se inverter quando os dados são segmentados. Sempre analise por grupos relevantes.
- Quarteto de Anscombe: quatro conjuntos com as mesmas estatísticas e gráficos completamente diferentes. Moral: sempre visualize.
5.7 Relatório de EDA automatizado
# pip install ydata-profiling
from ydata_profiling import ProfileReport
ProfileReport(df, title="EDA Vendas").to_file("eda.html")
Útil como primeiro rascunho — nunca como substituto do olhar humano.
Capítulo 6 — Limpeza de Dados e Engenharia de Atributos
"Garbage in, garbage out": nenhum algoritmo salva dados ruins. Este capítulo é, na prática, o que mais separa bons profissionais.
6.1 Tratamento de valores faltantes
Primeiro, diagnostique o mecanismo da falta:
- MCAR (completamente aleatório): falta sem padrão. Remover ou imputar é seguro.
- MAR (aleatório condicional): a falta depende de outras variáveis observadas (ex.: renda falta mais entre jovens). Impute usando essas variáveis.
- MNAR (não aleatório): a falta depende do próprio valor (ex.: rendas altas omitidas). O mais perigoso — considere criar um indicador de faltante.
from sklearn.impute import SimpleImputer, KNNImputer
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
# Estratégias simples
imp = SimpleImputer(strategy="median") # ou "mean", "most_frequent", "constant"
X_num_imp = imp.fit_transform(X_num)
# KNN — imputa com base nos vizinhos mais parecidos
X_imp = KNNImputer(n_neighbors=5).fit_transform(X_num)
# Iterativa (MICE) — modela cada coluna em função das outras
X_imp = IterativeImputer(random_state=42).fit_transform(X_num)
# Flag de faltante — preserva a informação de que faltava
df["renda_faltante"] = df["renda"].isna().astype(int)
6.2 Outliers
# Regra do IQR
q1, q3 = df["receita"].quantile([0.25, 0.75])
iqr = q3 - q1
lim_inf, lim_sup = q1 - 1.5 * iqr, q3 + 1.5 * iqr
outliers = df[(df["receita"] < lim_inf) | (df["receita"] > lim_sup)]
# Z-score robusto (mediana e MAD, em vez de média e desvio)
from scipy import stats
z_rob = (df["receita"] - df["receita"].median()) / stats.median_abs_deviation(df["receita"])
# Winsorização — trunca em percentis em vez de remover
df["receita_w"] = df["receita"].clip(df["receita"].quantile(0.01),
df["receita"].quantile(0.99))
Decisão: outlier é erro de medição (corrigir/remover) ou fenômeno real (manter — fraudes e clientes VIP são outliers valiosos)? Métodos multivariados (Isolation Forest, LOF) aparecem no Capítulo 12.
6.3 Codificação de variáveis categóricas
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder
# One-Hot: uma coluna binária por categoria (sem ordem implícita)
ohe = OneHotEncoder(handle_unknown="ignore", sparse_output=False)
X_cat = ohe.fit_transform(df[["loja", "produto"]])
# Com Pandas:
pd.get_dummies(df, columns=["loja", "produto"], drop_first=True)
# Ordinal: quando EXISTE ordem natural
oe = OrdinalEncoder(categories=[["barato", "medio", "caro"]])
df["faixa_cod"] = oe.fit_transform(df[["faixa_preco"]])
# Target encoding: substitui a categoria pela média do alvo naquela categoria.
# Poderoso para alta cardinalidade, mas exige cuidado com vazamento
# (calcule dentro da validação cruzada!).
from sklearn.preprocessing import TargetEncoder # sklearn >= 1.3
te = TargetEncoder()
X_te = te.fit_transform(df[["loja"]], y)
Guia rápido: baixa cardinalidade → one-hot; ordem natural → ordinal; alta cardinalidade → target encoding ou embeddings; modelos de árvore (LightGBM/CatBoost) lidam nativamente com categorias.
6.4 Escalonamento de variáveis numéricas
Algoritmos baseados em distância ou gradiente (KNN, SVM, redes neurais, regressões regularizadas, K-Means, PCA) exigem escalas comparáveis. Árvores e florestas não precisam.
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler, PowerTransformer
StandardScaler() # (x - média) / desvio → média 0, desvio 1
MinMaxScaler() # para o intervalo [0, 1]
RobustScaler() # usa mediana e IQR → robusto a outliers
PowerTransformer() # Yeo-Johnson: aproxima de uma normal (corrige assimetria)
Regra anti-vazamento:
fitdo scaler somente no treino; depoistransformno treino e no teste. Nuncafitno conjunto completo.
6.5 Engenharia de atributos (feature engineering)
É a arte de criar variáveis que exponham o padrão ao modelo. Exemplos recorrentes:
# Datas → componentes e sinais cíclicos
df["hora"] = df["data"].dt.hour
df["dia_semana"] = df["data"].dt.dayofweek
df["fim_de_semana"] = (df["dia_semana"] >= 5).astype(int)
df["hora_sin"] = np.sin(2 * np.pi * df["hora"] / 24) # 23h fica perto de 0h
df["hora_cos"] = np.cos(2 * np.pi * df["hora"] / 24)
# Razões e interações
df["preco_por_item"] = df["receita"] / df["quantidade"]
# Agregações por grupo (comportamento relativo)
df["receita_vs_loja"] = df["receita"] / df.groupby("loja")["receita"].transform("mean")
# Janelas temporais (lags e móveis) — ESSENCIAL em séries temporais
diario = df.set_index("data")["receita"].resample("D").sum().to_frame("receita")
diario["lag_1"] = diario["receita"].shift(1)
diario["lag_7"] = diario["receita"].shift(7)
diario["mm_7"] = diario["receita"].shift(1).rolling(7).mean()
# Transformações de distribuição
df["log_receita"] = np.log1p(df["receita"])
# Polinômios e interações automáticas
from sklearn.preprocessing import PolynomialFeatures
X_poly = PolynomialFeatures(degree=2, include_bias=False).fit_transform(X)
# Texto simples
df["n_palavras"] = df["descricao"].str.split().str.len()
6.6 Vazamento de dados (data leakage)
O erro mais traiçoeiro de ML: informações do futuro (ou do alvo) contaminam o treino, gerando métricas ilusoriamente ótimas que desabam em produção.
Formas comuns: 1. Vazamento de alvo: uma feature é consequência do alvo (ex.: "recebeu ligação de cobrança" para prever inadimplência). 2. Vazamento treino-teste: escalar/imputar/selecionar features usando o conjunto completo antes do split. 3. Vazamento temporal: treinar com dados posteriores aos de teste. 4. Vazamento de grupo: o mesmo paciente/cliente presente no treino e no teste.
Antídotos: split antes de qualquer transformação; Pipeline do sklearn (Capítulo 13); splits temporais (TimeSeriesSplit) e por grupo (GroupKFold); desconfiança saudável de resultados "bons demais".
Capítulo 7 — Matemática e Estatística para ML
Você não precisa ser matemático, mas precisa entender quatro pilares: álgebra linear, cálculo, probabilidade e estatística. Este capítulo dá a intuição e o vocabulário mínimo — com profundidade suficiente para os capítulos avançados.
7.1 Álgebra linear
- Vetor: lista de números; em ML, cada amostra é um vetor de features (um ponto no espaço n-dimensional).
- Matriz: tabela de números; o dataset inteiro é uma matriz
Xde shape(n_amostras, n_features). - Produto escalar
x·w = Σ xᵢwᵢ: mede alinhamento entre vetores; é a operação central de uma regressão linear e de um neurônio. - Multiplicação de matrizes
C = A @ B: composição de transformações lineares; uma camada de rede neural éX @ W + b. - Norma
‖x‖: comprimento do vetor; normas L1 e L2 fundamentam as regularizações Lasso e Ridge. - Autovalores/autovetores: direções que uma transformação apenas estica; base do PCA.
7.2 Cálculo: derivadas e gradientes
- A derivada mede a taxa de variação. O gradiente ∇f é o vetor de derivadas parciais: aponta na direção de maior crescimento da função.
- Gradiente descendente: para minimizar uma função de custo
L(w), atualize os parâmetros no sentido oposto ao gradiente:
w ← w − η · ∇L(w) (η = taxa de aprendizado)
# Gradiente descendente do zero: minimizar f(w) = (w - 3)²
w, lr = 0.0, 0.1
for passo in range(50):
grad = 2 * (w - 3) # derivada de (w-3)²
w -= lr * grad
print(w) # ≈ 3.0 — o mínimo
- A regra da cadeia permite derivar funções compostas — é o coração do backpropagation (Capítulo 14).
7.3 Probabilidade
- Variável aleatória e distribuições: Bernoulli (moeda), Binomial (n moedas), Poisson (contagens), Normal/Gaussiana (a onipresente, pelo Teorema Central do Limite), Exponencial (tempos de espera).
- Probabilidade condicional:
P(A|B) = P(A ∩ B) / P(B). - Teorema de Bayes — atualizar crenças com evidência:
P(H|D) = P(D|H) · P(H) / P(D)
posterior = verossimilhança × prior / evidência
Exemplo clínico clássico: doença com prevalência 1%, teste com 95% de sensibilidade e 95% de especificidade. Se o teste deu positivo:
p_d = 0.01
p_pos_dado_d = 0.95
p_pos_dado_nd = 0.05
p_pos = p_pos_dado_d * p_d + p_pos_dado_nd * (1 - p_d)
p_d_dado_pos = p_pos_dado_d * p_d / p_pos
print(p_d_dado_pos) # ≈ 0.161 — apenas 16%! O prior baixo domina.
- Esperança E[X] (média ponderada) e variância Var(X) (dispersão).
- Máxima verossimilhança (MLE): escolher os parâmetros que tornam os dados observados mais prováveis — é o princípio por trás do treino da maioria dos modelos (minimizar log-loss = maximizar verossimilhança).
7.4 Estatística inferencial
- População vs. amostra; estimadores e seus erros padrão.
- Intervalo de confiança: faixa plausível para um parâmetro. Com bootstrap:
import numpy as np
np.random.seed(0)
dados = np.random.exponential(2, size=200)
medias = [np.random.choice(dados, size=len(dados), replace=True).mean()
for _ in range(5000)]
np.percentile(medias, [2.5, 97.5]) # IC 95% para a média
- Teste de hipóteses: p-valor = probabilidade de observar um efeito tão extremo quanto o obtido, assumindo que a hipótese nula é verdadeira. p < 0,05 é convenção, não verdade revelada.
from scipy import stats
a = np.random.normal(100, 10, 200) # grupo controle
b = np.random.normal(103, 10, 200) # grupo tratamento
t, p = stats.ttest_ind(a, b)
print(f"t={t:.2f}, p={p:.4f}")
- Testes úteis: t de Student (médias), Mann-Whitney (não paramétrico), qui-quadrado (
stats.chi2_contingency) para categorias, ANOVA para 3+ grupos. - Erros tipo I/II, poder estatístico e o problema das comparações múltiplas (teste 20 hipóteses a 5% e espere 1 falso positivo).
- Testes A/B são a aplicação direta disso em produto e marketing.
7.5 Teoria da informação (breve)
- Entropia
H = −Σ p log p: incerteza de uma distribuição. Base do critério de divisão de árvores de decisão. - Entropia cruzada: distância entre a distribuição prevista e a real — é a função de custo padrão de classificadores e LLMs.
- Divergência KL: quanta informação se perde ao aproximar uma distribuição por outra (aparece em VAEs e no treinamento com RLHF).
Capítulo 8 — Fundamentos de Machine Learning
8.1 Notação e anatomia de um problema supervisionado
X: matriz de features, shape(n_amostras, n_features).y: vetor alvo, shape(n_amostras,).- Objetivo: aprender
ftal quef(X) ≈ yem dados novos — a palavra-chave é generalização.
8.2 Treino, validação e teste
from sklearn.model_selection import train_test_split
X_temp, X_test, y_temp, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y) # stratify preserva proporções
X_train, X_val, y_train, y_val = train_test_split(
X_temp, y_temp, test_size=0.25, random_state=42, stratify=y_temp)
- Treino: ajusta os parâmetros do modelo.
- Validação: escolhe hiperparâmetros e compara modelos.
- Teste: usado uma única vez, no final, para estimar o desempenho real. Se você olhar o teste várias vezes e ajustar o modelo, ele vira um segundo conjunto de validação e a estimativa fica otimista.
8.3 Underfitting, overfitting e o dilema viés–variância
- Underfitting (alto viés): modelo simples demais; erra no treino e no teste. Sintoma: métricas ruins em ambos.
- Overfitting (alta variância): modelo decora o treino, inclusive o ruído; excelente no treino, ruim no teste. Sintoma: grande diferença treino × validação.
Erro esperado = viés² (rigidez das suposições) + variância (sensibilidade à amostra) + ruído irredutível.
Remédios para overfitting: mais dados, modelo mais simples, regularização, seleção de features, parada antecipada, ensembles. Para underfitting: modelo mais expressivo, melhores features, menos regularização.
# Curvas de validação: visualizando o dilema
from sklearn.model_selection import validation_curve
from sklearn.tree import DecisionTreeClassifier
profundidades = range(1, 21)
tr, val = validation_curve(DecisionTreeClassifier(random_state=42), X, y,
param_name="max_depth", param_range=profundidades,
cv=5, scoring="accuracy")
import matplotlib.pyplot as plt
plt.plot(profundidades, tr.mean(axis=1), label="treino")
plt.plot(profundidades, val.mean(axis=1), label="validação")
plt.xlabel("max_depth"); plt.ylabel("acurácia"); plt.legend()
# Treino sobe sempre; validação sobe, atinge o pico e cai → overfitting à direita.
8.4 Validação cruzada (cross-validation)
Uma única divisão treino/validação depende da sorte do sorteio. A validação cruzada k-fold divide os dados em k partes, treina k vezes (cada parte é validação uma vez) e reporta média ± desvio:
from sklearn.model_selection import cross_val_score, KFold, StratifiedKFold
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(modelo, X, y, cv=cv, scoring="f1")
print(f"{scores.mean():.3f} ± {scores.std():.3f}")
Variações críticas: - StratifiedKFold: preserva a proporção de classes (padrão para classificação). - GroupKFold: garante que o mesmo grupo (paciente, cliente) não apareça em treino e validação. - TimeSeriesSplit: para dados temporais, valida sempre no futuro do treino — nunca embaralhe séries temporais.
8.5 Métricas de avaliação
Regressão
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import numpy as np
mae = mean_absolute_error(y_true, y_pred) # erro médio absoluto (na unidade do alvo)
rmse = np.sqrt(mean_squared_error(y_true, y_pred)) # penaliza erros grandes
r2 = r2_score(y_true, y_pred) # fração da variância explicada
mape = np.mean(np.abs((y_true - y_pred) / y_true)) # erro percentual (cuidado com y=0)
Classificação
A matriz de confusão organiza acertos e erros:
| Previsto Positivo | Previsto Negativo | |
|---|---|---|
| Real Positivo | VP (verdadeiro positivo) | FN (falso negativo) |
| Real Negativo | FP (falso positivo) | VN (verdadeiro negativo) |
- Acurácia = (VP+VN)/total. Engana em classes desbalanceadas: com 99% de não-fraude, prever "nunca é fraude" dá 99% de acurácia e zero utilidade.
- Precisão = VP/(VP+FP): dos que previ positivos, quantos eram? (custo do falso alarme)
- Recall (sensibilidade) = VP/(VP+FN): dos positivos reais, quantos achei? (custo de deixar passar)
- F1 = média harmônica de precisão e recall.
- ROC-AUC: probabilidade de o modelo ranquear um positivo acima de um negativo; independe do limiar.
- PR-AUC (average precision): preferível com forte desbalanceamento.
- Log-loss: qualidade das probabilidades, não só da classe.
from sklearn.metrics import (classification_report, confusion_matrix,
roc_auc_score, average_precision_score,
RocCurveDisplay, PrecisionRecallDisplay)
y_pred = modelo.predict(X_test)
y_proba = modelo.predict_proba(X_test)[:, 1]
print(confusion_matrix(y_test, y_pred))
print(classification_report(y_test, y_pred, digits=3))
print("ROC-AUC:", roc_auc_score(y_test, y_proba))
print("PR-AUC :", average_precision_score(y_test, y_proba))
RocCurveDisplay.from_predictions(y_test, y_proba)
Escolhendo o limiar de decisão
predict usa 0,5 por padrão, mas o limiar deve refletir os custos do negócio (perder uma fraude custa mais que um falso alarme?):
from sklearn.metrics import precision_recall_curve
prec, rec, thr = precision_recall_curve(y_test, y_proba)
# escolha, por exemplo, o maior limiar com recall >= 0.90
8.6 Classes desbalanceadas
Estratégias:
1. Métricas certas (PR-AUC, F1, recall por classe) — nunca acurácia isolada.
2. class_weight="balanced" nos modelos do sklearn.
3. Reamostragem: undersampling da majoritária, oversampling da minoritária, SMOTE (gera sintéticos interpolando vizinhos):
# pip install imbalanced-learn
from imblearn.over_sampling import SMOTE
X_res, y_res = SMOTE(random_state=42).fit_resample(X_train, y_train)
# Importante: reamostrar SOMENTE o treino, nunca o teste.
- Ajuste de limiar orientado a custo (quase sempre a solução mais simples e eficaz).
8.7 O "No Free Lunch" e a prática
Não existe algoritmo universalmente melhor. Na prática, para dados tabulares, a hierarquia empírica costuma ser: gradient boosting (LightGBM/XGBoost) ≥ florestas ≥ modelos lineares bem preparados ≥ redes neurais. Para imagens, áudio e texto, deep learning domina. Sempre comece com um baseline simples (média, classe majoritária, regressão linear/logística): ele define o piso que qualquer modelo sofisticado precisa superar.
Capítulo 9 — Regressão
9.1 Regressão Linear
Modelo: ŷ = w₀ + w₁x₁ + ... + wₙxₙ. Os pesos são encontrados minimizando a soma dos erros ao quadrado (mínimos quadrados ordinários — solução fechada w = (XᵀX)⁻¹Xᵀy ou gradiente descendente).
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error, r2_score
# Dataset sintético: preço de imóvel
rng = np.random.default_rng(42)
n = 500
area = rng.uniform(30, 200, n)
quartos = rng.integers(1, 5, n)
idade = rng.uniform(0, 40, n)
preco = 2000*area + 15000*quartos - 800*idade + rng.normal(0, 20000, n)
X = np.column_stack([area, quartos, idade])
X_tr, X_te, y_tr, y_te = train_test_split(X, preco, test_size=0.2, random_state=42)
lr = LinearRegression().fit(X_tr, y_tr)
print("Coeficientes:", lr.coef_) # ≈ [2000, 15000, -800]
print("Intercepto :", lr.intercept_)
pred = lr.predict(X_te)
print("MAE:", mean_absolute_error(y_te, pred))
print("R² :", r2_score(y_te, pred))
Interpretação: cada coeficiente é o efeito no alvo de +1 unidade na feature, mantidas as demais constantes. É a grande vantagem dos modelos lineares: transparência.
Diagnóstico de resíduos (erros y − ŷ): devem parecer ruído sem padrão. Padrões nos resíduos indicam não linearidade, heterocedasticidade ou variável omitida:
res = y_te - pred
import matplotlib.pyplot as plt
plt.scatter(pred, res, alpha=0.4); plt.axhline(0, color="red")
plt.xlabel("previsto"); plt.ylabel("resíduo")
Para inferência estatística completa (p-valores, IC dos coeficientes), use statsmodels:
import statsmodels.api as sm
X_sm = sm.add_constant(X_tr)
print(sm.OLS(y_tr, X_sm).fit().summary())
9.2 Regressão Polinomial e não linearidade
from sklearn.preprocessing import PolynomialFeatures
from sklearn.pipeline import make_pipeline
modelo = make_pipeline(PolynomialFeatures(degree=3), LinearRegression())
modelo.fit(X_tr, y_tr)
Grau alto demais = overfitting garantido. Combine com regularização.
9.3 Regularização: Ridge, Lasso e Elastic Net
Adiciona ao custo uma penalidade sobre o tamanho dos pesos, controlando a complexidade:
- Ridge (L2): penaliza
Σw². Encolhe todos os pesos; ótimo com features correlacionadas. - Lasso (L1): penaliza
Σ|w|. Zera pesos → seleção automática de features. - Elastic Net: combinação das duas.
from sklearn.linear_model import Ridge, Lasso, ElasticNet, RidgeCV, LassoCV
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
# Regularização EXIGE features na mesma escala
ridge = make_pipeline(StandardScaler(), RidgeCV(alphas=np.logspace(-3, 3, 20)))
lasso = make_pipeline(StandardScaler(), LassoCV(cv=5, random_state=42))
ridge.fit(X_tr, y_tr); lasso.fit(X_tr, y_tr)
print("Pesos Lasso:", lasso[-1].coef_) # alguns exatamente 0
alpha controla a força: alpha→0 vira regressão linear pura; alpha grande → underfitting.
9.4 Outras regressões importantes
from sklearn.neighbors import KNeighborsRegressor # média dos k vizinhos
from sklearn.tree import DecisionTreeRegressor # partições retangulares
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.svm import SVR # margens com tolerância ε
from sklearn.linear_model import HuberRegressor # robusta a outliers
from sklearn.linear_model import QuantileRegressor # prevê quantis (intervalos!)
- Regressão quantílica prevê, por exemplo, os percentis 10 e 90 — gerando intervalos de previsão, valiosos para decisão sob incerteza.
- GLMs (Poisson, Gamma —
PoissonRegressor) para contagens e valores positivos assimétricos (sinistros, demanda).
9.5 Comparando modelos de regressão — template
from sklearn.model_selection import cross_validate
from sklearn.ensemble import RandomForestRegressor
candidatos = {
"linear": make_pipeline(StandardScaler(), LinearRegression()),
"ridge": make_pipeline(StandardScaler(), Ridge(alpha=1.0)),
"floresta": RandomForestRegressor(n_estimators=300, random_state=42),
}
for nome, m in candidatos.items():
cv = cross_validate(m, X, preco, cv=5,
scoring=("neg_mean_absolute_error", "r2"))
print(f"{nome:10s} MAE={-cv['test_neg_mean_absolute_error'].mean():,.0f} "
f"R²={cv['test_r2'].mean():.3f}")
Capítulo 10 — Classificação
10.1 Regressão Logística
Apesar do nome, é um classificador. Passa a combinação linear pela função sigmoide, produzindo uma probabilidade:
p = σ(z) = 1 / (1 + e^(−z)), onde z = w·x + b
Treinada minimizando a entropia cruzada (log-loss). Os coeficientes têm leitura em odds: exp(w) é o fator multiplicativo na chance do evento por unidade da feature.
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, roc_auc_score
X, y = load_breast_cancer(return_X_y=True, as_frame=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2,
stratify=y, random_state=42)
clf = make_pipeline(StandardScaler(),
LogisticRegression(max_iter=2000, C=1.0))
clf.fit(X_tr, y_tr)
proba = clf.predict_proba(X_te)[:, 1]
print(classification_report(y_te, clf.predict(X_te), digits=3))
print("ROC-AUC:", roc_auc_score(y_te, proba))
C é o inverso da regularização (C pequeno = mais regularizado). Para multiclasse, o sklearn usa softmax automaticamente.
10.2 K-Nearest Neighbors (KNN)
Classifica pelo voto dos k vizinhos mais próximos. Não há "treino" real (método preguiçoso); toda a conta acontece na predição.
from sklearn.neighbors import KNeighborsClassifier
knn = make_pipeline(StandardScaler(),
KNeighborsClassifier(n_neighbors=7, weights="distance"))
- Exige escalonamento (distâncias!).
kpequeno → variância alta;kgrande → viés alto.- Sofre com a maldição da dimensionalidade: em muitas dimensões, tudo fica "longe" e as distâncias perdem significado.
10.3 Naive Bayes
Aplica o teorema de Bayes assumindo (ingenuamente) independência entre as features dado a classe. Rápido, funciona surpreendentemente bem em texto:
from sklearn.naive_bayes import MultinomialNB, GaussianNB
from sklearn.feature_extraction.text import TfidfVectorizer
textos = ["oferta imperdível clique agora", "reunião amanhã às 10",
"ganhe dinheiro rápido", "segue relatório em anexo"]
rotulos = [1, 0, 1, 0] # 1 = spam
nb = make_pipeline(TfidfVectorizer(), MultinomialNB())
nb.fit(textos, rotulos)
print(nb.predict(["clique e ganhe uma oferta"])) # [1]
10.4 Support Vector Machines (SVM)
Busca o hiperplano de margem máxima entre as classes. O truque do kernel projeta implicitamente os dados em dimensões maiores, tornando separável o que não era:
from sklearn.svm import SVC
svm = make_pipeline(StandardScaler(),
SVC(kernel="rbf", C=1.0, gamma="scale", probability=True))
C: tolerância a erros na margem (C grande → margens duras → risco de overfitting).gamma(kernel RBF): alcance da influência de cada ponto (grande → fronteiras muito flexíveis).- Forte em datasets pequenos/médios com fronteiras complexas; caro em dados massivos.
10.5 Árvores de Decisão
Aprendem perguntas sequenciais ("idade > 30?") que particionam o espaço, escolhendo em cada nó a divisão que mais reduz a impureza (Gini ou entropia).
from sklearn.tree import DecisionTreeClassifier, plot_tree
import matplotlib.pyplot as plt
arv = DecisionTreeClassifier(max_depth=3, min_samples_leaf=20, random_state=42)
arv.fit(X_tr, y_tr)
plt.figure(figsize=(16, 7))
plot_tree(arv, feature_names=X.columns, class_names=["maligno", "benigno"],
filled=True, fontsize=8)
Prós: interpretáveis, sem necessidade de escala, capturam interações e não linearidades. Contras: instáveis e propensas a overfitting se não podadas (max_depth, min_samples_leaf, ccp_alpha). A solução para a instabilidade são os ensembles (próximo capítulo).
10.6 Qual classificador escolher?
| Situação | Boa primeira escolha |
|---|---|
| Baseline rápido e interpretável | Regressão logística |
| Tabular, desempenho máximo | Gradient boosting (Cap. 11) |
| Texto clássico | TF-IDF + Logística ou Naive Bayes |
| Poucos dados, fronteira complexa | SVM (RBF) |
| Precisa explicar cada decisão | Árvore rasa ou logística |
| Imagens/áudio/linguagem em escala | Deep learning (Caps. 14–18) |
Capítulo 11 — Métodos de Ensemble: Bagging, Random Forest e Boosting
Ideia central: combinar muitos modelos fracos produz um modelo forte, porque erros descorrelacionados se cancelam ("sabedoria das multidões").
11.1 Bagging e Random Forest
Bagging (bootstrap aggregating): treine cada árvore em uma amostra bootstrap dos dados e agregue por voto/média. Reduz variância.
Random Forest = bagging + em cada divisão, apenas um subconjunto aleatório de features é considerado — descorrelacionando ainda mais as árvores.
from sklearn.ensemble import RandomForestClassifier
import pandas as pd
rf = RandomForestClassifier(
n_estimators=500, # nº de árvores (mais = melhor, com retornos decrescentes)
max_depth=None, # árvores profundas; a floresta controla o overfitting
max_features="sqrt", # features sorteadas por divisão
min_samples_leaf=2,
n_jobs=-1, random_state=42,
oob_score=True, # validação "grátis" com amostras fora do bootstrap
)
rf.fit(X_tr, y_tr)
print("OOB score:", rf.oob_score_)
# Importância de features (baseada em impureza — enviesada p/ alta cardinalidade)
imp = pd.Series(rf.feature_importances_, index=X.columns).sort_values()
imp.tail(10).plot.barh()
# Importância por permutação — mais confiável (mede queda de desempenho
# ao embaralhar cada feature no conjunto de validação)
from sklearn.inspection import permutation_importance
pi = permutation_importance(rf, X_te, y_te, n_repeats=10, random_state=42)
11.2 Boosting: aprender com os erros
Em vez de árvores paralelas e independentes, o boosting constrói árvores sequenciais, cada uma corrigindo os erros (resíduos/gradientes) das anteriores. Reduz viés e costuma vencer em dados tabulares.
Gradient Boosting: a cada passo, ajusta uma árvore pequena ao gradiente da função de perda e soma sua contribuição multiplicada pela learning_rate.
from sklearn.ensemble import GradientBoostingClassifier, HistGradientBoostingClassifier
# HistGradientBoosting: implementação rápida do sklearn (estilo LightGBM),
# lida nativamente com NaN e com categorias
hgb = HistGradientBoostingClassifier(
max_iter=400, learning_rate=0.08, max_depth=None,
early_stopping=True, validation_fraction=0.15, random_state=42)
hgb.fit(X_tr, y_tr)
11.3 XGBoost e LightGBM
As implementações dominantes em competições e indústria:
# pip install xgboost lightgbm
import xgboost as xgb
import lightgbm as lgb
xgbc = xgb.XGBClassifier(
n_estimators=2000, learning_rate=0.05,
max_depth=6, subsample=0.8, colsample_bytree=0.8,
reg_lambda=1.0, eval_metric="auc",
early_stopping_rounds=100, random_state=42)
xgbc.fit(X_tr, y_tr, eval_set=[(X_te, y_te)], verbose=False)
print("Melhor iteração:", xgbc.best_iteration)
lgbc = lgb.LGBMClassifier(
n_estimators=3000, learning_rate=0.05, num_leaves=31,
subsample=0.8, colsample_bytree=0.8, random_state=42)
lgbc.fit(X_tr, y_tr, eval_set=[(X_te, y_te)],
callbacks=[lgb.early_stopping(100), lgb.log_evaluation(0)])
Hiperparâmetros que mais importam (em ordem):
1. learning_rate × n_estimators: taxa menor + mais árvores + early stopping = padrão vencedor.
2. Complexidade da árvore: max_depth / num_leaves, min_child_weight.
3. Aleatoriedade: subsample (linhas), colsample_bytree (colunas).
4. Regularização: reg_alpha (L1), reg_lambda (L2).
CatBoost merece menção: trata variáveis categóricas nativamente com target encoding ordenado, exigindo pouquíssimo pré-processamento.
11.4 Stacking e Voting
from sklearn.ensemble import StackingClassifier, VotingClassifier
from sklearn.linear_model import LogisticRegression
stack = StackingClassifier(
estimators=[("rf", rf), ("hgb", hgb)],
final_estimator=LogisticRegression(max_iter=1000),
cv=5) # as previsões-base são geradas via CV (anti-vazamento)
stack.fit(X_tr, y_tr)
- Voting: média simples (soft) ou voto majoritário (hard) dos modelos.
- Stacking: um meta-modelo aprende a combinar as previsões dos modelos-base. Ganhos marginais, mas frequentes em competições.
Capítulo 12 — Aprendizado Não Supervisionado
12.1 K-Means
Particiona os dados em k grupos minimizando a distância de cada ponto ao centróide do seu grupo. Algoritmo: inicializa centróides → atribui pontos → recalcula centróides → repete até convergir.
from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import silhouette_score
import matplotlib.pyplot as plt
import numpy as np
Xs = StandardScaler().fit_transform(X_clientes) # SEMPRE escale antes
# Escolhendo k: método do cotovelo + silhueta
inercias, silhuetas = [], []
for k in range(2, 11):
km = KMeans(n_clusters=k, n_init=10, random_state=42).fit(Xs)
inercias.append(km.inertia_)
silhuetas.append(silhouette_score(Xs, km.labels_))
fig, ax = plt.subplots(1, 2, figsize=(12, 4))
ax[0].plot(range(2, 11), inercias, "o-"); ax[0].set_title("Cotovelo (inércia)")
ax[1].plot(range(2, 11), silhuetas, "o-"); ax[1].set_title("Silhueta (maior = melhor)")
km = KMeans(n_clusters=4, n_init=10, random_state=42).fit(Xs)
df_clientes["cluster"] = km.labels_
# Perfil de cada cluster — o passo mais importante: dar SIGNIFICADO aos grupos
print(df_clientes.groupby("cluster").mean().round(2))
Limitações: assume clusters esféricos e de tamanho similar; sensível a outliers e à inicialização (n_init mitiga); k deve ser fixado a priori.
12.2 Clusterização hierárquica
Constrói uma árvore de fusões (dendrograma); você "corta" na altura desejada. Não exige k a priori e revela estrutura aninhada.
from scipy.cluster.hierarchy import dendrogram, linkage
from sklearn.cluster import AgglomerativeClustering
Z = linkage(Xs[:200], method="ward")
dendrogram(Z, truncate_mode="level", p=5)
agg = AgglomerativeClustering(n_clusters=4, linkage="ward").fit(Xs)
12.3 DBSCAN e HDBSCAN
Clusteriza por densidade: regiões densas viram clusters; pontos isolados viram ruído (rótulo −1). Encontra formas arbitrárias e detecta outliers de graça.
from sklearn.cluster import DBSCAN, HDBSCAN
db = DBSCAN(eps=0.5, min_samples=10).fit(Xs) # eps: raio de vizinhança
hdb = HDBSCAN(min_cluster_size=25).fit(Xs) # dispensa eps; mais robusto
print(np.unique(db.labels_, return_counts=True))
12.4 Mistura de Gaussianas (GMM)
Clusterização probabilística: cada ponto tem probabilidade de pertencer a cada componente gaussiano (soft clustering). Treinada por Expectation-Maximization; BIC/AIC ajudam a escolher o número de componentes.
from sklearn.mixture import GaussianMixture
gmm = GaussianMixture(n_components=4, covariance_type="full", random_state=42)
gmm.fit(Xs)
probs = gmm.predict_proba(Xs) # pertencimento suave
12.5 Redução de dimensionalidade
PCA (Análise de Componentes Principais)
Projeta os dados nas direções (componentes) de maior variância — combinações lineares das features originais, ortogonais entre si.
from sklearn.decomposition import PCA
pca = PCA(n_components=0.95) # nº de componentes p/ reter 95% da variância
Xp = pca.fit_transform(Xs)
print("Componentes:", pca.n_components_)
print("Variância explicada:", pca.explained_variance_ratio_.round(3))
# Visualização 2D
p2 = PCA(n_components=2).fit_transform(Xs)
plt.scatter(p2[:, 0], p2[:, 1], c=km.labels_, cmap="tab10", alpha=0.5)
Usos: visualização, remoção de multicolinearidade, compressão, aceleração de modelos. Perde-se interpretabilidade direta das features.
t-SNE e UMAP
Técnicas não lineares para visualização, preservando vizinhanças locais:
from sklearn.manifold import TSNE
Xt = TSNE(n_components=2, perplexity=30, random_state=42).fit_transform(Xs)
# pip install umap-learn — mais rápido, preserva melhor a estrutura global
import umap
Xu = umap.UMAP(n_neighbors=15, min_dist=0.1, random_state=42).fit_transform(Xs)
Cuidado na leitura: distâncias entre clusters e tamanhos aparentes em t-SNE não são fielmente interpretáveis; use apenas como mapa qualitativo.
12.6 Detecção de anomalias
from sklearn.ensemble import IsolationForest
from sklearn.neighbors import LocalOutlierFactor
iso = IsolationForest(contamination=0.02, random_state=42).fit(Xs)
anomalia = iso.predict(Xs) # -1 = anomalia, 1 = normal
score = iso.score_samples(Xs) # quanto menor, mais anômalo
lof = LocalOutlierFactor(n_neighbors=20)
anomalia_lof = lof.fit_predict(Xs) # densidade local vs. vizinhos
Aplicações: fraude, falhas de máquinas, intrusão de rede, erros de cadastro.
12.7 Regras de associação (market basket)
"Quem compra X tende a comprar Y" — suporte, confiança e lift:
# pip install mlxtend
from mlxtend.frequent_patterns import apriori, association_rules
freq = apriori(cesta_binaria, min_support=0.02, use_colnames=True)
regras = association_rules(freq, metric="lift", min_threshold=1.2)
Capítulo 13 — Pipelines, Tuning e Interpretabilidade
13.1 Pipelines: pré-processamento sem vazamento
O Pipeline encapsula transformações + modelo em um único objeto: o fit só vê o treino, e a validação cruzada refaz o pré-processamento dentro de cada fold — eliminando o vazamento por construção.
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.ensemble import HistGradientBoostingClassifier
col_num = ["idade", "renda", "tempo_cliente"]
col_cat = ["estado", "plano", "canal"]
prep = ColumnTransformer([
("num", Pipeline([("imp", SimpleImputer(strategy="median")),
("sc", StandardScaler())]), col_num),
("cat", Pipeline([("imp", SimpleImputer(strategy="most_frequent")),
("ohe", OneHotEncoder(handle_unknown="ignore"))]), col_cat),
])
pipe = Pipeline([
("prep", prep),
("modelo", HistGradientBoostingClassifier(random_state=42)),
])
pipe.fit(X_tr, y_tr) # tudo em uma linha, sem vazamento
13.2 Busca de hiperparâmetros
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV
from scipy.stats import loguniform, randint
# Grid: exaustivo — só para poucos parâmetros
grid = GridSearchCV(pipe, {
"modelo__learning_rate": [0.03, 0.1, 0.3],
"modelo__max_depth": [3, 6, None],
}, cv=5, scoring="roc_auc", n_jobs=-1)
# Randomizado: amostra o espaço — melhor custo-benefício
rnd = RandomizedSearchCV(pipe, {
"modelo__learning_rate": loguniform(1e-3, 3e-1),
"modelo__max_iter": randint(100, 1500),
"modelo__max_leaf_nodes": randint(15, 127),
}, n_iter=40, cv=5, scoring="roc_auc", n_jobs=-1, random_state=42)
rnd.fit(X_tr, y_tr)
print(rnd.best_params_, rnd.best_score_)
Otimização bayesiana com Optuna
Aprende com as tentativas anteriores para propor as próximas — mais eficiente que busca aleatória:
# pip install optuna
import optuna
from sklearn.model_selection import cross_val_score
def objetivo(trial):
params = {
"learning_rate": trial.suggest_float("learning_rate", 1e-3, 0.3, log=True),
"max_iter": trial.suggest_int("max_iter", 200, 2000),
"max_leaf_nodes": trial.suggest_int("max_leaf_nodes", 15, 255),
"l2_regularization": trial.suggest_float("l2", 1e-8, 10, log=True),
}
m = HistGradientBoostingClassifier(**params, random_state=42)
return cross_val_score(m, X_tr, y_tr, cv=5, scoring="roc_auc").mean()
study = optuna.create_study(direction="maximize")
study.optimize(objetivo, n_trials=60, show_progress_bar=True)
print(study.best_params, study.best_value)
Validação aninhada: se você usa CV para escolher hiperparâmetros, a estimativa final honesta exige um loop externo de CV (ou o conjunto de teste intocado).
13.3 Seleção de features
from sklearn.feature_selection import (SelectKBest, mutual_info_classif,
RFE, SelectFromModel)
SelectKBest(mutual_info_classif, k=20) # filtro univariado
RFE(estimator=LogisticRegression(max_iter=1000), n_features_to_select=15)
SelectFromModel(lgbc, threshold="median") # via importância do modelo
13.4 Interpretabilidade: SHAP e efeitos parciais
SHAP distribui, com base em teoria dos jogos (valores de Shapley), a contribuição de cada feature para cada previsão individual:
# pip install shap
import shap
modelo = lgbc.fit(X_tr, y_tr)
explainer = shap.TreeExplainer(modelo)
sv = explainer(X_te)
shap.plots.beeswarm(sv) # visão global: importância + direção do efeito
shap.plots.bar(sv) # importância média absoluta
shap.plots.waterfall(sv[0]) # explicação de UMA previsão (por que este cliente?)
shap.plots.scatter(sv[:, "renda"]) # efeito da feature ao longo do seu range
Outras ferramentas: Partial Dependence Plots (sklearn.inspection.PartialDependenceDisplay), importância por permutação (Cap. 11) e LIME para explicações locais aproximadas.
13.5 Calibração de probabilidades
Muitos modelos produzem escores mal calibrados (um "0,8" que não acontece 80% das vezes). Se as probabilidades serão usadas para decisão (precificação, risco), calibre:
from sklearn.calibration import CalibratedClassifierCV, CalibrationDisplay
cal = CalibratedClassifierCV(modelo, method="isotonic", cv=5).fit(X_tr, y_tr)
CalibrationDisplay.from_estimator(cal, X_te, y_te, n_bins=10)
13.6 Salvando e servindo o modelo
import joblib
joblib.dump(pipe, "modelo_v1.joblib") # persiste o pipeline INTEIRO
pipe = joblib.load("modelo_v1.joblib")
# API mínima com FastAPI:
# pip install fastapi uvicorn
"""
from fastapi import FastAPI
import joblib, pandas as pd
app = FastAPI()
modelo = joblib.load("modelo_v1.joblib")
@app.post("/prever")
def prever(dados: dict):
df = pd.DataFrame([dados])
proba = float(modelo.predict_proba(df)[0, 1])
return {"probabilidade": proba}
"""
Capítulo 14 — Deep Learning: Redes Neurais do Zero ao PyTorch
14.1 O neurônio artificial
Um neurônio calcula uma soma ponderada das entradas e aplica uma função de ativação não linear:
saída = ativação(w·x + b)
Ativações comuns:
- ReLU max(0, z): padrão em camadas ocultas — simples, evita saturação do gradiente.
- Sigmoide 1/(1+e^-z): saída em (0,1) — probabilidade binária na camada final.
- Softmax: transforma um vetor em distribuição de probabilidade — multiclasse.
- tanh, GELU (usada em Transformers), LeakyReLU.
Sem não linearidade, empilhar camadas seria inútil: composição de funções lineares é linear. A não linearidade é o que permite aproximar qualquer função (teorema da aproximação universal).
14.2 A rede e o treinamento
Uma MLP (perceptron multicamadas) empilha camadas: entrada → ocultas → saída. O treinamento repete o ciclo:
- Forward pass: dados atravessam a rede e produzem previsões.
- Perda (loss): mede o erro (MSE para regressão; entropia cruzada para classificação).
- Backpropagation: a regra da cadeia calcula o gradiente da perda em relação a cada peso, propagando o erro de trás para frente.
- Otimizador: atualiza os pesos (SGD, Adam/AdamW — os padrões atuais).
Conceitos de treino: - Batch: subconjunto de amostras por atualização (mini-batch, tipicamente 32–512). - Época: uma passada completa pelos dados de treino. - Learning rate: o hiperparâmetro mais importante; alto demais diverge, baixo demais não sai do lugar. Schedulers (cosine, step) reduzem a taxa ao longo do treino.
14.3 Uma rede neural do zero em NumPy
Para desmistificar: MLP de 1 camada oculta resolvendo o XOR (impossível para modelos lineares):
import numpy as np
rng = np.random.default_rng(0)
X = np.array([[0,0],[0,1],[1,0],[1,1]], dtype=float)
y = np.array([[0],[1],[1],[0]], dtype=float) # XOR
W1 = rng.normal(0, 1, (2, 8)); b1 = np.zeros((1, 8))
W2 = rng.normal(0, 1, (8, 1)); b2 = np.zeros((1, 1))
sig = lambda z: 1 / (1 + np.exp(-z))
lr = 0.5
for epoca in range(5000):
# forward
h = np.tanh(X @ W1 + b1)
p = sig(h @ W2 + b2)
# perda: entropia cruzada binária
loss = -np.mean(y*np.log(p+1e-9) + (1-y)*np.log(1-p+1e-9))
# backward (regra da cadeia, à mão)
dp = (p - y) / len(X) # dL/dz2 (gradiente da saída)
dW2 = h.T @ dp; db2 = dp.sum(0, keepdims=True)
dh = dp @ W2.T * (1 - h**2) # derivada da tanh
dW1 = X.T @ dh; db1 = dh.sum(0, keepdims=True)
# atualização
W1 -= lr*dW1; b1 -= lr*db1; W2 -= lr*dW2; b2 -= lr*db2
print(p.round(2).ravel()) # ≈ [0, 1, 1, 0] — aprendeu o XOR
14.4 PyTorch: o framework
PyTorch fornece tensores (arrays com GPU), autograd (backprop automático) e módulos de rede prontos.
import torch
import torch.nn as nn
# Autograd em ação
x = torch.tensor(2.0, requires_grad=True)
f = x**3 + 2*x
f.backward()
print(x.grad) # 3x² + 2 = 14 — derivada calculada automaticamente
Classificação tabular completa:
import torch
from torch import nn
from torch.utils.data import DataLoader, TensorDataset
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
X, y = load_breast_cancer(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2,
stratify=y, random_state=42)
sc = StandardScaler().fit(X_tr)
to_t = lambda a: torch.tensor(a, dtype=torch.float32)
train_ds = TensorDataset(to_t(sc.transform(X_tr)), to_t(y_tr).unsqueeze(1))
train_dl = DataLoader(train_ds, batch_size=64, shuffle=True)
device = "cuda" if torch.cuda.is_available() else "cpu"
class MLP(nn.Module):
def __init__(self, n_in):
super().__init__()
self.rede = nn.Sequential(
nn.Linear(n_in, 64), nn.ReLU(), nn.Dropout(0.2),
nn.Linear(64, 32), nn.ReLU(), nn.Dropout(0.2),
nn.Linear(32, 1), # logit (sem sigmoide: a loss cuida disso)
)
def forward(self, x):
return self.rede(x)
modelo = MLP(X.shape[1]).to(device)
loss_fn = nn.BCEWithLogitsLoss()
otim = torch.optim.AdamW(modelo.parameters(), lr=1e-3, weight_decay=1e-4)
for epoca in range(30):
modelo.train()
for xb, yb in train_dl:
xb, yb = xb.to(device), yb.to(device)
otim.zero_grad()
loss = loss_fn(modelo(xb), yb)
loss.backward()
otim.step()
# Avaliação
modelo.eval()
with torch.no_grad():
logits = modelo(to_t(sc.transform(X_te)).to(device))
proba = torch.sigmoid(logits).cpu().numpy().ravel()
print("Acurácia:", ((proba > 0.5) == y_te).mean())
14.5 Regularização e diagnóstico em redes
- Dropout: desliga neurônios aleatoriamente no treino — ensemble implícito.
- Weight decay (L2 no otimizador).
- Batch Normalization / Layer Normalization: estabilizam e aceleram o treino.
- Early stopping: pare quando a perda de validação parar de cair.
- Data augmentation (Cap. 15): mais dados "de graça".
Diagnóstico pelas curvas de perda (treino × validação por época): - Ambas altas e estagnadas → underfitting (rede maior, mais épocas, lr maior). - Treino caindo e validação subindo → overfitting (regularize, mais dados). - Perda explodindo/NaN → learning rate alto demais ou dados sem normalizar.
14.6 Quando usar redes vs. boosting em dados tabulares
Para tabelas pequenas/médias, gradient boosting geralmente vence com menos esforço. Redes brilham quando: dados são massivos, há sinais não estruturados (texto/imagem junto à tabela), embeddings de categorias de altíssima cardinalidade, ou aprendizado multi-tarefa.
Capítulo 15 — Redes Convolucionais (CNNs) e Visão Computacional
15.1 A convolução
Imagens têm estrutura espacial: pixels vizinhos se relacionam. A convolução desliza um filtro (kernel, ex.: 3×3) pela imagem, produzindo mapas de características que detectam padrões locais (bordas → texturas → partes → objetos, conforme a profundidade).
Vantagens sobre camadas densas: compartilhamento de pesos (o mesmo detector varre a imagem toda — pouquíssimos parâmetros) e invariância a translação.
Componentes:
- Conv2d(canais_in, canais_out, kernel, stride, padding)
- Pooling (MaxPool2d): reduz resolução, agrega informação.
- Blocos modernos: conexões residuais (ResNet) permitem redes de centenas de camadas.
15.2 CNN para classificação de dígitos (MNIST)
import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
tfm = transforms.Compose([transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))])
train_ds = datasets.MNIST("data", train=True, download=True, transform=tfm)
test_ds = datasets.MNIST("data", train=False, download=True, transform=tfm)
train_dl = DataLoader(train_ds, batch_size=128, shuffle=True)
test_dl = DataLoader(test_ds, batch_size=256)
class CNN(nn.Module):
def __init__(self):
super().__init__()
self.conv = nn.Sequential(
nn.Conv2d(1, 32, 3, padding=1), nn.ReLU(),
nn.Conv2d(32, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2),
)
self.cabeca = nn.Sequential(
nn.Flatten(), nn.Linear(64*7*7, 128), nn.ReLU(),
nn.Dropout(0.3), nn.Linear(128, 10))
def forward(self, x):
return self.cabeca(self.conv(x))
device = "cuda" if torch.cuda.is_available() else "cpu"
m = CNN().to(device)
otim = torch.optim.AdamW(m.parameters(), lr=1e-3)
loss_fn = nn.CrossEntropyLoss()
for epoca in range(3):
m.train()
for xb, yb in train_dl:
xb, yb = xb.to(device), yb.to(device)
otim.zero_grad()
loss_fn(m(xb), yb).backward()
otim.step()
m.eval(); acertos = 0
with torch.no_grad():
for xb, yb in test_dl:
pred = m(xb.to(device)).argmax(1).cpu()
acertos += (pred == yb).sum().item()
print("Acurácia:", acertos / len(test_ds)) # ~99%
15.3 Transfer learning: o atalho profissional
Ninguém treina do zero na prática. Pegue um modelo pré-treinado em milhões de imagens (ImageNet) e ajuste-o ao seu problema:
from torchvision import models
modelo = models.resnet18(weights="IMAGENET1K_V1")
for p in modelo.parameters():
p.requires_grad = False # congela o corpo
modelo.fc = nn.Linear(modelo.fc.in_features, 3) # nova cabeça: 3 classes
# Treine só a cabeça; depois, opcionalmente, descongele tudo com lr baixo (fine-tuning)
Data augmentation multiplica o treino com variações realistas:
transforms.Compose([
transforms.RandomResizedCrop(224),
transforms.RandomHorizontalFlip(),
transforms.ColorJitter(0.2, 0.2, 0.2),
transforms.ToTensor(),
])
15.4 Além da classificação
- Detecção de objetos (onde está + o que é): YOLO, Faster R-CNN.
- Segmentação (rótulo por pixel): U-Net (medicina), Mask R-CNN.
- Vision Transformers (ViT): tratam a imagem como sequência de patches — estado da arte atual em larga escala.
- Modelos multimodais (CLIP): conectam imagem e texto — busca por descrição, zero-shot.
Capítulo 16 — Sequências: RNNs, LSTMs e Séries Temporais
16.1 RNNs e LSTMs
Dados sequenciais (texto, sensores, vendas diárias) têm ordem e dependência temporal. A RNN processa a sequência passo a passo mantendo um estado oculto — uma memória do que já viu:
h_t = tanh(W_x·x_t + W_h·h_{t−1} + b)
Problema: em sequências longas, os gradientes desvanecem e a memória se perde. A LSTM (e a GRU) resolve com portões (gates) que aprendem o que lembrar, esquecer e emitir.
import torch.nn as nn
class PrevisorLSTM(nn.Module):
def __init__(self, n_features=1, hidden=64):
super().__init__()
self.lstm = nn.LSTM(n_features, hidden, num_layers=2,
batch_first=True, dropout=0.2)
self.fc = nn.Linear(hidden, 1)
def forward(self, x): # x: (batch, seq_len, n_features)
out, _ = self.lstm(x)
return self.fc(out[:, -1]) # previsão a partir do último passo
Hoje, para linguagem, RNNs foram superadas pelos Transformers (Cap. 18), mas continuam úteis em sensores, streaming e dispositivos leves.
16.2 Séries temporais: fundamentos
- Componentes: tendência + sazonalidade + ciclo + ruído.
- Estacionariedade: propriedades estatísticas constantes no tempo; muitos modelos a exigem (teste ADF; diferenciação
y.diff()para alcançá-la). - ACF/PACF: autocorrelações que revelam lags relevantes.
from statsmodels.tsa.seasonal import seasonal_decompose
from statsmodels.tsa.stattools import adfuller
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
decomp = seasonal_decompose(serie, model="additive", period=7)
decomp.plot()
print("p-valor ADF:", adfuller(serie.dropna())[1]) # < 0.05 → estacionária
plot_acf(serie.dropna(), lags=30); plot_pacf(serie.dropna(), lags=30)
16.3 Modelos clássicos: ARIMA/SARIMA e suavização
from statsmodels.tsa.statespace.sarimax import SARIMAX
from statsmodels.tsa.holtwinters import ExponentialSmoothing
# SARIMA(p,d,q)(P,D,Q,s): AR + diferenciação + MA, com componente sazonal
sarima = SARIMAX(serie, order=(1, 1, 1), seasonal_order=(1, 1, 1, 7)).fit()
prev = sarima.forecast(steps=14)
print(sarima.summary())
# Holt-Winters: tendência + sazonalidade por suavização exponencial
hw = ExponentialSmoothing(serie, trend="add", seasonal="add",
seasonal_periods=7).fit()
Bibliotecas de alto nível: Prophet (Meta — tendências + sazonalidades + feriados com pouca configuração), statsforecast/Nixtla (rápidas, AutoARIMA), darts (unifica clássicos e deep learning).
16.4 Séries temporais como ML supervisionado
Abordagem frequentemente vencedora na prática: criar lags e janelas (Cap. 6) e usar gradient boosting:
import lightgbm as lgb
from sklearn.model_selection import TimeSeriesSplit
dfs = serie.to_frame("y")
for lag in [1, 7, 14, 28]:
dfs[f"lag_{lag}"] = dfs["y"].shift(lag)
dfs["mm7"] = dfs["y"].shift(1).rolling(7).mean()
dfs["mm28"] = dfs["y"].shift(1).rolling(28).mean()
dfs["dia_semana"] = dfs.index.dayofweek
dfs["mes"] = dfs.index.month
dfs = dfs.dropna()
X, y = dfs.drop(columns="y"), dfs["y"]
tscv = TimeSeriesSplit(n_splits=5) # valida SEMPRE no futuro
for tr_idx, te_idx in tscv.split(X):
m = lgb.LGBMRegressor(n_estimators=500, learning_rate=0.05)
m.fit(X.iloc[tr_idx], y.iloc[tr_idx])
print("MAE:", (y.iloc[te_idx] - m.predict(X.iloc[te_idx])).abs().mean())
Regras de ouro: nunca embaralhar; features só com informação disponível até o momento da previsão; backtesting com janelas deslizantes; baseline ingênuo (repetir o último valor / o valor de 7 dias atrás) como referência obrigatória.
Capítulo 17 — Processamento de Linguagem Natural (NLP)
17.1 O desafio: transformar texto em números
Modelos só entendem números. A história do NLP é a história de representações de texto cada vez melhores: contagens → TF-IDF → embeddings estáticos → embeddings contextuais (Transformers).
17.2 Pré-processamento clássico
import re
def limpar(texto):
texto = texto.lower()
texto = re.sub(r"http\S+", " ", texto) # URLs
texto = re.sub(r"[^a-zà-ú\s]", " ", texto) # pontuação/números
texto = re.sub(r"\s+", " ", texto).strip()
return texto
# Tokenização, stopwords, lematização — spaCy para português:
# pip install spacy && python -m spacy download pt_core_news_sm
import spacy
nlp = spacy.load("pt_core_news_sm")
doc = nlp("Os clientes compraram vários produtos ontem em São Paulo.")
tokens = [t.lemma_ for t in doc if not t.is_stop and not t.is_punct]
print(tokens) # ['cliente', 'comprar', 'produto', 'ontem', 'São', 'Paulo']
# Entidades nomeadas (NER)
print([(e.text, e.label_) for e in doc.ents]) # [('São Paulo', 'LOC')]
Nota: com Transformers modernos, o pré-processamento pesado (remover stopwords, lematizar) é desnecessário e até prejudicial — o tokenizador do modelo cuida de tudo. Essas técnicas seguem úteis para métodos clássicos e EDA de texto.
17.3 Bag-of-Words e TF-IDF
- Bag-of-Words: vetor de contagem de cada palavra do vocabulário. Ignora ordem.
- TF-IDF: pondera a contagem (TF) pela raridade da palavra no corpus (IDF) — palavras comuns a todos os documentos ("de", "para") perdem peso.
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.model_selection import cross_val_score
textos = df["avaliacao"] # ex.: reviews de produtos
y = df["sentimento"] # 1 = positivo, 0 = negativo
clf = make_pipeline(
TfidfVectorizer(ngram_range=(1, 2), # unigramas e bigramas ("muito bom")
min_df=3, max_df=0.9, sublinear_tf=True),
LogisticRegression(max_iter=2000, C=1.0),
)
print(cross_val_score(clf, textos, y, cv=5, scoring="f1").mean())
# Interpretação: pesos das palavras
clf.fit(textos, y)
vocab = clf[0].get_feature_names_out()
pesos = clf[1].coef_.ravel()
import numpy as np
top_pos = vocab[np.argsort(pesos)[-10:]] # palavras mais "positivas"
TF-IDF + Logística ainda é um baseline fortíssimo e barato para classificação de texto.
17.4 Word embeddings: palavras como vetores densos
Word2Vec/GloVe/fastText aprendem vetores densos (~100–300 dims) em que palavras de contexto similar ficam próximas — capturando semântica: rei − homem + mulher ≈ rainha.
# pip install gensim
from gensim.models import Word2Vec
frases = [t.split() for t in df["avaliacao_limpa"]]
w2v = Word2Vec(frases, vector_size=100, window=5, min_count=3,
sg=1, epochs=10, seed=42)
w2v.wv.most_similar("ótimo", topn=5)
w2v.wv.similarity("bom", "excelente")
# Representando um documento: média dos vetores das palavras
import numpy as np
def doc_vec(tokens):
vs = [w2v.wv[t] for t in tokens if t in w2v.wv]
return np.mean(vs, axis=0) if vs else np.zeros(100)
Limitação: o vetor de "banco" é o mesmo em "banco de dados" e "banco da praça". A solução — embeddings contextuais — veio com os Transformers.
17.5 Tarefas de NLP e o estado atual
| Tarefa | Exemplo | Abordagem atual |
|---|---|---|
| Classificação | sentimento, spam, triagem | fine-tuning de BERT ou LLM |
| NER | extrair nomes, valores, datas | BERT fine-tuned / LLM |
| Similaridade/busca | FAQ, deduplicação, RAG | sentence embeddings |
| Sumarização | resumir contratos | LLMs |
| Tradução | pt ↔ en | Transformers seq2seq |
| QA / chat | assistentes | LLMs + RAG |
Capítulo 18 — Transformers e Grandes Modelos de Linguagem (LLMs)
18.1 Atenção: a ideia que mudou tudo
O paper "Attention Is All You Need" (2017) removeu a recorrência: em vez de processar a sequência passo a passo, a auto-atenção permite que cada token "olhe" para todos os outros de uma vez e pondere sua relevância.
Mecanismo (intuição): cada token gera três vetores — Query (o que procuro), Key (o que ofereço) e Value (meu conteúdo):
Attention(Q, K, V) = softmax(Q·Kᵀ / √d) · V
O produto Q·Kᵀ mede a afinidade entre cada par de tokens; o softmax vira pesos; a saída de cada token é uma média ponderada dos Values de todos. Em "o banco estava fechado porque o gerente saiu", o token "banco" atende fortemente a "gerente" — e sua representação passa a significar instituição financeira. Multi-head attention roda várias atenções em paralelo, cada uma capturando um tipo de relação (sintática, semântica, posicional).
O bloco Transformer completo: atenção multi-cabeça → conexão residual + LayerNorm → MLP → residual + LayerNorm. Empilhe dezenas desses blocos e adicione embeddings posicionais (a atenção em si não sabe ordem).
Implementação didática da atenção em PyTorch:
import torch, math
import torch.nn.functional as F
def atencao(Q, K, V, mascara_causal=False):
d = Q.size(-1)
scores = Q @ K.transpose(-2, -1) / math.sqrt(d) # (batch, seq, seq)
if mascara_causal: # GPT: não ver o futuro
seq = scores.size(-1)
mask = torch.triu(torch.ones(seq, seq), diagonal=1).bool()
scores = scores.masked_fill(mask, float("-inf"))
pesos = F.softmax(scores, dim=-1)
return pesos @ V, pesos
18.2 As famílias de Transformers
- Encoder-only (BERT): vê o texto inteiro (bidirecional); treinado mascarando palavras. Ideal para entender: classificação, NER, embeddings.
- Decoder-only (GPT, Llama, Claude): atenção causal; treinado para prever o próximo token. Ideal para gerar. É a arquitetura dos LLMs modernos.
- Encoder-decoder (T5): tradução, sumarização.
18.3 Como um LLM é construído
- Pré-treinamento: prever o próximo token em trilhões de tokens de texto. Auto-supervisionado — o texto é seu próprio rótulo. Daí emergem gramática, fatos, raciocínio.
- Fine-tuning supervisionado (SFT): exemplos de instrução → resposta ensinam o modelo a ser um assistente.
- Alinhamento por preferências (RLHF/DPO): humanos comparam respostas; o modelo é otimizado para as preferidas — mais útil, menos nocivo.
Conceitos operacionais: - Token: subpalavra (~4 caracteres em média). Modelos cobram e limitam por tokens. - Janela de contexto: quanto o modelo "enxerga" de uma vez. - Temperatura: 0 = determinístico; alto = criativo/arriscado. - Alucinação: o modelo gera texto plausível porém falso — mitigue com RAG, verificação e pedidos de citação. - Leis de escala: desempenho cresce previsivelmente com parâmetros + dados + computação.
18.4 Hugging Face na prática
# pip install transformers datasets
from transformers import pipeline
# Modelos prontos em uma linha
sentimento = pipeline("sentiment-analysis",
model="nlptown/bert-base-multilingual-uncased-sentiment")
print(sentimento("Produto excelente, chegou antes do prazo!"))
ner = pipeline("ner", model="Davlan/bert-base-multilingual-cased-ner-hrl",
aggregation_strategy="simple")
print(ner("Maria trabalha na Petrobras no Rio de Janeiro."))
Fine-tuning de BERT para classificação
from transformers import (AutoTokenizer, AutoModelForSequenceClassification,
TrainingArguments, Trainer)
from datasets import Dataset
import numpy as np
import evaluate
modelo_base = "neuralmind/bert-base-portuguese-cased" # BERTimbau
tok = AutoTokenizer.from_pretrained(modelo_base)
ds = Dataset.from_pandas(df[["texto", "label"]])
ds = ds.map(lambda x: tok(x["texto"], truncation=True, max_length=256),
batched=True)
ds = ds.train_test_split(test_size=0.2, seed=42)
modelo = AutoModelForSequenceClassification.from_pretrained(modelo_base,
num_labels=2)
metrica = evaluate.load("f1")
def computar(eval_pred):
logits, labels = eval_pred
return metrica.compute(predictions=np.argmax(logits, -1),
references=labels)
args = TrainingArguments(
output_dir="saida", num_train_epochs=3,
per_device_train_batch_size=16, learning_rate=2e-5,
eval_strategy="epoch", save_strategy="epoch",
load_best_model_at_end=True, metric_for_best_model="f1")
Trainer(model=modelo, args=args, train_dataset=ds["train"],
eval_dataset=ds["test"], compute_metrics=computar).train()
Fine-tuning eficiente de LLMs: LoRA
Ajustar bilhões de parâmetros é caro. LoRA congela o modelo e treina apenas pequenas matrizes de baixo posto inseridas nas camadas (<1% dos parâmetros); QLoRA combina isso com quantização em 4 bits — viabilizando fine-tuning em uma única GPU. Biblioteca: peft.
18.5 Embeddings de sentenças e busca semântica
# pip install sentence-transformers
from sentence_transformers import SentenceTransformer, util
enc = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2")
docs = ["Como emitir a segunda via do boleto?",
"Política de troca e devolução",
"Prazo de entrega para o Sudeste"]
emb_docs = enc.encode(docs, normalize_embeddings=True)
consulta = enc.encode("perdi meu boleto, e agora?", normalize_embeddings=True)
scores = util.cos_sim(consulta, emb_docs)
print(docs[int(scores.argmax())]) # acha o documento certo sem palavras em comum
Para milhões de documentos: índices vetoriais (FAISS) ou bancos vetoriais (Chroma, Qdrant, pgvector).
18.6 RAG: Retrieval-Augmented Generation
O padrão de arquitetura mais importante para aplicações com LLMs: em vez de esperar que o modelo "saiba" seus dados privados, busque os trechos relevantes e injete-os no prompt.
Pipeline: 1. Ingestão: dividir documentos em trechos (chunks) → gerar embeddings → indexar. 2. Consulta: embutir a pergunta → recuperar top-k trechos similares (+ re-ranking). 3. Geração: prompt = instruções + trechos recuperados + pergunta → LLM responde com base nas fontes.
Benefícios: conhecimento atualizado sem retreinar, respostas citáveis, menos alucinação. Pontos de atenção: qualidade do chunking, avaliação da recuperação (o LLM não conserta contexto errado).
18.7 Engenharia de prompts e agentes
Técnicas com evidência de ganho: - Instruções claras + formato de saída explícito (ex.: "responda em JSON com os campos ..."). - Few-shot: 2–5 exemplos de entrada → saída desejada. - Cadeia de raciocínio: pedir passos intermediários em problemas complexos. - Decomposição: dividir tarefas grandes em subtarefas encadeadas.
Agentes: LLMs em um laço de raciocínio + uso de ferramentas (busca, código, APIs) + observação do resultado, até concluir a tarefa. Function calling estrutura isso: o modelo devolve chamadas de função em JSON que seu código executa.
18.8 Avaliação e riscos de LLMs
- Avaliação: benchmarks públicos, conjuntos de teste próprios, LLM-como-juiz (com auditoria humana), métricas de RAG (fidelidade ao contexto, relevância da resposta).
- Riscos: alucinação, injeção de prompt (instruções maliciosas escondidas em dados de entrada), vazamento de dados sensíveis, vieses herdados do corpus, custo/latência. Trate saídas de LLM como não confiáveis por padrão: valide, restrinja permissões de ferramentas e monitore.
Capítulo 19 — Tópicos Complementares: Recomendação, RL, MLOps e Ética
19.1 Sistemas de recomendação
- Filtragem colaborativa: "usuários parecidos gostam de itens parecidos". Implementação clássica: fatoração de matrizes — decompor a matriz usuário×item em embeddings latentes.
- Baseada em conteúdo: recomenda itens similares aos que o usuário consumiu (TF-IDF/embeddings dos itens).
- Híbridos e deep learning: two-tower networks, sequência de interações com Transformers.
# Fatoração de matrizes com SVD truncado (esparso)
import numpy as np
from scipy.sparse import csr_matrix
from scipy.sparse.linalg import svds
# R: matriz usuário × item com notas (0 = não avaliado)
R = csr_matrix(matriz_notas, dtype=float)
U, s, Vt = svds(R, k=30) # 30 fatores latentes
pred = U @ np.diag(s) @ Vt # notas previstas para TODOS os pares
ja_vistos = matriz_notas > 0
pred[ja_vistos] = -np.inf # não recomendar o que já consumiu
top5_usuario0 = np.argsort(pred[0])[::-1][:5]
Métricas próprias: Precision@k, Recall@k, NDCG, cobertura e diversidade. Desafios: cold start (usuário/item novo), feedback implícito (clique ≠ gostou), loops de retroalimentação.
19.2 Aprendizado por Reforço (visão executiva)
Formalização: MDP (estados, ações, transições, recompensas, fator de desconto γ). O agente aprende uma política π(a|s) que maximiza o retorno esperado.
- Q-Learning: aprende Q(s, a) — o valor de cada ação em cada estado — pela equação de Bellman:
Q(s,a) ← Q(s,a) + α[r + γ·max Q(s',·) − Q(s,a)]. - DQN: Q-Learning com rede neural (Atari, 2013).
- Métodos de política (REINFORCE, PPO): otimizam a política diretamente — PPO é a base do RLHF em LLMs.
- Dilema exploração × exploração (ε-greedy, UCB); bandits são o caso sem estados — muito usados em otimização de layout/preço.
# Q-Learning tabular em poucas linhas (ambiente FrozenLake do Gymnasium)
# pip install gymnasium
import gymnasium as gym
import numpy as np
env = gym.make("FrozenLake-v1", is_slippery=False)
Q = np.zeros((env.observation_space.n, env.action_space.n))
alfa, gama, eps = 0.8, 0.95, 0.1
for ep in range(2000):
s, _ = env.reset()
fim = False
while not fim:
a = env.action_space.sample() if np.random.rand() < eps else Q[s].argmax()
s2, r, term, trunc, _ = env.step(a)
Q[s, a] += alfa * (r + gama * Q[s2].max() - Q[s, a])
s, fim = s2, term or trunc
print("Política aprendida:", Q.argmax(axis=1).reshape(4, 4))
19.3 MLOps: modelos em produção
Um modelo só gera valor em produção — e produção é onde tudo quebra.
- Versionamento: código (Git), dados (DVC), experimentos e modelos (MLflow, Weights & Biases).
# pip install mlflow
import mlflow
with mlflow.start_run():
mlflow.log_params({"modelo": "lgbm", "lr": 0.05})
mlflow.log_metric("auc", 0.912)
mlflow.sklearn.log_model(pipe, "modelo")
- Serving: API REST (FastAPI, Cap. 13), batch scoring, ou streaming. Empacote com Docker.
- Monitoramento: além de latência e erros, vigie data drift (a distribuição das entradas mudou — teste PSI/KS) e concept drift (a relação X→y mudou). Ferramenta: Evidently.
- Retreinamento: agendado ou disparado por alarmes de drift; valide contra o modelo campeão antes de promover (shadow deployment, testes A/B).
- Feature stores garantem que as features do treino e da produção sejam calculadas identicamente (evita training-serving skew).
19.4 Ética, viés e regulação
- Fontes de viés: dados históricos discriminatórios, amostragem enviesada, rótulos subjetivos, proxies (CEP correlaciona com raça).
- Métricas de justiça: paridade demográfica, igualdade de oportunidade (recall igual entre grupos), calibração por grupo — matematicamente impossível satisfazer todas ao mesmo tempo; a escolha é uma decisão de valores, não técnica. Ferramenta: Fairlearn.
- Privacidade: anonimização é frágil (re-identificação); técnicas: privacidade diferencial, aprendizado federado. Marco legal no Brasil: LGPD — dados pessoais exigem base legal, minimização e direito à explicação.
- Transparência: decisões de alto impacto (crédito, saúde, justiça) exigem explicabilidade (Cap. 13) e supervisão humana. O AI Act europeu classifica sistemas por risco e é referência global.
- Checklist mínimo por projeto: quem pode ser prejudicado? O desempenho é igual entre subgrupos? Há recurso humano contra a decisão? O uso dos dados é consentido e proporcional?
Capítulo 20 — Projeto Guiado de Ponta a Ponta
Vamos consolidar tudo em um projeto realista: prever churn (cancelamento) de clientes de uma operadora. O mesmo esqueleto serve para crédito, fraude, conversão etc.
20.1 Dados sintéticos (para você executar já)
import numpy as np
import pandas as pd
rng = np.random.default_rng(42)
n = 6000
df = pd.DataFrame({
"tempo_cliente_meses": rng.integers(1, 72, n),
"mensalidade": np.round(rng.uniform(30, 200, n), 2),
"chamados_suporte_90d": rng.poisson(1.2, n),
"atraso_pagamento_90d": rng.binomial(3, 0.15, n),
"plano": rng.choice(["basico", "padrao", "premium"], n, p=[0.4, 0.4, 0.2]),
"contrato": rng.choice(["mensal", "anual", "bienal"], n, p=[0.55, 0.3, 0.15]),
"uso_gb_mes": np.round(rng.gamma(3, 4, n), 1),
})
# Probabilidade real de churn (o "mundo" que o modelo tentará descobrir)
logit = (-2.2
- 0.03 * df["tempo_cliente_meses"]
+ 0.012 * df["mensalidade"]
+ 0.45 * df["chamados_suporte_90d"]
+ 0.60 * df["atraso_pagamento_90d"]
- 0.8 * (df["contrato"] == "anual")
- 1.5 * (df["contrato"] == "bienal"))
p = 1 / (1 + np.exp(-logit))
df["churn"] = rng.binomial(1, p)
# Sujeira realista
df.loc[rng.choice(n, 300, replace=False), "uso_gb_mes"] = np.nan
df.loc[rng.choice(n, 150, replace=False), "mensalidade"] = np.nan
print(df["churn"].mean()) # ~0.25 — desbalanceado, mas não extremo
20.2 EDA dirigida ao problema
import seaborn as sns, matplotlib.pyplot as plt
print(df.isna().mean().round(3))
print(df.groupby("churn")[["tempo_cliente_meses", "mensalidade",
"chamados_suporte_90d"]].mean().round(2))
fig, ax = plt.subplots(1, 3, figsize=(15, 4))
sns.kdeplot(data=df, x="tempo_cliente_meses", hue="churn", common_norm=False, ax=ax[0])
sns.barplot(data=df, x="contrato", y="churn", ax=ax[1])
sns.boxplot(data=df, x="churn", y="chamados_suporte_90d", ax=ax[2])
plt.tight_layout()
Descobertas esperadas: contrato mensal e chamados de suporte elevam o churn; tempo de casa protege. Isso orienta features e a narrativa ao negócio.
20.3 Baseline honesto
from sklearn.model_selection import train_test_split, cross_val_score, StratifiedKFold
from sklearn.dummy import DummyClassifier
X = df.drop(columns="churn"); y = df["churn"]
X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.2,
stratify=y, random_state=42)
cv = StratifiedKFold(5, shuffle=True, random_state=42)
dummy = DummyClassifier(strategy="prior")
print("Baseline PR-AUC ~ prevalência:", y_tr.mean().round(3))
20.4 Pipeline + modelos + comparação
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.model_selection import cross_validate
col_num = ["tempo_cliente_meses", "mensalidade", "chamados_suporte_90d",
"atraso_pagamento_90d", "uso_gb_mes"]
col_cat = ["plano", "contrato"]
prep = ColumnTransformer([
("num", Pipeline([("imp", SimpleImputer(strategy="median")),
("sc", StandardScaler())]), col_num),
("cat", OneHotEncoder(handle_unknown="ignore"), col_cat),
])
candidatos = {
"logistica": Pipeline([("prep", prep),
("m", LogisticRegression(max_iter=2000, class_weight="balanced"))]),
"hgb": Pipeline([("prep", prep),
("m", HistGradientBoostingClassifier(random_state=42))]),
}
for nome, pipe in candidatos.items():
r = cross_validate(pipe, X_tr, y_tr, cv=cv,
scoring=["roc_auc", "average_precision"])
print(f"{nome:10s} ROC-AUC={r['test_roc_auc'].mean():.3f} "
f"PR-AUC={r['test_average_precision'].mean():.3f}")
20.5 Tuning, avaliação final e limiar de negócio
from sklearn.model_selection import RandomizedSearchCV
from scipy.stats import loguniform, randint
from sklearn.metrics import (classification_report, roc_auc_score,
average_precision_score, precision_recall_curve)
import numpy as np
melhor = RandomizedSearchCV(
candidatos["hgb"],
{"m__learning_rate": loguniform(1e-2, 0.3),
"m__max_iter": randint(150, 800),
"m__max_leaf_nodes": randint(15, 127),
"m__l2_regularization": loguniform(1e-6, 1.0)},
n_iter=30, cv=cv, scoring="average_precision",
n_jobs=-1, random_state=42).fit(X_tr, y_tr)
modelo = melhor.best_estimator_
proba = modelo.predict_proba(X_te)[:, 1]
print("ROC-AUC teste:", roc_auc_score(y_te, proba).round(3))
print("PR-AUC teste:", average_precision_score(y_te, proba).round(3))
# Limiar orientado ao negócio: reter custa R$30; perder um cliente custa R$300.
# Vale agir sempre que p > 30/300 = 0.10 (limiar = razão de custos)
limiar = 0.10
pred = (proba >= limiar).astype(int)
print(classification_report(y_te, pred, digits=3))
# Valor esperado da campanha no teste
vp = ((pred == 1) & (y_te == 1)).sum()
fp = ((pred == 1) & (y_te == 0)).sum()
print(f"Ganho estimado: R$ {vp*300 - (vp+fp)*30:,.0f}")
20.6 Explicar e entregar
import shap, joblib
# Explicabilidade para o negócio
Xt_te = modelo.named_steps["prep"].transform(X_te)
nomes = modelo.named_steps["prep"].get_feature_names_out()
sv = shap.TreeExplainer(modelo.named_steps["m"]).shap_values(Xt_te)
shap.summary_plot(sv, Xt_te, feature_names=nomes)
# Persistência
joblib.dump(modelo, "churn_v1.joblib")
Entregáveis de um projeto profissional: notebook de EDA com conclusões, pipeline reprodutível, relatório de métricas com intervalo (CV), análise de erros e de subgrupos, cartão do modelo (model card) e plano de monitoramento.
Capítulo 21 — Trilha de Estudos e Próximos Passos
21.1 Trilha sugerida (com esta apostila como mapa)
- Semanas 1–3: Python + NumPy + Pandas (Caps. 2–4). Refaça cada bloco de código.
- Semanas 4–6: EDA, limpeza e estatística (Caps. 5–7) em um dataset real (Kaggle: Titanic, House Prices, Telco Churn).
- Semanas 7–10: ML clássico completo (Caps. 8–13). Meta: dominar validação cruzada, métricas e pipelines.
- Semanas 11–14: Deep learning (Caps. 14–16). Reimplemente a rede em NumPy e depois em PyTorch.
- Semanas 15–18: NLP e LLMs (Caps. 17–18). Faça um classificador com BERTimbau e um RAG simples.
- Contínuo: projetos próprios + portfólio no GitHub. Um projeto de ponta a ponta bem documentado vale mais que dez notebooks soltos.
21.2 Referências clássicas
- Livros: Hands-On Machine Learning (Géron); An Introduction to Statistical Learning (gratuito em statlearning.com); Python for Data Analysis (McKinney, criador do Pandas); Deep Learning (Goodfellow); Designing Machine Learning Systems (Huyen).
- Cursos: Machine Learning Specialization e Deep Learning Specialization (Andrew Ng); fast.ai (prático); CS229/CS231n/CS224n (Stanford, no YouTube).
- Prática: Kaggle (competições e datasets), Hugging Face (modelos e cursos de NLP), papers with code.
- Documentações — leitura obrigatória e subestimada: scikit-learn User Guide, Pandas User Guide, PyTorch Tutorials.
21.3 Os dez mandamentos do praticante
- Comece pelo problema e pela métrica de negócio, não pelo algoritmo.
- Olhe os dados. Depois olhe de novo.
- Estabeleça um baseline simples antes de qualquer sofisticação.
- Separe o teste cedo e não o toque até o fim.
- Tema o vazamento de dados mais que o overfitting.
- Use pipelines; valide com validação cruzada apropriada ao problema.
- Desconfie de resultados bons demais.
- Prefira o modelo mais simples que atinge a meta.
- Explique o modelo — para o negócio e para os afetados por ele.
- Em produção, monitore: todo modelo degrada.
Fim da apostila. Bons estudos — e mãos aos dados!