@@ -271,6 +271,65 @@ def _predict_core(self, s: pd.Series) -> pd.Series:
271
271
return predicted
272
272
273
273
274
+ class VolatilityRangeAD (_TrainableUnivariateDetector ):
275
+ """Anomaly detector based on standard deviation range.
276
+
277
+ This detector flags anomalies for values that are outside the range of [mean - c * std, mean + c * std].
278
+
279
+ Parameters
280
+ ----------
281
+ c : float, optional (default=1.0)
282
+ The multiplier for the standard deviation to set the range for anomaly detection.
283
+ """
284
+
285
+ def __init__ (
286
+ self ,
287
+ c : Union [
288
+ Optional [float ], Tuple [Optional [float ], Optional [float ]]
289
+ ] = 3.0 ,
290
+ ) -> None :
291
+ super ().__init__ ()
292
+ self .c = c
293
+
294
+ @property
295
+ def _param_names (self ) -> Tuple [str , ...]:
296
+ return ("c" ,)
297
+
298
+ def _fit_core (self , s : pd .Series ) -> None :
299
+ if s .count () == 0 :
300
+ raise RuntimeError ("Valid values are not enough for training." )
301
+ mean_val = s .mean ()
302
+ std_val = s .std ()
303
+
304
+ self .abs_low_ = (
305
+ (
306
+ mean_val
307
+ - std_val
308
+ * (self .c if (not isinstance (self .c , tuple )) else self .c [0 ])
309
+ )
310
+ if (
311
+ (self .c if (not isinstance (self .c , tuple )) else self .c [0 ])
312
+ is not None
313
+ )
314
+ else - float ("inf" )
315
+ )
316
+ self .abs_high_ = (
317
+ mean_val
318
+ + std_val
319
+ * (self .c if (not isinstance (self .c , tuple )) else self .c [1 ])
320
+ if (
321
+ (self .c if (not isinstance (self .c , tuple )) else self .c [1 ])
322
+ is not None
323
+ )
324
+ else float ("inf" )
325
+ )
326
+
327
+ def _predict_core (self , s : pd .Series ) -> pd .Series :
328
+ predicted = (s > self .abs_high_ ) | (s < self .abs_low_ )
329
+ predicted [s .isna ()] = np .nan
330
+ return predicted
331
+
332
+
274
333
class GeneralizedESDTestAD (_TrainableUnivariateDetector ):
275
334
"""Detector that detects anomaly based on generalized ESD test.
276
335
0 commit comments