Repository: mne-tools/mne-python

Issue:
Question: multiple events per trial
Hello,
I'm trying to import some data from fieldtrip to MNE-Python.

However, my data contains multiple events per trial: e.g. (visual or auditory) x (left or right) x (relevant or distractor).

What do you think is the best way to keep these various information in the events, and how can I then some interaction (e.g. visual x left x relevant; e.g. visual left OR auditory right)?

Thanks!



Discussion:
@kingjr do you say that this is not multiple categories per event or different events in one trial?

 (visual or auditory) x (left or right) x (relevant or distractor)

could read like one event that is visual + left + relevant, and so on, just single cells from a 2 x 2 x 2 design. If that's the case you just need sufficient event codes, 6 different codes for example. If you say you have to deal with let's say 3 different, individual events in one trial, it's gonna be complicated with the existing functions.

no it's three categories of events per trial: in one trial, you only have one event with multiple features. 

I have up to 10 categories of events per trial, which is a bit of a pain to combine and then parse but ok. I'll go for summing them up... Could be easier to have multidimensional arrays of events like in fieldtrip?

One way to keep things simple(ish) is to use different decimal places to encode the type. For example, the ones digit could be relevant or distractor as 1 or 2, tens could be LR could (10 or 20), and hundreds could be vis/aud (100 or 200). Then seeing / coding a trial as 211 you know it's a visual left relevant trial. But this is really up to you...

I'm not really sure what the multidimensional array would gain us. One problem with adding that would be that, historically, event numbers only have a single dimension. This means that the file formats `-eve.lst` and `-eve.fif` are only designed to handle this case. So it's unlikely we'd go to multidimensional arrays in the near future, especially if there are equivalent not-too-bad ways of doing things with a simpler single-dimensional solution.

+1 on what Eric said.

Use a good event coding scheme and collapse acros factors as needed. Dictionaries also help.

@Eric89GXL yes that's what I was thinking of doing.
@dengemann : What do you mean by using dictionaries here?

Hi, the best way to deal with this in my opinion is to use the binary nature of the triggers. I have done this in my own experiments, so if I have say three different event types: famous faces, scrambled faces, and unfamiliar faces, with three different subtypes (first presentation, second presentation, third presentation). I can modulate these differently. I can use 1,2,3 to represent the presentation subtype and the 4, 8, 16 to represent the image type. Thus my events become: 5, 6, 7 for famous faces (first, second, and third presentation). 9, 10, 11 for scrambled, and 17 18 19 for unfamiliar faces.

Furthermore, I know at least the C-code has ways of intelligently combining these (with the --ignore option). I'm not sure if this has been added to mne-python (I haven't gotten comfortable yet with the way it deals with averaging/epochs).

@dgwakeman: Yes, this is close to Eric's proposal, isn't it?

In any case, from what I understand, these are ad-hoc tricks. Note that one may want to code for continuous values (e.g. the angle of a Gabor, and multicategorial etc). I was thus wondering whether MNE-Python had a flexible and efficient way of dealing with this variability of event coding.

any suggestion of API is welcome. Basically if you had to propose a
pythonic way of doing this boolean logic of conditions how would like to
write it?

