Caractéristiques de l'ingénierie et de la sélection
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 * IDFDes 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
- 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.
- Implémenter un codeur cible exclusif: pour chaque ligne, calculer la moyenne cible en excluant la valeur cible de cette ligne.
- 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
| Term | What people say | What 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
- Feature Engineering and Selection (Max Kuhn & Kjell Johnson)- livre en ligne gratuit couvrant l'ensemble du paysage de l'ingénierie des fonctionnalités
- scikit-learn Preprocessing Guide- référence pratique pour toutes les transformations standard
- Target Encoding Done Right (Micci-Barreca, 2001)- le document original sur la codage cible avec l'allumage
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.