Spaces:
Running
Running
Forecast Tiew Copper revenue and customer
Browse files- 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 —
|
| 1694 |
-
#
|
| 1695 |
-
#
|
| 1696 |
-
#
|
| 1697 |
-
#
|
| 1698 |
-
#
|
| 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 |
-
|
| 1705 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 1720 |
-
|
| 1721 |
-
|
| 1722 |
-
|
| 1723 |
-
|
| 1724 |
-
|
| 1725 |
-
|
| 1726 |
-
|
| 1727 |
-
|
| 1728 |
-
|
| 1729 |
-
|
| 1730 |
-
|
| 1731 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1732 |
)
|
| 1733 |
-
|
| 1734 |
-
|
| 1735 |
-
|
| 1736 |
-
|
| 1737 |
-
|
| 1738 |
-
|
| 1739 |
-
|
| 1740 |
-
|
| 1741 |
-
|
| 1742 |
-
|
| 1743 |
-
|
| 1744 |
-
|
| 1745 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|