Spaces:
Running
Running
Add Monthly forecast and sub-cat in P&L
Browse files- 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": "
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1880 |
cats = [_all_cats_label] + sorted(pl_filt["Cat"].dropna().unique().tolist())
|
| 1881 |
-
|
| 1882 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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")
|