Repository: nilearn/nilearn

Issue:
Prevent user passing `standardize_confounds=False` with confound variables to `signal.clean` 
<!--Provide a brief description what you would like changes and why.-->

While working on the test in #2946, I noticed there's no mechanism in place to prevent user from passing some illogical options, such as `standardize_confounds=False, detrend=False, standardize=False` and `standardize_confounds=False, detrend=False` when passing confound variables. When confound variables are present, they should always be demeaned or standardised.

<!--Please fill in the following information, to the best of your ability.-->
### Benefits to the change

Reduce the chance of mistake for users who decided not to use the default parameters. 

### Pseudocode for the new behaviour, if applicable

I have thought of several ways:

1. Raise error when bad parameter combinations are passed to the function
2. Raise warning and fall back to default options
3. Warn the user without changing the provided parameters (proposed by @NicolasGensollen)

Here's a list of option combinations that should not be allowed in `signal.clean`.

|`standardize_confounds`|`detrend`|`standardize`|
|--|--|--|
|False|False|zscore|
|False|False|psc|
|False|False|False|

Comments:

Patch:
diff --git a/doc/whats_new.rst b/doc/whats_new.rst
index a3db610d8..38aae48a7 100644
--- a/doc/whats_new.rst
+++ b/doc/whats_new.rst
@@ -18,6 +18,12 @@ Enhancements
   When `two_sided` is `False`, only values greater than or equal to the threshold
   are retained.
 
+- :func:`nilearn.signal.clean` raises a warning when the user sets 
+  parameters `detrend` and `standardize_confound` to False.
+  The user is suggested to set one of
+  those options to `True`, or standardize/demean the confounds before using the 
+  function.
+
 Changes
 -------
 
diff --git a/nilearn/_utils/data_gen.py b/nilearn/_utils/data_gen.py
index a8855459a..9f70959a2 100644
--- a/nilearn/_utils/data_gen.py
+++ b/nilearn/_utils/data_gen.py
@@ -12,7 +12,6 @@ from scipy import ndimage
 
 from sklearn.utils import check_random_state
 import scipy.linalg
-import nibabel
 
 from nibabel import Nifti1Image
 
