Coverage for nilearn/decoding/decoder.py: 13%

400 statements  

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

1"""High-level decoding object. 

2 

3Exposes standard classification and 

4regression strategies such as SVM, LogisticRegression and Ridge, 

5with optional feature selection, 

6integrated hyper-parameter selection and aggregation 

7strategy in which the best models within a cross validation loop are averaged. 

8 

9Also exposes a high-level method FREM that uses clustering and model 

10ensembling to achieve state of the art performance 

11""" 

12 

13import itertools 

14import warnings 

15from collections.abc import Iterable 

16 

17import numpy as np 

18from joblib import Parallel, delayed 

19from sklearn import clone 

20from sklearn.base import ( 

21 BaseEstimator, 

22 ClassifierMixin, 

23 MultiOutputMixin, 

24 RegressorMixin, 

25) 

26from sklearn.dummy import DummyClassifier, DummyRegressor 

27from sklearn.linear_model import ( 

28 LassoCV, 

29 LogisticRegressionCV, 

30 RidgeClassifierCV, 

31 RidgeCV, 

32) 

33from sklearn.metrics import check_scoring, get_scorer 

34from sklearn.model_selection import ( 

35 LeaveOneGroupOut, 

36 ParameterGrid, 

37 ShuffleSplit, 

38 StratifiedShuffleSplit, 

39 check_cv, 

40) 

41from sklearn.preprocessing import LabelBinarizer 

42from sklearn.svm import SVR, LinearSVC, l1_min_c 

43from sklearn.utils.extmath import safe_sparse_dot 

44from sklearn.utils.validation import check_is_fitted, check_X_y 

45 

46from nilearn._utils import CacheMixin, fill_doc 

47from nilearn._utils.cache_mixin import check_memory 

48from nilearn._utils.logger import find_stack_level 

49from nilearn._utils.masker_validation import ( 

50 check_compatibility_mask_and_images, 

51 check_embedded_masker, 

52) 

53from nilearn._utils.param_validation import ( 

54 check_feature_screening, 

55 check_params, 

56) 

57from nilearn._utils.tags import SKLEARN_LT_1_6 

58from nilearn.maskers import SurfaceMasker 

59from nilearn.regions.rena_clustering import ReNA 

60from nilearn.surface import SurfaceImage 

61 

62SUPPORTED_ESTIMATORS = { 

63 "svc_l1": LinearSVC(penalty="l1", dual=False, max_iter=10000), 

64 "svc_l2": LinearSVC(penalty="l2", dual=True, max_iter=10000), 

65 "svc": LinearSVC(penalty="l2", dual=True, max_iter=10000), 

66 "logistic_l1": LogisticRegressionCV(penalty="l1", solver="liblinear"), 

67 "logistic_l2": LogisticRegressionCV(penalty="l2", solver="liblinear"), 

68 "logistic": LogisticRegressionCV(penalty="l2", solver="liblinear"), 

69 "ridge_classifier": RidgeClassifierCV(), 

70 "ridge_regressor": RidgeCV(), 

71 "ridge": RidgeCV(), 

72 "lasso": LassoCV(), 

73 "lasso_regressor": LassoCV(), 

74 "svr": SVR(kernel="linear", max_iter=10000), 

75 "dummy_classifier": DummyClassifier(strategy="stratified", random_state=0), 

76 "dummy_regressor": DummyRegressor(strategy="mean"), 

77} 

78 

79 

80@fill_doc 

81def _check_param_grid(estimator, X, y, param_grid=None): 

82 """Check param_grid and return sensible default if param_grid is None. 

83 

84 Parameters 

85 ---------- 

86 estimator : str 

87 The estimator to choose among: 

88 %(classifier_options)s 

89 %(regressor_options)s 

90 

91 X : list of Niimg-like objects 

92 See :ref:`extracting_data`. 

93 Data on which model is to be fitted. If this is a list, 

94 the affine is considered the same for all. 

95 

96 y : array or list of shape (n_samples) 

97 The dependent variable (age, sex, IQ, yes/no, etc.). 

98 Target variable to predict. Must have exactly as many elements as 

99 3D images in niimg. 

100 

101 param_grid : dict of str to sequence, or sequence of such. Default None 

102 The parameter grid to explore, as a dictionary mapping estimator 

103 parameters to sequences of allowed values. 

104 

105 An empty dict signifies default parameters. 

106 

107 A sequence of dicts signifies a sequence of grids to search, and is 

108 useful to avoid exploring parameter combinations that make no sense 

109 or have no effect. See scikit-learn documentation for more information. 

110 

111 For Dummy estimators, parameter grid defaults to empty as these 

112 estimators do not have hyperparameters to grid search. 

113 

114 Returns 

115 ------- 

116 param_grid : dict of str to sequence, or sequence of such. Sensible default 

117 dict has size 1 for linear models. 

118 

119 """ 

120 if param_grid is None: 

121 param_grid = _default_param_grid(estimator, X, y) 

122 

123 elif isinstance(estimator, LogisticRegressionCV): 

124 param_grid = _replace_param_grid_key(param_grid, "C", "Cs") 

125 param_grid = _wrap_param_grid(param_grid, "Cs") 

126 

127 elif isinstance(estimator, (RidgeCV, RidgeClassifierCV, LassoCV)): 

128 param_grid = _wrap_param_grid(param_grid, "alphas") 

129 

130 return param_grid 

131 

132 

133def _default_param_grid(estimator, X, y): 

134 """Generate sensible default for param_grid. 

135 

136 Parameters 

137 ---------- 

138 estimator : str 

139 The estimator to choose among: 

140 %(classifier_options)s 

141 %(regressor_options)s 

142 

143 X : list of Niimg-like objects 

144 See :ref:`extracting_data`. 

145 Data on which model is to be fitted. If this is a list, 

146 the affine is considered the same for all. 

147 

148 y : array or list of shape (n_samples) 

149 The dependent variable (age, sex, IQ, yes/no, etc.). 

150 Target variable to predict. Must have exactly as many elements as 

151 3D images in niimg. 

152 

153 Returns 

154 ------- 

155 param_grid : dict of str to sequence, or sequence of such. Sensible default 

156 dict has size 1 for linear models. 

157 """ 

158 param_grid = {} 

159 

160 # validate estimator 

161 if isinstance(estimator, (DummyClassifier, DummyRegressor)): 

162 if estimator.strategy == "constant": 

163 message = ( 

164 "Dummy classification implemented only for strategies" 

165 ' "most_frequent", "prior", "stratified"' 

166 ) 

167 raise NotImplementedError(message) 

168 elif not isinstance( 

169 estimator, 

170 ( 

171 LogisticRegressionCV, 

172 LinearSVC, 

173 RidgeCV, 

174 RidgeClassifierCV, 

175 SVR, 

176 LassoCV, 

177 ), 

178 ): 

179 raise ValueError( 

180 "Invalid estimator. The supported estimators are:" 

181 f" {list(SUPPORTED_ESTIMATORS.keys())}" 

182 ) 

183 

184 # use l1_min_c to get lower bound for estimators with L1 penalty 

185 if hasattr(estimator, "penalty") and (estimator.penalty == "l1"): 

186 # define loss function 

187 if isinstance(estimator, LogisticRegressionCV): 

188 loss = "log" 

189 elif isinstance(estimator, LinearSVC): 

190 loss = "squared_hinge" 

191 

192 min_c = l1_min_c(X, y, loss=loss) 

193 

194 # otherwise use 0.5 which will give param_grid["C"] = [1, 10, 100] 

195 else: 

196 min_c = 0.5 

197 

198 # define sensible default for different types of estimators 

199 if isinstance(estimator, (RidgeCV, RidgeClassifierCV)): 

200 param_grid["alphas"] = [np.geomspace(1e-3, 1e4, 8)] 

201 elif isinstance(estimator, LogisticRegressionCV): 

202 # min_c value is set to 0.5 unless the estimator uses L1 penalty, 

203 # in which case min_c is computed with sklearn.svm.l1_min_c(), 

204 # so for L2 penalty, param_grid["Cs"] is either 1e-3, ..., 1e4, and 

205 # for L1 penalty the values are obtained in a more data-driven way 

206 param_grid["Cs"] = [np.geomspace(2e-3, 2e4, 8) * min_c] 

207 elif isinstance(estimator, LassoCV): 

208 # the default is to generate 30 alphas based on the data 

209 # (alpha values can also be set with the 'alphas' parameter, in which 

210 # case 'n_alphas' is ignored) 

211 param_grid["n_alphas"] = [30] 

212 elif isinstance(estimator, (LinearSVC, SVR)): 

213 # similar logic as above: 

214 # - for L2 penalty this is [1, 10, 100] 

215 # - for L1 penalty the values depend on the data 

216 param_grid["C"] = np.array([2, 20, 200]) * min_c 

217 else: 

