Repository: sktime/sktime

Issue:
[ENH] interface the `seasonal` package as a parameter estimator for seasonality (`BaseParamFitter` descendant)
We now have an interface design for generic parameter estimators, with `BaseParamFitter`.
(original design issue: https://github.com/sktime/sktime/issues/1056)

Still experimental, we should further explore it by interfacing useful parameter estimators.

@mloning suggested the methods in the `seasonal` package in https://github.com/sktime/sktime/issues/1056, given the existing base class interface and seasonality examples it seems like a good first issue.


Discussion:
Can I work on this issue?
of course, all yours!
okay. Thanks.
There's no extension template for parameter fitters yet, so I recommend taking `SeasonalityACF` as a template.

Writing an extension template would also be a nice (but separate) contribution.
Hey! @fkiraly  I see this issue is pending for a lot time now! Can I take this? (Also, I need to create a new file or change seasonality.py-SeasonalityACF ?)
> Can I take this? 

Yes, it is more than a month, so it's open again!

> I need to create a new file or change seasonality.py-SeasonalityACF

You should start with a new file, but you can copy `seasonality.py` and take it as a template (just a suggestion).
Hey! I did the task, but I have a few questions,
1. Does seasonalityACF and seasonal have to return the same result? (because the one I interfaced gave a negative `sp` on airline_data)
2. What to name the new file? What should be the class name? Or should I keep that in the same file with a different class name?
3. Should I also write the test function for it the test file?
> Does seasonalityACF and seasonal have to return the same result? (because the one I interfaced gave a negative sp on airline_data)

Not necessarily, there are different algorithms inside. Although I'm surprise to see negative `sp`, shouldn't seasonality parameter be an integer strictly larger than 1?

> What to name the new file? What should be the class name? Or should I keep that in the same file with a different class name?

Not sure - could be in the same file (`seasonality`), or `seasonal` after the package name being interfaced. The class name could be the interfaced method, in UpperCamelCase.

> Should I also write the test function for it the test file?

There are some default test for parameter estimators, a test to add would be checking of the output of the `sktime` interface against the interfaced logic (should give same output on same/comparable data)
Hey! I ran some tests on my own (pardon my naive approach, **Should I still make a PR for this?** Or should I properly research the working of  `seasonal` and our implementations for trends/seasonality?)

![image](https://user-images.githubusercontent.com/86467990/216127159-e7947e47-4728-4a88-9fdd-43a0fe36d7ed.png)

![image](https://user-images.githubusercontent.com/86467990/216129394-0184f906-1ad0-4364-92dc-fa4e4966d9b7.png)

![image](https://user-images.githubusercontent.com/86467990/216129508-8c4709a9-37fe-4127-a7b1-19b74327e363.png)

> Hey! I ran some tests on my own (pardon my naive approach, Should I still make a PR for this? 

PR would be much appreciated! Even if it's not done, we can always discuss in a draft.

Your printouts look interesting, looks like you're almost there!

Regarding the output, I think what you wrote is close to a transformer than a parameter estimator:

* a parameter estimator would tell you at which periodicities the estimator detected a seasonality. That would be integers, e.g., 2 (months), 12 (months), etc. Example for this is `SeasonalityACF`.
* a transformer is sth that removes seasonality, or produces the seasonal component. An example for this is `STLTransformer`, or `Deseasonalizer`

From your printout, I suspect that you could easily turn your current class into the first one, you just need to return the seasonality that is fitted instead of the deseasonalized series as the `sp` fitted parameter.
Hey! @fkiraly  I forgot to create a new PR for this, but [here's the link](https://github.com/sktime/sktime/pull/4146/commits/1e3ccfd2c9e1c2c97b325b171d284394201b66ac) to it; I have college exams in a few days, so I will make changes and create a new PR after that only.
@blazingbhavneek, could you kindly make a separate pull request for the seasonal estimator?
Instead of putting it in the same PR as the fourier transform.

Resolving patch:
diff --git a/sktime/param_est/seasonality.py b/sktime/param_est/seasonality.py
--- a/sktime/param_est/seasonality.py
+++ b/sktime/param_est/seasonality.py
@@ -2,8 +2,8 @@
 # copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
 """Parameter estimators for seasonality."""
 
-__author__ = ["fkiraly"]
-__all__ = ["SeasonalityACF"]
+__author__ = ["fkiraly", "blazingbhavneek"]
+__all__ = ["SeasonalityACF", "SeasonalityPeriodogram"]
 
 import numpy as np
 
@@ -407,3 +407,116 @@ def get_test_params(cls, parameter_set="default"):
         params2 = {"candidate_sp": [3, 7, 12]}
 
         return [params1, params2]
+
+
+class SeasonalityPeriodogram(BaseParamFitter):
+    """Score periodicities by their spectral power.
+
+    Interfacing `seasonal.periodogram` to determine candidate seasonality parameters.
+
+    Parameters
+    ----------
+    min_period : int
+        Disregard periods shorter than this number of samples.
+        Defaults to 4
+    max_period : int
+        Disregard periods longer than this number of samples.
+        Defaults to None
+    thresh : float (0..1)
+        Retain periods scoring above thresh*maxscore. Defaults to 0.10
+
+    Attributes
+    ----------
+    sp_ : int, seasonality period with highest power, if any sub-threshold, else 1
+    sp_significant_ : list of int, array of Fourier periods in descending order
+        of their powers.
+
+    Examples
+    --------
+    >>> from sktime.datasets import load_airline
+    >>> from sktime.param_est.seasonality import SeasonalityPeriodogram
+    >>> X = load_airline().diff()[1:]  # doctest: +SKIP
+    >>> sp_est = SeasonalityPeriodogram()  # doctest: +SKIP
+    >>> sp_est.fit(X)  # doctest: +SKIP
+    SeasonalityPeriodogram(...)
+    >>> sp_est.get_fitted_params()["sp"]  # doctest: +SKIP
+    6
+    >>> sp_est.get_fitted_params()["sp_significant"]  # doctest: +SKIP
+    array([6, 12, 14, 4, 10, 5])
+    """
+
+    _tags = {
+        "X_inner_mtype": "pd.Series",
+        "scitype:X": "Series",
+        "capability:missing_values": True,
+        "capability:multivariate": False,
+        "python_dependencies": "seasonal",
+    }
+
+    def __init__(self, min_period=4, max_period=None, thresh=0.10):
+        self.min_period = min_period
+        self.max_period = max_period
+        self.thresh = thresh
+        super(SeasonalityPeriodogram, self).__init__()
+
+    def _fit(self, X):
+        """Fit estimator and estimate parameters.
+
+        private _fit containing the core logic, called from fit
+
+        Writes to self:
+            Sets fitted model attributes ending in "_".
+
+        Parameters
+        ----------
+        X : guaranteed to be of a type in self.get_tag("X_inner_mtype")
+            Time series to which to fit the estimator.
+
+        Returns
+        -------
+        self : reference to self
+        """
+        from seasonal.periodogram import periodogram_peaks
+
+        seasons = periodogram_peaks(
+            X,
+            min_period=self.min_period,
+            max_period=self.max_period,
+            thresh=self.thresh,
+        )
+
+        seasons = [x[0] for x in seasons]
+
+        if seasons is None or len(seasons) == 0:
+            self.sp_ = 1
+            self.sp_significant_ = []
+        else:
+            self.sp_significant_ = seasons
+            self.sp_ = self.sp_significant_[0]
+
+        return self
+
+    @classmethod
+    def get_test_params(cls, parameter_set="default"):
+        """Return testing parameter settings for the estimator.
+
+        Parameters
+        ----------
+        parameter_set : str, default="default"
+            Name of the set of test parameters to return, for use in tests. If no
+            special parameters are defined for a value, will return `"default"` set.
+            There are currently no reserved values for transformers.
+
+        Returns
+        -------
+        params : dict or list of dict, default = {}
+            Parameters to create testing instances of the class
+            Each dict are parameters to construct an "interesting" test instance, i.e.,
+            `MyClass(**params)` or `MyClass(**params[i])` creates a valid test instance.
+            `create_test_instance` uses the first (or only) dictionary in `params`
+        """
+        params1 = {}
+        params2 = {"min_period": 5, "max_period": 24, "thresh": 0.1}
+        params3 = {"min_period": 5}
+
+        return [params1, params2, params3]


Test patch: