Forecasting for business: how to predict what you actually need

TL;DR

Difficulty: Intermediate

Forecasting is matching the method to the product, then being honest about the error.

  • Two families: numbers (time series, regression, machine learning) when you have stable history, structured judgement (Delphi, scenarios) when you do not.
  • Start simple: every model has to beat the naive and seasonal naive forecasts on data it has not seen.
  • Seasonality first: a moving average on seasonal data is always late. Use seasonal naive, Holt Winters, SARIMA or Prophet.
  • Give a range: a forecast without a prediction interval is of little use.
  • Check it: MAE, RMSE, MAPE, and the bias with its sign.

Forecasting sounds simple until you try to do it properly. I walk through the two families of methods, from the naive benchmark to SARIMA, Prophet, regression and the Delphi method, show which one fits which product in an invented Cork department store, and turn each forecast into a range you can check, with the Python to do it.

Two families of forecasting: numbers and judgement

Every forecasting method belongs to one of two families.

Quantitative forecasting uses historical numbers and a mathematical model to predict future values. It is data driven and repeatable, and it works well when you have reliable history and the future is likely to behave somewhat like the past.

Qualitative forecasting, also called judgemental forecasting, relies on expert opinion, market research and human judgement. It is subjective by nature, but it is often the only option when there is no history at all: a new product, a new competitor, a market that has never looked like this before.1

Neither is better. The trick is knowing when to use which, and often combining them. Where data exist, the standard advice is to start from a statistical forecast and let judgement adjust it, not the other way round.1

The worked examples in this guide are invented, and so are the businesses and every figure in them. The methods and their formulas follow Hyndman and Athanasopoulos, Forecasting: Principles and Practice, a textbook that is free to read online and the one I would hand anyone starting out.2

The four patterns inside a time series

A time series is simply one number recorded at regular intervals: sales per month, visitors per day, orders per week. Most of the quantitative methods below work on time series, and they all start from the same idea: the series is made of a few patterns laid on top of each other.3

Trend is the long term direction. Are sales broadly rising, falling or flat over the years? A trend does not have to be a straight line, and it can change direction.

Seasonality is a pattern tied to the calendar, with a fixed and known period: Christmas every December, a summer dip, busy weekends, a lunchtime rush.

Cycles are rises and falls without a fixed period, usually driven by the economy, and typically lasting two years or more. They look like seasonality, but they do not keep time.

Noise is what is left: the unpredictable variation that no model can capture.

The core assumption is that past patterns carry on. If umbrella sales rise every November, they will probably rise next November too.

Start with the simple benchmarks

Before anything clever, every forecast should be compared with the simplest possible ones. If a sophisticated model cannot beat them, it has not earned its keep. Hyndman and Athanasopoulos list four.4 The two that matter most for a shop are the naive method and the seasonal naive method.

The naive method says every future value equals the last one you saw:

y^T+h∣T=yT\hat{y}_{T+h|T} = y_T

Here y^T+h∣T\hat{y}_{T+h|T} is the forecast for hh periods ahead, made with data up to period TT, TT is the last period you have data for, hh is the number of periods ahead you are forecasting, and yTy_T is the last observed value. It is the baseline behind every comparison in Foundations: seasonality.

The seasonal naive method says every future value equals the last value from the same season:

y^T+h∣T=yT+h−m(k+1)\hat{y}_{T+h|T} = y_{T+h-m(k+1)}

Here mm is the length of the season (12 for monthly data with a yearly pattern), kk is the whole number part of (h−1)/m(h-1)/m, which counts the complete years between now and the forecast, and yT+h−m(k+1)y_{T+h-m(k+1)} is the observation exactly one season back from the period you are forecasting. The ideas of a season and a seasonal index are set out in Foundations: seasonality.

Worked example: naive and seasonal naive for sunglasses

An invented department store in Cork, which I will call Mulcahy's, sells designer sunglasses. Monthly sunglasses sales, in thousands of euro, were:

Month20242025
Jan1213
Feb1415
Mar2224
Apr3538
May5256
Jun6873
Jul7480
Aug6671
Sep3841
Oct2021
Nov1516
Dec2426
Total440474

It is now the end of December 2025, and the buyer wants a forecast for July 2026.

The naive method takes the last value, December 2025, and says July 2026 will be €26k. Every month of 2026 gets €26k. For a product that triples between spring and summer, that is plainly wrong, which is the point: it shows what ignoring seasonality costs.

The seasonal naive method looks one season back. July 2026 is h=7h = 7 months ahead, so kk is the whole number part of (7−1)/12=0.5(7-1)/12 = 0.5, which is 0. The formula points at yT+7−12=yT−5y_{T+7-12} = y_{T-5}, five months before December 2025, which is July 2025. The forecast is €80k.

Anything more sophisticated has to beat that on held back data to justify itself.

Moving averages smooth the past

A moving average replaces each value with the average of its neighbours, so short term wobbles cancel out and the underlying level shows through.5 A trailing moving average over the last kk periods is:

MAt=1k∑i=0k−1xt−iMA_t = \frac{1}{k}\sum_{i=0}^{k-1} x_{t-i}

Here MAtMA_t is the moving average at period tt, kk is the number of periods in the window, ∑i=0k−1\sum_{i=0}^{k-1} means add up the terms for ii from 0 to k−1k-1, and xt−ix_{t-i} is the value ii periods before tt, so the sum runs over the latest kk values. The same formula, with a worked example, is in Foundations: seasonality, and the plain average underneath it is in Foundations: the mean.

