Case study · Forecasting · Python & SQL

Store Sales Forecasting

Store managers plan staff and stock six weeks ahead, so every forecast error turns into an empty shelf or a queue at the till. This project forecasts daily sales for drugstores six weeks ahead, tests four methods the way they would really be used, and shows where each one breaks.

PythonSQLDuckDBLightGBMExponential smoothingBacktestingPrediction intervals

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

No promotionPromotion

Sales lift from a promotion, by store format

Like for like · and how much of it comes from more customers

Sales liftCustomer lift

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.

ActualLightGBM forecast80% rangePlanner average

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

Planner averageLightGBM

How many stores get a usable forecast

Planner averageLightGBM

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

    MethodHow it forecastsUses promotions?
    Seasonal naiveSame weekday 52 weeks earlierNo
    Planner averageThe store's average for that weekday over the last 12 weeks, split by promotion week. What a planner would build in a spreadsheet.Yes
    ETSExponential smoothing with a weekly season, fitted per store on the last year (statsforecast AutoETS)No
    LightGBMOne 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.