OSCR

Neuroanatomy reflects individual variability in impulsivity in youth.

Code ↔ Paper

2 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 2 matches
  1. [1] § Methods › Predictive modeling ↔ nbdc_abcd_bpm_smri_impulsivity.ipynb, lines 611 to 760 · score 0.59 · evaluated model, permuting, shuffled, variable, optimized, split · Report a mismatch
  2. [2] § Methods › Predictive modeling ↔ nbdc_abcd_bpm_smri_impulsivity.ipynb, lines 443 to 489 · score 0.53 · ridge regression models, females, thickness, volume, brain · Report a mismatch

Paper

Loaded from Europe PMC by your browser; OSCR does not store it: doi.org · Europe PMC

The paper is loaded when this pane is shown.

The authors' code

Jupyter notebook · 2,873 lines · 116 KB · no license · 2 matches

  1. # %% [markdown]
  2. # ## Import Libraries and Packages
  3. # %%
  4. #general imports
  5. import os
  6. import sys
  7. import warnings
  8. import numpy as np
  9. import pandas as pd
  10. import pickle
  11. #suppress warnings
  12. warnings.filterwarnings('ignore')
  13. #sklearn imports
  14. from sklearn.metrics import explained_variance_score
  15. from sklearn.metrics.pairwise import cosine_similarity
  16. from sklearn.linear_model import Ridge
  17. from sklearn.model_selection import (train_test_split, GridSearchCV, cross_val_score,
  18. GroupKFold, GroupShuffleSplit, StratifiedKFold)
  19. #tqdm import
  20. from tqdm import tqdm
  21. #stats import
  22. from statsmodels.stats.multitest import fdrcorrection as fdr
  23. #plotting import
  24. import matplotlib.pyplot as plt
  25. import seaborn as sns
  26. # %% [markdown]
  27. # ## Load in Data
  28. # %%
  29. #set directories
  30. base_dir = '/Users/elvishadhamala/OneDrive_Northwell/NBDC_PHI/'
  31. results_dir = '/Users/elvishadhamala/Documents/BPM/nbdc_analyses/impulsivity/unrelatedparticipants/'
  32. #define data files to load
  33. data_files = {
  34. 'gen': 'abcd_6.0/core/g.csv',
  35. 'qc': 'abcd_6.0/core/mri/mr_qc.csv',
  36. 'adm': 'abcd_6.0/core/mri/mr_adm.csv',
  37. 'smri': 'abcd_6.0/core/mri/mr_smri.csv',
  38. 'mh': 'abcd_6.0/core/mh.csv'
  39. }
  40. #load all data files
  41. print("Loading ABCD 6.0 data files...")
  42. data = {}
  43. for name, filepath in data_files.items():
  44. data[name] = pd.read_csv(os.path.join(base_dir, filepath), header=0)
  45. print(f"{name}: {data[name].shape}")
  46. print("Data loading complete!")
  47. #extract to individual variables (if you prefer)
  48. gen, qc, adm, smri, mh = data['gen'], data['qc'], data['adm'], data['smri'], data['mh']
  49. # %% [markdown]
  50. # ## Clean Up Data
  51. # %%
  52. #function to apply standard QC criteria for sMRI
  53. def smri_qc_filter(qc_df):
  54. """Apply standard sMRI QC criteria"""
  55. mask = (qc_df.mr_y_qc__incl__smri__t1_indicator == 1) & (qc_df.mr_y_qc__clfind_score < 3)
  56. print(f"sMRI QC: {len(qc_df)} → {mask.sum()} subjects/timepoints")
  57. return qc_df[mask]
  58. #perform sMRI QC
  59. qc_pass = smri_qc_filter(qc)
  60. #get QC-passed participant/session combinations
  61. qc_lookup = qc_pass[['participant_id', 'session_id']]
  62. #filter all other dataframes based on imaging QC
  63. gen_qc = gen.merge(qc_lookup, on=['participant_id', 'session_id'])
  64. mh_qc = mh.merge(qc_lookup, on=['participant_id', 'session_id'])
  65. smri_qc = smri.merge(qc_lookup, on=['participant_id', 'session_id'])
  66. adm_qc = adm.merge(qc_lookup, on=['participant_id', 'session_id'])
  67. print(f"General: {len(gen)} → {len(gen_qc)}")
  68. print(f"Mental Health: {len(mh)} → {len(mh_qc)}")
  69. print(f"Imaging: {len(smri)} → {len(smri_qc)}")
  70. print(f"MR Admin: {len(adm)} → {len(adm_qc)}")
  71. #validation check
  72. def validate_qc(qc_df, other_df, df_name):
  73. qc_set = set(zip(qc_df.participant_id, qc_df.session_id))
  74. data_set = set(zip(other_df.participant_id, other_df.session_id))
  75. invalid = data_set - qc_set
  76. print(f"✓ {df_name}: All data passed QC" if len(invalid) == 0 else f"✗ {df_name}: {len(invalid)} entries didn't pass QC!")
  77. validate_qc(qc_pass, gen_qc, "General")
  78. validate_qc(qc_pass, mh_qc, "Mental Health")
  79. validate_qc(qc_pass, smri_qc, "Imaging")
  80. validate_qc(qc_pass, adm_qc, "MR Admin")
  81. # %%
  82. #find relevant mental health data
  83. #cbcl_cols = [col for col in mh_qc.columns if col.startswith('mh_p_cbcl') and col.endswith('_sum')]
  84. #pps_cols = ['mh_y_pps__severity_score', 'mh_y_pps_count']
  85. #gbi_cols = ['mh_p_gbi_sum']
  86. #bpm_cols = [col for col in mh_qc.columns if col.startswith('mh_t_bpm') and col.endswith('_sum')]
  87. mh_cols = ['mh_y_bisbas__bis_sum',
  88. 'mh_y_bisbas__bas__rr_sum', 'mh_y_bisbas__bas__dr_sum', 'mh_y_bisbas__bas__fs_sum',
  89. 'mh_y_upps__nurg_sum', 'mh_y_upps__purg_sum', 'mh_y_upps__pers_sum', 'mh_y_upps__plan_sum', 'mh_y_upps__sens_sum']
  90. mh_subset = mh_qc[['participant_id', 'session_id'] + mh_cols]
  91. #keep only participants with complete MH data
  92. mh_complete = mh_subset.dropna()
  93. print(f"MH variables: {len(mh_cols)}")
  94. print(f"Complete cases: {len(mh_qc)} → {len(mh_complete)}")
  95. # %%
  96. #get the participant/session combinations with complete MH data
  97. mh_lookup = mh_complete[['participant_id', 'session_id']]
  98. #filter all other dataframes to only include those with complete MH data
  99. gen_mh = gen_qc.merge(mh_lookup, on=['participant_id', 'session_id'])
  100. smri_mh = smri_qc.merge(mh_lookup, on=['participant_id', 'session_id'])
  101. adm_mh = adm_qc.merge(mh_lookup, on=['participant_id', 'session_id'])
  102. print("After filtering for complete MH data:")
  103. print(f"General: {len(gen_qc)} → {len(gen_mh)}")
  104. print(f"Imaging: {len(smri_qc)} → {len(smri_mh)}")
  105. print(f"MR Admin: {len(adm_qc)} → {len(adm_mh)}")
  106. print(f"MH: {len(mh_complete)} (complete cases)")
  107. # ==========================================
  108. # ENSURE COMPLETE IMAGING DATA
  109. # ==========================================
  110. #get participant/session combinations that have BOTH complete MH AND complete imaging
  111. smri_mh = smri_mh.dropna()
  112. complete_imaging_lookup = smri_mh[['participant_id', 'session_id']]
  113. #filter all dataframes to only include those with BOTH complete MH and imaging
  114. gen_complete = gen_mh.merge(complete_imaging_lookup, on=['participant_id', 'session_id'])
  115. smri_complete = smri_mh.merge(complete_imaging_lookup, on=['participant_id', 'session_id'])
  116. adm_complete = adm_mh.merge(complete_imaging_lookup, on=['participant_id', 'session_id'])
  117. mh_complete = mh_complete.merge(complete_imaging_lookup, on=['participant_id', 'session_id'])
  118. print("\nAfter filtering for complete MH AND imaging data:")
  119. print(f"General: {len(gen_mh)} → {len(gen_complete)}")
  120. print(f"Imaging: {len(smri_mh)} → {len(smri_complete)}")
  121. print(f"MR Admin: {len(adm_mh)} → {len(adm_complete)}")
  122. print(f"MH: {len(mh_complete)} → {len(mh_complete)}")
  123. # %%
  124. # ==========================================
  125. # FAMILY FILTERING - ONE PARTICIPANT PER FAMILY
  126. # ==========================================
  127. if(results_dir=='/Users/elvishadhamala/Documents/BPM/nbdc_analyses/impulsivity/unrelatedparticipants/'):
  128. print("\n" + "="*60)
  129. print("FILTERING TO ONE PARTICIPANT PER FAMILY")
  130. print("="*60)
  131. def select_one_participant_per_family(data_dict, family_col='ab_g_stc__design_id__fam',
  132. participant_col='participant_id', session_col='session_id',
  133. random_seed=42):
  134. """
  135. Select one participant per family, keeping all their timepoints
  136. """
  137. print("Selecting one participant per family...")
  138. # Get participant-family mapping from general data
  139. participant_info = data_dict['gen_complete'][[participant_col, family_col, session_col]].copy()
  140. participant_info = participant_info.dropna(subset=[family_col])
  141. print(f"Total participant-session records: {len(participant_info)}")
  142. print(f"Unique participants: {participant_info[participant_col].nunique()}")
  143. print(f"Unique families: {participant_info[family_col].nunique()}")
  144. # Get unique participant-family pairs
  145. participant_family_map = participant_info[[participant_col, family_col]].drop_duplicates()
  146. print(f"Participant-family combinations: {len(participant_family_map)}")
  147. # Count participants per family
  148. family_counts = participant_family_map[family_col].value_counts()
  149. print(f"Family size distribution:")
  150. for size, count in family_counts.value_counts().sort_index().items():
  151. print(f" {size} participant(s): {count} families")
  152. # For each family, randomly select one participant
  153. np.random.seed(random_seed)
  154. selected_participants = []
  155. for family_id in participant_family_map[family_col].unique():
  156. family_participants = participant_family_map[
  157. participant_family_map[family_col] == family_id
  158. ][participant_col].tolist()
  159. # Randomly select one participant from this family
  160. selected_participant = np.random.choice(family_participants)
  161. selected_participants.append(selected_participant)
  162. print(f"Selected {len(selected_participants)} participants (one per family)")
  163. # Create lookup for selected participants (all their sessions)
  164. selected_participant_sessions = []
  165. for participant_id in selected_participants:
  166. participant_sessions = participant_info[
  167. participant_info[participant_col] == participant_id
  168. ][[participant_col, session_col]]
  169. selected_participant_sessions.append(participant_sessions)
  170. if selected_participant_sessions:
  171. selected_lookup_df = pd.concat(selected_participant_sessions, ignore_index=True)
  172. else:
  173. selected_lookup_df = pd.DataFrame(columns=[participant_col, session_col])
  174. print(f"Total participant-session combinations: {len(selected_lookup_df)}")
  175. # Filter all dataframes
  176. filtered_data = {}
  177. for name, df in data_dict.items():
  178. if participant_col in df.columns and session_col in df.columns:
  179. filtered_df = df.merge(selected_lookup_df, on=[participant_col, session_col])
  180. filtered_data[name] = filtered_df
  181. print(f"Filtered {name}: {len(df)} → {len(filtered_df)} records")
  182. else:
  183. filtered_data[name] = df
  184. # Create summary DataFrame
  185. selected_summary = pd.DataFrame({
  186. participant_col: selected_participants
  187. })
  188. # Add family info
  189. selected_summary = selected_summary.merge(
  190. participant_family_map, on=participant_col
  191. )
  192. print(f"\nFamily filtering summary:")
  193. print(f" Original participants: {participant_info[participant_col].nunique()}")
  194. print(f" Original families: {participant_info[family_col].nunique()}")
  195. print(f" Selected participants: {len(selected_participants)}")
  196. print(f" Selected families: {selected_summary[family_col].nunique()}")
  197. print(f" Records per dataset: {len(selected_lookup_df)}")
  198. # Verify one participant per family
  199. family_check = selected_summary[family_col].value_counts()
  200. max_participants_per_family = family_check.max()
  201. if max_participants_per_family > 1:
  202. print(f" ⚠️ WARNING: Some families have more than one participant (max: {max_participants_per_family})")
  203. else:
  204. print(f" ✅ Confirmed: Each family has exactly one participant")
  205. return filtered_data, selected_summary
  206. # Apply family filtering
  207. print("Applying family filtering to complete datasets...")
  208. # Create dictionary of complete datasets
  209. complete_data = {
  210. 'gen_complete': gen_complete,
  211. 'smri_complete': smri_complete,
  212. 'mh_complete': mh_complete,
  213. 'adm_complete': adm_complete
  214. }
  215. # Apply family filtering
  216. filtered_family_data, selected_families = select_one_participant_per_family(
  217. complete_data,
  218. family_col='ab_g_stc__design_id__fam',
  219. participant_col='participant_id',
  220. session_col='session_id',
  221. random_seed=42 # Set seed for reproducibility
  222. )
  223. # Update your dataframes
  224. gen_complete = filtered_family_data['gen_complete']
  225. smri_complete = filtered_family_data['smri_complete']
  226. mh_complete = filtered_family_data['mh_complete']
  227. adm_complete = filtered_family_data['adm_complete']
  228. print(f"\nFinal sample sizes after family filtering:")
  229. print(f"General: {len(gen_complete)}")
  230. print(f"Imaging: {len(smri_complete)}")
  231. print(f"MH: {len(mh_complete)}")
  232. print(f"Admin: {len(adm_complete)}")
  233. # Show sample sizes by timepoint
  234. print(f"\nSample sizes by timepoint after family filtering:")
  235. timepoints = sorted(gen_complete['session_id'].unique())
  236. for tp in timepoints:
  237. n_participants = len(gen_complete[gen_complete['session_id'] == tp])
  238. n_families = gen_complete[gen_complete['session_id'] == tp]['ab_g_stc__design_id__fam'].nunique()
  239. n_males = np.sum(gen_complete[gen_complete['session_id'] == tp]['ab_g_stc__cohort_sex'] == 1)
  240. n_females = np.sum(gen_complete[gen_complete['session_id'] == tp]['ab_g_stc__cohort_sex'] == 2)
  241. print(f" {tp}: {n_participants} participants from {n_families} families,{n_males} males, {n_females} females")
  242. # Final verification
  243. print(f"\nFinal verification:")
  244. total_participants = gen_complete['participant_id'].nunique()
  245. total_families = gen_complete['ab_g_stc__design_id__fam'].nunique()
  246. print(f" Unique participants: {total_participants}")
  247. print(f" Unique families: {total_families}")
  248. if total_participants == total_families:
  249. print(f" ✅ SUCCESS: Participants = Families (one participant per family)")
  250. else:
  251. print(f" ⚠️ WARNING: Participants ≠ Families")
  252. # Show family information
  253. print(f"\nFamily selection details:")
  254. print(f" Random seed used: 42")
  255. print(f" Selection method: Random choice per family")
  256. print(f" All timepoints preserved for selected participants")
  257. print("✅ Family filtering complete!")
  258. else:
  259. print("Family filtering skipped.")
  260. # %%
  261. #extract structural columns, excluding the summary totals
  262. thickness_cols = [col for col in smri_complete.columns
  263. if col.startswith('mr_y_smri__thk__dsk')
  264. and col.endswith('mean')
  265. and col != 'mr_y_smri__thk__dsk__lh_mean'
  266. and col != 'mr_y_smri__thk__dsk__rh_mean'
  267. and col != 'mr_y_smri__thk__dsk_mean']
  268. area_cols = [col for col in smri_complete.columns
  269. if col.startswith('mr_y_smri__area__dsk')
  270. and col.endswith('sum')
  271. and col != 'mr_y_smri__area__dsk__lh_sum'
  272. and col != 'mr_y_smri__area__dsk__rh_sum'
  273. and col != 'mr_y_smri__area__dsk_sum']
  274. volume_cols = [col for col in smri_complete.columns
  275. if col.startswith('mr_y_smri__vol__dsk')
  276. and col.endswith('sum')
  277. and col != 'mr_y_smri__vol__dsk__lh_sum'
  278. and col != 'mr_y_smri__vol__dsk__rh_sum'
  279. and col != 'mr_y_smri__vol__dsk_sum']
  280. subc = ['_ab', '_ag', '_cbc', '_cd', '_hc', '_pl', '_pt', '_th', '_vdc', '_bs']
  281. subcvolume_cols = [col for col in smri_complete.columns
  282. if col.startswith('mr_y_smri__vol__aseg')
  283. and col.endswith('sum')
  284. and any(phrase in col for phrase in subc)]
  285. icv_col = ['mr_y_smri__vol__aseg__icv_sum']
  286. # all structural measures (individual regions only)
  287. all_structural_cols = thickness_cols + area_cols + volume_cols + subcvolume_cols + icv_col
  288. smri_all = smri_complete[['participant_id', 'session_id'] + all_structural_cols].copy()
  289. smri_all_complete = smri_all.dropna()
  290. print(f"All structural measures: {len(smri_all)} → {len(smri_all_complete)} complete cases")
  291. print(f"Total measures: {len(all_structural_cols)} ({len(thickness_cols)} thickness + {len(area_cols)} area + {len(volume_cols)} volume + {len(subcvolume_cols)} subcortical volume + 1 ICV)")
  292. # %%
  293. #make a copy of the sMRI data to work with
  294. smri_normalized = smri_all_complete.copy()
  295. #get the ICV values for division
  296. icv_values = smri_normalized['mr_y_smri__vol__aseg__icv_sum']
  297. #apply ICV proportion correction to area columns
  298. for col in area_cols:
  299. smri_normalized[col] = smri_normalized[col] / icv_values
  300. #apply ICV proportion correction to volume columns
  301. for col in volume_cols:
  302. smri_normalized[col] = smri_normalized[col] / icv_values
  303. #apply ICV proportion correction to subcortical volume columns
  304. for col in subcvolume_cols:
  305. smri_normalized[col] = smri_normalized[col] / icv_values
  306. #thickness columns remain unchanged
  307. print(f"Normalized {len(area_cols)} area columns and {len(volume_cols)} volume columns and {len(subcvolume_cols)} subcortical volume columns by ICV")
  308. print(f"Thickness columns ({len(thickness_cols)}) left unchanged")
  309. print(f"Final dataset shape: {smri_normalized.shape}")
  310. #check the normalization worked for area
  311. print(f"\nExample area column before/after normalization:")
  312. print(f"Original range: {smri_all_complete[area_cols[0]].min():.2f} - {smri_all_complete[area_cols[0]].max():.2f}")
  313. print(f"Normalized range: {smri_normalized[area_cols[0]].min():.6f} - {smri_normalized[area_cols[0]].max():.6f}")
  314. #check the normalization worked for volume
  315. print(f"\nExample volume column before/after normalization:")
  316. print(f"Original range: {smri_all_complete[volume_cols[0]].min():.2f} - {smri_all_complete[volume_cols[0]].max():.2f}")
  317. print(f"Normalized range: {smri_normalized[volume_cols[0]].min():.6f} - {smri_normalized[volume_cols[0]].max():.6f}")
  318. #check the normalization worked for subcortical volume
  319. print(f"\nExample subcortical volume column before/after normalization:")
  320. print(f"Original range: {smri_all_complete[subcvolume_cols[0]].min():.2f} - {smri_all_complete[subcvolume_cols[0]].max():.2f}")
  321. print(f"Normalized range: {smri_normalized[subcvolume_cols[0]].min():.6f} - {smri_normalized[subcvolume_cols[0]].max():.6f}")
  322. # %%
  323. #define ML configuration
  324. ML_CONFIG = {
  325. 'rep': 100, # number of repetitions
  326. 'k': 3, # number of CV folds
  327. 'train_size': 0.80, # training set proportion
  328. 'test_size': 0.20, # test set proportion
  329. 'max_iter': 1000000,
  330. 'solver': 'lsqr',
  331. 'alphas': [0, 0.00001, 0.0001, 0.001, 0.004, 0.007, 0.01, 0.04, 0.07, 0.1,
  332. 0.4, 0.7, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5, 10, 15, 20, 30, 40, 50,
  333. 60, 70, 80, 100]
  334. }
  335. #define how many null models to do
  336. n_null = 1000
  337. print(f"ML Configuration loaded:")
  338. print(f" Repetitions: {ML_CONFIG['rep']}")
  339. print(f" CV folds: {ML_CONFIG['k']}")
  340. print(f" Train/test split: {ML_CONFIG['train_size']:.1%}/{ML_CONFIG['test_size']:.1%}")
  341. print(f" Alpha values: {len(ML_CONFIG['alphas'])} values")
  342. # %%
  343. def train_test_ridge_model(X, Y, site, beh_names, brain_type, group_name, timepoint,
  344. all_results, results_dir, config=ML_CONFIG):
  345. """
  346. Train ans Test ridge regression models with nested CV, apply Haufe transformation
  347. Parameters:
  348. -----------
  349. X : array
  350. Brain features (n_samples x n_features)
  351. Y : array
  352. Behavioral targets (n_samples x n_behaviors)
  353. site : array
  354. Site IDs for grouping
  355. beh_names : list
  356. Names of behavioral variables
  357. brain_type : str
  358. Type of brain data ('thickness', 'area', 'volume')
  359. group_name : str
  360. Group identifier ('all', 'male', 'female')
  361. timepoint : str
  362. Timepoint identifier
  363. all_results : dict
  364. Master results dictionary to update
  365. results_dir : str
  366. Directory to save results
  367. config : dict
  368. ML configuration parameters
  369. Returns:
  370. --------
  371. dict : Results dictionary with all metrics
  372. """
  373. rep = config['rep']
  374. k = config['k']
  375. train_size = config['train_size']
  376. alphas = config['alphas']
  377. param_grid = {'alpha': alphas}
  378. #get dimensions of X and Y data
  379. n_feat = X.shape[1]
  380. n_beh = Y.shape[1]
  381. print(f"\nTraining {brain_type} → MH model")
  382. print(f"Group: {group_name}, Timepoint: {timepoint}")
  383. print(f"Samples: {X.shape[0]}, Features: {n_feat}, Behaviors: {n_beh}")
  384. #initialize result arrays
  385. results = {
  386. 'r2': np.zeros([n_beh, rep]),
  387. 'var': np.zeros([n_beh, rep]),
  388. 'corr': np.zeros([n_beh, rep]),
  389. 'opt_alpha': np.zeros([n_beh, rep]),
  390. 'featimp': np.zeros([n_beh, rep, n_feat]),
  391. 'featimp_haufe': np.zeros([n_beh, rep, n_feat]),
  392. 'config': {
  393. 'brain_type': brain_type,
  394. 'group_name': group_name,
  395. 'timepoint': timepoint,
  396. 'n_samples': X.shape[0],
  397. 'n_features': n_feat,
  398. 'n_behaviors': n_beh,
  399. 'behavior_names': beh_names
  400. }
  401. }
  402. #train model for each behavior
  403. for b in range(n_beh):
  404. beh_data = Y[:, b]
  405. print(f"\n Training {beh_names[b]} ({b+1}/{n_beh})")
  406. #progress bar for repetitions
  407. pbar = tqdm(range(rep), desc=f" Reps",
  408. bar_format='{l_bar}{bar}| {n_fmt}/{total_fmt} [{elapsed}<{remaining}]')
  409. #repetitions loop with progress bar
  410. for p in pbar:
  411. # Train/test split
  412. train_inds, test_inds = next(
  413. GroupShuffleSplit(test_size=1-train_size, n_splits=1, random_state=p)
  414. .split(X, groups=site)
  415. )
  416. #split data
  417. x_train, x_test = X[train_inds], X[test_inds]
  418. y_train, y_test = beh_data[train_inds], beh_data[test_inds]
  419. site_train = site[train_inds]
  420. #convert to double
  421. y_train, y_test = np.double(y_train), np.double(y_test)
  422. #nested CV for hyperparameter optimization
  423. regr = Ridge(normalize=True, max_iter=config['max_iter'], solver=config['solver'])
  424. cv_split = GroupKFold(n_splits=k)
  425. grid_search = GridSearchCV(
  426. estimator=regr,
  427. param_grid=param_grid,
  428. cv=cv_split,
  429. scoring='explained_variance',
  430. n_jobs=-1,
  431. verbose=0
  432. )
  433. #fit and get best parameters
  434. grid_search.fit(x_train, y_train, groups=site_train)
  435. best_alpha = grid_search.best_params_['alpha']
  436. results['opt_alpha'][b, p] = best_alpha
  437. #train final model with optimized alpha
  438. model = Ridge(alpha=best_alpha, normalize=True,
  439. max_iter=config['max_iter'], solver=config['solver'])
  440. model.fit(x_train, y_train)
  441. #evaluate model
  442. results['r2'][b, p] = model.score(x_test, y_test)
  443. preds = model.predict(x_test).ravel()
  444. results['var'][b, p] = explained_variance_score(y_test, preds)
  445. corr_val = np.corrcoef(y_test.ravel(), preds)[1, 0]
  446. results['corr'][b, p] = corr_val
  447. #feature importance (original and Haufe-transformed)
  448. results['featimp'][b, p, :] = model.coef_
  449. #haufe transformation
  450. y_train_preds = model.predict(x_train).ravel()
  451. cov_x = np.cov(x_train.T)
  452. cov_y = np.cov(y_train_preds)
  453. results['featimp_haufe'][b, p, :] = np.matmul(cov_x, model.coef_) * (1/cov_y)
  454. #update progress bar with current correlation
  455. pbar.set_postfix({'corr': f'{corr_val:.3f}'})
  456. pbar.close()
  457. #print summary for this behavior
  458. mean_corr = np.mean(results['corr'][b, :])
  459. std_corr = np.std(results['corr'][b, :])
  460. print(f" Final: {mean_corr:.3f} ± {std_corr:.3f}")
  461. #create result key and add to master results
  462. result_key = f"{timepoint}_{brain_type}_{group_name}"
  463. all_results['models'][result_key] = results
  464. #add to summary
  465. all_results['summary'][result_key] = {
  466. 'mean_correlation': float(np.mean(results['corr'])),
  467. 'std_correlation': float(np.std(results['corr'])),
  468. 'mean_r2': float(np.mean(results['r2'])),
  469. 'std_r2': float(np.std(results['r2'])),
  470. 'mean_explained_var': float(np.mean(results['var'])),
  471. 'std_explained_var': float(np.std(results['var'])),
  472. }
  473. #save incremental backup
  474. backup_filename = f"{results_dir}/all_results_backup.pkl"
  475. with open(backup_filename, 'wb') as f:
  476. pickle.dump(all_results, f)
  477. print(f"✓ Saved to master results: {result_key}")
  478. print(f"✓ Mean correlation: {np.mean(results['corr']):.3f}")
  479. return results
  480. # %%
  481. def generate_null_models(X, Y, site, beh_names, brain_type, group_name, timepoint,
  482. real_results, all_results, results_dir, n_null, config=ML_CONFIG):
  483. """
  484. Generate null models with permuted behavioral data using random alphas from real models
  485. Parameters:
  486. -----------
  487. X : array
  488. Brain features (n_samples x n_features)
  489. Y : array
  490. Behavioral targets (n_samples x n_behaviors)
  491. site : array
  492. Site IDs for grouping
  493. beh_names : list
  494. Names of behavioral variables
  495. brain_type : str
  496. Type of brain data ('thickness', 'area', 'volume')
  497. group_name : str
  498. Group identifier ('all', 'male', 'female')
  499. timepoint : str
  500. Timepoint identifier
  501. real_results : dict
  502. Results from real models (contains optimized alphas)
  503. all_results : dict
  504. Master results dictionary to update
  505. results_dir : str
  506. Directory to save results
  507. n_null : int
  508. Number of null permutations to generate
  509. config : dict
  510. ML configuration parameters (default: ML_CONFIG)
  511. Returns:
  512. --------
  513. dict : Null results dictionary
  514. """
  515. train_size = config['train_size']
  516. #get dimensions of X and Y data
  517. n_feat = X.shape[1]
  518. n_beh = Y.shape[1]
  519. print(f"\nGenerating {n_null} NULL {brain_type} → MH models")
  520. print(f"Group: {group_name}, Timepoint: {timepoint}")
  521. print(f"Samples: {X.shape[0]}, Features: {n_feat}, Behaviors: {n_beh}")
  522. #initialize result arrays
  523. null_results = {
  524. 'r2': np.zeros([n_beh, n_null]),
  525. 'var': np.zeros([n_beh, n_null]),
  526. 'corr': np.zeros([n_beh, n_null]),
  527. 'alpha_used': np.zeros([n_beh, n_null]),
  528. 'config': {
  529. 'brain_type': brain_type,
  530. 'group_name': group_name,
  531. 'timepoint': timepoint,
  532. 'n_samples': X.shape[0],
  533. 'n_features': n_feat,
  534. 'n_behaviors': n_beh,
  535. 'behavior_names': beh_names,
  536. 'n_null': n_null,
  537. 'model_type': 'null'
  538. }
  539. }
  540. #generate null models for each behavior
  541. for b in range(n_beh):
  542. beh_data = Y[:, b]
  543. print(f"\n Generating NULL {beh_names[b]} ({b+1}/{n_beh})")
  544. #get optimized alphas from real model for this behavior
  545. if real_results is not None and 'opt_alpha' in real_results:
  546. available_alphas = real_results['opt_alpha'][b, :]
  547. else:
  548. available_alphas = np.array([1.0])
  549. print(f" Warning: No real model alphas available, using default")
  550. #permute behavioral data while preserving site structure
  551. unique_sites = np.unique(site)
  552. permuted_beh = np.zeros_like(beh_data)
  553. for site_id in unique_sites:
  554. site_mask = site == site_id
  555. site_indices = np.where(site_mask)[0]
  556. permuted_indices = np.random.permutation(site_indices)
  557. permuted_beh[site_indices] = beh_data[permuted_indices]
  558. #null models loop
  559. for p in range(n_null):
  560. # Train/test split (same random state pattern as real models)
  561. train_inds, test_inds = next(
  562. GroupShuffleSplit(test_size=1-train_size, n_splits=1, random_state=p)
  563. .split(X, groups=site)
  564. )
  565. #split data
  566. x_train, x_test = X[train_inds], X[test_inds]
  567. y_train, y_test = permuted_beh[train_inds], permuted_beh[test_inds]
  568. #convert to double
  569. y_train, y_test = np.double(y_train), np.double(y_test)
  570. #randomly select alpha from real model's optimized alphas
  571. random_alpha = np.random.choice(available_alphas)
  572. null_results['alpha_used'][b, p] = random_alpha
  573. #train null model with random alpha
  574. model = Ridge(alpha=random_alpha, normalize=True,
  575. max_iter=config['max_iter'], solver=config['solver'])
  576. model.fit(x_train, y_train)
  577. #evaluate model
  578. null_results['r2'][b, p] = model.score(x_test, y_test)
  579. preds = model.predict(x_test).ravel()
  580. null_results['var'][b, p] = explained_variance_score(y_test, preds)
  581. corr_val = np.corrcoef(y_test.ravel(), preds)[1, 0]
  582. null_results['corr'][b, p] = corr_val
  583. #print summary for this behavior
  584. mean_corr = np.mean(null_results['corr'][b, :])
  585. std_corr = np.std(null_results['corr'][b, :])
  586. print(f" NULL Final: {mean_corr:.3f} ± {std_corr:.3f}")
  587. #create result key and add to master results
  588. null_key = f"{timepoint}_{brain_type}_{group_name}"
  589. all_results['null_models'][null_key] = null_results
  590. #add to null summary
  591. all_results['null_summary'][null_key] = {
  592. 'mean_null_correlation': float(np.mean(null_results['corr'])),
  593. 'std_null_correlation': float(np.std(null_results['corr'])),
  594. 'mean_null_r2': float(np.mean(null_results['r2'])),
  595. 'std_null_r2': float(np.std(null_results['r2'])),
  596. 'mean_null_var': float(np.mean(null_results['var'])),
  597. 'std_null_var': float(np.std(null_results['var'])),
  598. 'n_null': n_null
  599. }
  600. #save incremental backup
  601. backup_filename = f"{results_dir}/null_results_backup.pkl"
  602. with open(backup_filename, 'wb') as f:
  603. pickle.dump(all_results, f)
  604. print(f"✓ Saved NULL to master results: {null_key}")
  605. print(f"✓ Mean NULL correlation: {np.mean(null_results['corr']):.3f}")
  606. return null_results
  607. # %%
  608. # gt unique timepoints and define brain types
  609. timepoints = sorted(smri_normalized['session_id'].unique())
  610. sexes = ['all', 'female', 'male']
  611. brain_types = {
  612. 'thickness': thickness_cols,
  613. 'area': area_cols,
  614. 'volume': volume_cols,
  615. 'subcvolume': subcvolume_cols
  616. }
  617. final_filename = f"{results_dir}/all_results_complete.pkl"
  618. if os.path.exists(final_filename):
  619. print(f"Loading existing results from {final_filename}")
  620. with open(final_filename, 'rb') as f:
  621. all_results = pickle.load(f)
  622. print(f"Loaded {len(all_results['models'])} existing models")
  623. print(f"Loaded {len(all_results['null_models'])} existing null models")
  624. else:
  625. print("Creating new results dictionary")
  626. #master dictionary to store all results (including null models)
  627. all_results = {
  628. 'models': {}, #real model results
  629. 'null_models': {}, #null model results
  630. 'metadata': { #analysis info
  631. 'ml_config': ML_CONFIG,
  632. 'mh_behaviors': mh_cols,
  633. 'feature_columns': {
  634. 'thickness': thickness_cols,
  635. 'area': area_cols,
  636. 'volume': volume_cols,
  637. 'subcvolume': subcvolume_cols
  638. },
  639. 'timepoints': [],
  640. 'brain_types': ['thickness', 'area', 'volume', 'subcvolume'],
  641. 'sex_groups': ['all', 'female', 'male']
  642. },
  643. 'summary': {}, #real model summaries
  644. 'null_summary': {} #null model summaries
  645. }
  646. print("Starting ML Pipeline...")
  647. print(f"Timepoints: {timepoints}")
  648. print(f"Brain types: {list(brain_types.keys())}")
  649. print(f"Groups: {sexes}")
  650. print(f"Results will be saved to: {results_dir}")
  651. print("="*60)
  652. #main loops
  653. for tp in timepoints:
  654. print(f"\n{'='*20} TIMEPOINT {tp} {'='*20}")
  655. #add timepoint to metadata if not already there
  656. if tp not in all_results['metadata']['timepoints']:
  657. all_results['metadata']['timepoints'].append(tp)
  658. #get data for this timepoint
  659. smri_tp = smri_normalized[smri_normalized['session_id'] == tp].copy()
  660. mh_tp = mh_complete[mh_complete['session_id'] == tp].copy()
  661. gen_tp = gen_mh[gen_mh['session_id'] == tp].copy() # Get site and demographics
  662. #merge all datasets
  663. merged_tp = smri_tp.merge(mh_tp, on=['participant_id', 'session_id'])
  664. merged_tp = merged_tp.merge(gen_tp[['participant_id', 'session_id', 'ab_g_dyn__design_site', 'ab_g_stc__cohort_sex']],
  665. on=['participant_id', 'session_id'])
  666. if len(merged_tp) == 0:
  667. print(f"No data for timepoint {tp}, skipping...")
  668. continue
  669. print(f"Total participants at timepoint {tp}: {len(merged_tp)}")
  670. #get site information
  671. site = merged_tp['ab_g_dyn__design_site'].values
  672. #prepare behavioral data
  673. Y = merged_tp[mh_cols].values
  674. #loop through sexes
  675. for sex in sexes:
  676. print(f"\n--- GROUP: {sex.upper()} ---")
  677. #filter by sex
  678. if sex == 'all':
  679. data_subset = merged_tp.copy()
  680. Y_subset = Y.copy()
  681. site_subset = site.copy()
  682. elif sex == 'male':
  683. sex_mask = merged_tp['ab_g_stc__cohort_sex'] == 1 #'1' for male
  684. data_subset = merged_tp[sex_mask].copy()
  685. Y_subset = Y[sex_mask]
  686. site_subset = site[sex_mask]
  687. elif sex == 'female':
  688. sex_mask = merged_tp['ab_g_stc__cohort_sex'] == 2 #'2' for female
  689. data_subset = merged_tp[sex_mask].copy()
  690. Y_subset = Y[sex_mask]
  691. site_subset = site[sex_mask]
  692. if len(data_subset) < 50: # Minimum sample size
  693. print(f"Skipping {sex} group (n={len(data_subset)}, too small)")
  694. continue
  695. print(f"{sex} group: {len(data_subset)} participants")
  696. print(f"Number of sites: {len(np.unique(site_subset))}")
  697. #loop through brain types
  698. #loop through brain types
  699. for brain_type, feature_cols in brain_types.items():
  700. result_key = f"{tp}_{brain_type}_{sex}"
  701. # Check if this model already exists
  702. if result_key in all_results['models']:
  703. print(f" {result_key} already exists, skipping...")
  704. continue
  705. print(f"\nTraining {brain_type} model for {sex} at timepoint {tp}...")
  706. #prepare brain data for this type
  707. X_brain = data_subset[feature_cols].values
  708. # ==========================================
  709. # REAL MODEL
  710. # ==========================================
  711. print(" Training REAL model...")
  712. results = train_test_ridge_model(
  713. X_brain, Y_subset, site_subset, mh_cols,
  714. brain_type, sex, tp, all_results, results_dir
  715. )
  716. # ==========================================
  717. # NULL MODEL (shuffled Y data)
  718. # ==========================================
  719. print(" Training NULL model...")
  720. # Generate null models using same structure
  721. null_results = generate_null_models(
  722. X_brain, Y_subset, site_subset, mh_cols,
  723. brain_type, sex, tp, results, all_results, results_dir,
  724. config=ML_CONFIG, n_null=n_null
  725. )
  726. result_key = f"{tp}_{brain_type}_{sex}"
  727. print(f" Completed: {result_key}")
  728. print(f" Real mean correlation: {np.mean(results['corr']):.3f}")
  729. print(f" Null mean correlation: {np.mean(null_results['corr']):.3f}")
  730. print(f" Improvement: {np.mean(results['corr']) - np.mean(null_results['corr']):.3f}")
  731. print("-" * 40)
  732. #final comprehensive save
  733. print(f"\n{'='*50}")
  734. print("SAVING FINAL RESULTS...")
  735. #add final metadata
  736. all_results['metadata']['total_models'] = len(all_results['models'])
  737. all_results['metadata']['total_null_models'] = len(all_results['null_models'])
  738. #save final complete results
  739. final_filename = f"{results_dir}/all_results_complete.pkl"
  740. with open(final_filename, 'wb') as f:
  741. pickle.dump(all_results, f)
  742. #save just the summary for quick access
  743. summary_filename = f"{results_dir}/summary_only.pkl"
  744. with open(summary_filename, 'wb') as f:
  745. pickle.dump(all_results['summary'], f)
  746. #save null summary
  747. null_summary_filename = f"{results_dir}/null_summary_only.pkl"
  748. with open(null_summary_filename, 'wb') as f:
  749. pickle.dump(all_results['null_summary'], f)
  750. #save just metadata
  751. metadata_filename = f"{results_dir}/metadata_only.pkl"
  752. with open(metadata_filename, 'wb') as f:
  753. pickle.dump(all_results['metadata'], f)
  754. print("\nML Pipeline completed!")
  755. print("🎉 FINAL RESULTS SAVED!")
  756. print(f"📁 Directory: {results_dir}/")
  757. print(f"📊 Total real models: {len(all_results['models'])}")
  758. print(f"📊 Total null models: {len(all_results['null_models'])}")
  759. print("📋 Files saved:")
  760. print(" - all_results_complete.pkl")
  761. print(" - summary_only.pkl")
  762. print(" - null_summary_only.pkl")
  763. print(" - metadata_only.pkl")
  764. print(" - all_results_backup.pkl (latest backup)")
  765. print(f"📊 Models in final results:")
  766. for key in sorted(all_results['models'].keys()):
  767. print(f" ✓ {key}")
  768. # %%
  769. def generate_brain_predictions_from_existing_models(all_results, merged_data_dict, config):
  770. """
  771. Generate brain predictions using the same methodology as existing models
  772. but store predictions and test indices for MLM analysis
  773. """
  774. brain_predictions_dict = {}
  775. # Get metadata
  776. mh_cols = all_results['metadata']['mh_behaviors']
  777. feature_columns = all_results['metadata']['feature_columns']
  778. for result_key, brain_results in all_results['models'].items():
  779. tp, brain_type, sex = result_key.split('_')
  780. print(f"Generating predictions for {result_key}...")
  781. if tp not in merged_data_dict:
  782. continue
  783. merged_tp = merged_data_dict[tp]
  784. feature_cols = feature_columns[brain_type]
  785. # Filter by sex
  786. if sex == 'all':
  787. data_subset = merged_tp.copy()
  788. elif sex == 'male':
  789. sex_mask = merged_tp['ab_g_stc__cohort_sex'] == 1
  790. data_subset = merged_tp[sex_mask].copy()
  791. elif sex == 'female':
  792. sex_mask = merged_tp['ab_g_stc__cohort_sex'] == 2
  793. data_subset = merged_tp[sex_mask].copy()
  794. if len(data_subset) < 50:
  795. continue
  796. # Prepare data
  797. X_brain = data_subset[feature_cols].values
  798. Y = data_subset[mh_cols].values
  799. site = data_subset['ab_g_dyn__design_site'].values
  800. # Storage for all iterations
  801. all_predictions = []
  802. all_true_values = []
  803. all_test_indices = []
  804. all_test_data = []
  805. # Generate predictions using same methodology as original models
  806. for p in range(config['rep']):
  807. # Same train/test split as original
  808. train_inds, test_inds = next(
  809. GroupShuffleSplit(test_size=config['test_size'], n_splits=1, random_state=p)
  810. .split(X_brain, groups=site)
  811. )
  812. X_train, X_test = X_brain[train_inds], X_brain[test_inds]
  813. Y_train, Y_test = Y[train_inds], Y[test_inds]
  814. site_train = site[train_inds]
  815. test_data = data_subset.iloc[test_inds].copy()
  816. # Get optimized alphas from original results
  817. opt_alphas = brain_results['opt_alpha'][:, p] # alphas for this iteration
  818. iteration_predictions = []
  819. # Train model for each behavior
  820. for b, behavior in enumerate(mh_cols):
  821. y_train_beh = Y_train[:, b]
  822. best_alpha = opt_alphas[b]
  823. # Train Ridge model with optimized alpha
  824. model = Ridge(alpha=best_alpha, normalize=True,
  825. max_iter=config['max_iter'], solver=config['solver'])
  826. model.fit(X_train, y_train_beh)
  827. # Generate predictions
  828. predictions = model.predict(X_test)
  829. iteration_predictions.append(predictions)
  830. # Store results
  831. iteration_predictions = np.column_stack(iteration_predictions)
  832. all_predictions.append(iteration_predictions)
  833. all_true_values.append(Y_test)
  834. all_test_indices.append(test_inds)
  835. all_test_data.append(test_data)
  836. brain_predictions_dict[result_key] = {
  837. 'predictions': all_predictions, # List of arrays: [iter][sample, behavior]
  838. 'true_values': all_true_values, # List of arrays: [iter][sample, behavior]
  839. 'test_indices': all_test_indices, # List of arrays: [iter][sample]
  840. 'test_data': all_test_data, # List of DataFrames: [iter]
  841. 'behaviors': mh_cols
  842. }
  843. print(f" Generated {len(all_predictions)} iterations of predictions")
  844. return brain_predictions_dict
  845. # %%
  846. def create_merged_data_dict():
  847. """
  848. Create merged_data_dict using the same data preparation as your ML pipeline
  849. """
  850. print("Creating merged_data_dict...")
  851. # Get unique timepoints (same as your original code)
  852. timepoints = sorted(smri_normalized['session_id'].unique())
  853. merged_data_dict = {}
  854. for tp in timepoints:
  855. print(f"Processing timepoint {tp}...")
  856. # Get data for this timepoint (same as your original code)
  857. smri_tp = smri_normalized[smri_normalized['session_id'] == tp].copy()
  858. mh_tp = mh_complete[mh_complete['session_id'] == tp].copy()
  859. gen_tp = gen_mh[gen_mh['session_id'] == tp].copy()
  860. # Merge all datasets (same as your original code)
  861. merged_tp = smri_tp.merge(mh_tp, on=['participant_id', 'session_id'])
  862. merged_tp = merged_tp.merge(gen_tp[['participant_id', 'session_id', 'ab_g_dyn__design_site', 'ab_g_stc__cohort_sex']],
  863. on=['participant_id', 'session_id'])
  864. if len(merged_tp) == 0:
  865. print(f"No data for timepoint {tp}, skipping...")
  866. continue
  867. print(f" {tp}: {len(merged_tp)} participants")
  868. # Store in dictionary
  869. merged_data_dict[tp] = merged_tp
  870. print(f"Created merged_data_dict with {len(merged_data_dict)} timepoints")
  871. return merged_data_dict
  872. # Create the merged_data_dict
  873. merged_data_dict = create_merged_data_dict()
  874. # Verify the structure
  875. print("\nMerged data structure:")
  876. for tp, data in merged_data_dict.items():
  877. print(f"{tp}: {data.shape}")
  878. print(f" Columns include: {data.columns.tolist()[:10]}...") # Show first 10 columns
  879. # Now you can use it with your function
  880. if 'all_results' in locals():
  881. brain_predictions_dict = generate_brain_predictions_from_existing_models(
  882. all_results, merged_data_dict, ML_CONFIG
  883. )
  884. # %%
  885. #add the significance testing function
  886. def get_null_p(x, null):
  887. """Compare performance between a model and its null"""
  888. pval = 1 - np.mean(x - null >= 0)
  889. return pval
  890. # %%
  891. # ==========================================
  892. # SIGNIFICANCE TESTING WITH BISBAS AND UPPS SUBSET CORRECTION
  893. # ==========================================
  894. print(f"\n{'='*50}")
  895. print("TESTING MODEL SIGNIFICANCE WITH BISBAS AND UPPS SUBSET CORRECTION...")
  896. print(f"{'='*50}")
  897. # Load results if not already in memory
  898. if 'all_results' not in locals():
  899. with open(f"{results_dir}/all_results_complete.pkl", 'rb') as f:
  900. all_results = pickle.load(f)
  901. # ==========================================
  902. # DEFINE BEHAVIORAL SUBSETS FOR BISBAS AND UPPS
  903. # ==========================================
  904. # Define your behavioral subsets
  905. behavioral_subsets = {
  906. 'bisbas': [
  907. 'mh_y_bisbas__bis_sum',
  908. 'mh_y_bisbas__bas__rr_sum',
  909. 'mh_y_bisbas__bas__dr_sum',
  910. 'mh_y_bisbas__bas__fs_sum'
  911. ], # 4 BIS/BAS behaviors
  912. 'upps': [
  913. 'mh_y_upps__nurg_sum',
  914. 'mh_y_upps__purg_sum',
  915. 'mh_y_upps__pers_sum',
  916. 'mh_y_upps__plan_sum',
  917. 'mh_y_upps__sens_sum'
  918. ] # 5 UPPS behaviors
  919. }
  920. # Print subset information
  921. print(f"Behavioral subsets defined:")
  922. for subset_name, behaviors in behavioral_subsets.items():
  923. print(f" {subset_name.upper()}: {len(behaviors)} behaviors")
  924. for beh in behaviors:
  925. print(f" - {beh}")
  926. print(f"\nTotal behaviors: {sum(len(behaviors) for behaviors in behavioral_subsets.values())}")
  927. print(f"Expected total: {len(mh_cols)}")
  928. # Verify all behaviors are included
  929. all_subset_behaviors = []
  930. for behaviors in behavioral_subsets.values():
  931. all_subset_behaviors.extend(behaviors)
  932. missing_behaviors = [beh for beh in mh_cols if beh not in all_subset_behaviors]
  933. extra_behaviors = [beh for beh in all_subset_behaviors if beh not in mh_cols]
  934. if missing_behaviors:
  935. print(f"⚠️ Missing behaviors from subsets: {missing_behaviors}")
  936. if extra_behaviors:
  937. print(f"⚠️ Extra behaviors in subsets: {extra_behaviors}")
  938. if not missing_behaviors and not extra_behaviors:
  939. print(f"✅ All behaviors correctly assigned to subsets")
  940. model_keys = list(all_results['models'].keys())
  941. # Group model keys by timepoint and sex
  942. timepoint_sex_groups = {}
  943. for model_key in model_keys:
  944. parts = model_key.split('_')
  945. timepoint = parts[0] # e.g., 'ses-00A'
  946. sex_group = parts[2] # e.g., 'all', 'male', 'female'
  947. group_key = f"{timepoint}_{sex_group}"
  948. if group_key not in timepoint_sex_groups:
  949. timepoint_sex_groups[group_key] = []
  950. timepoint_sex_groups[group_key].append(model_key)
  951. print(f"\nTimepoint-Sex groups found: {list(timepoint_sex_groups.keys())}")
  952. for group_key, keys in timepoint_sex_groups.items():
  953. print(f" {group_key}: {len(keys)} models")
  954. # Initialize dictionaries to store significance results
  955. significance_results = {}
  956. fdr_results = {}
  957. group_summaries = {}
  958. # ==========================================
  959. # PROCESS EACH TIMEPOINT-SEX GROUP SEPARATELY
  960. # ==========================================
  961. for group_key, group_model_keys in timepoint_sex_groups.items():
  962. timepoint, sex_group = group_key.split('_', 1)
  963. print(f"\n{'='*40}")
  964. print(f"PROCESSING: {timepoint.upper()} - {sex_group.upper()} GROUP")
  965. print(f"{'='*40}")
  966. # Initialize storage for this group
  967. group_significance_results = {}
  968. group_fdr_results = {}
  969. # ==========================================
  970. # PROCESS EACH BEHAVIORAL SUBSET SEPARATELY
  971. # ==========================================
  972. for subset_name, subset_behaviors in behavioral_subsets.items():
  973. print(f"\n--- Processing behavioral subset: {subset_name.upper()} ---")
  974. # Extract data for this timepoint-sex group and behavioral subset
  975. subset_real_corrs = []
  976. subset_null_corrs = []
  977. subset_model_info = []
  978. subset_significance_results = {}
  979. for model_key in group_model_keys:
  980. real_model = all_results['models'][model_key]
  981. null_model = all_results['null_models'][model_key]
  982. behavior_names = real_model['config']['behavior_names']
  983. # Find indices of behaviors in this subset
  984. subset_indices = []
  985. subset_behavior_names = []
  986. for i, behavior in enumerate(behavior_names):
  987. if behavior in subset_behaviors:
  988. subset_indices.append(i)
  989. subset_behavior_names.append(behavior)
  990. if len(subset_indices) == 0:
  991. print(f" No behaviors from {subset_name.upper()} found in {model_key}")
  992. continue
  993. print(f" {model_key}: {len(subset_indices)} behaviors from {subset_name.upper()}")
  994. # Extract correlations for subset behaviors only
  995. real_corrs = np.mean(real_model['corr'], axis=1)[subset_indices] # Shape: (n_subset_behaviors,)
  996. null_corrs = null_model['corr'][subset_indices, :] # Shape: (n_subset_behaviors, n_reps)
  997. # Store for FDR correction within this subset
  998. subset_real_corrs.extend(real_corrs)
  999. subset_null_corrs.extend(null_corrs.T)
  1000. # Store model info for each behavior in subset
  1001. for i, behavior_idx in enumerate(subset_indices):
  1002. subset_model_info.append({
  1003. 'model_key': model_key,
  1004. 'behavior_idx': behavior_idx,
  1005. 'behavior_name': behavior_names[behavior_idx],
  1006. 'subset_name': subset_name
  1007. })
  1008. # Calculate p-values for this model's subset behaviors
  1009. p_vals = np.zeros(len(subset_indices))
  1010. for i, behavior_idx in enumerate(subset_indices):
  1011. real_mean = real_corrs[i]
  1012. null_dist = null_corrs[i, :]
  1013. p_vals[i] = get_null_p(real_mean, null_dist)
  1014. # Store results for this model and subset
  1015. subset_key = f"{model_key}_{subset_name}"
  1016. subset_significance_results[subset_key] = {
  1017. 'p_values': p_vals,
  1018. 'real_correlations': real_corrs,
  1019. 'null_correlations': null_corrs,
  1020. 'behavior_names': subset_behavior_names,
  1021. 'behavior_indices': subset_indices,
  1022. 'subset_name': subset_name
  1023. }
  1024. if len(subset_real_corrs) == 0:
  1025. print(f" No data found for subset {subset_name.upper()} in {group_key}")
  1026. continue
  1027. print(f" Collected {len(subset_real_corrs)} tests for {subset_name.upper()} in {group_key}")
  1028. # ==========================================
  1029. # FDR CORRECTION WITHIN THIS BEHAVIORAL SUBSET
  1030. # ==========================================
  1031. print(f" Applying FDR correction within {subset_name.upper()}...")
  1032. # Collect all p-values for this subset
  1033. subset_all_p_vals = []
  1034. for subset_key in subset_significance_results.keys():
  1035. if subset_name in subset_key:
  1036. subset_all_p_vals.extend(subset_significance_results[subset_key]['p_values'])
  1037. subset_all_p_vals = np.array(subset_all_p_vals)
  1038. if len(subset_all_p_vals) == 0:
  1039. continue
  1040. # Apply FDR correction within this subset
  1041. subset_fdr_rejected, subset_fdr_p_corrected = fdr(subset_all_p_vals, alpha=0.05, method='indep')
  1042. print(f" Tests in {subset_name.upper()}: {len(subset_all_p_vals)}")
  1043. print(f" Significant after FDR: {np.sum(subset_fdr_rejected)}")
  1044. print(f" Significance rate: {np.mean(subset_fdr_rejected):.3f}")
  1045. # ==========================================
  1046. # ORGANIZE FDR RESULTS BY MODEL WITHIN SUBSET
  1047. # ==========================================
  1048. idx = 0
  1049. for model_key in group_model_keys:
  1050. subset_key = f"{model_key}_{subset_name}"
  1051. if subset_key not in subset_significance_results:
  1052. continue
  1053. n_beh = len(subset_significance_results[subset_key]['p_values'])
  1054. # Extract FDR results for this model and subset
  1055. model_fdr_rejected = subset_fdr_rejected[idx:idx+n_beh]
  1056. model_fdr_p_corrected = subset_fdr_p_corrected[idx:idx+n_beh]
  1057. # Store FDR results
  1058. group_fdr_results[subset_key] = {
  1059. 'fdr_rejected': model_fdr_rejected,
  1060. 'fdr_p_corrected': model_fdr_p_corrected,
  1061. 'n_significant': np.sum(model_fdr_rejected),
  1062. 'real_correlations': subset_significance_results[subset_key]['real_correlations'],
  1063. 'p_values_uncorrected': subset_significance_results[subset_key]['p_values'],
  1064. 'behavior_names': subset_significance_results[subset_key]['behavior_names'],
  1065. 'behavior_indices': subset_significance_results[subset_key]['behavior_indices'],
  1066. 'subset_name': subset_name
  1067. }
  1068. idx += n_beh
  1069. # Store results for this timepoint-sex group
  1070. significance_results.update(subset_significance_results)
  1071. fdr_results.update(group_fdr_results)
  1072. # Create group summary across all subsets
  1073. group_total_tests = 0
  1074. group_total_significant = 0
  1075. subset_summaries = {}
  1076. for subset_name in behavioral_subsets.keys():
  1077. subset_tests = 0
  1078. subset_significant = 0
  1079. for key in group_fdr_results.keys():
  1080. if f"_{subset_name}" in key and group_key in key:
  1081. subset_tests += len(group_fdr_results[key]['fdr_rejected'])
  1082. subset_significant += group_fdr_results[key]['n_significant']
  1083. if subset_tests > 0:
  1084. subset_summaries[subset_name] = {
  1085. 'tests': subset_tests,
  1086. 'significant': subset_significant,
  1087. 'rate': subset_significant / subset_tests
  1088. }
  1089. group_total_tests += subset_tests
  1090. group_total_significant += subset_significant
  1091. group_summaries[group_key] = {
  1092. 'timepoint': timepoint,
  1093. 'sex_group': sex_group,
  1094. 'total_tests': group_total_tests,
  1095. 'total_significant': group_total_significant,
  1096. 'significance_rate': float(group_total_significant/group_total_tests) if group_total_tests > 0 else 0.0,
  1097. 'subset_summaries': subset_summaries,
  1098. 'models': group_model_keys,
  1099. 'fdr_alpha': 0.05,
  1100. 'fdr_method': 'indep'
  1101. }
  1102. # Print summary for this group
  1103. print(f"\n{group_key} SUMMARY:")
  1104. print(f" Total tests across subsets: {group_total_tests}")
  1105. print(f" Total significant: {group_total_significant}")
  1106. if group_total_tests > 0:
  1107. print(f" Overall rate: {group_total_significant/group_total_tests:.3f}")
  1108. else:
  1109. print(f" Overall rate: 0.000")
  1110. for subset_name, subset_summary in subset_summaries.items():
  1111. print(f" {subset_name.upper()}: {subset_summary['significant']}/{subset_summary['tests']} ({subset_summary['rate']:.3f})")
  1112. # Show significant models for this group
  1113. print(f" Significant models by subset:")
  1114. for subset_name in behavioral_subsets.keys():
  1115. print(f" {subset_name.upper()}:")
  1116. for model_key in group_model_keys:
  1117. subset_key = f"{model_key}_{subset_name}"
  1118. if subset_key in group_fdr_results:
  1119. n_sig = group_fdr_results[subset_key]['n_significant']
  1120. n_total = len(group_fdr_results[subset_key]['fdr_rejected'])
  1121. if n_sig > 0:
  1122. print(f" {model_key}: {n_sig}/{n_total} ✓")
  1123. else:
  1124. print(f" {model_key}: {n_sig}/{n_total}")
  1125. # ==========================================
  1126. # OVERALL SUMMARY BY TIMEPOINT, SEX, AND BEHAVIORAL SUBSET
  1127. # ==========================================
  1128. print(f"\n{'='*60}")
  1129. print("SIGNIFICANCE SUMMARY BY TIMEPOINT, SEX, AND BEHAVIORAL SUBSET:")
  1130. print("="*60)
  1131. # Create organized summary
  1132. timepoint_summary = {}
  1133. total_tests_all = 0
  1134. total_significant_all = 0
  1135. for group_key in sorted(group_summaries.keys()):
  1136. summary = group_summaries[group_key]
  1137. timepoint = summary['timepoint']
  1138. sex_group = summary['sex_group']
  1139. if timepoint not in timepoint_summary:
  1140. timepoint_summary[timepoint] = {}
  1141. timepoint_summary[timepoint][sex_group] = summary
  1142. total_tests_all += summary['total_tests']
  1143. total_significant_all += summary['total_significant']
  1144. # Print organized summary
  1145. for timepoint in sorted(timepoint_summary.keys()):
  1146. print(f"\n{timepoint}:")
  1147. for sex_group in ['all', 'female', 'male']:
  1148. if sex_group in timepoint_summary[timepoint]:
  1149. summary = timepoint_summary[timepoint][sex_group]
  1150. print(f" {sex_group:>6}: {summary['total_significant']:>3}/{summary['total_tests']:<3} ({summary['significance_rate']:.3f})")
  1151. # Show subset breakdown
  1152. for subset_name, subset_summary in summary['subset_summaries'].items():
  1153. print(f" {subset_name.upper():>8}: {subset_summary['significant']:>2}/{subset_summary['tests']:<2} ({subset_summary['rate']:.3f})")
  1154. else:
  1155. print(f" {sex_group:>6}: No data")
  1156. print(f"\nGRAND TOTAL:")
  1157. print(f" Total tests: {total_tests_all}")
  1158. print(f" Total significant: {total_significant_all}")
  1159. if total_tests_all > 0:
  1160. print(f" Overall rate: {total_significant_all/total_tests_all:.3f}")
  1161. else:
  1162. print(f" Overall rate: 0.000")
  1163. # Summary by subset across all timepoints and sexes
  1164. print(f"\nSUMMARY BY BEHAVIORAL SUBSET:")
  1165. subset_totals = {}
  1166. for subset_name in behavioral_subsets.keys():
  1167. subset_tests = 0
  1168. subset_significant = 0
  1169. for group_summary in group_summaries.values():
  1170. if subset_name in group_summary['subset_summaries']:
  1171. subset_tests += group_summary['subset_summaries'][subset_name]['tests']
  1172. subset_significant += group_summary['subset_summaries'][subset_name]['significant']
  1173. subset_totals[subset_name] = {
  1174. 'tests': subset_tests,
  1175. 'significant': subset_significant,
  1176. 'rate': subset_significant / subset_tests if subset_tests > 0 else 0.0
  1177. }
  1178. print(f" {subset_name.upper():>8}: {subset_significant:>3}/{subset_tests:<3} ({subset_totals[subset_name]['rate']:.3f})")
  1179. # ==========================================
  1180. # ADD TO MASTER RESULTS AND SAVE
  1181. # ==========================================
  1182. print(f"\nAdding BIS/BAS and UPPS subset significance results to master results...")
  1183. # Add significance results to all_results
  1184. all_results['significance_subset'] = significance_results
  1185. all_results['fdr_results_subset'] = fdr_results
  1186. all_results['significance_summary_subset'] = {
  1187. 'behavioral_subsets': behavioral_subsets,
  1188. 'by_timepoint_and_sex': group_summaries,
  1189. 'by_timepoint': timepoint_summary,
  1190. 'by_subset': subset_totals,
  1191. 'overall': {
  1192. 'total_tests': total_tests_all,
  1193. 'total_significant': total_significant_all,
  1194. 'significance_rate': float(total_significant_all/total_tests_all) if total_tests_all > 0 else 0.0,
  1195. 'fdr_alpha': 0.05,
  1196. 'fdr_method': 'indep',
  1197. 'correction_level': 'within_bisbas_and_upps_subsets'
  1198. }
  1199. }
  1200. # Save updated results
  1201. final_filename = f"{results_dir}/all_results_complete_with_bisbas_upps_significance.pkl"
  1202. with open(final_filename, 'wb') as f:
  1203. pickle.dump(all_results, f)
  1204. # Save just subset significance results
  1205. subset_significance_filename = f"{results_dir}/bisbas_upps_significance_results_only.pkl"
  1206. with open(subset_significance_filename, 'wb') as f:
  1207. pickle.dump({
  1208. 'behavioral_subsets': behavioral_subsets,
  1209. 'significance': significance_results,
  1210. 'fdr_results': fdr_results,
  1211. 'summary': all_results['significance_summary_subset']
  1212. }, f)
  1213. print(f"\n🎉 BIS/BAS AND UPPS SUBSET SIGNIFICANCE TESTING COMPLETED!")
  1214. print(f"📁 Results saved to:")
  1215. print(f" - all_results_complete_with_bisbas_upps_significance.pkl")
  1216. print(f" - bisbas_upps_significance_results_only.pkl")
  1217. # %%
  1218. #print out the significant models
  1219. def show_significant_models():
  1220. """Show only models with significant results"""
  1221. print("Models with significant results:")
  1222. print("="*50)
  1223. for model_key, results in fdr_results.items():
  1224. if results['n_significant'] > 0:
  1225. print(f"{model_key}: {results['n_significant']} significant behaviors")
  1226. sig_mask = results['fdr_rejected']
  1227. for i, is_sig in enumerate(sig_mask):
  1228. if is_sig:
  1229. behavior = results['behavior_names'][i]
  1230. corr = results['real_correlations'][i]
  1231. p_corr = results['fdr_p_corrected'][i]
  1232. print(f" {behavior}: r={corr:.3f}, p_fdr={p_corr:.4f}")
  1233. print()
  1234. #run quick summaries
  1235. show_significant_models()
  1236. # %%
  1237. def analyze_model_performance_vs_age_correlation_by_behavior_significant_only(brain_predictions_dict, gen_complete, mh_cols, all_results):
  1238. """
  1239. Analyze correlation between model performance and age-behavior correlations
  1240. - Separately for each behavior
  1241. - Only for models that predicted behaviors better than chance (FDR significant)
  1242. For each significant behavior-model combination:
  1243. a = correlation between observed and predicted values
  1244. b = correlation between observed values and age
  1245. Then compute correlation between a and b across iterations for that specific behavior
  1246. """
  1247. print("\n🔍 ANALYZING MODEL PERFORMANCE vs AGE-BEHAVIOR CORRELATIONS")
  1248. print("BY BEHAVIOR - SIGNIFICANT MODELS ONLY")
  1249. print("=" * 80)
  1250. # Get significance results
  1251. if 'fdr_results_subset' not in all_results:
  1252. print("❌ No significance results found. Run significance testing first!")
  1253. return None
  1254. fdr_results = all_results['fdr_results_subset']
  1255. behavioral_subsets = all_results['significance_summary_subset']['behavioral_subsets']
  1256. age_analysis_results = {}
  1257. total_significant_behaviors = 0
  1258. total_analyzed_behaviors = 0
  1259. for result_key, pred_data in brain_predictions_dict.items():
  1260. print(f"\nAnalyzing {result_key}...")
  1261. # Parse model components - result_key format is "timepoint_braintype_sexgroup"
  1262. try:
  1263. parts = result_key.split('_')
  1264. timepoint = parts[0]
  1265. brain_type = parts[1]
  1266. sex_group = parts[2]
  1267. except:
  1268. print(f" ⚠️ Could not parse {result_key}, skipping...")
  1269. continue
  1270. model_key = result_key # This is the model key without behavior name
  1271. behaviors = pred_data['behaviors'] # List of all behaviors for this model
  1272. print(f" Model: {model_key}")
  1273. print(f" Behaviors in model: {len(behaviors)}")
  1274. # Process each behavior in this model
  1275. for behavior_idx, behavior_name in enumerate(behaviors):
  1276. print(f" Checking behavior: {behavior_name}")
  1277. # Find which subset this behavior belongs to
  1278. behavior_subset = None
  1279. for subset_name, subset_behaviors in behavioral_subsets.items():
  1280. if behavior_name in subset_behaviors:
  1281. behavior_subset = subset_name
  1282. break
  1283. if behavior_subset is None:
  1284. print(f" ⚠️ Behavior {behavior_name} not found in any subset, skipping...")
  1285. continue
  1286. # Check significance for this specific behavior
  1287. subset_key = f"{model_key}_{behavior_subset}"
  1288. if subset_key not in fdr_results:
  1289. print(f" ⚠️ No significance results for {subset_key}, skipping...")
  1290. continue
  1291. fdr_result = fdr_results[subset_key]
  1292. fdr_behavior_names = fdr_result['behavior_names']
  1293. fdr_rejected = fdr_result['fdr_rejected']
  1294. # Find index of this behavior in the significance results
  1295. try:
  1296. fdr_behavior_idx = fdr_behavior_names.index(behavior_name)
  1297. is_significant = fdr_rejected[fdr_behavior_idx]
  1298. except ValueError:
  1299. print(f" ⚠️ Behavior {behavior_name} not found in significance results, skipping...")
  1300. continue
  1301. if not is_significant:
  1302. print(f" ❌ {behavior_name} not significant, skipping...")
  1303. continue
  1304. print(f" ✅ {behavior_name} is significant, analyzing...")
  1305. total_significant_behaviors += 1
  1306. n_iterations = len(pred_data['predictions'])
  1307. # Storage for correlations across iterations for this specific behavior
  1308. behavior_model_performance_corrs = [] # a values
  1309. behavior_age_behavior_corrs = [] # b values
  1310. # Process each iteration
  1311. for iteration in range(n_iterations):
  1312. predictions = pred_data['predictions'][iteration] # Shape: (n_samples, n_behaviors)
  1313. true_values = pred_data['true_values'][iteration] # Shape: (n_samples, n_behaviors)
  1314. test_indices = pred_data['test_indices'][iteration] # Shape: (n_samples,)
  1315. # Extract age data for these participants
  1316. try:
  1317. ages = gen_complete.iloc[test_indices]['ab_g_dyn__visit_age'].values
  1318. except:
  1319. continue
  1320. if len(ages) != len(true_values):
  1321. continue
  1322. # Extract data for this specific behavior
  1323. obs = true_values[:, behavior_idx] # Use behavior_idx from the behaviors list
  1324. pred = predictions[:, behavior_idx]
  1325. # Remove NaN values
  1326. valid_mask = ~(np.isnan(obs) | np.isnan(pred) | np.isnan(ages))
  1327. if valid_mask.sum() < 10: # Need minimum samples
  1328. continue
  1329. obs_clean = obs[valid_mask]
  1330. pred_clean = pred[valid_mask]
  1331. ages_clean = ages[valid_mask]
  1332. # a: model performance correlation for this behavior
  1333. try:
  1334. model_perf_corr = np.corrcoef(obs_clean, pred_clean)[0, 1]
  1335. if np.isnan(model_perf_corr):
  1336. continue
  1337. except:
  1338. continue
  1339. # b: age-behavior correlation for this behavior
  1340. try:
  1341. age_behavior_corr = np.corrcoef(obs_clean, ages_clean)[0, 1]
  1342. if np.isnan(age_behavior_corr):
  1343. continue
  1344. except:
  1345. continue
  1346. behavior_model_performance_corrs.append(model_perf_corr)
  1347. behavior_age_behavior_corrs.append(age_behavior_corr)
  1348. # Convert to arrays
  1349. behavior_model_performance_corrs = np.array(behavior_model_performance_corrs)
  1350. behavior_age_behavior_corrs = np.array(behavior_age_behavior_corrs)
  1351. print(f" Collected {len(behavior_model_performance_corrs)} valid iterations")
  1352. if len(behavior_model_performance_corrs) < 5:
  1353. print(f" ⚠️ Insufficient data for correlation analysis")
  1354. continue
  1355. # Compute correlation between a and b for this specific behavior
  1356. try:
  1357. from scipy.stats import pearsonr
  1358. performance_age_corr, p_value = pearsonr(behavior_model_performance_corrs, behavior_age_behavior_corrs)
  1359. except:
  1360. performance_age_corr = np.nan
  1361. p_value = np.nan
  1362. # Create unique result key for this behavior-model combination
  1363. behavior_result_key = f"{model_key}_{behavior_name.replace('mh_y_', '').replace('_', '')}"
  1364. # Store results for this specific behavior
  1365. age_analysis_results[behavior_result_key] = {
  1366. 'behavior_name': behavior_name,
  1367. 'model_key': model_key,
  1368. 'timepoint': timepoint,
  1369. 'brain_type': brain_type,
  1370. 'sex_group': sex_group,
  1371. 'behavior_subset': behavior_subset,
  1372. 'behavior_idx': behavior_idx,
  1373. 'performance_age_correlation': performance_age_corr,
  1374. 'p_value': p_value,
  1375. 'n_datapoints': len(behavior_model_performance_corrs),
  1376. 'model_perf_mean': np.mean(behavior_model_performance_corrs),
  1377. 'model_perf_std': np.std(behavior_model_performance_corrs),
  1378. 'age_corr_mean': np.mean(behavior_age_behavior_corrs),
  1379. 'age_corr_std': np.std(behavior_age_behavior_corrs),
  1380. 'is_significant_predictor': True # All included behaviors are significant
  1381. }
  1382. print(f" Performance-age correlation: {performance_age_corr:.3f} (p={p_value:.3f})")
  1383. total_analyzed_behaviors += 1
  1384. print(f"\n📊 ANALYSIS SUMMARY:")
  1385. print(f" Total significant behaviors found: {total_significant_behaviors}")
  1386. print(f" Total behaviors analyzed: {total_analyzed_behaviors}")
  1387. return age_analysis_results
  1388. # %%
  1389. def print_significant_age_correlations_by_behavior_fdr_corrected(age_analysis_results):
  1390. """
  1391. Print correlation values for significant model-behavior combinations,
  1392. with FDR correction across all tests
  1393. """
  1394. print(f"\n🎯 SIGNIFICANT PERFORMANCE vs AGE-BEHAVIOR CORRELATIONS")
  1395. print("BY INDIVIDUAL BEHAVIOR (SIGNIFICANT PREDICTORS ONLY)")
  1396. print("WITH FDR CORRECTION ACROSS ALL TESTS")
  1397. print("=" * 80)
  1398. if not age_analysis_results:
  1399. print("No results to display.")
  1400. return
  1401. # Collect all results with valid p-values
  1402. all_results = []
  1403. all_p_values = []
  1404. for result_key, results in age_analysis_results.items():
  1405. corr = results['performance_age_correlation']
  1406. p_val = results['p_value']
  1407. # Only include results with valid p-values
  1408. if not np.isnan(p_val):
  1409. all_results.append({
  1410. 'result_key': result_key,
  1411. 'behavior_name': results['behavior_name'],
  1412. 'timepoint': results['timepoint'],
  1413. 'brain_type': results['brain_type'],
  1414. 'sex_group': results['sex_group'],
  1415. 'behavior_subset': results['behavior_subset'],
  1416. 'correlation': corr,
  1417. 'p_value': p_val,
  1418. 'n_datapoints': results['n_datapoints'],
  1419. 'model_perf_mean': results['model_perf_mean'],
  1420. 'age_corr_mean': results['age_corr_mean']
  1421. })
  1422. all_p_values.append(p_val)
  1423. if len(all_results) == 0:
  1424. print("No valid results to analyze.")
  1425. return []
  1426. print(f"Total tests performed: {len(all_results)}")
  1427. # Apply FDR correction
  1428. from statsmodels.stats.multitest import fdrcorrection
  1429. all_p_values = np.array(all_p_values)
  1430. fdr_rejected, fdr_p_corrected = fdrcorrection(all_p_values, alpha=0.05, method='indep')
  1431. # Add FDR results to each result
  1432. for i, result in enumerate(all_results):
  1433. result['fdr_rejected'] = fdr_rejected[i]
  1434. result['fdr_p_corrected'] = fdr_p_corrected[i]
  1435. # Determine significance level for uncorrected p-value
  1436. if result['p_value'] < 0.001:
  1437. result['uncorrected_significance'] = "***"
  1438. elif result['p_value'] < 0.01:
  1439. result['uncorrected_significance'] = "**"
  1440. elif result['p_value'] < 0.05:
  1441. result['uncorrected_significance'] = "*"
  1442. else:
  1443. result['uncorrected_significance'] = ""
  1444. # Determine significance level for FDR corrected p-value
  1445. if result['fdr_p_corrected'] < 0.001:
  1446. result['fdr_significance'] = "***"
  1447. elif result['fdr_p_corrected'] < 0.01:
  1448. result['fdr_significance'] = "**"
  1449. elif result['fdr_p_corrected'] < 0.05:
  1450. result['fdr_significance'] = "*"
  1451. else:
  1452. result['fdr_significance'] = ""
  1453. # Separate significant results (uncorrected vs FDR corrected)
  1454. uncorrected_significant = [r for r in all_results if r['p_value'] < 0.05]
  1455. fdr_significant = [r for r in all_results if r['fdr_rejected']]
  1456. print(f"Significant before FDR correction: {len(uncorrected_significant)}")
  1457. print(f"Significant after FDR correction: {len(fdr_significant)}")
  1458. print(f"FDR correction alpha: 0.05")
  1459. # Print FDR significant results
  1460. if len(fdr_significant) > 0:
  1461. print(f"\n📊 FDR-CORRECTED SIGNIFICANT RESULTS:")
  1462. print("=" * 80)
  1463. # Sort by FDR corrected p-value
  1464. fdr_significant.sort(key=lambda x: x['fdr_p_corrected'])
  1465. # Print header
  1466. print(f"{'Behavior':<25} {'Model':<25} {'r':<8} {'p_uncorr':<10} {'p_FDR':<10} {'FDR_sig':<7}")
  1467. print("-" * 85)
  1468. # Print each FDR significant result
  1469. for result in fdr_significant:
  1470. behavior_clean = result['behavior_name'].replace('mh_y_', '').replace('_', '')[:24]
  1471. timepoint_label = result['timepoint'].replace('ses-', 'Y').replace('A', '')
  1472. model_desc = f"{result['brain_type'][:4]}-{result['sex_group'][:3]}-{timepoint_label}"[:24]
  1473. print(f"{behavior_clean:<25} {model_desc:<25} {result['correlation']:>7.3f} "
  1474. f"{result['p_value']:>9.4f} {result['fdr_p_corrected']:>9.4f} "
  1475. f"{result['fdr_significance']:>6s}")
  1476. else:
  1477. print(f"\n❌ No results survive FDR correction at α = 0.05")
  1478. # Print comparison table of uncorrected significant results
  1479. if len(uncorrected_significant) > 0:
  1480. print(f"\n📋 ALL UNCORRECTED SIGNIFICANT RESULTS (with FDR status):")
  1481. print("=" * 100)
  1482. # Sort by uncorrected p-value
  1483. uncorrected_significant.sort(key=lambda x: x['p_value'])
  1484. # Print header
  1485. print(f"{'Behavior':<25} {'Model':<25} {'r':<8} {'p_uncorr':<10} {'p_FDR':<10} {'FDR_pass':<8}")
  1486. print("-" * 95)
  1487. # Print each uncorrected significant result
  1488. for result in uncorrected_significant:
  1489. behavior_clean = result['behavior_name'].replace('mh_y_', '').replace('_', '')[:24]
  1490. timepoint_label = result['timepoint'].replace('ses-', 'Y').replace('A', '')
  1491. model_desc = f"{result['brain_type'][:4]}-{result['sex_group'][:3]}-{timepoint_label}"[:24]
  1492. fdr_pass = "✓" if result['fdr_rejected'] else "✗"
  1493. print(f"{behavior_clean:<25} {model_desc:<25} {result['correlation']:>7.3f} "
  1494. f"{result['p_value']:>9.4f} {result['fdr_p_corrected']:>9.4f} "
  1495. f"{fdr_pass:>7s}")
  1496. # Summary statistics
  1497. print(f"\n📊 SUMMARY STATISTICS:")
  1498. print("=" * 50)
  1499. # Before FDR correction
  1500. if len(uncorrected_significant) > 0:
  1501. uncorr_corr_values = [r['correlation'] for r in uncorrected_significant]
  1502. print(f"\nUNCORRECTED SIGNIFICANT (p < 0.05):")
  1503. print(f" Count: {len(uncorrected_significant)}")
  1504. print(f" Mean correlation: {np.mean(uncorr_corr_values):6.3f}")
  1505. print(f" Range: {np.min(uncorr_corr_values):6.3f} to {np.max(uncorr_corr_values):6.3f}")
  1506. # Count by significance level (uncorrected)
  1507. uncorr_highly_sig = len([r for r in uncorrected_significant if r['p_value'] < 0.001])
  1508. uncorr_very_sig = len([r for r in uncorrected_significant if 0.001 <= r['p_value'] < 0.01])
  1509. uncorr_sig = len([r for r in uncorrected_significant if 0.01 <= r['p_value'] < 0.05])
  1510. print(f" By uncorrected significance level:")
  1511. print(f" p < 0.001: {uncorr_highly_sig}")
  1512. print(f" p < 0.01: {uncorr_very_sig}")
  1513. print(f" p < 0.05: {uncorr_sig}")
  1514. # After FDR correction
  1515. if len(fdr_significant) > 0:
  1516. fdr_corr_values = [r['correlation'] for r in fdr_significant]
  1517. print(f"\nFDR-CORRECTED SIGNIFICANT:")
  1518. print(f" Count: {len(fdr_significant)}")
  1519. print(f" Mean correlation: {np.mean(fdr_corr_values):6.3f}")
  1520. print(f" Range: {np.min(fdr_corr_values):6.3f} to {np.max(fdr_corr_values):6.3f}")
  1521. # Count by significance level (FDR corrected)
  1522. fdr_highly_sig = len([r for r in fdr_significant if r['fdr_p_corrected'] < 0.001])
  1523. fdr_very_sig = len([r for r in fdr_significant if 0.001 <= r['fdr_p_corrected'] < 0.01])
  1524. fdr_sig = len([r for r in fdr_significant if 0.01 <= r['fdr_p_corrected'] < 0.05])
  1525. print(f" By FDR-corrected significance level:")
  1526. print(f" p_FDR < 0.001: {fdr_highly_sig}")
  1527. print(f" p_FDR < 0.01: {fdr_very_sig}")
  1528. print(f" p_FDR < 0.05: {fdr_sig}")
  1529. # Group by categories (FDR significant only)
  1530. print(f"\n FDR significant by brain type:")
  1531. brain_counts = {}
  1532. for result in fdr_significant:
  1533. bt = result['brain_type']
  1534. brain_counts[bt] = brain_counts.get(bt, 0) + 1
  1535. for bt, count in sorted(brain_counts.items()):
  1536. print(f" {bt}: {count}")
  1537. print(f"\n FDR significant by behavioral subset:")
  1538. subset_counts = {}
  1539. for result in fdr_significant:
  1540. subset = result['behavior_subset']
  1541. subset_counts[subset] = subset_counts.get(subset, 0) + 1
  1542. for subset, count in sorted(subset_counts.items()):
  1543. print(f" {subset.upper()}: {count}")
  1544. # Interpretation
  1545. pos_corrs = len([r for r in fdr_significant if r['correlation'] > 0])
  1546. neg_corrs = len([r for r in fdr_significant if r['correlation'] < 0])
  1547. print(f"\n FDR significant interpretation:")
  1548. print(f" Positive correlations: {pos_corrs} (better prediction of age-related behaviors)")
  1549. print(f" Negative correlations: {neg_corrs} (better prediction of age-independent behaviors)")
  1550. else:
  1551. print(f"\nNo results survive FDR correction.")
  1552. # False discovery rate
  1553. if len(all_results) > 0:
  1554. estimated_false_discoveries = len(uncorrected_significant) * 0.05 # Expected under null
  1555. print(f"\n📈 FALSE DISCOVERY ESTIMATION:")
  1556. print(f" Expected false discoveries (5% of {len(all_results)} tests): {estimated_false_discoveries:.1f}")
  1557. print(f" Observed uncorrected significant: {len(uncorrected_significant)}")
  1558. print(f" FDR-corrected significant: {len(fdr_significant)}")
  1559. if len(uncorrected_significant) > 0:
  1560. print(f" Estimated FDR in uncorrected results: {estimated_false_discoveries/len(uncorrected_significant):.3f}")
  1561. return {
  1562. 'all_results': all_results,
  1563. 'uncorrected_significant': uncorrected_significant,
  1564. 'fdr_significant': fdr_significant,
  1565. 'fdr_alpha': 0.05,
  1566. 'n_tests': len(all_results)
  1567. }
  1568. # Update the main analysis section
  1569. if 'brain_predictions_dict' in locals() and brain_predictions_dict is not None:
  1570. print("🚀 RUNNING BEHAVIOR-SPECIFIC MODEL PERFORMANCE vs AGE CORRELATION ANALYSIS")
  1571. print("SIGNIFICANT PREDICTORS ONLY WITH FDR CORRECTION")
  1572. # Run analysis
  1573. age_analysis_by_behavior = analyze_model_performance_vs_age_correlation_by_behavior_significant_only(
  1574. brain_predictions_dict, gen_complete, mh_cols, all_results
  1575. )
  1576. if age_analysis_by_behavior:
  1577. # Print significant results with FDR correction
  1578. fdr_corrected_results = print_significant_age_correlations_by_behavior_fdr_corrected(age_analysis_by_behavior)
  1579. print(f"\n🎉 BEHAVIOR-SPECIFIC AGE CORRELATION ANALYSIS COMPLETED!")
  1580. print(f"📊 Results available in age_analysis_by_behavior dictionary")
  1581. print(f"📊 FDR-corrected results available in fdr_corrected_results dictionary")
  1582. else:
  1583. print("❌ No behavior-specific results generated")
  1584. else:
  1585. print("❌ No brain_predictions_dict found. Generate predictions first!")
  1586. # %%
  1587. # ==========================================
  1588. # FIND LONGITUDINAL PARTICIPANTS USING EXISTING VARIABLES
  1589. # ==========================================
  1590. def find_longitudinal_participants_from_existing(gen_complete, smri_normalized, mh_complete):
  1591. """Find participants with both baseline and 2-year data using existing complete datasets"""
  1592. print("🔍 FINDING LONGITUDINAL PARTICIPANTS FROM EXISTING DATASETS")
  1593. print("=" * 60)
  1594. # Get participants with baseline data
  1595. baseline_gen = set(gen_complete[gen_complete['session_id'] == 'ses-00A']['participant_id'])
  1596. baseline_smri = set(smri_normalized[smri_normalized['session_id'] == 'ses-00A']['participant_id'])
  1597. baseline_mh = set(mh_complete[mh_complete['session_id'] == 'ses-00A']['participant_id'])
  1598. print(f"Baseline participants:")
  1599. print(f" Demographics: {len(baseline_gen)}")
  1600. print(f" Brain (SMRI): {len(baseline_smri)}")
  1601. print(f" Behavior (MH): {len(baseline_mh)}")
  1602. # Get participants with 4-year data
  1603. sixyear_gen = set(gen_complete[gen_complete['session_id'] == 'ses-02A']['participant_id'])
  1604. sixyear_smri = set(smri_normalized[smri_normalized['session_id'] == 'ses-02A']['participant_id'])
  1605. sixyear_mh = set(mh_complete[mh_complete['session_id'] == 'ses-02A']['participant_id'])
  1606. print(f"\n2-year participants:")
  1607. print(f" Demographics: {len(sixyear_gen)}")
  1608. print(f" Brain (SMRI): {len(sixyear_smri)}")
  1609. print(f" Behavior (MH): {len(sixyear_mh)}")
  1610. # Find longitudinal participants (have both baseline and 4-year)
  1611. longitudinal_gen = baseline_gen.intersection(sixyear_gen)
  1612. longitudinal_smri = baseline_smri.intersection(sixyear_smri)
  1613. longitudinal_mh = baseline_mh.intersection(sixyear_mh)
  1614. print(f"\nLongitudinal participants:")
  1615. print(f" Demographics: {len(longitudinal_gen)}")
  1616. print(f" Brain (SMRI): {len(longitudinal_smri)}")
  1617. print(f" Behavior (MH): {len(longitudinal_mh)}")
  1618. # Find complete longitudinal participants (have both brain and behavior longitudinal data)
  1619. complete_longitudinal = longitudinal_smri.intersection(longitudinal_mh).intersection(longitudinal_gen)
  1620. print(f" Complete (Brain + Behavior + Demographics): {len(complete_longitudinal)}")
  1621. return {
  1622. 'longitudinal_gen': list(longitudinal_gen),
  1623. 'longitudinal_smri': list(longitudinal_smri),
  1624. 'longitudinal_mh': list(longitudinal_mh),
  1625. 'complete_longitudinal': list(complete_longitudinal)
  1626. }
  1627. # Find longitudinal participants
  1628. longitudinal_participants = find_longitudinal_participants_from_existing(
  1629. gen_complete, smri_normalized, mh_complete
  1630. )
  1631. # ==========================================
  1632. # CREATE SMRI_DIFF (BRAIN DIFFERENCES)
  1633. # ==========================================
  1634. def create_smri_diff(smri_normalized, longitudinal_smri_participants):
  1635. """Create smri_diff dataset with baseline to 2-year differences"""
  1636. print(f"\n🧠 CREATING SMRI_DIFF DATASET")
  1637. print("=" * 40)
  1638. # Filter for longitudinal participants only
  1639. smri_longitudinal = smri_normalized[
  1640. smri_normalized['participant_id'].isin(longitudinal_smri_participants)
  1641. ].copy()
  1642. print(f"SMRI longitudinal data: {len(smri_longitudinal)} rows")
  1643. # Separate baseline and 4-year data
  1644. smri_baseline = smri_longitudinal[smri_longitudinal['session_id'] == 'ses-00A'].copy()
  1645. smri_sixYear = smri_longitudinal[smri_longitudinal['session_id'] == 'ses-02A'].copy()
  1646. print(f" Baseline: {len(smri_baseline)} participants")
  1647. print(f" 2-year: {len(smri_sixYear)} participants")
  1648. # Merge baseline and 4-year data
  1649. smri_merged = smri_baseline.merge(
  1650. smri_sixYear,
  1651. on='participant_id',
  1652. suffixes=('_baseline', '_6year')
  1653. )
  1654. print(f" Merged: {len(smri_merged)} participants")
  1655. # Get brain measure columns (exclude ID and session columns)
  1656. id_cols = ['participant_id', 'session_id_baseline', 'session_id_6year']
  1657. brain_cols = [col for col in smri_baseline.columns
  1658. if col not in ['participant_id', 'session_id']]
  1659. print(f" Brain measure columns: {len(brain_cols)}")
  1660. # Calculate differences (4-year - baseline)
  1661. smri_diff_data = {'participant_id': smri_merged['participant_id']}
  1662. for col in brain_cols:
  1663. baseline_col = f"{col}_baseline"
  1664. sixYear_col = f"{col}_6year"
  1665. if baseline_col in smri_merged.columns and sixYear_col in smri_merged.columns:
  1666. smri_diff_data[col] = smri_merged[sixYear_col] - smri_merged[baseline_col]
  1667. else:
  1668. print(f" ⚠️ Missing columns for {col}")
  1669. # Create smri_diff DataFrame
  1670. smri_diff = pd.DataFrame(smri_diff_data)
  1671. print(f"✅ Created smri_diff: {smri_diff.shape}")
  1672. print(f" Participants: {len(smri_diff)}")
  1673. print(f" Brain measures: {len(smri_diff.columns) - 1}") # -1 for participant_id
  1674. return smri_diff
  1675. # Create smri_diff
  1676. smri_diff = create_smri_diff(smri_normalized, longitudinal_participants['longitudinal_smri'])
  1677. # ==========================================
  1678. # CREATE MH_DIFF (BEHAVIOR DIFFERENCES)
  1679. # ==========================================
  1680. def create_mh_diff(mh_complete, longitudinal_mh_participants):
  1681. """Create mh_diff dataset with baseline to 2-year differences"""
  1682. print(f"\n📈 CREATING MH_DIFF DATASET")
  1683. print("=" * 40)
  1684. # Filter for longitudinal participants only
  1685. mh_longitudinal = mh_complete[
  1686. mh_complete['participant_id'].isin(longitudinal_mh_participants)
  1687. ].copy()
  1688. print(f"MH longitudinal data: {len(mh_longitudinal)} rows")
  1689. # Separate baseline and 4-year data
  1690. mh_baseline = mh_longitudinal[mh_longitudinal['session_id'] == 'ses-00A'].copy()
  1691. mh_sixYear = mh_longitudinal[mh_longitudinal['session_id'] == 'ses-02A'].copy()
  1692. print(f" Baseline: {len(mh_baseline)} participants")
  1693. print(f" 4-year: {len(mh_sixYear)} participants")
  1694. # Merge baseline and 4-year data
  1695. mh_merged = mh_baseline.merge(
  1696. mh_sixYear,
  1697. on='participant_id',
  1698. suffixes=('_baseline', '_6year')
  1699. )
  1700. print(f" Merged: {len(mh_merged)} participants")
  1701. # Get behavior columns (exclude ID and session columns)
  1702. id_cols = ['participant_id', 'session_id_baseline', 'session_id_6year']
  1703. behavior_cols = [col for col in mh_baseline.columns
  1704. if col not in ['participant_id', 'session_id', 'site_id_l']]
  1705. print(f" Behavior columns: {len(behavior_cols)}")
  1706. # Calculate differences (4-year - baseline)
  1707. mh_diff_data = {'participant_id': mh_merged['participant_id']}
  1708. for col in behavior_cols:
  1709. baseline_col = f"{col}_baseline"
  1710. sixYear_col = f"{col}_6year"
  1711. if baseline_col in mh_merged.columns and sixYear_col in mh_merged.columns:
  1712. mh_diff_data[col] = mh_merged[sixYear_col] - mh_merged[baseline_col]
  1713. else:
  1714. print(f" ⚠️ Missing columns for {col}")
  1715. # Create mh_diff DataFrame
  1716. mh_diff = pd.DataFrame(mh_diff_data)
  1717. print(f"✅ Created mh_diff: {mh_diff.shape}")
  1718. print(f" Participants: {len(mh_diff)}")
  1719. print(f" Behavior measures: {len(mh_diff.columns) - 1}") # -1 for participant_id
  1720. return mh_diff
  1721. # Create mh_diff
  1722. mh_diff = create_mh_diff(mh_complete, longitudinal_participants['longitudinal_mh'])
  1723. # ==========================================
  1724. # CREATE FILTERED DATASETS FOR COMPLETE LONGITUDINAL PARTICIPANTS
  1725. # ==========================================
  1726. def create_complete_longitudinal_datasets(smri_diff, mh_diff, gen_complete, complete_longitudinal_participants):
  1727. """Create datasets filtered to participants with complete longitudinal data"""
  1728. print(f"\n🔄 CREATING COMPLETE LONGITUDINAL DATASETS")
  1729. print("=" * 50)
  1730. # Filter smri_diff to complete longitudinal participants
  1731. smri_diff_complete = smri_diff[
  1732. smri_diff['participant_id'].isin(complete_longitudinal_participants)
  1733. ].copy()
  1734. # Filter mh_diff to complete longitudinal participants
  1735. mh_diff_complete = mh_diff[
  1736. mh_diff['participant_id'].isin(complete_longitudinal_participants)
  1737. ].copy()
  1738. # Get demographics for complete longitudinal participants
  1739. gen_longitudinal = gen_complete[
  1740. gen_complete['participant_id'].isin(complete_longitudinal_participants)
  1741. ].copy()
  1742. print(f"Complete longitudinal datasets:")
  1743. print(f" smri_diff_complete: {smri_diff_complete.shape}")
  1744. print(f" mh_diff_complete: {mh_diff_complete.shape}")
  1745. print(f" gen_longitudinal: {gen_longitudinal.shape}")
  1746. return smri_diff_complete, mh_diff_complete, gen_longitudinal
  1747. # Create complete longitudinal datasets
  1748. smri_diff_complete, mh_diff_complete, gen_longitudinal = create_complete_longitudinal_datasets(
  1749. smri_diff, mh_diff, gen_complete, longitudinal_participants['complete_longitudinal']
  1750. )
  1751. # ==========================================
  1752. # SUMMARY AND VERIFICATION
  1753. # ==========================================
  1754. print(f"\n📋 LONGITUDINAL DATA EXTRACTION SUMMARY")
  1755. print("=" * 60)
  1756. print(f"Participant counts:")
  1757. print(f" Brain longitudinal (smri_diff): {len(smri_diff)}")
  1758. print(f" Behavior longitudinal (mh_diff): {len(mh_diff)}")
  1759. print(f" Complete longitudinal: {len(smri_diff_complete)}")
  1760. print(f"\nDataset shapes:")
  1761. print(f" smri_diff: {smri_diff.shape}")
  1762. print(f" mh_diff: {mh_diff.shape}")
  1763. print(f" smri_diff_complete: {smri_diff_complete.shape}")
  1764. print(f" mh_diff_complete: {mh_diff_complete.shape}")
  1765. print(f" gen_longitudinal: {gen_longitudinal.shape}")
  1766. print(f"\nData verification:")
  1767. # Check that all participants in complete datasets are the same
  1768. smri_diff_ids = set(smri_diff_complete['participant_id'])
  1769. mh_complete_ids = set(mh_diff_complete['participant_id'])
  1770. gen_complete_ids = set(gen_longitudinal['participant_id'])
  1771. if smri_diff_ids == mh_complete_ids == gen_complete_ids:
  1772. print(f" ✅ All complete datasets have matching participants")
  1773. else:
  1774. print(f" ⚠️ Participant mismatch in complete datasets")
  1775. print(f" SMRI: {len(smri_diff_ids)}, MH: {len(mh_complete_ids)}, GEN: {len(gen_complete_ids)}")
  1776. # Show sample of changes
  1777. print(f"\nSample brain changes (first participant, first 5 measures):")
  1778. brain_measures = [col for col in smri_diff.columns if col != 'participant_id'][:5]
  1779. for measure in brain_measures:
  1780. sample_change = smri_diff[measure].iloc[0]
  1781. print(f" {measure}: {sample_change:.4f}")
  1782. print(f"\nSample behavior changes (first participant, first 5 behaviors):")
  1783. behavior_measures = [col for col in mh_diff.columns if col != 'participant_id'][:5]
  1784. for behavior in behavior_measures:
  1785. sample_change = mh_diff[behavior].iloc[0]
  1786. print(f" {behavior}: {sample_change:.4f}")
  1787. print(f"\n✨ LONGITUDINAL DIFFERENCE DATASETS CREATED!")
  1788. print(f"📊 Available for analysis:")
  1789. print(f" - smri_diff: Brain changes (2yr - baseline) for {len(smri_diff)} participants")
  1790. print(f" - mh_diff: Behavior changes (2yr - baseline) for {len(mh_diff)} participants")
  1791. print(f" - smri_diff_complete: Brain changes for complete longitudinal sample")
  1792. print(f" - mh_diff_complete: Behavior changes for complete longitudinal sample")
  1793. print(f" - gen_longitudinal: Demographics for complete longitudinal sample")
  1794. # %%
  1795. # ==========================================
  1796. # LONGITUDINAL DIFFERENCE PREDICTION PIPELINE
  1797. # ==========================================
  1798. def run_longitudinal_diff_predictions(smri_diff, mh_diff, gen_longitudinal,
  1799. results_dir, ml_config, n_null=100):
  1800. """
  1801. Train NEW models optimized for longitudinal difference data using existing functions
  1802. """
  1803. print("🧠 TRAINING NEW MODELS FOR LONGITUDINAL DIFFERENCE DATA")
  1804. print("="*70)
  1805. # Create separate results directory for longitudinal analysis
  1806. longitudinal_results_dir = os.path.join(results_dir, 'longitudinal_diff_results_2y')
  1807. os.makedirs(longitudinal_results_dir, exist_ok=True)
  1808. # Check if results already exist
  1809. longitudinal_final_filename = f"{longitudinal_results_dir}/longitudinal_diff_results_complete.pkl"
  1810. if os.path.exists(longitudinal_final_filename):
  1811. print(f"Loading existing longitudinal results from {longitudinal_final_filename}")
  1812. with open(longitudinal_final_filename, 'rb') as f:
  1813. longitudinal_results = pickle.load(f)
  1814. print(f"Loaded {len(longitudinal_results['models'])} existing longitudinal models")
  1815. print(f"Loaded {len(longitudinal_results['null_models'])} existing longitudinal null models")
  1816. else:
  1817. print("Creating new longitudinal results dictionary...")
  1818. # Initialize SEPARATE results dictionary for longitudinal analysis
  1819. longitudinal_results = {
  1820. 'models': {}, # NEW trained models for differences
  1821. 'null_models': {}, # Null model results
  1822. 'metadata': { # Analysis info
  1823. 'analysis_type': 'longitudinal_differences_new_models',
  1824. 'ml_config': ml_config,
  1825. 'mh_behaviors': [col for col in mh_diff.columns if col != 'participant_id'],
  1826. 'feature_columns': {}, # Will be populated
  1827. 'timepoint': 'longitudinal_difference_2yr_minus_baseline',
  1828. 'brain_types': ['thickness', 'area', 'volume', 'subcvolume'], # Will be populated
  1829. 'sex_groups': ['all', 'female', 'male'],
  1830. 'creation_date': pd.Timestamp.now().isoformat()
  1831. },
  1832. 'summary': {}, # Real model summaries
  1833. 'null_summary': {} # Null model summaries
  1834. }
  1835. # Get behavior columns from mh_diff
  1836. mh_diff_cols = [col for col in mh_diff.columns if col != 'participant_id']
  1837. # Define brain types based on what's available in smri_diff
  1838. print("🔍 Identifying brain measures in difference data...")
  1839. # Get all brain columns from smri_diff (excluding participant_id)
  1840. all_brain_cols = [col for col in smri_diff.columns if col != 'participant_id']
  1841. # Categorize columns by brain type (match your original column patterns)
  1842. longitudinal_brain_types = {}
  1843. # Thickness columns - look for thickness patterns in difference data
  1844. thickness_diff_cols = [col for col in all_brain_cols if 'thick' in col.lower()]
  1845. if len(thickness_diff_cols) > 0:
  1846. longitudinal_brain_types['thickness'] = thickness_diff_cols
  1847. # Area columns - look for area patterns in difference data
  1848. area_diff_cols = [col for col in all_brain_cols if 'area' in col.lower()]
  1849. if len(area_diff_cols) > 0:
  1850. longitudinal_brain_types['area'] = area_diff_cols
  1851. # Volume columns - look for volume patterns in difference data
  1852. volume_diff_cols = [col for col in all_brain_cols if 'vol' in col.lower() and 'aseg' not in col.lower()]
  1853. if len(volume_diff_cols) > 0:
  1854. longitudinal_brain_types['volume'] = volume_diff_cols
  1855. # Subcortical volume columns - look for subcortical patterns
  1856. subcvolume_diff_cols = [col for col in all_brain_cols if 'vol' in col.lower() and 'aseg' in col.lower()]
  1857. if len(subcvolume_diff_cols) > 0:
  1858. longitudinal_brain_types['subcvolume'] = subcvolume_diff_cols
  1859. print(f"Brain types found in difference data:")
  1860. for brain_type, cols in longitudinal_brain_types.items():
  1861. print(f" {brain_type}: {len(cols)} features")
  1862. # Update metadata
  1863. longitudinal_results['metadata']['feature_columns'] = longitudinal_brain_types
  1864. longitudinal_results['metadata']['brain_types'] = list(longitudinal_brain_types.keys())
  1865. longitudinal_results['metadata']['mh_behaviors'] = mh_diff_cols
  1866. print(f"\nLongitudinal participants:")
  1867. print(f" Brain differences: {len(smri_diff)}")
  1868. print(f" Behavior differences: {len(mh_diff)}")
  1869. print(f" Behavior measures: {len(mh_diff_cols)}")
  1870. print(f"Results will be saved to: {longitudinal_results_dir}")
  1871. print("="*70)
  1872. # Merge datasets for analysis
  1873. print("\n📊 MERGING LONGITUDINAL DATA...")
  1874. # Merge smri_diff with mh_diff
  1875. merged_diff = smri_diff.merge(mh_diff, on='participant_id', how='inner')
  1876. print(f"After merging brain and behavior differences: {len(merged_diff)} participants")
  1877. # Merge with demographics (need sex and site info)
  1878. # Use baseline demographics since we need sex/site info
  1879. gen_baseline = gen_longitudinal[gen_longitudinal['session_id'] == 'ses-00A'].copy()
  1880. merged_diff = merged_diff.merge(
  1881. gen_baseline[['participant_id', 'ab_g_dyn__design_site', 'ab_g_stc__cohort_sex']],
  1882. on='participant_id',
  1883. how='inner'
  1884. )
  1885. print(f"After merging with demographics: {len(merged_diff)} participants")
  1886. if len(merged_diff) == 0:
  1887. print("❌ No merged longitudinal data available!")
  1888. return None
  1889. # Update participant count
  1890. longitudinal_results['metadata']['n_participants'] = len(merged_diff)
  1891. # Prepare data
  1892. Y_diff = merged_diff[mh_diff_cols].values # Behavior differences
  1893. site = merged_diff['ab_g_dyn__design_site'].values
  1894. print(f"Final dataset shape: {merged_diff.shape}")
  1895. print(f"Y (behavior differences) shape: {Y_diff.shape}")
  1896. print(f"Number of sites: {len(np.unique(site))}")
  1897. # Sex groups to analyze
  1898. sexes = ['all', 'female', 'male']
  1899. # Loop through sexes
  1900. for sex in sexes:
  1901. print(f"\n{'='*20} SEX GROUP: {sex.upper()} {'='*20}")
  1902. # Filter by sex
  1903. if sex == 'all':
  1904. data_subset = merged_diff.copy()
  1905. Y_subset = Y_diff.copy()
  1906. site_subset = site.copy()
  1907. elif sex == 'male':
  1908. sex_mask = merged_diff['ab_g_stc__cohort_sex'] == 1
  1909. data_subset = merged_diff[sex_mask].copy()
  1910. Y_subset = Y_diff[sex_mask]
  1911. site_subset = site[sex_mask]
  1912. elif sex == 'female':
  1913. sex_mask = merged_diff['ab_g_stc__cohort_sex'] == 2
  1914. data_subset = merged_diff[sex_mask].copy()
  1915. Y_subset = Y_diff[sex_mask]
  1916. site_subset = site[sex_mask]
  1917. if len(data_subset) < 50: # Minimum sample size
  1918. print(f"Skipping {sex} group (n={len(data_subset)}, too small)")
  1919. continue
  1920. print(f"{sex} group: {len(data_subset)} participants")
  1921. print(f"Number of sites: {len(np.unique(site_subset))}")
  1922. # Loop through brain types
  1923. for brain_type, feature_cols in longitudinal_brain_types.items():
  1924. print(f"\n--- BRAIN TYPE: {brain_type.upper()} ---")
  1925. # Check if this model already exists
  1926. result_key = f"longitudinal_diff_{brain_type}_{sex}"
  1927. if result_key in longitudinal_results['models']:
  1928. print(f" {result_key} already exists, skipping...")
  1929. continue
  1930. # Prepare brain difference data
  1931. X_brain_diff = data_subset[feature_cols].values
  1932. print(f"X (brain differences) shape: {X_brain_diff.shape}")
  1933. print(f"Feature columns: {len(feature_cols)}")
  1934. # Check for valid data
  1935. if X_brain_diff.shape[1] == 0:
  1936. print(f"No valid brain features for {brain_type}, skipping...")
  1937. continue
  1938. print(f"\nTraining NEW {brain_type} model for {sex} (longitudinal differences)...")
  1939. # ==========================================
  1940. # TRAIN NEW REAL MODEL using your existing function
  1941. # ==========================================
  1942. print(" Training NEW REAL model on difference data...")
  1943. results = train_test_ridge_model(
  1944. X_brain_diff, Y_subset, site_subset, mh_diff_cols,
  1945. f"{brain_type}_diff", sex, "longitudinal_diff",
  1946. longitudinal_results, longitudinal_results_dir,
  1947. config=ml_config
  1948. )
  1949. # ==========================================
  1950. # TRAIN NEW NULL MODEL using your existing function
  1951. # ==========================================
  1952. print(" Training NEW NULL model on difference data...")
  1953. null_results = generate_null_models(
  1954. X_brain_diff, Y_subset, site_subset, mh_diff_cols,
  1955. f"{brain_type}_diff", sex, "longitudinal_diff", results,
  1956. longitudinal_results, longitudinal_results_dir,
  1957. config=ml_config, n_null=n_null
  1958. )
  1959. print(f" Completed: {result_key}")
  1960. print(f" Real mean correlation: {np.mean(results['corr']):.3f}")
  1961. print(f" Null mean correlation: {np.mean(null_results['corr']):.3f}")
  1962. print(f" Improvement over null: {np.mean(results['corr']) - np.mean(null_results['corr']):.3f}")
  1963. print("-" * 50)
  1964. # Save NEW longitudinal results
  1965. print(f"\n{'='*50}")
  1966. print("SAVING NEW LONGITUDINAL DIFFERENCE RESULTS...")
  1967. # Update metadata
  1968. longitudinal_results['metadata']['total_models'] = len(longitudinal_results['models'])
  1969. longitudinal_results['metadata']['total_null_models'] = len(longitudinal_results['null_models'])
  1970. longitudinal_results['metadata']['last_updated'] = pd.Timestamp.now().isoformat()
  1971. # Save complete longitudinal results
  1972. with open(longitudinal_final_filename, 'wb') as f:
  1973. pickle.dump(longitudinal_results, f)
  1974. # Save summary only
  1975. longitudinal_summary_filename = f"{longitudinal_results_dir}/longitudinal_diff_summary.pkl"
  1976. with open(longitudinal_summary_filename, 'wb') as f:
  1977. pickle.dump(longitudinal_results['summary'], f)
  1978. # Save null summary
  1979. longitudinal_null_summary_filename = f"{longitudinal_results_dir}/longitudinal_diff_null_summary.pkl"
  1980. with open(longitudinal_null_summary_filename, 'wb') as f:
  1981. pickle.dump(longitudinal_results['null_summary'], f)
  1982. # Save metadata
  1983. longitudinal_metadata_filename = f"{longitudinal_results_dir}/longitudinal_diff_metadata.pkl"
  1984. with open(longitudinal_metadata_filename, 'wb') as f:
  1985. pickle.dump(longitudinal_results['metadata'], f)
  1986. print("\nNEW Longitudinal Difference ML Pipeline completed!")
  1987. print("🎉 NEW LONGITUDINAL MODELS TRAINED AND SAVED!")
  1988. print(f"📁 Directory: {longitudinal_results_dir}/")
  1989. print(f"📊 Total NEW longitudinal models: {len(longitudinal_results['models'])}")
  1990. print(f"📊 Total NEW longitudinal null models: {len(longitudinal_results['null_models'])}")
  1991. print("📋 Files saved:")
  1992. print(" - longitudinal_diff_results_complete.pkl")
  1993. print(" - longitudinal_diff_summary.pkl")
  1994. print(" - longitudinal_diff_null_summary.pkl")
  1995. print(" - longitudinal_diff_metadata.pkl")
  1996. print(f"\n📊 NEW longitudinal models trained:")
  1997. for key in sorted(longitudinal_results['models'].keys()):
  1998. if key in longitudinal_results['summary']:
  1999. mean_corr = longitudinal_results['summary'][key]['mean_correlation']
  2000. print(f" ✓ {key}: {mean_corr:.3f}")
  2001. return longitudinal_results
  2002. # ==========================================
  2003. # RUN NEW LONGITUDINAL DIFFERENCE ANALYSIS
  2004. # ==========================================
  2005. print("🚀 TRAINING NEW MODELS FOR LONGITUDINAL DIFFERENCE DATA...")
  2006. # Train NEW models optimized for longitudinal differences
  2007. longitudinal_results = run_longitudinal_diff_predictions(
  2008. smri_diff=smri_diff,
  2009. mh_diff=mh_diff,
  2010. gen_longitudinal=gen_longitudinal,
  2011. results_dir=results_dir,
  2012. ml_config=ML_CONFIG,
  2013. n_null=100 # Adjust as needed
  2014. )
  2015. if longitudinal_results:
  2016. print(f"\n✨ LONGITUDINAL DIFFERENCE ANALYSIS COMPLETED!")
  2017. print(f"📊 NEW models trained specifically for longitudinal changes")
  2018. print(f"📁 Original cross-sectional results: {results_dir}/all_results_complete.pkl")
  2019. print(f"📁 NEW longitudinal results: {results_dir}/longitudinal_diff_results/longitudinal_diff_results_complete.pkl")
  2020. # Show summary
  2021. print(f"\n📊 Longitudinal model performance:")
  2022. for key, summary in longitudinal_results['summary'].items():
  2023. print(f" {key}: {summary['mean_correlation']:.3f} ± {summary['std_correlation']:.3f}")
  2024. else:
  2025. print("❌ Longitudinal analysis failed")
  2026. # %%
  2027. # ==========================================
  2028. # SIGNIFICANCE TESTING FOR LONGITUDINAL DIFFERENCE MODELS
  2029. # ==========================================
  2030. def test_longitudinal_significance(longitudinal_results, results_dir):
  2031. """
  2032. Test significance for longitudinal difference models with FDR correction
  2033. """
  2034. print(f"\n{'='*60}")
  2035. print("TESTING LONGITUDINAL MODEL SIGNIFICANCE...")
  2036. print(f"{'='*60}")
  2037. # Load longitudinal results if not provided
  2038. if longitudinal_results is None:
  2039. longitudinal_results_dir = os.path.join(results_dir, 'longitudinal_diff_results_2y')
  2040. longitudinal_final_filename = f"{longitudinal_results_dir}/longitudinal_diff_results_complete.pkl"
  2041. if os.path.exists(longitudinal_final_filename):
  2042. with open(longitudinal_final_filename, 'rb') as f:
  2043. longitudinal_results = pickle.load(f)
  2044. else:
  2045. print("❌ No longitudinal results found!")
  2046. return None
  2047. model_keys = list(longitudinal_results['models'].keys())
  2048. print(f"Longitudinal models found: {len(model_keys)}")
  2049. for key in model_keys:
  2050. print(f" - {key}")
  2051. # Group model keys by sex group (since all are same timepoint: longitudinal_diff)
  2052. sex_groups = {}
  2053. for model_key in model_keys:
  2054. # Extract sex group from model key (e.g., 'longitudinal_diff_thickness_all' -> 'all')
  2055. parts = model_key.split('_')
  2056. sex_group = parts[-1] # Last part is sex group
  2057. if sex_group not in sex_groups:
  2058. sex_groups[sex_group] = []
  2059. sex_groups[sex_group].append(model_key)
  2060. print(f"\nSex groups found: {list(sex_groups.keys())}")
  2061. for sex_group, keys in sex_groups.items():
  2062. print(f" {sex_group}: {len(keys)} models")
  2063. # Initialize dictionaries to store significance results
  2064. longitudinal_significance_results = {}
  2065. longitudinal_fdr_results = {}
  2066. longitudinal_group_summaries = {}
  2067. # Process each sex group separately
  2068. for sex_group, group_model_keys in sex_groups.items():
  2069. print(f"\n{'='*40}")
  2070. print(f"PROCESSING SEX GROUP: {sex_group.upper()}")
  2071. print(f"{'='*40}")
  2072. # Extract data for this sex group only
  2073. group_real_corrs = []
  2074. group_null_corrs = []
  2075. group_model_info = []
  2076. group_significance_results = {}
  2077. print(f"Extracting correlation data for {sex_group} group...")
  2078. for model_key in group_model_keys:
  2079. real_model = longitudinal_results['models'][model_key]
  2080. null_model = longitudinal_results['null_models'][model_key]
  2081. n_beh = real_model['config']['n_behaviors']
  2082. # Get mean correlations across repetitions for each behavior
  2083. real_corrs = np.mean(real_model['corr'], axis=1) # Shape: (n_behaviors,)
  2084. null_corrs = null_model['corr'] # Shape: (n_behaviors, n_reps)
  2085. # Store for FDR correction within this sex group
  2086. group_real_corrs.extend(real_corrs)
  2087. group_null_corrs.extend(null_corrs.T) # Transpose to get (n_reps, n_behaviors), then flatten
  2088. # Store model info for each behavior
  2089. for b in range(n_beh):
  2090. group_model_info.append({
  2091. 'model_key': model_key,
  2092. 'behavior_idx': b,
  2093. 'behavior_name': real_model['config']['behavior_names'][b]
  2094. })
  2095. # Calculate p-values for this model
  2096. p_vals = np.zeros(n_beh)
  2097. for b in range(n_beh):
  2098. real_mean = real_corrs[b]
  2099. null_dist = null_corrs[b, :]
  2100. p_vals[b] = get_null_p(real_mean, null_dist)
  2101. # Store results for this model
  2102. group_significance_results[model_key] = {
  2103. 'p_values': p_vals,
  2104. 'real_correlations': real_corrs,
  2105. 'null_correlations': null_corrs,
  2106. 'behavior_names': real_model['config']['behavior_names']
  2107. }
  2108. print(f"Collected data for {len(group_real_corrs)} tests in {sex_group} group")
  2109. # ==========================================
  2110. # FDR CORRECTION WITHIN THIS SEX GROUP
  2111. # ==========================================
  2112. print(f"Applying FDR correction within {sex_group} group...")
  2113. # Collect all p-values for this sex group
  2114. group_all_p_vals = []
  2115. for model_key in group_model_keys:
  2116. group_all_p_vals.extend(group_significance_results[model_key]['p_values'])
  2117. group_all_p_vals = np.array(group_all_p_vals)
  2118. # Apply FDR correction within this sex group
  2119. group_fdr_rejected, group_fdr_p_corrected = fdr(group_all_p_vals, alpha=0.05, method='indep')
  2120. print(f" Tests in {sex_group}: {len(group_all_p_vals)}")
  2121. print(f" Significant after FDR: {np.sum(group_fdr_rejected)}")
  2122. print(f" Significance rate: {np.mean(group_fdr_rejected):.3f}")
  2123. # ==========================================
  2124. # ORGANIZE FDR RESULTS BY MODEL WITHIN SEX GROUP
  2125. # ==========================================
  2126. print(f"Organizing FDR results for {sex_group}...")
  2127. idx = 0
  2128. group_fdr_results = {}
  2129. for model_key in group_model_keys:
  2130. n_beh = len(group_significance_results[model_key]['p_values'])
  2131. # Extract FDR results for this model
  2132. model_fdr_rejected = group_fdr_rejected[idx:idx+n_beh]
  2133. model_fdr_p_corrected = group_fdr_p_corrected[idx:idx+n_beh]
  2134. # Store FDR results
  2135. group_fdr_results[model_key] = {
  2136. 'fdr_rejected': model_fdr_rejected,
  2137. 'fdr_p_corrected': model_fdr_p_corrected,
  2138. 'n_significant': np.sum(model_fdr_rejected),
  2139. 'real_correlations': group_significance_results[model_key]['real_correlations'],
  2140. 'p_values_uncorrected': group_significance_results[model_key]['p_values'],
  2141. 'behavior_names': group_significance_results[model_key]['behavior_names']
  2142. }
  2143. idx += n_beh
  2144. # Store results for this sex group
  2145. longitudinal_significance_results.update(group_significance_results)
  2146. longitudinal_fdr_results.update(group_fdr_results)
  2147. # Create group summary
  2148. group_total_significant = sum([group_fdr_results[key]['n_significant'] for key in group_model_keys])
  2149. longitudinal_group_summaries[sex_group] = {
  2150. 'sex_group': sex_group,
  2151. 'total_tests': len(group_all_p_vals),
  2152. 'total_significant': int(group_total_significant),
  2153. 'significance_rate': float(group_total_significant/len(group_all_p_vals)) if len(group_all_p_vals) > 0 else 0.0,
  2154. 'models': group_model_keys,
  2155. 'fdr_alpha': 0.05,
  2156. 'fdr_method': 'indep'
  2157. }
  2158. # Print summary for this group
  2159. print(f"\n{sex_group} GROUP SUMMARY:")
  2160. print(f" Total tests: {len(group_all_p_vals)}")
  2161. print(f" Significant: {group_total_significant}")
  2162. print(f" Rate: {group_total_significant/len(group_all_p_vals):.3f}" if len(group_all_p_vals) > 0 else " Rate: 0.000")
  2163. # Show significant models for this group
  2164. print(f" Model results:")
  2165. for model_key in group_model_keys:
  2166. n_sig = group_fdr_results[model_key]['n_significant']
  2167. n_total = len(group_fdr_results[model_key]['fdr_rejected'])
  2168. brain_type = model_key.split('_')[2] # Extract brain type from model key
  2169. print(f" {brain_type}: {n_sig}/{n_total} significant")
  2170. # ==========================================
  2171. # OVERALL SUMMARY FOR LONGITUDINAL MODELS
  2172. # ==========================================
  2173. print(f"\n{'='*60}")
  2174. print("LONGITUDINAL MODEL SIGNIFICANCE SUMMARY:")
  2175. print("="*60)
  2176. total_tests_all = 0
  2177. total_significant_all = 0
  2178. for sex_group in sorted(longitudinal_group_summaries.keys()):
  2179. summary = longitudinal_group_summaries[sex_group]
  2180. print(f"{sex_group:>8}: {summary['total_significant']:>3}/{summary['total_tests']:<3} ({summary['significance_rate']:.3f})")
  2181. total_tests_all += summary['total_tests']
  2182. total_significant_all += summary['total_significant']
  2183. print(f"\nGRAND TOTAL:")
  2184. print(f" Total tests: {total_tests_all}")
  2185. print(f" Total significant: {total_significant_all}")
  2186. print(f" Overall rate: {total_significant_all/total_tests_all:.3f}")
  2187. # ==========================================
  2188. # DETAILED RESULTS BY BRAIN TYPE AND BEHAVIOR
  2189. # ==========================================
  2190. print(f"\n{'='*60}")
  2191. print("DETAILED RESULTS BY BRAIN TYPE:")
  2192. print("="*60)
  2193. # Organize results by brain type
  2194. brain_type_results = {}
  2195. for model_key, fdr_result in longitudinal_fdr_results.items():
  2196. brain_type = model_key.split('_')[2] # Extract brain type
  2197. sex_group = model_key.split('_')[3] # Extract sex group
  2198. if brain_type not in brain_type_results:
  2199. brain_type_results[brain_type] = {}
  2200. brain_type_results[brain_type][sex_group] = fdr_result
  2201. # Print detailed results
  2202. for brain_type in sorted(brain_type_results.keys()):
  2203. print(f"\n{brain_type.upper()}:")
  2204. for sex_group in ['all', 'female', 'male']:
  2205. if sex_group in brain_type_results[brain_type]:
  2206. result = brain_type_results[brain_type][sex_group]
  2207. print(f" {sex_group:>6}: {result['n_significant']}/{len(result['fdr_rejected'])} significant")
  2208. # Show top significant behaviors
  2209. if result['n_significant'] > 0:
  2210. sig_indices = np.where(result['fdr_rejected'])[0]
  2211. sig_corrs = result['real_correlations'][sig_indices]
  2212. sig_behaviors = [result['behavior_names'][i] for i in sig_indices]
  2213. # Sort by correlation strength
  2214. sorted_indices = np.argsort(np.abs(sig_corrs))[::-1]
  2215. print(f" Top significant behaviors:")
  2216. for i in sorted_indices[:3]: # Show top 3
  2217. behavior = sig_behaviors[i]
  2218. corr = sig_corrs[i]
  2219. print(f" {behavior}: r={corr:.3f}")
  2220. # ==========================================
  2221. # ADD TO LONGITUDINAL RESULTS AND SAVE
  2222. # ==========================================
  2223. print(f"\nAdding significance results to longitudinal results...")
  2224. # Add significance results to longitudinal_results
  2225. longitudinal_results['significance'] = longitudinal_significance_results
  2226. longitudinal_results['fdr_results'] = longitudinal_fdr_results
  2227. longitudinal_results['significance_summary'] = {
  2228. 'by_sex_group': longitudinal_group_summaries,
  2229. 'overall': {
  2230. 'total_tests': total_tests_all,
  2231. 'total_significant': total_significant_all,
  2232. 'significance_rate': float(total_significant_all/total_tests_all) if total_tests_all > 0 else 0.0,
  2233. 'fdr_alpha': 0.05,
  2234. 'fdr_method': 'indep',
  2235. 'correction_level': 'within_sex_group'
  2236. }
  2237. }
  2238. # Save updated longitudinal results
  2239. longitudinal_results_dir = os.path.join(results_dir, 'longitudinal_diff_results_2y')
  2240. longitudinal_final_filename = f"{longitudinal_results_dir}/longitudinal_diff_results_complete_with_significance.pkl"
  2241. with open(longitudinal_final_filename, 'wb') as f:
  2242. pickle.dump(longitudinal_results, f)
  2243. # Save just significance results
  2244. longitudinal_significance_filename = f"{longitudinal_results_dir}/longitudinal_significance_results_only.pkl"
  2245. with open(longitudinal_significance_filename, 'wb') as f:
  2246. pickle.dump({
  2247. 'significance': longitudinal_significance_results,
  2248. 'fdr_results': longitudinal_fdr_results,
  2249. 'summary': longitudinal_results['significance_summary']
  2250. }, f)
  2251. print(f"\n🎉 LONGITUDINAL SIGNIFICANCE TESTING COMPLETED!")
  2252. print(f"📁 Results saved to:")
  2253. print(f" - longitudinal_diff_results_complete_with_significance.pkl")
  2254. print(f" - longitudinal_significance_results_only.pkl")
  2255. return longitudinal_results
  2256. # ==========================================
  2257. # RUN LONGITUDINAL SIGNIFICANCE TESTING
  2258. # ==========================================
  2259. print("🧠 TESTING SIGNIFICANCE FOR LONGITUDINAL MODELS...")
  2260. # Test significance for longitudinal models
  2261. longitudinal_results_with_significance = test_longitudinal_significance(
  2262. longitudinal_results,
  2263. results_dir
  2264. )
  2265. # ==========================================
  2266. # COMPARE CROSS-SECTIONAL VS LONGITUDINAL SIGNIFICANCE
  2267. # ==========================================
  2268. def compare_significance_rates(results_dir):
  2269. """Compare significance rates between cross-sectional and longitudinal models"""
  2270. print(f"\n{'='*60}")
  2271. print("COMPARING CROSS-SECTIONAL VS LONGITUDINAL SIGNIFICANCE:")
  2272. print("="*60)
  2273. # Load cross-sectional results
  2274. cross_sectional_file = f"{results_dir}/all_results_complete_with_significance_by_timepoint_sex.pkl"
  2275. if os.path.exists(cross_sectional_file):
  2276. with open(cross_sectional_file, 'rb') as f:
  2277. cross_sectional_results = pickle.load(f)
  2278. cs_overall = cross_sectional_results['significance_summary']['overall']
  2279. print(f"Cross-sectional significance rate: {cs_overall['significance_rate']:.3f}")
  2280. print(f" ({cs_overall['total_significant']}/{cs_overall['total_tests']} tests)")
  2281. else:
  2282. print("Cross-sectional significance results not found")
  2283. # Load longitudinal results
  2284. longitudinal_dir = os.path.join(results_dir, 'longitudinal_diff_results_2y')
  2285. longitudinal_file = f"{longitudinal_dir}/longitudinal_diff_results_complete_with_significance.pkl"
  2286. if os.path.exists(longitudinal_file):
  2287. with open(longitudinal_file, 'rb') as f:
  2288. longitudinal_results = pickle.load(f)
  2289. long_overall = longitudinal_results['significance_summary']['overall']
  2290. print(f"Longitudinal significance rate: {long_overall['significance_rate']:.3f}")
  2291. print(f" ({long_overall['total_significant']}/{long_overall['total_tests']} tests)")
  2292. else:
  2293. print("Longitudinal significance results not found")
  2294. # Run comparison
  2295. compare_significance_rates(results_dir)
  2296. # %%

