Repository: alteryx/evalml
Pull request #3811 (2022-11-02 15:53:34)

Decomposer valid frequency logic
Consolidates all the logic for whether a frequency will work with a given decomposer to the decomposer classes themselves.

codecov[bot]: # [Codecov](https://codecov.io/gh/alteryx/evalml/pull/3811?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx) Report
> Merging [#3811](https://codecov.io/gh/alteryx/evalml/pull/3811?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx) (c62b536) into [main](https://codecov.io/gh/alteryx/evalml/commit/ab3e0f9b1208aa6e6e80f15db3a8caa88172ec22?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx) (ab3e0f9) will **decrease** coverage by `46.4%`.
> The diff coverage is `100.0%`.

```diff
@@           Coverage Diff            @@
##            main   #3811      +/-   ##
========================================
- Coverage   99.7%   53.3%   -46.3%     
========================================
  Files        343     343              
  Lines      35798   35803       +5     
========================================
- Hits       35661   19074   -16587     
- Misses       137   16729   +16592     
```


| [Impacted Files](https://codecov.io/gh/alteryx/evalml/pull/3811?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx) | Coverage Δ | |
|---|---|---|
| [evalml/tests/pipeline\_tests/test\_pipeline\_utils.py](https://codecov.io/gh/alteryx/evalml/pull/3811/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-ZXZhbG1sL3Rlc3RzL3BpcGVsaW5lX3Rlc3RzL3Rlc3RfcGlwZWxpbmVfdXRpbHMucHk=) | `0.0% <ø> (-99.7%)` | :arrow_down: |
| [...omponents/transformers/preprocessing/decomposer.py](https://codecov.io/gh/alteryx/evalml/pull/3811/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-ZXZhbG1sL3BpcGVsaW5lcy9jb21wb25lbnRzL3RyYW5zZm9ybWVycy9wcmVwcm9jZXNzaW5nL2RlY29tcG9zZXIucHk=) | `99.3% <100.0%> (+0.1%)` | :arrow_up: |
| [...nents/transformers/preprocessing/stl\_decomposer.py](https://codecov.io/gh/alteryx/evalml/pull/3811/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-ZXZhbG1sL3BpcGVsaW5lcy9jb21wb25lbnRzL3RyYW5zZm9ybWVycy9wcmVwcm9jZXNzaW5nL3N0bF9kZWNvbXBvc2VyLnB5) | `100.0% <100.0%> (ø)` | |
| [evalml/pipelines/utils.py](https://codecov.io/gh/alteryx/evalml/pull/3811/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-ZXZhbG1sL3BpcGVsaW5lcy91dGlscy5weQ==) | `58.1% <100.0%> (-41.4%)` | :arrow_down: |
| [evalml/\_\_main\_\_.py](https://codecov.io/gh/alteryx/evalml/pull/3811/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-ZXZhbG1sL19fbWFpbl9fLnB5) | `0.0% <0.0%> (-100.0%)` | :arrow_down: |
| [evalml/tests/utils\_tests/test\_logger.py](https://codecov.io/gh/alteryx/evalml/pull/3811/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-ZXZhbG1sL3Rlc3RzL3V0aWxzX3Rlc3RzL3Rlc3RfbG9nZ2VyLnB5) | `0.0% <0.0%> (-100.0%)` | :arrow_down: |
| [evalml/tests/automl\_tests/test\_search.py](https://codecov.io/gh/alteryx/evalml/pull/3811/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-ZXZhbG1sL3Rlc3RzL2F1dG9tbF90ZXN0cy90ZXN0X3NlYXJjaC5weQ==) | `0.0% <0.0%> (-100.0%)` | :arrow_down: |
| [evalml/model\_understanding/force\_plots.py](https://codecov.io/gh/alteryx/evalml/pull/3811/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-ZXZhbG1sL21vZGVsX3VuZGVyc3RhbmRpbmcvZm9yY2VfcGxvdHMucHk=) | `0.0% <0.0%> (-100.0%)` | :arrow_down: |
| [evalml/tests/automl\_tests/test\_progress.py](https://codecov.io/gh/alteryx/evalml/pull/3811/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-ZXZhbG1sL3Rlc3RzL2F1dG9tbF90ZXN0cy90ZXN0X3Byb2dyZXNzLnB5) | `0.0% <0.0%> (-100.0%)` | :arrow_down: |
| [evalml/tests/pipeline\_tests/test\_graphs.py](https://codecov.io/gh/alteryx/evalml/pull/3811/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx#diff-ZXZhbG1sL3Rlc3RzL3BpcGVsaW5lX3Rlc3RzL3Rlc3RfZ3JhcGhzLnB5) | `0.0% <0.0%> (-100.0%)` | :arrow_down: |
| ... and [102 more](https://codecov.io/gh/alteryx/evalml/pull/3811/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx) | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=alteryx)


Patch:
diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst
index dcae503d..2c8f91f6 100644
--- a/docs/source/release_notes.rst
+++ b/docs/source/release_notes.rst
@@ -10,6 +10,7 @@ Release Notes
         * Updated ``STLDecomposer`` to infer the time index frequency if it's not present :pr:`3829`
         * Updated ``_drop_time_index`` to move the time index from X to both ``X.index`` and ``y.index`` :pr:`3829`
     * Changes
+        * Consolidated decomposition frequency validation logic to ``Decomposer`` class :pr:`3811`
         * Removed Featuretools version upper bound and prevent Woodwork 0.20.0 from being installed :pr:`3813`
         * Updated min Featuretools version to 0.16.0, min nlp-primitives version to 2.9.0 and min Dask version to 2022.2.0 :pr:`3823`
     * Documentation Changes
diff --git a/evalml/pipelines/components/transformers/preprocessing/decomposer.py b/evalml/pipelines/components/transformers/preprocessing/decomposer.py
index 5226a549..c9241053 100644
--- a/evalml/pipelines/components/transformers/preprocessing/decomposer.py
+++ b/evalml/pipelines/components/transformers/preprocessing/decomposer.py
@@ -1,6 +1,7 @@
 """Component that removes trends from time series and returns the decomposed components."""
 from __future__ import annotations
 
+import re
 from abc import abstractmethod
 
 import matplotlib.pyplot as plt
@@ -32,6 +33,7 @@ class Decomposer(Transformer):
     modifies_features = False
     modifies_target = True
     needs_fitting = True
+    invalid_frequencies = []
 
     def __init__(
         self,
@@ -95,6 +97,20 @@ class Decomposer(Transformer):
         """
         return self.fit(X, y).transform(X, y)
 
+    @classmethod
+    def is_freq_valid(self, freq: str):
+        """Determines if the given string represents a valid frequency for this decomposer.
+
+        Args:
+            freq (str): A frequency to validate. See the pandas docs at https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#offset-aliases for options.
+
+        Returns:
+            boolean representing whether the frequency is valid or not.
+        """
+        match = re.match(r"(^\d+)?([A-Z]+)-?([A-Z]+)?", freq)
+        _, freq, _ = match.groups()
+        return freq not in self.invalid_frequencies
+
     @abstractmethod
     def get_trend_dataframe(self, y: pd.Series):
         """Return a list of dataframes, each with 3 columns: trend, seasonality, residual."""
diff --git a/evalml/pipelines/components/transformers/preprocessing/stl_decomposer.py b/evalml/pipelines/components/transformers/preprocessing/stl_decomposer.py
index 1b31da7f..e185577d 100644
--- a/evalml/pipelines/components/transformers/preprocessing/stl_decomposer.py
+++ b/evalml/pipelines/components/transformers/preprocessing/stl_decomposer.py
@@ -35,6 +35,24 @@ class STLDecomposer(Decomposer):
 
     modifies_features = False
     modifies_target = True
+    invalid_frequencies = [
+        "SM",
+        "BM",
+        "SMS",
+        "BMS",
+        "BQ",
+        "BQS",
+        "T",
+        "S",
+        "L",
+        "U",
+        "N",
+        "A",
+        "BA",
+        "AS",
+        "BAS",
+        "BH",
+    ]
 
     def __init__(
         self,
diff --git a/evalml/pipelines/utils.py b/evalml/pipelines/utils.py
index d48bab7a..97eb59b1 100644
--- a/evalml/pipelines/utils.py
+++ b/evalml/pipelines/utils.py
@@ -65,8 +65,6 @@ from evalml.problem_types import (
 from evalml.utils import get_time_index, infer_feature_types
 from evalml.utils.gen_utils import contains_all_ts_parameters
 
-_UNSUPPORTED_FREQUENCIES_STL_DECOMPOSER = ["A", "Y", "T", "AS", "YS"]
-
 
 def _get_label_encoder(X, y, problem_type, estimator_class, sampler_name=None):
     component = []
@@ -234,11 +232,8 @@ def _get_decomposer(X, y, problem_type, estimator_class, sampler_name=None):
         # If the time index frequency is uninferrable, STL will fail
         if time_index.freq is None:
             return components
-        freq = time_index.freq.name.split("-")[0]
-        if (
-            freq[-1] not in _UNSUPPORTED_FREQUENCIES_STL_DECOMPOSER
-            and freq not in _UNSUPPORTED_FREQUENCIES_STL_DECOMPOSER
-        ):
+        freq = time_index.freq.name
+        if STLDecomposer.is_freq_valid(freq):
             components.append(STLDecomposer)
     return components
 


Test patch:
diff --git a/evalml/tests/pipeline_tests/test_pipeline_utils.py b/evalml/tests/pipeline_tests/test_pipeline_utils.py
index c7234d73..44f6825e 100644
--- a/evalml/tests/pipeline_tests/test_pipeline_utils.py
+++ b/evalml/tests/pipeline_tests/test_pipeline_utils.py
@@ -206,6 +206,8 @@ def test_make_pipeline(
         ("10T", False),
         ("AS-JAN", False),
         ("YS", False),
+        ("S", False),
+        ("2BQ", False),
         (None, False),
     ],
 )