04 · SDK Assortment optimization

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

The panel

A panel is one row per customer. {id: profile} works; richer rows weight the answer and let you cut the result afterwards:

ColumnMeaning
id, profileRequired — who this is and how they shop.
audiencebuyer (default) · non-buyer · lapsed. Add people who do not buy the category yet to see who a new line pulls in.
category_spend28-day category spend — weights retention.
total_spend28-day store spend — sizes what walks with a leaver.
segment, store_typeCut the result afterwards.
current_skuThe line they buy today — feeds the transfer matrix.

Scenarios

KindYou specifyOutcomes per customer
delistchange — the lines leaving; remaining — what stays on the shelf.switches · spends less · leaves the category · leaves the store · unaffected
introducechange — the line arriving; remaining — the shelf it joins.adopts · tries once · ignores · enters the category
resetlines_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

FieldMeaning
retainedSpend-weighted share of category spend kept (0–1). Weighted by each buyer's 28-day category spend; upside never counts above 1.
countsAffected customers by outcome.
watchlistThe store leavers, largest 28-day total spend first — who walks, and the basket at stake.
rowsOne 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.
sweepReset 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

What this run will not do