Repository: mne-tools/mne-python

Issue:
BUG: sklearn cross_val_predict changed, now incompatible with SlidingEstimator with predict proba
sklearn `cross_val_predict` has recently changed, and is now incompatible with SlidingEstimator with classification estimators, as it requires `n_classes`

```python
import numpy as np
from mne.decoding import SlidingEstimator
from sklearn.linear_model import LogisticRegression, LinearRegression
from sklearn.model_selection import cross_val_predict
X = np.random.randn(100, 20, 10)
y = np.random.randint(0, 2, 100)

base_estimator = LinearRegression()
cross_val_predict(SlidingEstimator(base_estimator), X, y)  # ok

base_estimator = LogisticRegression()
cross_val_predict(SlidingEstimator(base_estimator), X, y)  # ok

base_estimator = LogisticRegression()
cross_val_predict(SlidingEstimator(base_estimator), X, y,
                             method='predict_proba')  # not ok
```

Three possible fixes:
~~1) Detect when `base_estimator ` is classifier, and add n_classes attribute to SlidingEstimator~~
2) make `mne.decoding.cross_val_multipredict`, similar to `mne.decoding.cross_val_multiscore`
3) add a 'method' argument to SlidingEstimator

Discussion:

Patch:
diff --git a/.gitignore b/.gitignore
index bb2f48639..3f14a575e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -68,3 +68,8 @@ examples/visualization/foobar.html
 
 # Visual Studio Code
 .vscode
+
+# Emacs
+*.py#
+*.rst#
+
diff --git a/doc/whats_new.rst b/doc/whats_new.rst
index 6735b45cd..75797cfdb 100644
--- a/doc/whats_new.rst
+++ b/doc/whats_new.rst
@@ -84,6 +84,8 @@ Bug
 
 - Fix error when running LCMV on MEG channels with compensation using reference channels (like for CTF data) by `Alex Gramfort`_
 
+- Fix the use of :func:`sklearn.model_selection.cross_val_predict` with :class:`mne.decoding.SlidingEstimator` by `Alex Gramfort`_
+
 API
 ~~~
 
