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

1# ============================================================================ # 

2# # 

3# Title: Stationarity Tests # 

4# Purpose: Convenience functions for stationarity algorithms. # 

5# # 

6# ============================================================================ # 

7 

8 

9# ---------------------------------------------------------------------------- # 

10# # 

11# Overview #### 

12# # 

13# ---------------------------------------------------------------------------- # 

14 

15 

16# ---------------------------------------------------------------------------- # 

17# Description #### 

18# ---------------------------------------------------------------------------- # 

19 

20 

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""" 

25 

26 

27# ---------------------------------------------------------------------------- # 

28# # 

29# Setup #### 

30# # 

31# ---------------------------------------------------------------------------- # 

32 

33 

34# ---------------------------------------------------------------------------- # 

35# Imports #### 

36# ---------------------------------------------------------------------------- # 

37 

38 

39# ## Python StdLib Imports ---- 

40from typing import Any, Callable, Union 

41 

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 

47 

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 

59 

60 

61# ---------------------------------------------------------------------------- # 

62# Exports #### 

63# ---------------------------------------------------------------------------- # 

64 

65 

66__all__: list[str] = ["stationarity", "is_stationary"] 

67 

68 

69# ---------------------------------------------------------------------------- # 

70# Type Aliases #### 

71# ---------------------------------------------------------------------------- # 

72 

73 

74STATIONARITY_ITEM = Union[float, int, dict[str, float], NDArray[np.float64], ResultsStore] 

75"""Type alias for the items in the stationarity test result tuple.""" 

76 

77STATIONARITY_RETURN_TYPE = tuple[STATIONARITY_ITEM, ...] 

78"""Type alias for the return type of stationarity tests.""" 

79 

80 

81# ---------------------------------------------------------------------------- # 

82# # 

83# Tests #### 

84# # 

85# ---------------------------------------------------------------------------- # 

86 

87 

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. 

97 

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] 

107 

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. 

123 

124 Raises: 

125 (ValueError): 

126 When the given value for `algorithm` is not valid. 

127 

128 Returns: 

129 (tuple[Union[float, int, dict, ResultsStore, None], ...]): 

130 The result of the stationarity test. 

131 

132 !!! success "Credit" 

133 Calculations are performed by `statsmodels`, `arch`, and `pmdarima`. 

134 

135 ???+ example "Examples" 

136 

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 

141 

142 ``` 

143 

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 

148 

149 ``` 

150 

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 

155 

156 ``` 

157 

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 

162 

163 ``` 

164 

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 

169 

170 ``` 

171 

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 

176 

177 ``` 

178 

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 

183 

184 ``` 

185 

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 

190 

191 ``` 

192 

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')} 

198 

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 } 

210 

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. 

219 

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. 

225 

226 Returns: 

227 (STATIONARITY_RETURN_TYPE): 

228 The result of the function call. 

229 

230 ???+ example "Examples" 

231 

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 ``` 

237 

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 

242 

243 ``` 

244 """ 

245 return func(**args) 

246 

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) 

261 

262 raise ValueError( 

263 generate_error_message( 

264 parameter_name="algorithm", 

265 value_parsed=algorithm, 

266 options=options, 

267 ) 

268 ) 

269 

270 

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. 

281 

282 ???+ abstract "Details" 

283 This function checks the results of a stationarity test against a significance level `alpha`. 

284 

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. 

288 

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. 

298 

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. 

307 

308 ???+ example "Examples" 

309 

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 

314 

315 ``` 

316 

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 

323 

324 ``` 

325 

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 

332 

333 ``` 

334 

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 

341 

342 ``` 

343 """ 

344 res: Any = stationarity(x=x, algorithm=algorithm, **kwargs) 

345 

346 stat: Any 

347 pvalue: Any 

348 

349 # stationarity() always returns a tuple 

350 res_tuple: Any = res 

351 stat = res_tuple[0] 

352 pvalue_or_bool = res_tuple[1] 

353 

354 # Handle H0 logic 

355 stationary_h0 = ( 

356 "kpss", 

357 "kwiatkowski_phillips_schmidt_shin", 

358 ) 

359 

360 is_stat: bool = False 

361 pvalue = None 

362 

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) 

371 

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 } 

380 

381 return ret