OSCR

Brain morphology in Anorexia Nervosa and its subtypes: A multi-cohort study of individual participant data.

Code ↔ Paper

8 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 8 matches
  1. [1] § Methods › Machine learning classification › Classification pipelines. ↔ src/an_heterogeneity/tools/model_training.py, lines 107–212 · score 0.75 · model training, Hyperparameter optimization, cross validation, scikit-learn, nested, fold
  2. [2] § Methods › Machine learning classification › Hyperparameters optimization and performance estimation. ↔ src/an_heterogeneity/tools/model_pipelines.py, lines 36–71 · score 0.68 · precision recall, model pipeline, PR AUC, optimization, curve, classes
  3. [3] § Methods › Machine learning classification › Hyperparameters optimization and performance estimation. ↔ src/an_heterogeneity/model_evaluation/cross_validation.py, lines 43–158 · score 0.58 · class ratios, cross validation, trained, curve, PR, ROC
  4. [4] § Methods › Image acquisition and processing ↔ src/an_heterogeneity/tools/load_parse_neuromaps.py, lines 33–51 · score 0.58 · Desikan Killiany, FreeSurfer, atlas
  5. [5] § Methods › Normative modeling ↔ src/an_heterogeneity/tools/normative_tools.py, lines 873–919 · score 0.57 · infra normal, regional CT, supranormal, deviation, normative, SA
  6. [6] § Results › z-scores from CentileBrain normative model › AN versus HC. ↔ src/an_heterogeneity/tools/normative_tools.py, lines 873–919 · score 0.55 · subcortical volume, cortical thickness, surface area, threshold, supranormal, infranormal
  7. [7] § Results › Univariate comparisons ↔ src/an_heterogeneity/ANHC_univariate.ipynb, lines 461–472 · score 0.51 · FDR correction, CI, SD, ventricles, Univariate
  8. [8] § Results › Machine learning classification ↔ src/an_heterogeneity/model_evaluation/performance_metric_visualization.py, lines 89–144 · score 0.50 · precision recall, PR AUC, baseline, metrics

Paper

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

The paper is loaded when this pane is shown.

The authors' code

