OSCR

Brain network dynamics reflect psychiatric illness status and transdiagnostic symptom profiles across health and disease.

Code ↔ Paper

12 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 12 matches
  1. [1] § Methods › Dimensional symptom analysis ↔ Fingerprints_Cocuzza.py, lines 698–750 · score 0.92 · score_samples, log likelihood, nbClust, distance matrix, linkage, scikit-learn
  2. [2] § Methods › Data collection › Study outline ↔ Data_Splitting_Cocuzza.py, lines 213–275 · score 0.91 · depression NOS, social anxiety, eating disorder, panic disorder, OCD, cyclothymia
  3. [3] § Methods › Dimensional symptom analysis ↔ Fingerprints_Cocuzza.py, lines 89–222 · score 0.83 · childhood trauma questionnaire, raw score, redundancies, subscale, curated, inflated
  4. [4] § Methods › Dimensional symptom analysis ↔ Fingerprints_Cocuzza.py, lines 332–415 · score 0.83 · Anderson Darling, Kolmogorov Smirnov, Shapiro Wilk, Gaussianity, zero, transformations
  5. [5] § Methods › Dimensional symptom analysis ↔ Fingerprints_Cocuzza.py, lines 473–597 · score 0.76 · miss forest, min max normalization, Random forest, imputed, space, binarizing
  6. [6] § Methods › Brain network dynamics analysis › Non-negative matrix factorization (NMF) ↔ NMF_Cocuzza.py, lines 223–274 · score 0.75 · feasibility, scikit-learn, computational, divergence, solver, evenly
  7. [7] § Results › Brain network dynamics are flattened in psychiatric illness ↔ NMF_Cocuzza.py, lines 513–581 · score 0.73 · encoding matrix, features matrix, brain regions, brain network, tuned, beta
  8. [8] § Results › Brain network dynamics are flattened in psychiatric illness ↔ NMF_Cocuzza.py, lines 167–221 · score 0.71 · upper triangle, configuration matrix, network assignment, fMRI, parcel, thresholded
  9. [9] § Methods › Data collection › Participants ↔ Data_Splitting_Cocuzza.py, lines 213–275 · score 0.67 · eating disorder, panic disorder, phobia, ADHD, anxiety, depression
  10. [10] § Methods › Brain network dynamics analysis › Non-negative matrix factorization (NMF) ↔ NMF_Cocuzza.py, lines 513–581 · score 0.62 · encoding matrix, brain regions, brain network, components, weights, coefficients
  11. [11] § Methods › Data collection › Participants ↔ NMF_Cocuzza.py, lines 167–221 · score 0.51 · fMRI, functional network, age, Connectome, validation, Brain
  12. [12] § Results › Symptom fingerprints: core dimensions of functioning are exhibited across health and psychiatric illness ↔ Fingerprints_Cocuzza.py, lines 698–750 · score 0.50 · log likelihood, Agglomerative, PCA, Ward, component, distance

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 · 776 lines · 47 KB · GPL-3.0 · 5 matches

  1. # C.V. Cocuzza, 2025. Symptom fingerprinting code for "Brain network dynamics reflect psychiatric illness status and transdiagnostic symptom profiles across health and disease". Cocuzza et al., 2025.
  2. # The steps of this pipeline are organized into python functions that are possible to adapt to other datasets,
  3. # but the TCP dataset is quite unique (at the time of preparing this manuscript) in that it collected many more
  4. # behavioral, cognitive, and clinical measures (i.e., self-report surveys, cognitive batteries, clinical instruments, etc.)
  5. # than is typical. Thus, we've uploaded (to GitHub) de-identified behavioral data files to make implementing the functions here easier
  6. # (and hopefully easier to adapt to other datasets). The participant IDs match those openly available on OpenNeuro and NDA for
  7. # TCP dataset, but all PHI has been removed. Example usage is recommended throughout along with explanatory notes.
  8. # NOTE: the data in all_TCP_behav_concatenated_mapped_domains.pkl was pre-labelled and verified by coauthors
  9. # (see Methods of manuscript; and large Supplemental Table that also reflects this data)
  10. # WORKFLOW (corresponds to modular functions below; so see each function for more detailed notes):
  11. # 1) Organize and curate data: fingerprints_curate_Cocuzza()
  12. # 2) Transform measures toward Gaussianity: fingerprints_transform_Cocuzza(), cnd_project_imputation_R_pipe.Rmd
  13. # ** NOTE: this step is multi-part and partially involves RStudio. See cnd_project_imputation_R_pipe.Rmd
  14. # 3) Binarize 0-inflated variables: fingerprints_binarize_Cocuzza()
  15. # 4) Impute missing values/scores: fingerprints_imputation_Cocuzza()
  16. # 5) Estimate distance matrix (individual differences correlations of normalized and transformed measures): fingerprints_distance_Cocuzza()
  17. # 6) Input distance matrix into agglomerative hiearchical clustering to partition data into empirically-driven dimensions of functioning: fingerprints_clustering_Cocuzza()
  18. # ** NOTE: this step partially involves RStudio, see cnd_project_clustering_checks_pipe.Rmd
  19. # 7) Perform PCA on each cluster of measures (normalized and imputed): fingerprints_PCA_Cocuzza()
  20. # NOTE: steps 8 and 9 are TBA to GitHub.
  21. # 8) Use PCA results (i.e., weight by factor loadings) and pre-labellings to name clusters: fingerprints_cluster_naming_Cocuzza()
  22. # 9) Quantify multi-dimensional symptom profiles (i.e., symptom fingerprints) for each participant: fingerprints_profiles_Cocuzza()
  23. # NOTE: we were able to take many of the steps here *because* of the Transdiagnostic Connectome Project (TCP) dataset
  24. # having a relatively *very large* set of behavioral, cognitive, and clinical measures to use. That is to say,
  25. # if we excluded a given measure, it was very likely that there were still other measures to cover that proportion of variance in functioning.
  26. # Even with all the above steps, the final set of behavioral data included 110 variables.
  27. # This may not be the case (and is likely not) for other datasets, thus these steps should be adapted with caution and thorough consideration.
  28. '''
  29. # Example code to load behavioral data shared in GitHub repo
  30. # (copy and paste this section into jupyter notebook, python script, etc.)
  31. # dataHere_All can be used in first function below; see notes for all functions for usages (these are modular and in-order).
  32. import pandas as pd
  33. import numpy as np
  34. import pickle
  35. dirHere = '/directory/path/to/where/you/saved/scripts/and/data/' # CHANGE
  36. keysAll = np.load(dirHere + 'all_TCP_behav_concatenated_keys.npy',allow_pickle=True).copy()
  37. dataHere_All = pd.read_csv(dirHere + 'all_TCP_behav_concatenated.csv')
  38. with open(dirHere + 'all_TCP_behav_concatenated_mapped_domains.pkl', 'rb') as f:
  39. behavTestLegend = pickle.load(f)
  40. numMeasures = dataHere_All.shape[1]-1 # 1st column is subject ID
  41. print(f"Initial data dimensions: N = {dataHere_All.shape[0]} participants and {numMeasures} behavioral/clinical/cognitive measures.")
  42. '''
  43. # Other notes:
  44. # Using missForest to impute missing values in observed data
  45. # NOTE: can use missCompare in R to see if missForest is still best solution (TBA), but for now going straight to missForest
  46. # See here: https://github.com/rawat126/Feature-Engineering/blob/c523d48154d7808ee26214e3c70066a0981d634a/Miss%20Forest%20Imputation/Miss_forest_imputation.ipynb#L6
  47. # And here: https://ragvenderrawat.medium.com/miss-forest-imputaion-the-best-way-to-handle-missing-data-feature-engineering-techniques-2e6922e5cecb
  48. # Also see: https://scikit-learn.org/stable/modules/impute.html
  49. # Also see: https://ragvenderrawat.medium.com/miss-forest-imputaion-the-best-way-to-handle-missing-data-feature-engineering-techniques-2e6922e5cecb
  50. # See: https://www.analyticsvidhya.com/blog/2022/05/handling-missing-values-with-random-forest/
  51. ####################################################################################
  52. # IMPORTS (some may need to be installed)
  53. import sys
  54. import os
  55. #import subprocess
  56. #import time
  57. import numpy as np
  58. import pandas as pd
  59. import pickle
  60. import h5py as h5
  61. import scipy
  62. #import scipy.io as spio
  63. import scipy.stats as stats
  64. #from scipy import signal
  65. import sklearn
  66. import statistics as stat
  67. from sklearn.ensemble import RandomForestClassifier # for categorical data
  68. from sklearn.ensemble import RandomForestRegressor # for continuous data
  69. ####################################################################################
  70. def fingerprints_curate_Cocuzza(dataHere_All,keysAll,saveResults=False,saveDir=None,percentThresh=25,removeRedunancies=True,removeSparseSubjs=False,percentThresh_Subjs=50):
  71. '''
  72. Data organizing & curating: trim measures based on a series of related conditions:
  73. a) remove measures with > threshold % of subjects missing
  74. b) remove redundant meaures (e.g., having both t-scores and raw scores) -- this was manually coded for TCP and agreed upon by coauthors (and based on literature/best practices)
  75. c) remove subjects above a certain threshold of sparseness. This was done in the TCP data release manuscript but we did *not* do it here because downstream steps tended to exclude those anyway (see imputation function for how we accounted for this)
  76. INPUTS:
  77. dataHere_All: see top of script for how to load example data; from csv file with all behavioral data concatenated into dataframe
  78. keysAll: see top of script for how to load example data; from npy file with all keys for dataHere_All
  79. saveResults: Boolean w/ Default False; whether or not to also save out the curated dataframe (into .csv)
  80. saveDir: if saveResults=True; a directory path (include entire string) to save out results (csv) from curation steps
  81. percentThresh: subject-missingness threshold for measure removal, Default is 25
  82. removeRedunancies: Boolean w/ Default True; whether or not to remove redundant measures (recommended)
  83. removeSparseSubjs: Boolean w/ Default False; whether or not to remove sparse subjects (not recommended for this study)
  84. percentThresh_Subjs: If removeSparseSubjs is True (not recommended), this is the sparsity threshold to use. 50% (Default) is relatively lenient.
  85. OUTPUTS:
  86. dataHere_TrimFinal: new dataframe with measures trimmed; returned and also saved to [saveDir + 'dataHere_TrimFinal_BeforeGaussTest.csv'] when saveResults=True
  87. '''
  88. # a) Remove measures with more than a given % of subjects missing
  89. numMeasures = dataHere_All.shape[1]-1 # ignore 1st column, which is subject ID
  90. allKeys = dataHere_All.keys().to_numpy().copy()
  91. # Loop over measures and find those with >percentThresh% of subjects' missing data
  92. measuresToRemove = []
  93. measuresToRemove_Ixs = []
  94. for measureIx in range(numMeasures):
  95. thisMeasureStr = keysAll[1:][measureIx] # skip 1st column (subject ID)
  96. measureVec_AllSubjs = dataHere_All[thisMeasureStr].to_numpy().copy()
  97. numMissingSubjs = np.where(np.isnan(measureVec_AllSubjs))[0].shape[0]
  98. percentMissingSubjs = np.floor((numMissingSubjs / dataHere_All.shape[0]) * 100)
  99. if percentMissingSubjs > percentThresh:
  100. print(f"{thisMeasureStr} has {percentMissingSubjs}% subjects missing, flagging it for removal...")
  101. measuresToRemove.append(thisMeasureStr)
  102. measuresToRemove_Ixs.append(measureIx)
  103. measuresToRemove = np.asarray(measuresToRemove)
  104. measuresToRemove_Ixs = np.asarray(measuresToRemove_Ixs)
  105. dataHere_Trimmed = dataHere_All.copy()
  106. dataHere_Trimmed = dataHere_Trimmed.drop(measuresToRemove,axis=1)
  107. keys_Trimmed = allKeys[1:].copy()
  108. keys_Trimmed = np.delete(keys_Trimmed,measuresToRemove_Ixs)
  109. print(f"\nTrimming the dataset by {measuresToRemove.shape[0]} behavioral/clinical/cognitive measures that had > {percentThresh}% of subjects missing. "+
  110. f"New number of measures = {dataHere_Trimmed.shape[1]-1}.\n")
  111. # b) data curating continued from above: trim redundant variables
  112. # NOTE: important to avoid results being overly driven by 1 scale; but I'm making it optional
  113. # NOTE: these were decided by hand because it typically involved reviewing the available literature and finding the field standard/preference for that measure
  114. # NOTES on what was trimmed:
  115. # POMS, SHIPLEY: keep t-scores
  116. # YMRS: keep sub-scales and remove total (sub-scales likely offer more diverse coverage of behavioral/cognitive constructs / explain more variance)
  117. # BAPQ, BIS, MSPSS, TCI, POMS, COGFQ, RSRI: keep sub-scales and remove total
  118. # MADRS: keep total and remove subscales because the subscales are 0 inflated and would end up being binarized (which isn't ideal) later anyway
  119. # LRIFT: keep total and remove subscales because the subscales are 0 inflated and would end up being binarized (which isn't ideal) later anyway
  120. # PUM: keep total-via-average and remove total-via-sum
  121. # CTQ: removing to keep as a demographic variable/covariate (childhood trauma questionnaire is unique in that it's based on past experiences)
  122. # CRT: these variables end up being binarized (not ideal) and overfit the clustering solutions downstream
  123. dataHere_Trimmed_NonRedundant = dataHere_Trimmed.copy()
  124. if removeRedunancies:
  125. # This list corresponds to the notes above (re: "what was trimmed" and why)
  126. keysToRM = ['lrift_employment','lrift_household', 'lrift_school', 'lrift_work','lrift_spouse_relate', 'lrift_children_relate','lrift_relatives_relate',
  127. 'lrift_friends_relate','lrift_relationships', 'lrift_satisfaction', 'lrift_recreation',
  128. 'madrs_reduced_appetite', 'madrs_apparent_sadness','madrs_concentration_issues', 'madrs_inability_feel','madrs_lassitude',
  129. 'madrs_pessimism', 'madrs_reported_sadness','madrs_reduced_sleep', 'madrs_suicidal_thoughts','madrs_inner_tension',
  130. 'ctq_denial_validity', 'ctq_emo_abuse', 'ctq_emo_neglect','ctq_phys_abuse', 'ctq_phys_neglect', 'ctq_sex_abuse',
  131. 'POMS_anger','POMS_confusion','POMS_depression','POMS_fatigue','POMS_tension','POMS_vigour','POMS_total_raw',
  132. 'SHIPLEY_vocab','ymrs_total','bapq_total_avg','bis_total_sum','mspss_total','tci_total_sum','cogfq_total',
  133. 'crt_correct_first_3','crt_intuitive_first_3', 'crt_correct_all_5','crt_intuitive_all_5','rsri_total_average','pum_total_sum']
  134. dataHere_Trimmed_NonRedundant = dataHere_Trimmed_NonRedundant.drop(keysToRM,axis=1)
  135. print(f"Removing {len(keysToRM)} behavioral/clinical/cognitive measures that were redundant and/or circular with other measures. "+
  136. f"New number of measures = {dataHere_Trimmed_NonRedundant.shape[1]-1}.\n")
  137. # c) remove subjects with > a certain % of missing variables
  138. # NOTE: for the purposes of CND project we did NOT do this, but keeping here for other projects/uses/datasets. Reasons:
  139. # (1) subjects will be removed later anyway (only certain functional runs were of-interest for NMF/dynamics pipeline)
  140. # (2) we need as many subjects as possible for train / test / validation splits,
  141. # (3) so instead we opted for a well-validated imputation procedure (see later function)
  142. dataHere_TrimFinal = dataHere_Trimmed_NonRedundant.copy()
  143. if removeSparseSubjs:
  144. subjKeysAll = dataHere_Trimmed_NonRedundant['subjectkey'].to_numpy().copy()
  145. keysHere = dataHere_Trimmed_NonRedundant.keys().to_numpy().copy()
  146. subjsToRemove_IDs = []
  147. subjsToRemove_Ixs = []
  148. for subjIx in range(dataHere_Trimmed_NonRedundant.shape[0]):
  149. subjID = subjKeysAll[subjIx]
  150. subjVec = dataHere_Trimmed_NonRedundant[dataHere_Trimmed_NonRedundant['subjectkey']==subjID][keysHere[1:]].values[0,:].copy()
  151. numNaNs = np.where(np.isnan(subjVec))[0].shape[0]
  152. percNaNs = np.floor(((numNaNs/subjVec.shape[0])*100))
  153. if percNaNs > percentThresh_Subjs:
  154. subjsToRemove_IDs.append(subjID)
  155. subjsToRemove_Ixs.append(subjIx)
  156. print(f"Flagging subject {subjID} for having {percNaNs}% missing behavioral/clinical/cognitive measures...")
  157. subjsToRemove_IDs = np.asarray(subjsToRemove_IDs)
  158. subjsToRemove_Ixs = np.asarray(subjsToRemove_Ixs)
  159. dataHere_TrimFinal = dataHere_TrimFinal.drop(subjsToRemove_Ixs,axis=0)
  160. print(f"\nTrimming the dataset by {subjsToRemove_Ixs.shape[0]} subjects that were missing > {percentThresh_Subjs}% of behavioral/clinical/cognitive measures. "+
  161. f"New number of subjects = {dataHere_TrimFinal.shape[0]}.\n")
  162. # QA:
  163. subjIxVec_All = np.arange(dataHere_Trimmed_NonRedundant.shape[0])
  164. subjIxVec_All = np.delete(subjIxVec_All,subjsToRemove_Ixs)
  165. if not np.all(subjKeysAll[subjIxVec_All] == dataHere_TrimFinal['subjectkey'].to_numpy()):
  166. print(f"WARNING: subject ID order is off, please check.")
  167. elif not removeSparseSubjs:
  168. print(f"\nNOT trimming the dataset by subjects missing behavioral/clinical/cognitive measures. Number of subjects = {dataHere_TrimFinal.shape[0]}.\n")
  169. # SAVE
  170. if saveResults:
  171. dataHere_TrimFinal.to_csv(saveDir + 'dataHere_TrimFinal_BeforeGaussTest.csv')
  172. return dataHere_TrimFinal
  173. ####################################################################################
  174. def fingerprints_transform_Cocuzza(dataHere_TrimFinal,normData=False,useZ=False,saveResults=False,saveDir=None):
  175. '''
  176. The goal of this step is to normalize variables that are deemed non-Gaussian. NOTE that this was aided by an RStudio toolkit called bestNormalize, with the following workflow:
  177. 1) Run fingerprints_curate_Cocuzza() [function above] to curate which variables will be part of the final set for analyses.
  178. 2) Run those variables through this function fingerprints_transform_Cocuzza() to identify which need to be normalized --> save the identified non-Gaussian variables.
  179. 3) Load non-Gaussian variables identified in step 2 into RStudio and run bestNormalize (see cnd_project_imputation_R_pipe.Rmd).
  180. 4) Re-run this function fingerprints_transform_Cocuzza() to see if any of the variables are still being flagged as non-Gaussian (likely a small proportion of original set of variables)
  181. 5) The variables that are still non-Gaussian will need to be manually inspected. The vast majority were 0-inflated (or otherwise point-inflated), in which case we binarized (see next function, fingerprints_binarize_Cocuzza()), but a small number were reasonably close to having a normal distribution and were kept as-is. It may also be reasonable to trim those last remaining variables from further analyses, depending on how problematic the distribution is for later steps (e.g., strongly multimodal distributions might need further consideration)
  182. INPUTS:
  183. dataHere_TrimFinal: output dataframe of fingerprints_curate_Cocuzza(); NOTE if saveResults=True in fingerprints_curate_Cocuzza(), you could load this csv then use as input (or some variant thereof)
  184. normData: Boolean w/ Default False. Whether or not to pre-normalize with z-score or min-max.
  185. useZ: Boolean w/ Default False. Only when normData=True. If True, pre-normalize with z-score; if False, pre-normalize with min-max method.
  186. saveResults: Boolean w/ Default False (but we recommend setting to True); whether or not to also save out the curated dataframe (into .csv)
  187. saveDir: if saveResults=True; a directory path (include entire string) to save out results (csv) from transformation steps
  188. OUTPUTS:
  189. dataHere_CND_ToTransform: a dataframe of the variables you should pass to R for transformation; if you do a 2nd pass of this function (see notes above), the resulting dataframe from R will be the input dataHere_TrimFinal (make sure normData is False)
  190. dataHere_TrimFinal_Normed: only if normData=True; an intermedite output dataframe; all the curated data before identifying which variables need to go to R for transforming; i.e., all data in the input dataHere_TrimFinal plus min-max normalization or z-scoring (based on useZ input)
  191. '''
  192. # Normalize data: optional
  193. if normData:
  194. dataHere_TrimFinal_Normed = dataHere_TrimFinal.copy()
  195. if useZ:
  196. # NOTE: indexed at 1 to skip the first column (subject IDs), but may need to adjust to 2 if another, extra index column was added in the last step
  197. for keyIx in range(1,dataHere_TrimFinal.keys().to_numpy().shape[0]):
  198. keyHere = dataHere_TrimFinal.keys().to_numpy()[keyIx]
  199. vecHere = dataHere_TrimFinal[keyHere].to_numpy().copy()
  200. zVec = stats.zscore(vecHere,nan_policy='omit').copy()
  201. dataHere_TrimFinal_Normed[keyHere] = zVec.copy()
  202. # SAVE:
  203. if saveResults:
  204. dataHere_TrimFinal_Normed.to_csv(saveDir + 'dataHere_TrimFinal_Normed_Z.csv')
  205. elif not useZ:
  206. # NOTE: indexed at 1 to skip the first column (subject IDs), but may need to adjust to 2 if another, extra index column was added in the last step
  207. for keyIx in range(1,dataHere_TrimFinal.keys().to_numpy().shape[0]):
  208. keyHere = dataHere_TrimFinal.keys().to_numpy()[keyIx]
  209. vecHere = dataHere_TrimFinal[keyHere].to_numpy().copy()
  210. normVec = minMaxNorm(vecHere,minVal=0,maxVal=1).copy()
  211. dataHere_TrimFinal_Normed[keyHere] = normVec.copy()
  212. # SAVE:
  213. if saveResults:
  214. dataHere_TrimFinal_Normed.to_csv(saveDir + 'dataHere_TrimFinal_Normed_MinMax.csv')
  215. elif not normData:
  216. # SAVE:
  217. if saveResults:
  218. dataHere_TrimFinal.to_csv(saveDir + 'dataHere_TrimFinal_NotNormed.csv')
  219. # Identify non-gaussian data for transformation
  220. # Here, I'm running some tests to identify which variables need transforming, I'll then save those out --> go to R --> load them back in
  221. # See cnd_project_imputation_R_pipe.rmd
  222. if normData:
  223. keysHere_All = dataHere_TrimFinal_Normed.keys().to_numpy().copy()
  224. dataHere_TestGauss_Pass1 = dataHere_TrimFinal_Normed.copy()
  225. elif not normData:
  226. keysHere_All = dataHere_TrimFinal.keys().to_numpy().copy()
  227. dataHere_TestGauss_Pass1 = dataHere_TrimFinal.copy()
  228. # Test whether scale's distribution differs from normal distribution (D’Agostino’s K^2 test)
  229. keyIxs_NonNormal = []
  230. keyIxs_NonNormal_Cusp = [] # i.e., metrics that only just fail the test, so inspect further
  231. for keyIx in range(1,keysHere_All.shape[0]):
  232. keyHere = keysHere_All[keyIx]
  233. dataVecHere = dataHere_TestGauss_Pass1[keyHere].to_numpy().copy()
  234. kStat,pVal = stats.normaltest(dataVecHere,nan_policy='omit')
  235. if pVal<0.05:
  236. #print(f"{keyHere} (py index {keyIx}) is likely not normal")
  237. keyIxs_NonNormal.append(keyIx)
  238. if pVal > 0.01:
  239. print(f"{keyHere} (py index {keyIx}) may be on the cusp, please check")
  240. keyIxs_NonNormal_Cusp.append(keyIx)
  241. keyIxs_NonNormal = np.asarray(keyIxs_NonNormal)
  242. keyIxs_NonNormal_Cusp = np.asarray(keyIxs_NonNormal_Cusp)
  243. print(f"\nOf {keysHere_All.shape[0]-1} measures, {keyIxs_NonNormal.shape[0]} were identified as not matching normal distribution\n")
  244. # Test Gaussianity with Shapiro-Wilk test
  245. keyIxs_ShapiroWilk = []
  246. keyIxs_ShapiroWilk_Cusp = [] # i.e., metrics that only just fail the test, so inspect further
  247. for keyIx in range(1,keysHere_All.shape[0]):
  248. keyHere = keysHere_All[keyIx]
  249. dataVecHere = dataHere_TestGauss_Pass1[keyHere].to_numpy().copy()
  250. nanIxs = np.where(np.isnan(dataVecHere))[0]
  251. if nanIxs.shape[0]!=0:
  252. dataVecHere = np.delete(dataVecHere,nanIxs)
  253. kStat,pVal = stats.shapiro(dataVecHere)
  254. if pVal<0.05:
  255. #print(f"{keyHere} (py index {keyIx}) is likely not normal")
  256. keyIxs_ShapiroWilk.append(keyIx)
  257. if pVal > 0.01:
  258. print(f"{keyHere} (py index {keyIx}) may be on the cusp, please check")
  259. keyIxs_ShapiroWilk_Cusp.append(keyIx)
  260. keyIxs_ShapiroWilk = np.asarray(keyIxs_ShapiroWilk)
  261. keyIxs_ShapiroWilk_Cusp = np.asarray(keyIxs_ShapiroWilk_Cusp)
  262. print(f"\nOf {keysHere_All.shape[0]-1} measures, {keyIxs_ShapiroWilk.shape[0]} were identified as non-Gaussian with Shapiro-Wilk\n")
  263. # Test normality with Anderson-Darling test
  264. keyIxs_AD = []
  265. keyIxs_AD_Cusp = [] # i.e., metrics that only just fail the test, so inspect further
  266. for keyIx in range(1,keysHere_All.shape[0]):
  267. keyHere = keysHere_All[keyIx]
  268. dataVecHere = dataHere_TestGauss_Pass1[keyHere].to_numpy().copy()
  269. nanIxs = np.where(np.isnan(dataVecHere))[0]
  270. if nanIxs.shape[0]!=0:
  271. dataVecHere = np.delete(dataVecHere,nanIxs)
  272. testModel = stats.anderson(dataVecHere,dist='norm')
  273. if testModel.statistic>testModel.critical_values[2]: # see testModel.significance_level for associated alphas for each critical value:
  274. keyIxs_AD.append(keyIx)
  275. if testModel.statistic<testModel.critical_values[4]: # see testModel.significance_level for associated alphas for each critical value
  276. print(f"{keyHere} (py index {keyIx}) may be on the cusp, please check")
  277. keyIxs_AD_Cusp.append(keyIx)
  278. keyIxs_AD = np.asarray(keyIxs_AD)
  279. keyIxs_AD_Cusp = np.asarray(keyIxs_AD_Cusp)
  280. print(f"\nOf {keysHere_All.shape[0]-1} measures, {keyIxs_AD.shape[0]} were identified as non-normal with Anderson-Darling\n")
  281. # Test normality with Kolmogorov-Smirnov test
  282. keyIxs_KS = []
  283. keyIxs_KS_Cusp = [] # i.e., metrics that only just fail the test, so inspect further
  284. for keyIx in range(1,keysHere_All.shape[0]):
  285. keyHere = keysHere_All[keyIx]
  286. dataVecHere = dataHere_TestGauss_Pass1[keyHere].to_numpy().copy()
  287. nanIxs = np.where(np.isnan(dataVecHere))[0]
  288. if nanIxs.shape[0]!=0:
  289. dataVecHere = np.delete(dataVecHere,nanIxs)
  290. kStat,pVal = stats.kstest(dataVecHere,'norm')
  291. if pVal<0.05:
  292. #print(f"{keyHere} (py index {keyIx}) is likely not normal")
  293. keyIxs_KS.append(keyIx)
  294. if pVal > 0.01:
  295. print(f"{keyHere} (py index {keyIx}) may be on the cusp, please check")
  296. keyIxs_KS_Cusp.append(keyIx)
  297. keyIxs_KS = np.asarray(keyIxs_KS)
  298. keyIxs_KS_Cusp = np.asarray(keyIxs_KS_Cusp)
  299. print(f"\nOf {keysHere_All.shape[0]-1} measures, {keyIxs_KS.shape[0]} were identified as non-normal with Kolmogorov-Smirnov\n")
  300. # Identify which measures/variables were identified across a few tests for normality/Gaussianity
  301. uniqueVec,countsVec = np.unique(np.concatenate((keyIxs_NonNormal,keyIxs_ShapiroWilk,keyIxs_AD,keyIxs_KS)),return_counts=True)
  302. nonNormalWinnerIxs = np.where(countsVec>=2)[0].copy()
  303. nonNormalWinners = uniqueVec[nonNormalWinnerIxs]
  304. #print(f"\nAcross all 4 tests, {nonNormalWinners.shape[0]} measures were identified as "+
  305. # f"non-normal for >=2 tests: \n{keysHere_All[nonNormalWinners]} \n(py indices: {nonNormalWinners})\n")
  306. print(f"\nAcross all 4 tests, {nonNormalWinners.shape[0]} measures were identified as non-normal for >=2 tests:\n")
  307. # And identify which metrics were on the cusp across a few tests
  308. uniqueVec,countsVec = np.unique(np.concatenate((keyIxs_NonNormal_Cusp,keyIxs_ShapiroWilk_Cusp,keyIxs_AD_Cusp,keyIxs_KS_Cusp)),return_counts=True)
  309. cuspWinnerIxs = np.where(countsVec>=3)[0].copy() # lower number = less strict; higher number = more strict; max = 4 (b/c that's the number of tests I used)
  310. if cuspWinnerIxs.shape[0]!=0:
  311. cuspWinners = uniqueVec[cuspWinnerIxs]
  312. print(f"\nAcross all 4 tests, the following {cuspWinners.shape[0]} measures were on the cusp "+
  313. f"for being non-normal for >=3 tests: \n{keysHere_All[cuspWinners]} \n(py indices: {cuspWinners})\n")
  314. cuspWinnerIxs_Adj = np.zeros_like(cuspWinnerIxs)
  315. for ixHere in range(cuspWinnerIxs.shape[0]):
  316. cuspWinnerIxs_Adj[ixHere] = np.where(nonNormalWinners==cuspWinners[ixHere])[0]
  317. # And generate the final list to send to R
  318. nonNormalWinners = np.delete(nonNormalWinners,cuspWinnerIxs_Adj)
  319. keysToTransform = np.concatenate((np.asarray(['subjectkey']),keysHere_All[nonNormalWinners]))
  320. # Curate dataframe and save
  321. dataHere_CND_ToTransform = dataHere_TestGauss_Pass1.copy()
  322. dataHere_CND_ToTransform = dataHere_CND_ToTransform[keysToTransform]
  323. if saveResults:
  324. if normData:
  325. if useZ:
  326. saveFile = saveDir + 'dataHere_TrimFinal_Normed_Z_ToTransform.csv'
  327. elif not useZ:
  328. saveFile = saveDir + 'dataHere_TrimFinal_Normed_MinMax_ToTransform.csv'
  329. elif not normData:
  330. saveFile = saveDir + 'dataHere_TrimFinal_NotNormed_ToTransform.csv'
  331. dataHere_CND_ToTransform.to_csv(saveFile)
  332. if normData:
  333. return dataHere_CND_ToTransform, dataHere_TrimFinal_Normed
  334. elif not normData:
  335. return dataHere_CND_ToTransform
  336. ####################################################################################
  337. def fingerprints_binarize_Cocuzza(dataHere_ToBinarize,normData=False,useZ=False,saveResults=False,saveDir=None):
  338. '''
  339. The goal of this step is to binarize measures that have been identified as 0-inflated (after 2 runs through fingerprints_transform_Cocuzza() above and RStudio's bestNormalize).
  340. NOTE that there are a few possible ways to deal with point-inflated data (e.g., poisson transforms; gamma models; etc.), but we opted for binarization given that it's straightforward.
  341. INPUTS:
  342. dataHere_ToBinarize: dataframe; where general formatting is the same as other functions, but the variables (columns) here are those identified for binarization
  343. normData: whether or not data was normed in prior transformation step/function; note this is only relevant for strings used to save results if saveResults=True
  344. useZ: if normData=True, whether or not data was normalized with z-scoring in prior transformation step/function; note this is only relevant for strings used to save results if saveResults=True
  345. saveResults: Boolean w/ Default False (but we recommend setting to True); whether or not to also save out the results dataframe (into .csv)
  346. saveDir: if saveResults=True; a directory path (include entire string) to save out results (csv) from binarization steps
  347. OUTPUTS:
  348. dataHere_Transformed_Binarized: dataframe with measures/variables binarized
  349. '''
  350. # Binarize highly non-gaussians (0-inflated etc.): binarize based on mode
  351. # Initialize dictionary to make new dataframe
  352. dataHere_Transformed_Pass2_DICT = {'subjectkey':dataHere_ToBinarize['subjectkey'].to_numpy()}
  353. for metricIx in range(1,keysHere_All.shape[0]):
  354. thisKey = keysHere_All[metricIx]
  355. vecHere = dataHere_ToBinarize[thisKey].to_numpy()
  356. modeVal,countVal = stats.mode(vecHere)
  357. nanIxs = np.where(np.isnan(vecHere))[0] # hold out nans temporarily
  358. vecHere[nanIxs] = 0
  359. modeIxs = np.where(np.round(vecHere,4)==np.round(modeVal,4))[0]
  360. vecHere_Bin = np.zeros((vecHere.shape[0]))
  361. vecHere_Bin[modeIxs] = 1
  362. vecHere_Bin[nanIxs] = np.nan # add nans back in
  363. # NOTE: can use sklearn (see line below), but I think it does the opposite mapping from what we want (1's given to the non-mode)
  364. # vecHere_Bin = sklearn.preprocessing.binarize(vecHere.reshape(-1,1),threshold=modeVal)[:,0].copy()
  365. dataHere_Transformed_Pass2_DICT[thisKey] = vecHere_Bin.copy()
  366. dataHere_Transformed_Binarized = pd.DataFrame(dataHere_Transformed_Pass2_DICT).copy()
  367. # SAVE
  368. if saveResults:
  369. if normData:
  370. if useZ:
  371. dataHere_Transformed_Binarized.to_csv(saveDir + 'dataHere_TrimFinal_Normed_Z_Transformed_Pass2.csv')
  372. elif not useZ:
  373. dataHere_Transformed_Binarized.to_csv(saveDir + 'dataHere_TrimFinal_Normed_MinMax_Transformed_Pass2.csv')
  374. elif not normData:
  375. dataHere_Transformed_Binarized.to_csv(saveDir + 'dataHere_TrimFinal_NotNormed_Transformed_Pass2.csv')
  376. return dataHere_Transformed_Binarized
  377. ####################################################################################
  378. def fingerprints_imputation_Cocuzza(dataHere_Collated_ToImpute,percentTest=20,reNormData=True,useZ=False,saveResults=False,saveDir=None):
  379. '''
  380. The goal of this step is to impute missing data points (example: a given participant did not complete a survey) using the MissForest algorithm. See Chopra et al. 2025, Sci Data, for extensive testing on the TCP dataset, which suggested that missforest yielded the most robust imputation results.
  381. NOTE: in the manuscript, we used a conservative train/test/validation split of the data. It is up to the user to divvy up their data appropriately.
  382. INPUTS:
  383. dataHere_Collated_ToImpute: this is a dataframe where all of the results of the prior steps (curating, transforming (twice), and binarizing) are all collated back together (i.e., the columns are stacked back into 1 dataframe)
  384. percentTest: percent of the data to set as the test set. Default is 20. e.g., if 20 is given, this is an 80/20 train/test split.
  385. reNormData: whether or not to re-normalize the data; the bestNormalize transformation process in R appears to put some (but not all) variables in a different space/range; and this is particularly important for min-max norming (i.e., putting data back into range of 0-1); Also should consider using this when prior steps/functions had normData=False. The idea here is that the prior transformations were on raw scores, then here normalize just before imputation.
  386. useZ: Boolean w/ Default False. Only when normData=True. If True, pre-normalize with z-score; if False, pre-normalize with min-max method.
  387. saveResults: Boolean w/ Default False (but we recommend setting to True); whether or not to also save out the curated dataframe (into .csv)
  388. saveDir: if saveResults=True; a directory path (include entire string) to save out results (csv) from transformation steps
  389. OUTPUTS:
  390. dataHere_Imputed: dataframe where formatting matches inputted dataHere_Collated_ToImpute, but missing values have been imputed
  391. '''
  392. # Note: dataHere_Collated_ToImpute was pre-computed (and saved, if saveResults=True) above; user needs to collate those dataframes before using this function
  393. if reNormData:
  394. dataHere_ToImpute_Final = dataHere_Collated_ToImpute.copy()
  395. if not useZ:
  396. for keyIx in range(1,dataHere_Collated_ToImpute.keys().to_numpy().shape[0]):
  397. keyStrHere = dataHere_Collated_ToImpute.keys().to_numpy()[keyIx]
  398. vecHere = dataHere_Collated_ToImpute[keyStrHere].to_numpy().copy()
  399. vecHere_Normed = minMaxNorm(vecHere).copy()
  400. dataHere_ToImpute_Final[keyStrHere] = vecHere_Normed.copy()
  401. elif not reNormData:
  402. dataHere_ToImpute_Final = dataHere_Collated_ToImpute.copy()
  403. # MissForest with sklearn
  404. subjIDs_DF = dataHere_ToImpute_Final['subjectkey'].to_numpy().copy()
  405. dataHere_ToImpute_Final_TrainTest = dataHere_ToImpute_Final.copy()
  406. nSubjsCND = dataHere_ToImpute_Final.shape[0]
  407. nSubjsTrainTest = dataHere_ToImpute_Final_TrainTest.shape[0]
  408. percTrainTest = (nSubjsTrainTest * 100)/nSubjsCND
  409. print(f"Final dataset for imputation: {nSubjsTrainTest} subjects (of N={nSubjsCND} total) by {dataHere_ToImpute_Final_TrainTest.shape[1]} measures. "+
  410. f"{percTrainTest}% subjects (n = {nSubjsTrainTest}) will be used for train/test")
  411. percentTrain = 100 - percentTest
  412. nSubjs_ImputePipe = dataHere_ToImpute_Final_TrainTest.shape[0]
  413. numTrain = int(np.round((percentTrain * nSubjs_ImputePipe)/100))
  414. numTest = int(np.round((percentTest * nSubjs_ImputePipe)/100))
  415. if numTest + numTrain != nSubjs_ImputePipe:
  416. print(f"train and test subject numbers do not equal full N, please check.")
  417. print(f"\nFor missForest: training set: {percentTrain}% of subjects (n = {numTrain}). Test set: {percentTest}% of subjects (n = {numTest}). (Total N={nSubjs_ImputePipe}).")
  418. # Loop over scales; NOTE: can loop over shuffles and take average (for continuous data) or mode (categorical data)
  419. subjVec_Shuffled = np.arange(nSubjs_ImputePipe)
  420. np.random.shuffle(subjVec_Shuffled)
  421. subjsTrain = subjVec_Shuffled[:numTrain].copy()
  422. subjsTest = subjVec_Shuffled[numTrain:].copy()
  423. keysHere_All = dataHere_ToImpute_Final_TrainTest.keys().to_numpy()[1:].copy()
  424. dataHere_Imputed_DICT = {'subjectkey':dataHere_ToImpute_Final_TrainTest['subjectkey'].to_numpy()}
  425. for keyIx in range(keysHere_All.shape[0]):
  426. keyStr = keysHere_All[keyIx]
  427. allOtherKeys = keysHere_All.copy()
  428. allOtherKeys = np.delete(allOtherKeys,keyIx)
  429. yData_Orig = pd.DataFrame({keyStr:dataHere_ToImpute_Final_TrainTest[keyStr].to_numpy()})[keyStr].copy()
  430. nanIxs = np.where(np.isnan(yData_Orig))[0]
  431. xData = dataHere_ToImpute_Final_TrainTest[allOtherKeys].values.copy()
  432. if nanIxs.shape[0]!=0:
  433. #print(f"Predicting {keyStr}...")
  434. if keyStr in keysHere_Trans2_Adj: # Categorical (binarized)
  435. yData = mode_imputation(yData_Orig).to_numpy().copy() # for categorical data
  436. xData_Train = xData[subjsTrain,:].copy()
  437. xData_Test = xData[subjsTest,:].copy()
  438. yData_Train = yData[subjsTrain].copy()
  439. yData_Test = yData[subjsTest].copy()
  440. modelHere = RandomForestClassifier()
  441. modelHere.fit(xData,yData)
  442. elif keyStr not in keysHere_Trans2_Adj: # Continuous
  443. #yData = mean_median_imputation(yData_Orig,kind='mean').to_numpy().copy() # for continuous data
  444. yData = mean_median_imputation(yData_Orig,kind='median').to_numpy().copy() # for continuous data
  445. xData_Train = xData[subjsTrain,:].copy()
  446. xData_Test = xData[subjsTest,:].copy()
  447. yData_Train = yData[subjsTrain].copy()
  448. yData_Test = yData[subjsTest].copy()
  449. modelHere = RandomForestRegressor()
  450. modelHere.fit(xData,yData)
  451. yPred = modelHere.predict(xData_Test[:nanIxs.shape[0],:]) # values
  452. yData_Imputed = yData_Orig.copy()
  453. yData_Imputed[nanIxs] = yPred
  454. else:
  455. print(f"{keyStr} is not missing data, skipping...")
  456. yData_Imputed = yData_Orig.copy()
  457. dataHere_Imputed_DICT[keyStr] = yData_Imputed.copy()
  458. dataHere_Imputed = pd.DataFrame(dataHere_Imputed_DICT)
  459. nanIxsDF = dataHere_Imputed.isnull().sum()[dataHere_Imputed.isnull().sum() != 0].index # use ==0 to get not-nans
  460. if nanIxsDF.shape[0] != 0:
  461. print(f"There is still missing data points, please check.")
  462. if saveResults:
  463. if reNormData:
  464. if useZ:
  465. dataHere_Imputed.to_csv(saveDir + 'dataHere_Normed_Z_Transformed_Imputed.csv')
  466. elif not useZ:
  467. dataHere_Imputed.to_csv(saveDir + 'dataHere_Normed_MinMax_Transformed_Imputed.csv')
  468. elif not reNormData:
  469. dataHere_Imputed.to_csv(saveDir + 'dataHere_NotNormed_Imputed.csv')
  470. return dataHere_Imputed
  471. ####################################################################################
  472. def fingerprints_distance_Cocuzza(dataHere_Imputed,startIx_CSV=2):
  473. '''
  474. The goal of this step is to estimate individual differences correlation. Here, each variable-variable pair (i.e., each pair of behavioral measures) is correlated across subjects. So the resulting correlation score indicates the extent that individual differences (across those 2 given measures) are similar. NOTE: we used spearman rho because of it's inference for rank correlation that doesn't assume specific distributions a priori (i.e., is less parametric than r)
  475. INPUTS:
  476. dataHere_Imputed: output dataframe from prior step/function (imputation step) above.
  477. startIx_CSV: an artifact of pandas is that sometimes an indexing column is added. You always want this to be at least 1 (because we want to skip the true 1st colum which is subject IDs), but maybe increase to 2 (default) or even 3 etc. if more columns were added to left of that subjectkey column.
  478. OUTPUTS:
  479. behavCorrs_IndivDiffs: an adjacency matrix (square, symmetric). x/y dimensions correspond to behavioral variables in input dataframe.
  480. '''
  481. # Correlation matrix: individual differences across subjects
  482. numVarsCND = dataHere_Imputed.shape[1]-startIx_CSV
  483. behavCorrs_IndivDiffs = np.zeros((numVarsCND,numVarsCND))
  484. for metricIx in range(numVarsCND):
  485. thisTargetKey = dataHere_Imputed.keys().to_numpy()[startIx_CSV:][metricIx]
  486. for metricIx_Next in range(numVarsCND):
  487. thisSourceKey = dataHere_Imputed.keys().to_numpy()[startIx_CSV:][metricIx_Next]
  488. r,p = stats.spearmanr(dataHere_Imputed[thisTargetKey].to_numpy(),dataHere_Imputed[thisSourceKey].to_numpy(),nan_policy='omit')
  489. behavCorrs_IndivDiffs[metricIx,metricIx_Next] = r
  490. np.fill_diagonal(behavCorrs_IndivDiffs,0)
  491. return behavCorrs_IndivDiffs
  492. ####################################################################################
  493. def fingerprints_clustering_Cocuzza(behavCorrs_IndivDiffs,dataHere_Imputed,nClusters=4,startIx_CSV=2):
  494. '''
  495. The goal of this step is to perform heirarchical agglomerative clustering on individual differences correlations. See cnd_project_clustering_checks_pipe.Rmd for example usage of. Rpackage that helps optimize number of clusters to use.
  496. INPUTS:
  497. behavCorrs_IndivDiffs: output matrix from step/function above (distance step).
  498. dataHere_Imputed: output dataframe from step/function above (imputation step).
  499. nClusters: Number of clusters to use; Default = 4; based on R package nbClust.
  500. startIx_CSV: an artifact of pandas is that sometimes an indexing column is added. You always want this to be at least 1 (because we want to skip the true 1st colum which is subject IDs), but maybe increase to 2 (default) or even 3 etc. if more columns were added to left of that subjectkey column.
  501. OUTPUTS:
  502. clusterBoundaries: array of size nClusters x 3: column 1 = start ix, column 2 = stop ix (py --> +1), column 3 = number of features in cluster.
  503. clusterOrder: variable that assigns each variable from the input dataframe to a cluster.
  504. '''
  505. originalOrder = dataHere_Imputed.keys().to_numpy()[startIx_CSV:].copy()
  506. scoreNames = dataHere_Imputed.keys().to_numpy()[startIx_CSV:].copy()
  507. linkagMatrix = scipy.cluster.hierarchy.ward(behavCorrs_IndivDiffs) # same as colLink
  508. cutTree = scipy.cluster.hierarchy.cut_tree(linkagMatrix, n_clusters = nClusters).flatten() # note: n_clusters can be 2D, will return 2D group membership
  509. percentileList = pd.DataFrame({'column_names':scoreNames,'cluster_membership':cutTree})
  510. clusterList = percentileList.sort_values(by='cluster_membership')
  511. clusterBoundaries = np.zeros((nClusters,3)) # Col 1 = start ix, Col 2 = stop ix (py --> +1), Col 3 = number of features in cluster
  512. clusterOrder = np.zeros((originalOrder.shape[0])) # Use this as a sorting/indexing variable
  513. for clusterIx in range(nClusters):
  514. if not useHandOrder:
  515. clusterSetIxs = np.where(clusterList['cluster_membership'].to_numpy() == clusterIx)[0]
  516. elif useHandOrder:
  517. clusterSetIxs = np.where(clusterListAdj['cluster_membership'].to_numpy() == clusterIx)[0] # NOTE: hand-ordered above
  518. clusterBoundaries[clusterIx,0] = clusterSetIxs[0]
  519. clusterBoundaries[clusterIx,1] = clusterSetIxs[-1] + 1
  520. clusterBoundaries[clusterIx,2] = clusterSetIxs.shape[0]
  521. if not useHandOrder:
  522. variablesThisCluster = clusterList['column_names'].to_numpy().copy()
  523. elif useHandOrder:
  524. variablesThisCluster = clusterListAdj['column_names'].to_numpy().copy() # NOTE: hand-ordered above
  525. print(f"\nCluster {clusterIx+1}:\n{clusterList[clusterList['cluster_membership']==clusterIx]['column_names'].to_numpy()}")
  526. for varIx in range(variablesThisCluster.shape[0]):
  527. thisVar = variablesThisCluster[varIx]
  528. origIx = np.where(originalOrder==thisVar)[0]
  529. if not useHandOrder:
  530. newIx = np.where(clusterList['column_names'].to_numpy()==thisVar)[0]
  531. elif useHandOrder:
  532. newIx = np.where(clusterListAdj['column_names'].to_numpy()==thisVar)[0] # NOTE: hand-ordered above
  533. #clusterOrder[varIx] = origIx
  534. clusterOrder[newIx] = origIx
  535. # NOTE: can verify with:
  536. # np.array_equal(originalOrder[clusterOrder.astype(int)],clusterList['column_names'].to_numpy())
  537. # NOTE: an alternative with sklearn; results are effectively equivalent to scipy method above:
  538. #numClustersStr = 4
  539. # Appending distance between clusters at every iteration in for loop
  540. #hClustering_Distances_All = []
  541. #hClustering_CH_Index_All = []
  542. #for numClusters in range(2,numVarsCND):
  543. # hClustering = sklearn.cluster.AgglomerativeClustering(n_clusters=numClusters, metric='euclidean', linkage='ward', compute_distances=True).fit(behavCorrs_IndivDiffs)
  544. # hClustering_Distances_All.append(hClustering.distances_)
  545. # hClustering_CH_Index_All.append(sklearn.metrics.calinski_harabasz_score(behavCorrs_IndivDiffs,hClustering.labels_))
  546. #hClustering_CH_Index_All = np.asarray(hClustering_CH_Index_All)
  547. #hClustering_Distances = hClustering_Distances_All[0].copy() # note: all sub-arrays appear to be equal? why do loop? this appears to be equal to colLink
  548. #xNumClusters = np.array([indexHere for indexHere in range(numVarsCND,1,-1)]) # number of clusters in descending oder
  549. return clusterBoundaries, clusterOrder
  550. ####################################################################################
  551. def fingerprints_PCA_Cocuzza(behavCorrs_IndivDiffs,dataHere_Imputed,numClusters=4, startIx_CSV=2,printTestName=True,numComps=1,useAbs=Fale):
  552. '''
  553. After clustering has been performed (see above), run PCA on the original data (i.e.., not the distance matrix), one cluster at a time. In the manuscript we focused on the 1st PC for each cluster.
  554. INPUTS:
  555. behavCorrs_IndivDiffs: output matrix from step/function above (distance step).
  556. dataHere_Imputed: output dataframe from step/function above (imputation step).
  557. numClusters: Number of clusters to use; Default = 4; based on R package nbClust.
  558. startIx_CSV: an artifact of pandas is that sometimes an indexing column is added. You always want this to be at least 1 (because we want to skip the true 1st colum which is subject IDs), but maybe increase to 2 (default) or even 3 etc. if more columns were added to left of that subjectkey column.
  559. printTestName: Default True; whether to include test names in the print
  560. numComps: Default 1; For PCA, either use a number (less than # features) or None
  561. useAbs: Default False; Use absolute values of PCA scores for visualizing
  562. OUTPUTS:
  563. pcaScores_All: array of size number of subjects x number of clusters. This gives log-likelihood of each subject to express each cluster.
  564. loadings_All: dictionary where keys = cluster IDs; in each, an array of factor loadings corresponding to measures in that cluster
  565. '''
  566. modelHere = sklearn.cluster.AgglomerativeClustering(n_clusters=numClusters, metric='euclidean', linkage='ward', compute_distances=True)
  567. modelHere.fit_predict(behavCorrs_IndivDiffs)
  568. labelsHere = modelHere.labels_
  569. pcaScores_All = np.zeros((dataHere_Imputed.shape[0],numClusters))
  570. loadings_All = {} # keys given by cluster, e.g., 'cluster_1', etc.
  571. for labelIx in range(numClusters):
  572. tempList = []
  573. clusterIxsHere = np.where(labelsHere==labelIx)[0]
  574. testNamesHere = dataHere_Imputed.keys().to_numpy()[startIx_CSV:][clusterIxsHere]
  575. dataHere_Cluster = dataHere_Imputed[testNamesHere].copy()
  576. if numComps is not None:
  577. modelHere = sklearn.decomposition.PCA(n_components=numComps)
  578. else:
  579. modelHere = sklearn.decomposition.PCA()
  580. modelHere.fit(dataHere_Cluster)
  581. modelHere_Transform = modelHere.fit_transform(dataHere_Cluster).copy() # samples (subjects) x components
  582. # NOTE: other variables are here that aren't returned; feel free to edit to save out/return for other research studies
  583. compsHere = modelHere.components_.copy() # components x features (psych measures)
  584. explVarHere = modelHere.explained_variance_.copy() # components
  585. explVarRatioHere = modelHere.explained_variance_ratio_.copy() # components
  586. inverseTransformHere = modelHere.inverse_transform(modelHere_Transform).copy() # samples (subjects) x features (psych measures) -> original space
  587. pcaScoreHere = modelHere.score_samples(dataHere_Cluster).copy() # samples (subjects) -> NOTE: this is the log-likelihood of each sample (subject)
  588. loadingsHere = compsHere.T * np.sqrt(explVarHere) # features (psych measures) x components
  589. print(f"Cluster {labelIx+1}: the 1st PC explains {np.round(explVarRatioHere[0]*100,2)}% of the variance (of {np.round(np.nansum(explVarRatioHere)*100,2)}% total).")
  590. pcaScores_All[:,labelIx] = pcaScoreHere.copy()
  591. loadings_All['cluster_'+str(labelIx+1)] = loadingsHere.copy()
  592. return pcaScores_All, loadings_All
  593. ####################################################################################
  594. # Helper function to perform min-max normalization. Also known as min-max scaling and feature scaling
  595. def minMaxNorm(data,minVal=0,maxVal=1):
  596. normedData = minVal + (((data-np.nanmin(data))*(maxVal-minVal))/(np.nanmax(data)-np.nanmin(data)))
  597. return normedData
  598. ####################################################################################
  599. # Helper function for imputation
  600. def add_label(data,attr,name_notnan = 'Training', name_nan = 'Predict'):
  601. null_pos = data[attr][data[attr].isnull()].index
  602. data['Label'] = name_notnan
  603. data['Label'].iloc[null_pos] = name_nan
  604. ####################################################################################
  605. # Helper function for imputation
  606. def mean_median_imputation(data, kind = 'mean'):
  607. if kind=='mean':
  608. return data.fillna(np.mean(data.dropna()))
  609. elif kind == 'median':
  610. return data.fillna(np.median(data.dropna()))
  611. ####################################################################################
  612. # Helper function for imputation
  613. def mode_imputation(data):
  614. return data.fillna(stat.mode(data))

