Repository: mne-tools/mne-python

Issue:
Edge artefacts in ReceptiveField
I chatted briefly with @choldgraf about this issue today, but I've been giving the new `ReceptiveField` code a try on some old data today. And I'm getting strange artefacts on the reconstructed signal. 

Here I've been using the stimulus reconstruction approach (backward model) to predict my stimulus from the EEG. The data are 10s epochs, and I have about 50-70 of those.

Here's what the reconstruction looks like (all the reconstructed epochs are concatenated inside the `predict` method). X axis is time samples @ 256Hz.
![figure_3](https://cloud.githubusercontent.com/assets/10333715/25858243/8a84a4ee-34db-11e7-8d08-c7c1ff4edd56.png)
![figure_2](https://cloud.githubusercontent.com/assets/10333715/25858242/8a84b042-34db-11e7-97b1-7b871b4391a7.png)
![figure_1](https://cloud.githubusercontent.com/assets/10333715/25858241/8a6a414e-34db-11e7-9016-5d9d7c6a91a2.png)

The artefact is located in the first `len(time_lags)` samples of each epoch, so I assume it has to do with how the delays are handled in `predict`.



Discussion:
Does this also happen when you pass `Ridge` as your solver? That will tell us whether @choldgraf or I am to blame :)
good point @Eric89GXL - for context, @nbara, these two keywords will run very different pipelines.
Actually I always pass `Ridge` as my solver. Didn't realize it was different from just giving the alpha value. 

```
rf = ReceptiveField(lmin, lmax, fs, feature_names=None,
                    estimator=Ridge(alphas[ii]),
                    scoring='corrcoef',
                    fit_intercept=False)
```

EDIT: passing a float seems to work actually. At least there's no visible artefacts at the edges.
Can you try just passing alpha?

> passing a float seems to work actually. At least there's no visible artefacts at the edges.
Is it easy to share some data to reproduce?

Do you have time to look @choldgraf?

Sure! Here are the files:

https://www.dropbox.com/s/q1tdh5ojaxof7kt/test-epo.fif?dl=0
https://www.dropbox.com/s/5dzdp0bvpgv6ink/stimulus.npy?dl=0

The stimulus is already reshaped to be the same size as the epoched data. Let me know when you've downloaded the files so that I can remove them from my dropbox.
This is what I'm seeing in the `stim` data, is that correct?

(this is not predicted values, just raw data)

![image](https://cloud.githubusercontent.com/assets/1839645/25916743/9e64a8e8-357a-11e7-91b7-4653493968b6.png)

Can you share the code you use to generate the plots above?
Some quick thoughts:

* @Eric89GXL I noticed that in the call to `fit`, it will delay/reshape the data only if we're not using the TimeDelayingRidge instance, but that in `predict` it will delay/reshape regardless. Will that be a problem?
* The problem noted in this issue is because calling `predict` does not pass `remove=True` to `self._delay_and_reshape`. When we call `score` this automatically happen with `keep_samples_` being used to pull only timepoints w/o missing values. This doesn't happen with `predict`, which I believe was intended behavior but perhaps we can add a `remove=False` flag to the predict method, in case people are calling it on their own and not part of a sklearn chain. WDYT?

one way to get around this right now is to simply reshape the output of `predict` so that it's the of shape `(n_samples[, n_epochs], n_outputs)`. Then you can apply `rf.keep_samples_` to the first dimension (which will be time). This will remove timepoints that have missing values.
> This is what I'm seeing in the stim data, is that correct?

Correct, they're pixel luminance values :)

> one way to get around this right now is to simply reshape the output of predict so that it's the of shape (n_samples[, n_epochs], n_outputs). Then you can apply rf.keep_samples_ to the first dimension (which will be time). This will remove timepoints that have missing values.

Yeah this is what I've done (well I've zeroed those values but the idea is the same)
> I noticed that in the call to fit, it will delay/reshape the data only if we're not using the TimeDelayingRidge instance, but that in predict it will delay/reshape regardless. Will that be a problem?

Problem in what way? It seems to be working okay so far I think.

> one way to get around this right now is to simply reshape the output of predict so that it's the of shape (n_samples[, n_epochs], n_outputs)

The alternative would be to apply the model with an assumption of zeros (this is effectively what TimeDelayingRidge does). I wonder if all the work to remove samples is making more work than it's worth. We could also do something similar in `fit` by padding with zeros at the start and end (of each epoch). 
> Problem in what way? It seems to be working okay so far I think.

Because doesn't your estimator do the delaying internally? I was worried that we'd be delaying already delayed data.

> We could also do something similar in fit by padding with zeros at the start and end (of each epoch).

We could do something like that...I guess in my mind I'm considering any of those edge datapoints as "invalid" anyway, so better to throw them away. However I see foresee this reshaping etc to be confusing to folks...

> Because doesn't your estimator do the delaying internally? I was worried that we'd be delaying already delayed data.

We'll have to check the code, I can't remember offhand. But the tests pass so I suspect it's working okay.

> I guess in my mind I'm considering any of those edge datapoints as "invalid" anyway, so better to throw them away. However I see foresee this reshaping etc to be confusing to folks...

Yeah it's a decision about whether we want a "valid" or "full" type of operation (in terms of what `np.convolve` would do). I don't mind using "full" and letting people trim if necessary. It seems a bit simpler from a code standpoint. And it will unify the `Ridge` and `TimeDelayingRidge` code a bit.
so what changes to the codebase would we need to do then? Right now calling `fit` will remove edges as necessary before fitting the model, calling `score` will also remove edges before scoring, but calling `predict` will not remove any edges. An easy solution would be to add a `remove_edges=True` flag, but I get the feeling you'll think that's a little hacky? :)
although OTOH, if we add a "remove_edges=True" flag, then maybe we don't even need to worry about the masking from a user standpoint, it could be a hidden attribute. We could add an attribute that tells users how much the length of the new time dimension should be so they can easily reshape or something...
or we could add another flag to `predict` called `vectorize=True` that, if True, would reshape the predicted outputs to be the sklearn compatible 2D shape, and if False would retain the original shape
> Right now calling fit will remove edges as necessary before fitting the model, calling score will also remove edges before scoring, but calling predict will not remove any edges.

Basically no edges would be removed in any step. Zeros would be assumed outside the boundaries. I don't think this part would be too difficult.

Do you foresee problems with this approach? If not, it seems simplest and actually is what I'd expect as a user.
... as for what `predict` returns, we can add a flag regardless of how we decide to treat the edges.
Looking into this a bit now.

> Because doesn't your estimator do the delaying internally? I was worried that we'd be delaying already delayed data.

@choldgraf looking into this deeper, I'm not sure what you mean because it looks like [fit](https://github.com/mne-tools/mne-python/blob/master/mne/decoding/receptive_field.py#L185) and [predict](https://github.com/mne-tools/mne-python/blob/master/mne/decoding/receptive_field.py#L212) both use `self._delay_and_reshape(...)`. It's just that, in both cases, the call to `_delay_and_reshape(...)` won't really do any delaying when `TimeDelayingRidge` is used.

I'd like to rename `keep_samples_` to `valid_samples_` to mirror what `np.convolve()` and `np.correlate` call this mode / these samples, i.e., the valid ones.

I'm also fixing a but where, for `TimeDelayingRidge` with epochs, they were just concatenated. Now they are padded before concatenation to ensure that the tail end of epoch 0 does not contaminate the start of epoch 1 and so forth.

Resolving patch:
diff --git a/examples/decoding/plot_receptive_field.py b/examples/decoding/plot_receptive_field.py
--- a/examples/decoding/plot_receptive_field.py
+++ b/examples/decoding/plot_receptive_field.py
@@ -21,6 +21,7 @@
 """  # noqa: E501
 
 # Authors: Chris Holdgraf <choldgraf@gmail.com>
+#          Eric Larson <larson.eric.d@gmail.com>
 #
 # License: BSD (3-clause)
 # sphinx_gallery_thumbnail_number = 3
diff --git a/mne/decoding/receptive_field.py b/mne/decoding/receptive_field.py
--- a/mne/decoding/receptive_field.py
+++ b/mne/decoding/receptive_field.py
@@ -39,9 +39,10 @@ class ReceptiveField(BaseEstimator):
         float is passed, it will be interpreted as the `alpha` parameter
         to be passed to a Ridge regression model. If `None`, then a Ridge
         regression model with an alpha of 0 will be used.
-    fit_intercept : bool
+    fit_intercept : bool | None
         If True (default), the sample mean is removed before fitting.
-        Ignored if ``estimator`` is a :class:`sklearn.base.BaseEstimator`.
+        If ``estimator`` is a :class:`sklearn.base.BaseEstimator`,
+        this must be None or match ``estimator.fit_intercept``.
     scoring : ['r2', 'corrcoef']
         Defines how predictions will be scored. Currently must be one of
         'r2' (coefficient of determination) or 'corrcoef' (the correlation
@@ -49,17 +50,22 @@ class ReceptiveField(BaseEstimator):
 
     Attributes
     ----------
-    ``coef_`` : array, shape (n_outputs, n_features, n_delays)
+    ``coef_`` : array, shape ([n_outputs, ]n_features, n_delays)
         The coefficients from the model fit, reshaped for easy visualization.
-        If you want the raw (1d) coefficients, access them from the estimator
-        stored in ``self.estimator_``.
+        During :meth:`mne.decoding.ReceptiveField.fit`, if ``y`` has one
+        dimension (time), the ``n_outputs`` dimension here is omitted.
     ``delays_``: array, shape (n_delays,), dtype int
         The delays used to fit the model, in indices. To return the delays
         in seconds, use ``self.delays_ / self.sfreq``
-    ``keep_samples_`` : slice
+    ``valid_samples_`` : slice
         The rows to keep during model fitting after removing rows with
-        missing values due to time delaying.
+        missing values due to time delaying. This can be used to get an
+        output equivalent to using :func:`numpy.convolve` or
+        :func:`numpy.correlate` with ``mode='valid'``.
 
+    See Also
+    --------
+    mne.decoding.TimeDelayingRidge
 
     References
     ----------
@@ -83,7 +89,7 @@ class ReceptiveField(BaseEstimator):
     """
 
     def __init__(self, tmin, tmax, sfreq, feature_names=None, estimator=None,
-                 fit_intercept=True, scoring='r2'):  # noqa: D102
+                 fit_intercept=None, scoring='r2'):  # noqa: D102
         self.feature_names = feature_names
         self.sfreq = float(sfreq)
         self.tmin = tmin