nbdc_abcd_bpm_smri_impulsivity.ipynb at commit 85451d8, no license · at the source

Overview

Authors: Elvisha Dhamala1,2, Erynn Christensen1, Jamie L Hanson3,4, Jocelyn A Ricard5,6, Noelle Arcaro1, Simran Bhola1, Lisa Wiersch1, Katharina Brosch1, B T Thomas Yeo7,8,9,10,11,12, Avram J Holmes13, Sarah W Yip14,15,16,17
17 affiliations
  1. Institute of Behavioral Sciences, Feinstein Institutes for Medical Research, Manhasset, USA
  2. Department of Psychiatry, Donald and Barbara Zucker School of Medicine at Hofstra/Northwell, Uniondale, Hempstead, USA
  3. Learning Research and Development Center, University of Pittsburgh, Pittsburgh, USA
  4. Department of Psychology, University of Pittsburgh, Pittsburgh, USA
  5. Stanford Neurosciences Interdepartmental Program, Stanford University School of Medicine, Stanford, USA
  6. Department of Psychology, Stanford University, Stanford, USA
  7. Centre for Sleep & Cognition & Centre for Translational Magnetic Resonance Research, Yong Loo Lin School of Medicine, Singapore, National University of Singapore, Singapore, Singapore
  8. Department of Electrical and Computer Engineering, National University of Singapore, Singapore, Singapore
  9. N.1 Institute for Health, National University of Singapore, Singapore, Singapore
  10. Integrative Sciences and Engineering Programme (ISEP), National University of Singapore, Singapore, Singapore
  11. Department of Medicine, Healthy Longevity Research Programme, Human Potential Translational Research Programme & Institute for Digital Medicine (WisDM), Yong Loo Lin School of Medicine, National University of Singapore, Singapore, Singapore
  12. Martinos Center for Biomedical Imaging, Massachusetts General Hospital, Charlestown, USA
  13. Department of Psychiatry, Brain Health Institute, Rutgers University, Piscataway, USA
  14. Department of Psychiatry, Yale School of Medicine, New Haven, USA
  15. Child Study Center, Yale School of Medicine, New Haven, USA
  16. Center for Brain and Mind Health, Yale School of Medicine, New Haven, USA
  17. Wu Tsai Institute, Yale University, New Haven, USA