A moving average is a good tool for seeing the level of a stable series. It is a poor forecast for a seasonal one, because it always looks backwards. On highly seasonal data it gives forecasts that are always late and always wrong in the same direction.

For monthly data, the textbook way to strip out the seasonal pattern is a centred moving average over 12 months, averaged again over two (a 2 by 12 moving average), so that each calendar month gets the same weight.5 Dividing each month by that smooth level gives its seasonal index.

Worked example: a three month average misses the turn

Mulcahy's buyer tries a three month moving average to forecast October 2025 from the July, August and September figures:

MA=80+71+413=1923=64MA = \frac{80 + 71 + 41}{3} = \frac{192}{3} = 64

Here 8080, 7171 and 4141 are July, August and September sales in €k and 33 is the window, so the forecast is €64k. October actually came in at €21k. The average is three times too high, because it is still looking at summer.

Now the same data used the right way. Average monthly sales in 2025 were 474/12=39.5474 / 12 = 39.5 thousand euro. July's ratio to that level is 80/39.5=2.0380 / 39.5 = 2.03, and January's is 13/39.5=0.3313 / 39.5 = 0.33. (This simple ratio to the yearly average stands in for the 2 by 12 moving average, which gives a similar answer on a series this smooth.) July sells about twice an average month, January about a third. That is the seasonality, measured, and it is exactly what the three month average ignored.

Exponential smoothing weights the recent past more

Simple exponential smoothing is a weighted average of all past values, where the weights shrink the further back you go. What happened last month counts more than what happened three years ago.6 In its recursive form:

ℓt=αyt+(1−α)ℓt−1\ell_t = \alpha y_t + (1-\alpha)\ell_{t-1}

Here ℓt\ell_t is the smoothed level after period tt, which is also the forecast for every future period, α\alpha is the smoothing parameter between 0 and 1, yty_t is the value observed in period tt, and ℓt−1\ell_{t-1} is the level from the period before. A large α\alpha follows recent values closely, a small α\alpha gives older values more say, and α=1\alpha = 1 is the naive method. The weighted average idea builds on Foundations: the mean.

Simple exponential smoothing suits data with no clear trend and no seasonality. Holt's method adds a trend, and the Holt Winters method adds a seasonal component on top. Holt Winters comes in two versions: additive, when the seasonal swing is roughly the same size every year, and multiplicative, when the swing grows in proportion to the level.7 Sunglasses sales that grow and swing harder each summer are a multiplicative case.

Worked example: smoothing watch sales

Mulcahy's sells luxury watches, a category with little seasonality. Units sold over four months were 30, 34, 29 and 33. Start the level at the first value, ℓ0=30\ell_0 = 30, and use α=0.3\alpha = 0.3:

ℓ1=0.3×30+0.7×30=30ℓ2=0.3×34+0.7×30=10.2+21=31.2ℓ3=0.3×29+0.7×31.2=8.7+21.84=30.54ℓ4=0.3×33+0.7×30.54=9.9+21.378=31.28\begin{aligned} \ell_1 &= 0.3 \times 30 + 0.7 \times 30 = 30 \\ \ell_2 &= 0.3 \times 34 + 0.7 \times 30 = 10.2 + 21 = 31.2 \\ \ell_3 &= 0.3 \times 29 + 0.7 \times 31.2 = 8.7 + 21.84 = 30.54 \\ \ell_4 &= 0.3 \times 33 + 0.7 \times 30.54 = 9.9 + 21.378 = 31.28 \end{aligned}

Each line weights the new month by 0.30.3 and the previous level by 0.70.7, so the forecast for next month is ℓ4\ell_4, about 31 watches (the weighted average idea is in Foundations: the mean). With α=0.8\alpha = 0.8 the same steps give 30, 33.2, 29.84 and 32.37, a forecast of about 32. The higher α\alpha chases the latest month, the lower one holds steady. On noisy data like this, steady is usually the better bet, and in practice the software picks α\alpha by minimising the past forecast errors.

ARIMA, SARIMA and Prophet

Exponential smoothing describes a series by its trend and seasonality. ARIMA (AutoRegressive Integrated Moving Average) models take a different route and describe how each value relates to the values before it, the autocorrelation.8 Seasonal ARIMA (SARIMA) adds the same idea at the seasonal lag, so this July is related to last July. In the Python library statsmodels, the seasonal part is set with four numbers (P,D,Q,s)(P, D, Q, s), where ss is the season length, 12 for monthly data.9 When you have several years of a strong, steady seasonal pattern, SARIMA is often a sound choice.

Prophet is an open source forecasting tool released by Facebook's Core Data Science team. It fits an additive model with trend, yearly and weekly seasonality and holiday effects, and it is robust to missing data and shifts in the trend.10 That makes it practical for business data with bank holidays, promotions and the odd gap, which is exactly the data most shops have.

I have written separately about demand forecasting for eCommerce with Prophet and SARIMA, so here they get one paragraph each and a code example further down.

Regression forecasts from drivers, not from the calendar

Time series methods ask "what will sales be next month, given previous months?" Regression asks a different question: "what drives sales, and what do those drivers say about next month?"11

