Skip to content

Inconsistent behavior between Epochs and Evoked in get_data() method when picking EEG channels and bads are present #12577

Description

@hoechenberger

Description of the problem

Epochs.get_data(picks="eeg") excludes bad channels, but Evoked.get_data(picks="eeg") doesn't

Steps to reproduce

# %%
import mne

sample_dir = mne.datasets.sample.data_path()
sample_fname = sample_dir / "MEG" / "sample" / "sample_audvis_raw.fif"

raw = mne.io.read_raw_fif(sample_fname)
raw.crop(tmax=60)

events = mne.find_events(raw, stim_channel="STI 014")
raw.pick("eeg")

epochs = mne.Epochs(raw, events=events, preload=True)
evoked = epochs.average()

# %%
for inst_type, inst in zip(["epochs", "evoked"], [epochs, evoked]):
    print(f"{inst_type}:")
    print(f"    bads: {inst.info['bads']}")
    print(f"    data shape: {inst.get_data(picks='eeg').shape}")

Link to data

No response

Expected results

Both methods should behave the same in regards to bad channel treatment.

Actual results

epochs:
    bads: ['EEG 053']
    data shape: (86, 59, 421)
evoked:
    bads: ['EEG 053']
    data shape: (60, 421)

As you can see, the data from Evoked contains the bad channel, while the data from Epochs does not.

Additional information

This is with main

