Repository: nilearn/nilearn

Issue:
warn for BIDS events file with no `trial_type` column
Technically `trial_type` columns in BIDS events.tsv files are optional.

https://bids-specification.readthedocs.io/en/latest/modality-specific-files/task-events.html

So it can happen to have valid BIDS dataset with either no `trial_type` column or trial type information stored under a different column.

See from the BIDS examples:
- https://github.com/bids-standard/bids-examples/blob/master/ds000117/sub-01/ses-mri/func/sub-01_ses-mri_task-facerecognition_run-01_events.tsv

The default behavior of `make_first_level_design_mat` to create dummy events makes sense when there is no `trial_type` column but may not be appropriate when trial type information is stored under another column.

https://github.com/nilearn/nilearn/blob/5ed3b2471038c1098412196aa38c5232e166e445/nilearn/glm/first_level/design_matrix.py#L269

It may be nice to warn users by throwing a warning when events are collected by `first_level_from_bids`.

Note that this should happen rarely so the number of users affected should be low and they may probably already know about it if they have explored the dataset they are working with.



Discussion:
 @Remi-Gau   Could you please assign this to me?
Done. 😊
@Remi-Gau what exactly should be the warning string here ? That "trial_type" column should be correctly named "trial_type" in the "events.tsv" file ?


I would say that if there is only an onset and duration column then there is no point throwing a warning.
If there are other columns but none called trial_type I would tell users all rows will be considered a belonging to the same condition and that if there is column of the data frame  containing trial information they may consider renaming it to trial_type. 
By the way given that the first_level_from_bids function is going through a big refactoring you may want to just write a simple function to perform that check on a pandas data frame and throw the warning.

It will be easier to write a test for and prevent annoying merge conflicts later on. 

Resolving patch:
diff --git a/nilearn/glm/first_level/first_level.py b/nilearn/glm/first_level/first_level.py
--- a/nilearn/glm/first_level/first_level.py
+++ b/nilearn/glm/first_level/first_level.py
@@ -235,6 +235,33 @@ def run_glm(Y, X, noise_model='ar1', bins=100,
     return labels, results
 
 
+def _check_trial_type(events):
+    """Check that the event files contain a "trial_type" column.
+
+    Parameters
+    ----------
+    events : :obj:`list` of :obj:`str` or :obj:`pathlib.Path``.
+              A list of paths of events.tsv files.
+
+    """
+    file_names = []
+
+    for event_ in events:
+        df = pd.read_csv(event_, sep='\t')
+        if 'trial_type' not in df.columns:
+            file_names.append(os.path.basename(event_))
+
+    if file_names:
+        file_names = "\n -".join(file_names)
+        warn(
+            f"No column named 'trial_type' found in:{file_names}.\n "
+            "All rows in those files will be treated "
+            "as if they are instances of same experimental condition.\n"
+            "If there is a column in the dataframe "
+            "corresponding to trial information, "
+            "consider renaming it to 'trial_type'.")
+
+
 @fill_doc
 class FirstLevelModel(BaseGLM):
     """Implement the General Linear Model for single session fMRI data.
@@ -1665,6 +1692,7 @@ def _check_bids_events_list(
             "Same number of event files "
             "as the number of runs is expected."
         )
+    _check_trial_type(events=events)
 
     supported_filters = [
         "sub",


Test patch:
diff --git a/nilearn/glm/tests/test_first_level.py b/nilearn/glm/tests/test_first_level.py
--- a/nilearn/glm/tests/test_first_level.py
+++ b/nilearn/glm/tests/test_first_level.py
@@ -34,7 +34,7 @@
     check_design_matrix,
     make_first_level_design_matrix,
 )
-from nilearn.glm.first_level.first_level import _yule_walker
+from nilearn.glm.first_level.first_level import _check_trial_type, _yule_walker
 from nilearn.glm.regression import ARModel, OLSModel
 from nilearn.image import get_data
 from nilearn.interfaces.bids import get_bids_files
@@ -1599,3 +1599,35 @@ def test_slice_time_ref_warning_only_when_not_provided(bids_dataset):
     # check that no warnings were raised
     for r in record:
         assert "'slice_time_ref' not provided" not in r.message.args[0]
+
+
+def test_check_trial_type_warning(tmp_path):
+    """Check that warning is thrown when an events file has no trial_type."""
+    events = pd.DataFrame({"onset": [0, 1, 2], "duration": [1, 1, 1]})
+    event_file = tmp_path / "events.tsv"
+    events.to_csv(event_file, sep="\t", index=False)
+    with pytest.warns(UserWarning, match="No column named 'trial_type' found"):
+        _check_trial_type([event_file])
+
+
+def test_missing_trial_type_column_warning(tmp_path_factory):
+    """Check that warning is thrown when an events file has no trial_type.
+
+    Ensure that the warning is thrown when running first_level_from_bids.
+    """
+    bids_dataset = _new_bids_dataset(
+        tmp_path_factory.mktemp("one_event_missing")
+    )
+    events_files = get_bids_files(main_path=bids_dataset, file_tag="events")
+    # remove trial type column from one events.tsv file
+    events = pd.read_csv(events_files[0], sep="\t")
+    events.drop(columns="trial_type", inplace=True)
+    events.to_csv(events_files[0], sep="\t", index=False)
+
+    with pytest.warns() as record:
+        first_level_from_bids(
+            dataset_path=bids_dataset, task_label="main", space_label="MNI",
+            slice_time_ref=None,
+        )
+        assert (any("No column named 'trial_type' found" in r.message.args[0]
+                    for r in record))