Transactions alone miss people who saw the price and chose not to buy. Build a row for every eligible product-market-period, using posted-price and availability logs to create zero-sales rows. A missing row is not automatically zero demand.
| Field | Required meaning |
|---|---|
sku_id, market_id, decision_at, horizon_end | The decision unit and the interval governed by the price |
posted_price, effective_paid_price, currency | Price shown, price paid after promotions, and consistent monetary units |
price_valid_from, price_valid_to | Actual duration of exposure to each price; split a day when price changes |
eligible_exposure, exposure_definition | Eligible visits, offers shown, or hours of availability; state the denominator |
units_sold, orders, returns | Distinct outcome definitions, with arrival and maturity timestamps |
inventory_start, replenishment, stockout_at | Capacity and evidence that observed sales were censored |
unit_cost_snapshot, fulfillment_cost, fee_rules | Costs known at decision time; reconcile realised costs afterwards |
promotion, placement, campaign_assignment | Discounts, visibility and concurrent interventions |
competitor_snapshot_at, competitor_price, competitor_availability | Comparable product, collection time and missing/stale indicators |
policy_version, experiment_id, assignment_probability | How assignment was selected; retain the full available action set and any later override |
event_at, available_at, snapshot_version | When a fact occurred, when the service could know it, and which revision was used |
Measure exposure carefully. For visit-level conversion, a customer who does not buy is a valid
negative example. For aggregate counts with unequal exposure, a GLM can use a log-exposure offset.
The
scikit-learn Poisson example
shows the related rate-target and exposure-weight set-up. Zero exposure means no chance to observe
demand and cannot go through log(0).
Exposure can itself respond to price. Search placement, traffic or customer visits may change after a price change. Conditioning on that realised exposure can hide part of the total price effect. State whether you estimate conversion given exposure or total period sales. For a total effect, use pre-change exposure forecasts or model traffic jointly.
Sales are not demand during a stockout. Selling the last ten units tells you demand was at least ten, not exactly ten. Keep censored periods and mark them. When they matter, use a censored likelihood or an availability-aware choice/demand model. An uncensored subset is fine for a narrow first baseline if you state its selection limits. Dropping stockouts can leave mostly low-demand periods. Availability also changes substitution across products, as Conlon and Mortimer show.
Check support for inventory levels too. If comparable observations were always capped at ten units, they cannot show the demand tail needed to value an inventory of twenty without extra assumptions. Lower-stock decisions may need the tail only up to their own cap. Mark model-based extrapolation clearly and abstain when the needed tail or censoring assumptions are unvalidated.
Include day of week, holidays, seasonality, item age, category, store and lagged demand, using only information available before the recommendation. Compute rolling features separately inside each validation history. A future realised competitor price, end-of-day inventory, post-change page views or a future return flag is not a pre-decision feature. Event time and availability time must both come before the decision: a backfilled Monday transaction first loaded on Wednesday was unavailable to Tuesday's recommendation.
Promotions, loyalty benefits and offer recommendations change the effective price. Track them together, including who funds a discount. Optimising posted price and discount separately can spend the same margin twice. Cross-item prices and availability matter for substitutes and complements, so begin with an isolated category and test that assumption.