Forecasting demand: autocorrelation, seasonality and honest error bands

TL;DR

Difficulty: Advanced

  • A demand forecast is regression on the calendar: a trend plus dummy variables for seasons, moving holidays and events.
  • Time series errors are autocorrelated, so plain regression overstates its own precision. Check Durbin Watson and the residual ACF, then fit AR(1) errors.
  • Judge every model on weeks it has never seen, with MAE and MAPE, and make it beat "same week last year plus growth" before it earns its complexity.
  • Report an 80% prediction interval, not just a point, and order stock and roster staff against the band.
  • A forecast is what happens if nothing changes, a target is what you want. Name the gap and make marketing earn it.

A forecast is a regression with the calendar as the driver, and the calendar is the easy part. The hard part is the error: in a time series a good week follows a good week, the usual standard errors flatter the model, and a point forecast without a band is a target in disguise. Using three years of weekly orders from an invented Cork hamper shop, this chapter builds a seasonal model with AR(1) errors, races it against simpler methods on a holdout, and turns the winner into a Christmas forecast with an 80% band that becomes a stock order and a packing rota. Part 10 of 21 of the Marketing Analytics series.

The Friday the packing room ran out of ribbon

In week 51 of 2024 Northgate Hampers ran out of the wide satin ribbon that goes round every Christmas hamper. Not out of hampers, out of ribbon. Northgate is the invented online hamper shop in Cork from earlier chapters, run by Priya. Someone had ordered "a bit more than last year", which turned out to be about a fifth less than the week actually brought. Two packers spent the Friday evening driving round County Cork for ribbon, and the margin on that week did not survive the petrol, the overtime and the emergency courier.

Nothing about that week was unforeseeable. The shop had years of weekly orders, and Christmas arrives on the same date every year. What was missing was not data. It was a forecast with an honest error band, and someone willing to plan against the upper end of it rather than the middle.

That is this chapter: predicting demand through time, with the seasons built in and the uncertainty stated in orders rather than adjectives. Northgate Hampers and every number in this chapter are made up so I can show each step. The method is what I use.

Where this sits in the series

This is chapter 10 of 21 in my Marketing Analytics series, and it closes part two, the dependent variable techniques. The previous chapter, on panel regression and same store sales, followed many shops through time. This one follows a single shop and takes time seriously. The next chapter, on simultaneous equations, opens part three, where the arrow between marketing and sales points both ways.

A forecast is regression with the calendar as the driver

Most of the mystique around forecasting goes once you see it as regression whose drivers are made out of the calendar: a trend (a counter that goes 1, 2, 3, one step per week), indicators for seasons and holidays, and events such as a corporate campaign or a stockout. The outcome is orders per week.

