Repository: nilearn/nilearn

Issue:
Add "dummy" option as an estimator in the Decoder / DecoderRegressor
<!--
Hi! 
If you have: 
 -1 Questions about how to use Nilearn or 
 -2 Need analysis suggestions & recommendations?

A bunch of fMRI researchers hang out at Neurostars (http://neurostars.org/). 
Post those questions there. 
Add the tag `nilearn` (we get an email from Neurostars if you do).

Posting them here makes life more complicated for the Nilearn developers.  
-->

<!-- 
For the Feature Request,
Include the following:
------------------------
What would you like changed/added and why?
What would be the benefit? Does the change make something easier to use?
Clarifies something?
If it is a new feature, what is the benefit? 
-->

1 - Accept "dummy" with smart default in the Decoder / DecoderRegressor to have an API consistent way to test chance for the decoding problem at hand

2 - Add in section 2.1 of the userguide an Introduction and example plot_intro_decoding a mention of this and a comparaison of results to chance in the haxby case



Discussion:
great idea!
> 1 - Accept "dummy" with smart default in the Decoder / DecoderRegressor to
> have an API consistent way to test chance for the decoding problem at hand

Can you elaborate more on this point?
> > 1 - Accept "dummy" with smart default in the Decoder / DecoderRegressor to
> > have an API consistent way to test chance for the decoding problem at hand
> 
> Can you elaborate more on this point?

Really just make needed tradeoffs to have a reasonable baseline for chance in our current examples.

Specifics I can think of :
- add "dummy" to the list of acceptable keyword
- when it's passed instantiate DummyClassifier / DummyRegressor with suitable strategy and parameter (but I think sklearn default will be a great start)
- find how it would work to fit this dummy as is, for example through the current _parallel_fit() function I think it should work
- I think this can override potential conflict with screening_percentile, param_grid.

Resolving patch:
diff --git a/examples/02_decoding/plot_haxby_full_analysis.py b/examples/02_decoding/plot_haxby_full_analysis.py
--- a/examples/02_decoding/plot_haxby_full_analysis.py
+++ b/examples/02_decoding/plot_haxby_full_analysis.py
@@ -59,13 +59,11 @@
 from nilearn.decoding import Decoder
 
 # Make a data splitting object for cross validation
-from sklearn.model_selection import LeaveOneGroupOut, cross_val_score
+from sklearn.model_selection import LeaveOneGroupOut
 cv = LeaveOneGroupOut()
 
 ##############################################################
-# We use :class:`sklearn.dummy.DummyClassifier` as a baseline.
-from sklearn.dummy import DummyClassifier
-dummy_classifier = DummyClassifier()
+# We use :class:`nilearn.decoding.Decoder` to estimate a baseline.
 
 mask_names = ['mask_vt', 'mask_face', 'mask_house']
 
@@ -77,7 +75,6 @@
     # For decoding, standardizing is often very important
     mask_filename = haxby_dataset[mask_name][0]
     masker = NiftiMasker(mask_img=mask_filename, standardize=True)
-    masked_timecourses = masker.fit_transform(func_filename)[task_mask]
     mask_scores[mask_name] = {}
     mask_chance_scores[mask_name] = {}
 
@@ -95,14 +92,11 @@
               np.mean(mask_scores[mask_name][category]),
               np.std(mask_scores[mask_name][category])))
 
-        mask_chance_scores[mask_name][category] = cross_val_score(
-            dummy_classifier,
-            masked_timecourses,
-            classification_target,
-            cv=cv,
-            groups=session_labels,
-            scoring="roc_auc",
-        )
+        dummy_classifier = Decoder(estimator='dummy_classifier', cv=cv,
+                                   mask=masker, scoring='roc_auc')
+        dummy_classifier.fit(task_data, classification_target,
+                             groups=session_labels)
+        mask_chance_scores[mask_name][category] = dummy_classifier.cv_scores_[1]
 
 
 ##########################################################################
diff --git a/examples/plot_decoding_tutorial.py b/examples/plot_decoding_tutorial.py
--- a/examples/plot_decoding_tutorial.py
+++ b/examples/plot_decoding_tutorial.py
@@ -300,6 +300,25 @@
     title="SVM weights", dim=-1
 )
 
