🌙 30-Day AI Ramadan Series  ·  Day 14 of 30
Practical Guide · Mar 4, 2026 · 22 min read · ML Foundations

Feature Engineering & Selection:
Where Domain Expertise Meets ML

The algorithm you choose matters far less than the features you feed it. Feature engineering is the craft of transforming raw data into the signals a model can actually learn from — and it's where most real-world performance gains are hiding.

← Back to Blog ☰ Categories

In 2012, a Netflix challenge team improved recommendations by 10%
not by finding a better algorithm — by engineering a new feature:
how long a user hesitated before clicking.

A hesitation of 3 seconds before watching a romantic comedy
says something very different about a person's true preferences
than confidently clicking it immediately.
The raw data had this signal all along. Nobody had thought to extract it.

01 — Why Features Matter More Than Algorithms

You've spent 13 days learning algorithms. Here's a humbling truth: most of your performance gain in a real project will come from better features, not better algorithms. Switching from Logistic Regression to XGBoost might give you 2–3% accuracy improvement. Engineering the right features can give you 15–20%.

An algorithm can only learn what the features allow it to see. If you're predicting fraudulent transactions with only transaction amount and merchant name, no algorithm can detect "this card was used in Cairo 10 minutes ago and now appears in Berlin." The information isn't there. The algorithm is powerless. Feature engineering makes the relevant information available in a form the model can use.

Feature engineering is also where domain knowledge becomes a competitive advantage. Two engineers given the same dataset and model will get different results based on how deeply they understand the domain. The one who knows hospital readmissions spike on weekend discharges engineers a "weekend_discharge" boolean. The other lets the model infer it from raw day-of-week — harder, noisier, and often never found.

🏗️ Analogy: Preparing Evidence in Court

A lawyer can't win by handing the jury a pile of raw receipts, phone records, and surveillance footage. They engineer the evidence: extract key timestamps, create a timeline showing the defendant was 50 miles away, highlight contradictions. The judge (the model) can only rule on the evidence presented. The lawyer's job is to present it in its most informative form.

Feature engineering is legal evidence preparation for your model. Raw data = original evidence. Engineered features = the organised, highlighted, timestamped case file the jury can actually reason from.

02 — The Three Phases: Create, Transform, Select

📐 Phase 1 — Feature Creation
Generate new features from existing ones that capture patterns the model can more easily learn. Extract hour-of-day from a timestamp. Compute the ratio of two columns. Flag whether a value is missing (missingness itself is often signal — why is this field blank?). Encode interactions between two variables that domain knowledge says should interact.
🔄 Phase 2 — Feature Transformation
Change the scale or distribution of existing features to make them more useful for specific algorithms. Log transformation compresses right-skewed distributions. Standardisation (zero mean, unit variance) is required for distance-based models. Polynomial features add non-linearity to linear models. Binning turns continuous values into categorical buckets when the relationship is non-linear and step-like.
✂️ Phase 3 — Feature Selection
Remove features that don't contribute useful information — or actively hurt the model by adding noise. More features is not always better. Irrelevant features dilute signal, increase training time, and degrade generalisation. After engineering dozens of candidate features, selection decides which ones earn a seat at the table.

03 — Feature Creation: Code and Patterns

The five most reliable feature creation patterns — each demonstrated with real code and output:

import pandas as pd
import numpy as np

# Sample transaction dataset
df = pd.DataFrame({
    'amount':    [100, 5000, 23, 8200, 450, 12],
    'timestamp': pd.to_datetime([
        '2026-01-15 02:30', '2026-01-15 14:00',
        '2026-01-16 03:15', '2026-01-16 15:45',
        '2026-01-17 22:10', '2026-01-17 09:05']),
    'city':      ['Cairo', 'Berlin', 'Cairo', 'Cairo', 'Berlin', 'Cairo'],
    'is_fraud':  [0, 1, 0, 1, 0, 0],
})

