OSCR

Biological brain aging, cognitive-motor decline and vascular risk: a multivariate imaging analysis of 40,579 individuals.

Code ↔ Paper

3 matches between paragraphs of the paper and lines of its authors' code, computed by the harvester (lexical-v1). Click a colored paragraph or line to see its counterpart.

The 3 matches
  1. [1] § Materials and methods › Statistics › Partial least square correlation analysis ↔ pyls/base.py, lines 438–527 · score 0.77 · singular vector, standard error, bootstrap resampling, bootstrap ratio, replacement, weight
  2. [2] § Materials and methods › Statistics › Partial least square correlation analysis ↔ pyls/structures.py, lines 299–326 · score 0.68 · confidence intervals, singular vector, latent variable, permuting, resampling, correlation
  3. [3] § Results › Imaging markers of biological brain aging are associated with cognitive and motor function ↔ pyls/types/behavioral.py, lines 172–227 · score 0.59 · confidence interval, cross validation, bootstrap ratio, squares, scores, PLS

Paper

Loaded from Europe PMC by your browser, not stored by OSCR: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Python · 759 lines · 31 KB · GPL-2.0 · 1 match

  1. # -*- coding: utf-8 -*-
  2. import gc
  3. import warnings
  4. import numpy as np
  5. from sklearn.utils.validation import check_random_state
  6. from . import compute, structures, utils
  7. def gen_permsamp(groups, n_cond, n_perm, seed=None, verbose=True):
  8. """
  9. Generates permutation arrays for PLS permutation testing
  10. Parameters
  11. ----------
  12. groups : (G,) list
  13. List with number of subjects in each of `G` groups
  14. n_cond : int
  15. Number of conditions, for each subject. Default: 1
  16. n_perm : int
  17. Number of permutations for which to generate resampling arrays
  18. seed : {int, :obj:`numpy.random.RandomState`, None}, optional
  19. Seed for random number generation. Default: None
  20. verbose : bool, optional
  21. Whether to print status updates as permutations are generated.
  22. Default: True
  23. Returns
  24. -------
  25. permsamp : (S, P) `numpy.ndarray`
  26. Subject permutation arrays, where `S` is the number of subjects and `P`
  27. is the requested number of permutations (i.e., `P = n_perm`)
  28. """
  29. Y = utils.dummy_code(groups, n_cond)
  30. permsamp = np.zeros(shape=(len(Y), n_perm), dtype=int)
  31. subj_inds = np.arange(np.sum(groups), dtype=int)
  32. rs = check_random_state(seed)
  33. warned = False
  34. # calculate some variables for permuting conditions within subject
  35. # do this here to save on calculation time
  36. indices, grps = np.where(Y)
  37. grp_conds = np.split(indices, np.where(np.diff(grps))[0] + 1)
  38. to_permute = [np.vstack(grp_conds[i:i + n_cond]) for i in
  39. range(0, Y.shape[-1], n_cond)]
  40. splitinds = np.cumsum(groups)[:-1]
  41. check_grps = utils.dummy_code(groups).T.astype(bool)
  42. for i in utils.trange(n_perm, verbose=verbose, desc='Making permutations'):
  43. count, duplicated = 0, True
  44. while duplicated and count < 500:
  45. count, duplicated = count + 1, False
  46. # generate conditions permuted w/i subject
  47. inds = np.hstack([utils.permute_cols(i, seed=rs) for i
  48. in to_permute])
  49. # generate permutation of subjects across groups
  50. perm = rs.permutation(subj_inds)
  51. # confirm subjects *are* mixed across groups
  52. if len(groups) > 1:
  53. for grp in check_grps:
  54. if np.all(np.sort(perm[grp]) == subj_inds[grp]):
  55. duplicated = True
  56. # permute conditions w/i subjects across groups and stack
  57. perminds = np.hstack([f.flatten('F') for f in
  58. np.split(inds[:, perm].T, splitinds)])
  59. # make sure permuted indices are not a duplicate sequence
  60. dupe_seq = perminds[:, None] == permsamp[:, :i]
  61. if dupe_seq.all(axis=0).any():
  62. duplicated = True
  63. # if we broke out because we tried 500 permutations and couldn't
  64. # generate a new one, just warn that we're using duplicate
  65. # permutations and give up
  66. if count == 500 and not warned:
  67. warnings.warn('WARNING: Duplicate permutations used.')
  68. warned = True
  69. # store the permuted indices
  70. permsamp[:, i] = perminds
  71. return permsamp
  72. def gen_bootsamp(groups, n_cond, n_boot, seed=None, verbose=True):
  73. """
  74. Generates bootstrap arrays for PLS bootstrap resampling
  75. Parameters
  76. ----------
  77. groups : (G,) list
  78. List with number of subjects in each of `G` groups
  79. n_cond : int
  80. Number of conditions, for each subject. Default: 1
  81. n_boot : int
  82. Number of boostraps for which to generate resampling arrays
  83. seed : {int, :obj:`numpy.random.RandomState`, None}, optional
  84. Seed for random number generation. Default: None
  85. verbose : bool, optional
  86. Whether to print status updates as bootstrap samples are genereated.
  87. Default: True
  88. Returns
  89. -------
  90. bootsamp : (S, B) `numpy.ndarray`
  91. Subject bootstrap arrays, where `S` is the number of subjects and `B`
  92. is the requested number of bootstraps (i.e., `B = n_boot`)
  93. """
  94. Y = utils.dummy_code(groups, n_cond)
  95. bootsamp = np.zeros(shape=(len(Y), n_boot), dtype=int)
  96. subj_inds = np.arange(np.sum(groups), dtype=int)
  97. rs = check_random_state(seed)
  98. warned = False
  99. min_subj = int(np.ceil(Y.sum(axis=0).min() * 0.5))
  100. # calculate some variables for ensuring we resample with replacement
  101. # subjects across all their conditions. do this here to save on
  102. # calculation time
  103. indices, grps = np.where(Y)
  104. grp_conds = np.split(indices, np.where(np.diff(grps))[0] + 1)
  105. inds = np.hstack([np.vstack(grp_conds[i:i + n_cond]) for i
  106. in range(0, len(grp_conds), n_cond)])
  107. splitinds = np.cumsum(groups)[:-1]
  108. check_grps = utils.dummy_code(groups).T.astype(bool)
  109. for i in utils.trange(n_boot, verbose=verbose, desc='Making bootstraps'):
  110. count, duplicated = 0, True
  111. while duplicated and count < 500:
  112. count, duplicated = count + 1, False
  113. # empty container to store current bootstrap attempt
  114. boot = np.zeros(shape=(subj_inds.size), dtype=int)
  115. # iterate through and resample from w/i groups
  116. for grp in check_grps:
  117. curr_grp, all_same = subj_inds[grp], True
  118. while all_same:
  119. num_subj = curr_grp.size
  120. boot[curr_grp] = np.sort(rs.choice(curr_grp,
  121. size=num_subj,
  122. replace=True),
  123. axis=0)
  124. # make sure bootstrap has enough unique subjs
  125. if np.unique(boot[curr_grp]).size >= min_subj:
  126. all_same = False
  127. # resample subjects (with conditions) and stack groups
  128. bootinds = np.hstack([f.flatten('F') for f in
  129. np.split(inds[:, boot].T, splitinds)])
  130. # make sure bootstrap is not a duplicated sequence
  131. for grp in check_grps:
  132. curr_grp = subj_inds[grp]
  133. check = bootinds[curr_grp, None] == bootsamp[curr_grp, :i]
  134. if check.all(axis=0).any():
  135. duplicated = True
  136. # if we broke out because we tried 500 bootstraps and couldn't
  137. # generate a new one, just warn that we're using duplicate
  138. # bootstraps and give up
  139. if count == 500 and not warned:
  140. warnings.warn('WARNING: Duplicate bootstraps used.')
  141. warned = True
  142. # store the bootstrapped indices
  143. bootsamp[:, i] = bootinds
  144. return bootsamp
  145. def gen_splits(groups, n_cond, n_split, seed=None, test_size=0.5):
  146. """
  147. Generates splitting arrays for PLS split-half resampling and CV
  148. Parameters
  149. ----------
  150. groups : (G,) list
  151. List with number of subjects in each of `G` groups
  152. n_cond : int
  153. Number of conditions, for each subject. Default: 1
  154. n_split : int
  155. Number of splits for which to generate resampling arrays
  156. seed : {int, :obj:`numpy.random.RandomState`, None}, optional
  157. Seed for random number generation. Default: None
  158. test_size : (0, 1) float, optional
  159. Percent of subjects to include in the split halves. Default: 0.5
  160. Returns
  161. -------
  162. splitsamp : (S, I) `numpy.ndarray`
  163. Subject split arrays, where `S` is the number of subjects and `I`
  164. is the requested number of splits (i.e., `I = n_split`)
  165. """
  166. Y = utils.dummy_code(groups, n_cond)
  167. splitsamp = np.zeros(shape=(len(Y), n_split), dtype=bool)
  168. subj_inds = np.arange(np.sum(groups), dtype=int)
  169. rs = check_random_state(seed)
  170. warned = False
  171. # calculate some variables for permuting conditions within subject
  172. # do this here to save on calculation time
  173. indices, grps = np.where(Y)
  174. grp_conds = np.split(indices, np.where(np.diff(grps))[0] + 1)
  175. inds = np.hstack([np.vstack(grp_conds[i:i + n_cond]) for i
  176. in range(0, len(grp_conds), n_cond)])
  177. splitinds = np.cumsum(groups)[:-1]
  178. check_grps = utils.dummy_code(groups).T.astype(bool)
  179. for i in range(n_split):
  180. count, duplicated = 0, True
  181. while duplicated and count < 500:
  182. count, duplicated = count + 1, False
  183. # empty containter to store current split half attempt
  184. split = np.zeros(shape=(subj_inds.size), dtype=bool)
  185. # iterate through and split each group separately
  186. for grp in check_grps:
  187. curr_grp = subj_inds[grp]
  188. take = rs.choice([np.ceil, np.floor])
  189. num_subj = int(take(curr_grp.size * (1 - test_size)))
  190. splinds = rs.choice(curr_grp,
  191. size=num_subj,
  192. replace=False)
  193. split[splinds] = True
  194. # split subjects (with conditions) and stack groups
  195. half = np.hstack([f.flatten('F') for f in
  196. np.split(((inds + 1).astype(bool)
  197. * [split[None]]).T,
  198. splitinds)])
  199. # make sure split half is not a duplicated sequence
  200. dupe_seq = half[:, None] == splitsamp[:, :i]
  201. if dupe_seq.all(axis=0).any():
  202. duplicated = True
  203. if count == 500 and not warned:
  204. warnings.warn('WARNING: Duplicate split halves used.')
  205. warned = True
  206. splitsamp[:, i] = half
  207. return splitsamp
  208. class BasePLS():
  209. """
  210. Base PLS class to be subclassed
  211. Contains most of the math required for PLS, leaving a few functions for PLS
  212. subclasses to implement. This will not run without those implementations.
  213. Parameters
  214. ----------
  215. {input_matrix}
  216. {groups}
  217. {conditions}
  218. **kwargs : optional
  219. Additional key-value pairs; see :obj:`pyls.structures.PLSInputs` for
  220. more info
  221. References
  222. ----------
  223. {references}
  224. """.format(**structures._pls_input_docs)
  225. def __init__(self, X, Y=None, groups=None, n_cond=1, **kwargs):
  226. # if groups aren't provided or are provided wrong, fix them
  227. if groups is None:
  228. groups = [len(X) // n_cond]
  229. elif not isinstance(groups, (list, np.ndarray)):
  230. groups = [groups]
  231. # coerce groups to integers
  232. groups = [int(g) for g in groups]
  233. # check that data matrices and groups + n_cond inputs jibe
  234. n_samples = sum([g * n_cond for g in groups])
  235. if len(X) != n_samples:
  236. raise ValueError('Number of samples specified by `groups` and '
  237. '`n_cond` does not match number of samples in '
  238. 'input array(s).\n'
  239. ' EXPECTED: {}\n'
  240. ' ACTUAL: {} (groups: {} * n_cond: {})'
  241. .format(len(X), n_samples, groups, n_cond))
  242. if Y is not None and len(X) != len(Y):
  243. raise ValueError('Provided `X` and `Y` matrices must have the '
  244. 'same number of samples. Provided matrices '
  245. 'differed: X: {}, Y: {}'.format(len(X), len(Y)))
  246. self.inputs = structures.PLSInputs(X=X, Y=Y, groups=groups,
  247. n_cond=n_cond, **kwargs)
  248. # store dummy-coded array of groups / conditions (save on computation)
  249. self.dummy = utils.dummy_code(groups, n_cond)
  250. self.rs = check_random_state(self.inputs.get('seed'))
  251. # check for parallel processing desire
  252. n_proc = self.inputs.get('n_proc')
  253. if n_proc is not None and n_proc != 1 and not utils.joblib_avail:
  254. self.inputs.n_proc = None
  255. warnings.warn('Setting n_proc > 1 requires the joblib module. '
  256. 'Considering installing joblib and re-running this '
  257. 'if you would like parallelization. Resetting '
  258. 'n_proc to 1 for now.')
  259. def gen_covcorr(self, X, Y, groups=None):
  260. """
  261. Should generate cross-covariance array to be used in `self._svd()`
  262. Must accept the listed parameters and return one array
  263. Parameters
  264. ----------
  265. X : (S, B) array_like
  266. Input data matrix, where `S` is observations and `B` is features
  267. Y : (S, T) array_like
  268. Input data matrix, where `S` is observations and `T` is features
  269. groups : (G,) array_like
  270. Array with number of subjects in each of `G` groups
  271. Returns
  272. -------
  273. crosscov : np.ndarray
  274. Covariance array for decomposition
  275. """
  276. raise NotImplementedError
  277. def gen_distrib(self, X, Y, groups=None, original=None):
  278. """
  279. Should generate behavioral correlations or contrast for bootstrap
  280. Parameters
  281. ----------
  282. X : (S, B) array_like
  283. Input data matrix, where `S` is observations and `B` is features
  284. Y : (S, T) array_like
  285. Input data matrix, where `S` is observations and `T` is features
  286. groups : (S, J) array_like
  287. Dummy coded array, where `S` is observations and `J` corresponds to
  288. the number of different groups x conditions represented in `X` and
  289. `Y`. A value of 1 indicates that an observation belongs to a
  290. specific group or condition
  291. Returns
  292. -------
  293. distrib : (T, L)
  294. Behavioral correlations or contrast for single bootstrap resample
  295. """
  296. raise NotImplementedError
  297. def run_pls(self, X, Y):
  298. """
  299. Runs PLS analysis
  300. Parameters
  301. ----------
  302. X : (S, B) array_like
  303. Input data matrix, where `S` is observations and `B` is features
  304. Y : (S, T) array_like
  305. Input data matrix, where `S` is observations and `T` is features
  306. Returns
  307. -------
  308. results : :obj:`pyls.structures.PLSResults`
  309. Results of PLS (not including PLS type-specific outputs)
  310. """
  311. # initate results structure
  312. self.res = res = structures.PLSResults(inputs=self.inputs)
  313. # get original singular vectors / values
  314. res['x_weights'], res['singvals'], res['y_weights'] = \
  315. self.svd(X, Y, seed=self.rs)
  316. res['x_scores'] = X @ res['x_weights']
  317. if self.inputs.n_perm > 0:
  318. # compute permutations and get statistical significance of LVs
  319. d_perm, ucorrs, vcorrs = self.permutation(X, Y, seed=self.rs)
  320. res['permres']['pvals'] = compute.perm_sig(res['singvals'], d_perm)
  321. res['permres']['permsamples'] = self.permsamp
  322. if self.inputs.n_split is not None:
  323. # get ucorr / vcorr (via split half resampling) for original,
  324. # unpermuted `X` and `Y` arrays
  325. di = np.linalg.inv(res['singvals'])
  326. orig_ucorr, orig_vcorr = self.split_half(X, Y,
  327. res['x_weights'] @ di,
  328. res['y_weights'] @ di,
  329. seed=self.rs)
  330. # get p-values for ucorr/vcorr
  331. ucorr_prob = compute.perm_sig(np.diag(orig_ucorr), ucorrs)
  332. vcorr_prob = compute.perm_sig(np.diag(orig_vcorr), vcorrs)
  333. # get confidence intervals for ucorr/vcorr
  334. ucorr_ll, ucorr_ul = compute.boot_ci(ucorrs, ci=self.inputs.ci)
  335. vcorr_ll, vcorr_ul = compute.boot_ci(vcorrs, ci=self.inputs.ci)
  336. # update results object with split-half resampling results
  337. res['splitres'].update(dict(ucorr=orig_ucorr,
  338. vcorr=orig_vcorr,
  339. ucorr_pvals=ucorr_prob,
  340. vcorr_pvals=vcorr_prob,
  341. ucorr_lolim=ucorr_ll,
  342. vcorr_lolim=vcorr_ll,
  343. ucorr_uplim=ucorr_ul,
  344. vcorr_uplim=vcorr_ul))
  345. return res
  346. def svd(self, X, Y, groups=None, seed=None):
  347. """
  348. Runs SVD on cross-covariance matrix computed from `X` and `Y`
  349. Parameters
  350. ----------
  351. X : (S, B) array_like
  352. Input data matrix, where `S` is observations and `B` is features
  353. Y : (S, T) array_like
  354. Input data matrix, where `S` is observations and `T` is features
  355. groups : (S, J) array_like
  356. Dummy coded array, where `S` is observations and `J` corresponds to
  357. the number of different groups x conditions represented in `X` and
  358. `Y`. A value of 1 indicates that an observation belongs to a
  359. specific group or condition
  360. seed : {int, :obj:`numpy.random.RandomState`, None}, optional
  361. Seed for random number generation. Default: None
  362. Returns
  363. -------
  364. U : (B, L) `numpy.ndarray`
  365. Left singular vectors from singular value decomposition
  366. d : (L, L) `numpy.ndarray`
  367. Diagonal array of singular values from singular value decomposition
  368. V : (J, L) `numpy.ndarray`
  369. Right singular vectors from singular value decomposition
  370. """
  371. # make dummy-coded grouping array if not provided
  372. if groups is None:
  373. groups = utils.dummy_code(self.inputs.groups, self.inputs.n_cond)
  374. # generate cross-covariance matrix and determine # of components
  375. crosscov = self.gen_covcorr(X, Y, groups=groups)
  376. U, d, V = compute.svd(crosscov, seed=seed)
  377. return U, d, V
  378. def bootstrap(self, X, Y, seed=None):
  379. """
  380. Bootstraps `X` and `Y` (w/replacement) and recomputes SVD
  381. Parameters
  382. ----------
  383. X : (S, B) array_like
  384. Input data matrix, where `S` is observations and `B` is features
  385. Y : (S, T) array_like
  386. Input data matrix, where `S` is observations and `T` is features
  387. seed : {int, :obj:`numpy.random.RandomState`, None}, optional
  388. Seed for random number generation. Default: None
  389. Returns
  390. -------
  391. distrib : (T, L) numpy.ndarray
  392. Either behavioral correlations or group x condition contrast;
  393. depends on PLS type
  394. u_sum : (B, L) numpy.ndarray
  395. Sum of the left singular vectors across all bootstraps
  396. u_square : (B, L) numpy.ndarray
  397. Sum of the squared left singular vectors across all bootstraps
  398. """
  399. # generate bootstrap resampled indices (unless already provided)
  400. self.bootsamp = self.inputs.get('bootsamples', None)
  401. if self.bootsamp is None:
  402. self.bootsamp = gen_bootsamp(self.inputs.groups,
  403. self.inputs.n_cond,
  404. self.inputs.n_boot,
  405. seed=seed,
  406. verbose=self.inputs.verbose)
  407. # make empty arrays to store bootstrapped singular vectors
  408. # these will be used to calculate the standard error later on for
  409. # creation of bootstrap ratios
  410. u_sum = np.zeros_like(self.res['x_weights'])
  411. u_square = np.zeros_like(self.res['x_weights'])
  412. # `distrib` corresponds either to the behavioral correlations (if
  413. # running a behavioral PLS) or to the group/condition contrast (if
  414. # running a mean-centered PLS); we'll just extend it and then stack
  415. # all the individual matrices together later (they're quite small so we
  416. # don't need to be too worried about memory usage, here)
  417. distrib = []
  418. # determine the number of bootstraps we'll run each iteration
  419. iters = 1 if self.inputs.n_proc is None else self.inputs.n_proc
  420. gen = utils.trange(self.inputs.n_boot, verbose=self.inputs.verbose,
  421. desc='Running bootstraps')
  422. with utils.get_par_func(self.inputs.n_proc,
  423. self.__class__._single_boot) as (par, func):
  424. boots = 0
  425. while boots < self.inputs.n_boot:
  426. # determine number of bootstraps to run this round
  427. # we don't want to overshoot the requested number, so make
  428. # sure to cut it off if that's what wold happen
  429. top = boots + iters
  430. if top >= self.inputs.n_boot:
  431. top = self.inputs.n_boot
  432. # run the bootstraps
  433. d, usu = zip(*par(func(self, X=X, Y=Y,
  434. inds=self.bootsamp[..., i],
  435. groups=self.dummy,
  436. original=self.res['x_weights'],
  437. seed=i)
  438. for i in range(boots, top)))
  439. # sum bootstrapped singular vectors and store
  440. u_sum += np.sum(usu, axis=0)
  441. u_square += np.sum(np.square(usu), axis=0)
  442. distrib.extend(d)
  443. # force garbage collection
  444. # this is only really needed when parallelizing bootstraps
  445. # the `usu` variable can get REALLY GIANT if either `X` or `Y`
  446. # is large and `n_proc` is > 1, so we really don't want to keep
  447. # it around for any longer than absolutely necessary
  448. if self.inputs.n_proc is not None:
  449. del usu
  450. gc.collect()
  451. # update progress bar and # of bootstraps already run
  452. gen.update(top - boots)
  453. boots = top
  454. gen.close()
  455. return np.stack(distrib, axis=-1), u_sum, u_square
  456. def _single_boot(self, X, Y, inds, groups=None, original=None, seed=None):
  457. """
  458. Bootstraps `X` and `Y` (w/replacement) and recomputes SVD
  459. Parameters
  460. ----------
  461. X : (S, B) array_like
  462. Input data matrix, where `S` is observations and `B` is features
  463. Y : (S, T) array_like
  464. Input data matrix, where `S` is observations and `T` is features
  465. groups : (S, J) array_like
  466. Dummy coded input array, where `S` is observations and `J`
  467. corresponds to the number of different groups x conditions. A value
  468. of 1 indicates that an observation belongs to a specific group or
  469. condition.
  470. original : (B, L) array_like
  471. Left singular vector from original decomposition of `X` and `Y`.
  472. Used to perform Procrustes rotation on permuted singular vectors
  473. seed : {int, :obj:`numpy.random.RandomState`, None}, optional
  474. Seed for random number generation. Default: None
  475. Returns
  476. -------
  477. distrib : np.ndarray
  478. Either behavioral correlations or contrast, depending on PLS type;
  479. generated with self.gen_distrib() which should be specified by the
  480. PLS subclass
  481. U_sum : (B, L) array_like
  482. Left singular vectors from decomposition of bootstrap resampled `X`
  483. and `Y`
  484. """
  485. # make sure we have original (non-bootstrapped) singular vectors
  486. # these are required for the procrustes rotation to ensure our
  487. # singular vectors are all in the same orientation
  488. if original is None:
  489. original = self.svd(X, Y, groups=groups, seed=seed)[0]
  490. # perform SVD of bootstrapped arrays and rotate left singular vectors
  491. U, d = self.svd(X[inds], Y[inds], groups=groups, seed=seed)[:-1]
  492. U_boot = compute.procrustes(original, U, d)
  493. # get contrast / behavcorrs (this function should be specified by the
  494. # subclass)
  495. distrib = self.gen_distrib(X[inds], Y[inds], original, groups)
  496. return distrib, U_boot
  497. def make_permutation(self, X, Y, perminds):
  498. """
  499. Permutes `Y` according to `perminds`, leaving `X` un-permuted
  500. Parameters
  501. ----------
  502. X : (S, B) array_like
  503. Input data matrix, where `S` is observations and `B` is features
  504. Y : (S, T) array_like
  505. Input data matrix, where `S` is observations and `T` is features
  506. perminds : (S,) array_like
  507. Array by which to permute `Y`
  508. Returns
  509. -------
  510. Xp : (S, B) array_like
  511. Identical to `X`
  512. Yp : (S, T) array_like
  513. `Y`, permuted according to `perminds`
  514. """
  515. return X, Y[perminds]
  516. def permutation(self, X, Y, seed=None):
  517. """
  518. Permutes `X` (w/o replacement) and recomputes SVD
  519. Parameters
  520. ----------
  521. X : (S, B) array_like
  522. Input data matrix, where `S` is observations and `B` is features
  523. Y : (S, T) array_like
  524. Input data matrix, where `S` is observations and `T` is features
  525. seed : {int, :obj:`numpy.random.RandomState`, None}, optional
  526. Seed for random number generation. Default: None
  527. Returns
  528. -------
  529. d_perm : (L, P) `numpy.ndarray`
  530. Permuted singular values, where `L` is the number of singular
  531. values and `P` is the number of permutations
  532. ucorrs : (L, P) `numpy.ndarray`
  533. Split-half correlations of left singular values. Only set if
  534. `self.inputs.n_split != 0`
  535. vcorrs : (L, P) `numpy.ndarray`
  536. Split-half correlations of right singular values. Only set if
  537. `self.inputs.n_split != 0`
  538. """
  539. # generate permuted indices (unless already provided)
  540. self.permsamp = self.inputs.get('permsamples')
  541. if self.permsamp is None:
  542. self.permsamp = gen_permsamp(self.inputs.groups,
  543. self.inputs.n_cond,
  544. self.inputs.n_perm,
  545. seed=seed,
  546. verbose=self.inputs.verbose)
  547. # get permuted values (parallelizing as requested)
  548. gen = utils.trange(self.inputs.n_perm, verbose=self.inputs.verbose,
  549. desc='Running permutations')
  550. with utils.get_par_func(self.inputs.n_proc,
  551. self.__class__._single_perm) as (par, func):
  552. out = par(func(self, X=X, Y=Y, inds=self.permsamp[:, i],
  553. groups=self.dummy, original=self.res['y_weights'],
  554. seed=i)
  555. for i in gen)
  556. d_perm, ucorrs, vcorrs = [np.stack(o, axis=-1) for o in zip(*out)]
  557. return d_perm, ucorrs, vcorrs
  558. def _single_perm(self, X, Y, inds, groups=None, original=None, seed=None):
  559. """
  560. Permutes `X` (w/o replacement) and recomputes SVD
  561. Parameters
  562. ----------
  563. X : (S, B) array_like
  564. Input data matrix, where `S` is observations and `B` is features
  565. Y : (S, T) array_like
  566. Input data matrix, where `S` is observations and `T` is features
  567. inds : (S,) array_like
  568. Permutation resampling array
  569. original : (J, L) array_like
  570. Right singular vector from original decomposition of `X` and `Y`.
  571. Used to perform Procrustes rotation on permuted singular values,
  572. if desired
  573. seed : {int, :obj:`numpy.random.RandomState`, None}, optional
  574. Seed for random number generation. Default: None
  575. Returns
  576. -------
  577. d_perm : (L,) `numpy.ndarray`
  578. Permuted singular values, where `L` is the number of singular
  579. values
  580. ucorrs : (L,) `numpy.ndarray`
  581. Split-half correlations of left singular values. Only set if
  582. `self.inputs.n_split != 0`
  583. vcorrs : (L,) `numpy.ndarray`
  584. Split-half correlations of right singular values. Only set if
  585. `self.inputs.n_split != 0`
  586. """
  587. # calculate SVD of permuted matrices
  588. Xp, Yp = self.make_permutation(X, Y, inds)
  589. U, d, V = self.svd(Xp, Yp, groups=groups, seed=seed)
  590. # optionally get rotated/rescaled singular values
  591. if self.inputs.rotate:
  592. if original is None:
  593. original = self.svd(X, Y, groups=groups, seed=seed)[-1]
  594. ssd = np.sqrt(np.sum(compute.procrustes(original, V, d)**2,
  595. axis=0))
  596. else:
  597. ssd = np.diag(d)
  598. # get ucorr/vcorr if split-half resampling requested
  599. if self.inputs.n_split is not None:
  600. di = np.linalg.inv(d)
  601. ucorr, vcorr = self.split_half(Xp, Yp, U @ di, V @ di,
  602. groups=groups, seed=seed)
  603. else:
  604. ucorr, vcorr = None, None
  605. return ssd, ucorr, vcorr
  606. def split_half(self, X, Y, ud=None, vd=None, groups=None, seed=None):
  607. """
  608. Parameters
  609. ----------
  610. X : (S, B) array_like
  611. Input data matrix, where `S` is observations and `B` is features
  612. Y : (S, T) array_like
  613. Input data matrix, where `S` is observations and `T` is features
  614. ud : (B, L) array_like
  615. Left singular vectors, scaled by singular values
  616. vd : (J, L) array_like
  617. Right singular vectors, scaled by singular values
  618. seed : {int, :obj:`numpy.random.RandomState`, None}, optional
  619. Seed for random number generation. Default: None
  620. Returns
  621. -------
  622. ucorr : (L,) `numpy.ndarray`
  623. Average correlation of left singular vectors across split-halves
  624. vcorr : (L,) `numpy.ndarray`
  625. Average correlation of right singular vectors across split-halves
  626. """
  627. # generate splits
  628. splitsamp = gen_splits(self.inputs.groups,
  629. self.inputs.n_cond,
  630. self.inputs.n_split,
  631. seed=seed,
  632. test_size=0.5).astype(bool)
  633. # make dummy-coded grouping array if not provided
  634. if groups is None:
  635. groups = utils.dummy_code(self.inputs.groups, self.inputs.n_cond)
  636. # generate original singular vectors if not provided
  637. if ud is None or vd is None:
  638. U, d, V = self.svd(X, Y, groups=groups, seed=seed)
  639. di = np.linalg.inv(d)
  640. ud, vd = U @ di, V @ di
  641. # empty arrays to hold split-half correlations
  642. ucorr = np.zeros(shape=(ud.shape[-1], self.inputs.n_split))
  643. vcorr = np.zeros(shape=(vd.shape[-1], self.inputs.n_split))
  644. for i in range(self.inputs.n_split):
  645. # calculate cross-covariance matrix for both splits
  646. spl = splitsamp[:, i]
  647. D1 = self.gen_covcorr(X[spl], Y[spl], groups=groups[spl])
  648. D2 = self.gen_covcorr(X[~spl], Y[~spl], groups=groups[~spl])
  649. # project cross-covariance matrices onto original SVD to obtain
  650. # left & right singular vector and correlate between split halves
  651. ucorr[:, i] = compute.efficient_corr(D1.T @ vd, D2.T @ vd)
  652. vcorr[:, i] = compute.efficient_corr(D1 @ ud, D2 @ ud)
  653. # return average correlations for singular vectors across `n_split`
  654. return np.mean(ucorr, axis=-1), np.mean(vcorr, axis=-1)

base.py at commit d8a19d5, under GPL-2.0 · at the source

Overview

Authors: Marvin Petersen1, Moritz A. Link1, Carola Mayer1, Felix L. Nägele1, Maximilian Schell1, Märit Jensen1, Eckhard Schlemm1, Jens Fiehler2, Jürgen Gallinat3, Simone Kühn3, Raphael Twerenbold4,5,6,7, Amir Omidvarnia8,9, Felix Hoffstaedter8,9, Kaustubh R. Patil8,9, Simon B. Eickhoff8,9, Götz Thomalla1, Bastian Cheng1
  1. Department of Neurology, University Medical Center Hamburg-Eppendorf, Hamburg, Germany
  2. Department of Diagnostic and Interventional Neuroradiology, University Medical Center Hamburg-Eppendorf, Hamburg, Germany
  3. Department of Psychiatry and Psychotherapy, University Medical Center Hamburg-Eppendorf, Hamburg, Germany
  4. Department of General and Interventional Cardiology, University Heart and Vascular Center, Hamburg, Germany
  5. Epidemiological Study Center, University Medical Center Hamburg-Eppendorf, Hamburg, Germany
  6. German Center for Cardiovascular Research (DZHK), Partner Site Hamburg/Kiel/Luebeck, Hamburg, Germany
  7. University Center of Cardiovascular Science, University Heart and Vascular Center, Hamburg, Germany
  8. Faculty of Medicine, Institute of Systems Neuroscience, Heinrich Heine University Düsseldorf, Düsseldorf, Germany
  9. Institute of Neuroscience and Medicine, Brain and Behaviour (INM-7), Research Center Jülich, Jülich, Germany
Journal: Frontiers in aging neuroscience, volume 18, article 1789408
Dates: received 16 January 2026; accepted 10 April 2026; published online 1 May 2026
Type: Research article · Language: English
License: CC BY
Identifiers: DOI 10.3389/fnagi.2026.1789408 · PMID 42147461 · PMCID PMC13176138 · OpenAlex W7159953708
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: structural MRI / diffusion (modality), human (organism), stroke (population), cognitive (subfield)
Methods: Connectivity, Statistics, Smoothing, state filtering, decompositions, Machine learning, Preprocessing, fMRI & imaging
Keywords: brain age, cardiovascular risk, cerebral small vessel disease, cognitive function, motor function, neuroimaging, white matter hyperintensities
Topic: Dementia and Cognitive Impairment Research (Psychiatry and Mental health, Medicine), according to OpenAlex
Citations: not cited yet (Europe PMC); 54 references in the paper

Abstract

Introduction: Age-related declines in cognitive and motor functions show highly variable trajectories. To better understand the underlying mechanisms, we investigated multivariate associative effects between modifiable vascular risk factors, biological brain aging, cognitive, and motor performance in 40,579 individuals from the population-based UK Biobank and Hamburg City Health Study.

Methods: We employed partial least squares correlation analysis (PLS) to model associations between multi-domain cognitive and motor test scores and three distinct MRI-derived markers of biological brain aging: relative brain age (from morphometric brain imaging), white matter hyperintensity load, and peak width of skeletonized mean diffusivity. Furthermore, we conducted mediation analyses to assess if these markers mediate the impact of vascular risk on functional decline.

Results: PLS identified a single dominant latent dimension explaining 94.7% of the shared variance between neuroimaging and behavior. This dimension linked higher biological brain aging markers – with relative brain age showing the strongest contribution – to poorer cognitive and motor performance, particularly in executive function and processing speed. Mediation analysis revealed that biological brain aging acts as a partial mediator for the negative effects of blood pressure, glucose, waist-hip ratio, and smoking load on cognitive and motor function. Notably, this mediating effect was not observed for cholesterol levels. These results were consistent across both cohorts.

Discussion: Our study illustrates the associative interplay between vascular health, biological brain aging, and cognitive and motor performance, emphasizing the need for preventive strategies to maintain late-life independence in aging populations.

Reproduced under the paper's license (CC BY), from the paper cited above.

Repository

Its files are read in the Code ↔ Paper reader above, with 3 matches between paragraphs and lines of code.

rmarkello/pyls

License: GPL-2.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: d8a19d564cc5804249527b68c937df3a5fd8c7cc, 4 November 2019
Languages: Python (32)
Size: 65 files, 32 scripts
Software Heritage: archived
Found in: the end of the paper
Holds: README, license file, environment (requirements.txt, setup.cfg, setup.py, docs/requirements.txt), tests, continuous integration, documentation
Not found: CITATION.cff
Tools: NumPy (18 files), scikit-learn (4 files), h5py (2 files), pandas (2 files), SciPy (2 files), seaborn (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
34 files

Tracing map

Proposed by the machine: these links were found in the paper and verified at the source, without human review. The map will receive a Zenodo DOI once one of the paper's authors has validated it with their ORCID.

What the map holds:

  • 1 repository of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 32 scripts, each with its path and the digest of its content;
  • 3 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

Its JSON (tracing-map.json) is deposited on Zenodo with its DOI once the map is validated.

Data

Datasets cited

Data availability statement

UK Biobank data can be obtained via its standardized data access procedure (https://www.ukbiobank.ac.uk/). HCHS participant data used in this analysis is not publicly available for privacy reasons, but access can be established via request to the HCHS steering committee.

Reproduced under the paper's license (CC BY), from the paper cited above.

Versions

The history of this record: each version stored by the harvester or made by a correction of its authors or of the maintainers of its code, and what changed in its facts. The texts of the paper (its abstract, its availability statements) are not part of it; versions that changed only those are not listed.

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, volume, pages, dates, 17 authors, 7 keywords, 54 references.

Cite

This paper

Petersen, M., Link, M. A., Mayer, C., Nägele, F. L., Schell, M., Jensen, M., Schlemm, E., Fiehler, J., Gallinat, J., Kühn, S., Twerenbold, R., Omidvarnia, A., Hoffstaedter, F., Patil, K. R., Eickhoff, S. B., Thomalla, G., & Cheng, B. (2026). Biological brain aging, cognitive-motor decline and vascular risk: a multivariate imaging analysis of 40,579 individuals. Frontiers in aging neuroscience, 18, 1789408. https://doi.org/10.3389/fnagi.2026.1789408

BibTeX

@article{petersen2026biological,
author = {Petersen, Marvin and Link, Moritz A. and Mayer, Carola and Nägele, Felix L. and Schell, Maximilian and Jensen, Märit and Schlemm, Eckhard and Fiehler, Jens and Gallinat, Jürgen and Kühn, Simone and Twerenbold, Raphael and Omidvarnia, Amir and Hoffstaedter, Felix and Patil, Kaustubh R. and Eickhoff, Simon B. and Thomalla, Götz and Cheng, Bastian},
title = {{Biological brain aging, cognitive-motor decline and vascular risk: a multivariate imaging analysis of 40,579 individuals}},
journal = {Frontiers in aging neuroscience},
year = {2026},
month = may,
volume = {18},
pages = {1789408},
publisher = {Frontiers Media SA},
issn = {1663-4365},
doi = {10.3389/fnagi.2026.1789408},
url = {https://doi.org/10.3389/fnagi.2026.1789408},
pmid = {42147461},
pmcid = {PMC13176138}
}

RIS

TY - JOUR
AU - Petersen, Marvin
AU - Link, Moritz A.
AU - Mayer, Carola
AU - Nägele, Felix L.
AU - Schell, Maximilian
AU - Jensen, Märit
AU - Schlemm, Eckhard
AU - Fiehler, Jens
AU - Gallinat, Jürgen
AU - Kühn, Simone
AU - Twerenbold, Raphael
AU - Omidvarnia, Amir
AU - Hoffstaedter, Felix
AU - Patil, Kaustubh R.
AU - Eickhoff, Simon B.
AU - Thomalla, Götz
AU - Cheng, Bastian
TI - Biological brain aging, cognitive-motor decline and vascular risk: a multivariate imaging analysis of 40,579 individuals
T2 - Frontiers in aging neuroscience
J2 - Front Aging Neurosci
PY - 2026
DA - 2026/05/01
VL - 18
SP - 1789408
SN - 1663-4365
PB - Frontiers Media SA
DO - 10.3389/fnagi.2026.1789408
UR - https://doi.org/10.3389/fnagi.2026.1789408
LA - en
ER -

CSL-JSON

{
"id": "10.3389/fnagi.2026.1789408",
"type": "article-journal",
"title": "Biological brain aging, cognitive-motor decline and vascular risk: a multivariate imaging analysis of 40,579 individuals",
"container-title": "Frontiers in aging neuroscience",
"author": [
{
"family": "Petersen",
"given": "Marvin"
},
{
"family": "Link",
"given": "Moritz A."
},
{
"family": "Mayer",
"given": "Carola"
},
{
"family": "Nägele",
"given": "Felix L."
},
{
"family": "Schell",
"given": "Maximilian"
},
{
"family": "Jensen",
"given": "Märit"
},
{
"family": "Schlemm",
"given": "Eckhard"
},
{
"family": "Fiehler",
"given": "Jens"
},
{
"family": "Gallinat",
"given": "Jürgen"
},
{
"family": "Kühn",
"given": "Simone"
},
{
"family": "Twerenbold",
"given": "Raphael"
},
{
"family": "Omidvarnia",
"given": "Amir"
},
{
"family": "Hoffstaedter",
"given": "Felix"
},
{
"family": "Patil",
"given": "Kaustubh R."
},
{
"family": "Eickhoff",
"given": "Simon B."
},
{
"family": "Thomalla",
"given": "Götz"
},
{
"family": "Cheng",
"given": "Bastian"
}
],
"container-title-short": "Front Aging Neurosci",
"volume": "18",
"page": "1789408",
"DOI": "10.3389/fnagi.2026.1789408",
"PMID": "42147461",
"PMCID": "PMC13176138",
"ISSN": "1663-4365",
"publisher": "Frontiers Media SA",
"URL": "https://doi.org/10.3389/fnagi.2026.1789408",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
1
]
]
}
}

The tracing map gets a citation of its own once an author has validated it and it has a DOI.

Similar papers

The papers with a page that share the most with this one: the tools found in their code, their categories, datasets, cited references and authors, the rarest counting most.

[1] doi:10.1371/journal.pbio.3003856 [code]
Aging and metabolism contribute separately to brain-body health.
Journal: PLoS biology
In common: seaborn, scikit-learn, pandas, 2 other tools, ukbiobank.ac.uk/media/0xsbmfmw, structural MRI / diffusion, 6 references
[2] doi:10.1038/s41467-026-71271-9 [code]
Exposome-wide patterns predict brain health in aging.
Journal: Nature communications
In common: seaborn, scikit-learn, pandas, 2 other tools, 5 references
[3] doi:10.7554/elife.108109 [code]
Multimodal MRI marker of cognition explains the association between cognition and mental health in the UK Biobank.
Journal: eLife
In common: seaborn, scikit-learn, pandas, 2 other tools, cognitive, structural MRI / diffusion, 5 references
[4] doi:10.1016/j.ebiom.2026.106312 [code]
Subgingival microbiota composition is associated with brain health in the general population-the PAROMIND study.
Journal: EBioMedicine
In common: scikit-learn, pandas, SciPy, 1 other tool, cognitive, 4 references
[5] doi:10.3389/frai.2026.1771088 [code]
Few-shot deployment of pretrained MRI transformers in brain imaging tasks.
Journal: Frontiers in artificial intelligence
In common: h5py, seaborn, scikit-learn, 3 other tools, structural MRI / diffusion, 4 references
[6] doi:10.1038/s41514-026-00456-9 [code]
Exploring the link between body physiology and cognition: the role of the brain and aging.
Journal: npj aging
In common: seaborn, scikit-learn, pandas, 2 other tools, cognitive, 4 references
[7] doi:10.1186/s40708-026-00316-y [code]
Generalizable and explainable deep learning for brain MRI: a multi-cohort evaluation of 3D architectures for age and sex prediction.
Journal: Brain informatics
In common: seaborn, scikit-learn, pandas, 2 other tools, structural MRI / diffusion, 3 references
[8] doi:10.1162/imag.a.1242 [code]
Stable individual differences dominate adult brain volume variation until later life.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: seaborn, pandas, SciPy, 1 other tool, structural MRI / diffusion, 4 references
[9] doi:10.1038/s41593-026-02359-0 [code]
The cross-site reproducibility of MRI morphometric phenotypes in psychiatric disorders.
Journal: Nature neuroscience
In common: scikit-learn, pandas, SciPy, 1 other tool, structural MRI / diffusion, 5 references
[10] doi:10.1016/j.isci.2026.117180 [code]
Developmental changes in similarity between neural representations of mental arithmetic and artificial neural networks.
Journal: iScience
In common: h5py, seaborn, scikit-learn, 3 other tools, 3 references

Contribute

The authors of this paper can claim it, correct its record and validate its tracing map, and the maintainers of its code (its owner, or a public member of its organization) correct what it says of their repository; anyone signed in can ask for its removal. Every request goes to OSCR's own machine, which answers it; your account page follows them.

Sign in with ORCID to claim this paper as one of its authors, correct its record or validate its tracing map: when the paper's metadata lists your ORCID iD, you are recognized at once. Maintainers of its code: sign in with GitHub, then claim the repository on your account page.

Request its removal

To ask OSCR to remove this record, the copies of its authors' scripts or its tracing map, use the removal request page: signed in, you say who you are, what to remove and why, then review and confirm the request. Published rules decide every request (how).

Discussion, reproductions, activity

Discussion: questions and error reports about this paper and its code, from signed-in readers and its authors. It opens with sign-in.

Reproductions: reports from readers who ran the authors' code: what they reproduced, with which environment, commit and data. It opens with sign-in.

Activity: what happens around this paper: new versions of its record, its map's validation, discussions and reproductions. It opens with sign-in.