ica.plot_overlay() gives strange results

  • MNE version: 1.3.1
  • operating system: macOS 13.1

Dear all,

this is a follow-up on my question during the mne office hour. Here is a screenshot of the ica.plot_overlay() for one participant I plotted after excluding EOG artifacts (I followed this tutorial: Artifacts in EEG Data — Data Science for Psychology and Neuroscience — in Python). The plot looks very different compared to the one shown on the mne website (Repairing artifacts with ICA — MNE 1.4.2 documentation) - especially the y-axis of the lower plot.

icaoverlay_WENA09AG

When comparing the raw data to the clean data after applying the ICA, it appears that the ICA did its job but I’m still wondering if something went wrong.
Here is a dropbox link for a reproducible example (@drammock @agramfort)

Thank you very much in advance!

Hello,

I started having a look, one point came-up:

  • Rejection of epochs via autoreject: you chose to fit the ICA on epochs instead of the raw recording, and you compute a PTP rejection threshold on the epochs via autoreject. But then, instead of dropping the “bad” epochs, you provide this rejection threshold to ica.fit(..., reject=reject). As per the documentation, this reject argument is only used if you fit on a Raw, not on Epochs, in which case you should directly use drop_bad to remove the bad epochs.

That said, this point does not explain the issue. I am able to reproduce, for anyone interested, here is a MWE shortening the provided script:

from pathlib import Path

from mne import Annotations, Epochs, make_fixed_length_events
from mne.io import read_raw_brainvision
from mne.preprocessing import ICA, annotate_break


fname = Path("data") / "brainvision_raw" / "WENA09AG.vhdr"
raw = read_raw_brainvision(fname, preload=True)
raw.set_montage("brainproducts-RNP-BA-128")
raw.drop_channels("ECG")
raw.annotations.delete(raw.annotations.description == "New Segment/")

break_annotations = annotate_break(
    raw=raw, min_break_duration=20, t_start_after_previous=2, t_stop_before_next=2
)

end_onset = raw.annotations[-1]["onset"] + 2.0  # add 2 seconds
end_duration = raw.times[-1] - end_onset
end_annotation = Annotations(
    onset=end_onset,
    duration=end_duration,
    description="BAD_end",
    orig_time=raw.annotations.orig_time,
)

raw.set_annotations(raw.annotations + break_annotations + end_annotation)

# filter and reref
raw.filter(1, 30)
raw.set_eeg_reference(ref_channels="average")

tstep = 1.0
events_ica = make_fixed_length_events(raw, duration=tstep)
epochs_ica = Epochs(
    raw,
    events_ica,
    tmin=0.0,
    tmax=tstep,
    reject_by_annotation=True,
    baseline=None,
    preload=True,
)

random_state = 42
ica_n_components = .99
ica = ICA(n_components=0.99, random_state=42)
ica.fit(epochs_ica)
ica.exclude = [0, 1]  # eye-movements
ica.plot_overlay(inst=raw)

I’ll have a deeper look when I can, nothing which explains this second plot comes up to mind in that part. It is however “fixed” by fitting on the non-re referenced raw directly.

from pathlib import Path

from mne import Annotations
from mne.io import read_raw_brainvision
from mne.preprocessing import ICA, annotate_break


fname = Path("data") / "brainvision_raw" / "WENA09AG.vhdr"
raw = read_raw_brainvision(fname, preload=True)
raw.set_montage("brainproducts-RNP-BA-128")
raw.drop_channels("ECG")
raw.annotations.delete(raw.annotations.description == "New Segment/")

break_annotations = annotate_break(
    raw=raw, min_break_duration=20, t_start_after_previous=2, t_stop_before_next=2
)

end_onset = raw.annotations[-1]["onset"] + 2.0  # add 2 seconds
end_duration = raw.times[-1] - end_onset
end_annotation = Annotations(
    onset=end_onset,
    duration=end_duration,
    description="BAD_end",
    orig_time=raw.annotations.orig_time,
)
raw.set_annotations(raw.annotations + break_annotations + end_annotation)

# filter and reref
raw.filter(1, 30)

random_state = 42
ica_n_components = .99
ica = ICA(n_components=0.99, random_state=42)
ica.fit(raw)
ica.exclude = [0, 1]
ica.plot_overlay(inst=raw)

Screenshot from 2023-07-10 14-03-28

EDIT: Removing my erroneous comment on the filters. They are correct.
EDIT 2: Second plot still looks wrong, I would expect to see the blink deflection in red.

Best,
Mathieu

2 Likes

Seems to be related to the EEG average reference. If you fit on non-re referenced epochs and then plot the overlay with the raw, it looks correct. Weird second plot in the 1e-20 scale reproducible with the sample dataset:

from mne import make_fixed_length_epochs
from mne.datasets import sample
from mne.io import read_raw_fif
from mne.preprocessing import ICA


