Repository: sktime/sktime

Issue:
[ENH] refactor BaseColumnEnsembleClassifier and ColumnEnsembleClassifier to comply with the new fit/predict/predict_proba model
**Is your feature request related to a problem? Please describe.**

location: classification.compose._column_ensemble.py

these classifiers need to conform to the new fit, predict, prdict_proba model, but it looks non-trivial. fit does not currently call check_X, but seems to do some of its own bespoke formatting (_validate_column_callables). It also inherits from _HeterogenousMetaEstimator which may complicate things.  I also think there are no unit tests for this classifier. I'm not sure why it is using a label encoder in fit, other classifiers dont. Also not sure why it needs a base class, which is only accessible through the private file (i.e. not in init). 

**Describe the solution you'd like**
We could just wrap this classifier. Should add unit tests.

**Describe alternatives you've considered**
We could deprecate this?

**Additional context**
in the general refactor of classifier base class. 


Discussion:

Resolving patch:
diff --git a/sktime/base/_meta.py b/sktime/base/_meta.py
--- a/sktime/base/_meta.py
+++ b/sktime/base/_meta.py
@@ -14,7 +14,7 @@
 class _HeterogenousMetaEstimator(BaseEstimator, metaclass=ABCMeta):
     """Handles parameter management for estimtators composed of named estimators.
 
-    From sklearn utils.metaestimator.py
+    From sklearn utils.metaestimator.py.
     """
 
     def get_params(self, deep=True):
diff --git a/sktime/classification/compose/_column_ensemble.py b/sktime/classification/compose/_column_ensemble.py
--- a/sktime/classification/compose/_column_ensemble.py
+++ b/sktime/classification/compose/_column_ensemble.py
@@ -19,6 +19,12 @@
 
 
 class BaseColumnEnsembleClassifier(BaseClassifier, _HeterogenousMetaEstimator):
+    """Base Class for column ensemble."""
+
+    _tags = {
+        "capability:multivariate": True,
+    }
+
     def __init__(self, estimators, verbose=False):
         self.verbose = verbose
         self.estimators = estimators
@@ -114,7 +120,7 @@ def _iter(self, replace_strings=False):
 
             yield name, estimator, column
 
-    def fit(self, X, y):
+    def _fit(self, X, y):
         # the data passed in could be an array of dataframes?
         """Fit all estimators, fit the data.
 
@@ -163,13 +169,13 @@ def _collect_probas(self, X):
             ]
         )
 
-    def predict_proba(self, X):
+    def _predict_proba(self, X):
         """Predict class probabilities for X using 'soft' voting."""
         self.check_is_fitted()
         avg = np.average(self._collect_probas(X), axis=0)
         return avg
 
-    def predict(self, X):
+    def _predict(self, X):
         maj = np.argmax(self.predict_proba(X), axis=1)
         return self.le_.inverse_transform(maj)
 
@@ -213,6 +219,19 @@ class ColumnEnsembleClassifier(BaseColumnEnsembleClassifier):
         By setting ``remainder`` to be an estimator, the remaining
         non-specified columns will use the ``remainder`` estimator. The
         estimator must support `fit` and `transform`.
+
+    Examples
+    --------
+    >>> from sktime.classification.interval_based import DrCIF
+    >>> from sktime.datasets import load_basic_motions
+    >>> X_train, y_train = load_basic_motions(split="train")
+    >>> X_test, y_test = load_basic_motions(split="test")
+    >>> clf = DrCIF(n_estimators=10)
+    >>> estimators = [("DrCIF", clf, [0, 1, 2, 3, 4, 5])]
+    >>> col_ens = ColumnEnsembleClassifier(estimators=estimators)
+    >>> col_ens.fit(X_train, y_train)
+    ColumnEnsembleClassifier(...)
+    >>> y_pred = col_ens.predict(X_test)
     """
 
     _required_parameters = ["estimators"]
