How to Perform the KPSS Test in R

Stationarity is the foundation of time series analysis. A stationary series has constant statistical properties over time—its mean, variance, and autocorrelation structure don't depend on when you...

Key Insights

  • The KPSS test assumes stationarity as its null hypothesis, making it the natural complement to the ADF test which assumes the opposite—use both together for robust conclusions
  • Choose level stationarity (type = "mu") when your series fluctuates around a constant mean, and trend stationarity (type = "tau") when there’s a deterministic trend you want to allow
  • A small p-value in KPSS means you reject stationarity, which is the opposite interpretation from ADF—getting this wrong will lead you to incorrect modeling decisions

Introduction to the KPSS Test

Stationarity is the foundation of time series analysis. A stationary series has constant statistical properties over time—its mean, variance, and autocorrelation structure don’t depend on when you observe it. Most forecasting methods, from ARIMA to exponential smoothing, assume stationarity or require you to transform your data to achieve it.

The Kwiatkowski-Phillips-Schmidt-Shin (KPSS) test, published in 1992, tests whether a time series is stationary. Its defining characteristic is the null hypothesis: KPSS assumes the series is stationary. You’re testing for evidence of non-stationarity.

This is the opposite of the Augmented Dickey-Fuller (ADF) test, where the null hypothesis assumes a unit root (non-stationarity). This difference matters enormously in practice. With ADF, failing to reject means you can’t confirm stationarity. With KPSS, failing to reject means you can’t reject stationarity. Using both tests together gives you a more complete picture than either alone.

Prerequisites and Setup

You’ll need two packages for comprehensive KPSS testing in R. The tseries package provides a simple interface for quick tests, while urca offers more control over technical parameters.

# Install packages if needed
install.packages("tseries")
install.packages("urca")

# Load the packages
library(tseries)
library(urca)

Both packages are mature and well-maintained. I recommend having both available—use tseries for quick exploratory analysis and urca when you need fine-grained control over lag selection or want detailed output for reporting.

Understanding KPSS Test Types

The KPSS test comes in two flavors, and choosing the wrong one will give you misleading results.

Level stationarity tests whether the series is stationary around a constant mean. The null hypothesis is that the series can be written as the sum of a constant, a stationary error, and a random walk component—with the random walk having zero variance. Use this when your data should fluctuate around a fixed level with no trend.

Trend stationarity tests whether the series is stationary around a deterministic linear trend. The null hypothesis allows for a trend in the mean, but assumes deviations from that trend are stationary. Use this when your data has an obvious upward or downward drift that you consider part of the signal, not evidence of non-stationarity.

Here’s the practical decision rule: if you’d difference the data to remove a trend before modeling, test for level stationarity. If you’d include a trend term in your model and keep the data undifferenced, test for trend stationarity.

Basic KPSS Test Implementation

Let’s start with the tseries package implementation. We’ll use the classic AirPassengers dataset, which contains monthly airline passenger totals from 1949 to 1960.

# Load built-in dataset
data("AirPassengers")

# Quick visual inspection
plot(AirPassengers, main = "Monthly Airline Passengers",
     ylab = "Passengers (thousands)", xlab = "Year")

The plot shows an obvious upward trend and increasing variance—classic signs of non-stationarity. Let’s confirm with the KPSS test.

# Test for level stationarity (default)
kpss_level <- kpss.test(AirPassengers, null = "Level")
print(kpss_level)

Output:

	KPSS Test for Level Stationarity

data:  AirPassengers
KPSS Level = 1.052, Truncation lag parameter = 4, p-value = 0.01

The p-value is 0.01, which is below any conventional significance level. We reject the null hypothesis of level stationarity. The series is not stationary around a constant mean.

Now let’s test for trend stationarity:

# Test for trend stationarity
kpss_trend <- kpss.test(AirPassengers, null = "Trend")
print(kpss_trend)

Output:

	KPSS Test for Trend Stationarity

data:  AirPassengers
KPSS Trend = 0.1082, Truncation lag parameter = 4, p-value = 0.01

Even allowing for a deterministic trend, we still reject stationarity. The AirPassengers series needs transformation—typically log transformation followed by differencing—before modeling.

Let’s compare with a stationary series:

# Generate a stationary AR(1) process
set.seed(42)
stationary_series <- arima.sim(model = list(ar = 0.7), n = 200)

# Test for level stationarity
kpss.test(stationary_series, null = "Level")

Output:

	KPSS Test for Level Stationarity

data:  stationary_series
KPSS Level = 0.1194, Truncation lag parameter = 4, p-value = 0.1

The p-value is 0.1 (the maximum reported by kpss.test), so we fail to reject stationarity. This is the expected result for a simulated stationary process.

Interpreting Results

