Coverage for src / ts_stat_tests / stationarity / tests.py: 100%
50 statements
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-01 09:48 +0000
« prev ^ index » next coverage.py v7.13.2, created at 2026-02-01 09:48 +0000
1# ============================================================================ #
2# #
3# Title: Stationarity Tests #
4# Purpose: Convenience functions for stationarity algorithms. #
5# #
6# ============================================================================ #
9# ---------------------------------------------------------------------------- #
10# #
11# Overview ####
12# #
13# ---------------------------------------------------------------------------- #
16# ---------------------------------------------------------------------------- #
17# Description ####
18# ---------------------------------------------------------------------------- #
21"""
22!!! note "Summary"
23 This module contains convenience functions and tests for stationarity measures, allowing for easy access to different unit root and stationarity algorithms.
24"""
27# ---------------------------------------------------------------------------- #
28# #
29# Setup ####
30# #
31# ---------------------------------------------------------------------------- #
34# ---------------------------------------------------------------------------- #
35# Imports ####
36# ---------------------------------------------------------------------------- #
39# ## Python StdLib Imports ----
40from typing import Any, Callable, Union
42# ## Python Third Party Imports ----
43import numpy as np
44from numpy.typing import ArrayLike, NDArray
45from statsmodels.stats.diagnostic import ResultsStore
46from typeguard import typechecked
48# ## Local First Party Imports ----
49from ts_stat_tests.stationarity.algorithms import (
50 adf as _adf,
51 ers as _ers,
52 kpss as _kpss,
53 pp as _pp,
54 rur as _rur,
55 vr as _vr,
56 za as _za,
57)
58from ts_stat_tests.utils.errors import generate_error_message
61# ---------------------------------------------------------------------------- #
62# Exports ####
63# ---------------------------------------------------------------------------- #
66__all__: list[str] = ["stationarity", "is_stationary"]
69# ---------------------------------------------------------------------------- #
70# Type Aliases ####
71# ---------------------------------------------------------------------------- #
74STATIONARITY_ITEM = Union[float, int, dict[str, float], NDArray[np.float64], ResultsStore]
75"""Type alias for the items in the stationarity test result tuple."""
77STATIONARITY_RETURN_TYPE = tuple[STATIONARITY_ITEM, ...]
78"""Type alias for the return type of stationarity tests."""
81# ---------------------------------------------------------------------------- #
82# #
83# Tests ####
84# #
85# ---------------------------------------------------------------------------- #
88@typechecked
89def stationarity(
90 x: ArrayLike,
91 algorithm: str = "adf",
92 **kwargs: Union[float, int, str, bool, ArrayLike, None],
93) -> STATIONARITY_RETURN_TYPE:
94 """
95 !!! note "Summary"
96 Perform a stationarity test on the given data.
98 ???+ abstract "Details"
99 This function is a convenience wrapper around multiple underlying algorithms:<br>
100 - [`adf()`][ts_stat_tests.stationarity.algorithms.adf]<br>
101 - [`kpss()`][ts_stat_tests.stationarity.algorithms.kpss]<br>
102 - [`pp()`][ts_stat_tests.stationarity.algorithms.pp]<br>
103 - [`za()`][ts_stat_tests.stationarity.algorithms.za]<br>
104 - [`ers()`][ts_stat_tests.stationarity.algorithms.ers]<br>
105 - [`vr()`][ts_stat_tests.stationarity.algorithms.vr]<br>
106 - [`rur()`][ts_stat_tests.stationarity.algorithms.rur]
108 Params:
109 x (ArrayLike):
110 The data to be checked.
111 algorithm (str):
112 Which stationarity algorithm to use.<br>
113 - `adf()`: `["adf", "augmented_dickey_fuller"]`<br>
114 - `kpss()`: `["kpss", "kwiatkowski_phillips_schmidt_shin"]`<br>
115 - `pp()`: `["pp", "phillips_perron"]`<br>
116 - `za()`: `["za", "zivot_andrews"]`<br>
117 - `ers()`: `["ers", "elliott_rothenberg_stock"]`<br>
118 - `vr()`: `["vr", "variance_ratio"]`<br>
119 - `rur()`: `["rur", "range_unit_root"]`<br>
120 Defaults to `"adf"`.
121 kwargs (Union[float, int, str, bool, ArrayLike, None]):
122 Additional arguments to pass to the underlying algorithm.
124 Raises:
125 (ValueError):
126 When the given value for `algorithm` is not valid.
128 Returns:
129 (tuple[Union[float, int, dict, ResultsStore, None], ...]):
130 The result of the stationarity test.
132 !!! success "Credit"
133 Calculations are performed by `statsmodels`, `arch`, and `pmdarima`.
135 ???+ example "Examples"
137 ```pycon {.py .python linenums="1" title="Setup"}
138 >>> from ts_stat_tests.stationarity.tests import stationarity
139 >>> from ts_stat_tests.utils.data import data_normal
140 >>> normal = data_normal
142 ```
144 ```pycon {.py .python linenums="1" title="Example 1: Augmented Dickey-Fuller"}
145 >>> result = stationarity(normal, algorithm="adf")
146 >>> print(f"ADF statistic: {result[0]:.4f}")
147 ADF statistic: -30.7838
149 ```
151 ```pycon {.py .python linenums="1" title="Example 2: Kwiatkowski-Phillips-Schmidt-Shin"}
152 >>> result = stationarity(normal, algorithm="kpss")
153 >>> print(f"KPSS statistic: {result[0]:.4f}")
154 KPSS statistic: 0.0858
156 ```
158 ```pycon {.py .python linenums="1" title="Example 3: Phillips-Perron"}
159 >>> result = stationarity(normal, algorithm="pp")
160 >>> print(f"PP statistic: {result[0]:.4f}")
161 PP statistic: -30.7758
163 ```
165 ```pycon {.py .python linenums="1" title="Example 4: Zivot-Andrews"}
166 >>> result = stationarity(normal, algorithm="za")
167 >>> print(f"ZA statistic: {result[0]:.4f}")
168 ZA statistic: -30.8800
170 ```
172 ```pycon {.py .python linenums="1" title="Example 5: Elliot-Rothenberg-Stock"}
173 >>> result = stationarity(normal, algorithm="ers")
174 >>> print(f"ERS statistic: {result[0]:.4f}")
175 ERS statistic: -30.1517
177 ```
179 ```pycon {.py .python linenums="1" title="Example 6: Variance Ratio"}
180 >>> result = stationarity(normal, algorithm="vr")
181 >>> print(f"VR statistic: {result[0]:.4f}")
182 VR statistic: -12.8518
184 ```
186 ```pycon {.py .python linenums="1" title="Example 7: Range Unit Root"}
187 >>> result = stationarity(normal, algorithm="rur")
188 >>> print(f"RUR statistic: {result[0]:.4f}")
189 RUR statistic: 0.3479
191 ```
193 ```pycon {.py .python linenums="1" title="Example 8: Invalid algorithm"}
194 >>> stationarity(normal, algorithm="invalid")
195 Traceback (most recent call last):
196 ...
197 ValueError: Invalid 'algorithm': invalid. Options: {'adf': ('adf', 'augmented_dickey_fuller'), 'kpss': ('kpss', 'kwiatkowski_phillips_schmidt_shin'), 'pp': ('pp', 'phillips_perron'), 'za': ('za', 'zivot_andrews'), 'ers': ('ers', 'elliott_rothenberg_stock'), 'vr': ('vr', 'variance_ratio'), 'rur': ('rur', 'range_unit_root')}
199 ```
200 """
201 options: dict[str, tuple[str, ...]] = {
202 "adf": ("adf", "augmented_dickey_fuller"),
203 "kpss": ("kpss", "kwiatkowski_phillips_schmidt_shin"),
204 "pp": ("pp", "phillips_perron"),
205 "za": ("za", "zivot_andrews"),
206 "ers": ("ers", "elliott_rothenberg_stock"),
207 "vr": ("vr", "variance_ratio"),
208 "rur": ("rur", "range_unit_root"),
209 }
211 # Internal helper to handle kwargs casting for ty
212 def _call(
213 func: Callable[..., STATIONARITY_RETURN_TYPE],
214 **args: Union[float, int, str, bool, ArrayLike, None],
215 ) -> STATIONARITY_RETURN_TYPE:
216 """
217 !!! note "Summary"
218 Internal helper to call the test function.
220 Params:
221 func (Callable[..., STATIONARITY_RETURN_TYPE]):
222 The function to call.
223 args (Union[float, int, str, bool, ArrayLike, None]):
224 The arguments to pass to the function.
226 Returns:
227 (STATIONARITY_RETURN_TYPE):
228 The result of the function call.
230 ???+ example "Examples"
232 ```pycon {.py .python linenums="1" title="Setup"}
233 >>> from ts_stat_tests.stationarity.tests import stationarity
234 >>> from ts_stat_tests.utils.data import data_normal
235 >>> normal = data_normal
236 ```
238 ```pycon {.py .python linenums="1" title="Example 1: ADF test via internal helper"}
239 >>> result = stationarity(normal, algorithm="adf")
240 >>> print(f"ADF statistic: {result[0]:.4f}")
241 ADF statistic: -30.7838
243 ```
244 """
245 return func(**args)
247 if algorithm in options["adf"]:
248 return _call(_adf, x=x, **kwargs)
249 if algorithm in options["kpss"]:
250 return _call(_kpss, x=x, **kwargs)
251 if algorithm in options["pp"]:
252 return _call(_pp, x=x, **kwargs)
253 if algorithm in options["za"]:
254 return _call(_za, x=x, **kwargs)
255 if algorithm in options["ers"]:
256 return _call(_ers, y=x, **kwargs)
257 if algorithm in options["vr"]:
258 return _call(_vr, y=x, **kwargs)
259 if algorithm in options["rur"]:
260 return _call(_rur, x=x, **kwargs)
262 raise ValueError(
263 generate_error_message(
264 parameter_name="algorithm",
265 value_parsed=algorithm,
266 options=options,
267 )
268 )
271@typechecked
272def is_stationary(
273 x: ArrayLike,
274 algorithm: str = "adf",
275 alpha: float = 0.05,
276 **kwargs: Union[float, int, str, bool, ArrayLike, None],
277) -> dict[str, Union[str, bool, STATIONARITY_ITEM, None]]:
278 """
279 !!! note "Summary"
280 Test whether a given data set is `stationary` or not.
282 ???+ abstract "Details"
283 This function checks the results of a stationarity test against a significance level `alpha`.
285 Note that different tests have different null hypotheses:
286 - For ADF, PP, ZA, ERS, VR, RUR: H0 is non-stationarity (unit root). Stationary if p-value < alpha.
287 - For KPSS: H0 is stationarity. Stationary if p-value > alpha.
289 Params:
290 x (ArrayLike):
291 The data to be checked.
292 algorithm (str):
293 Which stationarity algorithm to use. Defaults to `"adf"`.
294 alpha (float, optional):
295 The significance level for the test. Defaults to `0.05`.
296 kwargs (Union[float, int, str, bool, ArrayLike, None]):
297 Additional arguments to pass to the underlying algorithm.
299 Returns:
300 (dict[str, Union[str, float, bool, None]]):
301 A dictionary containing:
302 - `"result"` (bool): Indicator if the series is stationary.
303 - `"statistic"` (float): The test statistic.
304 - `"pvalue"` (float): The p-value of the test.
305 - `"alpha"` (float): The significance level used.
306 - `"algorithm"` (str): The algorithm used.
308 ???+ example "Examples"
310 ```pycon {.py .python linenums="1" title="Setup"}
311 >>> from ts_stat_tests.stationarity.tests import is_stationary
312 >>> from ts_stat_tests.utils.data import data_normal
313 >>> normal = data_normal
315 ```
317 ```pycon {.py .python linenums="1" title="Example 1: ADF test on stationary data"}
318 >>> res = is_stationary(normal, algorithm="adf")
319 >>> res["result"]
320 True
321 >>> print(f"p-value: {res['pvalue']:.4f}")
322 p-value: 0.0000
324 ```
326 ```pycon {.py .python linenums="1" title="Example 2: KPSS test on stationary data"}
327 >>> res = is_stationary(normal, algorithm="kpss")
328 >>> res["result"]
329 True
330 >>> print(f"p-value: {res['pvalue']:.4f}")
331 p-value: 0.1000
333 ```
335 ```pycon {.py .python linenums="1" title="Example 3: RUR test"}
336 >>> res = is_stationary(normal, algorithm="rur")
337 >>> res["result"]
338 True
339 >>> print(f"p-value: {res['pvalue']:.2f}")
340 p-value: 0.01
342 ```
343 """
344 res: Any = stationarity(x=x, algorithm=algorithm, **kwargs)
346 stat: Any
347 pvalue: Any
349 # stationarity() always returns a tuple
350 res_tuple: Any = res
351 stat = res_tuple[0]
352 pvalue_or_bool = res_tuple[1]
354 # Handle H0 logic
355 stationary_h0 = (
356 "kpss",
357 "kwiatkowski_phillips_schmidt_shin",
358 )
360 is_stat: bool = False
361 pvalue = None
363 if isinstance(pvalue_or_bool, bool):
364 is_stat = pvalue_or_bool
365 elif isinstance(pvalue_or_bool, (int, float)):
366 pvalue = pvalue_or_bool
367 if algorithm in stationary_h0:
368 is_stat = bool(pvalue > alpha)
369 else:
370 is_stat = bool(pvalue < alpha)
372 # Define return dict explicitly to match return type hint
373 ret: dict[str, Union[str, bool, STATIONARITY_ITEM, None]] = {
374 "result": bool(is_stat),
375 "statistic": float(stat) if isinstance(stat, (int, float)) else stat,
376 "pvalue": float(pvalue) if pvalue is not None else None,
377 "alpha": float(alpha),
378 "algorithm": str(algorithm),
379 }
381 return ret