Prepare observations

Assemble one record per decision with the history known at that time and labels from the window after it.

relflow reads one observation per row and learns its encodings from those rows. What it does not do is decide which records belong together, when the decision happens, or what the labels mean. This page defines those rules for the payment examples once. Later pages link here instead of repeating them.

One row is one decision

An observation is everything known about one decision at one moment. For an account review, that is the account’s transactions through the latest payment, in order, with the labels the review team learned later. Keep an application identifier beside the record so results can be joined back; it is not a model field.

payment_id: payment-42
trxns:
  - booked_at: "2026-05-21T10:00:00"
    amount: 32.00
    merchant: Station Cafe
    disputed: false
  - booked_at: "2026-05-27T18:15:00"
    amount: 56.20
    merchant: North Market
    disputed: true
  - booked_at: "2026-05-28T14:30:00"
    amount: 84.50
    merchant: North Market
    disputed: false

The latest payment is the final item of trxns. It needs no separate root fields: the branch reads the booking time, amount, and merchant of every transaction, including that one.

Choose a decision time

Every observation has a cutoff, the moment the decision is made. Here it is the account review immediately after the latest booking on May 28. The inputs may contain only what was known then. Each label answers one question about the following window: was a new dispute reported against this transaction in the 30 days after the cutoff?

A timeline with three transactions on May 21, May 27, and May 28 before a dashed review cutoff at 14:30 on May 28. A shaded label window covers the next 30 days. An event on June 3 is excluded from the inputs, and a dispute reported inside the window defines a label.

A timeline with three transactions on May 21, May 27, and May 28 before a dashed review cutoff at 14:30 on May 28. A shaded label window covers the next 30 days. An event on June 3 is excluded from the inputs, and a dispute reported inside the window defines a label.

Figure 1: Transactions booked before the review cutoff are inputs. Labels come from the 30 days after it. Events after the cutoff never enter the inputs.

Three consequences follow:

  • Every transaction is a target. All three transactions are known at the cutoff, so each one gets a label for the same window, and the full sequence is valid context for each prediction. These are not forecasts made separately at each transaction’s original booking time.
  • Labels arrive later. Attach disputed only after the window has closed. A payment still waiting for its outcome is not a negative example.
  • Prediction records look the same without labels. A request contains the same trxns records, cut at its own decision time, with disputed omitted.

Order the history

Supply trxns oldest to newest. The model reads order from list position, and overflow="tail" on a bounded branch keeps the most recent records, including the latest payment, when a history is longer than its cap. relflow does not sort, deduplicate, or join; do that before the row exists.

booked_at becomes recurring calendar positions through DateParts: day of month, day of week, and hour of day. Those cycles say when something happened, not how long ago. When the pace of activity matters, derive the gap between transactions in a preprocessor and read it as a Number.

Split by time

Train on earlier decision windows and validate on later ones. Build each observation from information available at its own cutoff, and keep later outcomes out of its inputs. The same account can appear at several decision times; keep their label windows from overlapping the validation period. relflow does not enforce these rules. Evaluation describes the comparisons that depend on them.

Bind the record to the schema

With the record prepared, binding is a matter of names. The parent keyword names each child, and each child reads the same-named value in its parent:

import relflow as rf

model = rf.Model.xs(
    trxns=rf.Branch(
        length=32,
        overflow="tail",
        booked_at=rf.DateParts(
            dateparts=["day_of_month", "day_of_week", "hour_of_day"],
        ),
        amount=rf.Number,
        merchant=rf.Category,
        disputed=rf.Boolean(mask=True),
    ),
)

trxns reads the list; its children read each transaction’s fields. Columns the schema does not name, such as payment_id, are ignored as inputs and can be retained in the output. A required field missing from the Arrow schema is a binding error; an explicit null is a value state governed by the field’s nullable option.

When names or nesting differ from the schema, read them in place with a query. When the source needs renaming, derived values, filtering, joins, or reshaping, attach a preprocessor to the data module. Its output is what the model binds.

Next, train and save the model on files prepared this way.