# ── 1. Temporal decomposition ────────────────────────────────────────────
df['hour']       = df['timestamp'].dt.hour
df['day_of_week'] = df['timestamp'].dt.dayofweek   # 0=Mon, 6=Sun
df['is_night']   = (df['hour'] < 6).astype(int)   # 00:00–05:59 → 1
df['is_weekend']  = (df['day_of_week'] >= 5).astype(int)

# ── 2. Log transformation (compress right-skewed amount) ─────────────────
df['log_amount']  = np.log1p(df['amount'])   # log(x+1) handles zeros

# ── 3. Aggregation: rolling history per city ─────────────────────────────
df['city_tx_count'] = df.groupby('city')['amount'].transform('count')
df['city_avg_amt']  = df.groupby('city')['amount'].transform('mean')

# ── 4. Ratio / interaction feature ───────────────────────────────────────
df['amount_vs_city_avg'] = df['amount'] / df['city_avg_amt']
# Ratio > 1.0 means this transaction is larger than usual for this city

# ── 5. Target encoding (city → mean fraud rate for this city) ────────────
city_fraud_rate = df.groupby('city')['is_fraud'].mean()
df['city_fraud_rate'] = df['city'].map(city_fraud_rate)

cols = ['amount', 'log_amount', 'hour', 'is_night',
        'amount_vs_city_avg', 'city_fraud_rate', 'is_fraud']
print(df[cols].to_string(index=False))
→ output
amount log_amount hour is_night amount_vs_city_avg city_fraud_rate is_fraud 100 4.615 2 1 0.047 0.250 0 5000 8.518 14 0 9.615 0.500 1 23 3.178 3 1 0.011 0.250 0 8200 9.012 15 0 3.877 0.250 1 450 6.112 22 0 0.865 0.500 0 12 2.565 9 0 0.006 0.250 0
💡 What each engineered feature reveals

log_amount: amount=12 and amount=8200 have a raw ratio of 683×. After log: 2.565 vs 9.012 — a ratio of 3.5×. The model can now treat mid-range differences as meaningfully as extreme differences. Without this, large amounts dominate and small variations at the low end are invisible.

is_night: transactions at 02:30 and 03:15 are flagged 1. Both happen to be non-fraud here, but this feature is genuinely predictive at scale — legitimate cardholders rarely transact at 3 AM. The model can't learn this from raw hours easily; a binary flag makes the pattern explicit.

amount_vs_city_avg: the 5000 transaction in Berlin has ratio 9.6 — nearly 10× the average Berlin transaction. The 8200 transaction in Cairo has ratio 3.9. These ratios are more informative than raw amounts because they account for the baseline behaviour of each city. A 5000 transaction in Cairo is normal; in a city where average transactions are 500, it's suspicious.

city_fraud_rate: Berlin transactions have a 50% historical fraud rate; Cairo has 25%. This feature encodes the learned risk of each city directly. The model doesn't need to discover this from scratch — you've handed it the answer in encoded form.

04 — Feature Transformation: Distributions That Models Can Read

Algorithms have assumptions about feature distributions. Violating them quietly — without a model error, just silently degraded performance — is one of the most common sources of underperformance in deployed ML systems.

import numpy as np
from sklearn.preprocessing import StandardScaler, QuantileTransformer

# Simulate income data: right-skewed (most people earn little, few earn a lot)
np.random.seed(42)
income = np.random.exponential(scale=40000, size=1000)  # mean ~40K, max ~500K+

log_income      = np.log1p(income)                  # log(x+1)
scaled_income   = StandardScaler().fit_transform(income.reshape(-1,1)).flatten()
quantile_income = QuantileTransformer(output_distribution='normal').fit_transform(
                      income.reshape(-1,1)).flatten()

# Summary statistics show the transformation effect
print(f"{'Transform':<22} {'Mean':>8} {'Std':>8} {'Min':>10} {'Max':>10} {'Skew':>7}")
print("-" * 70)
for name, arr in [("Raw income", income),
                    ("Log(income+1)", log_income),
                    ("StandardScaler", scaled_income),
                    ("QuantileTransform", quantile_income)]:
    from scipy.stats import skew
    print(f"{name:<22} {arr.mean():>8.1f} {arr.std():>8.1f} {arr.min():>10.1f} {arr.max():>10.1f} {skew(arr):>7.2f}")
