Point-in-time features for transaction data

reference
feature-engineering
evaluation
How to compute customer history features without leaking the future, why random splits lie on this kind of data, and how to check yourself with a rolling backtest.
Author

Alvin Alias

Published

May 31, 2026

last verified 2026-07

Every feature about a customer’s history answers a question with a hidden clock in it. “What is this customer’s return rate?” is not one question; it is a different question at every moment in their life. Compute it over their whole history and you have handed your model information from the future. This reference is the discipline I use to keep the clock honest, learned building a return-risk model on 1,067,371 real UK retail transactions, where getting it wrong inflates every metric you report.

The rule

When you score a transaction, every history feature must be computed from rows strictly before that transaction. Lifetime return rate, return velocity, average days to return, order counts, tenure: all of them get a cutoff at the moment being scored, none of them may see the row itself or anything after it.

The implementation shape that makes this cheap: sort by customer and timestamp, then build history features with expanding or windowed aggregations that shift by one row, so each transaction sees only its own past. In pandas that is a groupby plus shift before any expanding aggregation; in SQL it is a window frame ending at 1 PRECEDING. The moment you see a history feature built without a shift, assume leakage until proven otherwise.

Why the split has to be temporal too

Point-in-time features fix leakage inside rows; the split fixes it between training and evaluation. A random split on transaction data puts January’s rows in training and December’s siblings in test, for the same customers, in both directions of time. The model partly memorizes customers instead of learning behavior, and the test score inflates.

My split: train on December 2009 through June 2011, test on July through December 2011. The model never sees the future of any customer it is judged on. The classifier holds 0.992 ROC-AUC under that discipline, which is exactly why the number is worth quoting; the same architecture under a random split would report something flattering and useless.

The self-check: a rolling backtest

One temporal split is one snapshot. To know whether performance is stable rather than lucky, re-fit and score across rolling monthly windows: train up to month k, score month k+1, slide, repeat. Eighteen windows on this dataset gave a mean top-decile precision of 0.181, and, just as important, a spread you can look at. If a single test-set number is much better than your rolling mean, the snapshot is flattering you.

The checklist

  1. Every history feature has an explicit as-of cutoff, enforced by a shift or window frame, not by intention.
  2. The train/test boundary is a date, not a random seed.
  3. Customers do not appear on both sides of the boundary with the model expected to generalize to them as strangers, unless that is deliberately your setting; know which setting you are in.
  4. A rolling backtest exists, and the headline metric survives it.
  5. Anything the model consumes at serving time is something you could actually have known at that moment: if the feature table is rebuilt nightly, your features are as of last night, and evaluation should mimic that staleness.

Item five is the one that separates paper pipelines from production ones, and it is the first question I would ask anyone presenting a transaction model.

Where this is applied

The live return-risk demo at returns.alvinalias.com scores curated real invoice lines with features built under exactly these rules. The visible ledger shows customer, invoice, stock code, quantity, price, and country. The API also receives the historical context that would have been known at that invoice moment, including the quantity and price z-scores, weekend flag, month-end proximity, and product return rate. The repo with the feature pipeline, temporal split, demo-case builder, and backtest notebook is at github.com/aalias01/retail-returns-intelligence.

Changelog

  • 2026-07: updated the live-demo note after moving the frontend to curated real invoice samples.
  • 2026-05: first published.

Citation

BibTeX citation:
@online{alias2026,
  author = {Alias, Alvin},
  title = {Point-in-Time Features for Transaction Data},
  date = {2026-05-31},
  url = {https://alvinalias.com/notes/posts/point-in-time-features.html},
  langid = {en}
}
For attribution, please cite this work as:
Alias, Alvin. 2026. “Point-in-Time Features for Transaction Data.” May 31, 2026. https://alvinalias.com/notes/posts/point-in-time-features.html.