| 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() |
|
|