Phase 02: ML Fundamentals

Caractéristiques de l'ingénierie et de la sélection

Une bonne fonctionnalité vaut mille points de données.

Type: Build

Languages: Python

Prerequisites: Phase 1 (Statistics for ML, Linear Algebra), Phase 2 Lessons 1-7

Time: ~90 minutes

Objectifs d'apprentissage

  • Implémenter des transformations numériques (standardisation, mise à l'échelle minimale, transformation log, encastrement) et expliquer quand chacune est appropriée
  • Construire un code unique, une étiquette et un code cible pour les caractéristiques catégoriques et identifier le risque de fuite de données dans le code cible
  • Construire un vectorificateur TF-IDF à partir de zéro et expliquer pourquoi il dépasse le nombre de mots bruts pour la classification du texte
  • Appliquer la sélection des caractéristiques basée sur le filtre (troisons de variance, corrélation, informations mutuelles) pour réduire la dimensionnalité

Le problème

Vous avez un ensemble de données, vous choisissez un algorithme, vous le faites suivre, les résultats sont médiocres, vous essayez un algorithme plus sophistiqué, vous passez une semaine à régler les hyperparametres, vous améliorez marginellement.

Puis quelqu'un transforme les données brutes en meilleures fonctionnalités et une simple régression logistique bat votre ensemble réglé augmenté par gradient.

Dans le système de calcul classique, la représentation des données est plus importante que le choix de l'algorithme. Un modèle de prix de la maison avec "images carrées" et "nombre de chambres" va battre un modèle avec "adresse comme une chaîne brute" peu importe la sophistication de l'apprenant. L'algorithme ne peut fonctionner que avec ce que vous lui donnez.

L'ingénierie des caractéristiques est le processus de transformation des données brutes en représentations qui facilitent la recherche de modèles. La sélection des caractéristiques est le processus de rejet des caractéristiques qui ajoutent du bruit sans ajouter de signal. Ensemble, elles constituent l'activité à levier le plus élevé dans le ML classique.

Le concept

Le pipeline de caractéristiques

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]

Caractéristiques numériques

Les nombres bruts sont rarement prêts à être modelés.

Scaling:Mettre les caractéristiques sur la même plage afin que les algorithmes basés sur la distance (K-Means, KNN, SVM) traitent toutes les caractéristiques de manière égale.

Log transform:Comprime les répartitions à droite (revenu, population, nombre de mots). Transforme les relations multiplicatives en relations additives.

Binning:Convertisse les valeurs continues en catégories. Utilisée lorsque la relation entre caractéristique et cible est non linéaire mais passive (par exemple, groupes d'âge).

Polynomial features:Il crée des termes x^2, x^3, x1*x2. Les modèles linéaires captent des relations non linéaires au prix de plus de fonctionnalités.

Caractéristiques de la catégorie

Les modèles ont besoin de numéros, les catégories de code.

One-hot encoding:Créer une colonne binaire pour chaque catégorie. "color = red/blue/green" devient trois colonnes: is_red, is_blue, is_green. Fonctionne bien pour les fonctionnalités de basse cardinalité mais explose avec de nombreuses catégories.

Label encoding:Mape chaque catégorie à un entier: rouge = 0, bleu = 1, vert = 2. Introduit un faux ordre (le modèle pourrait penser vert > bleu > rouge).

Target encoding:Remplace chaque catégorie par la moyenne de la variable cible de cette catégorie. Puissant mais dangereux: risque élevé de fuite de données.

Les caractéristiques du texte

Count vectorizer:Comptez combien de fois chaque mot apparaît dans un document. "le chat s'est assis sur le tapis" devient {le: 2, le chat: 1, sat: 1, sur: 1, mat: 1}.

TF-IDF:Le terme fréquence-inverse document fréquence. pèse les mots par la façon dont ils sont uniques dans les documents. Les mots communs comme "le" ont un poids faible.

TF(word, doc) = count(word in doc) / total words in doc
IDF(word) = log(total docs / docs containing word)
TF-IDF = TF * IDF

Des valeurs manquantes

Les données réelles ont des trous.

  • Drop rows:Seulement lorsque les données manquantes sont rares et aléatoires
  • Mean/median imputation:Simple, préservant la forme de distribution (la moyenne est plus robuste aux écarts)
  • Mode imputation:Pour les caractéristiques catégoriques
  • Indicator column:Ajouter une colonne binaire "was_this_missing" avant d'imputer.
  • Forward/backward fill:Pour les données de séries temporelles

Interaction des caractéristiques

Parfois, la relation est dans la combinaison. "Ton poids" et "tête" seuls sont moins prédictifs que "BMI = poids / hauteur^2".

Sélection de fonctionnalités

Les caractéristiques plus importantes ajoutent du bruit, augmentent le temps d'entraînement et peuvent provoquer un surcoût.

Filter methods (pre-model):

  • Corrélation: supprimer les caractéristiques fortement corrélées entre elles (rédundantes)
  • Informations mutuelles: mesure la mesure dans laquelle la connaissance d'une caractéristique réduit l'incertitude quant à l'objectif
  • Térubin de variance: supprimer les caractéristiques qui varient à peine

Wrapper methods (model-based):

  • L1 régularisation (Lasso): entraîne des poids de caractéristiques irrélevants à zéro exactement
  • Élimination de la fonction récursive: entraînement, suppression de la fonction la moins importante, répétition

Why selection matters:Un modèle avec 10 bonnes caractéristiques surpasse généralement un modèle avec 10 bonnes caractéristiques et 90 bruyants.

Faites-le

Étape 1: Transformations numériques à partir de zéro

pythonimport 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

Étape 2: Codage catégorique à partir de zéro

pythondef 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

Étape 3: Caractéristiques du texte à partir de zéro

pythondef 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

Étape 4: Imputation de valeur manquante à partir de zéro

pythondef 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]