Python · 1,056 lines · 44 KB · no license · 2 matches

  1. import os
  2. import pandas as pd
  3. import numpy as np
  4. import seaborn as sns
  5. from docx import Document
  6. from docx.shared import Pt
  7. from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
  8. from docx.oxml import OxmlElement
  9. from docx.oxml.ns import qn
  10. import scipy.stats as stats
  11. from statsmodels.stats.proportion import proportions_ztest
  12. from statsmodels.stats.multitest import multipletests
  13. import matplotlib.pyplot as plt
  14. import matplotlib.colorbar as colorbar
  15. import matplotlib.colors as colors
  16. from PIL import Image, ImageDraw, ImageFont
  17. # MOST IMPORTANT FUNCTION:
  18. def compute_deviation_percentages(df, thr=1.96):
  19. """
  20. Compute the percentage of supra- and infranormal deviations (z > 1.96 and z < -1.96, respectively)
  21. for each column in a dataframe, along with p-values, FDR-corrected p-values, and FDR thresholds.
  22. Args:
  23. df (pd.DataFrame): A dataframe containing z-scores for participants (rows) and measures (columns).
  24. Returns:
  25. pd.DataFrame: A dataframe with columns:
  26. ['Measure', 'Supra (%)', 'Infra (%)', 'Supra p-value', 'Infra p-value',
  27. 'Supra FDR p-value', 'Infra FDR p-value', 'FDR Threshold Supra (%)',
  28. 'FDR Threshold Infra (%)']
  29. """
  30. # Expected proportion of supra/infranormal values under the null hypothesis
  31. pthr = {1.96:0.025, 1.28:0.05, 0.84:0.1}
  32. expected_proportion = pthr[thr]
  33. # Total number of participants (rows)
  34. n = len(df)
  35. # Calculate the percentage threshold for uncorrected significance
  36. uncorrected_threshold = stats.binom.ppf(0.95, n, expected_proportion) / n * 100
  37. # Initialize results
  38. results = []
  39. supra_p_values = []
  40. infra_p_values = []
  41. # Iterate through each column
  42. for col in df.columns:
  43. # Calculate supra- and infranormal percentages
  44. supra_percentage = (df[col] > thr).mean() * 100
  45. infra_percentage = (df[col] < -thr).mean() * 100
  46. # Calculate p-values for supra- and infranormal percentages
  47. supra_count = (df[col] > thr).sum()
  48. infra_count = (df[col] < -thr).sum()
  49. supra_p_value = stats.binomtest(supra_count, n, expected_proportion, alternative='greater').pvalue
  50. infra_p_value = stats.binomtest(infra_count, n, expected_proportion, alternative='greater').pvalue
  51. supra_p_values.append(supra_p_value)
  52. infra_p_values.append(infra_p_value)
  53. # Append results for the column
  54. results.append({
  55. 'Measure': col,
  56. 'Supra (%)': supra_percentage,
  57. 'Infra (%)': infra_percentage,
  58. 'Supra p-value': supra_p_value,
  59. 'Infra p-value': infra_p_value
  60. })
  61. # Correct p-values using FDR
  62. supra_fdr_corrected = multipletests(supra_p_values, alpha=0.05, method='fdr_bh')[1]
  63. infra_fdr_corrected = multipletests(infra_p_values, alpha=0.05, method='fdr_bh')[1]
  64. # Compute FDR thresholds dynamically
  65. fdr_threshold_supra = min(
  66. [results[i]['Supra (%)'] for i in range(len(results)) if supra_fdr_corrected[i] < 0.05],
  67. default=100
  68. )
  69. fdr_threshold_infra = min(
  70. [results[i]['Infra (%)'] for i in range(len(results)) if infra_fdr_corrected[i] < 0.05],
  71. default=100
  72. )
  73. # Add FDR-corrected p-values and thresholds to results
  74. for i, row in enumerate(results):
  75. row['Supra FDR p-value'] = supra_fdr_corrected[i]
  76. row['Infra FDR p-value'] = infra_fdr_corrected[i]
  77. row['FDR Threshold Supra (%)'] = fdr_threshold_supra
  78. row['FDR Threshold Infra (%)'] = fdr_threshold_infra
  79. # Convert results to a dataframe
  80. results_df = pd.DataFrame(results)
  81. return results_df
  82. # Example Usage:
  83. # results_ANbp = compute_deviation_percentages(df_T1.loc[(df_T1.dx==1) & (df_T1.subtype==1),thicknesses], thr=1.96)
  84. def test_global_heterogeneity_mean_sd(z_matrix, n_bootstrap=20000, random_state=42, verbose=True):
  85. """
  86. Assess whether the mean SD across ROIs (columns) is higher than expected under null using bootstrap.
  87. Parameters:
  88. - z_matrix: pd.DataFrame, z-score matrix (subjects x ROIs)
  89. - n_bootstrap: int, number of bootstrap samples
  90. - random_state: int, random seed for reproducibility
  91. - verbose: bool, if True prints result
  92. Returns:
  93. - observed_mean_sd: float, mean SD across ROIs in actual data
  94. - p_value: float, one-sided p-value
  95. - null_distribution: np.array, mean SDs from null model
  96. """
  97. np.random.seed(random_state)
  98. n_subjects, n_rois = z_matrix.shape
  99. # 1. Compute observed mean SD across ROIs
  100. observed_sds = z_matrix.std(axis=0, ddof=1)
  101. observed_mean_sd = observed_sds.mean()
  102. # 2. Bootstrap null distribution under standard normal
  103. null_distribution = []
  104. for _ in range(n_bootstrap):
  105. synthetic_data = np.random.normal(loc=0, scale=1, size=(n_subjects, n_rois))
  106. synthetic_sds = synthetic_data.std(axis=0, ddof=1)
  107. null_distribution.append(np.mean(synthetic_sds))
  108. null_distribution = np.array(null_distribution)
  109. # 3. Compute one-sided p-value (observed > null)
  110. p_value = np.mean(null_distribution >= observed_mean_sd)
  111. if verbose:
  112. print(f"Observed mean SD across ROIs: {observed_mean_sd:.4f}")
  113. print(f"Mean of null distribution: {null_distribution.mean():.4f}")
  114. print(f"One-sided p-value: {p_value:.4f}")
  115. return observed_mean_sd, p_value, null_distribution
  116. # OUTDATED FUNCTION COMPARING THE FREQUENCY OF EXTREME Z-SCORES BETWEEN GROUPS, BUT THIS DOES NOT MAKE MUCH SENSE SINCE IT
  117. # IS BETTER TO COMPARE THE FREQUENCY OF EXTREME Z-SCORES BETWEEN AN AND THE NORMATIVE REFERENCE
  118. def group_compare_extreme_zs(df, feats, group='dx', labels_dict={'HC': 0, 'AN': 1}, thr=1.96, mult_comp=None):
  119. # this function assesses whether there are significant differences for each measure in feats in the
  120. # proportion of supra/infra-normal deviations (it plots a table with p-values computed according to Fisher, Fisher Chi2, and Z tests)
  121. results = []
  122. p_values_pos_fisher_chi2 = []
  123. p_values_neg_fisher_chi2 = []
  124. p_values_pos_ztest = []
  125. p_values_neg_ztest = []
  126. stats_pos_ztest = []
  127. stats_neg_ztest = []
  128. fisher_used_pos = []
  129. fisher_used_neg = []
  130. for feature in feats:
  131. row = {}
  132. for label, group_value in labels_dict.items():
  133. row[f'{label}+'] = ((df[df[group] == group_value][feature] > thr).mean() * 100)
  134. row[f'{label}-'] = ((df[df[group] == group_value][feature] < -thr).mean() * 100)
  135. # Count of outliers and non-outliers for Fisher/Chi-Square Test
  136. pos_outliers_group = (df[df[group] == 1][feature] > thr).sum()
  137. pos_total_group = (df[group] == 1).sum()
  138. pos_outliers_control = (df[df[group] == 0][feature] > thr).sum()
  139. pos_total_control = (df[group] == 0).sum()
  140. neg_outliers_group = (df[df[group] == 1][feature] < -thr).sum()
  141. neg_total_group = (df[group] == 1).sum()
  142. neg_outliers_control = (df[df[group] == 0][feature] < -thr).sum()
  143. neg_total_control = (df[group] == 0).sum()
  144. contingency_table_pos = [[pos_outliers_group, pos_outliers_control],
  145. [pos_total_group - pos_outliers_group, pos_total_control - pos_outliers_control]]
  146. contingency_table_neg = [[neg_outliers_group, neg_outliers_control],
  147. [neg_total_group - neg_outliers_group, neg_total_control - neg_outliers_control]]
  148. # Check and perform Fisher's Exact Test or Chi-Square Test
  149. fisher_flag_pos = min(pos_outliers_group, pos_outliers_control, pos_total_group - pos_outliers_group,
  150. pos_total_control - pos_outliers_control) < 10
  151. fisher_flag_neg = min(neg_outliers_group, neg_outliers_control, neg_total_group - neg_outliers_group,
  152. neg_total_control - neg_outliers_control) < 10
  153. fisher_used_pos.append(int(fisher_flag_pos))
  154. fisher_used_neg.append(int(fisher_flag_neg))
  155. if fisher_flag_pos:
  156. _, p_pos_fisher_chi2 = stats.fisher_exact(contingency_table_pos)
  157. else:
  158. _, p_pos_fisher_chi2, _, _ = stats.chi2_contingency(contingency_table_pos)
  159. if fisher_flag_neg:
  160. _, p_neg_fisher_chi2 = stats.fisher_exact(contingency_table_neg)
  161. else:
  162. _, p_neg_fisher_chi2, _, _ = stats.chi2_contingency(contingency_table_neg)
  163. # Two Proportions Z-Test (with check for valid inputs)
  164. if any(n > 0 for n in [pos_outliers_group, pos_outliers_control]):
  165. stat_pos_ztest, p_pos_ztest = proportions_ztest([pos_outliers_group, pos_outliers_control],
  166. [pos_total_group, pos_total_control])
  167. else:
  168. stat_pos_ztest = 0
  169. p_pos_ztest = 1
  170. if any(n > 0 for n in [neg_outliers_group, neg_outliers_control]):
  171. stat_neg_ztest, p_neg_ztest = proportions_ztest([neg_outliers_group, neg_outliers_control],
  172. [neg_total_group, neg_total_control])
  173. else:
  174. stat_neg_ztest = 0
  175. p_neg_ztest = 1
  176. p_values_pos_fisher_chi2.append(p_pos_fisher_chi2)
  177. p_values_neg_fisher_chi2.append(p_neg_fisher_chi2)
  178. p_values_pos_ztest.append(p_pos_ztest)
  179. p_values_neg_ztest.append(p_neg_ztest)
  180. stats_pos_ztest.append(stat_pos_ztest)
  181. stats_neg_ztest.append(stat_neg_ztest)
  182. results.append(row)
  183. # Multiple comparisons correction
  184. if mult_comp == 'fdr':
  185. corrected_p_values_pos_fisher_chi2 = multipletests(p_values_pos_fisher_chi2, method='fdr_bh')[1]
  186. corrected_p_values_neg_fisher_chi2 = multipletests(p_values_neg_fisher_chi2, method='fdr_bh')[1]
  187. corrected_p_values_pos_ztest = multipletests(p_values_pos_ztest, method='fdr_bh')[1]
  188. corrected_p_values_neg_ztest = multipletests(p_values_neg_ztest, method='fdr_bh')[1]
  189. else:
  190. corrected_p_values_pos_fisher_chi2 = p_values_pos_fisher_chi2
  191. corrected_p_values_neg_fisher_chi2 = p_values_neg_fisher_chi2
  192. corrected_p_values_pos_ztest = p_values_pos_ztest
  193. corrected_p_values_neg_ztest = p_values_neg_ztest
  194. for i, row in enumerate(results):
  195. row['Fisher_Chi2_Pos_Test'] = corrected_p_values_pos_fisher_chi2[i]
  196. row['Fisher_Chi2_Neg_Test'] = corrected_p_values_neg_fisher_chi2[i]
  197. row['ZTest_Pos_Test'] = corrected_p_values_pos_ztest[i]
  198. row['ZTest_Neg_Test'] = corrected_p_values_neg_ztest[i]
  199. row['ZTest_Pos_Stat'] = stats_pos_ztest[i]
  200. row['ZTest_Neg_Stat'] = stats_neg_ztest[i]
  201. row['Fisher_Pos'] = fisher_used_pos[i]
  202. row['Fisher_Neg'] = fisher_used_neg[i]
  203. return pd.DataFrame(results, index=feats)
  204. # Example usage:
  205. # result_df = group_compare_extreme_zs(df, feats)
  206. # print(result_df)
  207. def set_cell_border(cell, **kwargs):
  208. """
  209. Set cell border to the specified styles.
  210. Usage: set_cell_border(cell, top="single", bottom="single", start="single", end="single")
  211. """
  212. tc = cell._element
  213. tcPr = tc.get_or_add_tcPr()
  214. for edge in ('top', 'start', 'bottom', 'end'):
  215. if edge in kwargs:
  216. tag = f'w:{edge}'
  217. element = OxmlElement(tag)
  218. element.set(qn('w:val'), kwargs[edge])
  219. element.set(qn('w:sz'), '4') # Size 4 for thin border
  220. element.set(qn('w:space'), '0')
  221. element.set(qn('w:color'), 'auto')
  222. tcPr.append(element)
  223. def format_number(value):
  224. """
  225. Format the number to have at most 4 digits after the comma.
  226. """
  227. if isinstance(value, (int, float)):
  228. return f"{value:.2f}"
  229. return str(value)
  230. def dataframe_to_word(df, filename):
  231. # Create a new Document
  232. doc = Document()
  233. # Add a table with an extra column for the index
  234. table = doc.add_table(rows=df.shape[0] + 1, cols=df.shape[1] + 1)
  235. # Add the header row
  236. table.cell(0, 0).text = "Index"
  237. set_cell_border(table.cell(0, 0), bottom="single")
  238. for j, column in enumerate(df.columns):
  239. table.cell(0, j + 1).text = column
  240. set_cell_border(table.cell(0, j + 1), bottom="single")
  241. # Add the rest of the DataFrame rows including the index
  242. for i in range(df.shape[0]):
  243. table.cell(i + 1, 0).text = str(df.index[i])
  244. set_cell_border(table.cell(i + 1, 0), right="single")
  245. for j in range(df.shape[1]):
  246. table.cell(i + 1, j + 1).text = format_number(df.iat[i, j])
  247. # Save the document
  248. doc.save(filename)
  249. print(f"Table saved to {filename}")
  250. def analyze_deviations(dataframe, columns, group_column, min_infrasupra=1, thr=1.96):
  251. """
  252. Analyze the percentage of participants with supranormal (z > 1.96) or infranormal (z < -1.96) deviations
  253. across specified columns, grouped by a specific group column. Includes chi-squared tests for group differences.
  254. Args:
  255. dataframe (pd.DataFrame): The input dataframe containing participant data.
  256. columns (list): List of column names to analyze for deviations.
  257. group_column (str): Name of the column indicating group membership.
  258. min_infrasupra (int or float, optional): Minimum number of deviations required.
  259. If between 0 and 0.1, interprets it as a p-value and computes the threshold
  260. using the binomial distribution.
  261. Returns:
  262. str: A textual summary of the results.
  263. """
  264. # Define thresholds for deviations
  265. nc = len(columns) # Number of columns (regions of interest)
  266. pthr = {1.96:0.025, 1.28:0.05, 0.84:0.1}
  267. if 0 < min_infrasupra < 0.1:
  268. p = pthr[thr] # Probability of |z| > 1.96
  269. mu = nc * p
  270. sigma = np.sqrt(nc * p * (1 - p))
  271. threshold = int(binom.ppf(1 - min_infrasupra / 2, n=nc, p=p))
  272. threshold_text = f"at least {threshold}"
  273. else:
  274. threshold = min_infrasupra
  275. threshold_text = f"at least {threshold}"
  276. # Create masks for deviations
  277. supranormal_mask = (dataframe[columns] > thr).sum(axis=1) >= threshold
  278. infranormal_mask = (dataframe[columns] < -thr).sum(axis=1) >= threshold
  279. any_deviation_mask = supranormal_mask | infranormal_mask
  280. # Group data by the specified group column
  281. grouped = dataframe.groupby(group_column)
  282. # Calculate percentages for each group
  283. results = {}
  284. for group, group_data in grouped:
  285. total = len(group_data)
  286. if total == 0:
  287. results[group] = {
  288. "any_percentage": 0, "any_count": 0,
  289. "supra_percentage": 0, "supra_count": 0,
  290. "infra_percentage": 0, "infra_count": 0,
  291. "total": 0
  292. }
  293. continue
  294. count_with_any_deviation = any_deviation_mask.loc[group_data.index].sum()
  295. count_with_supra_deviation = supranormal_mask.loc[group_data.index].sum()
  296. count_with_infra_deviation = infranormal_mask.loc[group_data.index].sum()
  297. results[group] = {
  298. "any_percentage": (count_with_any_deviation / total) * 100,
  299. "any_count": count_with_any_deviation,
  300. "supra_percentage": (count_with_supra_deviation / total) * 100,
  301. "supra_count": count_with_supra_deviation,
  302. "infra_percentage": (count_with_infra_deviation / total) * 100,
  303. "infra_count": count_with_infra_deviation,
  304. "total": total
  305. }
  306. # Create contingency tables for chi-squared tests
  307. contingency_table_any = []
  308. contingency_table_supra = []
  309. contingency_table_infra = []
  310. for group in results:
  311. # Any deviation
  312. count_with_any_deviation = results[group]["any_count"]
  313. count_without_any_deviation = results[group]["total"] - count_with_any_deviation
  314. contingency_table_any.append([count_with_any_deviation, count_without_any_deviation])
  315. # Supranormal deviation
  316. count_with_supra_deviation = results[group]["supra_count"]
  317. count_without_supra_deviation = results[group]["total"] - count_with_supra_deviation
  318. contingency_table_supra.append([count_with_supra_deviation, count_without_supra_deviation])
  319. # Infranormal deviation
  320. count_with_infra_deviation = results[group]["infra_count"]
  321. count_without_infra_deviation = results[group]["total"] - count_with_infra_deviation
  322. contingency_table_infra.append([count_with_infra_deviation, count_without_infra_deviation])
  323. # Perform chi-squared tests
  324. chi2_any, p_value_any, _, _ = stats.chi2_contingency(contingency_table_any)
  325. chi2_supra, p_value_supra, _, _ = stats.chi2_contingency(contingency_table_supra)
  326. chi2_infra, p_value_infra, _, _ = stats.chi2_contingency(contingency_table_infra)
  327. # Build the textual output
  328. output = []
  329. output.append("Analysis of deviations:")
  330. for group, sts in results.items():
  331. output.append(
  332. f"- In group '{group}':\n"
  333. f" * {sts['any_percentage']:.2f}% ({sts['any_count']} out of {sts['total']}) participants had {threshold_text} supranormal or infranormal deviation.\n"
  334. f" * {sts['supra_percentage']:.2f}% ({sts['supra_count']} out of {sts['total']}) participants had {threshold_text} supranormal deviation.\n"
  335. f" * {sts['infra_percentage']:.2f}% ({sts['infra_count']} out of {sts['total']}) participants had {threshold_text} infranormal deviation."
  336. )
  337. output.append(f"\nChi-squared test results:")
  338. output.append(f"- Any deviation:\n * Chi-squared statistic: {chi2_any:.2f}\n * p-value: {p_value_any:.4f}")
  339. if p_value_any < 0.05:
  340. output.append(" * The difference in percentages between groups for any deviation is statistically significant.")
  341. else:
  342. output.append(" * The difference in percentages between groups for any deviation is not statistically significant.")
  343. output.append(f"- Supranormal deviation:\n * Chi-squared statistic: {chi2_supra:.2f}\n * p-value: {p_value_supra:.4f}")
  344. if p_value_supra < 0.05:
  345. output.append(" * The difference in percentages between groups for supranormal deviations is statistically significant.")
  346. else:
  347. output.append(" * The difference in percentages between groups for supranormal deviations is not statistically significant.")
  348. output.append(f"- Infranormal deviation:\n * Chi-squared statistic: {chi2_infra:.2f}\n * p-value: {p_value_infra:.4f}")
  349. if p_value_infra < 0.05:
  350. output.append(" * The difference in percentages between groups for infranormal deviations is statistically significant.")
  351. else:
  352. output.append(" * The difference in percentages between groups for infranormal deviations is not statistically significant.")
  353. return "\n".join(output)
  354. #############################
  355. # Unuseful stuff?
  356. import pandas as pd
  357. import statsmodels.formula.api as smf
  358. from statsmodels.stats.multitest import multipletests
  359. def fit_models_and_extract_values(df, measures, model_terms, multcomp=None, filter_significant=0,
  360. vars_of_interest=None):
  361. """
  362. Fits a linear model for each measure and extracts p-values and t-values for specified factors, with optional filtering.
  363. Args:
  364. df (DataFrame): The DataFrame containing the data.
  365. measures (list): List of measures to fit models for.
  366. model_terms (list): List of model terms to include in the formula.
  367. multcomp (str, optional): Multiple comparison correction method. Defaults to None.
  368. filter_significant (int, optional): If 1, filters results based on significance of vars_of_interest.
  369. vars_of_interest (list, optional): Variables to check for significance if filtering.
  370. Returns:
  371. DataFrame: A DataFrame with each measure and corresponding p-values and t-values for factors.
  372. """
  373. results = []
  374. for measure in measures:
  375. # Construct the formula
  376. formula_terms = ' + '.join(model_terms)
  377. formula = f'{measure} ~ {formula_terms}'
  378. model = smf.ols(formula, data=df).fit()
  379. # Extract p-values and t-values
  380. relevant_terms = [term for term in model_terms if term in model.params.index]
  381. pvals = model.pvalues[relevant_terms].to_dict()
  382. tvals = model.tvalues[relevant_terms].to_dict()
  383. # Combine and add measure
  384. combined_dict = {**{f'{term}_pval': pvals[term] for term in relevant_terms},
  385. **{f'{term}_tval': tvals[term] for term in relevant_terms},
  386. 'measure': measure}
  387. results.append(combined_dict)
  388. results_df = pd.DataFrame(results)
  389. # Multiple comparisons correction
  390. if multcomp:
  391. pval_cols = [f'{term}_pval' for term in relevant_terms]
  392. corrected_pvals = multipletests(results_df[pval_cols].values.flatten(),
  393. method=multcomp)[1]
  394. results_df[pval_cols] = corrected_pvals.reshape(results_df[pval_cols].shape)
  395. # Filter for significant effects of interest
  396. if filter_significant and vars_of_interest:
  397. filter_conditions = [(results_df[f'{var}_pval'] < 0.05) for var in vars_of_interest]
  398. combined_condition = filter_conditions[0]
  399. for condition in filter_conditions[1:]:
  400. combined_condition |= condition
  401. results_df = results_df[combined_condition]
  402. return results_df
  403. def set_column_width(column, width_cm):
  404. # Adjust column width in cm
  405. for cell in column.cells:
  406. tc = cell._element
  407. tcPr = tc.get_or_add_tcPr()
  408. tcW = OxmlElement('w:tcW')
  409. tcW.set(qn('w:w'), str(int(width_cm * 567))) # 1cm ≈ 567 units in docx format
  410. tcW.set(qn('w:type'), 'dxa')
  411. tcPr.append(tcW)
  412. def create_word_table_from_dataframe(df, dest_dir, filename):
  413. # Initialize the document
  414. doc = Document()
  415. # Get the covariates by splitting column names (assuming X_pval and X_tval structure)
  416. covariates = sorted(set(col.split('_')[0] for col in df.columns))
  417. # Create the table: rows = number of rows in df + 2 (for headers), columns = 1 (feature names) + 2 * number of covariates
  418. table = doc.add_table(rows=len(df) + 2, cols=1 + 2 * len(covariates))
  419. # First row: feature names and covariate names
  420. table.cell(0, 0).text = 'Feature'
  421. for i, covariate in enumerate(covariates):
  422. cell = table.cell(0, 1 + 2 * i)
  423. cell.text = covariate
  424. cell.merge(table.cell(0, 1 + 2 * i + 1)) # Merge the two columns for covariate name
  425. # Make covariate name bold and center it
  426. for paragraph in cell.paragraphs:
  427. paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
  428. for run in paragraph.runs:
  429. run.bold = True
  430. # Second row: 'p' and 't' labels for each covariate
  431. table.cell(1, 0).text = '' # Blank cell for the feature names column header
  432. for i, covariate in enumerate(covariates):
  433. table.cell(1, 1 + 2 * i).text = 'p'
  434. table.cell(1, 1 + 2 * i + 1).text = 't'
  435. # Remaining rows: fill in the feature names, p-values, and t-values from the DataFrame
  436. for row_idx, feature_name in enumerate(df.index):
  437. # Insert feature name in the first column
  438. table.cell(row_idx + 2, 0).text = str(feature_name)
  439. # Insert p-values and t-values in the remaining columns
  440. for i, covariate in enumerate(covariates):
  441. pval_col = f'{covariate}_pval'
  442. tval_col = f'{covariate}_tval'
  443. # Format p-values to 4 decimal places and t-values to 2 decimal places
  444. pval = f'{df.iloc[row_idx][pval_col]:.4f}'
  445. tval = f'{df.iloc[row_idx][tval_col]:.2f}'
  446. # Insert formatted values into the table
  447. table.cell(row_idx + 2, 1 + 2 * i).text = pval
  448. table.cell(row_idx + 2, 1 + 2 * i + 1).text = tval
  449. # Adjust font size for the entire table (optional, for better readability)
  450. for row in table.rows:
  451. for cell in row.cells:
  452. for paragraph in cell.paragraphs:
  453. for run in paragraph.runs:
  454. run.font.size = Pt(10)
  455. # Set the column width for a more compact layout (width in cm)
  456. set_column_width(table.columns[0], 2.5) # Feature names column width
  457. for i in range(1, len(covariates) * 2 + 1):
  458. set_column_width(table.columns[i], 1.5) # Covariate columns width (for both p and t)
  459. # Save the document
  460. output_path = os.path.join(dest_dir, f'{filename}.docx')
  461. doc.save(output_path)
  462. return output_path
  463. import pandas as pd
  464. import statsmodels.formula.api as smf
  465. from statsmodels.stats.multitest import multipletests
  466. def fit_models_and_extract_values(df, measures, model_terms, multcomp=None, filter_significant=0,
  467. vars_of_interest=None):
  468. """
  469. Fits a linear model for each measure and extracts p-values and t-values for specified factors, with optional filtering.
  470. Args:
  471. df (DataFrame): The DataFrame containing the data.
  472. measures (list): List of measures to fit models for.
  473. model_terms (list): List of model terms to include in the formula.
  474. multcomp (str, optional): Multiple comparison correction method. Defaults to None.
  475. filter_significant (int, optional): If 1, filters results based on significance of vars_of_interest.
  476. vars_of_interest (list, optional): Variables to check for significance if filtering.
  477. Returns:
  478. DataFrame: A DataFrame with each measure and corresponding p-values and t-values for factors.
  479. """
  480. results = []
  481. for measure in measures:
  482. # Construct the formula
  483. formula_terms = ' + '.join(model_terms)
  484. formula = f'{measure} ~ {formula_terms}'
  485. model = smf.ols(formula, data=df).fit()
  486. # Extract p-values and t-values
  487. relevant_terms = [term for term in model_terms if term in model.params.index]
  488. pvals = model.pvalues[relevant_terms].to_dict()
  489. tvals = model.tvalues[relevant_terms].to_dict()
  490. # Combine and add measure
  491. combined_dict = {**{f'{term}_pval': pvals[term] for term in relevant_terms},
  492. **{f'{term}_tval': tvals[term] for term in relevant_terms},
  493. 'measure': measure}
  494. results.append(combined_dict)
  495. results_df = pd.DataFrame(results)
  496. # Multiple comparisons correction
  497. if multcomp:
  498. pval_cols = [f'{term}_pval' for term in relevant_terms]
  499. corrected_pvals = multipletests(results_df[pval_cols].values.flatten(),
  500. method=multcomp)[1]
  501. results_df[pval_cols] = corrected_pvals.reshape(results_df[pval_cols].shape)
  502. # Filter for significant effects of interest
  503. if filter_significant and vars_of_interest:
  504. filter_conditions = [(results_df[f'{var}_pval'] < 0.05) for var in vars_of_interest]
  505. combined_condition = filter_conditions[0]
  506. for condition in filter_conditions[1:]:
  507. combined_condition |= condition
  508. results_df = results_df[combined_condition]
  509. return results_df
  510. def set_column_width(column, width_cm):
  511. # Adjust column width in cm
  512. for cell in column.cells:
  513. tc = cell._element
  514. tcPr = tc.get_or_add_tcPr()
  515. tcW = OxmlElement('w:tcW')
  516. tcW.set(qn('w:w'), str(int(width_cm * 567))) # 1cm ≈ 567 units in docx format
  517. tcW.set(qn('w:type'), 'dxa')
  518. tcPr.append(tcW)
  519. def create_word_table_from_dataframe(df, dest_dir, filename):
  520. # Initialize the document
  521. doc = Document()
  522. # Get the covariates by splitting column names (assuming X_pval and X_tval structure)
  523. covariates = sorted(set(col.split('_')[0] for col in df.columns))
  524. # Create the table: rows = number of rows in df + 2 (for headers), columns = 1 (feature names) + 2 * number of covariates
  525. table = doc.add_table(rows=len(df) + 2, cols=1 + 2 * len(covariates))
  526. # First row: feature names and covariate names
  527. table.cell(0, 0).text = 'Feature'
  528. for i, covariate in enumerate(covariates):
  529. cell = table.cell(0, 1 + 2 * i)
  530. cell.text = covariate
  531. cell.merge(table.cell(0, 1 + 2 * i + 1)) # Merge the two columns for covariate name
  532. # Make covariate name bold and center it
  533. for paragraph in cell.paragraphs:
  534. paragraph.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
  535. for run in paragraph.runs:
  536. run.bold = True
  537. # Second row: 'p' and 't' labels for each covariate
  538. table.cell(1, 0).text = '' # Blank cell for the feature names column header
  539. for i, covariate in enumerate(covariates):
  540. table.cell(1, 1 + 2 * i).text = 'p'
  541. table.cell(1, 1 + 2 * i + 1).text = 't'
  542. # Remaining rows: fill in the feature names, p-values, and t-values from the DataFrame
  543. for row_idx, feature_name in enumerate(df.index):
  544. # Insert feature name in the first column
  545. table.cell(row_idx + 2, 0).text = str(feature_name)
  546. # Insert p-values and t-values in the remaining columns
  547. for i, covariate in enumerate(covariates):
  548. pval_col = f'{covariate}_pval'
  549. tval_col = f'{covariate}_tval'
  550. # Format p-values to 4 decimal places and t-values to 2 decimal places
  551. pval = f'{df.iloc[row_idx][pval_col]:.4f}'
  552. tval = f'{df.iloc[row_idx][tval_col]:.2f}'
  553. # Insert formatted values into the table
  554. table.cell(row_idx + 2, 1 + 2 * i).text = pval
  555. table.cell(row_idx + 2, 1 + 2 * i + 1).text = tval
  556. # Adjust font size for the entire table (optional, for better readability)
  557. for row in table.rows:
  558. for cell in row.cells:
  559. for paragraph in cell.paragraphs:
  560. for run in paragraph.runs:
  561. run.font.size = Pt(10)
  562. # Set the column width for a more compact layout (width in cm)
  563. set_column_width(table.columns[0], 2.5) # Feature names column width
  564. for i in range(1, len(covariates) * 2 + 1):
  565. set_column_width(table.columns[i], 1.5) # Covariate columns width (for both p and t)
  566. # Save the document
  567. output_path = os.path.join(dest_dir, f'{filename}.docx')
  568. doc.save(output_path)
  569. return output_path
  570. def create_combined_violin_plots(df, group_col, feature_cols, plotdir):
  571. sns.set(style="whitegrid", font_scale=1) # Set style and adjust font size
  572. num_features = len(feature_cols)
  573. # Determine the number of rows needed for the subplot grid
  574. num_rows = (num_features + 1) // 2
  575. plt.figure(figsize=(13, 4 * num_rows)) # Adjust the figure size as needed
  576. # Melt the DataFrame to have feature names as a categorical variable
  577. df_melted = df.melt(id_vars=group_col, value_vars=feature_cols, var_name='Feature', value_name='z-score')
  578. sns.violinplot(x='z-score', y='Feature', hue=group_col, data=df_melted, split=True, inner='quartile', orient="h")
  579. plt.title('z-scores distributions')
  580. plt.xlabel('z-score')
  581. # Add total counts for each site
  582. pvals=[]
  583. for s in df_melted['Feature'].unique():
  584. group1 = df_melted[(df_melted['Feature'] == s) & (df_melted[group_col] == 0)]['z-score']
  585. group2 = df_melted[(df_melted['Feature'] == s) & (df_melted[group_col] == 1)]['z-score']
  586. _, p_value = stats.ttest_ind(group1, group2, alternative='two-sided')
  587. #_, p_value = stats.mannwhitneyu(group1, group2, alternative='two-sided')
  588. pvals.append(p_value)
  589. corrected_pvals = multipletests(pvals, method='fdr_bh')[1]
  590. i=0
  591. for s in df_melted['Feature'].unique():
  592. plt.text(df_melted['z-score'].max() + 5, i, f'{s}: p = {corrected_pvals[i]:.4f}', va='center')
  593. i=i+1
  594. plt.tight_layout()
  595. plt.savefig(f'{plotdir}/{group_col}_combined_violin_plots.tif', format='tif', dpi=300)
  596. plt.close()
  597. # Example usage
  598. # df = pd.read_csv('your_data.csv') # Load your dataframe
  599. # group_col = 'dx' # Column name for the group (dx in your case)
  600. # feature_cols = df.columns.drop(group_col) # List of feature columns
  601. # plotdir = 'path_to_save_plots' # Directory where you want to save the plots
  602. # create_combined_violin_plots(df, group_col, feature_cols, plotdir)
  603. def calculate_group_stats(df, group_var, measure_dict, labels_dict, thr = 1.96):
  604. # Create a copy of the DataFrame to avoid SettingWithCopyWarning
  605. df = df.copy()
  606. # Calculate Average Deviation Scores for each measure group
  607. for group_name, measures in measure_dict.items():
  608. df[f'avg_dev_{group_name}'] = df[measures].mean(axis=1)
  609. # Calculate Global Average Deviation Score
  610. group_avg_columns = [f'avg_dev_{group_name}' for group_name in measure_dict.keys()]
  611. df['avg_dev_global'] = df[group_avg_columns].mean(axis=1)
  612. # Initialize the result DataFrame
  613. result_df = pd.DataFrame()
  614. # Iterate over measure groups and compute statistics
  615. for group_name in list(measure_dict.keys()) + ['global']:
  616. # Count occurrences for each group
  617. counts_positive = df.groupby(group_var)[f'avg_dev_{group_name}'].apply(lambda x: (x > thr).sum())
  618. counts_negative = df.groupby(group_var)[f'avg_dev_{group_name}'].apply(lambda x: (x < -thr).sum())
  619. # Perform Proportions Tests
  620. groups = df[group_var].unique()
  621. if len(groups) != 2:
  622. raise ValueError("The function currently supports exactly two groups for comparison.")
  623. count_pos = [counts_positive[group] for group in groups]
  624. count_neg = [counts_negative[group] for group in groups]
  625. nobs = [df[df[group_var] == group].shape[0] for group in groups]
  626. stat_pos, pval_pos = proportions_ztest(count_pos, nobs)
  627. stat_neg, pval_neg = proportions_ztest(count_neg, nobs)
  628. # Append the statistics to the result DataFrame
  629. for g, gl in labels_dict.items():
  630. result_df.loc[group_name, f'{g}_count_>{thr}'] = counts_positive[gl]
  631. result_df.loc[group_name, f'{g}_count_<-{thr}'] = counts_negative[gl]
  632. result_df.loc[group_name, f'p_value_>{thr}'] = pval_pos
  633. result_df.loc[group_name, f'p_value_<-{thr}'] = pval_neg
  634. return result_df
  635. # Example usage
  636. # df = [your_dataframe_here]
  637. # group_var = 'subtype'
  638. # measure_dict = {
  639. # 'Thicknesses': ['thickness_measure1', 'thickness_measure2'],
  640. # 'Volumes': ['volume_measure1', 'volume_measure2'],
  641. # 'Surfaces': ['surface_measure1', 'surface_measure2']
  642. # }
  643. # labels_dict = {'HC': 'Healthy Control', 'AN': 'Affected Group'}
  644. # result_df = calculate_group_stats(df, group_var, measure_dict, labels_dict)
  645. import numpy as np
  646. from scipy.stats import binom
  647. def print_group_percentages(dataframe, columns, group_column, min_infrasupra=1, thr = 1.96):
  648. """
  649. Prints the percentage of participants in each group with at least a specified number
  650. of supra or infranormal deviations.
  651. Parameters:
  652. dataframe (pd.DataFrame): DataFrame containing the data.
  653. columns (list): List of column names for the z-scores of ROIs.
  654. group_column (str): Column name for group labels.
  655. min_infrasupra (int or float, optional): Minimum number of deviations required.
  656. If between 0 and 0.1, interprets it as a p-value and computes the threshold
  657. using the binomial distribution.
  658. Returns:
  659. None
  660. """
  661. nc = len(columns) # Number of columns (regions of interest)
  662. pthr = {1.96:0.05, 1.28:0.1, 0.84:0.2}
  663. # Handle case where min_infrasupra is a p-value
  664. if 0 < min_infrasupra < 0.1:
  665. p = pthr[thr] # Probability of |z| > 1.96
  666. mu = nc * p
  667. sigma = np.sqrt(nc * p * (1 - p))
  668. # Compute the threshold corresponding to the (min_infrasupra / 2)% tail
  669. k = binom.ppf(1 - min_infrasupra / 2, n=nc, p=p)
  670. threshold = int(k)
  671. threshold_text = f"at least {threshold}"
  672. else:
  673. threshold = min_infrasupra
  674. threshold_text = f"at least {threshold}"
  675. for group in dataframe[group_column].unique():
  676. group_data = dataframe[dataframe[group_column] == group][columns]
  677. # Count supra and infra deviations for each participant
  678. supra_deviations = (group_data > thr).sum(axis=1)
  679. infra_deviations = (group_data < -thr).sum(axis=1)
  680. total_deviations = supra_deviations + infra_deviations
  681. total_percentage = (total_deviations >= threshold).mean() * 100
  682. supra_percentage = (supra_deviations >= threshold).mean() * 100
  683. infra_percentage = (infra_deviations >= threshold).mean() * 100
  684. total_count = (total_deviations >= threshold).sum()
  685. supra_count = (supra_deviations >= threshold).sum()
  686. infra_count = (infra_deviations >= threshold).sum()
  687. n_participants = len(group_data)
  688. print(f"- In group '{group}':")
  689. print(f" * {total_percentage:.2f}% ({total_count} out of {n_participants}) participants had {threshold_text} supranormal or infranormal deviation.")
  690. print(f" * {supra_percentage:.2f}% ({supra_count} out of {n_participants}) participants had {threshold_text} supranormal deviation.")
  691. print(f" * {infra_percentage:.2f}% ({infra_count} out of {n_participants}) participants had {threshold_text} infranormal deviation.")
  692. def summarize_deviations(df, thicknesses, surfaces, volumes, group_column='dx', group_labels={0:'HC', 1:'AN'}, thr = 1.96 ):
  693. """
  694. Summarizes the percentage of participants with supra-/infra-normal deviations in CT, SA, and SV measures for AN and HC groups.
  695. Args:
  696. df (pd.DataFrame): The dataframe containing participant data.
  697. thicknesses (list): List of column names for cortical thickness measures.
  698. surfaces (list): List of column names for surface area measures.
  699. volumes (list): List of column names for subcortical volume measures.
  700. Returns:
  701. str: A single summary sentence.
  702. """
  703. # Define thresholds
  704. supranormal_threshold = thr
  705. infranormal_threshold = -thr
  706. # Define groups
  707. an_group = df[df[group_column] == 1]
  708. hc_group = df[df[group_column] == 0]
  709. # Helper function to calculate percentages
  710. def calculate_percentages(group, measures):
  711. supra_mask = (group[measures] > supranormal_threshold).any(axis=1)
  712. infra_mask = (group[measures] < infranormal_threshold).any(axis=1)
  713. supra_percentage = supra_mask.mean() * 100
  714. infra_percentage = infra_mask.mean() * 100
  715. return supra_percentage, infra_percentage
  716. # Calculate for each measure type and group
  717. an_ct_supra, an_ct_infra = calculate_percentages(an_group, thicknesses)
  718. an_sa_supra, an_sa_infra = calculate_percentages(an_group, surfaces)
  719. an_sv_supra, an_sv_infra = calculate_percentages(an_group, volumes)
  720. hc_ct_supra, hc_ct_infra = calculate_percentages(hc_group, thicknesses)
  721. hc_sa_supra, hc_sa_infra = calculate_percentages(hc_group, surfaces)
  722. hc_sv_supra, hc_sv_infra = calculate_percentages(hc_group, volumes)
  723. # Create the summary sentence
  724. summary = (
  725. f"In the whole {group_labels[1]} group, supra-/infra-normal z-scores were observed in {an_ct_supra:.2f}%/{an_ct_infra:.2f}% of regional CT measures, "
  726. f"{an_sa_supra:.2f}%/{an_sa_infra:.2f}% of regional SA measures, and {an_sv_supra:.2f}%/{an_sv_infra:.2f}% of regional SV measures. "
  727. f"The percentages in the {group_labels[0]} group were {hc_ct_supra:.2f}%/{hc_ct_infra:.2f}%, {hc_sa_supra:.2f}%/{hc_sa_infra:.2f}%, "
  728. f"and {hc_sv_supra:.2f}%/{hc_sv_infra:.2f}%, respectively."
  729. )
  730. return summary
  731. def summarize_deviation_percentages(df, thicknesses, surfaces, volumes, group_column='dx', group_labels={0: 'HC', 1: 'AN'}, thr=1.96):
  732. """
  733. Summarizes the percentage of supra-normal and infra-normal values across all measures (thickness, surface, volume) for specified groups.
  734. Args:
  735. df (pd.DataFrame): The dataframe containing participant data.
  736. thicknesses (list): List of column names for cortical thickness measures.
  737. surfaces (list): List of column names for surface area measures.
  738. volumes (list): List of column names for subcortical volume measures.
  739. group_column (str): Name of the column indicating group membership.
  740. group_labels (dict): Dictionary mapping group values to labels.
  741. Returns:
  742. str: A summary of the percentages of supra-normal and infra-normal values for each group.
  743. """
  744. # Define thresholds
  745. supranormal_threshold = thr
  746. infranormal_threshold = -thr
  747. # Initialize results dictionary
  748. results = {}
  749. for group_value, group_label in group_labels.items():
  750. # Filter data for the current group
  751. group_data = df[df[group_column] == group_value]
  752. # Combine all measures
  753. measures = group_data[thicknesses + surfaces + volumes].values.flatten()
  754. measures = measures[~np.isnan(measures)] # Remove NaN values
  755. # Calculate percentages
  756. supra_percentage = np.mean(measures > supranormal_threshold) * 100
  757. infra_percentage = np.mean(measures < infranormal_threshold) * 100
  758. # Store results
  759. results[group_label] = {
  760. 'supra': supra_percentage,
  761. 'infra': infra_percentage
  762. }
  763. # Build summary text
  764. summary_lines = []
  765. for group_label, percentages in results.items():
  766. summary_lines.append(
  767. f"In the {group_label} group, {percentages['supra']:.2f}% of values are supra-normal (>{thr}) "
  768. f"and {percentages['infra']:.2f}% of values are infra-normal (<-{thr})."
  769. )
  770. return " \n".join(summary_lines)
  771. # Example usage:
  772. # summary = summarize_deviation_percentages(df, thicknesses, surfaces, volumes)
  773. # print(summary)
  774. def identify_columns_with_extreme_values(dataframe, threshold=1.96):
  775. """
  776. Identify columns with at least one participant with z > threshold or z < -threshold.
  777. Parameters:
  778. dataframe (pd.DataFrame): DataFrame containing the z-scores.
  779. threshold (float): Threshold for identifying extreme values (default is 1.96).
  780. Returns:
  781. dict: Dictionary with lists of columns for z > threshold and z < -threshold.
  782. """
  783. columns_z_gt_threshold = dataframe.columns[(dataframe > threshold).any()]
  784. columns_z_lt_threshold = dataframe.columns[(dataframe < -threshold).any()]
  785. return {
  786. "z_greater_than_threshold": list(columns_z_gt_threshold),
  787. "z_less_than_threshold": list(columns_z_lt_threshold)
  788. }
  789. #
  790. # Example usage
  791. #colswextremes = identify_columns_with_extreme_values(df_T1.loc[df_T1.dx==0.0,thicknesses], threshold=1.96)
  792. def save_custom_colorbar(cmap_name,
  793. vmin,
  794. vmax,
  795. ticks,
  796. out_path,
  797. orientation='vertical',
  798. label=None,
  799. dpi=300,
  800. background='black',
  801. tick_color='white',
  802. font_size=10,
  803. bar_width=0.3,
  804. bar_height=6):
  805. """
  806. Saves a styled standalone colorbar with control over size, orientation, and appearance.
  807. Parameters:
  808. cmap_name (str): Colormap name (registered in matplotlib).
  809. vmin, vmax (float): Value range for colorbar.
  810. ticks (list): List of tick values.
  811. out_path (str): Output file path (.tif, .png, etc.).
  812. orientation (str): 'vertical' or 'horizontal'.
  813. label (str): Optional axis label.
  814. dpi (int): Output resolution.
  815. background (str): Background color (e.g. 'black').
  816. tick_color (str): Tick label color.
  817. font_size (int): Font size of ticks.
  818. bar_width (float): Width in inches (thickness of the bar).
  819. bar_height (float): Height in inches (for vertical) or width (for horizontal).
  820. """
  821. if orientation == 'vertical':
  822. fig, ax = plt.subplots(figsize=(bar_width, bar_height))
  823. fig.subplots_adjust(left=0.3, right=0.7)
  824. else:
  825. fig, ax = plt.subplots(figsize=(bar_height, bar_width)) # horizontal: width=bar_height
  826. fig.subplots_adjust(bottom=0.3, top=0.7)
  827. # Background color
  828. fig.patch.set_facecolor(background)
  829. ax.set_facecolor(background)
  830. # Create colorbar
  831. norm = colors.Normalize(vmin=vmin, vmax=vmax)
  832. cb = colorbar.ColorbarBase(ax,
  833. cmap=plt.get_cmap(cmap_name),
  834. norm=norm,
  835. orientation=orientation,
  836. ticks=ticks)
  837. cb.ax.tick_params(labelsize=font_size, colors=tick_color)
  838. if label:
  839. cb.set_label(label, color=tick_color)
  840. # Save figure
  841. plt.savefig(out_path, bbox_inches='tight', dpi=dpi, facecolor=background)
  842. plt.close()