fname = sample.data_path() / "MEG" / "sample" / "sample_audvis_raw.fif"
raw = read_raw_fif(fname, preload=False)
raw.crop(0, 60).pick("eeg")
raw.load_data()
raw.filter(1., 40.)
raw.set_eeg_reference("average")
epochs = make_fixed_length_epochs(raw, duration=1.)

ica = ICA(n_components=0.99, random_state=101)
ica.fit(epochs)

Screenshot from 2023-07-10 16-36-19

Note the red trace despite ica.exclude = []. Something is going wrong, comment the line raw.set_eeg_reference("average") and plot looks OK, without the red trace:

Screenshot from 2023-07-10 16-38-49

Mathieu

Hi Mathieu,

thank you for your quick response! Regarding the ICA part, I changed the code to:

# 8. Ocular ICA

## Filter settings
ica_low = 1.0
ica_high = 30

raw_ica = raw_rereferenced.copy().filter(ica_low, ica_high)

## Break raw data into 1s epochs
tstep = 1.0
events_ica = mne.make_fixed_length_events(raw_ica, duration=tstep)
epochs_ica = mne.Epochs(raw_ica, events_ica,
                        tmin=0.0, 
                        tmax=tstep,
                        reject_by_annotation = True,
                        baseline=None,
                        preload=True)

## use autoreject package to automatically determine a threshold to use to find sections of the data that are excessively noisy
# reject = get_rejection_threshold(epochs_ica);  # <- CHANGED
# reject  # <- CHANGED

epochs_ica.drop_bad()  # <- CHANGED

## ICA parameters
random_state = 42
ica_n_components = .99

## Fit ICA
ica = mne.preprocessing.ICA(n_components = ica_n_components, random_state = random_state)

ica.fit(epochs_ica, tstep = tstep)  # <- CHANGED

## ICA components
ica_components = ica.plot_components(show = False)

for i, fig in enumerate(ica_components): 
    fig.savefig(f'/Users/elisabethsterner/Dropbox/ReproducibleExample_ES03/data/ica/icacomponents{i+1}_WENA09AG_mastoids.png')
    plt.close(fig)

# 9. Artifact rejection

## exclude ocular artifacts
ica_z_thresh = 1.96
eog_indices, eog_scores = ica.find_bads_eog(raw_ica, ch_name=['Fp1', 'F8'], threshold = "auto")
ica.exclude = eog_indices
ica_excluded = ica.plot_scores(eog_scores, show = False)
ica_excluded.savefig('/Users/elisabethsterner/Dropbox/ReproducibleExample_ES03/plots/ica/ica_excluded_WENA09AG_mastoids.png')
ica.save("/Users/elisabethsterner/Dropbox/ReproducibleExample_ES03/data/ica/ica_WENA09AG_mastoids.fif", overwrite = True); # ICA object includes the bads aatribute that indicates which components to reject as ocular artifacts

## plot rereferenced vs. clean data
ica_overlay = ica.plot_overlay(raw_rereferenced, picks = "eeg", show = False)
ica_overlay.savefig('/Users/elisabethsterner/Dropbox/ReproducibleExample_ES03/plots/ica/icaoverlay_WENA09AG_mastoids.png')

## Apply ICA to rereferenced data
raw_clean = raw_rereferenced.copy()
ica.apply(raw_clean)
raw_clean.save("/Users/elisabethsterner/Dropbox/ReproducibleExample_ES03/data/clean/WENA09AG_clean_mastoids.fif", overwrite = True)

Is this a better solution?

When I apply the ICA to my continuous referenced data. (“ica.apply(raw_clean)”) will the bad epochs be automatically dropped as well? Or are the bad epochs just not used to compute the ICA components?

Hi Mathieu,

thank you!! Do you have any idea why this might be the case? When I rereference to the mastoids (TP9, TP10) instead, the plot looks fine:

icaoverlay_WENA09AG_mastoids

I’m a little hesitant to use the mastoids though as both electrodes were quite noisy for some participants. Do you have any idea what I could do instead?

Best,
Elisabeth

Hello,

I haven’t looked at your code yet, will do a bit later and give you feedback.
For the reref, average is fine. I don’t like mastoid ref for the same reason. However, what I would do is clean the data on the default ref and then reref at the end.

raw = ...
raw.filter(...)
ica = ICA(...)
ica.fit(raw)
ica.apply(raw)
# and now re-reference
raw.set_eeg_reference(...)

That said, I think it’s a bug with ica.plot_overlay which affects only this viz function, but I haven’t looked through the codebase yet.

Mathieu

For your code update, it looks like yo are not dropping bad epochs on PTP anymore. In your code, epochs_ica.drop_bad() is probably doing nothing. tstep is also doing nothing in the ICA fit. I would do:

from autoreject import get_rejection_threshold
from mne import make_fixed_length_epochs
from mne.preprocessing import ICA

