Repository: mne-tools/mne-python

Issue:
mne.grand_average() behaves differently for evoked data than mne.evoked.grand_average()
#### Describe the bug
`mne.grand_average()` and `mne.evoked.grand_average()` produce different results on the same data: the weights used to calculate the average are not normalized in `mne.grand_average()`.

#### Steps and/or code to reproduce
```python
In[5]: mne.grand_average(evokeds).data.max()
Identifying common channels ...
all channels are corresponding, nothing to do.
Out[5]: 7.730973384174128e-05
```
```python
In[6]: mne.evoked.grand_average(evokeds).data.max()
Identifying common channels ...
all channels are corresponding, nothing to do.
Out[6]: 4.294985213430072e-06
```

#### Expected results
`mne.grand_average()`, when operating on a sequence of `Evoked`s, should produce the  same results as `mne.evoked.grand_average()`

#### Actual results
The results differ. `mne.grand_average()` calls `mne.evoked.combine_evoked()`, which doesn't normalize the weights first:
https://github.com/mne-tools/mne-python/blob/adb70865accfbf2e473d0f78e5be69c090581c0d/mne/utils/numerics.py#L607-L623
https://github.com/mne-tools/mne-python/blob/1189d1a755f4297dcb7212d053f7e1b0d12fd082/mne/evoked.py#L896

`mne.evoked.grand_average()`, on the other hand,  does call `mne.evoked.combine_evoked()` too, but explicitly passes normalized weights, producing the correct result:
https://github.com/mne-tools/mne-python/blob/1189d1a755f4297dcb7212d053f7e1b0d12fd082/mne/evoked.py#L831-L832

#### Additional information
I wonder if it even makes sense that`mne.grand_average()` calls `mne.evoked.combine_evoked()` directly, instead of going through `mne.evoked.grand_average()`, which takes care of the correct weighting.

This is on `master`.


