{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n\n# Handling bad channels\n\nThis tutorial covers manual marking of bad channels and reconstructing bad\nchannels based on good signals at other sensors.\n\nAs usual we'll start by importing the modules we need, and loading some example\ndata:\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "# Authors: The MNE-Python contributors.\n# License: BSD-3-Clause\n# Copyright the MNE-Python contributors." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "import os\nfrom copy import deepcopy\n\nimport numpy as np\n\nimport mne\n\nsample_data_folder = mne.datasets.sample.data_path()\nsample_data_raw_file = os.path.join(\n sample_data_folder, \"MEG\", \"sample\", \"sample_audvis_raw.fif\"\n)\nraw = mne.io.read_raw_fif(sample_data_raw_file, verbose=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Marking bad channels\n\nSometimes individual channels malfunction and provide data that is too noisy\nto be usable. MNE-Python makes it easy to ignore those channels in the\nanalysis stream without actually deleting the data in those channels. It does\nthis by\nkeeping track of the bad channel indices in a list and looking at that list\nwhen doing analysis or plotting tasks. The list of bad channels is stored in\nthe ``'bads'`` field of the :class:`~mne.Info` object that is attached to\n:class:`~mne.io.Raw`, :class:`~mne.Epochs`, and :class:`~mne.Evoked` objects.\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "print(raw.info[\"bads\"])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Here you can see that the :file:`.fif` file we loaded from disk must have\nbeen keeping track of channels marked as \"bad\" \u2014 which is good news, because\nit means any changes we make to the list of bad channels will be preserved if\nwe save our data at intermediate stages and re-load it later. Since we saw\nabove that ``EEG 053`` is one of the bad channels, let's look at it alongside\nsome other EEG channels to see what's bad about it. We can do this using the\nstandard :meth:`~mne.io.Raw.plot` method, and instead of listing the channel\nnames one by one (``['EEG 050', 'EEG 051', ...]``) we'll use a `regular\nexpression`_ to pick all the EEG channels between 050 and 059 with the\n:func:`~mne.pick_channels_regexp` function (the ``.`` is a wildcard\ncharacter):\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "picks = mne.pick_channels_regexp(raw.ch_names, regexp=\"EEG 05.\")\nraw.plot(order=picks, n_channels=len(picks))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can do the same thing for the bad MEG channel (``MEG 2443``). Since we\nknow that Neuromag systems (like the one used to record the example data) use\nthe last digit of the MEG channel number to indicate sensor type, here our\n`regular expression`_ will pick all the channels that start with 2 and end\nwith 3:\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "picks = mne.pick_channels_regexp(raw.ch_names, regexp=\"MEG 2..3\")\nraw.plot(order=picks, n_channels=len(picks))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Notice first of all that the channels marked as \"bad\" are plotted in a light\ngray color in a layer behind the other channels, to make it easy to\ndistinguish them from \"good\" channels. The plots make it clear that ``EEG\n053`` is not picking up scalp potentials at all, and ``MEG 2443`` looks like\nit's got a lot more internal noise than its neighbors \u2014 its signal is a few\norders of magnitude greater than the other MEG channels, making it a clear\ncandidate for exclusion.\n\nIf you want to change which channels are marked as bad, you can edit\n``raw.info['bads']`` directly; it's an ordinary Python :class:`list` so the\nusual list methods will work:\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "original_bads = deepcopy(raw.info[\"bads\"])\nraw.info[\"bads\"].append(\"EEG 050\") # add a single channel\nraw.info[\"bads\"].extend([\"EEG 051\", \"EEG 052\"]) # add a list of channels\nbad_chan = raw.info[\"bads\"].pop(-1) # remove the last entry in the list\nraw.info[\"bads\"] = original_bads # change the whole list at once" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ ".. admonition:: Blocking execution\n :class: sidebar hint\n\n If you want to build an interactive bad-channel-marking step into an\n analysis script, be sure to include the parameter ``block=True`` in your\n call to ``raw.plot()`` or ``epochs.plot()``. This will pause the script\n while the plot is open, giving you time to mark bad channels before\n subsequent analysis or plotting steps are executed. This can be\n especially helpful if your script loops over multiple subjects.\n\nYou can also interactively toggle whether a channel is marked \"bad\" in the\nplot windows of ``raw.plot()`` or ``epochs.plot()`` by clicking on the\nchannel name along the vertical axis (in ``raw.plot()`` windows you can also\ndo this by clicking the channel's trace in the plot area). The ``bads`` field\ngets updated immediately each time you toggle a channel, and will retain its\nmodified state after the plot window is closed.\n\nThe list of bad channels in the :class:`mne.Info` object's ``bads`` field is\nautomatically taken into account in dozens of functions and methods across\nthe MNE-Python codebase. This is done consistently with a parameter\n``exclude='bads'`` in the function or method signature. Typically this\n``exclude`` parameter also accepts a list of channel names or indices, so if\nyou want to *include* the bad channels you can do so by passing\n``exclude=[]`` (or some other list of channels to exclude). For example:\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "# default is exclude='bads':\ngood_eeg = mne.pick_types(raw.info, meg=False, eeg=True)\nall_eeg = mne.pick_types(raw.info, meg=False, eeg=True, exclude=[])\nprint(np.setdiff1d(all_eeg, good_eeg))\nprint(np.array(raw.ch_names)[np.setdiff1d(all_eeg, good_eeg)])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### When to look for bad channels\n\nYou can start looking for bad channels during the experiment session when the\ndata is being acquired. If you notice any flat or excessively noisy channels,\nyou can note them in your experiment log or protocol sheet. If your system\ncomputes online averages, these can be a good way to spot bad channels as\nwell. After the data has been collected, you can do a more thorough check for\nbad channels by browsing the raw data using :meth:`mne.io.Raw.plot`, without\nany projectors or ICA applied. Finally, you can compute offline averages\n(again with projectors, ICA, and EEG referencing disabled) to look for\nchannels with unusual properties. Here's an example of ERP/F plots where the\nbad channels were not properly marked:\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "raw2 = raw.copy()\nraw2.info[\"bads\"] = []\nevents = mne.find_events(raw2, stim_channel=\"STI 014\")\nepochs = mne.Epochs(raw2, events=events)[\"2\"].average().plot()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The bad EEG channel is not so obvious, but the bad gradiometer is easy to\nsee.\n\nRemember, marking bad channels should be done as early as possible in the\nanalysis pipeline. When bad channels are marked in a :class:`~mne.io.Raw`\nobject, the markings will be automatically transferred through the chain of\nderived object types: including :class:`~mne.Epochs` and :class:`~mne.Evoked`\nobjects, but also :class:`noise covariance ` objects,\n:class:`forward solution computations `, :class:`inverse\noperators `, etc. If you don't notice the\nbadness until later stages of your analysis pipeline, you'll probably need to\ngo back and re-run the pipeline, so it's a good investment of time to\ncarefully explore the data for bad channels early on.\n\n\n### Why mark bad channels at all?\n\nMany analysis computations can be strongly affected by the presence of bad\nchannels. For example, a malfunctioning channel with completely flat signal\nwill have zero channel variance, which will cause noise estimates to be\nunrealistically low. This low noise estimate will lead to a strong channel\nweight in the estimate of cortical current, and because the channel is flat,\nthe magnitude of cortical current estimates will shrink dramatically.\n\nConversely, very noisy channels can also cause problems. For example, they\ncan lead to too many epochs being discarded based on signal amplitude\nrejection thresholds, which in turn can lead to less robust estimation of the\nnoise covariance across sensors. Noisy channels can also interfere with\n:term:`SSP` computations, because the projectors will be\nspatially biased in the direction of the noisy channel, which can cause\nadjacent good channels to be suppressed. ICA is corrupted by noisy channels\nfor similar reasons. On the other hand, when performing machine learning\nanalyses, bad channels may have limited, if any impact (i.e., bad channels\nwill be uninformative and therefore ignored / deweighted by the algorithm).\n\n\n## Interpolating bad channels\n\nIn some cases simply excluding bad channels is sufficient (for example, if\nyou plan only to analyze a specific sensor ROI, and the bad channel is\noutside that ROI). However, in cross-subject analyses it is often helpful to\nmaintain the same data dimensionality for all subjects, and there is no\nguarantee that the same channels will be bad for all subjects. It is possible\nin such cases to remove each channel that is bad for even a single subject,\nbut that can lead to a dramatic drop in data rank (and ends up discarding a\nfair amount of clean data in the process). In such cases it is desirable to\nreconstruct bad channels by interpolating its signal based on the signals of\nthe good sensors around them.\n\n\n### How interpolation works\n\nInterpolation of EEG channels in MNE-Python is done using the spherical\nspline method :footcite:`PerrinEtAl1989`, which projects the sensor\nlocations onto a unit sphere\nand interpolates the signal at the bad sensor locations based on the signals\nat the good locations. Mathematical details are presented in\n`channel-interpolation`. Interpolation of MEG channels uses the field\nmapping algorithms used in computing the `forward solution\n`.\n\n\n### Interpolation in MNE-Python\n\nInterpolating bad channels in :class:`~mne.io.Raw` objects is done with the\n:meth:`~mne.io.Raw.interpolate_bads` method, which automatically applies the\ncorrect method (spherical splines or field interpolation) to EEG and MEG\nchannels, respectively (there is a corresponding method\n:meth:`mne.Epochs.interpolate_bads` that works for :class:`~mne.Epochs`\nobjects). To illustrate how it works, we'll start by cropping the raw object\nto just three seconds for easier plotting:\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "raw.crop(tmin=0, tmax=3).load_data()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "By default, :meth:`~mne.io.Raw.interpolate_bads` will clear out\n``raw.info['bads']`` after interpolation, so that the interpolated channels\nare no longer excluded from subsequent computations. Here, for illustration\npurposes, we'll prevent that by specifying ``reset_bads=False`` so that when\nwe plot the data before and after interpolation, the affected channels will\nstill plot in red:\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "eeg_data = raw.copy().pick(picks=\"eeg\")\neeg_data_interp = eeg_data.copy().interpolate_bads(reset_bads=False)\n\nfor title, data in zip([\"orig.\", \"interp.\"], [eeg_data, eeg_data_interp]):\n with mne.viz.use_browser_backend(\"matplotlib\"):\n fig = data.plot(butterfly=True, color=\"#00000022\", bad_color=\"r\")\n fig.subplots_adjust(top=0.9)\n fig.suptitle(title, size=\"xx-large\", weight=\"bold\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that the method :meth:`~mne.io.Raw.pick` default\narguments includes ``exclude=()`` which ensures that bad\nchannels are not\nautomatically dropped from the selection. Here is the corresponding example\nwith the interpolated gradiometer channel; since there are more channels\nwe'll use a more transparent gray color this time:\n\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "collapsed": false }, "outputs": [], "source": [ "grad_data = raw.copy().pick(picks=\"grad\")\ngrad_data_interp = grad_data.copy().interpolate_bads(reset_bads=False)\n\nfor data in (grad_data, grad_data_interp):\n data.plot(butterfly=True, color=\"#00000009\", bad_color=\"r\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Summary\n\nBad channel exclusion or interpolation is an important step in EEG/MEG\npreprocessing. MNE-Python provides tools for marking and interpolating bad\nchannels; the list of which channels are marked as \"bad\" is propagated\nautomatically through later stages of processing. For an even more automated\napproach to bad channel detection and interpolation, consider using the\n`autoreject package`_, which interfaces well with MNE-Python-based pipelines.\n\n\n## References\n\n.. footbibliography::\n\n\n.. LINKS\n\n\n" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.2" } }, "nbformat": 4, "nbformat_minor": 0 }