DatasetsGENERALTASK-SPECIFIC

What Is One Example?

One row is one user, or one transaction, or one user-day, or one query-document pair. Choosing the grain decides the snapshot date, the label window, and what counts as a duplicate.

Target & dataWhat to measureWhat must stay true

The problem, the obvious approach, and why it breaks

Every lesson starts where the work starts: someone has a problem, and the first model that comes to mind looks fine offline.

The question

A row in the training set represents what, as of when, with a label observed over which window — and does the same entity appear more than once?

The problem

A marketplace wants to predict which sellers will stop listing in the next quarter. The seller table has one row per seller. The ML team is asked whether that is the training set.

The obvious approach

One row per seller is the natural table. Take the current features, label from whether they have listed recently, train, and the model will tell you which sellers are at risk.

Why it breaks

A single row per seller means one label per seller and no time axis. Features describe the seller *now*, the label describes the seller *now*, and "did not list recently" is both a feature and the label.

How it breaks — usually after the offline metric looked fine
  • A single row per seller means one label per seller and no time axis. Features describe the seller *now*, the label describes the seller *now*, and "did not list recently" is both a feature and the label.
  • The training set has as many examples as sellers, and sellers who joined last month have the same weight and the same feature windows as sellers with three years of history, which the model cannot know.
  • When the team switches to seller-month rows to get more examples, every seller appears in thirty-six rows, and a random split puts most sellers on both sides of it. The metric measures recognition of sellers, not prediction of churn (Entity Leakage).
ProblemTargetDataRepresentationSplitModelTrainingEvaluationValidationDeploymentInferenceMonitoringDriftRetraining

What is being predicted, and from what data

This domain leads with these two. A target nobody defined precisely is a label nobody can trust, and a dataset nobody can describe is a model nobody can debug.

Target
  • Predict whether a seller who is active on a snapshot date lists nothing in the 90 days after it. The label is the absence of listings in that window, which can only be observed 90 days later.
  • The prediction is consumed by an account-management team that reviews a list once a month, so it must be producible for any seller on any month-start date.
Data
  • A sellers dimension table with one current row per seller; a listings fact table with one row per listing and its creation timestamp; an orders table; a monthly snapshot of seller tier that was only started last year.
  • A seller who has been active for three years has been in many states over that time. The dimension table describes only the latest one.
  • The candidate grains are: one seller (current state), one seller-month (state as of each month start), or one listing.

How it actually works

Precisely enough to predict its behaviour — not a framework API.

  • The grain is the unit that one row describes. It fixes three things at once: which entity the features summarise, the snapshot moment the features are computed as of, and the window after that moment in which the label is observed.
  • Entity-over-time problems almost always want a grain of entity-period — user-day, seller-month, machine-hour — because the prediction is made repeatedly for the same entity at different moments, and the training set should look like that stream of decisions.
  • Once the same entity appears in many rows, the rows are not independent. Two rows for the same seller a month apart share most of their history. The split has to respect that, or the evaluation measures the wrong thing (Group Split).

Three candidate grains for one problem

The same seller-churn problem can be posed at three grains, and each one answers a different question. The comparison is not about which is more accurate; it is about which one matches the decision the account-management team makes, and which one can be labelled honestly.

The table also shows the duplication rule and the split each grain requires, because those are consequences of the grain, not separate choices.

GrainOne row isSnapshotLabel windowDuplicatesSplit must
SellerA seller's current stateBuild time, implicitNone — label is also current stateNoneRandom — but there is nothing to predict
Seller-monthA seller as of a month startExplicit, one per month90 days after snapshotSame seller in many monthsRespect time and keep each seller on one side
ListingOne listing at creationListing timestampSold / expired within N daysSame seller across listingsGroup by seller; time-order if behaviour drifts

Building user-day rows with a label window

The shape of a correct entity-period dataset is the same everywhere: a calendar of snapshot dates, cross-joined with the entities active on each date, features aggregated from events before the date, and a label from events inside a window after it. The date is the only parameter.

The SQL below builds user-day rows for a 30-day churn label. Notice that the label for the most recent 30 days of snapshots cannot exist, and the final WHERE throws those rows away rather than letting them become negatives.

User-day snapshots with a forward label window
1WITH days AS (
2 SELECT d::date AS snapshot_date
3 FROM generate_series('2025-01-01', '2026-05-01', interval '1 day') AS d
4),
5active AS ( -- who was an active subscriber on each day
6 SELECT s.user_id, d.snapshot_date
7 FROM subscriptions s JOIN days d
8 ON s.started_at < d.snapshot_date
9 AND (s.ended_at IS NULL OR s.ended_at >= d.snapshot_date)
10),
11features AS ( -- events strictly before the snapshot
12 SELECT a.user_id, a.snapshot_date,
13 COUNT(e.event_id) FILTER (WHERE e.ts >= a.snapshot_date - interval '30 days') AS events_30d,
14 MAX(e.ts) AS last_event_ts
15 FROM active a LEFT JOIN events e
16 ON e.user_id = a.user_id AND e.ts < a.snapshot_date
17 GROUP BY 1, 2
18),
19labels AS ( -- cancellation strictly inside the 30 days after
20 SELECT a.user_id, a.snapshot_date,
21 BOOL_OR(c.cancelled_at >= a.snapshot_date
22 AND c.cancelled_at < a.snapshot_date + interval '30 days') AS churned_30d
23 FROM active a LEFT JOIN cancellations c ON c.user_id = a.user_id
24 GROUP BY 1, 2
25)
26SELECT f.*, l.churned_30d
27FROM features f JOIN labels l USING (user_id, snapshot_date)
28WHERE f.snapshot_date + interval '30 days' <= '2026-05-01'; -- window has closed