@@ -111,16 +117,13 @@ def __repr__(self):  # noqa: D105
             s += "scored (%s)" % self.scoring
         return "<ReceptiveField  |  %s>" % s
 
-    def _delay_and_reshape(self, X, y=None, remove=True):
+    def _delay_and_reshape(self, X, y=None):
         """Delay and reshape the variables."""
         if not isinstance(self.estimator_, TimeDelayingRidge):
             # X is now shape (n_times, n_epochs, n_feats, n_delays)
             X_del = _delay_time_series(X, self.tmin, self.tmax, self.sfreq,
-                                       newaxis=X.ndim)
-            # Remove timepoints that don't have lag data after delaying
-            if remove:
-                X_del = X_del[self.keep_samples_]
-                y = y[self.keep_samples_]
+                                       newaxis=X.ndim,
+                                       fill_mean=self.fit_intercept)
         else:
             X_del = X[..., np.newaxis]
 
@@ -137,7 +140,7 @@ def fit(self, X, y):
         ----------
         X : array, shape (n_times[, n_epochs], n_features)
             The input features for the model.
-        y : array, shape (n_times[, n_epochs], n_outputs)
+        y : array, shape (n_times[, n_epochs][, n_outputs])
             The output features for the model.
 
         Returns
@@ -149,20 +152,30 @@ def fit(self, X, y):
             raise ValueError('scoring must be one of %s, got'
                              '%s ' % (sorted(_SCORERS.keys()), self.scoring))
         from sklearn.base import is_regressor, clone
