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
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
1"""Canonical Independent Component Analysis."""
3import warnings as _warnings
4from operator import itemgetter
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
12from nilearn._utils import fill_doc
13from nilearn._utils.logger import find_stack_level
14from nilearn.decomposition._multi_pca import _MultiPCA
17@fill_doc
18class CanICA(_MultiPCA):
19 """Perform :term:`Canonical Independent Component Analysis<CanICA>`.
21 See :footcite:t:`Varoquaux2010c` and :footcite:t:`Varoquaux2010d`.
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.
33 n_components : :obj:`int`, default=20
34 Number of components to extract.
36 %(smoothing_fwhm)s
37 Default=6mm.
39 do_cca : :obj:`bool`, default=True
40 Indicate if a Canonical Correlation Analysis must be run after the
41 PCA.
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.
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.
51 detrend : :obj:`bool`, default=True
52 If detrend is True, the time-series will be detrended before
53 components extraction.
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].
64 n_init : :obj:`int`, default=10
65 The number of times the fastICA algorithm is restarted
67 %(random_state)s
69 %(target_affine)s
71 .. note::
72 This parameter is passed to :func:`nilearn.image.resample_img`.
74 %(target_shape)s
76 .. note::
77 This parameter is passed to :func:`nilearn.image.resample_img`.
79 %(low_pass)s
81 .. note::
82 This parameter is passed to :func:`nilearn.image.resample_img`.
84 %(high_pass)s
86 .. note::
87 This parameter is passed to :func:`nilearn.image.resample_img`.
89 %(t_r)s
91 .. note::
92 This parameter is passed to :func:`nilearn.image.resample_img`.
94 %(mask_strategy)s
96 Default='epi'.
98 .. note::
99 These strategies are only relevant for Nifti images and the
100 parameter is ignored for SurfaceImage objects.
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.
109 %(memory)s
111 %(memory_level)s
113 %(n_jobs)s
115 %(verbose0)s
117 %(base_decomposition_attributes)s
119 %(multi_pca_attributes)s
121 References
122 ----------
123 .. footbibliography::
125 """
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 )
173 self.threshold = threshold
174 self.n_init = n_init
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)
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 )
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))
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)
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 )
241 # Overriding _MultiPCA._raw_fit overrides _MultiPCA.fit behavior
242 def _raw_fit(self, data):
243 """Process unmasked data directly.
245 Useful when called by another estimator that has already
246 unmasked data.
248 Parameters
249 ----------
250 data : ndarray or memmap
251 Unmasked data to process
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)
265 self._unmix_components(components)
266 return self