+###########################################################################
+# What is the chance level accuracy?
+# ----------------------------------
+#
+# Does the model above perform better than chance?
+# To answer this question, we measure a score at random using simple strategies
+# that are implemented in the :class:`nilearn.decoding.Decoder` object. This is
+# useful to inspect the decoding performance by comparing to a score at chance.
+
+###########################################################################
+# Let's define a object with Dummy estimator replacing 'svc' for classification
+# setting. This object initializes estimator with default dummy strategy.
+dummy_decoder = Decoder(estimator='dummy_classifier', mask=mask_filename,
+                        cv=cv)
+dummy_decoder.fit(fmri_niimgs, conditions, groups=session_label)
+
+# Now, we can compare these scores by simply taking a mean over folds
+print(dummy_decoder.cv_scores_)
+
 ###########################################################################
 # Further reading
 # ----------------
diff --git a/nilearn/decoding/decoder.py b/nilearn/decoding/decoder.py
--- a/nilearn/decoding/decoder.py
+++ b/nilearn/decoding/decoder.py
@@ -22,11 +22,13 @@
 from sklearn.linear_model import LogisticRegression
 from sklearn.linear_model import LinearRegression
 from sklearn.linear_model import RidgeClassifierCV, RidgeCV
+from sklearn.dummy import DummyRegressor, DummyClassifier
 from sklearn.model_selection import (LeaveOneGroupOut, ParameterGrid,
                                      ShuffleSplit, StratifiedShuffleSplit,
                                      check_cv)
 from sklearn.preprocessing import LabelBinarizer
 from sklearn.svm import SVR, LinearSVC, l1_min_c
+from sklearn.utils import check_random_state
 from sklearn.utils.extmath import safe_sparse_dot
 from sklearn.utils.validation import check_is_fitted, check_X_y
 
@@ -54,6 +56,9 @@
     ridge_regressor=RidgeCV(),
     ridge=RidgeCV(),
     svr=SVR(kernel='linear', max_iter=1e4),
+    dummy_classifier = DummyClassifier(strategy='prior',
+                                       random_state=0),
+    dummy_regressor = DummyRegressor(strategy='mean'),
 )
 
 
@@ -89,10 +94,13 @@ def _check_param_grid(estimator, X, y, param_grid=None):
         useful to avoid exploring parameter combinations that make no sense
         or have no effect. See scikit-learn documentation for more information.
 
+        For Dummy estimators, parameter grid defaults to empty as these
+        estimators do not have hyperparameters to grid search.
+
     Returns
     -------
     param_grid: dict of str to sequence, or sequence of such. Sensible default
