Existe um ditado no meio: "garbage in, garbage out". Mas a versão mais honesta seria "features medíocres in, modelo medíocre out — não importa o algoritmo". Trocar uma regressão logística por um XGBoost dá um ganho marginal; construir a feature certa dá um salto. Esta aula é sobre transformar dados brutos em representações que o modelo consegue aprender — e sobre o inimigo número um desse processo, que reaparece aqui com força total: o vazamento de dados.
Dominar transformações de variáveis numéricas, encoding de categóricas (incluindo target encoding sem vazamento), engenharia de features temporais e de interação, tratamento do alvo (target engineering) e a detecção e prevenção sistemática de vazamento.
O princípio: features codificam conhecimento de domínio
Um modelo não "entende" nada — ele encontra padrões estatísticos nos números que você fornece. Feature engineering é o ato de traduzir seu conhecimento do problema em números que expõem esses padrões. Uma data bruta é inútil; "é fim de semana?", "dias desde a última compra" e "é feriado?" carregam sinal.
Transformações de variáveis numéricas
Escalonamento
Modelos baseados em distância (KNN, SVM) e em gradiente (redes, regressão logística) são sensíveis à escala. Árvores não são.
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
import numpy as np
X = np.array([[1.0], [2.0], [3.0], [100.0]]) # note o outlier
print(StandardScaler().fit_transform(X).ravel()) # média 0, desvio 1
print(MinMaxScaler().fit_transform(X).ravel()) # [0, 1]
print(RobustScaler().fit_transform(X).ravel()) # usa mediana e IQR
- StandardScaler: z-score. Padrão geral.
- MinMaxScaler: comprime para [0,1]. Sensível a outliers.
- RobustScaler: usa mediana e intervalo interquartil; resistente a outliers.
Transformações não-lineares
Variáveis muito assimétricas (renda, preço, contagens) frequentemente se beneficiam de log ou de transformações de potência que aproximam a normalidade.
from sklearn.preprocessing import PowerTransformer
import numpy as np
renda = np.array([1000, 1500, 2000, 3000, 50000, 80000]).reshape(-1, 1)
# log1p: seguro para zeros (log(1+x))
renda_log = np.log1p(renda)
# Yeo-Johnson: aceita valores negativos e zero; aproxima normalidade
pt = PowerTransformer(method="yeo-johnson")
renda_yj = pt.fit_transform(renda)
Discretização (binning)
Transformar contínuo em categórico pode capturar não-linearidades e reduzir ruído.
from sklearn.preprocessing import KBinsDiscretizer
idade = np.array([[18], [25], [35], [45], [60], [75]])
disc = KBinsDiscretizer(n_bins=4, encode="ordinal", strategy="quantile")
print(disc.fit_transform(idade).ravel()) # faixas etárias
Encoding de variáveis categóricas
O ponto mais delicado da feature engineering.
One-hot encoding
Uma coluna binária por categoria. Ideal para poucas categorias (baixa cardinalidade).
from sklearn.preprocessing import OneHotEncoder
import numpy as np
cores = np.array([["azul"], ["verde"], ["azul"], ["vermelho"]])
ohe = OneHotEncoder(sparse_output=False, handle_unknown="ignore")
print(ohe.fit_transform(cores))
print(ohe.categories_)
handle_unknown="ignore" evita quebrar quando aparece uma categoria nova no teste/produção.
Ordinal encoding
Para variáveis com ordem natural (baixo < médio < alto). Não use em categorias sem ordem para modelos lineares — inventa uma ordenação falsa.
from sklearn.preprocessing import OrdinalEncoder
tam = np.array([["P"], ["M"], ["G"], ["M"]])
oe = OrdinalEncoder(categories=[["P", "M", "G"]])
print(oe.fit_transform(tam).ravel()) # [0. 1. 2. 1.]
Target encoding — potente e perigoso
Substitui cada categoria pela média do alvo naquela categoria. Excelente para alta cardinalidade (CEP, produto, cidade), mas é uma fonte clássica de vazamento: se você calcula a média usando a linha atual, o alvo vaza para a própria feature.
A solução é calcular o encoding com validação cruzada interna (out-of-fold) e suavizar categorias raras com a média global.
from sklearn.preprocessing import TargetEncoder
from sklearn.pipeline import Pipeline
from sklearn.ensemble import HistGradientBoostingClassifier
# O TargetEncoder do sklearn (1.3+) já faz o encoding out-of-fold internamente
pipe = Pipeline([
("target_enc", TargetEncoder(smooth="auto", cv=5)),
("clf", HistGradientBoostingClassifier(random_state=42)),
])
# Dentro de CV externa, não há vazamento: o encoding é reajustado por fold.
Conceito da suavização: enc = (n·média_categoria + m·média_global) / (n + m), onde categorias com poucas amostras (n pequeno) são puxadas para a média global, evitando confiar em estimativas ruidosas.
Outros encodings úteis
- Frequency/count encoding: substitui a categoria pela sua frequência. Simples e sem vazar o alvo.
- Hashing encoding: projeta categorias em um número fixo de dimensões via hash; útil para cardinalidade altíssima e streaming, ao custo de colisões.
Engenharia de features temporais
Datas brutas são inúteis; o sinal está nas suas decomposições.
import pandas as pd, numpy as np
df = pd.DataFrame({"data": pd.to_datetime(
["2026-01-05", "2026-03-14", "2026-07-04", "2026-12-25"])})
df["ano"] = df["data"].dt.year
df["mes"] = df["data"].dt.month
df["dia_semana"] = df["data"].dt.dayofweek
df["fim_semana"] = (df["dia_semana"] >= 5).astype(int)
df["trimestre"] = df["data"].dt.quarter
# Codificação cíclica: mês 12 e mês 1 são "vizinhos"
df["mes_sin"] = np.sin(2 * np.pi * df["mes"] / 12)
df["mes_cos"] = np.cos(2 * np.pi * df["mes"] / 12)
A codificação cíclica (seno/cosseno) resolve o problema de que dezembro (12) e janeiro (1) estão próximos no calendário, mas distantes numericamente.
Para séries com histórico: features de defasagem (lags), médias móveis e janelas de agregação — sempre calculadas olhando apenas para o passado (ver seção 7).
Features de interação e agregação
# Interações: razões e produtos que codificam relações do domínio
df["preco_por_m2"] = df["preco"] / df["area"]
df["comodos_por_pessoa"] = df["comodos"] / df["moradores"]
# Agregações por grupo (cuidado com vazamento — calcule no treino)
stats = treino.groupby("cidade")["preco"].agg(["mean", "std"]).add_prefix("cidade_preco_")
treino = treino.merge(stats, on="cidade", how="left")
teste = teste.merge(stats, on="cidade", how="left") # usa stats DO TREINO
Note o padrão: a estatística por grupo é calculada no treino e apenas aplicada ao teste — nunca recalculada com dados do teste.
Target engineering — trabalhar o alvo, não só as features
Muitas vezes o maior ganho vem de transformar o alvo.
- Transformação da resposta em regressão: se o alvo é assimétrico (preços), treinar em
log(y)e exponenciar a predição costuma melhorar muito, pois estabiliza a variância.
from sklearn.compose import TransformedTargetRegressor
from sklearn.ensemble import GradientBoostingRegressor
modelo = TransformedTargetRegressor(
regressor=GradientBoostingRegressor(random_state=42),
func=np.log1p, inverse_func=np.expm1, # treina em log(y), prevê em escala original
)
- Reformulação do problema: transformar regressão em classificação por faixas, ou binarizar um alvo contínuo com um limiar de negócio.
- Definição da janela do alvo: em churn, "cancelou nos próximos 30 dias?" — a escolha da janela é uma decisão de target engineering que muda todo o problema.
Vazamento de dados: o inimigo silencioso
Vazamento é quando informação que não estaria disponível no momento da predição entra no treino, inflando o desempenho de forma irreal. Tipos comuns:
- Vazamento de pré-processamento: ajustar scaler/encoder/imputer no dataset inteiro. → Coloque tudo no
Pipeline. - Vazamento de alvo (target leakage): uma feature contém informação derivada do alvo ou que só existe depois de o alvo se concretizar (ex.: "valor_pago" prevendo "inadimplência" — só se sabe o valor pago quem pagou).
- Vazamento temporal: usar o futuro para prever o passado (Módulo 1).
- Vazamento por grupo/duplicatas: mesma entidade em treino e teste (Módulo 1,
GroupKFold). - Vazamento em target encoding: usar a linha atual no cálculo da média.
Um sintoma clássico: desempenho bom demais para ser verdade, ou uma única feature com importância dominante e suspeita. Investigue sempre a feature mais importante — pergunte-se: "eu teria esse valor no instante da predição, em produção?".
# Teste de sanidade: se uma feature sozinha dá AUC ~0.99, desconfie
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
for col in X.columns:
auc = cross_val_score(LogisticRegression(max_iter=500),
X[[col]], y, cv=5, scoring="roc_auc").mean()
if auc > 0.9:
print(f"⚠️ {col}: AUC={auc:.3f} — possível vazamento")
Em R (recipes)
library(tidymodels)
library(embed) # target/embedding encoders
receita <- recipe(alvo ~ ., data = treino) |>
step_impute_median(all_numeric_predictors()) |>
step_YeoJohnson(all_numeric_predictors()) |> # normaliza assimetria
step_normalize(all_numeric_predictors()) |>
step_date(data_col, features = c("dow", "month")) |> # features de data
step_dummy(all_nominal_predictors()) |>
step_lencode_mixed(cidade, outcome = vars(alvo)) # target encoding suavizado
# Toda a recipe é reajustada por fold dentro do workflow -> sem vazamento
O recipes torna o pré-processamento parte do modelo, reaplicado corretamente em cada fold — a mesma filosofia do Pipeline do sklearn, com passos prontos para praticamente toda transformação desta aula.
Boas features valem mais que algoritmos sofisticados, porque é nelas que o conhecimento de domínio entra no modelo. O escalonamento segue o modelo — Standard, MinMax ou Robust —, e a assimetria se trata com log ou Yeo-Johnson. Para categorias, one-hot em baixa cardinalidade, ordinal só quando a variável é de fato ordenada, e target encoding out-of-fold e suavizado quando a cardinalidade é alta. Datas rendem muito quando decompostas em dia da semana, feriado e codificação cíclica, e séries pedem lags e janelas que olhem apenas para o passado. Transformar ou reformular o próprio alvo costuma dar ganhos grandes e é frequentemente esquecido. O erro mais caro continua sendo o vazamento: mantenha o pré-processamento dentro do pipeline, pergunte de cada feature se ela existiria no momento da predição, e desconfie de desempenho bom demais.
Exercícios
-
Por que ajustar um
StandardScalerou umOneHotEncoderno dataset completo (antes do split) é vazamento, e oTargetEncoderé ainda mais perigoso?✓ Resposta:Ajustar scaler/encoder no dataset completo faz parâmetros (média, desvio, conjunto de categorias) incorporarem informação do teste, gerando estimativa otimista. O
TargetEncoderé pior porque usa o próprio alvo para construir a feature: se a média por categoria for calculada incluindo a linha que está sendo codificada (ou usando dados que cairão na validação), o valor do alvo vaza diretamente para dentro da variável preditora, podendo produzir features quase perfeitas que colapsam em produção. Solução: encoding out-of-fold (por CV interna) com suavização, sempre dentro do pipeline/recipe. -
Você tem uma variável "cidade" com 3000 valores distintos. Compare one-hot e target encoding para esse caso.
✓ Resposta:One-hot criaria ~3000 colunas esparsas, aumentando dimensionalidade, custo e risco de overfitting, com muitas categorias tendo pouquíssimas amostras. Target encoding substitui cada cidade por um único número (a média suavizada do alvo naquela cidade), mantendo a dimensionalidade baixa e capturando o sinal — desde que feito out-of-fold e suavizado para categorias raras. Alternativas sem usar o alvo: frequency/count encoding ou hashing. Para 3000 categorias, target encoding (bem-feito) ou embeddings tendem a superar one-hot.
-
Por que codificar o mês como seno e cosseno pode ser melhor do que usar o número do mês (1–12) diretamente?
✓ Resposta:O número do mês trata dezembro (12) e janeiro (1) como muito distantes (distância 11), quando na verdade são meses adjacentes no ciclo anual. A codificação cíclica projeta o mês em um círculo via
sin(2πm/12)ecos(2πm/12), de modo que dezembro e janeiro ficam próximos no espaço de features. Isso permite que o modelo capture a continuidade e sazonalidade do calendário, especialmente importante para modelos lineares e redes; árvores lidam parcialmente com isso via cortes, mas a codificação cíclica ainda ajuda. -
Dê um exemplo de target leakage sutil em um problema de previsão de inadimplência de empréstimo.
✓ Resposta:Incluir a feature "número de parcelas pagas em atraso" ou "valor total já quitado" para prever se o cliente vai inadimplir. Essas variáveis só assumem seus valores depois que o histórico de pagamento se desenrola — ou seja, carregam informação do desfecho (o alvo). No momento da concessão do crédito (quando a predição é realmente necessária), elas não existiriam. Um modelo treinado com elas mostra desempenho excelente na validação e falha em produção. O teste mental: "eu teria esse valor no instante da decisão de crédito?" — se não, é vazamento.
-
Quando transformar o alvo com
logem um problema de regressão é uma boa ideia, e o que é preciso lembrar ao gerar as predições?✓ Resposta:É boa ideia quando o alvo é positivo e fortemente assimétrico à direita (preços, receitas, contagens), pois o log estabiliza a variância, reduz o impacto de outliers e frequentemente lineariza relações, melhorando o ajuste. Ao gerar predições, é preciso reverter a transformação aplicando a exponencial (
expm1se usoulog1p) para voltar à escala original. Ferramentas comoTransformedTargetRegressor(Python) fazem isso automaticamente. Atenção: a média no espaço log não é a média no espaço original (viés de reversão via desigualdade de Jensen); em aplicações sensíveis, considere correções de retro-transformação.