Repository: mne-tools/mne-python

Issue:
HED with evokeds
HED tags no longer work as desired if you're using `linear_regression_raw`. the output is a dictionary of evokeds. so for instance you have the tags `aud/primed`, `aud/unprimed`, `vis/primed`, `vis/unprimed` if you wanted to look at the grand average from here of `aud`, you would need use `mne.grand_average` as opposed to `Epochs['aud'].average()`.
I guess the point I'm trying to ask is why is there no container for collection of evokeds. They are usually returned as a list.



Discussion:
To avoid API and object flooding. In the past I pulled out the hierarchical
dict part as a function to work with any dictionary. Maybe we can provide
such a tool in `mne.event.py`

On Fri, Feb 12, 2016 at 7:34 AM, Teon L Brooks notifications@github.com
wrote:

> HED tags no longer work as desired if you're using linear_regression_raw.
> the output is a dictionary of evokeds. so for instance you have the tags
> aud/primed, aud/unprimed, vis/primed, vis/unprimed if you wanted to look
> at the grand average from here of aud, you would need use
> mne.grand_average as opposed to Epochs['aud'].average().
> I guess the point I'm trying to ask is why is there no container for
> collection of evokeds. They are usually returned as a list.
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/mne-tools/mne-python/issues/2875.

There is an important difference here between averaging and rERPs: you can't really estimate your 2x2 design to give you 0/0, 0/1, 1/0, 1/1 as distinct evokeds, because the design matrix would not be full rank. One of the contrasts is fully redundant - e.g., if you know 0/0, 0/1, 1/0, you can perfectly predict 1/1. This means perfect multicollinearity, an unidentifiable model, and in the end an uninterpretable coefficient series. Instead, you could for example estimate 1. shared by all (just the mean evoked potential), 2. primed/unprimed, 3. auditory/visual (and possibly 4. an interaction). 2 and 3 (and 4) would correspond to difference waveforms.

Viewed like that, HED doesn't make sense for rERPs all that often.

On the other hand, it still does make sense in a few situations, and I think it might be worth having a dict-like object that works with HED selection. However, its `.get()` should IMO always return one evoked object - e.g., return the (unweighted) average of all select effects.

-1 on more objects for such a thing. A generic key selector function pulled out of epochs get item would be easier to maintain. 

> On 12 Feb 2016, at 12:59, jona-sassenhagen notifications@github.com wrote:
> 
> Tthere is an important difference here between averaging and rERPs: you can't really estimate your 2x2 design to give you 0/0, 0/1, 1/0, 1/1 as distinct evokeds, because the design matrix would not be full rank. One of the contrasts is fully redundant - e.g., if you know 0/0, 0/1, 1/0, you can perfectly predict 1/1. This means multicollinearity, an unidentifiable model, and in the end an uninterpretable coefficient series. Instead, you could for example estimate 1. shared by all (just the mean evoked potential), 2. primed/unprimed, 3. auditory/visual (and possibly 4. an interaction). 2 and 3 (and 4) would correspond to difference waveforms.
> 
> Viewed like that, HED doesn't make sense for rERPs all that often.
> 
> On the other hand, it still does make sense in a few situations, and I think it might be worth having a dict-like object that works with HED selection. However, its .get() should IMO always return one evoked object - e.g., return the (unweighted) average of all select effects.
> 
> —
> Reply to this email directly or view it on GitHub.

> Viewed like that, HED doesn't make sense for rERPs all that often

I agree. You should use an "aud" event in the rERP design to extract the
auditory response.

Teon in general, if it helps look at this:

``` Python

def subselect_keys(key, mapping, sep='/'):
    if isinstance(key, str):
        key = [key]

    mapping = deepcopy(mapping)

    if any(sep in k_i for k_i in mapping.keys()):
        if any(k_e not in mapping 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

            for k in mapping.keys():
                if not all(set(k_i.split('/')).issubset(k.split('/'))
                           for k_i in key):
                        del mapping[k]

            if len(key) == 0:
                raise KeyError('Attempting selection of keys via '
                               'multiple/partial matching, but no '
                               'event matches all criteria.')
    else:
        raise ValueError('Your keys are bad formatted.')
    return mapping
```

I'd support adding such a thing in some utils module.

We have the HED code basically twice - in epochy.py in lines 1355ff and 1600ff - once for the epochs get, and once for `equalize_event_counts`. So maybe we should refactor it out and it would be available as a private function anyways.

Btw a dict of evokeds is a really typical pattern in my MNE code (even when not using rERPs) and I think it's very useful in general, maybe there should be an example somewhere.

Yes, exactly I would put such a function somewhere and use it in all other context, it is actually pretty general and works with any dictionary and as you see it also is easy to use custom separators.

@teonlamont rename into "Refactor HED"? Or make a new issue?

sorry, I was just trying to give a simple example but that isn't the use case where this occurs. The HED is useful when you are trying to account for overlapping events but you are not actually interested in some of the events. you would have the fully factored out HED tag, say it's like w/x/y/z but you are actually only interested in x/y versus x/v. 

this could also be repurposed for the refactoring. 

@dengemann if you have the time ...

@dengemann do you have time to tackle this in the next two weeks?

Removing milestone


Resolving patch:
diff --git a/mne/epochs.py b/mne/epochs.py
--- a/mne/epochs.py
+++ b/mne/epochs.py
@@ -1201,11 +1201,10 @@ def __repr__(self):
         class_name = 'Epochs' if class_name == '_BaseEpochs' else class_name
         return '<%s  |  %s>' % (class_name, s)
 