yt=β0+β1xt+εty_t = \beta_0 + \beta_1 x_t + \varepsilon_t

Here yty_t is the value you want to forecast in period tt (sales, say), xtx_t is the driver in that period (temperature, footfall, ad spend), β0\beta_0 is the intercept, the predicted value of yty_t when xtx_t is zero, β1\beta_1 is the slope, the average change in yty_t for one more unit of xtx_t, and εt\varepsilon_t is the error, everything else that moves yty_t besides xtx_t. The method, with R2R^2 and its limits, is set out in Foundations: regression.

Multiple regression uses several drivers at once: temperature, day of the week, payday, a rail strike. Polynomial terms handle relationships that bend.

The catch is easy to miss. To forecast sales from next week's temperature, you first need next week's temperature, which is itself a forecast. A genuine forecast can only use information available at the time it is made.12 And a regression shows that drivers move together, not that one causes the other.

Worked example: sunglasses and the weather

Suppose a regression on two summers of weekly data at Mulcahy's gives:

sales=2,000+450×temperature\text{sales} = 2{,}000 + 450 \times \text{temperature}

Here sales\text{sales} is weekly sunglasses sales in euro, 2,0002{,}000 the intercept, 450450 the slope in euro per degree and temperature\text{temperature} the week's average daytime maximum in degrees Celsius. The model was fitted on weeks between 12°C and 28°C.

The forecast says next week's temperature will be 24°C:

2,000+450×24=2,000+10,800=12,8002{,}000 + 450 \times 24 = 2{,}000 + 10{,}800 = 12{,}800

So about €12,800. A cooler 18°C forecast gives 2,000+450×18=10,1002{,}000 + 450 \times 18 = 10{,}100, about €10,100. Each extra degree is worth about €450 a week, inside the 12°C to 28°C range only. At 35°C the model would say €17,750, but it has never seen a 35°C week in Cork, so it has no business saying anything.

Machine learning earns its place at scale

For many related series with many drivers, machine learning models can beat the classic statistical methods. I have written about where AI ends and machine learning begins, so here only the parts that matter for forecasting.

Random forests combine many decision trees and handle relationships that are not straight lines. Gradient boosted trees (XGBoost, LightGBM) build trees one after another, each correcting the last. Neural networks such as LSTMs are designed for long sequences where something from months ago still matters.

The evidence is worth knowing. In the M5 forecasting competition, built on tens of thousands of retail sales series, the top ranks in both tracks were dominated by entries using gradient boosted trees, LightGBM above all, ahead of deep learning entries.13 That is a result for large sets of related series with rich features. For one product line with three years of monthly data, it says very little.

The trade off is interpretability. A linear regression tells you exactly how each driver contributes to the forecast. A boosted model with 200 features might be more accurate, but explaining why it predicted a particular number to a buyer is much harder.

Judgement, when the numbers are not enough

When there is no history, or the history no longer applies, the forecast has to come from people. Judgement can be structured, and structure is what separates it from guessing.

How the Delphi method works

Delphi is structured expert consensus, developed at the RAND Corporation in the 1950s.14 A facilitator gives a panel of experts the forecasting task. Each expert forecasts independently, with a short justification. The facilitator summarises the answers and sends them back. The experts revise, and the round repeats, usually two or three times, until the spread of answers has narrowed enough. Consensus does not mean everyone gives the same number.14

The anonymity is the point. Nobody knows who said what, so seniority and loud voices cannot steamroll the room, and nobody anchors on the first number said aloud.

Market research and surveys

Sometimes the best way to predict what customers will buy is to ask them: surveys ("would you buy this at this price?"), focus groups, structured purchase intent studies.

The weakness is that a survey records what people say they will do, and the till records what they do. Someone may say they would definitely buy the sustainable option, then pick the lower priced one at the checkout. Treat stated intent as an upper bound, not a forecast.

The sales force composite

Staff on the shop floor know which products customers ask about and which gather dust. Adding up their estimates puts that to work. The risk is bias: optimism or caution, depending on how they are paid, and too much weight on last week.

Executive opinion

Senior leaders pool their judgement. It is quick and it costs little. The danger is groupthink and the HiPPO problem, where the Highest Paid Person's Opinion wins. Ask for independent estimates before anyone discusses them.

Scenario planning

Rather than predicting one future, you describe several plausible ones and forecast each: "if the economy grows, X. If there is a recession, Y. If a new competitor opens on the main street, Z." It is the tool for strategy and risk rather than for the budget line.

Adjusting a statistical forecast by hand

The most common form of judgement in practice is an adjustment to a model's forecast. The evidence is clear on when it helps: when you have real information the model does not, such as a planned promotion or a big event. Small adjustments made because a forecast "feels low" tend to make accuracy worse.15 Adjust sparingly, and write down every adjustment and its reason.

Seasonal or not: check before you choose

Seasonality cuts across both families. It comes at several scales: yearly (Christmas, summer, back to school), monthly (payday), weekly (weekends) and daily (the lunch rush).

When the pattern is there, the method must handle it: seasonal naive, Holt Winters, SARIMA, Prophet, or seasonal terms in a regression. When it genuinely is not there, as for industrial equipment bought on capital budgets rather than by season, simpler methods do well: simple exponential smoothing, ARIMA without seasonal terms, a regression without them, or the drift method that extends the line from the first observation to the last.4

