Add transaction history
The first model reads one flat payment. Now move that payment into a trxns list and prepend earlier transactions from the same account. Each listed transaction has its own dispute target.
The business question becomes more specific: which transactions in the account will receive a dispute report after review? Visits to the same merchant, changes in amount, and booking-time patterns can provide context. An event history preserves those observations so the model can learn their relationships. Compare it with your existing aggregate features to find out whether that detail improves dispute prediction.
The latest payment appears once, at the end of the same list as the earlier events. Its amount and merchant need no separate root fields.
Add a collection to each record
Your application supplies trxns in oldest-to-newest order, ending at the payment being assessed:
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: falseEvery transaction shares one review cutoff, immediately after the last listed booking on May 28, and disputed records whether a new dispute was reported in the 30 days after it. The whole sequence is therefore valid context for every prediction. Prepare observations defines the cutoff, the label window, and the ordering rules in full.
List order says which transaction came first. booked_at supplies calendar parts that can reveal recurring patterns, not the elapsed time between transactions.
A fixed feature table might retain total spend and transaction count. Here, the amount and merchant stay paired within each event: a cafe payment and a visit to North Market remain distinguishable. This gives the model evidence for learning account behavior without deciding every summary in advance.
Describe the collection with a branch
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),
),
)Branch describes a collection of records with the same fields. This branch keeps at most 32 transactions; overflow="tail" retains the last 32, including the latest payment. mask=True hides every transaction’s dispute label and trains the model to predict it from the shared context.
Use the same training steps
Train this new model with the history present in each record. Your application supplies Parquet files with the nested structure shown above:
data = rf.ArrowDataModule(
train="payments/train.parquet",
validate="payments/validation.parquet",
)
model.fit(data, batch_size=128, max_epochs=20)Prediction files keep the same transaction list and omit disputed from each record:
requests = rf.ArrowDataModule(
predict="payments/requests.parquet",
)
predictions = model.predict(datamodule=requests, batch_size=256, devices=1)predictions.trxns holds one result per retained transaction; Read predictions shows how to extract each probability.
Next, Featureless models names what these two models did without a feature pipeline. After that, Prepare observations defines the data contract these files follow, and Preprocessing shows how to build the histories from transaction tables.