Coverage for nilearn/decomposition/canica.py: 22%

49 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-20 10:58 +0200

1"""Canonical Independent Component Analysis.""" 

2 

3import warnings as _warnings 

4from operator import itemgetter 

5 

6import numpy as np 

7from joblib import Parallel, delayed 

8from scipy.stats import scoreatpercentile 

9from sklearn.decomposition import fastica 

10from sklearn.utils import check_random_state 

11 

12from nilearn._utils import fill_doc 

13from nilearn._utils.logger import find_stack_level 

14from nilearn.decomposition._multi_pca import _MultiPCA 

15 

16 

17@fill_doc 

18class CanICA(_MultiPCA): 

19 """Perform :term:`Canonical Independent Component Analysis<CanICA>`. 

20 

21 See :footcite:t:`Varoquaux2010c` and :footcite:t:`Varoquaux2010d`. 

22 

23 Parameters 

24 ---------- 

25 mask : Niimg-like object, :obj:`~nilearn.maskers.MultiNiftiMasker` or \ 

26 :obj:`~nilearn.surface.SurfaceImage` or \ 

27 :obj:`~nilearn.maskers.SurfaceMasker` object, optional 

28 Mask to be used on data. If an instance of masker is passed, 

29 then its mask will be used. If no mask is given, for Nifti images, 

30 it will be computed automatically by a MultiNiftiMasker with default 

31 parameters; for surface images, all the vertices will be used. 

32 

33 n_components : :obj:`int`, default=20 

34 Number of components to extract. 

35 

36 %(smoothing_fwhm)s 

37 Default=6mm. 

38 

39 do_cca : :obj:`bool`, default=True 

40 Indicate if a Canonical Correlation Analysis must be run after the 

41 PCA. 

42 

43 standardize : :obj:`bool`, default=True 

44 If standardize is True, the time-series are centered and normed: 

45 their mean is put to 0 and their variance to 1 in the time dimension. 

46 

47 standardize_confounds : :obj:`bool`, default=True 

48 If standardize_confounds is True, the confounds are zscored: 

49 their mean is put to 0 and their variance to 1 in the time dimension. 

50 

51 detrend : :obj:`bool`, default=True 

52 If detrend is True, the time-series will be detrended before 

53 components extraction. 

54 

55 threshold : None, 'auto' or :obj:`float`, default='auto' 

56 If None, no thresholding is applied. If 'auto', 

57 then we apply a thresholding that will keep the n_voxels, 

58 more intense voxels across all the maps, n_voxels being the number 

59 of voxels in a brain volume. A float value indicates the 

60 ratio of voxels to keep (2. means that the maps will together 

61 have 2 x n_voxels non-zero voxels ). The float value 

62 must be bounded by [0. and n_components]. 

63 

64 n_init : :obj:`int`, default=10 

65 The number of times the fastICA algorithm is restarted 

66 

67 %(random_state)s 

68 

69 %(target_affine)s 

70 

71 .. note:: 

72 This parameter is passed to :func:`nilearn.image.resample_img`. 

73 

74 %(target_shape)s 

75 

76 .. note:: 

77 This parameter is passed to :func:`nilearn.image.resample_img`. 

78 

79 %(low_pass)s 

80 

81 .. note:: 

82 This parameter is passed to :func:`nilearn.image.resample_img`. 

83 

84 %(high_pass)s 

85 

86 .. note:: 

87 This parameter is passed to :func:`nilearn.image.resample_img`. 

88 

89 %(t_r)s 

90 

91 .. note:: 

92 This parameter is passed to :func:`nilearn.image.resample_img`. 

93 

94 %(mask_strategy)s 

95 

96 Default='epi'. 

97 

98 .. note:: 

99 These strategies are only relevant for Nifti images and the 

100 parameter is ignored for SurfaceImage objects. 

101 

102 mask_args : :obj:`dict`, optional 

103 If mask is None, these are additional parameters passed to 

104 :func:`nilearn.masking.compute_background_mask`, 

105 or :func:`nilearn.masking.compute_epi_mask` 

106 to fine-tune mask computation. 

107 Please see the related documentation for details. 

108 

109 %(memory)s 

110 

111 %(memory_level)s 

112 

113 %(n_jobs)s 

114 

115 %(verbose0)s 

116 

117 %(base_decomposition_attributes)s 

118 

119 %(multi_pca_attributes)s 

120 

121 References 

122 ---------- 

123 .. footbibliography:: 

124 

125 """ 

126 

