Repository: nilearn/nilearn

Issue:
[BUG] Backward incompatible change in Decoder
### Is there an existing issue for this?

- [X] I have searched the existing issues

### Operating system

- [X] Linux
- [ ] Mac
- [ ] Windows

### Operating system version

- Linux Ubuntu 22.04



### Python version

- [ ] 3.12
- [ ] 3.11
- [X] 3.10
- [ ] 3.9
- [ ] 3.8

### nilearn version

0.11.dev

### Expected behavior

The doc build on main is failing because merge of #4188 broke one example. `decoder.decision_function` is called on a list of file paths which is not acceptable input. This input is neither documented nor tested on any of the decoder methods unless I missed something

### Current behavior & error messages

This is what I got:

Calling `decoder.predict` with a list of filepaths will pass this input to `decoder.decision_function` and fail


### Steps and code to reproduce bug

See https://nilearn.github.io/dev/auto_examples/02_decoding/plot_oasis_vbm.html#sphx-glr-auto-examples-02-decoding-plot-oasis-vbm-py



Discussion:
@ymzayek do you want to work on it ?
Working on it now. Will open a PR asap

Resolving patch:
diff --git a/nilearn/decoding/decoder.py b/nilearn/decoding/decoder.py
--- a/nilearn/decoding/decoder.py
+++ b/nilearn/decoding/decoder.py
@@ -836,11 +836,12 @@ def score(self, X, y, *args):
 
         Parameters
         ----------
-        X : Niimg-like
+        X : Niimg-like, :obj:`list` of either \
+            Niimg-like objects or :obj:`str` or path-like
             See :ref:`extracting_data`.
             Data on which prediction is to be made.
 
-        y : array-like
+        y : :class:`numpy.ndarray`
             Target values.
 
         args : Optional arguments that can be passed to
@@ -861,18 +862,20 @@ def decision_function(self, X):
 
         Parameters
         ----------
-        X: list of Niimg-like objects
+        X : Niimg-like, :obj:`list` of either \
+            Niimg-like objects or :obj:`str` or path-like
             See :ref:`extracting_data`.
             Data on prediction is to be made. If this is a list,
             the affine is considered the same for all.
 
         Returns
         -------
-        y_pred: ndarray, shape (n_samples,)
+        y_pred: :class:`numpy.ndarray`, shape (n_samples,)
             Predicted class label per sample.
         """
-        # for backwards compatibility
-        if not isinstance(X, np.ndarray):
+        # for backwards compatibility - apply masker transform if X is
+        # niimg-like or a list of strings
+        if not isinstance(X, np.ndarray) or len(np.shape(X)) == 1:
             X = self.masker_.transform(X)
         n_features = self.coef_.shape[1]
         if X.shape[1] != n_features:
@@ -893,7 +896,8 @@ def predict(self, X):
 
         Parameters
         ----------
-        X: Niimg-like
+        X : Niimg-like, :obj:`list` of either \
+            Niimg-like objects or :obj:`str` or path-like
             See :ref:`extracting_data`.
             Data on which prediction is to be made.
 
@@ -907,7 +911,7 @@ class would be predicted.
         check_is_fitted(self, "coef_")
         check_is_fitted(self, "masker_")
 
-        n_samples = X.shape[-1]
+        n_samples = np.shape(X)[-1]
 
         # Prediction for dummy estimator is different from others as there is
         # no fitted coefficient


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
@@ -19,6 +19,7 @@
 import numbers
 import warnings
 
+import nibabel
 import numpy as np
 import pytest
 import sklearn
@@ -1037,3 +1038,38 @@ def test_decoder_decision_function(binary_classification_data):
     X = model.masker_.transform(X)
     assert X.shape[1] == model.coef_.shape[1]
     model.decision_function(X)
+
+
+def test_decoder_strings_filepaths_input(
+    tiny_binary_classification_data, tmp_path
+):
+    """Smoke test for decoder methods to accept list of paths as input.
+
+    See https://github.com/nilearn/nilearn/issues/4226
+    """
+    X, y, _ = tiny_binary_classification_data
+    X_paths = [tmp_path / f"niimg{i}.nii" for i in range(X.shape[-1])]
+    for i, nii_path in enumerate(X_paths):
+        nibabel.save(X.slicer[..., i], nii_path)
+
+    model = Decoder(mask=NiftiMasker())
+    model.fit(X_paths, y)
+    model.predict(X_paths)
+    model.score(X_paths, y)
+
+
+def test_decoder_decision_function_raises_value_error(
+    binary_classification_data,
+):
+    """Test decision_function raises value error."""
+    X, y, _ = binary_classification_data
+
+    model = Decoder(mask=NiftiMasker())
+    model.fit(X, y)
+    X = model.masker_.transform(X)
+    X = np.delete(X, 0, axis=1)
+
+    with pytest.raises(
+        ValueError, match=f"X has {X.shape[1]} features per sample"
+    ):
+        model.decision_function(X)