Repository: mne-tools/mne-python

Issue:
ENH: add plotting method / function for global field power
Since we're polishing our sensor space / EEG edge I thought it would be time for it.
Like here: https://gist.github.com/dengemann/deb5e011d7d6b0c60fa9

cc @agramfort



Discussion:
sounds reasonable to me and useful to some of us

cc @jdsitt

I would overlay it in plot_evoked and avoid a new function

cf

http://neuroimage.usc.edu/brainstorm/Tutorials/TutSourceEstimation?action=AttachFile&do=get&target=sources1.gif

> I would overlay it in plot_evoked and avoid a new function

But what if users only want the GFP and not the evoked response?

good point. You would create a plot_global_field_power method?

> You would create a plot_global_field_power method?

see example above

+1 on adding an option to add the GFP to the evoked plot (like @agramfort's link) as well as having a standalone like @adykstra wants. Maybe both could make use of a new method `evoked.compute_global_field_power(meg=True, eeg=False)`? That way the channel-picking and such could be taken care of, even though the actual computation would essentially be a one-liner (right?).

> good point. You would create a plot_global_field_power method?

I guess so, yes.  If there are starting to be too many methods, is it reasonable to consider grouping them?  Such as:

```
evoked.plot.something_or_other?
```

Or is that too much O.O.?

Unfortunately it violates the "flat is better than nested" principle, and @agramfort tries to avoid classes when necessary. It is the same number of characters anyway :)

> flat is better than nested

Is this always taken for granted as part of the Zen?  It seems to me that nested makes a lot of sense in some contexts . . . e.g. <code>mne.io.</code>

Different people think of it differently, to me it's just a guiding principle. I consider the `mne.io.*` a compelling case where nesting makes everyone's (developers and users alike) lives better, whereas I don't get the same sense from `evoked.plot.*`.

I don't see a reason here to have nested code. Let's keep things clean for such a small thing we don't need to sacrifice the zen.

my 2 cents:
- add GFP by default to evoked plot
- add a method based on this https://gist.github.com/dengemann/deb5e011d7d6b0c60fa9 to create pure GFP plots.

> add GFP by default to evoked plot

I'd have it be off by default, with the option to turn it on.

let's start with the option to add gfp to plot evoked
and add the extra function later. I am pretty sure
that 99.786355% (p < 001) people will be
already happy about this

> that 99.786355% (p < 001) people will be

this is not a test, hence, cannot be significant. You should compute a \chi^2 test based on this ratio @agramfort ;-)

"There's lies, damned lies, and statistics."
On Jun 13, 2014 12:25 PM, "Denis A. Engemann" notifications@github.com
wrote:

> that 99.786355% (p < 001) people will be
> 
> this is not a test, hence, cannot be significant. You should compute a
> \chi^2 test based on this ratio @agramfort https://github.com/agramfort
> ;-)
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/mne-tools/mne-python/issues/1276#issuecomment-45996493
> .

in ref to #1586, how about adding an RMS as a function as well?

> in ref to #1586, how about adding an RMS as a function as well?

would you plot RMS only without butterfly? I think the main use case
is to plot both jointly.

ping @teonlamont 

haha, I had the same idea and forgot about it. seems like this could address https://github.com/mne-tools/mne-python/issues/1939. It could be combined with @jona-sassenhagen's suggestion for auto plotting predefined rois, etc. 

Do we need this even though we have the GFP? (We have the GFP right?) If it's about selecting a subset of sensors, can't they do `evoked.pick_types(...)` or `evoked.pick_channels(...).plot_white()` now?

