Phase 02 - Lesson 08
Engenharia e seleção de recursos
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Um bom recurso vale mais que mil pontos de dados.
Tipo: Construir Idiomas: Python Pré-requisitos: Fase 1 (Estatísticas para ML, Álgebra Linear), Fase 2, Lições 1-7 Tempo: ~90 minutos
Objetivos de aprendizagem
- Implementar transformações numéricas (padronização, escalonamento min-max, transformação logarítmica, binning) e explicar quando cada uma é apropriada
- Crie codificação one-hot, de rótulo e de destino para recursos categóricos e identifique o risco de vazamento de dados na codificação de destino
- Construir um vetorizador TF-IDF do zero e explicar por que ele supera a contagem bruta de palavras para classificação de texto
- Aplicar seleção de recursos baseada em filtros (limite de variação, correlação, informações mútuas) para reduzir a dimensionalidade
O problema
Você tem um conjunto de dados. Você escolhe um algoritmo. Você treina. Os resultados são medíocres. Você tenta um algoritmo mais sofisticado. Ainda medíocre. Você passa uma semana ajustando hiperparâmetros. Melhoria marginal.
Então, alguém transforma os dados brutos em recursos melhores e uma regressão logística simples supera seu conjunto ajustado com gradiente aprimorado.
Isso acontece constantemente. No ML clássico, a representação dos dados é mais importante do que a escolha do algoritmo. Um modelo de preço de casa com “metragem quadrada” e “número de quartos” superará um modelo com “endereço como uma sequência bruta”, não importa quão sofisticado o aluno seja. O algoritmo só pode funcionar com o que você fornece.
A engenharia de características é o processo de transformar dados brutos em representações que tornam os padrões mais fáceis de serem encontrados pelos modelos. A seleção de recursos é o processo de descartar recursos que adicionam ruído sem adicionar sinal. Juntos, eles são a atividade de maior alavancagem no ML clássico.
O Conceito
O Recurso Pipeline
flowchart LR
A[Raw Data] --> B[Handle Missing Values]
B --> C[Numerical Transforms]
B --> D[Categorical Encoding]
B --> E[Text Features]
C --> F[Feature Interactions]
D --> F
E --> F
F --> G[Feature Selection]
G --> H[Model-Ready Data]
Recursos Numéricos
Os números brutos raramente estão prontos para o modelo. Transformações comuns:
Escalonamento: Coloque recursos no mesmo intervalo para que algoritmos baseados em distância (K-Means, KNN, SVM) tratem todos os recursos igualmente. A escala mínimo-máximo é mapeada para [0, 1]. A padronização (pontuação z) é mapeada para média = 0, padrão = 1.
Transformação de log: compacta distribuições distorcidas à direita (renda, população, contagem de palavras). Transforma relações multiplicativas em relações aditivas.
Binning: Converte valores contínuos em categorias. Útil quando a relação entre recurso e destino não é linear, mas gradual (por exemplo, faixas etárias).
Recursos polinomiais: Cria termos x^2, x^3, x1*x2. Permite que modelos lineares capturem relacionamentos não lineares ao custo de mais recursos.
Recursos categóricos
Os modelos precisam de números. As categorias precisam de codificação.
Codificação one-hot: Cria uma coluna binária para cada categoria. "cor = vermelho/azul/verde" torna-se três colunas: is_red, is_blue, is_green. Funciona bem para recursos de baixa cardinalidade, mas explode em muitas categorias.
Codificação de rótulo: mapeia cada categoria para um número inteiro: vermelho=0, azul=1, verde=2. Introduz uma ordem falsa (o modelo pode pensar verde > azul > vermelho). Apropriado apenas para modelos baseados em árvore que se dividem em valores individuais.
Codificação de destino: Substitui cada categoria pela média da variável de destino dessa categoria. Poderoso, mas perigoso: alto risco de vazamento de dados. Deve ser calculado apenas em dados de treinamento e aplicado a dados de teste.
Recursos de texto
Contar vetorizador: Conta quantas vezes cada palavra aparece em um documento. "o gato sentou no tapete" torna-se {the: 2, cat: 1, sat: 1, on: 1, mat: 1}.
TF-IDF: Frequência de documento inversa de frequência de termo. Avalia as palavras de acordo com o quão únicas elas são nos documentos. Palavras comuns como “o” têm baixo peso. Palavras raras e distintas ganham grande peso.
TF(word, doc) = count(word in doc) / total words in doc
IDF(word) = log(total docs / docs containing word)
TF-IDF = TF * IDF
Valores ausentes
Os dados reais têm lacunas. Estratégias:
- Eliminar linhas: Somente quando os dados ausentes são raros e aleatórios
- Imputação de média/mediana: Simples, preserva o formato da distribuição (a mediana é mais robusta para valores discrepantes)
- Imputação de modo: Para recursos categóricos
- Coluna indicadora: Adicione uma coluna binária "was_this_missing" antes de imputar. O fato de faltarem dados pode ser informativo
- Preenchimento para frente/para trás: Para dados de série temporal
Interação de recursos
Às vezes o relacionamento está na combinação. “Altura” e “peso” por si só são menos preditivos do que “IMC = peso/altura ^ 2”. As interações de recursos multiplicam o espaço de recursos, portanto, use o conhecimento do domínio para escolher os corretos.
Seleção de recursos
Mais recursos nem sempre são melhores. Recursos irrelevantes adicionam ruído, aumentam o tempo de treinamento e podem causar overfitting.
Métodos de filtro (pré-modelo):
- Correlação: remova recursos altamente correlacionados entre si (redundantes)
- Informação mútua: mede o quanto conhecer uma característica reduz a incerteza sobre o alvo
- Limite de variação: remova recursos que variam pouco
Métodos wrapper (baseados em modelo):
- Regularização L1 (Lasso): leva pesos de recursos irrelevantes a exatamente zero
- Eliminação de recursos recursivos: treinar, remover recursos menos importantes, repetir
Por que a seleção é importante: Um modelo com 10 recursos bons geralmente terá desempenho superior a um modelo com 10 recursos bons e 90 barulhentos. Os recursos ruidosos dão ao modelo oportunidades de superajuste em padrões de dados de treinamento que não são generalizáveis.
Construa
Etapa 1: transformações numéricas do zero
import math
def min_max_scale(values):
min_val = min(values)
max_val = max(values)
if max_val == min_val:
return [0.0] * len(values)
return [(v - min_val) / (max_val - min_val) for v in values]
def standardize(values):
n = len(values)
mean = sum(values) / n
variance = sum((v - mean) ** 2 for v in values) / n
std = math.sqrt(variance) if variance > 0 else 1.0
return [(v - mean) / std for v in values]
def log_transform(values):
return [math.log(v + 1) for v in values]
def bin_values(values, n_bins=5):
min_val = min(values)
max_val = max(values)
bin_width = (max_val - min_val) / n_bins
if bin_width == 0:
return [0] * len(values)
result = []
for v in values:
bin_idx = int((v - min_val) / bin_width)
bin_idx = min(bin_idx, n_bins - 1)
result.append(bin_idx)
return result
def polynomial_features(row, degree=2):
n = len(row)
result = list(row)
if degree >= 2:
for i in range(n):
result.append(row[i] ** 2)
for i in range(n):
for j in range(i + 1, n):
result.append(row[i] * row[j])
return result
Etapa 2: codificação categórica do zero
def one_hot_encode(values):
categories = sorted(set(values))
cat_to_idx = {cat: i for i, cat in enumerate(categories)}
n_cats = len(categories)
encoded = []
for v in values:
row = [0] * n_cats
row[cat_to_idx[v]] = 1
encoded.append(row)
return encoded, categories
def label_encode(values):
categories = sorted(set(values))
cat_to_int = {cat: i for i, cat in enumerate(categories)}
return [cat_to_int[v] for v in values], cat_to_int
def target_encode(feature_values, target_values, smoothing=10):
global_mean = sum(target_values) / len(target_values)
category_stats = {}
for feat, target in zip(feature_values, target_values):
if feat not in category_stats:
category_stats[feat] = {"sum": 0.0, "count": 0}
category_stats[feat]["sum"] += target
category_stats[feat]["count"] += 1
encoding = {}
for cat, stats in category_stats.items():
cat_mean = stats["sum"] / stats["count"]
weight = stats["count"] / (stats["count"] + smoothing)
encoding[cat] = weight * cat_mean + (1 - weight) * global_mean
return [encoding[v] for v in feature_values], encoding
Etapa 3: recursos de texto do zero
def count_vectorize(documents):
vocab = {}
idx = 0
for doc in documents:
for word in doc.lower().split():
if word not in vocab:
vocab[word] = idx
idx += 1
vectors = []
for doc in documents:
vec = [0] * len(vocab)
for word in doc.lower().split():
vec[vocab[word]] += 1
vectors.append(vec)
return vectors, vocab
def tfidf(documents):
n_docs = len(documents)
vocab = {}
idx = 0
for doc in documents:
for word in doc.lower().split():
if word not in vocab:
vocab[word] = idx
idx += 1
doc_freq = {}
for doc in documents:
seen = set()
for word in doc.lower().split():
if word not in seen:
doc_freq[word] = doc_freq.get(word, 0) + 1
seen.add(word)
vectors = []
for doc in documents:
words = doc.lower().split()
word_count = len(words)
tf_map = {}
for word in words:
tf_map[word] = tf_map.get(word, 0) + 1
vec = [0.0] * len(vocab)
for word, count in tf_map.items():
tf = count / word_count
idf = math.log(n_docs / doc_freq[word])
vec[vocab[word]] = tf * idf
vectors.append(vec)
return vectors, vocab
Etapa 4: imputação de valor ausente do zero
def impute_mean(values):
present = [v for v in values if v is not None]
if not present:
return [0.0] * len(values), 0.0
mean = sum(present) / len(present)
return [v if v is not None else mean for v in values], mean
def impute_median(values):
present = sorted(v for v in values if v is not None)
if not present:
return [0.0] * len(values), 0.0
n = len(present)
if n % 2 == 0:
median = (present[n // 2 - 1] + present[n // 2]) / 2
else:
median = present[n // 2]
return [v if v is not None else median for v in values], median
def impute_mode(values):
present = [v for v in values if v is not None]
if not present:
return values, None
counts = {}
for v in present:
counts[v] = counts.get(v, 0) + 1
mode = max(counts, key=counts.get)
return [v if v is not None else mode for v in values], mode
def add_missing_indicator(values):
return [0 if v is not None else 1 for v in values]
Etapa 5: seleção de recursos do zero
def correlation(x, y):
n = len(x)
mean_x = sum(x) / n
mean_y = sum(y) / n
cov = sum((xi - mean_x) * (yi - mean_y) for xi, yi in zip(x, y)) / n
std_x = math.sqrt(sum((xi - mean_x) ** 2 for xi in x) / n)
std_y = math.sqrt(sum((yi - mean_y) ** 2 for yi in y) / n)
if std_x == 0 or std_y == 0:
return 0.0
return cov / (std_x * std_y)
def mutual_information(feature, target, n_bins=10):
feat_min = min(feature)
feat_max = max(feature)
bin_width = (feat_max - feat_min) / n_bins if feat_max != feat_min else 1.0
feat_binned = [
min(int((f - feat_min) / bin_width), n_bins - 1) for f in feature
]
n = len(feature)
target_classes = sorted(set(target))
feat_bins = sorted(set(feat_binned))
p_feat = {}
for b in feat_bins:
p_feat[b] = feat_binned.count(b) / n
p_target = {}
for t in target_classes:
p_target[t] = target.count(t) / n
mi = 0.0
for b in feat_bins:
for t in target_classes:
joint_count = sum(
1 for fb, tv in zip(feat_binned, target) if fb == b and tv == t
)
p_joint = joint_count / n
if p_joint > 0:
mi += p_joint * math.log(p_joint / (p_feat[b] * p_target[t]))
return mi
def variance_threshold(features, threshold=0.01):
n_features = len(features[0])
n_samples = len(features)
selected = []
for j in range(n_features):
col = [features[i][j] for i in range(n_samples)]
mean = sum(col) / n_samples
var = sum((v - mean) ** 2 for v in col) / n_samples
if var >= threshold:
selected.append(j)
return selected
def remove_correlated(features, threshold=0.9):
n_features = len(features[0])
n_samples = len(features)
to_remove = set()
for i in range(n_features):
if i in to_remove:
continue
col_i = [features[r][i] for r in range(n_samples)]
for j in range(i + 1, n_features):
if j in to_remove:
continue
col_j = [features[r][j] for r in range(n_samples)]
corr = abs(correlation(col_i, col_j))
if corr >= threshold:
to_remove.add(j)
return [i for i in range(n_features) if i not in to_remove]
Etapa 6: pipeline completo e demonstração
import random
def make_housing_data(n=200, seed=42):
random.seed(seed)
data = []
for _ in range(n):
sqft = random.uniform(500, 5000)
bedrooms = random.choice([1, 2, 3, 4, 5])
age = random.uniform(0, 50)
neighborhood = random.choice(["downtown", "suburbs", "rural"])
has_pool = random.choice([True, False])
sqft_with_missing = sqft if random.random() > 0.05 else None
age_with_missing = age if random.random() > 0.08 else None
price = (
50 * sqft
+ 20000 * bedrooms
- 1000 * age
+ (50000 if neighborhood == "downtown" else 10000 if neighborhood == "suburbs" else 0)
+ (15000 if has_pool else 0)
+ random.gauss(0, 20000)
)
data.append({
"sqft": sqft_with_missing,
"bedrooms": bedrooms,
"age": age_with_missing,
"neighborhood": neighborhood,
"has_pool": has_pool,
"price": price,
})
return data
if __name__ == "__main__":
data = make_housing_data(200)
print("=== Raw Data Sample ===")
for row in data[:3]:
print(f" {row}")
sqft_raw = [d["sqft"] for d in data]
age_raw = [d["age"] for d in data]
prices = [d["price"] for d in data]
print("\n=== Missing Value Handling ===")
sqft_missing = sum(1 for v in sqft_raw if v is None)
age_missing = sum(1 for v in age_raw if v is None)
print(f" sqft missing: {sqft_missing}/{len(sqft_raw)}")
print(f" age missing: {age_missing}/{len(age_raw)}")
sqft_indicator = add_missing_indicator(sqft_raw)
age_indicator = add_missing_indicator(age_raw)
sqft_imputed, sqft_fill = impute_median(sqft_raw)
age_imputed, age_fill = impute_mean(age_raw)
print(f" sqft filled with median: {sqft_fill:.0f}")
print(f" age filled with mean: {age_fill:.1f}")
print("\n=== Numerical Transforms ===")
sqft_scaled = standardize(sqft_imputed)
age_scaled = min_max_scale(age_imputed)
sqft_log = log_transform(sqft_imputed)
age_binned = bin_values(age_imputed, n_bins=5)
print(f" sqft standardized: mean={sum(sqft_scaled)/len(sqft_scaled):.4f}, std={math.sqrt(sum(v**2 for v in sqft_scaled)/len(sqft_scaled)):.4f}")
print(f" age min-max: [{min(age_scaled):.2f}, {max(age_scaled):.2f}]")
print(f" age bins: {sorted(set(age_binned))}")
print("\n=== Categorical Encoding ===")
neighborhoods = [d["neighborhood"] for d in data]
ohe, ohe_cats = one_hot_encode(neighborhoods)
print(f" One-hot categories: {ohe_cats}")
print(f" Sample encoding: {neighborhoods[0]} -> {ohe[0]}")
le, le_map = label_encode(neighborhoods)
print(f" Label encoding map: {le_map}")
te, te_map = target_encode(neighborhoods, prices, smoothing=10)
print(f" Target encoding: {({k: round(v) for k, v in te_map.items()})}")
print("\n=== Text Features ===")
descriptions = [
"large modern house with pool",
"small cozy cottage near downtown",
"spacious family home with large yard",
"modern apartment downtown with view",
"rustic cabin in rural area",
]
cv, cv_vocab = count_vectorize(descriptions)
print(f" Vocabulary size: {len(cv_vocab)}")
print(f" Doc 0 non-zero features: {sum(1 for v in cv[0] if v > 0)}")
tf, tf_vocab = tfidf(descriptions)
print(f" TF-IDF vocabulary size: {len(tf_vocab)}")
top_words = sorted(tf_vocab.keys(), key=lambda w: tf[0][tf_vocab[w]], reverse=True)[:3]
print(f" Doc 0 top TF-IDF words: {top_words}")
print("\n=== Polynomial Features ===")
sample_row = [sqft_scaled[0], age_scaled[0]]
poly = polynomial_features(sample_row, degree=2)
print(f" Input: {[round(v, 4) for v in sample_row]}")
print(f" Polynomial: {[round(v, 4) for v in poly]}")
print(f" Features: [x1, x2, x1^2, x2^2, x1*x2]")
print("\n=== Feature Selection ===")
feature_matrix = [
[sqft_scaled[i], age_scaled[i], float(sqft_indicator[i]), float(age_indicator[i])]
+ ohe[i]
for i in range(len(data))
]
print(f" Total features: {len(feature_matrix[0])}")
surviving_var = variance_threshold(feature_matrix, threshold=0.01)
print(f" After variance threshold (0.01): {len(surviving_var)} features kept")
surviving_corr = remove_correlated(feature_matrix, threshold=0.9)
print(f" After correlation filter (0.9): {len(surviving_corr)} features kept")
binary_prices = [1 if p > sum(prices) / len(prices) else 0 for p in prices]
print("\n Mutual information with target:")
feature_names = ["sqft", "age", "sqft_missing", "age_missing"] + [f"neigh_{c}" for c in ohe_cats]
for j in range(len(feature_matrix[0])):
col = [feature_matrix[i][j] for i in range(len(feature_matrix))]
mi = mutual_information(col, binary_prices, n_bins=10)
print(f" {feature_names[j]}: MI={mi:.4f}")
print("\n Correlation with price:")
for j in range(len(feature_matrix[0])):
col = [feature_matrix[i][j] for i in range(len(feature_matrix))]
corr = correlation(col, prices)
print(f" {feature_names[j]}: r={corr:.4f}")
Use-o
Com scikit-learn, essas transformações são pipelines combináveis:
from sklearn.preprocessing import StandardScaler, OneHotEncoder, PolynomialFeatures
from sklearn.impute import SimpleImputer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_selection import mutual_info_classif, VarianceThreshold
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
numeric_pipe = Pipeline([
("imputer", SimpleImputer(strategy="median")),
("scaler", StandardScaler()),
])
categorical_pipe = Pipeline([
("encoder", OneHotEncoder(sparse_output=False)),
])
preprocessor = ColumnTransformer([
("num", numeric_pipe, ["sqft", "age"]),
("cat", categorical_pipe, ["neighborhood"]),
])
As versões originais mostram exatamente o que acontece dentro de cada transformação. As versões da biblioteca adicionam manipulação de casos extremos, suporte de matriz esparsa e composição de pipeline, mas a matemática é a mesma.
Envie
Esta lição produz:
outputs/prompt-feature-engineer.md- um prompt para recursos de engenharia sistemática a partir de dados brutos
Exercícios
- Adicione uma escala robusta (usando mediana e intervalo interquartil em vez de média e desvio padrão) às transformações numéricas. Compare-o com o dimensionamento padrão em dados com valores discrepantes extremos.
- Implemente a codificação alvo omissa: para cada linha, calcule a média alvo excluindo o próprio valor alvo dessa linha. Mostre como isso reduz o overfitting em comparação com a codificação de destino ingênua.
- Construir um pipeline automatizado de seleção de recursos que combine limite de variação, filtragem de correlação e classificação de informações mútuas. Aplique-o ao conjunto de dados habitacionais e compare o desempenho do modelo (use uma regressão linear simples) com todos os recursos versus os recursos selecionados.
Termos-chave
| Prazo | O que as pessoas dizem | O que isso realmente significa |
|---|---|---|
| Engenharia de características | "Fazendo novas colunas" | Transformando dados brutos em representações que expõem padrões ao modelo |
| Padronização | "Tornando tudo normal" | Subtraindo a média e dividindo pelo desvio padrão para que o recurso tenha média=0 e padrão=1 |
| Codificação one-hot | "Fazendo variáveis fictícias" | Criando uma coluna binária por categoria, onde exatamente uma coluna é 1 para cada linha |
| Codificação de destino | "Usando a resposta para codificar" | Substituição de cada categoria pelo valor-alvo médio dessa categoria, com suavização para evitar sobreajustamento |
| TF-IDF | "Contagem de palavras sofisticadas" | Frequência do termo vezes Frequência inversa do documento: palavras ponderadas pela sua distinção no corpus |
| Imputação | "Preenchendo lacunas" | Substituição de valores em falta por valores estimados (média, mediana, moda ou previstos pelo modelo) |
| Seleção de recursos | "Jogando fora colunas ruins" | Removendo recursos que adicionam ruído ou redundância, mantendo apenas aqueles com sinal sobre o alvo |
| Informação mútua | "Quanto uma coisa diz sobre outra" | Uma medida da redução da incerteza sobre a variável Y obtida pela observação da variável X |
| Vazamento de dados | "Trapaceiro acidental" | Utilizar informações durante o treinamento que não estariam disponíveis no momento da previsão, gerando resultados falsamente otimistas |
Leitura Adicional
- Engenharia e Seleção de Recursos (Max Kuhn & Kjell Johnson) - livro online gratuito que cobre todo o panorama da engenharia de características
- scikit-learn Guia de pré-processamento - referência prática para todas as transformações padrão
- Target Encoding Done Right (Micci-Barreca, 2001) - o artigo original sobre codificação de destino com suavização