Repository: sktime/sktime (python)

Issue:
[BUG] `HierarchyEnsembleForecaster` returns unexpected predictions if data has only one hierarchy level and forecasters specified by node
**Describe the bug**

The `HierarchyEnsembleForecaster` returns unexpected predictions if the hierarchy has only one level and different forecasters are specified per node (`by=node`).


**To Reproduce**
The code snippet below is similar to the examples in the [documentation](https://www.sktime.net/en/latest/api_reference/auto_generated/sktime.forecasting.compose.HierarchyEnsembleForecaster.html) but with only one level instead of two and only 3 bottom nodes instead of 7.

```python
from sktime.forecasting.compose import HierarchyEnsembleForecaster
from sktime.forecasting.naive import NaiveForecaster
from sktime.forecasting.trend import PolynomialTrendForecaster, TrendForecaster
from sktime.utils._testing.hierarchical import _bottom_hier_datagen
y = _bottom_hier_datagen(
        no_bottom_nodes=3,
        no_levels=1,
        random_seed=123
)
y.groupby(["l1_agg"]).sum().index

# Example of by = 'node'
forecasters = [
    ('trend', TrendForecaster(), [("__total")]),
    ('poly', PolynomialTrendForecaster(degree=2), [('l1_node01')]),
]
forecaster = HierarchyEnsembleForecaster(
                forecasters=forecasters,
                by='node', default=NaiveForecaster()
)
forecaster.fit(y, fh=[1, 2, 3])
y_pred = forecaster.predict()
y_pred
```
*Output:*
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th></th>
      <th>passengers</th>
    </tr>
    <tr>
      <th>l1_agg</th>
      <th>timepoints</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th rowspan="3" valign="top">__total</th>
      <th>1961-01</th>
      <td>9024.895980</td>
    </tr>
    <tr>
      <th>1961-02</th>
      <td>9075.494395</td>
    </tr>
    <tr>
      <th>1961-03</th>
      <td>9126.092810</td>
    </tr>
    <tr>
      <th rowspan="6" valign="top">l1_node01</th>
      <th>1961-01</th>
      <td>497.671704</td>
    </tr>
    <tr>
      <th>1961-01</th>
      <td>432.000000</td>
    </tr>
    <tr>
      <th>1961-02</th>
      <td>501.352085</td>
    </tr>
    <tr>
      <th>1961-02</th>
      <td>432.000000</td>
    </tr>
    <tr>
      <th>1961-03</th>
      <td>505.046482</td>
    </tr>
    <tr>
      <th>1961-03</th>
      <td>432.000000</td>
    </tr>
    <tr>
      <th rowspan="3" valign="top">l1_node02</th>
      <th>1961-01</th>
      <td>5893.819230</td>
    </tr>
    <tr>
      <th>1961-02</th>
      <td>5893.819230</td>
    </tr>
    <tr>
      <th>1961-03</th>
      <td>5893.819230</td>
    </tr>
    <tr>
      <th rowspan="3" valign="top">l1_node03</th>
      <th>1961-01</th>
      <td>1919.395692</td>
    </tr>
    <tr>
      <th>1961-02</th>
      <td>1919.395692</td>
    </tr>
    <tr>
      <th>1961-03</th>
      <td>1919.395692</td>
    </tr>
  </tbody>
</table>
</div>

**Expected behavior**
*Output:*
<div>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th></th>
      <th>passengers</th>
    </tr>
    <tr>
      <th>l1_agg</th>
      <th>timepoints</th>
      <th></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th rowspan="3" valign="top">__total</th>
      <th>1961-01</th>
      <td>9024.895980</td>
    </tr>
    <tr>
      <th>1961-02</th>
      <td>9075.494395</td>
    </tr>
    <tr>
      <th>1961-03</th>
      <td>9126.092810</td>
    </tr>
    <tr>
      <th rowspan="3" valign="top">l1_node01</th>
      <th>1961-01</th>
      <td>497.671704</td>
    </tr>
    <tr>
      <th>1961-02</th>
      <td>501.352085</td>
    </tr>
    <tr>
      <th>1961-03</th>
      <td>505.046482</td>
    </tr>
    <tr>
      <th rowspan="3" valign="top">l1_node02</th>
      <th>1961-01</th>
      <td>5893.819230</td>
    </tr>
    <tr>
      <th>1961-02</th>
      <td>5893.819230</td>
    </tr>
    <tr>
      <th>1961-03</th>
      <td>5893.819230</td>
    </tr>
    <tr>
      <th rowspan="3" valign="top">l1_node03</th>
      <th>1961-01</th>
      <td>1919.395692</td>
    </tr>
    <tr>
      <th>1961-02</th>
      <td>1919.395692</td>
    </tr>
    <tr>
      <th>1961-03</th>
      <td>1919.395692</td>
    </tr>
  </tbody>
</table>
</div>



**Additional context**
The predictions for the node `l1_node01` are duplicated. The first occurrences are the predictions from `PolynomialTrendForecaster`. The second occurrences are predictions of the `NaiveForecaster` which is configured as default. If using two hierarchy levels as in the documentation the predictions are as expected (i.e., no duplicates from the default predictor).

I tried to stay close to the example in the doc. However, maybe I am just using the `HierarchyEnsembleForecaster` class wrong.

**Versions**

System:
    python: 3.11.5 | packaged by conda-forge | (main, Aug 27 2023, 03:35:23) [Clang 15.0.7 ]
executable: ../anaconda3/envs/sktime311/bin/python
   machine: macOS-14.0-x86_64-i386-64bit

Python dependencies:
          pip: 23.2.1
       sktime: 0.24.0
      sklearn: 1.3.0
       skbase: 0.5.1
        numpy: 1.25.2
        scipy: 1.11.2
       pandas: 2.0.3
   matplotlib: 3.7.2
       joblib: 1.3.2
        numba: None
  statsmodels: 0.14.0
     pmdarima: 2.0.3
statsforecast: None
      tsfresh: None
      tslearn: None
        torch: None
   tensorflow: None
tensorflow_probability: None

Pull request description:
[BUG] fix `HierarchyEnsembleForecaster` returned unexpected predictions if data had only one hierarchy level and forecasters specified by node
<!--
Welcome to sktime, and thanks for contributing!
Please have a look at our contribution guide:
https://www.sktime.net/en/latest/get_involved/contributing.html
-->

#### Reference Issues/PRs
<!--
Example: Fixes #1234. See also #3456.

Please use keywords (e.g., Fixes) to create link to the issues or pull requests
you resolved, so that they will automatically be closed when your pull request
is merged. See https://github.com/blog/1506-closing-issues-via-pull-requests.
If no issue exists, you can open one here: https://github.com/sktime/sktime/issues
-->
Fixes #5489

#### What does this implement/fix? Explain your changes.
<!--
A clear and concise description of what you have implemented.
-->
This PR fixes bug reported for HierarchyEnsembleForecaster which returned unexpected predictions if data had only one hierarchy level and forecasters specified by node.

#### Does your contribution introduce a new dependency? If yes, which one?

<!--
Only relevant if you changed pyproject.toml.
We try to minimize dependencies in the core dependency set. There
are also further specific instructions to follow for soft dependencies.
See here for handling dependencies in sktime: https://www.sktime.net/en/latest/developer_guide/dependencies.html
-->
No

#### Did you add any tests for the change?

<!-- This section is useful if you have added a test in addition to the existing ones. This will ensure that further changes to these files won't introduce the same kind of bug. It is considered good practice to add tests with newly added code to enforce the fact that the code actually works. This will reduce the chance of introducing logical bugs.
-->
Not added, but updated test for HierarchyEnsembleForecaster to include conditions mentioned in the bug.


#### PR checklist
<!--
Please go through the checklist below. Please feel free to remove points if they are not applicable.
-->

##### For all contributions
- [X] The PR title starts with either [ENH], [MNT], [DOC], or [BUG]. [BUG] - bugfix, [MNT] - CI, test framework, [ENH] - adding or improving code, [DOC] - writing or improving documentation or docstrings.

<!--
Thanks for contributing!
-->


Patch:
diff --git a/.all-contributorsrc b/.all-contributorsrc
index 2663647c5..68675014c 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -2462,6 +2462,15 @@
         "code"
       ]
     },