normative_tools.py, no license · at the source

Overview

Authors: Fabio Bernardoni1, Dominic Arold1, Luis Schoppik1, Klaas Bahnsen1,2, Ruiyang Ge3, Clara Moreau4, Lasse Bang5, Federico D’Agata6, Giovanni Abbate-Daga6,7, Christian K Tamnes8,9, Iain Campbell10, Owen O’Daly11, Ulrike Schmidt11, Guido Frank12,13, Stefanie Horndasch14,15, Andreas Hess16,17,18, Arnd Dörfler17, Hans-Christoph Friederich19, Joe Simon19, Angela Favaro20
and 16 other authorsLuca Lavagnino21, Christina E Wierenga12,13, Amanda Bischoff-Grethe12,13, Amy E Miles22, Allan Kaplan22, Aristotle Voineskos22, Paul A M Smeets23,24, Annemarie A van Elburg25,26, Unna Danner25,26, Sophia I Thomopoulos27, Laura Berner28, Neda Jahanshad27, Sophia Frangou3,28, Joseph A King1, Paul Thompson27, Stefan Ehrlich1,29
29 affiliations
  1. Translational Developmental Neuroscience Section, Division of Psychological and Social Medicine and Developmental Neurosciences, Faculty of Medicine, Technische Universität Dresden, Dresden, Germany
  2. Maurice Wohl Clinical Neuroscience Institute, Department of Psychological Medicine, Institute of Psychiatry, Psychology and Neuroscience, King’s College London, London, United Kingdom
  3. Djavad Mowafaghian Centre for Brain Health, University of British Columbia, Vancouver, British Columbia, Canada
  4. Centre de recherche CHU Sainte Justine, Department of Psychiatry and Addictology, University of Montreal, Montreal, Québec, Canada
  5. Department of Child Health and Development, Norwegian Institute of Public Health, Oslo, Norway
  6. Department of Neurosciences ‘Rita Levi Montalcini’, University of Turin, Turin, Italy
  7. Eating Disorders Center for Treatment and Research, University of Turin, Turin, Italy
  8. PROMENTA Research Center, Department of Psychology, University of Oslo, Oslo, Norway
  9. Division of Mental Health and Substance Abuse, Diakonhjemmet Hospital, Oslo, Norway
  10. Centre for Research in Eating and Weight Disorders, Institute of Psychitry, Psychology and Neuroscience, King’s College London, London, United Kingdom
  11. Department of Neuroimaging, Institute of Psychiatry, Psychology and Neuroscience, King’s College London, London, United Kingdom
  12. Department of Psychiatry, University of California San Diego, La Jolla, California, United States of America
  13. Eating Disorders Center for Treatment and Research, University of California San Diego, La Jolla, California, United States of America
  14. Department of Child and Adolescent Psychiatry, Bielefeld University, Medical School and University Medical Center OWL, Protestant Hospital of the Bethel Foundation, Bielefeld, Germany
  15. Department of Child and Adolescent Psychiatry, University Clinic Erlangen, Erlangen, Germany
  16. Institute of Experimental and Clinical Pharmacology and Toxicology, Emil Fischer Center, University of Erlangen-Nuremberg, Erlangen, Germany
  17. Department of Neuroradiology, University of Erlangen-Nuremberg, Erlangen, Germany
  18. FAU NeW - Research Center for New Bioactive Compounds, Friedrich-Alexander-Universität Erlangen-Nürnberg, Erlangen, Germany
  19. Centre for Psychosocial Medicine, Department of General Internal Medicine and Psychosomatics, University Hospital Heidelberg, Heidelberg, Germany
  20. Padova Neuroscience Center, Department of Neurosciences, University of Padova, Padova, Italy
  21. Department of Psychiatry and Behavioral Sciences, University of Texas Health Science Center, Houston, Texas, United States of America
  22. Campbell Family Mental Health Research Institute, Centre for Addiction and Mental Health, Toronto, Ontario, Canada
  23. UMC Utrecht Brain Center, Utrecht University, Utrecht, the Netherlands
  24. Division of Human Nutrition and Health, Wageningen University, Wageningen, the Netherlands
  25. Altrecht Eating Disorders Rintveld, Altrecht Mental Health Institute, Zeist, the Netherlands
  26. Faculty of Social Sciences, Utrecht University, Utrecht, the Netherlands
  27. Imaging Genetics Center, Stevens Institute for Neuroimaging and Informatics, Keck USC School of Medicine, Marina del Rey, California, United States of America
  28. Department of Psychiatry, Icahn School of Medicine at Mount Sinai, New York, New York, United States of America
  29. Eating Disorders Research and Treatment Center, Department of Child and Adolescent Psychiatry, Faculty of Medicine, Dresden University of Technology, Dresden, Germany
