Coverage for src / ts_stat_tests / seasonality / tests.py: 100%

59 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-02-01 09:48 +0000

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

2# # 

3# Title: Seasonality Tests # 

4# Purpose: Tests for seasonality detection 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 functions to assess the seasonality of time series data. 

24 

25 The implemented algorithms include: 

26 

27 - QS Test 

28 - OCSB Test 

29 - CH Test 

30 - Seasonal Strength 

31 - Trend Strength 

32 - Spikiness 

33 

34 Each function is designed to analyze a univariate time series and return relevant statistics or indicators of seasonality. This module provides both a dispatcher for flexible algorithm selection and a boolean check for easy integration into pipelines. 

35""" 

36 

37 

38# ---------------------------------------------------------------------------- # 

39# # 

40# Setup #### 

41# # 

42# ---------------------------------------------------------------------------- # 

43 

44 

45# ---------------------------------------------------------------------------- # 

46# Imports #### 

47# ---------------------------------------------------------------------------- # 

48 

49 

50# ## Python StdLib Imports ---- 

51from typing import Any, Callable, Optional, Union 

52 

53# ## Python Third Party Imports ---- 

54from numpy.typing import ArrayLike 

55from pmdarima.arima import ARIMA 

56from typeguard import typechecked 

57 

58# ## Local First Party Imports ---- 

59from ts_stat_tests.seasonality.algorithms import ( 

60 ch as _ch, 

61 ocsb as _ocsb, 

62 qs as _qs, 

63 seasonal_strength as _seasonal_strength, 

64 spikiness as _spikiness, 

65 trend_strength as _trend_strength, 

66) 

67from ts_stat_tests.utils.errors import generate_error_message 

68 

69 

70# ---------------------------------------------------------------------------- # 

71# Exports #### 

72# ---------------------------------------------------------------------------- # 

73 

74 

75__all__: list[str] = ["seasonality", "is_seasonal"] 

76 

77 

78# ---------------------------------------------------------------------------- # 

79# # 

80# Tests #### 

81# # 

82# ---------------------------------------------------------------------------- # 

83 

84 

85@typechecked 

86def seasonality( 

87 x: ArrayLike, 

88 algorithm: str = "qs", 

89 **kwargs: Union[float, int, str, bool, ArrayLike, None], 

90) -> Union[float, int, tuple[Union[float, int, ARIMA, None], ...]]: 

91 """ 

92 !!! note "Summary" 

93 Dispatcher for seasonality algorithms. This function provides a unified interface to call various seasonality tests. 

94 

95 ???+ abstract "Details" 

96 

97 The `seasonality` function acts as a centralized dispatcher for the various seasonality algorithms implemented in the `algorithms.seasonality` module. It allows users to easily switch between different tests by specifying the `algorithm` name. 

98 

99 The supported algorithms include: 

100 

101 - `"qs"`: The QS (Quenouille-Sarle) test for seasonality. 

102 - `"ocsb"`: The Osborn-Chui-Smith-Birchenhall test for seasonal differencing. 

103 - `"ch"`: The Canova-Hansen test for seasonal stability. 

104 - `"seasonal_strength"` (or `"ss"`): The STL-based seasonal strength measure. 

105 - `"trend_strength"` (or `"ts"`): The STL-based trend strength measure. 

106 - `"spikiness"`: The STL-based spikiness measure. 

107 

108 Params: 

109 x (ArrayLike): 

110 The data to be checked. 

111 algorithm (str, optional): 

112 Which seasonality algorithm to use.<br> 

113 Default: `"qs"` 

114 kwargs (Union[float, int, str, bool, ArrayLike, None]): 

115 Additional arguments to pass to the underlying algorithm. 

116 

117 Returns: 

118 (Union[float, int, tuple[Union[float, int, object, None], ...]]): 

119 The result of the seasonality test. The return type depends on the chosen algorithm: 

120 - `"qs"` returns a tuple `(statistic, pvalue)`. 

121 - `"ocsb"` and `"ch"` return an integer (0 or 1). 

122 - `"seasonal_strength"`, `"trend_strength"`, and `"spikiness"` return a float. 

123 

124 ???+ example "Examples" 

125 

126 ```pycon {.py .python linenums="1" title="Basic usage"} 

127 >>> from ts_stat_tests.utils.data import load_airline 

128 >>> from ts_stat_tests.seasonality.tests import seasonality 

129 >>> data = load_airline().values 

130 >>> # Using the default QS test 

131 >>> seasonality(x=data, freq=12) 

132 (194.469289..., 5.909223...-43) 

133 >>> # Using seasonal strength 

134 >>> seasonality(x=data, algorithm="ss", m=12) 

135 0.778721... 

136 

137 ``` 

138 """ 

139 options: dict[str, tuple[str, ...]] = { 

140 "qs": ("qs",), 

141 "ocsb": ("ocsb",), 

142 "ch": ("ch",), 

143 "seasonal_strength": ("seasonal_strength", "ss"), 

144 "trend_strength": ("trend_strength", "ts"), 

145 "spikiness": ("spikiness",), 

146 } 

147 

148 # Internal helper to handle kwargs casting for ty 

149 def _call( 

150 func: Callable[..., Any], 

151 **args: Any, 

152 ) -> Any: 

153 """ 

154 !!! note "Summary" 

155 Internal helper to call the test function. 

156 

157 Params: 

158 func (Callable[..., Any]): 

159 The function to call. 

160 args (Any): 

161 The arguments to pass. 

162 

163 Returns: 

164 (Any): 

