Skip to content
AI360XpertBeta

Stationarity Testing With ADF and KPSS

A steady series keeps the same mean and spread over time. These two tests check that steadiness before you trust a forecast.

Levels 5 to 15 drift from mean 7 to mean 13 while differences hold steady at 2, so differencing restores stationarity
Levels 5 to 15 drift from mean 7 to mean 13 while differences hold steady at 2, so differencing restores stationarity

Why Does This Exist?

ARIMA and most classical forecasters assume the past looks like the future in one narrow sense: same average, same spread, same lag behaviour. Feed them a climbing series and they learn a mean that no longer exists. Your intervals look tight in training, then the live series walks away from them.

You need a verdict before you model: steady or wandering, and which fix applies. Eyeballing helps, but two series can look alike while one needs differencing and the other needs detrending. The Augmented Dickey-Fuller and KPSS tests give you that verdict with a clear null each, so you stop guessing.

This page teaches the two tests, how to read them together, and the smallest fix that steadies the data. What steadiness means from zero lives in time series fundamentals. Splitting trend from season before you test lives in time-series decomposition. The model that needs the steady input is ARIMA.

Basics first: weak stationarity means constant mean, constant variance, and covariances that depend only on lag kk, not on time tt. If that triple is new, read fundamentals first. Everything below uses it.

Think of It Like This

Checking the floor before you align wheels

Wheel alignment assumes a level floor. On a tilt, every laser reading bends the same way and your fix corrects a slope that is not in the car.

Stationarity is that level floor. The tests check whether the statistical floor tilts before you run analysis on top of it. Differencing is the shim that levels it.

Where it stops: a level floor stays level. A steadied series can still break later if the world shifts. That later shift is drift in production, not non-stationarity in training, and no test on old data can rule it out.

How It Actually Works

A weakly stationary series yty_t keeps three promises for every tt and every lag kk. Mean μ\mu stays put, so E[yt]=μ\mathbb{E}[y_t] = \mu. Spread σ2\sigma^2 stays put, so Var(yt)=σ2\text{Var}(y_t) = \sigma^2. Lag behaviour stays put, so Cov(yt,yt−k)=γk\text{Cov}(y_t, y_{t-k}) = \gamma_k with no tt on the right. A trend breaks the first. Growing volatility breaks the second. A changing season breaks the third.

The ADF test hunts a unit root, the formal cause of most wandering. Its null H0H_0 says a unit root exists, meaning non-stationary. Its alternative says the series is stationary. So a small p-value below 0.05 rejects wandering and blesses the levels. A large p-value keeps the wandering verdict and sends you to differencing.

The KPSS test flips the burden. Its null says stationary. Its alternative says a unit root exists. So a small p-value below 0.05 rejects steadiness. A large p-value keeps the steady verdict.

Read them together, because each errs alone:

  • ADF rejects wandering and KPSS keeps steady means stationary. Model the levels.
  • ADF keeps wandering and KPSS rejects steady means a unit root. Difference once and retest.
  • Both keep their nulls means trend-stationary. Remove the trend rather than differencing blindly.
  • Both reject means too little data or a break in the middle. Plot rolling means and check autocorrelation before you difference.

The fixes are small and ordered. First differences Δyt=yt−yt−1\Delta y_t = y_t - y_{t-1} remove a straight trend. Seasonal differences yt−yt−my_t - y_{t-m} remove a fixed season with period mm. A log steadies spread that grows with level. The count of differences you needed is the integration order dd: 0 means already steady, 1 covers most business series, 2 is rare.

Worked example

Six weeks of signups: 5, 7, 9, 11, 13, 15. Overall mean is (5+7+9+11+13+15)/6=60/6=10(5 + 7 + 9 + 11 + 13 + 15) / 6 = 60 / 6 = 10.

  • First half mean is (5+7+9)/3=21/3=7(5 + 7 + 9) / 3 = 21 / 3 = 7.
  • Second half mean is (11+13+15)/3=39/3=13(11 + 13 + 15) / 3 = 39 / 3 = 13.
  • The mean moved 7 to 13, so levels fail the first promise.

First differences are 2, 2, 2, 2, 2, with mean 10/5=210 / 5 = 2. Every difference equals 2, so spread is 0 and the mean holds at 2 across halves. One difference turned I(1)I(1) wandering into I(0)I(0) steady. That is exactly what the diagram shows: drifting dots left, flat line at 2 right.

Code

This stdlib check mirrors the diagram: compare half means on levels, then on differences. No external tests, just the drift made visible:

def mean(xs: list[float]) -> float:    return sum(xs) / len(xs)

def differences(xs: list[float]) -> list[float]:    return [b - a for a, b in zip(xs, xs[1:])]

levels: list[float] = [5.0, 7.0, 9.0, 11.0, 13.0, 15.0]print(mean(levels))  # -> 10.0print(mean(levels[:3]))  # -> 7.0print(mean(levels[3:]))  # -> 13.0diffs: list[float] = differences(levels)print(diffs)  # -> [2.0, 2.0, 2.0, 2.0, 2.0]print(mean(diffs))  # -> 2.0

Levels drift 7.0 to 13.0 while differences sit at 2.0 throughout. That gap is your signal to difference once, then run ADF and KPSS on the steadied series.

Watch Out For

Trusting ADF on a short series

You run both tests on 40 points, get large p-values everywhere, and declare the series trend-stationary. The symptom is a confident verdict from starved data: rolling means still slope, yet you model levels because the test did not reject.

Fix it by plotting rolling mean and variance plus the correlogram before you believe a keep-the-null result. Under about 100 points both tests run weak, so visual drift overrules a quiet p-value.

Differencing a steady series

You difference because the pipeline always differences. The symptom is new trouble: a clean series gains a false moving-average wiggle, variance roughly doubles, and forecasts swing around the truth they used to hit.

Fix it by differencing only to the verdict. If ADF already rejects wandering at d=0d = 0 and KPSS keeps steady, stop. The smallest dd that steadies the mean wins.

The Quick Version

  • Stationarity means steady mean, steady spread, and lag behaviour that ignores clock time.
  • ADF null says wandering. Below 0.05 means steady enough to model in levels.
  • KPSS null says steady. Below 0.05 means wandering, so transform first.
  • Six levels from 5 to 15 drift 7 to 13, while differences hold at 2.
  • Difference straight trends, seasonally difference fixed seasons, log growing spread.
  • Short series fool both tests, so confirm with rolling means and the correlogram.