Statistical Analysis·Time Series Methods·Intermediate

ADF Stationarity Test

Implement the augmented Dickey-Fuller test for time series stationarity with automated lag order selection using information criteria, proper deterministic trend specification, and rigorous statistical interpretation for use in pairs trading candidate screening and model prerequisite checking.

quant-analysisstatistical-methodstime-series

Statistical Analysis: Augmented Dickey-Fuller (ADF) Stationarity Test

This notebook provides a comprehensive guide to understanding and applying the Augmented Dickey-Fuller (ADF) test for checking the stationarity of time series data. Stationarity is a critical assumption for many time series models, as non-stationary series can lead to spurious regressions and unreliable forecasts.

Introduction to Stationarity

A time series is considered stationary if its statistical properties (mean, variance, and autocorrelation) remain constant over time. This implies that the series does not exhibit trends, seasonality, or changes in variance over different periods.

Why is Stationarity Important?

Most traditional time series models, such as ARIMA, assume that the underlying process generating the series is stationary. If a series is non-stationary, these models may produce biased or inconsistent estimates. Techniques like differencing are often used to transform non-stationary series into stationary ones.

The Augmented Dickey-Fuller (ADF) Test

One of the most widely used statistical tests for determining the presence of a unit root in a time series (which indicates non-stationarity) is the Augmented Dickey-Fuller (ADF) test.

Key Concepts:

ConceptDescription
Unit RootA characteristic of stochastic processes that causes them to be non-stationary. If a series has a unit root, past shocks have permanent effects.
Null Hypothesis ($H_0$)The time series has a unit root (i.e., it is non-stationary).
Alternative Hypothesis ($H_1$)The time series does not have a unit root (i.e., it is stationary).
Test StatisticA value calculated from the sample data that is used to decide whether to reject the null hypothesis. For ADF, it's a negative value.
Critical ValuesThresholds (typically at 1%, 5%, and 10% significance levels) against which the test statistic is compared.
p-valueThe probability of observing a test statistic as extreme as, or more extreme than, the observed value, assuming the null hypothesis is true.
Decision RuleIf p-value < significance level (e.g., 0.05) OR Test Statistic < Critical Value, reject $H_0$ and conclude the series is stationary. Otherwise, fail to reject $H_0$.

This notebook will demonstrate how to simulate various types of time series data and then apply the ADF test to assess their stationarity, visualizing the results.

Dependency Installation

We will install the necessary Python libraries using pip.

[1]
# Install necessary libraries
!pip install pandas numpy matplotlib seaborn statsmodels scikit-learn
Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2)
Requirement already satisfied: numpy in /usr/local/lib/python3.12/dist-packages (2.0.2)
Requirement already satisfied: matplotlib in /usr/local/lib/python3.12/dist-packages (3.10.0)
Requirement already satisfied: seaborn in /usr/local/lib/python3.12/dist-packages (0.13.2)
Requirement already satisfied: statsmodels in /usr/local/lib/python3.12/dist-packages (0.14.6)
Requirement already satisfied: scikit-learn in /usr/local/lib/python3.12/dist-packages (1.6.1)
Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0)
Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2)
Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.2)
Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.3.3)
Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (4.63.0)
Requirement already satisfied: kiwisolver>=1.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.5.0)
Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (26.2)
Requirement already satisfied: pillow>=8 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (11.3.0)
Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (3.3.2)
Requirement already satisfied: scipy!=1.9.2,>=1.8 in /usr/local/lib/python3.12/dist-packages (from statsmodels) (1.16.3)
Requirement already satisfied: patsy>=0.5.6 in /usr/local/lib/python3.12/dist-packages (from statsmodels) (1.0.2)
Requirement already satisfied: joblib>=1.2.0 in /usr/local/lib/python3.12/dist-packages (from scikit-learn) (1.5.3)
Requirement already satisfied: threadpoolctl>=3.1.0 in /usr/local/lib/python3.12/dist-packages (from scikit-learn) (3.6.0)
Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)

Library Imports

All required libraries are imported in this section. Standard libraries are imported first, followed by third-party libraries.

[2]
# Standard library imports
import logging
from collections import deque
import time
import random

# Third-party library imports
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from statsmodels.tsa.stattools import adfuller
from sklearn.metrics import mean_squared_error

# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

# Set seaborn style for better plots
sns.set_theme(style="whitegrid")

Core Functions

This section defines the core functions for simulating time series data, performing the ADF test, and visualizing results. Each function is in its own code block and adheres to the specified formatting requirements.

Function Name: create_adf_state

This function initializes a dictionary to manage the state of our ADF test analysis. It sets up an empty structure to store simulated data, ADF test results, and visualization parameters.

Parameters:

  • series_name (str): A descriptive name for the time series being analyzed.

Returns:

  • (dict): An initialized state dictionary.
[3]
def create_adf_state(series_name: str) -> dict:
    """
    Initializes a state dictionary for managing ADF test related data.

    Parameters
    ----------
    series_name : str
        A descriptive name for the time series being analyzed.

    Returns
    -------
    dict
        An initialized state dictionary with keys for data, results, and plots.
    """
    logger.info(f"Initializing ADF state for series: {series_name}")
    state = {
        'series_name': series_name,
        'data': pd.Series(dtype=float),
        'adf_results': {},
        'plot_data': {}
    }
    logger.debug(f"ADF state initialized: {state}")
    return state

Function Name: simulate_timeseries_data

This function generates synthetic time series data based on specified parameters, allowing for the creation of stationary, non-stationary (with trend or random walk), and seasonal series. It uses a random seed for reproducibility.

Parameters:

  • state (dict): The current state dictionary.
  • n_samples (int): The number of data points to generate.
  • series_type (str): The type of series to generate ('stationary', 'random_walk', 'trend', 'seasonal').
  • ar_coeff (float, optional): Auto-regressive coefficient for stationary series. Defaults to 0.7.
  • trend_coeff (float, optional): Coefficient for linear trend. Defaults to 0.1.
  • season_period (int, optional): Period for seasonality. Defaults to 12.
  • season_strength (float, optional): Strength of the seasonal component. Defaults to 5.0.
  • noise_std (float, optional): Standard deviation of the random noise. Defaults to 1.0.
  • random_seed (int, optional): Seed for random number generation. Defaults to 42.

Returns:

  • (dict): The updated state dictionary with the simulated data.
[4]
def simulate_timeseries_data(state: dict,
                             n_samples: int,
                             series_type: str,
                             ar_coeff: float = 0.7,
                             trend_coeff: float = 0.1,
                             season_period: int = 12,
                             season_strength: float = 5.0,
                             noise_std: float = 1.0,
                             random_seed: int = 42) -> dict:
    """
    Simulates various types of time series data (stationary, random walk, trend, seasonal).

    Parameters
    ----------
    state : dict
        The current state dictionary.
    n_samples : int
        The number of data points to generate.
    series_type : str
        The type of series to generate: 'stationary', 'random_walk', 'trend', 'seasonal'.
    ar_coeff : float, optional
        Auto-regressive coefficient for stationary series, defaults to 0.7.
    trend_coeff : float, optional
        Coefficient for linear trend, defaults to 0.1.
    season_period : int, optional
        Period for seasonality, defaults to 12.
    season_strength : float, optional
        Strength of the seasonal component, defaults to 5.0.
    noise_std : float, optional
        Standard deviation of the random noise, defaults to 1.0.
    random_seed : int, optional
        Seed for random number generation, defaults to 42.

    Returns
    -------
    dict
        The updated state dictionary with the simulated data stored in 'data' key.
    """
    logger.info(f"Simulating {series_type} time series with {n_samples} samples.")
    np.random.seed(random_seed)
    data = np.zeros(n_samples)
    noise = np.random.normal(0, noise_std, n_samples)

    if series_type == 'stationary':
        # AR(1) process: y_t = c + ar_coeff * y_{t-1} + e_t
        data[0] = noise[0]
        for i in range(1, n_samples):
            data[i] = ar_coeff * data[i-1] + noise[i]
        logger.debug("Generated stationary AR(1) series.")
    elif series_type == 'random_walk':
        # Random Walk: y_t = y_{t-1} + e_t
        data[0] = noise[0]
        for i in range(1, n_samples):
            data[i] = data[i-1] + noise[i]
        logger.debug("Generated random walk series.")
    elif series_type == 'trend':
        # Series with a linear trend: y_t = trend_coeff * t + e_t
        data = trend_coeff * np.arange(n_samples) + noise
        logger.debug("Generated series with linear trend.")
    elif series_type == 'seasonal':
        # Series with seasonality: y_t = A * sin(2*pi*t / period) + e_t
        for i in range(n_samples):
            data[i] = season_strength * np.sin(2 * np.pi * i / season_period) + noise[i]
        logger.debug("Generated series with seasonality.")
    else:
        logger.warning(f"Unknown series type: {series_type}. Generating default stationary series.")
        data[0] = noise[0]
        for i in range(1, n_samples):
            data[i] = ar_coeff * data[i-1] + noise[i]

    state['data'] = pd.Series(data, name=state['series_name'])
    logger.info(f"Simulation complete for {state['series_name']}. First 5 values: {state['data'].head().tolist()}")
    return state

Function Name: plot_timeseries

This function visualizes a given time series using matplotlib and seaborn. It allows for clear representation of the series' behavior, including trends, seasonality, and volatility.

Parameters:

  • state (dict): The current state dictionary containing the time series data.
  • title (str): The title for the plot.
  • xlabel (str, optional): Label for the x-axis. Defaults to 'Time'.
  • ylabel (str, optional): Label for the y-axis. Defaults to 'Value'.
  • fig_size (tuple, optional): Dimensions of the plot figure. Defaults to (12, 6).

Returns:

  • (dict): The updated state dictionary, potentially storing plot information.
[5]
def plot_timeseries(state: dict,
                    title: str,
                    xlabel: str = 'Time',
                    ylabel: str = 'Value',
                    fig_size: tuple = (12, 6)) -> dict:
    """
    Plots a time series from the state dictionary.

    Parameters
    ----------
    state : dict
        The current state dictionary containing the 'data' Series.
    title : str
        The title for the plot.
    xlabel : str, optional
        Label for the x-axis, defaults to 'Time'.
    ylabel : str, optional
        Label for the y-axis, defaults to 'Value'.
    fig_size : tuple, optional
        Dimensions of the plot figure, defaults to (12, 6).

    Returns
    -------
    dict
        The updated state dictionary.
    """
    logger.info(f"Generating plot for {state['series_name']} with title: {title}")
    plt.figure(figsize=fig_size)
    sns.lineplot(x=state['data'].index, y=state['data'].values, color='skyblue')
    plt.title(title, fontsize=16)
    plt.xlabel(xlabel, fontsize=12)
    plt.ylabel(ylabel, fontsize=12)
    plt.grid(True, linestyle='--', alpha=0.7)
    plt.tight_layout()
    plt.show()
    logger.debug(f"Plot for {state['series_name']} displayed.")
    return state

Function Name: perform_adf_test

This function executes the Augmented Dickey-Fuller test on a given time series using statsmodels.tsa.stattools.adfuller. It calculates the test statistic, p-value, and critical values, then stores them in the state dictionary.

Parameters:

  • state (dict): The current state dictionary containing the time series data.
  • max_d_lags (int, optional): The maximum number of lags to be used in the regression. Defaults to None, which means it is determined automatically.
  • regression_type (str, optional): Type of regression to run: 'c' (constant), 'ct' (constant and trend), 'ctt' (constant, trend, and quadratic trend), 'nc' (no constant, no trend). Defaults to 'c'.

Returns:

  • (dict): The updated state dictionary with ADF test results.