165 The result. 

166 

167 ???+ example "Examples" 

168 

169 ```python 

170 # Internal helper. 

171 ``` 

172 """ 

173 return func(**args) 

174 

175 if algorithm in options["qs"]: 

176 return _call(_qs, x=x, **kwargs) 

177 if algorithm in options["ocsb"]: 

178 return _call(_ocsb, x=x, **kwargs) 

179 if algorithm in options["ch"]: 

180 return _call(_ch, x=x, **kwargs) 

181 if algorithm in options["seasonal_strength"]: 

182 return _call(_seasonal_strength, x=x, **kwargs) 

183 if algorithm in options["trend_strength"]: 

184 return _call(_trend_strength, x=x, **kwargs) 

185 if algorithm in options["spikiness"]: 

186 return _call(_spikiness, x=x, **kwargs) 

187 

188 raise ValueError( 

189 generate_error_message( 

190 parameter_name="algorithm", 

191 value_parsed=algorithm, 

192 options={k: str(v) for k, v in options.items()}, 

193 ) 

194 ) 

195 

196 

197@typechecked 

198def is_seasonal( 

199 x: ArrayLike, 

200 algorithm: str = "qs", 

201 alpha: float = 0.05, 

202 **kwargs: Union[float, int, str, bool, ArrayLike, None], 

203) -> dict[str, Union[str, float, bool, None]]: 

204 """ 

205 !!! note "Summary" 

206 Boolean check for seasonality. This function wraps the `seasonality` dispatcher and returns a standardized dictionary indicating whether the series is seasonal based on a significance level or threshold. 

207 

208 ???+ abstract "Details" 

209 

210 The `is_seasonal` function interprets the results of the underlying seasonality tests to provide a boolean `"result"`. 

211 

212 - For `"qs"`, the test is considered seasonal if the p-value is less than `alpha`. 

213 - For `"ocsb"` and `"ch"`, the test is considered seasonal if the returned integer is 1. 

214 - For `"seasonal_strength"`, the test is considered seasonal if the strength is greater than 0.64 (a common threshold in literature). 

215 - For others, it checks if the statistic is greater than 0. 

216 

217 Params: 

218 x (ArrayLike): 

219 The data to be checked. 

220 algorithm (str, optional): 

221 Which seasonality algorithm to use.<br> 

222 Default: `"qs"` 

223 alpha (float, optional): 

224 The significance level for the test (used by `"qs"`).<br> 

225 Default: `0.05` 

226 kwargs (Union[float, int, str, bool, ArrayLike, None]): 

227 Additional arguments to pass to the underlying algorithm. 

228 

229 Returns: 

230 (dict[str, Union[str, float, bool, None]]): 

231 A dictionary containing: 

232 

233 - `"result"` (bool): Indicator if the series is seasonal. 

234 - `"statistic"` (float): The test statistic (or strength). 

235 - `"pvalue"` (float, optional): The p-value of the test (if available). 

236 - `"alpha"` (float): The significance level used. 

237 - `"algorithm"` (str): The algorithm used. 

238 

239 ???+ example "Examples" 

240 

241 ```pycon {.py .python linenums="1" title="Standard check"} 

242 >>> from ts_stat_tests.utils.data import load_airline 

243 >>> from ts_stat_tests.seasonality.tests import is_seasonal 

244 >>> data = load_airline().values 

245 >>> res = is_seasonal(x=data, algorithm="qs", freq=12) 

246 >>> res["result"] 

247 True 

248 >>> res["algorithm"] 

249 'qs' 

250 

251 ``` 

252 """ 

253 res: Any = seasonality(x=x, algorithm=algorithm, **kwargs) 

254 

255 is_sea: bool = False 

256 stat: float = 0.0 

257 pval: Optional[float] = None 

258 

259 if algorithm in ("qs",): 

260 if isinstance(res, (tuple, list)): 

261 v0: Any = res[0] 

262 v1: Any = res[1] 

263 stat = float(v0) if isinstance(v0, (int, float)) else 0.0 

264 pval = float(v1) if isinstance(v1, (int, float)) else 1.0 

265 is_sea = bool(pval < alpha) 

266 elif algorithm in ("ocsb", "ch"): 

267 if isinstance(res, (tuple, list)): 

268 v0: Any = res[0] 

269 stat = float(v0) if isinstance(v0, (int, float)) else 0.0 

270 else: 

271 v_any: Any = res 

272 stat = float(v_any) if isinstance(res, (int, float)) else 0.0 

273 is_sea = bool(stat == 1) 

274 elif algorithm in ("seasonal_strength", "ss"): 

275 if isinstance(res, (tuple, list)): 

276 v0: Any = res[0] 

277 stat = float(v0) if isinstance(v0, (int, float)) else 0.0 

278 else: 

279 v_any: Any = res 

280 stat = float(v_any) if isinstance(res, (int, float)) else 0.0 

281 # Default threshold of 0.64 is often used for seasonal strength 

282 is_sea = bool(stat > 0.64) 

283 else: 

284 if isinstance(res, (tuple, list)): 

285 v0: Any = res[0] 

286 stat = float(v0) if isinstance(v0, (int, float)) else 0.0 

287 else: 

288 v_any: Any = res 

289 stat = float(v_any) if isinstance(res, (int, float)) else 0.0 

290 is_sea = bool(stat > 0) 

291 

292 return { 

293 "result": is_sea, 

294 "statistic": stat, 

295 "pvalue": pval, 

296 "alpha": alpha, 

297 "algorithm": algorithm, 

298 }