218 param_grid = {} 

219 

220 return param_grid 

221 

222 

223def _wrap_param_grid(param_grid, param_name): 

224 """Wrap a parameter's sequence of values with an outer list. 

225 

226 This can be desirable for models featuring built-in cross-validation, 

227 as it would leave it to the model's internal (optimized) cross-validation 

228 to loop over hyperparameter values. Does nothing if the parameter is 

229 already wrapped. 

230 

231 Parameters 

232 ---------- 

233 param_grid : dict of str to sequence, or sequence of such 

234 The parameter grid to wrap, as a dictionary mapping estimator 

235 parameters to sequences of allowed values. 

236 param_name : str 

237 Name of parameter whose sequence of values should be wrapped 

238 

239 Returns 

240 ------- 

241 dict of str to sequence, or sequence of such 

242 The updated parameter grid 

243 """ 

244 if param_grid is None: 

245 return param_grid 

246 

247 # param_grid can be either a dict or a sequence of dicts 

248 # we make sure that it is a sequence we can loop over 

249 input_is_dict = isinstance(param_grid, dict) 

250 if input_is_dict: 

251 param_grid = [param_grid] 

252 

253 # process dicts one by one and add them to a new list 

254 new_param_grid = [] 

255 for param_grid_item in param_grid: 

256 if param_name in param_grid_item and not isinstance( 

257 param_grid_item[param_name][0], Iterable 

258 ): 

259 warnings.warn( 

260 f"parameter '{param_name}' should be a sequence of iterables" 

261 f" (e.g., {{param_name: [[1, 10, 100]]}}) to benefit from" 

262 " the built-in cross-validation of the estimator." 

263 f" Wrapping {param_grid_item[param_name]} in an outer list.", 

264 stacklevel=find_stack_level(), 

265 ) 

266 

267 param_grid_item = dict(param_grid_item) # make a new dict 

268 param_grid_item[param_name] = [param_grid_item[param_name]] 

269 

270 new_param_grid.append(param_grid_item) 

271 

272 # return a dict (not a list) if the original input was a dict 

273 if input_is_dict: 

274 new_param_grid = new_param_grid[0] 

275 

276 return new_param_grid 

277 

278 

279def _replace_param_grid_key(param_grid, key_to_replace, new_key): 

280 """Replace a parameter name by another one. 

281 

282 Parameters 

283 ---------- 

284 param_grid : dict of str to sequence, or sequence of such 

285 The parameter grid to process, as a dictionary mapping estimator 

286 parameters to sequences of allowed values. 

287 key_to_replace : str 

288 Name of parameter to replace 

289 new_key : str 

290 New parameter name. If this key already exists in the parameter grid, 

291 it is overwritten 

292 

293 Returns 

294 ------- 

295 dict of str to sequence, or sequence of such 

296 The updated parameter grid 

297 """ 

298 # ensure param_grid is a list so that we can loop over it 

299 input_is_dict = isinstance(param_grid, dict) 

300 if input_is_dict: 

301 param_grid = [param_grid] 

302 

303 # replace old key by new key if needed 

304 new_param_grid = [] 

305 for param_grid_item in param_grid: 

306 param_grid_item = dict(param_grid_item) # make a new dict 

307 if key_to_replace in param_grid_item: 

308 warnings.warn( 

309 f'The "{key_to_replace}" parameter in "param_grid" is' 

310 f' being replaced by "{new_key}" due to a change in the' 

311 " choice of underlying scikit-learn estimator. In a future" 

312 " version, this will result in an error.", 

313 DeprecationWarning, 

314 stacklevel=find_stack_level(), 

315 ) 

316 param_grid_item[new_key] = param_grid_item.pop(key_to_replace) 

317 new_param_grid.append(param_grid_item) 

318 

319 # return a dict if input was a dict 

320 if input_is_dict: 

321 new_param_grid = new_param_grid[0] 

322 

323 return new_param_grid 

324 

325 

326def _check_estimator(estimator): 

327 if not isinstance(estimator, str): 

328 warnings.warn( 

329 "Use a custom estimator at your own risk " 

330 "of the process not working as intended.", 

331 stacklevel=find_stack_level(), 

332 ) 

333 elif estimator in SUPPORTED_ESTIMATORS: 

334 estimator = SUPPORTED_ESTIMATORS.get(estimator) 

335 else: 

336 raise ValueError( 

337 "Invalid estimator. Known estimators are: " 

338 f"{list(SUPPORTED_ESTIMATORS.keys())}" 

339 ) 

340 

341 return estimator 

342 

343 

344def _parallel_fit( 

345 estimator, 

346 X, 

347 y, 

348 train, 

349 test, 

350 param_grid, 

351 selector, 

352 scorer, 

353 mask_img, 

354 class_index, 

355 clustering_percentile, 

356): 

357 """Find the best estimator for a fold within a job. 

358 

359 This function tries several parameters for the estimator for the train and 

360 test fold provided and save the one that performs best. 

361 

362 Fit may be performed after some preprocessing step : 

363 * clustering with ReNA if clustering_percentile < 100 

364 * feature screening if screening_percentile < 100 

365 """ 

366 X_train, y_train = X[train], y[train] 

367 X_test, y_test = X[test], y[test] 

368 

369 # for FREM Classifier and Regressor : start by doing a quick ReNA 

370 # clustering to reduce the number of feature by agglomerating similar ones 

371 

372 if clustering_percentile < 100: 

373 n_clusters = int(X_train.shape[1] * clustering_percentile / 100.0) 

374 clustering = ReNA( 

375 mask_img, 

376 n_clusters=n_clusters, 

377 n_iter=20, 

378 threshold=1e-7, 

379 scaling=False, 

380 ) 

381 X_train = clustering.fit_transform(X_train) 

382 X_test = clustering.transform(X_test) 

383 

384 do_screening = (X_train.shape[1] > 100) and selector is not None 

385 

386 if do_screening: 

387 X_train = selector.fit_transform(X_train, y_train) 

388 X_test = selector.transform(X_test) 

389 

390 # If there is no parameter grid, then we use a suitable grid (by default) 

391 param_grid = ParameterGrid( 

392 _check_param_grid(estimator, X_train, y_train, param_grid) 

393 ) 

394 

395 # collect all parameter names from the grid 

396 all_params = set() 

397 for params in param_grid: 

398 all_params.update(params.keys()) 

399 

400 best_score = None 

401 for params in param_grid: 

402 estimator = clone(estimator).set_params(**params) 

403 estimator.fit(X_train, y_train) 

404 

405 score = scorer(estimator, X_test, y_test) 

406 

407 # Store best parameters and estimator coefficients 

408 if (best_score is None) or (score >= best_score): 

409 best_score = score 

410 if hasattr(estimator, "coef_"): 

411 best_coef = np.reshape(estimator.coef_, (1, -1)) 

412 best_intercept = estimator.intercept_ 

413 dummy_output = None 

414 else: 

415 best_coef, best_intercept = None, None 

416 if isinstance(estimator, DummyClassifier): 

417 dummy_output = estimator.class_prior_ 

418 elif isinstance(estimator, DummyRegressor): 

419 dummy_output = estimator.constant_ 

420 

421 if isinstance(estimator, (RidgeCV, RidgeClassifierCV, LassoCV)): 

422 params["best_alpha"] = estimator.alpha_ 

423 elif isinstance(estimator, LogisticRegressionCV): 

424 params["best_C"] = estimator.C_.item() 

425 best_params = params 

426 

427 # fill in any missing param from param_grid 

428 for param in all_params: 

429 if param not in best_params: 

430 best_params[param] = getattr(estimator, param) 

431 

432 if best_coef is not None: 

433 if do_screening: 

434 best_coef = selector.inverse_transform(best_coef) 

435 

436 if clustering_percentile < 100: 

437 best_coef = clustering.inverse_transform(best_coef) 

438 

439 return ( 

440 class_index, 

441 best_coef, 

442 best_intercept, 

443 best_params, 

444 best_score, 

445 dummy_output, 

446 ) 

447 

448 

449@fill_doc 

450class _BaseDecoder(CacheMixin, BaseEstimator): 

