2

I have a 4D structure containing EEG data with shape (3432,1,30,512)

This represents 3432 trials of data, where each trial contains 512 time samples for 30 electrodes

For each trial, I want to filter each electrode with certain filter parameters, which results in a new 2D (30,512) filtered array. Then, I want to stack these along axis 1 of the 4D structure.

Currently, I'm iterating over each trial, then each electrode, filtering the signal and inserting everything back into the 4D:

from scipy import signal    

NUM_CHANS = 30
NUM_TIMESTAMPS = 512
FREQ_BANDS = ((0.1, 3), (4, 7), (8, 13), (16, 31))

all_data = np.reshape(all_data, (-1, 1, NUM_CHANS, NUM_TIMESTAMPS))  # (3432,1,30,512)

for i in range(all_data.shape[0]):
    num_band = 1
    for band in FREQ_BANDS:
        lower = float(band[0])/(SAMPLE_RATE/2)
        upper = float(band[1])/(SAMPLE_RATE/2)

        # Design new filter for the current frequency band
        b, a = signal.butter(2, [lower, upper], 'bandpass')

        temp_trial = np.zeros((NUM_CHANS, NUM_TIMESTAMPS))

        for ch in range(NUM_CHANS):
            # Filter the current electrode
            output_signal = signal.filtfilt(b, a, all_data[i,0,ch,:])
            temp_trial[ch,:] = output_signal

        # Insert temp_trial (2D) into all_data (4D) along axis 1

        num_band += 1

Iterating over trials and electrodes is extremely slow (takes about 2 hours to complete the entire loop). Is there a more efficient way to apply this filter over all electrodes/trials?

I've been trying to find a way apply the filter over 2D, so I don't need to iterate over electrodes, but haven't been able to find anything.

EDIT:

Would this be the correct way to use filtfilt axis argument?

all_data = np.reshape(all_data, (-1, 1, NUM_CHANS, NUM_TIMESTAMPS))

filtered_data = [all_data]

for band in FREQ_BANDS:
    lower = float(band[0])/(SAMPLE_RATE/2)
    upper = float(band[1])/(SAMPLE_RATE/2)
    b, a = signal.butter(2, [lower, upper], 'bandpass')

    output_signal = signal.filtfilt(b, a, all_data, axis=3)

    filtered_data.append(output_signal)

all_data = np.concatenate(filtered_data, axis=1)
Simon
  • 9,762
  • 15
  • 62
  • 119
  • Hmm you might want to look into [MNE-Python](http://martinos.org/mne/stable/index.html), a Python package for M/EEG analysis. – jkr Dec 05 '16 at 01:23
  • Ive tried MNE, but I cant find a way to apply the filter to anything but the raw data. The 4D structure I have it actually epoched data. If I want to use MNEs filtering Id have to read the raw data, filter, epoch it, and then repeat the entire process which seems a bit redundant (i.e. Id rather read the data only once). Is there a way to apply filters to epoch data, instead of just the raw? – Simon Dec 05 '16 at 01:30
  • Maybe try [`mne.filter.filter_data`](http://martinos.org/mne/dev/generated/mne.filter.filter_data.html)? – jkr Dec 05 '16 at 01:42
  • Why do you have the trivial (length 1) dimension in `all_data`? Why not make it 3-d? – Warren Weckesser Dec 05 '16 at 02:13
  • its because I need to stack each of the frequency bands along that dimension so I end up with shape `(3432, 5, 30, 512)`. I didn't include the code this and just had a placeholder comment instead – Simon Dec 05 '16 at 02:17

1 Answers1

2

Here's one change that might improve performance. filtfilt has an axis argument, which allows you to apply the same 1-d filter along an axis of an n-dimensional array. You can replace this code

    temp_trial = np.zeros((NUM_CHANS, NUM_TIMESTAMPS))
    for ch in range(NUM_CHANS):
        # Filter the current electrode
        output_signal = signal.filtfilt(b, a, all_data[i,0,ch,:])
        temp_trial[ch,:] = output_signal

with

    temp_trial = signal.filtfilt(b, a, all_data[i,0,:,:], axis=1)

And since the default axis is axis=-1, you can leave out the axis argument if you prefer:

    temp_trial = signal.filtfilt(b, a, all_data[i,0,:,:])

You can do even better, by rearranging the outer loops. Make the "band" loop the outer loop. Then, for each band, you can apply filtfilt once to the 3-d array all_data[:, 0, :, :].

Something like this:

shp = all_data.shape

# This assumes `all_data` has shape (3432,1,30,512), but I don't
# think you need that trivial extra dimension in there if you
# preallocate `filtered_data` like I am doing here.
filtered_data = np.empty(shp[0:1] + (len(FREQ_BANDS),) + shp[2:])

for k, band in enumerate(FREQ_BANDS):
    lower = float(band[0])/(SAMPLE_RATE/2)
    upper = float(band[1])/(SAMPLE_RATE/2)

    # Design new filter for the current frequency band
    b, a = signal.butter(2, [lower, upper], 'bandpass')

    filtered_data[:, k, :, :] = filtfilt(b, a, all_data[:,0,:,:])

This doesn't include the original all_data in filtered_data. If you want that, use len(FREQ_BANDS)+1 in the call to np.empty() that creates filtered_data, set filtered_data[:,0,:,:] = all_data, and use k+1 instead of k in assignment statement that saves the result of the filtfilt() call.

Warren Weckesser
  • 110,654
  • 19
  • 194
  • 214
  • Couldn't I get rid of the outer loop completely (`for i in range(all_data.shape[0]):...`) and use `signal.filtfilt(b, a, all_data, axis=3)`? I think that will result in a 4D array where everything has been filtered? – Simon Dec 05 '16 at 02:06
  • Yes, I just added a comment about that to my answer. The only explicit python loop you need is the loop over the frequency bands. – Warren Weckesser Dec 05 '16 at 02:10
  • In your case youre operating on a 3D array. I've added to my question with your suggestions here...wondering if that would be the correct way to do this in 4D? – Simon Dec 05 '16 at 02:15
  • Your code produces `filtered_data` as a 5D array `(3432,4,1,30,512)`, which I think is the reason I'm getting the following error: `could not broadcast input array from shape (3432,30,512) into shape (3432,1,30,512)` – Simon Dec 05 '16 at 02:33
  • Sorry, I didn't test it, that's why I said "something like..." :) I made a small change of `shp[1:]` to `shp[2:]`. – Warren Weckesser Dec 05 '16 at 02:48
  • By the way, your idea to save the results in a list, and then concatenate those results into an array should also work fine. It will use more temporary memory, but that might not be an issue. – Warren Weckesser Dec 05 '16 at 02:52