taotanapol commited on
Commit
2eb6a03
·
verified ·
1 Parent(s): 1c773ac

Update Forecast bug

Browse files
Files changed (1) hide show
  1. streamlit_app.py +119 -63
streamlit_app.py CHANGED
@@ -96,9 +96,11 @@ LANG = {
96
  "fc_month_customers": "Forecast Customers (this month)",
97
  "fc_month_revenue": "Forecast Revenue (this month, est.)",
98
  "fc_month_basis": "Revenue is estimated as forecast customers × trailing 3-month Rev/Head per branch.",
99
- "fc_month_basis_full":"Copper Buffet uses the model-based daily forecast (Prediction × Rev/Head per branch). "
100
- "Tiew Copper has no per-day forecast, so its current-month tiles are estimated as the "
101
- "trailing 3-month average of monthly Customers and Revenue per branch.",
 
 
102
  "fc_header": "Copper Buffet — Forecast & Bookings",
103
  "fc_caption": "Forecasted customer counts and confirmed bookings for upcoming "
104
  "service dates. Data is captured only for Copper Buffet.",
@@ -222,9 +224,11 @@ LANG = {
222
  "fc_month_customers": "พยากรณ์จำนวนลูกค้า (เดือนนี้)",
223
  "fc_month_revenue": "พยากรณ์รายได้ (เดือนนี้, ประมาณการ)",
224
  "fc_month_basis": "ประมาณการรายได้จาก: พยากรณ์จำนวนลูกค้า × รายได้ต่อหัวเฉลี่ย 3 เดือนล่าสุดของแต่ละสาขา",
225
- "fc_month_basis_full":"อร์บุฟเฟ่ต์ใชพยากณ์รายวันกโเดล (พยากรณ์ลูกค้า × รายได้ต่อหัวต่อสาขา) "
226
- "เตี่ยวคอปเปอร์ไมมีพยากรณ์รายวัน ึงประมณค่าของเดือนนี้จาก "
227
- "ค่าเฉลี่ยลูกค้าและรายได้ต่อเดือน 3 เดือนล่าสุดของแต่ละสาขา",
 
 
228
  "fc_header": "คอปเปอร์บุฟเฟ่ต์ — พยากรณ์และการจอง",
229
  "fc_caption": "พยากรณ์จำนวนลูกค้าและการจองที่ยืนยันแล้วสำหรับวันที่บริการในอนาคต "
230
  "ข้อมูลมีเฉพาะของคอปเปอร์บุฟเฟ่ต์เท่านั้น",
@@ -2149,75 +2153,127 @@ with tab_forecast:
2149
  st.subheader(t("fc_header"))
2150
  st.caption(t("fc_caption"))
2151
 
2152
- # ── This Month forecast — per-restaurant customer + revenue────
2153
- # Copper Buffet uses fact_predictions (model-based daily prediction
2154
- # × trailing 3-month Rev/Head per branch). Tiew Copper has no
2155
- # per-day forecast in the dataset, so it falls back to the
2156
- # trailing 3-month average of monthly Customers / Revenue per
2157
- # brancha reasonable "what we usually do" baseline.
 
2158
  st.markdown(f"**{t('fc_month_title')}**")
2159
  _now = pd.Timestamp(_dt.now().date())
2160
  _month_start = _now.replace(day=1)
2161
  _month_end = (_month_start + pd.offsets.MonthEnd(0)).normalize()
2162
 
2163
- def _cb_month_forecast() -> tuple[int, float]:
2164
- """Copper Buffet predict from fact_predictions × Rev/Head."""
2165
- _mp = fact_predictions.copy() if not fact_predictions.empty else pd.DataFrame()
2166
- if _mp.empty:
2167
- return 0, 0.0
2168
- _mp["Date"] = pd.to_datetime(_mp["Date"], errors="coerce")
2169
- _mp = _mp[(_mp["Date"] >= _month_start) & (_mp["Date"] <= _month_end)]
2170
- if "Restaurant" in _mp.columns:
2171
- _mp = _mp[_mp["Restaurant"] == "Copper Buffet"]
2172
- if sel_branches and "Branch" in _mp.columns:
2173
- _mp = _mp[_mp["Branch"].isin(sel_branches)]
2174
- if _mp.empty:
2175
- return 0, 0.0
2176
- # Latest snapshot per (Date, Branch).
2177
- if "Date_Diff" in _mp.columns:
2178
- _mp = _mp.assign(_a=_mp["Date_Diff"].abs()) \
2179
- .sort_values("_a") \
2180
- .drop_duplicates(["Date", "Branch"], keep="first") \
2181
- .drop(columns="_a")
2182
- cust = int(_mp["Prediction"].sum())
2183
-
2184
- # Per-branch trailing 3-month Rev/Head from kpi_monthly.
2185
- rev = 0.0
2186
- if not kpi_monthly.empty and {"Restaurant", "Branch", "Rev_Per_Head", "Year", "Month"}.issubset(kpi_monthly.columns):
2187
- hist = kpi_monthly[kpi_monthly["Restaurant"] == "Copper Buffet"].sort_values(["Year", "Month"])
2188
- rph_map = (
2189
- hist.groupby("Branch").tail(3)
2190
- .groupby("Branch")["Rev_Per_Head"].mean().to_dict()
2191
- )
2192
- fallback = sum(rph_map.values()) / len(rph_map) if rph_map else 0.0
2193
- for _br, _cust in _mp.groupby("Branch")["Prediction"].sum().items():
2194
- rev += float(_cust) * rph_map.get(_br, fallback)
2195
- return cust, rev
2196
-
2197
- def _avg_month_forecast(restaurant: str) -> tuple[float, float]:
2198
- """Trailing 3-month avg of monthly Customers and Revenue,
2199
- summed across the branches the sidebar is filtered to."""
2200
- if kpi_monthly.empty:
2201
  return 0.0, 0.0
2202
- df = kpi_monthly[kpi_monthly.get("Restaurant", "") == restaurant].copy()
2203
- if sel_branches and "Branch" in df.columns:
2204
- df = df[df["Branch"].isin(sel_branches)]
2205
- if df.empty or not {"Year", "Month"}.issubset(df.columns):
 
 
 
 
 
2206
  return 0.0, 0.0
2207
- # One row per (Year, Month) sum across whichever branches survived.
2208
- monthly_totals = (
2209
- df.groupby(["Year", "Month"], as_index=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2210
  .agg(Customers=("Customers", "sum"),
2211
  Revenue=("Revenue", "sum"))
2212
- .sort_values(["Year", "Month"])
2213
- .tail(3)
2214
  )
2215
- if monthly_totals.empty:
2216
- return 0.0, 0.0
2217
- return monthly_totals["Customers"].mean(), monthly_totals["Revenue"].mean()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2218
 
2219
  cb_cust, cb_rev = _cb_month_forecast()
2220
- tc_cust, tc_rev = _avg_month_forecast("Tiew Copper")
2221
 
2222
  # Copper Buffet block
2223
  st.markdown("**Copper Buffet**")
 
96
  "fc_month_customers": "Forecast Customers (this month)",
97
  "fc_month_revenue": "Forecast Revenue (this month, est.)",
98
  "fc_month_basis": "Revenue is estimated as forecast customers × trailing 3-month Rev/Head per branch.",
99
+ "fc_month_basis_full":"This month total combines actual customers and revenue for days that have already "
100
+ "passed (from kpi_daily) with projections for remaining days. Copper Buffet's "
101
+ "remaining days use the model's per-day prediction × trailing 3-month Rev/Head per "
102
+ "branch. Tiew Copper's remaining days are projected at the trailing 90-day average "
103
+ "per day-of-week, so weekdays and weekends are weighted separately.",
104
  "fc_header": "Copper Buffet — Forecast & Bookings",
105
  "fc_caption": "Forecasted customer counts and confirmed bookings for upcoming "
106
  "service dates. Data is captured only for Copper Buffet.",
 
224
  "fc_month_customers": "พยากรณ์จำนวนลูกค้า (เดือนนี้)",
225
  "fc_month_revenue": "พยากรณ์รายได้ (เดือนนี้, ประมาณการ)",
226
  "fc_month_basis": "ประมาณการรายได้จาก: พยากรณ์จำนวนลูกค้า × รายได้ต่อหัวเฉลี่ย 3 เดือนล่าสุดของแต่ละสาขา",
227
+ "fc_month_basis_full":"ดรวมดืนนี้วมขอมูลจิงของวันที่ผ่าแ้ว (าก kpi_daily) "
228
+ "กับการประมาณการสำหรับวันที่เหลือ คอปเปอร์บุฟเฟต์ใช้พยากรณ์รายวันจากโมเด "
229
+ "× รายได้ต่อหัวฉลี่ย 3 เดือนล่าสุดของแต่ละสาขา ส่วนเตี่ยวคอปเปอร์ "
230
+ "ใช้ค่าเฉลี่ย 90 วันล่าสุดตามวันในสัปดาห์สำหรับวันที่เหลือ "
231
+ "(วันธรรมดาและวันหยุดสุดสัปดาห์จะถูกถ่วงน้ำหนักแยกกัน)",
232
  "fc_header": "คอปเปอร์บุฟเฟ่ต์ — พยากรณ์และการจอง",
233
  "fc_caption": "พยากรณ์จำนวนลูกค้าและการจองที่ยืนยันแล้วสำหรับวันที่บริการในอนาคต "
234
  "ข้อมูลมีเฉพาะของคอปเปอร์บุฟเฟ่ต์เท่านั้น",
 
2153
  st.subheader(t("fc_header"))
2154
  st.caption(t("fc_caption"))
2155
 
2156
+ # ── This Month forecast — actual MTD + projection for remaining
2157
+ # For days that have already passed, use real customers + revenue
2158
+ # from kpi_daily. For days that haven't happened yet:
2159
+ # Copper Buffet model-based per-day prediction from
2160
+ # fact_predictions × trailing 3-month Rev/Head per branch.
2161
+ # Tiew Copper trailing 3-month average daily rate ×
2162
+ # remaining days (no per-day model exists for Tiew).
2163
  st.markdown(f"**{t('fc_month_title')}**")
2164
  _now = pd.Timestamp(_dt.now().date())
2165
  _month_start = _now.replace(day=1)
2166
  _month_end = (_month_start + pd.offsets.MonthEnd(0)).normalize()
2167
 
2168
+ def _actual_mtd(restaurant: str) -> tuple[float, float]:
2169
+ """Sum of actual Customers + Revenue from kpi_daily for days
2170
+ in the current month that are strictly before today."""
2171
+ if kpi_daily.empty:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2172
  return 0.0, 0.0
2173
+ kd = kpi_daily.copy()
2174
+ if "Date" in kd.columns:
2175
+ kd["Date"] = pd.to_datetime(kd["Date"], errors="coerce")
2176
+ if "Restaurant" in kd.columns:
2177
+ kd = kd[kd["Restaurant"] == restaurant]
2178
+ if sel_branches and "Branch" in kd.columns:
2179
+ kd = kd[kd["Branch"].isin(sel_branches)]
2180
+ kd = kd[(kd["Date"] >= _month_start) & (kd["Date"] < _now)]
2181
+ if kd.empty:
2182
  return 0.0, 0.0
2183
+ cust = float(kd["Customers"].sum()) if "Customers" in kd.columns else 0.0
2184
+ rev = float(kd["Revenue"].sum()) if "Revenue" in kd.columns else 0.0
2185
+ return cust, rev
2186
+
2187
+ def _cb_month_forecast() -> tuple[float, float]:
2188
+ """Copper Buffet — actual MTD + per-day prediction for remaining."""
2189
+ actual_cust, actual_rev = _actual_mtd("Copper Buffet")
2190
+ pred_cust = 0.0
2191
+ pred_rev = 0.0
2192
+ if not fact_predictions.empty:
2193
+ _mp = fact_predictions.copy()
2194
+ _mp["Date"] = pd.to_datetime(_mp["Date"], errors="coerce")
2195
+ # Only days from today onwards within current month.
2196
+ _mp = _mp[(_mp["Date"] >= _now) & (_mp["Date"] <= _month_end)]
2197
+ if "Restaurant" in _mp.columns:
2198
+ _mp = _mp[_mp["Restaurant"] == "Copper Buffet"]
2199
+ if sel_branches and "Branch" in _mp.columns:
2200
+ _mp = _mp[_mp["Branch"].isin(sel_branches)]
2201
+ if not _mp.empty:
2202
+ # Latest snapshot per (Date, Branch).
2203
+ if "Date_Diff" in _mp.columns:
2204
+ _mp = _mp.assign(_a=_mp["Date_Diff"].abs()) \
2205
+ .sort_values("_a") \
2206
+ .drop_duplicates(["Date", "Branch"], keep="first") \
2207
+ .drop(columns="_a")
2208
+ pred_cust = float(_mp["Prediction"].sum())
2209
+ # Per-branch trailing 3-month Rev/Head from kpi_monthly.
2210
+ if not kpi_monthly.empty and {"Restaurant", "Branch", "Rev_Per_Head", "Year", "Month"}.issubset(kpi_monthly.columns):
2211
+ hist = kpi_monthly[kpi_monthly["Restaurant"] == "Copper Buffet"].sort_values(["Year", "Month"])
2212
+ rph_map = (
2213
+ hist.groupby("Branch").tail(3)
2214
+ .groupby("Branch")["Rev_Per_Head"].mean().to_dict()
2215
+ )
2216
+ fallback = sum(rph_map.values()) / len(rph_map) if rph_map else 0.0
2217
+ for _br, _cust in _mp.groupby("Branch")["Prediction"].sum().items():
2218
+ pred_rev += float(_cust) * rph_map.get(_br, fallback)
2219
+ return actual_cust + pred_cust, actual_rev + pred_rev
2220
+
2221
+ def _tc_month_forecast() -> tuple[float, float]:
2222
+ """Tiew Copper — actual MTD + per-day projection using day-of-week
2223
+ weighted averages from the trailing 90 days.
2224
+
2225
+ Why day-of-week? Restaurant traffic varies sharply by DOW (weekends
2226
+ ≫ weekdays in most cases). Averaging by DOW means a Sunday at the
2227
+ end of the month gets projected at a Sunday-typical rate instead
2228
+ of a "mean of every day this quarter" rate, which would massively
2229
+ under-count weekend nights and over-count weekday nights.
2230
+ """
2231
+ actual_cust, actual_rev = _actual_mtd("Tiew Copper")
2232
+ remaining = pd.date_range(_now, _month_end, freq="D")
2233
+ if len(remaining) == 0 or kpi_daily.empty:
2234
+ return actual_cust, actual_rev
2235
+
2236
+ # Trailing 90 days before the start of the current month.
2237
+ trailing_start = _month_start - pd.Timedelta(days=90)
2238
+ kd = kpi_daily.copy()
2239
+ kd["Date"] = pd.to_datetime(kd["Date"], errors="coerce")
2240
+ kd = kd[kd.get("Restaurant", "") == "Tiew Copper"]
2241
+ if sel_branches and "Branch" in kd.columns:
2242
+ kd = kd[kd["Branch"].isin(sel_branches)]
2243
+ kd = kd[(kd["Date"] >= trailing_start) & (kd["Date"] < _month_start)]
2244
+ if kd.empty:
2245
+ return actual_cust, actual_rev
2246
+
2247
+ # Sum branches first to get a single per-day total, then average
2248
+ # across days within each day-of-week bucket. DOW: Mon=0 … Sun=6.
2249
+ daily_totals = (
2250
+ kd.groupby("Date", as_index=False)
2251
  .agg(Customers=("Customers", "sum"),
2252
  Revenue=("Revenue", "sum"))
 
 
2253
  )
2254
+ daily_totals["DOW"] = daily_totals["Date"].dt.dayofweek
2255
+ dow_avg = (
2256
+ daily_totals.groupby("DOW", as_index=False)
2257
+ .agg(AvgCust=("Customers", "mean"),
2258
+ AvgRev=("Revenue", "mean"))
2259
+ )
2260
+ # Fallback rate if a DOW has no historical samples (e.g. closed
2261
+ # on Mondays during the trailing window).
2262
+ fb_cust = float(daily_totals["Customers"].mean())
2263
+ fb_rev = float(daily_totals["Revenue"].mean())
2264
+ dow_cust = dict(zip(dow_avg["DOW"], dow_avg["AvgCust"]))
2265
+ dow_rev = dict(zip(dow_avg["DOW"], dow_avg["AvgRev"]))
2266
+
2267
+ proj_cust = 0.0
2268
+ proj_rev = 0.0
2269
+ for d in remaining:
2270
+ dow = int(d.dayofweek)
2271
+ proj_cust += float(dow_cust.get(dow, fb_cust))
2272
+ proj_rev += float(dow_rev.get(dow, fb_rev))
2273
+ return actual_cust + proj_cust, actual_rev + proj_rev
2274
 
2275
  cb_cust, cb_rev = _cb_month_forecast()
2276
+ tc_cust, tc_rev = _tc_month_forecast()
2277
 
2278
  # Copper Buffet block
2279
  st.markdown("**Copper Buffet**")