-    def _key_match(self, key):
-        """Helper function for event dict use"""
-        if key not in self.event_id:
-            raise KeyError('Event "%s" is not in Epochs.' % key)
-        return self.events[:, 2] == self.event_id[key]
+    def _keys_to_idx(self, keys):
+        """Find entries in event dict."""
+        return np.array([self.events[:, 2] == self.event_id[k]
+                         for k in _hid_match(self.event_id, keys)]).any(axis=0)
 
     def __getitem__(self, item):
         """Return an Epochs object with a copied subset of epochs
@@ -1255,28 +1254,15 @@ def __getitem__(self, item):
         self._data, epochs._data = data, data
         del self
 
-        key = item
-        del item
-        if isinstance(key, string_types):
-            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 isinstance(item, string_types):
+            item = [item]
+
+        if isinstance(item, (list, tuple)) and \
+                isinstance(item[0], string_types):
+            select = epochs._keys_to_idx(item)
+            epochs.name = '+'.join(item)
         else:
-            select = key if isinstance(key, slice) else np.atleast_1d(key)
+            select = item if isinstance(item, slice) else np.atleast_1d(item)
 
         key_selection = epochs.selection[select]
         for k in np.setdiff1d(epochs.selection, key_selection):
@@ -1537,12 +1523,7 @@ def equalize_event_counts(self, event_ids, method='mintime', copy=None):
                                      "orthogonal selection.")
 
         for eq in event_ids:
-            eq = np.atleast_1d(eq)
-            # eq is now a list of types
-            key_match = np.zeros(epochs.events.shape[0])
-            for key in eq:
-                key_match = np.logical_or(key_match, epochs._key_match(key))
-            eq_inds.append(np.where(key_match)[0])
+            eq_inds.append(np.where(epochs._keys_to_idx(eq))[0])
 
         event_times = [epochs.events[e, 0] for e in eq_inds]
         indices = _get_drop_indices(event_times, method)
@@ -1553,6 +1534,36 @@ def equalize_event_counts(self, event_ids, method='mintime', copy=None):
         return epochs, indices
 
 
+def _hid_match(event_id, keys):
+    """Match event IDs using HID selection.
+
+    Parameters
+    ----------
+    event_id : dict
+        The event ID dictionary.
+    keys : list | str
+        The event ID or subset (for HID), or list of such items.
+
+    Returns
+    -------
+    use_keys : list
+        The full keys that fit the selection criteria.
+    """
+    # form the hierarchical event ID mapping
+    keys = [keys] if not isinstance(keys, (list, tuple)) else keys
+    use_keys = []
+    for key in keys:
+        if not isinstance(key, string_types):
+            raise KeyError('keys must be strings, got %s (%s)'
+                           % (type(key), key))
+        use_keys.extend(k for k in event_id.keys()
+                        if set(key.split('/')).issubset(k.split('/')))
+    if len(use_keys) == 0:
+        raise KeyError('Event "%s" is not in Epochs.' % key)
+    use_keys = list(set(use_keys))  # deduplicate if necessary
+    return use_keys
+
+
 def _check_baseline(baseline, tmin, tmax, sfreq):
     """Helper to check for a valid baseline"""
     if baseline is not None:
diff --git a/tutorials/plot_epoching_and_averaging.py b/tutorials/plot_epoching_and_averaging.py
--- a/tutorials/plot_epoching_and_averaging.py
+++ b/tutorials/plot_epoching_and_averaging.py
@@ -151,7 +151,7 @@
 epochs_left = epochs['Left']
 
 # ... or to select a very specific subset. This is the same as above:
-evoked_left = epochs['Auditory', 'Left'].average(picks=picks)
+evoked_left = epochs['Left/Auditory'].average(picks=picks)
 
 ###############################################################################
 # Finally, let's plot the evoked responses.


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
@@ -72,6 +72,28 @@ def _get_data(preload=False):
 flat = dict(grad=1e-15, mag=1e-15)
 
 
+def test_hierarchical():
+    """Test hierarchical access."""
+    raw, events, picks = _get_data()
+    event_id = {'a/1': 1, 'a/2': 2, 'b/1': 3, 'b/2': 4}
+    epochs = Epochs(raw, events, event_id, add_eeg_ref=False, preload=True)
+    epochs_a1 = epochs['a/1']
+    epochs_a2 = epochs['a/2']
+    epochs_b1 = epochs['b/1']
+    epochs_b2 = epochs['b/2']
+    epochs_a = epochs['a']
+    assert_equal(len(epochs_a), len(epochs_a1) + len(epochs_a2))
+    epochs_b = epochs['b']
+    assert_equal(len(epochs_b), len(epochs_b1) + len(epochs_b2))
+    epochs_1 = epochs['1']
+    assert_equal(len(epochs_1), len(epochs_a1) + len(epochs_b1))
+    epochs_2 = epochs['2']
+    assert_equal(len(epochs_2), len(epochs_a2) + len(epochs_b2))
+    epochs_all = epochs[('1', '2')]
+    assert_equal(len(epochs), len(epochs_all))
+    assert_array_equal(epochs.get_data(), epochs_all.get_data())
+
+
 @slow_test
 @testing.requires_testing_data
 def test_average_movements():
@@ -506,9 +528,13 @@ def test_epoch_multi_ids():
     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, add_eeg_ref=False)
-    epochs_regular = epochs[['a', 'b']]
+    epochs_regular = epochs['a/b']
+    epochs_reverse = epochs['b/a']
     epochs_multi = epochs[['a/b/a', 'a/b/b']]
-    assert_array_equal(epochs_regular.events, epochs_multi.events)
+    assert_array_equal(epochs_multi.events, epochs_regular.events)
+    assert_array_equal(epochs_reverse.events, epochs_regular.events)
+    assert_allclose(epochs_multi.get_data(), epochs_regular.get_data())
+    assert_allclose(epochs_reverse.get_data(), epochs_regular.get_data())
 
 
 def test_read_epochs_bad_events():