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:
git clone https://github.com/ccd-ia/featurizer.gitcd featurizer1. Install
Section titled “1. Install”featurizer is distributed through GitHub releases — deliberately no PyPI. Pin a tag:
# with uv (recommended)uv add "featurizer @ git+https://github.com/ccd-ia/featurizer.git@v1.0.0"
# or with pippip 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.
2. The data
Section titled “2. The data”Three tables. Two are yours; one small one is featurizer’s contract:
| table | role |
|---|---|
customers | the target entity — one row per customer (customer_id, signup_date, country, age) |
orders | a child event stream — one row per order (order_id, customer_id, order_date, amount, status) |
as_of_dates | one as_of_date column: the snapshot dates you want features as of |
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.
3. One YAML file
Section titled “3. One YAML file”The whole configuration for this scenario
(examples/01-basic-aggregations/config.yaml):
target: customers # the entity features are FORmax_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.
4. Render the SQL — no database needed
Section titled “4. Render the SQL — no database needed”uv run python examples/01-basic-aggregations/run_example.py --show-sqlor in Python:
from featurizer import Featurizer
f = Featurizer("examples/01-basic-aggregations/config.yaml")print(f.query) # a single PostgreSQL query — inspect before you runFor 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 aodcross 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 tThe 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.
5. Execute against PostgreSQL
Section titled “5. Execute against PostgreSQL”featurizer emits PostgreSQL-dialect SQL, so execution needs a real PostgreSQL. The repo manages a throwaway one:
just db-up # ephemeral postgres:16uv run python examples/01-basic-aggregations/create_data.py # seed example_01uv 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 featuresThe 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.
6. Read your features
Section titled “6. Read your features”Column names are self-describing — AGG(entity.column|interval=WINDOW):
COUNT(orders.order_id|interval=P7D) orders in the last 7 daysSUM(orders.amount|interval=P30D) spend in the last 30 daysMEAN(orders.ABS(orders.amount)) mean of a transformed child columncustomers.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) forMEAN(orders.amount|interval=P7D)— “no data” and “zero” are different facts. Opt into imputation explicitly viato_dataframe(impute=True)when you want it. - Every column is documented by the feature manifest.
f.feature_manifest(also persisted as a<target>_manifesttable byto_tables()) carries, per column: the full label, kind (variable / one_hot / derived), lineage (depth, parents, source column, interval) and a generated plain-language description.
7. Choose your primitives
Section titled “7. Choose your primitives”List everything the registry offers — 67 aggregations and 83 transformers:
uv run python -m featurizer list-primitives --type agg --categoryuv run python -m featurizer list-primitives --type transform --show-sqlThen 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.
8. Point-in-time joins (as-of)
Section titled “8. Point-in-time joins (as-of)”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 farfeaturizer 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.
9. Visualize the matrix
Section titled “9. Visualize the matrix”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 visibleReal output from a live 177k-row × 272-feature matrix:


10. Where next
Section titled “10. Where next”- The tutorials: five executed notebooks, from basic aggregations to custom primitives — examples/ (rendered versions join this site shortly).
- The theory: φ — the formalism behind feature creation, with an interactive explorable.
- References: the full
primitive registry and the complete
config.yamlschema. - Proof it holds up: every release is validated against three live databases — the v1.0.0 reports.