# Binzagr PoV: predictions for the demo dashboard (05_Predictions)

*Version 1, 23 Oct 2026. Riyadh van sales to 20 Oct 2026; Jeddah WMS to 30 Sep 2026. Builds on `02_Cleaned` and `03_Simulated`.*

This folder holds the predictions behind the wireframe screens (WF_1), as plain CSV files for the dashboard. There is one folder per screen. **No model is deployed.** Each prediction was made once, offline, and judged on weeks or outlets the model never saw.

| Start here | What it is |
|---|---|
| `00_KPI_TILES.csv` | Every headline tile, by screen. Each tile has the value, the text to display, a unit, a label and a note |
| `00_INDEX.csv` | Each file mapped to its screen, widget, grain (what one row is), row count and label |
| `00_COLUMNS.csv` | A description of every column in every file |
| `scripts/` | The code that produced the files (run order below) |

**Conventions**
- **Format:** UTF-8 with BOM, so Arabic names open correctly in Excel. Dates are YYYY-MM-DD. Amounts are SAR excluding VAT.
- **Weeks:** a week runs Wednesday to Tuesday and is named by its closing Tuesday (`week_end`), so the last full week ends on the data end, 20 Oct 2026. "Next week" means 21–27 Oct 2026.
- **IDs are text:**
  - Outlets: `outlet_id` = `dist_cd-cust_cd`, the same key in every outlet file.
  - Products: `sku` is the product code.
  - Vans: `van` is the rep code; rep names are never included.
  - Warehouse staff: `payroll_number` or `person_id` only; no names.
- **Labels** (wireframe legend):
  - **Actual:** measured in Binzagr data, or a test result measured on unseen data.
  - **Corrected:** rebuilt from related data, such as the receivables ageing.
  - **Simulated:** uses researched or assumed parameters, such as wages or margins.
  - **Forecast:** a model's prediction for weeks that were held out or have not happened yet.

---

## Screen by screen

### 1 Reconciliation (`01_reconciliation`)
This screen is not a prediction. It compares each Binzagr file total with the total we loaded:
- Sales and returns match to the riyal.
- Two differences are explained in the file: 174 extra customers came from the Lipton customer file, and 65,141 WMS rows were exact copies of the first day of the next month.

### 3 Forecast proof (`03_forecast`)
- **What:** weekly demand for every SKU (in cases), every van (in SAR) and every principal (in SAR).
  - The back-test covers 983 SKUs, 114 vans and 19 principals.
  - The forward forecast runs 12 weeks, to 12 Jan 2027, with a p10–p90 band and a suggested order quantity per SKU.
- **Selling status and orders:** each series is marked active, slowing or dormant from the weeks since its last sale, scaled to its own rhythm (the same idea as the outlet rule).
  - SKUs: 631 active, 90 slowing, 262 dormant.
  - Active SKUs get p50 + safety stock. Slowing SKUs get p50 only. Dormant SKUs get no order and go to review (delist, or a promotion to clear?). Without this rule, safety stock built while an item still sold would keep ordering it after it stopped.
  - Vans: 67 active, 2 slowing, 45 dormant. Filter on `selling_status` to hide vans that are off the road.
- **How:** 10 models raced on every series:
  - Moving average, SES, ETS and Theta.
  - Croston, SBA, TSB, ADIDA and IMAPA, for products that sell intermittently.
  - A global LightGBM model with lag, trend and Hijri-calendar features.
- **Choosing the strategy:** each level's strategy was chosen on the validation weeks (1–22 Sep) only:
  - SKU and van levels use LightGBM.
  - The principal level uses a 4-week moving average.
  - Picking a different winning model for each SKU was tested; it was not better than one global model.
  - Repeat-last-week is the benchmark, not a candidate.
- **Test:** the hold-out is 23 Sep–20 Oct 2026 (4 weeks). Each week was forecast from the data up to the week before, and those weeks were never used for fitting or for choosing a model.
- **Results:**

  | Level | Weekly error (WAPE) | Repeat-last-week |
  |---|---|---|
  | SKU | 73.3% | 98.2% (25% less error) |
  | Van | 38.1% | 51.7% |
  | Principal | 28.4% | 38.6% |

  - The 80% band held 85% of hold-out weeks.
  - Safety stock is 20% lower at the same 95% service level.
- **Be open about:**
  - In September (validation), repeat-last-week was slightly better at SKU level (69.6% vs 74.1%), because demand was flat. The model's advantage shows when demand moves.
  - At van and principal level the model wins in both periods.
  - Mid-October had a promotion-like burst, for example Tiffany Break mini bars at about 24,000 cases in one week. No model sees a promotion coming without a promotion calendar.
  - The p50 is a median, so SKU totals should come from the van or principal forecasts.

### 4 Warehouse labour (`04_warehouse_labour`)
- **Cost per activity:** the six activities on deck slide 6, plus pallet picking. Each shows SAR per 100 operations and how far the average is from the median and best-quartile person-month rate.
- **Badge-sharing fix:**
  - 625 high or medium-risk shifts.
  - Shifts over 12 hours are split into two people's hours: recorded hours vs corrected hours per person.
  - Each shift links to its scans by `person_session_id`.