raw = ...  # filtered between 1 and 30 Hz, default reference

# default to duration: 1 and reject_by_annotation: True
epochs = make_fixed_length_epochs(raw, preload=True)
reject = get_rejection_threshold(epochs)
epochs.drop_bad(reject)
ica = ICA(n_components=0.99, method="picard")
ica.fit(epochs)

# select ICA components to remove
ica.exclude = [...]
ica.apply(raw)

# re-ref to average
raw.set_eeg_reference("average")

And no, the reject and drop_bad is only here to drop epochs used to fit the ICA. Same if you provide them in ica.fit(...) as you did previously (and if you were fitting on a raw). They do not affect the raw signal on which the ICA is applied.

Mathieu

Hi Mathieu,

I have a few follow-up questions.

  1. I don’t want to exclude ICA components manually but correlate them with my eye electrodes to automatically exclude eye artifacts. So after this code section you sent me

"raw = … # filtered between 1 and 30 Hz, default reference

default to duration: 1 and reject_by_annotation: True

epochs = make_fixed_length_epochs(raw, preload=True)
reject = get_rejection_threshold(epochs)
epochs.drop_bad(reject)
ica = ICA(n_components=0.99, method=“picard”)
ica.fit(epochs)"

can I just add my previous code snippet:

“ica_z_thresh = 1.96
eog_indices, eog_scores = ica.find_bads_eog(raw_ica, ch_name=[‘Fp1’, ‘F8’], threshold = “auto”)
ica.exclude = eog_indices
ica_excluded = ica.plot_scores(eog_scores, show = False)
ica_excluded.savefig(‘/Users/elisabethsterner/Dropbox/ReproducibleExample_ES03/plots/ica/ica_excluded_WENA09AG_mastoids.png’)
ica.save(”/Users/elisabethsterner/Dropbox/ReproducibleExample_ES03/data/ica/ica_WENA09AG_mastoids.fif", overwrite = True); # ICA object includes the bads aatribute that indicates which components to reject as ocular artifacts" ?

  1. Is it common practice to rereference only after the ICA? I looked into some standardized pipelines and most of them rereference directly after filtering.

  2. My last question relates back to the ica.plot_averlay() issue. Can you elaborate why you think it’s a bug with ica.plot_overlay and not the rereferencing based on the average? If it is be a bug than I would expect a similar plot with rereferencing to the mastoids, but the plot looks fine in this case.

Thank you very much!

Best,
Elisabeth

Hello,

Yes you can do the exclusion however you see fit.
You can reref before or after, it doesn’t matter much, as long as you fit and apply on the same reference. With the bug within plot_overlay, reref after seems better in this case.

And it’s more of an hunch. set_eeg_reference, especially with an 'average' reference is used very often. Same for ica.apply which conceptually should do the same as plot_overlay. On the contrary, this viz function is not used as often. For instance, I don’t use it. Thus, I believe the bug lies within plot_overlay rather than ica.apply and set_eeg_reference.
But I still have to look into the code to figure that one out, I might have the time today.

Mathieu

Hello,

I had a look this morning. The good news, is that everything works as intended, this is not a bug. The bad news is that I don’t really know how to improve this plot for now. If you have EEG channels, set an average reference and then average the channels together you get values very close to 0. Which makes sense since you end up with roughly 50/50 positive and negative EEG channels. Demo below:

from matplotlib import pyplot as plt
from mne.datasets import sample
from mne.io import read_raw_fif


fname = sample.data_path() / "MEG" / "sample" / "sample_audvis_raw.fif"
raw = read_raw_fif(fname, preload=False)
raw.crop(0, 60).pick("eeg", exclude="bads")
raw.load_data()
raw.filter(1., 40.)
raw.set_eeg_reference("average")
plt.plot(raw.get_data().mean(axis=0))

Screenshot from 2023-07-14 10-50-35

from matplotlib import pyplot as plt
from mne.datasets import sample
from mne.io import read_raw_fif
from mne.viz import plot_topomap


fname = sample.data_path() / "MEG" / "sample" / "sample_audvis_raw.fif"
raw = read_raw_fif(fname, preload=False)
raw.crop(0, 60).pick("eeg", exclude="bads")
raw.load_data()
raw.filter(1., 40.)

f, ax = plt.subplots(1, 2)
ax[0].set_title("Default ref")
plot_topomap(raw.get_data().mean(axis=1), raw.info, axes=ax[0])
raw.set_eeg_reference("average")
ax[1].set_title("Average ref")
plot_topomap(raw.get_data().mean(axis=1), raw.info, axes=ax[1])

Screenshot from 2023-07-14 10-54-38

So the real issue is that the second plot of plot_overlay which averages channels together is absolutely not informative when a common average reference schema is in use. This second plot is very dependent on the reference in use, and my guess is that it was not designed with EEG data in mind.

Mathieu

2 Likes