Machine Learning: Engenharia de Features
Última atualização: 2026-08-26
Os dados definem o teto; o modelo apenas se aproxima dele — a engenharia de features é o que eleva esse teto.
1. O que você vai aprender
- Features numéricas: padronização/normalização, transformação log, binning, features polinomiais
- Features categóricas: codificação One-Hot/Ordinal/Target, tratamento de alta cardinalidade
- Extração de features temporais: ano/mês/semana/hora, flags de feriado, dias desde um evento
- Seleção de features: Filtro (correlação/informação mútua), Wrapper (RFE), Embutida (regularização L1/importância de árvore)
- A fábrica de features de e-commerce do Bob: construindo features de negócio como "frequência de compras nos últimos 7 dias" e "entropia de preferência por categoria"
2. A história real de um engenheiro de ML
(1) O problema: 3 dias de ajuste para 1% de ganho vs. uma feature para 10%
O Bob gastou 3 dias ajustando hiperparâmetros do XGBoost, elevando o R² de 0,78 para 0,79 — uma mera melhoria de 1%. Mas quando adicionou uma única feature nova, "frequência de compras nos últimos 7 dias", o R² saltou direto para 0,86 — um ganho de 8%. A engenharia de features oferece retornos muito maiores do que o ajuste de hiperparâmetros, mas a maioria dos engenheiros carece de uma metodologia sistemática para ela.
(2) Uma abordagem sistemática para engenharia de features
A engenharia de features não é experimentação aleatória — segue um fluxo de trabalho sistemático: entender o negócio → construir features → selecionar features → validar eficácia.
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
# Pipeline sistemático de engenharia de features
num_features = ["ad_spend", "traffic", "log_monetary", "purchase_freq_7d"]
cat_features = ["category", "tier"]
preprocessor = ColumnTransformer([
("num", StandardScaler(), num_features),
("cat", OneHotEncoder(drop="first", handle_unknown="ignore"), cat_features),
])
pipe = Pipeline([("prep", preprocessor), ("model", XGBRegressor())])
(3) O resultado: engenharia sistemática de features eleva o R² de 0,78 para 0,88
Depois que o Bob construiu sua fábrica de features, cada adição de nova feature passou a ser respaldada por evidência. O R² subiu sistematicamente de 0,78 para 0,88 — superando em muito o que apenas o ajuste poderia alcançar.
3. Transformações de features numéricas
(1) Padronização vs. Normalização
▶ Exemplo: Comparação entre StandardScaler, MinMaxScaler e RobustScaler
from sklearn.preprocessing import StandardScaler, MinMaxScaler, RobustScaler
import numpy as np
data = np.array([[1], [5], [10], [50], [100], [500], [1000]])
scalers = {
"Original": data,
"StandardScaler": StandardScaler().fit_transform(data),
"MinMaxScaler": MinMaxScaler().fit_transform(data),
"RobustScaler": RobustScaler().fit_transform(data),
}
for name, scaled in scalers.items():
print(f"{name:16s}: min={scaled.min():8.3f}, max={scaled.max():8.3f}, "
f"média={scaled.mean():8.3f}, desvio={scaled.std():8.3f}")
Saída:
# Executado com sucesso
| Scaler | Intervalo de saída | Robustez a outliers | Caso de uso |
|---|---|---|---|
| StandardScaler | Média 0, desvio 1 | Baixa | Distribuição normal, sem outliers |
| MinMaxScaler | [0, 1] | Baixa | Intervalo fixo, redes neurais |
| RobustScaler | Baseado em quartis | Alta | Presença de outliers |
(2) Log Transform e Binning
▶ Exemplo: Lidando com distribuições enviesadas
import numpy as np
import pandas as pd
# Dados com distribuição enviesada à direita
data = np.array([1, 5, 10, 50, 100, 500, 1000, 5000, 10000])
# Log transform
log_data = np.log1p(data)
# Binning em 3 grupos
bins = pd.cut(data, bins=3, labels=["Baixo", "Médio", "Alto"])
print(f"Dados originais: {data}")
print(f"Após log: {log_data.round(2)}")
print(f"Bins: {bins}")
Saída:
# Executado com sucesso
(3) Features polinomiais e interações
▶ Exemplo: Capturando relações não lineares
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
import numpy as np
rng = np.random.default_rng(42)
X = rng.uniform(-3, 3, (100, 1))
y = 0.5 * X[:, 0]**2 + X[:, 0] + rng.normal(0, 0.5, 100)
# Sem features polinomiais (linear)
linear = LinearRegression().fit(X, y)
print(f"Linear R²: {linear.score(X, y):.3f}")
# Com features polinomiais de grau 2
poly_pipe = Pipeline([
("poly", PolynomialFeatures(degree=2, include_bias=False)),
("model", LinearRegression()),
])
poly_pipe.fit(X, y)
print(f"Polynomial R²: {poly_pipe.score(X, y):.3f}")
Saída:
# Executado com sucesso
4. Codificação de features categóricas
(1) Comparação de métodos de codificação
▶ Exemplo: One-Hot vs. Ordinal vs. Target Encoding
import pandas as pd
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder
df = pd.DataFrame({
"category": ["Eletrônicos", "Roupas", "Alimentos", "Eletrônicos", "Casa"],
"tier": ["Bronze", "Prata", "Ouro", "Prata", "Bronze"],
"revenue": [1000, 500, 300, 800, 1200],
})
# One-Hot Encoding
ohe = OneHotEncoder(sparse_output=False, drop="first")
category_encoded = ohe.fit_transform(df[["category"]])
print(f"One-Hot shape: {category_encoded.shape}")
# Ordinal Encoding
tier_order = ["Bronze", "Prata", "Ouro"]
oe = OrdinalEncoder(categories=[tier_order])
tier_encoded = oe.fit_transform(df[["tier"]])
print(f"Ordinal: {tier_encoded.flatten()}")
Saída:
# Executado com sucesso
| Método | Quando usar | Vantagem | Risco |
|---|---|---|---|
| One-Hot | Baixa cardinalidade (<10) | Sem viés ordinal | Dimensionalidade explode |
| Ordinal | Tem ordem natural (Bronze<Prata<Ouro) |
Compacto | Presume ordem linear |
| Target | Alta cardinalidade (centenas) | Compacto + informativo | Risco de vazamento de dados |
(2) Target Encoding com Validação Cruzada
▶ Exemplo: Target encoding seguro
from sklearn.model_selection import KFold
def target_encode_cv(df, col, target, n_splits=5, smoothing=10):
"""Target encoding com validação cruzada para evitar vazamento."""
global_mean = df[target].mean()
encoded = pd.Series(index=df.index, dtype=float)
kf = KFold(n_splits=n_splits, shuffle=True, random_state=42)
for train_idx, val_idx in kf.split(df):
train = df.iloc[train_idx]
means = train.groupby(col)[target].agg(["mean", "count"])
smoothed = (means["mean"] * means["count"] + global_mean * smoothing) / (means["count"] + smoothing)
encoded.iloc[val_idx] = df.iloc[val_idx][col].map(smoothed)
return encoded.fillna(global_mean)
# Aplicar em uma coluna categórica de alta cardinalidade
df = pd.DataFrame({
"city": ["SP", "RJ", "SP", "MG", "RJ", "SP", "MG", "RJ"] * 100,
"revenue": range(800),
})
df["city_encoded"] = target_encode_cv(df, "city", "revenue")
print(df.groupby("city")["city_encoded"].mean())
Saída:
# Executado com sucesso
5. Features temporais
(1) Decomposição de data/hora
▶ Exemplo: Extraindo features temporais
import pandas as pd
import numpy as np
rng = np.random.default_rng(42)
dates = pd.date_range("2024-01-01", periods=365, freq="D")
df = pd.DataFrame({"date": dates, "sales": rng.normal(1000, 200, 365)})
# Decompor data
df["year"] = df["date"].dt.year
df["month"] = df["date"].dt.month
df["day"] = df["date"].dt.day
df["day_of_week"] = df["date"].dt.dayofweek
df["is_weekend"] = (df["day_of_week"] >= 5).astype(int)
df["quarter"] = df["date"].dt.quarter
df["is_month_start"] = df["date"].dt.is_month_start.astype(int)
df["is_month_end"] = df["date"].dt.is_month_end.astype(int)
# Features cíclicas (sazonalidade)
df["month_sin"] = np.sin(2 * np.pi * df["month"] / 12)
df["month_cos"] = np.cos(2 * np.pi * df["month"] / 12)
print(df.head())
Saída:
# Executado com sucesso
(2) Features de defasagem e janela rolante
▶ Exemplo: Lag features e rolling stats
import pandas as pd
import numpy as np
rng = np.random.default_rng(42)
sales = pd.Series(rng.normal(1000, 200, 90), index=pd.date_range("2024-01-01", periods=90))
# Lag features (valores passados)
df = pd.DataFrame({"sales": sales})
df["sales_lag1"] = df["sales"].shift(1) # Vendas de ontem
df["sales_lag7"] = df["sales"].shift(7) # Vendas de uma semana atrás
df["sales_lag30"] = df["sales"].shift(30) # Vendas de um mês atrás
# Rolling stats (médias móveis)
df["sales_ma7"] = df["sales"].rolling(window=7).mean()
df["sales_ma30"] = df["sales"].rolling(window=30).mean()
df["sales_std7"] = df["sales"].rolling(window=7).std()
# Diferenças (variação)
df["sales_diff1"] = df["sales"].diff(1)
df["sales_pct_change"] = df["sales"].pct_change()
print(df.tail())
Saída:
# Executado com sucesso
6. Seleção de features
(1) Filtro: Correlação e Informação Mútua
▶ Exemplo: Seleção por correlação
import pandas as pd
import numpy as np
rng = np.random.default_rng(42)
n = 500
df = pd.DataFrame({
"feat_strong": rng.normal(0, 1, n),
"feat_medium": rng.normal(0, 1, n),
"feat_noise": rng.normal(0, 1, n),
"feat_redundant": rng.normal(0, 1, n),
})
df["target"] = 3 * df["feat_strong"] + 1.5 * df["feat_medium"] + 0.1 * df["feat_redundant"] + rng.normal(0, 0.5, n)
# Calcular correlação com o alvo
correlations = df.corr()["target"].drop("target").abs().sort_values(ascending=False)
print("Features por correlação absoluta com o alvo:")
print(correlations.round(3))
Saída:
# Executado com sucesso
| Feature | Correlação com o alvo |
|---|---|
| feat_strong | 0.95 |
| feat_medium | 0.74 |
| feat_redundant | 0.10 |
| feat_noise | 0.04 |
(2) Wrapper: Eliminação Recursiva de Features
▶ Exemplo: RFE
from sklearn.feature_selection import RFE
from sklearn.ensemble import RandomForestRegressor
import numpy as np
rng = np.random.default_rng(42)
n, p = 200, 10
X = rng.standard_normal((n, p))
y = X[:, 0] * 3 + X[:, 2] * 2 + rng.normal(0, 0.5, n)
# Selecionar top 3 features
rfe = RFE(estimator=RandomForestRegressor(n_estimators=50, random_state=42),
n_features_to_select=3)
rfe.fit(X, y)
print(f"Features selecionadas: {np.where(rfe.support_)[0]}")
print(f"Ranking: {rfe.ranking_}")
Saída:
# Executado com sucesso
(3) Embutida: L1 Regularization
▶ Exemplo: Seleção automática via Lasso
from sklearn.linear_model import Lasso
import numpy as np
rng = np.random.default_rng(42)
n, p = 200, 10
X = rng.standard_normal((n, p))
true_coefs = np.zeros(p)
true_coefs[:3] = [3, -2, 1.5]
y = X @ true_coefs + rng.normal(0, 0.5, n)
# Lasso (L1) força coeficientes irrelevantes a zero
lasso = Lasso(alpha=0.5)
lasso.fit(X, y)
print(f"Coeficientes do Lasso: {lasso.coef_.round(2)}")
print(f"Features com coef ≠ 0: {np.where(np.abs(lasso.coef_) > 0.01)[0]}")
Saída:
# Executado com sucesso
7. A fábrica de features de e-commerce do Bob
▶ Exemplo: Features de negócio personalizadas
import pandas as pd
import numpy as np
rng = np.random.default_rng(42)
n = 5000
df = pd.DataFrame({
"user_id": range(n),
"category": rng.choice(["Eletrônicos", "Roupas", "Alimentos", "Casa"], n),
"purchase_date": pd.to_datetime("2024-01-01") + pd.to_timedelta(rng.integers(0, 365, n), unit="D"),
"amount": rng.exponential(100, n),
})
# Construir features de negócio
ref_date = pd.to_datetime("2024-12-31")
df["days_since_purchase"] = (ref_date - df["purchase_date"]).dt.days
# Frequência de compra nos últimos 7/30/90 dias
for window in [7, 30, 90]:
cutoff = ref_date - pd.Timedelta(days=window)
df[f"purchases_last_{window}d"] = (df["purchase_date"] >= cutoff).astype(int)
# Entropia de preferência por categoria (1 user = 1 linha)
user_stats = df.groupby("user_id").agg(
unique_categories=("category", "nunique"),
total_purchases=("amount", "count"),
total_spent=("amount", "sum"),
avg_purchase=("amount", "mean"),
).reset_index()
# Diversidade de categorias (alta = explorador, baixa = fiel a uma)
user_stats["category_diversity"] = user_stats["unique_categories"] / 4
# Indicador de recompra
user_stats["is_repeat_buyer"] = (user_stats["total_purchases"] > 1).astype(int)
print(user_stats.head())
Saída:
# Executado com sucesso
| Feature | Tipo | Significado de negócio |
|---|---|---|
| days_since_purchase | Numérica | Recência — quanto menor, mais ativo |
| purchases_last_7d | Binária | Engajamento recente |
| total_spent | Numérica | Valor Monetário — LTV |
| category_diversity | Numérica | Exploração vs. lealdade |
| is_repeat_buyer | Binária | Indicador de fidelidade |
❓ Perguntas Frequentes
P: Padronização ou normalização — qual escolher? R: Padronização (StandardScaler) para a maioria dos modelos (regressão, SVM, redes neurais). Use normalização (MinMaxScaler) quando precisa de intervalo [0,1] (por exemplo, entradas de rede neural com sigmoid). Use RobustScaler quando há outliers significativos.
P: Target Encoding causa vazamento de dados? R: Sim, se feito incorretamente. Use validação cruzada (computar a média apenas no fold de treino) e smoothing (combinar com a média global) para evitar isso. Nunca use todo o conjunto de dados para calcular a média de target encoding.
P: Quando usar features polinomiais? R: Quando você suspeita de relações não lineares, mas está usando um modelo linear. Para modelos baseados em árvore (Random Forest, XGBoost), features polinomiais são desnecessárias — as próprias árvores já capturam não linearidades.
P: Como lidar com alta cardinalidade em features categóricas? R: Três opções — 1) Target encoding com validação cruzada; 2) Embedding de entidades (aprendido pela rede neural); 3) Agrupar categorias raras em "Outros". Evite one-hot com 1000+ categorias.
P: Vale a pena selecionar features? R: Sim. Remove ruído, acelera o treinamento e melhora a interpretabilidade. Mas modelos de árvore têm seleção embutida, então o benefício é menor para eles. Modelos lineares se beneficiam muito da seleção de features.
P: Como validar se uma nova feature é útil? R: Adicione-a ao modelo e compare o desempenho com validação cruzada. Se a métrica melhora consistentemente em múltiplos folds, a feature é útil. Use SHAP ou permutation importance para quantificar a contribuição.
📖 Resumo
- Transformações numéricas: StandardScaler (padrão), MinMaxScaler (intervalo fixo), RobustScaler (com outliers)
- Codificação categórica: One-Hot (baixa cardinalidade), Ordinal (com ordem), Target Encoding (alta cardinalidade, com cuidado)
- Features temporais: decompor data/hora + lag features + rolling stats + features cíclicas
- Métodos de seleção: Filtro (rápido), Wrapper (preciso, lento), Embutido (Lasso/árvore)
- Features de negócio precisam estar alinhadas com a lógica de domínio, não apenas com estatísticas
- A engenharia sistemática de features > ajuste de hiperparâmetros: a fábrica de features do Bob elevou o R² de 0,78 para 0,88
📝 Exercícios
- Básico (Dificuldade ⭐): Aplique StandardScaler, MinMaxScaler e RobustScaler a um array de dados com outliers e compare os intervalos de saída. Dica: use
np.array([1, 5, 10, 50, 100, 500, 1000]). - Intermediário (Dificuldade ⭐⭐): Gere um DataFrame temporal de 365 dias, decomponha em features de mês/dia da semana/hora, crie lag features de 1/7/30 dias e calcule médias móveis. Dica:
pd.date_range()+df['col'].shift(). - Desafio (Dificuldade ⭐⭐⭐): Implemente o pipeline completo de feature engineering — gere dados sintéticos de e-commerce (categoria, gasto, data), construa 5 features de negócio, treine um modelo com/sem essas features e compare o R² para validar a eficácia. Dica: Use train_test_split + cross_val_score.
← Anterior: KNN e Agrupamento | Próximo: Aprendizado por Ensemble — XGBoost e LightGBM →