Edge effect in tfr

MNE version: 1.7.1
operating system: Windows 10
Hi, all
I’m using mne for time-frequency analysis of ERP data. I have some question. I have extracted the tfr-power of theta band. I want to use a curve to see how the time frequency data changes. But in the end of the power, I see a strangely smooth ascent. GPT tells me this is from edge effect. I want to know how to deal with it. Thanks.

tfr = data[key].compute_tfr(
    method="morlet",
    freqs=freqs,
    n_cycles=n_cycles,
    average=False,
    return_itc=False,
    decim=1,
    picks=tf_channels
).apply_baseline((-0.2, 0), mode='logratio')

I usually extend the original epochs by ±0.5 s (i.e., in your example I’d define them from -0.7 s to 2.5 s), compute the TFR, and then crop 0.5 s from each end of the result.

3 Likes

Thanks! I will do.

This is just an add-on for users who can’t extend the desired epoch length (e.g. pre-stimulus window, etc.). I wrote a function a while ago that mirrors or zero-pads epochs on both ends, which might be helpful. I also ran simulations and wrote a test for this function, feel free to reach out.

Run your time-frequency algorithm on the mirrored/zero-padded data before cropping it back to the desired window.

def zero_pad_or_mirror_epochs(epochs, zero_pad: bool, pad_length: int = 100):
    """
    Zero-pad or mirror an MNE.Epochs object on both sides to avoid edge artifacts.
    
    Args:
    ----
    epochs: mne.Epochs
        The input MNE Epochs object to be padded or mirrored.
    zero_pad: bool
        If True, zero-pad the data. If False, mirror the data.
    pad_length: int, optional
        Number of time points to pad (default is 100).

    Returns:
    -------
    padded_epochs: mne.EpochsArray
        The padded or mirrored MNE.EpochsArray object with adjusted time dimensions.
    """
    data = epochs.get_data()  # Extract the data from the epochs object
    n_epochs, n_channels, n_times = data.shape
    sfreq = epochs.info['sfreq']  # Get the sampling frequency
    times = epochs.times  # Original time array
    # save metadata
    metadata = epochs.metadata

    # Initialize list to collect padded/mirrored data
    padded_list = []

    if zero_pad:
        # Create a zero-padding array with shape (pad_length,)
        zero_pad_array = np.zeros(pad_length)

        # Loop over epochs and channels to zero-pad the data
        for epoch in range(n_epochs):
            ch_list = []
            for ch in range(n_channels):
                # Zero pad at beginning and end
                ch_list.append(np.concatenate([zero_pad_array, data[epoch][ch], zero_pad_array]))
            padded_list.append(ch_list)

    else:
        # Mirror data at the edges with a length equal to `pad_length`
        for epoch in range(n_epochs):
            ch_list = []
            for ch in range(n_channels):
                # Mirror pad at the beginning and end using `pad_length`
                ch_list.append(np.concatenate([
                    data[epoch][ch][:pad_length][::-1],  # Mirror the first `pad_length` points
                    data[epoch][ch],  # Original data
                    data[epoch][ch][-pad_length:][::-1]  # Mirror the last `pad_length` points
                ]))
            padded_list.append(ch_list)

    # Convert the list back to a numpy array with the correct shape
    padded_data = np.array(padded_list)

    # Adjust the time array to match the new data length
    time_step = times[1] - times[0]  # Time step based on sampling frequency
    new_times = np.arange(times[0] - pad_length * time_step, times[-1] + pad_length * time_step, time_step)

    # Create a new MNE EpochsArray with the padded/mirrored data
    padded_epochs = mne.EpochsArray(padded_data, epochs.info, tmin=new_times[0], events=epochs.events, event_id=epochs.event_id)

    # Add metadata back to the epochs object
    padded_epochs.metadata = metadata
    
    return padded_epochs
1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.