[6]
def perform_adf_test(state: dict,
                     max_d_lags: int = None,
                     regression_type: str = 'c',
                     retries: int = 3,
                     backoff_factor: float = 0.5) -> dict:
    """
    Performs the Augmented Dickey-Fuller (ADF) test on the time series data in the state.

    Parameters
    ----------
    state : dict
        The current state dictionary containing the 'data' Series.
    max_d_lags : int, optional
        Maximum number of lags to use in the regression. If None, it is determined automatically.
        Defaults to None.
    regression_type : str, optional
        Type of regression to run: 'c' (constant), 'ct' (constant and trend),
        'ctt' (constant, trend, and quadratic trend), 'nc' (no constant, no trend).
        Defaults to 'c'.
    retries : int, optional
        Number of retry attempts for the ADF test, defaults to 3.
    backoff_factor : float, optional
        Factor by which to multiply the delay between retries, defaults to 0.5.

    Returns
    -------
    dict
        The updated state dictionary with ADF test results stored in 'adf_results'.
    """
    logger.info(f"Performing ADF test for {state['series_name']} with regression type '{regression_type}'.")

    ts_data = state['data'].dropna().values
    if len(ts_data) < 2:
        logger.warning(f"Insufficient data points ({len(ts_data)}) for ADF test on {state['series_name']}. Skipping.")
        state['adf_results'] = {'error': 'Insufficient data'}
        return state

    for i in range(retries):
        try:
            adf_output = adfuller(ts_data, maxlag=max_d_lags, regression=regression_type)
            adf_statistic = adf_output[0]
            p_value = adf_output[1]
            num_lags = adf_output[2]
            num_observations = adf_output[3]
            critical_values = adf_output[4]

            state['adf_results'] = {
                'adf_statistic': adf_statistic,
                'p_value': p_value,
                'num_lags': num_lags,
                'num_observations': num_observations,
                'critical_values': critical_values
            }
            logger.info(f"ADF test successful for {state['series_name']}. Test Statistic: {adf_statistic:.4f}, p-value: {p_value:.4f}")
            return state
        except Exception as e:
            delay = (backoff_factor * (2 ** i)) + random.uniform(0, 0.1) # Add random jitter
            logger.warning(f"ADF test failed for {state['series_name']} (Attempt {i+1}/{retries}): {e}. Retrying in {delay:.2f} seconds...")
            time.sleep(delay)

    logger.error(f"ADF test failed after {retries} attempts for {state['series_name']}.")
    state['adf_results'] = {'error': f'ADF test failed after {retries} attempts.'}
    return state

Function Name: summarize_adf_results

This function takes the results of the ADF test from the state dictionary and presents them in a human-readable format, including a conclusion on stationarity based on the p-value and critical values. It outputs the summary as a pandas DataFrame.

Parameters:

  • state (dict): The current state dictionary containing the ADF test results.
  • significance_level (float, optional): The significance level to use for determining stationarity. Defaults to 0.05.

Returns:

  • (dict): The updated state dictionary, potentially storing the summary DataFrame.
[12]
def summarize_adf_results(state: dict, significance_level: float = 0.05) -> dict:
    """
    Summarizes the ADF test results and determines stationarity.

    Parameters
    ----------
    state : dict
        The current state dictionary containing 'adf_results'.
    significance_level : float, optional
        The significance level to use for determining stationarity, defaults to 0.05.

    Returns
    -------
    dict
        The updated state dictionary.
    """
    logger.info(f"Summarizing ADF results for {state['series_name']}.")
    results = state.get('adf_results', {})

    if 'error' in results:
        logger.error(f"Cannot summarize ADF results due to error: {results['error']}")
        print(f"Error: {results['error']}")
        return state

    adf_statistic = results.get('adf_statistic')
    p_value = results.get('p_value')
    critical_values = results.get('critical_values', {})

    is_stationary_p_value = "Reject H0 (Stationary)" if p_value < significance_level else "Fail to Reject H0 (Non-Stationary)"

    # Check stationarity based on critical values (more robust for ADF)
    is_stationary_critical = ("Reject H0 (Stationary)" if (
        adf_statistic is not None and
        critical_values.get(f'{int(significance_level*100)}%', -np.inf) is not None and
        adf_statistic < critical_values.get(f'{int(significance_level*100)}%', -np.inf)
    ) else "Fail to Reject H0 (Non-Stationary)")

    summary_df = pd.DataFrame({
        'Metric': [
            'ADF Statistic',
            'p-value',
            'Number of Lags Used',
            'Number of Observations',
            '1% Critical Value',
            '5% Critical Value',
            '10% Critical Value',
            f'Stationarity (p < {significance_level})',
            f'Stationarity (ADF Stat < {int(significance_level*100)}% Crit Value)'
        ],
        'Value': [
            f'{adf_statistic:.4f}' if adf_statistic is not None else 'N/A',
            f'{p_value:.4f}' if p_value is not None else 'N/A',
            results.get('num_lags', 'N/A'),
            results.get('num_observations', 'N/A'),
            critical_values.get('1%', 'N/A'),
            critical_values.get('5%', 'N/A'),
            critical_values.get('10%', 'N/A'),
            is_stationary_p_value,
            is_stationary_critical
        ]
    })

    print(f"\n--- ADF Test Results Summary for {state['series_name']} ---")
    display(summary_df)
    logger.info(f"ADF results summary displayed for {state['series_name']}.")
    state['adf_summary_df'] = summary_df # Store for potential later use
    return state