diff --git a/sktime/classification/dictionary_based/_muse.py b/sktime/classification/dictionary_based/_muse.py
--- a/sktime/classification/dictionary_based/_muse.py
+++ b/sktime/classification/dictionary_based/_muse.py
@@ -106,7 +106,7 @@ class MUSE(BaseClassifier):
     _tags = {
         "capability:multivariate": True,
         "capability:multithreading": True,
-        "X_inner_mtype": "nested_univ",  # which type do _fit/_predict, support for X?
+        "X_inner_mtype": "nested_univ",  # MUSE requires nested datafrane
     }
 
     def __init__(


Test patch:
diff --git a/sktime/classification/compose/tests/test_column_ensemble.py b/sktime/classification/compose/tests/test_column_ensemble.py
--- a/sktime/classification/compose/tests/test_column_ensemble.py
+++ b/sktime/classification/compose/tests/test_column_ensemble.py
@@ -1,3 +1,133 @@
 # -*- coding: utf-8 -*-
 """Column ensemble test code."""
 __author__ = ["TonyBagnall"]
+
+
+import numpy as np
+from numpy import testing
+
+from sktime.classification.compose import ColumnEnsembleClassifier
+from sktime.classification.dictionary_based import TemporalDictionaryEnsemble
+from sktime.classification.feature_based import FreshPRINCE
+from sktime.classification.interval_based import DrCIF
+from sktime.datasets import load_basic_motions, load_unit_test
+
+
+def test_col_ens_on_basic_motions():
+    """Test of ColumnEnsembleClassifier on basic motions data."""
+    # load basic motions data
+    X_train, y_train = load_basic_motions(split="train")
+    X_test, y_test = load_basic_motions(split="test")
+    indices = np.random.RandomState(4).choice(len(y_train), 10, replace=False)
+    fp = FreshPRINCE(
+        random_state=0,
+        default_fc_parameters="minimal",
+        n_estimators=10,
+    )
+    tde = TemporalDictionaryEnsemble(
+        n_parameter_samples=10,
+        max_ensemble_size=5,
+        randomly_selected_params=5,
+        random_state=0,
+    )
+    drcif = DrCIF(n_estimators=10, random_state=0, save_transformed_data=True)
+    estimators = [
+        ("FreshPrince", fp, [0, 1, 2]),
+        ("TDE", tde, [3, 4]),
+        ("DrCIF", drcif, [5]),
+    ]
+
+    # train column ensemble
+    col_ens = ColumnEnsembleClassifier(estimators=estimators)
+    col_ens.fit(X_train, y_train)
+    # preds = col_ens.predict(X_test.iloc[indices])
+
+    # assert preds[0] == 2
+    # assert probabilities are the same
+    probas = col_ens.predict_proba(X_test.iloc[indices])
+    testing.assert_array_almost_equal(probas, col_ens_basic_motions_probas, decimal=2)
+
+
+def test_col_ens_on_unit_test_data():
+    """Test of ColumnEnsembleClassifier on unit test data."""
+    # load unit test data
+    X_train, y_train = load_unit_test(split="train")
+    X_test, y_test = load_unit_test(split="test")
+    indices = np.random.RandomState(0).choice(len(y_train), 10, replace=False)
+
+    # train Column ensemble with a single
+    fp = FreshPRINCE(
+        random_state=0,
+        default_fc_parameters="minimal",
+        n_estimators=10,
+    )
+    estimators = [("FreshPrince", fp, [0])]
+    col_ens = ColumnEnsembleClassifier(estimators=estimators)
+    col_ens.fit(X_train, y_train)
+    # preds = col_ens.predict(X_test.iloc[indices])
+
+    # assert preds[0] == 2
+    # assert probabilities are the same
+    probas = col_ens.predict_proba(X_test.iloc[indices])
+
+    testing.assert_array_almost_equal(probas, col_ens_unit_test_probas, decimal=2)
+
+
+col_ens_unit_test_probas = np.array(
+    [
+        [
+            0.2,
+            0.8,
+        ],
+        [
+            1.0,
+            0.0,
+        ],
+        [
+            0.1,
+            0.9,
+        ],
+        [
+            1.0,
+            0.0,
+        ],
+        [
+            0.9,
+            0.1,
+        ],
+        [
+            1.0,
+            0.0,
+        ],
+        [
+            0.9,
+            0.1,
+        ],
+        [
+            0.8,
+            0.2,
+        ],
+        [
+            0.9,
+            0.1,
+        ],
+        [
+            1.0,
+            0.0,
+        ],
+    ]
+)
+col_ens_basic_motions_probas = np.array(
+    [
+        [0.00000, 0.07885, 0.03333, 0.88781],
+        [0.90585, 0.00000, 0.00000, 0.09415],
+        [0.00000, 0.09681, 0.82433, 0.07885],
+        [0.06667, 0.67615, 0.22385, 0.03333],
+        [0.00000, 0.00000, 0.06397, 0.93603],
+        [0.00000, 0.03063, 0.26667, 0.70270],
+        [0.72748, 0.03333, 0.06352, 0.17567],
+        [0.00000, 0.00000, 0.88781, 0.11219],
+        [0.00000, 0.76082, 0.16033, 0.07885],
+        [0.00000, 0.82429, 0.06352, 0.11219],
+    ]
+)