Repository: sktime/sktime
Pull request: [ENH] Add DummyClassifier
Fixes #2507.

#### What does this implement/fix? Explain your changes.

Adds a DummyClassifier that wraps over a sklearn DummyClassifier and inherits from BaseClassifier.

#### Does your contribution introduce a new dependency? If yes, which one?

No

#### PR checklist

##### For all contributions
- [x] I've added myself to the [list of contributors](https://github.com/alan-turing-institute/sktime/blob/main/.all-contributorsrc).
- [x] Optionally, I've updated sktime's [CODEOWNERS](https://github.com/alan-turing-institute/sktime/blob/main/CODEOWNERS) to receive notifications about future changes to these files.
- [x] I've added unit tests and made sure they pass locally.
- [x] The PR title starts with either [ENH], [DOC] or [BUG] indicating whether the PR topic is related to enhancement, documentation or bug


Problem statement:
[ENH] DummyClassifier
We should implement a `DummyClassifier`, similar to the one in `sklearn`.
Functionality can be equivalent, except that it should inherit from `BaseClassifier` in `sktime`.

Suggestion: impot `sklearn` `DummyClassifier`, loop through all the inputs and loop through `y`, create some 2D `X` for the `sklearn` classifier which will be ignored anyway.

Hints from issue discussion:
If no one objects I am interested in working on it to get my feet wet. Seems like a great first issue!
Great! Have fun!
Here's the developer guide on adding a new estimator:
https://www.sktime.org/en/stable/developer_guide/add_estimators.html
In this case, we are dealing with a time series classifier, so it should inherit from `BaseClassifier`.

Commit message: Call super().__init__()

Patch to review:
diff --git a/.all-contributorsrc b/.all-contributorsrc
index acc8b08f63a..0aa46f0f0b5 100644
--- a/.all-contributorsrc
+++ b/.all-contributorsrc
@@ -1566,6 +1566,15 @@
       "contributions": [
         "doc"
       ]
+    },
+    {
+      "login": "ZiyaoWei",
+      "name": "Ziyao Wei",
+      "avatar_url": "https://avatars.githubusercontent.com/u/940823?v=4",
+      "profile": "https://github.com/ZiyaoWei",
+      "contributions": [
+        "code"
+      ]
     }
   ],
   "projectName": "sktime",
diff --git a/CODEOWNERS b/CODEOWNERS
index 874f824d1b1..067ddb75ed4 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -3,6 +3,7 @@
 
 * @fkiraly @aiwalter @TonyBagnall
 
+sktime/classification/dummy/ @ZiyaoWei
 sktime/classification/dictionary_based/ @patrickzib @MatthewMiddlehurst @TonyBagnall
 sktime/classification/distance_based/ @jasonlines @goaster @TonyBagnall
 sktime/classification/early_classification/ @MatthewMiddlehurst @TonyBagnall
diff --git a/docs/source/api_reference/classification.rst b/docs/source/api_reference/classification.rst
index 984bfb9e5db..94445c14914 100644
--- a/docs/source/api_reference/classification.rst
+++ b/docs/source/api_reference/classification.rst
@@ -48,6 +48,17 @@ Distance-based
     ProximityTree
     ProximityStump
 
+Dummy
+--------------
+
+.. currentmodule:: sktime.classification.dummy
+
+.. autosummary::
+    :toctree: auto_generated/
+    :template: class.rst
+
+    DummyClassifier
+
 Hybrid
 --------------
 