The danger is assuming a series has no seasonality when you simply have not looked. Plot it, decompose it, check. For a yearly pattern, I want at least two, better three, years of history before trusting what the data shows.

Which method, when

The choice comes down to a few questions, asked in order.

Choosing a forecasting approachStart with the product to forecast. Plot it and look for trend and seasonality. If there are two or more years of stable history, start with the simple benchmarks and then try smoothing, SARIMA or a regression on drivers. If not, start with structured judgement such as a Delphi panel, analogous launches and three scenarios. Either way, measure the error on held back data and adjust only with real new information.A product or category to forecastPlot the history and look for trend andseasonalityTwo or more years of stablehistory?yesStart with the benchmarks, then trysmoothing, SARIMA or a regression ondriversnoStart with structured judgement: aDelphi panel, analogous launches, threescenariosMeasure the error on held back data,adjust only with real new information

The diagram reads from top to bottom. The one decision is about history: enough stable data sends you to the numbers, too little sends you to judgement, and both paths end in the same place, with the forecast measured against what actually happened.

Combine both when you want to adjust a model with judgement, check it against intuition, or handle categories that behave differently.

Worked example: one department store, six categories

I have worked on forecasting for a large department store, and the shape of this example comes from that kind of work. The store here, Mulcahy's in Cork, is invented, and so are all its figures and weights. The point is that one business needs several methods at once.

Designer sunglasses: seasonal, quantitative

Years of history and a steady summer peak make sunglasses a SARIMA case. A regression element on temperature forecasts helps, because a warm April pulls summer demand forward and a wet June holds it back.

Luxury watches: little seasonality, mixed methods

A €10,000 watch is not bought because the sun is out. Watch sales move instead with bonus season in finance, stock market performance, visitors from abroad, exchange rates and new model releases. Mulcahy's uses a multiple regression on those drivers, combined with input from the watch buyers, who know when new models are coming and which pieces have waiting lists.

