Coverage for nilearn/glm/tests/test_thresholding.py: 0%

141 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-16 12:32 +0200

1"""Test the thresholding utilities.""" 

2 

3import numpy as np 

4import pytest 

5from nibabel import Nifti1Image 

6from numpy.testing import assert_almost_equal, assert_equal 

7from scipy.stats import norm 

8 

9from nilearn.conftest import _shape_3d_default 

10from nilearn.glm import ( 

11 cluster_level_inference, 

12 fdr_threshold, 

13 threshold_stats_img, 

14) 

15from nilearn.glm.thresholding import _compute_hommel_value 

16from nilearn.image import get_data 

17 

18 

19def test_fdr(rng): 

20 n = 100 

21 x = np.linspace(0.5 / n, 1.0 - 0.5 / n, n) 

22 x[:10] = 0.0005 

23 x = norm.isf(x) 

24 rng.shuffle(x) 

25 

26 assert_almost_equal(fdr_threshold(x, 0.1), norm.isf(0.0005)) 

27 assert fdr_threshold(x, 0.001) == np.inf 

28 

29 # addresses #2879 

30 n = 10 

31 pvals = np.linspace(1 / n, 1, n) 

32 pvals[0] = 0.007 

33 

34 assert np.isfinite(fdr_threshold(norm.isf(pvals), 0.1)) 

35 

36 

37def test_fdr_error(rng): 

38 n = 100 

39 x = np.linspace(0.5 / n, 1.0 - 0.5 / n, n) 

40 x[:10] = 0.0005 

41 x = norm.isf(x) 

42 rng.shuffle(x) 

43 

44 match = "alpha should be between 0 and 1" 

45 with pytest.raises(ValueError, match=match): 

46 fdr_threshold(x, -0.1) 

47 with pytest.raises(ValueError, match=match): 

48 fdr_threshold(x, 1.5) 

49 

50 

51@pytest.fixture 

52def data_norm_isf(): 

53 p = np.prod(_shape_3d_default()) 

54 return norm.isf(np.linspace(1.0 / p, 1.0 - 1.0 / p, p)).reshape( 

55 _shape_3d_default() 

56 ) 

57 

58 

59def test_threshold_stats_img_no_height_control( 

60 data_norm_isf, img_3d_ones_eye, affine_eye 

61): 

62 data = data_norm_isf 

63 data[2:4, 5:7, 6:8] = 5.0 

64 stat_img = Nifti1Image(data, affine_eye) 

65 

66 # excessive cluster forming threshold 

67 th_map, _ = threshold_stats_img( 

68 stat_img, 

69 mask_img=img_3d_ones_eye, 

70 threshold=100, 

71 height_control=None, 

72 cluster_threshold=0, 

73 ) 

74 vals = get_data(th_map) 

75 

76 assert np.sum(vals > 0) == 0 

77 

78 # direct threshold 

79 th_map, _ = threshold_stats_img( 

80 stat_img, 

81 mask_img=img_3d_ones_eye, 

82 threshold=4.0, 

83 height_control=None, 

84 cluster_threshold=0, 

85 ) 

86 vals = get_data(th_map) 

87 

88 assert np.sum(vals > 0) == 8 

89 

90 # without mask 

91 th_map, _ = threshold_stats_img( 

92 stat_img, None, threshold=4.0, height_control=None, cluster_threshold=0 

93 ) 

94 vals = get_data(th_map) 

95 

96 assert np.sum(vals > 0) == 8 

97 

98 # without a map 

99 th_map, threshold = threshold_stats_img( 

100 None, None, threshold=3.0, height_control=None, cluster_threshold=0 

101 ) 

102 

103 assert threshold == 3.0 

104 assert th_map is None 

105 

106 

107def test_threshold_stats_img(data_norm_isf, img_3d_ones_eye, affine_eye): 

108 data = data_norm_isf 

109 data[2:4, 5:7, 6:8] = 5.0 

110 stat_img = Nifti1Image(data, affine_eye) 

111 