Demonstration and Visualization

In this section, we will demonstrate the usage of the core functions by simulating different types of time series data (stationary, random walk, trend) and applying the ADF test to each. We will visualize the time series and present the ADF test results in a clear, tabular format.

Scenario 1: Stationary Time Series (AR(1) Process)

[13]
# Initialize state for a stationary series
stationary_state = create_adf_state('Stationary AR(1) Series')

# Simulate stationary data
stationary_state = simulate_timeseries_data(stationary_state, n_samples=200, series_type='stationary', ar_coeff=0.8)

# Plot the stationary series
stationary_state = plot_timeseries(stationary_state, title='Simulated Stationary AR(1) Series')

# Perform ADF test on the stationary series
stationary_state = perform_adf_test(stationary_state)

# Summarize ADF test results
stationary_state = summarize_adf_results(stationary_state)
cell output

--- ADF Test Results Summary for Stationary AR(1) Series ---
Metric Value
0 ADF Statistic -5.1158
1 p-value 0.0000
2 Number of Lags Used 0
3 Number of Observations 199
4 1% Critical Value -3.463645
5 5% Critical Value -2.876176
6 10% Critical Value -2.574572
7 Stationarity (p < 0.05) Reject H0 (Stationary)
8 Stationarity (ADF Stat < 5% Crit Value) Reject H0 (Stationary)

Scenario 2: Non-Stationary Time Series (Random Walk)

[14]
# Initialize state for a random walk series
random_walk_state = create_adf_state('Random Walk Series')

# Simulate random walk data
random_walk_state = simulate_timeseries_data(random_walk_state, n_samples=200, series_type='random_walk')

# Plot the random walk series
random_walk_state = plot_timeseries(random_walk_state, title='Simulated Random Walk Series')

# Perform ADF test on the random walk series
random_walk_state = perform_adf_test(random_walk_state)

# Summarize ADF test results
random_walk_state = summarize_adf_results(random_walk_state)
cell output

--- ADF Test Results Summary for Random Walk Series ---
Metric Value
0 ADF Statistic -2.3073
1 p-value 0.1696
2 Number of Lags Used 0
3 Number of Observations 199
4 1% Critical Value -3.463645
5 5% Critical Value -2.876176
6 10% Critical Value -2.574572
7 Stationarity (p < 0.05) Fail to Reject H0 (Non-Stationary)
8 Stationarity (ADF Stat < 5% Crit Value) Fail to Reject H0 (Non-Stationary)

Scenario 3: Non-Stationary Time Series (with Trend)

[15]
# Initialize state for a series with a trend
trend_state = create_adf_state('Series with Linear Trend')

# Simulate data with a linear trend
trend_state = simulate_timeseries_data(trend_state, n_samples=200, series_type='trend', trend_coeff=0.2)

# Plot the series with trend
trend_state = plot_timeseries(trend_state, title='Simulated Series with Linear Trend')