Discussion:
why the heck do we have 2 functions to do this :( :(
one should be deprecated

@agramfort you can assign me, I'll get to it this afternoon. I'm the one who most recently dealt with grand average code I think.

On August 22, 2019 1:34:38 AM PDT, Alexandre Gramfort <notifications@github.com> wrote:
>why the heck do we have 2 functions to do this :( :(
>one should be deprecated


Resolving patch:
diff --git a/mne/evoked.py b/mne/evoked.py
--- a/mne/evoked.py
+++ b/mne/evoked.py
@@ -13,13 +13,12 @@
 
 from .baseline import rescale
 from .channels.channels import (ContainsMixin, UpdateChannelsMixin,
-                                SetChannelsMixin, InterpolationMixin,
-                                equalize_channels)
+                                SetChannelsMixin, InterpolationMixin)
 from .channels.layout import _merge_grad_data, _pair_grad_sensors
 from .filter import detrend, FilterMixin
 from .utils import (check_fname, logger, verbose, _time_mask, warn, sizeof_fmt,
                     SizeMixin, copy_function_doc_to_method_doc, _validate_type,
-                    fill_doc, _check_option)
+                    fill_doc, _check_option, deprecated)
 from .viz import (plot_evoked, plot_evoked_topomap, plot_evoked_field,
                   plot_evoked_image, plot_evoked_topo)
 from .viz.evoked import plot_evoked_white, plot_evoked_joint
@@ -785,7 +784,9 @@ def _get_evoked_node(fname):
     return evoked_node
 
 
-def grand_average(all_evoked, interpolate_bads=True):
+@deprecated('mne.evoked.grand_average is deprecated and will be removed in '
+            'v0.20; use mne.grand_average instead.')
+def grand_average(all_evoked, interpolate_bads=True, drop_bads=False):
     """Make grand average of a list evoked data.
 
     The function interpolates bad channels based on `interpolate_bads`
@@ -804,6 +805,11 @@ def grand_average(all_evoked, interpolate_bads=True):
         The evoked datasets.
     interpolate_bads : bool
         If True, bad MEG and EEG channels are interpolated.
+    drop_bads : bool
+        If True, drop all bad channels marked as bad in any data set.
+        If neither interpolate_bads nor drop_bads is True, in the output file,
+        every channel marked as bad in at least one of the input files will be
+        marked as bad, but no interpolation or dropping will be performed.
 
     Returns
     -------
@@ -814,27 +820,8 @@ def grand_average(all_evoked, interpolate_bads=True):
     -----
     .. versionadded:: 0.9.0
     """
-    # check if all elements in the given list are evoked data
-    if not all(isinstance(e, Evoked) for e in all_evoked):
-        raise ValueError("Not all the elements in list are evoked data")
-
-    # Copy channels to leave the original evoked datasets intact.
-    all_evoked = [e.copy() for e in all_evoked]
-
-    # Interpolates if necessary
-    if interpolate_bads:
-        all_evoked = [e.interpolate_bads() if len(e.info['bads']) > 0
-                      else e for e in all_evoked]
-
-    equalize_channels(all_evoked)  # apply equalize_channels
-    # make grand_average object using combine_evoked
-    weights = [1. / len(all_evoked)] * len(all_evoked)
-    grand_average = combine_evoked(all_evoked, weights=weights)
-    # change the grand_average.nave to the number of Evokeds
-    grand_average.nave = len(all_evoked)
-    # change comment field
-    grand_average.comment = "Grand average (n = %d)" % grand_average.nave
-    return grand_average
+    from ..utils import grand_average
+    return grand_average(all_evoked, interpolate_bads, drop_bads)
 
 
 def _check_evokeds_ch_names_times(all_evoked):
diff --git a/mne/utils/numerics.py b/mne/utils/numerics.py
--- a/mne/utils/numerics.py
+++ b/mne/utils/numerics.py
@@ -556,8 +556,8 @@ def _freq_mask(freqs, sfreq, fmin=None, fmax=None, raise_error=True):
 def grand_average(all_inst, interpolate_bads=True, drop_bads=True):
     """Make grand average of a list evoked or AverageTFR data.
 
-    For evoked data, the function interpolates bad channels based on
-    `interpolate_bads` parameter. If `interpolate_bads` is True, the grand
+    For evoked data, the function interpolates bad channels based on the
+    ``interpolate_bads`` parameter. If ``interpolate_bads`` is True, the grand
     average file will contain good channels and the bad channels interpolated
     from the good MEG/EEG channels.
     For AverageTFR data, the function takes the subset of channels not marked
@@ -610,8 +610,10 @@ def grand_average(all_inst, interpolate_bads=True, drop_bads=True):
                         else inst for inst in all_inst]
         equalize_channels(all_inst)  # apply equalize_channels
         from ..evoked import combine_evoked as combine
+        weights = [1. / len(all_inst)] * len(all_inst)
     else:  # isinstance(all_inst[0], AverageTFR):
         from ..time_frequency.tfr import combine_tfr as combine
+        weights = 'equal'
 
     if drop_bads:
         bads = list({b for inst in all_inst for b in inst.info['bads']})
@@ -620,7 +622,7 @@ def grand_average(all_inst, interpolate_bads=True, drop_bads=True):
                 inst.drop_channels(bads)
 
     # make grand_average object using combine_[evoked/tfr]
-    grand_average = combine(all_inst, weights='equal')
+    grand_average = combine(all_inst, weights=weights)
     # change the grand_average.nave to the number of Evokeds
     grand_average.nave = len(all_inst)
     # change comment field


Test patch:
diff --git a/mne/tests/test_evoked.py b/mne/tests/test_evoked.py
--- a/mne/tests/test_evoked.py
+++ b/mne/tests/test_evoked.py
@@ -16,13 +16,13 @@
 import pytest
 
 from mne import (equalize_channels, pick_types, read_evokeds, write_evokeds,
-                 grand_average, combine_evoked, create_info, read_events,
+                 combine_evoked, create_info, read_events,
                  Epochs, EpochsArray)
 from mne.evoked import _get_peak, Evoked, EvokedArray
 from mne.io import read_raw_fif
 from mne.io.constants import FIFF
 from mne.utils import (_TempDir, requires_pandas, requires_version,
-                       run_tests_if_main)
+                       run_tests_if_main, grand_average)
 
 base_dir = op.join(op.dirname(__file__), '..', 'io', 'tests', 'data')
 fname = op.join(base_dir, 'test-ave.fif')
@@ -541,13 +541,13 @@ def test_arithmetic():
     assert_equal(ch_names, gave.ch_names)
     assert_equal(gave.nave, 2)
     pytest.raises(TypeError, grand_average, [1, evoked1])
-    gave = grand_average([ev1, ev1, ev2])
-    assert_allclose(gave.data, np.ones_like(gave.data))
+    gave = grand_average([ev1, ev1, ev2])  # (1 + 1 + -1) / 3  =  1/3
+    assert_allclose(gave.data, np.full_like(gave.data, 1. / 3.))
 
     # test channel (re)ordering
     evoked1, evoked2 = read_evokeds(fname, condition=[0, 1], proj=True)
     data2 = evoked2.data  # assumes everything is ordered to the first evoked
-    data = (evoked1.data + evoked2.data)
+    data = (evoked1.data + evoked2.data) / 2.
     evoked2.reorder_channels(evoked2.ch_names[::-1])
     assert not np.allclose(data2, evoked2.data)
     with pytest.warns(RuntimeWarning, match='reordering'):