Case study
Forecasting restaurant demand, and proving the number honestly
Five years in operations at a multi-unit quick-service restaurant group, and the one number that decided whether a shift went well or badly was a forecast nobody could inspect.
The operator problem
A restaurant schedule is a bet placed three to ten days before the demand shows up. Labor is the largest controllable line after food cost, and the forecast is what turns a guess about Thursday into a dollar figure and names on a shift. Two people too many across a lunch block is labor burned on bodies with nothing to do; two too few and the drive-thru backs up, speed of service collapses, and some fraction of the cars in line leave. The forecast was also opaque: it produced a number, and when the number was wrong there was no way to ask why, or to tell it the high school down the road had a staff in-service day. So I built the forecasting engine I wanted as an operator, then spent most of the effort proving whether it worked.
What the system does
It forecasts a store's sales by hour with a confidence band and emits the demand curve a scheduler assigns headcount against. The model is a multiplicative decomposition where each factor answers one question: a level term for current daily volume, a day-of-week factor, an intraday shape that distributes the day across its hours, and multipliers for known events and operator-entered day features.
Two decisions matter more than the math. The scheduler staffs to a service-level quantile rather than the mean, because understaffing costs more than overstaffing, so staffing to the expected value chronically underspends coverage; the default sits at the 65th percentile, and realized coverage is then checked against that target rather than assumed. And it proposes without ever publishing. A manager approves or overrides every plan, and a diagnostics block shows the engine's work, because the opacity was half the original problem.
How it is validated, and why that method
Rolling-origin backtesting. Pick an origin date, fit on everything strictly before it, predict the next six days, score against held-out actuals, advance the origin, repeat. The run below covers 39 weekly windows with an expanding training window and a 28-day minimum, over 44 weeks of real hourly sales from a location I operated. Every figure is out-of-sample.
The method matters more than the result. The reflex in tabular machine learning is k-fold cross-validation, which shuffles rows into folds. On a time series that puts next Tuesday in the training set and grades the model on last Tuesday. The model then scores well on a leaked future it will never have, and the inflation does not surface until after deployment, when the vendor's accuracy number and the operator's experience stop agreeing. Rolling origin is strictly causal: forward-only, fit on the past, graded on a future the model has not seen. An accuracy claim that does not say how it was validated is not yet a measurement.
A baseline, not just an accuracy figure. Restaurant demand is strongly weekly-seasonal, so "the same weekday and hour last week" is already a good predictor, and an accuracy number with no baseline cannot tell you whether the model contributed anything. So the harness reports a skill ratio: engine test error over seasonal-naive test error, where under 1.0 means it genuinely beats last week. It came in at 0.85 on mean absolute error and 0.82 on RMSE.
Bias and calibration, not only error size. A forecast can have a respectable error magnitude and still run systematically high, which means overspending labor every week. Bias came in at -0.5 percent, and the p65 staffing quantile covered 64.9 percent of actuals against its 65 percent target.
The results
| Granularity | Weighted MAPE | Accuracy (100 - WMAPE) |
|---|---|---|
| Daily total | 7.9% | 92.1% |
| Daypart | 13.4% | 86.6% |
| Hourly | 18.5% | 81.5% |
Daily is the figure the industry quotes, and it benefits from within-day error cancellation, where an over-forecast lunch nets against an under-forecast dinner. A staffing tool staffs by daypart and hour, so 86.6 and 81.5 percent are the operationally honest numbers and 92.1 percent is the cross-industry comparison.
I do not claim a higher hourly figure because a higher one is not physically available. Hourly tickets behave like Poisson arrivals, carrying irreducible noise on the order of one over the square root of the ticket count, which caps achievable hourly accuracy near 85 to 92 percent for a busy store. A vendor advertising 95 to 99 percent is quoting the daily total, where it is real.
Where forecasts actually fail
Holidays, by a wide margin. On normal operating weeks the engine runs 93.3 percent daily, close to the floor. The all-weeks 92.1 percent is dragged down by roughly 2.8 percent of scored hours falling on untagged holidays, which run 96.5 percent WMAPE on their own: the model predicts an ordinary day while the store is closed or on reduced hours. One operator-entered calendar event per holiday pulls that from 96.5 to 53.8 percent even with crude flat guesses, which makes the headline conservative, since a real deployment tags its holidays.
Weather, which I tested and which hurt. Weather buckets backfilled from a public API across all 44 weeks made the forecast worse by 0.21 points of daily WMAPE, and payday flags were pure noise. The test was generous besides, applying observed weather to target days, which production never has, so the measured lift is an upper bound and the upper bound was negative. The response was to make that layer safe rather than strong: a driver with high ratio-variance now shrinks toward no effect, so a scattered signal cannot do damage.
Cross-store pooling, the textbook answer to thin history, also lost on real data, by 0.5 to 2.6 points of hourly WMAPE, because the two locations I had sit in different trade areas with different weekday shapes. It stayed off by default.
Promotions and local events fail the same way. They are heterogeneous, so auto-learning fails on them while the operator already knows. The model owns the regular rhythm, the operator owns the exceptions, and the product's job is to make entering an exception cheap and its effect visible.
Benchmarks I did not round in my favor
Scored against the enterprise forecast the location actually runs, purpose-built for that chain, over 1,319 paired hours across 14 consecutive weeks: corporate came in at 5.7 percent daily and 16.1 percent hourly, mine at 6.9 and 16.2, so hourly is a tie and corporate is a point better at daily. Two thirds of that gap is holidays, where corporate has event inputs and mine had none, and one third is a systematic 4 percent weekend under-forecast. Two red-team passes ran against every number here, one re-deriving the baselines from scratch. Between them they dropped an earlier claim that mine beat corporate, downgraded a calibration gain to within-noise, and flagged the weather result as an upper bound.
What this means for an implementation role
I have been the customer. The operator on the other end of a restaurant technology implementation call is the person I was: one day of notice on a schedule, 32 people on a shift, a terminal that just went down, and a forecast they are told to trust and cannot interrogate. Rollouts rarely break on the model. They break on data that is not shaped the way the vendor assumed, a store whose hours change on holidays and whose system has no field for that, and a configuration step written as though someone has a free hour during a lunch rush.
Most of the value above is not the 92.1 percent. It is knowing that daily flatters the number, that holidays are where the error lives, that weather did not help, and that the honest fix is a tag an operator enters in five seconds. And I will say "the number does not support that" about my own work, which is the only thing that makes an accuracy claim worth anything to the person buying it.
Scope and honesty
Components of this platform have been used in live restaurant operations. The full platform is not in production, has no customer base and no paying clients, and I am not claiming otherwise. The validation above is a backtest on real hourly sales from a location I operated. The operator, the brand and the locations are deliberately not named.
This is AI-assisted development: I specify the system, direct language models to generate and debug the implementation, read and evaluate what comes back, and own the result end to end, which here means the method choices, the backtest design, the adversarial review, and the decision to delete every claim that did not survive it.