Activity

  1. cbrnr commented on Apr 26, 2024

    @cbrnr
    Contributor

    The docstrings are identical, so I think both methods should behave identically. In addition, the docstring says:

    None (default) will pick all channels.

    I think this should be:

    None (default) will pick all good channels.

    At least that's how it currently works for Epochs, but not for Evoked apparently.

    https://mne.tools/dev/generated/mne.Epochs.html#mne.Epochs.get_data
    https://mne.tools/dev/generated/mne.Evoked.html#mne.Evoked.get_data

  2. hoechenberger commented on Apr 26, 2024

    @hoechenberger
    MemberAuthor

    @cbrnr Note that I did not pass None in my MWE! I did pass picks="eeg"

    The doctoring says:

    Note that channels in info['bads'] will be included if their names or indices are explicitly provided.

    So something about this point is inconsistent between epochs and evoked at least when passing picks="eeg".

    I think what you're mentioning here is being tracked at #12197

  3. changed the title [-]Inconsistent picking behavior between `Epochs` and `Evoked` in `get_data()` method[/-] [+]Inconsistent picking behavior between `Epochs` and `Evoked` in `get_data()` method when picking[/+] on Apr 26, 2024
  4. changed the title [-]Inconsistent picking behavior between `Epochs` and `Evoked` in `get_data()` method when picking[/-] [+]Inconsistent behavior between `Epochs` and `Evoked` in `get_data()` method when picking EEG channels[/+] on Apr 26, 2024
  5. changed the title [-]Inconsistent behavior between `Epochs` and `Evoked` in `get_data()` method when picking EEG channels[/-] [+]Inconsistent behavior between `Epochs` and `Evoked` in `get_data()` method when picking EEG channels and bads are present[/+] on Apr 26, 2024
  6. larsoner commented on Apr 26, 2024

    @larsoner
    Member

    So something about this point is inconsistent between epochs and evoked at least when passing picks="eeg".

    Neither get_data has an exclude param, sounds like one is implicitly using exclude="bads" and the other exclude=() under the hood. To change the behavior of one to match the other and be consistent I think we'd need a deprecation cycle, not sure whether or not it's worth the code churn. To update the docstring of both to reflect their actual behavior it wouldn't require a deprecation cycle. Adding exclude to both (to make the behavior more explicit and controllable) is probably worthwhile either way.

  7. cbrnr commented on Apr 26, 2024

    @cbrnr
    Contributor

    Even if documented I think it would be a bit surprising if Epochs and Evoked used different pick defaults. So I'd document the behavior with whatever version we think makes more sense, be consistent for both methods and deprecate the inconsistent behavior. And yes, an explicit exclude parameter would definitely make sense to me.

  8. hoechenberger commented on Apr 26, 2024

    @hoechenberger
    MemberAuthor

    I tested the MWE with Raw and now it seems it's actually Epochs who's the odd one:

    # %%
    import mne
    
    sample_dir = mne.datasets.sample.data_path()
    sample_fname = sample_dir / "MEG" / "sample" / "sample_audvis_raw.fif"
    
    raw = mne.io.read_raw_fif(sample_fname)
    raw.crop(tmax=60)
    
    events = mne.find_events(raw, stim_channel="STI 014")
    raw.pick("eeg")
    
    epochs = mne.Epochs(raw, events=events, preload=True)
    evoked = epochs.average()
    
    # %%
    for inst_type, inst in zip(["raw", "epochs", "evoked"], [raw, epochs, evoked]):
        print(f"{inst_type}:")
        print(f"    bads: {inst.info['bads']}")
        print(f"    data shape: {inst.get_data(picks='eeg').shape}")
    raw:
        bads: ['EEG 053']
        data shape: (60, 36038)
    epochs:
        bads: ['EEG 053']
        data shape: (86, 59, 421)
    evoked:
        bads: ['EEG 053']
        data shape: (60, 421)
  9. hoechenberger commented on Apr 26, 2024

    @hoechenberger
    MemberAuthor

    More digging:

    • BaseSpectrum.get_data() features exclude="bads'
    • BaseTFR ditto
  10. hoechenberger commented on Apr 26, 2024

    @hoechenberger
    MemberAuthor

    My feeling is that we should add exclude parameters to all these get_data() methods if they don't have one already, retaining the current behavior; and do a deprecation cycle for transitioning to either exclude=() or exclude="bads" for all of them

  11. removed their assignment
    on Jun 9, 2024
  12. added this to the 1.8 milestone on Jun 9, 2024
  13. modified the milestones: 1.8, 1.9 on Jul 15, 2024
  14. modified the milestones: 1.9, 1.10 on Dec 9, 2024
  15. modified the milestones: 1.10, 1.11 on Jun 26, 2025
  16. RenanFlorencio commented on Nov 6, 2025

    @RenanFlorencio

    Hello,

    Me and two friends (@luisademelo14 @Fooxfeir) are taking a course and we need to choose a tool and bug to fix. We chose MNE-Python, but since we are new to the community, we decided to pick a bug marked as easy and this one seems good.
    If there's a problem with that, please let us know!

  17. larsoner commented on Nov 7, 2025

    @larsoner
    Member

    #12197 is also related

    FYI not all of this issue is easy... the first part is probably uncontroversial: namely adding exclude params to functions that don't have them, retaining old behavior, and documenting things properly (#12197). I think documenting current behavior and making it changeable by adding exclude=... in places we don't have it is already a good start here, so @RenanFlorencio @luisademelo14 @Fooxfeir I think you can work on this part! 👍 So feel free to start on that

    The deprecation cycle to transition them all to have the same behavior will not be trivial. I suspect a lot of tests will need to be adjusted, and it will be hard to do without a reasonably deep working knowledge of the code.

    I am also not 100% sure we should do the deprecation to make everything have either exclude=() or exclude="bads everywhere. There are probably functions and classes where we should exclude bads -- like in multivariate connectivity fitting for example we've talked about it and there are probably other examples -- and other functions/classes (like constructing epochs maybe) it is convenient to include them by default. We'd have to look at all use cases (decoding, inverses, stats, time-freq/spectra, connectivity, etc.) to see what makes sense here. Moreover, if we change the behavior of something like Epochs the risk of breaking people's code is quite high even with a deprecation cycle... so we should think really hard about whether the potential disruption is worth the uniformity, and how much it buys us.

  18. RenanFlorencio commented on Nov 11, 2025

    @RenanFlorencio

    Okay @larsoner , we worked on the first part and we added the exclude parameter to all three functions (epochs, evoked and base, since spectrum and tfr already have it), this way we have three different behaviors for each. We would like you to confirm this is what's expected to write the docstring. We also want to ask you a question that is at the end of this comment.

    Maintained Previous Behavior:

    for inst_type, inst in zip(["raw", "epochs", "evoked"], [raw, epochs, evoked]):
        print(f"{inst_type}:")
        print(f"    bads: {inst.info['bads']}")
        print(f"    data shape: {inst.get_data(picks='eeg').shape}")

    Results in:

    raw:
        bads: ['EEG 053']
        data shape: (60, 36038)
    epochs:
        bads: ['EEG 053']
        data shape: (86, 59, 421)
    evoked:
        bads: ['EEG 053']
        data shape: (60, 421)
    

    Behavior when exclude=()

    Now, both functions will not exclude bad channels:

    for inst_type, inst in zip(["raw", "epochs", "evoked"], [raw, epochs, evoked]):
        print(f"{inst_type}:")
        print(f"    bads: {inst.info['bads']}")
        print(f"    data shape: {inst.get_data(picks='eeg', exclude=()).shape}")

    Gives us:

    raw:
        bads: ['EEG 053']
        data shape: (60, 36038)
    epochs:
        bads: ['EEG 053']
        data shape: (86, 60, 421)
    evoked:
        bads: ['EEG 053']
        data shape: (60, 421)
    

    Behavior when exclude="bads"

    Now, both functions will exclude bad channels:

    for inst_type, inst in zip(["raw", "epochs", "evoked"], [raw, epochs, evoked]):
        print(f"{inst_type}:")
        print(f"    bads: {inst.info['bads']}")
        print(f"    data shape: {inst.get_data(picks='eeg', exclude='bads').shape}")

    Resulting in:

    raw:
        bads: ['EEG 053']
        data shape: (59, 36038)
    epochs:
        bads: ['EEG 053']
        data shape: (86, 59, 421)
    evoked:
        bads: ['EEG 053']
        data shape: (59, 421)
    

    Question

    Lastly, we would like to ask you about the following lines of code in epochs.py at the _get_data function:

            if orig_picks is None:
                picks = _picks_to_idx(self.info, picks, "all", exclude=())
            else:
                picks = _picks_to_idx(self.info, picks, exclude=exclude)

    Here you can see that we changed the line inside the else block to add the exclude parameter. However, the if block is still "hard coded" to exclude=(). Should it also use the new exclude parameter? If so, we would have to figure out a way to keep the old behavior, since the else block would need a default exclude=bads, which we are using, and the if block would need exclude=().

    Conclusion

    There are two functions, in raw and evoked, use default exclude=(). The other three, in epochs, spectrum and tfr use default `exclude='bads'. If we were to do a deprecation cycle, we would have to think more about which one makes more sense. Moreover, do you have any suggestions of issues we could work on?

  19. modified the milestones: 1.11, 1.12 on Nov 13, 2025
  20. larsoner commented on Nov 25, 2025

    @larsoner
    Member

    Maintained Previous Behavior:

    For this, rather than using print how about you write some tiny test on main that should pass that asserts something about shapes of returned data?

    Then you add the exclude param defaults, and your test ensures that you haven't changed behavior.

    Then you append to that same test some calls with different exclude values and assert something new/different about the obtained shapes.

    This is a variant of test-driven development (strictly speaking there you'd write the whole test first even though it would fail, then modify code, but in practice it's nice to see stuff green / passing to start)

    Lastly, we would like to ask you about the following lines of code in epochs.py at the _get_data function:

    I would try not to mess with this one. Internal functions get split to handle specific use cases... the epochs code gets used all over the place so at the end of the day having CIs/tests green should tell us if things are okay or not.

    If we were to do a deprecation cycle, we would have to think more about which one makes more sense.

    Let's not do that for now... I think it would be more end-user pain that it's worth.

    Moreover, do you have any suggestions of issues we could work on?

    Anything with the EASY label should in principle be somewhat easy, at least that's the goal 😄

    https://github.com/mne-tools/mne-python/issues?q=state%3Aopen%20label%3AEASY

  21. added a commit that references this issue on Feb 5, 2026
  22. removed this from the 1.12 milestone on Apr 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions