Note
Go to the end to download the full example code.
Quality control (QC) reports with mne.Report#
Quality control (QC) is the process of systematically inspecting M/EEG data throughout all stages of an analysis pipeline, including raw data, intermediate preprocessing steps, and derived results.
While QC often begins with an initial inspection of the raw recording, it is equally important to verify that signals continue to “look reasonable” after operations such as filtering, artifact correction, epoching, and averaging. Issues introduced or missed at any stage can propagate downstream and invalidate later analyses.
This tutorial demonstrates how to create a single, narrative QC report
using mne.Report, focusing on what should be inspected and how the
results should be interpreted, rather than exhaustively covering the API.
For clarity and reproducibility, the examples below focus on common QC checks applied at representative stages of an analysis pipeline. The same reporting approach can—and should—be reused whenever new processing steps are applied.
We use the MNE sample dataset for demonstration. Not all QC sections are applicable to every dataset (e.g., continuous head-position tracking), and this tutorial explicitly handles such cases.
Note
For several additional examples of complete reports, see the MNE-BIDS-Pipeline QC reports.
# Authors: The MNE-Python contributors
# License: BSD-3-Clause
# Copyright the MNE-Python contributors.
from pathlib import Path
import mne
from mne.preprocessing import ICA, create_eog_epochs
Load the sample dataset#
We load a pre-filtered MEG/EEG recording from the MNE sample dataset. Only channels relevant for QC (MEG, EEG, EOG, stimulus) are retained.
data_path = Path(mne.datasets.sample.data_path(verbose=False))
subject = "sample"
sample_dir = data_path / "MEG" / subject
subjects_dir = data_path / "subjects"
raw_path = sample_dir / "sample_audvis_filt-0-40_raw.fif"
raw = mne.io.read_raw(raw_path)
# We will also crop the dataset for speed
raw.crop(0, 60).load_data()
# Retain only channels relevant for QC to simplify visualization and
# focus inspection on signals typically reviewed during data quality checks.
raw.pick(["meg", "eeg", "eog", "stim"])
sfreq = raw.info["sfreq"] # Sampling Frequency (Hz)
Opening raw data file /home/circleci/mne_data/MNE-sample-data/MEG/sample/sample_audvis_filt-0-40_raw.fif...
Read a total of 4 projection items:
PCA-v1 (1 x 102) idle
PCA-v2 (1 x 102) idle
PCA-v3 (1 x 102) idle
Average EEG reference (1 x 60) idle
Range : 6450 ... 48149 = 42.956 ... 320.665 secs
Ready.
Reading 0 ... 9009 = 0.000 ... 59.999 secs...
Create the QC report#
The report acts as a container that collects figures, tables, and text into a single HTML document.
report = mne.Report(
title="Sample dataset - Quality Control report",
subject=subject,
subjects_dir=subjects_dir,
)
Embedding : jquery-3.6.0.min.js
Embedding : bootstrap.bundle.min.js
Embedding : bootstrap.min.css
Embedding : bootstrap-table/bootstrap-table.min.js
Embedding : bootstrap-table/bootstrap-table.min.css
Embedding : bootstrap-table/bootstrap-table-copy-rows.min.js
Embedding : bootstrap-table/bootstrap-table-export.min.js
Embedding : bootstrap-table/tableExport.min.js
Embedding : bootstrap-icons/bootstrap-icons.mne.min.css
Embedding : highlightjs/highlight.min.js
Embedding : highlightjs/atom-one-dark-reasonable.min.css
Dataset overview#
A brief overview helps the reviewer immediately understand the scale and basic properties of the dataset.
html_overview = """
This report presents a quality control (QC) overview of the MNE sample dataset.<br><br>
For information about the paradigm, see
<a href="https://mne.tools/stable/documentation/datasets.html#sample">the MNE docs</a>.
"""
report.add_html(
title="Overview",
html=html_overview,
tags=("overview"),
)
Raw data inspection#
Visual inspection of raw data is the single most important QC step. Here we inspect both the time series and the power spectral density (PSD).
Look for channels with unusually large amplitudes or flat signals.
In the PSD, check for excessive low-frequency drift, strong line noise, or abnormal spectral shapes compared to neighboring channels.
report.add_raw(
raw,
title="Raw data overview",
psd=False, # omit just for speed here
)
Using matplotlib as 2D backend.
Using qt as 2D backend.
Events and stimulus timing#
Correct event detection is crucial for all subsequent epoch-based analyses.
Verify that the number of events matches expectations.
Check that event timing is plausible and evenly distributed.
Missing or duplicated events often indicate trigger channel issues.
events = mne.find_events(raw)
report.add_events(
events,
sfreq=sfreq,
title="Detected events",
)
Finding events on: STI 014
86 events found on stim channel STI 014
Event IDs: [ 1 2 3 4 5 32]
Epoching and rejection statistics#
Epoching allows inspection of data segments time-locked to events, along with automated rejection based on amplitude thresholds.
event_id = {
"auditory/left": 1,
"auditory/right": 2,
"visual/left": 3,
"visual/right": 4,
}
epochs = mne.Epochs(
raw,
events,
event_id=event_id,
tmin=-0.2,
tmax=0.5,
baseline=(None, 0),
reject=dict(eeg=150e-6),
preload=True,
)
report.add_epochs(
epochs,
title="Epochs and rejection statistics",
)
Not setting metadata
78 matching events found
Setting baseline interval to [-0.19979521315838786, 0.0] s
Applying baseline correction (mode: mean)
Created an SSP operator (subspace dimension = 4)
4 projection items activated
Using data from preloaded Raw for 78 events and 106 original time points ...
Rejecting epoch based on EEG : ['EEG 001', 'EEG 002', 'EEG 003', 'EEG 007']
1 bad epochs dropped
Using multitaper spectrum estimation with 7 DPSS windows
Plotting power spectral density (dB=True).
Averaging across epochs before plotting...
Averaging across epochs before plotting...
Averaging across epochs before plotting...
Evoked responses#
Averaged responses should show physiologically plausible waveforms and reasonable signal-to-noise ratios.
Check that evoked responses have the expected polarity and timing.
Absence of clear evoked structure may indicate poor data quality or incorrect event definitions.
cov_path = sample_dir / "sample_audvis-cov.fif"
evoked = mne.read_evokeds(
sample_dir / "sample_audvis-ave.fif",
baseline=(None, 0),
)[0] # just one for speed
evoked.decimate(4) # also for speed
report.add_evokeds(
evokeds=evoked,
noise_cov=cov_path,
n_time_points=5,
)
Reading /home/circleci/mne_data/MNE-sample-data/MEG/sample/sample_audvis-ave.fif ...
Read a total of 4 projection items:
PCA-v1 (1 x 102) active
PCA-v2 (1 x 102) active
PCA-v3 (1 x 102) active
Average EEG reference (1 x 60) active
Found the data of interest:
t = -199.80 ... 499.49 ms (Left Auditory)
0 CTF compensation matrices available
nave = 55 - aspect type = 100
Projections have already been applied. Setting proj attribute to True.
Applying baseline correction (mode: mean)
Read a total of 4 projection items:
PCA-v1 (1 x 102) active
PCA-v2 (1 x 102) active
PCA-v3 (1 x 102) active
Average EEG reference (1 x 60) active
Found the data of interest:
t = -199.80 ... 499.49 ms (Right Auditory)
0 CTF compensation matrices available
nave = 61 - aspect type = 100
Projections have already been applied. Setting proj attribute to True.
Applying baseline correction (mode: mean)
Read a total of 4 projection items:
PCA-v1 (1 x 102) active
PCA-v2 (1 x 102) active
PCA-v3 (1 x 102) active
Average EEG reference (1 x 60) active
Found the data of interest:
t = -199.80 ... 499.49 ms (Left visual)
0 CTF compensation matrices available
nave = 67 - aspect type = 100
Projections have already been applied. Setting proj attribute to True.
Applying baseline correction (mode: mean)
Read a total of 4 projection items:
PCA-v1 (1 x 102) active
PCA-v2 (1 x 102) active
PCA-v3 (1 x 102) active
Average EEG reference (1 x 60) active
Found the data of interest:
t = -199.80 ... 499.49 ms (Right visual)
0 CTF compensation matrices available
nave = 58 - aspect type = 100
Projections have already been applied. Setting proj attribute to True.
Applying baseline correction (mode: mean)
366 x 366 full covariance (kind = 1) found.
Read a total of 4 projection items:
PCA-v1 (1 x 102) active
PCA-v2 (1 x 102) active
PCA-v3 (1 x 102) active
Average EEG reference (1 x 60) active
NOTE: pick_types() is a legacy function. New code should use inst.pick(...).
Computing rank from covariance with rank=None
Using tolerance 4.7e-14 (2.2e-16 eps * 59 dim * 3.6 max singular value)
Estimated rank (eeg): 58
EEG: rank 58 computed from 59 data channels with 1 projector
Computing rank from covariance with rank=None
Using tolerance 1.8e-13 (2.2e-16 eps * 203 dim * 3.9 max singular value)
Estimated rank (grad): 203
GRAD: rank 203 computed from 203 data channels with 0 projectors
Computing rank from covariance with rank=None
Using tolerance 2.5e-14 (2.2e-16 eps * 102 dim * 1.1 max singular value)
Estimated rank (mag): 99
MAG: rank 99 computed from 102 data channels with 3 projectors
Created an SSP operator (subspace dimension = 4)
Computing rank from covariance with rank={'eeg': 58, 'grad': 203, 'mag': 99, 'meg': 302}
Setting small MEG eigenvalues to zero (without PCA)
Setting small EEG eigenvalues to zero (without PCA)
Created the whitener using a noise covariance matrix with rank 360 (4 small eigenvalues omitted)
ICA for artifact inspection#
Independent Component Analysis (ICA) can be used during QC to identify stereotypical artifacts such as eye blinks and eye movements.
For QC purposes, ICA is typically run with a lightweight configuration (e.g., fewer components or temporal decimation) to provide rapid feedback on data quality, rather than an optimized decomposition for final analysis.
Use the topographic maps to identify spatial patterns characteristic of artifacts (e.g., frontal patterns for eye blinks).
The component property viewer is intended for detailed inspection of individual components and is most informative when combined with epoched data or explicit artifact scoring.
Components correlated with EOG should show frontal topographies and stereotyped time courses.
Only components clearly associated with artifacts should be excluded.
ica = ICA(
n_components=15,
random_state=97,
max_iter=50, # just for speed!
)
# Fit ICA using a decimated signal for speed
ica.fit(raw, picks=("meg", "eeg"), decim=10, verbose="error")
# Identify EOG-related components
eog_epochs = create_eog_epochs(raw)
eog_inds, eog_scores = ica.find_bads_eog(eog_epochs)
ica.exclude = eog_inds
report.add_ica(
ica=ica,
inst=epochs,
eog_evoked=eog_epochs.average(),
eog_scores=eog_scores,
title="ICA components (artifact inspection)",
)
Using EOG channel: EOG 061
EOG channel index for this subject is: [375]
Filtering the data to remove DC offset to help distinguish blinks from saccades
Selecting channel EOG 061 for blink detection
Setting up band-pass filter from 1 - 10 Hz
FIR filter parameters
---------------------
Designing a two-pass forward and reverse, zero-phase, non-causal bandpass filter:
- Windowed frequency-domain design (firwin2) method
- Hann window
- Lower passband edge: 1.00
- Lower transition bandwidth: 0.50 Hz (-12 dB cutoff frequency: 0.75 Hz)
- Upper passband edge: 10.00 Hz
- Upper transition bandwidth: 0.50 Hz (-12 dB cutoff frequency: 10.25 Hz)
- Filter length: 1502 samples (10.003 s)
Now detecting blinks and generating corresponding events
Found 10 significant peaks
Number of EOG events detected: 10
Not setting metadata
10 matching events found
No baseline correction applied
Created an SSP operator (subspace dimension = 4)
Using data from preloaded Raw for 10 events and 151 original time points ...
0 bad epochs dropped
Using EOG channel: EOG 061
Applying ICA to Evoked instance
Transforming to ICA space (15 components)
Zeroing out 1 ICA component
Projecting back using 364 PCA components
Applying baseline correction (mode: mean)
Using matplotlib as 2D backend.
Using qt as 2D backend.
Using multitaper spectrum estimation with 7 DPSS windows
Not setting metadata
77 matching events found
No baseline correction applied
0 projection items activated
Using multitaper spectrum estimation with 7 DPSS windows
Not setting metadata
77 matching events found
No baseline correction applied
0 projection items activated
Using multitaper spectrum estimation with 7 DPSS windows
Not setting metadata
77 matching events found
No baseline correction applied
0 projection items activated
Using multitaper spectrum estimation with 7 DPSS windows
Not setting metadata
77 matching events found
No baseline correction applied
0 projection items activated
Using multitaper spectrum estimation with 7 DPSS windows
Not setting metadata
77 matching events found
No baseline correction applied
0 projection items activated
Using multitaper spectrum estimation with 7 DPSS windows
Not setting metadata
77 matching events found
No baseline correction applied
0 projection items activated
Using multitaper spectrum estimation with 7 DPSS windows
Not setting metadata
77 matching events found
No baseline correction applied
0 projection items activated
Using multitaper spectrum estimation with 7 DPSS windows
Not setting metadata
77 matching events found
No baseline correction applied
0 projection items activated
Using multitaper spectrum estimation with 7 DPSS windows
Not setting metadata
77 matching events found
No baseline correction applied
0 projection items activated
Using multitaper spectrum estimation with 7 DPSS windows
Not setting metadata
77 matching events found
No baseline correction applied
0 projection items activated
Using multitaper spectrum estimation with 7 DPSS windows
Not setting metadata
77 matching events found
No baseline correction applied
0 projection items activated
Using multitaper spectrum estimation with 7 DPSS windows
Not setting metadata
77 matching events found
No baseline correction applied
0 projection items activated
Using multitaper spectrum estimation with 7 DPSS windows
Not setting metadata
77 matching events found
No baseline correction applied
0 projection items activated
Using multitaper spectrum estimation with 7 DPSS windows
Not setting metadata
77 matching events found
No baseline correction applied
0 projection items activated
Using multitaper spectrum estimation with 7 DPSS windows
Not setting metadata
77 matching events found
No baseline correction applied
0 projection items activated
MEG–MRI coregistration#
Accurate coregistration is critical for source localization.
Head shape points should align well with the MRI scalp surface.
Systematic misalignment indicates digitization or transformation errors.
trans = sample_dir / "sample_audvis_raw-trans.fif"
report.add_trans(
trans,
info=raw.info,
title="MEG–MRI-head coregistration",
subject=subject,
subjects_dir=subjects_dir,
)
Using outer_skin.surf for head surface.
Getting helmet for system 306m
Channel types:: grad: 203, mag: 102, eeg: 59
Using surface from /home/circleci/mne_data/MNE-sample-data/subjects/sample/bem/sample-head.fif.
MRI and BEM surfaces#
Boundary Element Method (BEM) surfaces define the head model used for forward and inverse solutions.
Surfaces should be smooth, closed, and non-intersecting.
Poorly formed surfaces can severely degrade source estimates.
report.add_bem(
subject,
subjects_dir=subjects_dir,
title="BEM surfaces",
decim=20, # for speed
)
Using surface: /home/circleci/mne_data/MNE-sample-data/subjects/sample/bem/inner_skull.surf
Using surface: /home/circleci/mne_data/MNE-sample-data/subjects/sample/bem/outer_skull.surf
Using surface: /home/circleci/mne_data/MNE-sample-data/subjects/sample/bem/outer_skin.surf
View the final report#
You can set open_browser=True to have it pop open a browser tab if you want:
report.save("qc_report.html", overwrite=True, open_browser=False)
Saving report to : /home/circleci/project/tutorials/preprocessing/qc_report.html
Total running time of the script: (1 minutes 14.749 seconds)