Feature Construction
This is where you add what the table doesn't already know. A ratio, a lag, a count in a window — and every one of them computed as of the prediction time.
Why Does This Exist?
Scaling a column has roughly one right answer. Encoding one has about four, and cardinality picks. Construction has no answer until somebody knows something about the problem, which is why it holds the most headroom and the least automation.
A model combines your columns only in the ways its own family permits. A linear model adds them up; a tree cuts them one at a time, along axes. Neither divides two columns by each other, and neither knows that 03:00 on a Tuesday means something different from 13:00 on a Saturday. That knowledge lives in your head, or a domain expert's, or a table nobody joined. Construction writes it into the feature matrix as a column — the only step that adds information rather than rearranging it.
Think of It Like This
Two price tags and a tape measure
Two flats, both listed at £420,000. The sheet in your hand has the price and the floor area, so anything reading it sees two numbers and no argument.
The first flat is 55 square metres: £7,636 per square metre. The second is 92, which works out at £4,565. Same headline price, and one is nearly twice the deal.
Nobody printed £/m² on that sheet. You worked it out, because a price means nothing until you divide it by what you're getting. Then you wrote it down as a number, before anyone reasoned with it. That's the discipline.
How It Actually Works
The moves worth knowing
Nearly every good tabular feature is one of about eight shapes.
- Ratios and rates. Price per square metre, clicks per impression, debt-to-income. Dividing by size is the highest-yield move on tabular data.
- Differences and deltas. Promised delivery date minus actual, current reading minus the rolling mean.
- Date and time parts. Hour, day of week, is-weekend, days since signup, days until the next holiday. A raw timestamp only ever grows, so any split on it means "before or after this date".
- Aggregations over a group. Orders per customer, mean basket value, max session length, distinct devices.
- Lags and windows. Yesterday's value, the 7-day rolling mean, an expanding count since signup. A lag feature is how a model reaches the past.
- Counts in a trailing window. Failed payments in the last 30 days, logins in the last 24 hours. Heavy hitters in fraud and churn.
- Flags for a business rule.
is_first_order,exceeds_credit_limit. Often what a domain expert would name first. - Interaction terms. The product of two columns,
is_premium × discount_pct— an interaction feature is you doing the multiplication a linear model's form forbids.
A ratio costs you one line and costs a tree a staircase of dozens of splits that only approximates it.
As of when
One sentence separates a feature from a bug. Every aggregate and every window has to be computed as of the moment you'd actually have to predict.
Not as of today, and not as of whenever the export ran. As of the prediction timestamp for that row — so a customer's mean_order_value differs between a January row and a June row. If it doesn't, you've built something that cannot exist in production.
Warehouses make the wrong version easier to write: GROUP BY customer_id over the whole table, joined onto a January row, looks tidy. The right version recomputes per row against only the events before that row's timestamp, which is what an as-of join does — see SQL for feature extraction. The split obeys the same clock, so a time dimension means a temporal cut: train, validation and test splits.
Worked example
One customer, five orders, and a churn model scoring them on 1 March 2026: 14 Jan £40, 6 Feb £55, 19 Feb £25, 24 Feb £60, 7 Mar £300. That last order sits on the wrong side of the line. It's in the table you build features from today, and it did not exist on 1 March.
days_since_last_order is 1 March minus 24 February, so 5. orders_in_prior_30d counts back to 30 January, catching 6 Feb, 19 Feb and 24 Feb, so 3. mean_order_value is .
Build that last one over every row instead: . Same column name, more than double the value, and the extra came from an order placed six days after the prediction. Nothing is null, nothing errors, and your validation score goes up.
Show Me the Code
import numpy as np
stamps = np.array(["2026-01-14", "2026-02-06", "2026-02-19", "2026-02-24", "2026-03-07"], dtype="datetime64[D]")amounts = np.array([40.0, 55.0, 25.0, 60.0, 300.0]) # the last order lands AFTER the cut
def features_as_of(t: np.ndarray, amt: np.ndarray, cut: np.datetime64) -> dict[str, float]: known = t < cut # the whole discipline lives on this line days = (cut - t[known]) / np.timedelta64(1, "D") return { "days_since_last": float(days.min()), "orders_prior_30d": float((days <= 30).sum()), "mean_order_value": float(amt[known].mean()), }
feats = features_as_of(stamps, amounts, np.datetime64("2026-03-01"))print(feats) # -> {'days_since_last': 5.0, 'orders_prior_30d': 3.0, 'mean_order_value': 45.0}print(float(amounts.mean())) # -> 96.0 same column over all time, and that is the leakDrop known and the mean goes to 96.0, the count picks up a future order, and days_since_last turns into −6 — the only visible symptom.
Watch Out For
An aggregate that quietly averages the future
mean_order_value computed with one GROUP BY over the whole history, then joined onto every training row. No null, no error, no column name that sounds suspicious.
A row dated 12 January carries a mean built partly from orders in March and November. The model learns that customers with a high lifetime mean don't churn — true and useless, because the high mean is partly caused by not churning. In production the column is computed from data that exists, the distribution moves, and your best feature stops working.
The tell: a feature that ranks first on importance, costs nothing when removed under a random split, and costs a lot under a time-ordered backtest. Believe the backtest, then fix it structurally — every aggregate as an as-of value keyed on the row's timestamp, validated on a temporal split. See data leakage.
Generating a thousand ratios and shipping all of them
Forty numeric columns give 780 pairs, so 2,340 candidates once you divide, multiply and subtract each one. You keep the top 200 by importance, validation lands a shade above baseline, and you ship.
Here's what went wrong. With 2,340 candidates and a few thousand rows, some pure-noise columns score well by luck — that's what selection does once the pool is big enough. They survive, and the model gets worse while its feature list looks richer.
Generating candidates is fine. Selecting from them with the data you report on is not. Nest selection inside cross-validation folds, keep a final untouched split, and expect most to die. Automated feature engineering goes further.
The Quick Version
- Construction is the only step that adds information. Encoding, scaling and selection rearrange what's there.
- Eight shapes cover most of it: ratios, differences, date parts, group aggregates, lags and windows, trailing counts, rule flags, interactions.
- Dividing by size is the highest-yield move on tabular data. A raw timestamp is nearly useless; its parts are strong.
- Neither trees nor linear models divide one column by another.
- Every aggregate and every window is computed as of the row's prediction time.
- The same aggregate over all history is a leak with no null in it: 45.00 becomes 96.00, silently.
- Generating thousands of candidates is fine. Selecting from them with the data you report on is not.
What to Read Next
- Feature Engineering is the four-step frame; this page is step one.
- Exploratory Data Analysis is where the ideas for these columns come from.
- SQL for Feature Extraction is how an as-of aggregate gets written.
- Time Series Data goes deeper on lags, windows and the temporal split.
- Discretization and Binning is the move skipped above: continuous columns into ranges.
- Automated Feature Engineering is what a library generating the catalogue buys you.
- Definitions worth a look: Interaction Feature, Lag Feature, and Data Leakage.