Feature Engineering
A model never sees your data. It only ever sees the table of numbers you hand it, and feature engineering is every decision that turns one into the other.
Why Does This Exist?
Two teams take the same forecasting problem. Same gradient-boosted trees, same rows, same tuning budget. One lands at 12% error and the other at 4%. Nothing about the model differed. One team handed it a raw timestamp column. The other handed it hours_since_previous_order, orders_in_prior_7_days and is_public_holiday.
That gap is feature engineering, and on tabular problems it is routinely wider than the gap between two algorithms.
The reason runs deeper than tidiness. A model is a function fitted to a fixed set of columns. It does not read your database, infer your business, or notice that two of your columns should be divided by each other. It sees a grid: one row per thing you want to predict about, one column per number you decided to show it. That grid is the design matrix, usually written , and it is the model's entire universe. A pattern that is not expressed in cannot be learned from .
So "which model should I use" comes after a bigger question: what does the model get to look at?
The vocabulary, in one paragraph. A feature is one input column: distance_km, is_weekend, word_count. A row is one thing you are predicting about. The target is what you are predicting. Feature engineering is everything that turns your records into that grid: making columns that did not exist, turning non-numbers into numbers, putting numbers on a comparable footing, and deleting what hurts.
Think of It Like This
One display case, and an archive nobody visits
A museum holds 40,000 objects and one display case. The curator picks twelve: a coin, a letter, a photograph, each chosen because together they tell the story of one street. The visitor sees those twelve and nothing else. Whatever the archive holds beyond them might as well not exist for that visit.
The curator is you. The twelve objects are your features. The archive sitting in storage is not evidence that you did the job; an unopened box teaches no visitor anything.
Where it stops: a visitor who suspects something is missing can ask to see the archive. A model cannot ask. It treats your twelve columns as the whole world and answers confidently inside it. That is why a missing column fails silently while a wrong one at least looks odd.
How It Actually Works
Four operations, in this order
Each step assumes the previous one is done.
- Construct. Create columns that do not exist yet. Ratios, differences, date parts, rolling aggregates, lags. This is where domain knowledge enters, and it has the most headroom.
- Encode. Turn non-numbers into numbers. Categories, text, timestamps, identifiers. Every model consumes floats, so something has to do the conversion, and how you do it decides what the model can infer.
- Scale. Put numeric columns on a comparable footing, and reshape distributions that break a model's assumptions.
- Select. Remove columns that add noise, leak, cost money, or duplicate each other. Fewer features often means a better model, which surprises people.
Every one of those four steps learns from data
This matters more than any individual technique, and it is why this page points at leakage before anything else.
A scaler learns a mean and a standard deviation. An imputer learns a median. A one-hot encoder learns the list of categories. A target encoder learns a per-category average of the thing you are trying to predict. None of these are pure functions. Each is fitted, and what it gets fitted to is a choice you make.
Fit them on all your data and you have moved information out of your test set and into your training set. Your validation score improves. Production does not. That is data leakage, and preprocessing causes it far more often than anything exotic does.
The rule is short: fit on training rows, apply everywhere. Every fitted transform, every time, including inside every cross-validation fold.
What the model can and cannot work out for itself
Model families need different amounts of help, and knowing which is which saves a lot of pointless work.
- Linear models cannot discover interactions or curvature at all. If the effect is
price / square_metres, you have to build that column. They also need scaled inputs, because a coefficient's size is entangled with its column's units. - Trees and gradient boosting get monotone transforms for free. A split on
x > 3and a split onlog(x) > 1.1partition the rows identically, so scaling changes nothing. What they cannot do cheaply is arithmetic across columns. A ratio costs you one line and costs them a staircase of dozens of splits, approximated rather than exact. - Neural networks learn features, given enough data and scaled inputs. That is why feature engineering feels dead in vision and text, where the encoder does it, and very much alive on tabular data, where you rarely have the volume to earn it.
Feature engineering did not die with deep learning. On raw modalities it moved into curation and representation, which is what most of the rest of this band is about.
Worked example
Predicting a health outcome from weight_kg and height_m. Take two people: 95kg at 1.62m, and 61kg at 1.80m.
On the raw columns they are awkward to separate, because the heavier person is also the shorter one. Any rule using weight alone gets the light-and-tall case wrong. Body mass index is :
One column, one line of code, and a single threshold now separates them cleanly. A gradient-boosted tree given only weight and height would need many axis-aligned splits to trace that curve, and it would still only approximate it. You handed it the exact answer instead.
That is the shape of the whole discipline: you know something about the world that the loss function cannot discover from the columns in front of it.
Code
Two habits in muscle memory: constructing a column, and fitting a transform on training rows only.
import numpy as np
weight_kg = np.array([95.0, 61.0, 88.0, 54.0])height_m = np.array([1.62, 1.80, 1.91, 1.55])
def build_bmi(weight: np.ndarray, height: np.ndarray) -> np.ndarray: return weight / height**2
def fit_scaler(train: np.ndarray) -> tuple[float, float]: return float(train.mean()), float(train.std())
bmi = build_bmi(weight_kg, height_m)print(np.round(bmi, 1)) # -> [36.2 18.8 24.1 22.5]mean, std = fit_scaler(bmi[:2])print(np.round((bmi - mean) / std, 2)) # -> [ 1. -1. -0.39 -0.58]fit_scaler receives bmi[:2] and nothing else. That deliberate narrowing is the difference between a validation number you can trust and one you cannot, and it is why scikit-learn's Pipeline exists: it makes the narrowing structural instead of something you have to remember.
Watch Out For
Fitting the transform before splitting
Written as scaler.fit_transform(X) followed by train_test_split(X_scaled, y). It reads more tidily than the correct order, which is why it is everywhere.
That mean and standard deviation now carry information from every row, including the ones you are about to hold out. The test set has been standardised using knowledge of itself. Same story for a SimpleImputer fitted on the full frame, for a OneHotEncoder that saw a category present only in test, and much worse for anything derived from the target.
The damage is mild for a scaler on a large dataset and severe for a target encoder on a small one. That is the trap: the mild case teaches you the habit is harmless, then you carry it somewhere it is not. There is no error message either way, just a validation score you believed.
Fix it structurally rather than by being careful. Put every fitted step inside a Pipeline, pass the pipeline to the cross-validator, and each fold refits from scratch. Correct behaviour becomes the default.
Building a feature that will not exist at prediction time
This one produces the most exciting wrong results you will ever see.
You are predicting hospital readmission and the table has discharge_notes_length. You are predicting whether an order arrives late and there is total_delivery_duration. You are predicting churn and someone joined in days_since_account_closed. Every one of those is a real column in a real warehouse, and every one is a fact from after the moment you would actually have to predict.
The symptom is an AUC near 0.99 and a quiet sense that this went unusually well. In production the column arrives null, or worse, it arrives populated with something stale and the model keeps answering confidently.
The check takes a minute per column and nobody who has been burned skips it. Fix a prediction timestamp, then ask of every column whether its value was knowable at that instant. Not "is it in the table". Knowable, then. Anything that fails gets dropped, or recomputed as an as-of value.
The Quick Version
- The model only ever sees the design matrix you built. A pattern not expressed in the columns cannot be learned from them.
- Four operations, in order: construct, encode, scale, select.
- On tabular problems, the gap between good and bad features is usually wider than the gap between two algorithms.
- All four steps are fitted, each learns a statistic, so all four can leak. Fit on training rows, apply everywhere.
- Trees ignore monotone scaling but cannot do arithmetic across columns. Linear models cannot find interactions. Neural nets learn features, given volume.
- One good column replaces dozens of splits: instead of a staircase.
- Fewer features frequently beats more.
- For every column, ask whether its value was knowable at prediction time. If not, it is a leak wearing a useful name.