Journal: PLoS medicine, volume 23, issue 5, article e1004809
Dates: received 23 October 2025; accepted 30 April 2026; published online 20 May 2026
Type: Research article · Language: English
License: CC0
Identifiers: DOI 10.1371/journal.pmed.1004809 · PMID 42160333 · PMCID PMC13215615 · OpenAlex W7161753715
Open access: gold, a free copy (OpenAlex)
Status: code verified
Categories: structural MRI / diffusion (modality), human (organism), other condition (population)
Methods: Statistics, Smoothing, state filtering, decompositions, Machine learning, Preprocessing, fMRI & imaging
MeSH: Anorexia Nervosa*, Brain*, Adolescent, Adult, Cohort Studies, Female, Gray Matter, Humans, Magnetic Resonance Imaging, Neuroimaging, Young Adult (* major topic)
Topic: Eating Disorders and Behaviors (Clinical Psychology, Psychology), according to OpenAlex
Funding: Foundation for the National Institutes of Health (K23MH080135 and R01MH096777, R01MH113588, R21MH86017, U54 EB020403); Sächsisches Staatsministerium für Wissenschaft und Kunst (100770101); Helse Sør-Øst RHF (2021070, 2023012, and 500189); Else Kröner-Fresenius-Stiftung (2019_A118); Deutsche Forschungsgemeinschaft (EH 367/5-1 and EH 367/7-1, SI 2087/2-1 and BR 4852/1-1); Technische Universität Dresden (SFB940); South London and Maudsley NHS Foundation Trust; Schweizerische Anorexia Nervosa Stiftung (57-16); Centre for Addiction and Mental Health Foundation (CAM-14-001); Medizinische Fakultät Carl Gustav Carus, Technische Universität Dresden (Carus Promotionskolleg); National Institute for Health and Care Research (Senior Investigator Award); Norges Forskningsråd (288083 and 323951); National Institutes of Mental Health (R01MH134962)
Citations: not cited yet (Europe PMC); 88 references in the paper