-        X, y = self._check_dimensions(X, y)
+        X, y, _, self._y_dim = self._check_dimensions(X, y)
 
         # Initialize delays
         self.delays_ = _times_to_delays(self.tmin, self.tmax, self.sfreq)
 
         # Define the slice that we should use in the middle
-        self.keep_samples_ = _delays_to_slice(self.delays_)
+        self.valid_samples_ = _delays_to_slice(self.delays_)
 
         if isinstance(self.estimator, numbers.Real):
+            if self.fit_intercept is None:
+                self.fit_intercept = True
             estimator = TimeDelayingRidge(self.tmin, self.tmax, self.sfreq,
                                           alpha=self.estimator,
                                           fit_intercept=self.fit_intercept)
         elif is_regressor(self.estimator):
             estimator = clone(self.estimator)
+            if self.fit_intercept is not None and \
+                    estimator.fit_intercept != self.fit_intercept:
+                raise ValueError(
+                    'Estimator fit_intercept (%s) != initialization '
+                    'fit_intercept (%s), initialize ReceptiveField with the '
+                    'same fit_intercept value or use fit_intercept=None'
+                    % (estimator.fit_intercept, self.fit_itercept))
+            self.fit_intercept = estimator.fit_intercept
         else:
             raise ValueError('`estimator` must be a float or an instance'
                              ' of `BaseEstimator`,'
@@ -182,15 +195,24 @@ def fit(self, X, y):
                              '(%s != %s)' % (n_feats, len(self.feature_names)))
 
         # Create input features
