Referência prática de padrões de feature engineering em SQL (BigQuery) e Python para sistemas de ML em produção — deduplicação, enriquecimento, features temporais, encoders e validação.
Feature engineering é onde a maioria dos projetos de ML ganha ou perde qualidade. Este repositório documenta os padrões que resolvem os problemas mais recorrentes em pipelines de dados reais:
- Deduplicação — o mesmo registro aparece em múltiplas fontes ou foi ingerido em duplicata
- Features temporais — janelas deslizantes, lag features e point-in-time correto (sem data leakage)
- Enriquecimento — target encoding seguro, imputação hierárquica, features geográficas e de produto
- Encoders Python — implementações scikit-learn compatíveis para target encoding, WOE, cíclico e frequência
- Validação — detecta leakage, drift de distribuição (PSI), variância zero e multicolinearidade antes do treino
- Feature Store — padrão Dataform para feature store incremental no BigQuery
feature-engineering-patterns/
│
├── sql/
│ ├── deduplication/
│ │ └── deduplication_patterns.sql # 6 padrões: latest row, CPF dedup, SCD2, eventos, anti-fan-out, qualidade
│ ├── temporal/
│ │ └── temporal_features.sql # Rolling windows, lag, sazonalidade sin/cos, time-to-event, PIT-correct
│ ├── enrichment/
│ │ └── enrichment_patterns.sql # Geográfico, target encoding com smoothing, imputação, cross-sell, interações
│ └── aggregation/
│
├── python/
│ ├── encoders/
│ │ └── categorical_encoders.py # TargetEncoder (K-fold OOF), WOEEncoder + IV, CyclicEncoder, FrequencyEncoder
│ └── validators/
│ └── feature_validator.py # FeatureValidator: missing, leakage, variance, cardinality, PSI drift
│
├── dataform/definitions/features/
│ └── feat_master.sqlx # Feature store incremental: une behavioral + produto + CRM + RFM
│
└── data/
Padrão 1 — Registro mais recente por chave:
SELECT * EXCEPT(rn) FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY lead_id ORDER BY updated_at DESC) AS rn
FROM raw_leads
) WHERE rn = 1Padrão 4 — Dedup de eventos com tolerância temporal (mesma interação dentro do mesmo dia):
ROW_NUMBER() OVER (
PARTITION BY lead_id, event_type, DATE(event_date), channel
ORDER BY event_date ASC
) AS rnPadrão 5 — Anti-fan-out: agregar ANTES do JOIN:
-- ERRADO: leads × eventos → cardinalidade explode
SELECT l.*, e.event_type FROM leads l JOIN events e USING (lead_id)
-- CORRETO: uma linha por lead_id antes do JOIN
WITH event_summary AS (
SELECT lead_id, COUNT(*) AS n_events FROM events GROUP BY lead_id
)
SELECT l.*, e.n_events FROM leads l LEFT JOIN event_summary e USING (lead_id)Múltiplas janelas sem múltiplos scans (eficiente em BigQuery):
COUNTIF(event_date >= cutoff_7d) AS w7d_events,
COUNTIF(event_date >= cutoff_30d) AS w30d_events,
COUNT(*) AS w90d_events
-- Uma única passagem pelos dados, três janelas de resultadoPoint-in-time correto — evita data leakage no treino:
-- Filtro crítico: estritamente ANTES da data de observação
LEFT JOIN events AS e
ON e.lead_id = tp.lead_id
AND DATE(e.event_date) < tp.observation_date -- ← sem isso, há leakageSazonalidade com sin/cos (preserva ciclicidade para modelos lineares):
SIN(2 * ACOS(-1) * EXTRACT(DAYOFWEEK FROM event_date) / 7) AS dow_sin,
COS(2 * ACOS(-1) * EXTRACT(DAYOFWEEK FROM event_date) / 7) AS dow_cosfrom python.encoders.categorical_encoders import TargetEncoder
encoder = TargetEncoder(smoothing=10.0, n_folds=5)
# No treino: encoding out-of-fold (nunca vê o próprio target)
X_train_encoded = encoder.fit_transform_train(X_train, y_train)
# No teste/produção: usa o mapeamento fit no treino completo
X_test_encoded = encoder.transform(X_test)from python.encoders.categorical_encoders import WOEEncoder
encoder = WOEEncoder(regularization=0.5)
encoder.fit(X_train, y_train)
print(encoder.iv_summary())
# variavel iv classificacao
# broker_segment 0.312 alto (verificar leakage)
# lead_source 0.148 médio
# income_band 0.087 fraco
# uf 0.019 sem poderfrom python.encoders.categorical_encoders import CyclicEncoder
encoder = CyclicEncoder(col_periods={"hour": 24, "day_of_week": 7, "month": 12})
X_encoded = encoder.fit_transform(X)
# Cria: hour_sin, hour_cos, day_of_week_sin, day_of_week_cos, month_sin, month_cosfrom python.validators.feature_validator import FeatureValidator
validator = FeatureValidator(
target_col="converted",
missing_threshold=0.30,
leakage_threshold=0.90,
)
# Validação antes do treino
result = validator.validate(df_train)
print(result.summary())
if not result.passed:
raise ValueError("Validação falhou — corrigir antes de treinar")
# Detecção de drift antes do scoring (PSI)
result_drift = validator.validate_drift(df_train, df_score, psi_threshold=0.2)
# PSI > 0.2 → retreinar o modelofeat_master.sqlx implementa uma feature store incremental no BigQuery:
- Particionada por
snapshot_date→ queries de treino e scoring filtram por data sem full scan - Clusterizada por
lead_tier, broker_segment→ joins com mart de scoring são eficientes - Incremental → só processa a partição do dia atual, não reprocessa histórico
- Point-in-time safe → cada snapshot usa apenas features disponíveis naquela data
# Deploy via Dataform
dataform run --tags feature_storeSem K-fold, a linha de treino usa seu próprio target para calcular o encoding — o modelo aprende encoded_value → target trivialmente. O K-fold garante que cada linha seja encoded usando apenas as outras linhas.
Population Stability Index mede se a distribuição das features em produção mudou em relação ao treino. PSI > 0.2 indica que o modelo foi treinado em dados que não representam mais a população atual — o score deixa de ser confiável.
Um JOIN de leads (10k linhas) × events (500k linhas) sem GROUP BY anterior produz uma tabela com até 50M linhas. Modelos treinados nessa tabela superestimam a importância de leads com mais eventos. A solução é sempre agregar a tabela maior antes do JOIN.
Um modelo linear trata hora 23 e hora 0 como distantes (|23-0|=23). Com sin/cos, a distância circular é correta — hora 23 e hora 0 estão próximas no espaço encodado.
# Instala dependências Python
pip install pandas numpy scikit-learn scipy xgboost
# Testa encoders
python -c "
from python.encoders.categorical_encoders import TargetEncoder, WOEEncoder
print('Encoders OK')
"
# Testa validador
python -c "
from python.validators.feature_validator import FeatureValidator
print('Validator OK')
"MIT License.