Fourier-augmented unit-root tests (ADF, KPSS) with F-tests for linearity
Project description
funitroot: Fourier Unit Root Tests
A comprehensive Python package for testing unit roots in time series with structural breaks using Fourier approximations. Implements the methodologies of Enders & Lee (2012) and Becker, Enders & Lee (2006).
Features
- Fourier ADF Test (Enders & Lee, 2012): Tests for unit roots allowing for smooth structural breaks
- Fourier KPSS Test (Becker, Enders & Lee, 2006): Tests for stationarity with smooth breaks
- F-tests for Linearity: Test whether Fourier terms are statistically significant
- Automatic Frequency Selection: Optimal frequency selected via minimum SSR criterion
- Proper P-value Computation: Extrapolation beyond critical value bounds
- Interactive Visualizations: Beautiful plots using Plotly
- Publication-Ready Output: Results formatted for academic papers
What's New in v1.0.2
- Added F-tests for linearity: Test H₀: γ₁ = γ₂ = 0 (no Fourier terms needed)
- Fixed p-value computation: Now properly extrapolates beyond 10% critical value
- Corrected frequency selection: Uses minimum SSR (matching original GAUSS code)
- Improved validation: Raises errors for invalid parameters instead of silent capping
- Added standalone F-test functions:
fourier_adf_f_test()andfourier_kpss_f_test()
Installation
pip install funitroot
Or install from source:
git clone https://github.com/merwanroudane/funitroot.git
cd funitroot
pip install -e .
Quick Start
Fourier ADF Test
import numpy as np
from funitroot import fourier_adf_test
# Generate data with structural break
np.random.seed(42)
T = 200
t = np.arange(T)
# Stationary series with smooth mean shift (Fourier component)
y = 5 + 3 * np.sin(2 * np.pi * t / T) + np.random.randn(T) * 0.5
# Perform Fourier ADF test
result = fourier_adf_test(y, model='c', max_freq=3)
print(result.summary())
Output:
=================================================================
Fourier ADF Unit Root Test Results
Enders & Lee (2012) Economics Letters
=================================================================
Model: Constant
Sample size: 200
Maximum lag tested: 8
Maximum frequency: 3
Lag selection criterion: AIC
-----------------------------------------------------------------
Optimal frequency (k): 1
Optimal lag (p): 0
ADF statistic: -14.6971
P-value: 0.0010
-----------------------------------------------------------------
Critical values:
1% : -4.3700 *
5% : -3.7800 *
10% : -3.4700
-----------------------------------------------------------------
F-test for linearity (H0: no Fourier terms needed):
F-statistic: 104.2477
Critical values: 1%=10.02, 5%=7.41, 10%=6.25
→ Reject linearity: Fourier terms ARE significant
-----------------------------------------------------------------
Conclusion: Reject null hypothesis of unit root
at 5% significance level
=================================================================
Fourier KPSS Test
from funitroot import fourier_kpss_test
# Perform Fourier KPSS test
result = fourier_kpss_test(y, model='c', max_freq=3)
print(result.summary())
# For KPSS: Fail to reject = Evidence of stationarity
if not result.reject_null:
print("Series is stationary around Fourier components")
F-test for Linearity
The F-test determines whether Fourier terms are statistically significant. If not rejected, use standard ADF/KPSS instead.
from funitroot import fourier_adf_test, fourier_adf_f_test
# Method 1: Via the main test object
result = fourier_adf_test(y, model='c', max_freq=3)
f_result = result.f_test_linearity()
print(f"F-statistic: {f_result.f_statistic:.4f}")
print(f"P-value: {f_result.pvalue:.4f}")
print(f"Reject linearity (use Fourier): {f_result.reject_null}")
# Method 2: Standalone function with specific k and p
f_result = fourier_adf_f_test(y, model='c', k=1, p=4)
Complete Analysis Workflow
import numpy as np
from funitroot import fourier_adf_test, fourier_kpss_test
def analyze_series(y, name="Series"):
"""Complete unit root analysis with Fourier tests."""
print(f"\n{'='*60}")
print(f"Analysis of: {name}")
print('='*60)
# Step 1: Fourier ADF test
adf_result = fourier_adf_test(y, model='c', max_freq=5)
f_adf = adf_result.f_test_linearity()
print(f"\n[Fourier ADF Test]")
print(f" Statistic: {adf_result.statistic:.4f}")
print(f" P-value: {adf_result.pvalue:.4f}")
print(f" Optimal k: {adf_result.optimal_frequency}")
print(f" F-test (linearity): {f_adf.f_statistic:.2f} (p={f_adf.pvalue:.4f})")
# Step 2: Fourier KPSS test
kpss_result = fourier_kpss_test(y, model='c', max_freq=5)
f_kpss = kpss_result.f_test_linearity()
print(f"\n[Fourier KPSS Test]")
print(f" Statistic: {kpss_result.statistic:.6f}")
print(f" P-value: {kpss_result.pvalue:.4f}")
print(f" Optimal k: {kpss_result.optimal_frequency}")
print(f" F-test (linearity): {f_kpss.f_statistic:.2f} (p={f_kpss.pvalue:.4f})")
# Step 3: Joint interpretation
print(f"\n[Conclusion]")
if f_adf.reject_null or f_kpss.reject_null:
print(" Fourier terms are significant → Structural breaks present")
else:
print(" Fourier terms not significant → Consider standard tests")
if adf_result.reject_null and not kpss_result.reject_null:
print(" → Series is STATIONARY (both tests agree)")
elif not adf_result.reject_null and kpss_result.reject_null:
print(" → Series has UNIT ROOT (both tests agree)")
elif not adf_result.reject_null and not kpss_result.reject_null:
print(" → Inconclusive (ADF: unit root, KPSS: stationary)")
else:
print(" → Contradictory results (needs further analysis)")
# Example usage
np.random.seed(42)
T = 200
t = np.arange(T)
# Stationary with break
y_stationary = 5 + 3 * np.sin(2 * np.pi * t / T) + np.random.randn(T) * 0.5
analyze_series(y_stationary, "Stationary with Fourier break")
# Unit root (random walk)
y_random_walk = np.cumsum(np.random.randn(T))
analyze_series(y_random_walk, "Random Walk")
Mathematical Background
Fourier ADF Test (Enders & Lee, 2012)
The Fourier ADF test extends the standard ADF test by incorporating Fourier terms to capture smooth structural breaks:
Test Equation:
Δyₜ = α + δyₜ₋₁ + γ₁sin(2πkt/T) + γ₂cos(2πkt/T) + Σφᵢ Δyₜ₋ᵢ + εₜ
For model with trend (model='ct'):
Δyₜ = α + βt + δyₜ₋₁ + γ₁sin(2πkt/T) + γ₂cos(2πkt/T) + Σφᵢ Δyₜ₋ᵢ + εₜ
Hypotheses:
- H₀: δ = 0 (unit root)
- H₁: δ < 0 (stationary around Fourier components)
Frequency Selection: k is selected by minimizing the sum of squared residuals (SSR) over k ∈ {1, 2, ..., kmax}.
F-test for Linearity: Tests H₀: γ₁ = γ₂ = 0. If not rejected, standard ADF is appropriate.
Fourier KPSS Test (Becker, Enders & Lee, 2006)
The Fourier KPSS test extends the standard KPSS test:
Test Equation:
yₜ = α + γ₁sin(2πkt/T) + γ₂cos(2πkt/T) + εₜ
For model with trend (model='ct'):
yₜ = α + βt + γ₁sin(2πkt/T) + γ₂cos(2πkt/T) + εₜ
Hypotheses:
- H₀: yₜ is stationary around Fourier components
- H₁: yₜ has a unit root
Test Statistic:
τ = (1/T²) × Σ Sₜ² / σ²_lr
where Sₜ = Σⱼ₌₁ᵗ êⱼ (partial sum of residuals) and σ²_lr is the long-run variance.
API Reference
FourierADF Class
from funitroot import FourierADF
result = FourierADF(
data, # array-like: Time series data
model='c', # str: 'c' (constant) or 'ct' (constant + trend)
max_lag=None, # int: Maximum lag (default: 8)
max_freq=5, # int: Maximum frequency 1-5
ic='aic' # str: 'aic', 'bic', or 'tstat'
)
# Attributes
result.statistic # float: Test statistic
result.pvalue # float: P-value
result.optimal_frequency # int: Selected frequency k
result.optimal_lag # int: Selected lag p
result.critical_values # dict: {'1%': ..., '5%': ..., '10%': ...}
result.reject_null # bool: Reject unit root?
# Methods
result.summary() # str: Formatted results
result.f_test_linearity() # FTestResult: F-test for Fourier terms
FourierKPSS Class
from funitroot import FourierKPSS
result = FourierKPSS(
data, # array-like: Time series data
model='c', # str: 'c' (level) or 'ct' (trend stationarity)
max_freq=5, # int: Maximum frequency 1-5
lags='auto' # str/int: Newey-West lags ('auto' or integer)
)
# Attributes
result.statistic # float: KPSS statistic (τ)
result.pvalue # float: P-value
result.optimal_frequency # int: Selected frequency k
result.critical_values # dict: {'1%': ..., '5%': ..., '10%': ...}
result.reject_null # bool: Reject stationarity?
# Methods
result.summary() # str: Formatted results
result.f_test_linearity() # FTestResult: F-test for Fourier terms
Standalone F-test Functions
from funitroot import fourier_adf_f_test, fourier_kpss_f_test
# F-test for Fourier ADF
f_result = fourier_adf_f_test(
data, # array-like: Time series
model='c', # str: 'c' or 'ct'
k=1, # int: Frequency to test
p=0 # int: Number of lags
)
# F-test for Fourier KPSS
f_result = fourier_kpss_f_test(
data, # array-like: Time series
model='c', # str: 'c' or 'ct'
k=1 # int: Frequency to test
)
# FTestResult attributes
f_result.f_statistic # float: F-statistic
f_result.critical_values # dict: Critical values
f_result.pvalue # float: P-value
f_result.reject_null # bool: Reject linearity?
f_result.frequency # int: Tested frequency
Convenience Functions
from funitroot import fourier_adf_test, fourier_kpss_test
# Same as FourierADF() but returns result directly
result = fourier_adf_test(y, model='c', max_freq=5)
# Same as FourierKPSS() but returns result directly
result = fourier_kpss_test(y, model='c', max_freq=5)
Visualization Functions
from funitroot import (
plot_series_with_fourier,
plot_test_results,
plot_frequency_search,
plot_comparative_analysis,
plot_residual_diagnostics
)
# Plot series with fitted Fourier components
plot_series_with_fourier(data, optimal_frequency, model='c')
# Plot test statistic with critical values
plot_test_results(test_result)
# Plot statistics across frequencies
plot_frequency_search(data, model='c', max_freq=5, test_type='adf')
# Compare ADF and KPSS results
plot_comparative_analysis(data, model='c', max_freq=5)
# Residual diagnostics
plot_residual_diagnostics(test_result)
Critical Values
Fourier ADF Critical Values (Enders & Lee, 2012)
Model: Constant only (model='c')
| T | k | 1% | 5% | 10% |
|---|---|---|---|---|
| ≤150 | 1 | -4.42 | -3.81 | -3.49 |
| ≤150 | 2 | -3.97 | -3.27 | -2.91 |
| ≤150 | 3 | -3.77 | -3.07 | -2.71 |
Model: Constant + Trend (model='ct')
| T | k | 1% | 5% | 10% |
|---|---|---|---|---|
| ≤150 | 1 | -4.95 | -4.35 | -4.05 |
| ≤150 | 2 | -4.69 | -4.05 | -3.71 |
| ≤150 | 3 | -4.45 | -3.78 | -3.44 |
Fourier KPSS Critical Values (Becker et al., 2006)
Model: Level stationarity (model='c')
| T | k | 1% | 5% | 10% |
|---|---|---|---|---|
| ≤250 | 1 | 0.2699 | 0.1720 | 0.1318 |
| ≤250 | 2 | 0.6671 | 0.4152 | 0.3150 |
Model: Trend stationarity (model='ct')
| T | k | 1% | 5% | 10% |
|---|---|---|---|---|
| ≤250 | 1 | 0.0716 | 0.0546 | 0.0471 |
| ≤250 | 2 | 0.2022 | 0.1321 | 0.1034 |
References
-
Enders, W., and Lee, J. (2012)
"The flexible Fourier form and Dickey-Fuller type unit root tests"
Economics Letters, 117, 196-199. -
Becker, R., Enders, W., and Lee, J. (2006)
"A stationarity test in the presence of an unknown number of smooth breaks"
Journal of Time Series Analysis, 27(3), 381-409. -
Nazlioglu, S. (2019)
GAUSS implementation (TSPDLIB)
License
This project is licensed under the MIT License - see the LICENSE file for details.
Author
Dr. Merwan Roudane
Citation
If you use this package in your research, please cite:
@software{funitroot2024,
author = {Roudane, Merwan},
title = {funitroot: Fourier Unit Root Tests for Python},
version = {1.0.2},
year = {2024},
url = {https://github.com/merwanroudane/funitroot}
}
Acknowledgments
This package implements the methods developed by:
- Walter Enders (University of Alabama)
- Junsoo Lee (University of Alabama)
- Ralf Becker (University of Manchester)
Based on the GAUSS implementation by Saban Nazlioglu (TSPDLIB).
Support
If you encounter any issues or have questions, please open an issue on GitHub: https://github.com/merwanroudane/funitroot/issues
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file funitroot-1.0.2.tar.gz.
File metadata
- Download URL: funitroot-1.0.2.tar.gz
- Upload date:
- Size: 33.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cf9c0a3c31b839fa3fe7a3ccd67520c7628ce55a0e5b5025cacac5d5a8280868
|
|
| MD5 |
fe429982066b95405bb928a252c2b8f3
|
|
| BLAKE2b-256 |
447a8f09b9cf2edc5d5e0f526e8cd2ccbe55d35c4ef99a5cc4d1eae6c99c2400
|
File details
Details for the file funitroot-1.0.2-py3-none-any.whl.
File metadata
- Download URL: funitroot-1.0.2-py3-none-any.whl
- Upload date:
- Size: 24.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d2f90e8e03971c344a007a27c203c3c5d190d7abf7a97e8f42faf328109a7a1a
|
|
| MD5 |
ce6de8800e2e1894426fae40b9de1389
|
|
| BLAKE2b-256 |
8c8be3e424d21b4150e2f7d5edd4e1e29bbb3868333e2a4b9d115f210687a988
|