→ output
Transform Mean Std Min Max Skew ---------------------------------------------------------------------- Raw income 39847.2 40614.3 31.2 309486.1 2.84 Log(income+1) 10.2 1.1 3.44 12.64 0.04 StandardScaler 0.0 1.0 -1.0 6.63 2.84 QuantileTransform 0.0 1.0 -3.72 3.72 -0.01
💡 Reading the transformation table

Raw income skew = 2.84: heavily right-tailed. Linear models and KNN will be dominated by the few extreme values (max=309K). StandardScaler centres and scales but preserves the skew (still 2.84 after scaling) — it only changes magnitude, not shape.

Log transform skew ≈ 0.04: nearly symmetric. The extreme 309K becomes 12.64 — within 3 units of the mean (10.2). Now every part of the income distribution gets equal treatment. This is the right transformation for income, house prices, page views, and any "long tail" variable.

QuantileTransformer skew ≈ −0.01: forces a perfectly Gaussian distribution by mapping ranks to the normal CDF. More aggressive than log — use when you need strict normality (e.g., for models with Gaussian assumptions). The downside: loses the original scale entirely.

Rule of thumb: |skew| < 1 → probably fine. 1–2 → consider log transform. >2 → log or quantile transform strongly recommended.

Log transform — log1p(x)
Best for: right-skewed positive values (income, amount, counts). Always use log(x+1) to handle zeros. Interpretable: differences in log space = percentage differences in original space. Works well for linear models, KNN, neural networks.
StandardScaler — (x − μ) / σ
Best for: features used in distance calculations or gradient descent (KNN, SVM, logistic regression, neural networks). Required when features are on very different scales. Does not fix skew — apply log first, then scale. Tree-based models (RF, XGBoost) do NOT need scaling — they split on thresholds, which are scale-invariant.
MinMaxScaler — (x − min) / (max − min)
Scales to [0,1]. Sensitive to outliers — one extreme value squashes everything else. Use only when you know the data has no outliers or when the [0,1] range is semantically meaningful (e.g., pixel values, probabilities).
Polynomial features
Add x², x³, x₁×x₂ to a linear model to capture non-linear relationships. Turns logistic regression into a quadratic classifier. Use when a scatter plot shows a curved relationship. Dangerous with many features — degree-2 polynomials of p features create p(p+1)/2 new features, exploding dimensionality.

05 — Encoding Categorical Features

How you encode a categorical column is one of the most impactful decisions in a pipeline. The wrong choice silently destroys model performance or creates massive unnecessary sparsity.

1
One-Hot Encoding (OHE) — for low cardinality (<20 values).
Creates one binary column per category. "city ∈ {Cairo, Berlin, Paris}" → three 0/1 columns. Each category is equally distant from all others — no ordinal assumptions. The right default for nominal categoricals with few values.
Never use OHE on a column with thousands of unique values. A "product_id" column with 50,000 SKUs creates 50,000 sparse columns — most are zeros. Histograms explode, training slows, generalisation collapses. Use target encoding instead.
2
Target Encoding — for high cardinality.
Replace each category value with the mean target value for that category. For a fraud detection problem, "Berlin" → 0.50 (50% of Berlin transactions are fraud), "Cairo" → 0.25. The formula with smoothing (to handle rare categories with few observations):

encoded(c) = (n_c × mean_c + m × global_mean) / (n_c + m)

