OSCR

Quantifying extra-lesional interhemispheric cortical asymmetry in focal cortical dysplasia type II.

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] § RESULTS ↔ Bonn_visualization.ipynb, lines 449–497 · score 0.62 · occipital lobe, temporal lobe, frontal lobe, insular, subset, lesions
  2. [2] § METHODOLOGY › Surface‐based morphometry ↔ Bonn_visualization.ipynb, lines 518–524 · score 0.55 · mri_vol2surf, lesion volume, cortical surface, space

Paper

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

The paper is loaded when this pane is shown.

The authors' code

Jupyter notebook · 5,297 lines · 195 KB · MIT · 2 matches

  1. # %% [markdown]
  2. # # Visualization of Dataset
  3. # %%
  4. import pandas as pd
  5. import matplotlib.pyplot as plt
  6. import seaborn as sns
  7. # Load the participants.aparc.tsv data into a DataFrame
  8. df2 = pd.read_csv('derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv', sep='\t')
  9. # Define the age intervals and corresponding labels
  10. age_bins = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
  11. age_labels = [
  12. "0-5 years", "6-10 years", "11-15 years", "16-20 years", "21-25 years",
  13. "26-30 years", "31-35 years", "36-40 years", "41-45 years", "46-50 years",
  14. "51-55 years", "56-60 years", "61-65 years"
  15. ]
  16. # Drop missing values in the 'age_epilepsyonset' column
  17. df2 = df2.dropna(subset=['age_epilepsyonset'])
  18. # Bin the 'age_epilepsyonset' data into the defined age intervals
  19. df2['age_interval'] = pd.cut(df2['age_epilepsyonset'], bins=age_bins, labels=age_labels, right=False)
  20. # Plotting the histogram
  21. plt.figure(figsize=(12, 7))
  22. sns.histplot(data=df2, x='age_interval', shrink=0.8, color="teal", edgecolor="black", linewidth=1.2)
  23. # Customizing the plot
  24. plt.title("Distribution of Epilepsy Onset Age", fontsize=18, fontweight='bold', color='darkblue')
  25. plt.xlabel("Age Interval", fontsize=14, fontweight='bold')
  26. plt.ylabel("Number of Subjects", fontsize=14, fontweight='bold')
  27. plt.xticks(rotation=45, ha='right')
  28. plt.grid(axis='y', linestyle='--', alpha=0.7)
  29. # Adding a legend
  30. plt.legend(["Epilepsy Onset Age"], fontsize=12, loc="upper right", title="Legend", title_fontsize='13')
  31. # Display the plot
  32. plt.tight_layout()
  33. plt.show()
  34. # %%
  35. import pandas as pd
  36. import matplotlib.pyplot as plt
  37. import seaborn as sns
  38. # Load the participants.aparc.tsv data into a DataFrame
  39. df2 = pd.read_csv('derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv', sep='\t')
  40. # Define the age intervals and corresponding labels
  41. age_bins = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
  42. age_labels = [
  43. "0-5 years", "6-10 years", "11-15 years", "16-20 years", "21-25 years",
  44. "26-30 years", "31-35 years", "36-40 years", "41-45 years", "46-50 years",
  45. "51-55 years", "56-60 years", "61-65 years"
  46. ]
  47. # Drop missing values in the 'age_scan' column
  48. df2 = df2.dropna(subset=['age_scan'])
  49. # Bin the 'age_scan' data into the defined age intervals
  50. df2['age_interval'] = pd.cut(df2['age_scan'], bins=age_bins, labels=age_labels, right=False)
  51. # Plotting the histogram
  52. plt.figure(figsize=(12, 7))
  53. sns.histplot(data=df2, x='age_interval', shrink=0.8, color="teal", edgecolor="black", linewidth=1.2)
  54. # Customizing the plot
  55. plt.title("Distribution of age at scan onset", fontsize=18, fontweight='bold', color='darkblue')
  56. plt.xlabel("Age Interval", fontsize=14, fontweight='bold')
  57. plt.ylabel("Number of Subjects", fontsize=14, fontweight='bold')
  58. plt.xticks(rotation=45, ha='right')
  59. plt.grid(axis='y', linestyle='--', alpha=0.7)
  60. # Adding a legend
  61. plt.legend(["age_scan"], fontsize=12, loc="upper right", title="Legend", title_fontsize='13')
  62. # Display the plot
  63. plt.tight_layout()
  64. plt.show()
  65. # %%
  66. import pandas as pd
  67. import matplotlib.pyplot as plt
  68. import seaborn as sns
  69. # Load the participants.aparc.tsv data into a DataFrame
  70. df2 = pd.read_csv('derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv', sep='\t')
  71. # Define the age intervals and corresponding labels
  72. age_bins = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
  73. age_labels = [
  74. "0-5 years", "6-10 years", "11-15 years", "16-20 years", "21-25 years",
  75. "26-30 years", "31-35 years", "36-40 years", "41-45 years", "46-50 years",
  76. "51-55 years", "56-60 years", "61-65 years"
  77. ]
  78. # Drop missing values in the 'age_op' column
  79. df2 = df2.dropna(subset=['age_op'])
  80. # Bin the 'age_op' data into the defined age intervals
  81. df2['age_interval'] = pd.cut(df2['age_op'], bins=age_bins, labels=age_labels, right=False)
  82. # Plotting the histogram
  83. plt.figure(figsize=(12, 7))
  84. sns.histplot(data=df2, x='age_interval', shrink=0.8, color="teal", edgecolor="black", linewidth=1.2)
  85. # Customizing the plot
  86. plt.title("Distribution of age at operation onset", fontsize=18, fontweight='bold', color='darkblue')
  87. plt.xlabel("Age Interval", fontsize=14, fontweight='bold')
  88. plt.ylabel("Number of Subjects", fontsize=14, fontweight='bold')
  89. plt.xticks(rotation=45, ha='right')
  90. plt.grid(axis='y', linestyle='--', alpha=0.7)
  91. # Adding a legend
  92. plt.legend(["Age at operation onset"], fontsize=12, loc="upper right", title="Legend", title_fontsize='13')
  93. # Display the plot
  94. plt.tight_layout()
  95. plt.show()
  96. # %%
  97. import pandas as pd
  98. import matplotlib.pyplot as plt
  99. import seaborn as sns
  100. # Load the participants.aparc.tsv data into a DataFrame
  101. df2 = pd.read_csv('derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv', sep='\t')
  102. # Define the age intervals and corresponding labels
  103. age_bins = [1, 2, 3, 4, 5, 6, 7, 8]
  104. age_labels = [
  105. "less than 1 year", "1-2 years", "3-4 years", "5-6 years", "7-8 years",
  106. "9-10 years", "11-12 years"]
  107. # Drop missing values in the 'time_follow-up' column
  108. df2 = df2.dropna(subset=['time_follow-up'])
  109. # Bin the 'time_follow-up' data into the defined age intervals
  110. df2['age_interval'] = pd.cut(df2['time_follow-up'], bins=age_bins, labels=age_labels, right=False)
  111. # Plotting the histogram
  112. plt.figure(figsize=(12, 7))
  113. sns.histplot(data=df2, x='age_interval', shrink=0.8, color="teal", edgecolor="black", linewidth=1.2)
  114. # Customizing the plot
  115. plt.title("Distribution of time_follow-up", fontsize=18, fontweight='bold', color='darkblue')
  116. plt.xlabel("Age Interval", fontsize=14, fontweight='bold')
  117. plt.ylabel("Number of Subjects", fontsize=14, fontweight='bold')
  118. plt.xticks(rotation=45, ha='right')
  119. plt.grid(axis='y', linestyle='--', alpha=0.7)
  120. # Adding a legend
  121. plt.legend(["time_follow-up"], fontsize=12, loc="upper right", title="Legend", title_fontsize='13')
  122. # Display the plot
  123. plt.tight_layout()
  124. plt.show()
  125. # %%
  126. import pandas as pd
  127. import matplotlib.pyplot as plt
  128. import seaborn as sns
  129. # Load the participants.aparc.tsv data into a DataFrame
  130. df2 = pd.read_csv('derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv', sep='\t')
  131. # Drop missing values in the 'group' column
  132. df2 = df2.dropna(subset=['group'])
  133. # Plotting the histogram
  134. plt.figure(figsize=(12, 7))
  135. sns.histplot(data=df2, x='group', hue='group', shrink=0.8, multiple="dodge",
  136. palette={"fcd": "teal", "hc": "cyan"}, # Define colors for each group
  137. edgecolor="black", linewidth=1.2)
  138. # Customizing the plot
  139. plt.title("Number of Subjects with FCD and Healthy Controls", fontsize=18, fontweight='bold', color='darkblue')
  140. plt.xlabel("Group", fontsize=14, fontweight='bold')
  141. plt.ylabel("Number of Subjects", fontsize=14, fontweight='bold')
  142. plt.xticks(rotation=45, ha='right')
  143. plt.grid(axis='y', linestyle='--', alpha=0.7)
  144. # Adding a legend
  145. plt.legend(['fcd','hc'], fontsize=12, loc="upper right", title="Legend", title_fontsize='13')
  146. # Display the plot
  147. plt.tight_layout()
  148. plt.show()
  149. # %%
  150. import pandas as pd
  151. import matplotlib.pyplot as plt
  152. import seaborn as sns
  153. # Load the participants.aparc.tsv data into a DataFrame
  154. df2 = pd.read_csv('derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv', sep='\t')
  155. # Drop missing values in the 'histopathology' column
  156. df2 = df2.dropna(subset=['histopathology'])
  157. # Plotting the histogram
  158. plt.figure(figsize=(12, 7))
  159. sns.histplot(data=df2, x='histopathology', hue='histopathology', shrink=0.8, multiple="dodge",
  160. palette={"IIa": "teal", "IIb": "cyan"}, # Define colors for each group
  161. edgecolor="black", linewidth=1.2)
  162. # Customizing the plot
  163. plt.title("Distrubution of patients with fcd", fontsize=18, fontweight='bold', color='darkblue')
  164. plt.xlabel("histopathology", fontsize=14, fontweight='bold')
  165. plt.ylabel("Number of Subjects", fontsize=14, fontweight='bold')
  166. plt.xticks(rotation=45, ha='right')
  167. plt.grid(axis='y', linestyle='--', alpha=0.7)
  168. # Adding a legend
  169. plt.legend(['fcdIIa','fcdIIb'], fontsize=12, loc="upper right", title="Legend", title_fontsize='13')
  170. # Display the plot
  171. plt.tight_layout()
  172. plt.show()
  173. # %%
  174. import pandas as pd
  175. import matplotlib.pyplot as plt
  176. import seaborn as sns
  177. # Load the participants.aparc.tsv data into a DataFrame
  178. df2 = pd.read_csv('derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv', sep='\t')
  179. # Assuming cdf_fcd contains your data
  180. cdf_fcd = pd.DataFrame(df2)
  181. # Filter the DataFrame for the 'fcd' group
  182. fcd_data = cdf_fcd[cdf_fcd['group'] == 'fcd'].copy() # Use .copy() to avoid SettingWithCopyWarning
  183. # Replace NaN with a string to handle them in counts
  184. fcd_data.loc[:, 'histopathology'] = fcd_data['histopathology'].fillna('No operation')
  185. # Replace 'NaN' with 'No Operation' in the 'histopathology' column
  186. cdf_fcd['histopathology'] = cdf_fcd['histopathology'].replace('NaN', 'No Operation')
  187. # If we have actual NaN values (as missing data) and want to replace them:
  188. cdf_fcd['histopathology'] = cdf_fcd['histopathology'].fillna('No Operation')
  189. # Count the number of patients in each histopathology category within the 'fcd' group
  190. group_counts_fcd = fcd_data['histopathology'].value_counts()
  191. # Define colors for categories if known
  192. colors = ['teal', 'cyan', 'darkgreen'] # Adjust colors as needed for each category
  193. # Plot the histogram for all histopathology categories in the 'fcd' group
  194. group_counts_fcd.plot(kind='bar', color=colors)
  195. # Adding titles and labels
  196. plt.title('Distribution of Histopathology Types diagnosis in FCD Group', fontsize=18, fontweight='bold', color='darkblue')
  197. plt.xlabel('Histopathology Type', fontsize=14, fontweight='bold')
  198. plt.ylabel('Number of Patients', fontsize=14, fontweight='bold')
  199. plt.xticks(rotation=45, ha='right')
  200. plt.grid(axis='y', linestyle='--', alpha=0.7)
  201. # Display the plot
  202. plt.tight_layout()
  203. plt.show()
  204. # %%
  205. import pandas as pd
  206. import matplotlib.pyplot as plt
  207. import seaborn as sns
  208. # Load the participants.aparc.tsv data into a DataFrame
  209. df2 = pd.read_csv('derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv', sep='\t')
  210. # Drop missing values in the 'sex' column
  211. df2 = df2.dropna(subset=['sex'])
  212. # Plotting the histogram
  213. plt.figure(figsize=(12, 7))
  214. sns.histplot(data=df2, x='sex', hue='sex', shrink=0.8, multiple="dodge",
  215. palette={"F": "teal", "M": "cyan"}, # Define colors for each sex
  216. edgecolor="black", linewidth=1.2)
  217. # Customizing the plot
  218. plt.title("Distribution of sex", fontsize=18, fontweight='bold', color='darkblue')
  219. plt.xlabel("sex", fontsize=14, fontweight='bold')
  220. plt.ylabel("Number of Subjects", fontsize=14, fontweight='bold')
  221. plt.xticks(rotation=45, ha='right')
  222. plt.grid(axis='y', linestyle='--', alpha=0.7)
  223. # Adding a legend
  224. plt.legend(['Female','Male'], fontsize=12, loc="upper right", title="Legend", title_fontsize='13')
  225. # Display the plot
  226. plt.tight_layout()
  227. plt.show()
  228. # %%
  229. import pandas as pd
  230. import matplotlib.pyplot as plt
  231. import seaborn as sns
  232. # Load the participants.aparc.tsv data into a DataFrame
  233. df2 = pd.read_csv('derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv', sep='\t')
  234. # Define the angel levels and corresponding labels
  235. angel_bins = [1, 2, 3, 4, 5, 6, 7]
  236. angel_labels = [
  237. "IA,Completely seizure free since surgery", "IB,Nondisabling simple partial seizures only since surgery", "IIA,Initially free of disabling seizures but has rare seizures now", "IIB,Rare disabling seizures since surgery", "IIIA,Engel IIIA: Worthwhile seizure reduction",
  238. "IVB,No appreciable change"]
  239. # Drop missing values in the '1year_outcome' column
  240. df2 = df2.dropna(subset=['1year_outcome'])
  241. # Plotting the histogram
  242. plt.figure(figsize=(12, 7))
  243. sns.histplot(data=df2, x='1year_outcome', shrink=0.8, color="teal", edgecolor="black", linewidth=1.2)
  244. # Customizing the plot
  245. plt.title("distribution of 1year outcome", fontsize=18, fontweight='bold', color='darkblue')
  246. plt.xlabel("1year outcome", fontsize=14, fontweight='bold')
  247. plt.ylabel("Number of Subjects", fontsize=14, fontweight='bold')
  248. plt.xticks(rotation=45, ha='right')
  249. plt.grid(axis='y', linestyle='--', alpha=0.7)
  250. # Display the plot
  251. plt.tight_layout()
  252. plt.show()
  253. # %%
  254. import pandas as pd
  255. import matplotlib.pyplot as plt
  256. import seaborn as sns
  257. import os
  258. # Load the full path
  259. file_path = '/Volumes/groups/tohkagroup/Bonn_Epilepsy/derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv'
  260. # === Check if the file exists before reading ===
  261. if not os.path.exists(file_path):
  262. raise FileNotFoundError(f"File not found: {file_path}")
  263. # === Load the TSV file ===
  264. df2 = pd.read_csv(file_path, sep='\t')
  265. # === Drop missing values in the 'latest_outcome' column ===
  266. df2 = df2.dropna(subset=['latest_outcome'])
  267. # === Plotting the histogram ===
  268. plt.figure(figsize=(12, 7))
  269. sns.histplot(data=df2, x='latest_outcome', shrink=0.8, color="teal", edgecolor="black", linewidth=1.2)
  270. # === Customize the plot ===
  271. plt.title("Distribution of Latest Outcome", fontsize=18, fontweight='bold', color='darkblue')
  272. plt.xlabel("Latest Outcome (Engel Classification)", fontsize=14, fontweight='bold')
  273. plt.ylabel("Number of Subjects", fontsize=14, fontweight='bold')
  274. plt.xticks(rotation=45, ha='right')
  275. plt.grid(axis='y', linestyle='--', alpha=0.7)
  276. # === Display the plot ===
  277. plt.tight_layout()
  278. plt.show()
  279. # %%
  280. import pandas as pd
  281. import matplotlib.pyplot as plt
  282. import seaborn as sns
  283. # Load the participants.aparc.tsv data into a DataFrame
  284. df2 = pd.read_csv('derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv', sep='\t')
  285. # Drop missing values in the 'hemisphere' column
  286. df2 = df2.dropna(subset=['hemisphere'])
  287. # Plotting the histogram
  288. plt.figure(figsize=(12, 7))
  289. sns.histplot(data=df2, x='hemisphere', hue='hemisphere', shrink=0.8, multiple="dodge",
  290. palette={"L": "teal", "R": "cyan"}, # Define colors for each hemisphere
  291. edgecolor="black", linewidth=1.2)
  292. # Customizing the plot
  293. plt.title("Hemisphere affected by the FCD", fontsize=18, fontweight='bold', color='darkblue')
  294. plt.xlabel("hemisphere", fontsize=14, fontweight='bold')
  295. plt.ylabel("Number of Subjects", fontsize=14, fontweight='bold')
  296. plt.xticks(rotation=45, ha='right')
  297. plt.grid(axis='y', linestyle='--', alpha=0.7)
  298. # Adding a legend
  299. plt.legend(['Right','Left'], fontsize=12, loc="upper right", title="Legend", title_fontsize='13')
  300. # Display the plot
  301. plt.tight_layout()
  302. plt.show()
  303. # %%
  304. import pandas as pd
  305. import matplotlib.pyplot as plt
  306. import seaborn as sns
  307. # Load the participants.aparc.tsv data into a DataFrame
  308. df2 = pd.read_csv('derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv', sep='\t')
  309. # Define the lobe levels and corresponding labels
  310. lobe_bins = [1, 2, 3, 4, 5, 6]
  311. lobe_labels = [
  312. "frontal lobe", "temporal lobe", "pariatal lobe", "occipital lobe", "insular lobe"]
  313. # Drop missing values in the 'lobe' column
  314. df2 = df2.dropna(subset=['lobe'])
  315. # Plotting the histogram
  316. plt.figure(figsize=(12, 7))
  317. sns.histplot(data=df2, x='lobe', shrink=0.8, color="teal", edgecolor="black", linewidth=1.2)
  318. # Customizing the plot
  319. plt.title("distribution of lobe affected by FCD", fontsize=18, fontweight='bold', color='darkblue')
  320. plt.xlabel("lobe levels", fontsize=14, fontweight='bold')
  321. plt.ylabel("Number of Subjects", fontsize=14, fontweight='bold')
  322. plt.xticks(rotation=45, ha='right')
  323. plt.grid(axis='y', linestyle='--', alpha=0.7)
  324. # Display the plot
  325. plt.tight_layout()
  326. plt.show()
  327. # %% [markdown]
  328. # # Distribution of affected lobe by lesion in Stage 3
  329. # %%
  330. import pandas as pd
  331. import matplotlib.pyplot as plt
  332. import seaborn as sns
  333. # Load the participants.aparc.tsv data into a DataFrame
  334. df2 = pd.read_excel('/Volumes/groups/tohkagroup/Bonn_Epilepsy/derivatives_fsaverage/freesurfer7.4.1/participants.aparc_stage3.xlsx')
  335. # Define the lobe levels and corresponding labels
  336. lobe_bins = [1, 2, 3, 4, 5, 6]
  337. lobe_labels = [
  338. "frontal lobe", "temporal lobe", "pariatal lobe", "occipital lobe", "insular lobe"]
  339. # Drop missing values in the 'lobe' column
  340. df2 = df2.dropna(subset=['lobe'])
  341. # Total number of subjects for percentage calculation
  342. total= len(df2)
  343. # Plotting the histogram
  344. plt.figure(figsize=(12, 7))
  345. ax=sns.histplot(data=df2, x='lobe', shrink=0.8, color="teal", edgecolor="black", linewidth=1.2)
  346. # Add percentage labels on top of each bar
  347. for p in ax.patches:
  348. count = p.get_height()
  349. percentage = (count / total) * 100
  350. ax.annotate(
  351. f"{percentage:.1f}%",
  352. (p.get_x() + p.get_width() / 2., count),
  353. ha='center',
  354. va='bottom',
  355. fontsize=12,
  356. fontweight='bold'
  357. )
  358. # Customizing the plot
  359. plt.title("Distribution of lobe affected by FCD II", fontsize=18, fontweight='bold', color='darkblue')
  360. plt.xlabel("lobe", fontsize=14, fontweight='bold')
  361. plt.ylabel("Number of Subjects", fontsize=14, fontweight='bold')
  362. plt.xticks(rotation=45, ha='right')
  363. plt.grid(axis='y', linestyle='--', alpha=0.7)
  364. # Display the plot
  365. plt.tight_layout()
  366. plt.show()
  367. # %% [markdown]
  368. # ## Asymmetry Analysis
  369. # %% [markdown]
  370. # # Set the lesion mask to the same directory ( copy all the lesion mask to their own subject files)
  371. # %% [markdown]
  372. # # In high-performance servers like Kudos, perform the following three steps
  373. # %% [markdown]
  374. # Step 1: Convert NIfTI to Freesurfer Volume
  375. # %%
  376. mri_vol2vol --mov sub-00146_acq-T2sel_FLAIR_roi.nii.gz \
  377. --targ /research/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdna/sub-00146/mri/orig.mgz \
  378. --regheader \
  379. --o sub-00146_lesion_fs.mgz
  380. # %% [markdown]
  381. # Step 2: Convert Lesion Volume to a Surface Label
  382. # Once the lesion is in Freesurfer’s space, map it onto the cortical surface: (but please consider it based on your lesion lateralization use either right or left hemisphere ) for example for this subject the lesion is located in left hemisphere, and of course, export the directory to put the result in the actual directory:
  383. # %%
  384. export SUBJECTS_DIR=/research/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdna
  385. mri_vol2surf --mov sub-00146_lesion_fs.mgz --o sub-00146_lesion_surf.mgz --regheader sub-00146 --projfrac 0.5 --hemi rh
  386. # %% [markdown]
  387. # Step 3: Identify ROIs Overlapping with the Lesion
  388. # Better Solution: Using mri_segstats to Get ROI Overlaps at Once
  389. # %%
  390. export SUBJECTS_DIR=/research/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdna
  391. mri_segstats --annot sub-00146 rh aparc \
  392. --i $SUBJECTS_DIR/sub-00146/sub-00146_lesion_surf.mgz \
  393. --sum $SUBJECTS_DIR/sub-00146/stats/rh.lesion_roi_overlap.txt
  394. # %% [markdown]
  395. # # Removing cortical overlap:
  396. # %% [markdown]
  397. # # Filtering overlapped ROIs from lh.w-g.pct.stats and rh.w-g.pct.stats Based on StructName
  398. # %%
  399. import os
  400. import pandas as pd
  401. from io import StringIO
  402. # Input and output paths
  403. input_file = "/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdna/sub-00146/stats/rh.lesion_roi_overlap.txt"
  404. output_file = input_file.replace(".txt", "_filtered.txt")
  405. # Read the original file
  406. with open(input_file, "r") as f:
  407. lines = f.readlines()
  408. # Locate the '# ColHeaders' line
  409. col_header_idx = None
  410. for i, line in enumerate(lines):
  411. if line.startswith("# ColHeaders"):
  412. col_header_idx = i
  413. break
  414. if col_header_idx is None:
  415. raise ValueError("Could not find '# ColHeaders' line in the file.")
  416. # Extract column names from that line
  417. column_names = lines[col_header_idx].strip().replace("# ColHeaders", "").split()
  418. # Header + table sections
  419. header_lines = lines[:col_header_idx + 1]
  420. table_lines = lines[col_header_idx + 1:]
  421. # Load table into DataFrame
  422. df = pd.read_csv(StringIO("".join(table_lines)), delim_whitespace=True, names=column_names)
  423. # Filter for non-zero Mean
  424. filtered_df = df[df["Mean"] != 0]
  425. # Save back the filtered data
  426. with open(output_file, "w") as out:
  427. # Write header first
  428. for line in header_lines:
  429. out.write(line)
  430. # Write filtered table without header row (ColHeaders already written)
  431. filtered_df.to_csv(out, sep="\t", index=False, header=False)
  432. print(f"Filtered data saved to: {output_file}")
  433. # %% [markdown]
  434. # # Removing filtered ROIs from stats file for all measurements
  435. # %%
  436. import pandas as pd
  437. from io import StringIO
  438. #Input files
  439. lh_stats_file = "/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdna/sub-00146/stats/lh.w-g.pct.stats"
  440. rh_overlap_file = "/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdna/sub-00146/stats/rh.lesion_roi_overlap_filtered.txt"
  441. output_file = lh_stats_file.replace(".stats", ".filtered.stats")
  442. #Step 1: Read the overlap file and collect Index values
  443. indices_to_exclude = set()
  444. with open(rh_overlap_file, "r") as f:
  445. for line in f:
  446. if line.strip() and not line.startswith("#"):
  447. try:
  448. index = int(line.strip().split()[0]) # Assume index is first column
  449. indices_to_exclude.add(index)
  450. except ValueError:
  451. continue # Skip non-numeric or malformed lines
  452. #Step 2: Read the stats file and extract header + table
  453. with open(lh_stats_file, "r") as f:
  454. lines = f.readlines()
  455. #Find column header line
  456. col_header_index = None
  457. for i, line in enumerate(lines):
  458. if line.startswith("# ColHeaders"):
  459. col_header_index = i
  460. break
  461. if col_header_index is None:
  462. raise ValueError("'# ColHeaders' not found in the stats file.")
  463. # Get actual column names from header
  464. columns = lines[col_header_index].strip().replace("# ColHeaders", "").split()
  465. # Parse table lines into DataFrame
  466. table_data = lines[col_header_index + 1:]
  467. df = pd.read_csv(StringIO("".join(table_data)), delim_whitespace=True, names=columns)
  468. # Step 3: Filter out rows with Index in exclusion list
  469. df_filtered = df[~df["Index"].isin(indices_to_exclude)]
  470. # Step 4: Write back the filtered stats file
  471. with open(output_file, "w") as f:
  472. # Write all lines before table
  473. for line in lines[:col_header_index + 1]:
  474. f.write(line)
  475. # Write filtered table
  476. df_filtered.to_csv(f, sep="\t", index=False, header=False)
  477. print(f"Filtered stats saved to: {output_file}")
  478. # %%
  479. import pandas as pd
  480. from io import StringIO
  481. # Input files
  482. lh_stats_file = "/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdna/sub-00146/stats/rh.w-g.pct.stats"
  483. rh_overlap_file = "/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdna/sub-00146/stats/rh.lesion_roi_overlap_filtered.txt"
  484. output_file = lh_stats_file.replace(".stats", ".filtered.stats")
  485. # Step 1: Read the overlap file and collect Index values
  486. indices_to_exclude = set()
  487. with open(rh_overlap_file, "r") as f:
  488. for line in f:
  489. if line.strip() and not line.startswith("#"):
  490. try:
  491. index = int(line.strip().split()[0]) # Assume index is first column
  492. indices_to_exclude.add(index)
  493. except ValueError:
  494. continue # Skip non-numeric or malformed lines
  495. # Step 2: Read the stats file and extract header + table
  496. with open(lh_stats_file, "r") as f:
  497. lines = f.readlines()
  498. # Find column header line
  499. col_header_index = None
  500. for i, line in enumerate(lines):
  501. if line.startswith("# ColHeaders"):
  502. col_header_index = i
  503. break
  504. if col_header_index is None:
  505. raise ValueError("'# ColHeaders' not found in the stats file.")
  506. # Get actual column names from header
  507. columns = lines[col_header_index].strip().replace("# ColHeaders", "").split()
  508. # Parse table lines into DataFrame
  509. table_data = lines[col_header_index + 1:]
  510. df = pd.read_csv(StringIO("".join(table_data)), delim_whitespace=True, names=columns)
  511. # Step 3: Filter out rows with Index in exclusion list
  512. df_filtered = df[~df["Index"].isin(indices_to_exclude)]
  513. # Step 4: Write back the filtered stats file
  514. with open(output_file, "w") as f:
  515. # Write all lines before table
  516. for line in lines[:col_header_index + 1]:
  517. f.write(line)
  518. # Write filtered table
  519. df_filtered.to_csv(f, sep="\t", index=False, header=False)
  520. print(f" Filtered stats saved to: {output_file}")
  521. # %% [markdown]
  522. # # Asymmetry Index
  523. # %% [markdown]
  524. # # FCD, Intensity
  525. # %%
  526. import os
  527. import pandas as pd
  528. import numpy as np
  529. import seaborn as sns
  530. import matplotlib.pyplot as plt
  531. # Define the main subject directory
  532. main_folder = "/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdna/sub-00146/stats"
  533. # Function to extract Mean intensity values from FreeSurfer stats files
  534. def extract_intensity_values(stats_file):
  535. extracted_values = {}
  536. with open(stats_file, "r") as file:
  537. for line in file:
  538. if line.startswith("#") or not line.strip():
  539. continue # Skip headers and blank lines
  540. parts = line.split()
  541. if len(parts) >= 6:
  542. region = parts[4] # 5th column: StructName
  543. try:
  544. value = float(parts[5]) # 6th column: Mean intensity
  545. extracted_values[region] = value
  546. except ValueError:
  547. extracted_values[region] = np.nan
  548. return extracted_values
  549. # Input paths for left and right hemisphere stats
  550. lh_intensity_path = os.path.join(main_folder, "lh.w-g.pct.filtered.stats")
  551. rh_intensity_path = os.path.join(main_folder, "rh.w-g.pct.filtered.stats")
  552. # Extract data
  553. lh_intensity_data = extract_intensity_values(lh_intensity_path)
  554. rh_intensity_data = extract_intensity_values(rh_intensity_path)
  555. # Compute AI for each ROI
  556. roi_data = []
  557. for region in set(lh_intensity_data) | set(rh_intensity_data):
  558. left_value = lh_intensity_data.get(region, np.nan)
  559. right_value = rh_intensity_data.get(region, np.nan)
  560. if not np.isnan(left_value) and not np.isnan(right_value) and (left_value + right_value) != 0:
  561. ai_value = (left_value - right_value) / (left_value + right_value)
  562. else:
  563. ai_value = np.nan
  564. roi_data.append({
  565. "StructName": region,
  566. "Asymmetry_Index": ai_value,
  567. "Left": left_value,
  568. "Right": right_value
  569. })
  570. # Convert to DataFrame and save
  571. df = pd.DataFrame(roi_data)
  572. output_csv_path = "/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdna/sub-00146_intensity_freesurfer_stats.csv"
  573. df.to_csv(output_csv_path, index=False)
  574. print(f"Intensity Asymmetry Index saved to: {output_csv_path}")
  575. # Visualization
  576. plt.figure(figsize=(20, 8))
  577. sns.barplot(x="StructName", y="Asymmetry_Index", data=df.sort_values("Asymmetry_Index"), palette="coolwarm")
  578. plt.title("Intensity Asymmetry Index (AI) per Brain Region in sub-00146", fontsize=18)
  579. plt.xlabel("Brain Structure", fontsize=16)
  580. plt.ylabel("Asymmetry Index (AI)", fontsize=16)
  581. plt.xticks(rotation=90, fontsize=12)
  582. plt.axhline(0, color='gray', linestyle='--')
  583. plt.tight_layout()
  584. plt.show()
  585. # %% [markdown]
  586. # # FCD, other structural measurements : (thickness, volume, curvature, surface area)
  587. # %%
  588. import os
  589. import pandas as pd
  590. import numpy as np
  591. import seaborn as sns
  592. import matplotlib.pyplot as plt
  593. # === CONFIGURATION ===
  594. stats_dir = "/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdna/sub-00146/stats_cleaned"
  595. lh_stats = os.path.join(stats_dir, "lh.aparc.stats")
  596. rh_stats = os.path.join(stats_dir, "rh.aparc.stats")
  597. # Extract subject ID from path
  598. subject_id = os.path.basename(os.path.dirname(stats_dir))
  599. # Define metric columns (0-based)
  600. metric_columns = {
  601. "SurfArea": 2,
  602. "GrayVol": 3,
  603. "ThickAvg": 4,
  604. "MeanCurv": 6
  605. }
  606. # === FUNCTIONS ===
  607. def extract_metric(file_path, metric_name):
  608. metric_idx = metric_columns[metric_name]
  609. data = {}
  610. with open(file_path, "r") as file:
  611. for line in file:
  612. if line.startswith("#") or not line.strip():
  613. continue
  614. parts = line.split()
  615. roi = parts[0]
  616. try:
  617. value = float(parts[metric_idx])
  618. data[roi] = value
  619. except ValueError:
  620. data[roi] = np.nan
  621. return data
  622. def plot_ai(metric_name):
  623. lh_data = extract_metric(lh_stats, metric_name)
  624. rh_data = extract_metric(rh_stats, metric_name)
  625. roi_data = []
  626. for roi in sorted(set(lh_data.keys()) | set(rh_data.keys())):
  627. left = lh_data.get(roi, np.nan)
  628. right = rh_data.get(roi, np.nan)
  629. if not np.isnan(left) and not np.isnan(right) and (left + right) != 0:
  630. ai = (left - right) / (left + right)
  631. else:
  632. ai = np.nan
  633. roi_data.append({"StructName": roi, "Left": left, "Right": right, "Asymmetry_Index": ai})
  634. df = pd.DataFrame(roi_data)
  635. # Save to CSV with subject ID
  636. output_file = f"{subject_id}_{metric_name}_asymmetry.csv"
  637. output_path = os.path.join(stats_dir, output_file)
  638. df.to_csv(output_path, index=False)
  639. print(f"Saved {metric_name} AI to: {output_path}")
  640. # Plot
  641. plt.figure(figsize=(20, 8))
  642. sns.barplot(x="StructName", y="Asymmetry_Index", data=df.sort_values("Asymmetry_Index"), palette="coolwarm")
  643. plt.title(f"Asymmetry Index (AI) per Brain Region - {metric_name}", fontsize=18)
  644. plt.xlabel("Brain Structure", fontsize=14)
  645. plt.ylabel("Asymmetry Index (AI)", fontsize=14)
  646. plt.xticks(rotation=90, fontsize=10)
  647. plt.axhline(0, color='gray', linestyle='--')
  648. plt.tight_layout()
  649. plt.show()
  650. # === Example run ===
  651. for metric in metric_columns.keys():
  652. plot_ai(metric)
  653. # %% [markdown]
  654. # # Healthy individuals: intensity
  655. # %%
  656. import os
  657. import pandas as pd
  658. import numpy as np
  659. import seaborn as sns
  660. import matplotlib.pyplot as plt
  661. # Define the main subject directory
  662. main_folder = "/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_hc/sub-00170/stats"
  663. # Function to extract Mean intensity values from FreeSurfer stats files
  664. def extract_intensity_values(stats_file):
  665. extracted_values = {}
  666. with open(stats_file, "r") as file:
  667. for line in file:
  668. if line.startswith("#") or not line.strip():
  669. continue # Skip headers and blank lines
  670. parts = line.split()
  671. if len(parts) >= 6:
  672. region = parts[4] # 5th column: StructName
  673. try:
  674. value = float(parts[5]) # 6th column: Mean intensity
  675. extracted_values[region] = value
  676. except ValueError:
  677. extracted_values[region] = np.nan
  678. return extracted_values
  679. # Input paths for left and right hemisphere stats
  680. lh_intensity_path = os.path.join(main_folder, "lh.w-g.pct.stats")
  681. rh_intensity_path = os.path.join(main_folder, "rh.w-g.pct.stats")
  682. # Extract data
  683. lh_intensity_data = extract_intensity_values(lh_intensity_path)
  684. rh_intensity_data = extract_intensity_values(rh_intensity_path)
  685. # Compute asymmetry index for each ROI
  686. roi_data = []
  687. for region in set(lh_intensity_data) | set(rh_intensity_data):
  688. left_value = lh_intensity_data.get(region, np.nan)
  689. right_value = rh_intensity_data.get(region, np.nan)
  690. if not np.isnan(left_value) and not np.isnan(right_value) and (left_value + right_value) != 0:
  691. ai_value =(left_value - right_value) / (left_value + right_value)
  692. else:
  693. ai_value = np.nan
  694. roi_data.append({
  695. "StructName": region,
  696. "Asymmetry_Index": ai_value,
  697. "Left": left_value,
  698. "Right": right_value
  699. })
  700. # Convert to DataFrame and save
  701. df = pd.DataFrame(roi_data)
  702. output_csv_path = "/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_hc/sub-00170_intensity_freesurfer_stats.csv"
  703. df.to_csv(output_csv_path, index=False)
  704. print(f"Intensity Asymmetry Index saved to: {output_csv_path}")
  705. # Visualization
  706. plt.figure(figsize=(20, 8))
  707. sns.barplot(x="StructName", y="Asymmetry_Index", data=df.sort_values("Asymmetry_Index"), palette="coolwarm")
  708. plt.title("Intensity Asymmetry Index (AI) per Brain Region in sub-00170", fontsize=18)
  709. plt.xlabel("Brain Structure", fontsize=16)
  710. plt.ylabel("Asymmetry Index (AI)", fontsize=16)
  711. plt.xticks(rotation=90, fontsize=12)
  712. plt.axhline(0, color='gray', linestyle='--')
  713. plt.tight_layout()
  714. plt.show()
  715. # %% [markdown]
  716. # # Healthy individuals: other cortical measurements: (thickness, volume, curvature, surface area)
  717. # %%
  718. import os
  719. import pandas as pd
  720. import numpy as np
  721. import seaborn as sns
  722. import matplotlib.pyplot as plt
  723. # === CONFIGURATION ===
  724. stats_dir = "/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_hc/sub-00170/stats"
  725. lh_stats = os.path.join(stats_dir, "lh.aparc.stats")
  726. rh_stats = os.path.join(stats_dir, "rh.aparc.stats")
  727. # Extract subject ID from path
  728. subject_id = os.path.basename(os.path.dirname(stats_dir))
  729. # Define metric columns (0-based)
  730. metric_columns = {
  731. "SurfArea": 2,
  732. "GrayVol": 3,
  733. "ThickAvg": 4,
  734. "MeanCurv": 6
  735. }
  736. # === FUNCTIONS ===
  737. def extract_metric(file_path, metric_name):
  738. metric_idx = metric_columns[metric_name]
  739. data = {}
  740. with open(file_path, "r") as file:
  741. for line in file:
  742. if line.startswith("#") or not line.strip():
  743. continue
  744. parts = line.split()
  745. roi = parts[0]
  746. try:
  747. value = float(parts[metric_idx])
  748. data[roi] = value
  749. except ValueError:
  750. data[roi] = np.nan
  751. return data
  752. def plot_ai(metric_name):
  753. lh_data = extract_metric(lh_stats, metric_name)
  754. rh_data = extract_metric(rh_stats, metric_name)
  755. roi_data = []
  756. for roi in sorted(set(lh_data.keys()) | set(rh_data.keys())):
  757. left = lh_data.get(roi, np.nan)
  758. right = rh_data.get(roi, np.nan)
  759. if not np.isnan(left) and not np.isnan(right) and (left + right) != 0:
  760. ai = (left - right) / (left + right)
  761. else:
  762. ai = np.nan
  763. roi_data.append({"StructName": roi, "Left": left, "Right": right, "Asymmetry_Index": ai})
  764. df = pd.DataFrame(roi_data)
  765. # Save to CSV with subject ID
  766. output_file = f"{subject_id}_{metric_name}_asymmetry.csv"
  767. output_path = os.path.join(stats_dir, output_file)
  768. df.to_csv(output_path, index=False)
  769. print(f"Saved {metric_name} AI to: {output_path}")
  770. # Plot
  771. plt.figure(figsize=(20, 8))
  772. sns.barplot(x="StructName", y="Asymmetry_Index", data=df.sort_values("Asymmetry_Index"), palette="coolwarm")
  773. plt.title(f"Asymmetry Index (AI) per Brain Region - {metric_name}", fontsize=18)
  774. plt.xlabel("Brain Structure", fontsize=14)
  775. plt.ylabel("Asymmetry Index (AI)", fontsize=14)
  776. plt.xticks(rotation=90, fontsize=10)
  777. plt.axhline(0, color='gray', linestyle='--')
  778. plt.tight_layout()
  779. plt.show()
  780. # === Example run ===
  781. for metric in metric_columns.keys():
  782. plot_ai(metric)
  783. # %% [markdown]
  784. # # Independence check
  785. # %%
  786. import os
  787. import pandas as pd
  788. import numpy as np
  789. from scipy.stats import spearmanr, chi2_contingency
  790. import warnings
  791. warnings.filterwarnings("ignore")
  792. # === Base directory and metadata path ===
  793. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy"
  794. metadata_csv = os.path.join(data_root, "derivatives_fsaverage/participants.aparc.csv")
  795. metadata_tsv = os.path.join(data_root, "derivatives_fsaverage/participants.aparc.tsv")
  796. # === Load participant metadata ===
  797. try:
  798. if os.path.exists(metadata_csv):
  799. participants_df = pd.read_csv(metadata_csv, sep=None, engine='python', on_bad_lines='skip')
  800. elif os.path.exists(metadata_tsv):
  801. participants_df = pd.read_csv(metadata_tsv, sep='\t', engine='python', on_bad_lines='skip')
  802. else:
  803. raise FileNotFoundError("Neither CSV nor TSV metadata file found.")
  804. except Exception as e:
  805. raise RuntimeError(f"Failed to load metadata: {e}")
  806. # --- Clean metadata ---
  807. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  808. participants_df['group'] = participants_df['group'].astype(str).str.lower()
  809. participants_df['histopathology'] = participants_df['histopathology'].astype(str).str.lower()
  810. participants_df.dropna(subset=['sex', 'group', 'age_scan'], inplace=True)
  811. # Encode sex for numeric correlations
  812. sex_map = {'F': 0, 'M': 1}
  813. participants_df['sex_numeric'] = participants_df['sex'].map(sex_map)
  814. # === Prepare results container ===
  815. results = []
  816. # === Robust independence check function ===
  817. def check_independence(df, numeric_col=None, cat_col=None, contrast_name=""):
  818. n_total = len(df)
  819. n_per_group = df[cat_col].value_counts().to_dict() if cat_col else None
  820. if df.empty:
  821. results.append({
  822. "contrast": contrast_name,
  823. "test_type": "info",
  824. "statistic": None,
  825. "p_value": None,
  826. "note": "No data available"
  827. })
  828. return
  829. # Spearman correlation
  830. if numeric_col is not None and 'age_scan' in df.columns:
  831. if df[numeric_col].nunique() > 1:
  832. corr, pval = spearmanr(df['age_scan'], df[numeric_col])
  833. results.append({
  834. "contrast": contrast_name,
  835. "test_type": "spearman",
  836. "statistic": corr,
  837. "p_value": pval,
  838. "note": f"n_total={n_total}, n_per_group={n_per_group}"
  839. })
  840. else:
  841. results.append({
  842. "contrast": contrast_name,
  843. "test_type": "spearman",
  844. "statistic": None,
  845. "p_value": None,
  846. "note": f"Only one unique value, n_total={n_total}, n_per_group={n_per_group}"
  847. })
  848. # Chi-square test
  849. if cat_col is not None:
  850. contingency = pd.crosstab(df['sex'], df[cat_col])
  851. if contingency.size == 0 or contingency.shape[0] < 2 or contingency.shape[1] < 2:
  852. results.append({
  853. "contrast": contrast_name,
  854. "test_type": "chi2",
  855. "statistic": None,
  856. "p_value": None,
  857. "note": f"Not enough data, n_total={n_total}, n_per_group={n_per_group}"
  858. })
  859. else:
  860. chi2, p, _, _ = chi2_contingency(contingency)
  861. results.append({
  862. "contrast": contrast_name,
  863. "test_type": "chi2",
  864. "statistic": chi2,
  865. "p_value": p,
  866. "note": f"n_total={n_total}, n_per_group={n_per_group}"
  867. })
  868. # === 1. HC vs FCD (use 'group' column) ===
  869. hc_vs_fcd = participants_df[participants_df['group'].isin(['hc','fcd'])].copy()
  870. hc_vs_fcd['group_numeric'] = hc_vs_fcd['group'].map({'hc':0,'fcd':1})
  871. check_independence(hc_vs_fcd, numeric_col='group_numeric', cat_col='group', contrast_name="HC vs FCD")
  872. # === 2-4. FCD subtype contrasts (use 'histopathology' column) ===
  873. fcd_subtypes = participants_df[participants_df['histopathology'].isin(['fcdna','iia','iib'])].copy()
  874. subtype_pairs = [('fcdna','iia'), ('fcdna','iib'), ('iia','iib')]
  875. for sub1, sub2 in subtype_pairs:
  876. df_pair = fcd_subtypes[fcd_subtypes['histopathology'].isin([sub1, sub2])].copy()
  877. df_pair['histopathology_numeric'] = df_pair['histopathology'].map({sub1:0, sub2:1})
  878. check_independence(df_pair, numeric_col='histopathology_numeric', cat_col='histopathology',
  879. contrast_name=f"{sub1.upper()} vs {sub2.upper()}")
  880. # === Save results to CSV and Excel ===
  881. results_df = pd.DataFrame(results)
  882. csv_file = os.path.join(data_root, "independence_results.csv")
  883. excel_file = os.path.join(data_root, "independence_results.xlsx")
  884. results_df.to_csv(csv_file, index=False)
  885. results_df.to_excel(excel_file, index=False)
  886. print(f"Independence check results saved to:\nCSV: {csv_file}\nExcel: {excel_file}")
  887. # === Show results after running ===
  888. print("\n=== Independence Check Results ===")
  889. print(results_df)
  890. # %% [markdown]
  891. # ## Beta regression for intensity FCD vs HC
  892. # %%
  893. pip install rpy2
  894. # %%
  895. import os
  896. import pandas as pd
  897. import numpy as np
  898. from sklearn.preprocessing import MinMaxScaler
  899. from scipy.stats import spearmanr, chi2_contingency, kstest
  900. import matplotlib.pyplot as plt
  901. import seaborn as sns
  902. from statsmodels.stats.multitest import multipletests
  903. import rpy2.robjects as ro
  904. from rpy2.robjects import pandas2ri
  905. from rpy2.robjects.packages import importr
  906. from rpy2.robjects.conversion import localconverter
  907. from rpy2.robjects import r, globalenv, Formula
  908. import warnings
  909. warnings.filterwarnings("ignore")
  910. # ==== Load R package betareg ====
  911. utils = importr('utils')
  912. betareg = importr('betareg')
  913. # === Base directory and metadata path ===
  914. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy"
  915. metadata_csv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.csv")
  916. metadata_tsv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv")
  917. # === Load participant metadata safely ===
  918. try:
  919. if os.path.exists(metadata_csv):
  920. participants_df = pd.read_csv(metadata_csv, sep=None, engine='python', on_bad_lines='skip')
  921. elif os.path.exists(metadata_tsv):
  922. participants_df = pd.read_csv(metadata_tsv, sep='\t', engine='python', on_bad_lines='skip')
  923. else:
  924. raise FileNotFoundError("Neither CSV nor TSV metadata file found.")
  925. except Exception as e:
  926. raise RuntimeError(f"Failed to load metadata: {e}")
  927. # Clean metadata
  928. participants_df['group'] = participants_df['group'].replace({'fcdlla': 'fcd', 'fcdllb': 'fcd', 'fcdna': 'fcd'})
  929. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  930. participants_df['group'] = participants_df['group'].astype(str).str.lower()
  931. # Encode categorical variables
  932. sex_map = {'F': 0, 'M': 1}
  933. group_map = {'hc': 0, 'fcd': 1}
  934. participants_df['sex'] = participants_df['sex'].map(sex_map)
  935. participants_df['group'] = participants_df['group'].map(group_map)
  936. participants_df.dropna(subset=['sex', 'group', 'age_scan'], inplace=True)
  937. # === Independence Checks ===
  938. print("\n=== Independence Checks ===")
  939. print("Spearman correlation between age_scan and sex:", spearmanr(participants_df['age_scan'], participants_df['sex']))
  940. print("Spearman correlation between age_scan and group:", spearmanr(participants_df['age_scan'], participants_df['group']))
  941. contingency_sex_group = pd.crosstab(participants_df['sex'], participants_df['group'])
  942. chi2, p, _, _ = chi2_contingency(contingency_sex_group)
  943. print("Chi-square test between sex and group: chi2 =", chi2, ", p =", p)
  944. # === Load asymmetry data ===
  945. group_folders = ["asymmetry_fcdlla", "asymmetry_fcdllb", "asymmetry_fcdna", "asymmetry_hc"]
  946. all_data = []
  947. for group_folder in group_folders:
  948. group_path = os.path.join(data_root, group_folder)
  949. if not os.path.exists(group_path):
  950. continue
  951. for file_name in os.listdir(group_path):
  952. if not file_name.endswith("_intensity_freesurfer_stats.csv"):
  953. continue
  954. file_path = os.path.join(group_path, file_name)
  955. subject = file_name.replace("_intensity_freesurfer_stats.csv", "")
  956. try:
  957. df = pd.read_csv(file_path)
  958. if not all(col in df.columns for col in ['StructName', 'Asymmetry_Index']):
  959. continue
  960. df['participant_id'] = subject
  961. df['Unsigned_Asymmetry'] = np.abs(df['Asymmetry_Index'])
  962. all_data.append(df[['participant_id', 'StructName', 'Unsigned_Asymmetry']])
  963. except Exception as e:
  964. print(f"Failed loading {file_path}: {e}")
  965. # Merge all data
  966. asymmetry_df = pd.concat(all_data, ignore_index=True)
  967. full_df = pd.merge(asymmetry_df, participants_df, on='participant_id', how='inner')
  968. full_df.dropna(subset=['Unsigned_Asymmetry', 'sex', 'group', 'age_scan'], inplace=True)
  969. # === Check response distribution and transform if needed ===
  970. plt.figure(figsize=(8, 4))
  971. sns.histplot(full_df['Unsigned_Asymmetry'], kde=True)
  972. plt.title("Distribution of Intensity Asymmetry FCD vs HC")
  973. plt.show()
  974. ks_stat, ks_p = kstest(full_df['Unsigned_Asymmetry'], 'norm')
  975. if ks_p < 0.05:
  976. full_df['Unsigned_Asymmetry'] = np.sqrt(full_df['Unsigned_Asymmetry'])
  977. print("Data was not normal. Applied square root transformation.")
  978. # %%
  979. !pip install nilearn
  980. # %% [markdown]
  981. # ## Beta regression for thickness FCD vs HC
  982. # %%
  983. import os
  984. import pandas as pd
  985. import numpy as np
  986. import statsmodels.formula.api as smf
  987. from statsmodels.discrete.discrete_model import MNLogit
  988. from sklearn.preprocessing import MinMaxScaler
  989. from scipy.stats import spearmanr, chi2_contingency, kstest
  990. import statsmodels.api as sm
  991. import matplotlib.pyplot as plt
  992. import seaborn as sns
  993. from statsmodels.stats.multitest import multipletests
  994. import rpy2.robjects as ro
  995. from rpy2.robjects import pandas2ri
  996. from rpy2.robjects.packages import importr
  997. from rpy2.robjects.conversion import localconverter
  998. from rpy2.robjects import r, globalenv, Formula
  999. from rpy2.robjects.packages import importr
  1000. betareg = importr('betareg')
  1001. import warnings
  1002. warnings.filterwarnings("ignore")
  1003. # ==== 3. Load R packages ====
  1004. utils = importr('utils')
  1005. utils.install_packages('betareg') # will skip if already installed
  1006. betareg = importr('betareg')
  1007. # === Base directory and metadata path ===
  1008. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy"
  1009. metadata_csv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.csv")
  1010. metadata_tsv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv")
  1011. # === Load participant metadata safely ===
  1012. try:
  1013. if os.path.exists(metadata_csv):
  1014. participants_df = pd.read_csv(metadata_csv, sep=None, engine='python', on_bad_lines='skip')
  1015. elif os.path.exists(metadata_tsv):
  1016. participants_df = pd.read_csv(metadata_tsv, sep='\t', engine='python', on_bad_lines='skip')
  1017. else:
  1018. raise FileNotFoundError("Neither CSV nor TSV metadata file found.")
  1019. except Exception as e:
  1020. raise RuntimeError(f"Failed to load metadata: {e}")
  1021. # Clean metadata
  1022. participants_df['group'] = participants_df['group'].replace({'fcdlla': 'fcd', 'fcdllb': 'fcd', 'fcdna': 'fcd'})
  1023. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  1024. participants_df['group'] = participants_df['group'].astype(str).str.lower()
  1025. # Encode categorical variables
  1026. sex_map = {'F': 0, 'M': 1}
  1027. group_map = {'hc': 0, 'fcd': 1}
  1028. participants_df['sex'] = participants_df['sex'].map(sex_map)
  1029. participants_df['group'] = participants_df['group'].map(group_map)
  1030. participants_df.dropna(subset=['sex', 'group', 'age_scan'], inplace=True)
  1031. # === Independence Checks ===
  1032. print("\n=== Independence Checks ===")
  1033. print("Spearman correlation between age_scan and sex:", spearmanr(participants_df['age_scan'], participants_df['sex']))
  1034. print("Spearman correlation between age_scan and group:", spearmanr(participants_df['age_scan'], participants_df['group']))
  1035. contingency_sex_group = pd.crosstab(participants_df['sex'], participants_df['group'])
  1036. chi2, p, _, _ = chi2_contingency(contingency_sex_group)
  1037. print("Chi-square test between sex and group: chi2 =", chi2, ", p =", p)
  1038. # === Load asymmetry data ===
  1039. group_folders = ["asymmetry_fcdlla", "asymmetry_fcdllb", "asymmetry_fcdna", "asymmetry_hc"]
  1040. all_data = []
  1041. # Load thickness asymmetry data from nested structure
  1042. for group_folder in group_folders:
  1043. group_path = os.path.join(data_root, group_folder)
  1044. if not os.path.exists(group_path):
  1045. print(f"Missing: {group_path}")
  1046. continue
  1047. # Iterate over subject folders inside group folder
  1048. for subject in os.listdir(group_path):
  1049. subj_folder_path = os.path.join(group_path, subject)
  1050. if not os.path.isdir(subj_folder_path):
  1051. continue
  1052. # Use 'stats_cleaned' except for HC group which uses 'stats'
  1053. if group_folder == "asymmetry_hc":
  1054. stats_folder = os.path.join(subj_folder_path, "stats")
  1055. else:
  1056. stats_folder = os.path.join(subj_folder_path, "stats_cleaned")
  1057. file_name = f"{subject}_ThickAvg_asymmetry.csv"
  1058. file_path = os.path.join(stats_folder, file_name)
  1059. if not os.path.exists(file_path):
  1060. print(f"Not found: {file_path}")
  1061. continue
  1062. try:
  1063. df = pd.read_csv(file_path)
  1064. # Check necessary columns
  1065. if not all(col in df.columns for col in ['StructName', 'Asymmetry_Index']):
  1066. print(f"Missing columns in {file_path}")
  1067. continue
  1068. df['participant_id'] = subject
  1069. df['Unsigned_Asymmetry'] = np.abs(df['Asymmetry_Index'])
  1070. all_data.append(df[['participant_id', 'StructName', 'Unsigned_Asymmetry']])
  1071. except Exception as e:
  1072. print(f"Failed loading {file_path}: {e}")
  1073. # Merge all data
  1074. asymmetry_df = pd.concat(all_data, ignore_index=True)
  1075. full_df = pd.merge(asymmetry_df, participants_df, on='participant_id', how='inner')
  1076. full_df.dropna(subset=['Unsigned_Asymmetry', 'sex', 'group', 'age_scan'], inplace=True)
  1077. # === Check response distribution and transform if needed ===
  1078. plt.figure(figsize=(8, 4))
  1079. sns.histplot(full_df['Unsigned_Asymmetry'], kde=True)
  1080. plt.title("Distribution of Thickness Asymmetry FCD vs HC")
  1081. plt.show()
  1082. ks_stat, ks_p = kstest(full_df['Unsigned_Asymmetry'], 'norm')
  1083. if ks_p < 0.05:
  1084. full_df['Unsigned_Asymmetry'] = np.sqrt(full_df['Unsigned_Asymmetry'])
  1085. print("Data was not normal. Applied square root transformation.")
  1086. # %% [markdown]
  1087. # ## Beta regression for volume FCD vs HC
  1088. # %%
  1089. import os
  1090. import pandas as pd
  1091. import numpy as np
  1092. import statsmodels.formula.api as smf
  1093. from statsmodels.discrete.discrete_model import MNLogit
  1094. from sklearn.preprocessing import MinMaxScaler
  1095. from scipy.stats import spearmanr, chi2_contingency, kstest
  1096. import statsmodels.api as sm
  1097. import matplotlib.pyplot as plt
  1098. import seaborn as sns
  1099. from statsmodels.stats.multitest import multipletests
  1100. import rpy2.robjects as ro
  1101. from rpy2.robjects import pandas2ri
  1102. from rpy2.robjects.packages import importr
  1103. from rpy2.robjects.conversion import localconverter
  1104. from rpy2.robjects import r, globalenv, Formula
  1105. from rpy2.robjects.packages import importr
  1106. betareg = importr('betareg')
  1107. import warnings
  1108. warnings.filterwarnings("ignore")
  1109. # ==== 3. Load R packages ====
  1110. utils = importr('utils')
  1111. utils.install_packages('betareg') # will skip if already installed
  1112. betareg = importr('betareg')
  1113. # === Base directory and metadata path ===
  1114. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy"
  1115. metadata_csv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.csv")
  1116. metadata_tsv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv")
  1117. # === Load participant metadata safely ===
  1118. try:
  1119. if os.path.exists(metadata_csv):
  1120. participants_df = pd.read_csv(metadata_csv, sep=None, engine='python', on_bad_lines='skip')
  1121. elif os.path.exists(metadata_tsv):
  1122. participants_df = pd.read_csv(metadata_tsv, sep='\t', engine='python', on_bad_lines='skip')
  1123. else:
  1124. raise FileNotFoundError("Neither CSV nor TSV metadata file found.")
  1125. except Exception as e:
  1126. raise RuntimeError(f"Failed to load metadata: {e}")
  1127. # Clean metadata
  1128. participants_df['group'] = participants_df['group'].replace({'fcdlla': 'fcd', 'fcdllb': 'fcd', 'fcdna': 'fcd'})
  1129. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  1130. participants_df['group'] = participants_df['group'].astype(str).str.lower()
  1131. # Encode categorical variables
  1132. sex_map = {'F': 0, 'M': 1}
  1133. group_map = {'hc': 0, 'fcd': 1}
  1134. participants_df['sex'] = participants_df['sex'].map(sex_map)
  1135. participants_df['group'] = participants_df['group'].map(group_map)
  1136. participants_df.dropna(subset=['sex', 'group', 'age_scan'], inplace=True)
  1137. # === Independence Checks ===
  1138. print("\n=== Independence Checks ===")
  1139. print("Spearman correlation between age_scan and sex:", spearmanr(participants_df['age_scan'], participants_df['sex']))
  1140. print("Spearman correlation between age_scan and group:", spearmanr(participants_df['age_scan'], participants_df['group']))
  1141. contingency_sex_group = pd.crosstab(participants_df['sex'], participants_df['group'])
  1142. chi2, p, _, _ = chi2_contingency(contingency_sex_group)
  1143. print("Chi-square test between sex and group: chi2 =", chi2, ", p =", p)
  1144. # === Load asymmetry data ===
  1145. group_folders = ["asymmetry_fcdlla", "asymmetry_fcdllb", "asymmetry_fcdna", "asymmetry_hc"]
  1146. all_data = []
  1147. # Load volume asymmetry data from nested structure
  1148. for group_folder in group_folders:
  1149. group_path = os.path.join(data_root, group_folder)
  1150. if not os.path.exists(group_path):
  1151. print(f"Missing: {group_path}")
  1152. continue
  1153. # Iterate over subject folders inside group folder
  1154. for subject in os.listdir(group_path):
  1155. subj_folder_path = os.path.join(group_path, subject)
  1156. if not os.path.isdir(subj_folder_path):
  1157. continue
  1158. # Use 'stats_cleaned' except for HC group which uses 'stats'
  1159. if group_folder == "asymmetry_hc":
  1160. stats_folder = os.path.join(subj_folder_path, "stats")
  1161. else:
  1162. stats_folder = os.path.join(subj_folder_path, "stats_cleaned")
  1163. file_name = f"{subject}_GrayVol_asymmetry.csv"
  1164. file_path = os.path.join(stats_folder, file_name)
  1165. if not os.path.exists(file_path):
  1166. print(f"Not found: {file_path}")
  1167. continue
  1168. try:
  1169. df = pd.read_csv(file_path)
  1170. # Check necessary columns
  1171. if not all(col in df.columns for col in ['StructName', 'Asymmetry_Index']):
  1172. print(f"Missing columns in {file_path}")
  1173. continue
  1174. df['participant_id'] = subject
  1175. df['Unsigned_Asymmetry'] = np.abs(df['Asymmetry_Index'])
  1176. all_data.append(df[['participant_id', 'StructName', 'Unsigned_Asymmetry']])
  1177. except Exception as e:
  1178. print(f"Failed loading {file_path}: {e}")
  1179. # Merge all data
  1180. asymmetry_df = pd.concat(all_data, ignore_index=True)
  1181. full_df = pd.merge(asymmetry_df, participants_df, on='participant_id', how='inner')
  1182. full_df.dropna(subset=['Unsigned_Asymmetry', 'sex', 'group', 'age_scan'], inplace=True)
  1183. # === Check response distribution and transform if needed ===
  1184. plt.figure(figsize=(8, 4))
  1185. sns.histplot(full_df['Unsigned_Asymmetry'], kde=True)
  1186. plt.title("Distribution of Volume Asymmetry FCD vs HC")
  1187. plt.show()
  1188. ks_stat, ks_p = kstest(full_df['Unsigned_Asymmetry'], 'norm')
  1189. if ks_p < 0.05:
  1190. full_df['Unsigned_Asymmetry'] = np.sqrt(full_df['Unsigned_Asymmetry'])
  1191. print("Data was not normal. Applied square root transformation.")
  1192. # %% [markdown]
  1193. # ## Beta regression for curvature FCD vs HC
  1194. # %%
  1195. import os
  1196. import pandas as pd
  1197. import numpy as np
  1198. import statsmodels.formula.api as smf
  1199. from statsmodels.discrete.discrete_model import MNLogit
  1200. from sklearn.preprocessing import MinMaxScaler
  1201. from scipy.stats import spearmanr, chi2_contingency, kstest
  1202. import statsmodels.api as sm
  1203. import matplotlib.pyplot as plt
  1204. import seaborn as sns
  1205. from statsmodels.stats.multitest import multipletests
  1206. import rpy2.robjects as ro
  1207. from rpy2.robjects import pandas2ri
  1208. from rpy2.robjects.packages import importr
  1209. from rpy2.robjects.conversion import localconverter
  1210. from rpy2.robjects import r, globalenv, Formula
  1211. from rpy2.robjects.packages import importr
  1212. betareg = importr('betareg')
  1213. import warnings
  1214. warnings.filterwarnings("ignore")
  1215. # ==== 3. Load R packages ====
  1216. utils = importr('utils')
  1217. utils.install_packages('betareg') # will skip if already installed
  1218. betareg = importr('betareg')
  1219. # === Base directory and metadata path ===
  1220. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy"
  1221. metadata_csv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.csv")
  1222. metadata_tsv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv")
  1223. # === Load participant metadata safely ===
  1224. try:
  1225. if os.path.exists(metadata_csv):
  1226. participants_df = pd.read_csv(metadata_csv, sep=None, engine='python', on_bad_lines='skip')
  1227. elif os.path.exists(metadata_tsv):
  1228. participants_df = pd.read_csv(metadata_tsv, sep='\t', engine='python', on_bad_lines='skip')
  1229. else:
  1230. raise FileNotFoundError("Neither CSV nor TSV metadata file found.")
  1231. except Exception as e:
  1232. raise RuntimeError(f"Failed to load metadata: {e}")
  1233. # Clean metadata
  1234. participants_df['group'] = participants_df['group'].replace({'fcdlla': 'fcd', 'fcdllb': 'fcd', 'fcdna': 'fcd'})
  1235. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  1236. participants_df['group'] = participants_df['group'].astype(str).str.lower()
  1237. # Encode categorical variables
  1238. sex_map = {'F': 0, 'M': 1}
  1239. group_map = {'hc': 0, 'fcd': 1}
  1240. participants_df['sex'] = participants_df['sex'].map(sex_map)
  1241. participants_df['group'] = participants_df['group'].map(group_map)
  1242. participants_df.dropna(subset=['sex', 'group', 'age_scan'], inplace=True)
  1243. # === Independence Checks ===
  1244. print("\n=== Independence Checks ===")
  1245. print("Spearman correlation between age_scan and sex:", spearmanr(participants_df['age_scan'], participants_df['sex']))
  1246. print("Spearman correlation between age_scan and group:", spearmanr(participants_df['age_scan'], participants_df['group']))
  1247. contingency_sex_group = pd.crosstab(participants_df['sex'], participants_df['group'])
  1248. chi2, p, _, _ = chi2_contingency(contingency_sex_group)
  1249. print("Chi-square test between sex and group: chi2 =", chi2, ", p =", p)
  1250. # === Load asymmetry data ===
  1251. group_folders = ["asymmetry_fcdlla", "asymmetry_fcdllb", "asymmetry_fcdna", "asymmetry_hc"]
  1252. all_data = []
  1253. # Load curvature asymmetry data from nested structure
  1254. for group_folder in group_folders:
  1255. group_path = os.path.join(data_root, group_folder)
  1256. if not os.path.exists(group_path):
  1257. print(f"Missing: {group_path}")
  1258. continue
  1259. # Iterate over subject folders inside group folder
  1260. for subject in os.listdir(group_path):
  1261. subj_folder_path = os.path.join(group_path, subject)
  1262. if not os.path.isdir(subj_folder_path):
  1263. continue
  1264. # Use 'stats_cleaned' except for HC group which uses 'stats'
  1265. if group_folder == "asymmetry_hc":
  1266. stats_folder = os.path.join(subj_folder_path, "stats")
  1267. else:
  1268. stats_folder = os.path.join(subj_folder_path, "stats_cleaned")
  1269. file_name = f"{subject}_MeanCurv_asymmetry.csv"
  1270. file_path = os.path.join(stats_folder, file_name)
  1271. if not os.path.exists(file_path):
  1272. print(f"Not found: {file_path}")
  1273. continue
  1274. try:
  1275. df = pd.read_csv(file_path)
  1276. # Check necessary columns
  1277. if not all(col in df.columns for col in ['StructName', 'Asymmetry_Index']):
  1278. print(f"Missing columns in {file_path}")
  1279. continue
  1280. df['participant_id'] = subject
  1281. df['Unsigned_Asymmetry'] = np.abs(df['Asymmetry_Index'])
  1282. all_data.append(df[['participant_id', 'StructName', 'Unsigned_Asymmetry']])
  1283. except Exception as e:
  1284. print(f"Failed loading {file_path}: {e}")
  1285. # Merge all data
  1286. asymmetry_df = pd.concat(all_data, ignore_index=True)
  1287. full_df = pd.merge(asymmetry_df, participants_df, on='participant_id', how='inner')
  1288. full_df.dropna(subset=['Unsigned_Asymmetry', 'sex', 'group', 'age_scan'], inplace=True)
  1289. # === Check response distribution and transform if needed ===
  1290. plt.figure(figsize=(8, 4))
  1291. sns.histplot(full_df['Unsigned_Asymmetry'], kde=True)
  1292. plt.title("Distribution of Curvature Asymmetry FCD vs HC")
  1293. plt.show()
  1294. ks_stat, ks_p = kstest(full_df['Unsigned_Asymmetry'], 'norm')
  1295. if ks_p < 0.05:
  1296. full_df['Unsigned_Asymmetry'] = np.sqrt(full_df['Unsigned_Asymmetry'])
  1297. print("Data was not normal. Applied square root transformation.")
  1298. # %% [markdown]
  1299. # ## Beta regression for surface area FCD vs HC
  1300. # %%
  1301. import os
  1302. import pandas as pd
  1303. import numpy as np
  1304. import statsmodels.formula.api as smf
  1305. from statsmodels.discrete.discrete_model import MNLogit
  1306. from sklearn.preprocessing import MinMaxScaler
  1307. from scipy.stats import spearmanr, chi2_contingency, kstest
  1308. import statsmodels.api as sm
  1309. import matplotlib.pyplot as plt
  1310. import seaborn as sns
  1311. from statsmodels.stats.multitest import multipletests
  1312. import rpy2.robjects as ro
  1313. from rpy2.robjects import pandas2ri
  1314. from rpy2.robjects.packages import importr
  1315. from rpy2.robjects.conversion import localconverter
  1316. from rpy2.robjects import r, globalenv, Formula
  1317. from rpy2.robjects.packages import importr
  1318. betareg = importr('betareg')
  1319. import warnings
  1320. warnings.filterwarnings("ignore")
  1321. # ==== 3. Load R packages ====
  1322. utils = importr('utils')
  1323. utils.install_packages('betareg') # will skip if already installed
  1324. betareg = importr('betareg')
  1325. # === Base directory and metadata path ===
  1326. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy"
  1327. metadata_csv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.csv")
  1328. metadata_tsv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv")
  1329. # === Load participant metadata safely ===
  1330. try:
  1331. if os.path.exists(metadata_csv):
  1332. participants_df = pd.read_csv(metadata_csv, sep=None, engine='python', on_bad_lines='skip')
  1333. elif os.path.exists(metadata_tsv):
  1334. participants_df = pd.read_csv(metadata_tsv, sep='\t', engine='python', on_bad_lines='skip')
  1335. else:
  1336. raise FileNotFoundError("Neither CSV nor TSV metadata file found.")
  1337. except Exception as e:
  1338. raise RuntimeError(f"Failed to load metadata: {e}")
  1339. # Clean metadata
  1340. participants_df['group'] = participants_df['group'].replace({'fcdlla': 'fcd', 'fcdllb': 'fcd', 'fcdna': 'fcd'})
  1341. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  1342. participants_df['group'] = participants_df['group'].astype(str).str.lower()
  1343. # Encode categorical variables
  1344. sex_map = {'F': 0, 'M': 1}
  1345. group_map = {'hc': 0, 'fcd': 1}
  1346. participants_df['sex'] = participants_df['sex'].map(sex_map)
  1347. participants_df['group'] = participants_df['group'].map(group_map)
  1348. participants_df.dropna(subset=['sex', 'group', 'age_scan'], inplace=True)
  1349. # === Independence Checks ===
  1350. print("\n=== Independence Checks ===")
  1351. print("Spearman correlation between age_scan and sex:", spearmanr(participants_df['age_scan'], participants_df['sex']))
  1352. print("Spearman correlation between age_scan and group:", spearmanr(participants_df['age_scan'], participants_df['group']))
  1353. contingency_sex_group = pd.crosstab(participants_df['sex'], participants_df['group'])
  1354. chi2, p, _, _ = chi2_contingency(contingency_sex_group)
  1355. print("Chi-square test between sex and group: chi2 =", chi2, ", p =", p)
  1356. # === Load asymmetry data ===
  1357. group_folders = ["asymmetry_fcdlla", "asymmetry_fcdllb", "asymmetry_fcdna", "asymmetry_hc"]
  1358. all_data = []
  1359. # Load surface area asymmetry data from nested structure
  1360. for group_folder in group_folders:
  1361. group_path = os.path.join(data_root, group_folder)
  1362. if not os.path.exists(group_path):
  1363. print(f"Missing: {group_path}")
  1364. continue
  1365. # Iterate over subject folders inside group folder
  1366. for subject in os.listdir(group_path):
  1367. subj_folder_path = os.path.join(group_path, subject)
  1368. if not os.path.isdir(subj_folder_path):
  1369. continue
  1370. # Use 'stats_cleaned' except for HC group which uses 'stats'
  1371. if group_folder == "asymmetry_hc":
  1372. stats_folder = os.path.join(subj_folder_path, "stats")
  1373. else:
  1374. stats_folder = os.path.join(subj_folder_path, "stats_cleaned")
  1375. file_name = f"{subject}_SurfArea_asymmetry.csv"
  1376. file_path = os.path.join(stats_folder, file_name)
  1377. if not os.path.exists(file_path):
  1378. print(f"Not found: {file_path}")
  1379. continue
  1380. try:
  1381. df = pd.read_csv(file_path)
  1382. # Check necessary columns
  1383. if not all(col in df.columns for col in ['StructName', 'Asymmetry_Index']):
  1384. print(f"Missing columns in {file_path}")
  1385. continue
  1386. df['participant_id'] = subject
  1387. df['Unsigned_Asymmetry'] = np.abs(df['Asymmetry_Index'])
  1388. all_data.append(df[['participant_id', 'StructName', 'Unsigned_Asymmetry']])
  1389. except Exception as e:
  1390. print(f"Failed loading {file_path}: {e}")
  1391. # Merge all data
  1392. asymmetry_df = pd.concat(all_data, ignore_index=True)
  1393. full_df = pd.merge(asymmetry_df, participants_df, on='participant_id', how='inner')
  1394. full_df.dropna(subset=['Unsigned_Asymmetry', 'sex', 'group', 'age_scan'], inplace=True)
  1395. # === Check response distribution and transform if needed ===
  1396. plt.figure(figsize=(8, 4))
  1397. sns.histplot(full_df['Unsigned_Asymmetry'], kde=True)
  1398. plt.title("Distribution of Surface Area Asymmetry FCD vs HC")
  1399. plt.show()
  1400. ks_stat, ks_p = kstest(full_df['Unsigned_Asymmetry'], 'norm')
  1401. if ks_p < 0.05:
  1402. full_df['Unsigned_Asymmetry'] = np.sqrt(full_df['Unsigned_Asymmetry'])
  1403. print("Data was not normal. Applied square root transformation.")
  1404. # %% [markdown]
  1405. # ## Beta regression for intensity FCD subtypes
  1406. # %%
  1407. import os
  1408. import pandas as pd
  1409. import numpy as np
  1410. import statsmodels.formula.api as smf
  1411. from statsmodels.discrete.discrete_model import MNLogit
  1412. from sklearn.preprocessing import MinMaxScaler
  1413. from scipy.stats import spearmanr, chi2_contingency, kstest
  1414. import statsmodels.api as sm
  1415. import matplotlib.pyplot as plt
  1416. import seaborn as sns
  1417. from statsmodels.stats.multitest import multipletests
  1418. import rpy2.robjects as ro
  1419. from rpy2.robjects import pandas2ri
  1420. from rpy2.robjects.packages import importr
  1421. from rpy2.robjects.conversion import localconverter
  1422. from rpy2.robjects import r, globalenv, Formula
  1423. from rpy2.robjects.packages import importr
  1424. betareg = importr('betareg')
  1425. import warnings
  1426. warnings.filterwarnings("ignore")
  1427. # ==== 3. Load R packages ====
  1428. utils = importr('utils')
  1429. utils.install_packages('betareg') # will skip if already installed
  1430. betareg = importr('betareg')
  1431. # === Base directory and metadata path ===
  1432. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy"
  1433. metadata_csv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.csv")
  1434. metadata_tsv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv")
  1435. # === Load participant metadata safely ===
  1436. try:
  1437. if os.path.exists(metadata_csv):
  1438. participants_df = pd.read_csv(metadata_csv, sep=None, engine='python', on_bad_lines='skip')
  1439. elif os.path.exists(metadata_tsv):
  1440. participants_df = pd.read_csv(metadata_tsv, sep='\t', engine='python', on_bad_lines='skip')
  1441. else:
  1442. raise FileNotFoundError("Neither CSV nor TSV metadata file found.")
  1443. except Exception as e:
  1444. raise RuntimeError(f"Failed to load metadata: {e}")
  1445. # === Standardize and encode columns ===
  1446. # Drop rows missing important fields first
  1447. participants_df.dropna(subset=['sex', 'age_scan','histopathology'], inplace=True)
  1448. # Clean metadata
  1449. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  1450. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  1451. # Encode categorical variables
  1452. sex_map = {'F': 0, 'M': 1}
  1453. participants_df['sex'] = participants_df['sex'].map(sex_map)
  1454. # Encode histopathology (use instead of group)
  1455. participants_df['histopathology'] = participants_df['histopathology'].str.lower().map({'iia': 0, 'iib': 1, 'na': 2})
  1456. # Ensure 'participant_id' exists and is string
  1457. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  1458. # === Independence Checks ===
  1459. print("\n=== Independence Checks ===")
  1460. print("Spearman correlation between age_scan and sex:", spearmanr(participants_df['age_scan'], participants_df['sex']))
  1461. print("Spearman correlation between age_scan and histopathology:", spearmanr(participants_df['age_scan'], participants_df['histopathology']))
  1462. contingency_sex_histopathology = pd.crosstab(participants_df['sex'], participants_df['histopathology'])
  1463. chi2, p, _, _ = chi2_contingency(contingency_sex_histopathology)
  1464. print("Chi-square test between sex and histopathology: chi2 =", chi2, ", p =", p)
  1465. # === Load asymmetry data ===
  1466. group_folders = ["asymmetry_fcdlla", "asymmetry_fcdllb", "asymmetry_fcdna"]
  1467. all_data = []
  1468. for group_folder in group_folders:
  1469. group_path = os.path.join(data_root, group_folder)
  1470. if not os.path.exists(group_path):
  1471. continue
  1472. for file_name in os.listdir(group_path):
  1473. if not file_name.endswith("_intensity_freesurfer_stats.csv"):
  1474. continue
  1475. file_path = os.path.join(group_path, file_name)
  1476. subject = file_name.replace("_intensity_freesurfer_stats.csv", "")
  1477. try:
  1478. df = pd.read_csv(file_path)
  1479. if not all(col in df.columns for col in ['StructName', 'Asymmetry_Index']):
  1480. continue
  1481. df['participant_id'] = subject
  1482. df['Unsigned_Asymmetry'] = np.abs(df['Asymmetry_Index'])
  1483. all_data.append(df[['participant_id', 'StructName', 'Unsigned_Asymmetry']])
  1484. except Exception as e:
  1485. print(f"Failed loading {file_path}: {e}")
  1486. # Merge all data
  1487. asymmetry_df = pd.concat(all_data, ignore_index=True)
  1488. full_df = pd.merge(asymmetry_df, participants_df, on='participant_id', how='inner')
  1489. full_df.dropna(subset=['Unsigned_Asymmetry', 'sex', 'histopathology', 'age_scan'], inplace=True)
  1490. # === Check response distribution and transform if needed ===
  1491. plt.figure(figsize=(8, 4))
  1492. sns.histplot(full_df['Unsigned_Asymmetry'], kde=True)
  1493. plt.title("Distribution of Intensity Asymmetry between FCD subtypes (FCDIIa, FCDIIb, and non-operated FCD)")
  1494. plt.show()
  1495. ks_stat, ks_p = kstest(full_df['Unsigned_Asymmetry'], 'norm')
  1496. if ks_p < 0.05:
  1497. full_df['Unsigned_Asymmetry'] = np.sqrt(full_df['Unsigned_Asymmetry'])
  1498. print("Data was not normal. Applied square root transformation.")
  1499. # %% [markdown]
  1500. # ## Beta regression for thickness FCD subtypes
  1501. # %%
  1502. import os
  1503. import pandas as pd
  1504. import numpy as np
  1505. import statsmodels.formula.api as smf
  1506. from statsmodels.discrete.discrete_model import MNLogit
  1507. from sklearn.preprocessing import MinMaxScaler
  1508. from scipy.stats import spearmanr, chi2_contingency, kstest
  1509. import statsmodels.api as sm
  1510. import matplotlib.pyplot as plt
  1511. import seaborn as sns
  1512. from statsmodels.stats.multitest import multipletests
  1513. import rpy2.robjects as ro
  1514. from rpy2.robjects import pandas2ri
  1515. from rpy2.robjects.packages import importr
  1516. from rpy2.robjects.conversion import localconverter
  1517. from rpy2.robjects import r, globalenv, Formula
  1518. from rpy2.robjects.packages import importr
  1519. betareg = importr('betareg')
  1520. import warnings
  1521. warnings.filterwarnings("ignore")
  1522. # ==== 3. Load R packages ====
  1523. utils = importr('utils')
  1524. utils.install_packages('betareg') # will skip if already installed
  1525. betareg = importr('betareg')
  1526. # === Base directory and metadata path ===
  1527. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy"
  1528. metadata_csv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.csv")
  1529. metadata_tsv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv")
  1530. # === Load participant metadata safely ===
  1531. try:
  1532. if os.path.exists(metadata_csv):
  1533. participants_df = pd.read_csv(metadata_csv, sep=None, engine='python', on_bad_lines='skip')
  1534. elif os.path.exists(metadata_tsv):
  1535. participants_df = pd.read_csv(metadata_tsv, sep='\t', engine='python', on_bad_lines='skip')
  1536. else:
  1537. raise FileNotFoundError("Neither CSV nor TSV metadata file found.")
  1538. except Exception as e:
  1539. raise RuntimeError(f"Failed to load metadata: {e}")
  1540. # === Standardize and encode columns ===
  1541. # Drop rows missing important fields first
  1542. participants_df.dropna(subset=['sex', 'age_scan','histopathology'], inplace=True)
  1543. # Clean metadata
  1544. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  1545. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  1546. # Encode categorical variables
  1547. sex_map = {'F': 0, 'M': 1}
  1548. participants_df['sex'] = participants_df['sex'].map(sex_map)
  1549. # Encode histopathology (use instead of group)
  1550. participants_df['histopathology'] = participants_df['histopathology'].str.lower().map({'iia': 0, 'iib': 1, 'na': 2})
  1551. # Ensure 'participant_id' exists and is string
  1552. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  1553. # === Independence Checks ===
  1554. print("\n=== Independence Checks ===")
  1555. print("Spearman correlation between age_scan and sex:", spearmanr(participants_df['age_scan'], participants_df['sex']))
  1556. print("Spearman correlation between age_scan and histopathology:", spearmanr(participants_df['age_scan'], participants_df['histopathology']))
  1557. contingency_sex_histopathology = pd.crosstab(participants_df['sex'], participants_df['histopathology'])
  1558. chi2, p, _, _ = chi2_contingency(contingency_sex_histopathology)
  1559. print("Chi-square test between sex and histopathology: chi2 =", chi2, ", p =", p)
  1560. # === Load asymmetry data ===
  1561. group_folders = ["asymmetry_fcdlla", "asymmetry_fcdllb", "asymmetry_fcdna"]
  1562. all_data = []
  1563. for group_folder in group_folders:
  1564. group_path = os.path.join(data_root, group_folder)
  1565. if not os.path.exists(group_path):
  1566. print(f"Missing: {group_path}")
  1567. continue
  1568. # Iterate over subject folders inside group folder
  1569. for subject in os.listdir(group_path):
  1570. subj_folder_path = os.path.join(group_path, subject)
  1571. if not os.path.isdir(subj_folder_path):
  1572. continue
  1573. # Use 'stats_cleaned' except for HC group which uses 'stats'
  1574. stats_folder = os.path.join(subj_folder_path, "stats_cleaned")
  1575. file_name = f"{subject}_ThickAvg_asymmetry.csv"
  1576. file_path = os.path.join(stats_folder, file_name)
  1577. if not os.path.exists(file_path):
  1578. print(f"Not found: {file_path}")
  1579. continue
  1580. try:
  1581. df = pd.read_csv(file_path)
  1582. # Check necessary columns
  1583. if not all(col in df.columns for col in ['StructName', 'Asymmetry_Index']):
  1584. print(f"Missing columns in {file_path}")
  1585. continue
  1586. df['participant_id'] = subject
  1587. df['Unsigned_Asymmetry'] = np.abs(df['Asymmetry_Index'])
  1588. all_data.append(df[['participant_id', 'StructName', 'Unsigned_Asymmetry']])
  1589. except Exception as e:
  1590. print(f"Failed loading {file_path}: {e}")
  1591. # Merge all data
  1592. asymmetry_df = pd.concat(all_data, ignore_index=True)
  1593. full_df = pd.merge(asymmetry_df, participants_df, on='participant_id', how='inner')
  1594. full_df.dropna(subset=['Unsigned_Asymmetry', 'sex', 'histopathology', 'age_scan'], inplace=True)
  1595. # === Check response distribution and transform if needed ===
  1596. plt.figure(figsize=(8, 4))
  1597. sns.histplot(full_df['Unsigned_Asymmetry'], kde=True)
  1598. plt.title("Distribution of Thickness Asymmetry between FCD subtypes (FCDIIa, FCDIIb, and non-operated FCD)")
  1599. plt.show()
  1600. ks_stat, ks_p = kstest(full_df['Unsigned_Asymmetry'], 'norm')
  1601. if ks_p < 0.05:
  1602. full_df['Unsigned_Asymmetry'] = np.sqrt(full_df['Unsigned_Asymmetry'])
  1603. print("Data was not normal. Applied square root transformation.")
  1604. # %% [markdown]
  1605. # ## Beta regression for volume FCD subtypes
  1606. # %%
  1607. import os
  1608. import pandas as pd
  1609. import numpy as np
  1610. import statsmodels.formula.api as smf
  1611. from statsmodels.discrete.discrete_model import MNLogit
  1612. from sklearn.preprocessing import MinMaxScaler
  1613. from scipy.stats import spearmanr, chi2_contingency, kstest
  1614. import statsmodels.api as sm
  1615. import matplotlib.pyplot as plt
  1616. import seaborn as sns
  1617. from statsmodels.stats.multitest import multipletests
  1618. import rpy2.robjects as ro
  1619. from rpy2.robjects import pandas2ri
  1620. from rpy2.robjects.packages import importr
  1621. from rpy2.robjects.conversion import localconverter
  1622. from rpy2.robjects import r, globalenv, Formula
  1623. from rpy2.robjects.packages import importr
  1624. betareg = importr('betareg')
  1625. import warnings
  1626. warnings.filterwarnings("ignore")
  1627. # ==== 3. Load R packages ====
  1628. utils = importr('utils')
  1629. utils.install_packages('betareg') # will skip if already installed
  1630. betareg = importr('betareg')
  1631. # === Base directory and metadata path ===
  1632. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy"
  1633. metadata_csv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.csv")
  1634. metadata_tsv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv")
  1635. # === Load participant metadata safely ===
  1636. try:
  1637. if os.path.exists(metadata_csv):
  1638. participants_df = pd.read_csv(metadata_csv, sep=None, engine='python', on_bad_lines='skip')
  1639. elif os.path.exists(metadata_tsv):
  1640. participants_df = pd.read_csv(metadata_tsv, sep='\t', engine='python', on_bad_lines='skip')
  1641. else:
  1642. raise FileNotFoundError("Neither CSV nor TSV metadata file found.")
  1643. except Exception as e:
  1644. raise RuntimeError(f"Failed to load metadata: {e}")
  1645. # === Standardize and encode columns ===
  1646. # Drop rows missing important fields first
  1647. participants_df.dropna(subset=['sex', 'age_scan','histopathology'], inplace=True)
  1648. # Clean metadata
  1649. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  1650. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  1651. # Encode categorical variables
  1652. sex_map = {'F': 0, 'M': 1}
  1653. participants_df['sex'] = participants_df['sex'].map(sex_map)
  1654. # Encode histopathology (use instead of group)
  1655. participants_df['histopathology'] = participants_df['histopathology'].str.lower().map({'iia': 0, 'iib': 1, 'na': 2})
  1656. # Ensure 'participant_id' exists and is string
  1657. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  1658. # === Independence Checks ===
  1659. print("\n=== Independence Checks ===")
  1660. print("Spearman correlation between age_scan and sex:", spearmanr(participants_df['age_scan'], participants_df['sex']))
  1661. print("Spearman correlation between age_scan and histopathology:", spearmanr(participants_df['age_scan'], participants_df['histopathology']))
  1662. contingency_sex_histopathology = pd.crosstab(participants_df['sex'], participants_df['histopathology'])
  1663. chi2, p, _, _ = chi2_contingency(contingency_sex_histopathology)
  1664. print("Chi-square test between sex and histopathology: chi2 =", chi2, ", p =", p)
  1665. # === Load asymmetry data ===
  1666. group_folders = ["asymmetry_fcdlla", "asymmetry_fcdllb", "asymmetry_fcdna"]
  1667. all_data = []
  1668. for group_folder in group_folders:
  1669. group_path = os.path.join(data_root, group_folder)
  1670. if not os.path.exists(group_path):
  1671. print(f"Missing: {group_path}")
  1672. continue
  1673. # Iterate over subject folders inside group folder
  1674. for subject in os.listdir(group_path):
  1675. subj_folder_path = os.path.join(group_path, subject)
  1676. if not os.path.isdir(subj_folder_path):
  1677. continue
  1678. # Use 'stats_cleaned' except for HC group which uses 'stats'
  1679. stats_folder = os.path.join(subj_folder_path, "stats_cleaned")
  1680. file_name = f"{subject}_GrayVol_asymmetry.csv"
  1681. file_path = os.path.join(stats_folder, file_name)
  1682. if not os.path.exists(file_path):
  1683. print(f"Not found: {file_path}")
  1684. continue
  1685. try:
  1686. df = pd.read_csv(file_path)
  1687. # Check necessary columns
  1688. if not all(col in df.columns for col in ['StructName', 'Asymmetry_Index']):
  1689. print(f"Missing columns in {file_path}")
  1690. continue
  1691. df['participant_id'] = subject
  1692. df['Unsigned_Asymmetry'] = np.abs(df['Asymmetry_Index'])
  1693. all_data.append(df[['participant_id', 'StructName', 'Unsigned_Asymmetry']])
  1694. except Exception as e:
  1695. print(f"Failed loading {file_path}: {e}")
  1696. # Merge all data
  1697. asymmetry_df = pd.concat(all_data, ignore_index=True)
  1698. full_df = pd.merge(asymmetry_df, participants_df, on='participant_id', how='inner')
  1699. full_df.dropna(subset=['Unsigned_Asymmetry', 'sex', 'histopathology', 'age_scan'], inplace=True)
  1700. # === Check response distribution and transform if needed ===
  1701. plt.figure(figsize=(8, 4))
  1702. sns.histplot(full_df['Unsigned_Asymmetry'], kde=True)
  1703. plt.title("Distribution of Volume Asymmetry between FCD subtypes (FCDIIa, FCDIIb, and non-operated FCD)")
  1704. plt.show()
  1705. ks_stat, ks_p = kstest(full_df['Unsigned_Asymmetry'], 'norm')
  1706. if ks_p < 0.05:
  1707. full_df['Unsigned_Asymmetry'] = np.sqrt(full_df['Unsigned_Asymmetry'])
  1708. print("Data was not normal. Applied square root transformation.")
  1709. # %% [markdown]
  1710. # ## Beta regression for curvature FCD subtypes
  1711. # %%
  1712. import os
  1713. import pandas as pd
  1714. import numpy as np
  1715. import statsmodels.formula.api as smf
  1716. from statsmodels.discrete.discrete_model import MNLogit
  1717. from sklearn.preprocessing import MinMaxScaler
  1718. from scipy.stats import spearmanr, chi2_contingency, kstest
  1719. import statsmodels.api as sm
  1720. import matplotlib.pyplot as plt
  1721. import seaborn as sns
  1722. from statsmodels.stats.multitest import multipletests
  1723. import rpy2.robjects as ro
  1724. from rpy2.robjects import pandas2ri
  1725. from rpy2.robjects.packages import importr
  1726. from rpy2.robjects.conversion import localconverter
  1727. from rpy2.robjects import r, globalenv, Formula
  1728. from rpy2.robjects.packages import importr
  1729. betareg = importr('betareg')
  1730. import warnings
  1731. warnings.filterwarnings("ignore")
  1732. # ==== 3. Load R packages ====
  1733. utils = importr('utils')
  1734. utils.install_packages('betareg') # will skip if already installed
  1735. betareg = importr('betareg')
  1736. # === Base directory and metadata path ===
  1737. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy"
  1738. metadata_csv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.csv")
  1739. metadata_tsv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv")
  1740. # === Load participant metadata safely ===
  1741. try:
  1742. if os.path.exists(metadata_csv):
  1743. participants_df = pd.read_csv(metadata_csv, sep=None, engine='python', on_bad_lines='skip')
  1744. elif os.path.exists(metadata_tsv):
  1745. participants_df = pd.read_csv(metadata_tsv, sep='\t', engine='python', on_bad_lines='skip')
  1746. else:
  1747. raise FileNotFoundError("Neither CSV nor TSV metadata file found.")
  1748. except Exception as e:
  1749. raise RuntimeError(f"Failed to load metadata: {e}")
  1750. # === Standardize and encode columns ===
  1751. # Drop rows missing important fields first
  1752. participants_df.dropna(subset=['sex', 'age_scan','histopathology'], inplace=True)
  1753. # Clean metadata
  1754. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  1755. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  1756. # Encode categorical variables
  1757. sex_map = {'F': 0, 'M': 1}
  1758. participants_df['sex'] = participants_df['sex'].map(sex_map)
  1759. # Encode histopathology (use instead of group)
  1760. participants_df['histopathology'] = participants_df['histopathology'].str.lower().map({'iia': 0, 'iib': 1, 'na': 2})
  1761. # Ensure 'participant_id' exists and is string
  1762. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  1763. # === Independence Checks ===
  1764. print("\n=== Independence Checks ===")
  1765. print("Spearman correlation between age_scan and sex:", spearmanr(participants_df['age_scan'], participants_df['sex']))
  1766. print("Spearman correlation between age_scan and histopathology:", spearmanr(participants_df['age_scan'], participants_df['histopathology']))
  1767. contingency_sex_histopathology = pd.crosstab(participants_df['sex'], participants_df['histopathology'])
  1768. chi2, p, _, _ = chi2_contingency(contingency_sex_histopathology)
  1769. print("Chi-square test between sex and histopathology: chi2 =", chi2, ", p =", p)
  1770. # === Load asymmetry data ===
  1771. group_folders = ["asymmetry_fcdlla", "asymmetry_fcdllb", "asymmetry_fcdna"]
  1772. all_data = []
  1773. for group_folder in group_folders:
  1774. group_path = os.path.join(data_root, group_folder)
  1775. if not os.path.exists(group_path):
  1776. print(f"Missing: {group_path}")
  1777. continue
  1778. # Iterate over subject folders inside group folder
  1779. for subject in os.listdir(group_path):
  1780. subj_folder_path = os.path.join(group_path, subject)
  1781. if not os.path.isdir(subj_folder_path):
  1782. continue
  1783. # Use 'stats_cleaned' except for HC group which uses 'stats'
  1784. stats_folder = os.path.join(subj_folder_path, "stats_cleaned")
  1785. file_name = f"{subject}_MeanCurv_asymmetry.csv"
  1786. file_path = os.path.join(stats_folder, file_name)
  1787. if not os.path.exists(file_path):
  1788. print(f"Not found: {file_path}")
  1789. continue
  1790. try:
  1791. df = pd.read_csv(file_path)
  1792. # Check necessary columns
  1793. if not all(col in df.columns for col in ['StructName', 'Asymmetry_Index']):
  1794. print(f"Missing columns in {file_path}")
  1795. continue
  1796. df['participant_id'] = subject
  1797. df['Unsigned_Asymmetry'] = np.abs(df['Asymmetry_Index'])
  1798. all_data.append(df[['participant_id', 'StructName', 'Unsigned_Asymmetry']])
  1799. except Exception as e:
  1800. print(f"Failed loading {file_path}: {e}")
  1801. # Merge all data
  1802. asymmetry_df = pd.concat(all_data, ignore_index=True)
  1803. full_df = pd.merge(asymmetry_df, participants_df, on='participant_id', how='inner')
  1804. full_df.dropna(subset=['Unsigned_Asymmetry', 'sex', 'histopathology', 'age_scan'], inplace=True)
  1805. # === Check response distribution and transform if needed ===
  1806. plt.figure(figsize=(8, 4))
  1807. sns.histplot(full_df['Unsigned_Asymmetry'], kde=True)
  1808. plt.title("Distribution of Curvature Asymmetry between FCD subtypes (FCDIIa, FCDIIb, and non-operated FCD)")
  1809. plt.show()
  1810. ks_stat, ks_p = kstest(full_df['Unsigned_Asymmetry'], 'norm')
  1811. if ks_p < 0.05:
  1812. full_df['Unsigned_Asymmetry'] = np.sqrt(full_df['Unsigned_Asymmetry'])
  1813. print("Data was not normal. Applied square root transformation.")
  1814. # %% [markdown]
  1815. # ## Beta regression for surface area FCD subtypes
  1816. # %%
  1817. import os
  1818. import pandas as pd
  1819. import numpy as np
  1820. import statsmodels.formula.api as smf
  1821. from statsmodels.discrete.discrete_model import MNLogit
  1822. from sklearn.preprocessing import MinMaxScaler
  1823. from scipy.stats import spearmanr, chi2_contingency, kstest
  1824. import statsmodels.api as sm
  1825. import matplotlib.pyplot as plt
  1826. import seaborn as sns
  1827. from statsmodels.stats.multitest import multipletests
  1828. import rpy2.robjects as ro
  1829. from rpy2.robjects import pandas2ri
  1830. from rpy2.robjects.packages import importr
  1831. from rpy2.robjects.conversion import localconverter
  1832. from rpy2.robjects import r, globalenv, Formula
  1833. from rpy2.robjects.packages import importr
  1834. betareg = importr('betareg')
  1835. import warnings
  1836. warnings.filterwarnings("ignore")
  1837. # ==== 3. Load R packages ====
  1838. utils = importr('utils')
  1839. utils.install_packages('betareg') # will skip if already installed
  1840. betareg = importr('betareg')
  1841. # === Base directory and metadata path ===
  1842. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy"
  1843. metadata_csv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.csv")
  1844. metadata_tsv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv")
  1845. # === Load participant metadata safely ===
  1846. try:
  1847. if os.path.exists(metadata_csv):
  1848. participants_df = pd.read_csv(metadata_csv, sep=None, engine='python', on_bad_lines='skip')
  1849. elif os.path.exists(metadata_tsv):
  1850. participants_df = pd.read_csv(metadata_tsv, sep='\t', engine='python', on_bad_lines='skip')
  1851. else:
  1852. raise FileNotFoundError("Neither CSV nor TSV metadata file found.")
  1853. except Exception as e:
  1854. raise RuntimeError(f"Failed to load metadata: {e}")
  1855. # === Standardize and encode columns ===
  1856. # Drop rows missing important fields first
  1857. participants_df.dropna(subset=['sex', 'age_scan','histopathology'], inplace=True)
  1858. # Clean metadata
  1859. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  1860. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  1861. # Encode categorical variables
  1862. sex_map = {'F': 0, 'M': 1}
  1863. participants_df['sex'] = participants_df['sex'].map(sex_map)
  1864. # Encode histopathology (use instead of group)
  1865. participants_df['histopathology'] = participants_df['histopathology'].str.lower().map({'iia': 0, 'iib': 1, 'na': 2})
  1866. # Ensure 'participant_id' exists and is string
  1867. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  1868. # === Independence Checks ===
  1869. print("\n=== Independence Checks ===")
  1870. print("Spearman correlation between age_scan and sex:", spearmanr(participants_df['age_scan'], participants_df['sex']))
  1871. print("Spearman correlation between age_scan and histopathology:", spearmanr(participants_df['age_scan'], participants_df['histopathology']))
  1872. contingency_sex_histopathology = pd.crosstab(participants_df['sex'], participants_df['histopathology'])
  1873. chi2, p, _, _ = chi2_contingency(contingency_sex_histopathology)
  1874. print("Chi-square test between sex and histopathology: chi2 =", chi2, ", p =", p)
  1875. # === Load asymmetry data ===
  1876. group_folders = ["asymmetry_fcdlla", "asymmetry_fcdllb", "asymmetry_fcdna"]
  1877. all_data = []
  1878. for group_folder in group_folders:
  1879. group_path = os.path.join(data_root, group_folder)
  1880. if not os.path.exists(group_path):
  1881. print(f"Missing: {group_path}")
  1882. continue
  1883. # Iterate over subject folders inside group folder
  1884. for subject in os.listdir(group_path):
  1885. subj_folder_path = os.path.join(group_path, subject)
  1886. if not os.path.isdir(subj_folder_path):
  1887. continue
  1888. # Use 'stats_cleaned' except for HC group which uses 'stats'
  1889. stats_folder = os.path.join(subj_folder_path, "stats_cleaned")
  1890. file_name = f"{subject}_SurfArea_asymmetry.csv"
  1891. file_path = os.path.join(stats_folder, file_name)
  1892. if not os.path.exists(file_path):
  1893. print(f"Not found: {file_path}")
  1894. continue
  1895. try:
  1896. df = pd.read_csv(file_path)
  1897. # Check necessary columns
  1898. if not all(col in df.columns for col in ['StructName', 'Asymmetry_Index']):
  1899. print(f"Missing columns in {file_path}")
  1900. continue
  1901. df['participant_id'] = subject
  1902. df['Unsigned_Asymmetry'] = np.abs(df['Asymmetry_Index'])
  1903. all_data.append(df[['participant_id', 'StructName', 'Unsigned_Asymmetry']])
  1904. except Exception as e:
  1905. print(f"Failed loading {file_path}: {e}")
  1906. # Merge all data
  1907. asymmetry_df = pd.concat(all_data, ignore_index=True)
  1908. full_df = pd.merge(asymmetry_df, participants_df, on='participant_id', how='inner')
  1909. full_df.dropna(subset=['Unsigned_Asymmetry', 'sex', 'histopathology', 'age_scan'], inplace=True)
  1910. # === Check response distribution and transform if needed ===
  1911. plt.figure(figsize=(8, 4))
  1912. sns.histplot(full_df['Unsigned_Asymmetry'], kde=True)
  1913. plt.title("Distribution of Surface Area Asymmetry between FCD subtypes (FCDIIa, FCDIIb, and non-operated FCD)")
  1914. plt.show()
  1915. ks_stat, ks_p = kstest(full_df['Unsigned_Asymmetry'], 'norm')
  1916. if ks_p < 0.05:
  1917. full_df['Unsigned_Asymmetry'] = np.sqrt(full_df['Unsigned_Asymmetry'])
  1918. print("Data was not normal. Applied square root transformation.")
  1919. # %% [markdown]
  1920. # ## Beta regression for intensity FCDIIb vs FCDIIa
  1921. # %%
  1922. import os
  1923. import pandas as pd
  1924. import numpy as np
  1925. import statsmodels.formula.api as smf
  1926. from statsmodels.discrete.discrete_model import MNLogit
  1927. from sklearn.preprocessing import MinMaxScaler
  1928. from scipy.stats import spearmanr, chi2_contingency, kstest
  1929. import statsmodels.api as sm
  1930. import matplotlib.pyplot as plt
  1931. import seaborn as sns
  1932. from statsmodels.stats.multitest import multipletests
  1933. import rpy2.robjects as ro
  1934. from rpy2.robjects import pandas2ri
  1935. from rpy2.robjects.packages import importr
  1936. from rpy2.robjects.conversion import localconverter
  1937. from rpy2.robjects import r, globalenv, Formula
  1938. from rpy2.robjects.packages import importr
  1939. betareg = importr('betareg')
  1940. import warnings
  1941. warnings.filterwarnings("ignore")
  1942. # ==== 3. Load R packages ====
  1943. utils = importr('utils')
  1944. utils.install_packages('betareg') # will skip if already installed
  1945. betareg = importr('betareg')
  1946. # === Base directory and metadata path ===
  1947. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy"
  1948. metadata_csv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.csv")
  1949. metadata_tsv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv")
  1950. # === Load participant metadata safely ===
  1951. try:
  1952. if os.path.exists(metadata_csv):
  1953. participants_df = pd.read_csv(metadata_csv, sep=None, engine='python', on_bad_lines='skip')
  1954. elif os.path.exists(metadata_tsv):
  1955. participants_df = pd.read_csv(metadata_tsv, sep='\t', engine='python', on_bad_lines='skip')
  1956. else:
  1957. raise FileNotFoundError("Neither CSV nor TSV metadata file found.")
  1958. except Exception as e:
  1959. raise RuntimeError(f"Failed to load metadata: {e}")
  1960. # === Standardize and encode columns ===
  1961. # Drop rows missing important fields first
  1962. participants_df.dropna(subset=['sex', 'age_scan','histopathology'], inplace=True)
  1963. # Clean metadata
  1964. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  1965. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  1966. # Encode categorical variables
  1967. sex_map = {'F': 0, 'M': 1}
  1968. participants_df['sex'] = participants_df['sex'].map(sex_map)
  1969. # Encode histopathology (use instead of group)
  1970. participants_df['histopathology'] = participants_df['histopathology'].str.lower().map({'iia': 0, 'iib': 1, 'na': 2})
  1971. # Ensure 'participant_id' exists and is string
  1972. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  1973. # === Independence Checks ===
  1974. print("\n=== Independence Checks ===")
  1975. print("Spearman correlation between age_scan and sex:", spearmanr(participants_df['age_scan'], participants_df['sex']))
  1976. print("Spearman correlation between age_scan and histopathology:", spearmanr(participants_df['age_scan'], participants_df['histopathology']))
  1977. contingency_sex_histopathology = pd.crosstab(participants_df['sex'], participants_df['histopathology'])
  1978. chi2, p, _, _ = chi2_contingency(contingency_sex_histopathology)
  1979. print("Chi-square test between sex and histopathology: chi2 =", chi2, ", p =", p)
  1980. # === Load asymmetry data ===
  1981. group_folders = ["asymmetry_fcdlla", "asymmetry_fcdllb"]
  1982. all_data = []
  1983. for group_folder in group_folders:
  1984. group_path = os.path.join(data_root, group_folder)
  1985. if not os.path.exists(group_path):
  1986. continue
  1987. for file_name in os.listdir(group_path):
  1988. if not file_name.endswith("_intensity_freesurfer_stats.csv"):
  1989. continue
  1990. file_path = os.path.join(group_path, file_name)
  1991. subject = file_name.replace("_intensity_freesurfer_stats.csv", "")
  1992. try:
  1993. df = pd.read_csv(file_path)
  1994. if not all(col in df.columns for col in ['StructName', 'Asymmetry_Index']):
  1995. continue
  1996. df['participant_id'] = subject
  1997. df['Unsigned_Asymmetry'] = np.abs(df['Asymmetry_Index'])
  1998. all_data.append(df[['participant_id', 'StructName', 'Unsigned_Asymmetry']])
  1999. except Exception as e:
  2000. print(f"Failed loading {file_path}: {e}")
  2001. # Merge all data
  2002. asymmetry_df = pd.concat(all_data, ignore_index=True)
  2003. full_df = pd.merge(asymmetry_df, participants_df, on='participant_id', how='inner')
  2004. full_df.dropna(subset=['Unsigned_Asymmetry', 'sex', 'histopathology', 'age_scan'], inplace=True)
  2005. # === Check response distribution and transform if needed ===
  2006. plt.figure(figsize=(8, 4))
  2007. sns.histplot(full_df['Unsigned_Asymmetry'], kde=True)
  2008. plt.title("Distribution of Intensity Asymmetry between FCD subtypes (FCDIIa, FCDIIb)")
  2009. plt.show()
  2010. ks_stat, ks_p = kstest(full_df['Unsigned_Asymmetry'], 'norm')
  2011. if ks_p < 0.05:
  2012. full_df['Unsigned_Asymmetry'] = np.sqrt(full_df['Unsigned_Asymmetry'])
  2013. print("Data was not normal. Applied square root transformation.")
  2014. # %% [markdown]
  2015. # ## Beta regression for thickness FCDIIb vs FCDIIa
  2016. # %%
  2017. import os
  2018. import pandas as pd
  2019. import numpy as np
  2020. import statsmodels.formula.api as smf
  2021. from statsmodels.discrete.discrete_model import MNLogit
  2022. from sklearn.preprocessing import MinMaxScaler
  2023. from scipy.stats import spearmanr, chi2_contingency, kstest
  2024. import statsmodels.api as sm
  2025. import matplotlib.pyplot as plt
  2026. import seaborn as sns
  2027. from statsmodels.stats.multitest import multipletests
  2028. import rpy2.robjects as ro
  2029. from rpy2.robjects import pandas2ri
  2030. from rpy2.robjects.packages import importr
  2031. from rpy2.robjects.conversion import localconverter
  2032. from rpy2.robjects import r, globalenv, Formula
  2033. from rpy2.robjects.packages import importr
  2034. betareg = importr('betareg')
  2035. import warnings
  2036. warnings.filterwarnings("ignore")
  2037. # ==== 3. Load R packages ====
  2038. utils = importr('utils')
  2039. utils.install_packages('betareg') # will skip if already installed
  2040. betareg = importr('betareg')
  2041. # === Base directory and metadata path ===
  2042. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy"
  2043. metadata_csv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.csv")
  2044. metadata_tsv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv")
  2045. # === Load participant metadata safely ===
  2046. try:
  2047. if os.path.exists(metadata_csv):
  2048. participants_df = pd.read_csv(metadata_csv, sep=None, engine='python', on_bad_lines='skip')
  2049. elif os.path.exists(metadata_tsv):
  2050. participants_df = pd.read_csv(metadata_tsv, sep='\t', engine='python', on_bad_lines='skip')
  2051. else:
  2052. raise FileNotFoundError("Neither CSV nor TSV metadata file found.")
  2053. except Exception as e:
  2054. raise RuntimeError(f"Failed to load metadata: {e}")
  2055. # === Standardize and encode columns ===
  2056. # Drop rows missing important fields first
  2057. participants_df.dropna(subset=['sex', 'age_scan','histopathology'], inplace=True)
  2058. # Clean metadata
  2059. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  2060. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  2061. # Encode categorical variables
  2062. sex_map = {'F': 0, 'M': 1}
  2063. participants_df['sex'] = participants_df['sex'].map(sex_map)
  2064. # Encode histopathology (use instead of group)
  2065. participants_df['histopathology'] = participants_df['histopathology'].str.lower().map({'iia': 0, 'iib': 1, 'na': 2})
  2066. # Ensure 'participant_id' exists and is string
  2067. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  2068. # === Independence Checks ===
  2069. print("\n=== Independence Checks ===")
  2070. print("Spearman correlation between age_scan and sex:", spearmanr(participants_df['age_scan'], participants_df['sex']))
  2071. print("Spearman correlation between age_scan and histopathology:", spearmanr(participants_df['age_scan'], participants_df['histopathology']))
  2072. contingency_sex_histopathology = pd.crosstab(participants_df['sex'], participants_df['histopathology'])
  2073. chi2, p, _, _ = chi2_contingency(contingency_sex_histopathology)
  2074. print("Chi-square test between sex and histopathology: chi2 =", chi2, ", p =", p)
  2075. # === Load asymmetry data ===
  2076. group_folders = ["asymmetry_fcdlla", "asymmetry_fcdllb"]
  2077. all_data = []
  2078. for group_folder in group_folders:
  2079. group_path = os.path.join(data_root, group_folder)
  2080. if not os.path.exists(group_path):
  2081. print(f"Missing: {group_path}")
  2082. continue
  2083. # Iterate over subject folders inside group folder
  2084. for subject in os.listdir(group_path):
  2085. subj_folder_path = os.path.join(group_path, subject)
  2086. if not os.path.isdir(subj_folder_path):
  2087. continue
  2088. # Use 'stats_cleaned' except for HC group which uses 'stats'
  2089. stats_folder = os.path.join(subj_folder_path, "stats_cleaned")
  2090. file_name = f"{subject}_ThickAvg_asymmetry.csv"
  2091. file_path = os.path.join(stats_folder, file_name)
  2092. if not os.path.exists(file_path):
  2093. print(f"Not found: {file_path}")
  2094. continue
  2095. try:
  2096. df = pd.read_csv(file_path)
  2097. # Check necessary columns
  2098. if not all(col in df.columns for col in ['StructName', 'Asymmetry_Index']):
  2099. print(f"Missing columns in {file_path}")
  2100. continue
  2101. df['participant_id'] = subject
  2102. df['Unsigned_Asymmetry'] = np.abs(df['Asymmetry_Index'])
  2103. all_data.append(df[['participant_id', 'StructName', 'Unsigned_Asymmetry']])
  2104. except Exception as e:
  2105. print(f"Failed loading {file_path}: {e}")
  2106. # Merge all data
  2107. asymmetry_df = pd.concat(all_data, ignore_index=True)
  2108. full_df = pd.merge(asymmetry_df, participants_df, on='participant_id', how='inner')
  2109. full_df.dropna(subset=['Unsigned_Asymmetry', 'sex', 'histopathology', 'age_scan'], inplace=True)
  2110. # === Check response distribution and transform if needed ===
  2111. plt.figure(figsize=(8, 4))
  2112. sns.histplot(full_df['Unsigned_Asymmetry'], kde=True)
  2113. plt.title("Distribution of Thickness Asymmetry between FCD subtypes (FCDIIa, FCDIIb)")
  2114. plt.show()
  2115. ks_stat, ks_p = kstest(full_df['Unsigned_Asymmetry'], 'norm')
  2116. if ks_p < 0.05:
  2117. full_df['Unsigned_Asymmetry'] = np.sqrt(full_df['Unsigned_Asymmetry'])
  2118. print("Data was not normal. Applied square root transformation.")
  2119. # %% [markdown]
  2120. # ## Beta regression for volume FCDIIb vs FCDIIa
  2121. # %%
  2122. import os
  2123. import pandas as pd
  2124. import numpy as np
  2125. import statsmodels.formula.api as smf
  2126. from statsmodels.discrete.discrete_model import MNLogit
  2127. from sklearn.preprocessing import MinMaxScaler
  2128. from scipy.stats import spearmanr, chi2_contingency, kstest
  2129. import statsmodels.api as sm
  2130. import matplotlib.pyplot as plt
  2131. import seaborn as sns
  2132. from statsmodels.stats.multitest import multipletests
  2133. import rpy2.robjects as ro
  2134. from rpy2.robjects import pandas2ri
  2135. from rpy2.robjects.packages import importr
  2136. from rpy2.robjects.conversion import localconverter
  2137. from rpy2.robjects import r, globalenv, Formula
  2138. from rpy2.robjects.packages import importr
  2139. betareg = importr('betareg')
  2140. import warnings
  2141. warnings.filterwarnings("ignore")
  2142. # ==== 3. Load R packages ====
  2143. utils = importr('utils')
  2144. utils.install_packages('betareg') # will skip if already installed
  2145. betareg = importr('betareg')
  2146. # === Base directory and metadata path ===
  2147. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy"
  2148. metadata_csv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.csv")
  2149. metadata_tsv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv")
  2150. # === Load participant metadata safely ===
  2151. try:
  2152. if os.path.exists(metadata_csv):
  2153. participants_df = pd.read_csv(metadata_csv, sep=None, engine='python', on_bad_lines='skip')
  2154. elif os.path.exists(metadata_tsv):
  2155. participants_df = pd.read_csv(metadata_tsv, sep='\t', engine='python', on_bad_lines='skip')
  2156. else:
  2157. raise FileNotFoundError("Neither CSV nor TSV metadata file found.")
  2158. except Exception as e:
  2159. raise RuntimeError(f"Failed to load metadata: {e}")
  2160. # === Standardize and encode columns ===
  2161. # Drop rows missing important fields first
  2162. participants_df.dropna(subset=['sex', 'age_scan','histopathology'], inplace=True)
  2163. # Clean metadata
  2164. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  2165. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  2166. # Encode categorical variables
  2167. sex_map = {'F': 0, 'M': 1}
  2168. participants_df['sex'] = participants_df['sex'].map(sex_map)
  2169. # Encode histopathology (use instead of group)
  2170. participants_df['histopathology'] = participants_df['histopathology'].str.lower().map({'iia': 0, 'iib': 1, 'na': 2})
  2171. # Ensure 'participant_id' exists and is string
  2172. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  2173. # === Independence Checks ===
  2174. print("\n=== Independence Checks ===")
  2175. print("Spearman correlation between age_scan and sex:", spearmanr(participants_df['age_scan'], participants_df['sex']))
  2176. print("Spearman correlation between age_scan and histopathology:", spearmanr(participants_df['age_scan'], participants_df['histopathology']))
  2177. contingency_sex_histopathology = pd.crosstab(participants_df['sex'], participants_df['histopathology'])
  2178. chi2, p, _, _ = chi2_contingency(contingency_sex_histopathology)
  2179. print("Chi-square test between sex and histopathology: chi2 =", chi2, ", p =", p)
  2180. # === Load asymmetry data ===
  2181. group_folders = ["asymmetry_fcdlla", "asymmetry_fcdllb"]
  2182. all_data = []
  2183. for group_folder in group_folders:
  2184. group_path = os.path.join(data_root, group_folder)
  2185. if not os.path.exists(group_path):
  2186. print(f"Missing: {group_path}")
  2187. continue
  2188. # Iterate over subject folders inside group folder
  2189. for subject in os.listdir(group_path):
  2190. subj_folder_path = os.path.join(group_path, subject)
  2191. if not os.path.isdir(subj_folder_path):
  2192. continue
  2193. # Use 'stats_cleaned' except for HC group which uses 'stats'
  2194. stats_folder = os.path.join(subj_folder_path, "stats_cleaned")
  2195. file_name = f"{subject}_GrayVol_asymmetry.csv"
  2196. file_path = os.path.join(stats_folder, file_name)
  2197. if not os.path.exists(file_path):
  2198. print(f"Not found: {file_path}")
  2199. continue
  2200. try:
  2201. df = pd.read_csv(file_path)
  2202. # Check necessary columns
  2203. if not all(col in df.columns for col in ['StructName', 'Asymmetry_Index']):
  2204. print(f"Missing columns in {file_path}")
  2205. continue
  2206. df['participant_id'] = subject
  2207. df['Unsigned_Asymmetry'] = np.abs(df['Asymmetry_Index'])
  2208. all_data.append(df[['participant_id', 'StructName', 'Unsigned_Asymmetry']])
  2209. except Exception as e:
  2210. print(f"Failed loading {file_path}: {e}")
  2211. # Merge all data
  2212. asymmetry_df = pd.concat(all_data, ignore_index=True)
  2213. full_df = pd.merge(asymmetry_df, participants_df, on='participant_id', how='inner')
  2214. full_df.dropna(subset=['Unsigned_Asymmetry', 'sex', 'histopathology', 'age_scan'], inplace=True)
  2215. # === Check response distribution and transform if needed ===
  2216. plt.figure(figsize=(8, 4))
  2217. sns.histplot(full_df['Unsigned_Asymmetry'], kde=True)
  2218. plt.title("Distribution of Volume Asymmetry between FCD subtypes (FCDIIa, FCDIIb)")
  2219. plt.show()
  2220. ks_stat, ks_p = kstest(full_df['Unsigned_Asymmetry'], 'norm')
  2221. if ks_p < 0.05:
  2222. full_df['Unsigned_Asymmetry'] = np.sqrt(full_df['Unsigned_Asymmetry'])
  2223. print("Data was not normal. Applied square root transformation.")
  2224. # %% [markdown]
  2225. # ## Beta regression for curvature FCDIIb vs FCDIIa
  2226. # %%
  2227. import os
  2228. import pandas as pd
  2229. import numpy as np
  2230. import statsmodels.formula.api as smf
  2231. from statsmodels.discrete.discrete_model import MNLogit
  2232. from sklearn.preprocessing import MinMaxScaler
  2233. from scipy.stats import spearmanr, chi2_contingency, kstest
  2234. import statsmodels.api as sm
  2235. import matplotlib.pyplot as plt
  2236. import seaborn as sns
  2237. from statsmodels.stats.multitest import multipletests
  2238. import rpy2.robjects as ro
  2239. from rpy2.robjects import pandas2ri
  2240. from rpy2.robjects.packages import importr
  2241. from rpy2.robjects.conversion import localconverter
  2242. from rpy2.robjects import r, globalenv, Formula
  2243. from rpy2.robjects.packages import importr
  2244. betareg = importr('betareg')
  2245. import warnings
  2246. warnings.filterwarnings("ignore")
  2247. # ==== 3. Load R packages ====
  2248. utils = importr('utils')
  2249. utils.install_packages('betareg') # will skip if already installed
  2250. betareg = importr('betareg')
  2251. # === Base directory and metadata path ===
  2252. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy"
  2253. metadata_csv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.csv")
  2254. metadata_tsv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv")
  2255. # === Load participant metadata safely ===
  2256. try:
  2257. if os.path.exists(metadata_csv):
  2258. participants_df = pd.read_csv(metadata_csv, sep=None, engine='python', on_bad_lines='skip')
  2259. elif os.path.exists(metadata_tsv):
  2260. participants_df = pd.read_csv(metadata_tsv, sep='\t', engine='python', on_bad_lines='skip')
  2261. else:
  2262. raise FileNotFoundError("Neither CSV nor TSV metadata file found.")
  2263. except Exception as e:
  2264. raise RuntimeError(f"Failed to load metadata: {e}")
  2265. # === Standardize and encode columns ===
  2266. # Drop rows missing important fields first
  2267. participants_df.dropna(subset=['sex', 'age_scan','histopathology'], inplace=True)
  2268. # Clean metadata
  2269. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  2270. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  2271. # Encode categorical variables
  2272. sex_map = {'F': 0, 'M': 1}
  2273. participants_df['sex'] = participants_df['sex'].map(sex_map)
  2274. # Encode histopathology (use instead of group)
  2275. participants_df['histopathology'] = participants_df['histopathology'].str.lower().map({'iia': 0, 'iib': 1, 'na': 2})
  2276. # Ensure 'participant_id' exists and is string
  2277. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  2278. # === Independence Checks ===
  2279. print("\n=== Independence Checks ===")
  2280. print("Spearman correlation between age_scan and sex:", spearmanr(participants_df['age_scan'], participants_df['sex']))
  2281. print("Spearman correlation between age_scan and histopathology:", spearmanr(participants_df['age_scan'], participants_df['histopathology']))
  2282. contingency_sex_histopathology = pd.crosstab(participants_df['sex'], participants_df['histopathology'])
  2283. chi2, p, _, _ = chi2_contingency(contingency_sex_histopathology)
  2284. print("Chi-square test between sex and histopathology: chi2 =", chi2, ", p =", p)
  2285. # === Load asymmetry data ===
  2286. group_folders = ["asymmetry_fcdlla", "asymmetry_fcdllb"]
  2287. all_data = []
  2288. for group_folder in group_folders:
  2289. group_path = os.path.join(data_root, group_folder)
  2290. if not os.path.exists(group_path):
  2291. print(f"Missing: {group_path}")
  2292. continue
  2293. # Iterate over subject folders inside group folder
  2294. for subject in os.listdir(group_path):
  2295. subj_folder_path = os.path.join(group_path, subject)
  2296. if not os.path.isdir(subj_folder_path):
  2297. continue
  2298. # Use 'stats_cleaned' except for HC group which uses 'stats'
  2299. stats_folder = os.path.join(subj_folder_path, "stats_cleaned")
  2300. file_name = f"{subject}_MeanCurv_asymmetry.csv"
  2301. file_path = os.path.join(stats_folder, file_name)
  2302. if not os.path.exists(file_path):
  2303. print(f"Not found: {file_path}")
  2304. continue
  2305. try:
  2306. df = pd.read_csv(file_path)
  2307. # Check necessary columns
  2308. if not all(col in df.columns for col in ['StructName', 'Asymmetry_Index']):
  2309. print(f"Missing columns in {file_path}")
  2310. continue
  2311. df['participant_id'] = subject
  2312. df['Unsigned_Asymmetry'] = np.abs(df['Asymmetry_Index'])
  2313. all_data.append(df[['participant_id', 'StructName', 'Unsigned_Asymmetry']])
  2314. except Exception as e:
  2315. print(f"Failed loading {file_path}: {e}")
  2316. # Merge all data
  2317. asymmetry_df = pd.concat(all_data, ignore_index=True)
  2318. full_df = pd.merge(asymmetry_df, participants_df, on='participant_id', how='inner')
  2319. full_df.dropna(subset=['Unsigned_Asymmetry', 'sex', 'histopathology', 'age_scan'], inplace=True)
  2320. # === Check response distribution and transform if needed ===
  2321. plt.figure(figsize=(8, 4))
  2322. sns.histplot(full_df['Unsigned_Asymmetry'], kde=True)
  2323. plt.title("Distribution of Curvature Asymmetry between FCD subtypes (FCDIIa, FCDIIb)")
  2324. plt.show()
  2325. ks_stat, ks_p = kstest(full_df['Unsigned_Asymmetry'], 'norm')
  2326. if ks_p < 0.05:
  2327. full_df['Unsigned_Asymmetry'] = np.sqrt(full_df['Unsigned_Asymmetry'])
  2328. print("Data was not normal. Applied square root transformation.")
  2329. # %% [markdown]
  2330. # ## Beta regression for surface area FCDIIb vs FCDIIa
  2331. # %%
  2332. import os
  2333. import pandas as pd
  2334. import numpy as np
  2335. import statsmodels.formula.api as smf
  2336. from statsmodels.discrete.discrete_model import MNLogit
  2337. from sklearn.preprocessing import MinMaxScaler
  2338. from scipy.stats import spearmanr, chi2_contingency, kstest
  2339. import statsmodels.api as sm
  2340. import matplotlib.pyplot as plt
  2341. import seaborn as sns
  2342. from statsmodels.stats.multitest import multipletests
  2343. import rpy2.robjects as ro
  2344. from rpy2.robjects import pandas2ri
  2345. from rpy2.robjects.packages import importr
  2346. from rpy2.robjects.conversion import localconverter
  2347. from rpy2.robjects import r, globalenv, Formula
  2348. from rpy2.robjects.packages import importr
  2349. betareg = importr('betareg')
  2350. import warnings
  2351. warnings.filterwarnings("ignore")
  2352. # ==== 3. Load R packages ====
  2353. utils = importr('utils')
  2354. utils.install_packages('betareg') # will skip if already installed
  2355. betareg = importr('betareg')
  2356. # === Base directory and metadata path ===
  2357. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy"
  2358. metadata_csv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.csv")
  2359. metadata_tsv = os.path.join(data_root, "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv")
  2360. # === Load participant metadata safely ===
  2361. try:
  2362. if os.path.exists(metadata_csv):
  2363. participants_df = pd.read_csv(metadata_csv, sep=None, engine='python', on_bad_lines='skip')
  2364. elif os.path.exists(metadata_tsv):
  2365. participants_df = pd.read_csv(metadata_tsv, sep='\t', engine='python', on_bad_lines='skip')
  2366. else:
  2367. raise FileNotFoundError("Neither CSV nor TSV metadata file found.")
  2368. except Exception as e:
  2369. raise RuntimeError(f"Failed to load metadata: {e}")
  2370. # === Standardize and encode columns ===
  2371. # Drop rows missing important fields first
  2372. participants_df.dropna(subset=['sex', 'age_scan','histopathology'], inplace=True)
  2373. # Clean metadata
  2374. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  2375. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  2376. # Encode categorical variables
  2377. sex_map = {'F': 0, 'M': 1}
  2378. participants_df['sex'] = participants_df['sex'].map(sex_map)
  2379. # Encode histopathology (use instead of group)
  2380. participants_df['histopathology'] = participants_df['histopathology'].str.lower().map({'iia': 0, 'iib': 1, 'na': 2})
  2381. # Ensure 'participant_id' exists and is string
  2382. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  2383. # === Independence Checks ===
  2384. print("\n=== Independence Checks ===")
  2385. print("Spearman correlation between age_scan and sex:", spearmanr(participants_df['age_scan'], participants_df['sex']))
  2386. print("Spearman correlation between age_scan and histopathology:", spearmanr(participants_df['age_scan'], participants_df['histopathology']))
  2387. contingency_sex_histopathology = pd.crosstab(participants_df['sex'], participants_df['histopathology'])
  2388. chi2, p, _, _ = chi2_contingency(contingency_sex_histopathology)
  2389. print("Chi-square test between sex and histopathology: chi2 =", chi2, ", p =", p)
  2390. # === Load asymmetry data ===
  2391. group_folders = ["asymmetry_fcdlla", "asymmetry_fcdllb"]
  2392. all_data = []
  2393. for group_folder in group_folders:
  2394. group_path = os.path.join(data_root, group_folder)
  2395. if not os.path.exists(group_path):
  2396. print(f"Missing: {group_path}")
  2397. continue
  2398. # Iterate over subject folders inside group folder
  2399. for subject in os.listdir(group_path):
  2400. subj_folder_path = os.path.join(group_path, subject)
  2401. if not os.path.isdir(subj_folder_path):
  2402. continue
  2403. # Use 'stats_cleaned' except for HC group which uses 'stats'
  2404. stats_folder = os.path.join(subj_folder_path, "stats_cleaned")
  2405. file_name = f"{subject}_SurfArea_asymmetry.csv"
  2406. file_path = os.path.join(stats_folder, file_name)
  2407. if not os.path.exists(file_path):
  2408. print(f"Not found: {file_path}")
  2409. continue
  2410. try:
  2411. df = pd.read_csv(file_path)
  2412. # Check necessary columns
  2413. if not all(col in df.columns for col in ['StructName', 'Asymmetry_Index']):
  2414. print(f"Missing columns in {file_path}")
  2415. continue
  2416. df['participant_id'] = subject
  2417. df['Unsigned_Asymmetry'] = np.abs(df['Asymmetry_Index'])
  2418. all_data.append(df[['participant_id', 'StructName', 'Unsigned_Asymmetry']])
  2419. except Exception as e:
  2420. print(f"Failed loading {file_path}: {e}")
  2421. # Merge all data
  2422. asymmetry_df = pd.concat(all_data, ignore_index=True)
  2423. full_df = pd.merge(asymmetry_df, participants_df, on='participant_id', how='inner')
  2424. full_df.dropna(subset=['Unsigned_Asymmetry', 'sex', 'histopathology', 'age_scan'], inplace=True)
  2425. # === Check response distribution and transform if needed ===
  2426. plt.figure(figsize=(8, 4))
  2427. sns.histplot(full_df['Unsigned_Asymmetry'], kde=True)
  2428. plt.title("Distribution of Surface Area Asymmetry between FCD subtypes (FCDIIa, FCDIIb)")
  2429. plt.show()
  2430. ks_stat, ks_p = kstest(full_df['Unsigned_Asymmetry'], 'norm')
  2431. if ks_p < 0.05:
  2432. full_df['Unsigned_Asymmetry'] = np.sqrt(full_df['Unsigned_Asymmetry'])
  2433. print("Data was not normal. Applied square root transformation.")
  2434. # %%
  2435. import rpy2.robjects.packages as rpackages
  2436. utils = rpackages.importr('utils')
  2437. utils.install_packages('statmod')
  2438. # %%
  2439. import rpy2.robjects as robjects
  2440. robjects.r('install.packages("numDeriv", repos="https://cloud.r-project.org/")')
  2441. robjects.r('install.packages("betareg", repos="https://cloud.r-project.org/")')
  2442. # %% [markdown]
  2443. # # FDR for all predictors * measures* ROIs, FCD vs HC Storey’s q-values FDR Correction
  2444. # %%
  2445. from rpy2.robjects.packages import importr
  2446. utils = importr("utils")
  2447. utils.install_packages("qvalue")
  2448. # %% [markdown]
  2449. # # For group only FCD vs HC
  2450. # %%
  2451. import os
  2452. from pathlib import Path
  2453. import logging
  2454. import pandas as pd
  2455. import numpy as np
  2456. from sklearn.preprocessing import MinMaxScaler
  2457. from statsmodels.stats.multitest import multipletests
  2458. import matplotlib.pyplot as plt
  2459. import seaborn as sns
  2460. from tqdm import tqdm
  2461. import warnings
  2462. warnings.filterwarnings("ignore")
  2463. import rpy2.robjects as ro
  2464. from rpy2.robjects import pandas2ri, r, Formula, globalenv
  2465. from rpy2.robjects.packages import importr
  2466. from rpy2.robjects.conversion import localconverter
  2467. import rpy2.robjects.packages as rpackages
  2468. from rpy2.robjects.packages import importr
  2469. utils = importr('utils')
  2470. utils.chooseCRANmirror(ind=1)
  2471. # Try BiocManager if qvalue is not on CRAN
  2472. if not rpackages.isinstalled('qvalue'):
  2473. try:
  2474. if not rpackages.isinstalled('BiocManager'):
  2475. utils.install_packages('BiocManager')
  2476. biocmanager = importr('BiocManager')
  2477. biocmanager.install('qvalue')
  2478. except Exception as e:
  2479. raise RuntimeError(f"Failed to install qvalue via BiocManager: {e}")
  2480. # Now import
  2481. qvalue_pkg = importr('qvalue')
  2482. utils = importr('utils')
  2483. utils.chooseCRANmirror(ind=1)
  2484. if not rpackages.isinstalled('qvalue'):
  2485. try:
  2486. utils.install_packages('qvalue', repos='https://cloud.r-project.org', Ncpus=4)
  2487. except Exception as e:
  2488. raise RuntimeError(f"Failed to install qvalue via CRAN: {e}")
  2489. # Logging
  2490. logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
  2491. logger = logging.getLogger("betareg_global")
  2492. # R packages
  2493. utils = importr('utils')
  2494. utils.install_packages('betareg')
  2495. try:
  2496. betareg = importr('betareg')
  2497. except Exception as e:
  2498. raise RuntimeError("R package 'betareg' not available. Please install it in R.") from e
  2499. # User paths
  2500. DATA_ROOT = Path("/Volumes/groups/tohkagroup/Bonn_Epilepsy")
  2501. METADATA_XLSX = DATA_ROOT / "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.xlsx"
  2502. METADATA_CSV = DATA_ROOT / "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.csv"
  2503. METADATA_TSV = DATA_ROOT / "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv"
  2504. OUT_ROOT = DATA_ROOT / "betareg_global_results_fcd_vs_hc_storey_qvalue_&_benjamini_hochberg_ group_only"
  2505. OUT_ROOT.mkdir(parents=True, exist_ok=True)
  2506. # Load participant metadata
  2507. participants_df = None
  2508. if METADATA_XLSX.exists():
  2509. try:
  2510. participants_df = pd.read_excel(METADATA_XLSX, engine='openpyxl')
  2511. logger.info(f"Loaded metadata from Excel: {METADATA_XLSX}")
  2512. except Exception as e:
  2513. logger.warning(f"Failed Excel read {METADATA_XLSX}: {e}")
  2514. if participants_df is None and METADATA_CSV.exists():
  2515. try:
  2516. participants_df = pd.read_csv(METADATA_CSV, sep=';', engine='python', on_bad_lines='skip')
  2517. logger.info(f"Loaded metadata from CSV: {METADATA_CSV}")
  2518. except Exception as e:
  2519. logger.warning(f"Failed CSV read {METADATA_CSV}: {e}")
  2520. if participants_df is None and METADATA_TSV.exists():
  2521. try:
  2522. participants_df = pd.read_csv(METADATA_TSV, sep='\t', engine='python', on_bad_lines='skip')
  2523. logger.info(f"Loaded metadata from TSV: {METADATA_TSV}")
  2524. except Exception as e:
  2525. logger.warning(f"Failed TSV read {METADATA_TSV}: {e}")
  2526. if participants_df is None:
  2527. raise FileNotFoundError("Metadata not found or unreadable.")
  2528. # Clean metadata
  2529. if 'participant_id' not in participants_df.columns and 'subject_id' in participants_df.columns:
  2530. participants_df = participants_df.rename(columns={'subject_id': 'participant_id'})
  2531. participants_df['group'] = participants_df['group'].astype(str).str.lower().replace({
  2532. 'fcdlla': 'fcd', 'fcdllb': 'fcd', 'fcdna': 'fcd'
  2533. })
  2534. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  2535. sex_map = {'F': 0, 'M': 1}
  2536. group_map = {'hc': 0, 'fcd': 1}
  2537. participants_df['sex'] = participants_df['sex'].map(sex_map)
  2538. participants_df['group'] = participants_df['group'].map(group_map)
  2539. participants_df = participants_df.dropna(subset=['participant_id', 'sex', 'group', 'age_scan'])
  2540. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  2541. logger.info(f"Loaded metadata: {len(participants_df)} participants after cleaning.")
  2542. # Settings
  2543. MEASURES = ["intensity", "thickness", "volume", "curvature", "surface_area"]
  2544. MEASURE_FILE_PATTERNS = {
  2545. "intensity": "_intensity_freesurfer_stats.csv",
  2546. "thickness": "_ThickAvg_asymmetry.csv",
  2547. "volume": "_GrayVol_asymmetry.csv",
  2548. "curvature": "_MeanCurv_asymmetry.csv",
  2549. "surface_area": "_SurfArea_asymmetry.csv",
  2550. }
  2551. GROUP_FOLDERS = ["asymmetry_fcdlla", "asymmetry_fcdllb", "asymmetry_fcdna", "asymmetry_hc"]
  2552. MIN_SUBJ_PER_ROI = 12
  2553. EPS = 1e-4
  2554. ALPHA = 0.05
  2555. CI_Z = 1.96
  2556. # ---------- Helper functions ----------
  2557. def collect_intensity_files():
  2558. files = []
  2559. for folder in GROUP_FOLDERS:
  2560. gp = DATA_ROOT / folder
  2561. if not gp.exists():
  2562. continue
  2563. for fn in os.listdir(gp):
  2564. if fn.endswith(MEASURE_FILE_PATTERNS['intensity']):
  2565. files.append(gp / fn)
  2566. return files
  2567. def collect_nested_measure_files(measure_key):
  2568. files = []
  2569. pattern_suffix = MEASURE_FILE_PATTERNS[measure_key]
  2570. for folder in GROUP_FOLDERS:
  2571. gp = DATA_ROOT / folder
  2572. if not gp.exists():
  2573. continue
  2574. for subj in os.listdir(gp):
  2575. subj_folder = gp / subj
  2576. if not subj_folder.is_dir():
  2577. continue
  2578. stats_folder = subj_folder / ("stats" if folder=="asymmetry_hc" else "stats_cleaned")
  2579. if not stats_folder.exists():
  2580. stats_folder = subj_folder / "stats"
  2581. if not stats_folder.exists():
  2582. continue
  2583. fpath = stats_folder / f"{subj}{pattern_suffix}"
  2584. if fpath.exists():
  2585. files.append(fpath)
  2586. return files
  2587. # ---------- Main loop ----------
  2588. all_results = []
  2589. failures = []
  2590. for measure in MEASURES:
  2591. logger.info(f"Processing measure: {measure}")
  2592. measure_out = OUT_ROOT / measure
  2593. measure_out.mkdir(parents=True, exist_ok=True)
  2594. files = collect_intensity_files() if measure=="intensity" else collect_nested_measure_files(measure)
  2595. if not files:
  2596. logger.warning(f"No files found for {measure}")
  2597. continue
  2598. rows = []
  2599. for fpath in files:
  2600. try:
  2601. df = pd.read_csv(fpath)
  2602. except:
  2603. continue
  2604. asym_col = 'Asymmetry_Index' if 'Asymmetry_Index' in df.columns else ('AsymmetryIndex' if 'AsymmetryIndex' in df.columns else None)
  2605. if asym_col is None:
  2606. continue
  2607. fname = fpath.name
  2608. participant_id = fname.replace(MEASURE_FILE_PATTERNS[measure], '') if measure=='intensity' else fname[:-len(MEASURE_FILE_PATTERNS[measure])]
  2609. df = df[['StructName', asym_col]].rename(columns={asym_col:'Asymmetry_Index'})
  2610. df['participant_id'] = participant_id
  2611. df['Unsigned_Asymmetry'] = np.abs(df['Asymmetry_Index'])
  2612. rows.append(df[['participant_id', 'StructName', 'Unsigned_Asymmetry']])
  2613. if not rows:
  2614. continue
  2615. asymmetry_df = pd.concat(rows, ignore_index=True)
  2616. merged = pd.merge(asymmetry_df, participants_df, on='participant_id', how='inner')
  2617. merged = merged.dropna(subset=['Unsigned_Asymmetry','sex','group','age_scan'])
  2618. if merged.empty:
  2619. continue
  2620. # Save distribution plot
  2621. plt.figure(figsize=(6,3))
  2622. sns.histplot(merged['Unsigned_Asymmetry'], bins=40)
  2623. plt.title(f"{measure} - Unsigned_Asymmetry distribution")
  2624. plt.tight_layout()
  2625. plt.savefig(measure_out / f"{measure}_scaled_dist.png")
  2626. plt.close()
  2627. roi_list = merged['StructName'].unique()
  2628. formula = Formula('Unsigned_Asymmetry ~ group + sex + age_scan')
  2629. for roi in tqdm(roi_list, desc=f"{measure} ROIs"):
  2630. roi_df = merged[merged['StructName']==roi].copy()
  2631. nsubj = roi_df['participant_id'].nunique()
  2632. if nsubj < MIN_SUBJ_PER_ROI or roi_df['Unsigned_Asymmetry'].nunique()<3:
  2633. failures.append({'Measure':measure,'ROI':roi,'Reason':'too_few_or_low_variability','N':nsubj})
  2634. continue
  2635. try:
  2636. with localconverter(ro.default_converter + pandas2ri.converter):
  2637. rdf = ro.conversion.py2rpy(roi_df[['Unsigned_Asymmetry','group','sex','age_scan']])
  2638. globalenv['rdf'] = rdf
  2639. model = betareg.betareg(formula, data=rdf)
  2640. summary_model = r.summary(model)
  2641. # --- Residual plots ---
  2642. try:
  2643. r_resid = r.residuals(model)
  2644. with localconverter(ro.default_converter + pandas2ri.converter):
  2645. py_resid = ro.conversion.rpy2py(r_resid)
  2646. resid_series = pd.Series(py_resid).astype(float)
  2647. fig, axes = plt.subplots(1, 2, figsize=(10, 4))
  2648. # Histogram
  2649. axes[0].hist(resid_series, bins=30, color='skyblue', edgecolor='black')
  2650. axes[0].set_title(f"{measure} {roi} residuals histogram")
  2651. # QQ plot
  2652. import statsmodels.api as sm
  2653. sm.graphics.qqplot(resid_series, line='45', ax=axes[1])
  2654. axes[1].set_title(f"{measure} {roi} residuals QQ")
  2655. plt.tight_layout()
  2656. plt.savefig(measure_out / f"{roi}_residuals.png", dpi=200)
  2657. plt.close()
  2658. except Exception as e:
  2659. logger.warning(f"Residuals plot failed for {measure} {roi}: {e}")
  2660. coef_table = summary_model.rx2('coefficients').rx2('mean')
  2661. coef_names = list(coef_table.rownames)
  2662. for name in coef_names:
  2663. est = float(coef_table.rx(name, 'Estimate')[0])
  2664. se = float(coef_table.rx(name, 'Std. Error')[0])
  2665. z = float(coef_table.rx(name, 'z value')[0])
  2666. p = float(coef_table.rx(name, 'Pr(>|z|)')[0])
  2667. ci_low = est - CI_Z * se
  2668. ci_high = est + CI_Z * se
  2669. all_results.append({
  2670. 'Measure': measure,
  2671. 'ROI': roi,
  2672. 'Predictor': name,
  2673. 'Estimate': est,
  2674. 'StdErr': se,
  2675. 'z': z,
  2676. 'Pval': p, # raw p-value
  2677. 'CI_low': ci_low,
  2678. 'CI_high': ci_high,
  2679. 'Pseudo_R2': float(summary_model.rx2('pseudo.r.squared')[0]) if 'pseudo.r.squared' in summary_model.names else np.nan,
  2680. 'N_subjects': nsubj
  2681. })
  2682. except Exception as e:
  2683. failures.append({'Measure':measure,'ROI':roi,'Reason':f"betareg_failed:{e}",'N':nsubj})
  2684. continue
  2685. # ---------- Results and corrections ----------
  2686. results_df = pd.DataFrame(all_results)
  2687. fails_df = pd.DataFrame(failures)
  2688. if results_df.empty:
  2689. raise RuntimeError("No model results collected.")
  2690. # --- Keep raw p-values separately for both stages ---
  2691. results_df['Pval_raw_global'] = results_df['Pval'].astype(float)
  2692. results_df['Pval_raw_predictor'] = results_df['Pval'].astype(float)
  2693. # --- Global FDR (all ROIs and measures) but only for group/histopathology ---
  2694. mask_interest = results_df['Predictor'].isin(['group', 'histopathology'])
  2695. pvals_all = results_df.loc[mask_interest, 'Pval_raw_global'].astype(float).values
  2696. # Benjamini-Hochberg (FDR)
  2697. fdr_adj = multipletests(pvals_all, alpha=ALPHA, method='fdr_bh')[1]
  2698. results_df.loc[mask_interest, 'FDR_p_global'] = fdr_adj
  2699. # Bonferroni correction
  2700. bonf_adj = multipletests(pvals_all, alpha=ALPHA, method='bonferroni')[1]
  2701. results_df.loc[mask_interest, 'Bonferroni_p'] = bonf_adj
  2702. # Flags
  2703. results_df.loc[mask_interest, 'significant_FDR_global'] = fdr_adj < ALPHA
  2704. results_df.loc[mask_interest, 'significant_Bonferroni'] = bonf_adj < ALPHA
  2705. # --- Storey q-values (using R's qvalue package) for global correction ---
  2706. try:
  2707. qvalue_pkg = importr('qvalue')
  2708. with localconverter(ro.default_converter + pandas2ri.converter):
  2709. r_pvals = ro.FloatVector(pvals_all)
  2710. qobj = qvalue_pkg.qvalue(r_pvals)
  2711. qvalues = np.array(qobj.rx2('qvalues'))
  2712. results_df.loc[mask_interest, 'qvalue_global'] = qvalues
  2713. results_df.loc[mask_interest, 'significant_qvalue'] = qvalues < ALPHA
  2714. logger.info("Storey q-values successfully computed for group/histopathology only (global correction).")
  2715. except Exception as e:
  2716. logger.warning(f"Storey q-value computation failed for global correction: {e}")
  2717. results_df['qvalue_global'] = np.nan
  2718. results_df['significant_qvalue'] = False
  2719. logger.info("Storey q-values successfully computed and added.")
  2720. except Exception as e:
  2721. logger.warning(f"Storey q-value computation failed: {e}")
  2722. results_df['qvalue_global'] = np.nan
  2723. results_df['significant_qvalue'] = False
  2724. # --- Per-predictor FDR (only for 'group' or 'histopathology') ---
  2725. results_df['FDR_p_within_predictor'] = np.nan
  2726. results_df['qvalue_within_predictor'] = np.nan
  2727. results_df['significant_qvalue_within_predictor'] = False
  2728. predictors_to_correct = ['group', 'histopathology'] # <only group or histopathology
  2729. for (measure, predictor), grp in results_df.groupby(['Measure','Predictor']):
  2730. if predictor not in predictors_to_correct:
  2731. continue # skip all other predictors like age, sex, etc.
  2732. # FDR correction (Benjamini-Hochberg)
  2733. adj_bh = multipletests(grp['Pval_raw_predictor'].values, method='fdr_bh')[1]
  2734. results_df.loc[grp.index, 'FDR_p_within_predictor'] = adj_bh
  2735. # Storey q-value
  2736. try:
  2737. with localconverter(ro.default_converter + pandas2ri.converter):
  2738. r_pvals = ro.FloatVector(grp['Pval_raw_predictor'].values)
  2739. qobj = qvalue_pkg.qvalue(r_pvals)
  2740. qvals = np.array(qobj.rx2('qvalues'))
  2741. results_df.loc[grp.index, 'qvalue_within_predictor'] = qvals
  2742. results_df.loc[grp.index, 'significant_qvalue_within_predictor'] = qvals < ALPHA
  2743. except Exception as e:
  2744. logger.warning(f"Storey q-value failed for predictor {predictor} in {measure}: {e}")
  2745. # --- Save outputs ---
  2746. results_df.to_csv(OUT_ROOT / "all_measures_betareg_results_with_global_corrections.csv", index=False)
  2747. try:
  2748. results_df.to_excel(OUT_ROOT / "all_measures_betareg_results_with_global_corrections.xlsx", index=False)
  2749. except:
  2750. logger.warning("Could not write Excel (check openpyxl)")
  2751. fails_df.to_csv(OUT_ROOT / "model_failures.csv", index=False)
  2752. logger.info(f"Saved results and failures in {OUT_ROOT}")
  2753. # %% [markdown]
  2754. # # Plot for FCD vs HC only group
  2755. # %%
  2756. import os
  2757. import numpy as np
  2758. import pandas as pd
  2759. import matplotlib.pyplot as plt
  2760. from nilearn import datasets, plotting
  2761. from nibabel.freesurfer.io import read_annot
  2762. # === Directories ===
  2763. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy/betareg_global_results_fcd_vs_hc_storey_qvalue_&_benjamini_hochberg_ group_only"
  2764. results_csv = os.path.join(data_root, "all_measures_betareg_results_with_global_corrections.csv")
  2765. output_dir = os.path.join(data_root, "brain_maps_hc_fcd_zscore_global_by_measure")
  2766. os.makedirs(output_dir, exist_ok=True)
  2767. # === Load results ===
  2768. df = pd.read_csv(results_csv)
  2769. # === Filter significant ROIs (global q < 0.05, exclude intercepts) ===
  2770. sig_df = df[
  2771. (df['qvalue_global'] < 0.05) &
  2772. (~df['Predictor'].str.lower().str.contains("intercept"))
  2773. ]
  2774. if sig_df.empty:
  2775. print(" No significant ROIs found with qvalue_global < 0.05 — plotting all instead.")
  2776. sig_df = df.copy()
  2777. # === Load fsaverage5 + aparc labels ===
  2778. fsaverage = datasets.fetch_surf_fsaverage('fsaverage5')
  2779. labels_left, ctab_left, names_left = read_annot(os.path.join(data_root, "lh.aparc.annot"))
  2780. labels_right, ctab_right, names_right = read_annot(os.path.join(data_root, "rh.aparc.annot"))
  2781. names_left = [n.decode("utf-8").lower() for n in names_left]
  2782. names_right = [n.decode("utf-8").lower() for n in names_right]
  2783. # === Map ROI z-scores to surface ===
  2784. def roi_to_surface(df_pred):
  2785. roi_dict = {roi.lower(): z for roi, z in zip(df_pred['ROI'], df_pred['z'])}
  2786. data_left = np.zeros(len(labels_left))
  2787. data_right = np.zeros(len(labels_right))
  2788. for roi_name, z in roi_dict.items():
  2789. if roi_name in names_left:
  2790. idx = names_left.index(roi_name)
  2791. data_left[labels_left == idx] = z
  2792. if roi_name in names_right:
  2793. idx = names_right.index(roi_name)
  2794. data_right[labels_right == idx] = z
  2795. return data_left, data_right
  2796. # === Plotting function ===
  2797. def save_views(data, hemi, predictor, measure):
  2798. for view in ['lateral', 'medial']:
  2799. fname = os.path.join(output_dir, f"{measure}_{predictor}_{hemi}_{view}.png")
  2800. fig = plt.figure(facecolor='black')
  2801. ax = fig.add_subplot(111, projection='3d')
  2802. plotting.plot_surf_stat_map(
  2803. fsaverage[f'infl_{hemi}'], data,
  2804. hemi=hemi,
  2805. bg_map=fsaverage[f'sulc_{hemi}'],
  2806. cmap='bwr', colorbar=True,
  2807. bg_on_data=True, darkness=0.5, alpha=1.0, axes=ax,
  2808. vmin=-5, vmax=5, view=view
  2809. )
  2810. for child_ax in fig.get_axes():
  2811. child_ax.set_facecolor("black")
  2812. fig.savefig(fname, dpi=300, facecolor='black', bbox_inches='tight')
  2813. plt.close(fig)
  2814. # === Generate plots per measure ===
  2815. measures = ['intensity', 'thickness', 'volume', 'curvature', 'surface_area']
  2816. for measure in measures:
  2817. df_measure = sig_df[sig_df['Measure'].str.lower() == measure.lower()]
  2818. if df_measure.empty:
  2819. print(f" No significant results found for {measure}, skipping.")
  2820. continue
  2821. predictors = df_measure['Predictor'].unique()
  2822. # Step 1: generate surface images
  2823. for predictor in predictors:
  2824. df_pred = df_measure[df_measure['Predictor'] == predictor]
  2825. data_left, data_right = roi_to_surface(df_pred)
  2826. save_views(data_left, 'left', predictor, measure)
  2827. save_views(data_right, 'right', predictor, measure)
  2828. print("Saved all individual predictor brain maps to:", output_dir)
  2829. # Step 2: assemble composite for this measure
  2830. n_preds = len(predictors)
  2831. fig, axes = plt.subplots(n_preds, 4, figsize=(16, 4 * n_preds), facecolor='black')
  2832. if n_preds == 1:
  2833. axes = np.array([axes])
  2834. for i, predictor in enumerate(predictors):
  2835. for j, hemi in enumerate(['left', 'right']):
  2836. for k, view in enumerate(['lateral', 'medial']):
  2837. img_path = os.path.join(output_dir, f"{measure}_{predictor}_{hemi}_{view}.png")
  2838. if os.path.exists(img_path):
  2839. img = plt.imread(img_path)
  2840. axes[i, j*2 + k].imshow(img)
  2841. axes[i, j*2 + k].axis("off")
  2842. axes[i, j*2 + k].set_facecolor('black')
  2843. axes[i, j*2 + k].set_title(f"{predictor} ({hemi} {view})", color='yellow', fontsize=10)
  2844. plt.tight_layout()
  2845. final_fig = os.path.join(output_dir, f"composite_brain_maps_{measure}_global_qvalue.png")
  2846. plt.show()
  2847. plt.savefig(final_fig, dpi=300, bbox_inches='tight', facecolor='black')
  2848. plt.close(fig)
  2849. print(f"Saved composite figure for {measure}: {final_fig}")
  2850. print("\n All measure-specific brain maps saved successfully!")
  2851. # %% [markdown]
  2852. # # FDR for all predictors * measures* ROIs, FCD subtypes Storey’s q-values FDR Correction
  2853. # %% [markdown]
  2854. # # FCD subtypes group only FCDllb vs FCDlla and FCDllb vs FCDna
  2855. # %%
  2856. import os
  2857. from pathlib import Path
  2858. import logging
  2859. import pandas as pd
  2860. import numpy as np
  2861. from statsmodels.stats.multitest import multipletests
  2862. import matplotlib.pyplot as plt
  2863. import seaborn as sns
  2864. from tqdm import tqdm
  2865. import warnings
  2866. warnings.filterwarnings("ignore")
  2867. import rpy2.robjects as ro
  2868. from rpy2.robjects import pandas2ri, r, Formula, globalenv
  2869. from rpy2.robjects.packages import importr
  2870. from rpy2.robjects.conversion import localconverter
  2871. import rpy2.robjects.packages as rpackages
  2872. from rpy2.robjects.packages import importr
  2873. utils = importr('utils')
  2874. utils.chooseCRANmirror(ind=1)
  2875. # Try BiocManager if qvalue is not on CRAN
  2876. if not rpackages.isinstalled('qvalue'):
  2877. try:
  2878. if not rpackages.isinstalled('BiocManager'):
  2879. utils.install_packages('BiocManager')
  2880. biocmanager = importr('BiocManager')
  2881. biocmanager.install('qvalue')
  2882. except Exception as e:
  2883. raise RuntimeError(f"Failed to install qvalue via BiocManager: {e}")
  2884. # Now import
  2885. qvalue_pkg = importr('qvalue')
  2886. utils = importr('utils')
  2887. utils.chooseCRANmirror(ind=1)
  2888. if not rpackages.isinstalled('qvalue'):
  2889. try:
  2890. utils.install_packages('qvalue', repos='https://cloud.r-project.org', Ncpus=4)
  2891. except Exception as e:
  2892. raise RuntimeError(f"Failed to install qvalue via CRAN: {e}")
  2893. # Logging
  2894. logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
  2895. logger = logging.getLogger("betareg_global")
  2896. # R packages
  2897. utils = importr('utils')
  2898. utils.install_packages('betareg')
  2899. try:
  2900. betareg = importr('betareg')
  2901. except Exception as e:
  2902. raise RuntimeError("R package 'betareg' not available. Please install it in R.") from e
  2903. # User paths
  2904. DATA_ROOT = Path("/Volumes/groups/tohkagroup/Bonn_Epilepsy")
  2905. METADATA_XLSX = DATA_ROOT / "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.xlsx"
  2906. METADATA_CSV = DATA_ROOT / "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.csv"
  2907. METADATA_TSV = DATA_ROOT / "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv"
  2908. OUT_ROOT = DATA_ROOT / "betareg_global_results_fcdsubtypes_storey_qvalue_&_benjamini_hochberg_group_only"
  2909. OUT_ROOT.mkdir(parents=True, exist_ok=True)
  2910. # Load participant metadata
  2911. participants_df = None
  2912. if METADATA_XLSX.exists():
  2913. try:
  2914. participants_df = pd.read_excel(METADATA_XLSX, engine='openpyxl')
  2915. logger.info(f"Loaded metadata from Excel: {METADATA_XLSX}")
  2916. except Exception as e:
  2917. logger.warning(f"Failed Excel read {METADATA_XLSX}: {e}")
  2918. if participants_df is None and METADATA_CSV.exists():
  2919. try:
  2920. participants_df = pd.read_csv(METADATA_CSV, sep=';', engine='python', on_bad_lines='skip')
  2921. logger.info(f"Loaded metadata from CSV: {METADATA_CSV}")
  2922. except Exception as e:
  2923. logger.warning(f"Failed CSV read {METADATA_CSV}: {e}")
  2924. if participants_df is None and METADATA_TSV.exists():
  2925. try:
  2926. participants_df = pd.read_csv(METADATA_TSV, sep='\t', engine='python', on_bad_lines='skip')
  2927. logger.info(f"Loaded metadata from TSV: {METADATA_TSV}")
  2928. except Exception as e:
  2929. logger.warning(f"Failed TSV read {METADATA_TSV}: {e}")
  2930. if participants_df is None:
  2931. raise FileNotFoundError("Metadata not found or unreadable.")
  2932. # Clean metadata
  2933. if 'participant_id' not in participants_df.columns and 'subject_id' in participants_df.columns:
  2934. participants_df = participants_df.rename(columns={'subject_id': 'participant_id'})
  2935. # Maps
  2936. sex_map = {'F': 0, 'M': 1}
  2937. hist_map = {'IIa': 0, 'IIb': 1, 'fcdna': 2}
  2938. # Apply mappings
  2939. participants_df['histopathology'] = participants_df['histopathology'].astype(str)
  2940. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  2941. participants_df = participants_df.dropna(subset=['participant_id', 'sex', 'histopathology', 'age_scan'])
  2942. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  2943. logger.info(f"Loaded metadata: {len(participants_df)} participants after cleaning.")
  2944. # Settings
  2945. MEASURES = ["intensity", "thickness", "volume", "curvature", "surface_area"]
  2946. MEASURE_FILE_PATTERNS = {
  2947. "intensity": "_intensity_freesurfer_stats.csv",
  2948. "thickness": "_ThickAvg_asymmetry.csv",
  2949. "volume": "_GrayVol_asymmetry.csv",
  2950. "curvature": "_MeanCurv_asymmetry.csv",
  2951. "surface_area": "_SurfArea_asymmetry.csv",
  2952. }
  2953. GROUP_FOLDERS = ["asymmetry_fcdlla", "asymmetry_fcdllb", "asymmetry_fcdna"]
  2954. MIN_SUBJ_PER_ROI = 8
  2955. ALPHA = 0.05
  2956. CI_Z = 1.96
  2957. # Helper functions
  2958. def collect_intensity_files():
  2959. files = []
  2960. for folder in GROUP_FOLDERS:
  2961. gp = DATA_ROOT / folder
  2962. if not gp.exists():
  2963. continue
  2964. for fn in os.listdir(gp):
  2965. if fn.endswith(MEASURE_FILE_PATTERNS['intensity']):
  2966. files.append(gp / fn)
  2967. return files
  2968. def collect_nested_measure_files(measure_key):
  2969. files = []
  2970. pattern_suffix = MEASURE_FILE_PATTERNS[measure_key]
  2971. for folder in GROUP_FOLDERS:
  2972. gp = DATA_ROOT / folder
  2973. if not gp.exists():
  2974. continue
  2975. for subj in os.listdir(gp):
  2976. subj_folder = gp / subj
  2977. if not subj_folder.is_dir():
  2978. continue
  2979. stats_folder = subj_folder / ("stats" if folder=="asymmetry_hc" else "stats_cleaned")
  2980. if not stats_folder.exists():
  2981. stats_folder = subj_folder / "stats"
  2982. if not stats_folder.exists():
  2983. continue
  2984. fpath = stats_folder / f"{subj}{pattern_suffix}"
  2985. if fpath.exists():
  2986. files.append(fpath)
  2987. return files
  2988. # Main loop
  2989. all_results = []
  2990. failures = []
  2991. for measure in MEASURES:
  2992. logger.info(f"Processing measure: {measure}")
  2993. measure_out = OUT_ROOT / measure
  2994. measure_out.mkdir(parents=True, exist_ok=True)
  2995. files = collect_intensity_files() if measure=="intensity" else collect_nested_measure_files(measure)
  2996. if not files:
  2997. logger.warning(f"No files found for {measure}")
  2998. continue
  2999. rows = []
  3000. for fpath in files:
  3001. try:
  3002. df = pd.read_csv(fpath)
  3003. except:
  3004. continue
  3005. asym_col = 'Asymmetry_Index' if 'Asymmetry_Index' in df.columns else ('AsymmetryIndex' if 'AsymmetryIndex' in df.columns else None)
  3006. if asym_col is None:
  3007. continue
  3008. fname = fpath.name
  3009. participant_id = fname.replace(MEASURE_FILE_PATTERNS[measure], '') if measure=='intensity' else fname[:-len(MEASURE_FILE_PATTERNS[measure])]
  3010. df = df[['StructName', asym_col]].rename(columns={asym_col:'Asymmetry_Index'})
  3011. df['participant_id'] = participant_id
  3012. df['Unsigned_Asymmetry'] = np.abs(df['Asymmetry_Index'])
  3013. rows.append(df[['participant_id', 'StructName', 'Unsigned_Asymmetry']])
  3014. if not rows:
  3015. continue
  3016. asymmetry_df = pd.concat(rows, ignore_index=True)
  3017. merged = pd.merge(asymmetry_df, participants_df, on='participant_id', how='inner')
  3018. merged = merged.dropna(subset=['Unsigned_Asymmetry','sex','histopathology','age_scan'])
  3019. if merged.empty:
  3020. continue
  3021. # Save distribution plot
  3022. plt.figure(figsize=(6,3))
  3023. sns.histplot(merged['Unsigned_Asymmetry'], bins=40)
  3024. plt.title(f"{measure} - Unsigned_Asymmetry distribution")
  3025. plt.tight_layout()
  3026. plt.savefig(measure_out / f"{measure}_scaled_dist.png")
  3027. plt.close()
  3028. roi_list = merged['StructName'].unique()
  3029. formula = Formula('Unsigned_Asymmetry ~ histopathology + sex + age_scan')
  3030. for roi in tqdm(roi_list, desc=f"{measure} ROIs"):
  3031. roi_df = merged[merged['StructName']==roi].copy()
  3032. # Drop rows with missing asymmetry values or missing metadata
  3033. roi_df = roi_df.dropna(subset=['Unsigned_Asymmetry','histopathology','sex','age_scan'])
  3034. roi_df = roi_df.reset_index(drop=True)
  3035. logger.info("Using predictor column: %s, unique values = %s",
  3036. 'histopathology', roi_df['histopathology'].unique())
  3037. nsubj = roi_df['participant_id'].nunique()
  3038. if nsubj < MIN_SUBJ_PER_ROI or roi_df['Unsigned_Asymmetry'].nunique()<3:
  3039. failures.append({'Measure':measure,'ROI':roi,'Reason':'too_few_or_low_variability','N':nsubj})
  3040. continue
  3041. try:
  3042. with localconverter(ro.default_converter + pandas2ri.converter):
  3043. rdf = ro.conversion.py2rpy(roi_df[['Unsigned_Asymmetry','histopathology','sex','age_scan']])
  3044. globalenv['rdf'] = rdf
  3045. model = betareg.betareg(formula, data=rdf)
  3046. summary_model = r.summary(model)
  3047. # --- Residual plots ---
  3048. try:
  3049. r_resid = r.residuals(model)
  3050. with localconverter(ro.default_converter + pandas2ri.converter):
  3051. py_resid = ro.conversion.rpy2py(r_resid)
  3052. resid_series = pd.Series(py_resid).astype(float)
  3053. fig, axes = plt.subplots(1, 2, figsize=(10, 4))
  3054. # Histogram
  3055. axes[0].hist(resid_series, bins=30, color='skyblue', edgecolor='black')
  3056. axes[0].set_title(f"{measure} {roi} residuals histogram")
  3057. # QQ plot
  3058. import statsmodels.api as sm
  3059. sm.graphics.qqplot(resid_series, line='45', ax=axes[1])
  3060. axes[1].set_title(f"{measure} {roi} residuals QQ")
  3061. plt.tight_layout()
  3062. plt.savefig(measure_out / f"{roi}_residuals.png", dpi=200)
  3063. plt.close()
  3064. except Exception as e:
  3065. logger.warning(f"Residuals plot failed for {measure} {roi}: {e}")
  3066. coef_table = summary_model.rx2('coefficients').rx2('mean')
  3067. coef_names = list(coef_table.rownames)
  3068. for name in coef_names:
  3069. est = float(coef_table.rx(name, 'Estimate')[0])
  3070. se = float(coef_table.rx(name, 'Std. Error')[0])
  3071. z = float(coef_table.rx(name, 'z value')[0])
  3072. p = float(coef_table.rx(name, 'Pr(>|z|)')[0])
  3073. ci_low = est - CI_Z * se
  3074. ci_high = est + CI_Z * se
  3075. all_results.append({
  3076. 'Measure': measure,
  3077. 'ROI': roi,
  3078. 'Predictor': name,
  3079. 'Estimate': est,
  3080. 'StdErr': se,
  3081. 'z': z,
  3082. 'Pval': p, # raw p-value
  3083. 'CI_low': ci_low,
  3084. 'CI_high': ci_high,
  3085. 'Pseudo_R2': float(summary_model.rx2('pseudo.r.squared')[0]) if 'pseudo.r.squared' in summary_model.names else np.nan,
  3086. 'N_subjects': nsubj
  3087. })
  3088. except Exception as e:
  3089. failures.append({'Measure':measure,'ROI':roi,'Reason':f"betareg_failed:{e}",'N':nsubj})
  3090. continue
  3091. # ---------- Results and corrections ----------
  3092. results_df = pd.DataFrame(all_results)
  3093. fails_df = pd.DataFrame(failures)
  3094. if results_df.empty:
  3095. raise RuntimeError("No model results collected.")
  3096. # --- Keep raw p-values separately for both stages ---
  3097. results_df['Pval_raw_global'] = results_df['Pval'].astype(float)
  3098. results_df['Pval_raw_predictor'] = results_df['Pval'].astype(float)
  3099. # --- Global FDR (all ROIs and measures) but only for group/histopathology ---
  3100. # Select all predictors related to histopathology
  3101. mask_interest = results_df['Predictor'].str.contains('histopathology', na=False)
  3102. pvals_all = results_df.loc[mask_interest, 'Pval_raw_global'].astype(float).values
  3103. if len(pvals_all) == 0:
  3104. raise RuntimeError("No histopathology coefficients found for global correction!")
  3105. # Benjamini-Hochberg (FDR)
  3106. fdr_adj = multipletests(pvals_all, alpha=ALPHA, method='fdr_bh')[1]
  3107. results_df.loc[mask_interest, 'FDR_p_global'] = fdr_adj
  3108. # Bonferroni correction
  3109. bonf_adj = multipletests(pvals_all, alpha=ALPHA, method='bonferroni')[1]
  3110. results_df.loc[mask_interest, 'Bonferroni_p'] = bonf_adj
  3111. # Flags
  3112. results_df.loc[mask_interest, 'significant_FDR_global'] = fdr_adj < ALPHA
  3113. results_df.loc[mask_interest, 'significant_Bonferroni'] = bonf_adj < ALPHA
  3114. # --- Storey q-values (using R's qvalue package) for global correction ---
  3115. try:
  3116. qvalue_pkg = importr('qvalue')
  3117. with localconverter(ro.default_converter + pandas2ri.converter):
  3118. r_pvals = ro.FloatVector(pvals_all)
  3119. qobj = qvalue_pkg.qvalue(r_pvals)
  3120. qvalues = np.array(qobj.rx2('qvalues'))
  3121. results_df.loc[mask_interest, 'qvalue_global'] = qvalues
  3122. results_df.loc[mask_interest, 'significant_qvalue'] = qvalues < ALPHA
  3123. logger.info("Storey q-values successfully computed for group/histopathology only (global correction).")
  3124. except Exception as e:
  3125. logger.warning(f"Storey q-value computation failed for global correction: {e}")
  3126. results_df['qvalue_global'] = np.nan
  3127. results_df['significant_qvalue'] = False
  3128. # --- Per-predictor FDR (only for 'group' or 'histopathology') ---
  3129. results_df['FDR_p_within_predictor'] = np.nan
  3130. results_df['qvalue_within_predictor'] = np.nan
  3131. results_df['significant_qvalue_within_predictor'] = False
  3132. predictors_to_correct = ['histopathology'] # <only histopathology
  3133. for (measure, predictor), grp in results_df.groupby(['Measure','Predictor']):
  3134. if 'histopathology' not in predictor:
  3135. continue # skip all other predictors like age, sex, etc.
  3136. # FDR correction (Benjamini-Hochberg)
  3137. adj_bh = multipletests(grp['Pval_raw_predictor'].values, method='fdr_bh')[1]
  3138. results_df.loc[grp.index, 'FDR_p_within_predictor'] = adj_bh
  3139. # Storey q-value
  3140. try:
  3141. with localconverter(ro.default_converter + pandas2ri.converter):
  3142. r_pvals = ro.FloatVector(grp['Pval_raw_predictor'].values)
  3143. qobj = qvalue_pkg.qvalue(r_pvals)
  3144. qvals = np.array(qobj.rx2('qvalues'))
  3145. results_df.loc[grp.index, 'qvalue_within_predictor'] = qvals
  3146. results_df.loc[grp.index, 'significant_qvalue_within_predictor'] = qvals < ALPHA
  3147. except Exception as e:
  3148. logger.warning(f"Storey q-value failed for predictor {predictor} in {measure}: {e}")
  3149. # --- Save outputs ---
  3150. results_df.to_csv(OUT_ROOT / "all_measures_betareg_results_with_global_corrections.csv", index=False)
  3151. try:
  3152. results_df.to_excel(OUT_ROOT / "all_measures_betareg_results_with_global_corrections.xlsx", index=False)
  3153. except:
  3154. logger.warning("Could not write Excel (check openpyxl)")
  3155. fails_df.to_csv(OUT_ROOT / "model_failures.csv", index=False)
  3156. logger.info(f"Saved results and failures in {OUT_ROOT}")
  3157. # %% [markdown]
  3158. # # Plot for FCD subtypes histopathology only
  3159. # %%
  3160. import os
  3161. import numpy as np
  3162. import pandas as pd
  3163. import matplotlib.pyplot as plt
  3164. from nilearn import datasets, plotting
  3165. from nibabel.freesurfer.io import read_annot
  3166. # === Directories ===
  3167. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy/betareg_global_results_fcdsubtypes_storey_qvalue_&_benjamini_hochberg_group_only"
  3168. results_csv = os.path.join(data_root, "all_measures_betareg_results_with_global_corrections.csv")
  3169. output_dir = os.path.join(data_root, "brain_maps_fcdsubtypes_zscore_global_by_measure")
  3170. os.makedirs(output_dir, exist_ok=True)
  3171. # === Load results ===
  3172. df = pd.read_csv(results_csv)
  3173. # === Filter significant ROIs (global q < 0.05, exclude intercepts) ===
  3174. sig_df = df[
  3175. (df['qvalue_global'] < 0.05) &
  3176. (~df['Predictor'].str.lower().str.contains("intercept"))
  3177. ]
  3178. if sig_df.empty:
  3179. print(" No significant ROIs found with qvalue_global < 0.05 — plotting all instead.")
  3180. sig_df = df.copy()
  3181. # === Load fsaverage5 + aparc labels ===
  3182. fsaverage = datasets.fetch_surf_fsaverage('fsaverage5')
  3183. labels_left, ctab_left, names_left = read_annot(os.path.join(data_root, "lh.aparc.annot"))
  3184. labels_right, ctab_right, names_right = read_annot(os.path.join(data_root, "rh.aparc.annot"))
  3185. names_left = [n.decode("utf-8").lower() for n in names_left]
  3186. names_right = [n.decode("utf-8").lower() for n in names_right]
  3187. # === Map ROI z-scores to surface ===
  3188. def roi_to_surface(df_pred):
  3189. # Only keep significant ROIs
  3190. roi_dict = {
  3191. roi.lower(): z
  3192. for roi, z, q in zip(df_pred['ROI'], df_pred['z'], df_pred['qvalue_global'])
  3193. if q < 0.05
  3194. }
  3195. data_left = np.zeros(len(labels_left))
  3196. data_right = np.zeros(len(labels_right))
  3197. for roi_name, z in roi_dict.items():
  3198. if roi_name in names_left:
  3199. idx = names_left.index(roi_name)
  3200. data_left[labels_left == idx] = z
  3201. if roi_name in names_right:
  3202. idx = names_right.index(roi_name)
  3203. data_right[labels_right == idx] = z
  3204. return data_left, data_right
  3205. # === Plotting function ===
  3206. def save_views(data, hemi, predictor, measure):
  3207. for view in ['lateral', 'medial']:
  3208. fname = os.path.join(output_dir, f"{measure}_{predictor}_{hemi}_{view}.png")
  3209. fig = plt.figure(facecolor='black')
  3210. ax = fig.add_subplot(111, projection='3d')
  3211. plotting.plot_surf_stat_map(
  3212. fsaverage[f'infl_{hemi}'], data,
  3213. hemi=hemi,
  3214. bg_map=fsaverage[f'sulc_{hemi}'],
  3215. cmap='bwr', colorbar=True,
  3216. bg_on_data=True, darkness=0.5, alpha=1.0, axes=ax,
  3217. vmin=-5, vmax=5, view=view
  3218. )
  3219. for child_ax in fig.get_axes():
  3220. child_ax.set_facecolor("black")
  3221. fig.savefig(fname, dpi=300, facecolor='black', bbox_inches='tight')
  3222. plt.close(fig)
  3223. # === Generate plots per measure ===
  3224. measures = ['intensity', 'thickness', 'volume', 'curvature', 'surface_area']
  3225. for measure in measures:
  3226. df_measure = sig_df[sig_df['Measure'].str.lower() == measure.lower()]
  3227. if df_measure.empty:
  3228. print(f" No significant results found for {measure}, skipping.")
  3229. continue
  3230. predictors = df_measure['Predictor'].unique()
  3231. # Step 1: generate surface images
  3232. for predictor in predictors:
  3233. df_pred = df_measure[df_measure['Predictor'] == predictor]
  3234. data_left, data_right = roi_to_surface(df_pred)
  3235. save_views(data_left, 'left', predictor, measure)
  3236. save_views(data_right, 'right', predictor, measure)
  3237. print("Saved all individual predictor brain maps to:", output_dir)
  3238. # Step 2: assemble composite for this measure
  3239. n_preds = len(predictors)
  3240. fig, axes = plt.subplots(n_preds, 4, figsize=(16, 4 * n_preds), facecolor='black')
  3241. if n_preds == 1:
  3242. axes = np.array([axes])
  3243. for i, predictor in enumerate(predictors):
  3244. for j, hemi in enumerate(['left', 'right']):
  3245. for k, view in enumerate(['lateral', 'medial']):
  3246. img_path = os.path.join(output_dir, f"{measure}_{predictor}_{hemi}_{view}.png")
  3247. if os.path.exists(img_path):
  3248. img = plt.imread(img_path)
  3249. axes[i, j*2 + k].imshow(img)
  3250. axes[i, j*2 + k].axis("off")
  3251. axes[i, j*2 + k].set_facecolor('black')
  3252. axes[i, j*2 + k].set_title(f"{predictor} ({hemi} {view})", color='yellow', fontsize=10)
  3253. plt.tight_layout()
  3254. final_fig = os.path.join(output_dir, f"composite_brain_maps_{measure}_global_qvalue.png")
  3255. plt.show()
  3256. plt.savefig(final_fig, dpi=300, bbox_inches='tight', facecolor='black')
  3257. plt.close(fig)
  3258. print(f"Saved composite figure for {measure}: {final_fig}")
  3259. print("\n All measure-specific brain maps saved successfully!")
  3260. # %% [markdown]
  3261. # # FDR for all predictors * measures* ROIs, FCDIIb vs FCDIIa Storey’s q-values FDR Correction
  3262. # %% [markdown]
  3263. # # For group or histopathology only FCDIIb vs FCDIIa
  3264. # %%
  3265. # unified_betareg_all_measures_global_corrections.py
  3266. # Single script to run beta regression across five measures, keep original file paths,
  3267. # apply global FDR + Bonferroni (FWER) across all tests,
  3268. # compute confidence intervals, and save residual plots and diagnostics.
  3269. import os
  3270. from pathlib import Path
  3271. import logging
  3272. import pandas as pd
  3273. import numpy as np
  3274. from statsmodels.stats.multitest import multipletests
  3275. import matplotlib.pyplot as plt
  3276. import seaborn as sns
  3277. from tqdm import tqdm
  3278. import warnings
  3279. warnings.filterwarnings("ignore")
  3280. import rpy2.robjects as ro
  3281. from rpy2.robjects import pandas2ri, r, Formula, globalenv
  3282. from rpy2.robjects.packages import importr
  3283. from rpy2.robjects.conversion import localconverter
  3284. import rpy2.robjects.packages as rpackages
  3285. from rpy2.robjects.packages import importr
  3286. utils = importr('utils')
  3287. utils.chooseCRANmirror(ind=1)
  3288. # Try BiocManager if qvalue is not on CRAN
  3289. if not rpackages.isinstalled('qvalue'):
  3290. try:
  3291. if not rpackages.isinstalled('BiocManager'):
  3292. utils.install_packages('BiocManager')
  3293. biocmanager = importr('BiocManager')
  3294. biocmanager.install('qvalue')
  3295. except Exception as e:
  3296. raise RuntimeError(f"Failed to install qvalue via BiocManager: {e}")
  3297. # Now import
  3298. qvalue_pkg = importr('qvalue')
  3299. utils = importr('utils')
  3300. utils.chooseCRANmirror(ind=1)
  3301. if not rpackages.isinstalled('qvalue'):
  3302. try:
  3303. utils.install_packages('qvalue', repos='https://cloud.r-project.org', Ncpus=4)
  3304. except Exception as e:
  3305. raise RuntimeError(f"Failed to install qvalue via CRAN: {e}")
  3306. # Logging
  3307. logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
  3308. logger = logging.getLogger("betareg_global")
  3309. # R packages
  3310. utils = importr('utils')
  3311. utils.install_packages('betareg')
  3312. try:
  3313. betareg = importr('betareg')
  3314. except Exception as e:
  3315. raise RuntimeError("R package 'betareg' not available. Please install it in R.") from e
  3316. # User paths
  3317. DATA_ROOT = Path("/Volumes/groups/tohkagroup/Bonn_Epilepsy")
  3318. METADATA_XLSX = DATA_ROOT / "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.xlsx"
  3319. METADATA_CSV = DATA_ROOT / "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.csv"
  3320. METADATA_TSV = DATA_ROOT / "derivatives_fsaverage/freesurfer7.4.1/participants.aparc.tsv"
  3321. OUT_ROOT = DATA_ROOT / "betareg_global_results_fcdlla_vs_fcdllb_storey_qvalue_&_benjamini_hochberg_group_only"
  3322. OUT_ROOT.mkdir(parents=True, exist_ok=True)
  3323. # Load participant metadata
  3324. participants_df = None
  3325. if METADATA_XLSX.exists():
  3326. try:
  3327. participants_df = pd.read_excel(METADATA_XLSX, engine='openpyxl')
  3328. logger.info(f"Loaded metadata from Excel: {METADATA_XLSX}")
  3329. except Exception as e:
  3330. logger.warning(f"Failed Excel read {METADATA_XLSX}: {e}")
  3331. if participants_df is None and METADATA_CSV.exists():
  3332. try:
  3333. participants_df = pd.read_csv(METADATA_CSV, sep=';', engine='python', on_bad_lines='skip')
  3334. logger.info(f"Loaded metadata from CSV: {METADATA_CSV}")
  3335. except Exception as e:
  3336. logger.warning(f"Failed CSV read {METADATA_CSV}: {e}")
  3337. if participants_df is None and METADATA_TSV.exists():
  3338. try:
  3339. participants_df = pd.read_csv(METADATA_TSV, sep='\t', engine='python', on_bad_lines='skip')
  3340. logger.info(f"Loaded metadata from TSV: {METADATA_TSV}")
  3341. except Exception as e:
  3342. logger.warning(f"Failed TSV read {METADATA_TSV}: {e}")
  3343. if participants_df is None:
  3344. raise FileNotFoundError("Metadata not found or unreadable.")
  3345. # Clean metadata
  3346. if 'participant_id' not in participants_df.columns and 'subject_id' in participants_df.columns:
  3347. participants_df = participants_df.rename(columns={'subject_id': 'participant_id'})
  3348. participants_df['group'] = participants_df['group'].astype(str).str.lower()
  3349. participants_df['sex'] = participants_df['sex'].astype(str).str.upper()
  3350. sex_map = {'F': 0, 'M': 1}
  3351. hist_map = {'IIa': 0, 'IIb': 1}
  3352. participants_df['group'] = participants_df['histopathology'].map(hist_map)
  3353. participants_df = participants_df.dropna(subset=['participant_id', 'sex', 'group', 'age_scan'])
  3354. participants_df['participant_id'] = participants_df['participant_id'].astype(str)
  3355. logger.info(f"Loaded metadata: {len(participants_df)} participants after cleaning.")
  3356. # Settings
  3357. MEASURES = ["intensity", "thickness", "volume", "curvature", "surface_area"]
  3358. MEASURE_FILE_PATTERNS = {
  3359. "intensity": "_intensity_freesurfer_stats.csv",
  3360. "thickness": "_ThickAvg_asymmetry.csv",
  3361. "volume": "_GrayVol_asymmetry.csv",
  3362. "curvature": "_MeanCurv_asymmetry.csv",
  3363. "surface_area": "_SurfArea_asymmetry.csv",
  3364. }
  3365. GROUP_FOLDERS = ["asymmetry_fcdlla", "asymmetry_fcdllb"]
  3366. MIN_SUBJ_PER_ROI = 8
  3367. EPS = 1e-4
  3368. ALPHA = 0.05
  3369. CI_Z = 1.96
  3370. # ---------- Helper functions ----------
  3371. def collect_intensity_files():
  3372. files = []
  3373. for folder in GROUP_FOLDERS:
  3374. gp = DATA_ROOT / folder
  3375. if not gp.exists():
  3376. continue
  3377. for fn in os.listdir(gp):
  3378. if fn.endswith(MEASURE_FILE_PATTERNS['intensity']):
  3379. files.append(gp / fn)
  3380. return files
  3381. def collect_nested_measure_files(measure_key):
  3382. files = []
  3383. pattern_suffix = MEASURE_FILE_PATTERNS[measure_key]
  3384. for folder in GROUP_FOLDERS:
  3385. gp = DATA_ROOT / folder
  3386. if not gp.exists():
  3387. continue
  3388. for subj in os.listdir(gp):
  3389. subj_folder = gp / subj
  3390. if not subj_folder.is_dir():
  3391. continue
  3392. stats_folder = subj_folder / ("stats" if folder=="asymmetry_hc" else "stats_cleaned")
  3393. if not stats_folder.exists():
  3394. stats_folder = subj_folder / "stats"
  3395. if not stats_folder.exists():
  3396. continue
  3397. fpath = stats_folder / f"{subj}{pattern_suffix}"
  3398. if fpath.exists():
  3399. files.append(fpath)
  3400. return files
  3401. # ---------- Main loop ----------
  3402. all_results = []
  3403. failures = []
  3404. for measure in MEASURES:
  3405. logger.info(f"Processing measure: {measure}")
  3406. measure_out = OUT_ROOT / measure
  3407. measure_out.mkdir(parents=True, exist_ok=True)
  3408. files = collect_intensity_files() if measure=="intensity" else collect_nested_measure_files(measure)
  3409. if not files:
  3410. logger.warning(f"No files found for {measure}")
  3411. continue
  3412. rows = []
  3413. for fpath in files:
  3414. try:
  3415. df = pd.read_csv(fpath)
  3416. except:
  3417. continue
  3418. asym_col = 'Asymmetry_Index' if 'Asymmetry_Index' in df.columns else ('AsymmetryIndex' if 'AsymmetryIndex' in df.columns else None)
  3419. if asym_col is None:
  3420. continue
  3421. fname = fpath.name
  3422. participant_id = fname.replace(MEASURE_FILE_PATTERNS[measure], '') if measure=='intensity' else fname[:-len(MEASURE_FILE_PATTERNS[measure])]
  3423. df = df[['StructName', asym_col]].rename(columns={asym_col:'Asymmetry_Index'})
  3424. df['participant_id'] = participant_id
  3425. df['Unsigned_Asymmetry'] = np.abs(df['Asymmetry_Index'])
  3426. rows.append(df[['participant_id', 'StructName', 'Unsigned_Asymmetry']])
  3427. if not rows:
  3428. continue
  3429. asymmetry_df = pd.concat(rows, ignore_index=True)
  3430. merged = pd.merge(asymmetry_df, participants_df, on='participant_id', how='inner')
  3431. merged = merged.dropna(subset=['Unsigned_Asymmetry','sex','group','age_scan'])
  3432. if merged.empty:
  3433. continue
  3434. # Save distribution plot
  3435. plt.figure(figsize=(6,3))
  3436. sns.histplot(merged['Unsigned_Asymmetry'], bins=40)
  3437. plt.title(f"{measure} - Unsigned_Asymmetry distribution")
  3438. plt.tight_layout()
  3439. plt.savefig(measure_out / f"{measure}_scaled_dist.png")
  3440. plt.close()
  3441. roi_list = merged['StructName'].unique()
  3442. formula = Formula('Unsigned_Asymmetry ~ group + sex + age_scan')
  3443. for roi in tqdm(roi_list, desc=f"{measure} ROIs"):
  3444. roi_df = merged[merged['StructName'] == roi].copy()
  3445. nsubj = roi_df['participant_id'].nunique()
  3446. if nsubj < MIN_SUBJ_PER_ROI:
  3447. failures.append({'Measure': measure, 'ROI': roi, 'Reason': 'too_few_subjects', 'N': nsubj})
  3448. continue
  3449. try:
  3450. # Convert to R dataframe
  3451. with localconverter(ro.default_converter + pandas2ri.converter):
  3452. rdf = ro.conversion.py2rpy(roi_df[['Unsigned_Asymmetry', 'group', 'sex', 'age_scan']])
  3453. globalenv['rdf'] = rdf
  3454. # Fit beta regression
  3455. model = betareg.betareg(formula, data=rdf)
  3456. summary_model = r.summary(model)
  3457. # --- Residual plots ---
  3458. try:
  3459. r_resid = r.residuals(model)
  3460. with localconverter(ro.default_converter + pandas2ri.converter):
  3461. py_resid = ro.conversion.rpy2py(r_resid)
  3462. resid_series = pd.Series(py_resid).astype(float)
  3463. fig, axes = plt.subplots(1, 2, figsize=(10, 4))
  3464. # Histogram
  3465. axes[0].hist(resid_series, bins=30, color='skyblue', edgecolor='black')
  3466. axes[0].set_title(f"{measure} {roi} residuals histogram")
  3467. # QQ plot
  3468. import statsmodels.api as sm
  3469. sm.graphics.qqplot(resid_series, line='45', ax=axes[1])
  3470. axes[1].set_title(f"{measure} {roi} residuals QQ")
  3471. plt.tight_layout()
  3472. plt.savefig(measure_out / f"{roi}_residuals.png", dpi=200)
  3473. plt.close()
  3474. except Exception as e:
  3475. logger.warning(f"Residuals plot failed for {measure} {roi}: {e}")
  3476. # --- Coefficients table ---
  3477. coef_table = summary_model.rx2('coefficients').rx2('mean')
  3478. coef_names = list(coef_table.rownames)
  3479. for name in coef_names:
  3480. est = float(coef_table.rx(name, 'Estimate')[0])
  3481. se = float(coef_table.rx(name, 'Std. Error')[0])
  3482. z = float(coef_table.rx(name, 'z value')[0])
  3483. p = float(coef_table.rx(name, 'Pr(>|z|)')[0])
  3484. ci_low = est - CI_Z * se
  3485. ci_high = est + CI_Z * se
  3486. all_results.append({
  3487. 'Measure': measure,
  3488. 'ROI': roi,
  3489. 'Predictor': name,
  3490. 'Estimate': est,
  3491. 'StdErr': se,
  3492. 'z': z,
  3493. 'Pval': p,
  3494. 'CI_low': ci_low,
  3495. 'CI_high': ci_high,
  3496. 'Pseudo_R2': float(summary_model.rx2('pseudo.r.squared')[0]) if 'pseudo.r.squared' in summary_model.names else np.nan,
  3497. 'N_subjects': nsubj
  3498. })
  3499. except Exception as e:
  3500. failures.append({'Measure': measure, 'ROI': roi, 'Reason': f"betareg_failed:{e}", 'N': nsubj})
  3501. continue
  3502. # ---------- Results and corrections ----------
  3503. results_df = pd.DataFrame(all_results)
  3504. fails_df = pd.DataFrame(failures)
  3505. if results_df.empty:
  3506. raise RuntimeError("No model results collected.")
  3507. # --- Keep raw p-values separately for both stages ---
  3508. results_df['Pval_raw_global'] = results_df['Pval'].astype(float)
  3509. results_df['Pval_raw_predictor'] = results_df['Pval'].astype(float)
  3510. # --- Global FDR (all ROIs and measures) but only for group/histopathology ---
  3511. mask_interest = results_df['Predictor'].isin(['group', 'histopathology'])
  3512. pvals_all = results_df.loc[mask_interest, 'Pval_raw_global'].astype(float).values
  3513. # Benjamini-Hochberg (FDR)
  3514. fdr_adj = multipletests(pvals_all, alpha=ALPHA, method='fdr_bh')[1]
  3515. results_df.loc[mask_interest, 'FDR_p_global'] = fdr_adj
  3516. # Bonferroni correction
  3517. bonf_adj = multipletests(pvals_all, alpha=ALPHA, method='bonferroni')[1]
  3518. results_df.loc[mask_interest, 'Bonferroni_p'] = bonf_adj
  3519. # Flags
  3520. results_df.loc[mask_interest, 'significant_FDR_global'] = fdr_adj < ALPHA
  3521. results_df.loc[mask_interest, 'significant_Bonferroni'] = bonf_adj < ALPHA
  3522. # --- Storey q-values (using R's qvalue package) for global correction ---
  3523. try:
  3524. qvalue_pkg = importr('qvalue')
  3525. with localconverter(ro.default_converter + pandas2ri.converter):
  3526. r_pvals = ro.FloatVector(pvals_all)
  3527. qobj = qvalue_pkg.qvalue(r_pvals)
  3528. qvalues = np.array(qobj.rx2('qvalues'))
  3529. results_df.loc[mask_interest, 'qvalue_global'] = qvalues
  3530. results_df.loc[mask_interest, 'significant_qvalue'] = qvalues < ALPHA
  3531. logger.info("Storey q-values successfully computed for group/histopathology only (global correction).")
  3532. except Exception as e:
  3533. logger.warning(f"Storey q-value computation failed for global correction: {e}")
  3534. results_df['qvalue_global'] = np.nan
  3535. results_df['significant_qvalue'] = False
  3536. logger.info("Storey q-values successfully computed and added.")
  3537. except Exception as e:
  3538. logger.warning(f"Storey q-value computation failed: {e}")
  3539. results_df['qvalue_global'] = np.nan
  3540. results_df['significant_qvalue'] = False
  3541. # --- Per-predictor FDR (only for 'group' or 'histopathology') ---
  3542. results_df['FDR_p_within_predictor'] = np.nan
  3543. results_df['qvalue_within_predictor'] = np.nan
  3544. results_df['significant_qvalue_within_predictor'] = False
  3545. predictors_to_correct = ['group', 'histopathology'] # <only group or histopathology
  3546. for (measure, predictor), grp in results_df.groupby(['Measure','Predictor']):
  3547. if predictor not in predictors_to_correct:
  3548. continue # skip all other predictors like age, sex, etc.
  3549. # FDR correction (Benjamini-Hochberg)
  3550. adj_bh = multipletests(grp['Pval_raw_predictor'].values, method='fdr_bh')[1]
  3551. results_df.loc[grp.index, 'FDR_p_within_predictor'] = adj_bh
  3552. # Storey q-value
  3553. try:
  3554. with localconverter(ro.default_converter + pandas2ri.converter):
  3555. r_pvals = ro.FloatVector(grp['Pval_raw_predictor'].values)
  3556. qobj = qvalue_pkg.qvalue(r_pvals)
  3557. qvals = np.array(qobj.rx2('qvalues'))
  3558. results_df.loc[grp.index, 'qvalue_within_predictor'] = qvals
  3559. results_df.loc[grp.index, 'significant_qvalue_within_predictor'] = qvals < ALPHA
  3560. except Exception as e:
  3561. logger.warning(f"Storey q-value failed for predictor {predictor} in {measure}: {e}")
  3562. # --- Save outputs ---
  3563. results_df.to_csv(OUT_ROOT / "all_measures_betareg_results_with_global_corrections.csv", index=False)
  3564. try:
  3565. results_df.to_excel(OUT_ROOT / "all_measures_betareg_results_with_global_corrections.xlsx", index=False)
  3566. except:
  3567. logger.warning("Could not write Excel (check openpyxl)")
  3568. fails_df.to_csv(OUT_ROOT / "model_failures.csv", index=False)
  3569. logger.info(f"Saved results and failures in {OUT_ROOT}")
  3570. # %% [markdown]
  3571. # # Plot for FCDIIb vs FCDIIa histopathology only
  3572. # %%
  3573. import os
  3574. import numpy as np
  3575. import pandas as pd
  3576. import matplotlib.pyplot as plt
  3577. from nilearn import datasets, plotting
  3578. from nibabel.freesurfer.io import read_annot
  3579. # === Directories ===
  3580. data_root = "/Volumes/groups/tohkagroup/Bonn_Epilepsy/betareg_global_results_fcdlla_vs_fcdllb_storey_qvalue_&_benjamini_hochberg_group_only"
  3581. results_csv = os.path.join(data_root, "all_measures_betareg_results_with_global_corrections.csv")
  3582. output_dir = os.path.join(data_root, "brain_maps_fcdlla_vs_fcdllb_zscore_global_by_measure")
  3583. os.makedirs(output_dir, exist_ok=True)
  3584. # === Load results ===
  3585. df = pd.read_csv(results_csv)
  3586. # === Filter significant ROIs (global q < 0.05, exclude intercepts) ===
  3587. sig_df = df[
  3588. (df['qvalue_global'] < 0.05) &
  3589. (~df['Predictor'].str.lower().str.contains("intercept"))
  3590. ]
  3591. if sig_df.empty:
  3592. print(" No significant ROIs found with qvalue_global < 0.05 — plotting all instead.")
  3593. sig_df = df.copy()
  3594. # === Load fsaverage5 + aparc labels ===
  3595. fsaverage = datasets.fetch_surf_fsaverage('fsaverage5')
  3596. labels_left, ctab_left, names_left = read_annot(os.path.join(data_root, "lh.aparc.annot"))
  3597. labels_right, ctab_right, names_right = read_annot(os.path.join(data_root, "rh.aparc.annot"))
  3598. names_left = [n.decode("utf-8").lower() for n in names_left]
  3599. names_right = [n.decode("utf-8").lower() for n in names_right]
  3600. # === Map ROI z-scores to surface ===
  3601. def roi_to_surface(df_pred):
  3602. roi_dict = {roi.lower(): z for roi, z in zip(df_pred['ROI'], df_pred['z'])}
  3603. data_left = np.zeros(len(labels_left))
  3604. data_right = np.zeros(len(labels_right))
  3605. for roi_name, z in roi_dict.items():
  3606. if roi_name in names_left:
  3607. idx = names_left.index(roi_name)
  3608. data_left[labels_left == idx] = z
  3609. if roi_name in names_right:
  3610. idx = names_right.index(roi_name)
  3611. data_right[labels_right == idx] = z
  3612. return data_left, data_right
  3613. # === Plotting function ===
  3614. def save_views(data, hemi, predictor, measure):
  3615. for view in ['lateral', 'medial']:
  3616. fname = os.path.join(output_dir, f"{measure}_{predictor}_{hemi}_{view}.png")
  3617. fig = plt.figure(facecolor='black')
  3618. ax = fig.add_subplot(111, projection='3d')
  3619. plotting.plot_surf_stat_map(
  3620. fsaverage[f'infl_{hemi}'], data,
  3621. hemi=hemi,
  3622. bg_map=fsaverage[f'sulc_{hemi}'],
  3623. cmap='bwr', colorbar=True,
  3624. bg_on_data=True, darkness=0.5, alpha=1.0, axes=ax,
  3625. vmin=-5, vmax=5, view=view
  3626. )
  3627. for child_ax in fig.get_axes():
  3628. child_ax.set_facecolor("black")
  3629. fig.savefig(fname, dpi=300, facecolor='black', bbox_inches='tight')
  3630. plt.close(fig)
  3631. # === Generate plots per measure ===
  3632. measures = ['intensity', 'thickness', 'volume', 'curvature', 'surface_area']
  3633. for measure in measures:
  3634. df_measure = sig_df[sig_df['Measure'].str.lower() == measure.lower()]
  3635. if df_measure.empty:
  3636. print(f" No significant results found for {measure}, skipping.")
  3637. continue
  3638. predictors = df_measure['Predictor'].unique()
  3639. # Step 1: generate surface images
  3640. for predictor in predictors:
  3641. df_pred = df_measure[df_measure['Predictor'] == predictor]
  3642. data_left, data_right = roi_to_surface(df_pred)
  3643. save_views(data_left, 'left', predictor, measure)
  3644. save_views(data_right, 'right', predictor, measure)
  3645. print("Saved all individual predictor brain maps to:", output_dir)
  3646. # Step 2: assemble composite for this measure
  3647. n_preds = len(predictors)
  3648. fig, axes = plt.subplots(n_preds, 4, figsize=(16, 4 * n_preds), facecolor='black')
  3649. if n_preds == 1:
  3650. axes = np.array([axes])
  3651. for i, predictor in enumerate(predictors):
  3652. for j, hemi in enumerate(['left', 'right']):
  3653. for k, view in enumerate(['lateral', 'medial']):
  3654. img_path = os.path.join(output_dir, f"{measure}_{predictor}_{hemi}_{view}.png")
  3655. if os.path.exists(img_path):
  3656. img = plt.imread(img_path)
  3657. axes[i, j*2 + k].imshow(img)
  3658. axes[i, j*2 + k].axis("off")
  3659. axes[i, j*2 + k].set_facecolor('black')
  3660. axes[i, j*2 + k].set_title(f"{predictor} ({hemi} {view})", color='yellow', fontsize=10)
  3661. plt.tight_layout()
  3662. final_fig = os.path.join(output_dir, f"composite_brain_maps_{measure}_global_qvalue.png")
  3663. plt.show()
  3664. plt.savefig(final_fig, dpi=300, bbox_inches='tight', facecolor='black')
  3665. plt.close(fig)
  3666. print(f"Saved composite figure for {measure}: {final_fig}")
  3667. print("\n All measure-specific brain maps saved successfully!")
  3668. # %%
  3669. import pandas as pd
  3670. import numpy as np
  3671. import matplotlib.pyplot as plt
  3672. from matplotlib.colors import TwoSlopeNorm
  3673. import os
  3674. # --- File paths ---
  3675. paths = [
  3676. "/Volumes/groups/tohkagroup/Bonn_Epilepsy/study1_fcdll_asymmetry/results/betareg_global_results_fcd_vs_hc_storey_qvalue_benjamini_hochberg_group_only/all_measures_betareg_results_with_global_corrections.xlsx",
  3677. "/Volumes/groups/tohkagroup/Bonn_Epilepsy/study1_fcdll_asymmetry/results/betareg_global_results_fcdsubtypes_storey_qvalue_benjamini_hochberg_group_only/all_measures_betareg_results_with_global_corrections.xlsx",
  3678. "/Volumes/groups/tohkagroup/Bonn_Epilepsy/study1_fcdll_asymmetry/results/betareg_global_results_fcdlla_vs_fcdllb_storey_qvalue_benjamini_hochberg_group_only/all_measures_betareg_results_with_global_corrections.xlsx"
  3679. ]
  3680. # --- Corrected significance threshold ---
  3681. threshold = 3.12
  3682. # --- Load corrected z-values from the "z" column ---
  3683. z_values = []
  3684. for path in paths:
  3685. if os.path.exists(path):
  3686. df = pd.read_excel(path)
  3687. df.columns = df.columns.str.strip()
  3688. df["z"] = pd.to_numeric(df["z"].astype(str).str.replace(",", "."), errors="coerce")
  3689. z_values.extend(df["z"].dropna().values)
  3690. else:
  3691. print("File not found:", path)
  3692. z_values = np.array(z_values)
  3693. if len(z_values) == 0:
  3694. raise ValueError("No z-values loaded — check paths and column names")
  3695. # --- (based on your corrected Z distribution) ---
  3696. vmin, vmax = -4.92, 3.54
  3697. norm = TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
  3698. cmap = plt.cm.coolwarm
  3699. cmap.set_bad(color="lightgrey") # grey non-significant
  3700. # Mask any |Z| < corrected threshold
  3701. masked = np.ma.masked_where(np.abs(z_values) < threshold, z_values)
  3702. # --- Plot colorbar only ---
  3703. fig = plt.figure(figsize=(10, 1.0))
  3704. ax = fig.add_axes([0.1, 0.4, 0.8, 0.3])
  3705. cb = plt.colorbar(
  3706. plt.cm.ScalarMappable(norm=norm, cmap=cmap),
  3707. cax=ax,
  3708. orientation="horizontal"
  3709. )
  3710. # labeling
  3711. cb.set_label("Corrected Z-values (|Z| ≥ 3.12)", fontsize=11)
  3712. cb.ax.tick_params(labelsize=10)
  3713. # Threshold markers
  3714. for x in [-threshold, threshold]:
  3715. cb.ax.axvline(x, linestyle="--", color="black", linewidth=1)
  3716. plt.show()
  3717. # %% [markdown]
  3718. # # Box plots
  3719. # %%
  3720. pip install statannotations
  3721. # %% [markdown]
  3722. # ## Unsigned Intensity Asymmetry
  3723. # %%
  3724. import pandas as pd
  3725. import os
  3726. from glob import glob
  3727. from scipy.stats import kruskal
  3728. from statsmodels.stats.multicomp import pairwise_tukeyhsd
  3729. import matplotlib.pyplot as plt
  3730. import seaborn as sns
  3731. from statannotations.Annotator import Annotator
  3732. from matplotlib.patches import Patch
  3733. # === CONFIGURATION ===
  3734. show_as_stars = True # Set to False in the case of showing numeric p-values
  3735. base_dir = "/Volumes/groups/tohkagroup/Bonn_Epilepsy"
  3736. group_dirs = {
  3737. 'hc': os.path.join(base_dir, 'asymmetry_hc'),
  3738. 'fcdlla': os.path.join(base_dir, 'asymmetry_fcdlla'),
  3739. 'fcdllb': os.path.join(base_dir, 'asymmetry_fcdllb'),
  3740. 'fcdna': os.path.join(base_dir, 'asymmetry_fcdna'),
  3741. }
  3742. output_dir = os.path.join(base_dir, 'intensity_asymmetry_results')
  3743. os.makedirs(output_dir, exist_ok=True)
  3744. # === Load data and compute Unsigned Asymmetry ===
  3745. data_list = []
  3746. for group, path in group_dirs.items():
  3747. files = glob(os.path.join(path, '*.csv'))
  3748. for file in files:
  3749. df = pd.read_csv(file)
  3750. df['Subject_ID'] = os.path.basename(file).split('_')[0]
  3751. df['Group'] = group
  3752. # Compute unsigned asymmetry
  3753. df['Unsigned_Asymmetry'] = df['Asymmetry_Index'].abs()
  3754. data_list.append(df)
  3755. all_data = pd.concat(data_list, ignore_index=True)
  3756. # === Kruskal-Wallis ===
  3757. results = []
  3758. for struct in all_data['StructName'].unique():
  3759. subset = all_data[all_data['StructName'] == struct]
  3760. grouped = [group['Unsigned_Asymmetry'].values for name, group in subset.groupby('Group')]
  3761. if len(grouped) == len(group_dirs):
  3762. stat, p = kruskal(*grouped)
  3763. results.append({'StructName': struct, 'H-stat': stat, 'p-value': p})
  3764. results_df = pd.DataFrame(results).sort_values('p-value')
  3765. results_df['FDR'] = (results_df['p-value'] * len(results_df)).clip(upper=1.0)
  3766. # Save Kruskal-Wallis results
  3767. results_df.to_csv(os.path.join(output_dir, 'kruskal_unsigned_asymmetry.csv'), index=False)
  3768. # === Tukey HSD on top structure ===
  3769. top_struct = results_df.iloc[0]['StructName']
  3770. posthoc_data = all_data[all_data['StructName'] == top_struct]
  3771. tukey = pairwise_tukeyhsd(posthoc_data['Unsigned_Asymmetry'], posthoc_data['Group'])
  3772. # Save Tukey HSD results
  3773. tukey_df = pd.DataFrame(data=tukey._results_table.data[1:], columns=tukey._results_table.data[0])
  3774. tukey_df.to_csv(os.path.join(output_dir, f'tukey_{top_struct}_unsigned_asymmetry.csv'), index=False)
  3775. print("Top Structure:", top_struct)
  3776. print(tukey.summary())
  3777. # === Plot with annotations ===
  3778. plt.figure(figsize=(10, 6))
  3779. ax = sns.boxplot(data=posthoc_data, x='Group', y='Unsigned_Asymmetry', palette='Set2')
  3780. sns.stripplot(data=posthoc_data, x='Group', y='Unsigned_Asymmetry', color='black', alpha=0.4, jitter=True)
  3781. # Define pairs for annotation
  3782. pairs = [
  3783. ("hc", "fcdlla"),
  3784. ("hc", "fcdllb"),
  3785. ("hc", "fcdna"),
  3786. ("fcdlla", "fcdllb"),
  3787. ("fcdlla", "fcdna"),
  3788. ("fcdllb", "fcdna"),
  3789. ]
  3790. # Build p-value dictionary from Tukey HSD summary
  3791. pval_dict = {}
  3792. for _, row in tukey_df.iterrows():
  3793. pair = tuple(sorted([row['group1'], row['group2']]))
  3794. pval_dict[pair] = float(row['p-adj'])
  3795. # Prepare p-values for annotation
  3796. annotated_pvals = [(pair, pval_dict.get(tuple(sorted(pair)), 1.0)) for pair in pairs]
  3797. pvalues = [pval for pair, pval in annotated_pvals]
  3798. # Annotate
  3799. annotator = Annotator(ax, pairs, data=posthoc_data, x='Group', y='Unsigned_Asymmetry')
  3800. annotator.set_pvalues_and_annotate(pvalues)
  3801. # Title and layout
  3802. plt.title(f"Unsigned Asymmetry Comparison - {top_struct}", fontsize=14)
  3803. plt.grid(True, linestyle='--', alpha=0.5)
  3804. # High-quality legend for significance
  3805. legend_labels = ["* p < 0.05", "** p < 0.01", "*** p < 0.001", "ns (p ≥ 0.05)"]
  3806. legend_handles = [Patch(facecolor='none', edgecolor='none', label=lbl) for lbl in legend_labels]
  3807. plt.legend(handles=legend_handles, title="Significance", loc='center left',
  3808. bbox_to_anchor=(1.02, 0.5), borderaxespad=0.5, frameon=False)
  3809. plt.tight_layout()
  3810. plt.savefig(os.path.join(output_dir, f'unsigned_asymmetry_{top_struct}.png'), dpi=300)
  3811. plt.show()
  3812. # %% [markdown]
  3813. # ## Unsigned Thickness Asymmetry
  3814. # %%
  3815. import os
  3816. import pandas as pd
  3817. import scipy.stats as stats
  3818. from statsmodels.stats.multitest import multipletests
  3819. from statsmodels.stats.multicomp import pairwise_tukeyhsd
  3820. import seaborn as sns
  3821. import matplotlib.pyplot as plt
  3822. from matplotlib.patches import Patch
  3823. from statannotations.Annotator import Annotator
  3824. # Define the output directory
  3825. output_dir = '/Volumes/groups/tohkagroup/Bonn_Epilepsy/thick_asymetry_result'
  3826. if not os.path.exists(output_dir):
  3827. os.makedirs(output_dir)
  3828. # Group directories
  3829. base_dirs = {
  3830. 'hc': '/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_hc',
  3831. 'fcdlla': '/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdlla',
  3832. 'fcdllb': '/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdllb',
  3833. 'fcdna': '/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdna',
  3834. }
  3835. all_data = []
  3836. print("Searching for valid asymmetry files...")
  3837. for group, base_path in base_dirs.items():
  3838. for subj_name in os.listdir(base_path):
  3839. if not subj_name.startswith('sub-'):
  3840. continue
  3841. subject_path = os.path.join(base_path, subj_name)
  3842. stats_folder = 'stats' if group == 'hc' else 'stats_cleaned'
  3843. stats_path = os.path.join(subject_path, stats_folder)
  3844. if not os.path.isdir(stats_path):
  3845. continue
  3846. # Find correct *_ThickAvg_asymmetry.csv file
  3847. for file in os.listdir(stats_path):
  3848. if file.endswith('_ThickAvg_asymmetry.csv') and file.startswith(subj_name):
  3849. full_path = os.path.join(stats_path, file)
  3850. try:
  3851. df = pd.read_csv(full_path)
  3852. df['Subject'] = subj_name
  3853. df['Group'] = group
  3854. all_data.append(df[['Subject', 'Group', 'StructName', 'Asymmetry_Index']])
  3855. except Exception as e:
  3856. print(f"Error reading {full_path}: {e}")
  3857. break # Use only the first matching file
  3858. # Combine into a single dataframe
  3859. asym_df = pd.concat(all_data, ignore_index=True)
  3860. # Check for missing values in the Asymmetry_Index column
  3861. asym_df = asym_df.dropna(subset=['Asymmetry_Index'])
  3862. # Convert to absolute asymmetry (unsigned)
  3863. asym_df['Asymmetry_Index'] = asym_df['Asymmetry_Index'].abs()
  3864. print(f"\nLoaded data from {asym_df['Subject'].nunique()} unique subjects.")
  3865. print(f"Total brain structures: {asym_df['StructName'].nunique()}.\n")
  3866. # Function to calculate Cohen's d (effect size)
  3867. def cohen_d(group1, group2):
  3868. pooled_std = (((len(group1) - 1) * group1.std()**2 + (len(group2) - 1) * group2.std()**2) /
  3869. (len(group1) + len(group2) - 2))**0.5
  3870. return (group1.mean() - group2.mean()) / pooled_std
  3871. # === Kruskal-Wallis Test (Non-parametric ANOVA) ===
  3872. kruskal_results = []
  3873. for struct in asym_df['StructName'].unique():
  3874. subset = asym_df[asym_df['StructName'] == struct]
  3875. grouped = [group['Asymmetry_Index'].values for name, group in subset.groupby('Group')]
  3876. if len(grouped) == 4: # All groups present
  3877. stat, p = stats.kruskal(*grouped)
  3878. kruskal_results.append({'StructName': struct, 'H-stat': stat, 'p-value': p})
  3879. kruskal_df = pd.DataFrame(kruskal_results)
  3880. kruskal_df['FDR'] = multipletests(kruskal_df['p-value'], method='fdr_bh')[1]
  3881. kruskal_df = kruskal_df.sort_values('p-value')
  3882. # Save Kruskal-Wallis results if needed
  3883. kruskal_csv_path = os.path.join(output_dir, 'kruskal_results.csv')
  3884. kruskal_df.to_csv(kruskal_csv_path, index=False)
  3885. print(f"Kruskal-Wallis results saved to: {kruskal_csv_path}")
  3886. # === Pairwise t-tests: HC vs each FCD group ===
  3887. t_test_results = []
  3888. for struct in asym_df['StructName'].unique():
  3889. struct_data = asym_df[asym_df['StructName'] == struct]
  3890. hc_values = struct_data[struct_data['Group'] == 'hc']['Asymmetry_Index']
  3891. for fcd_group in ['fcdlla', 'fcdllb', 'fcdna']:
  3892. fcd_values = struct_data[struct_data['Group'] == fcd_group]['Asymmetry_Index']
  3893. if len(hc_values) > 1 and len(fcd_values) > 1:
  3894. t_stat, p_val = stats.ttest_ind(hc_values, fcd_values, equal_var=False) # Welch's t-test
  3895. effect_size = cohen_d(hc_values, fcd_values) # Cohen's d for effect size
  3896. t_test_results.append({
  3897. 'StructName': struct,
  3898. 'Comparison': f'hc vs {fcd_group}',
  3899. 't-stat': t_stat,
  3900. 'p-value': p_val,
  3901. 'cohen_d': effect_size
  3902. })
  3903. t_test_df = pd.DataFrame(t_test_results)
  3904. t_test_df['FDR'] = multipletests(t_test_df['p-value'], method='fdr_bh')[1]
  3905. t_test_df = t_test_df.sort_values('p-value')
  3906. # Save t-test results if needed
  3907. t_test_csv_path = os.path.join(output_dir, 't_test_results.csv')
  3908. t_test_df.to_csv(t_test_csv_path, index=False)
  3909. print(f"T-test results saved to: {t_test_csv_path}")
  3910. # === Optional: Tukey's HSD for top structure ===
  3911. top_struct = kruskal_df.iloc[0]['StructName']
  3912. posthoc_data = asym_df[asym_df['StructName'] == top_struct]
  3913. tukey = pairwise_tukeyhsd(posthoc_data['Asymmetry_Index'], posthoc_data['Group'])
  3914. print(f"Top structure by Kruskal-Wallis: {top_struct}")
  3915. print(tukey)
  3916. # === Optional: Boxplot visualization ===
  3917. plt.figure(figsize=(10, 6))
  3918. ax = sns.boxplot(data=posthoc_data, x='Group', y='Asymmetry_Index', palette='Set2')
  3919. sns.stripplot(data=posthoc_data, x='Group', y='Asymmetry_Index', color='black', alpha=0.3, jitter=True)
  3920. # Add statistical annotations
  3921. pairs = [
  3922. ("hc", "fcdlla"),
  3923. ("hc", "fcdllb"),
  3924. ("hc", "fcdna"),
  3925. ("fcdlla", "fcdllb"),
  3926. ("fcdlla", "fcdna"),
  3927. ("fcdllb", "fcdna")
  3928. ]
  3929. # Build p-value dictionary from Tukey HSD summary
  3930. tukey_df = pd.DataFrame(data=tukey._results_table.data[1:], columns=tukey._results_table.data[0])
  3931. pval_dict = {}
  3932. for _, row in tukey_df.iterrows():
  3933. pair = tuple(sorted([row['group1'], row['group2']]))
  3934. pval_dict[pair] = float(row['p-adj'])
  3935. # Annotate p-values with significance stars
  3936. annotated_pvals = [(pair, pval_dict.get(tuple(sorted(pair)), 1.0)) for pair in pairs]
  3937. pvalues = [pval for pair, pval in annotated_pvals]
  3938. # Annotator object for stars
  3939. annotator = Annotator(ax, pairs, data=posthoc_data, x='Group', y='Asymmetry_Index')
  3940. annotator.set_pvalues_and_annotate(pvalues) # Add stars
  3941. # Title and grid
  3942. plt.title(f"Gray Matter Thickness Asymmetry Index Comparison of {top_struct}")
  3943. plt.grid(True)
  3944. plt.tight_layout()
  3945. # Save the plot as PNG
  3946. plot_filename = f"{top_struct}_asymmetry_plot.png"
  3947. plt.savefig(os.path.join(output_dir, plot_filename))
  3948. # High-quality legend for significance levels
  3949. legend_labels = [
  3950. "* p < 0.05",
  3951. "** p < 0.01",
  3952. "*** p < 0.001",
  3953. "ns (p ≥ 0.05)"
  3954. ]
  3955. legend_handles = [Patch(facecolor='none', edgecolor='none', label=lbl) for lbl in legend_labels]
  3956. plt.legend(handles=legend_handles, title="Significance", loc='center left',
  3957. bbox_to_anchor=(1.02, 0.5), borderaxespad=0.5, frameon=False)
  3958. # Display the plot
  3959. plt.show()
  3960. plt.close()
  3961. print(f"Plot saved to: {os.path.join(output_dir, plot_filename)}")
  3962. # === Display the content of the CSV files ===
  3963. print("\nContent of the Kruskal-Wallis results:")
  3964. print(kruskal_df.head())
  3965. print("\nContent of the t-test results:")
  3966. print(t_test_df.head())
  3967. print("\nDone.")
  3968. # %% [markdown]
  3969. # ## Unsigned Gray Volume Asymmetry
  3970. # %%
  3971. import os
  3972. import pandas as pd
  3973. import scipy.stats as stats
  3974. from statsmodels.stats.multitest import multipletests
  3975. from statsmodels.stats.multicomp import pairwise_tukeyhsd
  3976. import seaborn as sns
  3977. import matplotlib.pyplot as plt
  3978. from matplotlib.patches import Patch
  3979. from statannotations.Annotator import Annotator
  3980. # Define the output directory
  3981. output_dir = '/Volumes/groups/tohkagroup/Bonn_Epilepsy/gvol_asymetry_result'
  3982. if not os.path.exists(output_dir):
  3983. os.makedirs(output_dir)
  3984. # Group directories
  3985. base_dirs = {
  3986. 'hc': '/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_hc',
  3987. 'fcdlla': '/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdlla',
  3988. 'fcdllb': '/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdllb',
  3989. 'fcdna': '/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdna',
  3990. }
  3991. all_data = []
  3992. print("Searching for valid asymmetry files...")
  3993. for group, base_path in base_dirs.items():
  3994. for subj_name in os.listdir(base_path):
  3995. if not subj_name.startswith('sub-'):
  3996. continue
  3997. subject_path = os.path.join(base_path, subj_name)
  3998. stats_folder = 'stats' if group == 'hc' else 'stats_cleaned'
  3999. stats_path = os.path.join(subject_path, stats_folder)
  4000. if not os.path.isdir(stats_path):
  4001. continue
  4002. # Find correct *_GrayVol_asymmetry.csv file
  4003. for file in os.listdir(stats_path):
  4004. if file.endswith('_GrayVol_asymmetry.csv') and file.startswith(subj_name):
  4005. full_path = os.path.join(stats_path, file)
  4006. try:
  4007. df = pd.read_csv(full_path)
  4008. df['Subject'] = subj_name
  4009. df['Group'] = group
  4010. all_data.append(df[['Subject', 'Group', 'StructName', 'Asymmetry_Index']])
  4011. except Exception as e:
  4012. print(f"Error reading {full_path}: {e}")
  4013. break # Use only the first matching file
  4014. # Combine into a single dataframe
  4015. asym_df = pd.concat(all_data, ignore_index=True)
  4016. # Check for missing values in the Asymmetry_Index column
  4017. asym_df = asym_df.dropna(subset=['Asymmetry_Index'])
  4018. # Convert to absolute asymmetry (unsigned)
  4019. asym_df['Asymmetry_Index'] = asym_df['Asymmetry_Index'].abs()
  4020. print(f"\nLoaded data from {asym_df['Subject'].nunique()} unique subjects.")
  4021. print(f"Total brain structures: {asym_df['StructName'].nunique()}.\n")
  4022. # Function to calculate Cohen's d (effect size)
  4023. def cohen_d(group1, group2):
  4024. pooled_std = (((len(group1) - 1) * group1.std()**2 + (len(group2) - 1) * group2.std()**2) /
  4025. (len(group1) + len(group2) - 2))**0.5
  4026. return (group1.mean() - group2.mean()) / pooled_std
  4027. # === Kruskal-Wallis Test (Non-parametric ANOVA) ===
  4028. kruskal_results = []
  4029. for struct in asym_df['StructName'].unique():
  4030. subset = asym_df[asym_df['StructName'] == struct]
  4031. grouped = [group['Asymmetry_Index'].values for name, group in subset.groupby('Group')]
  4032. if len(grouped) == 4: # All groups present
  4033. stat, p = stats.kruskal(*grouped)
  4034. kruskal_results.append({'StructName': struct, 'H-stat': stat, 'p-value': p})
  4035. kruskal_df = pd.DataFrame(kruskal_results)
  4036. kruskal_df['FDR'] = multipletests(kruskal_df['p-value'], method='fdr_bh')[1]
  4037. kruskal_df = kruskal_df.sort_values('p-value')
  4038. # Save Kruskal-Wallis results if needed
  4039. kruskal_csv_path = os.path.join(output_dir, 'kruskal_results.csv')
  4040. kruskal_df.to_csv(kruskal_csv_path, index=False)
  4041. print(f"Kruskal-Wallis results saved to: {kruskal_csv_path}")
  4042. # === Pairwise t-tests: HC vs each FCD group ===
  4043. t_test_results = []
  4044. for struct in asym_df['StructName'].unique():
  4045. struct_data = asym_df[asym_df['StructName'] == struct]
  4046. hc_values = struct_data[struct_data['Group'] == 'hc']['Asymmetry_Index']
  4047. for fcd_group in ['fcdlla', 'fcdllb', 'fcdna']:
  4048. fcd_values = struct_data[struct_data['Group'] == fcd_group]['Asymmetry_Index']
  4049. if len(hc_values) > 1 and len(fcd_values) > 1:
  4050. t_stat, p_val = stats.ttest_ind(hc_values, fcd_values, equal_var=False) # Welch's t-test
  4051. effect_size = cohen_d(hc_values, fcd_values) # Cohen's d for effect size
  4052. t_test_results.append({
  4053. 'StructName': struct,
  4054. 'Comparison': f'hc vs {fcd_group}',
  4055. 't-stat': t_stat,
  4056. 'p-value': p_val,
  4057. 'cohen_d': effect_size
  4058. })
  4059. t_test_df = pd.DataFrame(t_test_results)
  4060. t_test_df['FDR'] = multipletests(t_test_df['p-value'], method='fdr_bh')[1]
  4061. t_test_df = t_test_df.sort_values('p-value')
  4062. # Save t-test results if needed
  4063. t_test_csv_path = os.path.join(output_dir, 't_test_results.csv')
  4064. t_test_df.to_csv(t_test_csv_path, index=False)
  4065. print(f"T-test results saved to: {t_test_csv_path}")
  4066. # === Optional: Tukey's HSD for top structure ===
  4067. top_struct = kruskal_df.iloc[0]['StructName']
  4068. posthoc_data = asym_df[asym_df['StructName'] == top_struct]
  4069. tukey = pairwise_tukeyhsd(posthoc_data['Asymmetry_Index'], posthoc_data['Group'])
  4070. print(f"Top structure by Kruskal-Wallis: {top_struct}")
  4071. print(tukey)
  4072. # === Optional: Boxplot visualization ===
  4073. plt.figure(figsize=(10, 6))
  4074. ax = sns.boxplot(data=posthoc_data, x='Group', y='Asymmetry_Index', palette='Set2')
  4075. sns.stripplot(data=posthoc_data, x='Group', y='Asymmetry_Index', color='black', alpha=0.3, jitter=True)
  4076. # Add statistical annotations
  4077. pairs = [
  4078. ("hc", "fcdlla"),
  4079. ("hc", "fcdllb"),
  4080. ("hc", "fcdna"),
  4081. ("fcdlla", "fcdllb"),
  4082. ("fcdlla", "fcdna"),
  4083. ("fcdllb", "fcdna")
  4084. ]
  4085. # Build p-value dictionary from Tukey HSD summary
  4086. tukey_df = pd.DataFrame(data=tukey._results_table.data[1:], columns=tukey._results_table.data[0])
  4087. pval_dict = {}
  4088. for _, row in tukey_df.iterrows():
  4089. pair = tuple(sorted([row['group1'], row['group2']]))
  4090. pval_dict[pair] = float(row['p-adj'])
  4091. # Annotate p-values with significance stars
  4092. annotated_pvals = [(pair, pval_dict.get(tuple(sorted(pair)), 1.0)) for pair in pairs]
  4093. pvalues = [pval for pair, pval in annotated_pvals]
  4094. # Annotator object for stars
  4095. annotator = Annotator(ax, pairs, data=posthoc_data, x='Group', y='Asymmetry_Index')
  4096. annotator.set_pvalues_and_annotate(pvalues) # Add stars
  4097. # Title and grid
  4098. plt.title(f"Gray Matter Volume Asymmetry Index Comparison of {top_struct}")
  4099. plt.grid(True)
  4100. plt.tight_layout()
  4101. # Save the plot as PNG
  4102. plot_filename = f"{top_struct}_asymmetry_plot.png"
  4103. plt.savefig(os.path.join(output_dir, plot_filename))
  4104. # High-quality legend for significance levels
  4105. legend_labels = [
  4106. "* p < 0.05",
  4107. "** p < 0.01",
  4108. "*** p < 0.001",
  4109. "ns (p ≥ 0.05)"
  4110. ]
  4111. legend_handles = [Patch(facecolor='none', edgecolor='none', label=lbl) for lbl in legend_labels]
  4112. plt.legend(handles=legend_handles, title="Significance", loc='center left',
  4113. bbox_to_anchor=(1.02, 0.5), borderaxespad=0.5, frameon=False)
  4114. # Display the plot
  4115. plt.show()
  4116. plt.close()
  4117. print(f"Plot saved to: {os.path.join(output_dir, plot_filename)}")
  4118. # === Display the content of the CSV files ===
  4119. print("\nContent of the Kruskal-Wallis results:")
  4120. print(kruskal_df.head())
  4121. print("\nContent of the t-test results:")
  4122. print(t_test_df.head())
  4123. print("\nDone.")
  4124. # %% [markdown]
  4125. # ## Unsigned Curvature Asymmetry
  4126. # %%
  4127. import os
  4128. import pandas as pd
  4129. import scipy.stats as stats
  4130. from statsmodels.stats.multitest import multipletests
  4131. from statsmodels.stats.multicomp import pairwise_tukeyhsd
  4132. import seaborn as sns
  4133. import matplotlib.pyplot as plt
  4134. from matplotlib.patches import Patch
  4135. from statannotations.Annotator import Annotator
  4136. # Define the output directory
  4137. output_dir = '/Volumes/groups/tohkagroup/Bonn_Epilepsy/curv_asymetry_result'
  4138. if not os.path.exists(output_dir):
  4139. os.makedirs(output_dir)
  4140. # Group directories
  4141. base_dirs = {
  4142. 'hc': '/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_hc',
  4143. 'fcdlla': '/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdlla',
  4144. 'fcdllb': '/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdllb',
  4145. 'fcdna': '/Volumes/groups/tohkagroup/Bonn_Epilepsy/asymmetry_fcdna',
  4146. }
  4147. all_data = []
  4148. print("Searching for valid asymmetry files...")
  4149. for group, base_path in base_dirs.items():
  4150. for subj_name in os.listdir(base_path):
  4151. if not subj_name.startswith('sub-'):
  4152. continue
  4153. subject_path = os.path.join(base_path, subj_name)
  4154. stats_folder = 'stats' if group == 'hc' else 'stats_cleaned'
  4155. stats_path = os.path.join(subject_path, stats_folder)
  4156. if not os.path.isdir(stats_path):
  4157. continue
  4158. # Find correct *_MeanCurv_asymmetry.csv file
  4159. for file in os.listdir(stats_path):
  4160. if file.endswith('_MeanCurv_asymmetry.csv') and file.startswith(subj_name):
  4161. full_path = os.path.join(stats_path, file)
  4162. try:
  4163. df = pd.read_csv(full_path)
  4164. df['Subject'] = subj_name
  4165. df['Group'] = group
  4166. all_data.append(df[['Subject', 'Group', 'StructName', 'Asymmetry_Index']])
  4167. except Exception as e:
  4168. print(f"Error reading {full_path}: {e}")
  4169. break # Use only the first matching file
  4170. # Combine into a single dataframe
  4171. asym_df = pd.concat(all_data, ignore_index=True)
  4172. # Check for missing values in the Asymmetry_Index column
  4173. asym_df = asym_df.dropna(subset=['Asymmetry_Index'])
  4174. # Convert to absolute asymmetry (unsigned)
  4175. asym_df['Asymmetry_Index'] = asym_df['Asymmetry_Index'].abs()
  4176. print(f"\nLoaded data from {asym_df['Subject'].nunique()} unique subjects.")
  4177. print(f"Total brain structures: {asym_df['StructName'].nunique()}.\n")
  4178. # Function to calculate Cohen's d (effect size)
  4179. def cohen_d(group1, group2):
  4180. pooled_std = (((len(group1) - 1) * group1.std()**2 + (len(group2) - 1) * group2.std()**2) /
  4181. (len(group1) + len(group2) - 2))**0.5
  4182. return (group1.mean() - group2.mean()) / pooled_std
  4183. # === Kruskal-Wallis Test (Non-parametric ANOVA) ===
  4184. kruskal_results = []
  4185. for struct in asym_df['StructName'].unique():
  4186. subset = asym_df[asym_df['StructName'] == struct]
  4187. grouped = [group['Asymmetry_Index'].values for name, group in subset.groupby('Group')]
  4188. if len(grouped) == 4: # All groups present
  4189. stat, p = stats.kruskal(*grouped)
  4190. kruskal_results.append({'StructName': struct, 'H-stat': stat, 'p-value': p})
  4191. kruskal_df = pd.DataFrame(kruskal_results)
  4192. kruskal_df['FDR'] = multipletests(kruskal_df['p-value'], method='fdr_bh')[1]
  4193. kruskal_df = kruskal_df.sort_values('p-value')
  4194. # Save Kruskal-Wallis results if needed
  4195. kruskal_csv_path = os.path.join(output_dir, 'kruskal_results.csv')
  4196. kruskal_df.to_csv(kruskal_csv_path, index=False)
  4197. print(f"Kruskal-Wallis results saved to: {kruskal_csv_path}")
  4198. # === Pairwise t-tests: HC vs each FCD group ===
  4199. t_test_results = []
  4200. for struct in asym_df['StructName'].unique():
  4201. struct_data = asym_df[asym_df['StructName'] == struct]
  4202. hc_values = struct_data[struct_data['Group'] == 'hc']['Asymmetry_Index']
  4203. for fcd_group in ['fcdlla', 'fcdllb', 'fcdna']:
  4204. fcd_values = struct_data[struct_data['Group'] == fcd_group]['Asymmetry_Index']
  4205. if len(hc_values) > 1 and len(fcd_values) > 1:
  4206. t_stat, p_val = stats.ttest_ind(hc_values, fcd_values, equal_var=False) # Welch's t-test
  4207. effect_size = cohen_d(hc_values, fcd_values) # Cohen's d for effect size
  4208. t_test_results.append({
  4209. 'StructName': struct,
  4210. 'Comparison': f'hc vs {fcd_group}',
  4211. 't-stat': t_stat,
  4212. 'p-value': p_val,
  4213. 'cohen_d': effect_size
  4214. })
  4215. t_test_df = pd.DataFrame(t_test_results)
  4216. t_test_df['FDR'] = multipletests(t_test_df['p-value'], method='fdr_bh')[1]
  4217. t_test_df = t_test_df.sort_values('p-value')
  4218. # Save t-test results if needed
  4219. t_test_csv_path = os.path.join(output_dir, 't_test_results.csv')
  4220. t_test_df.to_csv(t_test_csv_path, index=False)
  4221. print(f"T-test results saved to: {t_test_csv_path}")
  4222. # === Optional: Tukey's HSD for top structure ===
  4223. top_struct = kruskal_df.iloc[0]['StructName']
  4224. posthoc_data = asym_df[asym_df['StructName'] == top_struct]
  4225. tukey = pairwise_tukeyhsd(posthoc_data['Asymmetry_Index'], posthoc_data['Group'])
  4226. print(f"Top structure by Kruskal-Wallis: {top_struct}")
  4227. print(tukey)
  4228. # === Optional: Boxplot visualization ===
  4229. plt.figure(figsize=(10, 6))
  4230. ax = sns.boxplot(data=posthoc_data, x='Group', y='Asymmetry_Index', palette='Set2')
  4231. sns.stripplot(data=posthoc_data, x='Group', y='Asymmetry

Bonn_visualization.ipynb at commit bcc3292, under MIT · at the source

Overview

  1. Department of Neurology, Institute of Clinical Medicine University of Eastern Finland Kuopio Finland
  2. A.I. Virtanen Institute for Molecular Sciences University of Eastern Finland Kuopio Finland
  3. Kuopio Epilepsy Center Kuopio University Hospital, Full Member of ERN EpiCARE Kuopio Finland
Journal: Epilepsia open, article 10.1002/epi4.70336
Dates: received 12 March 2026; accepted 27 July 2026; published online 5 September 2026; in print September 2026
Type: Research article · Language: English
License: CC BY-NC-ND
Identifiers: DOI 10.1002/epi4.70336 · PMID 42700155 · PMCID PMC13545978 · OpenAlex W7208828703
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: structural MRI / diffusion (modality), human (organism), epilepsy (population), clinical / translational (subfield)
Methods: Connectivity, Statistics, fMRI & imaging, Preprocessing
Keywords: cortex, epilepsy, focal cortical dysplasia, interhemispheric asymmetry
Topic: Epilepsy research and treatment (Psychiatry and Mental health, Medicine), according to OpenAlex
Funding: Research Council of Finland (358944); Jane ja Aatos Erkon Säätiö; European Union’s Horizon 2020 (101034307)
Citations: not cited yet (Europe PMC); 44 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.

faezeheidari/FCD_Asymmetry

License: MIT
State: the link answers, verified on 26 September 2026
Evidence: files inventoried
Commit: bcc32922fc78cf2afd6990a0e0a54a7b9520eab8, 6 March 2026
Languages: Jupyter (1)
Size: 7 files, 1 script
Software Heritage: not archived
Found in: “CODE AVAILABILITY STATEMENT”
Holds: README, license file, 1 notebook
Not found: CITATION.cff, environment file, tests, continuous integration, documentation
Tools: Matplotlib (1 file), NiBabel (1 file), Nilearn (1 file), NumPy (1 file), pandas (1 file), rpy2 (1 file), scikit-learn (1 file), SciPy (1 file), seaborn (1 file), statannotations (1 file), statsmodels (1 file)
Availability: 1 check, the latest on 26 September 2026: the link answers
  • 26 September 2026: the link answers
3 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.1002/epi4.70336.

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

Datasets cited

Data availability statement

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

Read it in the paper: doi.org/10.1002/epi4.70336.

Versions

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

Version 1, 27 September 2026: the first record

Recorded: type, language, journal, pages, dates, 4 authors, 4 keywords, 3 funders, 41 references.

Cite

This paper

Heidari, F., Torkamani‐Azar, M., Tohka, J., & Kälviäinen, R. (2026). Quantifying extra-lesional interhemispheric cortical asymmetry in focal cortical dysplasia type II. Epilepsia open, 10.1002/epi4.70336. https://doi.org/10.1002/epi4.70336

BibTeX

@article{heidari2026quantifying,
author = {Heidari, Faezeh and Torkamani‐Azar, Mastaneh and Tohka, Jussi and Kälviäinen, Reetta},
title = {{Quantifying extra-lesional interhemispheric cortical asymmetry in focal cortical dysplasia type II}},
journal = {Epilepsia open},
year = {2026},
month = sep,
pages = {10.1002/epi4.70336},
publisher = {Wiley},
issn = {2470-9239},
doi = {10.1002/epi4.70336},
url = {https://doi.org/10.1002/epi4.70336},
pmid = {42700155},
pmcid = {PMC13545978}
}

RIS

TY - JOUR
AU - Heidari, Faezeh
AU - Torkamani‐Azar, Mastaneh
AU - Tohka, Jussi
AU - Kälviäinen, Reetta
TI - Quantifying extra-lesional interhemispheric cortical asymmetry in focal cortical dysplasia type II
T2 - Epilepsia open
J2 - Epilepsia Open
PY - 2026
DA - 2026/09/05
SP - 10.1002/epi4.70336
SN - 2470-9239
PB - Wiley
DO - 10.1002/epi4.70336
UR - https://doi.org/10.1002/epi4.70336
LA - en
ER -

CSL-JSON

{
"id": "10.1002/epi4.70336",
"type": "article-journal",
"title": "Quantifying extra-lesional interhemispheric cortical asymmetry in focal cortical dysplasia type II",
"container-title": "Epilepsia open",
"author": [
{
"family": "Heidari",
"given": "Faezeh"
},
{
"family": "Torkamani‐Azar",
"given": "Mastaneh"
},
{
"family": "Tohka",
"given": "Jussi"
},
{
"family": "Kälviäinen",
"given": "Reetta"
}
],
"container-title-short": "Epilepsia Open",
"page": "10.1002/epi4.70336",
"DOI": "10.1002/epi4.70336",
"PMID": "42700155",
"PMCID": "PMC13545978",
"ISSN": "2470-9239",
"publisher": "Wiley",
"URL": "https://doi.org/10.1002/epi4.70336",
"language": "en",
"issued": {
"date-parts": [
[
2026,
9,
5
]
]
}
}

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.1007/s00234-026-04103-8 [code]
Enhanced detection of subtle cortical abnormalities in focal epilepsy using 7 T MRI surface-based models and graph neural networks.
Journal: Neuroradiology
In common: statannotations, Nilearn, statsmodels, 7 other tools, epilepsy, structural MRI / diffusion, 7 references
[2] doi:10.1016/j.isci.2026.117446 [code]
Volume-inflation registration (INFREG) for morphometric analysis of human focal cortical dysplasia type II.
Journal: iScience
In common: NiBabel, pandas, SciPy, 2 other tools, OpenNeuro ds004199, epilepsy, structural MRI / diffusion, 5 references
[3] doi:10.64898/2026.08.18.26360725 [code]
Temporal pole blurring in hippocampal sclerosis reflects seizure-disrupted myelination
Journal: medRxiv (preprint)
In common: statannotations, Nilearn, statsmodels, 7 other tools, epilepsy, structural MRI / diffusion, 3 references
[4] doi:10.64898/2026.04.02.26349812 [code]
9.4 Tesla MRI in focal epilepsy patients with high-resolution surface-based profiling of focal cortical dysplasias
Journal: medRxiv (preprint)
In common: Nilearn, NiBabel, seaborn, 4 other tools, epilepsy, structural MRI / diffusion, clinical / translational, 5 references
[5] doi:10.1186/s40708-026-00299-w
Pipeline evaluation of a state-of-the-art AI algorithm for detection of focal cortical dysplasia: insights into potential failure sources.
Journal: Brain informatics
In common: OpenNeuro ds004199, epilepsy, structural MRI / diffusion, 6 references
[6] doi:10.1126/sciadv.adu9309 [code]
Variations of global brain asymmetry are associated with aging and related diseases.
Journal: Science advances
In common: Nilearn, NiBabel, scikit-learn, 3 other tools, 5 references
[7] doi:10.1371/journal.pbio.3003856 [code]
Aging and metabolism contribute separately to brain-body health.
Journal: PLoS biology
In common: rpy2, Nilearn, statsmodels, 7 other tools, structural MRI / diffusion, clinical / translational, 1 reference
[8] doi:10.21203/rs.3.rs-9914920/v1 [code]
Prediction of cognitive performance by demographics, sleep, and brain morphometry: machine learning findings from ENIGMA-Sleep Working Group
Journal: Research Square (preprint)
In common: statannotations, Nilearn, statsmodels, 7 other tools, structural MRI / diffusion
[9] doi:10.1038/s41598-026-51531-w [code]
Multimodal age-dependent diffusion-MRI analysis of the neocortex in a rat model of cortical dysplasia.
Journal: Scientific reports
In common: NiBabel, scikit-learn, pandas, 3 other tools, structural MRI / diffusion, 4 references
[10] doi:10.1162/imag.a.1256 [code]
Gamer in the scanner: Event-related analysis of fMRI activity during retro videogame play guided by automated annotations of game content.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: statannotations, Nilearn, statsmodels, 7 other tools

Contribute

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

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

Request its removal

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

Discussion, reproductions, activity

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

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

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