Abstract

Background: In a recent coordinated meta-analysis of neuroimaging data, we reported gray matter (GM) alterations in acutely underweight patients with anorexia nervosa (AN). Here, we extend these findings by examining individual variation in brain structure within AN, individual-level differentiation between AN and healthy controls (HC), and differences between AN subtypes, with potential relevance for understanding clinical heterogeneity.

Methods and findings: We analyzed individual-level data from 11 international sites in the ENIGMA Eating Disorders Working Group, including 570 female participants with AN and 739 HC. We examined cortical thickness, cortical surface area and subcortical volumes in AN versus HC using three complementary approaches: (i) group-level differences in a mega-analysis correcting for age effects, (ii) frequencies of extreme deviations (infra-/supranormal; z < −1.96/z > 1.96) based on normative reference models by the CentileBrain Initiative, and (iii) individual-level classification performance using machine learning. The same analytic framework was applied to compare AN restricting versus binge-eating/purging subtype, additionally correcting for BMI effects.

Mega-analyses reinforced previous meta-analytic findings of pronounced and widespread GM deficits in AN compared to HC. Normative modelling revealed that the frequency of infranormal z-scores (23/68 cortical thickness, 13/14 subcortical volume metrics) and supranormal z-scores (35/68 cortical thickness, 17/68 cortical surface area metrics) was significantly higher in AN than expected based on reference data. Individuals with AN could be reliably differentiated from HC using machine-learning classifiers (ROC–AUC = 0.75–0.81). In contrast, neither group-level differences nor frequency of extreme z-scores differed between AN subtypes, and individuals with different subtypes could not be reliably differentiated from each other. Importantly, the observational design cannot distinguish neurobiological differences related to AN from the effects of starvation or low BMI in the AN versus HC analyses. The lack of differences between subtypes does not exclude brain structural differences between AN subtypes that might be detectable with other modalities or analytic approaches.