ideally, I would like to have some very simple ways to plot contrasts in sensor space (see discussion #1939). in the way of ERP analysis, it is useful to be able to do one of the following, for a given set of contrasts, e.g. evoked a, and evoked b:
1. select a single channel and plot the contrast of evoked a and evoked b
2. select a region of interest (pick several channels) and plot the contrast. for ERP, plot the average over the channels, for ERF, plot the RMS of the channels.
3. split the channels into pre-defined regions and plot the contrast. ERP, average, ERF, RMS.
(minor addition, the ability to flip the axes for ERPs to be à la mode)

This would be a simple but pretty use addition. it doesn't have to be wrapped in the `plot_gfp`.  it could be something like `plot_contrasts`. It's very useful with teaching, and it is still useful with ERP-related research.

GFP is independent of noise cov for the common use case.

@teonlamont can you open a different issue for that, then? The title and most of the discussion of this one is a bit confusing. 

@agramfort so you're saying we still need a separate function to plot the GFP?

ok, i just reopened the one I closed. this topic is mostly about gfp, but there were mentions of a general plotting function. so it seemed like this might be a relevant topic

> @agramfort https://github.com/agramfort so you're saying we still need
> a separate function to plot the GFP?
> 
> I would add it to plot evoked method. A green line like brainstorm does it.

http://neuroimage.usc.edu/brainstorm/Tutorials/TutSourceEstimation?action=AttachFile&do=get&target=sources1.gif

Does anyone have time for this one?

I'm +0 on this ... We should mark it 0.10, my 2 cents.

Done, if someone feels passionately about it, let them be the one to implement it for 0.9 :)

What would this entail?  Just a gfp trace at the bottom of <code>evoked.plot()</code> and/or a separate gfp plotting method for <code>Evoked</code> objects?  Are we talking the traditional definition from Lehmann and Skrandies using spatial variance?  I've been computing this a lot recently, so it would be relatively straight forward to implement I think.  I would propose the following:
1. An option to <code>evoked.plot()</code> called <code>gfp</code>, defaulted to <code>False</code>.  So the call <code>evoked.plot(gfp=True)</code> would produce the brainstorm-like plot that @agramfort mentions.
2. An <code>Evoked</code> method called <code>plot_gfp</code>, so that the call <code>evoked.plot_gfp()</code> would return only the gfp and not the butterfly.  Perhaps also an optional <code>cov</code> arg, defaulted to <code>None</code> that, if passed, would whiten the <code>Evoked</code> first?  I guess that's maybe not necessary since we already have <code>mne.whiten_evoked()</code>.

+1 for 1.

We have 2 already, `evoked.plot_white`

> We have 2 already, evoked.plot_white

I was wondering about that,  but wouldn't it make sense to have a basic method for <code>Evoked</code> objects for plotting just the gfp?

@teonlamont, are you indifferent or -1 on suggestion 2?

I wanted to say that would be complementary.

> On 23 Apr 2015, at 12:16, "Andrew R. Dykstra" notifications@github.com wrote:
> 
> @teonlamont, are you indifferent or -1 on suggestion 2?
> 
> —
> Reply to this email directly or view it on GitHub.

+1 for 1

plot_white is using a noise cov which makes it different.

how about gfp = True/False/'only'

I'd start with True/False but whatever

Do we really need to keep adding stuff just shortly before releasing?
We used to have a 2 week feature addition stop and usually many bugs
emerged and were fixed after that point.
Would it make sense to release in one or two weaks but stop with
implementations of whatever in 2 days?

2015-04-23 14:51 GMT+02:00 Alexandre Gramfort notifications@github.com:

> I'd start with True/False but whatever
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/mne-tools/mne-python/issues/1276#issuecomment-95575497
> .

+1000

this was not suggested for 0.9

Ah ok. Sorry then. Anyways. We should stop pushing features and spend more days on bug search and fixing.

> Do we really need to keep adding stuff just shortly before releasing?

Doesn't matter to me whether this goes into 0.9 or not - just thought that since no one was picking it up I could.  

I'm gathering that folks are -1 on creating separate gfp plots?  That would actually be my main use case, as opposed to including it at the bottom of an evoked butterfly plot.

@adykstra that's why I suggesting an option to gfp that if it is 'only', then it would plot just the gfp and not the butterfly plot. since it's in evoked.plot, it would save on a separate method. 

Would `gfp=True | False | 'only'` work for your case @adykstra?

Definitely, but a separate method to accomplish it seems cleaner and more explicit to me.  I'd be happy with either though.

I think the push to avoid more functions will make it easier just to have a param. @adykstra you up for making a PR? It may or may not make it into 0.9 depending on what we decide regarding a feature freeze, but it sounds like we want it eventually regardless.

> you up for making a PR?

Sure.

@dengemann, regarding rank, is <code>estimate_rank</code> from <code>mne.utils</code> sufficient?  For the example data, it returns 67 (65) for grads (mags), and 58 for eeg.  Also, if I understand correctly, the rank of SSS'd MEG data should be estimate jointly for grads and mags?  So ... <code>estimate_rank(evoked.pick_types(meg=True).data)</code>?

