taotanapol commited on
Commit
cbe5ac1
·
verified ·
1 Parent(s): db6147c

Add Monthly forecast and sub-cat in P&L

Browse files
Files changed (1) hide show
  1. streamlit_app.py +88 -5
streamlit_app.py CHANGED
@@ -42,7 +42,7 @@ LANG = {
42
  "en": {
43
  # Tab labels
44
  "tab_overview": "Overview",
45
- "tab_summary": "Summary",
46
  "tab_forecast": "Forecast",
47
  "tab_pl": "P&L",
48
  "tab_inventory": "Inventory",
@@ -82,6 +82,10 @@ LANG = {
82
  "sm_chart_premium": "%Premium — premium share of customers",
83
  "sm_chart_rounds": "Customers by Round (monthly)",
84
  # Forecast tab
 
 
 
 
85
  "fc_header": "Copper Buffet — Forecast & Bookings",
86
  "fc_caption": "Forecasted customer counts and confirmed bookings for upcoming "
87
  "service dates. Data is captured only for Copper Buffet.",
@@ -104,6 +108,8 @@ LANG = {
104
  "pl_monthly_ts": "Monthly P&L Time Series",
105
  "pl_cat_picker": "Filter to one category",
106
  "pl_all_categories": "All categories",
 
 
107
  "pl_amount_axis": "Amount (THB)",
108
  # Inventory tab
109
  "inv_no_data": "No inventory rows for the current filters.",
@@ -123,7 +129,7 @@ LANG = {
123
  "th": {
124
  # Tab labels
125
  "tab_overview": "ภาพรวม",
126
- "tab_summary": "สรุป",
127
  "tab_forecast": "พยากรณ์",
128
  "tab_pl": "งบกำไรขาดทุน",
129
  "tab_inventory": "สินค้าคงคลัง",
@@ -163,6 +169,10 @@ LANG = {
163
  "sm_chart_premium": "%ลูกค้าพรีเมียม",
164
  "sm_chart_rounds": "ลูกค้าตามรอบ (รายเดือน)",
165
  # Forecast tab
 
 
 
 
166
  "fc_header": "คอปเปอร์บุฟเฟ่ต์ — พยากรณ์และการจอง",
167
  "fc_caption": "พยากรณ์จำนวนลูกค้าและการจองที่ยืนยันแล้วสำหรับวันที่บริการในอนาคต "
168
  "ข้อมูลมีเฉพาะของคอปเปอร์บุฟเฟ่ต์เท่านั้น",
@@ -185,6 +195,8 @@ LANG = {
185
  "pl_monthly_ts": "งบกำไรขาดทุนรายเดือน",
186
  "pl_cat_picker": "กรองเฉพาะหมวด",
187
  "pl_all_categories": "ทุกหมวด",
 
 
188
  "pl_amount_axis": "จำนวน (บาท)",
189
  # Inventory tab
190
  "inv_no_data": "ไม่มีข้อมูลสินค้าคงคลังสำหรับตัวกรองปัจจุบัน",
@@ -1678,6 +1690,61 @@ with tab_forecast:
1678
  st.subheader(t("fc_header"))
1679
  st.caption(t("fc_caption"))
1680
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1681
  # Local horizon control — the sidebar date range is historical-focused
1682
  # by default (Jan 1 → yesterday), so the forecast tab keeps its own.
1683
  horizon_days = st.slider(
@@ -1876,10 +1943,26 @@ with tab_pl:
1876
  st.subheader(t("pl_monthly_ts"))
1877
  pl_filt = pl_filt.copy()
1878
  pl_filt["YearMonth"] = pl_filt["Date"].dt.to_period("M").astype(str)
1879
- _all_cats_label = t("pl_all_categories")
 
 
 
 
1880
  cats = [_all_cats_label] + sorted(pl_filt["Cat"].dropna().unique().tolist())
1881
- cat_pick = st.selectbox(t("pl_cat_picker"), cats)
1882
- ts_src = pl_filt if cat_pick == _all_cats_label else pl_filt[pl_filt["Cat"] == cat_pick]
 
 
 
 
 
 
 
 
 
 
 
 
1883
  ts = (
1884
  ts_src.groupby(["YearMonth", "Restaurant"], as_index=False)["Amount"].sum()
1885
  .sort_values("YearMonth")
 
42
  "en": {
43
  # Tab labels
44
  "tab_overview": "Overview",
45
+ "tab_summary": "Sales",
46
  "tab_forecast": "Forecast",
47
  "tab_pl": "P&L",
48
  "tab_inventory": "Inventory",
 
82
  "sm_chart_premium": "%Premium — premium share of customers",
83
  "sm_chart_rounds": "Customers by Round (monthly)",
84
  # Forecast tab
85
+ "fc_month_title": "This Month Forecast",
86
+ "fc_month_customers": "Forecast Customers (this month)",
87
+ "fc_month_revenue": "Forecast Revenue (this month, est.)",
88
+ "fc_month_basis": "Revenue is estimated as forecast customers × trailing 3-month Rev/Head per branch.",
89
  "fc_header": "Copper Buffet — Forecast & Bookings",
90
  "fc_caption": "Forecasted customer counts and confirmed bookings for upcoming "
91
  "service dates. Data is captured only for Copper Buffet.",
 
108
  "pl_monthly_ts": "Monthly P&L Time Series",
109
  "pl_cat_picker": "Filter to one category",
110
  "pl_all_categories": "All categories",
111
+ "pl_subcat_picker": "Filter to one sub-category",
112
+ "pl_all_subcats": "All sub-categories",
113
  "pl_amount_axis": "Amount (THB)",
114
  # Inventory tab
115
  "inv_no_data": "No inventory rows for the current filters.",
 
129
  "th": {
130
  # Tab labels
131
  "tab_overview": "ภาพรวม",
132
+ "tab_summary": "ยอดขาย",
133
  "tab_forecast": "พยากรณ์",
134
  "tab_pl": "งบกำไรขาดทุน",
135
  "tab_inventory": "สินค้าคงคลัง",
 
169
  "sm_chart_premium": "%ลูกค้าพรีเมียม",
170
  "sm_chart_rounds": "ลูกค้าตามรอบ (รายเดือน)",
171
  # Forecast tab
172
+ "fc_month_title": "พยากรณ์ของเดือนนี้",
173
+ "fc_month_customers": "พยากรณ์จำนวนลูกค้า (เดือนนี้)",
174
+ "fc_month_revenue": "พยากรณ์รายได้ (เดือนนี้, ประมาณการ)",
175
+ "fc_month_basis": "ประมาณการรายได้จาก: พยากรณ์จำนวนลูกค้า × รายได้ต่อหัวเฉลี่ย 3 เดือนล่าสุดของแต่ละสาขา",
176
  "fc_header": "คอปเปอร์บุฟเฟ่ต์ — พยากรณ์และการจอง",
177
  "fc_caption": "พยากรณ์จำนวนลูกค้าและการจองที่ยืนยันแล้วสำหรับวันที่บริการในอนาคต "
178
  "ข้อมูลมีเฉพาะของคอปเปอร์บุฟเฟ่ต์เท่านั้น",
 
195
  "pl_monthly_ts": "งบกำไรขาดทุนรายเดือน",
196
  "pl_cat_picker": "กรองเฉพาะหมวด",
197
  "pl_all_categories": "ทุกหมวด",
198
+ "pl_subcat_picker": "กรองเฉพาะหมวดย่อย",
199
+ "pl_all_subcats": "ทุกหมวดย่อย",
200
  "pl_amount_axis": "จำนวน (บาท)",
201
  # Inventory tab
202
  "inv_no_data": "ไม่มีข้อมูลสินค้าคงคลังสำหรับตัวกรองปัจจุบัน",
 
1690
  st.subheader(t("fc_header"))
1691
  st.caption(t("fc_caption"))
1692
 
1693
+ # ── This Month forecast — total customers + estimated revenue ────
1694
+ # Sum the latest Prediction snapshot for every day in the current
1695
+ # calendar month, across whichever branches the sidebar is
1696
+ # filtered to. Revenue is derived per-branch by multiplying that
1697
+ # branch's forecast customers by its trailing 3-month average
1698
+ # Rev/Head (from kpi_monthly).
1699
+ st.markdown(f"**{t('fc_month_title')}**")
1700
+ _now = pd.Timestamp(_dt.now().date())
1701
+ _month_start = _now.replace(day=1)
1702
+ _month_end = (_month_start + pd.offsets.MonthEnd(0)).normalize()
1703
+
1704
+ _mp = fact_predictions.copy() if not fact_predictions.empty else pd.DataFrame()
1705
+ if not _mp.empty:
1706
+ _mp["Date"] = pd.to_datetime(_mp["Date"], errors="coerce")
1707
+ _mp = _mp[(_mp["Date"] >= _month_start) & (_mp["Date"] <= _month_end)]
1708
+ if sel_branches and "Branch" in _mp.columns:
1709
+ _mp = _mp[_mp["Branch"].isin(sel_branches)]
1710
+ if "Restaurant" in _mp.columns:
1711
+ _mp = _mp[_mp["Restaurant"] == "Copper Buffet"]
1712
+ # Latest snapshot per (Date, Branch).
1713
+ if "Date_Diff" in _mp.columns and not _mp.empty:
1714
+ _mp = _mp.assign(_a=_mp["Date_Diff"].abs()) \
1715
+ .sort_values("_a") \
1716
+ .drop_duplicates(["Date", "Branch"], keep="first") \
1717
+ .drop(columns="_a")
1718
+
1719
+ _month_cust = int(_mp["Prediction"].sum()) if "Prediction" in _mp.columns and not _mp.empty else 0
1720
+
1721
+ # Trailing 3-month Rev/Head per branch from kpi_monthly.
1722
+ _rph_map: dict[str, float] = {}
1723
+ if not kpi_monthly.empty and {"Restaurant", "Branch", "Rev_Per_Head", "Year", "Month"}.issubset(kpi_monthly.columns):
1724
+ _hist = kpi_monthly[kpi_monthly["Restaurant"] == "Copper Buffet"].copy()
1725
+ _hist = _hist.sort_values(["Year", "Month"])
1726
+ _rph_map = (
1727
+ _hist.groupby("Branch")
1728
+ .tail(3)
1729
+ .groupby("Branch")["Rev_Per_Head"]
1730
+ .mean()
1731
+ .to_dict()
1732
+ )
1733
+
1734
+ _month_rev = 0.0
1735
+ if not _mp.empty and _rph_map:
1736
+ _fallback = sum(_rph_map.values()) / len(_rph_map) if _rph_map else 0.0
1737
+ _branch_cust = _mp.groupby("Branch")["Prediction"].sum()
1738
+ for _br, _cust in _branch_cust.items():
1739
+ _month_rev += float(_cust) * _rph_map.get(_br, _fallback)
1740
+
1741
+ mc1, mc2 = st.columns(2)
1742
+ mc1.metric(t("fc_month_customers"), fmt_num(_month_cust))
1743
+ mc2.metric(t("fc_month_revenue"),
1744
+ fmt_money(_month_rev) if _month_rev > 0 else "—")
1745
+ st.caption(t("fc_month_basis"))
1746
+ st.divider()
1747
+
1748
  # Local horizon control — the sidebar date range is historical-focused
1749
  # by default (Jan 1 → yesterday), so the forecast tab keeps its own.
1750
  horizon_days = st.slider(
 
1943
  st.subheader(t("pl_monthly_ts"))
1944
  pl_filt = pl_filt.copy()
1945
  pl_filt["YearMonth"] = pl_filt["Date"].dt.to_period("M").astype(str)
1946
+
1947
+ # Two cascading filters: Category, then Sub-Category (only the
1948
+ # sub-cats that exist inside the chosen Cat are offered).
1949
+ _all_cats_label = t("pl_all_categories")
1950
+ _all_subcats_label = t("pl_all_subcats")
1951
  cats = [_all_cats_label] + sorted(pl_filt["Cat"].dropna().unique().tolist())
1952
+ cat_col, subcat_col = st.columns(2)
1953
+ with cat_col:
1954
+ cat_pick = st.selectbox(t("pl_cat_picker"), cats, key="pl_ts_cat")
1955
+ # Narrow pool first by category, then offer the sub-cats that exist
1956
+ # in that pool. If "All categories" is selected we draw sub-cats
1957
+ # from the whole filtered set.
1958
+ ts_pool = pl_filt if cat_pick == _all_cats_label else pl_filt[pl_filt["Cat"] == cat_pick]
1959
+ if "SubCat" in ts_pool.columns:
1960
+ subcats = [_all_subcats_label] + sorted(ts_pool["SubCat"].dropna().unique().tolist())
1961
+ with subcat_col:
1962
+ subcat_pick = st.selectbox(t("pl_subcat_picker"), subcats, key="pl_ts_subcat")
1963
+ ts_src = ts_pool if subcat_pick == _all_subcats_label else ts_pool[ts_pool["SubCat"] == subcat_pick]
1964
+ else:
1965
+ ts_src = ts_pool
1966
  ts = (
1967
  ts_src.groupby(["YearMonth", "Restaurant"], as_index=False)["Amount"].sum()
1968
  .sort_values("YearMonth")