An example of the blend: the store gives the model 60% weight and the buyers 40% (the example's assumption). The model says 40 watches next month, the buyers say 50:

0.6×40+0.4×50=24+20=440.6 \times 40 + 0.4 \times 50 = 24 + 20 = 44

Here 0.60.6 and 0.40.4 are the two weights, 4040 the model's forecast and 5050 the buyers', so the forecast is 44 watches.

A new designer collection: pure judgement

A new exclusive collection has no history, so the forecast comes from a Delphi style panel of buyers, merchandisers and marketing, from analogous launches ("this looks like the collection two years ago, which did this"), from press and social media interest, and from pre orders. The panel produces three scenarios, cautious, expected and optimistic, and the store plans stock and staffing around them.

Food hall confectionery: seasonal, with events

Chocolates and biscuits have strong seasonality (Christmas is massive, Easter significant, Valentine's Day and Mother's Day moderate) and spikes from events: St Patrick's weekend, in store events, brand collaborations. Prophet suits this, because it handles weekly and yearly seasonality, a custom calendar of Irish holidays, changes in trend and gaps in the data, such as the months when shops were shut in the pandemic. A one off collaboration gets a judgemental overlay.

Beauty: mixed seasonality, heavy promotions

Some beauty products are seasonal (sun cream in summer, rich moisturisers in winter), others are not, and promotions drive large swings. Mulcahy's uses a baseline from Prophet or SARIMA, a regression that estimates the lift from each type of promotion, and judgement for launches. The promotion model matters most: a gift with purchase might triple sales for a week, and the baseline needs to know that was a promotion, or it will think demand has tripled for good.

Heritage handbags: supply constrained

For some heritage brands demand exceeds supply, and there are waiting lists. The question is not "how many will customers want?" but "how many will the brand allocate to us?" That flips the problem: a supply forecast from the brand relationship, an estimate of true demand even when sales are capped by stock, and a forecast of how many people on the waiting list actually buy. Mostly judgement, and mostly relationships.

A forecast is a range, not a number

A single number is always wrong. The useful question is how wrong. A prediction interval gives the range the actual value will fall in with a stated probability, and the textbook is blunt about it: point forecasts can be of almost no value without one.16

y^T+h∣T±c σ^h\hat{y}_{T+h|T} \pm c \, \hat{\sigma}_h

Here y^T+h∣T\hat{y}_{T+h|T} is the point forecast for hh periods ahead, cc is a multiplier set by the coverage you want (1.28 for 80%, 1.96 for 95%), and σ^h\hat{\sigma}_h is the estimated standard deviation of the forecast error at horizon hh. For the seasonal naive method σ^h=σ^k+1\hat{\sigma}_h = \hat{\sigma}\sqrt{k+1}, where σ^\hat{\sigma} is the standard deviation of past forecast errors and kk counts the complete years ahead, as before.16 The interval rests on Foundations: confidence intervals, and the standard deviation on Foundations: spread. The formula assumes the errors are roughly normal and average zero.

Worked example: an interval for July sunglasses

Go back to the seasonal naive forecast for July 2026: €80k. To size the interval, look at how the same method did in 2025, forecasting each month from the same month in 2024. The errors (2025 minus 2024) were, in €k: 1, 1, 2, 3, 4, 5, 6, 5, 3, 1, 1, 2. Their standard deviation is 1.80.

July 2026 is less than a year ahead, so k=0k = 0 and σ^h=1.80×1=1.80\hat{\sigma}_h = 1.80 \times \sqrt{1} = 1.80. The 95% interval is:

80±1.96×1.80=80±3.5380 \pm 1.96 \times 1.80 = 80 \pm 3.53

So €76.5k to €83.5k. The 80% interval is 80±1.28×1.80=80±2.3080 \pm 1.28 \times 1.80 = 80 \pm 2.30, €77.7k to €82.3k.

But look at those errors again. Every one is positive, with an average of 2.83. The method undershot every month, because the business is growing, and the interval formula assumes errors that average zero. The honest fix is to add the growth. 2025 was 474/440=1.077474 / 440 = 1.077 times 2024, so a growth adjusted forecast for July 2026 is 80×1.077=86.280 \times 1.077 = 86.2, about €86k. That sits outside the interval just calculated, which is the lesson: an interval is only as honest as the errors behind it.

Monthly sunglasses sales and a growth adjusted forecastLine chart of monthly sunglasses sales at an invented Cork department store, in thousands of euro. 2024 runs from 12 in January to a peak of 74 in July and 24 in December. 2025 runs from 13 to a peak of 80 in July and 26 in December. The 2026 forecast, 2025 times 1.077, runs from 14.0 in January to a peak of 86.2 in July and 28.0 in December.Sunglasses sales by month, € thousand€0k€25k€50k€75k€100kJanMarMayJulSepNovDec2024 actual2025 actual2026 forecast€ thousandMonth

The horizontal axis is the calendar month, the vertical axis is monthly sunglasses sales in thousands of euro. The three lines have the same shape, a summer peak in July and a small bump in December for gifts, and each year sits slightly above the last. The takeaway is that the seasonal shape is very stable, so the hard part of this forecast is not the shape but the growth, and that is where the uncertainty belongs.

Measuring accuracy honestly

Forecasting without checking is guessing with extra steps. The error for each period is the actual minus the forecast, and three summaries of it are standard.17

MAE=1n∑t=1n∣et∣,RMSE=1n∑t=1net2\text{MAE} = \frac{1}{n}\sum_{t=1}^{n} |e_t|, \qquad \text{RMSE} = \sqrt{\frac{1}{n}\sum_{t=1}^{n} e_t^2}

Here ete_t is the forecast error in period tt (actual minus forecast), nn is the number of periods checked, ∣et∣|e_t| is the error without its sign, and et2e_t^2 is the error squared. MAE (mean absolute error) is the typical miss in the data's own units. RMSE (root mean squared error) punishes large misses more, because squaring makes them bigger. Both are measures of spread, explained in Foundations: spread.

MAPE=1n∑t=1n∣100 etyt∣\text{MAPE} = \frac{1}{n}\sum_{t=1}^{n} \left| \frac{100\, e_t}{y_t} \right|

Here yty_t is the actual value in period tt, ete_t and nn are as before, and the 100 turns each error into a percentage of the actual. MAPE (mean absolute percentage error) lets you compare a €15k category with a €180k one. It breaks when actual values are zero or close to it, and it penalises over forecasts more heavily than under forecasts.17 Percentages themselves are in Foundations: percentages.

Two more habits matter. First, check the average error with its sign, the bias: a forecast that is always low is a problem MAE alone hides. Second, measure on data the model has not seen. Hold back the most recent stretch, about a fifth of the data and at least as long as the horizon you forecast, fit on the rest and score on the held back part.17 Time series cross validation repeats that with the cut point rolling forward, so no forecast ever uses the future.18

Worked example: two categories, two different problems

Mulcahy's checks two months of forecasts.

Sunglasses: January actual €14,500 against a forecast of €14,000, February actual €15,400 against €16,200. The errors are +500+500 and −800-800.

MAE=500+8002=650,RMSE=5002+80022=445,000=667\text{MAE} = \frac{500 + 800}{2} = 650, \qquad \text{RMSE} = \sqrt{\frac{500^2 + 800^2}{2}} = \sqrt{445{,}000} = 667

Here 500500 and 800800 are the two misses in euro. MAPE divides each by its own actual:

MAPE=12(50014,500+80015,400)×100=3.45+5.192=4.32%\text{MAPE} = \frac{1}{2}\left(\frac{500}{14{,}500} + \frac{800}{15{,}400}\right) \times 100 = \frac{3.45 + 5.19}{2} = 4.32\%

The bias is (500−800)/2=−150(500 - 800)/2 = -150, slightly over forecast, nothing alarming.

Watches: January actual €180,000 against €175,000, February €195,000 against €190,000. Both errors are +5,000+5{,}000. MAE and RMSE are both 5,000, MAPE is 12(2.78+2.56)=2.67%\frac{1}{2}(2.78 + 2.56) = 2.67\%, and the bias is +5,000+5{,}000.

By MAPE, watches look like the better forecast. But every watch error has the same sign: the model is consistently low. That is the more useful finding, and only the bias shows it.

The Python toolkit

These examples assume a CSV with a date column and a sales column. The file names are placeholders.

The libraries

# Core data handling
import pandas as pd
import numpy as np

# Charts
import matplotlib.pyplot as plt

# Statistical forecasting
from statsmodels.tsa.holtwinters import ExponentialSmoothing
from statsmodels.tsa.statespace.sarimax import SARIMAX
from statsmodels.tsa.seasonal import seasonal_decompose

# Prophet (released by Facebook, now Meta)
from prophet import Prophet

# Machine learning
from sklearn.linear_model import Ridge
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import mean_absolute_error, mean_absolute_percentage_error
import xgboost as xgb

Decompose before you forecast

import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.seasonal import seasonal_decompose

df = pd.read_csv('sunglasses_sales.csv', parse_dates=['date'], index_col='date')
df = df.asfreq('MS')  # monthly, dated to the start of each month

# Multiplicative, because the summer swing grows with the level
decomposition = seasonal_decompose(df['sales'], model='multiplicative', period=12)

fig, axes = plt.subplots(4, 1, figsize=(12, 10))
decomposition.observed.plot(ax=axes[0], title='Observed')
decomposition.trend.plot(ax=axes[1], title='Trend')
decomposition.seasonal.plot(ax=axes[2], title='Seasonal')
decomposition.resid.plot(ax=axes[3], title='Residual')
plt.tight_layout()
plt.show()

The four panels answer the first three questions: is there a trend, how strong is the seasonality, and how much noise is left.

SARIMA for sunglasses

import pandas as pd
from statsmodels.tsa.statespace.sarimax import SARIMAX
from sklearn.metrics import mean_absolute_error, mean_absolute_percentage_error

df = pd.read_csv('sunglasses_sales.csv', parse_dates=['date'], index_col='date')
df = df.asfreq('MS')

# Hold back the most recent year
train = df[:'2024']
test = df['2025':]

# order = (p, d, q), seasonal_order = (P, D, Q, s) with s = 12 for monthly data
model = SARIMAX(
    train['sales'],
    order=(1, 1, 1),
    seasonal_order=(1, 1, 1, 12),
    enforce_stationarity=False,
    enforce_invertibility=False,
)
results = model.fit(disp=False)

forecast = results.get_forecast(steps=len(test))
forecast_mean = forecast.predicted_mean
forecast_ci = forecast.conf_int(alpha=0.05)  # 95% prediction interval

print(f"MAE:  {mean_absolute_error(test['sales'], forecast_mean):,.0f}")
print(f"MAPE: {mean_absolute_percentage_error(test['sales'], forecast_mean):.1%}")

# Always compare with the seasonal naive benchmark: last year's same month
naive = train['sales'].iloc[-12:].values[:len(test)]
print(f"Seasonal naive MAE: {mean_absolute_error(test['sales'], naive):,.0f}")

If SARIMA cannot beat the seasonal naive line at the bottom, use the seasonal naive.

Prophet for confectionery

import pandas as pd
from prophet import Prophet

# Prophet expects two columns: ds (date) and y (value)
df = pd.read_csv('confectionery_sales.csv')
df.columns = ['ds', 'y']
df['ds'] = pd.to_datetime(df['ds'])

def event(name, dates, before, after):
    return pd.DataFrame({
        'holiday': name,
        'ds': pd.to_datetime(dates),
        'lower_window': before,
        'upper_window': after,
    })

holidays = pd.concat([
    event('christmas', ['2022-12-25', '2023-12-25', '2024-12-25', '2025-12-25'], -30, 5),
    event('easter', ['2022-04-17', '2023-04-09', '2024-03-31', '2025-04-20'], -14, 1),
    event('valentines', ['2022-02-14', '2023-02-14', '2024-02-14', '2025-02-14'], -7, 0),
    # Mother's Day in Ireland falls on Mothering Sunday, not the US date
    event('mothers_day', ['2022-03-27', '2023-03-19', '2024-03-10', '2025-03-30'], -7, 0),
])

model = Prophet(
    holidays=holidays,
    yearly_seasonality=True,
    weekly_seasonality=True,
    daily_seasonality=False,
    changepoint_prior_scale=0.05,  # how freely the trend may bend
)
model.fit(df)

future = model.make_future_dataframe(periods=365)
forecast = model.predict(future)

# yhat is the forecast, yhat_lower and yhat_upper the interval
print(forecast[['ds', 'yhat', 'yhat_lower', 'yhat_upper']].tail())
model.plot_components(forecast)

Each holiday becomes its own column in the forecast, so you can see what Christmas is worth on its own.19

Regression on drivers for watches

import pandas as pd
import numpy as np
from sklearn.linear_model import Ridge
from sklearn.model_selection import TimeSeriesSplit
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import make_pipeline
from sklearn.metrics import mean_absolute_error

df = pd.read_csv('watch_sales_with_drivers.csv', parse_dates=['date'])

# The month as a point on a circle, so December sits next to January
df['month_sin'] = np.sin(2 * np.pi * df['date'].dt.month / 12)
df['month_cos'] = np.cos(2 * np.pi * df['date'].dt.month / 12)

features = [
    'ftse_100_change',    # stock market
    'visitors_abroad',    # international visitors
    'gbp_usd',            # exchange rate
    'bonus_month',        # 1 in bonus months, else 0
    'new_model_release',  # 1 when a new model launches, else 0
    'month_sin', 'month_cos',
]
X, y = df[features], df['sales']

# Scaling inside the pipeline, so each fold is scaled on its own training data
model = make_pipeline(StandardScaler(), Ridge(alpha=1.0))

scores = []
for train_idx, test_idx in TimeSeriesSplit(n_splits=5).split(X):
    model.fit(X.iloc[train_idx], y.iloc[train_idx])
    scores.append(mean_absolute_error(y.iloc[test_idx], model.predict(X.iloc[test_idx])))
print(f'Average MAE: {np.mean(scores):,.0f}')

model.fit(X, y)
coefs = pd.Series(model[-1].coef_, index=features).sort_values(key=abs, ascending=False)
print(coefs)  # standardised: larger means a bigger effect per standard deviation

Remember the catch from earlier: to forecast next month you need next month's drivers, and those are forecasts too.

Gradient boosting for beauty

import pandas as pd
import numpy as np
import xgboost as xgb
from sklearn.model_selection import TimeSeriesSplit
from sklearn.metrics import mean_absolute_error

df = pd.read_csv('beauty_sales.csv', parse_dates=['date']).sort_values('date')

df['day_of_week'] = df['date'].dt.dayofweek
df['month'] = df['date'].dt.month
df['is_weekend'] = df['day_of_week'].isin([5, 6]).astype(int)
df['payday_week'] = df['date'].dt.day.isin(range(25, 32)).astype(int)

# Lags: sales 7, 14 and 28 days earlier
for lag in (7, 14, 28):
    df[f'lag_{lag}'] = df['sales'].shift(lag)

# Rolling figures shifted by one day, so today's sales never feed today's forecast
past = df['sales'].shift(1)
df['mean_7'] = past.rolling(7).mean()
df['std_7'] = past.rolling(7).std()
df['mean_28'] = past.rolling(28).mean()

df = df.dropna()
features = ['day_of_week', 'month', 'is_weekend', 'payday_week',
            'lag_7', 'lag_14', 'lag_28', 'mean_7', 'std_7', 'mean_28']
X, y = df[features], df['sales']

scores = []
for train_idx, test_idx in TimeSeriesSplit(n_splits=5).split(X):
    model = xgb.XGBRegressor(n_estimators=300, max_depth=6, learning_rate=0.05,
                             subsample=0.8, colsample_bytree=0.8, random_state=42)
    model.fit(X.iloc[train_idx], y.iloc[train_idx])
    scores.append(mean_absolute_error(y.iloc[test_idx], model.predict(X.iloc[test_idx])))
print(f'Average MAE: {np.mean(scores):,.0f}')

The shift before the rolling averages matters. Without it, each day's average includes that day's own sales, the model quietly sees the answer, and the test score looks far better than any real forecast will be.

Blending a model with expert judgement

from scipy.stats import norm

def blend(model_value, expert_estimates, model_weight=0.5):
    """Weighted blend of one model forecast with each expert scenario."""
    return {name: model_weight * model_value + (1 - model_weight) * value
            for name, value in expert_estimates.items()}

def band(forecast, error_sd_pct, coverage=0.95):
    """Rough interval from the standard deviation of past percentage errors."""
    z = norm.ppf(0.5 + coverage / 2)  # 1.96 for 95%, 1.28 for 80%
    margin = forecast * error_sd_pct * z
    return forecast - margin, forecast + margin

model_forecast = 125_000  # from analogous past launches
experts = {'cautious': 95_000, 'expected': 140_000, 'optimistic': 200_000}

scenarios = blend(model_forecast, experts, model_weight=0.5)
for name, value in scenarios.items():
    print(f'{name}: €{value:,.0f}')

low, high = band(scenarios['expected'], error_sd_pct=0.30)
print(f'Expected, 95%: €{low:,.0f} to €{high:,.0f}')

Worked example: the new collection in numbers

Mulcahy's model, built on analogous launches, says €125,000. The buyer panel says €95,000 cautious, €140,000 expected and €200,000 optimistic. With equal weights, because the model has little to go on for a new collection:

cautious=0.5×125,000+0.5×95,000=110,000expected=0.5×125,000+0.5×140,000=132,500optimistic=0.5×125,000+0.5×200,000=162,500\begin{aligned} \text{cautious} &= 0.5 \times 125{,}000 + 0.5 \times 95{,}000 = 110{,}000 \\ \text{expected} &= 0.5 \times 125{,}000 + 0.5 \times 140{,}000 = 132{,}500 \\ \text{optimistic} &= 0.5 \times 125{,}000 + 0.5 \times 200{,}000 = 162{,}500 \end{aligned}

Here 0.50.5 is the weight on each side, 125,000125{,}000 the model and the second figure in each line the panel's scenario, a weighted average as in Foundations: the mean.

Assume past launch forecasts missed with a standard deviation of 30% (the example's assumption). The 95% band around the expected €132,500 is:

132,500×0.30×1.96=77,910132{,}500 \times 0.30 \times 1.96 = 77{,}910

Here 132,500132{,}500 is the expected forecast, 0.300.30 the assumed error standard deviation and 1.961.96 the 95% multiplier, so the band runs from €54,590 to €210,410. That band is wide, and it should be. A buyer with a plan for both ends is far better placed than one handed a single confident number.

Checking accuracy by category

import pandas as pd
import numpy as np

def metrics(actual, forecast):
    e = actual - forecast
    return pd.Series({
        'mae': np.mean(np.abs(e)),
        'rmse': np.sqrt(np.mean(e ** 2)),
        'mape': np.mean(np.abs(e / actual)) * 100,
        'bias': np.mean(e),  # positive means the forecast was too low
    })

results = pd.DataFrame({
    'category': ['Sunglasses', 'Sunglasses', 'Watches', 'Watches'],
    'month': ['Jan', 'Feb', 'Jan', 'Feb'],
    'actual': [14_500, 15_400, 180_000, 195_000],
    'forecast': [14_000, 16_200, 175_000, 190_000],
})

report = results.groupby('category').apply(lambda g: metrics(g['actual'], g['forecast']))
print(report.round(2))

This prints the figures from the accuracy example above. Run it monthly, per category, and the categories that need attention find you.

What to take away

Forecasting is not about finding the perfect method. It is about matching the method to the situation: numbers where there is stable history, structured judgement where there is none, and both where you can.

Look before you model, and beat the benchmarks. A simple method you can explain often wins, so add complexity only when it beats the seasonal naive forecast on data it has not seen.

Track the accuracy with its sign, adjust only with real information, and give ranges, not single numbers. The interval tells the reader how wrong to expect, whether the product is designer sunglasses or a box of chocolates.

Footnotes

  1. Rob J Hyndman and George Athanasopoulos, Forecasting: Principles and Practice, 3rd edition, chapter 6, Judgmental forecasts, OTexts, Melbourne, 2021, https://otexts.com/fpp3/judgmental.html

  2. Rob J Hyndman and George Athanasopoulos, Forecasting: Principles and Practice, 3rd edition, OTexts, Melbourne, 2021, https://otexts.com/fpp3/

  3. Rob J Hyndman and George Athanasopoulos, Forecasting: Principles and Practice, 3rd edition, section 2.3, Time series patterns, OTexts, 2021, https://otexts.com/fpp3/tspatterns.html

  4. Rob J Hyndman and George Athanasopoulos, Forecasting: Principles and Practice, 3rd edition, section 5.2, Some simple forecasting methods, OTexts, 2021, https://otexts.com/fpp3/simple-methods.html

  5. Rob J Hyndman and George Athanasopoulos, Forecasting: Principles and Practice, 3rd edition, section 3.3, Moving averages, OTexts, 2021, https://otexts.com/fpp3/moving-averages.html

  6. Rob J Hyndman and George Athanasopoulos, Forecasting: Principles and Practice, 3rd edition, section 8.1, Simple exponential smoothing, OTexts, 2021, https://otexts.com/fpp3/ses.html

  7. Rob J Hyndman and George Athanasopoulos, Forecasting: Principles and Practice, 3rd edition, section 8.3, Methods with seasonality, OTexts, 2021, https://otexts.com/fpp3/holt-winters.html

  8. Rob J Hyndman and George Athanasopoulos, Forecasting: Principles and Practice, 3rd edition, chapter 9, ARIMA models, OTexts, 2021, https://otexts.com/fpp3/arima.html

  9. statsmodels, statsmodels.tsa.statespace.sarimax.SARIMAX, documentation, accessed November 2026, https://www.statsmodels.org/stable/generated/statsmodels.tsa.statespace.sarimax.SARIMAX.html

  10. Meta, Prophet: forecasting at scale, project documentation, accessed November 2026, https://meta.github.io/prophet/

  11. Rob J Hyndman and George Athanasopoulos, Forecasting: Principles and Practice, 3rd edition, section 7.1, The linear model, OTexts, 2021, https://otexts.com/fpp3/regression-intro.html

  12. Rob J Hyndman and George Athanasopoulos, Forecasting: Principles and Practice, 3rd edition, section 7.6, Forecasting with regression, OTexts, 2021, https://otexts.com/fpp3/forecasting-regression.html

  13. Tim Januschowski, Yuyang Wang, Kari Torkkola, Timo Erkkilä, Hilaf Hasson and Jan Gasthaus, Forecasting with trees, International Journal of Forecasting, vol. 38, no. 4, 2022 (open access), https://cdn.amazon.science/17/31/0d99831d4add92e711a56abc75f8/forecasting-with-trees.pdf

  14. Rob J Hyndman and George Athanasopoulos, Forecasting: Principles and Practice, 3rd edition, section 6.3, The Delphi method, OTexts, 2021, https://otexts.com/fpp3/delphimethod.html

  15. Rob J Hyndman and George Athanasopoulos, Forecasting: Principles and Practice, 3rd edition, section 6.7, Judgmental adjustments, OTexts, 2021, https://otexts.com/fpp3/judgmental-adjustments.html

  16. Rob J Hyndman and George Athanasopoulos, Forecasting: Principles and Practice, 3rd edition, section 5.5, Distributional forecasts and prediction intervals, OTexts, 2021, https://otexts.com/fpp3/prediction-intervals.html

  17. Rob J Hyndman and George Athanasopoulos, Forecasting: Principles and Practice, 3rd edition, section 5.8, Evaluating point forecast accuracy, OTexts, 2021, https://otexts.com/fpp3/accuracy.html

  18. scikit-learn, sklearn.model_selection.TimeSeriesSplit, documentation, accessed November 2026, https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.TimeSeriesSplit.html

  19. Meta, Seasonality, holiday effects and regressors, Prophet documentation, accessed November 2026, https://meta.github.io/prophet/docs/seasonality,_holiday_effects,_and_regressors.html

I've built prediction models for inventory, demand and resource planning. If you want to work out which method fits your own products, bring your sales history to the free workshop and we'll work it out together. The first two weeks of work are on me.