pg032016 / app.py
XRachel's picture
Upload 5 files
b62c059 verified
import os
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
st.set_page_config(
page_title="FreshWise Studio",
page_icon="🥬",
layout="wide",
initial_sidebar_state="expanded",
)
DATA_CANDIDATES = [
os.environ.get("DATA_PATH", ""),
"perishable_goods_management.csv",
"/app/perishable_goods_management.csv",
"/data/perishable_goods_management.csv",
"/mnt/data/perishable_goods_management.csv",
]
BRAND = {
"name": "FreshWise Studio",
"tagline": "Turn perishables into profit, loyalty, and lower waste.",
"manager_copy": "For operators who need sharper replenishment, clearer risk alerts, and smarter campaign design.",
"consumer_copy": "For shoppers who want better deals, curated bundles, and easy discovery of time-sensitive offers.",
}
REGION_COORDS = {
"North": (53.48, -2.24),
"South": (51.50, -0.12),
"East": (52.20, 0.12),
"West": (51.48, -3.18),
"Central": (52.48, -1.89),
}
def inject_css():
st.markdown(
"""
<style>
:root {
--fw-green: #124734;
--fw-teal: #1e8b73;
--fw-blue: #163d73;
--fw-border: rgba(18,71,52,.10);
--fw-shadow: 0 12px 32px rgba(18,71,52,.08);
}
.stApp {
background: linear-gradient(180deg, #f8fcfa 0%, #f4fbf8 38%, #f7f8fb 100%);
color: #12312a;
}
.block-container {padding-top: 1.2rem; padding-bottom: 2rem; max-width: 1440px;}
h1, h2, h3 {color: var(--fw-green); letter-spacing: -0.02em;}
.fw-hero {
background: linear-gradient(135deg, rgba(18,71,52,1) 0%, rgba(22,61,115,1) 100%);
color: white;
padding: 1.35rem 1.4rem;
border-radius: 24px;
box-shadow: 0 18px 40px rgba(18,71,52,.18);
margin-bottom: 1rem;
}
.fw-hero h1 {color: white; margin: 0; font-size: 2.1rem;}
.fw-hero p {margin: .45rem 0 0; color: rgba(255,255,255,.88); font-size: 1rem;}
.fw-card {
background: rgba(255,255,255,.92);
border: 1px solid var(--fw-border);
border-radius: 22px;
padding: 1rem 1rem .9rem;
box-shadow: var(--fw-shadow);
}
.fw-mini-card {
background: linear-gradient(180deg, #ffffff 0%, #f7fbfa 100%);
border: 1px solid var(--fw-border);
border-radius: 18px;
padding: .9rem 1rem;
box-shadow: 0 8px 18px rgba(18,71,52,.05);
min-height: 116px;
}
.fw-role-card {
background: linear-gradient(180deg, rgba(255,255,255,.95) 0%, rgba(247,251,250,.96) 100%);
border: 1px solid var(--fw-border);
border-radius: 24px;
padding: 1.1rem 1.1rem 1rem;
box-shadow: var(--fw-shadow);
min-height: 210px;
}
.fw-kicker {
display: inline-block;
font-size: .78rem;
color: var(--fw-blue);
background: #eaf1fb;
padding: .25rem .55rem;
border-radius: 999px;
font-weight: 600;
margin-bottom: .55rem;
}
.fw-tag {
display: inline-block;
font-size: .75rem;
background: #eef8f4;
color: var(--fw-teal);
border: 1px solid #d8efe6;
border-radius: 999px;
padding: .2rem .5rem;
margin: .1rem .15rem .1rem 0;
}
.fw-info {
background: linear-gradient(180deg, #ecf7f2 0%, #f4fbf8 100%);
border: 1px solid #d6ede3;
border-radius: 16px;
padding: .8rem .95rem;
color: var(--fw-green);
}
.fw-footer {
padding: .9rem 1rem;
border-radius: 18px;
background: rgba(255,255,255,.75);
border: 1px solid var(--fw-border);
}
[data-testid="stMetricValue"] {font-size: 1.5rem;}
[data-testid="stMetric"] {
background: rgba(255,255,255,.82);
border: 1px solid var(--fw-border);
padding: .8rem .9rem;
border-radius: 18px;
box-shadow: 0 8px 16px rgba(18,71,52,.04);
}
.stTabs [data-baseweb="tab-list"] {
gap: .4rem;
background: rgba(255,255,255,.66);
padding: .35rem;
border-radius: 16px;
border: 1px solid var(--fw-border);
}
.stTabs [data-baseweb="tab"] {
background: transparent;
border-radius: 12px;
padding: .45rem .8rem;
height: auto;
}
.stTabs [aria-selected="true"] {
background: #eff8f4;
color: var(--fw-green);
font-weight: 700;
}
</style>
""",
unsafe_allow_html=True,
)
def find_data_path() -> str:
for path in DATA_CANDIDATES:
if path and os.path.exists(path):
return path
raise FileNotFoundError("perishable_goods_management.csv not found. Put it next to app.py or set DATA_PATH.")
@st.cache_data(show_spinner=False)
def load_data() -> pd.DataFrame:
df = pd.read_csv(find_data_path())
df["transaction_date"] = pd.to_datetime(df["transaction_date"], errors="coerce")
df["expiration_date"] = pd.to_datetime(df["expiration_date"], errors="coerce")
df["sell_through_pct"] = np.where(df["initial_quantity"] > 0, df["units_sold"] / df["initial_quantity"], 0)
df["stock_demand_ratio"] = np.where(df["daily_demand"] > 0, df["initial_quantity"] / df["daily_demand"], np.nan)
df["gross_margin"] = df["selling_price"] - df["cost_price"]
df["leftover_units"] = (df["initial_quantity"] - df["units_sold"]).clip(lower=0)
df["savings"] = df["base_price"] - df["selling_price"]
df["value_score"] = (
(1 - df["waste_pct"].clip(0, 1)) * 0.25
+ df["discount_pct"].clip(0, 0.6) * 0.35
+ df["sell_through_pct"].clip(0, 1) * 0.2
+ (1 - np.minimum(df["days_until_expiry"], 14) / 14) * 0.2
)
df["expiry_bucket"] = pd.cut(
df["days_until_expiry"],
bins=[-1, 1, 3, 7, 30, 10000],
labels=["<=1d", "2-3d", "4-7d", "8-30d", ">30d"],
)
df["high_waste_flag"] = (df["waste_pct"] >= df["waste_pct"].quantile(0.75)).astype(int)
df["promo_readiness"] = (
df["discount_pct"] * 0.3
+ (1 - np.minimum(df["days_until_expiry"], 7) / 7) * 0.3
+ df["spoilage_risk"] * 0.2
+ (1 - df["sell_through_pct"].clip(0, 1)) * 0.2
)
return attach_store_locations(df)
def _region_anchor(region: str):
return REGION_COORDS.get(region, (52.0, 0.0))
def attach_store_locations(df: pd.DataFrame) -> pd.DataFrame:
stores = sorted(df["store_id"].dropna().unique())
rows = []
for store in stores:
sub = df[df["store_id"] == store]
region = str(sub["region"].mode().iloc[0]) if not sub.empty else "Central"
base_lat, base_lon = _region_anchor(region)
seed = abs(hash(store)) % 10000
rng = np.random.default_rng(seed)
rows.append(
{
"store_id": store,
"store_lat": base_lat + rng.uniform(-0.35, 0.35),
"store_lon": base_lon + rng.uniform(-0.45, 0.45),
}
)
loc = pd.DataFrame(rows)
return df.merge(loc, on="store_id", how="left")
@st.cache_resource(show_spinner=False)
def risk_model(df: pd.DataFrame):
features = [
"daily_demand", "initial_quantity", "shelf_life_days", "days_until_expiry",
"temp_deviation", "temp_abuse_events", "handling_score", "packaging_score",
"spoilage_risk", "discount_pct", "markdown_applied", "is_weekend", "supplier_score",
]
X = df[features]
y = df["high_waste_flag"]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
model = RandomForestClassifier(n_estimators=150, random_state=42, max_depth=10, n_jobs=-1)
model.fit(X_train, y_train)
importances = pd.Series(model.feature_importances_, index=features).sort_values(ascending=False)
return model, importances
def ensure_state():
if "auth_role" not in st.session_state:
st.session_state.auth_role = None
if "auth_name" not in st.session_state:
st.session_state.auth_name = ""
if "logged_in" not in st.session_state:
st.session_state.logged_in = False
def hero(title: str, subtitle: str):
st.markdown(
f"""
<div class='fw-hero'>
<div class='fw-kicker'>Fresh retail intelligence</div>
<h1>{title}</h1>
<p>{subtitle}</p>
</div>
""",
unsafe_allow_html=True,
)
def landing_page():
hero(BRAND["name"], BRAND["tagline"])
c1, c2 = st.columns(2)
with c1:
st.markdown(
f"""
<div class='fw-role-card'>
<div class='fw-kicker'>Manager portal</div>
<h3>Operate smarter stores</h3>
<p>{BRAND['manager_copy']}</p>
<div>
<span class='fw-tag'>Waste alerts</span>
<span class='fw-tag'>Store map</span>
<span class='fw-tag'>Promotion simulation</span>
<span class='fw-tag'>Replenishment</span>
</div>
</div>
""",
unsafe_allow_html=True,
)
with st.form("manager_login"):
name = st.text_input("Manager name", placeholder="Alex Chen")
team = st.text_input("Team / region", placeholder="Operations - North")
submitted = st.form_submit_button("Enter Manager Portal", use_container_width=True)
if submitted:
st.session_state.logged_in = True
st.session_state.auth_role = "Manager"
st.session_state.auth_name = name or "Manager"
st.session_state.auth_team = team or "Operations"
st.rerun()
with c2:
st.markdown(
f"""
<div class='fw-role-card'>
<div class='fw-kicker'>Consumer app</div>
<h3>Find timely deals that fit life</h3>
<p>{BRAND['consumer_copy']}</p>
<div>
<span class='fw-tag'>Deal finder</span>
<span class='fw-tag'>Smart bundles</span>
<span class='fw-tag'>Store map</span>
<span class='fw-tag'>Personalized picks</span>
</div>
</div>
""",
unsafe_allow_html=True,
)
with st.form("consumer_login"):
name = st.text_input("Your name", placeholder="Jamie")
home_store = st.text_input("Preferred store / area", placeholder="South")
submitted = st.form_submit_button("Enter Consumer App", use_container_width=True)
if submitted:
st.session_state.logged_in = True
st.session_state.auth_role = "Consumer"
st.session_state.auth_name = name or "Guest"
st.session_state.auth_team = home_store or "Nearby stores"
st.rerun()
a, b, c = st.columns(3)
a.markdown("<div class='fw-mini-card'><h4>Reduce waste</h4><p>Turn near-expiry inventory into higher sell-through with timing-aware recommendations.</p></div>", unsafe_allow_html=True)
b.markdown("<div class='fw-mini-card'><h4>Improve margin</h4><p>Simulate promotions before launch and balance discount depth, channel, and duration.</p></div>", unsafe_allow_html=True)
c.markdown("<div class='fw-mini-card'><h4>Boost shopper value</h4><p>Guide customers toward bundles, bargains, and nearby offers that still feel fresh.</p></div>", unsafe_allow_html=True)
def sidebar_filters(df: pd.DataFrame):
st.sidebar.markdown(f"### Welcome, {st.session_state.auth_name}")
st.sidebar.caption(f"{st.session_state.auth_role} · {st.session_state.auth_team}")
if st.sidebar.button("Log out", use_container_width=True):
st.session_state.logged_in = False
st.session_state.auth_role = None
st.rerun()
st.sidebar.markdown("---")
st.sidebar.subheader("Filters")
regions = st.sidebar.multiselect("Region", sorted(df["region"].dropna().unique()))
stores = st.sidebar.multiselect("Store", sorted(df["store_id"].dropna().unique())[:300])
categories = st.sidebar.multiselect("Category", sorted(df["category"].dropna().unique()))
expiry_range = st.sidebar.slider("Days until expiry", 0, int(df["days_until_expiry"].max()), (0, 30))
weekend_choice = st.sidebar.selectbox("Day type", ["All", "Weekday", "Weekend"])
out = df.copy()
if regions:
out = out[out["region"].isin(regions)]
if stores:
out = out[out["store_id"].isin(stores)]
if categories:
out = out[out["category"].isin(categories)]
out = out[(out["days_until_expiry"] >= expiry_range[0]) & (out["days_until_expiry"] <= expiry_range[1])]
if weekend_choice == "Weekday":
out = out[out["is_weekend"] == 0]
elif weekend_choice == "Weekend":
out = out[out["is_weekend"] == 1]
return out
def metric_row(df: pd.DataFrame):
c1, c2, c3, c4, c5 = st.columns(5)
c1.metric("Waste rate", f"{df['waste_pct'].mean():.1%}")
c2.metric("Avg profit", f"€{df['profit'].mean():.2f}")
c3.metric("Sell-through", f"{df['sell_through_pct'].mean():.1%}")
c4.metric("Units wasted", f"{df['units_wasted'].mean():.1f}")
c5.metric("Markdown rate", f"{df['markdown_applied'].mean():.1%}")
def store_map(df: pd.DataFrame, color_col: str, size_col: str, title: str):
m = (
df.groupby(["store_id", "region", "store_lat", "store_lon"])[[color_col, size_col, "profit", "units_sold"]]
.mean()
.reset_index()
)
fig = px.scatter_mapbox(
m, lat="store_lat", lon="store_lon", color=color_col, size=size_col,
hover_name="store_id", hover_data={"region": True, "profit": ':.2f', "units_sold": ':.1f'},
zoom=4.3, height=520, color_continuous_scale="Viridis", size_max=22, title=title,
)
fig.update_layout(mapbox_style="open-street-map", margin=dict(l=0, r=0, t=48, b=0))
return fig
def manager_dashboard(df: pd.DataFrame):
hero("Manager command center", "Monitor store health, waste exposure, profitability, and campaign readiness in one place.")
metric_row(df)
a, b = st.columns([1.25, 1])
with a:
trend = df.groupby(df["transaction_date"].dt.to_period("M").astype(str))[["waste_pct", "profit"]].mean().reset_index()
fig = go.Figure()
fig.add_trace(go.Scatter(x=trend.iloc[:, 0], y=trend["waste_pct"], mode="lines+markers", name="Waste %"))
fig.add_trace(go.Scatter(x=trend.iloc[:, 0], y=trend["profit"], mode="lines+markers", name="Profit", yaxis="y2"))
fig.update_layout(title="Monthly performance curve", yaxis=dict(title="Waste %"), yaxis2=dict(title="Profit", overlaying="y", side="right"), legend=dict(orientation="h"), margin=dict(l=10, r=10, t=48, b=10))
st.plotly_chart(fig, use_container_width=True)
with b:
top = df.groupby("category")[["waste_pct", "profit", "stock_demand_ratio"]].mean().sort_values("waste_pct", ascending=False).head(8).reset_index()
fig = px.bar(top, x="waste_pct", y="category", orientation="h", title="Highest-waste categories", color="profit", color_continuous_scale="RdYlGn")
st.plotly_chart(fig, use_container_width=True)
c1, c2 = st.columns(2)
with c1:
st.plotly_chart(store_map(df, "waste_pct", "units_sold", "Store map: waste hotspots and sales density"), use_container_width=True)
with c2:
expiry = df.groupby("expiry_bucket")[["waste_pct", "profit", "discount_pct"]].mean().reset_index()
fig = px.line(expiry, x="expiry_bucket", y=["waste_pct", "profit", "discount_pct"], markers=True, title="Expiry-stage economics")
st.plotly_chart(fig, use_container_width=True)
def manager_inventory(df: pd.DataFrame):
st.markdown("## Inventory & replenishment studio")
rec = df.copy()
rec["recommended_order_qty"] = 1.2 * rec["daily_demand"] * (1 + rec["demand_variability"]) - rec["leftover_units"]
rec.loc[rec["shelf_life_days"] <= 7, "recommended_order_qty"] *= 0.7
rec.loc[rec["spoilage_risk"] >= rec["spoilage_risk"].quantile(0.75), "recommended_order_qty"] *= 0.8
rec["recommended_order_qty"] = rec["recommended_order_qty"].clip(lower=0).round()
rec["recommended_action"] = np.select(
[rec["recommended_order_qty"] < rec["daily_demand"] * 0.4, rec["recommended_order_qty"] > rec["daily_demand"] * 1.1],
["Cut order", "Increase order"], default="Keep steady",
)
x1, x2 = st.columns([1.1, 1])
with x1:
cat = rec.groupby("category")[["initial_quantity", "recommended_order_qty", "waste_pct", "profit"]].mean().reset_index()
cat["order_reduction_pct"] = 1 - cat["recommended_order_qty"] / cat["initial_quantity"]
fig = px.bar(cat.sort_values("order_reduction_pct", ascending=False), x="order_reduction_pct", y="category", orientation="h", color="waste_pct", title="Recommended order reduction by category")
st.plotly_chart(fig, use_container_width=True)
with x2:
shortlist = rec.sort_values(["waste_pct", "stock_demand_ratio"], ascending=[False, False])[["store_id", "product_name", "category", "days_until_expiry", "initial_quantity", "daily_demand", "recommended_order_qty", "recommended_action"]].head(18)
st.dataframe(shortlist, use_container_width=True, hide_index=True)
st.markdown("### What-if simulator")
c1, c2, c3, c4 = st.columns(4)
selected_category = c1.selectbox("Category", sorted(df["category"].unique()))
order_cut = c2.slider("Reduce order %", 0, 40, 12)
markdown_shift = c3.slider("Advance markdown by days", 0, 6, 2)
transfer_share = c4.slider("Inter-store transfer share %", 0, 30, 10)
sim = df[df["category"] == selected_category].copy()
current_waste = sim["waste_pct"].mean()
current_profit = sim["profit"].mean()
current_sell = sim["sell_through_pct"].mean()
waste_reduction = 0.38 * (order_cut / 100) + 0.018 * markdown_shift + 0.12 * (transfer_share / 100)
profit_uplift = 0.06 * (order_cut / 100) + 0.025 * markdown_shift + 0.08 * (transfer_share / 100)
sell_uplift = 0.03 * markdown_shift + 0.05 * (transfer_share / 100)
sim_waste = max(current_waste * (1 - waste_reduction), 0)
sim_profit = current_profit * (1 + profit_uplift)
sim_sell = min(current_sell * (1 + sell_uplift), 1.0)
s1, s2, s3 = st.columns(3)
s1.metric("Simulated waste", f"{sim_waste:.1%}", delta=f"-{(current_waste - sim_waste):.1%}")
s2.metric("Simulated profit", f"€{sim_profit:.2f}", delta=f"€{(sim_profit - current_profit):.2f}")
s3.metric("Simulated sell-through", f"{sim_sell:.1%}", delta=f"+{(sim_sell - current_sell):.1%}")
def manager_promotions(df: pd.DataFrame):
st.markdown("## Promotion simulation studio")
left, right = st.columns([1, 1.2])
with left:
promo_category = st.selectbox("Category", sorted(df["category"].unique()))
expiry_target = st.selectbox("Expiry segment", ["<=1d", "2-3d", "4-7d", "8-30d", ">30d"])
channel = st.selectbox("Primary channel", ["In-store signage", "App push", "Email", "Social media", "Bundle endcap"])
objective = st.selectbox("Campaign objective", ["Reduce waste", "Grow traffic", "Lift margin", "Clear slow movers"])
discount = st.slider("Discount %", 0, 50, 18)
duration = st.slider("Campaign duration (days)", 1, 14, 4)
budget = st.slider("Media / display budget (€)", 0, 20000, 4000, step=500)
bundle = st.checkbox("Bundle with complementary items", value=True)
weekend_only = st.checkbox("Weekend only", value=False)
geo_boost = st.checkbox("Geo-target high-risk stores", value=True)
sub = df[(df["category"] == promo_category) & (df["expiry_bucket"].astype(str) == expiry_target)].copy()
if weekend_only:
sub = sub[sub["is_weekend"] == 1]
base_units = sub["units_sold"].mean() if len(sub) else 0
base_waste = sub["waste_pct"].mean() if len(sub) else 0
base_profit = sub["profit"].mean() if len(sub) else 0
channel_factor = {"In-store signage": 0.09, "App push": 0.12, "Email": 0.08, "Social media": 0.10, "Bundle endcap": 0.14}[channel]
objective_factor = {"Reduce waste": 0.14, "Grow traffic": 0.11, "Lift margin": 0.07, "Clear slow movers": 0.13}[objective]
demand_lift = channel_factor + objective_factor + discount / 180 + min(duration / 50, 0.12)
if bundle:
demand_lift += 0.05
if geo_boost:
demand_lift += 0.03
if weekend_only:
demand_lift += 0.02
est_sales = base_units * (1 + demand_lift)
est_waste = max(base_waste * (1 - min(0.48, demand_lift)), 0)
est_profit = base_profit * (1 + demand_lift - discount / 140 - budget / 100000)
roi = ((est_profit - base_profit) * max(len(sub), 1)) / max(budget, 1)
m1, m2 = st.columns(2)
m1.metric("Estimated avg units sold", f"{est_sales:.2f}", delta=f"+{(est_sales-base_units):.2f}")
m2.metric("Estimated avg waste", f"{est_waste:.1%}", delta=f"-{(base_waste-est_waste):.1%}")
m3, m4 = st.columns(2)
m3.metric("Estimated avg profit", f"€{est_profit:.2f}", delta=f"€{(est_profit-base_profit):.2f}")
m4.metric("Campaign ROI proxy", f"{roi:.2f}x")
with right:
promo_base = df.groupby("expiry_bucket")[["discount_pct", "waste_pct", "profit", "promo_readiness"]].mean().reset_index()
fig = px.bar(promo_base, x="expiry_bucket", y=["discount_pct", "waste_pct"], barmode="group", title="Current discount and waste by expiry")
st.plotly_chart(fig, use_container_width=True)
scenario = pd.DataFrame({
"Metric": ["Sales lift", "Waste reduction", "Profit change", "Customer reach"],
"Current": [1.0, 0.0, 0.0, 1.0],
"Simulated": [1 + demand_lift, min(0.48, demand_lift), (est_profit - base_profit) / max(abs(base_profit), 1), 1 + (0.04 if geo_boost else 0) + (0.05 if channel in ["App push", "Social media"] else 0)],
})
fig2 = px.bar(scenario, x="Metric", y=["Current", "Simulated"], barmode="group", title="Scenario comparison")
st.plotly_chart(fig2, use_container_width=True)
st.markdown("### Suggested campaign brief")
st.markdown(
f"""
<div class='fw-info'>
Launch a <b>{discount}% {promo_category}</b> campaign for <b>{expiry_target}</b> inventory via <b>{channel}</b> for <b>{duration} days</b>.
Prioritize the objective <b>{objective}</b>, {'bundle with complementary items' if bundle else 'keep as a single-item offer'},
and {'target high-risk stores first' if geo_boost else 'deploy evenly across stores'}.
This scenario is expected to improve sell-through while lowering expiry-driven waste pressure.
</div>
""",
unsafe_allow_html=True,
)
def manager_risk(df: pd.DataFrame):
st.markdown("## Risk monitor")
_, importances = risk_model(df)
c1, c2 = st.columns([1.05, 1])
with c1:
fig = px.bar(importances.head(10).sort_values(), orientation="h", title="Top drivers of high waste risk")
st.plotly_chart(fig, use_container_width=True)
with c2:
st.plotly_chart(store_map(df, "temp_deviation", "temp_abuse_events", "Store map: temperature risk and handling exposure"), use_container_width=True)
alerts = (
df.groupby("store_id")[["temp_deviation", "temp_abuse_events", "waste_pct", "profit", "spoilage_risk"]]
.mean()
.assign(alert_score=lambda x: 0.28 * x["temp_deviation"] + 0.18 * x["temp_abuse_events"] + 0.34 * x["waste_pct"] * 10 + 0.2 * x["spoilage_risk"])
.sort_values("alert_score", ascending=False)
.head(15)
.reset_index()
)
st.dataframe(alerts, use_container_width=True, hide_index=True)
def consumer_deals(df: pd.DataFrame):
hero("Consumer deal finder", "Discover nearby offers, value-packed products, and time-sensitive bargains.")
c1, c2, c3 = st.columns(3)
budget = c1.slider("Budget (€)", 5, 60, 20)
preferred_category = c2.selectbox("Category", ["All"] + sorted(df["category"].unique()))
max_expiry = c3.slider("Max days until expiry", 1, 14, 5)
deals = df[df["days_until_expiry"] <= max_expiry].copy()
if preferred_category != "All":
deals = deals[deals["category"] == preferred_category]
deals["deal_score"] = deals["discount_pct"] * 0.4 + deals["value_score"] * 0.4 + deals["savings"].clip(lower=0) / deals["base_price"].replace(0, np.nan).fillna(1) * 0.2
deals = deals.sort_values(["deal_score", "savings"], ascending=False)
st.plotly_chart(store_map(deals, "discount_pct", "units_sold", "Nearby stores with strong deal intensity"), use_container_width=True)
st.dataframe(deals[["product_name", "category", "store_id", "days_until_expiry", "base_price", "selling_price", "discount_pct", "savings"]].head(30), use_container_width=True, hide_index=True)
best = deals[deals["selling_price"] <= budget].head(9)
cols = st.columns(3)
for i, (_, row) in enumerate(best.iterrows()):
with cols[i % 3]:
st.markdown(
f"""
<div class='fw-card'>
<div class='fw-kicker'>{row['category']}</div>
<h4 style='margin:.1rem 0 .45rem'>{row['product_name']}</h4>
<p style='margin:.2rem 0'><b>€{row['selling_price']:.2f}</b> now · save €{row['savings']:.2f}</p>
<p style='margin:.2rem 0'>Store: {row['store_id']}</p>
<p style='margin:.2rem 0'>Expires in {int(row['days_until_expiry'])} day(s)</p>
</div>
""",
unsafe_allow_html=True,
)
def build_bundle(df: pd.DataFrame, budget: float, people: int, theme: str):
work = df[df["days_until_expiry"] <= 7].copy()
work["score"] = work["value_score"] + work["discount_pct"] + np.where(work["selling_price"] <= budget / max(people, 1), 0.15, 0)
theme_map = {
"Quick dinner": ["Ready_to_Eat", "Produce", "Bakery", "Dairy"],
"Healthy protein": ["Meat", "Seafood", "Dairy", "Produce"],
"Family breakfast": ["Bakery", "Dairy", "Beverages", "Produce"],
"Budget saver": list(work["category"].unique()),
}
work = work[work["category"].isin(theme_map.get(theme, list(work["category"].unique())))]
work = work.sort_values(["score", "selling_price"], ascending=[False, True])
chosen, remaining, target_items, used = [], budget, min(max(people + 1, 3), 6), set()
for _, row in work.iterrows():
if row["selling_price"] <= remaining:
if theme != "Budget saver" and row["category"] in used:
continue
chosen.append(row)
remaining -= row["selling_price"]
used.add(row["category"])
if len(chosen) >= target_items:
break
if not chosen:
return pd.DataFrame(), 0.0, 0.0
bundle = pd.DataFrame(chosen)
return bundle, float(bundle["selling_price"].sum()), float(bundle["savings"].sum())
def consumer_bundles(df: pd.DataFrame):
st.markdown("## Bundle builder")
c1, c2, c3 = st.columns(3)
budget = c1.slider("Bundle budget (€)", 8, 90, 28)
people = c2.slider("People", 1, 6, 2)
theme = c3.selectbox("Bundle theme", ["Quick dinner", "Healthy protein", "Family breakfast", "Budget saver"])
bundle, total, saved = build_bundle(df, budget, people, theme)
if bundle.empty:
st.warning("No bundle found for these settings.")
return
m1, m2, m3 = st.columns(3)
m1.metric("Bundle total", f"€{total:.2f}")
m2.metric("You save", f"€{saved:.2f}")
m3.metric("Items", f"{len(bundle)}")
st.dataframe(bundle[["product_name", "category", "store_id", "selling_price", "base_price", "discount_pct", "days_until_expiry"]], use_container_width=True, hide_index=True)
st.info("Managers can reuse these bundles as prebuilt campaign templates for near-expiry conversion.")
def consumer_personal(df: pd.DataFrame):
st.markdown("## Personalized promotions")
c1, c2, c3 = st.columns(3)
favorite = c1.selectbox("Favorite category", sorted(df["category"].unique()))
price_cap = c2.slider("Max item price (€)", 1, 30, 10)
safe_window = c3.checkbox("Hide items expiring within 1 day", value=False)
recs = df[df["category"] == favorite].copy()
recs = recs[recs["selling_price"] <= price_cap]
if safe_window:
recs = recs[recs["days_until_expiry"] > 1]
recs["score"] = recs["discount_pct"] * 0.5 + recs["value_score"] * 0.5
recs = recs.sort_values("score", ascending=False).head(12)
cols = st.columns(3)
for i, (_, row) in enumerate(recs.iterrows()):
with cols[i % 3]:
st.markdown(
f"""
<div class='fw-card'>
<div class='fw-kicker'>Recommended for you</div>
<h4 style='margin:.1rem 0 .35rem'>{row['product_name']}</h4>
<p style='margin:.2rem 0'>{row['category']} · {row['store_id']}</p>
<p style='margin:.2rem 0'><b>€{row['selling_price']:.2f}</b> now · save €{row['savings']:.2f}</p>
<p style='margin:.2rem 0'>Expires in {int(row['days_until_expiry'])} day(s)</p>
</div>
""",
unsafe_allow_html=True,
)
st.button("Save offer", key=f"save_{i}", use_container_width=True)
def manager_shell(df: pd.DataFrame):
tabs = st.tabs(["Overview", "Inventory", "Promotion Studio", "Risk", "Store Map"])
with tabs[0]:
manager_dashboard(df)
with tabs[1]:
manager_inventory(df)
with tabs[2]:
manager_promotions(df)
with tabs[3]:
manager_risk(df)
with tabs[4]:
st.markdown("## Store network view")
st.plotly_chart(store_map(df, "profit", "units_sold", "Store map: profitability and sales concentration"), use_container_width=True)
by_store = df.groupby(["store_id", "region"])[["profit", "waste_pct", "units_sold", "temp_deviation"]].mean().reset_index()
st.dataframe(by_store.sort_values("profit", ascending=False), use_container_width=True, hide_index=True)
def consumer_shell(df: pd.DataFrame):
tabs = st.tabs(["Deals", "Bundles", "Personalized", "Store Map"])
with tabs[0]:
consumer_deals(df)
with tabs[1]:
consumer_bundles(df)
with tabs[2]:
consumer_personal(df)
with tabs[3]:
st.markdown("## Nearby store map")
st.plotly_chart(store_map(df, "discount_pct", "units_sold", "Store map: where deals are strongest right now"), use_container_width=True)
shortlist = df.sort_values(["discount_pct", "value_score"], ascending=False)[["store_id", "region", "product_name", "category", "selling_price", "discount_pct", "days_until_expiry"]].head(25)
st.dataframe(shortlist, use_container_width=True, hide_index=True)
def main():
inject_css()
ensure_state()
try:
df = load_data()
except Exception as e:
st.error(str(e))
st.stop()
if not st.session_state.logged_in:
landing_page()
st.stop()
filtered = sidebar_filters(df)
if filtered.empty:
st.warning("No data left after filtering.")
st.stop()
st.caption(f"Logged in as {st.session_state.auth_name} · {st.session_state.auth_role}")
if st.session_state.auth_role == "Manager":
manager_shell(filtered)
else:
consumer_shell(filtered)
st.markdown(
"""
<div class='fw-footer'>
<b>FreshWise Studio</b> combines managerial insight, promotional simulation, and shopper-facing discovery in one deployable app.
</div>
""",
unsafe_allow_html=True,
)
if __name__ == "__main__":
main()