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

  1. Por que ajustar um StandardScaler ou um OneHotEncoder no dataset completo (antes do split) é vazamento, e o TargetEncoder é 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.

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

  3. 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) e cos(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.

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

  5. Quando transformar o alvo com log em 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 (expm1 se usou log1p) para voltar à escala original. Ferramentas como TransformedTargetRegressor (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.

Fontes e leituras recomendadas