-        X_del, y = self._delay_and_reshape(X, y)
-        self.estimator_.fit(X_del, y)
-
-        coefs = get_coef(self.estimator_, 'coef_')
-        coefs = coefs.reshape([-1, n_feats, len(self.delays_)])
-        if len(coefs) == 1:
-            # Remove a singleton first dimension if only 1 output
-            coefs = coefs[0]
-        self.coef_ = coefs
+        # (eventually the FFT-based method could be made more memory efficient
+        # by moving the padding to TimeDelayingRidge, which would need to be
+        # made epochs-aware)
+
+        # zero-pad if necessary
+        if isinstance(self.estimator, TimeDelayingRidge):
+            X = _pad_time_series(X, n_delays=len(self.delays_),
+                                 fill_mean=self.fit_intercept)
+            y = _pad_time_series(y, n_delays=len(self.delays_),
+                                 fill_mean=self.fit_intercept)
+        # convert to sklearn and back
+        X, y = self._delay_and_reshape(X, y)
+        self.estimator_.fit(X, y)
+        del X, y
+        coef = get_coef(self.estimator_, 'coef_')  # (n_targets, n_features)
+        shape = [n_feats, len(self.delays_)]
+        shape = ([-1] if self._y_dim > 1 else []) + shape
+        self.coef_ = coef.reshape(shape)
         return self
 
     def predict(self, X):
@@ -203,14 +225,37 @@ def predict(self, X):
 
         Returns
         -------
