Repository: mne-tools/mne-bids

Issue:
events.tsv with same trial_type, but different value
I have some `events.tsv` files that look like this:

```
onset    	duration 	sample 	trial_type 	stim_file 	value
9.6211   	0.0      	9853   	response   	n/a       	202
15.1250  	0.0      	15489  	response   	n/a       	202
19.4121  	0.2      	19879  	stimulus   	n/a       	11
19.8926  	0.0      	20371  	response   	n/a       	201
20.8623  	0.2      	21364  	stimulus   	n/a       	12
21.3164  	0.0      	21829  	response   	n/a       	201
22.3779  	0.2      	22916  	stimulus   	n/a       	14
22.7969  	0.0      	23345  	response   	n/a       	201
```
As you can see, the `response` `trial_type` can either be associated with a `201` (correct) or `202` (incorrect) value.

When this file is read by MNE-BIDS, the distinction between the two types of response disappears, as we ignore the `value` column if we find a `trial_type` column.

There's a similar pattern for the `stimulus` trial type.

I would propose that in the case where we find that the `trial_type` <> `value` mapping is not unique, we create hierarchical event names composed of both fields, i.e. in this specific case, `response/201` and `response/202`.
Also we should make it easy for users to rename the events on the BIDS level.

Thoughts?

cc @sappelhoff @adam2392 @jasmainak @agramfort 


Discussion:
The BIDS specification, by the way, is IMHO **very** lenient regarding the `trial_type` column and encourages the behavior we see above:

> `trial_type` Primary categorisation of each trial to identify them as instances of the experimental conditions. For example: for a response inhibition task, it could take on values "go" and "no-go" to refer to response initiation and response inhibition experimental conditions.

https://bids-specification.readthedocs.io/en/stable/04-modality-specific-files/05-task-events.html


great idea !

>


Resolving patch:
diff --git a/mne_bids/read.py b/mne_bids/read.py
--- a/mne_bids/read.py
+++ b/mne_bids/read.py
@@ -278,11 +278,33 @@ def _handle_events_reading(events_fname, raw):
     if trial_type_col_name is not None:
         # Drop events unrelated to a trial type
         events_dict = _drop(events_dict, 'n/a', trial_type_col_name)
-        descriptions = np.asarray(events_dict[trial_type_col_name], dtype=str)
 
-    # If we don't have a proper description of the events, perhaps we have
-    # at least an event value?
+        if 'value' in events_dict:
+            # Check whether the `trial_type` <> `value` mapping is unique.
+            trial_types = events_dict[trial_type_col_name]
+            values = np.asarray(events_dict['value'], dtype=str)
+            for trial_type in np.unique(trial_types):
+                idx = np.where(trial_type == np.atleast_1d(trial_types))[0]
+                matching_values = values[idx]
+
+                if len(np.unique(matching_values)) > 1:
+                    # Event type descriptors are ambiguous; create hierarchical
+                    # event descriptors.
+                    logger.info(
+                        f'The event "{trial_type}" refers to multiple event '
+                        f'values. Creating hierarchical event names.')
+                    for ii in idx:
+                        new_name = f'{trial_type}/{values[ii]}'
+                        logger.info(f'    Renaming event: {trial_type} -> '
+                                    f'{new_name}')
+                        trial_types[ii] = new_name
+            descriptions = np.asarray(trial_types, dtype=str)
+        else:
+            descriptions = np.asarray(events_dict[trial_type_col_name],
+                                      dtype=str)
     elif 'value' in events_dict:
+        # If we don't have a proper description of the events, perhaps we have
+        # at least an event value?
         # Drop events unrelated to value
         events_dict = _drop(events_dict, 'n/a', 'value')
         descriptions = np.asarray(events_dict['value'], dtype=str)


Test patch:
diff --git a/mne_bids/tests/test_read.py b/mne_bids/tests/test_read.py
--- a/mne_bids/tests/test_read.py
+++ b/mne_bids/tests/test_read.py
@@ -280,6 +280,24 @@ def test_handle_events_reading():
         raw = _handle_events_reading(events_fname, raw)
     events, event_id = mne.events_from_annotations(raw)
 
+    # Test with same `trial_type` referring to different `value`
+    events = {'onset': [11, 12, 13],
+              'duration': ['n/a', 'n/a', 'n/a'],
+              'trial_type': ["event1", "event1", "event2"],
+              'value': [1, 2, 3]}
+    tmp_dir = _TempDir()
+    events_fname = op.join(tmp_dir, 'sub-01_task-test_events.json')
+    _to_tsv(events, events_fname)
+
+    raw = _handle_events_reading(events_fname, raw)
+    events, event_id = mne.events_from_annotations(raw)
+
+    assert len(events) == 3
+    assert 'event1/1' in event_id
+    assert 'event1/2' in event_id
+    # The event with unique value mapping should not be renamed
+    assert 'event2' in event_id
+
     # Test without any kind of event description.
     events = {'onset': [11, 12, 'n/a'],
               'duration': ['n/a', 'n/a', 'n/a']}