How to Perform the KPSS Test in Python
The Kwiatkowski-Phillips-Schmidt-Shin (KPSS) test is a statistical test for checking the stationarity of a time series. Unlike the more commonly used Augmented Dickey-Fuller (ADF) test, the KPSS test...
Key Insights
- The KPSS test uses stationarity as its null hypothesis, making it complementary to the ADF test which tests for unit roots—using both together provides more reliable conclusions about your time series data.
- Choose
regression='c'for level stationarity (constant mean) orregression='ct'for trend stationarity (stationary around a deterministic trend) based on your data’s characteristics. - When KPSS and ADF tests give conflicting results, your series likely has a unit root with a trend component, requiring both differencing and detrending to achieve stationarity.
Introduction to the KPSS Test
The Kwiatkowski-Phillips-Schmidt-Shin (KPSS) test is a statistical test for checking the stationarity of a time series. Unlike the more commonly used Augmented Dickey-Fuller (ADF) test, the KPSS test flips the hypothesis structure: its null hypothesis states that the series is stationary.
This distinction matters more than you might think. The ADF test’s null hypothesis assumes the presence of a unit root (non-stationarity), so failing to reject it doesn’t confirm stationarity—it simply means you lack evidence against non-stationarity. The KPSS test provides direct evidence for stationarity, making it a valuable confirmatory tool.
The test works by decomposing the series into a deterministic trend, a random walk, and a stationary error. It then tests whether the variance of the random walk component equals zero. If it does, the series is stationary.
When to Use the KPSS Test
You’ll reach for the KPSS test in several scenarios:
ARIMA model preparation: Before fitting ARIMA models, you need stationary data. The KPSS test helps confirm your differencing strategy worked.
Financial time series analysis: Stock returns, exchange rates, and volatility measures require stationarity testing before modeling. The KPSS test provides a second opinion alongside ADF.
Economic data validation: GDP growth rates, inflation, and unemployment figures often need stationarity verification for valid regression analysis.
The KPSS test offers two modes via the regression parameter:
- Level stationarity (
'c'): Tests whether the series is stationary around a constant mean. Use this for data without obvious trends. - Trend stationarity (
'ct'): Tests whether the series is stationary around a deterministic linear trend. Use this when your data shows a clear upward or downward trajectory.
Choosing the wrong mode leads to incorrect conclusions. A trending series tested with 'c' will almost always reject stationarity, even if it’s perfectly trend-stationary.
Implementation with statsmodels
The statsmodels library provides a straightforward implementation of the KPSS test. Here’s the basic usage:
import numpy as np
import pandas as pd
from statsmodels.tsa.stattools import kpss
# Generate sample stationary data
np.random.seed(42)
stationary_data = np.random.normal(loc=10, scale=2, size=200)
# Perform KPSS test for level stationarity
statistic, p_value, n_lags, critical_values = kpss(stationary_data, regression='c')
print(f"KPSS Statistic: {statistic:.4f}")
print(f"P-Value: {p_value:.4f}")
print(f"Lags Used: {n_lags}")
print(f"Critical Values: {critical_values}")
The regression parameter accepts two values:
# Level stationarity - series fluctuates around a constant
kpss(data, regression='c')
# Trend stationarity - series fluctuates around a linear trend
kpss(data, regression='ct')
By default, the function uses an automatic lag selection method based on the sample size. You can override this with the nlags parameter if you have specific requirements:
# Specify exact number of lags
statistic, p_value, n_lags, critical_values = kpss(data, regression='c', nlags=12)
# Use automatic lag selection methods
statistic, p_value, n_lags, critical_values = kpss(data, regression='c', nlags='auto')
Interpreting KPSS Results
The KPSS test returns four values that require careful interpretation:
- Test statistic: The computed KPSS statistic. Larger values indicate stronger evidence against stationarity.
- P-value: The probability of observing this statistic if the series is truly stationary. Low p-values suggest non-stationarity.
- Lags used: The number of lags included in the calculation to account for autocorrelation.
- Critical values: Threshold values at 10%, 5%, 2.5%, and 1% significance levels.
Here’s a function that produces interpretable output:
from statsmodels.tsa.stattools import kpss
def kpss_test(series, regression='c', significance_level=0.05):
"""
Perform KPSS test and return formatted results.
Parameters:
-----------
series : array-like
Time series data to test
regression : str
'c' for level stationarity, 'ct' for trend stationarity
significance_level : float
Significance level for decision (default 0.05)
Returns:
--------
dict : Test results and interpretation
"""
statistic, p_value, n_lags, critical_values = kpss(series, regression=regression)
# Determine stationarity type being tested
test_type = "level" if regression == 'c' else "trend"
# Make decision
is_stationary = p_value > significance_level
results = {
'test_statistic': statistic,
'p_value': p_value,
'lags_used': n_lags,
'critical_values': critical_values,
'is_stationary': is_stationary,
'test_type': test_type
}
# Print formatted output
print("=" * 50)
print(f"KPSS Test Results ({test_type} stationarity)")
print("=" * 50)
print(f"Test Statistic: {statistic:.6f}")
print(f"P-Value: {p_value:.4f}")
print(f"Lags Used: {n_lags}")
print("\nCritical Values:")
for key, value in critical_values.items():
print(f" {key}: {value:.4f}")
print("\n" + "-" * 50)
print(f"Significance Level: {significance_level}")
if is_stationary:
print(f"Result: FAIL TO REJECT null hypothesis")
print(f"Conclusion: Series is {test_type}-stationary")
else:
print(f"Result: REJECT null hypothesis")
print(f"Conclusion: Series is NOT {test_type}-stationary")
print("=" * 50)
return results
Remember: rejecting the null hypothesis means the series is not stationary. This is the opposite of the ADF test interpretation.
Practical Example: Testing Real-World Data
Let’s work through a complete example using stock price data:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from statsmodels.tsa.stattools import kpss
import yfinance as yf
# Download stock data
ticker = yf.Ticker("AAPL")
df = ticker.history(period="2y")
prices = df['Close'].dropna()
# Visualize the data
fig, axes = plt.subplots(2, 1, figsize=(12, 8))
# Plot raw prices
axes[0].plot(prices.index, prices.values, 'b-', linewidth=1)
axes[0].set_title('AAPL Closing Prices (Raw)')
axes[0].set_xlabel('Date')
axes[0].set_ylabel('Price ($)')
axes[0].grid(True, alpha=0.3)
# Calculate and plot returns
returns = prices.pct_change().dropna()
axes[1].plot(returns.index, returns.values, 'g-', linewidth=0.8)
axes[1].set_title('AAPL Daily Returns')
axes[1].set_xlabel('Date')
axes[1].set_ylabel('Return')
axes[1].grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('stock_analysis.png', dpi=150)
plt.show()
# Test raw prices for trend stationarity
print("\n--- Testing Raw Prices ---")
results_prices = kpss_test(prices.values, regression='ct')
# Test returns for level stationarity
print("\n--- Testing Returns ---")
results_returns = kpss_test(returns.values, regression='c')
Stock prices typically fail the KPSS test because they follow a random walk. Returns, however, usually pass because they’re approximately stationary. This example demonstrates why we model returns rather than prices in financial applications.
Combining KPSS with ADF Test
Using both tests together creates a more robust stationarity assessment. Here are the four possible outcome scenarios:
| ADF Result | KPSS Result | Interpretation |
|---|---|---|
| Reject H0 (no unit root) | Fail to reject H0 (stationary) | Stationary - Both tests agree |
| Fail to reject H0 | Reject H0 (not stationary) | Non-stationary - Both tests agree |
| Reject H0 | Reject H0 | Trend-stationary - Stationary around trend |
| Fail to reject H0 | Fail to reject H0 | Inconclusive - Need more data or different approach |
Here’s a function that runs both tests:
from statsmodels.tsa.stattools import adfuller, kpss
def stationarity_tests(series, significance_level=0.05):
"""
Run both ADF and KPSS tests for comprehensive stationarity assessment.
"""
# ADF Test
adf_result = adfuller(series, autolag='AIC')
adf_statistic = adf_result[0]
adf_pvalue = adf_result[1]
adf_reject = adf_pvalue < significance_level
# KPSS Test (level stationarity)
kpss_statistic, kpss_pvalue, _, _ = kpss(series, regression='c')
kpss_reject = kpss_pvalue < significance_level
print("=" * 60)
print("STATIONARITY TEST COMPARISON")
print("=" * 60)
print(f"\nADF Test (H0: Unit root exists)")
print(f" Statistic: {adf_statistic:.4f}")
print(f" P-Value: {adf_pvalue:.4f}")
print(f" Decision: {'Reject H0' if adf_reject else 'Fail to reject H0'}")
print(f"\nKPSS Test (H0: Series is stationary)")
print(f" Statistic: {kpss_statistic:.4f}")
print(f" P-Value: {kpss_pvalue:.4f}")
print(f" Decision: {'Reject H0' if kpss_reject else 'Fail to reject H0'}")
print("\n" + "-" * 60)
print("COMBINED INTERPRETATION:")
if adf_reject and not kpss_reject:
conclusion = "Series is STATIONARY"
elif not adf_reject and kpss_reject:
conclusion = "Series is NON-STATIONARY"
elif adf_reject and kpss_reject:
conclusion = "Series is TREND-STATIONARY (difference or detrend)"
else:
conclusion = "Results INCONCLUSIVE (consider more data)"
print(f" {conclusion}")
print("=" * 60)
return {
'adf_pvalue': adf_pvalue,
'kpss_pvalue': kpss_pvalue,
'conclusion': conclusion
}
Handling Non-Stationary Results
When the KPSS test indicates non-stationarity, you have several options:
Differencing removes unit roots by computing the change between consecutive observations:
def difference_until_stationary(series, max_diff=3, significance_level=0.05):
"""
Apply differencing until series passes KPSS test.
"""
current_series = series.copy()
for d in range(max_diff + 1):
if d > 0:
current_series = np.diff(current_series)
_, p_value, _, _ = kpss(current_series, regression='c')
print(f"Differencing order {d}: KPSS p-value = {p_value:.4f}")
if p_value > significance_level:
print(f"\nSeries is stationary after {d} difference(s)")
return current_series, d
print(f"\nWarning: Series not stationary after {max_diff} differences")
return current_series, max_diff
# Example usage
np.random.seed(42)
random_walk = np.cumsum(np.random.randn(200)) # Non-stationary
stationary_series, order = difference_until_stationary(random_walk)
# Verify with visualization
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].plot(random_walk)
axes[0].set_title('Original (Non-stationary)')
axes[1].plot(stationary_series)
axes[1].set_title(f'After {order} Difference(s)')
plt.tight_layout()
plt.show()
For trend-stationary series, detrending works better than differencing:
from scipy import signal
def detrend_series(series):
"""Remove linear trend from series."""
return signal.detrend(series)
# Apply detrending and re-test
detrended = detrend_series(trending_data)
kpss_test(detrended, regression='c')
The KPSS test is a fundamental tool for time series analysis. Use it alongside the ADF test, choose the right regression mode, and you’ll make confident decisions about your data’s stationarity before building models.