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( """ """, 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('
Executive logic: no filter = enterprise-wide finance view. Active region/store filters = scoped monthly trend and diagnosis for the selected operational slice.
', 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'
Focus: {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.
', 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('
Audience: supply chain lead. Use this page for reorder, transfer, and stock balancing decisions.
', 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('
Audience: marketing lead. Use this page to test markdown depth, bundle logic, and campaign copy.
', 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( """
🐱🐟
FRESHIE
Perishable retail optimization for managers and consumers
""", 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()