fix Ui
parent
84162554d4
commit
8b209e7f26
132
app.py
132
app.py
|
|
@ -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}")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue