Assortment Optimization SDK
Who follows the shelf, and who walks?
The range review says six slow lines in chilled juice have to go. Sales history tells you what those lines sold; it cannot tell you whether the people who bought them move to what is left, buy less juice, or stop buying juice here at all. Put the change to every customer in the panel and get one answer each — switches, spends less, leaves the category, leaves the store — with the spend attached to it and the walkers named.
Prices hold and nothing is on promotion — stated in the prompt and carried on every result — so the shelf is the only thing that changed.
Quick start
from eggai import AssortmentOptimizer shelf = AssortmentOptimizer(api_key) impact = shelf.impact( # the customers who buy the category today panel = customers, scenario = { "kind": "delist", # introduce | reset "category": "chilled juice", "change": "Tropicana Orange 1L 119", "remaining": "own-label 1L 79 · Malee 1L 95", }, ) impact.retained # 0.91 of category spend kept impact.counts # {"switches": 214, "leaves_store": 6} impact.watchlist # who walks, and the basket at stake impact.rows[0].substitute # where the demand lands impact.rows[0].reason # in the customer's words
import { AssortmentOptimizer } from '@eggai-sdk/core'; const shelf = new AssortmentOptimizer(apiKey); const impact = await shelf.impact(customers, { scenario: { kind: 'delist', // introduce | reset category: 'chilled juice', change: 'Tropicana Orange 1L 119', remaining: 'own-label 1L 79 · Malee 1L 95', }, }); impact.retained; // 0.91 of category spend kept impact.counts; // { switches: 214, leaves_store: 6 } impact.watchlist; // who walks, and the basket at stake impact.rows[0].substitute; // where the demand lands impact.rows[0].reason; // in the customer's words
The panel
A panel is one row per customer. {id: profile} works; richer rows weight the answer
and let you cut the result afterwards:
| Column | Meaning |
|---|---|
id, profile | Required — who this is and how they shop. |
audience | buyer (default) · non-buyer ·
lapsed. Add people who do not buy the category yet to see who a new line pulls in. |
category_spend | 28-day category spend — weights retention. |
total_spend | 28-day store spend — sizes what walks with a leaver. |
segment, store_type | Cut the result afterwards. |
current_sku | The line they buy today — feeds the transfer matrix. |
Scenarios
| Kind | You specify | Outcomes per customer |
|---|---|---|
delist | change — the lines leaving; remaining —
what stays on the shelf. | switches · spends less · leaves the category · leaves the store · unaffected |
introduce | change — the line arriving; remaining
— the shelf it joins. | adopts · tries once · ignores · enters the category |
reset | lines_ranked — the range, ranked;
cut_depth — % of lines cut; protected — lines that stay regardless;
sweep — extra depths in one pass. | as delist, per surviving range |
A reset with sweep answers several cut depths in one job (aggregate-only for the
extra depths) — so you see the depth at which retention starts falling faster than the lines you
removed were worth. The per-depth curve comes back on the job summary as sweep.
The result object
| Field | Meaning |
|---|---|
retained | Spend-weighted share of category spend kept (0–1). Weighted by each buyer's 28-day category spend; upside never counts above 1. |
counts | Affected customers by outcome. |
watchlist | The store leavers, largest 28-day total spend first — who walks, and the basket at stake. |
rows | One per customer: outcome, p,
substitute (where the demand lands), spend_frac (share of category spend
kept), reason (the sentence they would say), plus your panel columns. |
sweep | Reset runs: the retention curve across cut depths. |
Preview: a what-if in seconds
Big panels run as jobs; questions come faster than jobs finish. preview answers a
sampled what-if synchronously — up to 64 twins, nothing persisted:
quick = shelf.preview(
panel = sample_of_panel, # ≤ 64 rows
scenario = {"kind": "delist", "category": "chilled juice",
"change": "Malee 1L 95"},
)
quick.retained # directional, in seconds
quick.sampled # True — this was a sample, not the panel
const quick = await shelf.preview(sampleOfPanel /* ≤ 64 rows */, { scenario: { kind: 'delist', category: 'chilled juice', change: 'Malee 1L 95' }, }); quick.retained; // directional, in seconds quick.sampled; // true — this was a sample, not the panel
What this run will not do
- Nothing here proposes anything. You decide what leaves the shelf; the model only says who follows it. No second model is in a position to quietly improve the scenario it is being marked on.
- Whether they buy the category at all is read from each customer's own record, never guessed by another model.
- Calibration, never causal lift. Runs started from the EGGai app seal 5% of the panel — answered like everyone else, predictions withheld until your actuals come back, then scored as calibration. It says how close the numbers were, never that the change caused what followed.