pg033120 / app.py
XRachel's picture
Upload app.py
c0c64f7 verified
import os
import math
from functools import lru_cache
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.cluster import KMeans
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.tree import DecisionTreeClassifier, plot_tree
import matplotlib.pyplot as plt
st.set_page_config(
page_title="freshie - Perishable Retail Optimization",
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",
]
CATEGORY_COLORS = {
"Produce": "#2E8B57",
"Dairy": "#1E90FF",
"Meat": "#B22222",
"Seafood": "#20B2AA",
"Bakery": "#D2691E",
"Ready_to_Eat": "#8A2BE2",
}
FOCUS_CATEGORY = "Bakery"
REGION_COORDS = {
"West": (34.05, -118.24),
"Northeast": (40.71, -74.00),
"Southeast": (33.75, -84.39),
"Midwest": (41.88, -87.63),
"Southwest": (32.78, -96.80),
}
COLUMN_GROUPS = {
"Identity & network": [
"record_id", "product_id", "product_name", "category", "store_id", "region", "supplier_id"
],
"Time & expiry": [
"transaction_date", "expiration_date", "shelf_life_days", "day_of_week", "month", "days_until_expiry"
],
"Storage & handling": [
"storage_temp", "temp_deviation", "temp_abuse_events", "handling_score", "packaging_score", "spoilage_risk"
],
"Demand & inventory": [
"initial_quantity", "daily_demand", "units_sold", "leftover_units", "stockout_flag", "lost_sales_units", "sell_through_pct"
],
"Pricing & promotions": [
"base_price", "cost_price", "selling_price", "discount_pct", "markdown_applied", "is_promoted"
],
"Waste & profitability": [
"units_wasted", "waste_pct", "profit", "profit_margin_pct"
]
}
def inject_css():
st.markdown(
"""
<style>
.stApp {
background:
linear-gradient(rgba(255,250,245,0.92), rgba(247,251,255,0.93)),
url("https://images.unsplash.com/photo-1542838132-92c53300491e?auto=format&fit=crop&w=1600&q=80");
background-size: cover;
background-position: center;
background-attachment: fixed;
}
[data-testid="stDataFrame"] {
background: rgba(255,255,255,0.88);
border-radius: 16px;
padding: 6px;
box-shadow: 0 8px 20px rgba(0,0,0,0.04);
}
.section-card {
background: rgba(255,255,255,0.82);
border: 1px solid rgba(31,45,61,0.07);
border-radius: 16px;
padding: 12px 14px;
box-shadow: 0 8px 18px rgba(0,0,0,0.04);
}
.main-title {
display:flex;
align-items:center;
gap:14px;
margin-bottom:6px;
}
.logo-badge {
font-size: 2.2rem;
line-height: 1;
background: linear-gradient(135deg, #ffe6cc 0%, #ffd7eb 100%);
border-radius: 18px;
padding: 10px 14px;
box-shadow: 0 8px 22px rgba(0,0,0,0.06);
}
.brand-title {
font-size: 2.8rem;
font-weight: 900;
color: #1f2d3d;
letter-spacing: -0.03em;
text-shadow: 0 2px 8px rgba(255,255,255,0.55);
}
.brand-sub {
color: #5e6b78;
margin-top: -2px;
margin-bottom: 12px;
}
div[data-testid="stMetric"] {
background: rgba(255,255,255,0.82);
border: 1px solid rgba(31,45,61,0.07);
border-radius: 16px;
padding: 12px 14px;
box-shadow: 0 8px 18px rgba(0,0,0,0.04);
}
.mini-note {
background:#fff7ed;
border-left:4px solid #fb923c;
padding:10px 12px;
border-radius:10px;
margin: 8px 0 14px 0;
color:#7c4a03;
}
</style>
""",
unsafe_allow_html=True,
)
def category_icon(category: str) -> str:
mapping = {
"Bakery": "🥐",
"Dairy": "🥛",
"Meat": "🥩",
"Seafood": "🐟",
"Produce": "🥬",
"Ready_to_Eat": "🍱",
"Beverages": "🧃",
"Frozen_Meals": "🧊",
"Pharmaceuticals": "💊",
"Deli": "🧺",
}
return mapping.get(str(category), "📦")
def with_product_elements(frame: pd.DataFrame, product_col: str = "product_name", category_col: str = "category") -> pd.DataFrame:
out = frame.copy()
if category_col in out.columns:
out["category_tag"] = out[category_col].apply(lambda x: f"{category_icon(x)} {x}")
if product_col in out.columns and category_col in out.columns:
out["product_item"] = out.apply(lambda r: f"{category_icon(r[category_col])} {r[product_col]}", axis=1)
return out
@st.cache_data(show_spinner=False)
def load_financial_report() -> pd.DataFrame | None:
paths = [
"final_integrated_report.csv",
"/app/final_integrated_report.csv",
"/mnt/data/final_integrated_report.csv",
]
for p in paths:
if os.path.exists(p):
try:
return pd.read_csv(p)
except Exception:
return None
return None
def infer_manager_persona(df: pd.DataFrame) -> str:
"""
Infer the most relevant manager persona from the current filtered scope.
"""
n_stores = df["store_id"].nunique() if "store_id" in df.columns else 0
n_regions = df["region"].nunique() if "region" in df.columns else 0
if n_stores <= 1:
return "Store Manager"
elif n_regions <= 1:
return "Regional Manager"
return "C-Suite"
def region_anchor(region: str):
return REGION_COORDS.get(region, (39.0, -96.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 "West"
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.55, 0.55),
"store_lon": base_lon + rng.uniform(-0.75, 0.75),
}
)
loc = pd.DataFrame(rows)
return df.merge(loc, on="store_id", how="left")
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:
path = find_data_path()
df = pd.read_csv(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["stockout_flag"] = (df["daily_demand"] > df["initial_quantity"]).astype(int)
df["lost_sales_units"] = (df["daily_demand"] - df["units_sold"]).clip(lower=0)
if "waste_pct" in df.columns and df["waste_pct"].max() > 1:
df["waste_pct"] = df["waste_pct"] / 100
df["waste_pct"] = df["waste_pct"].clip(lower=0, upper=1)
df["value_score"] = (
(1 - df["waste_pct"].clip(0, 1)) * 0.35
+ df["profit_margin_pct"].clip(lower=0) / 100 * 0.25
+ (1 - df["days_until_expiry"].clip(upper=14) / 14) * 0.15
+ df["discount_pct"].clip(0, 0.5) * 0.25
)
df["expiry_bucket"] = pd.cut(
df["days_until_expiry"],
bins=[-1, 1, 3, 7, 30, 10_000],
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["waste_high"] = (df["waste_pct"] > df["waste_pct"].median()).astype(int)
df["profit_high"] = (df["profit"] > df["profit"].median()).astype(int)
df["promo_effective"] = ((df["is_promoted"] == 1) & (df["sell_through_pct"] > df["sell_through_pct"].median())).astype(int)
return df
@st.cache_data(show_spinner=False)
def fit_segments(df: pd.DataFrame) -> pd.DataFrame:
work = df[[
"daily_demand",
"initial_quantity",
"waste_pct",
"shelf_life_days",
"stock_demand_ratio",
"sell_through_pct",
]].replace([np.inf, -np.inf], np.nan).dropna().copy()
sample_size = min(len(work), 20000)
work = work.sample(sample_size, random_state=42)
scaler = StandardScaler()
X = scaler.fit_transform(work)
km = KMeans(n_clusters=4, random_state=42, n_init=10)
work["cluster"] = km.fit_predict(X)
return work
@st.cache_resource(show_spinner=False)
def fit_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=120, random_state=42, n_jobs=-1, max_depth=10
)
model.fit(X_train, y_train)
importances = pd.Series(model.feature_importances_, index=features).sort_values(ascending=False)
return model, importances
@lru_cache(maxsize=1)
def cluster_name_map():
return {
0: "Stable performers",
1: "Overstocked slow movers",
2: "Short-life high risk",
3: "High demand fast movers",
}
def apply_filters(df: pd.DataFrame):
st.sidebar.header("Filters")
if "filter_regions" not in st.session_state:
st.session_state["filter_regions"] = []
if "filter_stores" not in st.session_state:
st.session_state["filter_stores"] = []
all_regions = sorted(df["region"].dropna().unique())
all_stores = sorted(df["store_id"].dropna().unique())
# If the user selected stores directly, infer the matching region(s).
if st.session_state["filter_stores"] and not st.session_state["filter_regions"]:
inferred_regions = sorted(
df.loc[df["store_id"].isin(st.session_state["filter_stores"]), "region"]
.dropna()
.unique()
)
st.session_state["filter_regions"] = inferred_regions
# Region selection drives store options.
regions = st.sidebar.multiselect(
"Region",
all_regions,
key="filter_regions",
)
available_stores = sorted(
df.loc[df["region"].isin(regions), "store_id"].dropna().unique()
) if regions else all_stores
# Keep only stores that still belong to the selected region(s).
st.session_state["filter_stores"] = [
s for s in st.session_state["filter_stores"] if s in available_stores
]
stores = st.sidebar.multiselect(
"Store",
available_stores,
key="filter_stores",
)
# If stores are selected, make region selection follow them exactly.
if stores:
inferred_regions = sorted(
df.loc[df["store_id"].isin(stores), "region"].dropna().unique()
)
if inferred_regions != regions:
st.session_state["filter_regions"] = inferred_regions
regions = inferred_regions
category_choice = st.sidebar.selectbox("Category", ["All"] + 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"])
filtered = df.copy()
if regions:
filtered = filtered[filtered["region"].isin(regions)]
if stores:
filtered = filtered[filtered["store_id"].isin(stores)]
if category_choice != "All":
filtered = filtered[filtered["category"] == category_choice]
filtered = filtered[
(filtered["days_until_expiry"] >= expiry_range[0])
& (filtered["days_until_expiry"] <= expiry_range[1])
]
if weekend_choice == "Weekday":
filtered = filtered[filtered["is_weekend"] == 0]
elif weekend_choice == "Weekend":
filtered = filtered[filtered["is_weekend"] == 1]
return filtered
def metric_row(df: pd.DataFrame):
c1, c2, c3, c4, c5 = st.columns(5)
c1.metric("Waste %", f"{df['waste_pct'].mean():.1%}")
c2.metric("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 manager_dashboard(df: pd.DataFrame):
st.subheader("Manager Mode")
metric_row(df)
a, b = st.columns([1.2, 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["transaction_date"], y=trend["waste_pct"], name="Waste %", mode="lines+markers"))
fig.add_trace(go.Scatter(x=trend["transaction_date"], y=trend["profit"], name="Profit", mode="lines+markers", yaxis="y2"))
fig.update_layout(
title="Monthly Waste and Profit Trend",
yaxis=dict(title="Waste %"),
yaxis2=dict(title="Profit", overlaying="y", side="right"),
legend=dict(orientation="h"),
margin=dict(l=10, r=10, t=40, b=10),
)
st.plotly_chart(fig, use_container_width=True)
with b:
top_risk = (
df.groupby("category")[["waste_pct", "profit", "stock_demand_ratio"]]
.mean()
.sort_values("waste_pct", ascending=False)
.head(8)
.reset_index()
)
fig = px.bar(top_risk, x="waste_pct", y="category", orientation="h", title="High Waste Categories")
st.plotly_chart(fig, use_container_width=True)
c1, c2 = st.columns(2)
with c1:
store_risk = (
df.groupby("store_id")[["waste_pct", "profit", "temp_deviation"]]
.mean()
.sort_values(["waste_pct", "temp_deviation"], ascending=[False, False])
.head(15)
.reset_index()
)
st.dataframe(store_risk, use_container_width=True, hide_index=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 Performance")
st.plotly_chart(fig, use_container_width=True)
def forecast_region_demand(cat_df: pd.DataFrame, region: str) -> pd.DataFrame:
d = cat_df[cat_df["region"] == region].copy()
if d.empty:
return pd.DataFrame()
ts = d.groupby("transaction_date")["daily_demand"].mean().reset_index().sort_values("transaction_date")
if len(ts) < 14:
return pd.DataFrame()
recent = ts.tail(56).copy()
weekday_avg = recent.groupby(recent["transaction_date"].dt.dayofweek)["daily_demand"].mean().to_dict()
last_date = ts["transaction_date"].max()
future_dates = pd.date_range(last_date + pd.Timedelta(days=1), periods=14, freq="D")
future = pd.DataFrame({
"transaction_date": future_dates,
"daily_demand": [weekday_avg.get(d.dayofweek, ts["daily_demand"].tail(14).mean()) for d in future_dates],
"series": "Forecast"
})
hist = ts.tail(60).copy()
hist["series"] = "Actual"
return pd.concat([hist, future], ignore_index=True)
def build_weekpart_inventory_views(cat_df: pd.DataFrame):
tmp = cat_df.copy()
tmp["week_part"] = np.where(tmp["is_weekend"] == 1, "Weekend", "Weekday")
store_view = (
tmp.groupby(["store_id", "region", "week_part"])
.agg(
avg_inventory=("initial_quantity", "mean"),
avg_remaining=("leftover_units", "mean"),
avg_demand=("daily_demand", "mean"),
avg_units_sold=("units_sold", "mean"),
sell_through=("sell_through_pct", "mean"),
stockout_rate=("stockout_flag", "mean"),
unmet_demand=("lost_sales_units", "mean"),
waste_pct=("waste_pct", "mean"),
units_wasted=("units_wasted", "mean"),
avg_profit=("profit", "mean"),
avg_margin=("profit_margin_pct", "mean"),
markdown_rate=("markdown_applied", "mean"),
promo_rate=("is_promoted", "mean"),
avg_days_until_expiry=("days_until_expiry", "mean"),
temp_dev=("temp_deviation", "mean"),
spoilage_risk=("spoilage_risk", "mean"),
)
.reset_index()
)
region_view = (
tmp.groupby(["region", "week_part"])
.agg(
avg_inventory=("initial_quantity", "mean"),
avg_remaining=("leftover_units", "mean"),
avg_demand=("daily_demand", "mean"),
avg_units_sold=("units_sold", "mean"),
sell_through=("sell_through_pct", "mean"),
stockout_rate=("stockout_flag", "mean"),
unmet_demand=("lost_sales_units", "mean"),
waste_pct=("waste_pct", "mean"),
units_wasted=("units_wasted", "mean"),
avg_profit=("profit", "mean"),
avg_margin=("profit_margin_pct", "mean"),
markdown_rate=("markdown_applied", "mean"),
promo_rate=("is_promoted", "mean"),
avg_days_until_expiry=("days_until_expiry", "mean"),
temp_dev=("temp_deviation", "mean"),
spoilage_risk=("spoilage_risk", "mean"),
)
.reset_index()
)
return store_view, region_view
def manager_overview(df: pd.DataFrame, full_df: pd.DataFrame):
st.subheader("FRESHIE · Executive Overview")
fin = load_financial_report()
no_filter = len(df) == len(full_df)
c1, c2, c3, c4 = st.columns(4)
if fin is not None and not fin.empty:
fin_view = fin.copy()
if not no_filter:
if "region" in fin_view.columns and "region" in df.columns:
regions = df["region"].dropna().unique().tolist()
if regions:
fin_view = fin_view[fin_view["region"].isin(regions)]
if "store_id" in fin_view.columns and "store_id" in df.columns:
stores = df["store_id"].dropna().unique().tolist()
if stores:
fin_view = fin_view[fin_view["store_id"].isin(stores)]
c1.metric("Revenue", f"EUR {fin_view['revenue'].sum():,.0f}")
c2.metric("Profit", f"EUR {fin_view['profit'].sum():,.0f}")
c3.metric("Units sold", f"{fin_view['units_sold'].sum():,.0f}")
c4.metric("Units wasted", f"{fin_view['units_wasted'].sum():,.0f}")
else:
c1.metric("Revenue proxy", f"EUR {df['selling_price'].mul(df['units_sold']).sum():,.0f}")
c2.metric("Profit", f"EUR {df['profit'].sum():,.0f}")
c3.metric("Units sold", f"{df['units_sold'].sum():,.0f}")
c4.metric("Units wasted", f"{df['units_wasted'].sum():,.0f}")
if fin is None or fin.empty:
st.info("Integrated financial report not available, so overview is based on operational data only.")
manager_dashboard(df)
return
fin_view = fin.copy()
if not no_filter:
if "region" in fin_view.columns and "region" in df.columns:
regions = df["region"].dropna().unique().tolist()
if regions:
fin_view = fin_view[fin_view["region"].isin(regions)]
if "store_id" in fin_view.columns and "store_id" in df.columns:
stores = df["store_id"].dropna().unique().tolist()
if stores:
fin_view = fin_view[fin_view["store_id"].isin(stores)]
if fin_view.empty:
st.warning("No financial rows match the current filters.")
return
st.markdown('<div class="mini-note"><b>Executive logic:</b> no filter = enterprise-wide finance view. Active region/store filters = scoped monthly trend and diagnosis for the selected operational slice.</div>', unsafe_allow_html=True)
monthly = fin_view.groupby("month")[["revenue", "profit", "units_sold", "units_wasted", "waste_rate"]].sum().reset_index().sort_values("month")
monthly["profit_to_revenue"] = monthly["profit"] / monthly["revenue"].replace(0, pd.NA)
monthly["rev_profit_gap"] = (monthly["revenue"] - monthly["profit"]).abs()
monthly["gap_z"] = (monthly["rev_profit_gap"] - monthly["rev_profit_gap"].mean()) / (monthly["rev_profit_gap"].std(ddof=0) if monthly["rev_profit_gap"].std(ddof=0) else 1)
top_left, top_right = st.columns([1.25, 1])
with top_left:
fig = go.Figure()
fig.add_trace(go.Scatter(x=monthly["month"], y=monthly["revenue"], name="Revenue", mode="lines+markers"))
fig.add_trace(go.Scatter(x=monthly["month"], y=monthly["profit"], name="Profit", mode="lines+markers", yaxis="y2"))
fig.update_layout(
title="Monthly revenue and profit trend",
yaxis=dict(title="Revenue"),
yaxis2=dict(title="Profit", overlaying="y", side="right"),
margin=dict(l=10, r=10, t=40, b=10),
)
st.plotly_chart(fig, use_container_width=True)
fig2 = px.bar(
monthly.melt(id_vars="month", value_vars=["units_sold", "units_wasted"], var_name="metric", value_name="value"),
x="month", y="value", color="metric", barmode="group",
title="Units sold vs units wasted by month",
)
st.plotly_chart(fig2, use_container_width=True)
with top_right:
st.markdown("### Executive diagnosis")
if no_filter:
flagged = monthly[monthly["gap_z"] > 1].copy()
if flagged.empty:
flagged = monthly.nlargest(min(3, len(monthly)), "rev_profit_gap").copy()
for _, row in flagged.iterrows():
mo = row["month"]
scoped = fin_view[fin_view["month"] == mo].copy()
top_region = scoped.groupby("region")["profit"].sum().sort_values().index[0] if "region" in scoped.columns and not scoped.empty else "N/A"
# identify likely operational cause from base data
op = df.copy()
if "transaction_date" in op.columns:
op_month = op["transaction_date"].dt.to_period("M").astype(str)
op = op[op_month == str(mo)]
cause_cat = op.groupby("category")["waste_pct"].mean().sort_values(ascending=False).index[0] if not op.empty else "N/A"
cause_region = op.groupby("region")["stockout_flag"].mean().sort_values(ascending=False).index[0] if not op.empty else "N/A"
volatility_reason = []
if not op.empty:
if op["waste_pct"].mean() > df["waste_pct"].mean():
volatility_reason.append("higher-than-usual waste")
if op["stockout_flag"].mean() > df["stockout_flag"].mean():
volatility_reason.append("stockout pressure")
if op["discount_pct"].mean() > df["discount_pct"].mean():
volatility_reason.append("heavier markdowns")
reason = ", ".join(volatility_reason) if volatility_reason else "mixed category-region performance"
st.markdown(
f"- **{mo}**: revenue-profit fit weakened. Likely drag came from **{cause_cat}** and pressure in **{cause_region}**. "
f"Lowest profit region in the integrated report was **{top_region}**. Main reason: {reason}."
)
else:
st.markdown("- Current view is filter-scoped, so diagnosis is based on the selected store/region slice.")
peak_rev = monthly.loc[monthly["revenue"].idxmax(), "month"]
low_profit = monthly.loc[monthly["profit"].idxmin(), "month"]
st.markdown(f"- Highest revenue month: **{peak_rev}**.")
st.markdown(f"- Weakest profit month: **{low_profit}**.")
st.markdown("- Compare sold vs wasted units to see whether the selected slice suffers more from demand softness, waste, or stockouts.")
if not no_filter:
lower_left, lower_right = st.columns([1.2, 1])
with lower_left:
fig3 = px.line(
monthly,
x="month",
y=["revenue", "profit", "units_sold", "units_wasted"],
title="Scoped monthly performance timeline",
)
st.plotly_chart(fig3, use_container_width=True)
with lower_right:
st.markdown("### Scoped recommendation")
if df["waste_pct"].mean() > full_df["waste_pct"].mean():
st.markdown("- Waste is above enterprise baseline: tighten replenishment and markdown timing.")
if df["stockout_flag"].mean() > full_df["stockout_flag"].mean():
st.markdown("- Stockout pressure is above baseline: raise safety stock for the selected slice.")
if df["profit"].mean() < full_df["profit"].mean():
st.markdown("- Profit underperforms enterprise baseline: review category mix, markdown depth, and spoilage exposure.")
st.markdown("- Use Category Intelligence for category drivers, Inventory for supply-chain action, and Promotion for commercial response.")
def manager_category_intelligence(df: pd.DataFrame):
st.subheader("FRESHIE · Category Intelligence")
visible_categories = sorted(df["category"].dropna().unique())
if not visible_categories:
st.warning("No category remains after filtering.")
return
cat_df = df.copy()
focus = ", ".join(visible_categories)
st.markdown(
f'<div class="mini-note"><b>Focus:</b> {focus}. This page keeps only the highest-value category insights from the current sidebar filters. It covers all visible categories and uses charts rather than tables.</div>',
unsafe_allow_html=True,
)
k1, k2, k3, k4 = st.columns(4)
k1.metric("Avg demand", f"{cat_df['daily_demand'].mean():.1f}")
k2.metric("Waste rate", f"{cat_df['waste_pct'].mean():.1%}")
k3.metric("Stockout rate", f"{cat_df['stockout_flag'].mean():.1%}")
k4.metric("Avg profit", f"EUR {cat_df['profit'].mean():.2f}")
region_summary = cat_df.groupby("region").agg(
avg_demand=("daily_demand", "mean"),
avg_stock=("initial_quantity", "mean"),
avg_remaining=("leftover_units", "mean"),
sell_through=("sell_through_pct", "mean"),
avg_profit=("profit", "mean"),
avg_margin=("profit_margin_pct", "mean"),
waste_pct=("waste_pct", "mean"),
stockout_rate=("stockout_flag", "mean"),
lost_sales=("lost_sales_units", "mean"),
markdown_rate=("markdown_applied", "mean"),
promo_rate=("is_promoted", "mean"),
temp_dev=("temp_deviation", "mean"),
days_until_expiry=("days_until_expiry", "mean"),
).reset_index()
region_summary["profit_size"] = region_summary["avg_profit"].clip(lower=0) + 1
store_summary = cat_df.groupby("store_id").agg(
avg_demand=("daily_demand", "mean"),
avg_stock=("initial_quantity", "mean"),
avg_remaining=("leftover_units", "mean"),
sell_through=("sell_through_pct", "mean"),
avg_profit=("profit", "mean"),
waste_pct=("waste_pct", "mean"),
stockout_rate=("stockout_flag", "mean"),
lost_sales=("lost_sales_units", "mean"),
).reset_index()
week_summary = cat_df.groupby(np.where(cat_df["is_weekend"] == 1, "Weekend", "Weekday")).agg(
avg_inventory=("initial_quantity", "mean"),
avg_demand=("daily_demand", "mean"),
avg_remaining=("leftover_units", "mean"),
stockout_rate=("stockout_flag", "mean"),
waste_pct=("waste_pct", "mean"),
avg_profit=("profit", "mean"),
).reset_index(names="week_part")
top_left, top_right = st.columns([1.2, 1])
with top_left:
fig = px.bar(
week_summary.melt(id_vars="week_part", var_name="metric", value_name="value"),
x="metric",
y="value",
color="week_part",
barmode="group",
title="Weekday vs weekend category snapshot",
)
st.plotly_chart(fig, use_container_width=True)
if len(region_summary) > 0:
fig2 = px.scatter(
region_summary,
x="stockout_rate",
y="waste_pct",
size="profit_size",
color="region",
hover_data=["avg_demand", "avg_stock", "avg_remaining", "lost_sales"],
title="Regional trade-off: stockout vs waste",
)
st.plotly_chart(fig2, use_container_width=True)
with top_right:
indicator_lines = []
recommendation_lines = []
if len(region_summary) > 0:
for _, r in region_summary.iterrows():
indicator_lines.append(
f"**{r['region']}**: demand {r['avg_demand']:.1f}, stock {r['avg_stock']:.1f}, waste {r['waste_pct']:.1%}, stockout {r['stockout_rate']:.1%}, profit EUR {r['avg_profit']:.2f}."
)
advice = []
if r["stockout_rate"] > region_summary["stockout_rate"].mean():
advice.append("increase replenishment")
if r["waste_pct"] > region_summary["waste_pct"].mean():
advice.append("start markdown earlier")
if r["avg_margin"] > 0 and r["avg_profit"] < region_summary["avg_profit"].mean():
advice.append("review category mix and margin capture")
if r["temp_dev"] > region_summary["temp_dev"].mean():
advice.append("tighten storage handling")
if r["days_until_expiry"] < region_summary["days_until_expiry"].mean():
advice.append("prioritize fresher inbound stock")
if not advice:
advice.append("maintain current playbook")
recommendation_lines.append(f"**{r['region']}**: " + "; ".join(advice) + ".")
left, right = st.columns(2)
with left:
st.markdown("### Core indicators")
for line in indicator_lines:
st.markdown(f"- {line}")
with right:
st.markdown("### Recommendations")
for line in recommendation_lines:
st.markdown(f"- {line}")
lower_left, lower_right = st.columns([1.15, 1])
with lower_left:
if len(store_summary) > 0:
fig3 = px.bar(
store_summary.sort_values("avg_demand", ascending=False),
x="store_id",
y=["avg_demand", "avg_stock", "avg_remaining"],
barmode="group",
title="Filtered stores: demand, stock, and remaining inventory",
)
st.plotly_chart(fig3, use_container_width=True)
fig4 = px.line(
store_summary.sort_values("store_id"),
x="store_id",
y=["waste_pct", "stockout_rate", "sell_through"],
title="Filtered stores: waste, stockout, and sell-through",
)
st.plotly_chart(fig4, use_container_width=True)
with lower_right:
region_choice = region_summary["region"].iloc[0] if len(region_summary) == 1 else cat_df["region"].mode().iloc[0]
forecast_df = forecast_region_demand(cat_df, region_choice)
if not forecast_df.empty:
fig5 = px.line(
forecast_df,
x="transaction_date",
y="daily_demand",
color="series",
title=f"Demand forecast for {region_choice}",
)
st.plotly_chart(fig5, use_container_width=True)
if len(region_summary) > 0:
best_region = region_summary.sort_values("avg_profit", ascending=False).iloc[0]["region"]
riskiest_region = region_summary.sort_values("waste_pct", ascending=False).iloc[0]["region"]
st.markdown("### High-value takeaways")
st.markdown(f"- **Best profit signal:** {best_region}")
st.markdown(f"- **Highest waste exposure:** {riskiest_region}")
st.markdown("- Use Inventory for supply action and Promotion for commercial action once the category driver is identified.")
def build_transfer_suggestions(view_df: pd.DataFrame, full_df: pd.DataFrame):
if "store_lat" not in view_df.columns or "store_lon" not in view_df.columns:
view_df = attach_store_locations(view_df.copy())
if "store_lat" not in full_df.columns or "store_lon" not in full_df.columns:
full_df = attach_store_locations(full_df.copy())
store_summary = (
view_df.groupby(["store_id", "region"])
.agg(
total_inventory=("initial_quantity", "sum"),
units_sold=("units_sold", "sum"),
remaining_inventory=("leftover_units", "sum"),
total_demand=("daily_demand", "sum"),
unmet_demand=("lost_sales_units", "sum"),
)
.reset_index()
)
store_summary["inventory_gap"] = store_summary["remaining_inventory"] - store_summary["unmet_demand"]
receiving_need = (
view_df.groupby(["store_id", "region", "category", "store_lat", "store_lon"])
.agg(
remaining_inventory=("leftover_units", "sum"),
demand=("daily_demand", "sum"),
unmet_demand=("lost_sales_units", "sum"),
receiver_days_until_expiry=("days_until_expiry", "mean"),
)
.reset_index()
)
receiving_need = receiving_need[receiving_need["unmet_demand"] > 0].copy()
if receiving_need.empty:
return store_summary, pd.DataFrame(columns=[
"store_id", "region", "category", "remaining_inventory", "demand", "unmet_demand",
"recommended_transfer_qty", "same_region_options", "cross_region_options", "best_route"
])
donor_pool = (
full_df.groupby(["store_id", "region", "category", "store_lat", "store_lon"])
.agg(
donor_remaining=("leftover_units", "sum"),
donor_demand=("daily_demand", "sum"),
donor_days_until_expiry=("days_until_expiry", "mean"),
)
.reset_index()
)
donor_pool["surplus_qty"] = donor_pool["donor_remaining"] - donor_pool["donor_demand"]
donor_pool = donor_pool[donor_pool["surplus_qty"] > 0].copy()
def distance_km(lat1, lon1, lat2, lon2):
x = (math.radians(lon2) - math.radians(lon1)) * math.cos((math.radians(lat1) + math.radians(lat2)) / 2)
y = math.radians(lat2) - math.radians(lat1)
return 6371 * math.sqrt(x * x + y * y)
rows = []
for _, r in receiving_need.iterrows():
donors = donor_pool[
(donor_pool["category"] == r["category"]) &
(donor_pool["store_id"] != r["store_id"]) &
(donor_pool["surplus_qty"] > 0)
].copy()
if donors.empty:
row = r.to_dict()
row["recommended_transfer_qty"] = 0
row["same_region_options"] = "No same-region donor"
row["cross_region_options"] = "No cross-region donor"
row["best_route"] = "No feasible transfer"
rows.append(row)
continue
donors["priority_rank"] = (donors["region"] != r["region"]).astype(int)
donors["distance_km"] = donors.apply(
lambda d: distance_km(r["store_lat"], r["store_lon"], d["store_lat"], d["store_lon"]),
axis=1,
)
donors = donors.sort_values(
["priority_rank", "distance_km", "donor_days_until_expiry", "surplus_qty"],
ascending=[True, True, False, False]
)
same_region = donors[donors["priority_rank"] == 0].head(3)
cross_region = donors[donors["priority_rank"] == 1].head(3)
best = donors.iloc[0]
transfer_qty = int(min(r["unmet_demand"], max(best["surplus_qty"], 0)))
def donor_label(d):
tier = "same-region" if d["priority_rank"] == 0 else "cross-region"
return f"{d['store_id']} ({tier}, {d['distance_km']:.0f} km, expiry {d['donor_days_until_expiry']:.1f}d, surplus {int(d['surplus_qty'])})"
row = r.to_dict()
row["recommended_transfer_qty"] = transfer_qty
row["same_region_options"] = "; ".join(donor_label(d) for _, d in same_region.iterrows()) if not same_region.empty else "No same-region donor"
row["cross_region_options"] = "; ".join(donor_label(d) for _, d in cross_region.iterrows()) if not cross_region.empty else "No cross-region donor"
row["best_route"] = donor_label(best)
rows.append(row)
transfer_df = pd.DataFrame(rows)
return store_summary, transfer_df
def manager_inventory(df: pd.DataFrame, full_df: pd.DataFrame):
st.subheader("FRESHIE · Inventory & Replenishment")
st.markdown('<div class="mini-note"><b>Audience:</b> supply chain lead. Use this page for reorder, transfer, and stock balancing decisions.</div>', unsafe_allow_html=True)
store_summary, transfer_df = build_transfer_suggestions(df, full_df)
overstock = df.copy()
overstock["recommended_order_qty"] = (
1.2 * overstock["daily_demand"] * (1 + overstock["demand_variability"])
- overstock["leftover_units"]
)
overstock.loc[overstock["shelf_life_days"] <= 7, "recommended_order_qty"] *= 0.7
overstock.loc[overstock["spoilage_risk"] >= overstock["spoilage_risk"].quantile(0.75), "recommended_order_qty"] *= 0.8
overstock["recommended_order_qty"] = overstock["recommended_order_qty"].clip(lower=0).round()
c1, c2 = st.columns([1.3, 1])
with c1:
category_summary = overstock.groupby("category")[["initial_quantity", "recommended_order_qty", "waste_pct", "profit"]].mean().reset_index()
category_summary["order_reduction_pct"] = 1 - category_summary["recommended_order_qty"] / category_summary["initial_quantity"]
fig = px.bar(
category_summary.sort_values("order_reduction_pct", ascending=False),
x="order_reduction_pct",
y="category",
orientation="h",
title="Recommended Order Reduction by Category",
)
st.plotly_chart(fig, use_container_width=True)
with c2:
st.markdown("**Action shortlist**")
shortlist = overstock.sort_values(["waste_pct", "stock_demand_ratio"], ascending=[False, False])[[
"store_id", "product_name", "category", "initial_quantity", "daily_demand",
"days_until_expiry", "waste_pct", "recommended_order_qty"
]].head(20)
shortlist = with_product_elements(shortlist)
st.dataframe(
shortlist[["store_id", "product_item", "category_tag", "initial_quantity", "daily_demand", "days_until_expiry", "waste_pct", "recommended_order_qty"]],
use_container_width=True,
hide_index=True,
)
st.markdown("### What-if Simulator")
col1, col2, col3 = st.columns(3)
selected_category = col1.selectbox("Category for simulation", sorted(df["category"].unique()))
order_cut = col2.slider("Reduce order quantity by %", 0, 40, 10)
markdown_shift = col3.slider("Advance markdown trigger by days", 0, 5, 2)
sim = df[df["category"] == selected_category].copy()
current_waste = sim["waste_pct"].mean()
current_profit = sim["profit"].mean()
waste_reduction = 0.35 * (order_cut / 100) + 0.015 * markdown_shift
sim_waste = max(current_waste * (1 - waste_reduction), 0)
sim_profit = current_profit * (1 + 0.08 * (order_cut / 100) + 0.03 * markdown_shift)
s1, s2, s3 = st.columns(3)
s1.metric("Current waste", f"{current_waste:.1%}")
s2.metric("Simulated waste", f"{sim_waste:.1%}", delta=f"-{(current_waste-sim_waste):.1%}")
s3.metric("Simulated avg profit", f"€{sim_profit:.2f}", delta=f"€{(sim_profit-current_profit):.2f}")
st.markdown("### Store inventory balance")
st.dataframe(store_summary.sort_values(["unmet_demand", "remaining_inventory"], ascending=[False, False]), use_container_width=True, hide_index=True)
st.markdown("### Transfer suggestions for stores where demand exceeds available inventory")
st.caption("Routing logic: same-region donors are prioritized first, cross-region donors are used as second-best options, and donors are ranked by transport distance then remaining shelf life.")
if transfer_df.empty:
st.success("No filtered store currently shows unmet demand that needs transfer support.")
else:
show_cols = [
"store_id", "region", "category", "remaining_inventory", "demand", "unmet_demand",
"recommended_transfer_qty", "best_route", "same_region_options", "cross_region_options"
]
transfer_show = transfer_df.sort_values(["unmet_demand", "recommended_transfer_qty"], ascending=[False, False])[show_cols].copy()
transfer_show = with_product_elements(transfer_show, product_col="category", category_col="category")
st.dataframe(
transfer_show,
use_container_width=True,
hide_index=True,
)
def manager_promotions(df: pd.DataFrame):
st.subheader("FRESHIE · Promotion Designer")
st.markdown('<div class="mini-note"><b>Audience:</b> marketing lead. Use this page to test markdown depth, bundle logic, and campaign copy.</div>', unsafe_allow_html=True)
left, right = st.columns([1, 1.2])
with left:
promo_category = st.selectbox("Promotion category", sorted(df["category"].unique()), key="promo_cat")
expiry_target = st.selectbox("Target expiry bucket", ["<=1d", "2-3d", "4-7d", "8-30d", ">30d"])
discount = st.slider("Discount %", 0, 50, 18)
bundle = st.checkbox("Bundle with complementary items", value=True)
weekend_only = st.checkbox("Weekend campaign only", value=False)
sub = df[(df["category"] == promo_category) & (df["expiry_bucket"].astype(str) == expiry_target)].copy()
if weekend_only:
sub = sub[sub["is_weekend"] == 1]
demand_lift = 0.08 + discount / 200
if bundle:
demand_lift += 0.06
est_sales_uplift = sub["units_sold"].mean() * demand_lift if len(sub) else 0
est_waste_drop = sub["waste_pct"].mean() * min(0.35, demand_lift) if len(sub) else 0
est_profit = sub["profit"].mean() * (1 + demand_lift - discount / 150) if len(sub) else 0
st.metric("Estimated sales uplift", f"{est_sales_uplift:.2f} units")
st.metric("Estimated waste reduction", f"{est_waste_drop:.1%}")
st.metric("Estimated avg profit", f"€{est_profit:.2f}")
with right:
promo_base = df.groupby(["expiry_bucket"])[["discount_pct", "waste_pct", "profit"]].mean().reset_index()
fig = px.bar(promo_base, x="expiry_bucket", y=["discount_pct", "waste_pct"], barmode="group", title="Current Discount vs Waste by Expiry")
st.plotly_chart(fig, use_container_width=True)
st.markdown("**Recommended promotion copy**")
st.info(
f"Run a {discount}% {promo_category} campaign for {expiry_target} items"
+ (" on weekends" if weekend_only else "")
+ (" with bundle offers" if bundle else " as single-item markdown")
+ ". Position the offer at high-traffic display zones and highlight value + freshness."
)
def manager_risk(df: pd.DataFrame):
st.subheader("Risk & Store Operations")
_, importances = fit_risk_model(df)
c1, c2 = st.columns([1.1, 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:
heat = df.groupby(["region", "category"])["temp_deviation"].mean().reset_index()
fig = px.density_heatmap(heat, x="category", y="region", z="temp_deviation", title="Temperature Deviation Heatmap")
st.plotly_chart(fig, use_container_width=True)
alerts = (
df.groupby("store_id")[["temp_deviation", "temp_abuse_events", "waste_pct", "profit"]]
.mean()
.assign(alert_score=lambda x: 0.35 * x["temp_deviation"] + 0.25 * x["temp_abuse_events"] + 0.4 * x["waste_pct"] * 10)
.sort_values("alert_score", ascending=False)
.head(15)
.reset_index()
)
st.markdown("### Automated store alerts")
st.dataframe(alerts, use_container_width=True, hide_index=True)
def consumer_deals(df: pd.DataFrame):
st.subheader("FRESHIE · Consumer Mode")
c1, c2, c3 = st.columns(3)
max_budget = c1.slider("Budget (€)", 5, 60, 20)
preferred_category = c2.selectbox("Preferred category", ["All"] + sorted(df["category"].unique()))
max_expiry = c3.slider("Maximum 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 = deals.assign(
savings=lambda x: x["base_price"] - x["selling_price"],
deal_score=lambda x: x["discount_pct"] * 0.5 + x["value_score"] * 0.35 + (x["profit_margin_pct"].clip(lower=0) / 100) * 0.15,
).sort_values(["deal_score", "savings"], ascending=False)
display = deals[[
"product_name", "category", "store_id", "days_until_expiry",
"base_price", "selling_price", "discount_pct", "savings"
]].head(25)
st.dataframe(display, use_container_width=True, hide_index=True)
fig = px.scatter(
deals.head(500), x="selling_price", y="discount_pct", color="category",
hover_data=["product_name", "store_id", "days_until_expiry"],
title="Discounted Items Map"
)
st.plotly_chart(fig, use_container_width=True)
affordable = deals[deals["selling_price"] <= max_budget].head(10)
if not affordable.empty:
st.markdown("### Best picks for your budget")
for _, row in affordable.iterrows():
st.success(
f"Now €{row['selling_price']:.2f} (save €{row['base_price'] - row['selling_price']:.2f}) · expires in {int(row['days_until_expiry'])} day(s)"
)
st.markdown(
f"""
🛒 **{row['product_name']}**
📦 Category: {row['category']}
🏪 Store: {row['store_id']}
💸 Discount: {row['discount_pct']*100:.0f}%
⏳ Expiry: {row['days_until_expiry']} days
"""
)
def build_bundle(df: pd.DataFrame, budget: float, people: int, theme: str):
work = df.copy()
work = work[work["days_until_expiry"] <= 7].copy()
work["score"] = work["value_score"] + work["discount_pct"]
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()),
}
cats = theme_map.get(theme, list(work["category"].unique()))
work = work[work["category"].isin(cats)].sort_values(["score", "selling_price"], ascending=[False, True])
chosen = []
remaining = budget
target_items = min(max(people + 1, 3), 6)
used_categories = set()
for _, row in work.iterrows():
if row["selling_price"] <= remaining:
if theme != "Budget saver" and row["category"] in used_categories:
continue
chosen.append(row)
remaining -= row["selling_price"]
used_categories.add(row["category"])
if len(chosen) >= target_items:
break
if not chosen:
return pd.DataFrame(), 0.0, 0.0
bundle = pd.DataFrame(chosen)
total = bundle["selling_price"].sum()
saved = (bundle["base_price"] - bundle["selling_price"]).sum()
return bundle, total, saved
def consumer_bundles(df: pd.DataFrame):
st.subheader("FRESHIE · Bundle Builder")
c1, c2, c3 = st.columns(3)
budget = c1.slider("Bundle budget (€)", 8, 80, 25)
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 the current filters.")
return
k1, k2, k3 = st.columns(3)
k1.metric("Bundle total", f"€{total:.2f}")
k2.metric("You save", f"€{saved:.2f}")
k3.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(
"Suggested marketing use: turn these bundles into one-click promotions for end customers or pre-designed campaign packs for store managers."
)
def consumer_personal(df: pd.DataFrame):
st.subheader("FRESHIE · Personalized Promotions")
favorite = st.selectbox("Favorite category", sorted(df["category"].unique()))
price_cap = st.slider("Max item price (€)", 1, 30, 10)
not_too_close = st.checkbox("Hide items expiring within 1 day", value=False)
recs = df[df["category"] == favorite].copy()
recs = recs[recs["selling_price"] <= price_cap]
if not_too_close:
recs = recs[recs["days_until_expiry"] > 1]
recs = recs.assign(score=lambda x: x["discount_pct"] * 0.55 + x["value_score"] * 0.45).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"### {row['product_name']}")
st.write(f"{row['category']} · {row['store_id']}")
st.write(f"Now **€{row['selling_price']:.2f}** | Save **€{(row['base_price'] - row['selling_price']):.2f}**")
st.write(f"Expires in {int(row['days_until_expiry'])} day(s)")
st.button("Add to shortlist", key=f"short_{i}")
def manager_diagnose(df: pd.DataFrame):
st.subheader("FRESHIE · Diagnose")
st.markdown("### Key operational diagnostics")
c1, c2, c3 = st.columns(3)
c1.metric("High waste share", f"{(df['waste_pct']>df['waste_pct'].median()).mean():.1%}")
c2.metric("High profit share", f"{(df['profit']>df['profit'].median()).mean():.1%}")
c3.metric("Stockout rate", f"{df['stockout_flag'].mean():.1%}")
st.markdown("### Drivers of waste (proxy)")
fig = px.scatter(
df.sample(min(500, len(df))),
x="days_until_expiry",
y="waste_pct",
color="category",
title="Expiry vs Waste relationship"
)
st.plotly_chart(fig, use_container_width=True)
def manager_manual():
st.subheader("FRESHIE · User Manual (Manager)")
st.markdown("""
**Executive Overview**
- Shows financial + operational performance
- Identify abnormal revenue vs profit gaps
**Category Intelligence**
- Compare categories across demand, waste, stockout
- Use charts to detect drivers
**Inventory**
- Reorder suggestions
- Transfer recommendations
**Promotion**
- Simulate discount & bundle strategies
**Diagnose**
- Identify waste drivers and operational risks
""")
def consumer_manual():
st.subheader("FRESHIE · User Manual (Consumer)")
st.markdown("""
**Deal Finder**
- Discover discounted products
**Bundle Builder**
- Create optimized shopping bundles
**Personalized Promotions**
- Get recommendations based on preferences
""")
def main():
inject_css()
st.markdown(
"""
<div class="main-title">
<div class="logo-badge">🐱🐟</div>
<div>
<div class="brand-title">FRESHIE</div>
<div class="brand-sub">Perishable retail optimization for managers and consumers</div>
</div>
</div>
""",
unsafe_allow_html=True,
)
try:
df = load_data()
except Exception as e:
st.error(str(e))
st.stop()
filtered = apply_filters(df)
if filtered.empty:
st.warning("No data left after filtering.")
st.stop()
role = st.radio("Choose your mode", ["Manager", "Consumer"], horizontal=True)
if role == "Manager":
tabs = st.tabs([
"FRESHIE · Executive Overview",
"FRESHIE · Category Intelligence",
"FRESHIE · Inventory & Replenishment",
"FRESHIE · Promotion Designer",
"FRESHIE · Diagnose",
"FRESHIE · User Manual",
])
with tabs[0]:
manager_overview(filtered, df)
with tabs[1]:
manager_category_intelligence(filtered)
with tabs[2]:
manager_inventory(filtered, df)
with tabs[3]:
manager_promotions(filtered)
with tabs[4]:
manager_diagnose(filtered)
with tabs[5]:
manager_manual()
else:
tabs = st.tabs([
"FRESHIE · Deal Finder",
"FRESHIE · Bundle Builder",
"FRESHIE · Personalized Promotions",
"FRESHIE · User Manual",
])
with tabs[0]:
consumer_deals(filtered)
with tabs[1]:
consumer_bundles(filtered)
with tabs[2]:
consumer_personal(filtered)
with tabs[3]:
consumer_manual()
with st.expander("About this app"):
st.markdown(
"""
- **Manager mode** turns data into inventory, markdown, and operational decisions.
- **Consumer mode** surfaces discounted products, smart bundles, and personalized promotions.
- Built for deployment on Hugging Face Docker Spaces with Streamlit.
- User manuals are available inside both Manager and Consumer modes.
"""
)
if __name__ == "__main__":
main()