XRachel commited on
Commit
2fd08ff
·
verified ·
1 Parent(s): 39c8249

Upload 6 files

Browse files
Files changed (7) hide show
  1. .gitattributes +1 -0
  2. Dockerfile +21 -0
  3. README.md +16 -4
  4. app.py +719 -0
  5. gitattributes +36 -0
  6. perishable_goods_management.csv +3 -0
  7. requirements.txt +6 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ perishable_goods_management.csv filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ PIP_NO_CACHE_DIR=1 \
6
+ PORT=7860
7
+
8
+ WORKDIR /app
9
+
10
+ RUN apt-get update && apt-get install -y --no-install-recommends \
11
+ build-essential \
12
+ && rm -rf /var/lib/apt/lists/*
13
+
14
+ COPY requirements.txt /app/requirements.txt
15
+ RUN pip install --upgrade pip && pip install -r /app/requirements.txt
16
+
17
+ COPY . /app
18
+
19
+ EXPOSE 7860
20
+
21
+ CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0"]
README.md CHANGED
@@ -1,10 +1,22 @@
1
  ---
2
- title: Pg032913
3
- emoji: 📚
4
  colorFrom: green
5
- colorTo: yellow
6
  sdk: docker
 
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: FreshWise Studio
3
+ emoji: 🥐
4
  colorFrom: green
5
+ colorTo: blue
6
  sdk: docker
7
+ app_port: 7860
8
+ short_description: Bakery-focused perishable retail optimization app
9
  pinned: false
10
  ---
11
 
12
+ # FreshWise Studio
13
+
14
+ This version adds a bakery-focused category intelligence layer.
15
+
16
+ ## Added in this package
17
+ - Bakery as the featured category
18
+ - Regional comparison of operations, inventory, waste, profitability and demand
19
+ - 14-day regional demand forecast for the featured category
20
+ - Stockout / lost-sales / waste trade-off analysis
21
+ - Category-level promotion simulator
22
+ - Existing linked Region ↔ Store filters retained
app.py ADDED
@@ -0,0 +1,719 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from functools import lru_cache
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ import plotly.express as px
7
+ import plotly.graph_objects as go
8
+ import streamlit as st
9
+ from sklearn.cluster import KMeans
10
+ from sklearn.ensemble import RandomForestClassifier
11
+ from sklearn.model_selection import train_test_split
12
+ from sklearn.preprocessing import StandardScaler
13
+
14
+ st.set_page_config(
15
+ page_title="FreshWise - Perishable Retail Optimization",
16
+ page_icon="🥗",
17
+ layout="wide",
18
+ initial_sidebar_state="expanded",
19
+ )
20
+
21
+ DATA_CANDIDATES = [
22
+ os.environ.get("DATA_PATH", ""),
23
+ "perishable_goods_management.csv",
24
+ "/app/perishable_goods_management.csv",
25
+ "/data/perishable_goods_management.csv",
26
+ "/mnt/data/perishable_goods_management.csv",
27
+ ]
28
+
29
+ CATEGORY_COLORS = {
30
+ "Produce": "#2E8B57",
31
+ "Dairy": "#1E90FF",
32
+ "Meat": "#B22222",
33
+ "Seafood": "#20B2AA",
34
+ "Bakery": "#D2691E",
35
+ "Ready_to_Eat": "#8A2BE2",
36
+ }
37
+
38
+ FOCUS_CATEGORY = "Bakery"
39
+
40
+
41
+ def find_data_path() -> str:
42
+ for path in DATA_CANDIDATES:
43
+ if path and os.path.exists(path):
44
+ return path
45
+ raise FileNotFoundError(
46
+ "perishable_goods_management.csv not found. Put it next to app.py or set DATA_PATH."
47
+ )
48
+
49
+
50
+ @st.cache_data(show_spinner=False)
51
+ def load_data() -> pd.DataFrame:
52
+ path = find_data_path()
53
+ df = pd.read_csv(path)
54
+
55
+ df["transaction_date"] = pd.to_datetime(df["transaction_date"], errors="coerce")
56
+ df["expiration_date"] = pd.to_datetime(df["expiration_date"], errors="coerce")
57
+
58
+ df["sell_through_pct"] = np.where(
59
+ df["initial_quantity"] > 0, df["units_sold"] / df["initial_quantity"], 0
60
+ )
61
+ df["stock_demand_ratio"] = np.where(
62
+ df["daily_demand"] > 0, df["initial_quantity"] / df["daily_demand"], np.nan
63
+ )
64
+ df["gross_margin"] = df["selling_price"] - df["cost_price"]
65
+ df["leftover_units"] = (df["initial_quantity"] - df["units_sold"]).clip(lower=0)
66
+ df["stockout_flag"] = (df["daily_demand"] > df["initial_quantity"]).astype(int)
67
+ df["lost_sales_units"] = (df["daily_demand"] - df["units_sold"]).clip(lower=0)
68
+ df["value_score"] = (
69
+ (1 - df["waste_pct"].clip(0, 1)) * 0.35
70
+ + df["profit_margin_pct"].clip(lower=0) / 100 * 0.25
71
+ + (1 - df["days_until_expiry"].clip(upper=14) / 14) * 0.15
72
+ + df["discount_pct"].clip(0, 0.5) * 0.25
73
+ )
74
+ df["expiry_bucket"] = pd.cut(
75
+ df["days_until_expiry"],
76
+ bins=[-1, 1, 3, 7, 30, 10_000],
77
+ labels=["<=1d", "2-3d", "4-7d", "8-30d", ">30d"],
78
+ )
79
+ df["high_waste_flag"] = (df["waste_pct"] >= df["waste_pct"].quantile(0.75)).astype(int)
80
+ return df
81
+
82
+
83
+ @st.cache_data(show_spinner=False)
84
+ def fit_segments(df: pd.DataFrame) -> pd.DataFrame:
85
+ work = df[[
86
+ "daily_demand",
87
+ "initial_quantity",
88
+ "waste_pct",
89
+ "shelf_life_days",
90
+ "stock_demand_ratio",
91
+ "sell_through_pct",
92
+ ]].replace([np.inf, -np.inf], np.nan).dropna().copy()
93
+
94
+ sample_size = min(len(work), 20000)
95
+ work = work.sample(sample_size, random_state=42)
96
+ scaler = StandardScaler()
97
+ X = scaler.fit_transform(work)
98
+ km = KMeans(n_clusters=4, random_state=42, n_init=10)
99
+ work["cluster"] = km.fit_predict(X)
100
+ return work
101
+
102
+
103
+ @st.cache_resource(show_spinner=False)
104
+ def fit_risk_model(df: pd.DataFrame):
105
+ features = [
106
+ "daily_demand",
107
+ "initial_quantity",
108
+ "shelf_life_days",
109
+ "days_until_expiry",
110
+ "temp_deviation",
111
+ "temp_abuse_events",
112
+ "handling_score",
113
+ "packaging_score",
114
+ "spoilage_risk",
115
+ "discount_pct",
116
+ "markdown_applied",
117
+ "is_weekend",
118
+ "supplier_score",
119
+ ]
120
+ X = df[features]
121
+ y = df["high_waste_flag"]
122
+ X_train, X_test, y_train, y_test = train_test_split(
123
+ X, y, test_size=0.2, random_state=42, stratify=y
124
+ )
125
+ model = RandomForestClassifier(
126
+ n_estimators=120, random_state=42, n_jobs=-1, max_depth=10
127
+ )
128
+ model.fit(X_train, y_train)
129
+ importances = pd.Series(model.feature_importances_, index=features).sort_values(ascending=False)
130
+ return model, importances
131
+
132
+
133
+ @lru_cache(maxsize=1)
134
+ def cluster_name_map():
135
+ return {
136
+ 0: "Stable performers",
137
+ 1: "Overstocked slow movers",
138
+ 2: "Short-life high risk",
139
+ 3: "High demand fast movers",
140
+ }
141
+
142
+
143
+ def apply_filters(df: pd.DataFrame):
144
+ st.sidebar.header("Filters")
145
+
146
+ if "filter_regions" not in st.session_state:
147
+ st.session_state["filter_regions"] = []
148
+ if "filter_stores" not in st.session_state:
149
+ st.session_state["filter_stores"] = []
150
+
151
+ all_regions = sorted(df["region"].dropna().unique())
152
+ all_stores = sorted(df["store_id"].dropna().unique())
153
+
154
+ # If the user selected stores directly, infer the matching region(s).
155
+ if st.session_state["filter_stores"] and not st.session_state["filter_regions"]:
156
+ inferred_regions = sorted(
157
+ df.loc[df["store_id"].isin(st.session_state["filter_stores"]), "region"]
158
+ .dropna()
159
+ .unique()
160
+ )
161
+ st.session_state["filter_regions"] = inferred_regions
162
+
163
+ # Region selection drives store options.
164
+ regions = st.sidebar.multiselect(
165
+ "Region",
166
+ all_regions,
167
+ key="filter_regions",
168
+ )
169
+
170
+ available_stores = sorted(
171
+ df.loc[df["region"].isin(regions), "store_id"].dropna().unique()
172
+ ) if regions else all_stores
173
+
174
+ # Keep only stores that still belong to the selected region(s).
175
+ st.session_state["filter_stores"] = [
176
+ s for s in st.session_state["filter_stores"] if s in available_stores
177
+ ]
178
+
179
+ stores = st.sidebar.multiselect(
180
+ "Store",
181
+ available_stores,
182
+ key="filter_stores",
183
+ )
184
+
185
+ # If stores are selected, make region selection follow them exactly.
186
+ if stores:
187
+ inferred_regions = sorted(
188
+ df.loc[df["store_id"].isin(stores), "region"].dropna().unique()
189
+ )
190
+ if inferred_regions != regions:
191
+ st.session_state["filter_regions"] = inferred_regions
192
+ regions = inferred_regions
193
+
194
+ categories = st.sidebar.multiselect("Category", sorted(df["category"].dropna().unique()), default=[])
195
+ expiry_range = st.sidebar.slider("Days until expiry", 0, int(df["days_until_expiry"].max()), (0, 30))
196
+ weekend_choice = st.sidebar.selectbox("Day type", ["All", "Weekday", "Weekend"])
197
+
198
+ filtered = df.copy()
199
+ if regions:
200
+ filtered = filtered[filtered["region"].isin(regions)]
201
+ if stores:
202
+ filtered = filtered[filtered["store_id"].isin(stores)]
203
+ if categories:
204
+ filtered = filtered[filtered["category"].isin(categories)]
205
+ filtered = filtered[
206
+ (filtered["days_until_expiry"] >= expiry_range[0])
207
+ & (filtered["days_until_expiry"] <= expiry_range[1])
208
+ ]
209
+ if weekend_choice == "Weekday":
210
+ filtered = filtered[filtered["is_weekend"] == 0]
211
+ elif weekend_choice == "Weekend":
212
+ filtered = filtered[filtered["is_weekend"] == 1]
213
+ return filtered
214
+
215
+
216
+ def metric_row(df: pd.DataFrame):
217
+ c1, c2, c3, c4, c5 = st.columns(5)
218
+ c1.metric("Waste %", f"{df['waste_pct'].mean():.1%}")
219
+ c2.metric("Profit", f"€{df['profit'].mean():.2f}")
220
+ c3.metric("Sell-through", f"{df['sell_through_pct'].mean():.1%}")
221
+ c4.metric("Units wasted", f"{df['units_wasted'].mean():.1f}")
222
+ c5.metric("Markdown rate", f"{df['markdown_applied'].mean():.1%}")
223
+
224
+
225
+ def manager_dashboard(df: pd.DataFrame):
226
+ st.subheader("Manager Mode")
227
+ metric_row(df)
228
+
229
+ a, b = st.columns([1.2, 1])
230
+ with a:
231
+ trend = df.groupby(df["transaction_date"].dt.to_period("M").astype(str))[["waste_pct", "profit"]].mean().reset_index()
232
+ fig = go.Figure()
233
+ fig.add_trace(go.Scatter(x=trend["transaction_date"], y=trend["waste_pct"], name="Waste %", mode="lines+markers"))
234
+ fig.add_trace(go.Scatter(x=trend["transaction_date"], y=trend["profit"], name="Profit", mode="lines+markers", yaxis="y2"))
235
+ fig.update_layout(
236
+ title="Monthly Waste and Profit Trend",
237
+ yaxis=dict(title="Waste %"),
238
+ yaxis2=dict(title="Profit", overlaying="y", side="right"),
239
+ legend=dict(orientation="h"),
240
+ margin=dict(l=10, r=10, t=40, b=10),
241
+ )
242
+ st.plotly_chart(fig, use_container_width=True)
243
+ with b:
244
+ top_risk = (
245
+ df.groupby("category")[["waste_pct", "profit", "stock_demand_ratio"]]
246
+ .mean()
247
+ .sort_values("waste_pct", ascending=False)
248
+ .head(8)
249
+ .reset_index()
250
+ )
251
+ fig = px.bar(top_risk, x="waste_pct", y="category", orientation="h", title="High Waste Categories")
252
+ st.plotly_chart(fig, use_container_width=True)
253
+
254
+ c1, c2 = st.columns(2)
255
+ with c1:
256
+ store_risk = (
257
+ df.groupby("store_id")[["waste_pct", "profit", "temp_deviation"]]
258
+ .mean()
259
+ .sort_values(["waste_pct", "temp_deviation"], ascending=[False, False])
260
+ .head(15)
261
+ .reset_index()
262
+ )
263
+ st.dataframe(store_risk, use_container_width=True, hide_index=True)
264
+ with c2:
265
+ expiry = df.groupby("expiry_bucket")[["waste_pct", "profit", "discount_pct"]].mean().reset_index()
266
+ fig = px.line(expiry, x="expiry_bucket", y=["waste_pct", "profit", "discount_pct"], markers=True, title="Expiry Stage Performance")
267
+ st.plotly_chart(fig, use_container_width=True)
268
+
269
+
270
+
271
+ def forecast_region_demand(cat_df: pd.DataFrame, region: str) -> pd.DataFrame:
272
+ d = cat_df[cat_df["region"] == region].copy()
273
+ if d.empty:
274
+ return pd.DataFrame()
275
+ ts = d.groupby("transaction_date")["daily_demand"].mean().reset_index().sort_values("transaction_date")
276
+ if len(ts) < 14:
277
+ return pd.DataFrame()
278
+ recent = ts.tail(56).copy()
279
+ weekday_avg = recent.groupby(recent["transaction_date"].dt.dayofweek)["daily_demand"].mean().to_dict()
280
+ last_date = ts["transaction_date"].max()
281
+ future_dates = pd.date_range(last_date + pd.Timedelta(days=1), periods=14, freq="D")
282
+ future = pd.DataFrame({
283
+ "transaction_date": future_dates,
284
+ "daily_demand": [weekday_avg.get(d.dayofweek, ts["daily_demand"].tail(14).mean()) for d in future_dates],
285
+ "series": "Forecast"
286
+ })
287
+ hist = ts.tail(60).copy()
288
+ hist["series"] = "Actual"
289
+ return pd.concat([hist, future], ignore_index=True)
290
+
291
+
292
+ def manager_category_intelligence(df: pd.DataFrame):
293
+ st.subheader("Category Intelligence")
294
+ categories = sorted(df["category"].dropna().unique())
295
+ default_idx = categories.index(FOCUS_CATEGORY) if FOCUS_CATEGORY in categories else 0
296
+ focus = st.selectbox("Focus category", categories, index=default_idx)
297
+ cat_df = df[df["category"] == focus].copy()
298
+
299
+ if cat_df.empty:
300
+ st.warning("No data for the selected category.")
301
+ return
302
+
303
+ st.markdown(
304
+ f"Selected category: **{focus}**. This page compares regional operations, inventory, profitability, demand, stockout and waste trade-offs for a distinctive perishable category."
305
+ )
306
+
307
+ c1, c2, c3, c4 = st.columns(4)
308
+ c1.metric("Avg demand", f"{cat_df['daily_demand'].mean():.1f}")
309
+ c2.metric("Avg stock", f"{cat_df['initial_quantity'].mean():.1f}")
310
+ c3.metric("Stockout rate", f"{cat_df['stockout_flag'].mean():.1%}")
311
+ c4.metric("Waste rate", f"{cat_df['waste_pct'].mean():.1%}")
312
+
313
+ region_summary = (
314
+ cat_df.groupby("region")
315
+ .agg(
316
+ avg_demand=("daily_demand", "mean"),
317
+ avg_stock=("initial_quantity", "mean"),
318
+ avg_profit=("profit", "mean"),
319
+ avg_margin=("profit_margin_pct", "mean"),
320
+ waste_pct=("waste_pct", "mean"),
321
+ units_wasted=("units_wasted", "mean"),
322
+ markdown_rate=("markdown_applied", "mean"),
323
+ promo_rate=("is_promoted", "mean"),
324
+ temp_dev=("temp_deviation", "mean"),
325
+ shelf_life=("shelf_life_days", "mean"),
326
+ days_until_expiry=("days_until_expiry", "mean"),
327
+ stockout_rate=("stockout_flag", "mean"),
328
+ lost_sales=("lost_sales_units", "mean"),
329
+ )
330
+ .reset_index()
331
+ )
332
+
333
+ a, b = st.columns([1.2, 1])
334
+ with a:
335
+ melt = region_summary.melt(
336
+ id_vars="region",
337
+ value_vars=["avg_demand", "avg_stock", "avg_profit"],
338
+ var_name="metric",
339
+ value_name="value",
340
+ )
341
+ fig = px.bar(
342
+ melt, x="region", y="value", color="metric", barmode="group",
343
+ title=f"{focus}: regional operations, inventory and profit comparison",
344
+ )
345
+ st.plotly_chart(fig, use_container_width=True)
346
+ with b:
347
+ fig = px.scatter(
348
+ region_summary, x="stockout_rate", y="waste_pct", size="avg_profit", color="region",
349
+ hover_data=["avg_demand", "avg_stock", "markdown_rate", "promo_rate", "lost_sales"],
350
+ title=f"{focus}: stockout vs waste trade-off by region",
351
+ )
352
+ st.plotly_chart(fig, use_container_width=True)
353
+
354
+ c1, c2 = st.columns([1, 1.2])
355
+ with c1:
356
+ st.dataframe(region_summary.sort_values("avg_profit", ascending=False), use_container_width=True, hide_index=True)
357
+ with c2:
358
+ region_choice = st.selectbox("Forecast region", sorted(cat_df["region"].dropna().unique()))
359
+ forecast_df = forecast_region_demand(cat_df, region_choice)
360
+ if not forecast_df.empty:
361
+ fig = px.line(
362
+ forecast_df, x="transaction_date", y="daily_demand", color="series",
363
+ title=f"{focus}: 60-day actual + 14-day demand forecast for {region_choice}",
364
+ )
365
+ st.plotly_chart(fig, use_container_width=True)
366
+
367
+ st.markdown("### Regional recommendations")
368
+ mean_stockout = region_summary["stockout_rate"].mean()
369
+ mean_waste = region_summary["waste_pct"].mean()
370
+ mean_margin = region_summary["avg_margin"].mean()
371
+ mean_temp = region_summary["temp_dev"].mean()
372
+ for _, r in region_summary.iterrows():
373
+ advice = []
374
+ if r["stockout_rate"] > mean_stockout:
375
+ advice.append("raise replenishment and morning safety stock")
376
+ if r["waste_pct"] > mean_waste:
377
+ advice.append("start markdown earlier")
378
+ if r["avg_margin"] < mean_margin:
379
+ advice.append("use bundles instead of deeper discounts")
380
+ if r["temp_dev"] > mean_temp:
381
+ advice.append("tighten storage handling")
382
+ if not advice:
383
+ advice.append("maintain and scale current playbook")
384
+ st.markdown(f"- **{r['region']}**: " + "; ".join(advice) + ".")
385
+
386
+ st.markdown("### Marketing design simulator")
387
+ m1, m2, m3, m4 = st.columns(4)
388
+ promo_region = m1.selectbox("Target region", sorted(cat_df["region"].dropna().unique()), key="cat_region")
389
+ promo_type = m2.selectbox("Promo type", ["Early markdown", "Breakfast bundle", "Happy-hour discount", "Loyalty coupon"])
390
+ discount = m3.slider("Discount %", 0, 40, 15, key="cat_discount")
391
+ duration = m4.slider("Duration (days)", 1, 10, 4, key="cat_duration")
392
+
393
+ base = cat_df[cat_df["region"] == promo_region].copy()
394
+ base_sales = base["units_sold"].mean()
395
+ base_waste = base["waste_pct"].mean()
396
+ base_profit = base["profit"].mean()
397
+ promo_factor = {"Early markdown": 0.12, "Breakfast bundle": 0.16, "Happy-hour discount": 0.10, "Loyalty coupon": 0.08}[promo_type]
398
+ sales_lift = promo_factor + discount / 180 + min(duration / 60, 0.10)
399
+ waste_drop = min(0.42, promo_factor + discount / 200)
400
+ margin_drag = discount / 160
401
+ if promo_type == "Breakfast bundle":
402
+ margin_drag *= 0.75
403
+
404
+ est_sales = base_sales * (1 + sales_lift)
405
+ est_waste = max(base_waste * (1 - waste_drop), 0)
406
+ est_profit = base_profit * (1 + sales_lift - margin_drag)
407
+
408
+ x1, x2, x3 = st.columns(3)
409
+ x1.metric("Estimated avg units sold", f"{est_sales:.2f}", delta=f"+{(est_sales-base_sales):.2f}")
410
+ x2.metric("Estimated waste", f"{est_waste:.1%}", delta=f"-{(base_waste-est_waste):.1%}")
411
+ x3.metric("Estimated avg profit", f"€{est_profit:.2f}", delta=f"€{(est_profit-base_profit):.2f}")
412
+
413
+ def manager_inventory(df: pd.DataFrame):
414
+ st.subheader("Inventory & Replenishment")
415
+
416
+ overstock = df.copy()
417
+ overstock["recommended_order_qty"] = (
418
+ 1.2 * overstock["daily_demand"] * (1 + overstock["demand_variability"])
419
+ - overstock["leftover_units"]
420
+ )
421
+ overstock.loc[overstock["shelf_life_days"] <= 7, "recommended_order_qty"] *= 0.7
422
+ overstock.loc[overstock["spoilage_risk"] >= overstock["spoilage_risk"].quantile(0.75), "recommended_order_qty"] *= 0.8
423
+ overstock["recommended_order_qty"] = overstock["recommended_order_qty"].clip(lower=0).round()
424
+
425
+ c1, c2 = st.columns([1.3, 1])
426
+ with c1:
427
+ category_summary = overstock.groupby("category")[["initial_quantity", "recommended_order_qty", "waste_pct", "profit"]].mean().reset_index()
428
+ category_summary["order_reduction_pct"] = 1 - category_summary["recommended_order_qty"] / category_summary["initial_quantity"]
429
+ fig = px.bar(
430
+ category_summary.sort_values("order_reduction_pct", ascending=False),
431
+ x="order_reduction_pct",
432
+ y="category",
433
+ orientation="h",
434
+ title="Recommended Order Reduction by Category",
435
+ )
436
+ st.plotly_chart(fig, use_container_width=True)
437
+ with c2:
438
+ st.markdown("**Action shortlist**")
439
+ shortlist = overstock.sort_values(["waste_pct", "stock_demand_ratio"], ascending=[False, False])[[
440
+ "store_id", "product_name", "category", "initial_quantity", "daily_demand",
441
+ "days_until_expiry", "waste_pct", "recommended_order_qty"
442
+ ]].head(20)
443
+ st.dataframe(shortlist, use_container_width=True, hide_index=True)
444
+
445
+ st.markdown("### What-if Simulator")
446
+ col1, col2, col3 = st.columns(3)
447
+ selected_category = col1.selectbox("Category for simulation", sorted(df["category"].unique()))
448
+ order_cut = col2.slider("Reduce order quantity by %", 0, 40, 10)
449
+ markdown_shift = col3.slider("Advance markdown trigger by days", 0, 5, 2)
450
+
451
+ sim = df[df["category"] == selected_category].copy()
452
+ current_waste = sim["waste_pct"].mean()
453
+ current_profit = sim["profit"].mean()
454
+
455
+ waste_reduction = 0.35 * (order_cut / 100) + 0.015 * markdown_shift
456
+ sim_waste = max(current_waste * (1 - waste_reduction), 0)
457
+ sim_profit = current_profit * (1 + 0.08 * (order_cut / 100) + 0.03 * markdown_shift)
458
+
459
+ s1, s2, s3 = st.columns(3)
460
+ s1.metric("Current waste", f"{current_waste:.1%}")
461
+ s2.metric("Simulated waste", f"{sim_waste:.1%}", delta=f"-{(current_waste-sim_waste):.1%}")
462
+ s3.metric("Simulated avg profit", f"€{sim_profit:.2f}", delta=f"€{(sim_profit-current_profit):.2f}")
463
+
464
+
465
+ def manager_promotions(df: pd.DataFrame):
466
+ st.subheader("Promotion Designer")
467
+ left, right = st.columns([1, 1.2])
468
+ with left:
469
+ promo_category = st.selectbox("Promotion category", sorted(df["category"].unique()), key="promo_cat")
470
+ expiry_target = st.selectbox("Target expiry bucket", ["<=1d", "2-3d", "4-7d", "8-30d", ">30d"])
471
+ discount = st.slider("Discount %", 0, 50, 18)
472
+ bundle = st.checkbox("Bundle with complementary items", value=True)
473
+ weekend_only = st.checkbox("Weekend campaign only", value=False)
474
+
475
+ sub = df[(df["category"] == promo_category) & (df["expiry_bucket"].astype(str) == expiry_target)].copy()
476
+ if weekend_only:
477
+ sub = sub[sub["is_weekend"] == 1]
478
+
479
+ demand_lift = 0.08 + discount / 200
480
+ if bundle:
481
+ demand_lift += 0.06
482
+
483
+ est_sales_uplift = sub["units_sold"].mean() * demand_lift if len(sub) else 0
484
+ est_waste_drop = sub["waste_pct"].mean() * min(0.35, demand_lift) if len(sub) else 0
485
+ est_profit = sub["profit"].mean() * (1 + demand_lift - discount / 150) if len(sub) else 0
486
+
487
+ st.metric("Estimated sales uplift", f"{est_sales_uplift:.2f} units")
488
+ st.metric("Estimated waste reduction", f"{est_waste_drop:.1%}")
489
+ st.metric("Estimated avg profit", f"€{est_profit:.2f}")
490
+
491
+ with right:
492
+ promo_base = df.groupby(["expiry_bucket"])[["discount_pct", "waste_pct", "profit"]].mean().reset_index()
493
+ fig = px.bar(promo_base, x="expiry_bucket", y=["discount_pct", "waste_pct"], barmode="group", title="Current Discount vs Waste by Expiry")
494
+ st.plotly_chart(fig, use_container_width=True)
495
+
496
+ st.markdown("**Recommended promotion copy**")
497
+ st.info(
498
+ f"Run a {discount}% {promo_category} campaign for {expiry_target} items"
499
+ + (" on weekends" if weekend_only else "")
500
+ + (" with bundle offers" if bundle else " as single-item markdown")
501
+ + ". Position the offer at high-traffic display zones and highlight value + freshness."
502
+ )
503
+
504
+
505
+ def manager_risk(df: pd.DataFrame):
506
+ st.subheader("Risk & Store Operations")
507
+ _, importances = fit_risk_model(df)
508
+ c1, c2 = st.columns([1.1, 1])
509
+ with c1:
510
+ fig = px.bar(importances.head(10).sort_values(), orientation="h", title="Top Drivers of High Waste Risk")
511
+ st.plotly_chart(fig, use_container_width=True)
512
+ with c2:
513
+ heat = df.groupby(["region", "category"])["temp_deviation"].mean().reset_index()
514
+ fig = px.density_heatmap(heat, x="category", y="region", z="temp_deviation", title="Temperature Deviation Heatmap")
515
+ st.plotly_chart(fig, use_container_width=True)
516
+
517
+ alerts = (
518
+ df.groupby("store_id")[["temp_deviation", "temp_abuse_events", "waste_pct", "profit"]]
519
+ .mean()
520
+ .assign(alert_score=lambda x: 0.35 * x["temp_deviation"] + 0.25 * x["temp_abuse_events"] + 0.4 * x["waste_pct"] * 10)
521
+ .sort_values("alert_score", ascending=False)
522
+ .head(15)
523
+ .reset_index()
524
+ )
525
+ st.markdown("### Automated store alerts")
526
+ st.dataframe(alerts, use_container_width=True, hide_index=True)
527
+
528
+
529
+ def consumer_deals(df: pd.DataFrame):
530
+ st.subheader("Consumer Mode")
531
+ c1, c2, c3 = st.columns(3)
532
+ max_budget = c1.slider("Budget (€)", 5, 60, 20)
533
+ preferred_category = c2.selectbox("Preferred category", ["All"] + sorted(df["category"].unique()))
534
+ max_expiry = c3.slider("Maximum days until expiry", 1, 14, 5)
535
+
536
+ deals = df[df["days_until_expiry"] <= max_expiry].copy()
537
+ if preferred_category != "All":
538
+ deals = deals[deals["category"] == preferred_category]
539
+ deals = deals.assign(
540
+ savings=lambda x: x["base_price"] - x["selling_price"],
541
+ deal_score=lambda x: x["discount_pct"] * 0.5 + x["value_score"] * 0.35 + (x["profit_margin_pct"].clip(lower=0) / 100) * 0.15,
542
+ ).sort_values(["deal_score", "savings"], ascending=False)
543
+
544
+ display = deals[[
545
+ "product_name", "category", "store_id", "days_until_expiry",
546
+ "base_price", "selling_price", "discount_pct", "savings"
547
+ ]].head(25)
548
+ st.dataframe(display, use_container_width=True, hide_index=True)
549
+
550
+ fig = px.scatter(
551
+ deals.head(500), x="selling_price", y="discount_pct", color="category",
552
+ hover_data=["product_name", "store_id", "days_until_expiry"],
553
+ title="Discounted Items Map"
554
+ )
555
+ st.plotly_chart(fig, use_container_width=True)
556
+
557
+ affordable = deals[deals["selling_price"] <= max_budget].head(10)
558
+ if not affordable.empty:
559
+ st.markdown("### Best picks for your budget")
560
+ for _, row in affordable.iterrows():
561
+ st.success(
562
+ f"Now €{row['selling_price']:.2f} (save €{row['base_price'] - row['selling_price']:.2f}) · expires in {int(row['days_until_expiry'])} day(s)"
563
+ )
564
+ st.markdown(
565
+ f"""
566
+ 🛒 **{row['product_name']}**
567
+ 📦 Category: {row['category']}
568
+ 🏪 Store: {row['store_id']}
569
+ 💸 Discount: {row['discount_pct']*100:.0f}%
570
+ ⏳ Expiry: {row['days_until_expiry']} days
571
+ """
572
+ )
573
+
574
+
575
+ def build_bundle(df: pd.DataFrame, budget: float, people: int, theme: str):
576
+ work = df.copy()
577
+ work = work[work["days_until_expiry"] <= 7].copy()
578
+ work["score"] = work["value_score"] + work["discount_pct"]
579
+
580
+ theme_map = {
581
+ "Quick dinner": ["Ready_to_Eat", "Produce", "Bakery", "Dairy"],
582
+ "Healthy protein": ["Meat", "Seafood", "Dairy", "Produce"],
583
+ "Family breakfast": ["Bakery", "Dairy", "Beverages", "Produce"],
584
+ "Budget saver": list(work["category"].unique()),
585
+ }
586
+ cats = theme_map.get(theme, list(work["category"].unique()))
587
+ work = work[work["category"].isin(cats)].sort_values(["score", "selling_price"], ascending=[False, True])
588
+
589
+ chosen = []
590
+ remaining = budget
591
+ target_items = min(max(people + 1, 3), 6)
592
+ used_categories = set()
593
+
594
+ for _, row in work.iterrows():
595
+ if row["selling_price"] <= remaining:
596
+ if theme != "Budget saver" and row["category"] in used_categories:
597
+ continue
598
+ chosen.append(row)
599
+ remaining -= row["selling_price"]
600
+ used_categories.add(row["category"])
601
+ if len(chosen) >= target_items:
602
+ break
603
+
604
+ if not chosen:
605
+ return pd.DataFrame(), 0.0, 0.0
606
+ bundle = pd.DataFrame(chosen)
607
+ total = bundle["selling_price"].sum()
608
+ saved = (bundle["base_price"] - bundle["selling_price"]).sum()
609
+ return bundle, total, saved
610
+
611
+
612
+ def consumer_bundles(df: pd.DataFrame):
613
+ st.subheader("Bundle Builder")
614
+ c1, c2, c3 = st.columns(3)
615
+ budget = c1.slider("Bundle budget (€)", 8, 80, 25)
616
+ people = c2.slider("People", 1, 6, 2)
617
+ theme = c3.selectbox("Bundle theme", ["Quick dinner", "Healthy protein", "Family breakfast", "Budget saver"])
618
+
619
+ bundle, total, saved = build_bundle(df, budget, people, theme)
620
+ if bundle.empty:
621
+ st.warning("No bundle found for the current filters.")
622
+ return
623
+
624
+ k1, k2, k3 = st.columns(3)
625
+ k1.metric("Bundle total", f"€{total:.2f}")
626
+ k2.metric("You save", f"€{saved:.2f}")
627
+ k3.metric("Items", f"{len(bundle)}")
628
+
629
+ st.dataframe(bundle[[
630
+ "product_name", "category", "store_id", "selling_price", "base_price", "discount_pct", "days_until_expiry"
631
+ ]], use_container_width=True, hide_index=True)
632
+
633
+ st.info(
634
+ "Suggested marketing use: turn these bundles into one-click promotions for end customers or pre-designed campaign packs for store managers."
635
+ )
636
+
637
+
638
+ def consumer_personal(df: pd.DataFrame):
639
+ st.subheader("Personalized Promotions")
640
+ favorite = st.selectbox("Favorite category", sorted(df["category"].unique()))
641
+ price_cap = st.slider("Max item price (€)", 1, 30, 10)
642
+ not_too_close = st.checkbox("Hide items expiring within 1 day", value=False)
643
+
644
+ recs = df[df["category"] == favorite].copy()
645
+ recs = recs[recs["selling_price"] <= price_cap]
646
+ if not_too_close:
647
+ recs = recs[recs["days_until_expiry"] > 1]
648
+ recs = recs.assign(score=lambda x: x["discount_pct"] * 0.55 + x["value_score"] * 0.45).sort_values("score", ascending=False).head(12)
649
+
650
+ cols = st.columns(3)
651
+ for i, (_, row) in enumerate(recs.iterrows()):
652
+ with cols[i % 3]:
653
+ st.markdown(f"### {row['product_name']}")
654
+ st.write(f"{row['category']} · {row['store_id']}")
655
+ st.write(f"Now **€{row['selling_price']:.2f}** | Save **€{(row['base_price'] - row['selling_price']):.2f}**")
656
+ st.write(f"Expires in {int(row['days_until_expiry'])} day(s)")
657
+ st.button("Add to shortlist", key=f"short_{i}")
658
+
659
+
660
+ def main():
661
+ st.title("🥗 FreshWise")
662
+ st.caption("Perishable retail optimization for managers and consumers")
663
+
664
+ try:
665
+ df = load_data()
666
+ except Exception as e:
667
+ st.error(str(e))
668
+ st.stop()
669
+
670
+ filtered = apply_filters(df)
671
+ if filtered.empty:
672
+ st.warning("No data left after filtering.")
673
+ st.stop()
674
+
675
+ role = st.radio("Choose your mode", ["Manager", "Consumer"], horizontal=True)
676
+
677
+ if role == "Manager":
678
+ tabs = st.tabs([
679
+ "Executive Dashboard",
680
+ "Category Intelligence",
681
+ "Inventory & Replenishment",
682
+ "Promotion Designer",
683
+ "Risk Monitor",
684
+ ])
685
+ with tabs[0]:
686
+ manager_dashboard(filtered)
687
+ with tabs[1]:
688
+ manager_category_intelligence(filtered)
689
+ with tabs[2]:
690
+ manager_inventory(filtered)
691
+ with tabs[3]:
692
+ manager_promotions(filtered)
693
+ with tabs[4]:
694
+ manager_risk(filtered)
695
+ else:
696
+ tabs = st.tabs([
697
+ "Deal Finder",
698
+ "Bundle Builder",
699
+ "Personalized Promotions",
700
+ ])
701
+ with tabs[0]:
702
+ consumer_deals(filtered)
703
+ with tabs[1]:
704
+ consumer_bundles(filtered)
705
+ with tabs[2]:
706
+ consumer_personal(filtered)
707
+
708
+ with st.expander("About this app"):
709
+ st.markdown(
710
+ """
711
+ - **Manager mode** turns data into inventory, markdown, and operational decisions.
712
+ - **Consumer mode** surfaces discounted products, smart bundles, and personalized promotions.
713
+ - Built for deployment on Hugging Face Docker Spaces with Streamlit.
714
+ """
715
+ )
716
+
717
+
718
+ if __name__ == "__main__":
719
+ main()
gitattributes ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ perishable_goods_management.csv filter=lfs diff=lfs merge=lfs -text
perishable_goods_management.csv ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:de94302b867c9debedfd45c431306623fdfc038f5ed8ca17736339b4460a6674
3
+ size 21095333
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ streamlit==1.44.1
2
+ pandas==2.2.3
3
+ numpy==2.2.4
4
+ plotly==6.0.1
5
+ scikit-learn==1.6.1
6
+ pyarrow==19.0.1