Repository: mne-tools/mne-python

Issue:
ENH/FIX: dealing with huge effect sizes in linear regression
In some cases if you encounter t-values around 17 the scipy cdf estimator yields values of 1. This leads to infs in then -log10(p value) computation. I'm wondering whether we should deal with this internally somehow.



Discussion:
perhaps you should regularize your estimates e.g. by using the "hat" option in the t-test calculation

> perhaps you should regularize your estimates e.g. by using the "hat" option in the t-test calculation

we could do that inside our regression function maybe ...
On the other hand I'm wondering what it tells you if your intercept estimate has such crazy t-values ... maybe some offset or baselining issue.

Yeah, either the means are way off (maybe baselining or offset) or the standard deviation estimate is too small to be realistic (could be fixed by "hat")


Resolving patch:
diff --git a/mne/stats/regression.py b/mne/stats/regression.py
--- a/mne/stats/regression.py
+++ b/mne/stats/regression.py
@@ -127,14 +127,28 @@ def _fit_lm(data, design_matrix, names):
     sqrt_noise_var = np.sqrt(resid_sum_squares / df).reshape(data.shape[1:])
     design_invcov = linalg.inv(np.dot(design_matrix.T, design_matrix))
     unscaled_stderrs = np.sqrt(np.diag(design_invcov))
-
+    tiny = np.finfo(np.float64).tiny
     beta, stderr, t_val, p_val, mlog10_p_val = (dict() for _ in range(5))
     for x, unscaled_stderr, predictor in zip(betas, unscaled_stderrs, names):
         beta[predictor] = x.reshape(data.shape[1:])
         stderr[predictor] = sqrt_noise_var * unscaled_stderr
-        t_val[predictor] = beta[predictor] / stderr[predictor]
-        cdf = stats.t.cdf(np.abs(t_val[predictor]), df)
-        p_val[predictor] = (1. - cdf) * 2.
+        p_val[predictor] = np.empty_like(stderr[predictor])
+        t_val[predictor] = np.empty_like(stderr[predictor])
+
+        stderr_pos = (stderr[predictor] > 0)
+        beta_pos = (beta[predictor] > 0)
+        t_val[predictor][stderr_pos] = (beta[predictor][stderr_pos] /
+                                        stderr[predictor][stderr_pos])
+        cdf = stats.t.cdf(np.abs(t_val[predictor][stderr_pos]), df)
+        p_val[predictor][stderr_pos] = np.clip((1. - cdf) * 2., tiny, 1.)
+        # degenerate cases
+        mask = (~stderr_pos & beta_pos)
+        t_val[predictor][mask] = np.inf * np.sign(beta[predictor][mask])
+        p_val[predictor][mask] = tiny
+        # could do NaN here, but hopefully this is safe enough
+        mask = (~stderr_pos & ~beta_pos)
+        t_val[predictor][mask] = 0
+        p_val[predictor][mask] = 1.
         mlog10_p_val[predictor] = -np.log10(p_val[predictor])
 
     return beta, stderr, t_val, p_val, mlog10_p_val


Test patch:
diff --git a/mne/stats/tests/test_regression.py b/mne/stats/tests/test_regression.py
--- a/mne/stats/tests/test_regression.py
+++ b/mne/stats/tests/test_regression.py
@@ -20,6 +20,8 @@
 from mne.stats.regression import linear_regression, linear_regression_raw
 from mne.io import RawArray
 
+warnings.simplefilter('always')
+
 data_path = testing.data_path(download=False)
 stc_fname = op.join(data_path, 'MEG', 'sample',
                     'sample_audvis_trunc-meg-lh.stc')
@@ -47,6 +49,7 @@ def test_regression():
     # creates contrast: aud_l=0, aud_r=1
     design_matrix[:, 1] -= 1
     with warnings.catch_warnings(record=True) as w:
+        warnings.simplefilter('always')
         lm = linear_regression(epochs, design_matrix, ['intercept', 'aud'])
         assert_true(w[0].category == UserWarning)
         assert_true('non-data' in '%s' % w[0].message)
@@ -62,8 +65,20 @@ def test_regression():
     stc_list = [stc, stc, stc]
     stc_gen = (s for s in stc_list)
     with warnings.catch_warnings(record=True):  # divide by zero
+        warnings.simplefilter('always')
         lm1 = linear_regression(stc_list, design_matrix[:len(stc_list)])
     lm2 = linear_regression(stc_gen, design_matrix[:len(stc_list)])
+    for val in lm2.values():
+        # all p values are 0 < p <= 1 to start, but get stored in float32
+        # data, so can actually be truncated to 0. Thus the mlog10_p_val
+        # actually maintains better precision for tiny p-values.
+        assert_true(np.isfinite(val.p_val.data).all())
+        assert_true((val.p_val.data <= 1).all())
+        assert_true((val.p_val.data >= 0).all())
+        # all -log10(p) are non-negative
+        assert_true(np.isfinite(val.mlog10_p_val.data).all())
+        assert_true((val.mlog10_p_val.data >= 0).all())
+        assert_true((val.mlog10_p_val.data >= 0).all())
 
     for k in lm1:
         for v1, v2 in zip(lm1[k], lm2[k]):