Skip to content
AI360Xpert
Data Concepts

Automated Feature Engineering

A generator is good at enumeration and bad at judgement. It will invent a thousand columns, and without a cutoff time most of them quietly know the future.

Stacking primitives up a chain of three tables turns four numeric columns into 21, then 126, then 651 candidates, and every aggregate stops at the cutoff line
Stacking primitives up a chain of three tables turns four numeric columns into 21, then 126, then 651 candidates, and every aggregate stops at the cutoff line

Why Does This Exist?

Nine related tables in a warehouse. Writing features by hand costs a day per idea, most of it spent working out which join gives the right grain. A library reads your schema, walks the relationships, and hands back 900 columns before lunch.

Real win, narrower than it looks. Automation is good at enumeration and bad at judgement, so it wins where the candidate space is large and mechanical — many tables, many plausible aggregates per relationship — and loses where the useful column needs knowing what the business does. No generator was producing days_until_contract_renewal unless told renewal dates exist and matter.

Let the machine enumerate. Keep the judgement, especially about time.

Think of It Like This

A machine that writes every recipe your kitchen allows

Point a machine at your kitchen and ask for dinner. It reads the shelves and returns 900 combinations in a second, all correct: every one uses ingredients you own and steps you can perform.

You'll throw most away. The machine has no idea nobody eats mackerel with custard, and no idea Tuesday is the night your flatmate cooks. It enumerated. It didn't judge.

Two things save the exercise. One is a taster who says no a lot. The other is a rule about the fridge: anything a recipe uses has to be there when you start cooking, not arriving Thursday. Break it and every dish looks fine on paper and can't be made tonight.

How It Actually Works

Deep feature synthesis

You hand the tool a set of tables and the keys joining them, and it stacks two kinds of primitive. Aggregation primitives cross a one-to-many relationship and collapse many child rows into one number per parent: count, sum, mean, max, std, num_unique, trend. Transform primitives stay inside a row and rewrite a value: day-of-week, time-since, percentile, cumulative sum.

Depth is how many times it will stack them. Depth 1 gives "number of orders per customer". Depth 2 gives "mean of the number of items per order, per customer" — nonsense until you notice it's basket size, and no analyst was going to name it. That composition is the value proposition.

Four ways it goes wrong

Leakage at scale, which matters more than everything else here. An aggregate has no notion of prediction time unless you give it one per row, so the default output covers every child row that exists today. A MEAN(orders.amount) on a January training row quietly includes November. Nothing is null, nothing errors, and the column ranks first on importance (data leakage).

Overfitting the selection. A thousand candidates scored against one validation set will find winners in pure noise, and the bigger the pool, the more certain that becomes (multiple comparisons).

Columns nobody can explain. STD(orders.MEAN(items.unit_price)) may well predict churn. Try defending it to a regulator, or to the analyst judging a drift alert.

Serving cost. Every column you keep must be recomputable per request, at prediction latency, from data that exists then — a depth-3 aggregate is a multi-table join under a latency budget (online and offline skew).

The automation that actually won

On unstructured input, what replaced hand-built features wasn't a primitive stacker. It was representation learning: a pretrained encoder turning text, an image or audio into a dense vector, learned from far more data than you have. That's automated feature engineering that works, and why this page hands off to vector embeddings. Structured tables never got an equivalent.

Worked example

Three tables in a chain: customers, orders, order_items, four numeric columns each. Six aggregation primitives, five needing a column and one — count — that doesn't.

One hop, order_items into orders: 5×4+1=215 \times 4 + 1 = 21 new columns. Two hops: orders now carries its own 4 plus those 21, so 25 things to aggregate: 5×25+1=1265 \times 25 + 1 = 126 columns land on a customer row. Three hops: 5×(4+126)+1=6515 \times (4 + 126) + 1 = 651.

Two more primitives makes depth 3 into 1,653, and a second child relationship roughly doubles it again. Almost all noise, which is the point: a generator is half a system, and the other half is feature selection with an evaluation you trust.

Show Me the Code

import numpy as np
AGGS = 6   # count, sum, mean, max, std, num_uniqueOWN = 4    # numeric columns in every table

def candidates(depth: int, aggs: int = AGGS, own: int = OWN) -> np.ndarray:    """Columns landing on the parent row after each extra hop up a chain of tables."""    counts = np.zeros(depth, dtype=int)    available = own    for level in range(depth):        counts[level] = (aggs - 1) * available + 1  # count needs no column, the five others do        available = own + counts[level]    return counts
print(candidates(3))          # -> [ 21 126 651]print(candidates(3, aggs=8))  # -> [  29  232 1653]

available = own + counts[level] is where it runs away: last level's output is this level's input, so growth is multiplicative in depth.

Watch Out For

Running a generator with no per-row cutoff time

The default call takes your tables and returns a matrix. Right shape, right join keys, validation goes up. Ship it and the model is worthless.

Every aggregate covered the child rows present when the job ran, so a training row dated 12 January carries SUM(orders.amount) including March and November. You didn't build a feature set, you built a summary of the outcome.

Every mature tool takes a cutoff time per row for this reason, and passing one isn't optional. The tell: importance ranks the aggregate first, removing it costs nothing under a random split, and costs a lot under a time-ordered backtest. Believe the backtest. Then check the cutoff is exclusive on the right, because an off-by-one admitting prediction-day rows leaks the most convincing amount.

Shipping 900 generated columns because validation improved

Validation went from 0.81 to 0.84 with the full set, so the full set ships. Two bills arrive later.

The first is statistical. With 900 candidates scored against one split, some noise columns beat the real ones by luck, so the 0.84 is partly luck. Nest selection inside the folds and hold a final split you never touched (cross-validation schemes). Expect the gain to shrink.

The second is operational, and it's the one that ends projects. Nine hundred columns is 900 expressions to recompute per request, from tables joinable at serving time, at the same grain, under the same cutoff logic. Nobody monitors 900 distributions, so drift lands silently. Generate widely, keep a number you can name, and require a serving path before anything counts.

The Quick Version

  • Automation is good at enumeration and bad at judgement: it wins on many related tables, loses where the column needs domain knowledge.
  • Deep feature synthesis stacks aggregation primitives across relationships and transform primitives within a row, to a chosen depth. At depth 2 that gives "mean of the number of items per order, per customer".
  • The arithmetic: 3 tables, 4 numeric columns, 6 primitives gives 21 candidates at one hop, 126 at two, 651 at three. Almost all noise, so a generator is half a system and the other half is selection.
  • Leakage at scale matters most: without a per-row cutoff, every aggregate reads the whole child table, future included.
  • The other three: overfitting the selection, columns nobody can explain, serving cost at prediction latency.
  • On unstructured data the automation that won is representation learning, not primitive stacking.

Related concepts