Rread the source code of evoked.plot_white and find the answers :)
We have special private functions.

> On 25 Apr 2015, at 13:24, "Andrew R. Dykstra" notifications@github.com wrote:
> 
> @dengemann, regarding rank, is estimate_rank from mne.utils sufficient? For the example data, it returns 67 (65) for grads (mags), and 58 for eeg. Also, if I understand correctly, the rank of SSS'd MEG data should be estimate jointly for grads and mags? So ... estimate_rank(evoked.pick_types(meg=True).data)?
> 
> —
> Reply to this email directly or view it on GitHub.


Resolving patch:
diff --git a/examples/plot_from_raw_to_epochs_to_evoked.py b/examples/plot_from_raw_to_epochs_to_evoked.py
--- a/examples/plot_from_raw_to_epochs_to_evoked.py
+++ b/examples/plot_from_raw_to_epochs_to_evoked.py
@@ -35,7 +35,7 @@
 events = mne.read_events(event_fname)
 
 #   Plot raw data
-fig = raw.plot(events=events, event_color={1: 'cyan', -1: 'lightgray'})
+raw.plot(events=events, event_color={1: 'cyan', -1: 'lightgray'})
 
 #   Set up pick list: EEG + STI 014 - bad channels (modify to your needs)
 include = []  # or stim channels ['STI 014']
@@ -65,7 +65,7 @@
 ###############################################################################
 # View evoked response
 
-evoked.plot()
+evoked.plot(gfp=True)
 
 ###############################################################################
 # Save evoked responses for different conditions to disk