+    {
+      "login": "rahulporuri",
+      "name": "Poruri Sai Rahul",
+      "avatar_url": "https://avatars.githubusercontent.com/u/1926457?v=4",
+      "profile": "https://github.com/rahulporuri",
+      "contributions": [
+        "doc"
+       ]
+    },
     {
       "login": "fspinna",
       "name": "Francesco Spinnato",
diff --git a/sktime/classification/kernel_based/_rocket_classifier.py b/sktime/classification/kernel_based/_rocket_classifier.py
index fb5a265de..c62173a41 100644
--- a/sktime/classification/kernel_based/_rocket_classifier.py
+++ b/sktime/classification/kernel_based/_rocket_classifier.py
@@ -30,15 +30,17 @@ class RocketClassifier(_DelegatedClassifier):
     Shorthand for the pipeline
     `rocket * StandardScaler(with_mean=False) * RidgeClassifierCV(alphas)`
     where `alphas = np.logspace(-3, 3, 10)`, and
-    where `rocket` depends on params `rocket_transform`, `use_multivariate` as follows:
-
-        | rocket_transform | `use_multivariate` | rocket (class)          |
-        |------------------|--------------------|-------------------------|
-        | "rocket"         | any                | Rocket                  |
-        | "minirocket"     | "yes               | MiniRocketMultivariate  |
-        | "minirocket"     | "no"               | MiniRocket              |
-        | "multirocket"    | "yes"              | MultiRocketMultivariate |
-        | "multirocket"    | "no"               | MultiRocket             |
+    where `rocket` depends on params `rocket_transform`, `use_multivariate` as follows
+
+    ================ ================== =======================
+    rocket_transform `use_multivariate` rocket (class)
+    ================ ================== =======================
+    "rocket"         any                Rocket
+    "minirocket"     "yes"               MiniRocketMultivariate
+    "minirocket"     "no"               MiniRocket
+    "multirocket"    "yes"              MultiRocketMultivariate
+    "multirocket"    "no"               MultiRocket
+    ================ ================== =======================
 
     classes are sktime classes, other parameters are passed on to the rocket class.
 
diff --git a/sktime/forecasting/arch/_uarch.py b/sktime/forecasting/arch/_uarch.py
index c63790215..62098f253 100644
--- a/sktime/forecasting/arch/_uarch.py
+++ b/sktime/forecasting/arch/_uarch.py
@@ -251,6 +251,7 @@ class ARCH(BaseForecaster):
         if fh:
             self._horizon = fh
 
+        y_name = y.name
         self._forecaster = _ARCH(
             y=y,
             x=X,
@@ -277,6 +278,7 @@ class ARCH(BaseForecaster):
             options=self.options,
             backcast=self.backcast,
         )
+        y.name = y_name
         return self
 
     def _get_arch_result_object(self, fh=None, X=None):
@@ -344,7 +346,7 @@ class ARCH(BaseForecaster):
         y_pred = pd.Series(
             ArchResultObject.mean.values[-1],
             index=full_range,
-            name=str(self._y.name),
+            name=self._y.name,
         )
         y_pred = y_pred.loc[abs_idx.to_pandas()]
         y_pred.index = self._horizon.to_absolute_index(self.cutoff)
@@ -398,11 +400,15 @@ class ARCH(BaseForecaster):
             upper_int = mean_forecast + (z_critical * std_err)
             lower_df = pd.DataFrame(
                 lower_int,
-                columns=[y_col_name + " " + str(alpha) + " " + "lower"],
+                columns=[
+                    y_col_name if y_col_name else "0" + " " + str(alpha) + " " + "lower"
+                ],
             )
             upper_df = pd.DataFrame(
                 upper_int,
-                columns=[y_col_name + " " + str(alpha) + " " + "upper"],
+                columns=[
+                    y_col_name if y_col_name else "0" + " " + str(alpha) + " " + "upper"
+                ],
             )
             df_list.append(pd.concat((lower_df, upper_df), axis=1))
         concat_df = pd.concat(df_list, axis=1)
@@ -410,7 +416,7 @@ class ARCH(BaseForecaster):
             OrderedDict.fromkeys(
                 [
                     col_df
-                    for col in y_col_name
+                    for col in (y_col_name if y_col_name else "0")
                     for col_df in concat_df.columns
                     if col in col_df
                 ]
@@ -425,7 +431,7 @@ class ARCH(BaseForecaster):
         final_columns = list(
             itertools.product(
                 *[
-                    [y_col_name],
+                    [y_col_name if y_col_name else 0],
                     coverage,
                     df.columns.get_level_values(2).unique(),
                 ]
diff --git a/sktime/forecasting/base/adapters/_fbprophet.py b/sktime/forecasting/base/adapters/_fbprophet.py
index 79ac65bf0..acc32d7f6 100644
--- a/sktime/forecasting/base/adapters/_fbprophet.py
+++ b/sktime/forecasting/base/adapters/_fbprophet.py
@@ -116,7 +116,10 @@ class _ProphetAdapter(BaseForecaster):
             df["cap"] = self.growth_cap
             df["floor"] = self.growth_floor
 
-        fit_kwargs = self.fit_kwargs or {}
+        if hasattr(self, "fit_kwargs") and isinstance(self.fit_kwargs, dict):
+            fit_kwargs = self.fit_kwargs
+        else:
+            fit_kwargs = {}
         if self.verbose:
             self._forecaster.fit(df=df, **fit_kwargs)
         else:
diff --git a/sktime/forecasting/compose/_hierarchy_ensemble.py b/sktime/forecasting/compose/_hierarchy_ensemble.py
index 1eca3af03..12cb104ca 100644
--- a/sktime/forecasting/compose/_hierarchy_ensemble.py
+++ b/sktime/forecasting/compose/_hierarchy_ensemble.py
@@ -285,7 +285,8 @@ class HierarchyEnsembleForecaster(_HeterogenousEnsembleForecaster):
                 if counter == 0:
                     nodes = mi
                 else:
-                    nodes.append(mi)
+                    # For nlevels = 2, 'nodes' is pd.Index object (L286)
+                    nodes = nodes.append(mi)
             else:
                 node_l = []
                 for i in range(len(node)):
@@ -495,6 +496,7 @@ class HierarchyEnsembleForecaster(_HeterogenousEnsembleForecaster):
                     raise ValueError("Nodes cannot be empty.")
                 if z.index.nlevels == 2:
                     nodes_ix = pd.Index(nodes)
+                    nodes_t += nodes
                 else:
                     nodes_l = []
                     for i in range(len(nodes)):


Test patch:
diff --git a/sktime/forecasting/compose/tests/test_hierarchy_ensemble.py b/sktime/forecasting/compose/tests/test_hierarchy_ensemble.py
index b3a655fb4..e56194314 100644
--- a/sktime/forecasting/compose/tests/test_hierarchy_ensemble.py
+++ b/sktime/forecasting/compose/tests/test_hierarchy_ensemble.py
@@ -7,10 +7,11 @@ __author__ = ["VyomkeshVyas"]
 import numpy as np
 import pytest
 
+from sktime.base._meta import flatten
 from sktime.datatypes._utilities import get_window
 from sktime.forecasting.compose import HierarchyEnsembleForecaster
 from sktime.forecasting.naive import NaiveForecaster
-from sktime.forecasting.trend import PolynomialTrendForecaster
+from sktime.forecasting.trend import PolynomialTrendForecaster, TrendForecaster
 from sktime.transformations.hierarchical.aggregate import Aggregator
 from sktime.utils._testing.hierarchical import _bottom_hier_datagen, _make_hierarchical
 from sktime.utils.validation._dependencies import _check_soft_dependencies
@@ -145,3 +146,57 @@ def test_hierarchy_ensemble_exog(forecasters):
     estimator_instance.fit(y=y_train, X=X_train, fh=[1, 2, 3])
     estimator_instance.predict(X=X_test)
     estimator_instance.update(y=y_test, X=X_test)
+
+
+@pytest.mark.parametrize(
+    "forecasters",
+    [
+        [
+            ("trend", TrendForecaster(), ["l1_node01"]),
+            ("polytrend", PolynomialTrendForecaster(), ["l1_node02", "l1_node03"]),
+            ("naive", NaiveForecaster(), ["__total"]),
+        ],
+        [
+            (
+                "trend",
+                TrendForecaster(),
+                [("__total"), ("l1_node01"), ("l1_node02"), ("l1_node03")],
+            ),
+        ],
+    ],
+)
+@pytest.mark.parametrize("default", [NaiveForecaster(), None])
+def test_level_one_data(forecasters, default):
+    "Check for data with one level of hierarchy (excluding timepoints level)."
+    agg = Aggregator()
+
+    y = _bottom_hier_datagen(
+        no_bottom_nodes=3,
+        no_levels=1,
+        random_seed=123,
+    )
+
+    forecaster = HierarchyEnsembleForecaster(forecasters, by="node", default=default)
+
+    forecaster.fit(y, fh=[1, 2, 3])
+    actual_pred = forecaster.predict()
+
+    y = agg.fit_transform(y)
+
+    for i in range(len(forecasters)):
+        test_frcstr = forecasters[i][1].clone()
+        df = y[y.index.droplevel(-1).isin(forecaster.fitted_list[i][1])]
+        test_frcstr.fit(df, fh=[1, 2, 3])
+        test_pred = test_frcstr.predict()
+        msg = "Node predictions do not match"
+        assert np.all(actual_pred.loc[test_pred.index] == test_pred), msg
+
+    _, _, nodes = zip(*forecasters)
+    nodes = set(flatten(nodes))
+    if default is not None and len(nodes) != len(y.index.droplevel(-1).unique()):
+        def_frcstr = default
+        df = y[y.index.droplevel(-1).isin(forecaster.fitted_list[-1][1])]
+        def_frcstr.fit(df, fh=[1, 2, 3])
+        def_pred = def_frcstr.predict()
+        msg = "Node default predictions do not match"
+        assert np.all(actual_pred.loc[def_pred.index] == def_pred), msg
diff --git a/sktime/forecasting/tests/test_all_forecasters.py b/sktime/forecasting/tests/test_all_forecasters.py
index 3bff7ad35..07929a18e 100644
--- a/sktime/forecasting/tests/test_all_forecasters.py
+++ b/sktime/forecasting/tests/test_all_forecasters.py
@@ -383,7 +383,7 @@ class TestAllForecasters(ForecasterFixtureGenerator, QuickTester):
 
         found = pred_ints.columns.to_flat_index()
         msg = (
-            "columns of returned prediction interval DataFrame do not"
+            "columns of returned prediction interval DataFrame do not "
             f"match up with expected columns. Expected: {expected},"
             f"found: {found}"
         )