Skip to content

Commit

Permalink
fixing problem in remove seasonalty CUSUM
Browse files Browse the repository at this point in the history
Summary:
We used to have a problem with seasonaluty definition in cusum:
on cusum level we map string to hours on daily basis
but on SeasnalHandler expect to recieve hour-based.
Fortunately their no using this property in 1D.
New test for SeasnaltyHandle was added

Differential Revision: D50120272

fbshipit-source-id: 287c0636a719ffbe4a0b21d0784513ef5c8989ad
  • Loading branch information
irumata authored and facebook-github-bot committed Nov 3, 2023
1 parent 07efbb0 commit b844595
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 21 deletions.
10 changes: 2 additions & 8 deletions kats/detectors/cusum_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,6 @@
NORMAL_TOLERENCE = 1 # number of window
CHANGEPOINT_RETENTION: int = 7 * 24 * 60 * 60 # in seconds
MAX_CHANGEPOINT = 10
SEASON_PERIOD_FREQ_MAP: Dict[str, int] = {
"daily": 1,
"weekly": 7,
"monthly": 30,
"yearly": 365,
}

_log: logging.Logger = logging.getLogger("cusum_model")

Expand Down Expand Up @@ -197,7 +191,7 @@ def __init__(
] = CUSUMDefaultArgs.change_directions,
score_func: Union[str, CusumScoreFunction] = DEFAULT_SCORE_FUNCTION,
remove_seasonality: bool = CUSUMDefaultArgs.remove_seasonality,
season_period_freq: str = "daily",
season_period_freq: Union[str, int] = "daily",
vectorized: Optional[bool] = None,
adapted_pre_mean: Optional[bool] = None,
) -> None:
Expand Down Expand Up @@ -229,7 +223,7 @@ def __init__(
else:
self.remove_seasonality: bool = remove_seasonality

self.season_period_freq: str = previous_model.get(
self.season_period_freq: Union[str, int] = previous_model.get(
"season_period_freq", "daily"
)

Expand Down
13 changes: 12 additions & 1 deletion kats/tests/utils/test_decomposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from kats.consts import TimeSeriesData
from kats.data.utils import load_air_passengers, load_data
from kats.detectors.residual_translation import KDEResidualTranslator
from kats.utils.decomposition import TimeSeriesDecomposition
from kats.utils.decomposition import SeasonalityHandler, TimeSeriesDecomposition
from kats.utils.simulator import Simulator
from scipy.stats import ks_2samp
from statsmodels.tsa.seasonal import seasonal_decompose, STL
Expand Down Expand Up @@ -293,6 +293,17 @@ def test_plot(self) -> None:

m.plot()

def test_seasnality_handler(self) -> None:
sh_data = SeasonalityHandler(
data=self.ts_data_daily, seasonal_period=24 * 60 * 60
)
historical_data = sh_data.remove_seasonality()
self.assertNotEqual(self.ts_data_daily, historical_data)

sh_data = SeasonalityHandler(data=self.ts_data_daily, seasonal_period="daily")
historical_data = sh_data.remove_seasonality()
self.assertNotEqual(self.ts_data_daily, historical_data)

def test_multiplicative_assert(self) -> None:
data_new = self.ts_data.to_dataframe().copy()
data_new["y"] = -1.0 * data_new["y"]
Expand Down
31 changes: 19 additions & 12 deletions kats/utils/decomposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ class SeasonalityHandler:
SeasonalityHandler is a class that do timeseries STL decomposition for detecors
Attributes:
data: TimeSeriesData that need to be decomposed
seasonal_period: str, default value is 'daily'. Other possible values: 'hourly', 'weekly', 'biweekly', 'monthly', 'yearly'
seasonal_period: str, default value is 'daily'. Other possible values: 'hourly', 'weekly', 'biweekly', 'monthly', 'yearly' or integer which represent amoutn of seconds
>>> # Example usage:
>>> from kats.utils.simulator import Simulator
Expand All @@ -290,18 +290,18 @@ class SeasonalityHandler:
"""

PERIOD_MAP: Dict[str, int] = {
"hourly": 1,
"daily": 24,
"weekly": 7 * 24,
"biweekly": 14 * 24,
"monthly": 30 * 24,
"yearly": 365 * 24,
"hourly": 1 * 60 * 60,
"daily": 24 * 60 * 60,
"weekly": 7 * 24 * 60 * 60,
"biweekly": 14 * 24 * 60 * 60,
"monthly": 30 * 24 * 60 * 60,
"yearly": 365 * 24 * 60 * 60,
}

def __init__(
self,
data: TimeSeriesData,
seasonal_period: str = "daily",
seasonal_period: Union[str, int] = "daily",
ignore_irregular_freq: bool = False,
**kwargs: Any,
) -> None:
Expand All @@ -312,11 +312,18 @@ def __init__(

self.data = data

if seasonal_period not in SeasonalityHandler.PERIOD_MAP:
msg = "Invalid seasonal_period, possible values are 'hourly', 'daily', 'weekly', 'biweekly', 'monthly', and 'yearly'"
if isinstance(seasonal_period, str):
if seasonal_period not in SeasonalityHandler.PERIOD_MAP:
msg = "Invalid seasonal_period str value, possible values are integer or 'hourly', 'daily', 'weekly', 'biweekly', 'monthly', and 'yearly'"
logging.error(msg)
raise ParameterError(msg)
self.seasonal_period: int = SeasonalityHandler.PERIOD_MAP[seasonal_period]
elif type(seasonal_period) is int:
self.seasonal_period: int = seasonal_period
else:
msg = "Invalid seasonal_period type, possible values are integer or 'hourly', 'daily', 'weekly', 'biweekly', 'monthly', and 'yearly'"
logging.error(msg)
raise ParameterError(msg)
self.seasonal_period: int = SeasonalityHandler.PERIOD_MAP[seasonal_period]

self.low_pass_jump_factor: float = kwargs.get("lpj_factor", 0.15)
self.trend_jump_factor: float = kwargs.get("tj_factor", 0.15)
Expand Down Expand Up @@ -360,7 +367,7 @@ def __init__(
raise DataIrregularGranularityError(IRREGULAR_GRANULARITY_ERROR)

self.period: int = min(
int(self.seasonal_period * 60 * 60 / self.frequency.total_seconds()),
int(self.seasonal_period / self.frequency.total_seconds()),
len(self.data) // 2,
)

Expand Down

0 comments on commit b844595

Please sign in to comment.