Each training row is an eligible opportunity at a decision time, including those assigned no offer. A table of redeemed offers alone cannot estimate the comparison above. For each opportunity, join all relevant purchases, returns and costs over the fixed outcome window. Keep delivered, seen, clicked and redeemed as separate events.
Use at least these tables. The schema is my suggestion, not a library requirement.
| Table | Required fields | Why it exists |
|---|---|---|
customer_features | customer_id, feature_time, available_at, feature values, feature version | Recreate what the service could know at the decision |
offer_catalog | offer_id, version, active interval, eligibility, channel, cost rule, margin rule, stock dependency | Reproduce the feasible action set and accounting |
decisions | decision_id, customer/account ID, decision_at, eligible action IDs, selected action, policy/model version, experiment ID and arm, action probabilities, feature snapshot reference, budget reservation, fallback reason | Audit and evaluate the policy |
delivery_events | decision_id, event type, event time, status, position if applicable | Distinguish assignment from actual delivery and exposure |
transactions | customer/account ID, transaction time, recorded time, gross revenue, product cost, incentive cost, return/refund, channel | Compute comparable outcomes for every arm |
outcomes | decision_id, horizon, cutoff, label maturity, label availability, outcome coverage/status, net contribution, contact cost, conversion, guardrails | Store a reproducible training label |
For a feature window ending at decision_at, use only events whose event time and availability time
are both at or before the decision. A purchase dated Monday but first loaded on Wednesday was
unavailable to Tuesday's service. Joins to today's customer profile, today's stock or a backfilled
lifetime-spend field can all leak future information. Fit encoders, imputers and other learned
transforms on training data only.
Useful inputs include purchase recency and frequency, past contribution, category affinity, tenure, earlier eligible contacts, time since the last offer, pre-decision complaints, channel preference, offer/product attributes, calendar context and available stock. Add missingness and history length so thin histories are visible. Leave out post-assignment opens, coupon redemption and future spend.
Do not invent labels for unchosen actions. When a customer gets free shipping, you see their outcome under that assignment only, not under a coupon or no offer. Products never shown are unobserved, not confirmed dislikes. Negative sampling can keep retrieval training manageable, but it changes the training distribution. It does not turn missing interactions into causal negatives.
Make overlapping decisions explicit. One purchase cannot quietly become an independent positive label for five competing offers. Start with one decision per account per non-overlapping measurement window, or use a documented design that models sequences and concurrent campaigns.
Mature does not mean observed. A closed window with a reconciled transaction and return ledger can give a zero-purchase outcome. A closed window with lost identifiers, an incomplete channel feed or an unresolved refund cannot. Store observation status and label availability separately, and report missing outcomes by assigned arm and pre-decision cohort. Keep every assigned account in the experiment accounting. Dropping missing outcomes can select customers after assignment, and neither randomisation nor correct propensities repair that. Reconcile coverage, and for any remaining exclusions state the assumptions and sensitivity analysis they need, as the experiment inclusion guidance shows.