Repository: sktime/sktime

Issue:
[BUG] Ensemble forecaster fails on multivariate data
**Describe the bug**

The `EnsembleForecaster` fail when ensembling multivariate forecasting models. 

This is because `EnsembleForecaster` has the `"scitype:y": "univariate"` tag which means that the ensemble forecaster is fitted on each column separately. But multivariate forecasting models such as VAR assume that all columns are passed.

**To Reproduce**
<!--
Add a Minimal, Complete, and Verifiable example (for more details, see e.g. https://stackoverflow.com/help/mcve

If the code is too long, feel free to put it in a public gist and link it in the issue: https://gist.github.com
-->

```python
from sktime.forecasting.compose import EnsembleForecaster
from sktime.forecasting.exp_smoothing import ExponentialSmoothing
from sktime.datasets import load_longley
from sktime.forecasting.var import VAR

# load data
_, y = load_longley()

# run ensembler
forecaster = EnsembleForecaster(
    forecasters=[("var1", VAR()), ("var2", VAR())],
    aggfunc="mean"
)
forecaster.fit(y)
forecaster.predict(fh=[1,2,3])
```

**Expected behavior**

The ensemble forecaster should ensemble all models without any failure.

**Additional context**
<!--
Add any other context about the problem here.
-->

**Versions**
```raw
System:
    python: 3.8.9 (default, Oct 16 2021, 12:00:06)  [Clang 12.0.0 (clang-1200.0.32.29)]
executable: /Users/anhoang/Scratchpad/.venv/bin/python
   machine: macOS-12.5.1-x86_64-i386-64bit

Python dependencies:
          pip: 22.3
   setuptools: 49.2.1
      sklearn: 1.1.2
       sktime: 0.13.4
  statsmodels: 0.13.2
        numpy: 1.22.4
        scipy: 1.8.1
       pandas: 1.5.1
   matplotlib: None
       joblib: 1.2.0
        numba: 0.56.3
     pmdarima: None
      tsfresh: None
```

<!--
Please run the following code snippet and paste the output here:
 
from sktime import show_versions; show_versions()
-->

</details>

<!-- Thanks for contributing! -->



Discussion:

Resolving patch:
diff --git a/sktime/forecasting/compose/_ensemble.py b/sktime/forecasting/compose/_ensemble.py
--- a/sktime/forecasting/compose/_ensemble.py
+++ b/sktime/forecasting/compose/_ensemble.py
@@ -7,7 +7,7 @@
 combination of the predictions from underlying forecasts.
 """
 
-__author__ = ["mloning", "GuzalBulatova", "aiwalter", "RNKuhns"]
+__author__ = ["mloning", "GuzalBulatova", "aiwalter", "RNKuhns", "AnH0ang"]
 __all__ = ["EnsembleForecaster", "AutoEnsembleForecaster"]
 
 import numpy as np
@@ -304,7 +304,8 @@ class EnsembleForecaster(_HeterogenousEnsembleForecaster):
         "ignores-exogeneous-X": False,
         "requires-fh-in-fit": False,
         "handles-missing-data": False,
-        "scitype:y": "univariate",
+        "y_inner_mtype": ["pd.DataFrame", "pd-multiindex", "pd_multiindex_hier"],
+        "scitype:y": "both",
     }
 
     def __init__(self, forecasters, n_jobs=None, aggfunc="mean", weights=None):
@@ -345,9 +346,11 @@ def _predict(self, fh, X=None):
         y_pred : pd.Series
             Aggregated predictions.
         """
-        y_pred = pd.concat(self._predict_forecasters(fh, X), axis=1)
-        y_pred = _aggregate(y=y_pred, aggfunc=self.aggfunc, weights=self.weights)
-
+        names, _ = self._check_forecasters()
+        y_pred = pd.concat(self._predict_forecasters(fh, X), axis=1, keys=names)
+        y_pred = y_pred.groupby(level=1, axis=1).agg(
+            _aggregate, self.aggfunc, self.weights
+        )
         return y_pred
 
     @classmethod
diff --git a/sktime/forecasting/online_learning/_online_ensemble.py b/sktime/forecasting/online_learning/_online_ensemble.py
--- a/sktime/forecasting/online_learning/_online_ensemble.py
+++ b/sktime/forecasting/online_learning/_online_ensemble.py
@@ -28,6 +28,8 @@ class OnlineEnsembleForecaster(EnsembleForecaster):
         "ignores-exogeneous-X": True,
         "requires-fh-in-fit": False,
         "handles-missing-data": False,
+        "y_inner_mtype": ["pd.Series"],
+        "scitype:y": "univariate",
     }
 
     def __init__(self, forecasters, ensemble_algorithm=None, n_jobs=None):
diff --git a/sktime/regression/compose/_ensemble.py b/sktime/regression/compose/_ensemble.py
--- a/sktime/regression/compose/_ensemble.py
+++ b/sktime/regression/compose/_ensemble.py
@@ -3,7 +3,7 @@
 # copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
 """Implements a composite Time series Forest Regressor that accepts a pipeline."""
 
-__author__ = ["Markus Löning", "AyushmaanSeth"]
+__author__ = ["mloning", "AyushmaanSeth"]
 __all__ = ["ComposableTimeSeriesForestRegressor"]
 
 import numbers


