Detecting Seasonality Automatically in a New Series
9 min read · updated August 11, 2026
Detecting seasonality automatically means finding the lag at which a series correlates with itself. The arithmetic is short enough to do by hand, and doing it once by hand shows you both how the detection works and the specific circumstance in which it cannot give an answer.
Detrend first, or you only measure the trend
The autocorrelation of a trending series is large and positive at every small lag and decays slowly, because two nearby points of a rising series are both above the overall mean. That structure swamps any seasonal peak, and running peak detection on it finds lag 1 and stops.
So detrend before you look. First differencing is the cheap option and it is what an automatic pipeline usually does. Taking the seasonal plus remainder components of an STL decomposition is better, though it is circular here, since STL needs the period you are trying to find. The workable order is: difference once, find the candidate period from the differenced series, then run STL with that period and check that the seasonal component it produces is actually large relative to the remainder.
The autocorrelation arithmetic
The sample autocorrelation at lag k is the sum of products of deviations k apart, divided by the sum of squared deviations:
r_k = sum_{t=1..n-k} (y_t - ybar)(y_{t+k} - ybar) / sum_{t=1..n} (y_t - ybar)^2Take the quarterly series used on the STL page: 10, 14, 12, 8, 12, 17, 14, 9, 14, 20, 16, 10. It sums to 156 over twelve observations, so the mean is 13 and the deviations are
d: -3 +1 -1 -5 -1 +4 +1 -4 +1 +7 +3 -3
sum of d^2 = 9+1+1+25+1+16+1+16+1+49+9+9 = 138
lag 1: (-3)(1) + (1)(-1) + (-1)(-5) + (-5)(-1) + (-1)(4) + (4)(1)
+ (1)(-4) + (-4)(1) + (1)(7) + (7)(3) + (3)(-3) = 17
lag 2: ... = -83
lag 3: ... = 5
lag 4: (-3)(-1) + (1)(4) + (-1)(1) + (-5)(-4) + (-1)(1)
+ (4)(7) + (1)(3) + (-4)(-3) = 68
r_1 = 17/138 = 0.123 r_5 = -10/138 = -0.072
r_2 = -83/138 = -0.601 r_6 = -58/138 = -0.420
r_3 = 5/138 = 0.036 r_7 = -6/138 = -0.043
r_4 = 68/138 = 0.493 r_8 = 16/138 = 0.116Reading the peaks and the troughs
The signature of a period of 4 is exactly what those eight numbers show. The largest positive value at a lag above 1 is at lag 4. The values at lags 2 and 6 — odd multiples of half the period — are strongly negative, because half a period out of phase means a peak aligns with a trough. The values at the odd lags 3, 5 and 7 are near zero. A detector does not need anything cleverer than “take the first lag k > 1 at which r_k is a local maximum and positive” to return 4 here.
Notice also that r_8 is only 0.116, well below r_4, even though lag 8 is also a multiple of the period. That is not a contradiction. Only four products contribute at lag 8 against eight at lag 4, and the series has a rising trend which was not removed, so distant pairs are pulled apart. Expecting the peak at 2p to match the peak at p is a common misreading; on a finite series it never does.
The complementary check is a strength measure rather than a location. The standard one, computed from an STL decomposition, compares the variance of the remainder with the variance of the seasonal and remainder together: a strength near 1 means the seasonal component explains nearly all the non-trend variation, and near 0 means it explains almost none. Locating a period and establishing that it is worth modelling are two separate tests, and an automatic pipeline should run both — otherwise it will fit a seasonal component to a lag that happened to peak in noise.
How much data the test needs
Now the inconvenient part, and it is why this example was worked rather than asserted. Under a white-noise null hypothesis the sample autocorrelations are approximately normal with standard error 1/√n, so the usual 95 per cent band is ±1.96/√n. For n = 12 that is ±1.96/3.464 = ±0.566.
The lag-4 peak is 0.493. It does not clear the band. Only the lag-2 trough at −0.601 does, and that is the half-period effect combined with the untreated trend rather than a clean seasonal signal. On three years of quarterly data, the correct conclusion from this test is that seasonality is suggested and not established, even though we constructed the series to be seasonal and the peak landed in exactly the right place.
The band shrinks with √n, so the requirement is blunt: with quarterly data you want considerably more than three years, and the conventional minimum of two complete cycles is enough to make a seasonal pattern visible but not enough to make it statistically distinguishable from noise. When a pipeline must decide on a short series, the honest options are to accept a seasonal model only when the period is known a priori from the domain, or to accept it on a weaker criterion and let the holdout error decide, which at least tests the thing you care about.
The band also assumes the series is white noise under the null, and a differenced series generally is not, so the ±1.96/√n rule is an approximation that runs slightly optimistic. A portmanteau test such as Ljung-Box tests a block of lags jointly rather than one at a time, which avoids the multiple-comparison problem you create by scanning twenty lags and reporting the largest. If your detector scans lags and reports the best one, the significance of that best one is not the significance printed next to it.
Multiple and non-integer seasonality
- More than one period is normal. Hourly data usually has both a daily period of 24 and a weekly period of 168, and both appear in the autocorrelation. Take the peaks in increasing order rather than the largest one, and note that 168 is a multiple of 24, so a naive largest-peak rule can return either.
- Non-integer periods cannot be found this way. An annual cycle in daily data has a period of about 365.25, which never aligns to an integer lag, so the autocorrelation at 365 is smeared across neighbouring lags and weakens as the years accumulate. Fourier terms at the true frequency handle this; a lag does not.
- The frequency domain is the alternative. A periodogram turns the same question into finding the largest spectral peak, with an accompanying test for whether that peak is larger than a white-noise spectrum would produce. It is better at separating two close periods and worse at short series, where the frequency resolution is 1/n.
- Calendar effects are not seasonality. Moving holidays, month lengths and the number of trading days in a month produce apparent instability at a fixed lag. They are regressors, not periods; see forecasting with exogenous variables.
- Decide the sampling grain first. A period only exists relative to a sampling rate, and aggregating daily data to weekly destroys the day-of-week cycle entirely. That trade-off is the subject of choosing forecast granularity.