- **Heads needed vs on shift:**
  - Workload is measured in standard hours, from Binzagr's own median standard times.
  - It is turned into heads at each role's median rate and at the top-quartile rate.
  - Excess head-hours in Aug–Sep: 22% at median rates.
- **Workload forecast for October 2026:** the October numbers can be checked against Binzagr's actual October data.
  - Method: the median of the last 4 same weekdays per site, chosen on Jun–Aug.
  - September back-test: 25.5% daily error vs 26.8% for repeat-last-week.
  - The October forecast is spread over each site's hour profile to give heads needed by hour.
- **Role x activity:** hours by WMS menu level and activity. There are no job titles in the data (gap G22), so "forklift / picker / loader" rows are not possible yet.
- **Tiles:**
  - Paid FTE: 161.
  - Labour cost: SAR 9.22M.
  - Gap to median: SAR 502K.
  - Gap to top quartile: SAR 910K. The base case claims 70% of the top-quartile gap, which is SAR 639K.

### 5 Van rep day (`05_van_rep_day`)
- **Next-best SKU:** up to 5 SKUs per outlet that it has not bought yet. Each comes with the chance it buys within 8 weeks, its typical spend, the expected value and a plain reason.
  - **Method:** the score blends what similar outlets buy, what the outlet's own van sells to its other outlets, and overall popularity. The weights were tuned on a 30 Jun back-test. Only SKUs sold somewhere in the last 4 weeks can be picked, so a rep is never sent to sell an item that has stopped flowing; in the 25 Aug back-test this changed no pick.
  - **Test:** picks made on 25 Aug 2026, judged on the 8 unseen weeks after it (to 20 Oct). 13.4% of picks were bought unprompted, against 3.4% for pushing best-sellers and 0.71% for random picks.
  - 43% of outlets bought at least one pick.
- **Visit plan for 21–27 Oct:** which outlets each van is likely to visit each day, with at-risk outlets first.
  - The model scored AUC 0.89 vs 0.85 for the same-weekday rule, tested on 14–20 Oct.
  - Routes are not fixed by weekday. At probability 0.3 or higher the list catches 32% of visits, and 40% of listed outlets are visited. Use it as a guide; `expected_visits_van_day` shows how many visits a day to expect.
- **Suggested load:**
  - p50 and p90 cases per van and SKU for next week, from a LightGBM quantile model across 4,971 van x SKU series.
  - On the hold-out, a p90 load covered 82% of van-SKU weeks and 49% of cases sold. The promotion burst explains the cases figure. Loading last week's quantity instead covered 38% of cases.
  - The load follows the same selling-status rule as the SKU order on screen 3: p90 for active SKUs, p50 for slowing ones, and no load for SKUs that have not sold anywhere for 8+ weeks (`sku_selling_status`, `load_rule`).
  - This stays Simulated until van load data arrives.
- **New van:** V95626 started on 14 Oct 2026 and already serves 43 outlets; 36 of them were served by other vans in the 12 weeks before. With one week of history it has a load suggestion (18 SKUs) but no van forecast or visit plan yet.
- **Productivity:** 6 outlets per van-day (best quartile 8). 65% of outlets are served by 2 or more vans.

### 6 Outlets (`06_outlets`)
- **Churn risk:** the chance an active outlet makes no purchase in the next 8 weeks.
  - **Model:** LightGBM on recency, frequency, spend trend, purchase rhythm, returns, free goods, and whether the outlet's van is still active. It was chosen over logistic regression on a 30 Jun validation.
  - **Test:** at 25 Aug, on 1,314 outlets the model never saw. AUC 0.77 vs 0.73 for the recency rule. The riskiest 20% caught 52% of the outlets that went quiet.
  - **Be open about:** sorting by days since the last purchase catches about the same share in its top 20% (53%). The model's edge is in ranking all outlets (AUC) and in giving each one a calibrated probability, not in a sharper top-20% list. Logistic regression scored slightly higher on the test (AUC 0.78), but it lost on validation, where the choice was made, so it was not used.
  - **Final scores:** isotonic-calibrated, as of 20 Oct.
- **Lapsed outlets:** 500 outlets, worth SAR 1.43M a year. Value is their average per year since their first purchase, which is conservative; their run-rate while active was SAR 3.26M. The win-back list adds the last van, the nearest active van (straight-line km) and the next-best SKUs.
- **Map:** all 18,232 customers with GPS, coloured by status, risk, spend band or van.

### 7 Receivables (`07_receivables`)
- **Ageing:** rebuilt FIFO from the 20 Oct balance snapshot and the van-sales invoices (Corrected).
  - Credit book: SAR 3.82M.
  - Past due: SAR 591K, plus SAR 336K that matches no invoice since March.
  - DSO: 24 days.
- **Collection list:** P1–P4 priority from rule points and exposure, with plain reasons:
  - balance above 2x monthly buying while orders slow
  - no purchase in 45+ days, or none since March, while a balance is open
  - more than 60 days past due
  - unmatched balance
  - high returns
  - over a real limit
  - outlet likely to go quiet
- **Who collects (`collect_via`):** the van rep when the account's van is still on the road; otherwise credit control.
- **Result:** 93 P1 accounts hold SAR 413K of exposure:
  - 38 are on active van routes (SAR 154K).
  - 55 go to credit control (SAR 259K), mostly accounts with no purchase since March.
- **Anomaly flag:** 119 credit accounts carry SAR 296K but have bought nothing since March, so none of it matches an invoice in the data. It may be older debt or another channel's invoices; the customer ledger would settle it.
- **Why rules, not a model:** there is no payment history to train one on.

### 2 Impact tree and 8 Value roll-up (`08_value_rollup`)
- **Levers:** ten levers, each with its driver (taken from the predictions above), formula, low / base / high SAR a year, one-off cash, labels and its proof screen.
- **Total:** SAR 2.18M a year (SAR 1.06M–3.79M), plus SAR 0.63M one-off cash.
- **Not added to the total:** the van-rep productivity view (it overlaps with whitespace and win-back) and the price-spread proxy (it needs trade-spend data). Both are still shown.
- **Sliders:** `value_assumptions.csv` holds the slider values (gross margin, conversion uplift, win-back rate, cost of capital, carrying cost, van-day cost and others). `formula` names the assumption ids, so totals can recompute live.
- **Biggest assumption:** route productivity (fleet right-sizing) depends most on assumptions (van-day cost, share captured). Present it as a range.

---

## Wireframe placeholders vs these files

Use these numbers. The wireframe text came from an earlier analysis.

| Screen | Wireframe text | These files |
|---|---|---|
| 1 | Invoice lines 325,456 · WMS scans 4,495,846 (112,973 duplicates) | 325,544 · 4,539,533 (65,141 duplicates removed) |
| 2 | SAR 2.81M (1.31M–5.30M) + 0.78M one-off | SAR 2.18M (range SAR 1.06M - 3.79M) + SAR 0.63M one-off |
| 3 | SKU error 63.8% vs 82.2%; 22% less error; van 38.6% vs 47.2%; safety stock −12% | 73.3% vs 98.2%; 25% less error; van 38.1% vs 51.7%; safety stock 20% lower |
| 4 | Logged FTE 68 (SAR 3.46M); gap to median 233K (6.7%); top quartile 449K (13%); 485 shared-badge shifts; 14% excess head-hours | 161 paid FTE (SAR 9.22M); SAR 502K; SAR 910K; 625 shifts; 22% |
| 5 | 12.0% vs 4.0% vs 0.66%; 58% of outlets; 6 a day (best quartile 9); 65% on 2+ vans; re-zoning −16% km | 13.4% vs 3.4% vs 0.71%; 43%; 6 (best quartile 8); 65%; re-zoning not produced |
| 6 | AUC 0.77 vs 0.68; riskiest 20% = 48%; 597 lapsed worth SAR 926K | AUC 0.77 vs 0.73; 52%; 500 lapsed worth SAR 1,431K |
| 7 | Credit book 3.58M; past due 684K (19%); DSO 22; 77 P1 = SAR 502K | SAR 3.82M; SAR 591K (15%) + SAR 336K unmatched; 24 days; 93 P1 = SAR 413K |

## Not produced, and limits
- **Foundation models:** Chronos-2, TimesFM 2.5 and TiRex were not run, because their weights could not be downloaded in the build environment. They appear in the model race as "not run" and can be added on a machine that has access.
- **Re-zoning (−16% km):** not produced; it needs road distances or route plans. The nearest active van is straight-line only.
- **Ramadan / Eid seasonality:** cannot be learned from one Ramadan. The forecasts carry the Hijri events as features. The 12-week horizon to 12 Jan 2027 holds no major Islamic holiday.
- **No stock on hand:** the suggested order is an order-up-to quantity, and the van load is a recommendation until van load / unload data arrives.
- **Churn means "went quiet":** an outlet with no purchase in 8 weeks. True closures need visit logs or geo-fence flags.
- **Warehouse heads:** these count time inside scanned sessions, so the 59% of paid time with no scan is not in them. Attendance by payroll number would close that gap.
- **Personal data:** outlet names come from Binzagr's customer master. Reps and warehouse staff appear only as codes or payroll numbers.

## Reproduce
From `scripts/`, with `BZ_ROOT` pointing to `Binzagr_Data`, run these in order:

`p00_prepare` → `p01_forecast_series` (`sku`, `van` and `principal`) → `p02_forecast_outputs` → `p03_reco` → `p03b_reco_select` → `p03c_reco_tune` → `p03d_reco_final` → `p04_churn` → `p05_van` → `p06_receivables` → `p07_warehouse` → `p08_outlets` → `p09_write` → `p10_docs` → `p11_readme`

Requirements: pandas, numpy, scikit-learn, lightgbm, statsforecast, scipy, pyarrow. Seeds are fixed, so a re-run on the same machine gives the same files.

Confidential: contains Binzagr data.