Journal: Molecular psychiatry, volume 31, issue 8, pages 4282-4296
Dates: received 16 June 2025; accepted 10 March 2026; published online 24 March 2026; in print 2026
Type: Research article · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1038/s41380-026-03526-2 · PMID 41876706 · PMCID PMC13364653 · OpenAlex W7140211364
Open access: hybrid, a free copy (OpenAlex)
Status: code verified (how the code is found and verified)
Categories: structural MRI / diffusion (modality), human (organism)
Methods: Statistics, Machine learning, Preprocessing
Keywords: Neuroscience, Biological techniques, Psychiatric disorders
MeSH: Impulsive Behavior*, Adolescent, Brain, Brain Mapping, Decision Making, Female, Gray Matter, Humans, Individuality, Machine Learning, Magnetic Resonance Imaging, Male, Neuroanatomy (* major topic)
Topic: Neural and Behavioral Psychology Studies (Cognitive Neuroscience, Neuroscience), according to OpenAlex
Citations: not cited yet (Europe PMC); 125 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.

Repository

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

elvisha/neuroanat_impulsivity

License: none: the authors keep all their rights
State: the link answers, verified on 30 September 2026
Evidence: files inventoried
Commit: 85451d816fb0de87cbbaa4bbcc04285f6fc54225, 14 October 2025
Languages: Jupyter (1)
Size: 2 files, 1 script
Software Heritage: not archived
Found in: “Code availability”
Holds: README, 1 notebook
Not found: license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Matplotlib (1 file), NumPy (1 file), pandas (1 file), scikit-learn (1 file), SciPy (1 file), seaborn (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 30 September 2026: the link answers
  • 30 September 2026: the link answers
2 files

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/s41380-026-03526-2.

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;
  • 1 script, each with its path and the digest of its content;
  • 2 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

No dataset and no data link were found in the paper.

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:

  • no repository, dataset or request procedure was recognized in it

Read it in the paper: doi.org/10.1038/s41380-026-03526-2.

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, 30 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 11 authors, 3 keywords, 13 MeSH terms, 11 funders, 112 references.

Cite

This paper

Dhamala, E., Christensen, E., Hanson, J. L., Ricard, J. A., Arcaro, N., Bhola, S., Wiersch, L., Brosch, K., Yeo, B. T. T., Holmes, A. J., & Yip, S. W. (2026). Neuroanatomy reflects individual variability in impulsivity in youth. Molecular psychiatry, 31(8), 4282-4296. https://doi.org/10.1038/s41380-026-03526-2

BibTeX

@article{dhamala2026neuroanatomy,
author = {Dhamala, Elvisha and Christensen, Erynn and Hanson, Jamie L and Ricard, Jocelyn A and Arcaro, Noelle and Bhola, Simran and Wiersch, Lisa and Brosch, Katharina and Yeo, B T Thomas and Holmes, Avram J and Yip, Sarah W},
title = {{Neuroanatomy reflects individual variability in impulsivity in youth}},
journal = {Molecular psychiatry},
year = {2026},
month = mar,
volume = {31},
number = {8},
pages = {4282--4296},
publisher = {Springer Nature},
issn = {1359-4184},
doi = {10.1038/s41380-026-03526-2},
url = {https://doi.org/10.1038/s41380-026-03526-2},
pmid = {41876706},
pmcid = {PMC13364653}
}

RIS

TY - JOUR
AU - Dhamala, Elvisha
AU - Christensen, Erynn
AU - Hanson, Jamie L
AU - Ricard, Jocelyn A
AU - Arcaro, Noelle
AU - Bhola, Simran
AU - Wiersch, Lisa
AU - Brosch, Katharina
AU - Yeo, B T Thomas
AU - Holmes, Avram J
AU - Yip, Sarah W
TI - Neuroanatomy reflects individual variability in impulsivity in youth
T2 - Molecular psychiatry
J2 - Mol Psychiatry
PY - 2026
DA - 2026/03/24
VL - 31
IS - 8
SP - 4282
EP - 4296
SN - 1359-4184
PB - Springer Nature
DO - 10.1038/s41380-026-03526-2
UR - https://doi.org/10.1038/s41380-026-03526-2
LA - en
ER -

CSL-JSON

{
"id": "10.1038/s41380-026-03526-2",
"type": "article-journal",
"title": "Neuroanatomy reflects individual variability in impulsivity in youth",
"container-title": "Molecular psychiatry",
"author": [
{
"family": "Dhamala",
"given": "Elvisha"
},
{
"family": "Christensen",
"given": "Erynn"
},
{
"family": "Hanson",
"given": "Jamie L"
},
{
"family": "Ricard",
"given": "Jocelyn A"
},
{
"family": "Arcaro",
"given": "Noelle"
},
{
"family": "Bhola",
"given": "Simran"
},
{
"family": "Wiersch",
"given": "Lisa"
},
{
"family": "Brosch",
"given": "Katharina"
},
{
"family": "Yeo",
"given": "B T Thomas"
},
{
"family": "Holmes",
"given": "Avram J"
},
{
"family": "Yip",
"given": "Sarah W"
}
],
"container-title-short": "Mol Psychiatry",
"volume": "31",
"issue": "8",
"page": "4282-4296",
"DOI": "10.1038/s41380-026-03526-2",
"PMID": "41876706",
"PMCID": "PMC13364653",
"ISSN": "1359-4184",
"publisher": "Springer Nature",
"URL": "https://doi.org/10.1038/s41380-026-03526-2",
"language": "en",
"issued": {
"date-parts": [
[
2026,
3,
24
]
]
}
}

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.1126/sciadv.adn4202 [code]
Functional brain networks are associated with both sex and gender in children.
Journal: Science advances
In common: statsmodels, seaborn, scikit-learn, 4 other tools, 18 references, 2 authors
[2] doi:10.1126/sciadv.adn1862 [code]
Generalizable and replicable brain-based predictions of cognitive functioning across common psychiatric illness.
Journal: Science advances
In common: scikit-learn, pandas, SciPy, 2 other tools, 12 references, 3 authors
[3] doi:10.1038/s41467-026-73668-y [code]
Convergent and divergent brain-cognition development in early adolescence.
Journal: Nature communications
In common: seaborn, scikit-learn, pandas, 3 other tools, 13 references, author Elvisha Dhamala
[4] doi:10.1038/s42003-024-06836-9 [code]
A shared spatial topography links the functional connectome correlates of cocaine use disorder and dopamine D&lt;sub&gt;2/3&lt;/sub&gt; receptor densities.
Journal: Communications biology
In common: seaborn, pandas, SciPy, 2 other tools, 1 reference, 3 authors
[5] 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: statsmodels, seaborn, scikit-learn, 4 other tools, 10 references
[6] doi:10.1038/s44220-025-00523-2 [code]
Sex-specific differences in brain activity dynamics of youth with a family history of substance use disorder.
Journal: Nature. Mental health
In common: SciPy, Matplotlib, NumPy, 11 references
[7] doi:10.1038/s41380-025-03196-6 [code]
Impulsivity and neuroticism share distinct functional connectivity signatures with alcohol-use risk in youth.
Journal: Molecular psychiatry
In common: 6 references, author Sarah W. Yip
[8] doi:10.1016/j.dcn.2025.101624 [code]
Brain functional connectivity, but not neuroanatomy, captures the interrelationship between sex and gender in preadolescents.
Journal: Developmental cognitive neuroscience
In common: statsmodels, seaborn, scikit-learn, 4 other tools, structural MRI / diffusion, 7 references
[9] doi:10.1038/s41586-025-09250-1 [code]
Longer scans boost prediction and cut costs in brain-wide association studies.
Journal: Nature
In common: scikit-learn, SciPy, Matplotlib, 1 other tool, 9 references
[10] doi:10.1162/imag_a_00233 [code]
Multilayer meta-matching: Translating phenotypic prediction models from multiple datasets to small data.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: scikit-learn, SciPy, Matplotlib, 1 other tool, 9 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.

Add the badge to the code's README

OSCR: the badge of this paper

If you are an author or a maintainer of this paper's code, you can add this badge. It says what OSCR knows of the paper (its code, its validated tracing map) and links the code to this page. Paste one of these snippets under the README's title. Keep the two marks around it, so that the badge can be replaced in place and is never added twice. OSCR changes nothing in your repository.

Markdown
<!-- oscr-badge:start -->
[![OSCR](https://openscicode.org/badge/doi/10.1038/s41380-026-03526-2.svg)](https://openscicode.org/badge/doi/10.1038/s41380-026-03526-2)
<!-- oscr-badge:end -->
reStructuredText
.. oscr-badge:start

.. image:: https://openscicode.org/badge/doi/10.1038/s41380-026-03526-2.svg
   :target: https://openscicode.org/badge/doi/10.1038/s41380-026-03526-2
   :alt: OSCR

.. oscr-badge:end
HTML
<!-- oscr-badge:start -->
<a href="https://openscicode.org/badge/doi/10.1038/s41380-026-03526-2"><img src="https://openscicode.org/badge/doi/10.1038/s41380-026-03526-2.svg" alt="OSCR"></a>
<!-- oscr-badge:end -->

On GitHub, open the README, choose Edit (the pencil), paste the snippet under the title, then "Commit changes".

If you choose to, you can have the badge kept up to date for you, with oscr badge --write from the command line or with the GitHub Action in your repository's workflows. Every other form and option (a card, AsciiDoc, LaTeX, shields.io) is on the badge 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: code errors and mismatches between the code and the paper about this paper and its code, from readers and its authors, beside the paper and its tracing map.

Report a mismatch between the code and the paper · Report a code error (signed in)

Reproductions: reports from readers who ran the authors' code and did not get the paper's result, what they ran, with which environment, commit and data.

Report a reproduction failure (signed in)

Versions of its code: the releases of its code tied to a version of this paper (the preprint, the accepted manuscript, the version of record…), each with the tracing map versioned with it.

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