main
Talha Ahmed 2026-07-31 20:42:09 +05:00
parent 84162554d4
commit 8b209e7f26
1 changed files with 17 additions and 115 deletions

132
app.py
View File

@ -243,13 +243,12 @@ ACTION_GLYPH = {"Increase": "↑", "Decrease": "↓", "Maintain": "→", "Invest
# this product, can I trust the model, what are my options, why this, then reference. # this product, can I trust the model, what are my options, why this, then reference.
# "Track record" was previously buried at the bottom of the last tab — it is the one # "Track record" was previously buried at the bottom of the last tab — it is the one
# thing that says how wrong the engine has been, so it sits second. # thing that says how wrong the engine has been, so it sits second.
VIEW_KEYS = ["price", "track", "scenarios", "why", "inventory", "repricing", VIEW_KEYS = ["price", "track", "scenarios", "why", "inventory",
"competitors", "ppc", "costs"] "competitors", "ppc", "costs"]
VIEW_LABELS = { VIEW_LABELS = {
"price": "📈 Price & demand", "track": "🎯 Track record", "price": "📈 Price & demand", "track": "🎯 Track record",
"scenarios": "🧮 Scenarios", "why": "🔍 Why this", "scenarios": "🧮 Scenarios", "why": "🔍 Why this",
"inventory": "📦 Inventory outlook", "inventory": "📦 Inventory outlook",
"repricing": "🔁 Stock at the new price",
"competitors": "🥊 Competitors", "competitors": "🥊 Competitors",
"ppc": "📣 PPC", "costs": "💰 Costs", "ppc": "📣 PPC", "costs": "💰 Costs",
} }
@ -1568,9 +1567,13 @@ for _, r in rows.iterrows():
# "out on 24 Aug" is a diary entry someone can act on. # "out on 24 Aug" is a diary entry someone can act on.
ol = d.get("outlook") or {} ol = d.get("outlook") or {}
so = ol.get("stockout_date") so = ol.get("stockout_date")
m3.metric("Cover", f"{d['rec_cover_days']} d", # "Cover after the move", not "Cover". This is cover at the RECOMMENDED
# price, while the header tile and the Inventory grid both show cover at
# TODAY's price — 75 d against 78 d on one screen, under the same word, with
# nothing saying which was which. Both were right; the label was the bug.
m3.metric("Cover after the move", f"{d['rec_cover_days']} d",
(f"out ~{so:%d %b}" if so else (f"out ~{so:%d %b}" if so else
f"{d['rec_cover_days'] - d['cover_days']:+d} d"), f"{d['rec_cover_days'] - d['cover_days']:+d} d vs today"),
delta_color="off") delta_color="off")
m4.metric("PPC/day", f"${d['ppc']['spend_day']:,.0f}", m4.metric("PPC/day", f"${d['ppc']['spend_day']:,.0f}",
f"TACoS {d['ppc']['tacos']:.1f}%", delta_color="off") f"TACoS {d['ppc']['tacos']:.1f}%", delta_color="off")
@ -1816,6 +1819,15 @@ for _, r in rows.iterrows():
f"**{_last['units_new']:,} units** left at the end of the horizon " f"**{_last['units_new']:,} units** left at the end of the horizon "
f"against {_last['units_now']:,} at today's price " f"against {_last['units_now']:,} at today's price "
f"(**{_last['delta_units']:+,}**).")) f"(**{_last['delta_units']:+,}**)."))
# The one line worth interrupting for: the move empties the shelf sooner than
# COSMOS projects. Stated in words as well as drawn on the chart.
_out = next((r for r in _rows if r["stockout"]), None)
if _out:
st.error(esc(
f"At ${d['rec_price']:,.2f} the shelf empties by "
f"**{pd.Timestamp(_out['date']):%d %b}** — sooner than COSMOS "
f"projects at today's price. A cut sells the remaining stock faster; "
f"it does not create any."))
if all(r["delta_units"] == 0 for r in _rows): if all(r["delta_units"] == 0 for r in _rows):
st.caption(esc( st.caption(esc(
f"COSMOS projects no depletion for this SKU — the same " f"COSMOS projects no depletion for this SKU — the same "
@ -1837,117 +1849,7 @@ for _, r in rows.iterrows():
"the same shelf at the recommended price, scaling each week's SALES by " "the same shelf at the recommended price, scaling each week's SALES by "
"the elasticity the Scenarios tab uses. Arrivals are unchanged — a PO " "the elasticity the Scenarios tab uses. Arrivals are unchanged — a PO "
"already placed does not move because we re-priced — and stock is floored " "already placed does not move because we re-priced — and stock is floored "
"at zero. Full week-by-week numbers are on **🔁 Stock at the new price**.") "at zero.")
elif sel == "repricing":
# What the recommended price does to the SHELF. The Inventory tab above is
# COSMOS's projection at TODAY's price, so a raise or a cut is otherwise shown
# with no stock consequence at all.
econ = {x["key"]: x for x in (d.get("scen_econ") or [])}
cur_e = econ.get("current") or {}
# The row at the price we actually recommend today, not the rung the cascade
# named — those differ whenever the move is step-capped.
rec_e = next((x for x in (d.get("scen_econ") or [])
if abs(x["price"] - d["rec_price"]) < 0.011), None)
# Demand response from the PRICE RATIO, not from the scenario table's unit
# counts. Those are rounded to whole units, and at low volume the rounding eats
# the entire signal: one real SKU sells 10 a month, and a 5% cut should lift that
# to 10.7 — both round to 10, so the re-projection reported a price cut with no
# effect on stock whatsoever.
#
# (p_new / p_now) ** elasticity is exact, and it is the same relationship the
# Scenarios tab applies: their common anchor cancels when you take the ratio of
# two rows, so this cannot drift from that table.
_ed = d.get("elasticity_detail") or {}
el = _ed.get("elasticity") or FALLBACK_ELASTICITY
# COSMOS's own daily sale — the figure its Inventory Planning grid prints — so the
# cover days here reproduce COSMOS's coverDays rather than sitting a day or two off.
u_now = float((d.get("invp_stats") or {}).get("avg_sale")
or d.get("units_day") or 0.0)
ratio = ((d["rec_price"] / d["current_price"]) ** el
if d.get("current_price") else 1.0)
u_new = u_now * ratio
rows = reproject_inventory(d.get("inv_projection") or [], u_now or 0, u_new or 0)
if d["action"] == "Maintain" or abs(d["rec_price"] - d["current_price"]) < 0.005:
st.info("**No price change is recommended**, so the stock outlook is the "
"one on the Inventory tab. Nothing to re-project.")
elif not rows:
st.warning(esc(
"Cannot re-project the shelf for this SKU — that needs COSMOS's weekly "
"projection and a demand response at both prices, and one of them is "
"missing. The Inventory tab still shows the outlook at today's price."))
else:
pct = (u_new / u_now - 1) * 100 if u_now else 0.0
verb = "slows" if u_new < u_now else "speeds up"
r1, r2, r3 = st.columns(3)
r1.metric("Price", f"${d['rec_price']:,.2f}",
f"{d['delta_pct']:+.1f}% vs ${d['current_price']:,.2f}",
delta_color="off")
r2.metric("Units/day", f"{u_new:,.0f}", f"{pct:+.1f}% vs {u_now:,.0f} today",
delta_color="off")
last = rows[-1]
r3.metric("Stock at horizon", f"{last['units_new']:,}",
f"{last['delta_units']:+,} vs today's price", delta_color="off")
# COSMOS sometimes projects a completely flat shelf — same units every week,
# no draw at all. That happens on very low-velocity SKUs (one real example
# holds 167 units and COSMOS shows 167 for all eleven weeks). There is then no
# depletion to re-scale, so the units column cannot move however the price
# changes, and only cover responds. Say so: a table of "+0" with no
# explanation reads as a broken feature rather than an upstream gap.
if all(r["delta_units"] == 0 for r in rows):
st.info(esc(
"COSMOS projects **no depletion** for this SKU — the same "
f"{rows[0]['units_now']:,} units in every one of its "
f"{len(rows)} weekly snapshots. There is no draw to re-scale, so the "
"unit columns cannot respond to a price change; only cover days move, "
"because those are computed from our own velocity."))
first_out = next((r for r in rows if r["stockout"]), None)
if first_out:
st.error(esc(
f"At ${d['rec_price']:,.2f} the shelf empties by "
f"**{pd.Timestamp(first_out['date']):%d %b}** — earlier than COSMOS "
f"projects at today's price. A cut sells the remaining stock faster; "
f"it does not create any."))
else:
st.success(esc(
f"Demand {verb} by {abs(pct):.1f}%, so the shelf lasts "
f"{'longer' if u_new < u_now else 'less time'}. No stockout inside "
f"COSMOS's {len(rows)}-week horizon at this price."))
st.dataframe(
pd.DataFrame([{
"Week": pd.Timestamp(r["date"]).strftime("%m/%d/%Y"),
"Units (today's price)": r["units_now"],
"Units (new price)": r["units_new"],
"Change": r["delta_units"],
"Cover now": r["cover_now"],
"Cover new": r["cover_new"],
"Arriving": int(r["arrival"]),
} for r in rows]),
hide_index=True, use_container_width=True,
column_config={
"Units (today's price)": st.column_config.NumberColumn(format="%d"),
"Units (new price)": st.column_config.NumberColumn(format="%d"),
"Change": st.column_config.NumberColumn(format="%+d"),
"Cover now": st.column_config.NumberColumn(format="%d d"),
"Cover new": st.column_config.NumberColumn(format="%d d"),
"Arriving": st.column_config.NumberColumn(format="%d"),
},
)
st.caption(
"**This table is OUR projection, not COSMOS's.** The Inventory tab is "
"COSMOS's own forecast at today's price; this re-runs it at the "
"recommended price by scaling each week's SALES with the same elasticity "
"the Scenarios tab uses. Arrivals are unchanged — a PO already placed "
"does not move because we re-priced — and unit value stays on COSMOS's "
"cost basis. Stock is floored at zero; a negative shelf is not a forecast. "
"**Both cover columns divide by OUR velocity** so the difference between "
"them is the price effect alone — which is why they can sit a day or two "
"off the Inventory tab, where COSMOS's own figure is shown verbatim."
)
elif sel == "scenarios": elif sel == "scenarios":
render_scenarios(d, key_prefix=f"sc_{sku}") render_scenarios(d, key_prefix=f"sc_{sku}")