451 """A wrapper for popular classification/regression strategies in \ 

452 neuroimaging. 

453 

454 The `BaseDecoder` object supports classification and regression methods. 

455 It implements a model selection scheme that averages the best models 

456 within a cross validation loop (a technique sometimes known as CV bagging). 

457 The resulting average model is the one used as a classifier or a regressor. 

458 This object also leverages the `NiftiMaskers` to provide a direct interface 

459 with the Nifti files on disk. 

460 

461 Parameters 

462 ---------- 

463 estimator : str, default='svc' 

464 The estimator to use. For classification, choose among: 

465 %(classifier_options)s 

466 For regression, choose among: 

467 %(regressor_options)s 

468 

469 mask : filename, Nifti1Image, NiftiMasker, MultiNiftiMasker, or\ 

470 SurfaceMasker, default=None 

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

472 then its mask and parameters will be used. If no mask is given, mask 

473 will be computed automatically from provided images by an inbuilt 

474 masker with default parameters. Refer to NiftiMasker, MultiNiftiMasker 

475 or SurfaceMasker to check for default parameters. For use with 

476 SurfaceImage data, a SurfaceMasker instance must be passed. 

477 

478 cv : cross-validation generator or int, default=10 

479 A cross-validation generator. 

480 See: https://scikit-learn.org/stable/modules/cross_validation.html 

481 

482 param_grid : dict of str to sequence, or sequence of such, default=None 

483 The parameter grid to explore, as a dictionary mapping estimator 

484 parameters to sequences of allowed values. 

485 

486 None or an empty dict signifies default parameters. 

487 

488 A sequence of dicts signifies a sequence of grids to search, and is 

489 useful to avoid exploring parameter combinations that make no sense 

490 or have no effect. See scikit-learn documentation for more information, 

491 for example: https://scikit-learn.org/stable/modules/grid_search.html 

492 

493 For Dummy estimators, parameter grid defaults to empty dictionary. 

494 

495 clustering_percentile : int, float, in the [0, 100], default=100 

496 Percentile of features to keep after clustering. If it is lower 

497 than 100, a ReNA clustering is performed as a first step of fit 

498 to agglomerate similar features together. ReNA is typically efficient 

499 for clustering_percentile equal to 10. Only used with 

500 :class:`nilearn.decoding.FREMClassifier` and 

501 :class:`nilearn.decoding.FREMRegressor`. 

502 

503 screening_percentile : int, float, \ 

504 in the closed interval [0, 100], \ 

505 default=20 

506 The percentage of brain volume that will be kept with respect to a full 

507 MNI template. In particular, if it is lower than 100, a univariate 

508 feature selection based on the Anova F-value for the input data will be 

509 performed. A float according to a percentile of the highest 

510 scores. If None is passed, the percentile is set to 100. 

511 

512 scoring : str, callable or None, 

513 default=None 

514 The scoring strategy to use. See the scikit-learn documentation at 

515 https://scikit-learn.org/stable/modules/model_evaluation.html#the-scoring-parameter-defining-model-evaluation-rules 

516 If callable, takes as arguments the fitted estimator, the 

517 test data (X_test) and the test target (y_test) if y is 

518 not None. 

519 e.g. scorer(estimator, X_test, y_test) 

520 

521 For regression, valid entries are: 'r2', 'neg_mean_absolute_error', or 

522 'neg_mean_squared_error'. Defaults to 'r2'. 

523 

524 For classification, valid entries are: 'accuracy', 'f1', 'precision', 

525 'recall' or 'roc_auc'. Defaults to 'roc_auc'. 

526 

527 %(smoothing_fwhm)s 

528 

529 %(standardize)s 

530 

531 %(target_affine)s 

532 

533 %(target_shape)s 

534 

535 %(low_pass)s 

536 

537 %(high_pass)s 

538 

539 %(t_r)s 

540 

541 %(mask_strategy)s 

542 

543 .. note:: 

544 This parameter will be ignored if a mask image is provided. 

545 

546 .. note:: 

547 Depending on this value, the mask will be computed from 

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

549 :func:`nilearn.masking.compute_epi_mask`, or 

550 :func:`nilearn.masking.compute_brain_mask`. 

551 

552 Default is 'background'. 

553 

554 %(memory)s 

555 

556 %(memory_level)s 

557 

558 %(n_jobs)s 

559 

560 %(verbose0)s 

561 

562 See Also 

563 -------- 

564 nilearn.decoding.Decoder: Classification strategies for Neuroimaging, 

565 nilearn.decoding.DecoderRegressor: Regression strategies for Neuroimaging, 

566 nilearn.decoding.FREMClassifier: State of the art classification pipeline 

567 for Neuroimaging 

568 nilearn.decoding.FREMRegressor: State of the art regression pipeline 

569 for Neuroimaging 

570 nilearn.decoding.SpaceNetClassifier: Graph-Net and TV-L1 priors/penalties 

571 

572 """ 

573 

574 def __init__( 

575 self, 

576 estimator="svc", 

577 mask=None, 

578 cv=10, 

579 param_grid=None, 

580 clustering_percentile=100, 

581 screening_percentile=20, 

582 scoring=None, 

583 smoothing_fwhm=None, 

584 standardize=True, 

585 target_affine=None, 

586 target_shape=None, 

587 low_pass=None, 

588 high_pass=None, 

589 t_r=None, 

590 mask_strategy="background", 

591 is_classification=True, 

592 memory=None, 

593 memory_level=0, 

594 n_jobs=1, 

595 verbose=0, 

596 ): 

597 self.estimator = estimator 

598 self.mask = mask 

599 self.cv = cv 

600 self.param_grid = param_grid 

601 self.screening_percentile = screening_percentile 

602 self.scoring = scoring 

603 self.is_classification = is_classification 

604 self.clustering_percentile = clustering_percentile 

605 self.smoothing_fwhm = smoothing_fwhm 

606 self.standardize = standardize 

607 self.target_affine = target_affine 

608 self.target_shape = target_shape 

609 self.mask_strategy = mask_strategy 

610 self.low_pass = low_pass 

611 self.high_pass = high_pass 

612 self.t_r = t_r 

613 self.memory = memory 

614 self.memory_level = memory_level 

615 self.n_jobs = n_jobs 

616 self.verbose = verbose 

617 

618 @fill_doc 

619 def fit(self, X, y, groups=None): 

620 """Fit the decoder (learner). 

621 

622 Parameters 

623 ---------- 

624 X : list of Niimg-like or :obj:`~nilearn.surface.SurfaceImage` objects 

625 See :ref:`extracting_data`. 

626 Data on which model is to be fitted. If this is a list, 

627 the affine is considered the same for all. 

628 

629 y : numpy.ndarray of shape=(n_samples) or list of length n_samples 

630 The dependent variable (age, sex, IQ, yes/no, etc.). 

631 Target variable to predict. Must have exactly as many elements as 

632 3D images in niimg. 

633 

634 %(groups)s 

635 

636 %(base_decoder_fit_attributes)s 

637 

638 """ 

639 check_params(self.__dict__) 

640 self.estimator_ = _check_estimator(self.estimator) 

641 self.memory_ = check_memory(self.memory, self.verbose) 

642 

643 X = self._apply_mask(X) 

644 X, y = check_X_y(X, y, dtype=np.float64, multi_output=True) 

645 

646 self.n_outputs_ = 1 if y.ndim == 1 else y.shape[1] 

647 

648 self._set_scorer() 

649 

650 # Setup cross-validation object. Default is StratifiedKFold when groups 

651 # is None. If groups is specified but self.cv is not set to custom CV 

652 # splitter, default is LeaveOneGroupOut. If self.cv is manually set to 

653 # a CV splitter object do check_cv regardless of groups parameter. 

654 cv = self.cv 

655 

656 if isinstance(cv, int) and isinstance(self, FREMClassifier): 

657 cv_object = StratifiedShuffleSplit(cv, random_state=0) 

658 

659 elif isinstance(cv, int) and isinstance(self, FREMRegressor): 

660 cv_object = ShuffleSplit(cv, random_state=0) 

661 

662 elif (isinstance(cv, int) or cv is None) and groups is not None: 

663 warnings.warn( 

664 "groups parameter is specified but " 

665 "cv parameter is not set to custom CV splitter. " 

666 "Using default object LeaveOneGroupOut().", 

667 stacklevel=find_stack_level(), 

668 ) 

669 cv_object = LeaveOneGroupOut() 

670 

671 else: 

672 cv_object = check_cv(cv, y=y, classifier=self.is_classification) 

673 

674 self.cv_ = list(cv_object.split(X, y, groups=groups)) 

675 

676 # Define the number problems to solve. In case of classification this 

677 # number corresponds to the number of binary problems to solve 

678 y = self._binarize_y(y) if self.is_classification else y[:, np.newaxis] 

679 

680 if self.is_classification and self.n_classes_ > 2: 

681 n_problems = self.n_classes_ 

682 else: 

683 n_problems = 1 

684 

685 # Check if the size of the mask image and the number of features allow 

686 # to perform feature screening. 

687 # If the input data is a SurfaceImage, the number of vertices in the 

688 # mesh is needed to perform feature screening. 

689 mesh_n_vertices = ( 

690 self.mask_img_.mesh.n_vertices 

691 if isinstance(self.mask_img_, SurfaceImage) 

692 else None 

693 ) 

694 selector = check_feature_screening( 

695 self.screening_percentile, 

696 self.mask_img_, 

697 self.is_classification, 

698 mesh_n_vertices=mesh_n_vertices, 

699 ) 