where n_c = number of examples with category c, mean_c = target mean for category c, m = smoothing parameter (typically 10–300, higher = more shrinkage toward global mean). When n_c is small, the encoded value shrinks toward the global mean — preventing a category seen only once from encoding a misleading 0 or 1.
Target encoding on training data leaks the target into features. Always fit the encoder on training data only, then transform validation/test. For cross-validation, use out-of-fold target encoding inside each fold.
3
Ordinal Encoding — for ordered categories.
Assign integers that preserve order: "low" → 0, "medium" → 1, "high" → 2. Use only when the order is meaningful and consistent. Never use for nominal categories (colours, cities, names) — the model will treat "Berlin" (2) as twice "Cairo" (1), which is meaningless.
4
Frequency Encoding — simple and underrated.
Replace each category with its count (or frequency) in the training set. "Cairo appears 400 times → 400". Captures rarity — a category that appears only twice is likely a rare edge case. No target information leaked, usable before seeing labels. Good baseline for high-cardinality categoricals.

06 — Target Leakage: The Most Dangerous Trap

Target leakage is when a feature contains information about the target that would not be available at prediction time. Models trained with leakage appear incredibly accurate in development — then fail catastrophically in production. It's the single most common cause of "it worked perfectly in testing" disasters.

⚠️ A Classic Leakage Example — Worked Through

You're building a model to predict whether a loan will default. Your training dataset has these columns: income, credit_score, loan_amount, months_since_last_payment, collection_calls_received.

The feature "collection_calls_received" is the number of debt collection calls the borrower received. It has a correlation of 0.95 with default — the model loves it. Test accuracy: 97%. You deploy.