The KPSS test returns several components. Understanding each one helps you make informed decisions.

# Run test and store results
result <- kpss.test(AirPassengers, null = "Level")

# Extract components
cat("Test statistic:", result$statistic, "\n")
cat("P-value:", result$p.value, "\n")
cat("Lag parameter:", result$parameter, "\n")
cat("Test type:", result$method, "\n")

The test statistic measures how much the cumulative sum of residuals deviates from what you’d expect under stationarity. Larger values indicate stronger evidence against stationarity.

The p-value in tseries is interpolated from critical value tables. Note that it’s bounded between 0.01 and 0.1. If you see 0.01, the true p-value might be much smaller. If you see 0.1, the true p-value might be larger.

The truncation lag parameter controls how the test handles autocorrelation in the residuals. The tseries package uses a default formula based on sample size. This is usually adequate, but you can override it if you have domain knowledge suggesting a different choice.

Decision rules:

P-value Interpretation
< 0.05 Reject stationarity at 5% level
< 0.01 Strong evidence against stationarity
> 0.05 Cannot reject stationarity
= 0.1 Likely stationary (p-value may be higher)

Advanced Usage with the urca Package

The urca package provides ur.kpss(), which offers more flexibility and detailed output.

# KPSS test with urca - level stationarity
kpss_urca_mu <- ur.kpss(AirPassengers, type = "mu", lags = "short")
summary(kpss_urca_mu)

The type parameter accepts "mu" for level stationarity and "tau" for trend stationarity. The lags parameter controls the lag truncation: "short" uses 4 lags, "long" uses 12, and "nil" uses none. You can also specify an exact number.

# Compare different lag specifications
kpss_short <- ur.kpss(AirPassengers, type = "mu", lags = "short")
kpss_long <- ur.kpss(AirPassengers, type = "mu", lags = "long")
kpss_custom <- ur.kpss(AirPassengers, type = "mu", lags = 8)

# Extract test statistics
cat("Short lags (4):", kpss_short@teststat, "\n")
cat("Long lags (12):", kpss_long@teststat, "\n")
cat("Custom lags (8):", kpss_custom@teststat, "\n")

# View critical values
cat("\nCritical values:\n")
print(kpss_short@cval)

The urca output includes critical values at 10%, 5%, 2.5%, and 1% significance levels. Compare your test statistic directly to these values. If the test statistic exceeds the critical value, reject stationarity at that significance level.

Practical Workflow: Combining KPSS with Other Tests

Neither KPSS nor ADF alone tells the complete story. Their opposite null hypotheses create a useful decision matrix when used together.

# Function to run both tests and interpret results
test_stationarity <- function(series, alpha = 0.05) {
  # Run ADF test
  adf_result <- adf.test(series)
  adf_pvalue <- adf_result$p.value
  adf_reject <- adf_pvalue < alpha
  

  # Run KPSS test
  kpss_result <- kpss.test(series, null = "Level")
  kpss_pvalue <- kpss_result$p.value
  kpss_reject <- kpss_pvalue < alpha
  
  # Interpret combined results
  conclusion <- case_when(
    adf_reject & !kpss_reject ~ "Stationary",
    !adf_reject & kpss_reject ~ "Non-stationary",
    adf_reject & kpss_reject ~ "Trend stationary (difference or detrend)",
    !adf_reject & !kpss_reject ~ "Inconclusive (need more data)"
  )
  
  # Return summary
  list(
    adf_pvalue = round(adf_pvalue, 4),
    adf_reject_unit_root = adf_reject,
    kpss_pvalue = round(kpss_pvalue, 4),
    kpss_reject_stationarity = kpss_reject,
    conclusion = conclusion
  )
}

# Need dplyr for case_when
library(dplyr)

# Test on different series
cat("AirPassengers:\n")
print(test_stationarity(AirPassengers))

cat("\nStationary AR(1):\n")
print(test_stationarity(stationary_series))

cat("\nRandom walk:\n")
random_walk <- cumsum(rnorm(200))
print(test_stationarity(random_walk))

The interpretation matrix works as follows:

ADF rejects unit root KPSS rejects stationarity Conclusion
Yes No Series is stationary
No Yes Series is non-stationary
Yes Yes Trend stationary
No No Tests disagree—inconclusive

When tests disagree, you need additional analysis. Consider the sample size (small samples have low power), visual inspection, and domain knowledge about the data-generating process.

For production code, wrap this logic into a reusable function and include it in your time series preprocessing pipeline. Always test stationarity before fitting ARIMA models, and document which tests you ran and what transformations you applied.

The KPSS test is one tool in your stationarity testing toolkit. Use it alongside ADF, examine your data visually, and remember that statistical tests are guides, not oracles. When the tests disagree with obvious visual patterns, trust your eyes and investigate further.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.