Skip to main content
ByPrior Labs
Most real data does not live in one table. A shop has sellers, customers, orders, order items, products, and reviews, wired together by foreign keys — and the interesting questions run across those tables and forward in time: which of the sellers active today will stop selling in the next 30 days? Nothing in that database answers it directly. There is no churn column, and no feature matrix. Supervised learning needs both, and normally that means weeks of bespoke joins and window aggregations. TabPFN-Rel lets you declare them instead. You write two small YAML files — one describing your tables, one asking your question as a SQL query — and the harness builds the labels, aggregates each entity’s history across the foreign-key graph with Deep Feature Synthesis (DFS), keeps the future out of the features, and runs TabPFN on the result. This cookbook does that end to end on the raw Olist Brazilian E-Commerce CSVs, in a few minutes on CPU.
📃 Read the full report on RelArena, TabPFN-Rel and RPI here

The shape of the problem

One idea does most of the work here: the anchor time. A prediction is always made as of some moment t. The model sees the database truncated at t, and the label comes from the window after it.
That is what makes it a forecast rather than a lookup, and the harness enforces it — you cannot accidentally show the model a review written after t. Two consequences worth holding onto. First, there are many anchors: the same question is asked at 30-day intervals stepping back through history, so one seller contributes many training rows. Second, the splits are cuts in time, not random samples — you pick the two dates where train ends and test begins. RelArena follows the RelBench protocol for final prediction: predict() defaults to the task’s test_timestamp, and the feature database remains frozen at that cutoff. Configuring a later at_timestamp changes the prediction anchor and emits a warning, but it does not expose database rows written after test_timestamp.

Setup

RelArena—the package that ships TabPFN-Rel—is available as an alpha release on PyPI. The cell below installs it directly from the standard PyPI index. No GitHub token or local checkout is required. The tabpfn-rel-api extra is the no-GPU stack: featurization runs locally, while model fitting and prediction use the hosted TabPFN API.

The TabPFN token

Because the fit runs on the hosted API, you need a Prior Labs token. Grab one from ux.priorlabs.ai and add it as the Colab secret TABPFN_TOKEN (You can add a secret via the key icon on the right sidebar). Locally you can skip this — tabpfn-client opens a browser on the first fit and caches the token. This cell runs before every other import because tabpfn-client reads the token once, at import time.

The database

Olist is a real Brazilian marketplace; the public dataset covers roughly 100k orders from 2016–2018 and downloads anonymously, no Kaggle account needed. Seven of its CSVs matter here, and the foreign keys between them are what TabPFN-Rel walks:
Read it from the right: a seller has many order items; each belongs to an order, which belongs to a customer and may have a review, and points at a product with a category. Those chains are the paths DFS aggregates along.

Giving order_items a real event time

Event tables need a trustworthy timestamp, because that is the column the harness censors on. orders and order_reviews have one; order_items only has a shipping deadline. The cell below joins each item’s purchase time on from its order. This is the only table that needs any code — everything else is read straight from the raw CSVs.

The Database Schema

Now the first YAML: where each table lives, what its key is, which column carries its event time, and which foreign keys point where. That is the graph DFS will walk. The one judgement call is columns, an allow-list of the fields that become features. orders carries delivery dates and a status that are only filled in after the purchase — keeping them would hand the model facts that do not exist at prediction time, so they are left out. Time censoring is automatic; deciding which columns were backfilled is on you.

The Task File

The second YAML is the question. It names the entity you predict over, the target, the 30-day window, and the two dates that cut train from validation from test — then computes the label in SQL. test_timestamp is also the default prediction anchor. An optional later at_timestamp changes the anchor but does not expose database rows beyond test_timestamp. That query is the heart of it, and it reads as two windows around each anchor time: the WHERE EXISTS at the bottom looks backwards to keep only sellers who were active in the previous 30 days, and the NOT EXISTS at the top looks forwards and fires when no order arrives in the next 30. Churn only means something for sellers who had started, which is why both are needed. The rules for writing your own — which columns to emit, what timestamp_df is, how {timedelta} gets substituted — are in the predictive-task guide.

Check the labels before spending compute