@kingjr Yes, it is similar, although there is already code for dealing with this (at least in the C-code). I did not use it as an ad-hoc trick at all. I created my stimulus program to send these triggers to the machine, so they are built in already. I am currently trying to come up with a better strategy for implementing averaging (it looks like, I may have to mess with epoching as well, but I haven't had time to finish the other Issues I have on github, so I should get those done now).

I feel that there should be some way of combining epochs in a list of epochs. E.g., 

``` py
epochs.combine_trials([['visual', 'auditory'], ['left', 'right'], ['relevant', 'distractor']])
```

and this will either modify inplace or return a list of epochs with 3 items instead of 6. Does this address the use case intended?

assuming event_id is explicit epochs['visual*'] would return all visual
trials left or right?

So, the event dictionary would have keys `visual-left-relevant`, `visual-right-relevant` and so on? And the user could do `epochs[visual*]` ? That would be super-neat in my opinion.

yes that's my idea

how would that work for cases where you want to collapse one dimension but not the others. For example ignore relevance?

If we allow regular expressions, you can possibly do any kind of string operations such as ignore relevance?

yes regexp can do this.

but then you need a strict delimiter structure. The semantics is not clear otherwise. In a fully factorial design it would mean: collapse along the dimension that is not selected by the regexp. But what if you have strings that don't match:

`visual-left-relevant, visual-right-elephant`

or

`visual-left-relevant, visual_right:elephant`

basically, we allow regexp support and the rest is up to the user to exploit using right choice of strings. no?

I'd be happy to see the collapsibility extended to other functions like `plot_events` and its legends etc.

> basically, we allow regexp support and the rest is up to the user to exploit using right choice of strings. no? 

but collapsing won't work if the namings are not symmetric. If some data is e.g. not fully factorial we need to guess that and throw errors / warnings.

you seem to anticipate more than me the problem. Can you be more explicit?

@agramfort we need to properly engineer this, otherwise collapsing rules will be magic and it will be impossible to predict how many epochs and which ones will be put in a new label. If we unpack this a bit it means that what is not matched by the regexps will be collapsed. So if you would pass a regexp that for examples leads to collapsing left and right, the new epochs are expected to have the following keys (which leads to another point, auto renaming of keys ... and auto-resetting trigger vlaues):

visual-relevant, visual-irrelevant, auditory-releveant, auditory-irrelevant

Stuff like this will work predictably as long as the naming is clean. otherwise it's gonna be messy.

This stuff somewhat reminds me of the pandas.groupy object. There you can collaps by factor:

`df_group = data_frame.groupby([['modality','relevance']])`

This would lead to collapse direction.
But in the pandas case it's easier because you have column labels (factors) and row entries as factor levels. To imiate this we would at least need some sort of factorial indexing which relates certain triggers to a factor. But that would be easier on the long run, I think.

Putting all this in `__getitem__` might be too much. Maybe a groupy / subset function or a method would be cleaner. It could then also take arguments regarding key renaming and value resetting, etc. 

Or maybe we need to add two things. Regexp without collapsing, just get what is matched + additional functionality to easily handle collapsing along one or multiple dimensions of the design. 

I fear we need to sit down together to converge on this...

I agree with @dengemann  that we need to account for non fully-factorial design. Also, note that not all designs are categorical. For instance, one may code for the contrast of a stimulus, the angle of a bar, the pitch of a sound etc, so, ideally, one could give a name, a number, or a Boolean as a code.

At the moment, I am using a separate object coding for events and checking this conditions of interest in a small loop e.g.
<code>
selection = [((e['modality']=='visual' or e['modality']=='auditory') and e['relevant']==True)  for e in events]
</code>
<code>
epochs[selection]
</code>

So ideally, this separate object would actually be integrated within the epochs.

> So ideally, this separate object would actually be integrated within the
> epochs.
> 
> can you show the code of this object? I understand the examples but I miss
> the big picture to think of the best API.

hmm ... if we go for regexp without collapsing -- that is simply return all trials that match the criteria, wouldn't that do the trick? Numbers etc can all be coded in the dictionary keys. Why do we actually need to have new methods for collapsing because we already have `combine_event_ids` that does that, no?

yes, but then you could also argue not to add anything new. You can do all this by hand, using dicts, dict comprehension and the functions we have. it wll only take you a couple of lines. the question is if we can simplify it

ok suggestion. Let's say event_id can be 

event_id = {'left/auditory': 1, 'right/auditory':2, 'left/visual':3, 'right/visual':4}

and epochs['visual'] returns all visual epochs?

or maybe more readable

event_id = {'left__auditory': 1, 'right__auditory':2, 'left__visual':3, 'right__visual':4}

From a linguistics perspective and in a regression context, I agree with @kingjr continuous predictors would be very cool.

If that's of any help, with @dengemann, we now use panda.DataFrame so as to easily deal with multiple discrete and continuous conditions.

can you be more explicit how you do this within epochs in mne?

I do it outside..

``````
events = pd.DataFrame({cond1='foo', cond2=0.10}, {cond1='bar', cond2=3.})
selection = np.where(events['cond1']=='foo' & events['cond2']>1)[0]
epochs[selection].average()```
``````

if it's 3 lines of code why making the MNE code more complex???

> if it's 3 lines of code why making the MNE code more complex???

+1

> if it's 3 lines of code why making the MNE code more complex???
> I agree. The only minus is that keeping the two objects separated increases the risk of breaking their link. 
> 
> ```
> # sub selection of epochs
> sel = events[modality=='visual']
> epochs_vis = all_epochs[sel]
> # keep cond2 in events for further processing
> epochs_vis.events[:, 2] = events['left_right'][sel]   # whoo, careful what you do here boy
> ```
> 
> as opposed to
> 
> ```
> epochs_vis = all_epochs[sel]
> print epochs_vis.events['left_right']  # Ooh yeah! neat
> ```

But obviously, changing the mne.events into a pd.dataframe is rather expensive, so let's forget about this.

nobody thinks my:

event_id = {'left__auditory': 1, 'right__auditory':2, 'left__visual':3, 'right__visual':4}
epochs['visual']

would solve a practical problem?

I'm +1 on this

It would be useful, yeah. I am had not been pushing for it because I have built a system to take care of this in my own analysis pipelines already, as I imagine others have, too. That being said, thinking about it now, it would be _really_ nice to just get rid of that code and would probably simplify things at my end, actually, so +1 from me, too. If we got it in for 0.9 we could get feedback on it, too. Anybody have the bandwidth to implement it? If not, I might be able to squeeze it in next week.

Why use `__` instead of `/`, just because you expect people might have used `/` but not `__`? We could have a `condition_separator` argument that defaults to `/` that people could change -- I think `/` is a more natural choice. It naturally lends itself to the interpretation "or" or "hierarchy" (like a filesystem), which is nice.

No strong feeling. __ is used in django and scikit-learn but / is the
Eeglab way apparently. 

It's a really new EEGLAB thing though, I doubt many people would expect it. Though I agree with @Eric89GXL 's reasoning - / looks like hierarchy more.

ok but when you do visual__left  it's not a hierarchy but more multi labels.

I think that `/` is what I'd expect to be the separator as a user, as it can actually be interpreted either way. In fact I'd probably do `visual/left` or `/visual/left` depending if I wanted to think of it as multi labels or hierarchy...

Whatever on my side. As long as we document it. 

Would this be as simple as inserting this

```
if (any(["/" in k for k in self.event_id.keys()]) 
       and key not in self.event_id.keys()):
    key = [k for k in list(self.event_id.keys()) if key in k.split("/")]
```

in Epoch's `__getitem__` method? Or am I missing something?

Pretty much, I was thinking something similar, plus a bunch of tests to make sure it actually works as intended :) If you have time (and no other PRs pending for release!), please implement. I guess you have https://github.com/mne-tools/mne-python/pull/1795 but you're not the blocker there.

...also the seperator should be configurable via a property.

I think we have 50% of the PR here ;) the rest is a test ;)

...or more like 1-5% of the PR if we're counting by lines :)

I have another somewhat ready PR but it's not for 0.9, so I guess I'll try making a PR today or tomorrow.

@Eric89GXL specify separator string when the Epochs object is constructed? Like,
`epochs = mne.Epoch(raw, events, event_id, event_sep="__")`
With it defaulting to "/"

Let's not add yet another an argument to the constructor, people can just set it after the fact if they want like `epoch.event_sep = '__'`

-1 on allowing an option to set the separator. It seems like a YAGNI to me.
We should agree and stick to it.

Fair enough. I feel like `/` is more natural, so that gets my +1.

fair enough too

any other thought?

I'm setting it up so that for multiple matching, you can pass either a single string, or a list. If a list, it selects every event id that contains all items in the list. So epochs[['audio', 'left']] matches 'audio/left' and 'audio/left/loud' and also 'audio/loud/left', but not 'audio/right'.

This also means the linear order of '/'-separated items in the event ids is ignored.

Good?


Resolving patch:
diff --git a/examples/plot_topo_compare_conditions.py b/examples/plot_topo_compare_conditions.py
--- a/examples/plot_topo_compare_conditions.py
+++ b/examples/plot_topo_compare_conditions.py
@@ -52,22 +52,25 @@
                        include=include, exclude='bads')
 
 # Create epochs including different events
-epochs = mne.Epochs(raw, events, dict(audio_l=1, visual_r=3), tmin, tmax,
+event_id = {'audio/left': 1, 'audio/right': 2,
+            'visual/left': 3, 'visual/right': 4}
+epochs = mne.Epochs(raw, events, event_id, tmin, tmax,
                     picks=picks, baseline=(None, 0), reject=reject)
 
 # Generate list of evoked objects from conditions names
-evokeds = [epochs[name].average() for name in ('audio_l', 'visual_r')]
+evokeds = [epochs[name].average() for name in ('left', 'right')]
 
 ###############################################################################
 # Show topography for two different conditions
 
 colors = 'yellow', 'green'
-title = 'MNE sample data - left auditory and visual'
+title = 'MNE sample data - left vs right (A/V combined)'
 
 plot_topo(evokeds, color=colors, title=title)
 
 conditions = [e.comment for e in evokeds]
 for cond, col, pos in zip(conditions, colors, (0.025, 0.07)):
-    plt.figtext(0.775, pos, cond, color=col, fontsize=12)
+    plt.figtext(0.99, pos, cond, color=col, fontsize=12,
+                horizontalalignment='right')
 
 plt.show()
diff --git a/mne/epochs.py b/mne/epochs.py
--- a/mne/epochs.py
+++ b/mne/epochs.py
@@ -61,13 +61,13 @@ def __init__(self, info, event_id, tmin, tmax, baseline=(None, 0),
         self.name = name
 
         if isinstance(event_id, dict):
-            if not all([isinstance(v, int) for v in event_id.values()]):
+            if not all(isinstance(v, int) for v in event_id.values()):
                 raise ValueError('Event IDs must be of type integer')
-            if not all([isinstance(k, string_types) for k in event_id]):
+            if not all(isinstance(k, string_types) for k in event_id):
                 raise ValueError('Event names must be of type str')
             self.event_id = event_id
         elif isinstance(event_id, list):
-            if not all([isinstance(v, int) for v in event_id]):
+            if not all(isinstance(v, int) for v in event_id):
                 raise ValueError('Event IDs must be of type integer')
             self.event_id = dict(zip((str(i) for i in event_id), event_id))
         elif isinstance(event_id, int):
@@ -779,10 +779,21 @@ class Epochs(_BaseEpochs, ToDataFrameMixin):
         Return Epochs object with a subset of epochs corresponding to an
         experimental condition as specified by 'name'.
 
+        If conditions are tagged by names separated by '/' (e.g. 'audio/left',
+        'audio/right'), and 'name' is not in itself an event key, this selects
+        every event whose condition contains the 'name' tag (e.g., 'left'
+        matches 'audio/left' and 'visual/left'; but not 'audio_left'). Note
+        that tags like 'auditory/left' and 'left/auditory' will be treated the
+        same way when accessed using tags.
+
     epochs[['name_1', 'name_2', ... ]] : Epochs
         Return Epochs object with a subset of epochs corresponding to multiple
         experimental conditions as specified by 'name_1', 'name_2', ... .
 
+        If conditions are separated by '/', selects every item containing every
+        list tag (e.g. ['audio', 'left'] selects 'audio/left' and
+        'audio/center/left', but not 'audio/right').
+
     See also
     --------
     mne.epochs.combine_event_ids
@@ -1233,6 +1244,17 @@ def __getitem__(self, key):
             key = [key]
 
         if isinstance(key, (list, tuple)) and isinstance(key[0], string_types):
+            if any('/' in k_i for k_i in epochs.event_id.keys()):
+                if any(k_e not in epochs.event_id for k_e in key):
+                    # Select a given key if the requested set of
+                    # '/'-separated types are a subset of the types in that key
+                    key = [k for k in epochs.event_id.keys()
+                           if all(set(k_i.split('/')).issubset(k.split('/'))
+                                  for k_i in key)]
+                    if len(key) == 0:
+                        raise KeyError('Attempting selection of events via '
+                                       'multiple/partial matching, but no '
+                                       'event matches all criteria.')
             select = np.any(np.atleast_2d([epochs._key_match(k)
                                            for k in key]), axis=0)
             epochs.name = ('+'.join(key) if epochs.name == 'Unknown'
@@ -1860,7 +1882,7 @@ def equalize_epoch_counts(epochs_list, method='mintime'):
         list. If 'mintime', timing differences between each event list will be
         minimized.
     """
-    if not all([isinstance(e, Epochs) for e in epochs_list]):
+    if not all(isinstance(e, Epochs) for e in epochs_list):
         raise ValueError('All inputs must be Epochs instances')
 
     # make sure bad epochs are dropped
@@ -2195,7 +2217,7 @@ def add_channels_epochs(epochs_list, name='Unknown', add_eeg_ref=True,
     epochs : Epochs
         Concatenated epochs.
     """
-    if not np.all([e.preload for e in epochs_list]):
+    if not all(e.preload for e in epochs_list):
         raise ValueError('All epochs must be preloaded.')
 
     info = _merge_info([epochs.info for epochs in epochs_list])
@@ -2213,9 +2235,9 @@ def add_channels_epochs(epochs_list, name='Unknown', add_eeg_ref=True,
         raise RuntimeError(err)
 
     events = epochs_list[0].events.copy()
-    all_same = np.all([np.array_equal(events, epochs.events)
-                       for epochs in epochs_list[1:]], axis=0)
-    if not np.all(all_same):
+    all_same = all(np.array_equal(events, epochs.events)
+                   for epochs in epochs_list[1:])
+    if not all_same:
         raise ValueError('Events must be the same.')
 
     proj = any(e.proj for e in epochs_list) or add_eeg_ref


Test patch:
diff --git a/mne/tests/test_epochs.py b/mne/tests/test_epochs.py
--- a/mne/tests/test_epochs.py
+++ b/mne/tests/test_epochs.py
@@ -135,14 +135,25 @@ def test_epoch_combine_ids():
     """Test combining event ids in epochs compared to events
     """
     raw, events, picks = _get_data()
-    for preload in [False]:
-        epochs = Epochs(raw, events, {'a': 1, 'b': 2, 'c': 3,
-                                      'd': 4, 'e': 5, 'f': 32},
-                        tmin, tmax, picks=picks, preload=preload)
-        events_new = merge_events(events, [1, 2], 12)
-        epochs_new = combine_event_ids(epochs, ['a', 'b'], {'ab': 12})
-        assert_array_equal(events_new, epochs_new.events)
-        # should probably add test + functionality for non-replacement XXX
+    epochs = Epochs(raw, events, {'a': 1, 'b': 2, 'c': 3,
+                                  'd': 4, 'e': 5, 'f': 32},
+                    tmin, tmax, picks=picks, preload=False)
+    events_new = merge_events(events, [1, 2], 12)
+    epochs_new = combine_event_ids(epochs, ['a', 'b'], {'ab': 12})
+    assert_array_equal(events_new, epochs_new.events)
+    # should probably add test + functionality for non-replacement XXX
+
+
+def test_epoch_multi_ids():
+    """Test epoch selection via multiple/partial keys
+    """
+    raw, events, picks = _get_data()
+    epochs = Epochs(raw, events, {'a/b/a': 1, 'a/b/b': 2, 'a/c': 3,
+                                  'b/d': 4, 'a_b': 5},
+                    tmin, tmax, picks=picks, preload=False)
+    epochs_regular = epochs[['a', 'b']]
+    epochs_multi = epochs[['a/b/a', 'a/b/b']]
+    assert_array_equal(epochs_regular.events, epochs_multi.events)
 
 
 def test_read_epochs_bad_events():