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:
Here is the forecast for periods ahead, made with data up to period , is the last period you have data for, is the number of periods ahead you are forecasting, and 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:
Here is the length of the season (12 for monthly data with a yearly pattern), is the whole number part of , which counts the complete years between now and the forecast, and 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:
| Month | 2024 | 2025 |
|---|---|---|
| Jan | 12 | 13 |
| Feb | 14 | 15 |
| Mar | 22 | 24 |
| Apr | 35 | 38 |
| May | 52 | 56 |
| Jun | 68 | 73 |
| Jul | 74 | 80 |
| Aug | 66 | 71 |
| Sep | 38 | 41 |
| Oct | 20 | 21 |
| Nov | 15 | 16 |
| Dec | 24 | 26 |
| Total | 440 | 474 |
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 months ahead, so is the whole number part of , which is 0. The formula points at , 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 periods is:
Here is the moving average at period , is the number of periods in the window, means add up the terms for from 0 to , and is the value periods before , so the sum runs over the latest 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:
Here , and are July, August and September sales in €k and 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 thousand euro. July's ratio to that level is , and January's is . (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:
Here is the smoothed level after period , which is also the forecast for every future period, is the smoothing parameter between 0 and 1, is the value observed in period , and is the level from the period before. A large follows recent values closely, a small gives older values more say, and 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, , and use :
Each line weights the new month by and the previous level by , so the forecast for next month is , about 31 watches (the weighted average idea is in Foundations: the mean). With the same steps give 30, 33.2, 29.84 and 32.37, a forecast of about 32. The higher 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 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 , where 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
Here is the value you want to forecast in period (sales, say), is the driver in that period (temperature, footfall, ad spend), is the intercept, the predicted value of when is zero, is the slope, the average change in for one more unit of , and is the error, everything else that moves besides . The method, with 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:
Here is weekly sunglasses sales in euro, the intercept, the slope in euro per degree and 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:
So about €12,800. A cooler 18°C forecast gives , 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.
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:
Here and are the two weights, the model's forecast and 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
Here is the point forecast for periods ahead, is a multiplier set by the coverage you want (1.28 for 80%, 1.96 for 95%), and is the estimated standard deviation of the forecast error at horizon . For the seasonal naive method , where is the standard deviation of past forecast errors and 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 and . The 95% interval is:
So €76.5k to €83.5k. The 80% interval is , €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 times 2024, so a growth adjusted forecast for July 2026 is , about €86k. That sits outside the interval just calculated, which is the lesson: an interval is only as honest as the errors behind it.
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
Here is the forecast error in period (actual minus forecast), is the number of periods checked, is the error without its sign, and 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.
Here is the actual value in period , and 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 and .
Here and are the two misses in euro. MAPE divides each by its own actual:
The bias is , slightly over forecast, nothing alarming.
Watches: January actual €180,000 against €175,000, February €195,000 against €190,000. Both errors are . MAE and RMSE are both 5,000, MAPE is , and the bias is .
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:
Here is the weight on each side, 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:
Here is the expected forecast, the assumed error standard deviation and 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
-
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
-
Rob J Hyndman and George Athanasopoulos, Forecasting: Principles and Practice, 3rd edition, OTexts, Melbourne, 2021, https://otexts.com/fpp3/
-
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
-
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
-
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
-
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
-
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
-
Rob J Hyndman and George Athanasopoulos, Forecasting: Principles and Practice, 3rd edition, chapter 9, ARIMA models, OTexts, 2021, https://otexts.com/fpp3/arima.html
-
statsmodels, statsmodels.tsa.statespace.sarimax.SARIMAX, documentation, accessed November 2026, https://www.statsmodels.org/stable/generated/statsmodels.tsa.statespace.sarimax.SARIMAX.html
-
Meta, Prophet: forecasting at scale, project documentation, accessed November 2026, https://meta.github.io/prophet/
-
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
-
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
-
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
-
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
-
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
-
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
-
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
-
scikit-learn, sklearn.model_selection.TimeSeriesSplit, documentation, accessed November 2026, https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.TimeSeriesSplit.html
-
Meta, Seasonality, holiday effects and regressors, Prophet documentation, accessed November 2026, https://meta.github.io/prophet/docs/seasonality,_holiday_effects,_and_regressors.html