Skip to content

Walkthrough: zero to a feature matrix

featurizer implements Deep Feature Synthesis (Kanter & Veeramachaneni, IEEE DSAA 2015) for temporal, relational data — compiled to pure PostgreSQL. You describe entities and relationships in YAML; featurizer plans, renders, and (optionally) executes SQL that computes hundreds of point-in-time-correct features. The φ formalism explains why this construction cannot leak.

This walkthrough follows example 01 (an e-commerce scenario: customers and their orders). Clone the repo to run every step verbatim:

Terminal window
git clone https://github.com/ccd-ia/featurizer.git
cd featurizer

featurizer is distributed through GitHub releases — deliberately no PyPI. Pin a tag:

Terminal window
# with uv (recommended)
uv add "featurizer @ git+https://github.com/ccd-ia/featurizer.git@v1.0.0"
# or with pip
pip install "featurizer @ git+https://github.com/ccd-ia/featurizer.git@v1.0.0"

Add the [parquet] extra (featurizer[parquet] @ …) if you want Arrow/Parquet output. Working inside the cloned repo, uv sync is all you need.

Three tables. Two are yours; one small one is featurizer’s contract:

tablerole
customersthe target entity — one row per customer (customer_id, signup_date, country, age)
ordersa child event stream — one row per order (order_id, customer_id, order_date, amount, status)
as_of_datesone as_of_date column: the snapshot dates you want features as of

Entity-relationship diagram: customers 1-to-many orders, both filtered by the as_of_dates spine

The column featurizer cares most about is each entity’s temporal_ix — the event timestamp (order_date for orders). Every generated feature is computed using only rows with temporal_ix <= as_of_date, which is what makes the matrix point-in-time correct: a feature for June 1st cannot see June 2nd, so nothing leaks from the future into training data.

The whole configuration for this scenario (examples/01-basic-aggregations/config.yaml):

target: customers # the entity features are FOR
max_depth: 2 # how far to traverse relationships
intervals: # rolling windows, ISO-8601 durations
- P7D # last 7 days
- P30D # last 30 days
aggregations: # a focused set keeps the tutorial readable —
[count, sum, mean, min, max, stddev, nunique]
transformations:
[identity, abs]
entities:
- alias: customers
id: customer_id
table: customers
temporal_ix: signup_date
variables:
country:
type: categorical
role: categorical # one-hot against a FIXED vocabulary —
vocabulary: [AU, CA, DE, FR, UK, US] # split-blind, fit-free
age:
type: numeric
- alias: orders
id: order_id
table: orders
temporal_ix: order_date
variables:
amount: {type: numeric}
status: {type: categorical}
relationships:
- parent: {entity: customers, key: customer_id}
child: {entity: orders, key: customer_id}

Omit aggregations:/transformations: and featurizer applies its full default set — 67 aggregations × 83 transformers, which is usually far more than a tutorial (or PostgreSQL’s 1664-column row limit) wants.

Terminal window
uv run python examples/01-basic-aggregations/run_example.py --show-sql

or in Python:

from featurizer import Featurizer
f = Featurizer("examples/01-basic-aggregations/config.yaml")
print(f.query) # a single PostgreSQL query — inspect before you run

For this config that is one 56-line, ~19 KB query. Its skeleton is worth reading once, because every featurizer query has this shape:

select aod.as_of_date, t.*
from as_of_dates as aod
cross join lateral (
with
orders_synth as (…), -- child columns, selected
orders_transform as (…), -- transformers applied (abs(amount), …)
orders_aggs_for_customers as (
select customer_id,
count(order_id) as "COUNT(orders.order_id)",
count(order_id) filter (where daterange((aod.as_of_date
- interval 'P7D')::date, aod.as_of_date::date, '[]')
@> order_date::date) as "COUNT(orders.order_id|interval=P7D)",
sum(amount) as "SUM(orders.amount)"
-- … one column per (aggregation × variable × interval)
from orders_transform
where order_date <= aod.as_of_date -- the leakage guard
group by customer_id
),
customers_synth as (…), -- join aggregates onto the target
customers_transform as (…) -- target-level transformers + one-hots
select * from customers_transform
) as t

The cross join lateral re-evaluates the feature CTEs per as-of date, and the where order_date <= aod.as_of_date guard plus interval filter clauses are the point-in-time semantics, visible in plain SQL.

featurizer emits PostgreSQL-dialect SQL, so execution needs a real PostgreSQL. The repo manages a throwaway one:

Terminal window
just db-up # ephemeral postgres:16
uv run python examples/01-basic-aggregations/create_data.py # seed example_01
uv run python examples/01-basic-aggregations/run_example.py --execute

(Any PostgreSQL works: set DATABASE_URL or the PG* variables instead.) In Python, materialization is one call:

df = f.to_dataframe()
df.shape # (1200, 105) — 100 customers × 12 as-of dates, 105 features

The frame is indexed by (as_of_date, customer_id): the same customer appears once per snapshot date, with features computed from only what was knowable at that date.

Column names are self-describing — AGG(entity.column|interval=WINDOW):

COUNT(orders.order_id|interval=P7D) orders in the last 7 days
SUM(orders.amount|interval=P30D) spend in the last 30 days
MEAN(orders.ABS(orders.amount)) mean of a transformed child column
customers.country=US fixed-vocabulary one-hot (0/1)

Two conventions to know:

  • NULL is signal. A customer with no orders in the window gets NULL (not 0) for MEAN(orders.amount|interval=P7D) — “no data” and “zero” are different facts. Opt into imputation explicitly via to_dataframe(impute=True) when you want it.
  • Every column is documented by the feature manifest. f.feature_manifest (also persisted as a <target>_manifest table by to_tables()) carries, per column: the full label, kind (variable / one_hot / derived), lineage (depth, parents, source column, interval) and a generated plain-language description.

List everything the registry offers — 67 aggregations and 83 transformers:

Terminal window
uv run python -m featurizer list-primitives --type agg --category
uv run python -m featurizer list-primitives --type transform --show-sql

Then select per config, as example 01 does. Beyond the basics there are ordered-set aggregations (median, p90…), temporal-gap statistics (gap_mean, burstiness), categorical distributions (entropy, hhi, gini), and sequence features (ngram_2_freq, longest_streak) — the primitives reference lists all 150 with SQL examples.

When a parent record should contribute the most recent state as of each snapshot — a patient’s latest care plan, a school’s history — declare the relationship temporal:

relationships:
- parent: {entity: patients, key: patient_id}
child: {entity: care_plans, key: patient_id}
temporal:
mode: as_of
grace: P21D # optional: only look back this far

featurizer renders a left join lateral … order by … limit 1 that picks the newest child row at or before each as_of_date. Tutorial 02 (healthcare) works through this in depth — examples/02-temporal-joins.

The optional [viz] extra adds FeaturizerViz — distribution, missingness, correlation, embedding, and per-entity temporal diagnostics on the materialized matrix:

from featurizer import FeaturizerViz
viz = FeaturizerViz.from_featurizer(f, df=df)
viz.plot_feature_distributions(kind="violin")
viz.plot_missing_heatmap() # NULL-as-signal, made visible

Real output from a live 177k-row × 272-feature matrix:

Violin plots of top-variance features across entities

Missingness heatmap — NULLs kept as signal