-        y_pred : array, shape (n_times * n_epochs[, n_outputs])
-            The output predictions with time concatenated.
+        y_pred : array, shape (n_times[, n_epochs][, n_outputs])
+            The output predictions. "Note that valid samples (those
+            unaffected by edge artifacts during the time delaying step) can
+            be obtained using ``y_pred[rf.valid_samples_]``.
         """
         if not hasattr(self, 'delays_'):
             raise ValueError('Estimator has not been fit yet.')
-        X, _ = self._check_dimensions(X, None, predict=True)
-        X_del, _ = self._delay_and_reshape(X, remove=False)
-        y_pred = self.estimator_.predict(X_del)
+        X, _, X_dim = self._check_dimensions(X, None, predict=True)[:3]
+        del _
+        # zero-pad if necessary
+        if isinstance(self.estimator, TimeDelayingRidge):
+            X = _pad_time_series(X, n_delays=len(self.delays_),
+                                 fill_mean=self.fit_intercept)
+        # convert to sklearn and back
+        pred_shape = X.shape[:-1]
+        if self._y_dim > 1:
+            pred_shape = pred_shape + (self.coef_.shape[0],)
+        X, _ = self._delay_and_reshape(X)
+        y_pred = self.estimator_.predict(X)
+        y_pred = y_pred.reshape(pred_shape, order='F')
+        # undo padding
+        if isinstance(self.estimator, TimeDelayingRidge):
+            y_pred = y_pred[:-(len(self.delays_) - 1)]
+        shape = list(y_pred.shape)
+        if X_dim <= 2:
+            shape.pop(1)  # epochs
+            extra = 0
+        else:
+            extra = 1
+        shape = shape[:self._y_dim + extra]
+        y_pred.shape = shape
         return y_pred
 
     def score(self, X, y):
@@ -224,7 +269,7 @@ def score(self, X, y):
         ----------
         X : array, shape (n_times[, n_epochs], n_channels)
             The input features for the model.
-        y : array, shape (n_times[, n_epochs], n_outputs)
+        y : array, shape (n_times[, n_epochs][, n_outputs])
             Used for scikit-learn compatibility.
 
         Returns
@@ -237,64 +282,86 @@ def score(self, X, y):
         scorer_ = _SCORERS[self.scoring]
 
         # Generate predictions, then reshape so we can mask time
-        X, y = self._check_dimensions(X, y, predict=True)
+        X, y = self._check_dimensions(X, y, predict=True)[:2]
         n_times, n_epochs, n_outputs = y.shape
         y_pred = self.predict(X)
-
-        y_pred = y_pred.reshape(y.shape, order='F')
-        y_pred = y_pred[self.keep_samples_]
-        y = y[self.keep_samples_]
+        y_pred = y_pred[self.valid_samples_]
+        y = y[self.valid_samples_]
 
         # Re-vectorize and call scorer
         y = y.reshape([-1, n_outputs], order='F')
         y_pred = y_pred.reshape([-1, n_outputs], order='F')
+        assert y.shape == y_pred.shape
         scores = scorer_(y, y_pred, multioutput='raw_values')
         return scores
 
     def _check_dimensions(self, X, y, predict=False):
-        if X.ndim == 1:
-            raise ValueError('X must be shape (n_times[, n_epochs],'
-                             ' n_features)')
-        elif X.ndim == 2:
+        X_dim = X.ndim
+        y_dim = y.ndim if y is not None else 0
+        if X_dim == 2:
             # Ensure we have a 3D input by adding singleton epochs dimension
             X = X[:, np.newaxis, :]
-            if y is None:
-                pass
-            elif y.ndim == 1:
-                y = y[:, np.newaxis, np.newaxis]  # Add epochs and outputs dim
-            elif y.ndim == 2:
-                y = y[:, np.newaxis]  # Only add epochs dim
-            else:
-                raise ValueError('y must be shape (n_times[, n_epochs],'
-                                 ' n_outputs')
+            if y is not None:
+                if y_dim == 1:
+                    y = y[:, np.newaxis, np.newaxis]  # epochs, outputs
+                elif y_dim == 2:
+                    y = y[:, np.newaxis, :]  # epochs
+                else:
+                    raise ValueError('y must be shape (n_times[, n_epochs]'
+                                     '[,n_outputs], got %s' % (y.shape,))
         elif X.ndim == 3:
-            if y is None:
-                pass
-            elif y.ndim == 2:
-                y = y[:, :, np.newaxis]  # Add an outputs dim
-            elif y.ndim != 3:
-                raise ValueError('If X has 3 dimensions, '
-                                 'y must have 2 or 3 dimensions')
+            if y is not None:
+                if y.ndim == 2:
+                    y = y[:, :, np.newaxis]  # Add an outputs dim
+                elif y.ndim != 3:
+                    raise ValueError('If X has 3 dimensions, '
+                                     'y must have 2 or 3 dimensions')
         else:
-            raise ValueError('X must be of shape '
-                             '(n_times[, n_epochs], n_features)')
-        if y is None:
-            # If y is None, then we don't need any more checks
-            pass
-        elif X.shape[0] != y.shape[0]:
-            raise ValueError('X any y do not have the same n_times\n'
-                             '%s != %s' % (X.shape[0], y.shape[0]))
-        elif X.shape[1] != y.shape[1]:
-            raise ValueError('X any y do not have the same n_epochs\n'
-                             '%s != %s' % (X.shape[1], y.shape[1]))
-        elif predict is True:
-            if y.shape[-1] != len(self.estimator_.coef_):
-                raise ValueError('Number of outputs does not match'
-                                 ' estimator coefficients dimensions')
-        return X, y
-
-
-def _delay_time_series(X, tmin, tmax, sfreq, newaxis=0, axis=0):
+            raise ValueError('X must be shape (n_times[, n_epochs],'
+                             ' n_features), got %s' % (X.shape,))
+        if y is not None:
+            if X.shape[0] != y.shape[0]:
+                raise ValueError('X any y do not have the same n_times\n'
+                                 '%s != %s' % (X.shape[0], y.shape[0]))
+            if X.shape[1] != y.shape[1]:
+                raise ValueError('X any y do not have the same n_epochs\n'
+                                 '%s != %s' % (X.shape[1], y.shape[1]))
+            if predict and y.shape[-1] != len(self.estimator_.coef_):
+                    raise ValueError('Number of outputs does not match'
+                                     ' estimator coefficients dimensions')
+        return X, y, X_dim, y_dim
+
+
+def _pad_time_series(X, n_delays, fill_mean=True):
+    """Return a zero- or mean-padded input time series.
+
+    Parameters
+    ----------
+    X : array, shape (n_times[, n_epochs], n_features)
+        The time series to pad.
+    n_delays : int
+        The number of delays.
+    fill_mean : bool
+        If True, the fill value will be the mean along the time dimension
+        of the feature. If False, the fill value will be zero.
+
+    Returns
+    -------
+    padded : array, shape(n_padded[, n_epochs], n_features)
+        The padded data, where ``n_padded = n_times + n_delays - 1``.
+    """
+    fill_value = 0
+    if fill_mean:
+        fill_value = np.mean(X, axis=0, keepdims=True)
+        if X.ndim == 3:
+            fill_value = np.mean(fill_value, axis=1, keepdims=True)
+    X = np.pad(X, ((0, n_delays - 1),) + ((0, 0),) * (X.ndim - 1), 'constant')
+    X[-(n_delays - 1):] = fill_value
+    return X
+
+
+def _delay_time_series(X, tmin, tmax, sfreq, newaxis=0, axis=0,
+                       epoch_axis=1, fill_mean=False):
     """Return a time-lagged input time series.
 
     Parameters