112 th_map, _ = threshold_stats_img( 

113 stat_img, 

114 mask_img=img_3d_ones_eye, 

115 alpha=0.001, 

116 height_control="fpr", 

117 cluster_threshold=0, 

118 ) 

119 vals = get_data(th_map) 

120 

121 assert np.sum(vals > 0) == 8 

122 

123 # excessive size threshold 

124 th_map, z_th = threshold_stats_img( 

125 stat_img, 

126 mask_img=img_3d_ones_eye, 

127 alpha=0.001, 

128 height_control="fpr", 

129 cluster_threshold=10, 

130 ) 

131 vals = get_data(th_map) 

132 

133 assert np.sum(vals > 0) == 0 

134 assert z_th == norm.isf(0.0005) 

135 

136 # dr threshold + bonferroni 

137 for control in ["fdr", "bonferroni"]: 

138 th_map, _ = threshold_stats_img( 

139 stat_img, 

140 mask_img=img_3d_ones_eye, 

141 alpha=0.05, 

142 height_control=control, 

143 cluster_threshold=5, 

144 ) 

145 vals = get_data(th_map) 

146 

147 assert np.sum(vals > 0) == 8 

148 

149 # without a map or mask 

150 th_map, threshold = threshold_stats_img( 

151 None, None, alpha=0.05, height_control="fpr", cluster_threshold=0 

152 ) 

153 

154 assert threshold > 1.64 

155 assert th_map is None 

156 

157 

158def test_threshold_stats_img_errors(): 

159 with pytest.raises(ValueError, match="'stat_img' cannot be None"): 

160 threshold_stats_img(None, None, alpha=0.05, height_control="fdr") 

161 

162 with pytest.raises(ValueError, match="'stat_img' cannot be None"): 

163 threshold_stats_img( 

164 None, None, alpha=0.05, height_control="bonferroni" 

165 ) 

166 

167 with pytest.raises(ValueError, match="'height_control' should be one of"): 

168 threshold_stats_img(None, None, alpha=0.05, height_control="plop") 

169 

170 

171@pytest.mark.parametrize( 

172 "alpha, expected", 

173 [ 

174 (1.0e-9, 7), 

175 (1.0e-7, 6), 

176 (0.059, 6), 

177 (0.061, 5), 

178 (0.249, 5), 

179 (0.251, 4), 

180 (0.399, 4), 

181 (0.401, 3), 

182 (0.899, 3), 

183 (0.901, 0), 

184 ], 

185) 

186def test_hommel(alpha, expected): 

187 """Check that the computation of Hommel value. 

188 

189 For these, we take the example in Meijer et al. 2017 

190 'A shortcut for Hommel's procedure in linearithmic time' 

191 and check that we obtain the same values. 

192 https://arxiv.org/abs/1710.08273 

193 """ 

194 z = norm.isf([1.0e-8, 0.01, 0.08, 0.1, 0.5, 0.7, 0.9]) 

195 

196 assert _compute_hommel_value(z, alpha=alpha) == expected 

197 

198 

199@pytest.mark.timeout(0) 

200def test_all_resolution_inference(data_norm_isf, affine_eye): 

201 data = data_norm_isf 

202 data[2:4, 5:7, 6:8] = 5.0 

203 stat_img = Nifti1Image(data, affine_eye) 

204 

205 # standard case 

206 th_map = cluster_level_inference(stat_img, threshold=3, alpha=0.05) 

207 vals = get_data(th_map) 

208 

209 assert np.sum(vals > 0) == 8 

210 

211 # high threshold 

212 th_map = cluster_level_inference(stat_img, threshold=6, alpha=0.05) 

213 vals = get_data(th_map) 

214 

215 assert np.sum(vals > 0) == 0 

216 

217 # list of thresholds 

218 th_map = cluster_level_inference(stat_img, threshold=[3, 6], alpha=0.05) 

219 vals = get_data(th_map) 

220 

221 assert np.sum(vals > 0) == 8 

222 

223 # verbose mode 

224 th_map = cluster_level_inference( 

225 stat_img, threshold=3, alpha=0.05, verbose=1 

226 ) 

227 

228 