700 

701 # Return a suitable screening percentile according to the mask image 

702 if hasattr(selector, "percentile"): 

703 self.screening_percentile_ = selector.percentile 

704 elif self.screening_percentile is None: 

705 self.screening_percentile_ = 100.0 

706 else: 

707 self.screening_percentile_ = self.screening_percentile 

708 

709 n_final_features = int( 

710 X.shape[1] 

711 * self.screening_percentile_ 

712 * self.clustering_percentile 

713 / 10000 

714 ) 

715 if n_final_features < 50: 

716 warnings.warn( 

717 "After clustering and screening, the decoding model will " 

718 f"be trained only on {n_final_features} features. " 

719 "Consider raising clustering_percentile or " 

720 "screening_percentile parameters.", 

721 UserWarning, 

722 stacklevel=find_stack_level(), 

723 ) 

724 

725 parallel = Parallel(n_jobs=self.n_jobs, verbose=2 * self.verbose) 

726 

727 parallel_fit_outputs = parallel( 

728 delayed(self._cache(_parallel_fit))( 

729 estimator=self.estimator_, 

730 X=X, 

731 y=y[:, c], 

732 train=train, 

733 test=test, 

734 param_grid=self.param_grid, 

735 selector=selector, 

736 scorer=self.scorer_, 

737 mask_img=self.mask_img_, 

738 class_index=c, 

739 clustering_percentile=self.clustering_percentile, 

740 ) 

741 for c, (train, test) in itertools.product( 

742 range(n_problems), self.cv_ 

743 ) 

744 ) 

745 

746 coefs, intercepts = self._fetch_parallel_fit_outputs( 

747 parallel_fit_outputs, y, n_problems 

748 ) 

749 

750 # Build the final model (the aggregated one) 

751 if not isinstance(self.estimator_, (DummyClassifier, DummyRegressor)): 

752 self.coef_ = np.vstack( 

753 [ 

754 np.mean(coefs[class_index], axis=0) 

755 for class_index in self.classes_ 

756 ] 

757 ) 

758 self.std_coef_ = np.vstack( 

759 [ 

760 np.std(coefs[class_index], axis=0) 

761 for class_index in self.classes_ 

762 ] 

763 ) 

764 self.intercept_ = np.hstack( 

765 [ 

766 np.mean(intercepts[class_index], axis=0) 

767 for class_index in self.classes_ 

768 ] 

769 ) 

770 

771 self.coef_img_, self.std_coef_img_ = self._output_image( 

772 self.classes_, self.coef_, self.std_coef_ 

773 ) 

774 

775 if self.is_classification and (self.n_classes_ == 2): 

776 self.coef_ = self.coef_[0, :][np.newaxis, :] 

777 self.intercept_ = self.intercept_[0] 

778 else: 

779 # For Dummy estimators 

780 self.coef_ = None 

781 self.dummy_output_ = np.vstack( 

782 [ 

783 np.mean(self.dummy_output_[class_index], axis=0) 

784 for class_index in self.classes_ 

785 ] 

786 ) 

787 if self.is_classification and (self.n_classes_ == 2): 

788 self.dummy_output_ = self.dummy_output_[0, :][np.newaxis, :] 

789 

790 return self 

791 

792 def __sklearn_is_fitted__(self): 

793 return hasattr(self, "coef_") and hasattr(self, "masker_") 

794 

795 def score(self, X, y, *args): 

796 """Compute the prediction score using the scoring \ 

797 metric defined by the scoring attribute. 

798 

799 Parameters 

800 ---------- 

801 X : Niimg-like, :obj:`list` of either \ 

802 Niimg-like objects or :obj:`str` or path-like 

803 See :ref:`extracting_data`. 

804 Data on which prediction is to be made. 

805 

806 y : :class:`numpy.ndarray` 

807 Target values. 

808 

809 args : Optional arguments that can be passed to 

810 scoring metrics. Example: sample_weight. 

811 

812 Returns 

813 ------- 

814 score : float 

815 Prediction score. 

816 

817 """ 

818 check_is_fitted(self) 

819 return self.scorer_(self, X, y, *args) 

820 

821 def decision_function(self, X): 

822 """Predict class labels for samples in X. 

823 

824 Parameters 

825 ---------- 

826 X : Niimg-like, :obj:`list` of either \ 

827 Niimg-like objects or :obj:`str` or path-like 

828 See :ref:`extracting_data`. 

829 Data on prediction is to be made. If this is a list, 

830 the affine is considered the same for all. 

831 

832 Returns 

833 ------- 

834 y_pred : :class:`numpy.ndarray`, shape (n_samples,) 

835 Predicted class label per sample. 

836 """ 

837 check_is_fitted(self) 

838 # for backwards compatibility - apply masker transform if X is 

839 # niimg-like or a list of strings 

840 if not isinstance(X, np.ndarray) or len(np.shape(X)) == 1: 

841 X = self.masker_.transform(X) 

842 n_features = self.coef_.shape[1] 

843 if X.shape[1] != n_features: 

844 raise ValueError( 

845 f"X has {X.shape[1]} features per sample;" 

846 f" expecting {n_features}" 

847 ) 

848 

849 scores = ( 

850 safe_sparse_dot(X, self.coef_.T, dense_output=True) 

851 + self.intercept_ 

852 ) 

853 

854 return scores.ravel() if scores.shape[1] == 1 else scores 

855 

856 def predict(self, X): 

857 """Predict a label for all X vectors indexed by the first axis. 

858 

859 Parameters 

860 ---------- 

861 X : Niimg-like, :obj:`list` of either \ 

862 Niimg-like objects or :obj:`str` or path-like 

863 See :ref:`extracting_data`. 

864 Data on which prediction is to be made. 

865 

866 Returns 

867 ------- 

868 array, shape=(n_samples,) if n_classes == 2 else (n_samples, n_classes) 

869 Confidence scores per (sample, class) combination. In the binary 

870 case, confidence score for self.classes_[1] where >0 means this 

871 class would be predicted. 

872 """ 

873 check_is_fitted(self) 

874 

875 n_samples = np.shape(X)[-1] 

876 

877 # Prediction for dummy estimator is different from others as there is 

878 # no fitted coefficient 

879 if isinstance(self.estimator_, (DummyClassifier, DummyRegressor)): 

880 scores = self._predict_dummy(n_samples) 

881 else: 

882 scores = self.decision_function(X) 

883 

884 if self.is_classification: 

885 if scores.ndim == 1: 

886 indices = (scores > 0).astype(int) 

887 else: 

888 indices = scores.argmax(axis=1) 

889 return self.classes_[indices] 

890 

891 return scores 

892 

893 def _apply_mask(self, X): 

894 masker_type = "nii" 

895 # all elements of X should be of the similar type by now 

896 # so we can only check the first one 

897 to_check = X[0] if isinstance(X, Iterable) else X 

898 if isinstance(self.mask, (SurfaceMasker, SurfaceImage)) or ( 

899 isinstance(to_check, SurfaceImage) 

900 ): 

901 masker_type = "surface" 

902 

903 self.masker_ = check_embedded_masker(self, masker_type=masker_type) 

904 check_compatibility_mask_and_images(self.mask, X) 

905 

906 X = self.masker_.fit_transform(X) 

907 self.mask_img_ = self.masker_.mask_img_ 

908 

909 return X 

910 

911 def _fetch_parallel_fit_outputs( 

912 self, 

913 parallel_fit_outputs, 

914 y, # noqa: ARG002 

915 n_problems, 

916 ): 

917 """Fetch the outputs from parallel_fit to be ready for ensembling. 

918 

919 Parameters 

920 ---------- 

921 parallel_fit_outputs : list of tuples, 

922 each tuple contains results of 

923 one _parallel_fit for each cv fold (and each classification in the 

924 case of multiclass classification). 

925 

926 y : ndarray, shape = (n_samples, ) 

927 Vector of responses. 

928 

929 Returns 

930 ------- 

931 coefs : dict 

932 Coefficients for each classification/regression problem 

933 intercepts : dict 

934 Intercept for each classification/regression problem 

935 """ 

936 coefs = {} 

937 intercepts = {} 

938 cv_scores = {} 

939 self.cv_params_ = {} 

940 self.dummy_output_ = {} 

941 classes = self.classes_ 

942 

943 for ( 

944 class_index, 

945 coef, 

946 intercept, 

947 params, 

948 scores, 

949 dummy_output, 

950 ) in parallel_fit_outputs: 

951 coefs.setdefault(classes[class_index], []).append(coef) 

952 intercepts.setdefault(classes[class_index], []).append(intercept) 

953 

954 cv_scores.setdefault(classes[class_index], []).append(scores) 

955 

956 self.cv_params_.setdefault(classes[class_index], {}) 

957 if isinstance(self.estimator_, (DummyClassifier, DummyRegressor)): 

