diff --git a/doc/changes/dev/14205.newfeature.rst b/doc/changes/dev/14205.newfeature.rst new file mode 100644 index 00000000000..74f78033c36 --- /dev/null +++ b/doc/changes/dev/14205.newfeature.rst @@ -0,0 +1 @@ +Added ``meanlogratio`` baselining mode to :meth:`mne.time_frequency.EpochsTFR.apply_baseline` and related functions, by :newcontrib:`Virginie van Wassenhove`. diff --git a/doc/changes/names.inc b/doc/changes/names.inc index 8c5ed912edd..8a10bc6d8bb 100644 --- a/doc/changes/names.inc +++ b/doc/changes/names.inc @@ -476,6 +476,7 @@ .. _Victor Férat: https://github.com/vferat .. _Victoria Peterson: https://github.com/vpeterson .. _Vincent Gao: https://github.com/gaoflow +.. _Virginie van Wassenhove: https://brainthemind.com/virginie-van-wassenhove .. _Wei Xu: https://github.com/psyxw .. _Will Turner: https://bootstrapbill.github.io .. _Wouter Kroot: https://github.com/WouterKroot diff --git a/doc/references.bib b/doc/references.bib index 668e204543d..4a9a0b89f02 100644 --- a/doc/references.bib +++ b/doc/references.bib @@ -1025,6 +1025,15 @@ @unpublished{KingEtAl2018 note = {hal-01848442} } +@article{KinleyEtAl2026, + author={Kinley, Isaac and Roberts, Reece P and Meltzer, Jed A and Addis, Donna Rose}, + title={Spectral change or Jensen gap? Log-ratio baseline correction for time-frequency M/EEG is negatively biased}, + journal={Journal of Neuroscience Methods}, + pages={110826}, + year={2026}, + publisher={Elsevier} +} + @article{KnuutilaEtAl1993, author = {Knuutila, Jukka E. T. and Ahonen, Antti I. and Hämäläinen, Matti S. and Kajola, Matti J. and Laine, P. P. and Lounasmaa, Olli V. and Parkkonen, Lauri T. and Simola, Juha T. A. and Tesche, Claudia D.}, doi = {10.1109/20.281163}, diff --git a/mne/baseline.py b/mne/baseline.py index 4e73ed0ce95..ec22bafe923 100644 --- a/mne/baseline.py +++ b/mne/baseline.py @@ -15,7 +15,15 @@ def _log_rescale(baseline, mode="mean"): _check_option( "mode", mode, - ["logratio", "ratio", "zscore", "mean", "percent", "zlogratio"], + [ + "logratio", + "ratio", + "zscore", + "mean", + "percent", + "zlogratio", + "meanlogratio", + ], ) msg = f"Applying baseline correction (mode: {mode})" else: @@ -35,21 +43,7 @@ def rescale(data, times, baseline, mode="mean", copy=True, picks=None, verbose=N times : 1D array Time instants is seconds. %(baseline_rescale)s - mode : 'mean' | 'ratio' | 'logratio' | 'percent' | 'zscore' | 'zlogratio' - Perform baseline correction by - - - subtracting the mean of baseline values ('mean') - - dividing by the mean of baseline values ('ratio') - - dividing by the mean of baseline values and taking the log - ('logratio') - - subtracting the mean of baseline values followed by dividing by - the mean of baseline values ('percent') - - subtracting the mean of baseline values and dividing by the - standard deviation of baseline values ('zscore') - - dividing by the mean of baseline values, taking the log, and - dividing by the standard deviation of log baseline values - ('zlogratio') - + %(baseline_mode)s copy : bool Whether to return a new instance or modify in place. picks : list of int | None @@ -60,6 +54,10 @@ def rescale(data, times, baseline, mode="mean", copy=True, picks=None, verbose=N ------- data_scaled: array Array of same shape as data after rescaling. + + References + ---------- + .. footbibliography:: """ if copy: data = data.copy() @@ -114,6 +112,13 @@ def fun(d, m): d /= m np.log10(d, out=d) + elif mode == "meanlogratio": + + def fun(d, m): + d /= m + np.log10(d, out=d) + d -= np.mean(d[..., imin:imax], axis=-1, keepdims=True) + elif mode == "percent": def fun(d, m): diff --git a/mne/minimum_norm/time_frequency.py b/mne/minimum_norm/time_frequency.py index 00cc4d6fa79..6cf26f6a71e 100644 --- a/mne/minimum_norm/time_frequency.py +++ b/mne/minimum_norm/time_frequency.py @@ -220,21 +220,7 @@ def source_band_induced_power( If a is None the beginning of the data is used and if b is None then b is set to the end of the interval. If baseline is equal to (None, None) all the time interval is used. - baseline_mode : 'mean' | 'ratio' | 'logratio' | 'percent' | 'zscore' | 'zlogratio' - Perform baseline correction by - - - subtracting the mean of baseline values ('mean') - - dividing by the mean of baseline values ('ratio') - - dividing by the mean of baseline values and taking the log - ('logratio') - - subtracting the mean of baseline values followed by dividing by - the mean of baseline values ('percent') - - subtracting the mean of baseline values and dividing by the - standard deviation of baseline values ('zscore') - - dividing by the mean of baseline values, taking the log, and - dividing by the standard deviation of log baseline values - ('zlogratio') - + %(baseline_mode_mn)s pca : bool If True, the true dimension of data is estimated before running the time-frequency transforms. It reduces the computation times @@ -258,6 +244,10 @@ def source_band_induced_power( (n_vertices, n_frequencies, n_samples) if label=None or label=label. For lists of one or more labels, the induced power estimate has shape (n_labels, n_frequencies, n_samples). + + References + ---------- + .. footbibliography:: """ # noqa: E501 _check_option("method", method, INVERSE_METHODS) @@ -648,21 +638,7 @@ def source_induced_power( and if b is None then b is set to the end of the interval. If baseline is equal to (None, None) all the time interval is used. - baseline_mode : 'mean' | 'ratio' | 'logratio' | 'percent' | 'zscore' | 'zlogratio' - Perform baseline correction by - - - subtracting the mean of baseline values ('mean') - - dividing by the mean of baseline values ('ratio') - - dividing by the mean of baseline values and taking the log - ('logratio') - - subtracting the mean of baseline values followed by dividing by - the mean of baseline values ('percent') - - subtracting the mean of baseline values and dividing by the - standard deviation of baseline values ('zscore') - - dividing by the mean of baseline values, taking the log, and - dividing by the standard deviation of log baseline values - ('zlogratio') - + %(baseline_mode_mn)s pca : bool If True, the true dimension of data is estimated before running the time-frequency transforms. It reduces the computation times @@ -692,6 +668,10 @@ def source_induced_power( plv : array The phase-locking value array with shape (n_sources, n_freqs, n_samples). Only returned if ``return_plv=True``. + + References + ---------- + .. footbibliography:: """ # noqa: E501 _check_option("method", method, INVERSE_METHODS) _check_ori(pick_ori, inverse_operator["source_ori"], inverse_operator["src"]) diff --git a/mne/time_frequency/tests/test_tfr.py b/mne/time_frequency/tests/test_tfr.py index 02e256313eb..217a8403e35 100644 --- a/mne/time_frequency/tests/test_tfr.py +++ b/mne/time_frequency/tests/test_tfr.py @@ -1723,7 +1723,8 @@ def test_tfr_copy(average_tfr): @pytest.mark.parametrize( - "mode", ("mean", "ratio", "logratio", "percent", "zscore", "zlogratio") + "mode", + ("mean", "ratio", "logratio", "meanlogratio", "percent", "zscore", "zlogratio"), ) def test_tfr_apply_baseline(average_tfr, mode): """Test TFR baselining.""" diff --git a/mne/time_frequency/tfr.py b/mne/time_frequency/tfr.py index 65793b694f4..98e7940c256 100644 --- a/mne/time_frequency/tfr.py +++ b/mne/time_frequency/tfr.py @@ -1793,26 +1793,17 @@ def apply_baseline(self, baseline, mode="mean", verbose=None): %(baseline_rescale)s How baseline is computed is determined by the ``mode`` parameter. - mode : 'mean' | 'ratio' | 'logratio' | 'percent' | 'zscore' | 'zlogratio' - Perform baseline correction by - - - subtracting the mean of baseline values ('mean') - - dividing by the mean of baseline values ('ratio') - - dividing by the mean of baseline values and taking the log - ('logratio') - - subtracting the mean of baseline values followed by dividing by - the mean of baseline values ('percent') - - subtracting the mean of baseline values and dividing by the - standard deviation of baseline values ('zscore') - - dividing by the mean of baseline values, taking the log, and - dividing by the standard deviation of log baseline values - ('zlogratio') + %(baseline_mode)s %(verbose)s Returns ------- %(inst_tfr)s The modified instance. + + References + ---------- + .. footbibliography:: """ self._baseline = _check_baseline(baseline, times=self.times, sfreq=self.sfreq) rescale(self.data, self.times, self.baseline, mode, copy=False, verbose=verbose) @@ -1950,7 +1941,7 @@ def plot( %(baseline_rescale)s How baseline is computed is determined by the ``mode`` parameter. - %(mode_tfr_plot)s + %(baseline_mode)s %(dB_tfr_plot)s %(combine_tfr_plot)s @@ -2216,7 +2207,7 @@ def plot_joint( %(baseline_rescale)s How baseline is computed is determined by the ``mode`` parameter. - %(mode_tfr_plot)s + %(baseline_mode)s %(dB_tfr_plot)s %(yscale_tfr_plot)s %(vlim_tfr_plot_joint)s @@ -2514,7 +2505,7 @@ def plot_topo( %(baseline_rescale)s How baseline is computed is determined by the ``mode`` parameter. - %(mode_tfr_plot)s + %(baseline_mode)s %(tmin_tmax_psd)s %(fmin_fmax_tfr)s %(vmin_vmax_tfr_plot_topo)s diff --git a/mne/utils/docs.py b/mne/utils/docs.py index 851952400b7..92182d13885 100644 --- a/mne/utils/docs.py +++ b/mne/utils/docs.py @@ -511,6 +511,37 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): 2. Subtract this mean from the **entire** ``Evoked``. """ +_baseline_mode_desc = """\ + Perform baseline correction by: + + ``"mean"`` + Subtracting the mean of baseline values + ``"ratio"`` + Dividing by the mean of baseline values + ``"logratio"`` + Dividing by the mean of baseline values and taking the log + ``"meanlogratio"`` + Dividing by the mean of baseline values, taking the log and then + subtracting the mean (:footcite:`KinleyEtAl2026`) + + .. note:: this baseline mode has not been tested at the source-level! + ``"percent"`` + Subtracting the mean of baseline values followed by dividing by + the mean of baseline values + ``"zscore"`` + Subtracting the mean of baseline values and dividing by the + standard deviation of baseline values + ``"zlogratio"`` + Dividing by the mean of baseline values, taking the log, and + dividing by the standard deviation of log baseline values +""" + +docdict["baseline_mode"] = f"""\ +mode : 'mean' | 'ratio' | 'logratio' | 'meanlogratio' | 'percent' | 'zscore' | 'zlogratio' +{_baseline_mode_desc}""" # noqa: E501 +docdict["baseline_mode_mn"] = f"""\ +baseline_mode : 'mean' | 'ratio' | 'logratio' | 'meanlogratio' | 'percent' | 'zscore' | 'zlogratio' +{_baseline_mode_desc}""" # noqa: E501 docdict["baseline_report"] = f"""{_baseline_rescale_base} Correction is applied in the following way **to each channel:** @@ -2825,23 +2856,6 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75): * 'sum' : Sum of PSFs/CTFs across vertices. """ -docdict["mode_tfr_plot"] = """ -mode : 'mean' | 'ratio' | 'logratio' | 'percent' | 'zscore' | 'zlogratio' - Perform baseline correction by - - - subtracting the mean of baseline values ('mean') (default) - - dividing by the mean of baseline values ('ratio') - - dividing by the mean of baseline values and taking the log - ('logratio') - - subtracting the mean of baseline values followed by dividing by - the mean of baseline values ('percent') - - subtracting the mean of baseline values and dividing by the - standard deviation of baseline values ('zscore') - - dividing by the mean of baseline values, taking the log, and - dividing by the standard deviation of log baseline values - ('zlogratio') -""" - docdict["montage"] = """ montage : None | str | DigMontage A montage containing channel positions. If a string or diff --git a/mne/viz/topomap.py b/mne/viz/topomap.py index 75ca4371cb6..e4db37d7231 100644 --- a/mne/viz/topomap.py +++ b/mne/viz/topomap.py @@ -2065,20 +2065,7 @@ def plot_tfr_topomap( "b (s)". If a is None the beginning of the data is used and if b is None then b is set to the end of the interval. If baseline is equal to (None, None) the whole time interval is used. - mode : 'mean' | 'ratio' | 'logratio' | 'percent' | 'zscore' | 'zlogratio' | None - Perform baseline correction by - - - subtracting the mean baseline power ('mean') - - dividing by the mean baseline power ('ratio') - - dividing by the mean baseline power and taking the log ('logratio') - - subtracting the mean baseline power followed by dividing by the - mean baseline power ('percent') - - subtracting the mean baseline power and dividing by the standard - deviation of the baseline power ('zscore') - - dividing by the mean baseline power, taking the log, and dividing - by the standard deviation of the baseline power ('zlogratio') - - If None no baseline correction is applied. + %(baseline_mode)s %(sensors_topomap)s %(show_names_topomap)s %(mask_evoked_topomap)s @@ -2119,6 +2106,10 @@ def plot_tfr_topomap( ------- fig : matplotlib.figure.Figure The figure containing the topography. + + References + ---------- + .. footbibliography:: """ # noqa: E501 import matplotlib.pyplot as plt diff --git a/tutorials/time-freq/20_sensors_time_frequency.py b/tutorials/time-freq/20_sensors_time_frequency.py index 9175e700041..f64b92d7dbd 100644 --- a/tutorials/time-freq/20_sensors_time_frequency.py +++ b/tutorials/time-freq/20_sensors_time_frequency.py @@ -243,8 +243,81 @@ # # # power.apply_baseline(baseline=(-0.5, 0), mode='logratio') # +# +# Baseline correction considerations +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# Applying a baseline correction to time-frequency results can help +# express post-stimulus oscillatory activity relative to a reference period. +# It facilitates interpreting brain activity evoked or induced by stimuli, and +# reduce the influence of baseline power (e.g. the 1/f temporal structure). +# However, baseline correction is not a noise removal procedure and its +# interpretation depends on assumptions about how baseline (~resting-state) +# activity combines with post-stimulus activity (e.g. additively or multiplicatively). +# Thus, your choice of baseline should be theory- and experiment-driven. +# The baseline should be free of systematic anticipatory, preparatory, or +# sequential effects if it is intended to represent a neutral reference. +# Temporal leakage from the time–frequency transform must also be considered: +# activity after stimulus onset can contaminate estimates immediately +# preceding the stimulus, so the baseline should end sufficiently early. +# +# MNE-Python provides multiple baseline modes +# +# ``“mean”`` +# Subtracts the mean baseline power. It is simple and remains in +# the original power units, making it appropriate when an additive change +# from baseline is of interest. However, values remain frequency- and +# sensor-dependent, so magnitudes are not directly comparable across +# frequencies or sensor types. +# +# ``“ratio”`` +# Expresses power relative to baseline. It is intuitive because values +# indicate relative power (e.g., 1.2 means 20% above baseline), but the resulting +# distribution is typically positively skewed, which can be problematic for +# parametric statistical analyses (Grandchamp & Delorme 2011). +# +# ``“logratio”`` +# Expresses relative power on a log scale (dB). The log transformation +# reduces skewness and make the increase or decrease more symmetric, while providing +# a conveneient scale for comparing relative changes. However, +# Kinley et al. (2006; :footcite:`KinleyEtAl2026`) +# show that this conventional approach introduces a negative bias, which can make +# unchanged power appear to decrease and understimate genuine increases. Note that +# single-trial correction (on ``EpochsTFR``) is more affected by the logratio bias +# than correction of trial-averaged data (``AverageTFR``), because the bias scales +# with the variance of the quantity being corrected. +# +# ``"meanlogratio”`` +# Provides a logarithmic (dB) interpretation but avoids its negative bias by averaging +# the log-transformed baseline rather than first averaging baseline power. It retains +# the reduced skewness of the logarithmic transformation and ensures that the baseline +# is zero on average. It is therefore preferable to logratio when a multiplicative +# model and a logarithmic representation are desired (Kinley et al. 2026). +# +# ``"percent”`` +# Expresses the percentage change from baseline, making the magnitude +# of relative changes readily interpretable. Like ratio, it is asymmetric and +# typically positively skewed; this can be particularly problematic when normalization +# is performed at the single-trial level. +# +# ``"zscore”`` +# Expresses changes relative to the variability of the baseline, in +# units of baseline standard deviations. This can facilitate comparison across +# frequencies or sensors with different baseline variability, but the resulting +# values depend on the baseline variance and therefore do not directly represent +# absolute or proportional power changes. +# +# ``"zlogratio”`` +# Combines the logarithmic relative-power transformation with +# normalization by baseline variability. It is useful when the question concerns +# how large a relative power change is with respect to baseline variability, but +# shares the interpretational dependence on baseline variance of zscore. +# # Exercise # -------- # # - Visualize the inter-trial coherence values as topomaps as done with # power. +# +# References +# ---------- +# .. footbibliography::