Test patch:
diff --git a/sktime/forecasting/compose/tests/test_ensemble.py b/sktime/forecasting/compose/tests/test_ensemble.py
--- a/sktime/forecasting/compose/tests/test_ensemble.py
+++ b/sktime/forecasting/compose/tests/test_ensemble.py
@@ -15,6 +15,7 @@
 from sktime.forecasting.exp_smoothing import ExponentialSmoothing
 from sktime.forecasting.naive import NaiveForecaster
 from sktime.forecasting.trend import PolynomialTrendForecaster
+from sktime.forecasting.var import VAR
 from sktime.utils._testing.forecasting import make_forecasting_problem
 
 
@@ -41,43 +42,63 @@ def test_avg_mean(forecasters):
 
 @pytest.mark.parametrize("aggfunc", [*VALID_AGG_FUNCS.keys()])
 @pytest.mark.parametrize(
-    "forecasters",
+    "forecasters,y",
     [
-        [("trend", PolynomialTrendForecaster()), ("naive", NaiveForecaster())],
+        (
+            [("trend", PolynomialTrendForecaster()), ("naive", NaiveForecaster())],
+            pd.DataFrame(make_forecasting_problem()),
+        ),
+        (
+            [("var", VAR()), ("naive", NaiveForecaster())],
+            make_forecasting_problem(n_columns=3),
+        ),
     ],
 )
-def test_aggregation_unweighted(forecasters, aggfunc):
+def test_aggregation_unweighted(forecasters, y, aggfunc):
     """Assert aggfunc returns the correct values."""
-    y = make_forecasting_problem()
     forecaster = EnsembleForecaster(forecasters=forecasters, aggfunc=aggfunc)
     forecaster.fit(y, fh=[1, 2, 3])
     actual_pred = forecaster.predict()
 
     predictions = []
+
     _aggfunc = VALID_AGG_FUNCS[aggfunc]["unweighted"]
     for _, forecaster in forecasters:
         f = forecaster
         f.fit(y)
         f_pred = f.predict(fh=[1, 2, 3])
         predictions.append(f_pred)
-    predictions = pd.DataFrame(predictions).T
-    expected_pred = predictions.apply(func=_aggfunc, axis=1)
 
-    pd.testing.assert_series_equal(actual_pred, expected_pred)
+    expected_pred = pd.DataFrame()
+    for col in predictions[0].columns:
+        column_preds = pd.concat([p[col] for p in predictions], axis=1)
+        expected_pred.loc[:, col] = pd.Series(
+            _aggfunc(column_preds, axis=1),
+            index=column_preds.index,
+        )
+
+    # expected_pred = predictions.apply(func=_aggfunc, axis=1, weights=weights)
+    pd.testing.assert_frame_equal(actual_pred, expected_pred)
 
 
 @pytest.mark.parametrize("aggfunc", [*VALID_AGG_FUNCS.keys()])
 @pytest.mark.parametrize("weights", [[1.44, 1.2]])
 @pytest.mark.parametrize(
-    "forecasters",
+    "forecasters,y",
     [
-        [("trend", PolynomialTrendForecaster()), ("naive", NaiveForecaster())],
+        (
+            [("trend", PolynomialTrendForecaster()), ("naive", NaiveForecaster())],
+            pd.DataFrame(make_forecasting_problem()),
+        ),
+        (
+            [("var", VAR()), ("naive", NaiveForecaster())],
+            make_forecasting_problem(n_columns=3),
+        ),
     ],
 )
 @pytest.mark.skipif(sys.version_info < (3, 7), reason="requires python3.7 or higher")
-def test_aggregation_weighted(forecasters, aggfunc, weights):
+def test_aggregation_weighted(forecasters, y, aggfunc, weights):
     """Assert weighted aggfunc returns the correct values."""
-    y = make_forecasting_problem()
     forecaster = EnsembleForecaster(
         forecasters=forecasters, aggfunc=aggfunc, weights=weights
     )
@@ -90,14 +111,19 @@ def test_aggregation_weighted(forecasters, aggfunc, weights):
         f.fit(y)
         f_pred = f.predict(fh=[1, 2, 3])
         predictions.append(f_pred)
-    predictions = pd.DataFrame(predictions).T
+
     _aggfunc = VALID_AGG_FUNCS[aggfunc]["weighted"]
-    expected_pred = pd.Series(
-        _aggfunc(predictions, axis=1, weights=np.array(weights)),
-        index=predictions.index,
-    )
+
+    expected_pred = pd.DataFrame()
+    for col in predictions[0].columns:
+        column_preds = pd.concat([p[col] for p in predictions], axis=1)
+        expected_pred.loc[:, col] = pd.Series(
+            _aggfunc(column_preds, axis=1, weights=np.array(weights)),
+            index=column_preds.index,
+        )
+
     # expected_pred = predictions.apply(func=_aggfunc, axis=1, weights=weights)
-    pd.testing.assert_series_equal(actual_pred, expected_pred)
+    pd.testing.assert_frame_equal(actual_pred, expected_pred)
 
 
 @pytest.mark.parametrize("aggfunc", ["miin", "maximum", ""])