From c207a493ed62cf2abf6413b8f082890b8cab2eb5 Mon Sep 17 00:00:00 2001 From: Aniket Singh Yadav Date: Thu, 26 Feb 2026 10:01:08 +0000 Subject: [PATCH 1/3] Add hat-style variance regularization to f_oneway --- mne/stats/parametric.py | 32 ++++++++++++++++--- mne/stats/tests/test_parametric.py | 50 +++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 6 deletions(-) diff --git a/mne/stats/parametric.py b/mne/stats/parametric.py index 2cc0bff2ea1..3efdb14f39b 100644 --- a/mne/stats/parametric.py +++ b/mne/stats/parametric.py @@ -111,7 +111,7 @@ def ttest_ind_no_p(a, b, equal_var=True, sigma=0.0): return t -def f_oneway(*args): +def f_oneway(*args, sigma=0.0, method="absolute"): """Perform a 1-way ANOVA. The one-way ANOVA tests the null hypothesis that 2 or more groups have @@ -119,16 +119,27 @@ def f_oneway(*args): more groups, possibly with differing sizes :footcite:`Lowry2014`. This is a modified version of :func:`scipy.stats.f_oneway` that avoids - computing the associated p-value. + computing the associated p-value, and can adjust for implausibly small + variance values :footcite:`RidgwayEtAl2012`. Parameters ---------- *args : array_like The sample measurements should be given as arguments. + sigma : float + The within-group variance estimate (``MS_within``) will be + regularized as ``MS_within + sigma`` (``method='absolute'``) or + ``MS_within + sigma * mean(MS_within)`` (``method='relative'``). + By default this is 0 (no adjustment). See Notes for details. + method : {'absolute', 'relative'} + Controls how ``sigma`` is applied when ``sigma > 0``: + + - ``'absolute'``: adds ``sigma`` directly to ``MS_within``. + - ``'relative'``: adds ``sigma * mean(MS_within)`` to ``MS_within``. Returns ------- - F-value : float + F : float The computed F-value of the test. Notes @@ -136,8 +147,8 @@ def f_oneway(*args): The ANOVA test has important assumptions that must be satisfied in order for the associated p-value to be valid. - 1. The samples are independent - 2. Each sample is from a normally distributed population + 1. The samples are independent. + 2. Each sample is from a normally distributed population. 3. The population standard deviations of the groups are all equal. This property is known as homoscedasticity. @@ -147,10 +158,18 @@ def f_oneway(*args): The algorithm is from Heiman :footcite:`Heiman2002`, pp.394-7. + To use the "hat" adjustment method :footcite:`RidgwayEtAl2012` for + low-variance regularization, a value of ``sigma=1e-3`` may be a + reasonable choice. This mirrors the ``sigma`` parameter in + :func:`ttest_1samp_no_p`. + References ---------- .. footbibliography:: """ + _check_option("method", method, ["absolute", "relative"]) + if sigma < 0: + raise ValueError(f"sigma must be >= 0, got {sigma}") n_classes = len(args) n_samples_per_class = np.array([len(a) for a in args]) n_samples = np.sum(n_samples_per_class) @@ -168,6 +187,9 @@ def f_oneway(*args): dfwn = n_samples - n_classes msb = ssbn / float(dfbn) msw = sswn / float(dfwn) + if sigma > 0: + limit = sigma * np.mean(msw) if method == "relative" else sigma + msw = msw + limit f = msb / msw return f diff --git a/mne/stats/tests/test_parametric.py b/mne/stats/tests/test_parametric.py index 61ecbc43af3..c4dc70e570e 100644 --- a/mne/stats/tests/test_parametric.py +++ b/mne/stats/tests/test_parametric.py @@ -11,7 +11,7 @@ from numpy.testing import assert_allclose, assert_array_almost_equal, assert_array_less import mne -from mne.stats.parametric import _map_effects, f_mway_rm, f_threshold_mway_rm +from mne.stats.parametric import _map_effects, f_mway_rm, f_oneway, f_threshold_mway_rm # hardcoded external test results, manually transferred test_external = { @@ -175,3 +175,51 @@ def theirs(*a, **kw): # something to the divisor (var) assert_allclose(got, want, rtol=2e-1, atol=1e-2) assert_array_less(np.abs(got), np.abs(want)) + + +@pytest.mark.parametrize("sigma", [0.0, 1e-3]) +@pytest.mark.parametrize("method", ["absolute", "relative"]) +@pytest.mark.parametrize("seed", [0, 42, 1337]) +def test_f_oneway_hat(sigma, method, seed): + """Test f_oneway hat (low-variance) regularization.""" + rng = np.random.RandomState(seed) + X1 = rng.randn(10, 50) + X2 = rng.randn(10, 50) + + f_ours = f_oneway(X1, X2, sigma=0.0, method=method) + f_scipy = scipy.stats.f_oneway(X1, X2)[0] + assert_allclose(f_ours, f_scipy, rtol=1e-7, atol=1e-6) + + if sigma > 0: + f_reg = f_oneway(X1, X2, sigma=sigma, method=method) + f_unreg = f_oneway(X1, X2, sigma=0.0) + pos = f_unreg > 0 + assert_array_less(f_reg[pos], f_unreg[pos] + 1e-10) + + +def test_f_oneway_hat_small_variance(): + """Test that f_oneway hat stabilizes F-values for near-zero variance.""" + rng = np.random.RandomState(0) + X1 = rng.normal(0, 1e-6, (10, 100)) + X2 = rng.normal(1, 1e-6, (10, 100)) + + f_unreg = f_oneway(X1, X2, sigma=0.0) + f_abs = f_oneway(X1, X2, sigma=1e-3, method="absolute") + f_rel = f_oneway(X1, X2, sigma=1e-3, method="relative") + + assert np.median(f_unreg) > 1e6 + assert np.median(f_abs) < np.median(f_unreg) + assert np.median(f_rel) < np.median(f_unreg) + + +def test_f_oneway_hat_input_validation(): + """Test f_oneway input validation for sigma and method.""" + rng = np.random.RandomState(0) + X1 = rng.randn(5, 10) + X2 = rng.randn(5, 10) + + with pytest.raises(ValueError, match="sigma must be >= 0"): + f_oneway(X1, X2, sigma=-0.1) + + with pytest.raises(ValueError, match="method"): + f_oneway(X1, X2, sigma=1e-3, method="invalid") From 418f187a9de3ad1943b6e510be270e94e7c2bb98 Mon Sep 17 00:00:00 2001 From: Aniket Singh Yadav Date: Thu, 26 Feb 2026 10:06:38 +0000 Subject: [PATCH 2/3] Add hat-style variance regularization to f_oneway --- mne/stats/parametric.py | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/mne/stats/parametric.py b/mne/stats/parametric.py index 3efdb14f39b..38eefab94f9 100644 --- a/mne/stats/parametric.py +++ b/mne/stats/parametric.py @@ -119,27 +119,33 @@ def f_oneway(*args, sigma=0.0, method="absolute"): more groups, possibly with differing sizes :footcite:`Lowry2014`. This is a modified version of :func:`scipy.stats.f_oneway` that avoids - computing the associated p-value, and can adjust for implausibly small - variance values :footcite:`RidgwayEtAl2012`. + computing the associated p-value. Parameters ---------- *args : array_like The sample measurements should be given as arguments. sigma : float - The within-group variance estimate (``MS_within``) will be - regularized as ``MS_within + sigma`` (``method='absolute'``) or - ``MS_within + sigma * mean(MS_within)`` (``method='relative'``). - By default this is 0 (no adjustment). See Notes for details. + Regularization parameter applied to the within-group variance + (``MS_within``) to mitigate inflation of the F-statistic under + low-variance conditions. + + The adjustment is: + + - ``MS_within + sigma`` when ``method='absolute'`` + - ``MS_within * (1 + sigma)`` when ``method='relative'`` + + Defaults to 0 (no regularization). method : {'absolute', 'relative'} - Controls how ``sigma`` is applied when ``sigma > 0``: + Strategy used to regularize the within-group variance when + ``sigma > 0``: - - ``'absolute'``: adds ``sigma`` directly to ``MS_within``. - - ``'relative'``: adds ``sigma * mean(MS_within)`` to ``MS_within``. + - ``'absolute'`` adds a constant term + - ``'relative'`` scales the variance multiplicatively Returns ------- - F : float + F-value : float The computed F-value of the test. Notes @@ -147,8 +153,8 @@ def f_oneway(*args, sigma=0.0, method="absolute"): The ANOVA test has important assumptions that must be satisfied in order for the associated p-value to be valid. - 1. The samples are independent. - 2. Each sample is from a normally distributed population. + 1. The samples are independent + 2. Each sample is from a normally distributed population 3. The population standard deviations of the groups are all equal. This property is known as homoscedasticity. @@ -158,11 +164,6 @@ def f_oneway(*args, sigma=0.0, method="absolute"): The algorithm is from Heiman :footcite:`Heiman2002`, pp.394-7. - To use the "hat" adjustment method :footcite:`RidgwayEtAl2012` for - low-variance regularization, a value of ``sigma=1e-3`` may be a - reasonable choice. This mirrors the ``sigma`` parameter in - :func:`ttest_1samp_no_p`. - References ---------- .. footbibliography:: @@ -188,8 +189,10 @@ def f_oneway(*args, sigma=0.0, method="absolute"): msb = ssbn / float(dfbn) msw = sswn / float(dfwn) if sigma > 0: - limit = sigma * np.mean(msw) if method == "relative" else sigma - msw = msw + limit + if method == "absolute": + msw = msw + sigma + else: + msw = msw * (1.0 + sigma) f = msb / msw return f From a2a352fe2243237cc56ef6fce61ef8459682cb1a Mon Sep 17 00:00:00 2001 From: Aniket Singh Yadav Date: Fri, 27 Feb 2026 03:42:42 +0000 Subject: [PATCH 3/3] Add hat-style variance regularization to f_oneway --- doc/changes/dev/13698.other.rst | 1 + mne/stats/parametric.py | 10 ---------- 2 files changed, 1 insertion(+), 10 deletions(-) create mode 100644 doc/changes/dev/13698.other.rst diff --git a/doc/changes/dev/13698.other.rst b/doc/changes/dev/13698.other.rst new file mode 100644 index 00000000000..fab48e560d1 --- /dev/null +++ b/doc/changes/dev/13698.other.rst @@ -0,0 +1 @@ +Add optional low-variance ("hat") regularization to :func:`mne.stats.f_oneway` via new ``sigma`` and ``method`` parameters, by `Aniket Singh Yadav`_. \ No newline at end of file diff --git a/mne/stats/parametric.py b/mne/stats/parametric.py index 38eefab94f9..ad60bd70c6b 100644 --- a/mne/stats/parametric.py +++ b/mne/stats/parametric.py @@ -129,20 +129,10 @@ def f_oneway(*args, sigma=0.0, method="absolute"): Regularization parameter applied to the within-group variance (``MS_within``) to mitigate inflation of the F-statistic under low-variance conditions. - - The adjustment is: - - - ``MS_within + sigma`` when ``method='absolute'`` - - ``MS_within * (1 + sigma)`` when ``method='relative'`` - - Defaults to 0 (no regularization). method : {'absolute', 'relative'} Strategy used to regularize the within-group variance when ``sigma > 0``: - - ``'absolute'`` adds a constant term - - ``'relative'`` scales the variance multiplicatively - Returns ------- F-value : float