diff --git a/mne/cuda.py b/mne/cuda.py
index d2660d2f8..c322b89cb 100644
--- a/mne/cuda.py
+++ b/mne/cuda.py
@@ -377,10 +377,7 @@ def fft_resample(x, W, new_len, npads, to_removes, cuda_dict=None,
 
     # now let's trim it back to the correct size (if there was padding)
     if (to_removes > 0).any():
-        keep = np.ones((new_len), dtype='bool')
-        keep[:to_removes[0]] = False
-        keep[-to_removes[1]:] = False
-        y = np.compress(keep, y)
+        y = y[to_removes[0]:y.shape[0] - to_removes[1]]
 
     return y
 
diff --git a/mne/datasets/utils.py b/mne/datasets/utils.py
index d445bb413..0421ece4b 100644
--- a/mne/datasets/utils.py
+++ b/mne/datasets/utils.py
@@ -235,7 +235,7 @@ def _data_path(path=None, force_update=False, update_path=True, download=True,
     path = _get_path(path, key, name)
     # To update the testing or misc dataset, push commits, then make a new
     # release on GitHub. Then update the "releases" variable:
-    releases = dict(testing='0.50', misc='0.3')
+    releases = dict(testing='0.53', misc='0.3')
     # And also update the "hashes['testing']" variable below.
 
     # To update any other dataset, update the data archive itself (upload
@@ -315,7 +315,7 @@ def _data_path(path=None, force_update=False, update_path=True, download=True,
         sample='fc2d5b9eb0a144b1d6ba84dc3b983602',
         somato='77a7601948c9e38d2da52446e2eab10f',
         spm='9f43f67150e3b694b523a21eb929ea75',
-        testing='c67a798e6170caa634f147a732b8de8d',
+        testing='186b4e899b182fcfb3453aefff4733c5',
         multimodal='26ec847ae9ab80f58f204d09e2c08367',
         visual_92_categories=['74f50bbeb65740903eadc229c9fa759f',
                               '203410a98afc9df9ae8ba9f933370e20'],
diff --git a/mne/decoding/search_light.py b/mne/decoding/search_light.py
index a636134f2..6009c0f55 100644
--- a/mne/decoding/search_light.py
+++ b/mne/decoding/search_light.py
@@ -279,6 +279,14 @@ class SlidingEstimator(BaseEstimator, TransformerMixin):
         score = np.concatenate(score, axis=0)
         return score
 
+    @property
+    def classes_(self):
+        if not hasattr(self.estimators_[0], 'classes_'):
+            raise AttributeError('classes_ attribute available only if '
+                                 'base_estimator has it, and estimator %s does'
+                                 ' not' % (self.estimators_[0],))
+        return self.estimators_[0].classes_
+
 
 def _sl_fit(estimator, X, y, **fit_params):
     """Aux. function to fit SlidingEstimator in parallel.
diff --git a/mne/io/brainvision/brainvision.py b/mne/io/brainvision/brainvision.py
index 665948095..25a854b57 100644
--- a/mne/io/brainvision/brainvision.py
+++ b/mne/io/brainvision/brainvision.py
@@ -5,10 +5,9 @@
 #          Christian Brodbeck <christianbrodbeck@nyu.edu>
 #          Eric Larson <larson.eric.d@gmail.com>
 #          Jona Sassenhagen <jona.sassenhagen@gmail.com>
-#          Phillip Alday <phillip.alday@mpi.nl>
+#          Phillip Alday <phillip.alday@unisa.edu.au>
 #          Okba Bekhelifi <okba.bekhelifi@gmail.com>
 #          Stefan Appelhoff <stefan.appelhoff@mailbox.org>
-#
 # License: BSD (3-clause)
 
 import os
@@ -33,7 +32,7 @@ from ...externals.six.moves import configparser
 
 
 class RawBrainVision(BaseRaw):
-    """Raw object from a Brain Vision EEG file.
+    """Raw object from Brain Vision EEG file.
 
     Parameters
     ----------
@@ -88,7 +87,6 @@ class RawBrainVision(BaseRaw):
     See Also
     --------
     mne.io.Raw : Documentation of attribute and methods.
-
     """
 
     @verbose
@@ -176,7 +174,6 @@ class RawBrainVision(BaseRaw):
         events : array, shape (n_events, 3)
             Events, each row consisting of an (onset, duration, trigger)
             sequence.
-
         """
         return self._events.copy()
 
@@ -188,7 +185,6 @@ class RawBrainVision(BaseRaw):
         events : array, shape (n_events, 3)
             Events, each row consisting of an (onset, duration, trigger)
             sequence.
-
         """
         self._create_event_ch(events)
 
@@ -197,6 +193,7 @@ class RawBrainVision(BaseRaw):
         if n_samp is None:
             n_samp = self.last_samp - self.first_samp + 1
         events = np.array(events, int)
+
         if events.ndim != 2 or events.shape[1] != 3:
             raise ValueError("[n_events x 3] shaped array required")
         # update events
@@ -248,7 +245,6 @@ def _read_vmrk_events(fname, event_id=None, trig_shift_by_type=None):
     events : array, shape (n_events, 3)
         An array containing the whole recording's events, each row representing
         an event as (onset, duration, trigger) sequence.
-
     """
     if event_id is None:
         event_id = dict()
@@ -307,7 +303,7 @@ def _read_vmrk_events(fname, event_id=None, trig_shift_by_type=None):
         txt = txt.decode('latin-1')
 
     # extract Marker Infos block
-    m = re.search(r"\[Marker Infos\]", txt)
+    m = re.search(r"\[Marker Infos\]", txt, re.IGNORECASE)
     if not m:
         return np.zeros((0, 3))
     mk_txt = txt[m.end():]
@@ -372,6 +368,7 @@ def _check_hdr_version(header):
 def _check_mrk_version(header):
     """Check the marker version."""
     tags = ['Brain Vision Data Exchange Marker File, Version 1.0',
+            'Brain Vision Data Exchange Marker File Version 1.0',
             'Brain Vision Data Exchange Marker File, Version 2.0']
     if header not in tags:
         raise ValueError("Currently only support %r, not %r"
@@ -429,7 +426,6 @@ def _get_vhdr_info(vhdr_fname, eog, misc, scale, montage):
         A dict containing Brain Vision specific parameters.
     events : array, shape (n_events, 3)
         Events from the corresponding vmrk file.
-
     """
     scale = float(scale)
     ext = op.splitext(vhdr_fname)[-1]
@@ -479,16 +475,22 @@ def _get_vhdr_info(vhdr_fname, eog, misc, scale, montage):
 
     # get sampling info
     # Sampling interval is given in microsec
-    sfreq = 1e6 / cfg.getfloat('Common Infos', 'SamplingInterval')
+    cinfostr = 'Common Infos'
+    if not cfg.has_section(cinfostr):
+        cinfostr = 'Common infos'  # NeurOne BrainVision export workaround
+
+    # get sampling info
+    # Sampling interval is given in microsec
+    sfreq = 1e6 / cfg.getfloat(cinfostr, 'SamplingInterval')
     info = _empty_info(sfreq)
 
-    order = cfg.get('Common Infos', 'DataOrientation')
+    order = cfg.get(cinfostr, 'DataOrientation')
     if order not in _orientation_dict:
         raise NotImplementedError('Data Orientation %s is not supported'
                                   % order)
     order = _orientation_dict[order]
 
-    data_format = cfg.get('Common Infos', 'DataFormat')
+    data_format = cfg.get(cinfostr, 'DataFormat')
     if data_format == 'BINARY':
         fmt = cfg.get('Binary Infos', 'BinaryFormat')
         if fmt not in _fmt_dict:
@@ -504,8 +506,8 @@ def _get_vhdr_info(vhdr_fname, eog, misc, scale, montage):
 
     # locate EEG binary file and marker file for the stim channel
     path = op.dirname(vhdr_fname)
-    data_filename = op.join(path, cfg.get('Common Infos', 'DataFile'))
-    mrk_fname = op.join(path, cfg.get('Common Infos', 'MarkerFile'))
+    data_filename = op.join(path, cfg.get(cinfostr, 'DataFile'))
+    mrk_fname = op.join(path, cfg.get(cinfostr, 'MarkerFile'))
 
     # Try to get measurement date from marker file
     # Usually saved with a marker "New Segment", see BrainVision documentation
@@ -517,7 +519,7 @@ def _get_vhdr_info(vhdr_fname, eog, misc, scale, montage):
         match = re.findall(regexp, line.strip())
 
         # Always take first measurement date we find
-        if match:
+        if match and match[0] != '00000000000000000000':
             date_str = match[0]
             meas_date = datetime.strptime(date_str, '%Y%m%d%H%M%S%f')
 
@@ -534,11 +536,11 @@ def _get_vhdr_info(vhdr_fname, eog, misc, scale, montage):
         info['meas_date'] = DATE_NONE
 
     # load channel labels
-    nchan = cfg.getint('Common Infos', 'NumberOfChannels') + 1
+    nchan = cfg.getint(cinfostr, 'NumberOfChannels') + 1
     n_samples = None
     if order == 'C':
         try:
-            n_samples = cfg.getint('Common Infos', 'DataPoints')
+            n_samples = cfg.getint(cinfostr, 'DataPoints')
         except configparser.NoOptionError:
             logger.warning('No info on DataPoints found. Inferring number of '
                            'samples from the data file size.')
diff --git a/mne/minimum_norm/psf_ctf.py b/mne/minimum_norm/psf_ctf.py
index 360622286..33e79d78c 100644
--- a/mne/minimum_norm/psf_ctf.py
+++ b/mne/minimum_norm/psf_ctf.py
@@ -240,9 +240,6 @@ def _get_matrix_from_inverse_operator(inverse_operator, forward, labels=None,
     # convert identity matrix to evoked data type (pretending it's an epoch)
     ev_id = EvokedArray(id_mat, info=info, tmin=0.)
 
-    snr = 3.0
-    lambda2 = 1.0 / snr ** 2
-
     # apply inverse operator to identity matrix in order to get inverse matrix
     # free orientation constraint not possible because apply_inverse would
     # combined components
diff --git a/tutorials/plot_object_epochs.py b/tutorials/plot_object_epochs.py
index 6ecd2d0cc..9d7391880 100644
--- a/tutorials/plot_object_epochs.py
+++ b/tutorials/plot_object_epochs.py
@@ -75,6 +75,30 @@ print(epochs.event_id)
 print(epochs[1:5])
 print(epochs['Auditory/Right'])
 
+###############################################################################
+# Note the '/'s in the event code labels. These separators allow tag-based
+# selection of epoch sets; every string separated by '/' can be entered, and
+# returns the subset of epochs matching any of the strings. E.g.,
+
+print(epochs['Right'])
+print(epochs['Right', 'Left'])
+
+
+###############################################################################
+# Note that MNE will not complain if you ask for tags not present in the
+# object, as long as it can find some match: the below example is parsed as
+# (inclusive) 'Right' OR 'Left'. However, if no match is found, an error is
+# returned.
+
+epochs_r = epochs['Right']
+epochs_still_only_r = epochs_r[['Right', 'Left']]
+print(epochs_still_only_r)
+
+try:
+    epochs_still_only_r["Left"]
+except KeyError:
+    print("Tag-based selection without any matches raises a KeyError!")
+
 ###############################################################################
 # It is also possible to iterate through :class:`Epochs <mne.Epochs>` objects
 # in this way. Note that behavior is different if you iterate on `Epochs`


Test patch:
diff --git a/mne/decoding/tests/test_search_light.py b/mne/decoding/tests/test_search_light.py
index 38d5db3e5..5c7e08ed1 100644
--- a/mne/decoding/tests/test_search_light.py
+++ b/mne/decoding/tests/test_search_light.py
@@ -2,7 +2,6 @@
 #
 # License: BSD (3-clause)
 
-
 import numpy as np
 from numpy.testing import assert_array_equal, assert_equal
 import pytest
@@ -236,3 +235,38 @@ def test_generalization_light():
         features_shape = pipe.estimators_[0].steps[0][1].features_shape_
         assert_array_equal(features_shape, [3, 4])
     assert_array_equal(y_preds[0], y_preds[1])
+
+
+@requires_version('sklearn', '0.17')
+def test_cross_val_predict():
+    """Test cross_val_predict with predict_proba."""
+    from sklearn.linear_model import LinearRegression
+    from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
+    from sklearn.base import BaseEstimator, clone
+    from sklearn.model_selection import cross_val_predict
+    rng = np.random.RandomState(42)
+    X = rng.randn(10, 1, 3)
+    y = rng.randint(0, 2, 10)
+
+    estimator = SlidingEstimator(LinearRegression())
+    cross_val_predict(estimator, X, y, cv=2)
+
+    class Classifier(BaseEstimator):
+        """Moch class that does not have classes_ attribute."""
+
+        def __init__(self):
+            self.base_estimator = LinearDiscriminantAnalysis()
+
+        def fit(self, X, y):
+            self.estimator_ = clone(self.base_estimator).fit(X, y)
+            return self
+
+        def predict_proba(self, X):
+            return self.estimator_.predict_proba(X)
+
+    with pytest.raises(AttributeError, match="classes_ attribute"):
+        estimator = SlidingEstimator(Classifier())
+        cross_val_predict(estimator, X, y, method='predict_proba', cv=2)
+
+    estimator = SlidingEstimator(LinearDiscriminantAnalysis())
+    cross_val_predict(estimator, X, y, method='predict_proba', cv=2)
diff --git a/mne/io/brainvision/tests/test_brainvision.py b/mne/io/brainvision/tests/test_brainvision.py
index f6beae2b0..afe39accc 100644
--- a/mne/io/brainvision/tests/test_brainvision.py
+++ b/mne/io/brainvision/tests/test_brainvision.py
@@ -22,6 +22,7 @@ from mne.io.constants import FIFF
 from mne.io import read_raw_fif, read_raw_brainvision
 from mne.io.tests.test_raw import _test_raw_reader
 from mne.io.write import DATE_NONE
+from mne.datasets import testing
 
 FILE = inspect.getfile(inspect.currentframe())
 data_dir = op.join(op.dirname(op.abspath(FILE)), 'data')
@@ -52,6 +53,10 @@ vhdr_mixed_lowpass_path = op.join(data_dir, 'test_mixed_lowpass.vhdr')
 vhdr_lowpass_s_path = op.join(data_dir, 'test_lowpass_s.vhdr')
 vhdr_mixed_lowpass_s_path = op.join(data_dir, 'test_mixed_lowpass_s.vhdr')
 
+# VHDR exported with neuroone
+data_path = testing.data_path(download=False)
+neuroone_vhdr = op.join(data_path, 'Brainvision', 'test_NO.vhdr')
+
 # Test for nanovolts as unit
 vhdr_nV_path = op.join(data_dir, 'test_nV.vhdr')
 
@@ -583,4 +588,13 @@ def test_brainvision_with_montage():
             assert_array_equal(r['loc'], n['loc'])
 
 
+@testing.requires_testing_data
+def test_brainvision_neuroone_export():
+    """Test Brainvision file exported with neuroone system."""
+    raw = read_raw_brainvision(neuroone_vhdr, verbose='error')
+    assert raw.info['meas_date'] == DATE_NONE
+    assert len(raw.info['chs']) == 66
+    assert raw.info['sfreq'] == 5000.
+
+
 run_tests_if_main()
diff --git a/mne/tests/test_filter.py b/mne/tests/test_filter.py
index 3305f740f..c88767d25 100644
--- a/mne/tests/test_filter.py
+++ b/mne/tests/test_filter.py
@@ -284,6 +284,16 @@ def test_resample_stim_channel():
         assert new_data.shape[1] == new_data_len
 
 
+def test_resample_raw():
+    """Test resampling using RawArray."""
+    x = np.zeros((1, 1001))
+    sfreq = 2048.
+    raw = RawArray(x, create_info(1, sfreq, 'eeg'))
+    raw.resample(128, npad=10)
+    data = raw.get_data()
+    assert data.shape == (1, 63)
+
+
 @requires_version('scipy', '0.16')
 @pytest.mark.slowtest
 def test_filters():