Look at the series before testing anything
The first check is a plot, and it is not a formality. A visible upward drift, a
variance that widens as the level rises, a step change at a known date, a
repeating weekly shape, a run of zeros where a sensor was offline — all of these
are obvious in thirty seconds and all of them change what you do next. A test
statistic reduces the series to one number and will not tell you that the trend
reversed halfway through.
Plot the autocorrelation function alongside it. Slow, near-linear decay across
many lags indicates a trend or a unit root. Spikes at regular multiples of a lag
indicate seasonality and tell you the period. A single spike at lag one that then
collapses suggests a series already close to stationary.
What stationarity is and why it is required
A stationary series has a constant mean, a constant variance, and an
autocovariance that depends only on the separation between two points rather than
on where in the series they sit. Weak stationarity is what practice means by the
term, and it is the assumption underneath ARIMA-family models: they estimate
coefficients on the assumption that the relationship between a point and its lags
is the same everywhere in the series.
If the series has a trend, that assumption fails and the estimates are not
meaningful. Two unrelated series that both trend upwards will appear correlated,
which is the spurious-regression problem, and it is why the check comes before the
model rather than after a disappointing result.
Two tests are worth knowing and they are complementary because their null
hypotheses are opposites. The augmented Dickey-Fuller test has
non-stationarity as its null, so a small p-value is evidence for stationarity.
The KPSS test has stationarity as its null, so a small p-value is evidence
against it. Running both is the standard practice, and the informative case is
when they disagree: that usually means the series is trend-stationary rather than
containing a unit root, which is a different remedy — detrend rather than
difference.
Differencing replaces each value with the change since the previous value,
which removes a linear trend. Doing it once is usually enough, and twice is
occasionally justified for a series whose trend itself accelerates. A seasonal
difference subtracts the value from one full period earlier, which removes a
stable seasonal pattern along with any trend that is constant across periods.
The under-discussed direction is over-differencing. Each difference amplifies
noise and injects artificial negative autocorrelation at lag one, so a series
differenced more than it needed is harder to forecast than the original. The
symptom is an ACF with a large negative spike at lag one where the undifferenced
series had none. Difference the minimum amount that passes your tests and no more.
Variance is a separate problem from mean. If the series fans out as its level
rises, differencing will not fix it, because the issue is multiplicative. A log
transform converts multiplicative growth into additive growth and stabilises the
variance, which is why so many demand and revenue series are modelled in logs. The
consequence to remember is that a forecast made in log space and exponentiated
back is a median rather than a mean, so if you are aggregating forecasts into a
budget total you have to correct for that rather than sum them naively.
Decomposition splits the series into trend, seasonal and remainder
components, additively or multiplicatively. Its main use is diagnostic: the
remainder is where you look for the anomalies and structural breaks that the
level and the seasonal shape were hiding. STL decomposition handles a seasonal
shape that changes gradually over the years, which classical decomposition
assumes away.
Multiple seasonalities at once
Hourly and daily series routinely carry several periods simultaneously. Electricity
demand has a daily profile, a weekly profile because weekends differ, and an
annual profile driven by temperature and daylight. A single seasonal difference
cannot represent that, and a classical seasonal ARIMA term handles one period.
Dummy variables are the naive answer and they scale badly. Hour-of-week alone is
167 indicators, annual day-of-year is 365, and the interaction is unusable — each
dummy is estimated from very few observations, so the fit is noisy and does not
generalise.
The scalable answer is Fourier terms: for each seasonal period, add a small
number of sine and cosine pairs at that period and its harmonics as regressors.
Three or four pairs will describe a smooth daily shape with eight parameters
rather than twenty-four, several periods can coexist because each simply
contributes its own pairs, and the number of pairs is a tunable smoothness
control chosen by backtest. This is the mechanism behind the seasonality
components of the widely used additive forecasting models, and it composes with
any regression-based approach.
for k in range(1, 4):
df[f"wk_sin{k}"] = np.sin(2 * np.pi * k * df["hour_index"] / 168)
df[f"wk_cos{k}"] = np.cos(2 * np.pi * k * df["hour_index"] / 168)
Holidays and the calendar are features, not outliers
The most common practical error is treating calendar effects as noise. Christmas,
a bank holiday weekend, or a national election produces a large residual, and
excluding it as an outlier throws away information that will recur on a known
future date. It is knowable in advance, so it belongs in the model as a regressor.
Fixed-date holidays are straightforward. Moving ones are where the work is: Easter
shifts by more than a month across years, Diwali and Eid move against the Gregorian
calendar, and a bank holiday that falls on a Monday shifts trade into the preceding
Saturday. So an indicator on the day itself is rarely enough — you need a window
around it, and separate indicators for the days before and after, because demand
usually pulls forward and then collapses rather than simply spiking.
The rest of the calendar deserves the same treatment. Month-end and quarter-end
drive business processes. Paydays drive consumer spending. The number of trading
days in a month varies and will otherwise show up as unexplained monthly
variation. Whether a holiday fell midweek or adjacent to a weekend changes its
effect substantially. Each of these is a cheap feature that removes a residual you
would otherwise be trying to model as seasonality, and getting them right usually
improves accuracy more than swapping model families does.
© 2026 Preptima. Originally published at preptima.com (opens in a new tab).