Everything hangs on e.ts < a.snapshot_date and the closed-window filter at the end. Remove either and the dataset looks identical, the metric improves, and the model stops describing the future.

The grain is a serving-time promise

Choosing seller-month rows with month-start snapshots is a statement about production: the model will be asked about a seller as of a month start, with features computed over the same trailing windows. If the account team starts scoring sellers mid-month with features up to yesterday, the inputs come from a different distribution than the one the weights encode.

That is why the grain belongs in the model's documentation and, ideally, in a serving-side assertion, not only in a SQL file that someone will lose.

must stay trueSame grain at serving time

A production prediction is made for one entity as of an explicit snapshot moment, with feature windows anchored to that moment, exactly as the training rows were built.

holds when The scoring job passes the snapshot date to the same feature computation the pipeline used, and the label window that will later judge the prediction matches the training label window.

breaks when Scoring moves from month-start batch to on-demand requests anchored to wall-clock time; a "last 30 days" feature is computed by a service with a different anchor; the business changes what "churned" means from 90 to 60 days without retraining.

how you would know A feature-distribution comparison between scored requests and training rows on the first rollout, and a check that the outcome-measurement window in the monitoring job equals the training label window.

respond Align the anchor or the window on the serving side first. Retrain only if the decision itself changed grain.

How to build it

Most important first.

  • Choose the grain from the decision: if the product asks "should we act on this seller this month", a row is a seller-month with a snapshot at month start.
  • Build every row from a snapshot date parameter: features from events strictly before it, labels from events in a fixed window strictly after it. Write the SQL so the date is the only input.
  • Decide the duplication rule explicitly — one row per seller per month, and a group-aware split so a seller never appears in both train and test.
  • Reserve the most recent label window's worth of time: rows whose label window has not closed yet have no label and are not training rows, however tempting they are.
  • Document the grain in the dataset's metadata so a later reader knows what a row is without reading the pipeline (Grain: What Does One Row Represent? in Data Engineering says the same thing about warehouses).

What to measure

Which number actually maps to the decision — and which numbers look relevant and are not.

  • Number of distinct entities and rows per entity. A dataset with a million rows and ten thousand sellers has ten thousand independent-ish things in it, and the metric's uncertainty follows the smaller number (Metric Uncertainty).
  • The maximum feature timestamp per row against its snapshot date — it must never exceed it — and the minimum label event timestamp against the snapshot date, which must always exceed it.
  • Label prevalence per snapshot month. If it jumps in the last month, that month's label window has not closed and those rows are mislabelled negatives.

What must stay true after deployment

The field this whole domain exists for. A model is a set of assumptions with weights attached; these are the ones a monitor or a test should be checking.

Assumptions
  • At serving time, a prediction is made for the same grain the model was trained on — one seller as of a month-start snapshot — with features computed over the same window relative to that moment.
  • The label window is the same length in training and in how the outcome is later measured, so "churned" means the same thing to the model and to the account-management team.
  • The number of rows per entity in training does not systematically differ by outcome — long-lived sellers are not over-represented merely because they have more months.
How to verify — offline, online, and over time
  • Offline: for a sample of rows, delete all source events after the snapshot date and recompute the features; any change is a leak in the grain definition.
  • Online: assert that each production scoring request carries a snapshot date and that the feature service computes windows relative to it, not to wall-clock time.
  • Over time: track rows-per-entity and label prevalence by snapshot month on every rebuild; a shift means the grain or the label window silently changed.

What can go wrong

Failure modes in production
  • The snapshot SQL is correct, but a feature joins the sellers dimension, which holds current values, so tier and country describe today for every past row (Point-in-Time Correctness, SCD Type 2 in Practice).
  • The grain is seller-month, but the serving path scores sellers mid-month using features up to today; the model was trained on month-start features and sees a distribution it never learned.
  • Rows with an unclosed label window are included as negatives, and the model learns that the most recent month is unusually safe.
What the recommended approach costs
  • Entity-period grains multiply the row count and the compute for feature aggregation, and require a group-aware split that is more work than a random one.
  • Waiting for the label window to close means the training set always lags production by the window length, which is the price of a label that means what it says.
  • Choosing one grain forecloses others: a seller-month model cannot say which listing is at risk, and a listing-grain model cannot easily say which seller to call.
Misreads
  • "More rows is more data." Thirty-six rows of the same seller are thirty-six views of one entity. The effective sample size is closer to the number of sellers than to the number of rows.
  • "We have the current state, so we should predict from it." The current state has no future to label from. Every training row needs a past to compute features from and a future to observe the label in.
  • "Just take the last month as the test set." That is the right instinct for time, but it does not stop a seller from appearing in both halves; time and entity are two separate split questions.

Where this applies

ML advice is stated as universal far more often than it is. These labels say what each claim is specific to — and where CONTESTED appears, the note gives the strongest form of the opposing view.

  • GENERALEvery supervised dataset has a grain, a moment the features describe and a window the label is observed in, whether or not anyone wrote them down; the question applies to images and text as much as to tables.
  • TASK-SPECIFICThe snapshot-and-window form is for predicting an entity's future; for a static classification task such as labelling an image, the grain is the item and the label window collapses to the moment of annotation, and the duplication question becomes near-duplicate images rather than repeated entities.

Where the depth lives

This domain teaches the model and hands the rest off by name.