-    dict has size 1.
+    dict has size 1 for linear models.
 
     """
     if param_grid is None:
@@ -103,6 +111,12 @@ def _check_param_grid(estimator, X, y, param_grid=None):
         elif isinstance(estimator,
                         (LinearSVC, RidgeCV, RidgeClassifierCV, SVR)):
             loss = 'squared_hinge'
+        elif isinstance(estimator,
+                        (DummyClassifier, DummyRegressor)):
+            if estimator.strategy in ['constant', 'uniform']:
+                message = ('Dummy classification implemented only for strategies'
+                           ' "most_frequent", "prior", "stratified"')
+                raise NotImplementedError(message)
         else:
             raise ValueError(
                 "Invalid estimator. The supported estimators are: {}".format(
@@ -114,7 +128,8 @@ def _check_param_grid(estimator, X, y, param_grid=None):
         else:
             min_c = 0.5
 
-        if not isinstance(estimator, (RidgeCV, RidgeClassifierCV)):
+        if not isinstance(estimator, (RidgeCV, RidgeClassifierCV,
+                                      DummyClassifier, DummyRegressor)):
             param_grid['C'] = np.array([2, 20, 200]) * min_c
         else:
             param_grid = {}
@@ -176,7 +191,7 @@ def _parallel_fit(estimator, X, y, train, test, param_grid, is_classification,
 
         if is_classification:
             score = scorer(estimator, X_test, y_test)
-            if np.all(estimator.coef_ == 0):
+            if hasattr(estimator, 'coef_') and np.all(estimator.coef_ == 0):
                 score = 0
         else:  # regression
             score = scorer(estimator, X_test, y_test)
@@ -184,17 +199,26 @@ def _parallel_fit(estimator, X, y, train, test, param_grid, is_classification,
         # Store best parameters and estimator coefficients
         if (best_score is None) or (score >= best_score):
             best_score = score
-            best_coef = np.reshape(estimator.coef_, (1, -1))
-            best_intercept = estimator.intercept_
+            if hasattr(estimator, 'coef_'):
+                best_coef = np.reshape(estimator.coef_, (1, -1))
+                best_intercept = estimator.intercept_
+                dummy_output = None
+            else:
+                best_coef, best_intercept = None, None
+                if isinstance(estimator, DummyClassifier):
+                    dummy_output = estimator.class_prior_
+                elif isinstance(estimator, DummyRegressor):
+                    dummy_output = estimator.constant_
             best_param = param
 
-    if do_screening:
-        best_coef = selector.inverse_transform(best_coef)
+    if best_coef is not None:
+        if do_screening:
+            best_coef = selector.inverse_transform(best_coef)
 
-    if clustering_percentile < 100:
-        best_coef = clustering.inverse_transform(best_coef)
+        if clustering_percentile < 100:
+            best_coef = clustering.inverse_transform(best_coef)
 
-    return class_index, best_coef, best_intercept, best_param, best_score
+    return class_index, best_coef, best_intercept, best_param, best_score, dummy_output
 
 
 class _BaseDecoder(LinearRegression, CacheMixin):
@@ -214,8 +238,10 @@ class _BaseDecoder(LinearRegression, CacheMixin):
         The estimator to choose among: 'svc', 'svc_l2', 'svc_l1', 'logistic',
         'logistic_l1', 'logistic_l2', 'ridge', 'ridge_classifier',
         'ridge_regressor', and 'svr'. Note that the 'svc' and 'svc_l2';
-        'logistic' and 'logistic_l2'; 'ridge' and 'ridge_regressor'
-        correspond to the same estimator. Default 'svc'.
+        'logistic' and 'logistic_l2'; 'ridge' and 'ridge_regressor',
+        correspond to the same estimator.
+        Dummy estimators are named as 'dummy_classifier', 'dummy_regressor'.
+        Default 'svc'.
 
     mask: filename, Nifti1Image, NiftiMasker, or MultiNiftiMasker, optional
         Mask to be used on data. If an instance of masker is passed,
@@ -239,6 +265,8 @@ class _BaseDecoder(LinearRegression, CacheMixin):
         or have no effect. See scikit-learn documentation for more information,
         for example: https://scikit-learn.org/stable/modules/grid_search.html
 
+        For Dummy estimators, parameter grid defaults to empty dictionary.
+
     clustering_percentile: int, float, optional, in the [0, 100] Default: 10.
         Percentile of features to keep after clustering. If it is lower
         than 100, a ReNA clustering is performed as a first step of fit
@@ -403,15 +431,17 @@ def fit(self, X, y, groups=None):
 
         `coef_` : numpy.ndarray, shape=(n_classes, n_features)
             Contains the mean of the models weight vector across
-            fold for each class.
+            fold for each class. Returns None for Dummy estimators.
 
         `coef_img_` : dict of Nifti1Image
             Dictionary containing `coef_` with class names as keys,
             and `coef_` transformed in Nifti1Images as values. In the case of
             a regression, it contains a single Nifti1Image at the key 'beta'.
+            Ignored if Dummy estimators are provided.
 
         `intercept_` : ndarray, shape (nclasses,)
             Intercept (a.k.a. bias) added to the decision function.
+            Ignored if Dummy estimators are provided.
 
         `cv_` : list of pairs of lists
             List of the (n_folds,) folds. For the corresponding fold,
@@ -422,18 +452,31 @@ def fit(self, X, y, groups=None):
             Contains the standard deviation of the models weight vector across
             fold for each class. Note that folds are not independent, see
             https://scikit-learn.org/stable/modules/cross_validation.html#cross-validation-iterators-for-grouped-data
+            Ignored if Dummy estimators are provided.
 
         `std_coef_img_` : dict of Nifti1Image
             Dictionary containing `std_coef_` with class names as keys,
             and `coef_` transformed in Nifti1Image as values. In the case of
             a regression, it contains a single Nifti1Image at the key 'beta'.
+            Ignored if Dummy estimators are provided.
 
         `cv_params_` : dict of lists
             Best point in the parameter grid for each tested fold
-            in the inner cross validation loop.
+            in the inner cross validation loop. The grid is empty
+            when Dummy estimators are provided.
 
         `cv_scores_` : dict, (classes, n_folds)
             Scores (misclassification) for each parameter, and on each fold
+
+        `n_outputs_` : int
+            Number of outputs (column-wise)
+
+        `dummy_output_`: ndarray, shape=(n_classes, 2) or shape=(1, 1) for regression
+            Contains dummy estimator attributes after class predictions using strategies
+            of DummyClassifier (class_prior) and DummyRegressor (constant)
+            from scikit-learn. This attribute is necessary for estimating class
+            predictions after fit. Returns None if non-dummy estimators are
+            provided.
         """
         self.estimator = _check_estimator(self.estimator)
         self.memory_ = _check_memory(self.memory, self.verbose)
@@ -441,6 +484,11 @@ def fit(self, X, y, groups=None):
         X = self._apply_mask(X)
         X, y = check_X_y(X, y, dtype=np.float, multi_output=True)
 
+        if y.ndim == 1:
+            self.n_outputs_ = 1
+        else:
+            self.n_outputs_ = y.shape[1]
+
         # Setup scorer
         scorer = check_scoring(self.estimator, self.scoring)
 
@@ -511,19 +559,29 @@ def fit(self, X, y, groups=None):
             parallel_fit_outputs, y, n_problems)
 
         # Build the final model (the aggregated one)
-        self.coef_ = np.vstack([np.mean(coefs[class_index], axis=0)
-                                for class_index in self.classes_])
-        self.std_coef_ = np.vstack([np.std(coefs[class_index], axis=0)
+        if not isinstance(self.estimator, (DummyClassifier,
+                                           DummyRegressor)):
+            self.coef_ = np.vstack([np.mean(coefs[class_index], axis=0)
                                     for class_index in self.classes_])
-        self.intercept_ = np.hstack([np.mean(intercepts[class_index], axis=0)
-                                     for class_index in self.classes_])
+            self.std_coef_ = np.vstack([np.std(coefs[class_index], axis=0)
+                                        for class_index in self.classes_])
+            self.intercept_ = np.hstack([np.mean(intercepts[class_index], axis=0)
+                                         for class_index in self.classes_])
 
-        self.coef_img_, self.std_coef_img_ = self._output_image(
-            self.classes_, self.coef_, self.std_coef_)
+            self.coef_img_, self.std_coef_img_ = self._output_image(
+                self.classes_, self.coef_, self.std_coef_)
 
-        if self.is_classification and (self.n_classes_ == 2):
-            self.coef_ = self.coef_[0, :][np.newaxis, :]
-            self.intercept_ = self.intercept_[0]
+            if self.is_classification and (self.n_classes_ == 2):
+                self.coef_ = self.coef_[0, :][np.newaxis, :]
+                self.intercept_ = self.intercept_[0]
+        else:
+            # For Dummy estimators
+            self.coef_ = None
+            self.dummy_output_ = np.vstack([np.mean(self.dummy_output_[class_index], axis=0)
+                                            for class_index in self.classes_])
+            if self.is_classification and (self.n_classes_ == 2):
+                if not self.n_outputs_ > 1:
+                    self.dummy_output_ = self.dummy_output_[0, :][np.newaxis, :]
 
     def decision_function(self, X):
         """Predict class labels for samples in X.
@@ -541,8 +599,6 @@ def decision_function(self, X):
         y_pred: ndarray, shape (n_samples,)
             Predicted class label per sample.
         """
-        X = self.masker_.transform(X)
-
         n_features = self.coef_.shape[1]
         if X.shape[1] != n_features:
             raise ValueError(
@@ -573,6 +629,12 @@ class would be predicted.
         check_is_fitted(self, "coef_")
         check_is_fitted(self, "masker_")
 
+        X = self.masker_.transform(X)
+        n_samples = X.shape[0]
+
+        if isinstance(self.estimator, (DummyClassifier, DummyRegressor)):
+            return self._predict_dummy(n_samples)
+
         scores = self.decision_function(X)
 
         if self.is_classification:
@@ -618,10 +680,11 @@ def _fetch_parallel_fit_outputs(self, parallel_fit_outputs, y, n_problems):
         intercepts = {}
         cv_scores = {}
         self.cv_params_ = {}
+        self.dummy_output_ = {}
         classes = self.classes_
 
         for i, (class_index, coef, intercept, params,
-                scores) in enumerate(parallel_fit_outputs):
+                scores, dummy_output) in enumerate(parallel_fit_outputs):
 
             coefs.setdefault(classes[class_index], []).append(coef)
             intercepts.setdefault(classes[class_index], []).append(intercept)
@@ -629,18 +692,34 @@ def _fetch_parallel_fit_outputs(self, parallel_fit_outputs, y, n_problems):
             cv_scores.setdefault(classes[class_index], []).append(scores)
 
             self.cv_params_.setdefault(classes[class_index], {})
+            if isinstance(self.estimator, (DummyClassifier, DummyRegressor)):
+                self.dummy_output_.setdefault(classes[class_index],
+                                              []).append(dummy_output)
+            else:
+                self.dummy_output_.setdefault(classes[class_index],
+                                              []).append(None)
             for k in params:
                 self.cv_params_[classes[class_index]].setdefault(
                     k, []).append(params[k])
-
+ 
             if (n_problems <= 2) and self.is_classification:
                 # Binary classification
                 other_class = np.setdiff1d(classes, classes[class_index])[0]
-                coefs.setdefault(other_class, []).append(-coef)
-                intercepts.setdefault(other_class, []).append(-intercept)
+                if coef is not None:
+                    coefs.setdefault(other_class, []).append(-coef)
+                    intercepts.setdefault(other_class, []).append(-intercept)
+                else:
+                    coefs.setdefault(other_class, []).append(None)
+                    intercepts.setdefault(other_class, []).append(None)
+
                 cv_scores.setdefault(other_class, []).append(scores)
                 self.cv_params_[other_class] = self.cv_params_[
                     classes[class_index]]
+                if isinstance(self.estimator, (DummyClassifier, DummyRegressor)):
+                    self.dummy_output_.setdefault(other_class,
+                                                  []).append(dummy_output)
+                else:
+                    self.dummy_output_.setdefault(other_class, []).append(None)
 
         self.cv_scores_ = cv_scores
 
@@ -666,6 +745,31 @@ def _binarize_y(self, y):
         self.n_classes_ = len(self.classes_)
         return y
 
+    def _predict_dummy(self, n_samples):
+        """Non-sparse scikit-learn based prediction steps for classification
+           and regression"""
+        dummy_output = self.dummy_output_
+
+        if isinstance(self.estimator, DummyClassifier):
+            estimator_params = self.estimator.get_params()
+            strategy = estimator_params['strategy']
+            if strategy in ['most_frequent', 'prior']:
+                scores = np.tile([dummy_output[k].argmax() for
+                                  k in range(self.n_outputs_)], [n_samples, 1])
+            elif strategy == 'stratified':
+                rs = check_random_state(0)
+                proba = []
+                for k in range(self.n_outputs_):
+                    out = rs.multinomial(1, dummy_output[k], size=n_samples)
+                    out = out.astype(np.float64)
+                    proba.append(out)
+                scores = np.vstack([proba[k].argmax(axis=1) for
+                                    k in range(self.n_outputs_)]).T
+        elif isinstance(self.estimator, DummyRegressor):
+                scores = np.full((n_samples, self.n_outputs_), self.dummy_output_,
+                                  dtype=np.array(self.dummy_output_).dtype)
+        return scores.ravel() if scores.shape[1] == 1 else scores
+
 
 class Decoder(_BaseDecoder):
     """A wrapper for popular classification strategies in neuroimaging.
@@ -682,7 +786,7 @@ class Decoder(_BaseDecoder):
         The estimator to choose among: 'svc', 'svc_l2', 'svc_l1', 'logistic',
         'logistic_l1', 'logistic_l2' and 'ridge_classifier'. Note that
         'svc' and 'svc_l2'; 'logistic' and 'logistic_l2' correspond to the same
-        estimator. Default 'svc'.
+        estimator. Dummy classifier is named as 'dummy_classifier'. Default 'svc'.
 
     mask: filename, Nifti1Image, NiftiMasker, or MultiNiftiMasker, optional
         Mask to be used on data. If an instance of masker is passed,
@@ -706,6 +810,9 @@ class Decoder(_BaseDecoder):
         or have no effect. See scikit-learn documentation for more information,
         for example: https://scikit-learn.org/stable/modules/grid_search.html
 
+        For DummyClassifier, parameter grid defaults to empty dictionary, class
+        predictions are estimated using default strategy.
+
     screening_percentile: int, float, optional, in the closed interval [0, 100]
         The percentage of brain volume that will be kept with respect to a full
         MNI template. In particular, if it is lower than 100, a univariate
@@ -816,7 +923,7 @@ class DecoderRegressor(_BaseDecoder):
     estimator: str, optional
         The estimator to choose among: 'ridge', 'ridge_regressor', and 'svr'.
         Note that the 'ridge' and 'ridge_regressor' correspond to the same
-        estimator. Default 'svr'.
+        estimator. Dummy regressor is named as 'dummy_regressor'. Default 'svr'.
 
     mask: filename, Nifti1Image, NiftiMasker, or MultiNiftiMasker, optional
         Mask to be used on data. If an instance of masker is passed,
@@ -840,6 +947,9 @@ class DecoderRegressor(_BaseDecoder):
         or have no effect. See scikit-learn documentation for more information,
         for example: https://scikit-learn.org/stable/modules/grid_search.html
 
+        For DummyRegressor, parameter grid defaults to empty dictionary, class
+        predictions are estimated using default strategy.
+
     screening_percentile: int, float, optional, in the closed interval [0, 100]
         The percentage of brain volume that will be kept with respect to a full
         MNI template. In particular, if it is lower than 100, a univariate


Test patch:
diff --git a/nilearn/decoding/tests/test_decoder.py b/nilearn/decoding/tests/test_decoder.py
--- a/nilearn/decoding/tests/test_decoder.py
+++ b/nilearn/decoding/tests/test_decoder.py
@@ -22,10 +22,11 @@
 from sklearn.datasets import load_iris, make_classification, make_regression
 from sklearn.ensemble import RandomForestClassifier
 from sklearn.linear_model import LogisticRegression, RidgeClassifierCV, RidgeCV
-from sklearn.metrics import accuracy_score, r2_score
+from sklearn.metrics import accuracy_score, r2_score, roc_auc_score
 from sklearn.model_selection import KFold, LeaveOneGroupOut
 from sklearn.preprocessing import StandardScaler
 from sklearn.svm import SVR, LinearSVC
+from sklearn.dummy import DummyClassifier, DummyRegressor
 
 try:
     from sklearn.metrics import check_scoring
@@ -43,6 +44,9 @@
 ridge_classifier = RidgeClassifierCV()
 random_forest = RandomForestClassifier()
 
+dummy_classifier = DummyClassifier(random_state=0)
+dummy_regressor = DummyRegressor()
+
 regressors = {'ridge': (ridge, []),
               'svr': (svr, 'C')}
 classifiers = {'svc': (svc, 'C'),
@@ -78,6 +82,10 @@ def test_check_param_grid():
         pytest.raises(ValueError, _check_param_grid, estimator, X,
                       y_classification, None)
 
+    # Test return parameter grid is empty
+    param_grid = _check_param_grid(dummy_classifier, X, y_classification, None)
+    assert param_grid == {}
+
 
 def test_check_inputs_length():
     iris = load_iris()
@@ -101,7 +109,8 @@ def test_check_estimator():
     supported_estimators = ['svc', 'svc_l2', 'svc_l1',
                             'logistic', 'logistic_l1', 'logistic_l2',
                             'ridge', 'ridge_classifier',
-                            'ridge_regressor', 'svr']
+                            'ridge_regressor', 'svr', 'dummy_classifier',
+                            'dummy_regressor']
     unsupported_estimators = ['ridgo', 'svb']
     expected_warning = ('Use a custom estimator at your own risk '
                         'of the process not working as intended.')
@@ -172,6 +181,43 @@ def test_decoder_binary_classification():
     y_pred = model.predict(X)
     assert accuracy_score(y, y_pred) > 0.95
 
+    # decoder object use prior as strategy (default) for dummy classifier
+    model = Decoder(estimator='dummy_classifier', mask=mask)
+    model.fit(X, y)
+    y_pred = model.predict(X)
+    assert accuracy_score(y, y_pred) == 0.5
+
+    # decoder object use other strategy for dummy classifier
+    param = dict(strategy='stratified')
+    dummy_classifier.set_params(**param)
+    model = Decoder(estimator=dummy_classifier, mask=mask)
+    model.fit(X, y)
+    y_pred = model.predict(X)
+    assert accuracy_score(y, y_pred) >= 0.5
+    # Returns model coefficients for dummy estimators as None
+    assert model.coef_ is None
+    # Dummy output are nothing but the attributes of the dummy estimators
+    assert model.dummy_output_ is not None
+    assert model.cv_scores_ is not None
+    # model attribute n_outputs_ depending on target y ndim
+    assert model.n_outputs_ == 1
+
+    # decoder object use other scoring metric for dummy classifier
+    model = Decoder(estimator='dummy_classifier', mask=mask)
+    model.fit(X, y)
+    y_pred = model.predict(X)
+    assert roc_auc_score(y, y_pred) == 0.5
+
+    model = Decoder(estimator='dummy_classifier', mask=mask, scoring='roc_auc')
+    model.fit(X, y)
+    assert np.mean(model.cv_scores_[0]) >= 0.5
+
+    # Raises a not implemented error with strategy constant
+    param = dict(strategy='constant')
+    dummy_classifier.set_params(**param)
+    model = Decoder(estimator=dummy_classifier, mask=mask)
+    pytest.raises(NotImplementedError, model.fit, X, y)
+
     # check different screening_percentile value
     for screening_percentile in [100, 20, None]:
         model = Decoder(mask=mask, screening_percentile=screening_percentile)
@@ -211,6 +257,13 @@ def test_decoder_multiclass_classification():
     y_pred = model.predict(X)
     assert accuracy_score(y, y_pred) > 0.95
 
+    # check classification with masker object and dummy classifier
+    model = Decoder(estimator='dummy_classifier', mask=NiftiMasker())
+    model.fit(X, y)
+    y_pred = model.predict(X)
+    # 4-class classification
+    assert accuracy_score(y, y_pred) > 0.2
+
     # check different screening_percentile value
     for screening_percentile in [100, 20, None]:
         model = Decoder(mask=mask, screening_percentile=screening_percentile)
@@ -269,6 +322,26 @@ def test_decoder_regression():
             y_pred = model.predict(X)
             assert r2_score(y, y_pred) > 0.95
 
+    # Regression with dummy estimator
+    model = DecoderRegressor(estimator='dummy_regressor', mask=mask,
+                             scoring='r2', screening_percentile=1)
+    model.fit(X, y)
+    y_pred = model.predict(X)
+    assert r2_score(y, y_pred) <= 0.
+ 
+    # decoder object use other strategy for dummy regressor
+    param = dict(strategy='median')
+    dummy_regressor.set_params(**param)
+    model = DecoderRegressor(estimator=dummy_regressor, mask=mask)
+    model.fit(X, y)
+    y_pred = model.predict(X)
+    assert r2_score(y, y_pred) <= 0.
+    # Returns model coefficients for dummy estimators as None
+    assert model.coef_ is None
+    # Dummy output are nothing but the attributes of the dummy estimators
+    assert model.dummy_output_ is not None
+    assert model.cv_scores_ is not None
+
     dim = 5
     X, y = make_regression(n_samples=100, n_features=dim**3, n_informative=dim,
                            noise=1.5, bias=1.0, random_state=42)