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

Forecast Tiew Copper revenue and customer

Browse files
Files changed (1) hide show
  1. streamlit_app.py +76 -38
streamlit_app.py CHANGED
@@ -86,6 +86,9 @@ LANG = {
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.",
@@ -173,6 +176,9 @@ LANG = {
173
  "fc_month_customers": "พยากรณ์จำนวนลูกค้า (เดือนนี้)",
174
  "fc_month_revenue": "พยากรณ์รายได้ (เดือนนี้, ประมาณการ)",
175
  "fc_month_basis": "ประมาณการรายได้จาก: พยากรณ์จำนวนลูกค้า × รายได้ต่อหัวเฉลี่ย 3 เดือนล่าสุดของแต่ละสาขา",
 
 
 
176
  "fc_header": "คอปเปอร์บุฟเฟ่ต์ — พยากรณ์และการจอง",
177
  "fc_caption": "พยากรณ์จำนวนลูกค้าและการจองที่ยืนยันแล้วสำหรับวันที่บริการในอนาคต "
178
  "ข้อมูลมีเฉพาะของคอปเปอร์บุฟเฟ่ต์เท่านั้น",
@@ -1690,59 +1696,91 @@ with tab_forecast:
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
 
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_month_basis_full":"Copper Buffet uses the model-based daily forecast (Prediction × Rev/Head per branch). "
90
+ "Tiew Copper has no per-day forecast, so its current-month tiles are estimated as the "
91
+ "trailing 3-month average of monthly Customers and Revenue per branch.",
92
  "fc_header": "Copper Buffet — Forecast & Bookings",
93
  "fc_caption": "Forecasted customer counts and confirmed bookings for upcoming "
94
  "service dates. Data is captured only for Copper Buffet.",
 
176
  "fc_month_customers": "พยากรณ์จำนวนลูกค้า (เดือนนี้)",
177
  "fc_month_revenue": "พยากรณ์รายได้ (เดือนนี้, ประมาณการ)",
178
  "fc_month_basis": "ประมาณการรายได้จาก: พยากรณ์จำนวนลูกค้า × รายได้ต่อหัวเฉลี่ย 3 เดือนล่าสุดของแต่ละสาขา",
179
+ "fc_month_basis_full":"คอปเปอร์บุฟเฟ่ต์ใช้พยากรณ์รายวันจากโมเดล (พยากรณ์ลูกค้า × รายได้ต่อหัวต่อสาขา) "
180
+ "เตี่ยวคอปเปอร์ไม่มีพยากรณ์รายวัน จึงประมาณค่าของเดือนนี้จาก "
181
+ "ค่าเฉลี่ยลูกค้าและรายได้ต่อเดือน 3 เดือนล่าสุดของแต่ละสาขา",
182
  "fc_header": "คอปเปอร์บุฟเฟ่ต์ — พยากรณ์และการจอง",
183
  "fc_caption": "พยากรณ์จำนวนลูกค้าและการจองที่ยืนยันแล้วสำหรับวันที่บริการในอนาคต "
184
  "ข้อมูลมีเฉพาะของคอปเปอร์บุฟเฟ่ต์เท่านั้น",
 
1696
  st.subheader(t("fc_header"))
1697
  st.caption(t("fc_caption"))
1698
 
1699
+ # ── This Month forecast — per-restaurant customer + revenue ────
1700
+ # Copper Buffet uses fact_predictions (model-based daily prediction
1701
+ # × trailing 3-month Rev/Head per branch). Tiew Copper has no
1702
+ # per-day forecast in the dataset, so it falls back to the
1703
+ # trailing 3-month average of monthly Customers / Revenue per
1704
+ # branch a reasonable "what we usually do" baseline.
1705
  st.markdown(f"**{t('fc_month_title')}**")
1706
  _now = pd.Timestamp(_dt.now().date())
1707
  _month_start = _now.replace(day=1)
1708
  _month_end = (_month_start + pd.offsets.MonthEnd(0)).normalize()
1709
 
1710
+ def _cb_month_forecast() -> tuple[int, float]:
1711
+ """Copper Buffet — predict from fact_predictions × Rev/Head."""
1712
+ _mp = fact_predictions.copy() if not fact_predictions.empty else pd.DataFrame()
1713
+ if _mp.empty:
1714
+ return 0, 0.0
1715
  _mp["Date"] = pd.to_datetime(_mp["Date"], errors="coerce")
1716
  _mp = _mp[(_mp["Date"] >= _month_start) & (_mp["Date"] <= _month_end)]
 
 
1717
  if "Restaurant" in _mp.columns:
1718
  _mp = _mp[_mp["Restaurant"] == "Copper Buffet"]
1719
+ if sel_branches and "Branch" in _mp.columns:
1720
+ _mp = _mp[_mp["Branch"].isin(sel_branches)]
1721
+ if _mp.empty:
1722
+ return 0, 0.0
1723
  # Latest snapshot per (Date, Branch).
1724
+ if "Date_Diff" in _mp.columns:
1725
  _mp = _mp.assign(_a=_mp["Date_Diff"].abs()) \
1726
  .sort_values("_a") \
1727
  .drop_duplicates(["Date", "Branch"], keep="first") \
1728
  .drop(columns="_a")
1729
+ cust = int(_mp["Prediction"].sum())
1730
+
1731
+ # Per-branch trailing 3-month Rev/Head from kpi_monthly.
1732
+ rev = 0.0
1733
+ if not kpi_monthly.empty and {"Restaurant", "Branch", "Rev_Per_Head", "Year", "Month"}.issubset(kpi_monthly.columns):
1734
+ hist = kpi_monthly[kpi_monthly["Restaurant"] == "Copper Buffet"].sort_values(["Year", "Month"])
1735
+ rph_map = (
1736
+ hist.groupby("Branch").tail(3)
1737
+ .groupby("Branch")["Rev_Per_Head"].mean().to_dict()
1738
+ )
1739
+ fallback = sum(rph_map.values()) / len(rph_map) if rph_map else 0.0
1740
+ for _br, _cust in _mp.groupby("Branch")["Prediction"].sum().items():
1741
+ rev += float(_cust) * rph_map.get(_br, fallback)
1742
+ return cust, rev
1743
+
1744
+ def _avg_month_forecast(restaurant: str) -> tuple[float, float]:
1745
+ """Trailing 3-month avg of monthly Customers and Revenue,
1746
+ summed across the branches the sidebar is filtered to."""
1747
+ if kpi_monthly.empty:
1748
+ return 0.0, 0.0
1749
+ df = kpi_monthly[kpi_monthly.get("Restaurant", "") == restaurant].copy()
1750
+ if sel_branches and "Branch" in df.columns:
1751
+ df = df[df["Branch"].isin(sel_branches)]
1752
+ if df.empty or not {"Year", "Month"}.issubset(df.columns):
1753
+ return 0.0, 0.0
1754
+ # One row per (Year, Month) — sum across whichever branches survived.
1755
+ monthly_totals = (
1756
+ df.groupby(["Year", "Month"], as_index=False)
1757
+ .agg(Customers=("Customers", "sum"),
1758
+ Revenue=("Revenue", "sum"))
1759
+ .sort_values(["Year", "Month"])
1760
+ .tail(3)
1761
  )
1762
+ if monthly_totals.empty:
1763
+ return 0.0, 0.0
1764
+ return monthly_totals["Customers"].mean(), monthly_totals["Revenue"].mean()
1765
+
1766
+ cb_cust, cb_rev = _cb_month_forecast()
1767
+ tc_cust, tc_rev = _avg_month_forecast("Tiew Copper")
1768
+
1769
+ # Copper Buffet block
1770
+ st.markdown("**Copper Buffet**")
1771
+ cb1, cb2 = st.columns(2)
1772
+ cb1.metric(t("fc_month_customers"), fmt_num(cb_cust))
1773
+ cb2.metric(t("fc_month_revenue"),
1774
+ fmt_money(cb_rev) if cb_rev > 0 else "")
1775
+
1776
+ # Tiew Copper block
1777
+ st.markdown("**Tiew Copper**")
1778
+ tc1, tc2 = st.columns(2)
1779
+ tc1.metric(t("fc_month_customers"), fmt_num(tc_cust))
1780
+ tc2.metric(t("fc_month_revenue"),
1781
+ fmt_money(tc_rev) if tc_rev > 0 else "—")
1782
+
1783
+ st.caption(t("fc_month_basis_full"))
1784
  st.divider()
1785
 
1786
  # Local horizon control — the sidebar date range is historical-focused