127 def __init__( 

128 self, 

129 mask=None, 

130 n_components=20, 

131 smoothing_fwhm=6, 

132 do_cca=True, 

133 threshold="auto", 

134 n_init=10, 

135 random_state=None, 

136 standardize=True, 

137 standardize_confounds=True, 

138 detrend=True, 

139 low_pass=None, 

140 high_pass=None, 

141 t_r=None, 

142 target_affine=None, 

143 target_shape=None, 

144 mask_strategy="epi", 

145 mask_args=None, 

146 memory=None, 

147 memory_level=0, 

148 n_jobs=1, 

149 verbose=0, 

150 ): 

151 super().__init__( 

152 n_components=n_components, 

153 do_cca=do_cca, 

154 random_state=random_state, 

155 mask=mask, 

156 smoothing_fwhm=smoothing_fwhm, 

157 standardize=standardize, 

158 standardize_confounds=standardize_confounds, 

159 detrend=detrend, 

160 low_pass=low_pass, 

161 high_pass=high_pass, 

162 t_r=t_r, 

163 target_affine=target_affine, 

164 target_shape=target_shape, 

165 mask_strategy=mask_strategy, 

166 mask_args=mask_args, 

167 memory=memory, 

168 memory_level=memory_level, 

169 n_jobs=n_jobs, 

170 verbose=verbose, 

171 ) 

172 

173 self.threshold = threshold 

174 self.n_init = n_init 

175 

176 def _unmix_components(self, components): 

177 """Core function of CanICA than rotate components_ to maximize \ 

178 independence. 

179 """ 

180 random_state = check_random_state(self.random_state) 

181 

182 seeds = random_state.randint(np.iinfo(np.int32).max, size=self.n_init) 

183 # Note: fastICA is very unstable, hence we use 64bit on it 

184 results = Parallel(n_jobs=self.n_jobs, verbose=self.verbose)( 

185 delayed(self._cache(fastica, func_memory_level=2))( 

186 components.astype(np.float64), 

187 whiten="arbitrary-variance", 

188 fun="cube", 

189 random_state=seed, 

190 ) 

191 for seed in seeds 

192 ) 

193 

194 ica_maps_gen_ = (result[2].T for result in results) 

195 ica_maps_and_sparsities = ( 

196 (ica_map, np.sum(np.abs(ica_map), axis=1).max()) 

197 for ica_map in ica_maps_gen_ 

198 ) 

199 ica_maps, _ = min(ica_maps_and_sparsities, key=itemgetter(-1)) 

200 

201 # Thresholding 

202 ratio = None 

203 if isinstance(self.threshold, float): 

204 ratio = self.threshold 

205 elif self.threshold == "auto": 

206 ratio = 1.0 

207 elif self.threshold is not None: 

208 raise ValueError( 

209 "Threshold must be None, " 

210 f"'auto' or float. You provided {self.threshold}." 

211 ) 

212 if ratio is not None: 

213 abs_ica_maps = np.abs(ica_maps) 

214 percentile = 100.0 - (100.0 / len(ica_maps)) * ratio 

215 if percentile <= 0: 

216 _warnings.warn( 

217 "Nilearn's decomposition module " 

218 "obtained a critical threshold " 

219 f"(= {percentile} percentile).\n" 

220 "No threshold will be applied. " 

221 "Threshold should be decreased or " 

222 "number of components should be adjusted.", 

223 UserWarning, 

224 stacklevel=find_stack_level(), 

225 ) 

226 else: 

227 threshold = scoreatpercentile(abs_ica_maps, percentile) 

228 ica_maps[abs_ica_maps < threshold] = 0.0 

229 # We make sure that we keep the dtype of components 

230 self.components_ = ica_maps.astype(self.components_.dtype) 

231 

232 # flip signs in each component so that peak is +ve 

233 for component in self.components_: 

234 if component.max() < -component.min(): 

235 component *= -1 

236 if hasattr(self, "masker_"): 

237 self.components_img_ = self.masker_.inverse_transform( 

238 self.components_ 

239 ) 

240 

241 # Overriding _MultiPCA._raw_fit overrides _MultiPCA.fit behavior 

242 def _raw_fit(self, data): 

243 """Process unmasked data directly. 

244 

245 Useful when called by another estimator that has already 

246 unmasked data. 

247 

248 Parameters 

249 ---------- 

250 data : ndarray or memmap 

251 Unmasked data to process 

252 

253 """ 

254 if ( 

255 isinstance(self.threshold, float) 

256 and self.threshold > self.n_components 

257 ): 

258 raise ValueError( 

259 "Threshold must not be higher than number of maps. " 

260 f"Number of maps is {self.n_components} " 

261 f"and you provided threshold={self.threshold}." 

262 ) 

263 components = _MultiPCA._raw_fit(self, data) 

264 

265 self._unmix_components(components) 

266 return self