229def test_all_resolution_inference_with_mask( 

230 img_3d_ones_eye, affine_eye, data_norm_isf 

231): 

232 data = data_norm_isf 

233 data[2:4, 5:7, 6:8] = 5.0 

234 stat_img = Nifti1Image(data, affine_eye) 

235 

236 th_map = cluster_level_inference( 

237 stat_img, mask_img=img_3d_ones_eye, threshold=3, alpha=0.05 

238 ) 

239 vals = get_data(th_map) 

240 

241 assert np.sum(vals > 0) == 8 

242 

243 

244def test_all_resolution_inference_one_voxel(data_norm_isf, affine_eye): 

245 data = data_norm_isf 

246 data[3, 6, 7] = 10 

247 stat_img = Nifti1Image(data, affine_eye) 

248 

249 th_map = cluster_level_inference(stat_img, threshold=7, alpha=0.05) 

250 vals = get_data(th_map) 

251 

252 assert np.sum(vals > 0) == 1 

253 

254 

255def test_all_resolution_inference_one_sided( 

256 data_norm_isf, img_3d_ones_eye, affine_eye 

257): 

258 data = data_norm_isf 

259 data[2:4, 5:7, 6:8] = 5.0 

260 stat_img = Nifti1Image(data, affine_eye) 

261 

262 _, z_th = threshold_stats_img( 

263 stat_img, 

264 mask_img=img_3d_ones_eye, 

265 alpha=0.001, 

266 height_control="fpr", 

267 cluster_threshold=10, 

268 two_sided=False, 

269 ) 

270 assert_equal(z_th, norm.isf(0.001)) 

271 

272 

273@pytest.mark.parametrize("alpha", [-1, 2]) 

274def test_all_resolution_inference_errors(alpha, data_norm_isf, affine_eye): 

275 # test aberrant alpha 

276 data = data_norm_isf 

277 stat_img = Nifti1Image(data, affine_eye) 

278 

279 with pytest.raises(ValueError, match="alpha should be between 0 and 1"): 

280 cluster_level_inference(stat_img, threshold=3, alpha=alpha) 

281 

282 

283@pytest.mark.parametrize("control", ["fdr", "bonferroni"]) 

284def test_all_resolution_inference_height_control( 

285 control, affine_eye, img_3d_ones_eye, data_norm_isf 

286): 

287 # two-side fdr threshold + bonferroni 

288 data = data_norm_isf 

289 data[2:4, 5:7, 6:8] = 5.0 

290 data[0:2, 0:2, 6:8] = -5.0 

291 stat_img = Nifti1Image(data, affine_eye) 

292 

293 th_map, _ = threshold_stats_img( 

294 stat_img, 

295 mask_img=img_3d_ones_eye, 

296 alpha=0.05, 

297 height_control=control, 

298 cluster_threshold=5, 

299 ) 

300 vals = get_data(th_map) 

301 assert_equal(np.sum(vals > 0), 8) 

302 assert_equal(np.sum(vals < 0), 8) 

303 th_map, _ = threshold_stats_img( 

304 stat_img, 

305 mask_img=img_3d_ones_eye, 

306 alpha=0.05, 

307 height_control=control, 

308 cluster_threshold=5, 

309 two_sided=False, 

310 ) 

311 vals = get_data(th_map) 

312 

313 assert_equal(np.sum(vals > 0), 8) 

314 assert_equal(np.sum(vals < 0), 0) 

315 

316 

317@pytest.mark.parametrize("height_control", [None, "bonferroni", "fdr", "fpr"]) 

318def test_threshold_stats_img_surface(surf_img_1d, height_control): 

319 """Smoke test threshold_stats_img works on surface.""" 

320 threshold_stats_img(surf_img_1d, height_control=height_control) 

321 

322 

323def test_threshold_stats_img_surface_with_mask(surf_img_1d, surf_mask_1d): 

324 """Smoke test threshold_stats_img works on surface with a mask.""" 

325 threshold_stats_img( 

326 surf_img_1d, height_control="bonferroni", mask_img=surf_mask_1d 

327 )