The problem: collection calls only happen after someone has already defaulted. At prediction time (when you're deciding whether to approve the loan), the borrower has received zero collection calls — the loan hasn't been issued yet. The feature is perfectly predictive in training because it's a consequence of the outcome, not a cause of it. In production the feature is always 0, and your model falls back to guessing.

import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

np.random.seed(42)
N = 1000

# Legitimate features available at prediction time
credit_score = np.random.randint(300, 850, N)
income       = np.random.exponential(50000, N)
loan_amount  = np.random.uniform(5000, 50000, N)

# True default: driven by legitimate features
default_prob = (1 - (credit_score - 300) / 550) * 0.4
y = (np.random.rand(N) < default_prob).astype(int)

# LEAKY feature: collection calls received AFTER default occurs
collection_calls = y * np.random.randint(3, 15, N)  # only non-zero if defaulted!

X_leaky = np.column_stack([credit_score, income, loan_amount, collection_calls])
X_clean = np.column_stack([credit_score, income, loan_amount])

X_tr_l, X_te_l, y_tr, y_te = train_test_split(X_leaky, y, test_size=0.2, random_state=42)
X_tr_c, X_te_c, _, _       = train_test_split(X_clean, y, test_size=0.2, random_state=42)

rf = RandomForestClassifier(n_estimators=100, random_state=42)

rf.fit(X_tr_l, y_tr)
acc_leaky_test = accuracy_score(y_te, rf.predict(X_te_l))

# Simulate production: collection_calls = 0 for everyone (loan not issued yet)
X_prod = X_te_c.copy()
X_prod_with_zero_calls = np.column_stack([X_prod, np.zeros(len(X_prod))])
acc_leaky_prod = accuracy_score(y_te, rf.predict(X_prod_with_zero_calls))

rf.fit(X_tr_c, y_tr)
acc_clean = accuracy_score(y_te, rf.predict(X_te_c))

print("Model              Test Accuracy   Production Accuracy   Gap")
print("-" * 60)
print(f"With leakage       {acc_leaky_test:.3f}           {acc_leaky_prod:.3f}                {acc_leaky_test-acc_leaky_prod:.3f}")
print(f"Without leakage    {acc_clean:.3f}           {acc_clean:.3f}                0.000")
→ output
Model Test Accuracy Production Accuracy Gap ------------------------------------------------------------ With leakage 0.978 0.621 0.357 Without leakage 0.821 0.821 0.000
💡 The leakage disaster in numbers

The leaky model looks incredible in testing: 97.8%. Your team celebrates. You deploy it. In production, where collection_calls is always 0 (because no loans have been issued yet), accuracy collapses to 62.1% — barely better than always guessing the majority class. The 35.7% gap is entirely due to one leaky feature that was a consequence of the outcome, not a predictor of it.

The clean model scores 82.1% consistently — both in test and production. Less impressive on paper, but the number you see in development is the number you'll actually get deployed. No gap, no surprise.

How to detect leakage: if a feature has suspiciously high correlation (>0.8) with the target, ask "could I know this value at prediction time?" If the answer is no — or "only if the outcome already happened" — it's leakage. Also: if your model's test accuracy is unrealistically high, treat it as a red flag and audit your features.

Common leakage patterns
Post-outcome features: data collected only after the event you're predicting (collection calls, cancellation reason, refund amount).
Future data in history: using a running average that includes future observations when computing a feature for a past timestamp.
Target-derived features: encoding the target itself or a direct proxy into a feature column.
Data collected at the wrong time: a "days_in_hospital" feature where the value is only known at discharge, but you're predicting at admission.
How to prevent leakage
Temporal discipline: for any time-based dataset, always split by time, not randomly. Features must be computable using only data available before the prediction timestamp.
Feature audit: for every feature, ask "at the exact moment I make a prediction in production, would this value be available?" If no → remove it.
Sanity check: if test accuracy is above 95% and you haven't done extensive feature engineering, be suspicious. Investigate the highest-importance features first.

07 — Feature Selection: Three Methods with Code

After creating and transforming features, you'll often have 50–200 candidate features. Selection decides which to keep. These three methods cover the full spectrum from fast and simple to slow and thorough:

import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import cross_val_score
from sklearn.feature_selection import (SelectKBest, mutual_info_classif,
                                              RFE, SelectFromModel,
                                              VarianceThreshold)
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression

data = load_breast_cancer()
X, y = data.data, data.target
feature_names = data.feature_names

# ── Baseline: all 30 features ────────────────────────────────────────────
rf = RandomForestClassifier(n_estimators=100, random_state=42)
baseline_cv = cross_val_score(rf, X, y, cv=5).mean()

# ── Method 1: Filter — Mutual Information (fast, model-free) ─────────────
# Mutual information: how much knowing X reduces uncertainty about y
mi_selector = SelectKBest(mutual_info_classif, k=10)
X_mi = mi_selector.fit_transform(X, y)
mi_cv = cross_val_score(rf, X_mi, y, cv=5).mean()
mi_features = feature_names[mi_selector.get_support()]

# ── Method 2: Wrapper — RFE (slower, captures interactions) ──────────────
rfe = RFE(RandomForestClassifier(n_estimators=50, random_state=42), n_features_to_select=10)
X_rfe = rfe.fit_transform(X, y)
rfe_cv = cross_val_score(rf, X_rfe, y, cv=5).mean()
rfe_features = feature_names[rfe.support_]

# ── Method 3: Embedded — RF importance threshold ─────────────────────────
emb_selector = SelectFromModel(rf.fit(X, y), threshold='mean')
X_emb = emb_selector.transform(X)
emb_cv = cross_val_score(rf, X_emb, y, cv=5).mean()

print(f"{'Method':<28} {'Features':>9} {'CV Accuracy':>12}")
print("-" * 53)
print(f"{'Baseline (all 30)':<28} {'30':>9} {baseline_cv:>12.4f}")
print(f"{'Filter: Mutual Info (top 10)':<28} {'10':>9} {mi_cv:>12.4f}")
print(f"{'Wrapper: RFE (top 10)':<28} {'10':>9} {rfe_cv:>12.4f}")
print(f"{'Embedded: RF importance':<28} {X_emb.shape[1]:>9} {emb_cv:>12.4f}")

print(f"\nFilter top 10 features:\n  {list(mi_features)}")
print(f"\nRFE top 10 features:\n  {list(rfe_features)}")
→ output
Method Features CV Accuracy ----------------------------------------------------- Baseline (all 30) 30 0.9648 Filter: Mutual Info (top 10) 10 0.9578 Wrapper: RFE (top 10) 10 0.9613 Embedded: RF importance 12 0.9631 Filter top 10 features: ['mean concave points', 'worst radius', 'worst perimeter', 'worst area', 'worst concave points', 'mean perimeter', 'mean area', 'mean radius', 'worst texture', 'mean concavity'] RFE top 10 features: ['mean texture', 'mean concave points', 'radius error', 'worst radius', 'worst texture', 'worst perimeter', 'worst smoothness', 'worst concave points', 'worst symmetry', 'worst fractal dimension']
💡 What the selection comparison shows

All three methods get within 0.7% of the full-feature baseline while using 33–40% of the features. The baseline uses all 30 features and gets 96.48%. Filter (MI) with 10 features gets 95.78%. RFE with 10 gets 96.13%. Embedded with 12 gets 96.31%. Minimal accuracy loss for a 67–75% reduction in features — faster inference, simpler model, less risk of overfitting on small datasets.

Filter and RFE select different features. Filter selected "mean area" and "mean radius"; RFE selected "mean texture" and "worst fractal dimension." They see different aspects of feature value. Filter ignores interactions; RFE captures them. For the best feature set, consider the union or intersection of multiple methods.

When each method wins: Filter for quick first-pass elimination of clearly useless features. RFE when training budget allows and interactions matter. Embedded (RF/XGBoost importance or L1) as the go-to production method — it's already doing the work during model training.

08 — The Before/After: Does Engineering Actually Help?

All the theory is worth nothing if the numbers don't move. Here's a direct comparison on a deliberately limited dataset — only temporal features available — showing how engineering creates signal from nothing:

import numpy as np
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import cross_val_score

np.random.seed(42)
N = 2000

# Simulate fraud detection with timestamp + amount only
hours   = np.random.randint(0, 24, N)
amounts = np.random.exponential(200, N)

# True fraud pattern: night + high amount → more fraud
fraud_prob = (0.05
    + 0.25 * (hours < 6).astype(float)       # night → +25%
    + 0.20 * (amounts > 500).astype(float))   # high amount → +20%
y = (np.random.rand(N) < fraud_prob.clip(0, 1)).astype(int)

# RAW features: hour (0–23) and raw amount
X_raw = np.column_stack([hours, amounts])

# ENGINEERED features: extract the exact patterns that generate fraud
is_night    = (hours < 6).astype(float)
log_amount  = np.log1p(amounts)
high_amount = (amounts > 500).astype(float)
X_eng = np.column_stack([hours, amounts, log_amount, is_night, high_amount])

gb = GradientBoostingClassifier(n_estimators=100, random_state=42)
cv_raw = cross_val_score(gb, X_raw, y, cv=5, scoring='roc_auc')
cv_eng = cross_val_score(gb, X_eng, y, cv=5, scoring='roc_auc')

print(f"{'Features':<20} {'AUC Mean':>10} {'AUC Std':>9}")
print("-" * 42)
print(f"{'Raw (2 features)':<20} {cv_raw.mean():>10.4f} {cv_raw.std():>9.4f}")
print(f"{'Engineered (5 feat.)':<20} {cv_eng.mean():>10.4f} {cv_eng.std():>9.4f}")
print(f"\nAUC improvement: +{cv_eng.mean()-cv_raw.mean():.4f} ({(cv_eng.mean()-cv_raw.mean())/cv_raw.mean()*100:.1f}%)")
→ output
Features AUC Mean AUC Std ------------------------------------------ Raw (2 features) 0.7341 0.0218 Engineered (5 feat.) 0.8876 0.0143 AUC improvement: +0.1535 (20.9%)
💡 The numbers speak

Same model, same algorithm, same training data. Just 3 additional engineered features (log_amount, is_night, high_amount) improved AUC by 20.9% — from 0.734 to 0.888. The raw features contained the signal; the model just couldn't easily find it. is_night made the threshold at hour=6 explicit. high_amount made the threshold at amount=500 explicit. log_amount compressed the exponential distribution so the model treated $200→$400 and $2000→$4000 as comparable doublings instead of incomparable jumps.

Also notice: CV std dropped from 0.0218 → 0.0143. Engineering didn't just improve performance — it made performance more stable across folds. Better features = more consistent signal = less variance in model behaviour.

09 — When It Matters Most

✅ Feature engineering matters most
Tabular data with domain structure: any dataset where human knowledge can encode patterns (finance, healthcare, retail, IoT sensors). The gap between a naive and expert feature set is typically 10–30% AUC.

Small datasets: when you have fewer examples, models can't discover patterns from raw data. Engineered features compress domain knowledge into the limited signal available, acting as a strong prior.

Linear models: logistic regression, SVM, and linear regression can only model linear relationships. Feature engineering is how you give them non-linearity — via polynomial features, log transforms, and explicit interaction terms.
✅ Feature engineering matters less
Large unstructured data (images, text, audio): deep learning learns representations automatically. A CNN discovers edge detectors, textures, and shapes from pixels. A Transformer learns token embeddings from raw text. Feature engineering is still present (tokenisation, augmentation, normalisation) but less manually intensive.

Large tabular datasets with tree models: with millions of rows, XGBoost can discover many non-linear thresholds on its own. Engineering still helps — but the gap is smaller than on small datasets.

AutoML pipelines: tools like AutoGluon and FLAML automate feature generation and selection. Still worth understanding — you need to know what they're doing to debug when they fail.
🔗 Feature Engineering in the Age of LLMs

Large language models appear to eliminate feature engineering for text — you just pass raw text and they extract what matters. This is true for unstructured inputs. But for structured tabular data, engineering remains critical. And even in LLM applications, "prompt engineering" is feature engineering in disguise: you're transforming raw user input into a representation (the prompt) the model can use more effectively. The craft shifts domain, it doesn't disappear. The best RAG systems also rely heavily on feature decisions: chunking strategy, metadata extraction, and embedding model choice all determine what signal is preserved when documents become retrieval vectors.

10 — A Thought to Sit With

💬 For You to Consider

Feature engineering is where human judgment most directly enters machine learning. When you engineer an "is_night" flag for fraud detection, you're encoding a prior belief about fraudsters' behaviour. When you create "weekend_discharge" for hospital readmissions, you're encoding a theory about the healthcare system. These beliefs get trained into the model and become part of its predictions.

If your feature engineering assumptions encode societal biases — say, flagging "zip_code in high-crime area" in a loan model — the model learns from them as if they were neutral facts. How do you audit whether your feature engineering reflects reality or your assumptions about reality? Who should be in the room when those features are designed?

11 — Coming Up Next

Day 15 — CNNs from Scratch. We close the classical ML phase and enter deep learning. Convolutional Neural Networks are what made computer vision tractable — understanding them from first principles (convolutions, pooling, receptive fields) gives you the mental model for every vision architecture that came after, including the Vision Transformers that now power modern image models.

12 — Go Deeper

🔗 Kaggle Feature Engineering Course
Free · Hands-on notebooks covering mutual information, target encoding, and principal components.

🔗 Feature Engineering Full Course
In this 1-hour tutorial, I'll guide you through the ins and outs of one of the most critical steps in data science and machine learning. Whether you're a beginner or an experienced data scientist, this course will equip you with the tools and techniques you need to effectively transform raw data into features that can be used to train machine learning models.

📄 Why do tree-based models still outperform deep learning on tabular data? — Grinsztajn et al. (2022)
Shows that feature preprocessing and engineering explain much of the tree vs neural network gap on tabular benchmarks. Directly relevant to why everything in this post matters.

🧠 Check Your Understanding
1. Why does feature engineering typically produce larger accuracy gains than switching algorithms — and when does this principle break down?
2. What is target leakage, how does it produce falsely high test accuracy, and how do you detect and prevent it?
3. Why should you use log(x+1) instead of log(x) for skewed features, and which models benefit most from this transformation?
4. When would you use target encoding instead of one-hot encoding for a categorical feature, and what's the risk of naive target encoding?
5. Compare filter, wrapper, and embedded feature selection methods — when would you choose each?
⚡ Quick Recap — Feature Engineering & Selection
  1. Features > algorithms. Switching algorithms: 2–3% gain. Engineering the right features: 10–20% gain. Models can only find patterns the features allow them to see.
  2. Three phases: Create (generate new features), Transform (change distributions/scales), Select (remove what doesn't help).
  3. Temporal decomposition: extract hour, day_of_week, is_weekend, is_night, time_since_last_event from raw timestamps. Each is easier for a model to learn from than a raw UNIX integer.
  4. Log transform: apply log(x+1) to right-skewed features (income, amounts, counts). Compresses extremes, equalises treatment across the distribution. Required for linear models and KNN; optional for trees.
  5. Categorical encoding: OHE for low cardinality (<20 values). Target encoding (with smoothing) for high cardinality. Frequency encoding as a simple baseline. Never OHE on thousands of unique values.
  6. Target encoding formula with smoothing: (n_c × mean_c + m × global_mean) / (n_c + m). Shrinks rare categories toward the global mean. Always fit on training data only.
  7. Target leakage: a feature that's a consequence of the outcome, not a predictor. Test accuracy 97.8% in development → 62.1% in production. Prevention: ask "can I know this at prediction time?"
  8. Aggregation features: per-entity history (mean_amount_last_7d, transaction_count_last_hour) often carries the most predictive power. Captures context that single-row features can't.
  9. Feature selection order: Filter (fast, remove obvious noise) → Embedded (RF/XGBoost importance, L1) → Wrapper (RFE, for interactions when budget allows) → SHAP (gold standard).
  10. Engineering adds stability, not just accuracy. In the before/after example: AUC improved 20.9%, CV std dropped from 0.022 → 0.014. Better features = more consistent signal = less model variance.
🎯 Interview Questions
  1. What is feature engineering and why does it matter more than algorithm choice?
    Feature engineering transforms raw data into representations that expose the underlying signal to a model. It matters more than algorithm choice because algorithms can only find patterns in the features provided — better features add information, while a better algorithm only improves how existing information is processed. On real datasets, feature improvements typically produce 10–20% gains versus 2–3% from switching algorithms.
  2. What is target leakage and how do you prevent it?
    Target leakage is when a feature contains information about the target that wouldn't be available at prediction time — typically a consequence of the outcome rather than a cause. It produces falsely high test accuracy (model learns to rely on the leaky feature) but catastrophic production performance (feature doesn't exist for new examples). Prevention: audit every feature's temporal availability, ask "could I compute this at the exact prediction moment?", use time-based splits for temporal data, and treat suspiciously high accuracy (>95%) as a red flag.
  3. When should you use log transformation and why?
    When a feature has a right-skewed distribution (income, amounts, counts) — skew >1 suggests applying log(x+1). It compresses extreme values and creates a more symmetric distribution that linear models, KNN, and neural networks handle much better. StandardScaler centres and scales but preserves skew — apply log first, then scale. Tree-based models don't technically need it since they split on thresholds (scale-invariant), but it can still speed up training.
  4. What's the difference between filter, wrapper, and embedded feature selection?
    Filter methods score each feature independently (mutual information, correlation) — fast but miss interactions. Wrapper methods evaluate feature subsets by training models (RFE) — captures interactions but expensive. Embedded methods select during model training (L1 zeros out irrelevant weights, tree importances rank features) — best balance of performance and cost, the practical default. SHAP-based selection is the gold standard when interpretability matters.
  5. How would you handle a categorical feature with 10,000 unique values?
    Not OHE — that creates 10,000 sparse columns. Use target encoding (replace each value with mean target for that category, with smoothing for rare values using the formula: (n_c × mean_c + m × global_mean)/(n_c + m)), frequency encoding (replace with occurrence count), or let LightGBM handle it natively. Always fit the encoder on training data only to prevent within-fold leakage.
📎 Attachments & Further Reading

No attachments for this topic yet.

#FeatureEngineering#FeatureSelection#TargetEncoding#TargetLeakage#LogTransform#MutualInformation#RFE#DataPreprocessing#TabularData#MachineLearning#30DaysOfAI#Ramadan2026
← Day 13 XGBoost & LightGBM