How to Understand Stationarity in Time Series Data with Python

How to Understand Stationarity in Time Series Data with Python
Image by Author | Piktochart

The concept of stationarity in time series data — not to be mixed up with that of seasonality — refers to maintaining similar statistical properties across the time. In other words, aspects in the time series like mean, variances, and autocorrelation remain constant.

It is somehow the opposite to exhibiting trends or seasonality. When things seem to behave in a stable manner across time (stationarity), there are little or not patterns that hint at seasonal phenomena or trends.

This article describes how to analyze and understand stationarity in time series data in Python.

Step-by-Step Analysis

Following some previous articles in this series, we will use the Chicago rides public version with a CSV version I made available here.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from statsmodels.tsa.stattools import adfuller

# Load the dataset
url = "https://raw.githubusercontent.com/gakudo-ai/open-datasets/refs/heads/main/CTA_-_Ridership_-_Daily_Boarding_Totals.csv"
df = pd.read_csv(url)

df['service_date'] = pd.to_datetime(df['service_date'])
df.set_index('service_date', inplace=True)

df.sort_index(inplace=True)

One novel aspect in this initial part of the code is the imported function adfuller() from the statsmodels.tsa.stattools module, which we will use to conduct the Augmented Dickey-Fuller (ADF) test, a well-known statistical test for analyzing stationarity in time series data.

We fetch the bus rides variable dataset into an univariate time series, and perform an initial visual inspection:

bus_data = df['bus']

plt.figure(figsize=(14, 7))
plt.plot(bus_data, label='Bus Ridership', alpha=0.7)
plt.title('Bus Ridership Over Time')
plt.xlabel('Date')
plt.ylabel('Daily Boardings')
plt.legend()
plt.grid()
plt.show()

Chicago bus rides time series
Chicago bus rides time series

At first glance, the time series seems rather stable overall, except for the sudden shift from March 2020 onwards caused by the start of the COVID-19 pandemic and the normal life disruptions it brought. But this is just “at first glance”. Since the series is considerably large, we would need a close-up view to spot possible trends or seasonality patterns invisible to us in this initial visualization.

So, what does the ADF test for stationarity have to say? We will define a custom function that applies the test and analyzes the multiple outputs it returns to draw conclusions on our time series’ stationarity. The test returns multiple outputs, some of which are:

  1. An ADF statistic, which quantifies how strongly the time series deviates from non-stationarity (the null hypothesis of the test).
  2. Critical values, which are thresholds for the ADF statistic under three confidence levels.
  3. p-value: the probability, stemming from the AD statistic, of observing it under the null hypothesis of non-stationarity. If small (≤ 0.05) the series is considered stationary.

Here’s the implementation:

def adf_test(series):
    result = adfuller(series, autolag='AIC')
    print("ADF Statistic:", result[0])
    print("p-value:", result[1])
    print("Critical Values:", result[4])
    if result[1] <= 0.05:
        print("Conclusion: The series is stationary.")
    else:
        print("Conclusion: The series is non-stationary.")

print("\nADF Test on Original Data:")
adf_test(bus_data)

Output:

ADF Test on Original Data:
ADF Statistic: -2.5828785118818343
p-value: 0.09658781512192754
Critical Values: {'1%': -3.4311676749363924, '5%': -2.8619013537344533, '10%': -2.566962343890625}
Conclusion: The series is non-stationary.

The reported non-stationarity indicates the presence of trends or seasonal patterns, which often make other analysis tasks like forecasting more difficult. How to remove them? By applying a process called differencing, consisting in subtracting the value at the previous time step from the current value. Let’s see how:

bus_data_diff = bus_data.diff().dropna()

Well, that was pretty easy! How about visualizing the differenced time series and re-applying the ADF test on it?

plt.figure(figsize=(14, 7))
plt.plot(bus_data_diff, label='Differenced Bus Ridership', color='orange', alpha=0.7)
plt.title('Differenced Bus Ridership Over Time')
plt.xlabel('Date')
plt.ylabel('Daily Change in Boardings')
plt.legend()
plt.grid()
plt.show()

Differenced time series to achieve stationarity
Differenced time series to achieve stationarity

print("\nADF Test on Differenced Data:")
adf_test(bus_data_diff)

Output:

ADF Test on Differenced Data:
ADF Statistic: -22.918732110156395
p-value: 0.0
Critical Values: {'1%': -3.4311676749363924, '5%': -2.8619013537344533, '10%': -2.566962343890625}
Conclusion: The series is stationary.

Wrapping Up

This article showed how to analyze and interpret stationarity in time series data in Python. Making a time series stationary is important to use forecasting models like ARIMA that assume your data is stationary.

Leave a Reply

Your email address will not be published. Required fields are marked *