diff --git a/mne/evoked.py b/mne/evoked.py
--- a/mne/evoked.py
+++ b/mne/evoked.py
@@ -345,7 +345,7 @@ def shift_time(self, tshift, relative=True):
 
     def plot(self, picks=None, exclude='bads', unit=True, show=True, ylim=None,
              xlim='tight', proj=False, hline=None, units=None, scalings=None,
-             titles=None, axes=None):
+             titles=None, axes=None, gfp=False):
         """Plot evoked data as butterfly plots
 
         Left click to a line shows the channel name. Selecting an area by
@@ -389,11 +389,14 @@ def plot(self, picks=None, exclude='bads', unit=True, show=True, ylim=None,
             The axes to plot to. If list, the list must be a list of Axes of
             the same length as the number of channel types. If instance of
             Axes, there must be only one channel type plotted.
+        gfp : bool | 'only'
+            Plot GFP in green if True or "only". If "only", then the individual
+            channel traces will not be shown.
         """
         return plot_evoked(self, picks=picks, exclude=exclude, unit=unit,
                            show=show, ylim=ylim, proj=proj, xlim=xlim,
                            hline=hline, units=units, scalings=scalings,
-                           titles=titles, axes=axes)
+                           titles=titles, axes=axes, gfp=gfp)
 
     def plot_image(self, picks=None, exclude='bads', unit=True, show=True,
                    clim=None, xlim='tight', proj=False, units=None,
diff --git a/mne/viz/evoked.py b/mne/viz/evoked.py
--- a/mne/viz/evoked.py
+++ b/mne/viz/evoked.py
@@ -122,7 +122,7 @@ def _topo_closed(events, ax, lines, fill):
 def _plot_evoked(evoked, picks, exclude, unit, show,
                  ylim, proj, xlim, hline, units,
                  scalings, titles, axes, plot_type,
-                 cmap=None):
+                 cmap=None, gfp=False):
     """Aux function for plot_evoked and plot_evoked_image (cf. docstrings)
 
     Extra param is:
@@ -140,6 +140,8 @@ def _plot_evoked(evoked, picks, exclude, unit, show,
     if axes is not None and proj == 'interactive':
         raise RuntimeError('Currently only single axis figures are supported'
                            ' for interactive SSP selection.')
+    if isinstance(gfp, string_types) and gfp != 'only':
+        raise ValueError('gfp must be boolean or "only". Got %s' % gfp)
 
     scalings = _handle_default('scalings', scalings)
     titles = _handle_default('titles', titles)
@@ -203,6 +205,8 @@ def _plot_evoked(evoked, picks, exclude, unit, show,
     selectors = list()  # for keeping reference to span_selectors
     path_effects = [patheffects.withStroke(linewidth=2, foreground="w",
                                            alpha=0.75)]
+    gfp_path_effects = [patheffects.withStroke(linewidth=5, foreground="w",
+                                               alpha=0.75)]
     for ax, t in zip(axes, ch_types_used):
         ch_unit = units[t]
         this_scaling = scalings[t]
@@ -239,10 +243,28 @@ def _plot_evoked(evoked, picks, exclude, unit, show,
             # Set amplitude scaling
             D = this_scaling * evoked.data[idx, :]
             if plot_type == 'butterfly':
-                lines.append(ax.plot(times, D.T, picker=3., zorder=0))
-                for ii, line in zip(idx, lines[-1]):
-                    if ii in bad_ch_idx:
-                        line.set_zorder(1)
+                gfp_only = (isinstance(gfp, string_types) and gfp == 'only')
+                if not gfp_only:
+                    lines.append(ax.plot(times, D.T, picker=3., zorder=0))
+                    for ii, line in zip(idx, lines[-1]):
+                        if ii in bad_ch_idx:
+                            line.set_zorder(1)
+                if gfp:  # 'only' or boolean True
+                    gfp_color = (0., 1., 0.)
+                    this_gfp = np.sqrt((D * D).mean(axis=0))
+                    this_ylim = ax.get_ylim()
+                    if not gfp_only:
+                        y_offset = this_ylim[0]
+                    else:
+                        y_offset = 0.
+                    this_gfp += y_offset
+                    ax.fill_between(times, y_offset, this_gfp, color='none',
+                                    facecolor=gfp_color, zorder=0, alpha=0.25)
+                    ax.plot(times, this_gfp, color=gfp_color, zorder=2)
+                    ax.text(times[0] + 0.01 * (times[-1] - times[0]),
+                            this_gfp[0] + 0.05 * np.diff(ax.get_ylim())[0],
+                            'GFP', zorder=3, color=gfp_color,
+                            path_effects=gfp_path_effects)
                 ax.set_ylabel('data (%s)' % ch_unit)
                 # for old matplotlib, we actually need this to have a bounding
                 # box (!), so we have to put some valid text here, change
@@ -309,7 +331,7 @@ def _plot_evoked(evoked, picks, exclude, unit, show,
 
 def plot_evoked(evoked, picks=None, exclude='bads', unit=True, show=True,
                 ylim=None, xlim='tight', proj=False, hline=None, units=None,
-                scalings=None, titles=None, axes=None):
+                scalings=None, titles=None, axes=None, gfp=False):
     """Plot evoked data
 
     Left click to a line shows the channel name. Selecting an area by clicking
@@ -355,11 +377,15 @@ def plot_evoked(evoked, picks=None, exclude='bads', unit=True, show=True,
         The axes to plot to. If list, the list must be a list of Axes of
         the same length as the number of channel types. If instance of
         Axes, there must be only one channel type plotted.
+    gfp : bool | 'only'
+        Plot GFP in green if True or "only". If "only", then the individual
+        channel traces will not be shown.
     """
     return _plot_evoked(evoked=evoked, picks=picks, exclude=exclude, unit=unit,
                         show=show, ylim=ylim, proj=proj, xlim=xlim,
                         hline=hline, units=units, scalings=scalings,
-                        titles=titles, axes=axes, plot_type="butterfly")
+                        titles=titles, axes=axes, plot_type="butterfly",
+                        gfp=gfp)
 
 
 def plot_evoked_topo(evoked, layout=None, layout_scale=0.945, color=None,


Test patch:
diff --git a/mne/viz/tests/test_evoked.py b/mne/viz/tests/test_evoked.py
--- a/mne/viz/tests/test_evoked.py
+++ b/mne/viz/tests/test_evoked.py
@@ -105,6 +105,12 @@ def test_plot_evoked():
                       proj='interactive')
         assert_raises(RuntimeError, evoked_delayed_ssp.plot,
                       proj='interactive', axes='foo')
+        plt.close('all')
+
+        # test GFP plot overlay
+        evoked.plot(gfp=True)
+        evoked.plot(gfp='only')
+        assert_raises(ValueError, evoked.plot, gfp='foo')
 
         evoked.plot_image(proj=True)
         # plot with bad channels excluded