958 self.dummy_output_.setdefault(classes[class_index], []).append( 

959 dummy_output 

960 ) 

961 else: 

962 self.dummy_output_.setdefault(classes[class_index], []).append( 

963 None 

964 ) 

965 for k in params: 

966 self.cv_params_[classes[class_index]].setdefault(k, []).append( 

967 params[k] 

968 ) 

969 

970 if (n_problems <= 2) and self.is_classification: 

971 # Binary classification 

972 other_class = np.setdiff1d(classes, classes[class_index])[0] 

973 if coef is not None: 

974 coefs.setdefault(other_class, []).append(-coef) 

975 intercepts.setdefault(other_class, []).append(-intercept) 

976 else: 

977 coefs.setdefault(other_class, []).append(None) 

978 intercepts.setdefault(other_class, []).append(None) 

979 

980 cv_scores.setdefault(other_class, []).append(scores) 

981 self.cv_params_[other_class] = self.cv_params_[ 

982 classes[class_index] 

983 ] 

984 if isinstance( 

985 self.estimator_, (DummyClassifier, DummyRegressor) 

986 ): 

987 self.dummy_output_.setdefault(other_class, []).append( 

988 dummy_output 

989 ) 

990 else: 

991 self.dummy_output_.setdefault(other_class, []).append(None) 

992 

993 self.cv_scores_ = cv_scores 

994 

995 return coefs, intercepts 

996 

997 def _set_scorer(self): 

998 if self.scoring is not None: 

999 self.scorer_ = check_scoring(self.estimator_, self.scoring) 

1000 elif self.is_classification: 

1001 self.scorer_ = get_scorer("accuracy") 

1002 else: 

1003 self.scorer_ = get_scorer("r2") 

1004 

1005 def _output_image(self, classes, coefs, std_coef): 

1006 coef_img = {} 

1007 std_coef_img = {} 

1008 for class_index, coef, std in zip(classes, coefs, std_coef): 

1009 coef_img[class_index] = self.masker_.inverse_transform(coef) 

1010 std_coef_img[class_index] = self.masker_.inverse_transform(std) 

1011 

1012 return coef_img, std_coef_img 

1013 

1014 def _binarize_y(self, y): 

1015 """Encode target classes as -1 and 1. 

1016 

1017 Helper function invoked just before fitting a classifier. 

1018 """ 

1019 y = np.array(y) 

1020 

1021 self._enc = LabelBinarizer(pos_label=1, neg_label=-1) 

1022 y = self._enc.fit_transform(y) 

1023 self.classes_ = self._enc.classes_ 

1024 self.n_classes_ = len(self.classes_) 

1025 return y 

1026 

1027 def _predict_dummy(self, n_samples): 

1028 """Non-sparse scikit-learn based prediction steps for classification \ 

1029 and regression. 

1030 """ 

1031 if len(self.dummy_output_) == 1: 

1032 dummy_output = self.dummy_output_[0] 

1033 else: 

1034 dummy_output = self.dummy_output_[:, 1] 

1035 if isinstance(self.estimator_, DummyClassifier): 

1036 strategy = self.estimator_.get_params()["strategy"] 

1037 if strategy in ["most_frequent", "prior"]: 

1038 scores = np.tile(dummy_output, reps=(n_samples, 1)) 

1039 elif strategy == "stratified": 

1040 rs = np.random.default_rng(0) 

1041 scores = rs.multinomial(1, dummy_output, size=n_samples) 

1042 

1043 elif isinstance(self.estimator_, DummyRegressor): 

1044 scores = np.full( 

1045 (n_samples, self.n_outputs_), 

1046 self.dummy_output_, 

1047 dtype=np.array(self.dummy_output_).dtype, 

1048 ) 

1049 return scores.ravel() if scores.shape[1] == 1 else scores 

1050 

1051 def _more_tags(self): 

1052 """Return estimator tags. 

1053 

1054 TODO remove when bumping sklearn_version > 1.5 

1055 """ 

1056 return self.__sklearn_tags__() 

1057 

1058 def __sklearn_tags__(self): 

1059 """Return estimator tags. 

1060 

1061 See the sklearn documentation for more details on tags 

1062 https://scikit-learn.org/1.6/developers/develop.html#estimator-tags 

1063 """ 

1064 # TODO 

1065 # get rid of if block 

1066 # bumping sklearn_version > 1.5 

1067 # see https://github.com/scikit-learn/scikit-learn/pull/29677 

1068 if SKLEARN_LT_1_6: 

1069 from nilearn._utils.tags import tags 

1070 

1071 return tags(require_y=True, niimg_like=True, surf_img=True) 

1072 

1073 from nilearn._utils.tags import InputTags 

1074 

1075 tags = super().__sklearn_tags__() 

1076 tags.target_tags.required = True 

1077 tags.input_tags = InputTags(niimg_like=True, surf_img=True) 

1078 return tags 

1079 

1080 

1081@fill_doc 

1082class Decoder(ClassifierMixin, _BaseDecoder): 

1083 """A wrapper for popular classification strategies in neuroimaging. 

1084 

1085 The `Decoder` object supports classification methods. 

1086 It implements a model selection scheme that averages the best models 

1087 within a cross validation loop. The resulting average model is the 

1088 one used as a classifier. This object also leverages the`NiftiMaskers` to 

1089 provide a direct interface with the Nifti files on disk. 

1090 

1091 Parameters 

1092 ---------- 

1093 estimator : :obj:`str`, default='svc' 

1094 The estimator to choose among: 

1095 %(classifier_options)s 

1096 

1097 mask : filename, Nifti1Image, NiftiMasker, MultiNiftiMasker, \ 

1098 :obj:`~nilearn.surface.SurfaceImage` \ 

1099 or :obj:`~nilearn.maskers.SurfaceMasker`, default=None 

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

1101 then its mask and parameters will be used. If no mask is given, mask 

1102 will be computed automatically from provided images by an inbuilt 

1103 masker with default parameters. 

1104 Refer to :obj:`~nilearn.maskers.NiftiMasker` or 

1105 :obj:`~nilearn.maskers.MultiNiftiMasker` or 

1106 :obj:`~nilearn.maskers.SurfaceMasker` 

1107 to check for default parameters. 

1108 

1109 cv : cross-validation generator or :obj:`int`, default=10 

1110 A cross-validation generator. 

1111 See: https://scikit-learn.org/stable/modules/cross_validation.html. 

1112 The default 10 refers to K = 10 folds of 

1113 :class:`~sklearn.model_selection.StratifiedKFold` when groups is None 

1114 in the fit method for this class. If groups is specified but ``cv`` 

1115 is not set to custom CV splitter, default is 

1116 :class:`~sklearn.model_selection.LeaveOneGroupOut`. 

1117 

1118 param_grid : :obj:`dict` of :obj:`str` to sequence, or sequence of such, \ 

1119 or None, default=None 

1120 The parameter grid to explore, as a dictionary mapping estimator 

1121 parameters to sequences of allowed values. 

1122 

1123 None or an empty dict signifies default parameters. 

1124 

1125 A sequence of dicts signifies a sequence of grids to search, and is 

1126 useful to avoid exploring parameter combinations that make no sense 

1127 or have no effect. See scikit-learn documentation for more information, 

1128 for example: https://scikit-learn.org/stable/modules/grid_search.html 

1129 

1130 For DummyClassifier, parameter grid defaults to empty dictionary, class 

1131 predictions are estimated using default strategy. 

1132 

1133 screening_percentile : :obj:`int`, :obj:`float`, optional, \ 

1134 in the closed interval [0, 100], default=20 

1135 The percentage of brain volume that will be kept with respect to a full 

1136 MNI template. In particular, if it is lower than 100, a univariate 

1137 feature selection based on the Anova F-value for the input data will be 

1138 performed. A float according to a percentile of the highest scores. 

1139 

1140 scoring : :obj:`str`, callable or None, default='roc_auc' 

1141 The scoring strategy to use. See the scikit-learn documentation at 

1142 https://scikit-learn.org/stable/modules/model_evaluation.html#the-scoring-parameter-defining-model-evaluation-rules 

1143 If callable, takes as arguments the fitted estimator, the 

1144 test data (X_test) and the test target (y_test) if y is 

1145 not None. 

1146 e.g. scorer(estimator, X_test, y_test) 

1147 

1148 For classification, valid entries are: 'accuracy', 'f1', 'precision', 

1149 'recall' or 'roc_auc'. 

1150 

1151 %(smoothing_fwhm)s 

1152 

1153 %(standardize)s 

1154 

1155 %(target_affine)s 

1156 

1157 %(target_shape)s 

1158 

1159 %(low_pass)s 

1160 

1161 %(high_pass)s 

1162 

1163 %(t_r)s 

1164 

1165 %(mask_strategy)s 

1166 

1167 .. note:: 

1168 This parameter will be ignored if a mask image is provided. 

1169 

1170 .. note:: 

1171 Depending on this value, the mask will be computed from 

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

1173 :func:`nilearn.masking.compute_epi_mask`, or 

1174 :func:`nilearn.masking.compute_brain_mask`. 

1175 

1176 Default='background'. 

1177 

1178 %(memory)s 

1179 

1180 %(memory_level)s 

1181 

1182 %(n_jobs)s 

1183 

1184 %(verbose0)s 

1185 

1186 See Also 

1187 -------- 

1188 nilearn.decoding.DecoderRegressor: regression strategies for Neuro-imaging, 

1189 nilearn.decoding.FREMClassifier: State of the art classification pipeline 

1190 for Neuroimaging 

1191 nilearn.decoding.SpaceNetClassifier: Graph-Net and TV-L1 priors/penalties 

1192 """ 

