Repository: sktime/sktime

Issue:
[BUG] odd failure in `fh._coerce_to_period` that occurs only on windows, in context of hierarchical forecast
When making hierarchical forecasts on default output of `_make_hierarchical`, `fh.coerce_to_period` fails, on a windows run:
https://github.com/alan-turing-institute/sktime/runs/5503723513

This is quite strange, and I cannot reproduce the behaviour locally on my windows machine (python 3.8 or 3.9).

It looks like `freq` is not correctly preserved in subsetting from a hierarchical multi-index ... but only under specific conditions on a windows system???

Expected behaviour: the test does not fail, just like on linux/max and as on my local system.


Discussion:
any good ideas from those who have recently looked at `fh` or `freq`?
@khrapovs, @eenticott-shell, @k1m190r?
I'd give this bug high priority due to the hierarchical functionality attached to it.
suspicion: `get_cutoff(_make_hierarchical())`, for some reason, results in a time stamp without the `freq` attribute. Why, and why specifically on windows? On my local system, the `freq` attribute is not lost.

Any good ideas, @ltsaprounis?
Might be a data types issue, I'll look into it!

Resolving patch:
diff --git a/sktime/datatypes/_vectorize.py b/sktime/datatypes/_vectorize.py
--- a/sktime/datatypes/_vectorize.py
+++ b/sktime/datatypes/_vectorize.py
@@ -4,7 +4,6 @@
 
 Contains VectorizedDF class.
 """
-
 import pandas as pd
 
 from sktime.datatypes._check import check_is_scitype, mtype
@@ -150,6 +149,7 @@ def __getitem__(self, i: int):
         X = self.X_multiindex
         ind = self.get_iter_indices()[i]
         item = X.loc[ind]
+        item = _enforce_index_freq(item)
         # pd-multiindex type (Panel case) expects these index names:
         if self.iterate_as == "Panel":
             item.index.set_names(["instances", "timepoints"], inplace=True)
@@ -208,3 +208,20 @@ def reconstruct(self, df_list, convert_back=False, overwrite_index=True):
             )
 
             return X_reconstructed_orig_format
+
+
+def _enforce_index_freq(item: pd.Series) -> pd.Series:
+    """Enforce the frequency of a Series index using pd.infer_freq.
+
+    Parameters
+    ----------
+    item : pd.Series
+    Returns
+    -------
+    pd.Series
+        Pandas series with the inferred frequency. If the frequency cannot be inferred
+        it will stay None
+    """
+    if hasattr(item.index, "freq") and item.index.freq is None:
+        item.index.freq = pd.infer_freq(item.index)
+    return item


Test patch:
diff --git a/sktime/datatypes/tests/test_vectorize.py b/sktime/datatypes/tests/test_vectorize.py
--- a/sktime/datatypes/tests/test_vectorize.py
+++ b/sktime/datatypes/tests/test_vectorize.py
@@ -4,12 +4,13 @@
 __author__ = ["fkiraly"]
 
 import numpy as np
+import pandas as pd
 import pytest
 
 from sktime.datatypes import MTYPE_REGISTER, SCITYPE_REGISTER
 from sktime.datatypes._check import AMBIGUOUS_MTYPES, check_is_mtype
 from sktime.datatypes._examples import get_examples
-from sktime.datatypes._vectorize import VectorizedDF
+from sktime.datatypes._vectorize import VectorizedDF, _enforce_index_freq
 from sktime.utils._testing.deep_equals import deep_equals
 
 SCITYPES = ["Panel", "Hierarchical"]
@@ -321,3 +322,41 @@ def test_reconstruct_identical(scitype, mtype, fixture_index, iterate_as):
 
     # reconstructed fixture should equal original fixture if convert_back
     assert deep_equals(X_vect.reconstruct(X_list, convert_back=True), fixture)
+
+
+@pytest.mark.parametrize(
+    "item, freq",
+    [
+        (
+            pd.Series(
+                data=1,
+                index=pd.DatetimeIndex(
+                    ["2000-01-01", "2000-01-02", "2000-01-03", "2000-01-04"], freq="D"
+                ),
+            ),
+            "D",
+        ),
+        (
+            pd.Series(
+                data=1,
+                index=pd.DatetimeIndex(
+                    ["2000-01-01", "2000-01-02", "2000-01-03", "2000-01-04"], freq=None
+                ),
+            ),
+            "D",
+        ),
+        (
+            pd.Series(
+                data=1,
+                index=pd.DatetimeIndex(
+                    ["2000-01-01", "2000-01-03", "2000-01-03", "2000-01-09"], freq=None
+                ),
+            ),
+            None,
+        ),
+    ],
+)
+def test_enforce_index_freq(item, freq):
+    """Tests that enforce freq infers the right frequency."""
+    item = _enforce_index_freq(item)
+    assert item.index.freq == freq
diff --git a/sktime/forecasting/base/tests/test_base.py b/sktime/forecasting/base/tests/test_base.py
--- a/sktime/forecasting/base/tests/test_base.py
+++ b/sktime/forecasting/base/tests/test_base.py
@@ -4,15 +4,14 @@
 
 __author__ = ["fkiraly"]
 
-# from functools import reduce
-# from operator import mul
+from functools import reduce
+from operator import mul
 
 import pytest
 
-from sktime.datatypes import check_is_mtype, convert, get_examples
+from sktime.datatypes import check_is_mtype, convert
 from sktime.forecasting.arima import ARIMA
-
-# from sktime.utils._testing.hierarchical import _make_hierachical
+from sktime.utils._testing.hierarchical import _make_hierarchical
 from sktime.utils._testing.panel import _make_panel_X
 
 PANEL_MTYPES = ["pd-multiindex", "nested_univ", "numpy3D"]
@@ -64,14 +63,11 @@ def test_vectorization_series_to_hier(mtype):
     This test passes Panel data to the ARIMA forecaster which internally has an
     implementation for Series only, so the BaseForecaster has to vectorize.
     """
-    # hierarchy_levels = (2, 4)
-    # n_instances = reduce(mul, hierarchy_levels)
-
-    # y = _make_hierachical(hierarchy_levels=hierarchy_levels, random_state=84)
-    # y = convert(y, from_type="pd_multiindex_hier", to_type=mtype)
+    hierarchy_levels = (2, 4)
+    n_instances = reduce(mul, hierarchy_levels)
 
-    y = get_examples(mtype, "Hierarchical")[1]
-    n_instances = 6
+    y = _make_hierarchical(hierarchy_levels=hierarchy_levels, random_state=84)
+    y = convert(y, from_type="pd_multiindex_hier", to_type=mtype)
 
     y_pred = ARIMA().fit(y).predict([1, 2, 3])
     valid, _, metadata = check_is_mtype(y_pred, mtype, return_metadata=True)