@@ -143,7 +142,7 @@ def generate_maps(shape, n_regions, overlap=0, border=1,
     mask[border:-border, border:-border, border:-border] = 1
     ts = generate_regions_ts(mask.sum(), n_regions, overlap=overlap,
                              rand_gen=rand_gen, window=window)
-    mask_img = nibabel.Nifti1Image(mask, affine)
+    mask_img = Nifti1Image(mask, affine)
     return masking.unmask(ts, mask_img), mask_img
 
 
@@ -191,7 +190,7 @@ def generate_labeled_regions(shape, n_regions, rand_gen=None, labels=None,
         row[row > 0] = n
     data = np.zeros(shape, dtype=dtype)
     data[np.ones(shape, dtype=bool)] = regions.sum(axis=0).T
-    return nibabel.Nifti1Image(data, affine)
+    return Nifti1Image(data, affine)
 
 
 def generate_labeled_regions_large(shape, n_regions, rand_gen=None,
@@ -206,7 +205,7 @@ def generate_labeled_regions_large(shape, n_regions, rand_gen=None,
     data = rand_gen.randint(n_regions + 1, size=shape)
     if len(np.unique(data)) != n_regions + 1:
         raise ValueError("Some labels are missing. Maybe shape is too small.")
-    return nibabel.Nifti1Image(data, affine)
+    return Nifti1Image(data, affine)
 
 
 def generate_fake_fmri(shape=(10, 11, 12), length=17, kind="noise",
@@ -293,8 +292,8 @@ def generate_fake_fmri(shape=(10, 11, 12), length=17, kind="noise",
          shift[2]:shift[2] + width[2]] = 1
 
     if n_blocks is None:
-        return (nibabel.Nifti1Image(fmri, affine),
-                nibabel.Nifti1Image(mask, affine))
+        return (Nifti1Image(fmri, affine),
+                Nifti1Image(mask, affine))
 
     block_size = 3 if block_size is None else block_size
     flat_fmri = fmri[mask.astype(bool)]
@@ -330,8 +329,8 @@ def generate_fake_fmri(shape=(10, 11, 12), length=17, kind="noise",
         else target.astype(np.float64)
     fmri = np.zeros(fmri.shape)
     fmri[mask.astype(bool)] = flat_fmri
-    return (nibabel.Nifti1Image(fmri, affine),
-            nibabel.Nifti1Image(mask, affine), target)
+    return (Nifti1Image(fmri, affine),
+            Nifti1Image(mask, affine), target)
 
 
 def generate_fake_fmri_data_and_design(shapes, rk=3, affine=np.eye(4)):
diff --git a/nilearn/plotting/js_plotting_utils.py b/nilearn/plotting/js_plotting_utils.py
index 252ace1e7..9618d59a4 100644
--- a/nilearn/plotting/js_plotting_utils.py
+++ b/nilearn/plotting/js_plotting_utils.py
@@ -14,7 +14,7 @@ from matplotlib import cm as mpl_cm
 
 # included here for backward compatibility
 from nilearn.plotting.html_document import (
-    HTMLDocument, set_max_img_views_before_warning,)  # noqa
+    HTMLDocument, set_max_img_views_before_warning,)  # noqa: F401
 from .._utils.extmath import fast_abs_percentile
 from .._utils.param_validation import check_threshold
 from .. import surface
diff --git a/nilearn/reporting/glm_reporter.py b/nilearn/reporting/glm_reporter.py
index 50ea2e291..914b4351a 100644
--- a/nilearn/reporting/glm_reporter.py
+++ b/nilearn/reporting/glm_reporter.py
@@ -620,11 +620,11 @@ def _mask_to_svg(mask_img, bg_img):
 
     """
     if mask_img:
-        mask_plot = plot_roi(roi_img=mask_img,  # noqa: F841
-                             bg_img=bg_img,
-                             display_mode='z',
-                             cmap='Set1',
-                             )
+        plot_roi(roi_img=mask_img,
+                 bg_img=bg_img,
+                 display_mode='z',
+                 cmap='Set1',
+                 )
         mask_plot_svg = _plot_to_svg(plt.gcf())
         # prevents sphinx-gallery & jupyter from scraping & inserting plots
         plt.close()
@@ -899,7 +899,7 @@ def _stat_map_to_svg(stat_img,
         raise ValueError('Invalid plot type provided. Acceptable options are'
                          "'slice' or 'glass'.")
     with pd.option_context('display.precision', 2):
-        stat_map_plot = _add_params_to_plot(table_details, stat_map_plot)
+        _add_params_to_plot(table_details, stat_map_plot)
     fig = plt.gcf()
     stat_map_svg = _plot_to_svg(fig)
     # prevents sphinx-gallery & jupyter from scraping & inserting plots
@@ -933,10 +933,10 @@ def _add_params_to_plot(table_details, stat_map_plot):
                                  wrap=True,
                                  )
     fig = list(stat_map_plot.axes.values())[0].ax.figure
-    fig = _resize_plot_inches(plot=fig,
-                              width_change=.2,
-                              height_change=1,
-                              )
+    _resize_plot_inches(plot=fig,
+                        width_change=.2,
+                        height_change=1,
+                        )
     if stat_map_plot._black_bg:
         suptitle_text.set_color('w')
     return stat_map_plot
diff --git a/nilearn/signal.py b/nilearn/signal.py
index d1732b847..5809be638 100644
--- a/nilearn/signal.py
+++ b/nilearn/signal.py
@@ -529,6 +529,10 @@ def clean(signals, runs=None, detrend=True, standardize='zscore',
     --------
         nilearn.image.clean_img
     """
+    # Raise warning for some parameter combinations when confounds present
+    if confounds is not None:
+        _check_signal_parameters(detrend, standardize_confounds)
+
     # Read confounds and signals
     signals, runs, confounds = _sanitize_inputs(
         signals, runs, confounds, sample_mask, ensure_finite
@@ -824,3 +828,15 @@ def _sanitize_signals(signals, ensure_finite):
         if mask.any():
             signals[mask] = 0
     return _ensure_float(signals)
+
+
+def _check_signal_parameters(detrend, standardize_confounds):
+    """Raise warning if the combination is illogical"""
+    if not detrend and not standardize_confounds:
+        warnings.warn("When confounds are provided, one must perform detrend "
+                      "and/or standarize confounds. You provided detrend={0}, "
+                      "standardize_confounds={1}. If confounds were not "
+                      "standardized or demeaned before passing to signal.clean"
+                      " signal will not be correctly cleaned. ".format(
+                          detrend, standardize_confounds)
+                      )
diff --git a/nilearn/surface/surface.py b/nilearn/surface/surface.py
index 319a78fe1..c2c7261ca 100644
--- a/nilearn/surface/surface.py
+++ b/nilearn/surface/surface.py
@@ -3,10 +3,9 @@ Functions for surface manipulation.
 """
 import os
 import warnings
-import collections
 import gzip
 from distutils.version import LooseVersion
-from collections import namedtuple
+from collections import (namedtuple, Mapping)
 
 
 import numpy as np
@@ -959,7 +958,7 @@ def _check_mesh(mesh):
     """
     if isinstance(mesh, str):
         return datasets.fetch_surf_fsaverage(mesh)
-    if not isinstance(mesh, collections.Mapping):
+    if not isinstance(mesh, Mapping):
         raise TypeError("The mesh should be a str or a dictionary, "
                         "you provided: {}.".format(type(mesh).__name__))
     missing = {'pial_left', 'pial_right', 'sulc_left', 'sulc_right',


Test patch:
diff --git a/nilearn/tests/test_signal.py b/nilearn/tests/test_signal.py
index 76eed877d..1453c4c38 100644
--- a/nilearn/tests/test_signal.py
+++ b/nilearn/tests/test_signal.py
@@ -505,13 +505,17 @@ def test_clean_confounds():
 
     # Test without standardizing that constant parts of confounds are
     # accounted for
-    np.testing.assert_almost_equal(nisignal.clean(np.ones((20, 2)),
-                                                  standardize=False,
-                                                  confounds=np.ones(20),
-                                                  standardize_confounds=False,
-                                                  detrend=False,
-                                                  ).mean(),
-                                   np.zeros((20, 2)))
+    # passing standardize_confounds=False, detrend=False should raise warning
+    warning_message = r"must perform detrend and/or standarize confounds"
+    with pytest.warns(UserWarning, match=warning_message):
+        np.testing.assert_almost_equal(
+            nisignal.clean(np.ones((20, 2)),
+                           standardize=False,
+                           confounds=np.ones(20),
+                           standardize_confounds=False,
+                           detrend=False,
+                           ).mean(),
+            np.zeros((20, 2)))
 
     # Test to check that confounders effects are effectively removed from
     # the signals when having a detrending and filtering operation together.