What changes is the error term. In a time series a good week tends to follow a good week. If the shop was 40 orders above the model in week 20, it is probably above the model in week 21 too, because whatever caused the surplus (a press mention, a competitor's stockout, a warm weekend) does not switch off at midnight on Sunday. That is autocorrelation, and it quietly breaks ordinary regression the moment time is involved.

Autocorrelation: last week's surprise leaks into this week

The simplest description of it is the first order autoregressive process, written AR(1):

εt=ρ εt−1+ut\varepsilon_t = \rho\,\varepsilon_{t-1} + u_t

Here εt\varepsilon_t is the model's error in week tt, ρ\rho (rho) is the autocorrelation coefficient, a number between minus 1 and 1, εt−1\varepsilon_{t-1} is last week's error, and utu_t is a fresh, independent shock. With ρ=0\rho = 0 you are back in ordinary regression. The equation is itself a small regression of the error on its own past, see Foundations: regression, and ρ\rho behaves like a correlation, see Foundations: correlation.

An example: Northgate's model below finds ρ=0.34\rho = 0.34. Say the model was 50 orders short in week 20, so ε20=50\varepsilon_{20} = 50. The expected error in week 21 is 0.34×50=170.34 \times 50 = 17, in week 22 it is 0.34×17≈60.34 \times 17 \approx 6, and by week 23 it is about 2. About a third of any surprise carries into the following week, and it fades within a month.

Why does this matter? Ordinary least squares (OLS, the standard regression fit) still gives sensible coefficients when the errors are autocorrelated, but they are no longer the best estimates, and the tests on them are wrong. In most cases the p values come out too small, so drivers look more important than they are.1 Worse for forecasting, the model throws away what last week's error says about next week, which is free information.

Two diagnostics

The first is the Durbin Watson statistic:

DW=∑t=2T(et−et−1)2∑t=1Tet2≈2 (1−ρ^)DW = \frac{\sum_{t=2}^{T} (e_t - e_{t-1})^2}{\sum_{t=1}^{T} e_t^2} \approx 2\,(1 - \hat\rho)

where ete_t is the fitted residual in week tt, et−1e_{t-1} the residual a week earlier, TT the number of weeks and ρ^\hat\rho the estimated autocorrelation of the residuals. It lies between 0 and 4 and equals 2 when there is no autocorrelation.2 As a rough guide, below about 1.5 means positive autocorrelation, the usual case for sales data, and above about 2.5 means negative autocorrelation, often a sign the series was differenced once too often. The link to correlation is in Foundations: correlation.

An example: with ρ^=0.34\hat\rho = 0.34 the approximation gives 2×(1−0.34)=2×0.66=1.322 \times (1 - 0.34) = 2 \times 0.66 = 1.32. Northgate's seasonal model gives 1.31. Clearly below 2, so the errors need treating.

The second is the autocorrelation function, the ACF: one bar per lag, showing how strongly the residuals correlate with themselves one, two, three weeks back. The bar at lag kk is

rk=∑t=k+1T(et−eˉ)(et−k−eˉ)∑t=1T(et−eˉ)2r_k = \frac{\sum_{t=k+1}^{T} (e_t - \bar{e})(e_{t-k} - \bar{e})}{\sum_{t=1}^{T} (e_t - \bar{e})^2}

where rkr_k is the autocorrelation at lag kk, ete_t the residual in week tt, et−ke_{t-k} the residual kk weeks earlier, eˉ\bar{e} the mean residual and TT the number of weeks.3 It is Pearson's correlation of the series with a shifted copy of itself, see Foundations: correlation.

An example: for Northgate's residuals r1r_1 is about 0.34, r2r_2 about 0.12 and r3r_3 about 0.04, each roughly a third of the one before. A tall lag 1 with the rest decaying like that is the signature of AR(1). A tall bar at lag 52 would mean a season the model has missed.

Trend, season and what is left over

It helps to see what a model of the calendar is trying to separate. The additive decomposition writes each week as three parts:

yt=Tt+St+Rty_t = T_t + S_t + R_t

where yty_t is orders in week tt, TtT_t is the trend (where the business has got to), StS_t is the seasonal component (what this time of year adds or takes away) and RtR_t is the remainder, everything else.4 When the seasonal swing grows in proportion to the business, the multiplicative form yt=Tt×St×Rty_t = T_t \times S_t \times R_t fits better, which is the seasonal index of Foundations: seasonality.

An example: in week 51 of 2025 Northgate's trend stood at about 308 orders, the last order week before Christmas adds 523, so trend plus season is 308+523=831308 + 523 = 831. The shop actually took 845 orders, so the remainder is 845−831=14845 - 831 = 14. The forecast is the first two parts. The error band is about the third.

Seasons as dummy variables

A dummy variable is a column of zeros with ones in the weeks of a season. Several in one regression give the seasonal dummy model:

yt=β0+β1 t+∑k=1Kγk Dk,t+εty_t = \beta_0 + \beta_1\,t + \sum_{k=1}^{K} \gamma_k\,D_{k,t} + \varepsilon_t

Here yty_t is orders in week tt. β0\beta_0 is the level of a plain week at the start of the sample. β1\beta_1 is the trend, the change in weekly orders from one week to the next, and tt counts the weeks. Dk,tD_{k,t} equals 1 if week tt belongs to season kk and 0 otherwise, with KK seasons in total. γk\gamma_k (gamma) is how many orders season kk adds to, or removes from, a plain week. εt\varepsilon_t is the error, allowed to follow the AR(1) process above. The β0+β1t\beta_0 + \beta_1 t part is the trend TtT_t of the decomposition, the dummies are the season StS_t. It is ordinary regression, see Foundations: regression.

The temptation is to give every one of the 52 weeks its own dummy. With 156 weeks of history that is 52 coefficients from three observations each: the past reproduced perfectly, the future forecast badly, because every random wobble becomes a "season". Group weeks into seasons that mean something to the business instead. For a hamper shop: November, early December, the last order week before Christmas, the dead stretch around New Year, January, Mother's Day and Father's Day. St Patrick's Day, a public holiday on 17 March,5 is a fair candidate to test as well, not least because it can land in the same fortnight as Mother's Day. For Northgate it did not earn a dummy of its own. Everything else is a plain week, and the plain week is what the intercept measures.

Moving holidays get a dummy that moves with them.6 Mother's Day in Ireland falls on Mothering Sunday, the fourth Sunday in Lent, three weeks before Easter Sunday, so it wanders: 19 March in 2023, 10 March in 2024, 30 March in 2025, 15 March in 2026.7 In ISO week numbers that is anywhere from week 9 to week 13, so the dummy marks the two weeks before Mother's Day, wherever they land in a given year. Father's Day in Ireland is the third Sunday in June,8 week 24 or 25.

One more choice: linear or log trend. A linear trend adds a fixed number of orders each week. A log trend, where you regress the logarithm of orders, adds a fixed percentage, and the dummies become percentage effects too, which is right when Christmas triples orders rather than adding a fixed 500. For Northgate the Christmas peak grew year on year, but less than in proportion to the plain weeks, and the linear model won on the holdout. It is a testable choice. Logs are in Foundations: logarithms.

Worked example: Northgate's 156 weeks in one model

The data is weekly completed orders from the first ISO week of 2023 (starting Monday 2 January) to the last week of 2025, 156 weeks. I fitted on the first 130 weeks and kept the last 26, the second half of 2025 with the whole Christmas run, as a holdout the model never saw. Then I refitted on all 156 weeks to forecast 2026. Here is the refitted model with AR(1) errors.

TermCoefficient (orders per week)Std errorReading
Intercept (plain week, start of 2023)2129a plain week at the start of the sample
Trend per week0.620.09about 32 extra weekly orders each year
November (weeks 45 to 48)plus 14821gift buyers start early, corporate enquiries land
Early December (weeks 49 and 50)plus 39129the ramp
Last order week (week 51)plus 52341the peak, the ribbon week
Christmas to New Year (weeks 52 and 1)minus 9629nobody orders a hamper on St Stephen's Day
January (weeks 2 to 5)minus 6121the resolution weeks
Mother's Day (two weeks before)plus 11729second peak of the year
Father's Day (two weeks before)plus 6429smaller, and less certain
Corporate campaign running (event)plus 8826the weeks a corporate mailing was live

Fit statistics: R2R^2 of 0.91, a residual standard error of 36 orders per week for the fresh shocks utu_t, Durbin Watson 1.31, estimated ρ\rho of 0.34. The standard errors shown allow for the AR(1) errors. The naive ones from plain OLS were about 30% smaller and made the Father's Day effect look far more certain than it is.

Reading it line by line. A plain week at the start of 2023 brought 212 orders, and each week since has added 0.62 (0.62×52≈320.62 \times 52 \approx 32 a year), so a plain week at the end of 2025 sits at 212+0.62×156≈309212 + 0.62 \times 156 \approx 309. November adds 148 on top of the trend, early December 391 and week 51 523, so a week 51 at the end of 2025 comes to roughly 830 orders. Then the floor drops out: the weeks around New Year run 96 below a plain week and January 61 below.

Mother's Day is worth 117 extra orders in each of the two weeks before it. Father's Day is 64 with a standard error of 29, so the honest statement is a 90% interval of 64±1.645×2964 \pm 1.645 \times 29, roughly 16 to 112, "probably about 60". The corporate campaign dummy says a live mailing is worth about 88 orders a week, which is the number Priya wanted and the number I would not have believed without the standard error next to it.

A typical week in each seasonExpected orders in one week of each season, end of 2025: plain week 309, November 457, early December 700, week 51 832, Christmas to New Year 213, January 248, Mother's Day 426, Father's Day 373.Expected orders in one week, end of 202502505007501,000309Plainweek457November700EarlyDecember832Week 51213Christmasto NewYear248January426Mother'sDay373Father'sDayOrders per weekSeason

The horizontal axis lists the seasons, the vertical axis the expected orders in one week of that season, the plain week of 309 plus the season's coefficient. The takeaway: week 51 is nearly three plain weeks, and the weeks after it are the quietest of the year. Planning for "an average week" plans for none of them.

Simple models that are hard to beat

Before you fall in love with a regression, meet the benchmarks. Seasonal naive says each week will be what the same week was last year.9 Add a growth factor and you have a model many businesses would do well to adopt tomorrow:

y^t=g×yt−52\hat y_{t} = g \times y_{t-52}

where y^t\hat y_t is the forecast for week tt, yt−52y_{t-52} is the actual orders in the same week a year earlier and gg is the growth factor, 1.12 for 12% growth. A growth factor is a percentage change written as a multiplier, see Foundations: percentages, and comparing like weeks a year apart is the year on year idea of Foundations: seasonality.

An example: week 51 of 2024 brought 780 orders, the ribbon week. Seasonal naive with growth forecasts week 51 of 2025 as 1.12×780=873.61.12 \times 780 = 873.6, about 874. The actual was 845, a miss of 29 orders, or 29/845=3.4%29 / 845 = 3.4\%. It knows nothing, costs nothing, and is embarrassingly hard to beat.

Holt Winters exponential smoothing keeps three running numbers, a level, a trend and a seasonal pattern, and nudges each towards what just happened.10 It adapts when the business changes, which the dummy model does not, and it needs no calendar knowledge, which is also its weakness: it cannot know that Mother's Day moved.

ARIMA (AutoRegressive Integrated Moving Average) is three ideas bolted together.11 AR: regress the series on its own past. I: difference it, this week minus last week, to remove the trend. MA: regress on past errors. The smallest useful version, ARIMA(1,1,1), reads

yt′=c+ϕ1 yt−1′+θ1 ut−1+ut,yt′=yt−yt−1y'_t = c + \phi_1\,y'_{t-1} + \theta_1\,u_{t-1} + u_t, \qquad y'_t = y_t - y_{t-1}

where yt′y'_t is the change in orders from last week to this week, cc a constant drift, ϕ1\phi_1 (phi) how much of last week's change repeats, yt−1′y'_{t-1} last week's change, θ1\theta_1 (theta) how much of last week's shock carries over, ut−1u_{t-1} last week's shock and utu_t this week's. Seasonal ARIMA repeats the trick at lag 52, comparing each week with the same week a year earlier. Each piece is a regression on lagged values, see Foundations: regression.

An example, with coefficients made up for the illustration: c=0.6c = 0.6, ϕ1=0.3\phi_1 = 0.3, θ1=−0.5\theta_1 = -0.5. Last week orders rose by 20, and last week's shock was plus 10. The forecast change is 0.6+0.3×20−0.5×10=0.6+6−5=1.60.6 + 0.3 \times 20 - 0.5 \times 10 = 0.6 + 6 - 5 = 1.6, so next week is forecast at this week's orders plus about 2.

My dummy model with AR(1) errors is essentially a small ARIMA with calendar regressors built by hand, what the textbooks call regression with ARIMA errors,1 which is why the two land close on the scoreboard below.

My rule: the model that goes into production has to beat seasonal naive with growth on a holdout, by a margin that would change a decision. If it cannot, it has not earned its complexity, and the memo says so.

Judging a forecast on weeks it has never seen

Fit statistics are about the past. A forecast is judged on the holdout, the weeks hidden from the model, with two measures:

MAE=1n∑t=1n∣yt−y^t∣MAPE=100n∑t=1n∣yt−y^tyt∣\text{MAE} = \frac{1}{n}\sum_{t=1}^{n}\left|y_t - \hat y_t\right| \qquad \text{MAPE} = \frac{100}{n}\sum_{t=1}^{n}\left|\frac{y_t - \hat y_t}{y_t}\right|

yty_t is the actual value in holdout week tt, y^t\hat y_t the forecast for it, nn the number of holdout weeks and the vertical bars take the size of the miss without its sign. MAE, the mean absolute error, is in orders, the number the warehouse understands. MAPE, the mean absolute percentage error, compares across products and shops, but is undefined when a week has zero sales and extreme when it has very few.12 Both are means, see Foundations: the mean, and the percentages are in Foundations: percentages.

An example: a 28 order miss in the dead week after Christmas, with 236 actual orders, is 28/236=11.9%28 / 236 = 11.9\%. The same 28 orders in week 51, with 845 actual orders, would be 28/845=3.3%28 / 845 = 3.3\%. Same orders, very different percentage. So I plan with MAE and compare with MAPE.

Here is the scoreboard on the 26 week holdout. The last column is the forecast for the whole of Q4 2025 minus what actually happened, as a percentage.

ModelMAE (orders per week)MAPEQ4 2025 total, forecast minus actual
Seasonal naive (same week last year)4114.2%minus 7.8%
Seasonal naive times 1.12 growth3311.0%plus 1.9%
Seasonal dummy regression, plain OLS299.6%minus 3.6%
Seasonal dummy regression with AR(1) errors268.7%minus 3.1%
Holt Winters exponential smoothing309.9%plus 3.4%
Seasonal ARIMA, automatically selected3110.3%minus 4.1%

Reading it. Plain seasonal naive underestimates by nearly 8% because it does not know the shop is growing. With a growth factor it is suddenly respectable, within 2% on the quarter and only 7 orders a week behind the winner. The dummy model with AR(1) errors wins on MAE, 26 orders per week, and the AR(1) correction alone is worth 3 orders a week over plain OLS, 3/29≈10%3 / 29 \approx 10\% less error for zero extra data. All the serious models land within about 4% on the quarterly total. The differences are in the weeks, which is where the ribbon gets ordered.

Q4 2025 holdout, actual orders against forecastOrders in ISO weeks 40 to 52 of 2025 against the model fitted without them. Actual orders run from 281 to a peak of 845 in week 51, then 236. The forecast steps from about 304 in October to 453, 696, 828 in week 51 and 208.Actual and forecast orders, Q4 202502505007501,000W40W43W46W49W52ActualForecastOrders per weekISO week of 2025

The horizontal axis is the ISO week of 2025, the vertical axis orders per week. The forecast is a staircase, because dummies are flat within a season with a step between, and the actuals wobble around it.

Worked example: scoring the holdout quarter

Over these 13 weeks the absolute misses are 22, 30, 8, 37, 4, 21, 45, 1, 58, 27, 45, 17 and 28 orders. They add up to 343, so the MAE is 343/13≈26.4343 / 13 \approx 26.4 orders. The MAPE comes to 6.2%, lower than the 26 week figure because the low summer weeks in the longer holdout pull that percentage up. The forecast total for the quarter is 5,760 against 5,945 actual, 5,760/5,945−1=−3.1%5{,}760 / 5{,}945 - 1 = -3.1\%, the figure in the scoreboard.

The biggest relative miss is week 52, 208 forecast against 236 actual, 12% off on a week that does not matter operationally. The miss that would have mattered, week 51, was 828 against 845, 2%. That is the week the ribbon ran out on "a bit more than last year".

The error band is the forecast

A point forecast without a band is a target in a spreadsheet costume. The band is the prediction interval:

y^T+h±z0.90 σ^hσ^h2=σ^u2 1−ρ2h1−ρ2+xT+h⊤V xT+h\hat y_{T+h} \pm z_{0.90}\,\hat\sigma_h \qquad \hat\sigma_h^2 = \hat\sigma_u^2\,\frac{1 - \rho^{2h}}{1 - \rho^2} + \mathbf{x}_{T+h}^\top \mathbf{V}\,\mathbf{x}_{T+h}

y^T+h\hat y_{T+h} is the point forecast hh weeks after the last observed week TT. z0.90z_{0.90} is 1.28, the normal quantile that leaves 10% in each tail, so the band covers 80%.13 σ^h\hat\sigma_h is the forecast standard error at horizon hh and has two parts. The first is the accumulated AR(1) error variance: σ^u2\hat\sigma_u^2 is the variance of the fresh shocks, and the fraction grows with hh towards 1/(1−ρ2)1/(1-\rho^2), so far ahead the error variance is σ^u2/(1−ρ2)\hat\sigma_u^2/(1-\rho^2). The second is uncertainty about the coefficients: xT+h\mathbf{x}_{T+h} is the row of regressors for the forecast week (the trend value and which dummies are on), ⊤^\top turns it on its side, and V\mathbf{V} is the covariance matrix of the estimated coefficients, the standard errors squared plus how they move together. The normal curve and its quantiles are in Foundations: distributions, the standard error in Foundations: spread and the matrix bookkeeping in Foundations: matrices.

Worked example: the band for week 51 of 2026

Week 51 of 2026 is week 207 of the data, 51 weeks after the last observed week, so ρ2h\rho^{2h} is effectively zero. The error part is 362/(1−0.342)=1,296/0.8844≈1,46536^2 / (1 - 0.34^2) = 1{,}296 / 0.8844 \approx 1{,}465. The coefficient part has two pieces: the intercept and trend together, which in this example's covariance matrix come to about 123 at week 207 (they partly cancel, because a higher intercept goes with a flatter trend), and the week 51 dummy, 412=1,68141^2 = 1{,}681. In total σ^h2≈1,465+123+1,681=3,269\hat\sigma_h^2 \approx 1{,}465 + 123 + 1{,}681 = 3{,}269, so σ^h≈57.2\hat\sigma_h \approx 57.2. The point forecast is 212+0.62×207+523≈863212 + 0.62 \times 207 + 523 \approx 863. The band is 863±1.28×57.2=863±73863 \pm 1.28 \times 57.2 = 863 \pm 73, so 790 to 936. Week 51 gets the widest band of the quarter mostly because its dummy rests on three weeks of history.

Two things people mix up. A confidence interval says where the average week 51 would land if history could be rerun many times. A prediction interval says where the actual week 51 will land, and it is always wider. Planning needs the prediction interval, see Foundations: confidence intervals. And 80%, not 95%: a 95% band on a young shop is too wide to inform anything, while 80% means one week in five lands outside, a rate people can feel and plan around.

Here is the 2026 Q4 forecast from the model refitted on all 156 weeks. Weeks within one season differ only by the trend, so I show each season once. 2026 is a long year in the ISO calendar, with a week 53 that runs from 28 December to 3 January,14 and this example counts that week with the new year, so the quarter here is weeks 40 to 52.

Week of 2026Point forecast80% low80% highSeason active
40 to 44 (each week)335284386plain
45 to 48 (each week)486428544November
49 and 50 (each week)730667793early December
51863790936last order week
52245182308Christmas to New Year
Q4 total6,1905,8506,530

The total is 5×335+4×486+2×730+863+245=6,1875 \times 335 + 4 \times 486 + 2 \times 730 + 863 + 245 = 6{,}187, rounded to 6,190. The band on the quarterly total, 5,850 to 6,530, is narrower in relative terms than the weekly bands: plus or minus 5.5% against plus or minus 8.5% in week 51. Weekly errors partly cancel across a quarter, but less than independent errors would, because of the autocorrelation. Ignore ρ\rho and the error part of the quarterly band comes out more than a quarter too narrow.

Forecast, target, decision

The marketing plan for Northgate says 7,000 orders in Q4 2026. The forecast says 6,190, with an 80% band that tops out at 6,530. The two are not in conflict, they are different things. The forecast is what happens if the shop keeps doing what it has been doing. The target is what Priya wants. The gap, about 810 orders, is what the marketing has to earn above trend, and now it has a size, so it can have a budget and a test plan.

Never let the target quietly become the forecast. Then the warehouse plans for 7,000, the accounts book the revenue, and in January somebody discovers that hope is not a season.

Worked example: from the band to the order and the rota

The example assumes packaging costs about €1.80 per hamper to hold and keeps for years, so a spare costs little and a shortfall a lot. Packaging is therefore ordered to the upper band: 940 boxes and ribbon lengths for week 51, and 795 for each of the two weeks before. Perishables are ordered to the point forecast with a reorder trigger at the end of week 49.

The example also assumes a packer manages about 140 hampers a week. Week 51 needs 863/140≈6.2863 / 140 \approx 6.2 packers at the point forecast and 936/140≈6.7936 / 140 \approx 6.7 at the upper band, so six are rostered and a seventh shift is confirmed by week 49 if November comes in above 500 a week. The ribbon does not run out again, and nobody spends a Friday evening touring County Cork.

Running it yourself

Data you need: one row per week with completed orders, ideally revenue too, going back at least two full years and preferably three, because a season seen once is an anecdote and a season seen twice is a hypothesis. From a Solidus shop it is one query:

select date_trunc('week', completed_at)::date as week_start,
       count(*)                              as orders,
       sum(total)                            as revenue
from spree_orders
where state = 'complete'
  and completed_at >= '2023-01-02'
group by 1
order by 1;

Then the seasons, a holdout, plain OLS for the Durbin Watson check, and a refit with AR(1) errors using statsmodels' GLSAR, which estimates a regression with autoregressive errors by an iterative two stage procedure:15

import pandas as pd, statsmodels.api as sm

df = pd.read_csv("weekly_orders.csv", parse_dates=["week_start"])
df["t"] = range(1, len(df) + 1)
df["iso_week"] = df.week_start.dt.isocalendar().week.astype(int)
df["nov"] = df.iso_week.between(45, 48).astype(int)
df["early_dec"] = df.iso_week.between(49, 50).astype(int)
df["w51"] = (df.iso_week == 51).astype(int)
df["xmas_ny"] = df.iso_week.isin([52, 53, 1]).astype(int)
df["jan"] = df.iso_week.between(2, 5).astype(int)
# moving holidays (Mother's Day, Father's Day) come from a date table, not the week number

X = sm.add_constant(df[["t", "nov", "early_dec", "w51", "xmas_ny", "jan"]])
train, test = df.index < 130, df.index >= 130
ols = sm.OLS(df.orders[train], X[train]).fit()
print("Durbin Watson:", round(sm.stats.durbin_watson(ols.resid), 2))

ar1 = sm.GLSAR(df.orders[train], X[train], rho=1).iterative_fit(maxiter=5)
print("rho:", round(ar1.model.rho[0], 2))
pred = ar1.predict(X[test])  # the calendar part; last week's error adds rho^h on top and fades fast
print("holdout MAE:", round((pred - df.orders[test]).abs().mean(), 1))

naive = df.orders.shift(52) * 1.12
print("naive with growth MAE:", round((naive[test] - df.orders[test]).abs().mean(), 1))

I ran it on a simulated 156 week series built from the coefficients above. If your regression does not beat the last line on the holdout, stop and think before adding anything.

How long it takes: a day for clean weekly data and a first model, a week to settle the seasons, the holdout and the bands, a few more days for a table the warehouse and the marketing lead both accept. Longer if the history contains stockouts, because a sold out week records what you could ship, not what people wanted.

Checks before you trust it. Plot the residuals against time and look for runs above and below zero. Look at the residual ACF at lags 1 and 52. Check that the fitted quarterly totals agree with the accounts. Then show the band to the people who will act on it and ask what would have to happen for the actuals to leave it. Their answers are next year's event dummies.

Pitfalls

A dummy for every week. Fifty two dummies on three years of data is memorisation, not modelling. Group weeks into seasons the business recognises, and let the ACF tell you whether you have missed one.

Holidays that move. Mother's Day shifts between weeks 9 and 13,7 Easter wanders across March and April, and Black Friday, the Friday after the fourth Thursday in November,16 flips between weeks 47 and 48. Some years have 53 ISO weeks, as 2026 does.14 A fixed week dummy for a moving holiday smears the effect across two weeks and halves it. Build the dummy from the calendar, not the week number.

Grading on the fit. An R2R^2 of 0.91 on the training data says the model has learned the past. Only the holdout says whether it knows anything about the future. If you tune the seasons by looking at the holdout error, you have turned it into training data and need a fresh one.

Reporting the naive standard errors. The Father's Day effect had a t value of 3.1 before the AR(1) correction and 64/29=2.264 / 29 = 2.2 after. One is a finding, the other a maybe, and the owner deserves to know which.

Letting the target become the forecast. The plan says 7,000, so the forecast gets nudged, one assumption at a time, until it says 7,000 too. Then nobody has a forecast. Keep them in separate columns, name the gap, and make the marketing earn it.

How I do this for clients

The deliverable is a forecast the business can plan against, not a model: a table of weeks with a point forecast, an 80% band and the season, plus a one page decision memo that says what to order, whom to roster, and what would push the actuals out of the band. Behind it sits the model, in a repository you own, with the holdout scoreboard showing what it beat and by how much.

It starts with the free workshop, where we look at your history and agree the seasons, the events and the decisions the forecast has to support. Then two weeks of real work on me before you commit to anything: the weekly data, the calendar dummies, the benchmarks, the holdout and the memo. If the history is too short or too broken to forecast honestly, the memo says that instead, with what to collect and for how long.

The data I need is unglamorous: weekly orders and revenue for at least two years, the dates of every campaign, stockout and price change you can remember, and the last order dates you published each Christmas. For Solidus shops I take that straight from the database, part of my Solidus development work. The modelling is the Data Science side, and the fees are on the pricing page. Your raw data stays yours (the terms for code and models are in the analytics and data science terms), and the numbers come with error margins or they do not come at all.

For the wider toolkit, my complete guide to forecasting covers the method families, and the piece on AI powered demand forecasting for eCommerce covers what changes when you have thousands of products rather than one shop.

Questions to ask whoever hands you a forecast

  • What is the 80% band on this number, in units, and which week is the widest?
  • Which weeks were held out when the model was judged, and what was the MAE on them?
  • Did it beat "same week last year plus growth"? By how much?
  • What was the Durbin Watson statistic, and what did you do about it?
  • How are moving holidays handled, and what happens in a 53 week year?
  • Where is the target written down separately from the forecast, and how big is the gap?
  • What would have to happen for the actuals to fall outside the band, and who is watching for it?

The series is inspired by Mike Grigsby's Marketing Analytics.17 The explanations, examples and numbers here are my own.

Footnotes

  1. Hyndman and Athanasopoulos, Forecasting: Principles and Practice, 3rd ed., chapter 10 Dynamic regression models, section 10.1 Estimation (regression with ARIMA errors), OTexts, 2021, https://otexts.com/fpp3/regarima.html

  2. statsmodels, statsmodels.stats.stattools.durbin_watson, documentation, https://www.statsmodels.org/stable/generated/statsmodels.stats.stattools.durbin_watson.html

  3. Hyndman and Athanasopoulos, Forecasting: Principles and Practice, 3rd ed., section 2.8 Autocorrelation, OTexts, 2021, https://otexts.com/fpp3/acf.html

  4. Hyndman and Athanasopoulos, Forecasting: Principles and Practice, 3rd ed., section 3.2 Time series components, OTexts, 2021, https://otexts.com/fpp3/components.html

  5. Wikipedia, Saint Patrick's Day (17 March, a public holiday in the Republic of Ireland), https://en.wikipedia.org/wiki/Saint_Patrick%27s_Day

  6. Hyndman and Athanasopoulos, Forecasting: Principles and Practice, 3rd ed., section 7.4 Some useful predictors (seasonal dummy variables, Easter), OTexts, 2021, https://otexts.com/fpp3/useful-predictors.html

  7. Wikipedia, Mothering Sunday (kept in the United Kingdom and Ireland, on the fourth Sunday in Lent, three weeks before Easter Sunday), https://en.wikipedia.org/wiki/Mothering_Sunday

  8. Wikipedia, Father's Day (dates by country, third Sunday in June for Ireland), https://en.wikipedia.org/wiki/Father%27s_Day

  9. Hyndman and Athanasopoulos, Forecasting: Principles and Practice, 3rd ed., section 5.2 Some simple forecasting methods (seasonal naive), OTexts, 2021, https://otexts.com/fpp3/simple-methods.html

  10. Hyndman and Athanasopoulos, Forecasting: Principles and Practice, 3rd ed., section 8.3 Methods with seasonality (Holt Winters), OTexts, 2021, https://otexts.com/fpp3/holt-winters.html

  11. Hyndman and Athanasopoulos, Forecasting: Principles and Practice, 3rd ed., section 9.5 Non seasonal ARIMA models, OTexts, 2021, https://otexts.com/fpp3/non-seasonal-arima.html

  12. Hyndman and Athanasopoulos, Forecasting: Principles and Practice, 3rd ed., section 5.8 Evaluating point forecast accuracy, OTexts, 2021, https://otexts.com/fpp3/accuracy.html

  13. Hyndman and Athanasopoulos, Forecasting: Principles and Practice, 3rd ed., section 5.5 Distributional forecasts and prediction intervals (table of multipliers, 1.28 for 80%), OTexts, 2021, https://otexts.com/fpp3/prediction-intervals.html

  14. Wikipedia, ISO week date (weeks per year: years starting on a Thursday have 53 weeks), https://en.wikipedia.org/wiki/ISO_week_date

  15. statsmodels, statsmodels.regression.linear_model.GLSAR, documentation, https://www.statsmodels.org/stable/generated/statsmodels.regression.linear_model.GLSAR.html

  16. Wikipedia, Black Friday (shopping), https://en.wikipedia.org/wiki/Black_Friday_%28shopping%29

  17. Mike Grigsby, Marketing Analytics: A Practical Guide to Improving Consumer Insights Using Data Techniques, Kogan Page, 2022, ISBN 9781398608191, https://www.netgalley.co.uk/catalog/book/267990

If you have two or three years of weekly orders and a Christmas that keeps surprising you, bring the series to the free workshop, even as a rough export. We agree the seasons and the decisions the forecast has to support, and then the first two weeks of real work are on me. If your history is too short to forecast honestly, I will say so and tell you what to collect.