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

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

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).