Loading the spec validates both files, and constructing a PredictiveQuery runs your SQL once per split. The table that comes out is your supervised dataset, so it is worth a look before featurizing anything — if it is wrong, nothing downstream will save it.
Two things to notice: 11k training rows out of only ~3k sellers, because each seller recurs at many anchor times; and a churn rate around a quarter to a third, which is a healthy balance to model. A label that came out 99% one class would need a different approach. The integer seller_id is internal — RelArena reindexes keys, and predict maps them back.

Fit and predict

fit featurizes and trains; predict then scores the task’s configured prediction anchor. Because this task does not set at_timestamp, the anchor defaults to test_timestamp. The feature database is frozen at that cutoff, so rows from the forward 30-day label window are not visible to the model. Later, compute_test_labels() uses the complete source database only to evaluate the historical predictions. Two things to know. The model is a string, not part of the spec, so swapping tabpfn-rel-client for constant-global or constant-per-entity runs a different model against an identical task. tabpfn-rel-local provides the local GPU-backed variant when its corresponding package extra is installed. And n_trials=0 skips hyperparameter tuning, which is the right default for a first run against a hosted API. Most of the wall-clock time is spent on DFS rather than model inference—expect a couple of minutes. Stronger CPUs with more CPU cores can speed up this step significantly. If you are interested in caching the features created during this step (and speeding up re-runs), see the optional caching example below.

Optional: Caching to reuse relational features

Depending on the strength of your CPU, TabPFN-Rel can spend a significant part of its runtime constructing relational features with deep feature synthesis (DFS), rather than running the model. Pass a local cache_dir to fit() to persist those features. Missing entries are computed and stored as the query runs; later identical operations reuse them. Nothing in the cache is uploaded. This can be helpful in many scenarios, like:
  • You might want to play with different levels of tuning, e.g. n_trials=1 and n_trials=3. Caching is independent of tuning so you can reuse the relational features. Note that for n_trials=0 we don’t need to compute the cache for the inner split we tune, so going from n_trials=0 to n_trials>0 will still require some extra cache computation.
  • If you use the local version of tabpfn-rel, then you might want to run the cache pre-computation on a beefy CPU node for large datasets, while running the actual predictions of tabpfn-rel using a good GPU.
data_version identifies the exact database snapshot behind the cache keys. Keep it stable while the CSV contents are unchanged, and bump it whenever the row content changes. In Colab, ./relarena-cache lasts for the current runtime. To retain it across runtime restarts, use a directory on a mounted Google Drive. For more details, see the predictive-interface caching documentation. The caching API is optional and experimental. First, configure the cache and fit the model. fit() computes and stores the relational features it needs for training.
Assuming we ran the above cell for the first time, we can see that the second fit() is a lot faster even though it runs 3x the tuning than the first cell. Caching also works for predict(), as you can see in the cell below: Note: The speed-up is only calculated correctly the first time you run this cell. Delete/Rename/Change the cache directory in the cell above to rerun the measurements.

Does it beat simple baselines?

The Olist database contains the complete 30-day label window after its historical test timestamp, 2018-06-15. We can therefore materialize the held-out churn outcomes and compare the existing TabPFN-Rel predictions with two learning-free baselines on the same sellers.
  • constant-global predicts the global churn rate observed during training.
  • constant-per-entity uses each seller’s own history when available and falls back to the global rate otherwise.
preds and the fitted TabPFN-Rel query pq already exist from the previous section.
predict() scores every seller by default, whereas the churn task evaluates only sellers active during the preceding 30 days. The prediction frames are therefore larger than test_labels. Joining from the labels selects the relevant cohort and checks that every held-out seller received a prediction.
We observe that TabPFN-Rel improves significantly over the baseline. Depending on the dataset, tuning (e.g. n_trials=3) can give another nice boost.

Where to go next

Your own database. None of this is Olist-specific — a schema file and one label query is the whole setup. The two things to get right are a real event-time column on every event table, and a columns allow-list that excludes anything backfilled after the fact. If a fit hangs, the fan-out is too wide: DFS cost grows with how many tables an entity links out to. Prune the schema or use a lower featurization depth. Regression works identically — set task_type: regression and return a numeric target (“how much will this seller sell”) instead of a 0/1 one. Hint: We designed the predictive interface with your favorite coding AI agent in mind. Point it at the resources above and it should be able to help formulate your first predictive task.