Repository: sktime/sktime
Pull request #5539 (2023-11-06 22:42:28)

[ENH] refactor index generation in reducers to use `ForecastingHorizon` method
This PR refactors index generation in reducers to use the `ForecastingHorizon` method instead of custom functionality.

Also useful as a test for the former logic.

Depends on https://github.com/sktime/sktime/pull/5543 due to an incorrect test.


Patch:
diff --git a/sktime/base/_meta.py b/sktime/base/_meta.py
index 78901df76..6e49c88e6 100644
--- a/sktime/base/_meta.py
+++ b/sktime/base/_meta.py
@@ -97,29 +97,31 @@ class _HeterogenousMetaEstimator:
 
     def _get_params(self, attr, deep=True, fitted=False):
         if fitted:
-            method = "_get_fitted_params"
-            methodd = "get_fitted_params"
+            private_method = "_get_fitted_params"
+            public_method = "get_fitted_params"
             deepkw = {}
         else:
-            method = "get_params"
-            methodd = "get_params"
+            private_method = "get_params"
+            public_method = "get_params"
             deepkw = {"deep": deep}
 
-        out = getattr(super(), method)(**deepkw)
+        out = getattr(super(), private_method)(**deepkw)
         if deep and hasattr(self, attr):
             estimators = getattr(self, attr)
             estimators = [(x[0], x[1]) for x in estimators]
             out.update(estimators)
             for name, estimator in estimators:
                 # checks estimator has the method we want to call
-                cond1 = hasattr(estimator, methodd)
+                cond1 = hasattr(estimator, public_method)
                 # checks estimator is fitted if calling get_fitted_params
                 is_fitted = hasattr(estimator, "is_fitted") and estimator.is_fitted
                 # if we call get_params and not get_fitted_params, this is True
                 cond2 = not fitted or is_fitted
                 # check both conditions together
                 if cond1 and cond2:
-                    for key, value in getattr(estimator, methodd)(**deepkw).items():
+                    for key, value in getattr(estimator, public_method)(
+                        **deepkw
+                    ).items():
                         out[f"{name}__{key}"] = value
         return out
 
diff --git a/sktime/forecasting/compose/_reduce.py b/sktime/forecasting/compose/_reduce.py
index 1fa634987..9f179fc26 100644
--- a/sktime/forecasting/compose/_reduce.py
+++ b/sktime/forecasting/compose/_reduce.py
@@ -1584,68 +1584,31 @@ def _create_fcst_df(target_date, origin_df, fill=None):
     -------
     A pandas dataframe or series
     """
-    oi = origin_df.index
-    if not isinstance(oi, pd.MultiIndex):
-        if isinstance(origin_df, pd.Series):
-            if fill is None:
-                template = pd.Series(np.zeros(len(target_date)), index=target_date)
-            else:
-                template = pd.Series(fill, index=target_date)
-            template.name = origin_df.name
-            return template
-        else:
-            if fill is None:
-                template = pd.DataFrame(
-                    np.zeros((len(target_date), len(origin_df.columns))),
-                    index=target_date,
-                    columns=origin_df.columns.to_list(),
-                )
-            else:
-                template = pd.DataFrame(
-                    fill, index=target_date, columns=origin_df.columns.to_list()
-                )
-            return template
+    if not isinstance(target_date, ForecastingHorizon):
+        ix = pd.Index(target_date)
+        fh = ForecastingHorizon(ix, is_relative=False)
     else:
-        idx = origin_df.index.to_frame(index=False)
-        instance_names = idx.columns[0:-1].to_list()
-        time_names = idx.columns[-1]
-        idx = idx[instance_names].drop_duplicates()
-
-        timeframe = pd.DataFrame(target_date, columns=[time_names])
-        target_frame = idx.merge(timeframe, how="cross")
-        if hasattr(target_date, "freq"):
-            freq_inferred = target_date.freq
-            mi = (
-                target_frame.groupby(instance_names, as_index=True)
-                .apply(
-                    lambda df: df.drop(instance_names, axis=1)
-                    .set_index(time_names)
-                    .asfreq(freq_inferred)
-                )
-                .index
-            )
-        else:
-            mi = (
-                target_frame.groupby(instance_names, as_index=True)
-                .apply(lambda df: df.drop(instance_names, axis=1).set_index(time_names))
-                .index
-            )
+        fh = target_date.to_absolute()
 
-        if fill is None:
-            template = pd.DataFrame(
-                np.zeros((len(target_date) * idx.shape[0], len(origin_df.columns))),
-                index=mi,
-                columns=origin_df.columns.to_list(),
-            )
-        else:
-            template = pd.DataFrame(
-                fill,
-                index=mi,
-                columns=origin_df.columns.to_list(),
-            )
+    index = fh.get_expected_pred_idx(origin_df)
+
+    if isinstance(origin_df, pd.Series):
+        columns = [origin_df.name]
+    else:
+        columns = origin_df.columns.to_list()
+
+    if fill is None:
+        values = 0
+    else:
+        values = fill
+
+    res = pd.DataFrame(values, index=index, columns=columns)
+
+    if isinstance(origin_df, pd.Series) and not isinstance(index, pd.MultiIndex):
+        res = res.iloc[:, 0]
+        res.name = origin_df.name
 
-        template = template.astype(origin_df.dtypes.to_dict())
-        return template
+    return res
 
 
 def _coerce_col_str(X):


Test patch:
diff --git a/sktime/forecasting/compose/tests/test_reduce_global.py b/sktime/forecasting/compose/tests/test_reduce_global.py
index c332178dd..e2a78d278 100644
--- a/sktime/forecasting/compose/tests/test_reduce_global.py
+++ b/sktime/forecasting/compose/tests/test_reduce_global.py
@@ -106,6 +106,7 @@ def y_dict():
     # Create integer index data
     y_numeric = y_train.copy()
     y_numeric.index = pd.to_numeric(y_numeric.index)
+    y_numeric.index.names = [None]  # setting None to cover "no index name" case
     y_dict["y_numeric"] = y_numeric
 
     return y_dict
@@ -139,7 +140,7 @@ def check_eval(test_input, expected):
         ),
         (
             "y_train",
-            [None],
+            ["Period"],
         ),
         (
             "y_numeric",
@@ -186,7 +187,7 @@ def test_recursive_reduction(y, index_names, y_dict):
         ),
         (
             "y_train",
-            [None],
+            ["Period"],
         ),
         (
             "y_numeric",
@@ -232,7 +233,7 @@ def test_direct_reduction(y, index_names, y_dict):
         ),
         (
             "y_train",
-            [None],
+            ["Period"],
         ),
         (
             "y_numeric",