Key findings
What moves a store's sales
Before modelling, the drivers. Each effect below is measured like for like: the same store, on the same weekday, in the same month, so one factor changes at a time.
Average daily sales by weekday
Per open store · promotions run Monday to Friday only
Sales lift from a promotion, by store format
Like for like · and how much of it comes from more customers
When a competitor opens nearby
Weekly sales across the chain
Four ways to forecast, tested honestly
Each method forecasts 6 weeks at a time, using only data from before the window starts, in four separate windows that include Christmas and Easter. This is how the forecast would be used for real, and it is the step most portfolio forecasts skip.
Error in each test window
WAPE: total absolute error as a share of actual sales · lower is better
The scorecard
All four windows pooled · store-days
RMSPE is the root mean square percentage error used in the original Kaggle competition. Bias above zero means the forecast runs high. "Off 15%+" is the share of store-days where a roster built on the forecast would clearly be wrong. "Weekly" is the WAPE on each store's weekly totals.
Forecast explorer
Actual daily sales against the LightGBM forecast and its 80% range, for a typical store of each format and for the hardest store in the latest window.
Where the forecast struggles
An average error hides the days that hurt. Splitting the error by situation shows where a planner should still apply judgement.
Error by situation
WAPE · LightGBM vs the planner average
How many stores get a usable forecast
What the model relies on
Share of the average contribution to each forecast (SHAP values), grouped
Competition looks minor here even though an opening costs about 10% of sales: once a competitor has opened, the drop shows up in the store's recent sales level, which the model already uses.
Can the range be trusted?
An 80% range should contain the actual value about 80% of the time
What I would do with this
A forecast only pays off once it changes a decision.
How it was built
SQL builds the features, Python runs the models, one command reruns everything (python run.py).
Bronze · raw
store-days and 1,115 store records, loaded as published.
Silver · cleaned
Gold · features and insights
- One row per store-day with 24 model features
- Only facts known in advance, or values at least 42 days old
- Insight tables: weekday, promotion, holiday and competitor effects
The four methods
Each one is a step up in effort, and each has to earn its place against the one before
| Method | How it forecasts | Uses promotions? |
|---|---|---|
| Seasonal naive | Same weekday 52 weeks earlier | No |
| Planner average | The store's average for that weekday over the last 12 weeks, split by promotion week. What a planner would build in a spreadsheet. | Yes |
| ETS | Exponential smoothing with a weekly season, fitted per store on the last year (statsforecast AutoETS) | No |
| LightGBM | One model for all stores, trained on log sales with calendar, promotion, holiday, refurbishment, competitor and store-history features. Two extra quantile models give the 80% range. | Yes |
No leakage: every feature for a forecast day is either known in advance (the promotion calendar, public and school holidays, planned closures, store facts) or measured at least 42 days earlier, so the same features exist on the day the forecast is made. Test windows are never used for training or tuning.