Repository: sktime/sktime

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

Comments:

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}"
         )