diff --git a/sktime/classification/dummy/__init__.py b/sktime/classification/dummy/__init__.py
new file mode 100644
index 00000000000..e424775173c
--- /dev/null
+++ b/sktime/classification/dummy/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+"""Dummy classifier."""
+__all__ = [
+    "DummyClassifier",
+]
+
+from sktime.classification.dummy._dummy import DummyClassifier
diff --git a/sktime/classification/dummy/_dummy.py b/sktime/classification/dummy/_dummy.py
new file mode 100644
index 00000000000..81295ef4d71
--- /dev/null
+++ b/sktime/classification/dummy/_dummy.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+"""Dummy time series classifier."""
+
+__author__ = ["ZiyaoWei"]
+__all__ = ["DummyClassifier"]
+
+import numpy as np
+from sklearn.dummy import DummyClassifier as SklearnDummyClassifier
+
+from sktime.classification.base import BaseClassifier
+
+
+class DummyClassifier(BaseClassifier):
+    """DummyClassifier makes predictions that ignore the input features.
+
+    This classifier serves as a simple baseline to compare against other more
+    complex classifiers, similar to sklearn.dummy.DummyClassifier.
+    """
+
+    _tags = {
+        "capability:missing_values": True,
+    }
+
+    def __init__(self, strategy="prior", random_state=None, constant=None):
+        self.strategy = strategy
+        self.random_state = random_state
+        self.constant = constant
+        self.sklearn_dummy_classifier = SklearnDummyClassifier(
+            strategy=strategy, random_state=random_state, constant=constant
+        )
+        super(DummyClassifier, self).__init__()
+
+    def _fit(self, X, y):
+        """Fit the dummy classifier.
+
+        Parameters
+        ----------
+        X : sktime-format pandas dataframe with shape(n,d),
+        or numpy ndarray with shape(n,d,m)
+        y : array-like, shape = [n_instances] - the class labels
+
+        Returns
+        -------
+        self : reference to self.
+        """
+        self.sklearn_dummy_classifier.fit(X, y)
+        return self
+
+    def _predict(self, X) -> np.ndarray:
+        """Predict labels for sequences in X.
+
+        Parameters
+        ----------
+        X : sktime-format pandas dataframe or array-like, shape (n, d)
+
+        Returns
+        -------
+        y : predictions of labels for X, np.ndarray
+        """
+        return self.sklearn_dummy_classifier.predict(X)
+
+    def _predict_proba(self, X) -> np.ndarray:
+        """Predicts labels probabilities for sequences in X.
+
+        Parameters
+        ----------
+        X : sktime-format pandas dataframe or array-like, shape (n, d)
+
+        Returns
+        -------
+        y : predictions of probabilities for class values of X, np.ndarray
+        """
+        return self.sklearn_dummy_classifier.predict_proba(X)
diff --git a/sktime/classification/dummy/tests/__init__.py b/sktime/classification/dummy/tests/__init__.py
new file mode 100644
index 00000000000..4de3d2c4eee
--- /dev/null
+++ b/sktime/classification/dummy/tests/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+"""Dummy classifier test code."""
diff --git a/sktime/classification/dummy/tests/test_dummy.py b/sktime/classification/dummy/tests/test_dummy.py
new file mode 100644
index 00000000000..93620278ef3
--- /dev/null
+++ b/sktime/classification/dummy/tests/test_dummy.py
@@ -0,0 +1,18 @@
+# -*- coding: utf-8 -*-
+"""Test function of DummyClassifier."""
+import numpy as np
+
+from sktime.classification.dummy import DummyClassifier
+from sktime.datasets import load_unit_test
+
+
+def test_dummy_classifier():
+    """Test function for DummyClassifier."""
+    X_train, y_train = load_unit_test(split="train", return_type="numpy3D")
+    X_test, _ = load_unit_test(split="test", return_type="numpy3D")
+    dummy = DummyClassifier()
+    dummy.fit(X_train, y_train)
+    pred = dummy.predict(X_test)
+    assert all(i == "1" for i in pred)
+    pred_proba = dummy.predict_proba(X_test)
+    assert all(np.array_equal([0.5, 0.5], i) for i in pred_proba)


Code review comments:
diff_hunk: @@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+"""Dummy time series classifier."""
+
+__author__ = ["ZiyaoWei"]
+__all__ = ["DummyClassifier"]
+
+import numpy as np
+from sklearn.dummy import DummyClassifier as SklearnDummyClassifier
+
+from sktime.classification.base import BaseClassifier
+
+
+class DummyClassifier(BaseClassifier):
+    """DummyClassifier makes predictions that ignore the input features.
+
+    This classifier serves as a simple baseline to compare against other more
+    complex classifiers, similar to sklearn.dummy.DummyClassifier.
+    """
+
+    _tags = {
+        "capability:missing_values": True,
+    }
+
+    def __init__(self, strategy="prior", random_state=None, constant=None):
+        self.strategy = strategy
+        self.random_state = random_state
+        self.constant = constant
+        self.sklearn_dummy_classifier = SklearnDummyClassifier(
+            strategy=strategy, random_state=random_state, constant=constant
+        )
+        super(DummyClassifier, self).__init__()
+
+    def _fit(self, X, y):
+        """Fit the dummy classifier.
+
+        Parameters
+        ----------
+        X : sktime-format pandas dataframe with shape(n,d),
+        or numpy ndarray with shape(n,d,m)
+        y : array-like, shape = [n_instances] - the class labels
+
+        Returns
+        -------
+        self : reference to self.
+        """
+        self.sklearn_dummy_classifier.fit(X, y)
path: sktime/classification/dummy/_dummy.py
text: @user1:
I would just throw away `X` and create anything with equal length. The `sklearn` `DummyClassifier` ignores it anyway.

@author:
Thanks! I thought this has the advantage of not having to create anything so it's more efficient, but I guess that would be unnecessary optimization and creating a dummy value makes the code more readable - will change.

@user1:
well, the problem is that you don't get `X` in a `scikit-learn` compatible format if you want it to support unequal length. So you need to convert it, or create it.

diff_hunk: @@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+"""Dummy time series classifier."""
+
+__author__ = ["ZiyaoWei"]
+__all__ = ["DummyClassifier"]
+
+import numpy as np
+from sklearn.dummy import DummyClassifier as SklearnDummyClassifier
+
+from sktime.classification.base import BaseClassifier
+
+
+class DummyClassifier(BaseClassifier):
+    """DummyClassifier makes predictions that ignore the input features.
+
+    This classifier serves as a simple baseline to compare against other more
+    complex classifiers, similar to sklearn.dummy.DummyClassifier.
+    """
+
+    _tags = {
+        "capability:missing_values": True,
path: sktime/classification/dummy/_dummy.py
text: @user1:
the `DummyClassifier` should also be given capability for unequal length - after all, `X` is ignored!

I would further also ensure we convert to `pd-multiindex` and/or `nested_univ`, since we want to be able to deal with unequal length series (`X_inner_mtype` tag)

@author:
I had this initially but weren't sure how to make it work, and later forgot to add it back. Thanks for the pointers!

diff_hunk: @@ -48,6 +48,17 @@ Distance-based
     ProximityTree
     ProximityStump
 
+Dummy
+--------------
path: docs/source/api_reference/classification.rst
text: @user1:
length should be equal to string length in line before.
Kindly also fix this for `Hybrid`, directly below, while we are at it.