Étape 5: Sélection des fonctionnalités à partir de zéro

pythondef 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]

Étape 6: L'ensemble du pipeline et la démonstration

pythonimport 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}")

Utilisez-le

Avec scikit-learn, ces transformations sont des pipelines composables:

pythonfrom 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"]),
])

Les versions de la bibliothèque ajoutent une manipulation de bord, un support de matrice rare et une composition de pipeline, mais les mathématiques sont les mêmes.

La faire partir

Cette leçon donne:

  • outputs/prompt-feature-engineer.md- une demande de conception systématique des caractéristiques à partir de données brutes

Exercices

  1. Ajouter une mise à l'échelle robuste (en utilisant la plage médiane et interquartile au lieu de la moyenne et de l'écart standard) aux transformations numériques.
  2. Implémenter un codeur cible exclusif: pour chaque ligne, calculer la moyenne cible en excluant la valeur cible de cette ligne.
  3. Construisez un pipeline de sélection automatique de fonctionnalités qui combine le seuil de variance, le filtrage de corrélation et le classement des informations mutuelles. Appliquez-le à l'ensemble de données de logement et comparez les performances du modèle (utilisez une régression linéaire simple) avec toutes les fonctionnalités par rapport aux fonctionnalités sélectionnées.

Les termes clés

TermWhat people sayWhat it actually means
Feature engineering"Making new columns"Transforming raw data into representations that expose patterns to the model
Standardization"Making it normal"Subtracting the mean and dividing by standard deviation so the feature has mean=0 and std=1
One-hot encoding"Making dummy variables"Creating one binary column per category, where exactly one column is 1 for each row
Target encoding"Using the answer to encode"Replacing each category with the average target value for that category, with smoothing to prevent overfitting
TF-IDF"Fancy word counts"Term Frequency times Inverse Document Frequency: words weighted by how distinctive they are across the corpus
Imputation"Filling in blanks"Replacing missing values with estimated values (mean, median, mode, or model-predicted)
Feature selection"Throwing out bad columns"Removing features that add noise or redundancy, keeping only those with signal about the target
Mutual information"How much one thing tells you about another"A measure of the reduction in uncertainty about variable Y gained by observing variable X
Data leakage"Accidentally cheating"Using information during training that would not be available at prediction time, giving falsely optimistic results

Pour en savoir plus

This free lesson is part of the AI Engineering from Scratch curriculum. Read the full explanation, run the lesson code, and verify the result in the interactive reader or from the repository source.

Browse the complete course catalog or open this lesson on GitHub.