1193 

1194 def __init__( 

1195 self, 

1196 estimator="svc", 

1197 mask=None, 

1198 cv=10, 

1199 param_grid=None, 

1200 screening_percentile=20, 

1201 scoring="roc_auc", 

1202 smoothing_fwhm=None, 

1203 standardize=True, 

1204 target_affine=None, 

1205 target_shape=None, 

1206 mask_strategy="background", 

1207 low_pass=None, 

1208 high_pass=None, 

1209 t_r=None, 

1210 memory=None, 

1211 memory_level=0, 

1212 n_jobs=1, 

1213 verbose=0, 

1214 ): 

1215 super().__init__( 

1216 estimator=estimator, 

1217 mask=mask, 

1218 cv=cv, 

1219 param_grid=param_grid, 

1220 screening_percentile=screening_percentile, 

1221 scoring=scoring, 

1222 smoothing_fwhm=smoothing_fwhm, 

1223 standardize=standardize, 

1224 target_affine=target_affine, 

1225 target_shape=target_shape, 

1226 mask_strategy=mask_strategy, 

1227 low_pass=low_pass, 

1228 high_pass=high_pass, 

1229 t_r=t_r, 

1230 memory=memory, 

1231 is_classification=True, 

1232 memory_level=memory_level, 

1233 verbose=verbose, 

1234 n_jobs=n_jobs, 

1235 ) 

1236 # TODO remove for sklearn>=1.6 

1237 self._estimator_type = "classifier" 

1238 

1239 def _more_tags(self): 

1240 """Return estimator tags. 

1241 

1242 TODO remove when bumping sklearn_version > 1.5 

1243 """ 

1244 return self.__sklearn_tags__() 

1245 

1246 def __sklearn_tags__(self): 

1247 """Return estimator tags. 

1248 

1249 See the sklearn documentation for more details on tags 

1250 https://scikit-learn.org/1.6/developers/develop.html#estimator-tags 

1251 """ 

1252 # TODO 

1253 # get rid of if block 

1254 # bumping sklearn_version > 1.5 

1255 # see https://github.com/scikit-learn/scikit-learn/pull/29677 

1256 tags = super().__sklearn_tags__() 

1257 if SKLEARN_LT_1_6: 

1258 return tags 

1259 

1260 from sklearn.utils import ClassifierTags 

1261 

1262 tags.estimator_type = "classifier" 

1263 tags.classifier_tags = ClassifierTags() 

1264 

1265 return tags 

1266 

1267 

1268@fill_doc 

1269class DecoderRegressor(MultiOutputMixin, RegressorMixin, _BaseDecoder): 

1270 """A wrapper for popular regression strategies in neuroimaging. 

1271 

1272 The `DecoderRegressor` object supports regression methods. 

1273 It implements a model selection scheme that averages the best models 

1274 within a cross validation loop. The resulting average model is the 

1275 one used as a regressor. This object also leverages the `NiftiMaskers` 

1276 to provide a direct interface with the Nifti files on disk. 

1277 

1278 Parameters 

1279 ---------- 

1280 estimator : :obj:`str`, optional 

1281 The estimator to choose among: 

1282 %(regressor_options)s 

1283 Default 'svr'. 

1284 

1285 mask : filename, Nifti1Image, NiftiMasker, or MultiNiftiMasker, optional 

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

1287 then its mask and parameters will be used. If no mask is given, mask 

1288 will be computed automatically from provided images by an inbuilt 

1289 masker with default parameters. Refer to NiftiMasker or 

1290 MultiNiftiMasker to check for default parameters. Default None 

1291 

1292 cv : cross-validation generator or :obj:`int`, default=10 

1293 A cross-validation generator. 

1294 See: https://scikit-learn.org/stable/modules/cross_validation.html. 

1295 The default 10 refers to K = 10 folds of 

1296 :class:`~sklearn.model_selection.StratifiedKFold` when groups is None 

1297 in the fit method for this class. If groups is specified but ``cv`` 

1298 is not set to custom CV splitter, default is 

1299 :class:`~sklearn.model_selection.LeaveOneGroupOut`. 

1300 

1301 param_grid : :obj:`dict` of :obj:`str` to sequence, or sequence of such, \ 

1302 or None, default=None 

1303 The parameter grid to explore, as a dictionary mapping estimator 

1304 parameters to sequences of allowed values. 

1305 

1306 None or an empty dict signifies default parameters. 

1307 

1308 A sequence of dicts signifies a sequence of grids to search, and is 

1309 useful to avoid exploring parameter combinations that make no sense 

1310 or have no effect. See scikit-learn documentation for more information, 

1311 for example: https://scikit-learn.org/stable/modules/grid_search.html 

1312 

1313 For DummyRegressor, parameter grid defaults to empty dictionary, class 

1314 predictions are estimated using default strategy. 

1315 

1316 screening_percentile : :obj:`int`, :obj:`float`, \ 

1317 in the closed interval [0, 100], \ 

1318 default=20 

1319 The percentage of brain volume that will be kept with respect to a full 

1320 MNI template. In particular, if it is lower than 100, a univariate 

1321 feature selection based on the Anova F-value for the input data will be 

1322 performed. A float according to a percentile of the highest 

1323 scores. 

1324 

1325 scoring : :obj:`str`, callable or None, optional. default='r2' 

1326 The scoring strategy to use. See the scikit-learn documentation at 

1327 https://scikit-learn.org/stable/modules/model_evaluation.html#the-scoring-parameter-defining-model-evaluation-rules 

1328 If callable, takes as arguments the fitted estimator, the 

1329 test data (X_test) and the test target (y_test) if y is 

1330 not None. 

1331 e.g. scorer(estimator, X_test, y_test) 

1332 

1333 For regression, valid entries are: 'r2', 'neg_mean_absolute_error', 

1334 or 'neg_mean_squared_error'. 

1335 

1336 %(smoothing_fwhm)s 

1337 

1338 %(standardize)s 

1339 

1340 %(target_affine)s 

1341 

1342 %(target_shape)s 

1343 

1344 %(low_pass)s 

1345 

1346 %(high_pass)s 

1347 

1348 %(t_r)s 

1349 

1350 %(mask_strategy)s 

1351 

1352 .. note:: 

1353 This parameter will be ignored if a mask image is provided. 

1354 

1355 .. note:: 

1356 Depending on this value, the mask will be computed from 

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

1358 :func:`nilearn.masking.compute_epi_mask`, or 

1359 :func:`nilearn.masking.compute_brain_mask`. 

1360 

1361 Default='background'. 

1362 

1363 %(memory)s 

1364 

1365 %(memory_level)s 

1366 

1367 %(n_jobs)s 

1368 

1369 %(verbose0)s 

1370 

1371 See Also 

1372 -------- 

1373 nilearn.decoding.Decoder: classification strategies for Neuroimaging, 

1374 nilearn.decoding.FREMRegressor: State of the art regression pipeline 

1375 for Neuroimaging 

1376 nilearn.decoding.SpaceNetClassifier: Graph-Net and TV-L1 priors/penalties 

1377 """ 

1378 

1379 def __init__( 

1380 self, 

1381 estimator="svr", 

1382 mask=None, 

1383 cv=10, 

1384 param_grid=None, 

1385 screening_percentile=20, 

1386 scoring="r2", 

1387 smoothing_fwhm=None, 

1388 standardize=True, 

1389 target_affine=None, 

1390 target_shape=None, 

1391 mask_strategy="background", 

1392 low_pass=None, 

1393 high_pass=None, 

1394 t_r=None, 

1395 memory=None, 

1396 memory_level=0, 

1397 n_jobs=1, 

1398 verbose=0, 

1399 ): 