@@ -311,12 +378,15 @@ def _delay_time_series(X, tmin, tmax, sfreq, newaxis=0, axis=0):
     newaxis : int
         The axis in the output array that corresponds to time delays.
         Defaults to 0, for the first axis.
+    fill_mean : bool
+        If True, the fill value will be the mean along the time dimension
+        of the feature. If False, the fill value will be zero.
     axis : int
         The axis corresponding to the time dimension.
 
     Returns
     -------
-    delayed: array, shape(..., n_delays, ...)
+    delayed : array, shape(..., n_delays, ...)
         The delayed data. It has the same shape as X, with an extra dimension
         created at ``newaxis`` that corresponds to each delay.
 
@@ -337,6 +407,11 @@ def _delay_time_series(X, tmin, tmax, sfreq, newaxis=0, axis=0):
     # XXX : add Vectorize=True parameter to switch on/off 2D output
     # Iterate through indices and append
     delayed = np.zeros((len(delays),) + X.shape)
+    if fill_mean:
+        fill_value = X.mean(axis=axis, keepdims=True)
+        if epoch_axis is not None:
+            fill_value = np.mean(fill_value, axis=epoch_axis, keepdims=True)
+            delayed[...] = fill_value
     for ii, ix_delay in enumerate(delays):
         take = [slice(None)] * X.ndim
         put = [slice(None)] * X.ndim
@@ -357,8 +432,8 @@ def _delay_time_series(X, tmin, tmax, sfreq, newaxis=0, axis=0):
 def _times_to_delays(tmin, tmax, sfreq):
     """Convert a tmin/tmax in seconds to delays."""
     # Convert seconds to samples
-    delays = np.arange(np.round(tmin * sfreq),
-                       np.round(tmax * sfreq) + 1).astype(int)
+    delays = np.arange(int(np.round(tmin * sfreq)),
+                       int(np.round(tmax * sfreq) + 1))
     return delays
 
 
@@ -397,8 +472,10 @@ def _reshape_for_est(X_del):
 # Create a correlation scikit-learn-style scorer
 def _corr_score(y_true, y, multioutput=None):
     from scipy.stats import pearsonr
-    if any(ii.ndim != 2 for ii in [y_true, y]):
-        raise ValueError('inputs must shape (samples, outputs)')
+    for this_y in (y_true, y):
+        if this_y.ndim != 2:
+            raise ValueError('inputs must shape (samples, outputs), got %s'
+                             % (this_y.shape,))
     return [pearsonr(y_true[:, ii], y[:, ii])[0] for ii in range(y.shape[-1])]
 
 