# Perform ADF test on the series with trend
trend_state = perform_adf_test(trend_state, regression_type='ct') # Use 'ct' for trend and constant

# Summarize ADF test results
trend_state = summarize_adf_results(trend_state)
cell output

--- ADF Test Results Summary for Series with Linear Trend ---
Metric Value
0 ADF Statistic -8.4534
1 p-value 0.0000
2 Number of Lags Used 3
3 Number of Observations 196
4 1% Critical Value -4.005717
5 5% Critical Value -3.433131
6 10% Critical Value -3.140347
7 Stationarity (p < 0.05) Reject H0 (Stationary)
8 Stationarity (ADF Stat < 5% Crit Value) Reject H0 (Stationary)

Scenario 4: Non-Stationary Time Series (with Seasonality - often implies non-stationarity without proper differencing)

[16]
# Initialize state for a seasonal series
seasonal_state = create_adf_state('Series with Seasonality')

# Simulate seasonal data
seasonal_state = simulate_timeseries_data(seasonal_state, n_samples=200, series_type='seasonal', season_period=24, season_strength=10.0)

# Plot the seasonal series
seasonal_state = plot_timeseries(seasonal_state, title='Simulated Series with Seasonality')

# Perform ADF test on the seasonal series (using 'c' as default, assuming trend is handled by constant)
seasonal_state = perform_adf_test(seasonal_state)

# Summarize ADF test results
seasonal_state = summarize_adf_results(seasonal_state)
cell output

--- ADF Test Results Summary for Series with Seasonality ---
Metric Value
0 ADF Statistic -7.9988
1 p-value 0.0000
2 Number of Lags Used 14
3 Number of Observations 185
4 1% Critical Value -3.466201
5 5% Critical Value -2.877293
6 10% Critical Value -2.575168
7 Stationarity (p < 0.05) Reject H0 (Stationary)
8 Stationarity (ADF Stat < 5% Crit Value) Reject H0 (Stationary)

Production Considerations

When applying the ADF test and handling stationarity in a production environment, several best practices should be considered to ensure robustness, efficiency, and maintainability of the time series analysis pipeline.

Best PracticeDescription
Automated DifferencingImplement automated procedures to determine the optimal number of differences required to achieve stationarity (e.g., using pmdarima.auto_arima or custom logic).
Rolling Window AnalysisFor long time series, consider performing ADF tests on rolling windows to detect changes in stationarity over time, indicating structural breaks.
MonitoringContinuously monitor the stationarity of production time series. Significant shifts might require re-evaluation of models.
Error Handling & RetriesImplement try/except blocks with exponential backoff for external data fetching or computationally intensive tests to handle transient failures gracefully.
Logging & AlertingUse comprehensive logging (INFO, WARNING, ERROR) to track test outcomes, especially for non-stationary detections, and set up alerts for critical issues.
Parameter TuningBe mindful of the regression parameter in ADF. Choosing the correct model for the deterministic components (constant, trend) is crucial for valid results.
Data ValidationPre-validate input data for missing values, correct data types, and sufficient length before running statistical tests to prevent errors.
Performance OptimizationFor very large datasets, consider sampling or using distributed computing frameworks if ADF test becomes a bottleneck.
ReproducibilityAlways set random seeds for simulations and statistical processes to ensure that results can be replicated.
ExplainabilityDocument the assumptions made for each series (e.g., presence of trend, seasonality) and how they influenced the ADF test configuration.

Conclusion

This notebook has demonstrated the importance of stationarity in time series analysis and provided a practical guide to using the Augmented Dickey-Fuller (ADF) test. We covered:

  • Conceptual understanding of stationarity and the ADF test.
  • Implementation of core functions for data simulation, plotting, and ADF testing, adhering to strict coding standards.
  • Demonstration of the ADF test on various simulated time series (stationary, random walk, trend, seasonal).
  • Visualization of time series and presentation of ADF test results in an easy-to-interpret format.
  • Key production considerations for deploying stationarity checks in real-world applications.

Understanding and correctly applying stationarity tests like ADF is fundamental for building robust and reliable time series models.