1400 super().__init__( 

1401 estimator=estimator, 

1402 mask=mask, 

1403 cv=cv, 

1404 param_grid=param_grid, 

1405 screening_percentile=screening_percentile, 

1406 scoring=scoring, 

1407 smoothing_fwhm=smoothing_fwhm, 

1408 standardize=standardize, 

1409 target_affine=target_affine, 

1410 target_shape=target_shape, 

1411 low_pass=low_pass, 

1412 high_pass=high_pass, 

1413 t_r=t_r, 

1414 mask_strategy=mask_strategy, 

1415 memory=memory, 

1416 is_classification=False, 

1417 memory_level=memory_level, 

1418 verbose=verbose, 

1419 n_jobs=n_jobs, 

1420 ) 

1421 

1422 # TODO remove for sklearn>=1.6 

1423 self._estimator_type = "regressor" 

1424 

1425 def _more_tags(self): 

1426 """Return estimator tags. 

1427 

1428 TODO remove when bumping sklearn_version > 1.5 

1429 """ 

1430 return self.__sklearn_tags__() 

1431 

1432 def __sklearn_tags__(self): 

1433 """Return estimator tags. 

1434 

1435 See the sklearn documentation for more details on tags 

1436 https://scikit-learn.org/1.6/developers/develop.html#estimator-tags 

1437 """ 

1438 # TODO 

1439 # get rid of if block 

1440 # bumping sklearn_version > 1.5 

1441 # see https://github.com/scikit-learn/scikit-learn/pull/29677 

1442 tags = super().__sklearn_tags__() 

1443 if SKLEARN_LT_1_6: 

1444 tags["multioutput"] = True 

1445 return tags 

1446 from sklearn.utils import RegressorTags 

1447 

1448 tags.estimator_type = "regressor" 

1449 tags.regressor_tags = RegressorTags() 

1450 

1451 return tags 

1452 

1453 @fill_doc 

1454 def fit(self, X, y, groups=None): 

1455 """Fit the decoder (learner). 

1456 

1457 Parameters 

1458 ---------- 

1459 X : list of Niimg-like or :obj:`~nilearn.surface.SurfaceImage` objects 

1460 See :ref:`extracting_data`. 

1461 Data on which model is to be fitted. If this is a list, 

1462 the affine is considered the same for all. 

1463 

1464 y : numpy.ndarray of shape=(n_samples) or list of length n_samples 

1465 The dependent variable (age, sex, IQ, yes/no, etc.). 

1466 Target variable to predict. Must have exactly as many elements as 

1467 3D images in niimg. 

1468 

1469 %(groups)s 

1470 

1471 %(base_decoder_fit_attributes)s 

1472 

1473 """ 

1474 check_params(self.__dict__) 

1475 self.classes_ = ["beta"] 

1476 return super().fit(X, y, groups=groups) 

1477 

1478 

1479@fill_doc 

1480class FREMRegressor(_BaseDecoder): 

1481 """State of the art :term:`decoding` scheme applied \ 

1482 to usual regression estimators. 

1483 

1484 FREM uses an implicit spatial regularization through fast clustering and 

1485 aggregates a high number of estimators trained on various splits of the 

1486 training set, thus returning a very robust decoder 

1487 at a lower computational cost 

1488 than other spatially regularized methods :footcite:p:`Hoyos-Idrobo2018`. 

1489 

1490 Parameters 

1491 ---------- 

1492 estimator : :obj:`str`, optional 

1493 The estimator to choose among: 

1494 %(regressor_options)s 

1495 Default 'svr'. 

1496 

1497 mask : filename, Nifti1Image, NiftiMasker, or MultiNiftiMasker, \ 

1498 default=None 

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

1500 then its mask and parameters will be used. If no mask is given, mask 

1501 will be computed automatically from provided images by an inbuilt 

1502 masker with default parameters. Refer to NiftiMasker or 

1503 MultiNiftiMasker to check for default parameters. 

1504 

1505 cv : :obj:`int` or cross-validation generator, default=30 

1506 If int, number of shuffled splits returned, which is usually the right 

1507 way to train many different classifiers. A good trade-off between 

1508 stability of the aggregated model and computation time is 50 splits. 

1509 Shuffled splits are seeded by default for reproducibility. 

1510 Can also be a cross-validation generator. 

1511 

1512 param_grid : :obj:`dict` of :obj:`str` to sequence, or sequence of such. \ 

1513 or None, default=None 

1514 The parameter grid to explore, as a dictionary mapping estimator 

1515 parameters to sequences of allowed values. 

1516 

1517 None or an empty dict signifies default parameters. 

1518 

1519 A sequence of dicts signifies a sequence of grids to search, and is 

1520 useful to avoid exploring parameter combinations that make no sense 

1521 or have no effect. See scikit-learn documentation for more information, 

1522 for example: https://scikit-learn.org/stable/modules/grid_search.html 

1523 

1524 clustering_percentile : :obj:`int`, :obj:`float`, \ 

1525 in closed interval [0, 100] \ 

1526 default=10 

1527 Used to perform a fast ReNA clustering on input data as a first step of 

1528 fit. It agglomerates similar features together to reduce their number 

1529 by this percentile. ReNA is typically efficient for cluster_percentile 

1530 equal to 10. 

1531 

1532 screening_percentile : :obj:`int`, :obj:`float`, \ 

1533 in closed interval [0, 100] \ 

1534 default=20 

1535 The percentage of brain volume that will be kept with respect to a full 

1536 MNI template. In particular, if it is lower than 100, a univariate 

1537 feature selection based on the Anova F-value for the input data will be 

1538 performed. A float according to a percentile of the highest 

1539 scores. 

1540 

1541 scoring : :obj:`str`, callable or None, default= 'r2' 

1542 

1543 The scoring strategy to use. See the scikit-learn documentation at 

1544 https://scikit-learn.org/stable/modules/model_evaluation.html#the-scoring-parameter-defining-model-evaluation-rules 

1545 If callable, takes as arguments the fitted estimator, the 

1546 test data (X_test) and the test target (y_test) if y is 

1547 not None. 

1548 e.g. scorer(estimator, X_test, y_test) 

1549 

1550 For regression, valid entries are: 'r2', 'neg_mean_absolute_error', 

1551 or 'neg_mean_squared_error'. 

1552 %(smoothing_fwhm)s 

1553 %(standardize)s 

1554 %(target_affine)s 

1555 %(target_shape)s 

1556 %(low_pass)s 

1557 %(high_pass)s 

1558 %(t_r)s 

1559 %(mask_strategy)s 

1560 

1561 .. note:: 

1562 This parameter will be ignored if a mask image is provided. 

1563 

1564 .. note:: 

1565 Depending on this value, the mask will be computed from 

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

1567 :func:`nilearn.masking.compute_epi_mask`, or 

1568 :func:`nilearn.masking.compute_brain_mask`. 

1569 

1570 Default='background'. 

1571 %(memory)s 

1572 %(memory_level)s 

1573 %(n_jobs)s 

1574 %(verbose0)s 

1575 

1576 References 

1577 ---------- 

1578 .. footbibliography:: 

1579 

1580 See Also 

1581 -------- 

1582 nilearn.decoding.DecoderRegressor: Regression strategies for Neuroimaging, 

1583 nilearn.decoding.FREMClassifier: State of the art classification pipeline 

1584 for Neuroimaging 

1585 """ 

1586 

1587 def __init__( 

1588 self, 

1589 estimator="svr", 

1590 mask=None, 

1591 cv=30, 

1592 param_grid=None, 

1593 clustering_percentile=10, 

1594 screening_percentile=20, 

1595 scoring="r2", 

1596 smoothing_fwhm=None, 

1597 standardize=True, 

1598 target_affine=None, 

1599 target_shape=None, 

1600 mask_strategy="background", 

1601 low_pass=None, 

1602 high_pass=None, 

1603 t_r=None, 

1604 memory=None, 

1605 memory_level=0, 

1606 n_jobs=1, 

1607 verbose=0, 

1608 ): 

1609 super().__init__( 

1610 estimator=estimator, 

1611 mask=mask, 

1612 cv=cv, 

1613 param_grid=param_grid, 

1614 clustering_percentile=clustering_percentile, 

1615 screening_percentile=screening_percentile, 

1616 scoring=scoring, 

1617 smoothing_fwhm=smoothing_fwhm, 

1618 standardize=standardize, 

1619 target_affine=target_affine, 

1620 target_shape=target_shape, 

1621 low_pass=low_pass, 

1622 high_pass=high_pass, 

1623 t_r=t_r, 

1624 mask_strategy=mask_strategy, 

1625 memory=memory, 

1626 is_classification=False, 

1627 memory_level=memory_level, 

1628 verbose=verbose, 

1629 n_jobs=n_jobs, 

1630 ) 

1631 

1632 # TODO remove after sklearn>=1.6 

1633 self._estimator_type = "regressor" 

1634 

1635 def _more_tags(self): 

1636 """Return estimator tags. 

1637 

1638 TODO remove when bumping sklearn_version > 1.5 

1639 """ 

1640 return self.__sklearn_tags__() 

