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

  1. Introdução à Inteligência Artificial e ao Machine Learning
  2. Preparando o Ambiente e Python Essencial
  3. NumPy: Computação Numérica
  4. Análise de Dados com Pandas
  5. Análise Exploratória de Dados (EDA) e Visualização
  6. Limpeza de Dados e Engenharia de Atributos
  7. Matemática e Estatística para ML
  8. Fundamentos de Machine Learning
  9. Regressão
  10. Classificação
  11. Métodos de Ensemble: Bagging, Random Forest e Boosting
  12. Aprendizado Não Supervisionado
  13. Pipelines, Tuning e Interpretabilidade
  14. Deep Learning: Redes Neurais do Zero ao PyTorch
  15. Redes Convolucionais (CNNs) e Visão Computacional
  16. Sequências: RNNs, LSTMs e Séries Temporais
  17. Processamento de Linguagem Natural (NLP)
  18. Transformers e Grandes Modelos de Linguagem (LLMs)
  19. Tópicos Complementares: Recomendação, RL, MLOps e Ética
  20. Projeto Guiado de Ponta a Ponta
  21. 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)

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.

Aprendizado Não Supervisionado

Não há rótulos. O modelo descobre estrutura nos dados.

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):

  1. Entendimento do problema — qual decisão o modelo vai apoiar? Qual métrica de negócio importa?
  2. Coleta de dados — bancos de dados, APIs, arquivos, web scraping.
  3. Análise exploratória (EDA) — entender distribuições, correlações, problemas.
  4. Limpeza e preparação — tratar faltantes, outliers, codificar categorias.
  5. Engenharia de atributos — criar variáveis que ajudem o modelo.
  6. Modelagem — treinar e comparar algoritmos.
  7. Avaliação — métricas honestas em dados nunca vistos.
  8. Implantação (deploy) — colocar o modelo em produção.
  9. 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:

  1. Instale o Python (python.org) ou a distribuição Anaconda (anaconda.com), que já traz as principais bibliotecas.
  2. 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


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:

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 shape antes 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


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

  1. Visão geral: shape, info(), amostra de linhas, colunas constantes ou quase constantes.
  2. Qualidade: faltantes, duplicatas, tipos errados, valores impossíveis (idade negativa, datas no futuro).
  3. Análise univariada: distribuição de cada variável isoladamente.
  4. Análise bivariada: cada variável contra o alvo, e pares importantes entre si.
  5. Análise multivariada: correlações, interações, segmentos.
  6. 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

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:

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: fit do scaler somente no treino; depois transform no treino e no teste. Nunca fit no 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

7.2 Cálculo: derivadas e gradientes

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

7.3 Probabilidade

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.

7.4 Estatística inferencial

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
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}")

7.5 Teoria da informação (breve)


Capítulo 8 — Fundamentos de Machine Learning

8.1 Notação e anatomia de um problema supervisionado

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)

8.3 Underfitting, overfitting e o dilema viés–variância

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)
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.
  1. 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:

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!)

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"))

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))

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)

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:

  1. Forward pass: dados atravessam a rede e produzem previsões.
  2. Perda (loss): mede o erro (MSE para regressão; entropia cruzada para classificação).
  3. Backpropagation: a regra da cadeia calcula o gradiente da perda em relação a cada peso, propagando o erro de trás para frente.
  4. 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

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


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

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

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

18.3 Como um LLM é construído

  1. 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.
  2. Fine-tuning supervisionado (SFT): exemplos de instrução → resposta ensinam o modelo a ser um assistente.
  3. 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


Capítulo 19 — Tópicos Complementares: Recomendação, RL, MLOps e Ética

19.1 Sistemas de recomendação

# 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 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.

# 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")

19.4 Ética, viés e regulação


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)

  1. Semanas 1–3: Python + NumPy + Pandas (Caps. 2–4). Refaça cada bloco de código.
  2. Semanas 4–6: EDA, limpeza e estatística (Caps. 5–7) em um dataset real (Kaggle: Titanic, House Prices, Telco Churn).
  3. Semanas 7–10: ML clássico completo (Caps. 8–13). Meta: dominar validação cruzada, métricas e pipelines.
  4. Semanas 11–14: Deep learning (Caps. 14–16). Reimplemente a rede em NumPy e depois em PyTorch.
  5. Semanas 15–18: NLP e LLMs (Caps. 17–18). Faça um classificador com BERTimbau e um RAG simples.
  6. 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

21.3 Os dez mandamentos do praticante

  1. Comece pelo problema e pela métrica de negócio, não pelo algoritmo.
  2. Olhe os dados. Depois olhe de novo.
  3. Estabeleça um baseline simples antes de qualquer sofisticação.
  4. Separe o teste cedo e não o toque até o fim.
  5. Tema o vazamento de dados mais que o overfitting.
  6. Use pipelines; valide com validação cruzada apropriada ao problema.
  7. Desconfie de resultados bons demais.
  8. Prefira o modelo mais simples que atinge a meta.
  9. Explique o modelo — para o negócio e para os afetados por ele.
  10. Em produção, monitore: todo modelo degrada.

Fim da apostila. Bons estudos — e mãos aos dados!