Fingerprints_Cocuzza.py at commit 939f37d, under GPL-3.0 · at the source

Overview

Authors: Carrisa V Cocuzza1,2, Sidhant Chopra3,4, Ashlea Segal5, Loïc Labache1,2, Rowena Chin2, Kaley Joss1, Avram J Holmes1
  1. Department of Psychiatry, Brain Health Institute, Rutgers University, Piscataway, NJ USA
  2. Department of Psychology, Yale University, New Haven, CT USA
  3. Orygen, Melbourne, VIC Australia
  4. Center for Youth Mental Health, University of Melbourne, Melbourne, VIC Australia
  5. Department of Neuroscience, Wu Tsai Institute, Yale University, New Haven, CT USA
Institutions: Rutgers, The State University of New Jersey (United States); Yale University (United States); The University of Melbourne (Australia); Orygen (Australia); Brown University (United States)
Journal: Nature communications, volume 17, issue 1, article 6678
Dates: received 25 June 2025; accepted 23 June 2026; published online 21 July 2026
Type: Research article · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1038/s41467-026-75585-6 · PMID 42481484 · PMCID PMC13388696 · OpenAlex W4410826654
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: human (organism), depression (population), schizophrenia / psychosis (population)
Methods: Connectivity, Statistics, Smoothing, state filtering, decompositions, Machine learning, Graphs, fMRI & imaging
Keywords: Network models, Dynamical systems, Anxiety, Psychosis, Depression
MeSH: Brain*, Mental Disorders*, Nerve Net*, Adult, Brain Mapping, Case-Control Studies, Cognition, Connectome, Female, Humans, Magnetic Resonance Imaging, Male, Young Adult (* major topic)
Topic: Functional Brain Connectivity Studies (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Funding: U.S. Department of Health &amp; Human Services | NIH | National Institute of Mental Health (NIMH) (R01MH120080, R01MH123245); NIMH NIH HHS (RF1 MH123245, R01 MH120080)
Citations: cited by 2 papers (Europe PMC); 179 references in the paper

Abstract

The abstract is not reproduced here: the paper's license (CC BY-NC-ND) does not allow it. Read it in the paper, at the publisher or on Europe PMC.

Repositories

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

HolmesLab/ClinicalNetDynamics

License: GPL-3.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Commit: 939f37da13ad1e1c2e1eeb3d4671251cb325545f, 26 September 2026
Languages: Python (4), R (2)
Size: 11 files, 6 scripts
Software Heritage: not archived
Found in: “Code availability”
Holds: README, license file, 2 notebooks
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (4 files), pandas (2 files), scikit-learn (2 files), tidyverse (2 files), h5py (1 file), igraph (1 file), Matplotlib (1 file), psych (1 file), SciPy (1 file), seaborn (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers
  • 27 September 2026: the link answers
8 files

Zenodo 20074876

License: GPL-3.0
State: the link answers, verified on 27 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: the references
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: NumPy (4 files), pandas (2 files), scikit-learn (2 files), tidyverse (2 files), h5py (1 file), igraph (1 file), Matplotlib (1 file), psych (1 file), SciPy (1 file), seaborn (1 file)
Availability: 1 check, the latest on 27 September 2026: the link answers (HTTP 200)
  • 27 September 2026: the link answers (HTTP 200)
8 files
At the source:

Code availability statement

The paper has a code availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

Read it in the paper: doi.org/10.1038/s41467-026-75585-6.

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:

  • 2 repositories of the authors' code, each at its verified commit, with its license and how the link was found in the paper;
  • 12 scripts, each with its path and the digest of its content;
  • 12 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

The paper has a data availability statement. Its license (CC BY-NC-ND) does not allow reproducing it here; in short, from what the harvester recognized in it:

Read it in the paper: doi.org/10.1038/s41467-026-75585-6.

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, issue, pages, dates, 7 authors, 5 keywords, 13 MeSH terms, 2 funders, 168 references.

Cite

This paper

Cocuzza, C. V., Chopra, S., Segal, A., Labache, L., Chin, R., Joss, K., & Holmes, A. J. (2026). Brain network dynamics reflect psychiatric illness status and transdiagnostic symptom profiles across health and disease. Nature communications, 17(1), 6678. https://doi.org/10.1038/s41467-026-75585-6

BibTeX

@article{cocuzza2026brain,
author = {Cocuzza, Carrisa V and Chopra, Sidhant and Segal, Ashlea and Labache, Loïc and Chin, Rowena and Joss, Kaley and Holmes, Avram J},
title = {{Brain network dynamics reflect psychiatric illness status and transdiagnostic symptom profiles across health and disease}},
journal = {Nature communications},
year = {2026},
month = jul,
volume = {17},
number = {1},
pages = {6678},
publisher = {Nature Publishing Group},
issn = {2041-1723},
doi = {10.1038/s41467-026-75585-6},
url = {https://doi.org/10.1038/s41467-026-75585-6},
pmid = {42481484},
pmcid = {PMC13388696}
}

RIS

TY - JOUR
AU - Cocuzza, Carrisa V
AU - Chopra, Sidhant
AU - Segal, Ashlea
AU - Labache, Loïc
AU - Chin, Rowena
AU - Joss, Kaley
AU - Holmes, Avram J
TI - Brain network dynamics reflect psychiatric illness status and transdiagnostic symptom profiles across health and disease
T2 - Nature communications
J2 - Nat Commun
PY - 2026
DA - 2026/07/21
VL - 17
IS - 1
SP - 6678
SN - 2041-1723
PB - Nature Publishing Group
DO - 10.1038/s41467-026-75585-6
UR - https://doi.org/10.1038/s41467-026-75585-6
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41467-026-75585-6",
"type": "article-journal",
"title": "Brain network dynamics reflect psychiatric illness status and transdiagnostic symptom profiles across health and disease",
"container-title": "Nature communications",
"author": [
{
"family": "Cocuzza",
"given": "Carrisa V"
},
{
"family": "Chopra",
"given": "Sidhant"
},
{
"family": "Segal",
"given": "Ashlea"
},
{
"family": "Labache",
"given": "Loïc"
},
{
"family": "Chin",
"given": "Rowena"
},
{
"family": "Joss",
"given": "Kaley"
},
{
"family": "Holmes",
"given": "Avram J"
}
],
"container-title-short": "Nat Commun",
"volume": "17",
"issue": "1",
"page": "6678",
"DOI": "10.1038/s41467-026-75585-6",
"PMID": "42481484",
"PMCID": "PMC13388696",
"ISSN": "2041-1723",
"publisher": "Nature Publishing Group",
"URL": "https://doi.org/10.1038/s41467-026-75585-6",
"language": "en",
"issued": {
"date-parts": [
[
2026,
7,
21
]
]
}
}

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.1038/s41467-026-75959-w [code]
Charting higher-order models of brain function beyond pairwise interactions.
Journal: Nature communications
In common: h5py, seaborn, scikit-learn, 4 other tools, 13 references
[2] doi:10.21203/rs.3.rs-9326213/v1 [code]
Multi-task fMRI outperforms resting-state fMRI for revealing task-invariant organization of the human brain
Journal: Research Square (preprint)
In common: h5py, seaborn, scikit-learn, 4 other tools, 10 references
[3] doi:10.1038/s41467-026-75745-8 [code]
A language network in the individualized functional connectomes of 1199 human brains doing arbitrary tasks.
Journal: Nature communications
In common: h5py, seaborn, scikit-learn, 4 other tools, 8 references
[4] doi:10.1162/netn.a.570 [code]
Higher-order statistics for constructing centered edge functional connectivity.
Journal: Network neuroscience (Cambridge, Mass.)
In common: igraph, seaborn, scikit-learn, 4 other tools, 9 references
[5] doi:10.1038/s41467-026-72931-6 [code]
Three parsimonious spatiotemporal patterns in cerebellum reveal individual traits in function and behavior.
Journal: Nature communications
In common: seaborn, scikit-learn, pandas, 3 other tools, 8 references
[6] doi:10.1038/s41467-026-73153-6 [code]
Latent neural architecture organising shared aesthetic evaluations of visual artworks.
Journal: Nature communications
In common: h5py, seaborn, scikit-learn, 4 other tools, 8 references
[7] 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, 2 other tools, schizophrenia / psychosis, 7 references
[8] doi:10.1038/s41398-026-04025-2 [code]
Brain energetic landscapes shape state dysregulation in major depressive disorder: a morphological network controllability perspective.
Journal: Translational psychiatry
In common: h5py, seaborn, scikit-learn, 4 other tools, depression, 6 references
[9] doi:10.1038/s44220-026-00680-y [code]
The neuroimaging correlates of depression established across six large-scale population datasets.
Journal: Nature. Mental health
In common: seaborn, tidyverse, scikit-learn, 4 other tools, depression, 6 references
[10] doi:10.7554/elife.110294 [code]
Arousal modulates functional connectivity through structured and hemispherically asymmetric community architecture during wakefulness.
Journal: eLife
In common: seaborn, scikit-learn, pandas, 3 other tools, 7 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.