Conclusion: Using a mega-analytic approach, we confirm widespread GM deficits in AN, show that these alterations are (in some patients) extreme, and demonstrate that they enable robust classification with superior performance compared to most MRI-based psychiatric classification studies. The absence of differences between AN subtypes may reflect shared neurobiology, though other imaging modalities may reveal distinctions beyond brain structure.

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

Repository

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

OSF xrjkf

License: none: the authors keep all their rights
State: the link answers, verified on 28 September 2026
Evidence: files inventoried
Size: 1 file
Software Heritage: not checked
Found in: “Data Availability”
Not found: README, license file, CITATION.cff, environment file, tests, continuous integration, documentation
Tools: pandas (32 files), scikit-learn (32 files), NumPy (30 files), Matplotlib (15 files), seaborn (14 files), SciPy (13 files), statsmodels (11 files), Pillow (2 files), abagen (1 file), neuroHarmonize (1 file), neuromaps (1 file), NiBabel (1 file), Plotly (1 file), statannotations (1 file)
Availability: 1 check, the latest on 28 September 2026: the link answers (HTTP 200)
  • 28 September 2026: the link answers (HTTP 200)
45 files

The paper's code and data availability statement is in the Data section.

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;
  • 45 scripts, each with its path and the digest of its content;
  • 8 matches between paragraphs of the paper and lines of the code (method lexical-v1);
  • neither the text of the paper nor the code itself.

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