diff --git a/tutorials/plot_receptive_field.py b/tutorials/plot_receptive_field.py
--- a/tutorials/plot_receptive_field.py
+++ b/tutorials/plot_receptive_field.py
@@ -40,6 +40,7 @@
        Frontiers in Human Neuroscience 10, 604. doi:10.3389/fnhum.2016.00604
 """
 # Authors: Chris Holdgraf <choldgraf@gmail.com>
+#          Eric Larson <larson.eric.d@gmail.com>
 #
 # License: BSD (3-clause)
 
@@ -207,7 +208,7 @@
 # Choose the model that performed best on the held out data
 ix_best_alpha = np.argmax(scores)
 best_mod = models[ix_best_alpha]
-coefs = best_mod.coef_
+coefs = best_mod.coef_[0]
 best_pred = best_mod.predict(X_test)[:, 0]
 
 # Plot the original STRF, and the one that we recovered with modeling.
@@ -257,7 +258,7 @@
 # Plot the STRF of each ridge parameter
 for ii, (rf, i_alpha) in enumerate(zip(models, alphas)):
     ax = plt.subplot2grid([2, 10], [0, ii], 1, 1)
-    ax.pcolormesh(times, rf.feature_names, rf.coef_, **kwargs)
+    ax.pcolormesh(times, rf.feature_names, rf.coef_[0], **kwargs)
     plt.xticks([], [])
     plt.yticks([], [])
     plt.autoscale(tight=True)
@@ -281,7 +282,8 @@
 #           & & & -1 & 2 & -1 \\
 #           & & &    & -1 & 1\end{matrix}\right]
 #
-# This imposes a smoothness constraint of nearby time samples. Quoting [5]_:
+# This imposes a smoothness constraint of nearby time samples and/or features.
+# Quoting [5]_:
 #
 #    Tikhonov [identity] regularization (Equation 5) reduces overfitting by
 #    smoothing the TRF estimate in a way that is insensitive to
@@ -336,7 +338,7 @@
 # Plot the STRF of each ridge parameter
 for ii, (rf, i_alpha) in enumerate(zip(models, alphas)):
     ax = plt.subplot2grid([2, 10], [0, ii], 1, 1)
-    ax.pcolormesh(times, rf.feature_names, rf.coef_, **kwargs)
+    ax.pcolormesh(times, rf.feature_names, rf.coef_[0], **kwargs)
     plt.xticks([], [])
     plt.yticks([], [])
     plt.autoscale(tight=True)


Test patch:
diff --git a/mne/decoding/tests/test_receptive_field.py b/mne/decoding/tests/test_receptive_field.py
--- a/mne/decoding/tests/test_receptive_field.py
+++ b/mne/decoding/tests/test_receptive_field.py
@@ -6,8 +6,9 @@
 
 from nose.tools import assert_raises, assert_true, assert_equal
 import numpy as np
-from numpy.testing import (assert_array_equal, assert_array_almost_equal,
-                           assert_allclose)
+from scipy.sparse import csr_matrix
+
+from numpy.testing import assert_array_equal, assert_allclose
 
 from mne import io, pick_types
 from mne.utils import requires_sklearn_0_15, run_tests_if_main
@@ -34,10 +35,37 @@
 picks = picks[:2]
 
 
+@requires_sklearn_0_15
+def test_rank_deficiency():
+    """Test signals that are rank deficient."""
+    # See GH#4253
+    from sklearn.linear_model import Ridge
+    N = 256
+    fs = 1.
+    tmin, tmax = -50, 100
+    reg = 0.1
+    rng = np.random.RandomState(0)
+    eeg = rng.randn(N, 1)
+    eeg *= 100
+    eeg = np.fft.rfft(eeg, axis=0)
+    eeg[N // 4:] = 0  # rank-deficient lowpass
+    eeg = np.fft.irfft(eeg, axis=0)
+    win = np.hanning(N // 8)
+    win /= win.mean()
+    y = np.apply_along_axis(np.convolve, 0, eeg, win, mode='same')
+    y += rng.randn(*y.shape) * 100
+
+    for est in (Ridge(reg), reg):
+        rf = ReceptiveField(tmin, tmax, fs, estimator=est)
+        rf.fit(eeg, y)
+        pred = rf.predict(eeg)
+        assert_equal(y.shape, pred.shape)
+        corr = np.corrcoef(y.ravel(), pred.ravel())[0, 1]
+        assert_true(corr > 0.995, msg=corr)
+
+
 def test_time_delay():
     """Test that time-delaying w/ times and samples works properly."""
-    from scipy.sparse import csr_matrix
-
     # Explicit delays + sfreq
     X = rng.randn(2, 1000)
     X_sp = np.zeros([2, 10])
@@ -96,11 +124,10 @@ def test_receptive_field():
     assert_array_equal(rf.delays_, np.arange(tmin, tmax + 1))
 
     y_pred = rf.predict(X)
-    assert_array_almost_equal(y[rf.keep_samples_],
-                              y_pred.squeeze()[rf.keep_samples_], 2)
+    assert_allclose(y[rf.valid_samples_], y_pred[rf.valid_samples_], atol=1e-2)
     scores = rf.score(X, y)
     assert_true(scores > .99)
-    assert_array_almost_equal(rf.coef_.reshape(-1, order='F'), w, 2)
+    assert_allclose(rf.coef_.T.ravel(), w, atol=1e-2)
     # Make sure different input shapes work
     rf.fit(X[:, np.newaxis:, ], y[:, np.newaxis])
     rf.fit(X, y[:, np.newaxis])
@@ -139,11 +166,11 @@ def test_receptive_field():
         rf = ReceptiveField(tmin, tmax, 1, ['one'],
                             estimator=0, scoring=key)
         rf.fit(X[:, [0]], y)
-        y_pred = rf.predict(X[:, [0]])
-        assert_array_almost_equal(val(y[:, np.newaxis], y_pred),
-                                  rf.score(X[:, [0]], y), 4)
+        y_pred = rf.predict(X[:, [0]]).T.ravel()[:, np.newaxis]
+        assert_allclose(val(y[:, np.newaxis], y_pred),
+                        rf.score(X[:, [0]], y), rtol=1e-3)
     # Need 2D input
-    assert_raises(ValueError, _SCORERS['corrcoef'], y.squeeze(), y_pred)
+    assert_raises(ValueError, _SCORERS['corrcoef'], y.ravel(), y_pred)
     # Need correct scorers
     rf = ReceptiveField(tmin, tmax, 1., scoring='foo')
     assert_raises(ValueError, rf.fit, X, y)
@@ -154,32 +181,40 @@ def test_receptive_field_fast():
     """Test that the fast solving works like Ridge."""
     from sklearn.linear_model import Ridge
     rng = np.random.RandomState(0)
-    y = np.zeros(500)
-    x = rng.randn(500)
+    x = rng.randn(500, 1)
     for delay in range(-2, 3):
-        y.fill(0.)
+        y = np.zeros(500)
         slims = [(-4, 2)]
         if delay == 0:
-            y = x.copy()
+            y[:] = x[:, 0]
         elif delay < 0:
-            y[-delay:] = x[:delay]
+            y[-delay:] = x[:delay, 0]
             slims += [(-4, -1)]
         else:
-            y[:-delay] = x[delay:]
+            y[:-delay] = x[delay:, 0]
             slims += [(1, 2)]
-        for slim in slims:
-            tdr = TimeDelayingRidge(slim[0], slim[1], 1., 0.1, 'laplacian',
-                                    fit_intercept=False)
-            for estimator in (Ridge(alpha=0.), 0., 0.1, tdr):
-                model = ReceptiveField(slim[0], slim[1], 1.,
-                                       estimator=estimator)
-                model.fit(x[:, np.newaxis], y)
-                assert_array_equal(model.delays_,
-                                   np.arange(slim[0], slim[1] + 1))
-                expected = (model.delays_ == delay).astype(float)
-                assert_allclose(model.coef_[0], expected, atol=1e-2)
-                p = model.predict(x[:, np.newaxis])[:, 0]
-                assert_allclose(y, p, atol=5e-2)
+        for ndim in (1, 2):
+            y.shape = (y.shape[0],) + (1,) * (ndim - 1)
+            for slim in slims:
+                tdr = TimeDelayingRidge(slim[0], slim[1], 1., 0.1, 'laplacian',
+                                        fit_intercept=False)
+                for estimator in (Ridge(alpha=0.), 0., 0.1, tdr):
+                    model = ReceptiveField(slim[0], slim[1], 1.,
+                                           estimator=estimator)
+                    model.fit(x, y)
+                    assert_array_equal(model.delays_,
+                                       np.arange(slim[0], slim[1] + 1))
+                    expected = (model.delays_ == delay).astype(float)
+                    expected = expected[np.newaxis]  # features
+                    if y.ndim == 2:
+                        expected = expected[np.newaxis]  # outputs
+                    assert_equal(model.coef_.ndim, ndim + 1)
+                    assert_allclose(model.coef_, expected, atol=1e-2)
+                    p = model.predict(x)
+                    assert_allclose(y, p, atol=5e-2)
+                    score = model.score(x, y)
+                    assert_true(score > 0.9999, msg=score)
+
     # multidimensional
     x = rng.randn(1000, 3)
     y = np.zeros((1000, 2))
@@ -211,6 +246,7 @@ def test_receptive_field_fast():
     tdr = TimeDelayingRidge(slim[0], slim[1], 1., 0.01, reg_type=['laplacian'])
     model = ReceptiveField(slim[0], slim[1], 1., estimator=tdr)
     assert_raises(ValueError, model.fit, x, y)
+
     # Now check the intercept_
     tdr = TimeDelayingRidge(slim[0], slim[1], 1., 0.)
     tdr_no = TimeDelayingRidge(slim[0], slim[1], 1., 0., fit_intercept=False)
@@ -225,7 +261,7 @@ def test_receptive_field_fast():
         x += 1e3
         model.fit(x, y)
         if estimator.fit_intercept:
-            val, itol, ctol = [-6000, 4000], 20., 1e-1
+            val, itol, ctol = [-6000, 4000], 15, 5e-2
         else:
             val, itol, ctol = 0., 0., 2.  # much worse
         assert_allclose(model.estimator_.intercept_, val, atol=itol)
@@ -233,5 +269,8 @@ def test_receptive_field_fast():
         model = ReceptiveField(slim[0], slim[1], 1., fit_intercept=False)
         model.fit(x, y)
         assert_allclose(model.estimator_.intercept_, 0., atol=1e-7)
+        score = model.score(x, y)
+        assert_true(score > 0.6, msg=score)
+
 
 run_tests_if_main()