1641 

1642 def __sklearn_tags__(self): 

1643 """Return estimator tags. 

1644 

1645 See the sklearn documentation for more details on tags 

1646 https://scikit-learn.org/1.6/developers/develop.html#estimator-tags 

1647 """ 

1648 # TODO 

1649 # get rid of if block 

1650 # bumping sklearn_version > 1.5 

1651 # see https://github.com/scikit-learn/scikit-learn/pull/29677 

1652 tags = super().__sklearn_tags__() 

1653 if SKLEARN_LT_1_6: 

1654 tags["multioutput"] = True 

1655 return tags 

1656 

1657 from sklearn.utils import RegressorTags 

1658 

1659 tags.estimator_type = "regressor" 

1660 tags.regressor_tags = RegressorTags() 

1661 

1662 return tags 

1663 

1664 @fill_doc 

1665 def fit(self, X, y, groups=None): 

1666 """Fit the decoder (learner). 

1667 

1668 Parameters 

1669 ---------- 

1670 X : list of Niimg-like or :obj:`~nilearn.surface.SurfaceImage` objects 

1671 See :ref:`extracting_data`. 

1672 Data on which model is to be fitted. If this is a list, 

1673 the affine is considered the same for all. 

1674 

1675 y : numpy.ndarray of shape=(n_samples) or list of length n_samples 

1676 The dependent variable (age, sex, IQ, yes/no, etc.). 

1677 Target variable to predict. Must have exactly as many elements as 

1678 3D images in niimg. 

1679 

1680 %(groups)s 

1681 

1682 %(base_decoder_fit_attributes)s 

1683 

1684 """ 

1685 check_params(self.__dict__) 

1686 self.classes_ = ["beta"] 

1687 super().fit(X, y, groups=groups) 

1688 return self 

1689 

1690 

1691@fill_doc 

1692class FREMClassifier(_BaseDecoder): 

1693 """State of the art :term:`decoding` scheme applied to usual classifiers. 

1694 

1695 FREM uses an implicit spatial regularization through fast clustering and 

1696 aggregates a high number of estimators trained on various splits of the 

1697 training set, thus returning a very robust decoder 

1698 at a lower computational cost 

1699 than other spatially regularized methods :footcite:p:`Hoyos-Idrobo2018`. 

1700 

1701 Parameters 

1702 ---------- 

1703 estimator : :obj:`str`, default 'svc') 

1704 The estimator to choose among: 

1705 %(classifier_options)s 

1706 

1707 

1708 mask : filename, Nifti1Image, NiftiMasker, or MultiNiftiMasker, optional,\ 

1709 default=None 

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

1711 then its mask and parameters will be used. If no mask is given, mask 

1712 will be computed automatically from provided images by an inbuilt 

1713 masker with default parameters. Refer to NiftiMasker or 

1714 MultiNiftiMasker to check for default parameters. 

1715 

1716 cv : :obj:`int` or cross-validation generator, default=30 

1717 If int, number of stratified shuffled splits returned, which is usually 

1718 the right way to train many different classifiers. A good trade-off 

1719 between stability of the aggregated model and computation time is 

1720 50 splits. Shuffled splits are seeded by default for reproducibility. 

1721 Can also be a cross-validation generator. 

1722 

1723 param_grid : :obj:`dict` of :obj:`str` to sequence, or sequence of such. \ 

1724 default=None 

1725 The parameter grid to explore, as a dictionary mapping estimator 

1726 parameters to sequences of allowed values. 

1727 

1728 None or an empty dict signifies default parameters. 

1729 

1730 A sequence of dicts signifies a sequence of grids to search, and is 

1731 useful to avoid exploring parameter combinations that make no sense 

1732 or have no effect. See scikit-learn documentation for more information, 

1733 for example: https://scikit-learn.org/stable/modules/grid_search.html 

1734 

1735 clustering_percentile : :obj:`int`, :obj:`float`, \ 

1736 in closed interval [0, 100], \ 

1737 default=10 

1738 Used to perform a fast ReNA clustering on input data as a first step of 

1739 fit. It agglomerates similar features together to reduce their number 

1740 down to this percentile. ReNA is typically efficient for 

1741 cluster_percentile equal to 10. 

1742 

1743 screening_percentile : :obj:`int`, :obj:`float`, \ 

1744 in closed interval [0, 100], \ 

1745 default=20 

1746 The percentage of brain volume that will be kept with respect to a full 

1747 MNI template. In particular, if it is lower than 100, a univariate 

1748 feature selection based on the Anova F-value for the input data will be 

1749 performed. A float according to a percentile of the highest 

1750 scores. 

1751 

1752 scoring : :obj:`str`, callable or None, optional. default='roc_auc' 

1753 The scoring strategy to use. See the scikit-learn documentation at 

1754 https://scikit-learn.org/stable/modules/model_evaluation.html#the-scoring-parameter-defining-model-evaluation-rules 

1755 If callable, takes as arguments the fitted estimator, the 

1756 test data (X_test) and the test target (y_test) if y is 

1757 not None. 

1758 e.g. scorer(estimator, X_test, y_test) 

1759 

1760 For classification, valid entries are: 'accuracy', 'f1', 'precision', 

1761 'recall' or 'roc_auc'; default='roc_auc' 

1762 %(smoothing_fwhm)s 

1763 %(standardize)s 

1764 %(target_affine)s 

1765 %(target_shape)s 

1766 %(low_pass)s 

1767 %(high_pass)s 

1768 %(t_r)s 

1769 %(mask_strategy)s 

1770 

1771 .. note:: 

1772 This parameter will be ignored if a mask image is provided. 

1773 

1774 .. note:: 

1775 Depending on this value, the mask will be computed from 

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

1777 :func:`nilearn.masking.compute_epi_mask`, or 

1778 :func:`nilearn.masking.compute_brain_mask`. 

1779 

1780 Default='background'. 

1781 %(memory)s 

1782 %(memory_level)s 

1783 %(n_jobs)s 

1784 %(verbose0)s 

1785 

1786 References 

1787 ---------- 

1788 .. footbibliography:: 

1789 

1790 See Also 

1791 -------- 

1792 nilearn.decoding.Decoder: Classification strategies for Neuroimaging, 

1793 nilearn.decoding.FREMRegressor: State of the art regression pipeline 

1794 for Neuroimaging 

1795 

1796 """ 

1797 

1798 def __init__( 

1799 self, 

1800 estimator="svc", 

1801 mask=None, 

1802 cv=30, 

1803 param_grid=None, 

1804 clustering_percentile=10, 

1805 screening_percentile=20, 

1806 scoring="roc_auc", 

1807 smoothing_fwhm=None, 

1808 standardize=True, 

1809 target_affine=None, 

1810 target_shape=None, 

1811 mask_strategy="background", 

1812 low_pass=None, 

1813 high_pass=None, 

1814 t_r=None, 

1815 memory=None, 

1816 memory_level=0, 

1817 n_jobs=1, 

1818 verbose=0, 

1819 ): 

1820 super().__init__( 

1821 estimator=estimator, 

1822 mask=mask, 

1823 cv=cv, 

1824 param_grid=param_grid, 

1825 clustering_percentile=clustering_percentile, 

1826 screening_percentile=screening_percentile, 

1827 scoring=scoring, 

1828 smoothing_fwhm=smoothing_fwhm, 

1829 standardize=standardize, 

1830 target_affine=target_affine, 

1831 target_shape=target_shape, 

1832 mask_strategy=mask_strategy, 

1833 memory=memory, 

1834 is_classification=True, 

1835 memory_level=memory_level, 

1836 verbose=verbose, 

1837 n_jobs=n_jobs, 

1838 low_pass=low_pass, 

1839 high_pass=high_pass, 

1840 t_r=t_r, 

1841 ) 

1842 

1843 # TODO remove after sklearn>=1.6 

1844 self._estimator_type = "classifier" 

1845 

1846 def _more_tags(self): 

1847 """Return estimator tags. 

1848 

1849 TODO remove when bumping sklearn_version > 1.5 

1850 """ 

1851 return self.__sklearn_tags__() 

1852 

1853 def __sklearn_tags__(self): 

1854 """Return estimator tags. 

1855 

1856 See the sklearn documentation for more details on tags 

1857 https://scikit-learn.org/1.6/developers/develop.html#estimator-tags 

1858 """ 

1859 # TODO 

1860 # get rid of if block 

1861 # bumping sklearn_version > 1.5 

1862 # see https://github.com/scikit-learn/scikit-learn/pull/29677 

1863 tags = super().__sklearn_tags__() 

1864 if SKLEARN_LT_1_6: 

1865 return tags 

1866 

1867 from sklearn.utils import ClassifierTags 

1868 

1869 tags.estimator_type = "classifier" 

1870 tags.classifier_tags = ClassifierTags() 

1871 

1872 return tags