Data

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

Data Availability

Individual-level data underlying the findings of this study cannot be shared publicly because they are governed by site-specific ethical approvals and national and institutional data protection regulations at the 11 contributing ENIGMA Eating Disorders Working Group sites. The study authors are not the legal custodians of these data. Data access inquiries must be directed to the relevant institutional data custodian or ethics/governance office at each contributing site (Denver: , Dresden: , Erlangen: , Heidelberg: , London: , , Oslo: , Padova: , San Diego: , Torino: , Toronto: , Utrecht: , ). Any access is subject to local approval procedures and applicable legal and contractual restrictions. Summary-level data underlying the reported findings are provided in Tables A and B in the S1 Appendix. The code used for the analyses is available on OSF (https://osf.io/xrjkf/overview?view_only=b73a380dfaf94c36a69d9354e0c92679) or through the DOI (https://doi.org/10.17605/OSF.IO/XRJKF).

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

Versions

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

Version 1, 28 September 2026: the first record

Recorded: type, language, journal, volume, issue, pages, dates, 36 authors, 11 MeSH terms, 13 funders, 88 references.

Cite

This paper

Bernardoni, F., Arold, D., Schoppik, L., Bahnsen, K., Ge, R., Moreau, C., Bang, L., D’Agata, F., Abbate-Daga, G., Tamnes, C. K., Campbell, I., O’Daly, O., Schmidt, U., Frank, G., Horndasch, S., Hess, A., Dörfler, A., Friederich, H.-C., Simon, J., . . . Ehrlich, S. (2026). Brain morphology in Anorexia Nervosa and its subtypes: A multi-cohort study of individual participant data. PLoS medicine, 23(5), e1004809. https://doi.org/10.1371/journal.pmed.1004809

BibTeX

@article{bernardoni2026brain,
author = {Bernardoni, Fabio and Arold, Dominic and Schoppik, Luis and Bahnsen, Klaas and Ge, Ruiyang and Moreau, Clara and Bang, Lasse and D’Agata, Federico and Abbate-Daga, Giovanni and Tamnes, Christian K and Campbell, Iain and O’Daly, Owen and Schmidt, Ulrike and Frank, Guido and Horndasch, Stefanie and Hess, Andreas and Dörfler, Arnd and Friederich, Hans-Christoph and Simon, Joe and Favaro, Angela and Lavagnino, Luca and Wierenga, Christina E and Bischoff-Grethe, Amanda and Miles, Amy E and Kaplan, Allan and Voineskos, Aristotle and Smeets, Paul A M and van Elburg, Annemarie A and Danner, Unna and Thomopoulos, Sophia I and Berner, Laura and Jahanshad, Neda and Frangou, Sophia and King, Joseph A and Thompson, Paul and Ehrlich, Stefan},
title = {{Brain morphology in Anorexia Nervosa and its subtypes: A multi-cohort study of individual participant data}},
journal = {PLoS medicine},
year = {2026},
month = may,
volume = {23},
number = {5},
pages = {e1004809},
publisher = {PLOS},
issn = {1549-1277},
doi = {10.1371/journal.pmed.1004809},
url = {https://doi.org/10.1371/journal.pmed.1004809},
pmid = {42160333},
pmcid = {PMC13215615}
}

RIS

TY - JOUR
AU - Bernardoni, Fabio
AU - Arold, Dominic
AU - Schoppik, Luis
AU - Bahnsen, Klaas
AU - Ge, Ruiyang
AU - Moreau, Clara
AU - Bang, Lasse
AU - D’Agata, Federico
AU - Abbate-Daga, Giovanni
AU - Tamnes, Christian K
AU - Campbell, Iain
AU - O’Daly, Owen
AU - Schmidt, Ulrike
AU - Frank, Guido
AU - Horndasch, Stefanie
AU - Hess, Andreas
AU - Dörfler, Arnd
AU - Friederich, Hans-Christoph
AU - Simon, Joe
AU - Favaro, Angela
AU - Lavagnino, Luca
AU - Wierenga, Christina E
AU - Bischoff-Grethe, Amanda
AU - Miles, Amy E
AU - Kaplan, Allan
AU - Voineskos, Aristotle
AU - Smeets, Paul A M
AU - van Elburg, Annemarie A
AU - Danner, Unna
AU - Thomopoulos, Sophia I
AU - Berner, Laura
AU - Jahanshad, Neda
AU - Frangou, Sophia
AU - King, Joseph A
AU - Thompson, Paul
AU - Ehrlich, Stefan
TI - Brain morphology in Anorexia Nervosa and its subtypes: A multi-cohort study of individual participant data
T2 - PLoS medicine
J2 - PLoS Med
PY - 2026
DA - 2026/05/20
VL - 23
IS - 5
SP - e1004809
SN - 1549-1277
PB - PLOS
DO - 10.1371/journal.pmed.1004809
UR - https://doi.org/10.1371/journal.pmed.1004809
LA - en
ER -

CSL-JSON

{
"id": "10.1371/journal.pmed.1004809",
"type": "article-journal",
"title": "Brain morphology in Anorexia Nervosa and its subtypes: A multi-cohort study of individual participant data",
"container-title": "PLoS medicine",
"author": [
{
"family": "Bernardoni",
"given": "Fabio"
},
{
"family": "Arold",
"given": "Dominic"
},
{
"family": "Schoppik",
"given": "Luis"
},
{
"family": "Bahnsen",
"given": "Klaas"
},
{
"family": "Ge",
"given": "Ruiyang"
},
{
"family": "Moreau",
"given": "Clara"
},
{
"family": "Bang",
"given": "Lasse"
},
{
"family": "D’Agata",
"given": "Federico"
},
{
"family": "Abbate-Daga",
"given": "Giovanni"
},
{
"family": "Tamnes",
"given": "Christian K"
},
{
"family": "Campbell",
"given": "Iain"
},
{
"family": "O’Daly",
"given": "Owen"
},
{
"family": "Schmidt",
"given": "Ulrike"
},
{
"family": "Frank",
"given": "Guido"
},
{
"family": "Horndasch",
"given": "Stefanie"
},
{
"family": "Hess",
"given": "Andreas"
},
{
"family": "Dörfler",
"given": "Arnd"
},
{
"family": "Friederich",
"given": "Hans-Christoph"
},
{
"family": "Simon",
"given": "Joe"
},
{
"family": "Favaro",
"given": "Angela"
},
{
"family": "Lavagnino",
"given": "Luca"
},
{
"family": "Wierenga",
"given": "Christina E"
},
{
"family": "Bischoff-Grethe",
"given": "Amanda"
},
{
"family": "Miles",
"given": "Amy E"
},
{
"family": "Kaplan",
"given": "Allan"
},
{
"family": "Voineskos",
"given": "Aristotle"
},
{
"family": "Smeets",
"given": "Paul A M"
},
{
"family": "van Elburg",
"given": "Annemarie A"
},
{
"family": "Danner",
"given": "Unna"
},
{
"family": "Thomopoulos",
"given": "Sophia I"
},
{
"family": "Berner",
"given": "Laura"
},
{
"family": "Jahanshad",
"given": "Neda"
},
{
"family": "Frangou",
"given": "Sophia"
},
{
"family": "King",
"given": "Joseph A"
},
{
"family": "Thompson",
"given": "Paul"
},
{
"family": "Ehrlich",
"given": "Stefan"
}
],
"container-title-short": "PLoS Med",
"volume": "23",
"issue": "5",
"page": "e1004809",
"DOI": "10.1371/journal.pmed.1004809",
"PMID": "42160333",
"PMCID": "PMC13215615",
"ISSN": "1549-1277",
"publisher": "PLOS",
"URL": "https://doi.org/10.1371/journal.pmed.1004809",
"language": "en",
"issued": {
"date-parts": [
[
2026,
5,
20
]
]
}
}

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.1073/pnas.2521055123 [code]
Empirical validation of race-neutral normative brain morphometry models across ethnoracially diverse populations.
Journal: Proceedings of the National Academy of Sciences of the United States of America
In common: neuroHarmonize, pandas, NumPy, structural MRI / diffusion, 7 references, author Ruiyang Ge
[2] doi:10.1162/imag.a.1269 [code]
From early to contemporary normative modeling: Mapping individual differences in neurophysiological signals.
Journal: Imaging neuroscience (Cambridge, Mass.)
In common: Plotly, NiBabel, statsmodels, 6 other tools, 6 references
[3] doi:10.1002/aur.70243
Autism and Cortical Thickness Deviation From Neurotypical Controls: Evidence for a Spatial Association With Serotonin Receptors.
Journal: Autism research : official journal of the International Society for Autism Research
In common: structural MRI / diffusion, 3 authors
[4] doi:10.3389/fnins.2026.1803154 [code]
Multimodal machine learning reveals neurobiological signatures of binge-type eating disorders.
Journal: Frontiers in neuroscience
In common: statannotations, NiBabel, statsmodels, 6 other tools, other condition, 3 references
[5] doi:10.1002/alz.71649 [code]
Postmortem brain MRI reveals differential associations of subcortical and limbic volumes with cortical thinning and neurodegenerative pathologies.
Journal: Alzheimer's & dementia : the journal of the Alzheimer's Association
In common: statannotations, Plotly, Pillow, 8 other tools, structural MRI / diffusion, other condition, 1 reference
[6] doi:10.1038/s41380-026-03691-4 [code]
Breaking the norm: population-scale deviations of brain structure in depression and anxiety.
Journal: Molecular psychiatry
In common: NiBabel, statsmodels, seaborn, 5 other tools, structural MRI / diffusion, other condition, 4 references
[7] doi:10.21203/rs.3.rs-9326213/v1 [code]
Multi-task fMRI outperforms resting-state fMRI for revealing task-invariant organization of the human brain
Journal: Research Square (preprint)
In common: neuromaps, statannotations, Plotly, 8 other tools
[8] doi:10.64898/2026.03.09.710558 [code]
Multi-task fMRI outperforms resting-state fMRI for revealing task-invariant organization of the human brain
Journal: bioRxiv (preprint)
In common: neuromaps, statannotations, Plotly, 8 other tools
[9] 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: neuromaps, statannotations, NiBabel, 7 other tools, structural MRI / diffusion, 1 reference
[10] doi:10.1038/s41380-026-03497-4 [code]
Transcriptome-informed brain cartography of polygenic risk and association with brain structure in major psychiatric disorders.
Journal: Molecular psychiatry
In common: neuromaps, NiBabel, statsmodels, 4 other tools, other condition, 4 references

Contribute

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

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

Request its removal

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

Discussion, reproductions, activity

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

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

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